├── .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 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 |