Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7ddc396
add dependencies
Einhesari Jul 22, 2020
3780228
add packages
Einhesari Jul 22, 2020
231b62b
replace koin with dagger and create app component
Einhesari Jul 22, 2020
80ca13e
add dagger ViewModelModule and ViewModelFactoryModule
Einhesari Jul 22, 2020
6a6502b
add MainActivityViewModel
Einhesari Jul 22, 2020
1f7814b
place data sources and their implementations in right directories
Einhesari Jul 22, 2020
a1c7284
place repositories and their implementations in right directories
Einhesari Jul 22, 2020
1a49c89
add SharedPrefsModule
Einhesari Jul 22, 2020
6bb7e43
implement scenario for unauthorized user in main activity
Einhesari Jul 22, 2020
ebd05d2
rename LoginUriActivity to GithubActivity for better semantic
Einhesari Jul 22, 2020
faa0eb8
implement remote sources to get access token from server
Einhesari Jul 22, 2020
a801429
check user is authenticated or not in main activity
Einhesari Jul 23, 2020
8b77f66
change MainActivity name to AuthActivity
Einhesari Jul 23, 2020
f7ae7e1
add standard paddings and margins to dimens
Einhesari Jul 23, 2020
7548fb3
handle view states in auth activity
Einhesari Jul 23, 2020
a2634eb
rename GithubActivity to MainActivity for better semantic
Einhesari Jul 23, 2020
2962f92
add main nav graph
Einhesari Jul 23, 2020
fcb9e8e
remove AccessTokenUseCase
Einhesari Jul 23, 2020
498d381
change data source abstraction interfaces into better named packages
Einhesari Jul 23, 2020
031b4d5
change data source implementations into better named packages and imp…
Einhesari Jul 23, 2020
2405156
implement search repository and data source
Einhesari Jul 23, 2020
77886c8
implement search presentation layer
Einhesari Jul 23, 2020
7d6b353
fix navigation graph bug
Einhesari Jul 23, 2020
606a85f
configure network security
Einhesari Jul 23, 2020
d153f9b
fix models to fix search bug
Einhesari Jul 23, 2020
21ab412
add auth package to presentation layer
Einhesari Jul 23, 2020
4503c75
show repos in recyclerview
Einhesari Jul 23, 2020
531c9d0
change SearchDataSource name to GithubDataSource
Einhesari Jul 23, 2020
f88b995
ignore user 401 response and continue developing
Einhesari Jul 24, 2020
f1c93ec
fix get user bug by changing base url scheme to https
Einhesari Jul 24, 2020
0ca5b2a
some UI improvements in search and auth activity
Einhesari Jul 24, 2020
d00dc20
implement commit fragment ui and data source
Einhesari Jul 24, 2020
8da3227
some UI improvement in search view
Einhesari Jul 24, 2020
eb629e8
Implement user profile presentation layer
Einhesari Jul 24, 2020
1e049c0
choose icon for app
Einhesari Jul 24, 2020
896d1ed
check if user is already got in user profile fragment
Einhesari Jul 25, 2020
ce88134
add mockito mockMaker plugin
Einhesari Jul 25, 2020
b370d21
Improvements in data layer models
Einhesari Jul 25, 2020
8aa4b22
add const class to put const values
Einhesari Jul 25, 2020
83ffc78
remove redundant user use case in SearchFragmentViewModel
Einhesari Jul 25, 2020
6515f7a
some renames for better semantic
Einhesari Jul 25, 2020
eae56d2
retain scroll position in screen rotation in commit fragment
Einhesari Jul 25, 2020
43b4f02
unit test remote and local data sources
Einhesari Jul 25, 2020
eb8f3a6
unit test view models
Einhesari Jul 25, 2020
2fbb353
choose better icon for app
Einhesari Jul 25, 2020
ae2d67e
Merge pull request #1 from Einhesari/dev
Einhesari Jul 25, 2020
28c1f0e
Create README.md
Einhesari Jul 25, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions .idea/jarRepositories.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
104 changes: 79 additions & 25 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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'
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down
19 changes: 10 additions & 9 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mydigipay.challenge.github">
<uses-permission android:name="android.permission.INTERNET"/>
package="com.mydigipay.challenge.presentation.github">

<uses-permission android:name="android.permission.INTERNET" />
<application
android:name="com.mydigipay.challenge.app.App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:name="com.mydigipay.challenge.app.App">
android:theme="@style/AppTheme">
<activity
android:name="com.mydigipay.challenge.github.MainActivity"
android:name="com.mydigipay.challenge.presentation.auth.AuthActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity android:name="com.mydigipay.challenge.github.LoginUriActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
Expand All @@ -30,6 +29,8 @@
</intent-filter>
</activity>

<activity android:name="com.mydigipay.challenge.presentation.github.MainActivity" />

</application>

</manifest>
Binary file added app/src/main/ic_launcher-playstore.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 8 additions & 17 deletions app/src/main/java/com/mydigipay/challenge/app/App.kt
Original file line number Diff line number Diff line change
@@ -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)
}

}
32 changes: 32 additions & 0 deletions app/src/main/java/com/mydigipay/challenge/app/BindingAdapter.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
}


}
9 changes: 9 additions & 0 deletions app/src/main/java/com/mydigipay/challenge/app/Const.kt
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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<Class<out ViewModel>, Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return creators[modelClass]?.get() as? T
?: throw IllegalArgumentException("The requested ViewModel isn't bound")
}
}
Original file line number Diff line number Diff line change
@@ -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<ResponseAccessToken>

@GET("/search/repositories")
fun performSearch(@Query("q") query: String): Single<SearchResponse>

@GET("/user")
fun getUser(): Single<UserEntity>

@GET("/repos/{owner}/{repo}/commits")
fun getCommits(
@Path("owner") owner: String,
@Path("repo") repo: String,
@Query("sha") branch: String = "master"
): Single<List<CommitResponseEntity>>
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<List<RemoteRepository>>
fun getUser(): Single<User>
fun getCommits(owner: String, repo: String): Single<List<Commit>>
}
Loading