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">
-
+ android:layout_height="match_parent"
+ app:defaultNavHost="true"
+ app:navGraph="@navigation/main_nav_graph" />
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_commit.xml b/app/src/main/res/layout/fragment_commit.xml
new file mode 100644
index 00000000..b56ca410
--- /dev/null
+++ b/app/src/main/res/layout/fragment_commit.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml
new file mode 100644
index 00000000..1556cbc0
--- /dev/null
+++ b/app/src/main/res/layout/fragment_search.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_user_profile.xml b/app/src/main/res/layout/fragment_user_profile.xml
new file mode 100644
index 00000000..39da9561
--- /dev/null
+++ b/app/src/main/res/layout/fragment_user_profile.xml
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_commit.xml b/app/src/main/res/layout/item_commit.xml
new file mode 100644
index 00000000..a97834c6
--- /dev/null
+++ b/app/src/main/res/layout/item_commit.xml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_repo.xml b/app/src/main/res/layout/item_repo.xml
new file mode 100644
index 00000000..98603ba7
--- /dev/null
+++ b/app/src/main/res/layout/item_repo.xml
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/login_uri_activity.xml b/app/src/main/res/layout/login_uri_activity.xml
deleted file mode 100644
index 69635ae0..00000000
--- a/app/src/main/res/layout/login_uri_activity.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
index 9e80e0d5..10d92677 100644
--- a/app/src/main/res/menu/menu_main.xml
+++ b/app/src/main/res/menu/menu_main.xml
@@ -1,7 +1,7 @@