diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 00000000..a3fe6a01 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Github \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 00000000..a5f05cd8 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 37a75096..c37443c9 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/README.md b/README.md new file mode 100644 index 00000000..bc8c1ff4 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Github Login +## Overview +**Github Login** is a simple application which uses github oauth to access github APIs. User can search any repositories and see it's commits in addition to to review his/her github profile. + +## Technical Overview +The app is developed upon Clean + MVVM architecture. it has two data sources : +1. Remote data source which is based on [Github API v3](https://developer.github.com/v3/). first user login in to app using his/her github account then uses this api to search repos, review commits and his/her profile. +2. Offline data source which stores user's authentication status + +worth metioning that both data sources are **unit tested**, in addition to all viewmodels + +## Design +Icons are from AndroidStudio built-in Material Icon pack. The illustration icons are from [iconfinder.com](https://iconfinder.com) + +## Further Developments +Further developments can include these parts: +1. Add resiliency to the app, meaning that If there is no network available when a request is due, app park the call and perform it as +soon as the network is back. + 2. add integration and UI tests diff --git a/app/build.gradle b/app/build.gradle index c31daba5..f85b2e48 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,18 +1,18 @@ apply plugin: 'com.android.application' - apply plugin: 'kotlin-android' - apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' android { compileSdkVersion 29 - buildToolsVersion "29.0.1" defaultConfig { - applicationId "com.mydigipay.challenge.github" + applicationId "com.mydigipay.challenge.presentation.github" minSdkVersion 17 targetSdkVersion 29 + multiDexEnabled true versionCode 1 versionName "1.0" + vectorDrawables.useSupportLibrary = true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -21,30 +21,84 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + jvmTarget = "1.8" + } + } + buildFeatures{ + dataBinding = true + } } +def appcompat_version = '1.1.0' +def corektx_version = '1.3.0' +def junit_version = '4.13' +def extjunit_version = '1.1.1' +def espresso_core_version = '3.2.0' +def material_version = '1.1.0' +def constraint_layout_version = '1.1.3' +def mockito_version = '3.2.4' +def rxjava_version = '2.2.17' +def rxandroid_version = '2.1.1' +def rxrelay_version = '2.1.1' +def rxbindind_version = '3.1.0' +def retrofit_version = '2.7.2' +def retrofit_gson_converter_version = '2.7.2' +def retrofit_rxadapter_version = '1.0.0' +def okhttp_version = '4.4.0' +def okhttp_logging_version = '4.4.0' +def nav_version = "2.3.0" +def lifecycle_version = "2.3.0-alpha05" +def leakcanary_version = "2.1" +def glide_version = "4.11.0" +def dagger_version = "2.25.4" + dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.0.2' - implementation 'androidx.core:core-ktx:1.0.2' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation 'com.google.android.material:material:1.0.0' - implementation 'com.squareup.retrofit2:retrofit:2.5.0' - implementation 'com.squareup.okhttp3:okhttp:3.11.0' - implementation 'org.koin:koin-android:2.0.1' - implementation 'org.koin:koin-android-viewmodel:2.0.1' - - - implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.1' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.1' - implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0' - implementation 'androidx.preference:preference-ktx:1.1.0' - implementation 'com.squareup.retrofit2:converter-gson:2.4.0' - implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' - - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + implementation "androidx.appcompat:appcompat:$appcompat_version" + implementation "androidx.core:core-ktx:$corektx_version" + implementation "com.google.android.material:material:$material_version" + implementation "androidx.constraintlayout:constraintlayout:$constraint_layout_version" + + implementation "io.reactivex.rxjava2:rxjava:$rxjava_version" + implementation "io.reactivex.rxjava2:rxandroid:$rxandroid_version" + implementation "com.jakewharton.rxrelay2:rxrelay:$rxrelay_version" + implementation "com.jakewharton.rxbinding3:rxbinding:$rxbindind_version" + implementation "com.jakewharton.rxbinding3:rxbinding-material:$rxbindind_version" + + implementation "com.squareup.retrofit2:retrofit:$retrofit_version" + implementation "com.squareup.retrofit2:converter-gson:$retrofit_gson_converter_version" + implementation "com.jakewharton.retrofit:retrofit2-rxjava2-adapter:$retrofit_rxadapter_version" + + implementation "com.squareup.okhttp3:okhttp:$okhttp_version" + implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_logging_version" + + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" + + implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" + + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakcanary_version" + + implementation "com.github.bumptech.glide:glide:$glide_version" + kapt "com.github.bumptech.glide:compiler:$glide_version" + + api "com.google.dagger:dagger:$dagger_version" + kapt "com.google.dagger:dagger-compiler:$dagger_version" + + testImplementation "junit:junit:$junit_version" + androidTestImplementation "androidx.test.ext:junit:$extjunit_version" + androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_core_version" + + implementation "org.mockito:mockito-core:$mockito_version" + androidTestImplementation "org.mockito:mockito-android:$mockito_version" + + implementation 'com.android.support:multidex:1.0.3' } diff --git a/app/src/androidTest/java/com/mydigipay/challenge/github/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/mydigipay/challenge/presentation/github/ExampleInstrumentedTest.kt similarity index 88% rename from app/src/androidTest/java/com/mydigipay/challenge/github/ExampleInstrumentedTest.kt rename to app/src/androidTest/java/com/mydigipay/challenge/presentation/github/ExampleInstrumentedTest.kt index 7fa57ded..748e8cd9 100644 --- a/app/src/androidTest/java/com/mydigipay/challenge/github/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/mydigipay/challenge/presentation/github/ExampleInstrumentedTest.kt @@ -1,8 +1,7 @@ -package com.mydigipay.challenge.github +package com.mydigipay.challenge.presentation.github -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.runner.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry import org.junit.Test import org.junit.runner.RunWith diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 999179d8..919c59bd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,27 +1,26 @@ - + package="com.mydigipay.challenge.presentation.github"> + + + android:theme="@style/AppTheme"> - - - - @@ -30,6 +29,8 @@ + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 00000000..003c2968 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/mydigipay/challenge/app/App.kt b/app/src/main/java/com/mydigipay/challenge/app/App.kt index 65530767..6e562ce3 100644 --- a/app/src/main/java/com/mydigipay/challenge/app/App.kt +++ b/app/src/main/java/com/mydigipay/challenge/app/App.kt @@ -1,29 +1,20 @@ package com.mydigipay.challenge.app import android.app.Application -import androidx.preference.PreferenceManager -import com.mydigipay.challenge.network.di.accessTokenModule -import com.mydigipay.challenge.network.di.networkModule -import com.mydigipay.challenge.repository.token.TokenRepositoryImpl -import org.koin.android.ext.koin.androidContext -import org.koin.core.context.startKoin -import org.koin.core.qualifier.named -import org.koin.dsl.module -const val APPLICATION_CONTEXT = "APPLICATION_CONTEXT" +import com.mydigipay.challenge.di.component.AppComponent +import com.mydigipay.challenge.di.component.DaggerAppComponent + +lateinit var component: AppComponent + class App : Application() { override fun onCreate() { super.onCreate() - startKoin { - androidContext(this@App) - modules(listOf(appModule, networkModule, accessTokenModule)) - } + initDagger() } - val appModule = module { - factory { TokenRepositoryImpl(get()) } - single(named(APPLICATION_CONTEXT)) { applicationContext } - single { PreferenceManager.getDefaultSharedPreferences(get()) } + private fun initDagger() { + component = DaggerAppComponent.factory().create(this) } } \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/app/BindingAdapter.kt b/app/src/main/java/com/mydigipay/challenge/app/BindingAdapter.kt new file mode 100644 index 00000000..b926f2ef --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/app/BindingAdapter.kt @@ -0,0 +1,32 @@ +package com.mydigipay.challenge.app + +import android.widget.ImageView +import androidx.databinding.BindingAdapter +import com.bumptech.glide.Glide +import com.bumptech.glide.request.RequestOptions +import com.mydigipay.challenge.presentation.github.R + +class BindingAdapter { + + companion object { + + val movieImagePlaceHolder = R.drawable.ic_account + + @BindingAdapter("android:imageUrl") + @JvmStatic + fun loadImage(view: ImageView, imageUrl: String?) { + imageUrl?.let { + Glide.with(view) + .setDefaultRequestOptions( + RequestOptions().circleCrop() + ) + .load(imageUrl) + .placeholder(movieImagePlaceHolder) + .error(movieImagePlaceHolder) + .into(view) + } + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/app/Const.kt b/app/src/main/java/com/mydigipay/challenge/app/Const.kt new file mode 100644 index 00000000..2f14d274 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/app/Const.kt @@ -0,0 +1,9 @@ +package com.mydigipay.challenge.app + +object Const { + const val CLIENT_ID = "685e6244dd56a72db4c6" + const val CLIENT_SECRET = "50c7fa47bd384aaf6487c4ae2a375a8f6891cda0" + const val REDIRECT_URI = "challenge://mydigipay.com/mohsen/callback" + const val STATE = "0123456" + const val TOKEN_PREF_KEY = "TOKEN" +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/app/ViewModelProviderFactory.kt b/app/src/main/java/com/mydigipay/challenge/app/ViewModelProviderFactory.kt new file mode 100644 index 00000000..a02e04b3 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/app/ViewModelProviderFactory.kt @@ -0,0 +1,15 @@ +package com.mydigipay.challenge.app + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import javax.inject.Inject +import javax.inject.Provider + +class ViewModelProviderFactory @Inject constructor( + private val creators: MutableMap, Provider> +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return creators[modelClass]?.get() as? T + ?: throw IllegalArgumentException("The requested ViewModel isn't bound") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/data/datasource/api/ApiService.kt b/app/src/main/java/com/mydigipay/challenge/data/datasource/api/ApiService.kt new file mode 100644 index 00000000..41a204d2 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/datasource/api/ApiService.kt @@ -0,0 +1,29 @@ +package com.mydigipay.challenge.data.datasource.api + +import com.mydigipay.challenge.data.model.commit.CommitResponseEntity +import com.mydigipay.challenge.data.model.search.SearchResponse +import com.mydigipay.challenge.data.model.user.UserEntity +import com.mydigipay.challenge.data.model.token.RequestAccessToken +import com.mydigipay.challenge.data.model.token.ResponseAccessToken +import io.reactivex.Single +import retrofit2.http.* + +interface ApiService { + + @Headers("Accept:application/json") + @POST("https://github.com/login/oauth/access_token") + fun getAccessToken(@Body requestAccessToken: RequestAccessToken): Single + + @GET("/search/repositories") + fun performSearch(@Query("q") query: String): Single + + @GET("/user") + fun getUser(): Single + + @GET("/repos/{owner}/{repo}/commits") + fun getCommits( + @Path("owner") owner: String, + @Path("repo") repo: String, + @Query("sha") branch: String = "master" + ): Single> +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/data/datasource/local/LocalAccessTokenDataSource.kt b/app/src/main/java/com/mydigipay/challenge/data/datasource/local/LocalAccessTokenDataSource.kt new file mode 100644 index 00000000..082657b2 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/datasource/local/LocalAccessTokenDataSource.kt @@ -0,0 +1,8 @@ +package com.mydigipay.challenge.data.datasource.local + +import io.reactivex.Completable + +interface LocalAccessTokenDataSource { + fun readToken(): String + fun saveToken(token: String): Completable +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/data/datasource/remote/GithubDataSource.kt b/app/src/main/java/com/mydigipay/challenge/data/datasource/remote/GithubDataSource.kt new file mode 100644 index 00000000..b550b016 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/datasource/remote/GithubDataSource.kt @@ -0,0 +1,12 @@ +package com.mydigipay.challenge.data.datasource.remote + +import com.mydigipay.challenge.domain.model.Commit +import com.mydigipay.challenge.domain.model.RemoteRepository +import com.mydigipay.challenge.domain.model.User +import io.reactivex.Single + +interface GithubDataSource { + fun search(query: String): Single> + fun getUser(): Single + fun getCommits(owner: String, repo: String): Single> +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/data/datasource/remote/RemoteAccessTokenDataSource.kt b/app/src/main/java/com/mydigipay/challenge/data/datasource/remote/RemoteAccessTokenDataSource.kt new file mode 100644 index 00000000..edea63bb --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/datasource/remote/RemoteAccessTokenDataSource.kt @@ -0,0 +1,14 @@ +package com.mydigipay.challenge.data.datasource.remote + +import com.mydigipay.challenge.data.model.token.ResponseAccessToken +import io.reactivex.Single + +interface RemoteAccessTokenDataSource { + fun getAccessToken( + clientId: String, + clientSecret: String, + code: String, + redirectUrl: String, + state: String + ): Single +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/data/model/commit/CommitResponseEntity.kt b/app/src/main/java/com/mydigipay/challenge/data/model/commit/CommitResponseEntity.kt new file mode 100644 index 00000000..e647a9e1 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/model/commit/CommitResponseEntity.kt @@ -0,0 +1,33 @@ +package com.mydigipay.challenge.data.model.commit + +import com.google.gson.annotations.SerializedName +import com.mydigipay.challenge.domain.model.Commit + +data class CommitResponseEntity( + @SerializedName("author") + val author: RemoteAuthorEntity? = null, + @SerializedName("comments_url") + val commentsUrl: String? = null, + @SerializedName("commit") + val commit: RemoteCommitEntity? = null, + @SerializedName("committer") + val committer: RemoteCommiterEntity? = null, + @SerializedName("html_url") + val htmlUrl: String? = null, + @SerializedName("node_id") + val nodeId: String? = null, + @SerializedName("parents") + val parents: List? = null, + @SerializedName("sha") + val sha: String? = null, + @SerializedName("url") + val url: String? = null +) + +fun CommitResponseEntity.mapToDomainModel(): Commit { + return Commit( + message = commit?.message, + author = commit?.author?.mapToDomainModel(), + commentsCount = commit?.commentCount + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/data/model/commit/RemoteAuthorEntity.kt b/app/src/main/java/com/mydigipay/challenge/data/model/commit/RemoteAuthorEntity.kt new file mode 100644 index 00000000..98da38d0 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/model/commit/RemoteAuthorEntity.kt @@ -0,0 +1,21 @@ +package com.mydigipay.challenge.data.model.commit + +import com.google.gson.annotations.SerializedName +import com.mydigipay.challenge.domain.model.CommitAuthor + +data class RemoteAuthorEntity( + @SerializedName("date") + val date: String? = null, + @SerializedName("email") + val email: String? = null, + @SerializedName("name") + val name: String? = null +) + +fun RemoteAuthorEntity.mapToDomainModel(): CommitAuthor { + return CommitAuthor( + name = name, + email = email, + date = date + ) +} diff --git a/app/src/main/java/com/mydigipay/challenge/data/model/commit/RemoteCommitEntity.kt b/app/src/main/java/com/mydigipay/challenge/data/model/commit/RemoteCommitEntity.kt new file mode 100644 index 00000000..c3973f75 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/model/commit/RemoteCommitEntity.kt @@ -0,0 +1,20 @@ +package com.mydigipay.challenge.data.model.commit + +import com.google.gson.annotations.SerializedName + +data class RemoteCommitEntity( + @SerializedName("author") + val author: RemoteAuthorEntity?= null, + @SerializedName("comment_count") + val commentCount: Int?= null, + @SerializedName("committer") + val committer: RemoteCommiterEntity?= null, + @SerializedName("message") + val message: String?= null, + @SerializedName("tree") + val tree: Tree?= null, + @SerializedName("url") + val url: String?= null, + @SerializedName("verification") + val verification: Verification?= null +) \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/data/model/commit/RemoteCommiterEntity.kt b/app/src/main/java/com/mydigipay/challenge/data/model/commit/RemoteCommiterEntity.kt new file mode 100644 index 00000000..93d67697 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/model/commit/RemoteCommiterEntity.kt @@ -0,0 +1,12 @@ +package com.mydigipay.challenge.data.model.commit + +import com.google.gson.annotations.SerializedName + +data class RemoteCommiterEntity( + @SerializedName("date") + val date: String?= null, + @SerializedName("email") + val email: String?= null, + @SerializedName("name") + val name: String?= null +) \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/data/model/commit/RemoteParentEntity.kt b/app/src/main/java/com/mydigipay/challenge/data/model/commit/RemoteParentEntity.kt new file mode 100644 index 00000000..001c974b --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/model/commit/RemoteParentEntity.kt @@ -0,0 +1,10 @@ +package com.mydigipay.challenge.data.model.commit + +import com.google.gson.annotations.SerializedName + +data class RemoteParentEntity( + @SerializedName("sha") + val sha: String?= null, + @SerializedName("url") + val url: String?= null +) \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/data/model/commit/Tree.kt b/app/src/main/java/com/mydigipay/challenge/data/model/commit/Tree.kt new file mode 100644 index 00000000..98839682 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/model/commit/Tree.kt @@ -0,0 +1,10 @@ +package com.mydigipay.challenge.data.model.commit + +import com.google.gson.annotations.SerializedName + +data class Tree( + @SerializedName("sha") + val sha: String?= null, + @SerializedName("url") + val url: String?= null +) \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/data/model/commit/Verification.kt b/app/src/main/java/com/mydigipay/challenge/data/model/commit/Verification.kt new file mode 100644 index 00000000..268eb609 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/model/commit/Verification.kt @@ -0,0 +1,14 @@ +package com.mydigipay.challenge.data.model.commit + +import com.google.gson.annotations.SerializedName + +data class Verification( + @SerializedName("payload") + val payload: Any? = null, + @SerializedName("reason") + val reason: String? = null, + @SerializedName("signature") + val signature: Any? = null, + @SerializedName("verified") + val verified: Boolean? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/data/model/search/RemoteOwnerEntity.kt b/app/src/main/java/com/mydigipay/challenge/data/model/search/RemoteOwnerEntity.kt new file mode 100644 index 00000000..3fbaa0a4 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/model/search/RemoteOwnerEntity.kt @@ -0,0 +1,43 @@ +package com.mydigipay.challenge.data.model.search + +import com.google.gson.annotations.SerializedName +import com.mydigipay.challenge.domain.model.RemoteRepositoryOwner + +data class RemoteOwnerEntity( + @SerializedName("login") + val login: String? = null, + + @SerializedName("id") + val id: Int? = 0, + + @SerializedName("node_id") + val nodeId: String? = null, + + @SerializedName("avatar_url") + val avatarUrl: String? = null, + + @SerializedName("gravatar_id") + val gravatarId: String? = null, + + @SerializedName("url") + val url: String? = null, + + @SerializedName("received_events_url") + val receivedEventsUrl: String? = null, + + @SerializedName("type") + val type: String? = null +) + +fun RemoteOwnerEntity.mapToDomainModel(): RemoteRepositoryOwner { + return RemoteRepositoryOwner( + login = login, + id = id, + nodeId = nodeId, + avatarUrl = avatarUrl, + gravatarId = gravatarId, + url = url, + receivedEventsUrl = receivedEventsUrl, + type = type + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/data/model/search/RemoteRepositoryEntity.kt b/app/src/main/java/com/mydigipay/challenge/data/model/search/RemoteRepositoryEntity.kt new file mode 100644 index 00000000..42130595 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/model/search/RemoteRepositoryEntity.kt @@ -0,0 +1,103 @@ +package com.mydigipay.challenge.data.model.search + +import com.google.gson.annotations.SerializedName +import com.mydigipay.challenge.domain.model.RemoteRepository + +data class RemoteRepositoryEntity( + @SerializedName("id") + var id: Int? = 0, + + @SerializedName("node_id") + var nodeId: String? = null, + + @SerializedName("name") + var name: String? = null, + + @SerializedName("full_name") + var fullName: String? = null, + + @SerializedName("owner") + var remoteOwnerEntity: RemoteOwnerEntity? = null, + + @SerializedName("private") + var isPrivate: Boolean? = false, + + @SerializedName("html_url") + var htmlUrl: String? = null, + + @SerializedName("description") + var description: String? = null, + + @SerializedName("fork") + var isFork: Boolean? = false, + + @SerializedName("url") + var url: String? = null, + + @SerializedName("created_at") + var createdAt: String? = null, + + @SerializedName("updated_at") + var updatedAt: String? = null, + + @SerializedName("pushed_at") + var pushedAt: String? = null, + + @SerializedName("homepage") + var homepage: String? = null, + + @SerializedName("size") + var size: Int? = null, + + @SerializedName("stargazers_count") + var stargazersCount: Int? = 0, + + @SerializedName("watchers_count") + var watchersCount: Int? = 0, + + @SerializedName("language") + var language: String? = null, + + @SerializedName("forks_count") + var forksCount: Int? = 0, + + @SerializedName("open_issues_count") + var openIssuesCount: Int? = 0, + + @SerializedName("master_branch") + var masterBranch: String? = null, + + @SerializedName("default_branch") + var defaultBranch: String? = null, + + @SerializedName("score") + var score: Double? = 0.0 +) + +fun RemoteRepositoryEntity.mapToDomainModel(): RemoteRepository { + return RemoteRepository( + id = id, + nodeId = nodeId, + name = name, + fullName = fullName, + remoteRepositoryOwner = remoteOwnerEntity?.mapToDomainModel(), + isPrivate = isPrivate, + htmlUrl = htmlUrl, + description = description, + isFork = isFork, + url = url, + createdAt = createdAt, + updatedAt = updatedAt, + pushedAt = pushedAt, + homepage = homepage, + size = size, + stargazersCount = stargazersCount, + watchersCount = watchersCount, + language = language, + forksCount = forksCount, + openIssuesCount = openIssuesCount, + masterBranch = masterBranch, + defaultBranch = defaultBranch, + score = score + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/data/model/search/SearchResponse.kt b/app/src/main/java/com/mydigipay/challenge/data/model/search/SearchResponse.kt new file mode 100644 index 00000000..e076d462 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/model/search/SearchResponse.kt @@ -0,0 +1,14 @@ +package com.mydigipay.challenge.data.model.search + +import com.google.gson.annotations.SerializedName + +data class SearchResponse( + @SerializedName("total_count") + val totalCount: Int? = 0, + + @SerializedName("incomplete_results") + val isIncompleteResults: Boolean? = false, + + @SerializedName("items") + val remoteSearchItemEntities: List? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/network/oauth/RequestAccessToken.kt b/app/src/main/java/com/mydigipay/challenge/data/model/token/RequestAccessToken.kt similarity index 88% rename from app/src/main/java/com/mydigipay/challenge/network/oauth/RequestAccessToken.kt rename to app/src/main/java/com/mydigipay/challenge/data/model/token/RequestAccessToken.kt index 22e2aa0b..48406699 100644 --- a/app/src/main/java/com/mydigipay/challenge/network/oauth/RequestAccessToken.kt +++ b/app/src/main/java/com/mydigipay/challenge/data/model/token/RequestAccessToken.kt @@ -1,4 +1,4 @@ -package com.mydigipay.challenge.network.oauth +package com.mydigipay.challenge.data.model.token import com.google.gson.annotations.SerializedName diff --git a/app/src/main/java/com/mydigipay/challenge/network/oauth/ResponseAccessToken.kt b/app/src/main/java/com/mydigipay/challenge/data/model/token/ResponseAccessToken.kt similarity index 81% rename from app/src/main/java/com/mydigipay/challenge/network/oauth/ResponseAccessToken.kt rename to app/src/main/java/com/mydigipay/challenge/data/model/token/ResponseAccessToken.kt index d79c2340..cb6d20da 100644 --- a/app/src/main/java/com/mydigipay/challenge/network/oauth/ResponseAccessToken.kt +++ b/app/src/main/java/com/mydigipay/challenge/data/model/token/ResponseAccessToken.kt @@ -1,4 +1,4 @@ -package com.mydigipay.challenge.network.oauth +package com.mydigipay.challenge.data.model.token import com.google.gson.annotations.SerializedName diff --git a/app/src/main/java/com/mydigipay/challenge/data/model/user/UserEntity.kt b/app/src/main/java/com/mydigipay/challenge/data/model/user/UserEntity.kt new file mode 100644 index 00000000..8286a834 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/model/user/UserEntity.kt @@ -0,0 +1,78 @@ +package com.mydigipay.challenge.data.model.user + +import com.google.gson.annotations.SerializedName +import com.mydigipay.challenge.domain.model.User + +data class UserEntity( + @SerializedName("login") var login: String? = null, + @SerializedName("id") var id: Int? = null, + @SerializedName("node_id") var nodeId: String? = null, + @SerializedName("avatar_url") var avatarUrl: String? = null, + @SerializedName("gravatar_id") var gravatarId: String? = null, + @SerializedName("url") var url: String? = null, + @SerializedName("html_url") var htmlUrl: String? = null, + @SerializedName("followers_url") var followersUrl: String? = null, + @SerializedName("following_url") var followingUrl: String? = null, + @SerializedName("gists_url") var gistsUrl: String? = null, + @SerializedName("starred_url") var starredUrl: String? = null, + @SerializedName("subscriptions_url") var subscriptionsUrl: String? = null, + @SerializedName("organizations_url") var organizationsUrl: String? = null, + @SerializedName("repos_url") var reposUrl: String? = null, + @SerializedName("events_url") var eventsUrl: String? = null, + @SerializedName("received_events_url") var receivedEventsUrl: String? = null, + @SerializedName("type") var type: String? = null, + @SerializedName("site_admin") var site_admin: Boolean? = false, + @SerializedName("name") var name: String? = null, + @SerializedName("company") var company: String? = null, + @SerializedName("blog") var blog: String? = null, + @SerializedName("location") var location: String? = null, + @SerializedName("email") var email: String? = null, + @SerializedName("hireable") var hireable: Boolean? = false, + @SerializedName("bio") var bio: String? = null, + @SerializedName("twitter_username") var twitterUsername: String? = null, + @SerializedName("public_repos") var publicRepos: Int? = null, + @SerializedName("public_gists") var publicGists: Int? = null, + @SerializedName("followers") var followers: Int? = null, + @SerializedName("following") var following: Int? = null, + @SerializedName("created_at") var createdAt: String? = null, + @SerializedName("updated_at") var updatedAt: String? = null +) + +fun UserEntity.mapToDomainModel(): User { + return User( + login = login, + id = id, + nodeId = nodeId, + avatarUrl = avatarUrl, + gravatarId = gravatarId, + url = url, + htmlUrl = htmlUrl, + followersUrl = followersUrl, + followingUrl = followingUrl, + gistsUrl = gistsUrl, + starredUrl = starredUrl, + subscriptionsUrl = subscriptionsUrl, + organizationsUrl = organizationsUrl, + reposUrl = reposUrl, + eventsUrl = eventsUrl, + receivedEventsUrl = receivedEventsUrl, + type = type, + site_admin = site_admin, + name = name, + company = company, + blog = blog, + location = location, + email = email, + hireable = hireable, + bio = bio, + twitterUsername = twitterUsername, + publicRepos = publicRepos, + publicGists = publicGists, + followers = followers, + following = following, + createdAt = createdAt, + updatedAt = updatedAt + + ) +} + diff --git a/app/src/main/java/com/mydigipay/challenge/data/repository/GithubRepositoryImpl.kt b/app/src/main/java/com/mydigipay/challenge/data/repository/GithubRepositoryImpl.kt new file mode 100644 index 00000000..ace289ff --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/repository/GithubRepositoryImpl.kt @@ -0,0 +1,24 @@ +package com.mydigipay.challenge.data.repository + +import com.mydigipay.challenge.data.datasource.remote.GithubDataSource +import com.mydigipay.challenge.domain.model.Commit +import com.mydigipay.challenge.domain.model.RemoteRepository +import com.mydigipay.challenge.domain.model.User +import com.mydigipay.challenge.domain.repository.GithubRepository +import io.reactivex.Single +import javax.inject.Inject + +class GithubRepositoryImpl @Inject constructor(private val githubDataSource: GithubDataSource) : + GithubRepository { + override fun search(query: String): Single> { + return githubDataSource.search(query) + } + + override fun getUser(): Single { + return githubDataSource.getUser() + } + + override fun getCommits(owner: String, repo: String): Single> { + return githubDataSource.getCommits(owner, repo) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/data/repository/TokenRepositoryImpl.kt b/app/src/main/java/com/mydigipay/challenge/data/repository/TokenRepositoryImpl.kt new file mode 100644 index 00000000..a1af93e4 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/data/repository/TokenRepositoryImpl.kt @@ -0,0 +1,42 @@ +package com.mydigipay.challenge.data.repository + +import com.mydigipay.challenge.app.Const.CLIENT_ID +import com.mydigipay.challenge.app.Const.CLIENT_SECRET +import com.mydigipay.challenge.app.Const.REDIRECT_URI +import com.mydigipay.challenge.app.Const.STATE +import com.mydigipay.challenge.data.datasource.local.LocalAccessTokenDataSource +import com.mydigipay.challenge.data.datasource.remote.RemoteAccessTokenDataSource +import com.mydigipay.challenge.domain.repository.TokenRepository +import io.reactivex.Completable +import javax.inject.Inject + + +class TokenRepositoryImpl @Inject constructor( + private val localAccessTokenDataSource: LocalAccessTokenDataSource, + private val remoteAccessTokenDataSource: RemoteAccessTokenDataSource +) : + TokenRepository { + + override fun saveToken(token: String): Completable { + return localAccessTokenDataSource.saveToken(token) + } + + override fun readToken(): String { + return localAccessTokenDataSource.readToken() + } + + override fun fetchAccessToken(code: String): Completable { + return remoteAccessTokenDataSource.getAccessToken( + CLIENT_ID, + CLIENT_SECRET, + code, + REDIRECT_URI, + STATE + ).flatMapCompletable { + saveToken(it.accessToken) + }.onErrorResumeNext { + Completable.error(it) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/datasource/local/LocalAccessTokenDataSourceImpl.kt b/app/src/main/java/com/mydigipay/challenge/datasource/local/LocalAccessTokenDataSourceImpl.kt new file mode 100644 index 00000000..f5c5fbd0 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/datasource/local/LocalAccessTokenDataSourceImpl.kt @@ -0,0 +1,24 @@ +package com.mydigipay.challenge.datasource.local + +import android.content.SharedPreferences +import com.mydigipay.challenge.app.Const.TOKEN_PREF_KEY +import com.mydigipay.challenge.data.datasource.local.LocalAccessTokenDataSource +import io.reactivex.Completable +import javax.inject.Inject + + +class LocalAccessTokenDataSourceImpl @Inject constructor(private val sharedPreferences: SharedPreferences) : + LocalAccessTokenDataSource { + + + override fun readToken(): String { + return sharedPreferences.getString(TOKEN_PREF_KEY, "") ?: "" + } + + override fun saveToken(token: String): Completable { + return Completable.create { emitter -> + sharedPreferences.edit().apply { putString(TOKEN_PREF_KEY, token) }.apply() + emitter.onComplete() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/datasource/remote/GithubDataSourceImpl.kt b/app/src/main/java/com/mydigipay/challenge/datasource/remote/GithubDataSourceImpl.kt new file mode 100644 index 00000000..dcf23031 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/datasource/remote/GithubDataSourceImpl.kt @@ -0,0 +1,40 @@ +package com.mydigipay.challenge.datasource.remote + +import com.mydigipay.challenge.data.datasource.api.ApiService +import com.mydigipay.challenge.data.datasource.remote.GithubDataSource +import com.mydigipay.challenge.data.model.commit.mapToDomainModel +import com.mydigipay.challenge.data.model.search.mapToDomainModel +import com.mydigipay.challenge.data.model.user.mapToDomainModel +import com.mydigipay.challenge.domain.model.Commit +import com.mydigipay.challenge.domain.model.RemoteRepository +import com.mydigipay.challenge.domain.model.User +import io.reactivex.Single +import javax.inject.Inject + +class GithubDataSourceImpl @Inject constructor(private val apiService: ApiService) : + GithubDataSource { + override fun search(query: String): Single> { + return apiService.performSearch(query).flatMap { + return@flatMap Single.just( + it.remoteSearchItemEntities?.map { + it.mapToDomainModel() + } + ) + } + } + + override fun getUser(): Single { + return apiService.getUser().map { + it.mapToDomainModel() + } + } + + override fun getCommits(owner: String, repo: String): Single> { + return apiService.getCommits(owner, repo).map { + it.map { + it.mapToDomainModel() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/datasource/remote/RemoteAccessTokenDataSourceImpl.kt b/app/src/main/java/com/mydigipay/challenge/datasource/remote/RemoteAccessTokenDataSourceImpl.kt new file mode 100644 index 00000000..eb464a89 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/datasource/remote/RemoteAccessTokenDataSourceImpl.kt @@ -0,0 +1,28 @@ +package com.mydigipay.challenge.datasource.remote + +import com.mydigipay.challenge.data.datasource.remote.RemoteAccessTokenDataSource +import com.mydigipay.challenge.data.datasource.api.ApiService +import com.mydigipay.challenge.data.model.token.RequestAccessToken +import com.mydigipay.challenge.data.model.token.ResponseAccessToken +import io.reactivex.Single + +class RemoteAccessTokenDataSourceImpl(private val apiService: ApiService) : + RemoteAccessTokenDataSource { + override fun getAccessToken( + clientId: String, + clientSecret: String, + code: String, + redirectUrl: String, + state: String + ): Single { + return apiService.getAccessToken( + RequestAccessToken( + clientId, + clientSecret, + code, + redirectUrl, + state + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/di/component/AppComponent.kt b/app/src/main/java/com/mydigipay/challenge/di/component/AppComponent.kt new file mode 100644 index 00000000..6858860a --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/di/component/AppComponent.kt @@ -0,0 +1,46 @@ +package com.mydigipay.challenge.di.component + +import android.content.Context +import com.mydigipay.challenge.di.module.* +import com.mydigipay.challenge.presentation.auth.AuthActivity +import com.mydigipay.challenge.presentation.github.commit.CommitFragment +import com.mydigipay.challenge.presentation.github.search.SearchFragment +import com.mydigipay.challenge.presentation.github.user.UserProfileFragment +import dagger.BindsInstance +import dagger.Component +import dagger.Subcomponent +import javax.inject.Singleton + +@Component( + modules = [SharedPrefsModule::class, + DataSourceModule::class, + RepositoryModule::class, + ApiModule::class + ] +) +@Singleton +interface AppComponent { + + @Component.Factory + interface Factory { + fun create(@BindsInstance context: Context): AppComponent + } + + val viewModelProviderFactory: ViewModelComponent.Factory + +} + +@Subcomponent(modules = [ViewModelModule::class]) +interface ViewModelComponent { + + fun inject(authActivity: AuthActivity) + fun inject(searchFragment: SearchFragment) + fun inject(commitFragment: CommitFragment) + fun inject(userProfileFragment: UserProfileFragment) + + @Subcomponent.Factory + interface Factory { + fun create(): ViewModelComponent + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/di/module/ApiModule.kt b/app/src/main/java/com/mydigipay/challenge/di/module/ApiModule.kt new file mode 100644 index 00000000..ff806737 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/di/module/ApiModule.kt @@ -0,0 +1,77 @@ +package com.mydigipay.challenge.di.module + +import android.content.SharedPreferences +import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import com.mydigipay.challenge.app.Const.TOKEN_PREF_KEY +import com.mydigipay.challenge.data.datasource.api.ApiService +import dagger.Lazy +import dagger.Module +import dagger.Provides +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +@Module +class ApiModule { + + private val requestTimeout = 60L + private val baseUrl = "https://api.github.com/" + private val authTokenKey = "Authorization" + + @Provides + @Singleton + fun provideApiService(retrofit: Retrofit): ApiService { + return retrofit.create(ApiService::class.java) + } + + @Provides + @Singleton + fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit { + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(okHttpClient) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + } + + @Provides + @Singleton + fun provideOkHttpClient( + httpLoggingInterceptor: HttpLoggingInterceptor, + interceptor: Interceptor + ): OkHttpClient { + + return OkHttpClient().newBuilder() + .connectTimeout(requestTimeout, TimeUnit.SECONDS) + .readTimeout(requestTimeout, TimeUnit.SECONDS) + .writeTimeout(requestTimeout, TimeUnit.SECONDS) + .addInterceptor(interceptor) + .addInterceptor(httpLoggingInterceptor) + .build() + } + + @Provides + @Singleton + fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor { + return HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY) + } + + @Provides + @Singleton + fun provideInterceptor(sharedPreferences: Lazy): Interceptor { + return Interceptor { chain: Interceptor.Chain -> + val originalRequest = chain.request() + val accessToken = sharedPreferences.get().getString(TOKEN_PREF_KEY, "") + val requestBuilder = originalRequest.newBuilder() + .header(authTokenKey, "Bearer $accessToken") + chain.proceed(requestBuilder.build()) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/di/module/DataSourceModule.kt b/app/src/main/java/com/mydigipay/challenge/di/module/DataSourceModule.kt new file mode 100644 index 00000000..14e53639 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/di/module/DataSourceModule.kt @@ -0,0 +1,35 @@ +package com.mydigipay.challenge.di.module + +import android.content.SharedPreferences +import com.mydigipay.challenge.data.datasource.local.LocalAccessTokenDataSource +import com.mydigipay.challenge.data.datasource.remote.RemoteAccessTokenDataSource +import com.mydigipay.challenge.data.datasource.api.ApiService +import com.mydigipay.challenge.data.datasource.remote.GithubDataSource +import com.mydigipay.challenge.datasource.local.LocalAccessTokenDataSourceImpl +import com.mydigipay.challenge.datasource.remote.RemoteAccessTokenDataSourceImpl +import com.mydigipay.challenge.datasource.remote.GithubDataSourceImpl +import dagger.Module +import dagger.Provides + +@Module +class DataSourceModule { + + @Provides + fun provideLocalAccessTokenDataSource(sharedPreferences: SharedPreferences): LocalAccessTokenDataSource { + return LocalAccessTokenDataSourceImpl( + sharedPreferences + ) + } + + @Provides + fun provideRemoteAccessTokenDataSource(apiService: ApiService): RemoteAccessTokenDataSource { + return RemoteAccessTokenDataSourceImpl( + apiService + ) + } + + @Provides + fun provideGithubDataSource(apiService: ApiService): GithubDataSource { + return GithubDataSourceImpl(apiService) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/di/module/RepositoryModule.kt b/app/src/main/java/com/mydigipay/challenge/di/module/RepositoryModule.kt new file mode 100644 index 00000000..db6c5762 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/di/module/RepositoryModule.kt @@ -0,0 +1,28 @@ +package com.mydigipay.challenge.di.module + +import com.mydigipay.challenge.data.datasource.local.LocalAccessTokenDataSource +import com.mydigipay.challenge.data.datasource.remote.RemoteAccessTokenDataSource +import com.mydigipay.challenge.data.datasource.remote.GithubDataSource +import com.mydigipay.challenge.data.repository.GithubRepositoryImpl +import com.mydigipay.challenge.data.repository.TokenRepositoryImpl +import com.mydigipay.challenge.domain.repository.GithubRepository +import com.mydigipay.challenge.domain.repository.TokenRepository +import dagger.Module +import dagger.Provides + +@Module +class RepositoryModule { + + @Provides + fun provideTokenRepository( + localAccessTokenDataSource: LocalAccessTokenDataSource, + remoteAccessTokenDataSource: RemoteAccessTokenDataSource + ): TokenRepository { + return TokenRepositoryImpl(localAccessTokenDataSource, remoteAccessTokenDataSource) + } + + @Provides + fun provideGithubRepository(githubDataSource: GithubDataSource): GithubRepository { + return GithubRepositoryImpl(githubDataSource) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/di/module/SharedPrefsModule.kt b/app/src/main/java/com/mydigipay/challenge/di/module/SharedPrefsModule.kt new file mode 100644 index 00000000..c91876f4 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/di/module/SharedPrefsModule.kt @@ -0,0 +1,16 @@ +package com.mydigipay.challenge.di.module + +import android.content.Context +import android.content.SharedPreferences +import dagger.Module +import dagger.Provides + +@Module +class SharedPrefsModule { + + @Provides + fun provideSharedPrefs(context: Context): SharedPreferences { + return context.getSharedPreferences("APP_PREFS", Context.MODE_PRIVATE) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/di/module/ViewModelFactoryModule.kt b/app/src/main/java/com/mydigipay/challenge/di/module/ViewModelFactoryModule.kt new file mode 100644 index 00000000..f48ecf08 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/di/module/ViewModelFactoryModule.kt @@ -0,0 +1,10 @@ +package com.mydigipay.challenge.di.module + +import androidx.lifecycle.ViewModelProvider +import com.mydigipay.challenge.app.ViewModelProviderFactory +import dagger.Module + +@Module +abstract class ViewModelFactoryModule { + abstract fun bindViewModelFactory(viewModelProviderFactory: ViewModelProviderFactory): ViewModelProvider.Factory +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/di/module/ViewModelModule.kt b/app/src/main/java/com/mydigipay/challenge/di/module/ViewModelModule.kt new file mode 100644 index 00000000..4a5de9c4 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/di/module/ViewModelModule.kt @@ -0,0 +1,35 @@ +package com.mydigipay.challenge.di.module + +import androidx.lifecycle.ViewModel +import com.mydigipay.challenge.di.scope.ViewModelKey +import com.mydigipay.challenge.presentation.auth.AuthViewModel +import com.mydigipay.challenge.presentation.github.commit.CommitViewModel +import com.mydigipay.challenge.presentation.github.search.SearchViewModel +import com.mydigipay.challenge.presentation.github.user.UserProfileViewModel +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap + +@Module +abstract class ViewModelModule { + @Binds + @IntoMap + @ViewModelKey(AuthViewModel::class) + abstract fun bindAuthViewModel(authViewModel: AuthViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(SearchViewModel::class) + abstract fun bindSearchViewModel(searchViewModel: SearchViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(CommitViewModel::class) + abstract fun bindCommitViewModel(commitViewMode: CommitViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(UserProfileViewModel::class) + abstract fun bindUserProfileViewModel(userProfileViewModel: UserProfileViewModel): ViewModel + +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/di/scope/ViewModelKey.kt b/app/src/main/java/com/mydigipay/challenge/di/scope/ViewModelKey.kt new file mode 100644 index 00000000..05603713 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/di/scope/ViewModelKey.kt @@ -0,0 +1,13 @@ +package com.mydigipay.challenge.di.scope + +import androidx.lifecycle.ViewModel +import dagger.MapKey +import kotlin.reflect.KClass + + +@MustBeDocumented +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MapKey +annotation class ViewModelKey(val value: KClass) { +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/domain/model/Commit.kt b/app/src/main/java/com/mydigipay/challenge/domain/model/Commit.kt new file mode 100644 index 00000000..a7cc3fa4 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/domain/model/Commit.kt @@ -0,0 +1,19 @@ +package com.mydigipay.challenge.domain.model + +import com.mydigipay.challenge.presentation.model.CommitItem + +data class Commit( + val message: String?, + val author: CommitAuthor?, + val commentsCount: Int? +) + +fun Commit.mapToPresentationModel(): CommitItem { + return CommitItem( + message = message ?: "", + commentsCount = commentsCount ?: 0, + email = author?.email ?: "", + date = author?.date ?: "", + name = author?.name ?: "" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/domain/model/CommitAuthor.kt b/app/src/main/java/com/mydigipay/challenge/domain/model/CommitAuthor.kt new file mode 100644 index 00000000..ce744f74 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/domain/model/CommitAuthor.kt @@ -0,0 +1,8 @@ +package com.mydigipay.challenge.domain.model + + +data class CommitAuthor( + val name: String?, + val email: String?, + val date: String? +) diff --git a/app/src/main/java/com/mydigipay/challenge/domain/model/RemoteRepository.kt b/app/src/main/java/com/mydigipay/challenge/domain/model/RemoteRepository.kt new file mode 100644 index 00000000..691deab7 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/domain/model/RemoteRepository.kt @@ -0,0 +1,57 @@ +package com.mydigipay.challenge.domain.model + +import com.mydigipay.challenge.presentation.model.RepositoryItem + +data class RemoteRepository( + var id: Int?, + var nodeId: String?, + var name: String?, + var fullName: String?, + var remoteRepositoryOwner: RemoteRepositoryOwner?, + var isPrivate: Boolean? = false, + var htmlUrl: String?, + var description: String?, + var isFork: Boolean? = false, + var url: String?, + var createdAt: String?, + var updatedAt: String?, + var pushedAt: String?, + var homepage: String?, + var size: Int?, + var stargazersCount: Int?, + var watchersCount: Int?, + var language: String?, + var forksCount: Int?, + var openIssuesCount: Int?, + var masterBranch: String?, + var defaultBranch: String?, + var score: Double? +) + +fun RemoteRepository.mapToPresentationModel(): RepositoryItem { + return RepositoryItem( + id = id ?: 0, + nodeId = nodeId ?: "", + name = name ?: "", + fullName = fullName ?: "", + repoOwnerItem = remoteRepositoryOwner?.mapToPresentationModel(), + isPrivate = isPrivate ?: false, + htmlUrl = htmlUrl ?: "", + description = description ?: "", + isFork = isFork ?: false, + url = url ?: "", + createdAt = createdAt ?: "", + updatedAt = updatedAt ?: "", + pushedAt = pushedAt ?: "", + homepage = homepage ?: "", + size = size ?: 0, + stargazersCount = stargazersCount ?: 0, + watchersCount = watchersCount ?: 0, + language = language ?: "", + forksCount = forksCount ?: 0, + openIssuesCount = openIssuesCount ?: 0, + masterBranch = masterBranch ?: "", + defaultBranch = defaultBranch ?: "", + score = score ?: 0.0 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/domain/model/RemoteRepositoryOwner.kt b/app/src/main/java/com/mydigipay/challenge/domain/model/RemoteRepositoryOwner.kt new file mode 100644 index 00000000..0ed3769f --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/domain/model/RemoteRepositoryOwner.kt @@ -0,0 +1,27 @@ +package com.mydigipay.challenge.domain.model + +import com.mydigipay.challenge.presentation.model.RepositoryOwnerItem + +data class RemoteRepositoryOwner( + var login: String? , + var id: Int?, + var nodeId: String? , + var avatarUrl: String? , + var gravatarId: String? , + var url: String? , + var receivedEventsUrl: String? , + var type: String? +) + +fun RemoteRepositoryOwner.mapToPresentationModel(): RepositoryOwnerItem { + return RepositoryOwnerItem( + login = login ?: "N/A", + id = id ?: 0, + nodeId = nodeId ?: "N/A", + avatarUrl = avatarUrl?: "N/A", + gravatarId = gravatarId?: "N/A", + url = url?: "N/A", + receivedEventsUrl = receivedEventsUrl?: "N/A", + type = type?: "N/A" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/domain/model/User.kt b/app/src/main/java/com/mydigipay/challenge/domain/model/User.kt new file mode 100644 index 00000000..f2e23088 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/domain/model/User.kt @@ -0,0 +1,76 @@ +package com.mydigipay.challenge.domain.model + +import com.google.gson.annotations.SerializedName +import com.mydigipay.challenge.presentation.model.UserItem + +data class User( + var login: String? , + var id: Int? , + var nodeId: String? , + var avatarUrl: String? , + var gravatarId: String? , + var url: String? , + var htmlUrl: String? , + var followersUrl: String? , + var followingUrl: String? , + var gistsUrl: String? , + var starredUrl: String? , + var subscriptionsUrl: String? , + var organizationsUrl: String? , + var reposUrl: String? , + var eventsUrl: String? , + var receivedEventsUrl: String? , + var type: String? , + var site_admin: Boolean?, + var name: String? , + var company: String? , + var blog: String? , + var location: String? , + var email: String? , + var hireable: Boolean?, + var bio: String? , + var twitterUsername: String? , + var publicRepos: Int? , + var publicGists: Int? , + var followers: Int? , + var following: Int? , + var createdAt: String? , + var updatedAt: String? +) + +fun User.mapToPresentationModel(): UserItem { + return UserItem( + login = login ?: "N/A", + id = id ?: 0, + nodeId = nodeId ?: "", + avatarUrl = avatarUrl ?: "", + gravatarId = gravatarId ?: "", + url = url ?: "", + htmlUrl = htmlUrl ?: "", + followersUrl = followersUrl ?: "", + followingUrl = followingUrl ?: "", + gistsUrl = gistsUrl ?: "", + starredUrl = starredUrl ?: "", + subscriptionsUrl = subscriptionsUrl ?: "", + organizationsUrl = organizationsUrl ?: "", + reposUrl = reposUrl ?: "", + eventsUrl = eventsUrl ?: "", + receivedEventsUrl = receivedEventsUrl ?: "", + type = type ?: "", + site_admin = site_admin ?: false, + name = name ?: "N/A", + company = company ?: "N/A", + blog = blog ?: "N/A", + location = location ?: "N/A", + email = email ?: "N/A", + hireable = hireable ?: false, + bio = bio ?: "N/A", + twitterUsername = twitterUsername ?: "N/A", + publicRepos = publicRepos ?: 0, + publicGists = publicGists ?: 0, + followers = followers ?: 0, + following = following ?: 0, + createdAt = createdAt ?: "N/A", + updatedAt = updatedAt ?: "N/A" + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/domain/repository/GithubRepository.kt b/app/src/main/java/com/mydigipay/challenge/domain/repository/GithubRepository.kt new file mode 100644 index 00000000..4cff7043 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/domain/repository/GithubRepository.kt @@ -0,0 +1,12 @@ +package com.mydigipay.challenge.domain.repository + +import com.mydigipay.challenge.domain.model.Commit +import com.mydigipay.challenge.domain.model.RemoteRepository +import com.mydigipay.challenge.domain.model.User +import io.reactivex.Single + +interface GithubRepository { + fun search(query: String): Single> + fun getUser(): Single + fun getCommits(owner: String, repo: String): Single> +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/domain/repository/TokenRepository.kt b/app/src/main/java/com/mydigipay/challenge/domain/repository/TokenRepository.kt new file mode 100644 index 00000000..838280f5 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/domain/repository/TokenRepository.kt @@ -0,0 +1,9 @@ +package com.mydigipay.challenge.domain.repository + +import io.reactivex.Completable + +interface TokenRepository { + fun saveToken(token: String): Completable + fun readToken(): String + fun fetchAccessToken(code: String): Completable +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/domain/usecase/AuthUseCase.kt b/app/src/main/java/com/mydigipay/challenge/domain/usecase/AuthUseCase.kt new file mode 100644 index 00000000..09f2b064 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/domain/usecase/AuthUseCase.kt @@ -0,0 +1,17 @@ +package com.mydigipay.challenge.domain.usecase + +import com.mydigipay.challenge.domain.repository.TokenRepository +import io.reactivex.Completable +import io.reactivex.Single +import javax.inject.Inject + +class AuthUseCase @Inject constructor(private val tokenRepository: TokenRepository) { + + fun isUserAuthorized(): Boolean { + return !tokenRepository.readToken().isBlank() + } + + fun fetchAccessToken(code: String): Completable { + return tokenRepository.fetchAccessToken(code) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/domain/usecase/CommitUseCase.kt b/app/src/main/java/com/mydigipay/challenge/domain/usecase/CommitUseCase.kt new file mode 100644 index 00000000..02d2a210 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/domain/usecase/CommitUseCase.kt @@ -0,0 +1,13 @@ +package com.mydigipay.challenge.domain.usecase + +import com.mydigipay.challenge.data.datasource.remote.GithubDataSource +import com.mydigipay.challenge.domain.model.Commit +import io.reactivex.Single +import javax.inject.Inject + +class CommitUseCase @Inject constructor(private val githubDataSource: GithubDataSource) { + + fun getCommits(owner: String, repo: String): Single> { + return githubDataSource.getCommits(owner, repo) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/domain/usecase/SearchUseCase.kt b/app/src/main/java/com/mydigipay/challenge/domain/usecase/SearchUseCase.kt new file mode 100644 index 00000000..5689838f --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/domain/usecase/SearchUseCase.kt @@ -0,0 +1,14 @@ +package com.mydigipay.challenge.domain.usecase + +import com.mydigipay.challenge.domain.model.RemoteRepository +import com.mydigipay.challenge.domain.repository.GithubRepository +import io.reactivex.Observable +import io.reactivex.Single +import javax.inject.Inject + +class SearchUseCase @Inject constructor(private val githubRepository: GithubRepository) { + + fun searchRepository(query: String): Single> { + return githubRepository.search(query) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/domain/usecase/UserUseCase.kt b/app/src/main/java/com/mydigipay/challenge/domain/usecase/UserUseCase.kt new file mode 100644 index 00000000..88822891 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/domain/usecase/UserUseCase.kt @@ -0,0 +1,12 @@ +package com.mydigipay.challenge.domain.usecase + +import com.mydigipay.challenge.domain.model.User +import com.mydigipay.challenge.domain.repository.GithubRepository +import io.reactivex.Single +import javax.inject.Inject + +class UserUseCase @Inject constructor(private val githubRepository: GithubRepository) { + fun getUser(): Single { + return githubRepository.getUser() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/github/LoginUriActivity.kt b/app/src/main/java/com/mydigipay/challenge/github/LoginUriActivity.kt deleted file mode 100644 index 399278ed..00000000 --- a/app/src/main/java/com/mydigipay/challenge/github/LoginUriActivity.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.mydigipay.challenge.github - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import com.mydigipay.challenge.network.oauth.RequestAccessToken -import com.mydigipay.challenge.repository.oauth.AccessTokenDataSource -import com.mydigipay.challenge.repository.token.TokenRepository -import kotlinx.android.synthetic.main.login_uri_activity.* -import kotlinx.coroutines.* -import org.koin.android.ext.android.inject - -class LoginUriActivity : Activity() { - private val tokenRepository: TokenRepository by inject() - private val accessTokenDataSource: AccessTokenDataSource by inject() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.login_uri_activity) - } - - override fun onResume() { - super.onResume() - - val intent = intent - if (Intent.ACTION_VIEW == intent.action) { - val uri = intent.data - val code = uri?.getQueryParameter("code") ?: "" - code.takeIf { it.isNotEmpty() }?.let { code -> - val accessTokenJob = CoroutineScope(Dispatchers.IO).launch { - val response = accessTokenDataSource.accessToken( - RequestAccessToken( - CLIENT_ID, - CLIENT_SECRET, - code, - REDIRECT_URI, - "0" - ) - ).await() - - tokenRepository.saveToken(response.accessToken).await() - } - - accessTokenJob.invokeOnCompletion { - CoroutineScope(Dispatchers.Main).launch { - token.text = tokenRepository.readToken().await() - this.cancel() - accessTokenJob.cancelAndJoin() - } - } - } ?: run { finish() } - } - - - } -} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/github/MainActivity.kt b/app/src/main/java/com/mydigipay/challenge/github/MainActivity.kt deleted file mode 100644 index 3ba92da9..00000000 --- a/app/src/main/java/com/mydigipay/challenge/github/MainActivity.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.mydigipay.challenge.github - -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import kotlinx.android.synthetic.main.activity_main.* - -const val CLIENT_ID = "CLIENT_ID" -const val CLIENT_SECRET = "CLIENT_SECRET" -const val REDIRECT_URI = "REDIRECT_URI" - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - - authorize.setOnClickListener { view -> - val url = "https://github.com/login/oauth/authorize?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=repo user&state=0" - val i = Intent(Intent.ACTION_VIEW) - i.data = Uri.parse(url) - startActivity(i) - } - } -} diff --git a/app/src/main/java/com/mydigipay/challenge/network/di/AccessTokenModule.kt b/app/src/main/java/com/mydigipay/challenge/network/di/AccessTokenModule.kt deleted file mode 100644 index 34fcc6cf..00000000 --- a/app/src/main/java/com/mydigipay/challenge/network/di/AccessTokenModule.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.mydigipay.challenge.network.di - -import com.mydigipay.challenge.network.oauth.AccessTokenService -import com.mydigipay.challenge.repository.oauth.AccessTokenDataSource -import com.mydigipay.challenge.repository.oauth.AccessTokenDataSourceImpl -import org.koin.core.qualifier.named -import org.koin.dsl.module -import retrofit2.Retrofit - -val accessTokenModule = module { - factory { get(named(RETROFIT)).create(AccessTokenService::class.java) } - factory { AccessTokenDataSourceImpl(get()) as AccessTokenDataSource } -} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/network/di/NetworkModule.kt b/app/src/main/java/com/mydigipay/challenge/network/di/NetworkModule.kt deleted file mode 100644 index 60ae9e5b..00000000 --- a/app/src/main/java/com/mydigipay/challenge/network/di/NetworkModule.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.mydigipay.challenge.network.di - -import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory -import com.mydigipay.challenge.repository.token.TokenRepository -import com.mydigipay.challenge.repository.token.TokenRepositoryImpl -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import org.koin.core.qualifier.named -import org.koin.dsl.module -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import java.util.concurrent.TimeUnit - -const val OK_HTTP = "OK_HTTP" -const val RETROFIT = "RETROFIT" -const val READ_TIMEOUT = "READ_TIMEOUT" -const val WRITE_TIMEOUT = "WRITE_TIMEOUT" -const val CONNECTION_TIMEOUT = "CONNECTION_TIMEOUT" -val networkModule = module { - - single(named(READ_TIMEOUT)) { 30 * 1000 } - single(named(WRITE_TIMEOUT)) { 10 * 1000 } - single(named(CONNECTION_TIMEOUT)) { 10 * 1000 } - - factory { - HttpLoggingInterceptor() - .setLevel(HttpLoggingInterceptor.Level.HEADERS) - .setLevel(HttpLoggingInterceptor.Level.BODY) - } - - factory(named(OK_HTTP)) { - OkHttpClient.Builder() - .readTimeout(get(named(READ_TIMEOUT)), TimeUnit.MILLISECONDS) - .writeTimeout(get(named(WRITE_TIMEOUT)), TimeUnit.MILLISECONDS) - .connectTimeout(get(named(CONNECTION_TIMEOUT)), TimeUnit.MILLISECONDS) - .addInterceptor(get()) - .build() - } - - single(named(RETROFIT)) { - Retrofit.Builder() - .client(get(named(OK_HTTP))) - .baseUrl("http://api.github.com") - .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(CoroutineCallAdapterFactory()) - .build() - } - - single { - TokenRepositoryImpl(get()) as TokenRepository - } -} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/network/oauth/AccessTokenService.kt b/app/src/main/java/com/mydigipay/challenge/network/oauth/AccessTokenService.kt deleted file mode 100644 index 38dd31ad..00000000 --- a/app/src/main/java/com/mydigipay/challenge/network/oauth/AccessTokenService.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.mydigipay.challenge.network.oauth - -import kotlinx.coroutines.Deferred -import retrofit2.http.Body -import retrofit2.http.Headers -import retrofit2.http.POST - -interface AccessTokenService { - @Headers("Accept:application/json") - @POST("https://github.com/login/oauth/access_token") - fun accessToken(@Body requestAccessToken: RequestAccessToken) : Deferred -} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/presentation/auth/AuthActivity.kt b/app/src/main/java/com/mydigipay/challenge/presentation/auth/AuthActivity.kt new file mode 100644 index 00000000..4fd87ab5 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/presentation/auth/AuthActivity.kt @@ -0,0 +1,103 @@ +package com.mydigipay.challenge.presentation.auth + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import com.mydigipay.challenge.app.Const.CLIENT_ID +import com.mydigipay.challenge.app.Const.REDIRECT_URI +import com.mydigipay.challenge.app.Const.STATE +import com.mydigipay.challenge.app.ViewModelProviderFactory +import com.mydigipay.challenge.app.component +import com.mydigipay.challenge.presentation.github.MainActivity +import com.mydigipay.challenge.presentation.github.R +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.activity_auth.* +import javax.inject.Inject + + +class AuthActivity : AppCompatActivity() { + + private lateinit var compositeDisposable: CompositeDisposable + + @Inject + lateinit var factory: ViewModelProviderFactory + lateinit var viewModel: AuthViewModel + private val keyCode = "code" + private var code: String = "" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + component.viewModelProviderFactory.create().inject(this) + viewModel = ViewModelProvider(this, factory)[AuthViewModel::class.java] + + compositeDisposable = CompositeDisposable() + + if (viewModel.isUserAuthorized()) { + startActivity(Intent(this, MainActivity::class.java)) + finish() + } else { + setContentView(R.layout.activity_auth) + authorize_btn.setOnClickListener { + val url = + "https://github.com/login/oauth/authorize?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=repo user&state=$STATE" + val i = Intent(Intent.ACTION_VIEW) + i.data = Uri.parse(url) + startActivity(i) + } + } + + + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + intent?.let { + if (Intent.ACTION_VIEW == it.action) { + code = it.data?.getQueryParameter(keyCode) ?: "" + viewModel.fetchAccessToken(code) + viewModel.getState().observeOn(AndroidSchedulers.mainThread()) + .subscribe { + handleViewState(it) + }.let { + compositeDisposable.add(it) + } + } + } + } + + private fun handleViewState(state: AuthActivityState) { + when (state) { + is AuthActivityState.Loading -> { + networkErrorGroup.visibility = GONE + authorize_btn.visibility = GONE + loading.show() + + } + is AuthActivityState.SuccessfullyGotToken -> { + startActivity(Intent(this, MainActivity::class.java)) + finish() + } + is AuthActivityState.Error -> { + networkErrorGroup.visibility = VISIBLE + authorize_btn.visibility = GONE + loading.hide() + } + } + } + + fun tryAgain(view: View) { + viewModel.fetchAccessToken(code) + } + + override fun onDestroy() { + super.onDestroy() + compositeDisposable.dispose() + } +} diff --git a/app/src/main/java/com/mydigipay/challenge/presentation/auth/AuthViewModel.kt b/app/src/main/java/com/mydigipay/challenge/presentation/auth/AuthViewModel.kt new file mode 100644 index 00000000..104b764f --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/presentation/auth/AuthViewModel.kt @@ -0,0 +1,42 @@ +package com.mydigipay.challenge.presentation.auth + +import androidx.lifecycle.ViewModel +import com.jakewharton.rxrelay2.BehaviorRelay +import com.mydigipay.challenge.domain.usecase.AuthUseCase +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class AuthViewModel @Inject constructor(private val useCase: AuthUseCase) : + ViewModel() { + private val compositeDisposable = CompositeDisposable() + private val state: BehaviorRelay = BehaviorRelay.create() + fun getState() = state.hide() + + fun isUserAuthorized(): Boolean { + return useCase.isUserAuthorized() + } + + fun fetchAccessToken(code: String) { + state.accept(AuthActivityState.Loading) + useCase.fetchAccessToken(code).subscribeOn(Schedulers.io()) + .subscribe({ + state.accept(AuthActivityState.SuccessfullyGotToken) + }, { + state.accept(AuthActivityState.Error) + }).let { + compositeDisposable.add(it) + } + } + + override fun onCleared() { + super.onCleared() + compositeDisposable.dispose() + } +} + +sealed class AuthActivityState { + object Loading : AuthActivityState() + object Error : AuthActivityState() + object SuccessfullyGotToken : AuthActivityState() +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/presentation/github/MainActivity.kt b/app/src/main/java/com/mydigipay/challenge/presentation/github/MainActivity.kt new file mode 100644 index 00000000..756d16d3 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/presentation/github/MainActivity.kt @@ -0,0 +1,13 @@ +package com.mydigipay.challenge.presentation.github + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class MainActivity : AppCompatActivity() { + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/presentation/github/SearchToCommitViewModel.kt b/app/src/main/java/com/mydigipay/challenge/presentation/github/SearchToCommitViewModel.kt new file mode 100644 index 00000000..eefcdc9e --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/presentation/github/SearchToCommitViewModel.kt @@ -0,0 +1,8 @@ +package com.mydigipay.challenge.presentation.github + +import androidx.lifecycle.ViewModel + +class SearchToCommitViewModel : ViewModel() { + var owner: String = "" + var repo: String = "" +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/presentation/github/commit/CommitAdapter.kt b/app/src/main/java/com/mydigipay/challenge/presentation/github/commit/CommitAdapter.kt new file mode 100644 index 00000000..0f83c690 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/presentation/github/commit/CommitAdapter.kt @@ -0,0 +1,56 @@ +package com.mydigipay.challenge.presentation.github.commit + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.jakewharton.rxrelay2.PublishRelay +import com.mydigipay.challenge.presentation.github.R +import com.mydigipay.challenge.presentation.github.databinding.ItemCommitBinding +import com.mydigipay.challenge.presentation.github.databinding.ItemRepoBinding +import com.mydigipay.challenge.presentation.model.CommitItem +import com.mydigipay.challenge.presentation.model.RepositoryItem +import kotlinx.android.synthetic.main.item_repo.view.* + +class CommitAdapter : + ListAdapter( + DIFF_CALLBACK() + ) { + + + class DIFF_CALLBACK : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: CommitItem, newItem: CommitItem): Boolean { + return oldItem.name.equals(newItem.name) + } + + override fun areContentsTheSame(oldItem: CommitItem, newItem: CommitItem): Boolean { + return oldItem.name.equals(newItem.name) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommitViewHolder { + return CommitViewHolder( + DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.item_commit, + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: CommitViewHolder, position: Int) { + val commit = getItem(position) + holder.binding.commit = commit + } + + + class CommitViewHolder(val binding: ItemCommitBinding) : + RecyclerView.ViewHolder(binding.root) { + init { + binding.commitMessageTv.isSelected = true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/presentation/github/commit/CommitFragment.kt b/app/src/main/java/com/mydigipay/challenge/presentation/github/commit/CommitFragment.kt new file mode 100644 index 00000000..26530c51 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/presentation/github/commit/CommitFragment.kt @@ -0,0 +1,136 @@ +package com.mydigipay.challenge.presentation.github.commit + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.mydigipay.challenge.app.ViewModelProviderFactory +import com.mydigipay.challenge.app.component +import com.mydigipay.challenge.presentation.github.R +import com.mydigipay.challenge.presentation.github.SearchToCommitViewModel +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.fragment_commit.* +import kotlinx.android.synthetic.main.fragment_commit.errorTv +import kotlinx.android.synthetic.main.fragment_commit.loading +import kotlinx.android.synthetic.main.fragment_commit.tryAgainBtn +import javax.inject.Inject + +class CommitFragment : Fragment() { + + private val repoSelectionViewModel: SearchToCommitViewModel by activityViewModels() + private lateinit var compositeDisposable: CompositeDisposable + + @Inject + lateinit var factory: ViewModelProviderFactory + lateinit var viewModel: CommitViewModel + private var lastVisibleCommit = 0 + private val stateBundlePositionKey = "POSITION_KEY" + private lateinit var adapter: CommitAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_commit, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + compositeDisposable = CompositeDisposable() + component.viewModelProviderFactory.create().inject(this) + viewModel = ViewModelProvider(this, factory)[CommitViewModel::class.java] + + initViewInteraction(savedInstanceState) + initDataInteraction(savedInstanceState) + + } + + private fun initViewInteraction(savedInstanceState: Bundle?) { + savedInstanceState?.let { + lastVisibleCommit = it.getInt(stateBundlePositionKey) + } + adapter = + CommitAdapter() + + commitRv.layoutManager = LinearLayoutManager(requireContext()) + commitRv.adapter = adapter + commitRv.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + when (newState) { + RecyclerView.SCROLL_STATE_IDLE -> { + lastVisibleCommit = + (recyclerView.layoutManager as LinearLayoutManager).findLastCompletelyVisibleItemPosition() + } + } + } + }) + tryAgainBtn.setOnClickListener { + viewModel.getCommits(repoSelectionViewModel.owner, repoSelectionViewModel.repo) + } + } + + private fun initDataInteraction(savedInstanceState: Bundle?) { + if (savedInstanceState == null) + viewModel.getCommits(repoSelectionViewModel.owner, repoSelectionViewModel.repo) + viewModel.getState() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + handleState(it) + }.let { + compositeDisposable.add(it) + } + } + + private fun handleState(state: CommitFragmentState) { + when (state) { + is CommitFragmentState.Error -> { + loading.hide() + commitRv.visibility = View.GONE + errorTv.visibility = View.VISIBLE + tryAgainBtn.visibility = View.VISIBLE + errorTv.text = getString(R.string.netwrok_error) + } + is CommitFragmentState.GotCommits -> { + loading.hide() + adapter.submitList(state.commits) + errorTv.visibility = View.GONE + tryAgainBtn.visibility = View.GONE + commitRv.visibility = View.VISIBLE + commitRv.scrollToPosition(lastVisibleCommit) + } + is CommitFragmentState.Loading -> { + loading.show() + commitRv.visibility = View.GONE + errorTv.visibility = View.GONE + tryAgainBtn.visibility = View.GONE + } + is CommitFragmentState.NoCommits -> { + loading.hide() + commitRv.visibility = View.GONE + tryAgainBtn.visibility = View.GONE + errorTv.visibility = View.VISIBLE + errorTv.text = getString(R.string.no_commit) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + compositeDisposable.dispose() + + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putInt(stateBundlePositionKey, lastVisibleCommit) + super.onSaveInstanceState(outState) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/presentation/github/commit/CommitViewModel.kt b/app/src/main/java/com/mydigipay/challenge/presentation/github/commit/CommitViewModel.kt new file mode 100644 index 00000000..e808db72 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/presentation/github/commit/CommitViewModel.kt @@ -0,0 +1,47 @@ +package com.mydigipay.challenge.presentation.github.commit + +import androidx.lifecycle.ViewModel +import com.jakewharton.rxrelay2.BehaviorRelay +import com.mydigipay.challenge.domain.model.mapToPresentationModel +import com.mydigipay.challenge.domain.usecase.CommitUseCase +import com.mydigipay.challenge.presentation.model.CommitItem +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class CommitViewModel @Inject constructor(private val commitUseCase: CommitUseCase) : ViewModel() { + + private val compositeDisposable = CompositeDisposable() + private val state: BehaviorRelay = BehaviorRelay.create() + + fun getState() = state.hide() + + fun getCommits(owner: String, repo: String) { + state.accept(CommitFragmentState.Loading) + commitUseCase.getCommits(owner, repo) + .subscribeOn(Schedulers.io()) + .subscribe({ + if(it.isEmpty()){ + state.accept(CommitFragmentState.NoCommits) + }else{ + state.accept(CommitFragmentState.GotCommits(it.map { it.mapToPresentationModel() })) + } + }, { + state.accept(CommitFragmentState.Error) + }).let { + compositeDisposable.add(it) + } + } + + override fun onCleared() { + super.onCleared() + compositeDisposable.dispose() + } +} + +sealed class CommitFragmentState { + object Error : CommitFragmentState() + object Loading : CommitFragmentState() + object NoCommits : CommitFragmentState() + data class GotCommits(val commits: List) : CommitFragmentState() +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/presentation/github/search/RepoAdapter.kt b/app/src/main/java/com/mydigipay/challenge/presentation/github/search/RepoAdapter.kt new file mode 100644 index 00000000..4c6f362a --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/presentation/github/search/RepoAdapter.kt @@ -0,0 +1,59 @@ +package com.mydigipay.challenge.presentation.github.search + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.jakewharton.rxrelay2.PublishRelay +import com.mydigipay.challenge.presentation.github.R +import com.mydigipay.challenge.presentation.github.databinding.ItemRepoBinding +import com.mydigipay.challenge.presentation.model.RepositoryItem +import kotlinx.android.synthetic.main.item_repo.view.* + +class RepoAdapter : + ListAdapter( + DIFF_CALLBACK() + ) { + + private val onItemClick = PublishRelay.create() + + class DIFF_CALLBACK : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: RepositoryItem, newItem: RepositoryItem): Boolean { + return oldItem.fullName.equals(newItem.fullName) + } + + override fun areContentsTheSame(oldItem: RepositoryItem, newItem: RepositoryItem): Boolean { + return oldItem.fullName.equals(newItem.fullName) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RepoViewHolder { + return RepoViewHolder( + DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.item_repo, + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: RepoViewHolder, position: Int) { + val repo = getItem(position) + holder.binding.repo = repo + holder.itemView.setOnClickListener { + onItemClick.accept(repo) + } + } + + fun selectedRepo() = onItemClick.hide() + + class RepoViewHolder(val binding: ItemRepoBinding) : + RecyclerView.ViewHolder(binding.root) { + init { + binding.root.fullname_tv.isSelected = true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/presentation/github/search/SearchFragment.kt b/app/src/main/java/com/mydigipay/challenge/presentation/github/search/SearchFragment.kt new file mode 100644 index 00000000..45c517d7 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/presentation/github/search/SearchFragment.kt @@ -0,0 +1,187 @@ +package com.mydigipay.challenge.presentation.github.search + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.jakewharton.rxbinding3.widget.textChanges +import com.mydigipay.challenge.app.ViewModelProviderFactory +import com.mydigipay.challenge.app.component +import com.mydigipay.challenge.presentation.github.R +import com.mydigipay.challenge.presentation.github.SearchToCommitViewModel +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.fragment_search.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + + +class SearchFragment : Fragment() { + + private val searchInputDelay = 1000L + private val searchInputDelayTimeUnit = TimeUnit.MILLISECONDS + private lateinit var compositeDisposable: CompositeDisposable + private var lastVisibleRepo = 0 + private val stateBundlePositionKey = "POSITION_KEY" + private val stateBundleSearchQueryKey = "QUERY_KEY" + private lateinit var adapter: RepoAdapter + private var searchQuery = "" + private val repoSelectionViewModel: SearchToCommitViewModel by activityViewModels() + + @Inject + lateinit var factory: ViewModelProviderFactory + lateinit var viewModel: SearchViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_search, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + compositeDisposable = CompositeDisposable() + component.viewModelProviderFactory.create().inject(this) + viewModel = ViewModelProvider(this, factory)[SearchViewModel::class.java] + + initViewInteraction(savedInstanceState) + initDataInteraction() + } + + private fun initDataInteraction() { + + viewModel.getState() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + handleState(it) + }.let { + compositeDisposable.add(it) + } + + } + + private fun handleState(state: SearchFragmentState) { + when (state) { + is SearchFragmentState.Error -> { + loading.hide() + repoRv.visibility = GONE + errorTv.visibility = VISIBLE + tryAgainBtn.visibility = VISIBLE + errorTv.text = getString(R.string.netwrok_error) + } + is SearchFragmentState.SearchedRepository -> { + loading.hide() + adapter.submitList(state.repositories) + errorTv.visibility = GONE + tryAgainBtn.visibility = GONE + repoRv.visibility = VISIBLE + repoRv.scrollToPosition(lastVisibleRepo) + } + is SearchFragmentState.Loading -> { + loading.show() + repoRv.visibility = GONE + errorTv.visibility = GONE + tryAgainBtn.visibility = GONE + } + is SearchFragmentState.NoRepoFound -> { + loading.hide() + repoRv.visibility = GONE + errorTv.visibility = VISIBLE + tryAgainBtn.visibility = GONE + errorTv.text = getString(R.string.no_repo_found) + } + is SearchFragmentState.EmptyQuery -> { + loading.hide() + repoRv.visibility = GONE + errorTv.visibility = VISIBLE + tryAgainBtn.visibility = GONE + errorTv.text = getString(R.string.empty_search) + } + } + } + + private fun initViewInteraction(savedInstanceState: Bundle?) { + savedInstanceState?.let { + lastVisibleRepo = it.getInt(stateBundlePositionKey) + searchQuery = it.getString(stateBundleSearchQueryKey) ?: "" + } + initRecyclerView() + initSearchBar() + tryAgainBtn.setOnClickListener { + if (searchQuery.isNotEmpty() && searchQuery.isNotBlank()) + viewModel.searchRepository(searchQuery) + } + userAvatarImg.setOnClickListener { + findNavController().navigate(R.id.action_searchFragment_to_userProfileFragment) + } + } + + private fun initRecyclerView() { + adapter = + RepoAdapter() + adapter.selectedRepo() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + repoSelectionViewModel.owner = it.repoOwnerItem?.login ?: "" + repoSelectionViewModel.repo = it.name + findNavController().navigate(R.id.action_searchFragment_to_commitFragment) + }.let { + compositeDisposable.add(it) + } + repoRv.layoutManager = LinearLayoutManager(requireContext()) + repoRv.adapter = adapter + repoRv.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + when (newState) { + RecyclerView.SCROLL_STATE_IDLE -> { + lastVisibleRepo = + (recyclerView.layoutManager as LinearLayoutManager).findLastCompletelyVisibleItemPosition() + } + } + } + }) + } + + + private fun initSearchBar() { + searchEdt + .textChanges() + .skipInitialValue() + .debounce(searchInputDelay, searchInputDelayTimeUnit) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe({ + if (!it.toString().equals(searchQuery)) { + searchQuery = it.toString() + viewModel.searchRepository(searchQuery) + } + }, { + }).let { + compositeDisposable.add(it) + } + } + + override fun onDestroyView() { + super.onDestroyView() + compositeDisposable.dispose() + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putInt(stateBundlePositionKey, lastVisibleRepo) + outState.putString(stateBundleSearchQueryKey, searchQuery) + super.onSaveInstanceState(outState) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/presentation/github/search/SearchViewModel.kt b/app/src/main/java/com/mydigipay/challenge/presentation/github/search/SearchViewModel.kt new file mode 100644 index 00000000..03d97102 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/presentation/github/search/SearchViewModel.kt @@ -0,0 +1,66 @@ +package com.mydigipay.challenge.presentation.github.search + +import android.util.Log +import androidx.lifecycle.ViewModel +import com.jakewharton.rxrelay2.BehaviorRelay +import com.mydigipay.challenge.domain.model.mapToPresentationModel +import com.mydigipay.challenge.domain.usecase.SearchUseCase +import com.mydigipay.challenge.domain.usecase.UserUseCase +import com.mydigipay.challenge.presentation.model.RepositoryItem +import com.mydigipay.challenge.presentation.model.UserItem +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class SearchViewModel @Inject constructor( + private val searchUseCase: SearchUseCase +) : + ViewModel() { + + private val compositeDisposable = CompositeDisposable() + private val state: BehaviorRelay = BehaviorRelay.create() + private val unprocessableEntity = "422" + + fun getState() = state.hide() + + + fun searchRepository(query: String) { + state.accept(SearchFragmentState.Loading) + searchUseCase.searchRepository(query) + .subscribeOn(Schedulers.io()) + .subscribe({ + if (it.isEmpty()) { + state.accept(SearchFragmentState.NoRepoFound) + } else { + state.accept( + SearchFragmentState.SearchedRepository( + it.map { + it.mapToPresentationModel() + }) + ) + } + }, { + if (it.message != null && it.message.toString().contains(unprocessableEntity)) { + state.accept(SearchFragmentState.EmptyQuery) + } else { + state.accept(SearchFragmentState.Error) + } + }).let { + compositeDisposable.add(it) + } + + } + + override fun onCleared() { + super.onCleared() + compositeDisposable.dispose() + } +} + +sealed class SearchFragmentState() { + data class SearchedRepository(val repositories: List) : SearchFragmentState() + object Loading : SearchFragmentState() + object Error : SearchFragmentState() + object NoRepoFound : SearchFragmentState() + object EmptyQuery : SearchFragmentState() +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/presentation/github/user/UserProfileFragment.kt b/app/src/main/java/com/mydigipay/challenge/presentation/github/user/UserProfileFragment.kt new file mode 100644 index 00000000..0bc63da4 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/presentation/github/user/UserProfileFragment.kt @@ -0,0 +1,89 @@ +package com.mydigipay.challenge.presentation.github.user + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import com.mydigipay.challenge.app.ViewModelProviderFactory +import com.mydigipay.challenge.app.component +import com.mydigipay.challenge.presentation.github.R +import com.mydigipay.challenge.presentation.github.databinding.FragmentUserProfileBinding +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.fragment_user_profile.* +import javax.inject.Inject + +class UserProfileFragment : Fragment() { + + private lateinit var compositeDisposable: CompositeDisposable + + @Inject + lateinit var factory: ViewModelProviderFactory + lateinit var viewModel: UserProfileViewModel + private lateinit var binding: FragmentUserProfileBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = + DataBindingUtil.inflate(inflater, R.layout.fragment_user_profile, container, false) + return binding.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + compositeDisposable = CompositeDisposable() + component.viewModelProviderFactory.create().inject(this) + viewModel = ViewModelProvider(this, factory)[UserProfileViewModel::class.java] + + initDataInteraction(savedInstanceState) + + + } + + private fun initDataInteraction(savedInstanceState: Bundle?) { + if (savedInstanceState == null){ + viewModel.fetchUserInfo() + } + viewModel.getState() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + handleState(it) + }.let { + compositeDisposable.add(it) + } + + } + + private fun handleState(state: UserProfileFragmentState) { + when (state) { + is UserProfileFragmentState.GotUser -> { + loading.hide() + binding.user = state.user + } + is UserProfileFragmentState.Error -> { + loading.hide() + Toast.makeText(context, getString(R.string.user_profile_error), Toast.LENGTH_LONG) + .show() + findNavController().navigateUp() + } + is UserProfileFragmentState.Loading -> { + loading.show() + } + } + + } + + override fun onDestroyView() { + super.onDestroyView() + compositeDisposable.dispose() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/presentation/github/user/UserProfileViewModel.kt b/app/src/main/java/com/mydigipay/challenge/presentation/github/user/UserProfileViewModel.kt new file mode 100644 index 00000000..89f33646 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/presentation/github/user/UserProfileViewModel.kt @@ -0,0 +1,40 @@ +package com.mydigipay.challenge.presentation.github.user + +import androidx.lifecycle.ViewModel +import com.jakewharton.rxrelay2.BehaviorRelay +import com.mydigipay.challenge.domain.model.mapToPresentationModel +import com.mydigipay.challenge.domain.usecase.UserUseCase +import com.mydigipay.challenge.presentation.model.UserItem +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class UserProfileViewModel @Inject constructor(private val userUseCase: UserUseCase) : ViewModel() { + private val compositeDisposable = CompositeDisposable() + private val state: BehaviorRelay = BehaviorRelay.create() + + fun getState() = state.hide() + + fun fetchUserInfo() { + state.accept(UserProfileFragmentState.Loading) + userUseCase.getUser().subscribeOn(Schedulers.io()) + .subscribe({ + state.accept(UserProfileFragmentState.GotUser(it.mapToPresentationModel())) + }, { + state.accept(UserProfileFragmentState.Error) + }).let { + compositeDisposable.add(it) + } + } + + override fun onCleared() { + super.onCleared() + compositeDisposable.dispose() + } +} + +sealed class UserProfileFragmentState() { + data class GotUser(val user: UserItem) : UserProfileFragmentState() + object Loading : UserProfileFragmentState() + object Error : UserProfileFragmentState() +} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/presentation/model/CommitItem.kt b/app/src/main/java/com/mydigipay/challenge/presentation/model/CommitItem.kt new file mode 100644 index 00000000..34445f95 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/presentation/model/CommitItem.kt @@ -0,0 +1,11 @@ +package com.mydigipay.challenge.presentation.model + +import com.mydigipay.challenge.domain.model.CommitAuthor + +data class CommitItem( + val message: String, + val name: String, + val email: String, + val date: String, + val commentsCount: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/presentation/model/RepositoryItem.kt b/app/src/main/java/com/mydigipay/challenge/presentation/model/RepositoryItem.kt new file mode 100644 index 00000000..36285c2f --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/presentation/model/RepositoryItem.kt @@ -0,0 +1,27 @@ +package com.mydigipay.challenge.presentation.model + +data class RepositoryItem( + var id: Int, + var nodeId: String, + var name: String, + var fullName: String, + var repoOwnerItem: RepositoryOwnerItem?, + var isPrivate: Boolean, + var htmlUrl: String, + var description: String, + var isFork: Boolean, + var url: String, + var createdAt: String, + var updatedAt: String, + var pushedAt: String, + var homepage: String, + var size: Int, + var stargazersCount: Int, + var watchersCount: Int, + var language: String, + var forksCount: Int, + var openIssuesCount: Int, + var masterBranch: String, + var defaultBranch: String, + var score: Double +) \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/presentation/model/RepositoryOwnerItem.kt b/app/src/main/java/com/mydigipay/challenge/presentation/model/RepositoryOwnerItem.kt new file mode 100644 index 00000000..8bdea44a --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/presentation/model/RepositoryOwnerItem.kt @@ -0,0 +1,12 @@ +package com.mydigipay.challenge.presentation.model + +data class RepositoryOwnerItem( + var login: String, + var id: Int, + var nodeId: String, + var avatarUrl: String, + var gravatarId: String, + var url: String, + var receivedEventsUrl: String, + var type: String +) \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/presentation/model/UserItem.kt b/app/src/main/java/com/mydigipay/challenge/presentation/model/UserItem.kt new file mode 100644 index 00000000..cd7db0a5 --- /dev/null +++ b/app/src/main/java/com/mydigipay/challenge/presentation/model/UserItem.kt @@ -0,0 +1,36 @@ +package com.mydigipay.challenge.presentation.model + +data class UserItem( + var login: String, + var id: Int, + var nodeId: String, + var avatarUrl: String, + var gravatarId: String, + var url: String, + var htmlUrl: String, + var followersUrl: String, + var followingUrl: String, + var gistsUrl: String, + var starredUrl: String, + var subscriptionsUrl: String, + var organizationsUrl: String, + var reposUrl: String, + var eventsUrl: String, + var receivedEventsUrl: String, + var type: String, + var site_admin: Boolean = false, + var name: String, + var company: String, + var blog: String, + var location: String, + var email: String, + var hireable: Boolean = false, + var bio: String, + var twitterUsername: String, + var publicRepos: Int, + var publicGists: Int, + var followers: Int, + var following: Int, + var createdAt: String, + var updatedAt: String +) \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/repository/oauth/AccessTokenDataSource.kt b/app/src/main/java/com/mydigipay/challenge/repository/oauth/AccessTokenDataSource.kt deleted file mode 100644 index ad56e5d7..00000000 --- a/app/src/main/java/com/mydigipay/challenge/repository/oauth/AccessTokenDataSource.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.mydigipay.challenge.repository.oauth - -import com.mydigipay.challenge.network.oauth.RequestAccessToken -import com.mydigipay.challenge.network.oauth.ResponseAccessToken -import kotlinx.coroutines.Deferred - -interface AccessTokenDataSource { - fun accessToken(requestAccessToken: RequestAccessToken): Deferred -} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/repository/oauth/AccessTokenDataSourceImpl.kt b/app/src/main/java/com/mydigipay/challenge/repository/oauth/AccessTokenDataSourceImpl.kt deleted file mode 100644 index d480bc06..00000000 --- a/app/src/main/java/com/mydigipay/challenge/repository/oauth/AccessTokenDataSourceImpl.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.mydigipay.challenge.repository.oauth - -import com.mydigipay.challenge.network.oauth.AccessTokenService -import com.mydigipay.challenge.network.oauth.RequestAccessToken - -class AccessTokenDataSourceImpl(private val accessTokenService: AccessTokenService) : AccessTokenDataSource { - override fun accessToken(requestAccessToken: RequestAccessToken) = accessTokenService.accessToken(requestAccessToken) -} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/repository/token/TokenRepository.kt b/app/src/main/java/com/mydigipay/challenge/repository/token/TokenRepository.kt deleted file mode 100644 index 8338e729..00000000 --- a/app/src/main/java/com/mydigipay/challenge/repository/token/TokenRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.mydigipay.challenge.repository.token - -import kotlinx.coroutines.Deferred - -interface TokenRepository { - fun saveToken(token: String) : Deferred - fun readToken(): Deferred -} \ No newline at end of file diff --git a/app/src/main/java/com/mydigipay/challenge/repository/token/TokenRepositoryImpl.kt b/app/src/main/java/com/mydigipay/challenge/repository/token/TokenRepositoryImpl.kt deleted file mode 100644 index 5aad06ef..00000000 --- a/app/src/main/java/com/mydigipay/challenge/repository/token/TokenRepositoryImpl.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.mydigipay.challenge.repository.token - -import android.content.SharedPreferences -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async - -private const val TOKEN = "TOKEN" - -class TokenRepositoryImpl(private val sharedPreferences: SharedPreferences) : TokenRepository { - override fun saveToken(token: String): Deferred = - CoroutineScope(Dispatchers.IO).async { sharedPreferences.edit().apply { putString(TOKEN, token) }.apply() } - - - override fun readToken(): Deferred = - CoroutineScope(Dispatchers.IO).async { sharedPreferences.getString(TOKEN, "") ?: "" } -} \ No newline at end of file diff --git a/app/src/main/res/drawable/custom_card.xml b/app/src/main/res/drawable/custom_card.xml new file mode 100644 index 00000000..a27badbf --- /dev/null +++ b/app/src/main/res/drawable/custom_card.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_account.xml b/app/src/main/res/drawable/ic_account.xml new file mode 100644 index 00000000..1383bde5 --- /dev/null +++ b/app/src/main/res/drawable/ic_account.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_company.xml b/app/src/main/res/drawable/ic_company.xml new file mode 100644 index 00000000..6249d3e7 --- /dev/null +++ b/app/src/main/res/drawable/ic_company.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_created_at.xml b/app/src/main/res/drawable/ic_created_at.xml new file mode 100644 index 00000000..7c55895f --- /dev/null +++ b/app/src/main/res/drawable/ic_created_at.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_email.xml b/app/src/main/res/drawable/ic_email.xml new file mode 100644 index 00000000..1cd55538 --- /dev/null +++ b/app/src/main/res/drawable/ic_email.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml new file mode 100644 index 00000000..3fac6067 --- /dev/null +++ b/app/src/main/res/drawable/ic_github.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..5c08e84e --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_location.xml b/app/src/main/res/drawable/ic_location.xml new file mode 100644 index 00000000..635be676 --- /dev/null +++ b/app/src/main/res/drawable/ic_location.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_message.xml b/app/src/main/res/drawable/ic_message.xml new file mode 100644 index 00000000..7f504125 --- /dev/null +++ b/app/src/main/res/drawable/ic_message.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_update.xml b/app/src/main/res/drawable/ic_update.xml new file mode 100644 index 00000000..6afe0759 --- /dev/null +++ b/app/src/main/res/drawable/ic_update.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/user_avatar_ring.xml b/app/src/main/res/drawable/user_avatar_ring.xml new file mode 100644 index 00000000..97d9a32e --- /dev/null +++ b/app/src/main/res/drawable/user_avatar_ring.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_auth.xml b/app/src/main/res/layout/activity_auth.xml new file mode 100644 index 00000000..dde02668 --- /dev/null +++ b/app/src/main/res/layout/activity_auth.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index fe87f339..8e0bf951 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,17 +1,17 @@ - + android:orientation="vertical"> -