├── .gitignore
├── .idea
├── codeStyles
│ └── Project.xml
├── gradle.xml
└── runConfigurations.xml
├── README.md
├── app
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── omjoonkim
│ │ └── app
│ │ └── githubBrowserApp
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── omjoonkim
│ │ │ └── app
│ │ │ └── githubBrowserApp
│ │ │ ├── App.kt
│ │ │ ├── AppSchedulerProvider.kt
│ │ │ ├── BindningAdapters.kt
│ │ │ ├── Extensions.kt
│ │ │ ├── Logger.kt
│ │ │ ├── app_module.kt
│ │ │ ├── error
│ │ │ └── Error.kt
│ │ │ ├── rx
│ │ │ └── RxExtension.kt
│ │ │ ├── ui
│ │ │ ├── BaseActivity.kt
│ │ │ ├── BasePresenter.kt
│ │ │ ├── BaseView.kt
│ │ │ ├── main
│ │ │ │ └── MainActivity.kt
│ │ │ ├── repo
│ │ │ │ ├── RepoDetailActivity.kt
│ │ │ │ ├── RepoDetailPresenter.kt
│ │ │ │ └── RepoDetailView.kt
│ │ │ └── search
│ │ │ │ └── SearchActivity.kt
│ │ │ └── viewmodel
│ │ │ ├── MainViewModel.kt
│ │ │ ├── SearchViewModel.kt
│ │ │ └── ViewModel.kt
│ └── res
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── activity_repo.xml
│ │ ├── activity_search.xml
│ │ ├── viewholder_repo_fork.xml
│ │ ├── viewholder_user_info.xml
│ │ └── viewholder_user_repo.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── omjoonkim
│ └── app
│ └── githubBrowserApp
│ ├── MainViewModelSpec.kt
│ ├── SearchViewModelSpec.kt
│ └── di
│ ├── KoinSpek.kt
│ ├── TestDummyGithubBrowserService.kt
│ ├── TestSchedulersProvider.kt
│ └── test_modules.kt
├── build.gradle
├── data
├── build.gradle
└── src
│ └── main
│ └── java
│ └── com
│ └── omjoonkim
│ └── project
│ └── githubBrowser
│ └── data
│ ├── interactor
│ ├── ForkDataRepository.kt
│ ├── RepoDataRepository.kt
│ └── UserDataRepository.kt
│ └── source
│ └── GithubBrowserRemote.kt
├── domain
├── build.gradle
└── src
│ └── main
│ └── java
│ └── com
│ └── omjoonkim
│ └── project
│ └── githubBrowser
│ └── domain
│ ├── entity
│ ├── Fork.kt
│ ├── Repo.kt
│ └── User.kt
│ ├── exception
│ ├── NetworkException.kt
│ └── RateLimitException.kt
│ ├── interactor
│ ├── CompletableUseCase.kt
│ ├── SingleUseCase.kt
│ └── usecases
│ │ ├── GetRepoDetail.kt
│ │ └── GetUserData.kt
│ ├── repository
│ ├── ForkRepository.kt
│ ├── RepoRepository.kt
│ └── UserRepository.kt
│ └── schedulers
│ └── SchedulersProvider.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── presentaion
├── build.gradle
└── src
│ └── main
│ └── java
│ └── com
│ └── omjoonkim
│ └── project
│ └── githubBrowserApp
│ └── presentaion
│ └── MyClass.java
├── remote
├── build.gradle
└── src
│ └── main
│ └── java
│ └── com
│ └── omjoonkim
│ └── project
│ └── githubBrowser
│ └── remote
│ ├── GithubBrowserRemoteImpl.kt
│ ├── GithubBrowserService.kt
│ ├── GithubBrowserServiceFactory.kt
│ ├── NetworkExceptionTransforemer.kt
│ ├── mapper
│ ├── EntityMapper.kt
│ ├── ForkEntityMapper.kt
│ ├── RepoEntityMapper.kt
│ └── UserEntityMapper.kt
│ └── model
│ ├── ForkModel.kt
│ ├── RepoModel.kt
│ └── UserModel.kt
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/kotlin,android,androidstudio
3 |
4 | ### Android ###
5 | # Built application files
6 | *.apk
7 | *.ap_
8 |
9 | # Files for the ART/Dalvik VM
10 | *.dex
11 |
12 | # Java class files
13 | *.class
14 |
15 | # Generated files
16 | bin/
17 | gen/
18 | out/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # IntelliJ
40 | *.iml
41 | .idea/workspace.xml
42 | .idea/tasks.xml
43 | .idea/gradle.xml
44 | .idea/assetWizardSettings.xml
45 | .idea/dictionaries
46 | .idea/libraries
47 | .idea/caches
48 |
49 | # Keystore files
50 | # Uncomment the following line if you do not want to check your keystore files in.
51 | #*.jks
52 |
53 | # External native build folder generated in Android Studio 2.2 and later
54 | .externalNativeBuild
55 |
56 | # Google Services (e.g. APIs or Firebase)
57 | google-services.json
58 |
59 | # Freeline
60 | freeline.py
61 | freeline/
62 | freeline_project_description.json
63 |
64 | # fastlane
65 | fastlane/report.xml
66 | fastlane/Preview.html
67 | fastlane/screenshots
68 | fastlane/test_output
69 | fastlane/readme.md
70 |
71 | ### Android Patch ###
72 | gen-external-apklibs
73 |
74 | ### AndroidStudio ###
75 | # Covers files to be ignored for android development using Android Studio.
76 |
77 | # Built application files
78 |
79 | # Files for the ART/Dalvik VM
80 |
81 | # Java class files
82 |
83 | # Generated files
84 |
85 | # Gradle files
86 | .gradle
87 |
88 | # Signing files
89 | .signing/
90 |
91 | # Local configuration file (sdk path, etc)
92 |
93 | # Proguard folder generated by Eclipse
94 |
95 | # Log Files
96 |
97 | # Android Studio
98 | /*/build/
99 | /*/local.properties
100 | /*/out
101 | /*/*/build
102 | /*/*/production
103 | *.ipr
104 | *~
105 | *.swp
106 |
107 | # Android Patch
108 |
109 | # External native build folder generated in Android Studio 2.2 and later
110 |
111 | # NDK
112 | obj/
113 |
114 | # IntelliJ IDEA
115 | *.iws
116 | /out/
117 |
118 | # User-specific configurations
119 | .idea/caches/
120 | .idea/libraries/
121 | .idea/shelf/
122 | .idea/.name
123 | .idea/compiler.xml
124 | .idea/copyright/profiles_settings.xml
125 | .idea/encodings.xml
126 | .idea/misc.xml
127 | .idea/modules.xml
128 | .idea/scopes/scope_settings.xml
129 | .idea/vcs.xml
130 | .idea/jsLibraryMappings.xml
131 | .idea/datasources.xml
132 | .idea/dataSources.ids
133 | .idea/sqlDataSources.xml
134 | .idea/dynamic.xml
135 | .idea/uiDesigner.xml
136 |
137 | # OS-specific files
138 | .DS_Store
139 | .DS_Store?
140 | ._*
141 | .Spotlight-V100
142 | .Trashes
143 | ehthumbs.db
144 | Thumbs.db
145 |
146 | # Legacy Eclipse project files
147 | .classpath
148 | .project
149 | .cproject
150 | .settings/
151 |
152 | # Mobile Tools for Java (J2ME)
153 | .mtj.tmp/
154 |
155 | # Package Files #
156 | *.war
157 | *.ear
158 |
159 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
160 | hs_err_pid*
161 |
162 | ## Plugin-specific files:
163 |
164 | # mpeltonen/sbt-idea plugin
165 | .idea_modules/
166 |
167 | # JIRA plugin
168 | atlassian-ide-plugin.xml
169 |
170 | # Mongo Explorer plugin
171 | .idea/mongoSettings.xml
172 |
173 | # Crashlytics plugin (for Android Studio and IntelliJ)
174 | com_crashlytics_export_strings.xml
175 | crashlytics.properties
176 | crashlytics-build.properties
177 | fabric.properties
178 |
179 | ### AndroidStudio Patch ###
180 |
181 | !/gradle/wrapper/gradle-wrapper.jar
182 |
183 | ### Kotlin ###
184 | # Compiled class file
185 |
186 | # Log file
187 |
188 | # BlueJ files
189 | *.ctxt
190 |
191 | # Mobile Tools for Java (J2ME)
192 |
193 | # Package Files #
194 | *.jar
195 | *.nar
196 | *.zip
197 | *.tar.gz
198 | *.rar
199 |
200 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
201 |
202 |
203 | # End of https://www.gitignore.io/api/kotlin,android,androidstudio
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
20 |
21 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GitHubBrowserApp
2 |
3 | 여러 발표로 사용된 저장소 입니다 :)
4 |
5 |
6 | ## [Android : P AppComponentFactory](https://sites.google.com/view/gdg-io-ex-18-and)
7 |
8 |
9 |
10 | ## [Efficient and Testable MVVM pattern](http://techcon.naver.com/2018/android/)
11 |
12 |
13 |
14 | ## [예제에서는 알려주지 않는 Model 이야기](http://techcon.naver.com/)
15 |
16 | 1. Repository Pattern을 사용하라!
17 | - branch: [model_v1](https://github.com/omjoonkim/GitHubBrowserApp/tree/model_v1), [model_v1_resolve](https://github.com/omjoonkim/GitHubBrowserApp/tree/model_v1_resolve)
18 |
19 | 2. Business Logic을 분리하라!
20 | - branch: [model_v2](https://github.com/omjoonkim/GitHubBrowserApp/tree/model_v2), [model_v2_resolve](https://github.com/omjoonkim/GitHubBrowserApp/tree/mode_v2_resolve)
21 |
22 | 3. Exception handling
23 | - branch: [model_v3](https://github.com/omjoonkim/GitHubBrowserApp/tree/mode_v3)
24 |
25 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'kotlin-kapt'
5 | apply plugin: "de.mannodermaus.android-junit5"
6 | apply plugin: 'jacoco'
7 |
8 | android {
9 | compileSdkVersion 28
10 | defaultConfig {
11 | applicationId 'com.omjoonkim.app.githubBrowserApp'
12 | minSdkVersion 19
13 | targetSdkVersion 28
14 | versionCode 1
15 | versionName "1.0"
16 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
17 | testInstrumentationRunnerArgument "runnerBuilder", "de.mannodermaus.junit5.AndroidJUnit5Builder"
18 | }
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | dataBinding {
26 | enabled = true
27 | }
28 |
29 | sourceSets.each {
30 | it.java.srcDirs += "src/$it.name/kotlin"
31 | }
32 |
33 | testOptions {
34 | unitTests.returnDefaultValues = true
35 | junitPlatform {
36 | filters {
37 | engines {
38 | include 'spek2'
39 | }
40 | }
41 | jacocoOptions {
42 | html.enabled = true
43 | xml.enabled = false
44 | csv.enabled = false
45 | unitTests.all {
46 | testLogging.events = ["passed", "skipped", "failed"]
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
53 | jacoco {
54 | reportsDir = file("${buildDir}/reports")
55 | }
56 |
57 | dependencies {
58 | implementation fileTree(include: ['*.jar'], dir: 'libs')
59 |
60 | implementation project(':remote')
61 | implementation project(':domain')
62 |
63 | androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
64 | exclude group: 'com.android.support', module: 'support-annotations'
65 | })
66 | implementation 'androidx.appcompat:appcompat:1.0.2'
67 | implementation 'com.google.android.material:material:1.0.0'
68 | implementation 'androidx.recyclerview:recyclerview:1.0.0'
69 | implementation 'androidx.cardview:cardview:1.0.0'
70 | implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
71 | implementation "androidx.constraintlayout:constraintlayout:1.1.3"
72 | implementation 'androidx.lifecycle:lifecycle-reactivestreams:2.0.0'
73 | testImplementation 'androidx.arch.core:core-testing:2.0.1'
74 |
75 | //koin
76 | implementation "org.koin:koin-android:$koin_version"
77 | implementation "org.koin:koin-android-scope:$koin_version"
78 | implementation "org.koin:koin-androidx-viewmodel:$koin_version"
79 | testImplementation "org.koin:koin-test:$koin_version"
80 |
81 | //glide
82 | implementation 'com.github.bumptech.glide:glide:3.7.0'
83 | implementation 'io.reactivex.rxjava2:rxjava:2.2.5'
84 | implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
85 | implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
86 |
87 | // assertion
88 | testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
89 |
90 | // spek
91 | testImplementation "org.spekframework.spek2:spek-dsl-jvm:$spek_version"
92 | testImplementation "org.spekframework.spek2:spek-runner-junit5:$spek_version"
93 |
94 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
95 | androidTestRuntimeOnly "de.mannodermaus.junit5:android-instrumentation-test-runner:$junit5_runner"
96 | }
97 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/Omjoon/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/omjoonkim/app/githubBrowserApp/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp
2 |
3 | import androidx.test.InstrumentationRegistry
4 | import org.junit.Assert
5 | import org.junit.Test
6 |
7 | class ExampleInstrumentedTest {
8 | @Test
9 | fun useAppContext() {
10 | // Context of the app under test.
11 | val appContext = InstrumentationRegistry.getTargetContext()
12 | Assert.assertEquals("com.omjoonkim.app.githubBrowserApp", appContext.packageName)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
34 |
35 |
36 |
37 |
38 |
41 |
42 |
43 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/App.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp
2 |
3 | import android.app.Application
4 | import org.koin.android.ext.koin.androidContext
5 | import org.koin.core.context.startKoin
6 |
7 | class App : Application() {
8 |
9 | override fun onCreate() {
10 | super.onCreate()
11 | startKoin {
12 | androidContext(this@App)
13 | modules(myModule)
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/AppSchedulerProvider.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp
2 |
3 | import com.omjoonkim.project.githubBrowser.domain.schedulers.SchedulersProvider
4 | import io.reactivex.Scheduler
5 | import io.reactivex.android.schedulers.AndroidSchedulers
6 | import io.reactivex.schedulers.Schedulers
7 |
8 | class AppSchedulerProvider : SchedulersProvider {
9 |
10 | override fun io(): Scheduler = Schedulers.io()
11 | override fun ui(): Scheduler = AndroidSchedulers.mainThread()
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/BindningAdapters.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp
2 |
3 | import android.widget.ImageView
4 | import androidx.appcompat.widget.Toolbar
5 | import androidx.databinding.BindingAdapter
6 | import com.omjoonkim.app.githubBrowserApp.generated.callback.OnClickListener
7 |
8 |
9 | @BindingAdapter("android:url")
10 | fun ImageView.setImageByGlide(url: String) {
11 | setImageWithGlide(url)
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/Extensions.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp
2 |
3 | import android.content.Context
4 | import android.widget.ImageView
5 | import android.widget.Toast
6 | import androidx.lifecycle.LifecycleOwner
7 | import androidx.lifecycle.LiveData
8 | import androidx.lifecycle.MutableLiveData
9 | import com.bumptech.glide.Glide
10 | import com.bumptech.glide.load.engine.DiskCacheStrategy
11 |
12 | fun Context.showToast(text: String) = Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
13 |
14 | fun Context.showToast(resId: Int) = Toast.makeText(this, resId, Toast.LENGTH_SHORT).show()
15 |
16 | fun ImageView.setImageWithGlide( url: String?) =
17 | url?.let {
18 | try {
19 | Glide.with(context).load(url)
20 | .diskCacheStrategy(DiskCacheStrategy.ALL)
21 | .centerCrop()
22 | .error(R.mipmap.ic_launcher_round)
23 | .into(this)
24 | } catch (ignore: Exception) {
25 | }
26 | } ?: setImageResource(0)
27 |
28 |
29 | fun MutableLiveData.call(ignore: T) {
30 | value = kotlin.Unit
31 | }
32 |
33 | fun MutableLiveData.call() {
34 | value = kotlin.Unit
35 | }
36 |
37 | inline fun LiveData.observe(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) {
38 | this.observe(owner, androidx.lifecycle.Observer { observer(it) })
39 | }
40 |
41 | inline fun LiveData.observeNotNull(owner: LifecycleOwner, crossinline observer: (T) -> Unit) {
42 | this.observe(owner, androidx.lifecycle.Observer { observer(it!!) })
43 | }
44 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/Logger.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp
2 |
3 | import android.util.Log
4 |
5 | class Logger {
6 |
7 | fun d(t: Throwable) {
8 | t.printStackTrace()
9 | Log.e("e",t.toString())
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/app_module.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp
2 |
3 | import com.omjoonkim.app.githubBrowserApp.ui.repo.RepoDetailPresenter
4 | import com.omjoonkim.app.githubBrowserApp.ui.repo.RepoDetailView
5 | import com.omjoonkim.app.githubBrowserApp.viewmodel.MainViewModel
6 | import com.omjoonkim.app.githubBrowserApp.viewmodel.SearchViewModel
7 | import com.omjoonkim.project.githubBrowser.data.interactor.ForkDataRepository
8 | import com.omjoonkim.project.githubBrowser.data.interactor.RepoDataRepository
9 | import com.omjoonkim.project.githubBrowser.data.interactor.UserDataRepository
10 | import com.omjoonkim.project.githubBrowser.data.source.GithubBrowserRemote
11 | import com.omjoonkim.project.githubBrowser.domain.interactor.usecases.GetRepoDetail
12 | import com.omjoonkim.project.githubBrowser.domain.interactor.usecases.GetUserData
13 | import com.omjoonkim.project.githubBrowser.domain.repository.ForkRepository
14 | import com.omjoonkim.project.githubBrowser.domain.repository.RepoRepository
15 | import com.omjoonkim.project.githubBrowser.domain.repository.UserRepository
16 | import com.omjoonkim.project.githubBrowser.domain.schedulers.SchedulersProvider
17 | import com.omjoonkim.project.githubBrowser.remote.GithubBrowserRemoteImpl
18 | import com.omjoonkim.project.githubBrowser.remote.GithubBrowserServiceFactory
19 | import com.omjoonkim.project.githubBrowser.remote.mapper.ForkEntityMapper
20 | import com.omjoonkim.project.githubBrowser.remote.mapper.RepoEntityMapper
21 | import com.omjoonkim.project.githubBrowser.remote.mapper.UserEntityMapper
22 | import org.koin.androidx.viewmodel.dsl.viewModel
23 | import org.koin.core.module.Module
24 | import org.koin.dsl.module
25 |
26 | val myModule: Module = module {
27 | //presentation
28 | viewModel { (id: String) -> MainViewModel(id, get(), get()) }
29 | viewModel { SearchViewModel(get()) }
30 | factory { (view: RepoDetailView) -> RepoDetailPresenter(view, get()) }
31 |
32 | //app
33 | single { Logger() }
34 | single { AppSchedulerProvider() as SchedulersProvider }
35 |
36 | //domain
37 | factory { GetUserData(get(), get(), get()) }
38 | factory { GetRepoDetail(get(), get(), get()) }
39 |
40 | //data
41 | single { UserDataRepository(get()) as UserRepository }
42 | single { RepoDataRepository(get()) as RepoRepository }
43 | single { ForkDataRepository(get()) as ForkRepository }
44 |
45 | //remote
46 | single { GithubBrowserRemoteImpl(get(), get(), get(), get()) as GithubBrowserRemote }
47 | single { RepoEntityMapper() }
48 | single { UserEntityMapper() }
49 | single { ForkEntityMapper(get()) }
50 | single {
51 | GithubBrowserServiceFactory.makeGithubBrowserService(
52 | BuildConfig.DEBUG,
53 | "https://api.github.com"
54 | )
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/error/Error.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.error
2 |
3 | sealed class Error : Throwable() {
4 | abstract val errorText: String
5 | }
6 |
7 | object UnExpected : Error() {
8 | override val errorText: String = "알 수 없는 에러."
9 | }
10 |
11 | object NotFoundUser : Error() {
12 | override val errorText: String = "해당 사용자를 찾을 수 없습니다."
13 | }
14 |
15 | object NotConnectedNetwork : Error() {
16 | override val errorText: String = "인터넷이 연결되어 있지 않습니다."
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/rx/RxExtension.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.rx
2 |
3 | import androidx.lifecycle.LifecycleOwner
4 | import androidx.lifecycle.LiveData
5 | import io.reactivex.*
6 | import io.reactivex.functions.BiFunction
7 | import io.reactivex.subjects.Subject
8 |
9 | enum class Parameter {
10 | CLICK, EMPTY, SUCCESS, EVENT
11 | }
12 |
13 | fun Observable.handleToError(action: Subject? = null): Observable = doOnError { action?.onNext(it) }
14 | fun Observable.neverError(): Observable = onErrorResumeNext { _: Throwable -> Observable.empty() }
15 | fun Observable.neverError(action: Subject? = null): Observable = handleToError(action).neverError()
16 |
17 | fun Single.handleToError(action: Subject?): Single = doOnError { action?.onNext(it) }
18 | fun Single.neverError(): Maybe = toMaybe().neverError()
19 | fun Single.neverError(action: Subject? = null): Maybe = handleToError(action).neverError()
20 |
21 | fun Maybe.handleToError(action: Subject? = null): Maybe = doOnError { action?.onNext(it) }
22 | fun Maybe.neverError(): Maybe = onErrorResumeNext(onErrorComplete())
23 | fun Maybe.neverError(action: Subject? = null): Maybe = handleToError(action).neverError()
24 |
25 | fun Completable.handleToError(action: Subject? = null): Completable = doOnError { action?.onNext(it) }
26 | fun Completable.neverError(): Completable = onErrorResumeNext { it.printStackTrace();Completable.never() }
27 | fun Completable.neverError(action: Subject? = null): Completable = handleToError(action).neverError()
28 |
29 | fun Flowable.handleToError(action: Subject? = null): Flowable = doOnError { action?.onNext(it) }
30 | fun Flowable.neverError(): Flowable = onErrorResumeNext { _: Throwable -> Flowable.empty() }
31 | fun Flowable.neverError(action: Subject? = null): Flowable = handleToError(action).neverError()
32 |
33 | fun LiveData.observe(lifecycleOwner: LifecycleOwner, observer: (T) -> Unit) = observe({ lifecycleOwner.lifecycle }, observer)
34 |
35 | fun Observable.takeWhen(observable: Observable, biFunction: (T2, T1) -> R): Observable = compose {
36 | observable.withLatestFrom(it, BiFunction { t1, t2 -> biFunction.invoke(t1, t2) })
37 | }
38 |
39 |
40 | fun printStackTrace(t : Throwable) = t.printStackTrace()
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/ui/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.ui
2 |
3 | import android.graphics.Color
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.appcompat.widget.Toolbar
6 | import androidx.lifecycle.LiveData
7 | import com.omjoonkim.app.githubBrowserApp.R
8 | import com.omjoonkim.app.githubBrowserApp.observeNotNull
9 |
10 | abstract class BaseActivity : AppCompatActivity() {
11 |
12 | protected fun actionbarInit(toolbar: Toolbar, titleColor: Int = Color.WHITE, isEnableNavi: Boolean = true, onClickHomeButton: () -> Unit = {}) {
13 | toolbar.setTitleTextColor(titleColor)
14 | setSupportActionBar(toolbar)
15 | if (isEnableNavi) {
16 | supportActionBar?.setDisplayHomeAsUpEnabled(true)
17 | toolbar.setNavigationOnClickListener {
18 | onClickHomeButton.invoke()
19 | }
20 | }
21 | }
22 |
23 | protected fun LiveData.observe(observer: (T) -> Unit) = observeNotNull(this@BaseActivity, observer)
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/ui/BasePresenter.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.ui
2 |
3 | import io.reactivex.disposables.CompositeDisposable
4 |
5 | abstract class BasePresenter(protected val view: T) {
6 |
7 | protected val compositeDisposable = CompositeDisposable()
8 |
9 | fun onDestroy() {
10 | compositeDisposable.dispose()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/ui/BaseView.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.ui
2 |
3 | interface BaseView
4 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/ui/main/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.ui.main
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import android.view.ViewGroup
7 | import androidx.databinding.DataBindingUtil
8 | import androidx.recyclerview.widget.RecyclerView
9 | import com.omjoonkim.app.githubBrowserApp.databinding.ActivityMainBinding
10 | import com.omjoonkim.app.githubBrowserApp.databinding.ViewholderUserInfoBinding
11 | import com.omjoonkim.app.githubBrowserApp.databinding.ViewholderUserRepoBinding
12 | import com.omjoonkim.app.githubBrowserApp.showToast
13 | import com.omjoonkim.app.githubBrowserApp.ui.BaseActivity
14 | import com.omjoonkim.app.githubBrowserApp.viewmodel.MainViewModel
15 | import com.omjoonkim.project.githubBrowser.domain.entity.Repo
16 | import com.omjoonkim.app.githubBrowserApp.R
17 | import com.omjoonkim.app.githubBrowserApp.ui.repo.RepoDetailActivity
18 | import com.omjoonkim.project.githubBrowser.domain.entity.User
19 | import org.koin.androidx.viewmodel.ext.android.getViewModel
20 | import org.koin.core.parameter.parametersOf
21 |
22 | class MainActivity : BaseActivity() {
23 |
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | super.onCreate(savedInstanceState)
26 | val binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
27 | binding.setLifecycleOwner(this)
28 |
29 | val viewModel = getViewModel {
30 | parametersOf(intent.data.path.substring(1))
31 | }
32 | binding.viewModel = viewModel
33 |
34 | actionbarInit(binding.toolbar, onClickHomeButton = {
35 | viewModel.input.clickHomeButton()
36 | })
37 |
38 | with(viewModel.output) {
39 | refreshListData().observe { (user, repos) ->
40 | binding.recyclerView.adapter = MainListAdapter(
41 | user,
42 | repos,
43 | viewModel.input::clickUser,
44 | viewModel.input::clickRepo
45 | )
46 | }
47 | showErrorToast().observe { showToast(it) }
48 | goProfileActivity().observe {
49 | startActivity(
50 | Intent(
51 | Intent.ACTION_VIEW,
52 | Uri.parse("githubbrowser://repos/$it")
53 | )
54 | )
55 | }
56 | goRepoDetailActivity().observe {
57 | RepoDetailActivity.start(this@MainActivity, it.first, it.second)
58 | }
59 | finish().observe {
60 | onBackPressed()
61 | }
62 | }
63 | }
64 |
65 | private inner class MainListAdapter(
66 | val user: User,
67 | val repos: List,
68 | val onClickUser: (User) -> Unit,
69 | val onClickRepo: (User, Repo) -> Unit
70 | ) : RecyclerView.Adapter() {
71 |
72 | private val VIEWTYPE_USER_INFO = 0
73 | private val VIEWTYPE_USER_REPO = 1
74 |
75 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
76 | when (viewType) {
77 | 0 -> UserInfoViewHolder(ViewholderUserInfoBinding.inflate(layoutInflater)).apply {
78 | itemView.setOnClickListener {
79 | onClickUser.invoke(user)
80 | }
81 | }
82 | 1 -> UserRepoViewHolder(ViewholderUserRepoBinding.inflate(layoutInflater)).apply {
83 | itemView.setOnClickListener {
84 | onClickRepo.invoke(user, repos[adapterPosition - 1])
85 | }
86 | }
87 | else -> throw IllegalStateException()
88 | }
89 |
90 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
91 | when (holder) {
92 | is UserRepoViewHolder -> holder.bind(repos[position - 1])
93 | is UserInfoViewHolder -> holder.bind(user)
94 | }
95 | }
96 |
97 | override fun getItemViewType(position: Int): Int =
98 | if (position == 0)
99 | VIEWTYPE_USER_INFO
100 | else
101 | VIEWTYPE_USER_REPO
102 |
103 | override fun getItemCount(): Int = repos.size + 1
104 |
105 | private inner class UserInfoViewHolder(
106 | private val binding: ViewholderUserInfoBinding
107 | ) : RecyclerView.ViewHolder(binding.root) {
108 | fun bind(item: User) {
109 | binding.data = item
110 | }
111 | }
112 |
113 | private inner class UserRepoViewHolder(
114 | private val binding: ViewholderUserRepoBinding
115 | ) : RecyclerView.ViewHolder(binding.root) {
116 |
117 | fun bind(item: Repo) {
118 | binding.data = item
119 | }
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/ui/repo/RepoDetailActivity.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.ui.repo
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import androidx.recyclerview.widget.LinearLayoutManager
10 | import androidx.recyclerview.widget.RecyclerView
11 | import com.omjoonkim.app.githubBrowserApp.R
12 | import com.omjoonkim.app.githubBrowserApp.databinding.ViewholderRepoForkBinding
13 | import com.omjoonkim.app.githubBrowserApp.databinding.ViewholderUserInfoBinding
14 | import com.omjoonkim.app.githubBrowserApp.showToast
15 | import com.omjoonkim.app.githubBrowserApp.ui.BaseActivity
16 | import com.omjoonkim.project.githubBrowser.domain.entity.Fork
17 | import com.omjoonkim.project.githubBrowser.domain.entity.User
18 | import com.omjoonkim.project.githubBrowser.remote.model.ForkModel
19 | import kotlinx.android.synthetic.main.activity_repo.*
20 | import org.koin.android.ext.android.inject
21 | import org.koin.core.parameter.parametersOf
22 |
23 | class RepoDetailActivity : BaseActivity(), RepoDetailView {
24 |
25 | companion object {
26 | val KEY_USER_NAME = "userNameKey"
27 | val KEY_REPO_NAME = "repoNameKey"
28 |
29 | fun start(context: Context, userName: String, repoName: String) {
30 | context.startActivity(
31 | Intent(context, RepoDetailActivity::class.java)
32 | .putExtra(KEY_USER_NAME, userName)
33 | .putExtra(KEY_REPO_NAME, repoName)
34 | )
35 | }
36 | }
37 |
38 | private val presenter: RepoDetailPresenter by inject { parametersOf(this) }
39 |
40 | override fun onCreate(savedInstanceState: Bundle?) {
41 | super.onCreate(savedInstanceState)
42 | setContentView(R.layout.activity_repo)
43 | actionbarInit(toolbar, onClickHomeButton = {
44 | onBackPressed()
45 | })
46 | initRecyclerView()
47 | presenter.onCreate(
48 | intent.getStringExtra(KEY_USER_NAME),
49 | intent.getStringExtra(KEY_REPO_NAME)
50 | )
51 | }
52 |
53 | private fun initRecyclerView() {
54 | recyclerView.layoutManager = LinearLayoutManager(this)
55 | recyclerView.adapter = Adapter()
56 | }
57 |
58 | override fun setToolbarTitle(title: String) {
59 | supportActionBar?.title = title
60 | }
61 |
62 | override fun setName(name: String) {
63 | this.name.text = name
64 | }
65 |
66 | override fun setDescription(description: String) {
67 | this.description.text = description
68 | }
69 |
70 | override fun setStarCount(count: String) {
71 | this.starCount.text = count
72 | }
73 |
74 | override fun refreshForks(data: List) {
75 | (recyclerView.adapter as? Adapter)?.refresh(data)
76 | }
77 |
78 | override fun toastRateLimitError() {
79 | showToast("please check your rate limit")
80 | }
81 |
82 | override fun toastNetworkError() {
83 | showToast("please check your network")
84 | }
85 |
86 | override fun toastUnexpectedError() {
87 | showToast("Unexpected Error...... :(")
88 | }
89 |
90 | private inner class Adapter : RecyclerView.Adapter() {
91 |
92 | private val data = mutableListOf()
93 |
94 | fun refresh(data: List) {
95 | this.data.clear()
96 | this.data.addAll(data)
97 | notifyDataSetChanged()
98 | }
99 |
100 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
101 | ViewHolder(ViewholderRepoForkBinding.inflate(LayoutInflater.from(parent.context)))
102 |
103 | override fun getItemCount(): Int = data.size
104 |
105 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
106 | holder.bind(data[position])
107 | }
108 |
109 | private inner class ViewHolder(
110 | private val binding: ViewholderRepoForkBinding
111 | ) : RecyclerView.ViewHolder(binding.root) {
112 | fun bind(item: Fork) {
113 | binding.data = item
114 | }
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/ui/repo/RepoDetailPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.ui.repo
2 |
3 | import com.omjoonkim.app.githubBrowserApp.ui.BasePresenter
4 | import com.omjoonkim.project.githubBrowser.domain.exception.NetworkException
5 | import com.omjoonkim.project.githubBrowser.domain.exception.RateLimitException
6 | import com.omjoonkim.project.githubBrowser.domain.interactor.usecases.GetRepoDetail
7 |
8 | class RepoDetailPresenter(
9 | view: RepoDetailView,
10 | private val getRepoDetail: GetRepoDetail
11 | ) : BasePresenter(view) {
12 |
13 | fun onCreate(userName: String, repoName: String) {
14 | view.setToolbarTitle(userName)
15 | compositeDisposable.add(
16 | getRepoDetail.get(
17 | userName to repoName
18 | ).subscribe({ (repo, forks) ->
19 | view.setName(repo.name)
20 | view.setDescription(repo.description ?: "")
21 | view.setStarCount(repo.starCount)
22 | view.refreshForks(forks)
23 | }, ::handleException))
24 | }
25 |
26 | private fun handleException(throwable: Throwable) {
27 | when(throwable){
28 | is RateLimitException -> view.toastRateLimitError()
29 | is NetworkException -> view.toastNetworkError()
30 | else -> view.toastUnexpectedError()
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/ui/repo/RepoDetailView.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.ui.repo
2 |
3 | import com.omjoonkim.app.githubBrowserApp.ui.BaseView
4 | import com.omjoonkim.project.githubBrowser.domain.entity.Fork
5 |
6 | interface RepoDetailView : BaseView {
7 | fun setName(name: String)
8 | fun setDescription(description: String)
9 | fun setStarCount(count: String)
10 | fun refreshForks(data: List)
11 | fun setToolbarTitle(title: String)
12 | fun toastRateLimitError()
13 | fun toastNetworkError()
14 | fun toastUnexpectedError()
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/ui/search/SearchActivity.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.ui.search
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Bundle
7 | import android.view.inputmethod.InputMethodManager
8 | import androidx.databinding.DataBindingUtil
9 | import com.omjoonkim.app.githubBrowserApp.R
10 | import com.omjoonkim.app.githubBrowserApp.databinding.ActivitySearchBinding
11 | import com.omjoonkim.app.githubBrowserApp.ui.BaseActivity
12 | import com.omjoonkim.app.githubBrowserApp.viewmodel.SearchViewModel
13 | import org.koin.androidx.viewmodel.ext.android.getViewModel
14 |
15 | class SearchActivity : BaseActivity() {
16 |
17 | private val keyboardController by lazy { getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager }
18 |
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | super.onCreate(savedInstanceState)
21 | val binding = DataBindingUtil.setContentView(this, R.layout.activity_search)
22 | binding.setLifecycleOwner(this)
23 | actionbarInit(binding.toolbar, isEnableNavi = false)
24 |
25 | val viewModel = getViewModel()
26 | binding.viewModel = viewModel
27 |
28 | viewModel.output.goResultActivity()
29 | .observe {
30 | keyboardController.hideSoftInputFromWindow(binding.editText.windowToken, 0)
31 | startActivity(
32 | Intent(
33 | Intent.ACTION_VIEW,
34 | Uri.parse("githubbrowser://repos/$it")
35 | )
36 | )
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/viewmodel/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.viewmodel
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import com.omjoonkim.app.githubBrowserApp.Logger
6 | import com.omjoonkim.app.githubBrowserApp.call
7 | import com.omjoonkim.app.githubBrowserApp.error.Error
8 | import com.omjoonkim.app.githubBrowserApp.error.UnExpected
9 | import com.omjoonkim.app.githubBrowserApp.rx.Parameter
10 | import com.omjoonkim.app.githubBrowserApp.rx.neverError
11 | import com.omjoonkim.project.githubBrowser.domain.entity.Repo
12 | import com.omjoonkim.project.githubBrowser.domain.entity.User
13 | import com.omjoonkim.project.githubBrowser.domain.interactor.usecases.GetUserData
14 | import io.reactivex.Observable
15 | import io.reactivex.rxkotlin.Observables
16 | import io.reactivex.subjects.PublishSubject
17 |
18 |
19 | class MainViewModel(
20 | searchedUserName: String,
21 | private val getUserData: GetUserData,
22 | logger: Logger
23 | ) : BaseViewModel() {
24 |
25 | private val clickUser = PublishSubject.create()
26 | private val clickRepo = PublishSubject.create>()
27 | private val clickHomeButton = PublishSubject.create()
28 | val input: MainViewModelInputs = object : MainViewModelInputs {
29 | override fun clickUser(user: User) = clickUser.onNext(user)
30 | override fun clickRepo(user: User, repo: Repo) = clickRepo.onNext(user to repo)
31 | override fun clickHomeButton() = clickHomeButton.onNext(Parameter.CLICK)
32 | }
33 |
34 | private val state = MutableLiveData()
35 | private val refreshListData = MutableLiveData>>()
36 | private val showErrorToast = MutableLiveData()
37 | private val goProfileActivity = MutableLiveData()
38 | private val goRepoDetailActivity = MutableLiveData>()
39 | private val finish = MutableLiveData()
40 | val output = object : MainViewModelOutPuts {
41 | override fun state() = state
42 | override fun refreshListData() = refreshListData
43 | override fun showErrorToast() = showErrorToast
44 | override fun goProfileActivity() = goProfileActivity
45 | override fun goRepoDetailActivity() = goRepoDetailActivity
46 | override fun finish() = finish
47 | }
48 |
49 | init {
50 | val error = PublishSubject.create()
51 | val userName = Observable.just(searchedUserName).share()
52 | val requestListData = userName.flatMapMaybe {
53 | getUserData.get(it).neverError(error)
54 | }.share()
55 | compositeDisposable.addAll(
56 | Observables
57 | .combineLatest(
58 | Observable.merge(
59 | requestListData.map { false },
60 | error.map { false }
61 | ).startWith(true),
62 | userName,
63 | ::MainViewState
64 | ).subscribe(state::setValue, logger::d),
65 | requestListData.subscribe(refreshListData::setValue, logger::d),
66 | error.map {
67 | if (it is Error)
68 | it.errorText
69 | else UnExpected.errorText
70 | }.subscribe(showErrorToast::setValue, logger::d),
71 | clickUser.map { it.name }.subscribe(goProfileActivity::setValue, logger::d),
72 | clickRepo.map { it.first.name to it.second.name }.subscribe(goRepoDetailActivity::setValue, logger::d),
73 | clickHomeButton.subscribe(finish::call, logger::d)
74 | )
75 | }
76 | }
77 |
78 | interface MainViewModelInputs : Input {
79 | fun clickUser(user: User)
80 | fun clickRepo(user: User, repo: Repo)
81 | fun clickHomeButton()
82 | }
83 |
84 | interface MainViewModelOutPuts : Output {
85 | fun state(): LiveData
86 | fun refreshListData(): LiveData>>
87 | fun showErrorToast(): LiveData
88 | fun goProfileActivity(): LiveData
89 | fun goRepoDetailActivity(): LiveData>
90 | fun finish(): LiveData
91 | }
92 |
93 | data class MainViewState(
94 | val showLoading: Boolean,
95 | val title: String
96 | )
97 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/viewmodel/SearchViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.viewmodel
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import com.omjoonkim.app.githubBrowserApp.Logger
6 | import com.omjoonkim.app.githubBrowserApp.rx.Parameter
7 | import com.omjoonkim.app.githubBrowserApp.rx.takeWhen
8 | import io.reactivex.subjects.PublishSubject
9 |
10 | class SearchViewModel(
11 | logger: Logger
12 | ) : BaseViewModel() {
13 |
14 | private val name = PublishSubject.create()
15 | private val clickSearchButton = PublishSubject.create()
16 | val input = object : SearchViewModelInPuts {
17 | override fun name(name: String) =
18 | this@SearchViewModel.name.onNext(name)
19 | override fun clickSearchButton() =
20 | this@SearchViewModel.clickSearchButton.onNext(Parameter.CLICK)
21 | }
22 |
23 | private val state = MutableLiveData()
24 | private val goResultActivity = MutableLiveData()
25 | val output = object : SearchViewModelOutPuts {
26 | override fun state() = state
27 | override fun goResultActivity() = goResultActivity
28 | }
29 |
30 | init {
31 | compositeDisposable.addAll(
32 | name.map { SearchViewState(it.isNotEmpty()) }
33 | .subscribe(state::setValue, logger::d),
34 | name.takeWhen(clickSearchButton) { _, t2 -> t2 }
35 | .subscribe(goResultActivity::setValue, logger::d)
36 | )
37 | }
38 | }
39 |
40 | interface SearchViewModelInPuts : Input {
41 | fun name(name: String)
42 | fun clickSearchButton()
43 | }
44 |
45 | interface SearchViewModelOutPuts : Output {
46 | fun state(): LiveData
47 | fun goResultActivity(): LiveData
48 | }
49 |
50 | data class SearchViewState(
51 | val enableSearchButton: Boolean
52 | )
53 |
--------------------------------------------------------------------------------
/app/src/main/java/com/omjoonkim/app/githubBrowserApp/viewmodel/ViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import io.reactivex.disposables.CompositeDisposable
5 |
6 | interface Input
7 | interface Output
8 |
9 | abstract class BaseViewModel : ViewModel(){
10 | protected val compositeDisposable : CompositeDisposable = CompositeDisposable()
11 |
12 | override fun onCleared() {
13 | super.onCleared()
14 | compositeDisposable.clear()
15 | }
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
13 |
18 |
23 |
28 |
33 |
38 |
43 |
48 |
53 |
58 |
63 |
68 |
73 |
78 |
83 |
88 |
93 |
98 |
103 |
108 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
16 |
20 |
21 |
29 |
30 |
36 |
37 |
38 |
51 |
52 |
61 |
62 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_repo.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 |
16 |
21 |
22 |
23 |
28 |
29 |
34 |
35 |
45 |
46 |
57 |
58 |
69 |
70 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_search.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
22 |
23 |
27 |
28 |
34 |
35 |
36 |
42 |
43 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/viewholder_repo_fork.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
13 |
14 |
15 |
24 |
25 |
35 |
36 |
46 |
47 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/viewholder_user_info.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
11 |
12 |
21 |
22 |
28 |
29 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/viewholder_user_repo.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
11 |
12 |
13 |
18 |
19 |
24 |
25 |
33 |
34 |
44 |
45 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omjoonkim/GitHubBrowserApp/8904f6b9fecc06c95cda2c2d12a0ff94e47af842/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omjoonkim/GitHubBrowserApp/8904f6b9fecc06c95cda2c2d12a0ff94e47af842/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omjoonkim/GitHubBrowserApp/8904f6b9fecc06c95cda2c2d12a0ff94e47af842/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omjoonkim/GitHubBrowserApp/8904f6b9fecc06c95cda2c2d12a0ff94e47af842/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omjoonkim/GitHubBrowserApp/8904f6b9fecc06c95cda2c2d12a0ff94e47af842/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omjoonkim/GitHubBrowserApp/8904f6b9fecc06c95cda2c2d12a0ff94e47af842/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omjoonkim/GitHubBrowserApp/8904f6b9fecc06c95cda2c2d12a0ff94e47af842/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omjoonkim/GitHubBrowserApp/8904f6b9fecc06c95cda2c2d12a0ff94e47af842/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omjoonkim/GitHubBrowserApp/8904f6b9fecc06c95cda2c2d12a0ff94e47af842/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omjoonkim/GitHubBrowserApp/8904f6b9fecc06c95cda2c2d12a0ff94e47af842/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omjoonkim/GitHubBrowserApp/8904f6b9fecc06c95cda2c2d12a0ff94e47af842/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omjoonkim/GitHubBrowserApp/8904f6b9fecc06c95cda2c2d12a0ff94e47af842/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omjoonkim/GitHubBrowserApp/8904f6b9fecc06c95cda2c2d12a0ff94e47af842/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omjoonkim/GitHubBrowserApp/8904f6b9fecc06c95cda2c2d12a0ff94e47af842/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omjoonkim/GitHubBrowserApp/8904f6b9fecc06c95cda2c2d12a0ff94e47af842/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Mission
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/test/java/com/omjoonkim/app/githubBrowserApp/MainViewModelSpec.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp
2 |
3 | import androidx.arch.core.executor.ArchTaskExecutor
4 | import androidx.arch.core.executor.TaskExecutor
5 | import com.omjoonkim.app.githubBrowserApp.di.KoinSpek
6 | import com.omjoonkim.app.githubBrowserApp.di.test_module
7 | import com.omjoonkim.app.githubBrowserApp.viewmodel.MainViewModel
8 | import com.omjoonkim.project.githubBrowser.domain.interactor.usecases.GetUserData
9 | import org.koin.core.context.startKoin
10 | import org.koin.core.context.stopKoin
11 | import org.koin.core.parameter.parametersOf
12 | import org.koin.test.inject
13 | import org.spekframework.spek2.style.gherkin.Feature
14 | import kotlin.test.assertEquals
15 |
16 | object MainViewModelSpec : KoinSpek({
17 |
18 | beforeEachTest {
19 | startKoin {
20 | modules(test_module)
21 | }
22 | ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
23 | override fun executeOnDiskIO(runnable: Runnable) {
24 | runnable.run()
25 | }
26 |
27 | override fun isMainThread(): Boolean {
28 | return true
29 | }
30 |
31 | override fun postToMainThread(runnable: Runnable) {
32 | runnable.run()
33 | }
34 | })
35 | }
36 | afterEachTest {
37 | ArchTaskExecutor.getInstance().setDelegate(null)
38 | stopKoin()
39 | }
40 |
41 | lateinit var userName: String
42 | val viewModel: MainViewModel by inject { parametersOf(userName) }
43 | val getUserData: GetUserData by inject()
44 |
45 | Feature("MainViewModel spec") {
46 | Scenario("유저가 화면에 들어오면 검색한 유저의 프로필,저장소 데이터가 정상적으로 보여야 한다") {
47 | Given("검색하려는 유저의 이름은 omjoonkim이다") {
48 | userName = "omjoonkim"
49 | }
50 | Then("화면에 검색한 유저의 데이터가 정상적으로 나타난다") {
51 | assertEquals(
52 | getUserData.get(userName).blockingGet(),
53 | viewModel.output.refreshListData().value
54 | )
55 | }
56 | }
57 | Scenario("유저 프로필을 클릭하면 유저의 프로필 화면으로 이동되어야 한다") {
58 | When("프로필을 클릭 했을 때") {
59 | viewModel.input.clickUser(
60 | viewModel.output.refreshListData().value?.first
61 | ?: throw IllegalStateException()
62 | )
63 | }
64 | Then("해당 유저의 프로필 화면으로 이동 된다") {
65 | assertEquals(
66 | viewModel.output.refreshListData().value?.first?.name
67 | ?: throw IllegalStateException(),
68 | viewModel.output.goProfileActivity().value
69 | )
70 | }
71 | }
72 | Scenario("홈버튼을 클릭하면 화면이 정상적으로 종료되어야 한다.") {
73 | When("홈버튼을 클릭 했을 때") {
74 | viewModel.input.clickHomeButton()
75 | }
76 | Then("화면이 정상적으로 종료 된다.") {
77 | assertEquals(
78 | Unit,
79 | viewModel.output.finish().value
80 | )
81 | }
82 | }
83 | }
84 | })
85 |
--------------------------------------------------------------------------------
/app/src/test/java/com/omjoonkim/app/githubBrowserApp/SearchViewModelSpec.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp
2 |
3 | import androidx.arch.core.executor.ArchTaskExecutor
4 | import androidx.arch.core.executor.TaskExecutor
5 | import com.omjoonkim.app.githubBrowserApp.di.KoinSpek
6 | import com.omjoonkim.app.githubBrowserApp.di.test_module
7 | import com.omjoonkim.app.githubBrowserApp.viewmodel.SearchViewModel
8 | import org.koin.core.context.startKoin
9 | import org.koin.core.context.stopKoin
10 | import org.koin.test.inject
11 | import org.spekframework.spek2.style.gherkin.Feature
12 | import kotlin.test.assertEquals
13 |
14 | object SearchViewModelSpec : KoinSpek({
15 |
16 | beforeEachTest {
17 | startKoin {
18 | modules(test_module)
19 | }
20 | ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
21 | override fun executeOnDiskIO(runnable: Runnable) {
22 | runnable.run()
23 | }
24 |
25 | override fun isMainThread(): Boolean {
26 | return true
27 | }
28 |
29 | override fun postToMainThread(runnable: Runnable) {
30 | runnable.run()
31 | }
32 | })
33 | }
34 |
35 | afterEachTest {
36 | ArchTaskExecutor.getInstance().setDelegate(null)
37 | stopKoin()
38 | }
39 |
40 | val viewModel: SearchViewModel by inject()
41 |
42 | Feature("SearchViewModel spec") {
43 | val name = "omjoonkim"
44 | Scenario("빈값이 아닌 이름을 입력 받는다") {
45 | When("${name}을 입력 받았을 때") {
46 | viewModel.input.name(name)
47 | }
48 | Then("검색 버튼이 활성화 되어 있어야 한다") {
49 | assertEquals(true, viewModel.output.state().value?.enableSearchButton)
50 | }
51 | }
52 |
53 | Scenario("빈값의 이름을 입력 받는다") {
54 | When("빈값의 이름을 입력 받았을 때") {
55 | viewModel.input.name("")
56 | }
57 | Then("검색 버튼은 비활성화 되어 있어야 한다") {
58 | assertEquals(false, viewModel.output.state().value?.enableSearchButton)
59 | }
60 | }
61 |
62 | Scenario("검색 버튼을 클릭 한다") {
63 | Given("${name}이 입력되어 있다.") {
64 | viewModel.input.name(name)
65 | }
66 | When("검색 버튼을 클릭 하였을 때") {
67 | viewModel.input.clickSearchButton()
68 | }
69 | Then("결과 화면으로 이동한다") {
70 | assertEquals(name, viewModel.output.goResultActivity().value)
71 | }
72 | }
73 | }
74 | })
75 |
--------------------------------------------------------------------------------
/app/src/test/java/com/omjoonkim/app/githubBrowserApp/di/KoinSpek.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.di
2 |
3 | import org.koin.test.KoinTest
4 | import org.spekframework.spek2.Spek
5 | import org.spekframework.spek2.dsl.Root
6 |
7 | class KoinRoot(val root: Root) : KoinTest, Root by root
8 | abstract class KoinSpek(koinSpec: KoinRoot.() -> Unit) : Spek({
9 | koinSpec.invoke(KoinRoot(this))
10 | })
11 |
--------------------------------------------------------------------------------
/app/src/test/java/com/omjoonkim/app/githubBrowserApp/di/TestDummyGithubBrowserService.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.di
2 |
3 | import com.omjoonkim.project.githubBrowser.remote.GithubBrowserService
4 | import com.omjoonkim.project.githubBrowser.remote.model.RepoModel
5 | import com.omjoonkim.project.githubBrowser.remote.model.UserModel
6 | import io.reactivex.Single
7 |
8 | class TestDummyGithubBrowserService : GithubBrowserService {
9 | override fun getUser(userName: String): Single =
10 | Single.just(
11 | UserModel("omjoonkim", "")
12 | )
13 |
14 | override fun getRepos(userName: String): Single> =
15 | Single.just(
16 | listOf(
17 | RepoModel("repo1", "repo1 description", "1"),
18 | RepoModel("repo2", "repo2 description", "2"),
19 | RepoModel("repo3", "repo3 description", "3")
20 | )
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/test/java/com/omjoonkim/app/githubBrowserApp/di/TestSchedulersProvider.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.di
2 |
3 | import com.omjoonkim.project.githubBrowser.domain.schedulers.SchedulersProvider
4 | import io.reactivex.schedulers.Schedulers
5 |
6 |
7 | class TestSchedulerProvider : SchedulersProvider {
8 | override fun io() = Schedulers.trampoline()
9 |
10 | override fun ui() = Schedulers.trampoline()
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/omjoonkim/app/githubBrowserApp/di/test_modules.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.app.githubBrowserApp.di
2 |
3 | import com.omjoonkim.app.githubBrowserApp.myModule
4 | import com.omjoonkim.project.githubBrowser.domain.schedulers.SchedulersProvider
5 | import com.omjoonkim.project.githubBrowser.remote.GithubBrowserService
6 | import org.koin.dsl.module
7 |
8 | val testModule = module {
9 | single(override = true) {
10 | TestSchedulerProvider() as SchedulersProvider
11 | }
12 | single(override = true) {
13 | TestDummyGithubBrowserService() as GithubBrowserService
14 | }
15 | }
16 |
17 | val test_module = listOf(myModule, testModule)
18 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext {
5 | kotlin_version = '1.3.21'
6 | lifecycle_version = "2.0.0"
7 | spek_version = '2.0.0-rc.1'
8 | android_junit5_version = '1.3.2.0'
9 | junit5_runner = '0.2.2'
10 | jacoco_version = '0.8.3'
11 | koin_version = '2.0.0-rc-2'
12 | }
13 |
14 | repositories {
15 | jcenter()
16 | google()
17 | }
18 | dependencies {
19 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
20 | classpath 'com.android.tools.build:gradle:3.4.0'
21 | classpath "de.mannodermaus.gradle.plugins:android-junit5:$android_junit5_version"
22 | classpath "org.jacoco:org.jacoco.core:$jacoco_version"
23 | }
24 | }
25 |
26 | allprojects {
27 | repositories {
28 | jcenter()
29 | google()
30 | }
31 | }
32 |
33 | task clean(type: Delete) {
34 | delete rootProject.buildDir
35 | }
36 |
--------------------------------------------------------------------------------
/data/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 |
3 | dependencies {
4 | implementation fileTree(dir: 'libs', include: ['*.jar'])
5 | compile project(':domain')
6 |
7 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
8 |
9 | implementation 'io.reactivex.rxjava2:rxjava:2.2.5'
10 | implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
11 | }
12 |
--------------------------------------------------------------------------------
/data/src/main/java/com/omjoonkim/project/githubBrowser/data/interactor/ForkDataRepository.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.data.interactor
2 |
3 | import com.omjoonkim.project.githubBrowser.data.source.GithubBrowserRemote
4 | import com.omjoonkim.project.githubBrowser.domain.entity.Fork
5 | import com.omjoonkim.project.githubBrowser.domain.entity.Repo
6 | import com.omjoonkim.project.githubBrowser.domain.repository.ForkRepository
7 | import com.omjoonkim.project.githubBrowser.domain.repository.RepoRepository
8 | import io.reactivex.Single
9 |
10 | class ForkDataRepository(
11 | private val remote: GithubBrowserRemote
12 | ) : ForkRepository {
13 |
14 | override fun gets(
15 | userName: String,
16 | id: String
17 | ): Single> = remote.getForks(userName, id)
18 | }
19 |
--------------------------------------------------------------------------------
/data/src/main/java/com/omjoonkim/project/githubBrowser/data/interactor/RepoDataRepository.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.data.interactor
2 |
3 | import com.omjoonkim.project.githubBrowser.data.source.GithubBrowserRemote
4 | import com.omjoonkim.project.githubBrowser.domain.entity.Fork
5 | import com.omjoonkim.project.githubBrowser.domain.entity.Repo
6 | import com.omjoonkim.project.githubBrowser.domain.repository.RepoRepository
7 | import io.reactivex.Single
8 |
9 | class RepoDataRepository(
10 | private val remote: GithubBrowserRemote
11 | ) : RepoRepository {
12 |
13 | override fun gets(
14 | userName: String
15 | ): Single> = remote.getRepos(userName)
16 |
17 | override fun get(
18 | userName: String,
19 | id: String
20 | ): Single = remote.getRepo(userName, id)
21 | }
22 |
--------------------------------------------------------------------------------
/data/src/main/java/com/omjoonkim/project/githubBrowser/data/interactor/UserDataRepository.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.data.interactor
2 |
3 | import com.omjoonkim.project.githubBrowser.data.source.GithubBrowserRemote
4 | import com.omjoonkim.project.githubBrowser.domain.entity.User
5 | import com.omjoonkim.project.githubBrowser.domain.repository.UserRepository
6 | import io.reactivex.Single
7 |
8 | class UserDataRepository(
9 | private val remote: GithubBrowserRemote
10 | ) : UserRepository {
11 | override fun get(userName: String): Single = remote.getUserInfo(userName)
12 | }
13 |
--------------------------------------------------------------------------------
/data/src/main/java/com/omjoonkim/project/githubBrowser/data/source/GithubBrowserRemote.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.data.source
2 |
3 | import com.omjoonkim.project.githubBrowser.domain.entity.Fork
4 | import com.omjoonkim.project.githubBrowser.domain.entity.Repo
5 | import com.omjoonkim.project.githubBrowser.domain.entity.User
6 | import io.reactivex.Single
7 |
8 | interface GithubBrowserRemote{
9 | fun getUserInfo(userName: String): Single
10 | fun getRepos(userName: String): Single>
11 | fun getRepo(userName: String, id: String): Single
12 | fun getForks(userName: String, id: String): Single>
13 | }
14 |
--------------------------------------------------------------------------------
/domain/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 |
3 | dependencies {
4 | implementation fileTree(dir: 'libs', include: ['*.jar'])
5 |
6 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
7 |
8 | implementation 'io.reactivex.rxjava2:rxjava:2.2.5'
9 | implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
10 | }
11 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/omjoonkim/project/githubBrowser/domain/entity/Fork.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.domain.entity
2 |
3 |
4 | data class Fork(
5 | val name: String,
6 | val fullName: String,
7 | val owner: User
8 | )
9 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/omjoonkim/project/githubBrowser/domain/entity/Repo.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.domain.entity
2 |
3 |
4 | data class Repo(
5 | val name: String,
6 | val description: String,
7 | val starCount: String
8 | )
9 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/omjoonkim/project/githubBrowser/domain/entity/User.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.domain.entity
2 |
3 |
4 | data class User(
5 | val name: String,
6 | val profile: String
7 | )
8 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/omjoonkim/project/githubBrowser/domain/exception/NetworkException.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.domain.exception
2 |
3 | class NetworkException(
4 | message: String,
5 | val code: Int
6 | ) : Throwable(message)
7 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/omjoonkim/project/githubBrowser/domain/exception/RateLimitException.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.domain.exception
2 |
3 | class RateLimitException : Throwable()
4 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/omjoonkim/project/githubBrowser/domain/interactor/CompletableUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.domain.interactor
2 |
3 | import com.omjoonkim.project.githubBrowser.domain.schedulers.SchedulersProvider
4 | import io.reactivex.Completable
5 |
6 | abstract class CompletableUseCase(
7 | private val schedulersProvider: SchedulersProvider
8 | ) {
9 | protected abstract fun buildUseCaseCompletable(params: Params): Completable
10 |
11 | fun get(params: Params): Completable = buildUseCaseCompletable(params)
12 | .subscribeOn(schedulersProvider.io())
13 | .observeOn(schedulersProvider.ui())
14 | }
15 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/omjoonkim/project/githubBrowser/domain/interactor/SingleUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.domain.interactor
2 |
3 | import com.omjoonkim.project.githubBrowser.domain.schedulers.SchedulersProvider
4 | import io.reactivex.Single
5 | import io.reactivex.observers.DisposableSingleObserver
6 |
7 | abstract class SingleUseCase(
8 | private val schedulersProvider: SchedulersProvider
9 | ) {
10 | protected abstract fun buildUseCaseSingle(params: Params): Single
11 |
12 | fun get(params: Params) = buildUseCaseSingle(params)
13 | .subscribeOn(schedulersProvider.io())
14 | .observeOn(schedulersProvider.ui())
15 | }
16 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/omjoonkim/project/githubBrowser/domain/interactor/usecases/GetRepoDetail.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.domain.interactor.usecases
2 |
3 | import com.omjoonkim.project.githubBrowser.domain.entity.Fork
4 | import com.omjoonkim.project.githubBrowser.domain.entity.Repo
5 | import com.omjoonkim.project.githubBrowser.domain.exception.NetworkException
6 | import com.omjoonkim.project.githubBrowser.domain.exception.RateLimitException
7 | import com.omjoonkim.project.githubBrowser.domain.interactor.SingleUseCase
8 | import com.omjoonkim.project.githubBrowser.domain.repository.ForkRepository
9 | import com.omjoonkim.project.githubBrowser.domain.repository.RepoRepository
10 | import com.omjoonkim.project.githubBrowser.domain.schedulers.SchedulersProvider
11 | import io.reactivex.Single
12 | import io.reactivex.functions.BiFunction
13 |
14 | class GetRepoDetail(
15 | private val repoRepository: RepoRepository,
16 | private val forkRepository: ForkRepository,
17 | schedulersProvider: SchedulersProvider
18 | ) : SingleUseCase>, Pair>(
19 | schedulersProvider) {
20 | override fun buildUseCaseSingle(params: Pair)
21 | : Single>> =
22 | Single.zip(
23 | repoRepository.get(params.first, params.second),
24 | forkRepository.gets(params.first, params.second),
25 | BiFunction { t1: Repo, t2: List ->
26 | t1 to t2
27 | }
28 | ).onErrorResumeNext {
29 | if (it is NetworkException && it.code == 403)
30 | Single.error(RateLimitException())
31 | else Single.error(it)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/omjoonkim/project/githubBrowser/domain/interactor/usecases/GetUserData.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.domain.interactor.usecases
2 |
3 | import com.omjoonkim.project.githubBrowser.domain.entity.Repo
4 | import com.omjoonkim.project.githubBrowser.domain.entity.User
5 | import com.omjoonkim.project.githubBrowser.domain.interactor.SingleUseCase
6 | import com.omjoonkim.project.githubBrowser.domain.repository.RepoRepository
7 | import com.omjoonkim.project.githubBrowser.domain.repository.UserRepository
8 | import com.omjoonkim.project.githubBrowser.domain.schedulers.SchedulersProvider
9 | import io.reactivex.Single
10 | import io.reactivex.functions.BiFunction
11 |
12 | class GetUserData(
13 | private val userRepository: UserRepository,
14 | private val repoRepository: RepoRepository,
15 | schedulersProvider: SchedulersProvider
16 | ) : SingleUseCase>, String>(
17 | schedulersProvider
18 | ) {
19 | override fun buildUseCaseSingle(userName: String): Single>> =
20 | Single.zip(
21 | userRepository.get(userName),
22 | repoRepository.gets(userName),
23 | BiFunction, Pair>> { t1, t2 ->
24 | Pair(t1, t2.sortedByDescending { it.starCount })
25 | }
26 | )
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/omjoonkim/project/githubBrowser/domain/repository/ForkRepository.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.domain.repository
2 |
3 | import com.omjoonkim.project.githubBrowser.domain.entity.Fork
4 | import io.reactivex.Single
5 |
6 | interface ForkRepository {
7 | fun gets(userName: String, id: String): Single>
8 | }
9 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/omjoonkim/project/githubBrowser/domain/repository/RepoRepository.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.domain.repository
2 |
3 | import com.omjoonkim.project.githubBrowser.domain.entity.Repo
4 | import io.reactivex.Single
5 |
6 | interface RepoRepository {
7 | fun gets(userName: String): Single>
8 | fun get(userName: String, id: String): Single
9 | }
10 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/omjoonkim/project/githubBrowser/domain/repository/UserRepository.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.domain.repository
2 |
3 | import com.omjoonkim.project.githubBrowser.domain.entity.User
4 | import io.reactivex.Single
5 |
6 | interface UserRepository {
7 | fun get(userName: String): Single
8 | }
9 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/omjoonkim/project/githubBrowser/domain/schedulers/SchedulersProvider.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.domain.schedulers
2 |
3 | import io.reactivex.Scheduler
4 |
5 | interface SchedulersProvider {
6 | fun io(): Scheduler
7 | fun ui(): Scheduler
8 | }
9 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/omjoonkim/GitHubBrowserApp/8904f6b9fecc06c95cda2c2d12a0ff94e47af842/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Apr 18 11:47:44 KST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/presentaion/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 |
3 | dependencies {
4 | implementation fileTree(dir: 'libs', include: ['*.jar'])
5 |
6 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
7 | }
8 |
--------------------------------------------------------------------------------
/presentaion/src/main/java/com/omjoonkim/project/githubBrowserApp/presentaion/MyClass.java:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.presentaion;
2 |
3 | public class MyClass {
4 | }
5 |
--------------------------------------------------------------------------------
/remote/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 |
3 | dependencies {
4 | implementation fileTree(dir: 'libs', include: ['*.jar'])
5 | compile project(':data')
6 |
7 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
8 |
9 | //retrofit
10 | implementation 'com.squareup.retrofit2:retrofit:2.5.0'
11 | implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
12 | implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
13 | implementation 'com.squareup.okhttp3:okhttp:3.12.0'
14 | implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
15 | }
16 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/omjoonkim/project/githubBrowser/remote/GithubBrowserRemoteImpl.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.remote
2 |
3 | import com.omjoonkim.project.githubBrowser.data.source.GithubBrowserRemote
4 | import com.omjoonkim.project.githubBrowser.domain.entity.Fork
5 | import com.omjoonkim.project.githubBrowser.domain.entity.Repo
6 | import com.omjoonkim.project.githubBrowser.domain.entity.User
7 | import com.omjoonkim.project.githubBrowser.remote.mapper.ForkEntityMapper
8 | import com.omjoonkim.project.githubBrowser.remote.mapper.RepoEntityMapper
9 | import com.omjoonkim.project.githubBrowser.remote.mapper.UserEntityMapper
10 | import io.reactivex.Single
11 |
12 | class GithubBrowserRemoteImpl(
13 | private val githubBrowserAppService: GithubBrowserService,
14 | private val userEntityMapper: UserEntityMapper,
15 | private val repoEntityMapper: RepoEntityMapper,
16 | private val forkEntityMapper: ForkEntityMapper
17 | ) : GithubBrowserRemote {
18 |
19 | override fun getUserInfo(userName: String): Single = githubBrowserAppService
20 | .getUser(userName)
21 | .map { userEntityMapper.mapFromRemote(it) }
22 | .composeDomain()
23 |
24 | override fun getRepos(userName: String): Single> = githubBrowserAppService
25 | .getRepos(userName)
26 | .map { it.map { repoEntityMapper.mapFromRemote(it) } }
27 | .composeDomain()
28 |
29 | override fun getRepo(userName: String, id: String): Single = githubBrowserAppService
30 | .getRepo(userName, id)
31 | .map { repoEntityMapper.mapFromRemote(it) }
32 | .composeDomain()
33 |
34 | override fun getForks(
35 | userName: String,
36 | id: String
37 | ): Single> = githubBrowserAppService
38 | .getForks(userName, id)
39 | .map { it.map { forkEntityMapper.mapFromRemote(it) } }
40 | .composeDomain()
41 | }
42 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/omjoonkim/project/githubBrowser/remote/GithubBrowserService.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.remote
2 |
3 | import com.omjoonkim.project.githubBrowser.remote.model.ForkModel
4 | import com.omjoonkim.project.githubBrowser.remote.model.RepoModel
5 | import com.omjoonkim.project.githubBrowser.remote.model.UserModel
6 | import io.reactivex.Single
7 | import retrofit2.http.GET
8 | import retrofit2.http.Path
9 |
10 | interface GithubBrowserService {
11 |
12 | @GET("/users/{userName}")
13 | fun getUser(
14 | @Path("userName") userName: String
15 | ): Single
16 |
17 | @GET("/users/{userName}/repos")
18 | fun getRepos(
19 | @Path("userName") userName: String
20 | ): Single>
21 |
22 | @GET("/repos/{userName}/{repo}")
23 | fun getRepo(
24 | @Path("userName") userName: String,
25 | @Path("repo") repoName: String
26 | ): Single
27 |
28 | @GET("/repos/{userName}/{repo}/forks")
29 | fun getForks(
30 | @Path("userName") userName: String,
31 | @Path("repo") repoName: String
32 | ): Single>
33 | }
34 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/omjoonkim/project/githubBrowser/remote/GithubBrowserServiceFactory.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.remote
2 |
3 | import com.google.gson.Gson
4 | import com.google.gson.GsonBuilder
5 | import okhttp3.OkHttpClient
6 | import okhttp3.logging.HttpLoggingInterceptor
7 | import retrofit2.Retrofit
8 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
9 | import retrofit2.converter.gson.GsonConverterFactory
10 |
11 | object GithubBrowserServiceFactory {
12 |
13 | fun makeGithubBrowserService(debug: Boolean, baseUrl: String): GithubBrowserService {
14 | val okHttpClient = makeOkHttpClient(
15 | makeLoggingInterceptor(debug)
16 | )
17 | return makeGithubBrowserService(baseUrl, okHttpClient, Gson())
18 | }
19 |
20 | private fun makeGithubBrowserService(baseUrl: String, okHttpClient: OkHttpClient, gson: Gson): GithubBrowserService {
21 | val retrofit = Retrofit.Builder()
22 | .baseUrl(baseUrl)
23 | .client(okHttpClient)
24 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
25 | .addConverterFactory(GsonConverterFactory.create(gson))
26 | .build()
27 | return retrofit.create(GithubBrowserService::class.java)
28 | }
29 |
30 | private fun makeOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor) = OkHttpClient.Builder()
31 | .addInterceptor(httpLoggingInterceptor)
32 | .build()
33 |
34 | private fun makeLoggingInterceptor(debug: Boolean): HttpLoggingInterceptor {
35 | val logging = HttpLoggingInterceptor()
36 | logging.level = if (debug)
37 | HttpLoggingInterceptor.Level.BODY
38 | else
39 | HttpLoggingInterceptor.Level.NONE
40 | return logging
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/omjoonkim/project/githubBrowser/remote/NetworkExceptionTransforemer.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.remote
2 |
3 | import com.omjoonkim.project.githubBrowser.domain.exception.NetworkException
4 | import io.reactivex.*
5 | import retrofit2.HttpException
6 |
7 | internal class NetworkExceptionSingleTransformer : SingleTransformer {
8 | override fun apply(upstream: Single): SingleSource =
9 | upstream.onErrorResumeNext {
10 | Single.error(
11 | if (it is HttpException)
12 | NetworkException(it.message(), it.code())
13 | else it
14 | )
15 | }
16 | }
17 |
18 | internal class NetworkExceptionCompletableTransformer : CompletableTransformer {
19 | override fun apply(upstream: Completable): CompletableSource = upstream.onErrorResumeNext {
20 | Completable.error(
21 | if (it is HttpException)
22 | NetworkException(it.message(), it.code())
23 | else it
24 | )
25 | }
26 | }
27 |
28 | internal fun Single.composeDomain() =
29 | compose(NetworkExceptionSingleTransformer())
30 |
31 | internal fun Completable.composeDomain() = compose(NetworkExceptionCompletableTransformer())
32 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/omjoonkim/project/githubBrowser/remote/mapper/EntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.remote.mapper
2 |
3 | interface EntityMapper {
4 |
5 | fun mapFromRemote(model: M): E
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/omjoonkim/project/githubBrowser/remote/mapper/ForkEntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.remote.mapper
2 |
3 | import com.omjoonkim.project.githubBrowser.domain.entity.Fork
4 | import com.omjoonkim.project.githubBrowser.domain.entity.Repo
5 | import com.omjoonkim.project.githubBrowser.remote.model.ForkModel
6 | import com.omjoonkim.project.githubBrowser.remote.model.RepoModel
7 |
8 | class ForkEntityMapper(
9 | private val userMapper: UserEntityMapper
10 | ) : EntityMapper {
11 |
12 | override fun mapFromRemote(model: ForkModel) = Fork(
13 | model.name,
14 | model.fullName,
15 | userMapper.mapFromRemote(model.owner)
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/omjoonkim/project/githubBrowser/remote/mapper/RepoEntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.remote.mapper
2 |
3 | import com.omjoonkim.project.githubBrowser.domain.entity.Repo
4 | import com.omjoonkim.project.githubBrowser.remote.model.RepoModel
5 |
6 | class RepoEntityMapper : EntityMapper {
7 |
8 | override fun mapFromRemote(model: RepoModel) = Repo(
9 | model.name,
10 | model.description ?: "",
11 | model.starCount
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/omjoonkim/project/githubBrowser/remote/mapper/UserEntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.remote.mapper
2 |
3 | import com.omjoonkim.project.githubBrowser.domain.entity.User
4 | import com.omjoonkim.project.githubBrowser.remote.model.UserModel
5 |
6 | class UserEntityMapper : EntityMapper {
7 |
8 | override fun mapFromRemote(model: UserModel) = User(model.name, model.profile)
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/omjoonkim/project/githubBrowser/remote/model/ForkModel.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.remote.model
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class ForkModel(
6 | val name: String,
7 | @SerializedName("full_name") val fullName: String,
8 | val owner: UserModel
9 | )
10 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/omjoonkim/project/githubBrowser/remote/model/RepoModel.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.remote.model
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class RepoModel(
6 | val name: String,
7 | val description: String?,
8 | @SerializedName("stargazers_count") val starCount: String
9 | )
10 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/omjoonkim/project/githubBrowser/remote/model/UserModel.kt:
--------------------------------------------------------------------------------
1 | package com.omjoonkim.project.githubBrowser.remote.model
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class UserModel(
6 | @SerializedName("login") val name: String,
7 | @SerializedName("avatar_url") val profile: String
8 | )
9 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':domain', ':data', ':remote'
2 |
--------------------------------------------------------------------------------