├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── jaydev
│ │ └── github
│ │ ├── App.kt
│ │ ├── common
│ │ └── extension.kt
│ │ ├── di
│ │ ├── AppModuleProvider.kt
│ │ ├── DataSourceModule.kt
│ │ ├── DomainModule.kt
│ │ ├── NetworkModule.kt
│ │ ├── RemoteModule.kt
│ │ └── loader
│ │ │ ├── CoroutineQualifiers.kt
│ │ │ ├── CoroutineScopeModule.kt
│ │ │ └── ResourcesProvider.kt
│ │ ├── model
│ │ ├── AlertUIModel.kt
│ │ ├── PopupMessage.kt
│ │ ├── RepoData.kt
│ │ └── SearchListItem.kt
│ │ ├── network
│ │ └── NetworkConnectionInterceptor.kt
│ │ ├── ui
│ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ └── view
│ │ ├── MainActivity.kt
│ │ ├── base
│ │ ├── BaseActivity.kt
│ │ ├── BaseViewModel.kt
│ │ └── ContentLoadingProgress.kt
│ │ ├── repo
│ │ ├── RepoDetailActivity.kt
│ │ ├── RepoDetailScreen.kt
│ │ └── RepoDetailViewModel.kt
│ │ ├── result
│ │ ├── SearchResultActivity.kt
│ │ ├── SearchResultScreen.kt
│ │ └── SearchResultViewModel.kt
│ │ └── search
│ │ ├── SearchActivity.kt
│ │ └── SearchScreen.kt
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ └── ic_launcher_background.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
├── build.gradle.kts
├── data
├── .gitignore
├── build.gradle.kts
└── src
│ └── main
│ └── java
│ └── com
│ └── jaydev
│ └── github
│ └── model
│ ├── interactor
│ ├── GithubDataRepository.kt
│ └── NetErrorHandlerImpl.kt
│ └── source
│ └── GithubRemote.kt
├── domain
├── .gitignore
├── build.gradle.kts
└── src
│ └── main
│ └── java
│ └── com
│ └── jaydev
│ └── github
│ └── domain
│ ├── NetResult.kt
│ ├── entity
│ ├── Fork.kt
│ ├── NetError.kt
│ ├── Repo.kt
│ └── User.kt
│ ├── interactor
│ ├── ErrorHandler.kt
│ ├── NetworkConnectException.kt
│ └── usecase
│ │ ├── BaseUseCase.kt
│ │ └── GithubUseCase.kt
│ └── repository
│ └── GithubRepository.kt
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── remote
├── .gitignore
├── build.gradle.kts
└── src
│ └── main
│ └── java
│ └── com
│ └── jaydev
│ └── github
│ └── remote
│ ├── GithubRemoteImpl.kt
│ ├── GithubService.kt
│ ├── mapper
│ ├── EntityMapper.kt
│ ├── ForkEntityMapper.kt
│ ├── RepoEntityMapper.kt
│ └── UserEntityMapper.kt
│ └── model
│ ├── ForkModel.kt
│ ├── RepoModel.kt
│ └── UserModel.kt
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/android,androidstudio,macos
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=android,androidstudio,macos
3 |
4 | ### Android ###
5 | # Built application files
6 | *.apk
7 | *.aar
8 | *.ap_
9 | *.aab
10 |
11 | # Files for the ART/Dalvik VM
12 | *.dex
13 |
14 | # Java class files
15 | *.class
16 |
17 | # Generated files
18 | bin/
19 | gen/
20 | out/
21 | # Uncomment the following line in case you need and you don't have the release build type files in your app
22 | # release/
23 |
24 | # Gradle files
25 | .gradle/
26 | build/
27 |
28 | # Local configuration file (sdk path, etc)
29 | local.properties
30 |
31 | # Proguard folder generated by Eclipse
32 | proguard/
33 |
34 | # Log Files
35 | *.log
36 |
37 | # Android Studio Navigation editor temp files
38 | .navigation/
39 |
40 | # Android Studio captures folder
41 | captures/
42 |
43 | # IntelliJ
44 | *.iml
45 | .idea/workspace.xml
46 | .idea/tasks.xml
47 | .idea/gradle.xml
48 | .idea/assetWizardSettings.xml
49 | .idea/dictionaries
50 | .idea/libraries
51 | .idea/jarRepositories.xml
52 | # Android Studio 3 in .gitignore file.
53 | .idea/caches
54 | .idea/modules.xml
55 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
56 | .idea/navEditor.xml
57 |
58 | # Keystore files
59 | # Uncomment the following lines if you do not want to check your keystore files in.
60 | #*.jks
61 | #*.keystore
62 |
63 | # External native build folder generated in Android Studio 2.2 and later
64 | .externalNativeBuild
65 | .cxx/
66 |
67 | # Google Services (e.g. APIs or Firebase)
68 | # google-services.json
69 |
70 | # Freeline
71 | freeline.py
72 | freeline/
73 | freeline_project_description.json
74 |
75 | # fastlane
76 | fastlane/report.xml
77 | fastlane/Preview.html
78 | fastlane/screenshots
79 | fastlane/test_output
80 | fastlane/readme.md
81 |
82 | # Version control
83 | vcs.xml
84 |
85 | # lint
86 | lint/intermediates/
87 | lint/generated/
88 | lint/outputs/
89 | lint/tmp/
90 | # lint/reports/
91 |
92 | # Android Profiling
93 | *.hprof
94 |
95 | ### Android Patch ###
96 | gen-external-apklibs
97 | output.json
98 |
99 | # Replacement of .externalNativeBuild directories introduced
100 | # with Android Studio 3.5.
101 |
102 | ### macOS ###
103 | # General
104 | .DS_Store
105 | .AppleDouble
106 | .LSOverride
107 |
108 | # Icon must end with two \r
109 | Icon
110 |
111 |
112 | # Thumbnails
113 | ._*
114 |
115 | # Files that might appear in the root of a volume
116 | .DocumentRevisions-V100
117 | .fseventsd
118 | .Spotlight-V100
119 | .TemporaryItems
120 | .Trashes
121 | .VolumeIcon.icns
122 | .com.apple.timemachine.donotpresent
123 |
124 | # Directories potentially created on remote AFP share
125 | .AppleDB
126 | .AppleDesktop
127 | Network Trash Folder
128 | Temporary Items
129 | .apdisk
130 |
131 | ### AndroidStudio ###
132 | # Covers files to be ignored for android development using Android Studio.
133 |
134 | # Built application files
135 |
136 | # Files for the ART/Dalvik VM
137 |
138 | # Java class files
139 |
140 | # Generated files
141 |
142 | # Gradle files
143 | .gradle
144 |
145 | # Signing files
146 | .signing/
147 |
148 | # Local configuration file (sdk path, etc)
149 |
150 | # Proguard folder generated by Eclipse
151 |
152 | # Log Files
153 |
154 | # Android Studio
155 | /*/build/
156 | /*/local.properties
157 | /*/out
158 | /*/*/build
159 | /*/*/production
160 | *.ipr
161 | *~
162 | *.swp
163 |
164 | # Keystore files
165 | *.jks
166 | *.keystore
167 |
168 | # Google Services (e.g. APIs or Firebase)
169 | # google-services.json
170 |
171 | # Android Patch
172 |
173 | # External native build folder generated in Android Studio 2.2 and later
174 |
175 | # NDK
176 | obj/
177 |
178 | # IntelliJ IDEA
179 | *.iws
180 | /out/
181 |
182 | # User-specific configurations
183 | .idea/caches/
184 | .idea/libraries/
185 | .idea/shelf/
186 | .idea/.name
187 | .idea/compiler.xml
188 | .idea/copyright/profiles_settings.xml
189 | .idea/encodings.xml
190 | .idea/misc.xml
191 | .idea/scopes/scope_settings.xml
192 | .idea/vcs.xml
193 | .idea/jsLibraryMappings.xml
194 | .idea/datasources.xml
195 | .idea/dataSources.ids
196 | .idea/sqlDataSources.xml
197 | .idea/dynamic.xml
198 | .idea/uiDesigner.xml
199 |
200 | # OS-specific files
201 | .DS_Store?
202 | ehthumbs.db
203 | Thumbs.db
204 |
205 | # Legacy Eclipse project files
206 | .classpath
207 | .project
208 | .cproject
209 | .settings/
210 |
211 | # Mobile Tools for Java (J2ME)
212 | .mtj.tmp/
213 |
214 | # Package Files #
215 | *.war
216 | *.ear
217 |
218 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
219 | hs_err_pid*
220 |
221 | ## Plugin-specific files:
222 |
223 | # mpeltonen/sbt-idea plugin
224 | .idea_modules/
225 |
226 | # JIRA plugin
227 | atlassian-ide-plugin.xml
228 |
229 | # Mongo Explorer plugin
230 | .idea/mongoSettings.xml
231 |
232 | # Crashlytics plugin (for Android Studio and IntelliJ)
233 | com_crashlytics_export_strings.xml
234 | crashlytics.properties
235 | crashlytics-build.properties
236 | fabric.properties
237 |
238 | ### AndroidStudio Patch ###
239 |
240 | !/gradle/wrapper/gradle-wrapper.jar
241 |
242 | # End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,macos
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GithubBrowser Clean Architecture Sample
2 | ### Modern Clean Architecture + Retrofit(Kotlin FLow)
3 |
4 |
5 | * 본 샘플 프로젝트는 https://github.com/omjoonkim/GitHubBrowserApp 을 인용하였습니다.
6 |
7 | * 정석적인 Clean Architecture의 구조가 아님을 밝힙니다.
8 |
9 | * MVVM 브랜치 -> AAC + XML + MVVM + Clean Architecture 형태를 띄고 있습니다.
10 | * MVI 브랜치 -> AAC + Compose + Orbit MVI + Clean Architecture 형태를 띄고 있습니다.
11 |
12 | * Flow의 메소드 체이닝을 이용한 Retrofit Error Handling을 담고 있습니다.
13 |
14 |
15 | # 개발 환경
16 |
17 | * Android Studio Hedgehog | 2023.1.1 Canary 13
18 |
19 | * Android Gradle Plugin Version 8.2.0-alpha15
20 |
21 | * minSdk 24
22 |
23 | * targetSdk 33
24 |
25 | * Kotlin Version 1.8.10
26 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
2 | plugins {
3 | alias(libs.plugins.androidApplication)
4 | alias(libs.plugins.kotlinAndroid)
5 | alias(libs.plugins.kotlinKapt)
6 | alias(libs.plugins.kotlinSerialization)
7 | alias(libs.plugins.hilt)
8 | alias(libs.plugins.kotlinParcelize)
9 | }
10 |
11 | android {
12 | namespace = "com.jaydev.github"
13 | compileSdk = 33
14 |
15 | defaultConfig {
16 | applicationId = "com.jaydev.github"
17 | minSdk = 24
18 | targetSdk = 33
19 | versionCode = 1
20 | versionName = "1.0"
21 |
22 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
23 | vectorDrawables {
24 | useSupportLibrary = true
25 | }
26 | }
27 |
28 | buildTypes {
29 | release {
30 | isMinifyEnabled = false
31 | proguardFiles(
32 | getDefaultProguardFile("proguard-android-optimize.txt"),
33 | "proguard-rules.pro"
34 | )
35 | }
36 | }
37 | compileOptions {
38 | sourceCompatibility = JavaVersion.VERSION_17
39 | targetCompatibility = JavaVersion.VERSION_17
40 | }
41 | kotlinOptions {
42 | jvmTarget = "17"
43 | }
44 |
45 | buildFeatures {
46 | compose = true
47 | }
48 | composeOptions {
49 | kotlinCompilerExtensionVersion = "1.4.3"
50 | }
51 | packaging {
52 | resources {
53 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
54 | }
55 | }
56 | }
57 |
58 | dependencies {
59 | implementation(project(":domain"))
60 | implementation(project(":remote"))
61 |
62 | implementation(libs.core.ktx)
63 | implementation(libs.lifecycle.runtime.ktx)
64 | implementation(libs.activity.compose)
65 | implementation(platform(libs.compose.bom))
66 | implementation(libs.ui)
67 | implementation(libs.ui.graphics)
68 | implementation(libs.ui.tooling.preview)
69 | implementation(libs.material3)
70 | implementation(libs.navigation.compose)
71 | implementation(libs.paging.compose)
72 | implementation(libs.constraintlayout.compose)
73 | testImplementation(libs.junit)
74 | androidTestImplementation(libs.androidx.test.ext.junit)
75 | androidTestImplementation(libs.espresso.core)
76 | androidTestImplementation(platform(libs.compose.bom))
77 | androidTestImplementation(libs.ui.test.junit4)
78 | debugImplementation(libs.ui.tooling)
79 | debugImplementation(libs.ui.test.manifest)
80 |
81 | implementation(libs.lifecycle.viewmodel.ktx)
82 | implementation(libs.lifecycle.viewmodel.compose)
83 | implementation(libs.activity.ktx)
84 |
85 | implementation(libs.orbit.compose)
86 | implementation(libs.orbit.viewmodel)
87 |
88 | implementation(libs.hilt.android)
89 | implementation(libs.hilt.navigation.compose)
90 |
91 | implementation(libs.retrofit)
92 | implementation(libs.retrofit.serialization)
93 |
94 | implementation(platform(libs.okhttp.bom))
95 | implementation(libs.okhttp)
96 | implementation(libs.okhttp.logging.interceptor)
97 |
98 | implementation(libs.kotlin.stdlib)
99 | implementation(libs.kotlin.reflect)
100 | implementation(libs.kotlinx.coroutines.android)
101 | implementation(libs.kotlinx.serialization.json)
102 | implementation(libs.kotlinx.datetime)
103 |
104 | implementation(libs.coil.compose)
105 |
106 | implementation(libs.logger)
107 |
108 | kapt(libs.hilt.compiler)
109 | kapt(libs.hilt.androidx.compiler)
110 |
111 | implementation("com.squareup:javapoet:1.13.0")
112 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
29 |
30 |
31 |
32 |
33 |
36 |
37 |
38 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/App.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github
2 |
3 | import android.app.Application
4 | import com.orhanobut.logger.AndroidLogAdapter
5 | import com.orhanobut.logger.Logger
6 | import com.orhanobut.logger.PrettyFormatStrategy
7 | import dagger.hilt.android.HiltAndroidApp
8 |
9 | @HiltAndroidApp
10 | class App : Application() {
11 |
12 | override fun onCreate() {
13 | super.onCreate()
14 | initLogger()
15 | }
16 |
17 | private fun initLogger() {
18 | PrettyFormatStrategy.newBuilder()
19 | .tag("JayDev")
20 | .build()
21 | .let { AndroidLogAdapter(it) }
22 | .also {
23 | Logger.addLogAdapter(it)
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/common/extension.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.common
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.res.Resources
6 | import android.os.Looper
7 | import android.view.View
8 | import android.view.inputmethod.InputMethodManager
9 | import androidx.annotation.CheckResult
10 | import androidx.annotation.Dimension
11 | import kotlinx.coroutines.ExperimentalCoroutinesApi
12 | import kotlinx.coroutines.Job
13 | import kotlinx.coroutines.channels.awaitClose
14 | import kotlinx.coroutines.flow.Flow
15 | import kotlinx.coroutines.flow.callbackFlow
16 | import kotlinx.coroutines.flow.conflate
17 |
18 | @Dimension(unit = Dimension.PX)
19 | fun Int.toPx(): Int = (Resources.getSystem().displayMetrics.density * this).toInt()
20 |
21 | @Dimension(unit = Dimension.PX)
22 | fun Float.toPx() = (Resources.getSystem().displayMetrics.density * this).toInt()
23 |
24 | @Dimension(unit = Dimension.DP)
25 | fun Int.toDp(): Float = this / Resources.getSystem().displayMetrics.density
26 |
27 |
28 | @Dimension(unit = Dimension.PX)
29 | fun Context.dpToPx(@Dimension(unit = Dimension.DP) dp: Int): Int {
30 | return (resources.displayMetrics.density * dp).toInt()
31 | }
32 |
33 | @Dimension(unit = Dimension.PX)
34 | fun Context.dpToPx(@Dimension(unit = Dimension.DP) dp: Float): Int {
35 | return (resources.displayMetrics.density * dp).toInt()
36 | }
37 |
38 | @Dimension(unit = Dimension.DP)
39 | fun Context.pxToDp(@Dimension px: Int): Float {
40 | return px.toFloat() / resources.displayMetrics.density
41 | }
42 |
43 | inline fun T.ordinal(): Int {
44 | if (T::class.isSealed) {
45 | return T::class.java.classes.indexOfFirst { sub -> sub == javaClass }
46 | }
47 |
48 | val klass = if (T::class.isCompanion) {
49 | javaClass.declaringClass
50 | } else {
51 | javaClass
52 | }
53 |
54 | return klass.superclass?.classes?.indexOfFirst { it == klass } ?: -1
55 | }
56 |
57 | fun Job?.cancelIfActive() {
58 | if (this?.isActive == true) {
59 | cancel()
60 | }
61 | }
62 |
63 | fun View.showKeyboard(isForced: Boolean = false) {
64 | val inputMethodManager =
65 | context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
66 | inputMethodManager.showSoftInput(
67 | this,
68 | if (isForced) InputMethodManager.SHOW_FORCED else InputMethodManager.SHOW_IMPLICIT
69 | )
70 | }
71 |
72 | fun View.hideKeyboard() {
73 | val inputMethodManager =
74 | context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
75 | inputMethodManager.hideSoftInputFromWindow(windowToken, 0)
76 | }
77 |
78 | @CheckResult
79 | @OptIn(ExperimentalCoroutinesApi::class)
80 | fun View.clicks(): Flow = callbackFlow {
81 | checkMainThread()
82 | val listener = View.OnClickListener {
83 | trySend(Unit)
84 | }
85 | setOnClickListener(listener)
86 | awaitClose { setOnClickListener(null) }
87 | }.conflate()
88 |
89 | fun checkMainThread() {
90 | check(Looper.myLooper() == Looper.getMainLooper()) {
91 | "Expected to be called on the main thread but was " + Thread.currentThread().name
92 | }
93 | }
94 |
95 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/di/AppModuleProvider.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.di
2 |
3 | import android.content.ContentResolver
4 | import android.content.Context
5 | import com.jaydev.github.di.loader.ResourcesProvider
6 | import com.jaydev.github.di.loader.ResourcesProviderImpl
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.android.qualifiers.ApplicationContext
11 | import dagger.hilt.components.SingletonComponent
12 | import javax.inject.Singleton
13 |
14 | @InstallIn(SingletonComponent::class)
15 | @Module
16 | object AppModuleProvider {
17 |
18 | @Provides
19 | @Singleton
20 | fun provideContentResolver(
21 | @ApplicationContext context: Context
22 | ): ContentResolver {
23 | return context.contentResolver
24 | }
25 |
26 | @Provides
27 | @Singleton
28 | fun provideResourceProvider(
29 | @ApplicationContext context: Context
30 | ): ResourcesProvider {
31 | return ResourcesProviderImpl(context)
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/di/DataSourceModule.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.di
2 |
3 | import com.jaydev.github.model.source.GithubRemote
4 | import com.jaydev.github.remote.GithubRemoteImpl
5 | import com.jaydev.github.remote.GithubService
6 | import com.jaydev.github.remote.mapper.ForkEntityMapper
7 | import com.jaydev.github.remote.mapper.RepoEntityMapper
8 | import com.jaydev.github.remote.mapper.UserEntityMapper
9 | import dagger.Module
10 | import dagger.Provides
11 | import dagger.hilt.InstallIn
12 | import dagger.hilt.components.SingletonComponent
13 | import javax.inject.Singleton
14 |
15 | @InstallIn(SingletonComponent::class)
16 | @Module
17 | object DataSourceModule {
18 | @Provides
19 | @Singleton
20 | fun provideGithubRemote(
21 | githubService: GithubService,
22 | userEntityMapper: UserEntityMapper,
23 | repoEntityMapper: RepoEntityMapper,
24 | forkEntityMapper: ForkEntityMapper
25 | ): GithubRemote {
26 | return GithubRemoteImpl(
27 | githubService,
28 | userEntityMapper,
29 | repoEntityMapper,
30 | forkEntityMapper
31 | )
32 | }
33 |
34 | @Provides
35 | @Singleton
36 | fun provideRepoEntityMapper() = RepoEntityMapper()
37 |
38 | @Provides
39 | @Singleton
40 | fun provideUserEntityMapper() = UserEntityMapper()
41 |
42 | @Provides
43 | @Singleton
44 | fun provideForkEntityMapper(userEntityMapper: UserEntityMapper) =
45 | ForkEntityMapper(userEntityMapper)
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/di/DomainModule.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.di
2 |
3 | import com.jaydev.github.domain.interactor.ErrorHandler
4 | import com.jaydev.github.domain.repository.GithubRepository
5 | import com.jaydev.github.model.interactor.GithubDataRepository
6 | import com.jaydev.github.model.interactor.NetErrorHandlerImpl
7 | import com.jaydev.github.model.source.GithubRemote
8 | import dagger.Module
9 | import dagger.Provides
10 | import dagger.hilt.InstallIn
11 | import dagger.hilt.components.SingletonComponent
12 | import retrofit2.Retrofit
13 | import javax.inject.Singleton
14 |
15 | @InstallIn(SingletonComponent::class)
16 | @Module
17 | object DomainModule {
18 |
19 | @Provides
20 | @Singleton
21 | fun provideGithubRepository(githubRemote: GithubRemote): GithubRepository {
22 | return GithubDataRepository(githubRemote)
23 | }
24 |
25 | @Provides
26 | @Singleton
27 | fun provideErrorHandler(retrofit: Retrofit): ErrorHandler {
28 | return NetErrorHandlerImpl(retrofit)
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.di
2 |
3 | import android.content.Context
4 | import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
5 | import com.jaydev.github.network.NetworkConnectionInterceptor
6 | import com.orhanobut.logger.Logger
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.android.qualifiers.ApplicationContext
11 | import dagger.hilt.components.SingletonComponent
12 | import kotlinx.serialization.SerializationException
13 | import kotlinx.serialization.json.Json
14 | import kotlinx.serialization.json.JsonElement
15 | import okhttp3.MediaType.Companion.toMediaType
16 | import okhttp3.OkHttpClient
17 | import okhttp3.logging.HttpLoggingInterceptor
18 | import retrofit2.Retrofit
19 | import java.util.concurrent.TimeUnit
20 | import javax.inject.Singleton
21 |
22 | @InstallIn(SingletonComponent::class)
23 | @Module
24 | object NetworkModule {
25 | private const val TAG = "OkHttp"
26 | private const val connectTimeout = 20L
27 | private const val writeTimeout = 20L
28 | private const val readTimeout = 20L
29 |
30 | @Provides
31 | @Singleton
32 | fun provideNetworkConnectionInterceptor(@ApplicationContext context: Context): NetworkConnectionInterceptor {
33 | return NetworkConnectionInterceptor(context)
34 | }
35 |
36 | @Provides
37 | @Singleton
38 | fun provideLoggingInterceptor(): HttpLoggingInterceptor {
39 | return HttpLoggingInterceptor(PrettyPrintLogger()).apply {
40 | level = HttpLoggingInterceptor.Level.BODY
41 | }
42 | }
43 |
44 | @Provides
45 | @Singleton
46 | fun provideOkHttpClient(
47 | networkConnectionInterceptor: NetworkConnectionInterceptor,
48 | loggingInterceptor: HttpLoggingInterceptor,
49 | ): OkHttpClient {
50 | return OkHttpClient.Builder()
51 | .addInterceptor(networkConnectionInterceptor)
52 | .addInterceptor(loggingInterceptor)
53 | .connectTimeout(connectTimeout, TimeUnit.SECONDS)
54 | .writeTimeout(writeTimeout, TimeUnit.SECONDS)
55 | .readTimeout(readTimeout, TimeUnit.SECONDS)
56 | .build()
57 | }
58 |
59 | @Provides
60 | @Singleton
61 | fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
62 | val contentType = "application/json".toMediaType()
63 | val json = Json {
64 | ignoreUnknownKeys = true
65 | prettyPrint = true
66 | isLenient = true
67 | }
68 |
69 | return Retrofit.Builder()
70 | .client(okHttpClient)
71 | .baseUrl("https://api.github.com")
72 | .addConverterFactory(json.asConverterFactory(contentType))
73 | .callFactory { okHttpClient.newCall(it) }
74 | .build()
75 | }
76 |
77 | private class PrettyPrintLogger : HttpLoggingInterceptor.Logger {
78 | override fun log(message: String) {
79 | Logger.t(TAG).run {
80 | try {
81 | val json = Json {
82 | prettyPrint = true
83 | ignoreUnknownKeys = true
84 | isLenient = true
85 | }
86 | json.decodeFromString(message)
87 | json(message)
88 | } catch (ignore: SerializationException) {
89 | i(message)
90 | } catch (ignore: IllegalArgumentException) {
91 | i(message)
92 | } catch (e: Exception) {
93 | e.printStackTrace()
94 | }
95 | }
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/di/RemoteModule.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.di
2 |
3 | import com.jaydev.github.remote.GithubService
4 | import dagger.Module
5 | import dagger.Provides
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.components.SingletonComponent
8 | import retrofit2.Retrofit
9 | import javax.inject.Singleton
10 |
11 | @InstallIn(SingletonComponent::class)
12 | @Module
13 | object RemoteModule {
14 | @Provides
15 | @Singleton
16 | fun provideGithubService(retrofit: Retrofit): GithubService {
17 | return retrofit.create(GithubService::class.java)
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/di/loader/CoroutineQualifiers.kt:
--------------------------------------------------------------------------------
1 | package kr.co.plat.carplat.biz.di
2 |
3 | import javax.inject.Qualifier
4 |
5 | @Retention(AnnotationRetention.RUNTIME)
6 | @Qualifier
7 | annotation class DefaultDispatcher
8 |
9 | @Retention(AnnotationRetention.RUNTIME)
10 | @Qualifier
11 | annotation class IoDispatcher
12 |
13 | @Retention(AnnotationRetention.RUNTIME)
14 | @Qualifier
15 | annotation class MainDispatcher
16 |
17 | @Retention(AnnotationRetention.BINARY)
18 | @Qualifier
19 | annotation class MainImmediateDispatcher
20 |
21 | @Retention(AnnotationRetention.RUNTIME)
22 | @Qualifier
23 | annotation class ApplicationScope
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/di/loader/CoroutineScopeModule.kt:
--------------------------------------------------------------------------------
1 | package kr.co.plat.carplat.biz.di
2 |
3 | import android.app.Activity
4 | import androidx.activity.ComponentActivity
5 | import androidx.lifecycle.LifecycleCoroutineScope
6 | import androidx.lifecycle.lifecycleScope
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.android.components.ActivityComponent
11 | import dagger.hilt.components.SingletonComponent
12 | import kotlinx.coroutines.CoroutineDispatcher
13 | import kotlinx.coroutines.CoroutineScope
14 | import kotlinx.coroutines.SupervisorJob
15 | import javax.inject.Singleton
16 |
17 | @Module
18 | @InstallIn(ActivityComponent::class)
19 | object ActivityScopeModule {
20 | @Provides
21 | fun provideLifecycleCoroutineScope(activity: Activity): LifecycleCoroutineScope =
22 | (activity as ComponentActivity).lifecycleScope
23 | }
24 |
25 | @Module
26 | @InstallIn(SingletonComponent::class)
27 | object CoroutinesScopesModule {
28 | @Singleton
29 | @ApplicationScope
30 | @Provides
31 | fun providesCoroutineScope(
32 | @DefaultDispatcher defaultDispatcher: CoroutineDispatcher
33 | ): CoroutineScope = CoroutineScope(SupervisorJob() + defaultDispatcher)
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/di/loader/ResourcesProvider.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.di.loader
2 |
3 | import android.content.Context
4 | import android.graphics.drawable.Drawable
5 | import androidx.annotation.ColorInt
6 | import androidx.annotation.ColorRes
7 | import androidx.annotation.DrawableRes
8 | import androidx.annotation.StringRes
9 |
10 | interface ResourcesProvider {
11 | fun getDrawable(@DrawableRes res: Int): Drawable?
12 |
13 | @ColorInt
14 | fun getColor(@ColorRes res: Int): Int
15 | fun getString(@StringRes res: Int): String
16 | fun getString(@StringRes resId: Int, vararg formatArgs: Any): String
17 |
18 | fun loadImage(url: String)
19 | suspend fun getDrawable(url: String?): Drawable?
20 | }
21 |
22 | class ResourcesProviderImpl(private val context: Context) : ResourcesProvider {
23 | override fun getDrawable(res: Int) = context.getDrawable(res)
24 |
25 | override fun getColor(res: Int) = context.resources.getColor(res)
26 |
27 | override fun getString(res: Int) = context.getString(res)
28 |
29 | override fun getString(@StringRes resId: Int, vararg formatArgs: Any) =
30 | context.getString(resId, *formatArgs)
31 |
32 | override fun loadImage(url: String) {
33 | TODO("Not yet implemented")
34 | }
35 |
36 | override suspend fun getDrawable(url: String?): Drawable? {
37 | TODO("Not yet implemented")
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/model/AlertUIModel.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.model
2 |
3 | sealed class AlertUIModel {
4 | data class Dialog(
5 | val title: String? = null,
6 | val message: CharSequence,
7 | val positiveButton: String? = null,
8 | val negativeButton: String? = null,
9 | val isButtonRed: Boolean = false
10 | ) : AlertUIModel()
11 |
12 | data class Toast(
13 | val message: String
14 | ) : AlertUIModel()
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/model/PopupMessage.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.model
2 |
3 | data class PopupMessage(
4 | val title: String,
5 | val message: String
6 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/model/RepoData.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.model
2 |
3 | data class RepoData(
4 | val userName: String,
5 | val repoName: String
6 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/model/SearchListItem.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.model
2 |
3 | import com.jaydev.github.domain.entity.Repo
4 | import com.jaydev.github.domain.entity.User
5 |
6 | sealed class SearchListItem {
7 | data class Header(val user: User) : SearchListItem() {
8 | companion object
9 | }
10 |
11 | data class RepoItem(val repo: Repo) : SearchListItem() {
12 | companion object
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/network/NetworkConnectionInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.network
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.NetworkCapabilities
6 | import com.jaydev.github.domain.interactor.NetworkConnectException
7 | import okhttp3.Interceptor
8 | import okhttp3.Response
9 | import java.io.IOException
10 | import javax.inject.Inject
11 |
12 | class NetworkConnectionInterceptor @Inject constructor(context: Context) : Interceptor {
13 | private val applicationContext = context.applicationContext
14 |
15 | private val isConnected: Boolean
16 | get() {
17 | val result: Boolean
18 | val connectivityManager =
19 | applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
20 | val activeNetwork = connectivityManager.activeNetwork ?: return false
21 | val capabilities =
22 | connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
23 | result = when {
24 | capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
25 | capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
26 | capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
27 | else -> false
28 | }
29 |
30 | return result
31 | }
32 |
33 | @Throws(IOException::class)
34 | override fun intercept(chain: Interceptor.Chain): Response {
35 | if (isConnected) {
36 | val newRequest = chain.request().newBuilder()
37 | return chain.proceed(newRequest.build())
38 | } else {
39 | throw NetworkConnectException()
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.SideEffect
13 | import androidx.compose.ui.graphics.toArgb
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.platform.LocalView
16 | import androidx.core.view.WindowCompat
17 |
18 | private val DarkColorScheme = darkColorScheme(
19 | primary = Purple80,
20 | secondary = PurpleGrey80,
21 | tertiary = Pink80
22 | )
23 |
24 | private val LightColorScheme = lightColorScheme(
25 | primary = Purple40,
26 | secondary = PurpleGrey40,
27 | tertiary = Pink40
28 |
29 | /* Other default colors to override
30 | background = Color(0xFFFFFBFE),
31 | surface = Color(0xFFFFFBFE),
32 | onPrimary = Color.White,
33 | onSecondary = Color.White,
34 | onTertiary = Color.White,
35 | onBackground = Color(0xFF1C1B1F),
36 | onSurface = Color(0xFF1C1B1F),
37 | */
38 | )
39 |
40 | @Composable
41 | fun GithubBrowserTheme(
42 | darkTheme: Boolean = isSystemInDarkTheme(),
43 | // Dynamic color is available on Android 12+
44 | dynamicColor: Boolean = true,
45 | content: @Composable () -> Unit
46 | ) {
47 | val colorScheme = when {
48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
49 | val context = LocalContext.current
50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
51 | }
52 |
53 | darkTheme -> DarkColorScheme
54 | else -> LightColorScheme
55 | }
56 | val view = LocalView.current
57 | if (!view.isInEditMode) {
58 | SideEffect {
59 | val window = (view.context as Activity).window
60 | window.statusBarColor = colorScheme.primary.toArgb()
61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
62 | }
63 | }
64 |
65 | MaterialTheme(
66 | colorScheme = colorScheme,
67 | typography = Typography,
68 | content = content
69 | )
70 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/view/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.view
2 |
3 | import android.os.Bundle
4 | import android.os.PersistableBundle
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.compose.setContent
7 | import com.jaydev.github.view.search.SearchScreen
8 |
9 | class MainActivity : ComponentActivity() {
10 |
11 | override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
12 | super.onCreate(savedInstanceState, persistentState)
13 | setContent {
14 | SearchScreen()
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/view/base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.view.base
2 |
3 | import android.app.AlertDialog
4 | import android.os.Bundle
5 | import android.os.PersistableBundle
6 | import android.widget.Toast
7 | import androidx.activity.ComponentActivity
8 | import androidx.activity.OnBackPressedCallback
9 |
10 | open class BaseActivity : ComponentActivity() {
11 |
12 | override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
13 | super.onCreate(savedInstanceState, persistentState)
14 | onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
15 | }
16 |
17 | private val onBackPressedCallback = object : OnBackPressedCallback(true) {
18 | override fun handleOnBackPressed() {
19 | finishAfterTransition()
20 | }
21 | }
22 |
23 | protected open fun showActionDialog(
24 | title: String?,
25 | message: CharSequence?,
26 | action: InvokableAction
27 | ) {
28 | AlertDialog.Builder(this)
29 | .setTitle(title)
30 | .setMessage(message)
31 | .setPositiveButton(
32 | android.R.string.ok
33 | ) { dialog, _ ->
34 | action.invoke()
35 | dialog.dismiss()
36 | }.show()
37 | }
38 |
39 | protected open fun showAlertDialog(title: String?, message: CharSequence?) {
40 | AlertDialog.Builder(this)
41 | .setTitle(title)
42 | .setMessage(message)
43 | .setPositiveButton(
44 | android.R.string.ok
45 | ) { dialog, _ ->
46 | dialog.dismiss()
47 | }.show()
48 | }
49 |
50 | protected fun showToast(message: String) {
51 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
52 | }
53 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/view/base/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.view.base
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.jaydev.github.domain.NetResult
6 | import com.jaydev.github.domain.entity.NetError
7 | import com.jaydev.github.model.AlertUIModel
8 | import com.orhanobut.logger.Logger
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.MutableSharedFlow
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.flow.asSharedFlow
13 | import kotlinx.coroutines.flow.asStateFlow
14 | import kotlinx.coroutines.flow.cancellable
15 | import kotlinx.coroutines.flow.launchIn
16 | import kotlinx.coroutines.flow.onCompletion
17 | import kotlinx.coroutines.flow.onEach
18 | import kotlinx.coroutines.flow.onStart
19 | import kotlinx.coroutines.launch
20 |
21 | typealias InvokableAction = () -> Unit
22 |
23 | abstract class BaseViewModel : ViewModel() {
24 | private val loadingProgress = ContentLoadingProgress(viewModelScope)
25 |
26 | private val _loading = MutableStateFlow(false)
27 | val loading = _loading.asStateFlow()
28 |
29 | private val _showToast = MutableSharedFlow()
30 | val showToast = _showToast.asSharedFlow()
31 |
32 | private val _showAlertDialog = MutableSharedFlow()
33 | val showAlertDialog = _showAlertDialog.asSharedFlow()
34 |
35 | private val _showActionDialog = MutableSharedFlow>()
36 | val showActionDialog = _showActionDialog.asSharedFlow()
37 |
38 | private val _navigateToBack = MutableSharedFlow()
39 | val navigateToBack = _navigateToBack.asSharedFlow()
40 |
41 | protected fun navigateToBack() {
42 | viewModelScope.launch {
43 | _navigateToBack.emit(Unit)
44 | }
45 | }
46 |
47 | protected fun showAlert(alert: AlertUIModel, action: InvokableAction? = null) {
48 | when (alert) {
49 | is AlertUIModel.Dialog -> {
50 | if (action != null) showActionDialog(alert, action)
51 | else showAlertDialog(alert)
52 | }
53 |
54 | is AlertUIModel.Toast -> {
55 | showToast(alert.message)
56 | }
57 | }
58 | }
59 |
60 | protected fun showToast(message: String) {
61 | viewModelScope.launch {
62 | _showToast.emit(message)
63 | }
64 | }
65 |
66 | protected fun showActionDialog(alertDialog: AlertUIModel.Dialog, action: InvokableAction) {
67 | viewModelScope.launch {
68 | _showActionDialog.emit(Pair(alertDialog, action))
69 | }
70 | }
71 |
72 | protected fun showAlertDialog(alertDialog: AlertUIModel.Dialog) {
73 | viewModelScope.launch {
74 | _showAlertDialog.emit(alertDialog)
75 | }
76 | }
77 |
78 | protected fun showAlertDialog(title: String, message: CharSequence) {
79 | viewModelScope.launch {
80 | _showAlertDialog.emit(AlertUIModel.Dialog(title, message))
81 | }
82 | }
83 |
84 | fun progress(isLoading: Boolean) {
85 | _loading.value = isLoading
86 | }
87 |
88 | protected fun Flow>.onSuccess(
89 | success: (suspend (T) -> Unit)? = null
90 | ) = onEach {
91 | if (it is NetResult.Success) success?.invoke(it.response)
92 | }
93 |
94 | protected fun Flow>.onFailure(
95 | failure: (suspend (NetError.BadRequest) -> Unit)? = null
96 | ) = onEach {
97 | if (it is NetResult.Error && it.error is NetError.BadRequest) {
98 | failure?.invoke(it.error as NetError.BadRequest)
99 | }
100 | }
101 |
102 | protected fun Flow>.commonErrorHandler(
103 | action: InvokableAction = this@BaseViewModel::navigateToBack
104 | ) = onEach {
105 | handleError(it, action)
106 | }
107 |
108 | private fun handleError(result: NetResult<*>, action: InvokableAction) {
109 | if (result !is NetResult.Error) return
110 | viewModelScope.launch {
111 | when (result.error) {
112 | NetError.Network -> {
113 | val title = "네트워크 에러"
114 | val message = "인터넷 연결 안됨."
115 |
116 | _showActionDialog.emit(Pair(AlertUIModel.Dialog(title, message), action))
117 | }
118 |
119 | is NetError.InternalServer -> {
120 | val (errorCode, errorMessage) = result.error as NetError.InternalServer
121 |
122 | val title = "네트워크 에러"
123 | val message = "서버 에러\n" +
124 | "code: $errorCode\n" +
125 | "message: $errorMessage"
126 | _showActionDialog.emit(Pair(AlertUIModel.Dialog(title, message), action))
127 | }
128 |
129 | is NetError.Timeout -> {
130 | val title = "네트워크 에러"
131 | val message = "타임 아웃."
132 | _showActionDialog.emit(Pair(AlertUIModel.Dialog(title, message), action))
133 | }
134 |
135 | is NetError.Unknown -> {
136 | val title = "네트워크 에러"
137 | val message = "알 수 없는 에러."
138 | _showAlertDialog.emit(AlertUIModel.Dialog(title, message))
139 |
140 | val throwable = (result.error as NetError.Unknown).throwable
141 | Logger.e(throwable, "Unknown Error")
142 | }
143 |
144 | is NetError.BadRequest -> {
145 | // no-op
146 | }
147 | }
148 | }
149 | }
150 |
151 | protected fun Flow>.call() = cancellable().launchIn(viewModelScope)
152 |
153 | protected fun Flow>.load(
154 | loading: (Boolean) -> Unit
155 | ) = onStart {
156 | loading.invoke(true)
157 | }.onCompletion {
158 | loading.invoke(false)
159 | }.cancellable()
160 | .launchIn(viewModelScope)
161 |
162 | protected fun Flow>.load(
163 | loading: MutableStateFlow = _loading
164 | ) = onStart {
165 | loadingProgress.handleContentLoading(loading, true)
166 | }.onCompletion {
167 | loadingProgress.handleContentLoading(loading, false)
168 | }.cancellable()
169 | .launchIn(viewModelScope)
170 | }
171 |
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/view/base/ContentLoadingProgress.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.view.base
2 |
3 | import com.jaydev.github.common.cancelIfActive
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.Job
6 | import kotlinx.coroutines.delay
7 | import kotlinx.coroutines.flow.MutableStateFlow
8 | import kotlinx.coroutines.launch
9 |
10 | class ContentLoadingProgress(
11 | val coroutineScope: CoroutineScope
12 | ) {
13 | companion object {
14 | private const val MIN_LOADING_TIME = 1000L
15 | private const val MIN_LOADING_DELAY = 500L
16 | }
17 |
18 | private var postedLoadingHide = false
19 | private var postedLoadingShow = false
20 | private var loadingDismissed = false
21 | private var startTime: Long = -1
22 |
23 | private var delayedHideJob: Job? = null
24 | private var delayedShowJob: Job? = null
25 |
26 | suspend fun handleContentLoading(
27 | loading: MutableStateFlow,
28 | isLoading: Boolean
29 | ) {
30 | if (isLoading) showProgress(loading) else hideProgress(loading)
31 | }
32 |
33 | suspend fun showProgress(loading: MutableStateFlow) {
34 | startTime = -1
35 | loadingDismissed = false
36 | delayedHideJob?.cancelIfActive()
37 | postedLoadingHide = false
38 | if (!postedLoadingShow) {
39 | delayedShowJob?.cancelIfActive()
40 | delayedShowJob = coroutineScope.launch {
41 | delay(MIN_LOADING_DELAY)
42 | postedLoadingShow = false
43 | if (!loadingDismissed) {
44 | startTime = System.currentTimeMillis()
45 | loading.value = true
46 | }
47 | }
48 | postedLoadingShow = true
49 | }
50 | }
51 |
52 | suspend fun hideProgress(loading: MutableStateFlow) {
53 | loadingDismissed = true
54 | delayedShowJob?.cancelIfActive()
55 | postedLoadingShow = false
56 | val diff: Long = System.currentTimeMillis() - startTime
57 | if (diff >= MIN_LOADING_TIME || startTime == -1L) {
58 | loading.value = false
59 | } else {
60 | if (!postedLoadingHide) {
61 | delayedHideJob?.cancelIfActive()
62 | delayedHideJob = coroutineScope.launch {
63 | delay(MIN_LOADING_TIME - diff)
64 | postedLoadingHide = false
65 | startTime = -1
66 | loading.value = false
67 | }
68 | postedLoadingHide = true
69 | }
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/view/repo/RepoDetailActivity.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.view.repo
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import androidx.activity.compose.setContent
7 | import androidx.activity.viewModels
8 | import androidx.lifecycle.lifecycleScope
9 | import com.jaydev.github.view.base.BaseActivity
10 | import dagger.hilt.android.AndroidEntryPoint
11 | import kotlinx.coroutines.flow.launchIn
12 | import kotlinx.coroutines.flow.onEach
13 |
14 | @AndroidEntryPoint
15 | class RepoDetailActivity : BaseActivity() {
16 | private val viewModel by viewModels()
17 |
18 | override fun onCreate(savedInstanceState: Bundle?) {
19 | super.onCreate(savedInstanceState)
20 | setContent {
21 | RepoDetailScreen(viewModel = viewModel)
22 | }
23 |
24 | subscribeUI(viewModel)
25 | }
26 |
27 | private fun subscribeUI(viewModel: RepoDetailViewModel) {
28 | with(viewModel) {
29 |
30 | showToast.onEach {
31 | showToast(it)
32 | }.launchIn(lifecycleScope)
33 |
34 | showAlertDialog.onEach {
35 | showAlertDialog(it.title, it.message)
36 | }.launchIn(lifecycleScope)
37 |
38 | showActionDialog.onEach {
39 | showActionDialog(it.first.title, it.first.message, it.second)
40 | }.launchIn(lifecycleScope)
41 |
42 | navigateToBack.onEach {
43 | onBackPressedDispatcher.onBackPressed()
44 | }.launchIn(lifecycleScope)
45 | }
46 | }
47 |
48 |
49 | companion object {
50 | private const val EXTRA_USER_NAME = "userName"
51 | private const val EXTRA_REPO_NAME = "repoName"
52 |
53 | fun start(context: Context, userName: String, repoName: String) {
54 | context.startActivity(
55 | Intent(context, RepoDetailActivity::class.java).apply {
56 | putExtra(EXTRA_USER_NAME, userName)
57 | putExtra(EXTRA_REPO_NAME, repoName)
58 | }
59 | )
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/view/repo/RepoDetailScreen.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.view.repo
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.ExperimentalLayoutApi
5 | import androidx.compose.foundation.layout.WindowInsets
6 | import androidx.compose.foundation.layout.WindowInsetsSides
7 | import androidx.compose.foundation.layout.consumeWindowInsets
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.only
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.safeDrawing
12 | import androidx.compose.foundation.layout.size
13 | import androidx.compose.foundation.layout.windowInsetsPadding
14 | import androidx.compose.foundation.layout.wrapContentHeight
15 | import androidx.compose.foundation.lazy.LazyColumn
16 | import androidx.compose.foundation.lazy.items
17 | import androidx.compose.material3.Card
18 | import androidx.compose.material3.ExperimentalMaterial3Api
19 | import androidx.compose.material3.MaterialTheme
20 | import androidx.compose.material3.Scaffold
21 | import androidx.compose.material3.Text
22 | import androidx.compose.material3.TopAppBar
23 | import androidx.compose.material3.TopAppBarDefaults
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.runtime.collectAsState
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.text.font.FontWeight
28 | import androidx.compose.ui.unit.dp
29 | import androidx.constraintlayout.compose.ConstraintLayout
30 | import coil.compose.AsyncImage
31 | import com.jaydev.github.domain.entity.Fork
32 | import com.jaydev.github.ui.theme.GithubBrowserTheme
33 |
34 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
35 | @Composable
36 | fun RepoDetailScreen(viewModel: RepoDetailViewModel) {
37 | GithubBrowserTheme {
38 | Scaffold(
39 | topBar = {
40 |
41 | val title = viewModel.title.collectAsState().value
42 | TopAppBar(
43 | title = {
44 | Text(text = title)
45 | },
46 | colors = TopAppBarDefaults
47 | .topAppBarColors(containerColor = MaterialTheme.colorScheme.primary)
48 | )
49 | }
50 | ) { padding ->
51 | Card(
52 | modifier = Modifier
53 | .padding(padding)
54 | .consumeWindowInsets(padding)
55 | .windowInsetsPadding(
56 | WindowInsets.safeDrawing.only(
57 | WindowInsetsSides.Horizontal,
58 | ),
59 | )
60 | .padding(16.dp)
61 | ) {
62 | ConstraintLayout(
63 | modifier = Modifier
64 | .padding(16.dp)
65 | .fillMaxWidth()
66 | .wrapContentHeight()
67 | ) {
68 | val (repoName, description, starCount, forks) = createRefs()
69 |
70 | Text(
71 | text = viewModel.repoName.collectAsState().value,
72 | fontWeight = FontWeight.Bold,
73 | modifier = Modifier.constrainAs(repoName) {
74 | top.linkTo(parent.top)
75 | start.linkTo(parent.start)
76 | }
77 | )
78 |
79 | Text(
80 | text = viewModel.description.collectAsState().value,
81 | modifier = Modifier.constrainAs(description) {
82 | top.linkTo(repoName.bottom, 8.dp)
83 | start.linkTo(parent.start)
84 | }
85 | )
86 |
87 | Text(
88 | text = viewModel.starCount.collectAsState().value,
89 | color = MaterialTheme.colorScheme.secondary,
90 | modifier = Modifier.constrainAs(starCount) {
91 | top.linkTo(parent.top)
92 | end.linkTo(parent.end)
93 | }
94 | )
95 |
96 | val lists = viewModel.refreshForks.collectAsState().value
97 | LazyColumn(
98 | modifier = Modifier.constrainAs(forks) {
99 | top.linkTo(description.bottom, 8.dp)
100 | start.linkTo(parent.start)
101 | end.linkTo(parent.end)
102 | bottom.linkTo(parent.bottom)
103 | },
104 | verticalArrangement = Arrangement.spacedBy(8.dp)
105 | ) {
106 | items(lists) { item ->
107 | ForkItem(item)
108 | }
109 | }
110 | }
111 | }
112 | }
113 | }
114 | }
115 |
116 | @Composable
117 | private fun ForkItem(item: Fork) {
118 | ConstraintLayout(modifier = Modifier.padding(16.dp)) {
119 | val (forkName, userName, profileImage) = createRefs()
120 |
121 | Text(
122 | text = item.fullName,
123 | modifier = Modifier
124 | .constrainAs(forkName) {
125 | top.linkTo(parent.top)
126 | start.linkTo(parent.start)
127 | end.linkTo(parent.end)
128 | }
129 | )
130 | AsyncImage(
131 | model = item.owner.profileImageUrl,
132 | contentDescription = "User Profile Image",
133 | modifier = Modifier
134 | .size(32.dp)
135 | .constrainAs(profileImage) {
136 | top.linkTo(forkName.bottom)
137 | start.linkTo(parent.start)
138 | }
139 | )
140 | Text(
141 | text = item.name,
142 | modifier = Modifier
143 | .constrainAs(userName) {
144 | top.linkTo(forkName.bottom)
145 | start.linkTo(profileImage.end)
146 | }
147 | )
148 | }
149 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/view/repo/RepoDetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.view.repo
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import com.jaydev.github.domain.entity.Fork
5 | import com.jaydev.github.domain.interactor.usecase.GetRepoDetailUseCase
6 | import com.jaydev.github.model.AlertUIModel
7 | import com.jaydev.github.view.base.BaseViewModel
8 | import dagger.hilt.android.lifecycle.HiltViewModel
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.flow.asStateFlow
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | class RepoDetailViewModel @Inject constructor(
15 | handle: SavedStateHandle,
16 | getRepoDetail: GetRepoDetailUseCase
17 | ) : BaseViewModel() {
18 |
19 |
20 | private val _title = MutableStateFlow("")
21 | val title = _title.asStateFlow()
22 |
23 | private val _description = MutableStateFlow("")
24 | val description = _description.asStateFlow()
25 |
26 | private val _starCount = MutableStateFlow("")
27 | val starCount = _starCount.asStateFlow()
28 |
29 | private val _repoName = MutableStateFlow("")
30 | val repoName = _repoName.asStateFlow()
31 |
32 | private val _refreshForks = MutableStateFlow>(emptyList())
33 | val refreshForks = _refreshForks.asStateFlow()
34 |
35 | init {
36 | val userName = handle.get("userName") ?: ""
37 | val repoName = handle.get("repoName") ?: ""
38 |
39 | getRepoDetail(GetRepoDetailUseCase.Params(userName, repoName))
40 | .onSuccess {
41 | _title.value = userName
42 | _repoName.value = it.first.name
43 | _description.value = it.first.description
44 | _starCount.value = it.first.starCount
45 | _refreshForks.value = it.second
46 | }.onFailure {
47 | showAlertDialog(AlertUIModel.Dialog("통신 실패", it.message))
48 | }.commonErrorHandler()
49 | .call()
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/view/result/SearchResultActivity.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.view.result
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import androidx.activity.compose.setContent
7 | import androidx.activity.viewModels
8 | import androidx.core.os.bundleOf
9 | import androidx.lifecycle.DEFAULT_ARGS_KEY
10 | import androidx.lifecycle.lifecycleScope
11 | import androidx.lifecycle.viewmodel.MutableCreationExtras
12 | import com.jaydev.github.view.base.BaseActivity
13 | import com.jaydev.github.view.repo.RepoDetailActivity
14 | import dagger.hilt.android.AndroidEntryPoint
15 | import kotlinx.coroutines.flow.launchIn
16 | import kotlinx.coroutines.flow.onEach
17 |
18 | @AndroidEntryPoint
19 | class SearchResultActivity : BaseActivity() {
20 | private val viewModel by viewModels(
21 | extrasProducer = {
22 | val extras = MutableCreationExtras(defaultViewModelCreationExtras)
23 | intent?.data?.path?.substring(1)?.let { queryParamId ->
24 | extras[DEFAULT_ARGS_KEY] = bundleOf("userName" to queryParamId)
25 | }
26 | extras
27 | }
28 | )
29 |
30 | override fun onCreate(savedInstanceState: Bundle?) {
31 | super.onCreate(savedInstanceState)
32 | setContent {
33 | SearchResultScreen(viewModel = viewModel)
34 | }
35 |
36 | subscribeUI(viewModel)
37 | }
38 |
39 | private fun subscribeUI(viewModel: SearchResultViewModel) {
40 | with(viewModel) {
41 | navigateProfile.onEach {
42 | val uri = Uri.parse("githubbrowser://repos/$it")
43 | startActivity(Intent(Intent.ACTION_VIEW, uri))
44 | }.launchIn(lifecycleScope)
45 |
46 | navigateRepoDetail.onEach {
47 | RepoDetailActivity.start(this@SearchResultActivity, it.userName, it.repoName)
48 | }.launchIn(lifecycleScope)
49 |
50 | showToast.onEach {
51 | showToast(it)
52 | }.launchIn(lifecycleScope)
53 |
54 | showAlertDialog.onEach {
55 | showAlertDialog(it.title, it.message)
56 | }.launchIn(lifecycleScope)
57 |
58 | showActionDialog.onEach {
59 | showActionDialog(it.first.title, it.first.message, it.second)
60 | }.launchIn(lifecycleScope)
61 |
62 | navigateToBack.onEach {
63 | onBackPressedDispatcher.onBackPressed()
64 | }.launchIn(lifecycleScope)
65 | }
66 | }
67 |
68 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/view/result/SearchResultScreen.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.view.result
2 |
3 | import androidx.compose.animation.Crossfade
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.ExperimentalLayoutApi
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.WindowInsets
10 | import androidx.compose.foundation.layout.WindowInsetsSides
11 | import androidx.compose.foundation.layout.consumeWindowInsets
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.only
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.foundation.layout.safeDrawing
16 | import androidx.compose.foundation.layout.size
17 | import androidx.compose.foundation.layout.windowInsetsPadding
18 | import androidx.compose.foundation.layout.wrapContentHeight
19 | import androidx.compose.foundation.lazy.LazyColumn
20 | import androidx.compose.foundation.lazy.items
21 | import androidx.compose.foundation.shape.RoundedCornerShape
22 | import androidx.compose.material3.Card
23 | import androidx.compose.material3.CircularProgressIndicator
24 | import androidx.compose.material3.ExperimentalMaterial3Api
25 | import androidx.compose.material3.MaterialTheme
26 | import androidx.compose.material3.Scaffold
27 | import androidx.compose.material3.Text
28 | import androidx.compose.material3.TopAppBar
29 | import androidx.compose.material3.TopAppBarDefaults
30 | import androidx.compose.runtime.Composable
31 | import androidx.compose.runtime.collectAsState
32 | import androidx.compose.ui.Modifier
33 | import androidx.compose.ui.draw.clip
34 | import androidx.compose.ui.text.font.FontWeight
35 | import androidx.compose.ui.unit.dp
36 | import androidx.constraintlayout.compose.ConstraintLayout
37 | import coil.compose.AsyncImage
38 | import com.jaydev.github.domain.entity.Repo
39 | import com.jaydev.github.domain.entity.User
40 | import com.jaydev.github.model.SearchListItem
41 | import com.jaydev.github.ui.theme.GithubBrowserTheme
42 |
43 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
44 | @Composable
45 | fun SearchResultScreen(viewModel: SearchResultViewModel) {
46 | GithubBrowserTheme {
47 | Scaffold(
48 | topBar = {
49 | TopAppBar(
50 | title = {
51 | Text(text = "SearchList")
52 | },
53 | colors = TopAppBarDefaults
54 | .topAppBarColors(containerColor = MaterialTheme.colorScheme.primary)
55 | )
56 | },
57 |
58 | ) { padding ->
59 | val lists = viewModel.refreshListData.collectAsState().value
60 | val isLoading = viewModel.loading.collectAsState().value
61 |
62 | Crossfade(targetState = isLoading, label = "loading") {
63 | if (it) {
64 | CircularProgressIndicator()
65 | } else {
66 | LazyColumn(
67 | modifier = Modifier
68 | .padding(padding)
69 | .consumeWindowInsets(padding)
70 | .windowInsetsPadding(
71 | WindowInsets.safeDrawing.only(
72 | WindowInsetsSides.Horizontal,
73 | ),
74 | )
75 | .padding(16.dp),
76 | verticalArrangement = Arrangement.spacedBy(8.dp)
77 | ) {
78 | items(lists) { item ->
79 | when (item) {
80 | is SearchListItem.Header -> UserInfoItem(
81 | item,
82 | viewModel::onClickUser
83 | )
84 |
85 | is SearchListItem.RepoItem -> UserRepoItem(
86 | item,
87 | viewModel::onClickRepo
88 | )
89 | }
90 | }
91 | }
92 | }
93 | }
94 | }
95 | }
96 | }
97 |
98 | @Composable
99 | private fun UserInfoItem(
100 | item: SearchListItem.Header,
101 | onClick: (User) -> Unit
102 | ) {
103 | Row(Modifier.clickable {
104 | onClick.invoke(item.user)
105 | }) {
106 | AsyncImage(
107 | model = item.user.profileImageUrl,
108 | contentDescription = "User Profile Image",
109 | modifier = Modifier
110 | .clip(RoundedCornerShape(8.dp))
111 | )
112 | Spacer(modifier = Modifier.size(20.dp))
113 | Text(text = item.user.name)
114 | }
115 | }
116 |
117 | @OptIn(ExperimentalMaterial3Api::class)
118 | @Composable
119 | private fun UserRepoItem(
120 | item: SearchListItem.RepoItem,
121 | onClick: (Repo) -> Unit
122 | ) {
123 | Card(
124 | onClick = {
125 | onClick.invoke(item.repo)
126 | },
127 | modifier = Modifier
128 | .fillMaxWidth()
129 | .wrapContentHeight()
130 | ) {
131 | ConstraintLayout(
132 | modifier = Modifier
133 | .padding(16.dp)
134 | .fillMaxWidth()
135 | .wrapContentHeight()
136 | ) {
137 | val (repoName, description, starCount) = createRefs()
138 |
139 | Text(
140 | text = item.repo.name,
141 | fontWeight = FontWeight.Bold,
142 | modifier = Modifier.constrainAs(repoName) {
143 | top.linkTo(parent.top)
144 | start.linkTo(parent.start)
145 | }
146 | )
147 |
148 | Text(
149 | text = item.repo.description,
150 | modifier = Modifier.constrainAs(description) {
151 | top.linkTo(repoName.bottom)
152 | start.linkTo(parent.start)
153 | }
154 | )
155 |
156 | Text(
157 | text = item.repo.starCount,
158 | color = MaterialTheme.colorScheme.secondary,
159 | modifier = Modifier.constrainAs(starCount) {
160 | top.linkTo(parent.top)
161 | end.linkTo(parent.end)
162 | }
163 | )
164 | }
165 | }
166 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/view/result/SearchResultViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.view.result
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.lifecycle.viewModelScope
5 | import com.jaydev.github.domain.entity.Repo
6 | import com.jaydev.github.domain.entity.User
7 | import com.jaydev.github.domain.interactor.usecase.GetUserDataUseCase
8 | import com.jaydev.github.model.AlertUIModel
9 | import com.jaydev.github.model.RepoData
10 | import com.jaydev.github.model.SearchListItem
11 | import com.jaydev.github.view.base.BaseViewModel
12 | import dagger.hilt.android.lifecycle.HiltViewModel
13 | import kotlinx.coroutines.flow.MutableSharedFlow
14 | import kotlinx.coroutines.flow.MutableStateFlow
15 | import kotlinx.coroutines.flow.asSharedFlow
16 | import kotlinx.coroutines.flow.asStateFlow
17 | import kotlinx.coroutines.launch
18 | import javax.inject.Inject
19 |
20 | @HiltViewModel
21 | class SearchResultViewModel @Inject constructor(
22 | handle: SavedStateHandle,
23 | getUserData: GetUserDataUseCase
24 | ) : BaseViewModel() {
25 | private val _refreshListData = MutableStateFlow>(emptyList())
26 | val refreshListData = _refreshListData.asStateFlow()
27 |
28 | private val _navigateProfile = MutableSharedFlow()
29 | val navigateProfile = _navigateProfile.asSharedFlow()
30 |
31 | private val _navigateRepoDetail = MutableSharedFlow()
32 | val navigateRepoDetail = _navigateRepoDetail.asSharedFlow()
33 |
34 | init {
35 | val userName = handle.get("userName") ?: ""
36 |
37 | getUserData(GetUserDataUseCase.Params(userName))
38 | .onSuccess {
39 | val list = mutableListOf()
40 | list.add(SearchListItem.Header(it.first))
41 | list.addAll(
42 | it.second.map { repo ->
43 | SearchListItem.RepoItem(repo)
44 | }
45 | )
46 | _refreshListData.value = list
47 | }.onFailure {
48 | showAlertDialog(AlertUIModel.Dialog("통신 실패", it.message))
49 | }.commonErrorHandler()
50 | .load()
51 | }
52 |
53 |
54 | fun onClickUser(user: User) {
55 | viewModelScope.launch {
56 | _navigateProfile.emit(user.name)
57 | }
58 | }
59 |
60 | fun onClickRepo(repo: Repo) {
61 | viewModelScope.launch {
62 | val header =
63 | refreshListData.value.firstOrNull() as? SearchListItem.Header ?: return@launch
64 | _navigateRepoDetail.emit(RepoData(header.user.name, repo.name))
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/view/search/SearchActivity.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.view.search
2 |
3 | import android.os.Bundle
4 | import androidx.activity.compose.setContent
5 | import com.jaydev.github.view.base.BaseActivity
6 | import dagger.hilt.android.AndroidEntryPoint
7 |
8 | @AndroidEntryPoint
9 | class SearchActivity : BaseActivity() {
10 | override fun onCreate(savedInstanceState: Bundle?) {
11 | super.onCreate(savedInstanceState)
12 |
13 | setContent {
14 | SearchScreen()
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/jaydev/github/view/search/SearchScreen.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.view.search
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.ExperimentalLayoutApi
7 | import androidx.compose.foundation.layout.WindowInsets
8 | import androidx.compose.foundation.layout.WindowInsetsSides
9 | import androidx.compose.foundation.layout.consumeWindowInsets
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.only
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.layout.safeDrawing
14 | import androidx.compose.foundation.layout.windowInsetsPadding
15 | import androidx.compose.material3.Button
16 | import androidx.compose.material3.ExperimentalMaterial3Api
17 | import androidx.compose.material3.MaterialTheme
18 | import androidx.compose.material3.Scaffold
19 | import androidx.compose.material3.Text
20 | import androidx.compose.material3.TextField
21 | import androidx.compose.material3.TopAppBar
22 | import androidx.compose.material3.TopAppBarDefaults
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.runtime.getValue
25 | import androidx.compose.runtime.mutableStateOf
26 | import androidx.compose.runtime.remember
27 | import androidx.compose.runtime.setValue
28 | import androidx.compose.ui.Modifier
29 | import androidx.compose.ui.platform.LocalContext
30 | import androidx.compose.ui.unit.dp
31 | import com.jaydev.github.ui.theme.GithubBrowserTheme
32 |
33 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
34 | @Composable
35 | fun SearchScreen() {
36 | GithubBrowserTheme {
37 | Scaffold(
38 | topBar = {
39 | TopAppBar(
40 | title = {
41 | Text(text = "Search")
42 | },
43 | colors = TopAppBarDefaults
44 | .topAppBarColors(containerColor = MaterialTheme.colorScheme.primary),
45 | )
46 | }
47 | ) { padding ->
48 | val context = LocalContext.current
49 | var text by remember { mutableStateOf("") }
50 |
51 | Column(
52 | modifier = Modifier
53 | .padding(padding)
54 | .consumeWindowInsets(padding)
55 | .windowInsetsPadding(
56 | WindowInsets.safeDrawing.only(
57 | WindowInsetsSides.Horizontal,
58 | ),
59 | )
60 | ) {
61 | TextField(
62 | value = text,
63 | onValueChange = { text = it },
64 | label = {
65 | Text(text = "User Name")
66 | },
67 | placeholder = {
68 | Text(text = "Input User Name")
69 | },
70 | modifier = Modifier
71 | .fillMaxWidth()
72 | .padding(16.dp)
73 | )
74 | Button(
75 | onClick = {
76 | val uri = Uri.parse("githubbrowser://repos/$text")
77 | // TODO navigate to SearchListScreen by using navController
78 | context.startActivity(Intent(Intent.ACTION_VIEW, uri))
79 | },
80 | modifier = Modifier
81 | .fillMaxWidth()
82 | .padding(16.dp)
83 | ) {
84 | Text(text = "Search")
85 | }
86 | }
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/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/JayDev-Lee/GithubBrowserSample/d15102b9dd36bc7c61189076ffca9e801feffdd9/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JayDev-Lee/GithubBrowserSample/d15102b9dd36bc7c61189076ffca9e801feffdd9/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JayDev-Lee/GithubBrowserSample/d15102b9dd36bc7c61189076ffca9e801feffdd9/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JayDev-Lee/GithubBrowserSample/d15102b9dd36bc7c61189076ffca9e801feffdd9/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JayDev-Lee/GithubBrowserSample/d15102b9dd36bc7c61189076ffca9e801feffdd9/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JayDev-Lee/GithubBrowserSample/d15102b9dd36bc7c61189076ffca9e801feffdd9/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JayDev-Lee/GithubBrowserSample/d15102b9dd36bc7c61189076ffca9e801feffdd9/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JayDev-Lee/GithubBrowserSample/d15102b9dd36bc7c61189076ffca9e801feffdd9/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JayDev-Lee/GithubBrowserSample/d15102b9dd36bc7c61189076ffca9e801feffdd9/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JayDev-Lee/GithubBrowserSample/d15102b9dd36bc7c61189076ffca9e801feffdd9/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 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | GithubBrowser
3 |
4 | 알림
5 | 네트워크가 연결되지 않았습니다. 연결 상태 확인 후 다시 시도해주세요.
6 | 서버의 응답이 늦어져 연결에 실패했습니다. 잠시 후 다시 시도해주세요.
7 | 서버에서 데이터를 가져오지 못했습니다. 잠시 후 다시 시도해주세요.
8 | 예상치 못한 문제가 발생했습니다. 잠시 후 다시 시도해주세요.
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
3 | plugins {
4 | alias(libs.plugins.androidApplication) apply false
5 | alias(libs.plugins.kotlinAndroid) apply false
6 | alias(libs.plugins.kotlinKapt) apply false
7 | alias(libs.plugins.kotlinSerialization) apply false
8 | alias(libs.plugins.hilt) apply false
9 | alias(libs.plugins.kotlinParcelize) apply false
10 | }
11 | true // Needed to make the Suppress annotation work for the plugins block
--------------------------------------------------------------------------------
/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/data/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("kotlin")
3 | alias(libs.plugins.kotlinSerialization)
4 | }
5 |
6 | dependencies {
7 | api(project(":domain"))
8 |
9 | implementation(libs.kotlin.stdlib)
10 | implementation(libs.kotlinx.coroutines.android)
11 | implementation(libs.kotlinx.serialization.json)
12 |
13 | implementation(libs.retrofit)
14 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/jaydev/github/model/interactor/GithubDataRepository.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.model.interactor
2 |
3 | import com.jaydev.github.domain.repository.GithubRepository
4 | import com.jaydev.github.model.source.GithubRemote
5 |
6 | class GithubDataRepository(
7 | private val remote: GithubRemote
8 | ) : GithubRepository {
9 | override fun getForks(userName: String, id: String) = remote.getForks(
10 | userName = userName,
11 | id = id
12 | )
13 |
14 | override fun getRepositories(userName: String) = remote.getRepositories(userName)
15 |
16 |
17 | override fun getRepository(userName: String, id: String) = remote.getRepository(
18 | userName = userName,
19 | id = id
20 | )
21 |
22 | override fun getUser(userName: String) = remote.getUser(userName)
23 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/jaydev/github/model/interactor/NetErrorHandlerImpl.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.model.interactor
2 |
3 | import com.jaydev.github.domain.entity.NetError
4 | import com.jaydev.github.domain.interactor.ErrorHandler
5 | import com.jaydev.github.domain.interactor.NetworkConnectException
6 | import kotlinx.serialization.Serializable
7 | import retrofit2.HttpException
8 | import retrofit2.Response
9 | import retrofit2.Retrofit
10 | import java.io.IOException
11 | import java.net.SocketTimeoutException
12 |
13 | class NetErrorHandlerImpl(private val retrofit: Retrofit) : ErrorHandler {
14 | override fun getError(cause: Throwable): NetError {
15 | return when (cause) {
16 | is SocketTimeoutException -> NetError.Timeout
17 | is HttpException -> {
18 | when (cause.code()) {
19 | in 500..599 -> {
20 | val response = cause.response()
21 | val (code, message) = extractErrorMessage(response)
22 | NetError.InternalServer(code, message)
23 | }
24 |
25 | in 400..499 -> {
26 | val response = cause.response()
27 | val (code, message) = extractErrorMessage(response)
28 | NetError.BadRequest(code, message)
29 | }
30 |
31 | else -> NetError.Unknown(cause)
32 | }
33 | }
34 |
35 | is NetworkConnectException, is IOException -> NetError.Network
36 | else -> NetError.Unknown(cause)
37 | }
38 | }
39 |
40 | private fun extractErrorMessage(response: Response<*>?): ErrorResponse {
41 | return try {
42 | val converter = retrofit.responseBodyConverter(
43 | ErrorResponse::class.java,
44 | arrayOfNulls(0)
45 | )
46 | val netResponse = converter.convert(response?.errorBody()!!)
47 | return ErrorResponse(
48 | response.code(),
49 | netResponse?.message ?: response.errorBody()?.string().orEmpty(),
50 | netResponse?.documentation_url.orEmpty()
51 | )
52 | } catch (e: Exception) {
53 | ErrorResponse(
54 | response?.code()!!,
55 | response.errorBody()?.string().orEmpty(),
56 | ""
57 | )
58 | }
59 | }
60 |
61 | @Serializable
62 | private data class ErrorResponse(
63 | val code: Int,
64 | val message: String,
65 | val documentation_url: String
66 | )
67 | }
68 |
69 |
70 |
--------------------------------------------------------------------------------
/data/src/main/java/com/jaydev/github/model/source/GithubRemote.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.model.source
2 |
3 | import com.jaydev.github.domain.entity.Fork
4 | import com.jaydev.github.domain.entity.Repo
5 | import com.jaydev.github.domain.entity.User
6 | import kotlinx.coroutines.flow.Flow
7 |
8 | interface GithubRemote {
9 | fun getForks(userName: String, id: String): Flow>
10 | fun getRepositories(userName: String): Flow>
11 | fun getRepository(userName: String, id: String): Flow
12 | fun getUser(userName: String): Flow
13 | }
--------------------------------------------------------------------------------
/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/domain/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("kotlin")
3 | }
4 |
5 | dependencies {
6 | implementation(libs.kotlin.stdlib)
7 | implementation(libs.kotlinx.coroutines.android)
8 | implementation(libs.javax.inject)
9 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/jaydev/github/domain/NetResult.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.domain
2 |
3 | import com.jaydev.github.domain.entity.NetError
4 |
5 |
6 | sealed class NetResult {
7 | data class Success(val response: MODEL) : NetResult()
8 | data class Error(val error: NetError) : NetResult()
9 | }
10 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/jaydev/github/domain/entity/Fork.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.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/jaydev/github/domain/entity/NetError.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.domain.entity
2 |
3 | sealed interface NetError {
4 | object Network : NetError
5 | object Timeout : NetError
6 | data class BadRequest(val code: Int, val message: String) : NetError
7 | data class InternalServer(val code: Int, val message: String) : NetError
8 | class Unknown(val throwable: Throwable) : NetError
9 | }
10 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/jaydev/github/domain/entity/Repo.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.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/jaydev/github/domain/entity/User.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.domain.entity
2 |
3 |
4 | data class User(
5 | val name: String,
6 | val profileImageUrl: String
7 | )
8 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/jaydev/github/domain/interactor/ErrorHandler.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.domain.interactor
2 |
3 | import com.jaydev.github.domain.entity.NetError
4 |
5 | interface ErrorHandler {
6 | fun getError(throwable: Throwable): NetError
7 | }
8 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/jaydev/github/domain/interactor/NetworkConnectException.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.domain.interactor
2 |
3 | import java.io.IOException
4 |
5 | class NetworkConnectException : IOException("Network is Not Connected.")
6 |
7 | class InternalServerException(val errorCode: Int, val errorMessage: String) :
8 | IOException("Internal Server Error.")
9 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/jaydev/github/domain/interactor/usecase/BaseUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.domain.interactor.usecase
2 |
3 | import com.jaydev.github.domain.NetResult
4 | import com.jaydev.github.domain.interactor.ErrorHandler
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.catch
7 | import kotlinx.coroutines.flow.map
8 |
9 | abstract class BaseUseCase {
10 | abstract fun run(params: Params): Type
11 |
12 | operator fun invoke(
13 | params: Params
14 | ) = run(params)
15 |
16 | protected fun Flow.toResult(errorHandler: ErrorHandler) = map {
17 | NetResult.Success(it) as NetResult
18 | }.catch { cause ->
19 | emit(NetResult.Error(errorHandler.getError(cause)))
20 | }
21 |
22 | class None
23 | }
24 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/jaydev/github/domain/interactor/usecase/GithubUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.domain.interactor.usecase
2 |
3 | import com.jaydev.github.domain.NetResult
4 | import com.jaydev.github.domain.entity.Fork
5 | import com.jaydev.github.domain.entity.Repo
6 | import com.jaydev.github.domain.entity.User
7 | import com.jaydev.github.domain.interactor.ErrorHandler
8 | import com.jaydev.github.domain.repository.GithubRepository
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.zip
11 | import javax.inject.Inject
12 |
13 | class GetUserDataUseCase @Inject constructor(
14 | private val githubRepository: GithubRepository,
15 | private val errorHandler: ErrorHandler
16 | ) : BaseUseCase>>>, GetUserDataUseCase.Params>() {
17 | override fun run(params: Params) = githubRepository.getUser(params.userName)
18 | .zip(githubRepository.getRepositories(params.userName)) { user, repositories ->
19 | Pair(user, repositories.sortedByDescending { it.starCount })
20 | }.toResult(errorHandler)
21 |
22 | data class Params(
23 | val userName: String
24 | )
25 | }
26 |
27 | class GetRepoDetailUseCase @Inject constructor(
28 | private val githubRepository: GithubRepository,
29 | private val errorHandler: ErrorHandler
30 | ) : BaseUseCase>>>, GetRepoDetailUseCase.Params>() {
31 | override fun run(params: Params) =
32 | githubRepository.getRepository(params.userName, params.id)
33 | .zip(githubRepository.getForks(params.userName, params.id)) { repository, forks ->
34 | Pair(repository, forks)
35 | }.toResult(errorHandler)
36 |
37 | data class Params(
38 | val userName: String,
39 | val id: String
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/jaydev/github/domain/repository/GithubRepository.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.domain.repository
2 |
3 | import com.jaydev.github.domain.entity.Fork
4 | import com.jaydev.github.domain.entity.Repo
5 | import com.jaydev.github.domain.entity.User
6 | import kotlinx.coroutines.flow.Flow
7 |
8 | interface GithubRepository {
9 | fun getForks(userName: String, id: String): Flow>
10 | fun getRepositories(userName: String): Flow>
11 | fun getRepository(userName: String, id: String): Flow
12 | fun getUser(userName: String): Flow
13 | }
--------------------------------------------------------------------------------
/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=-Xmx2048m -Dfile.encoding=UTF-8
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 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.2.0-alpha15"
3 | kotlin = "1.8.10"
4 | core-ktx = "1.10.1"
5 | junit = "4.13.2"
6 | androidx-test-ext-junit = "1.1.5"
7 | espresso-core = "3.5.1"
8 | lifecycle-runtime-ktx = "2.6.1"
9 | activity-compose = "1.7.2"
10 | compose-bom = "2023.06.01"
11 | orbit = "6.0.0"
12 | hilt = "2.47"
13 | retrofit = "2.9.0"
14 | okhttp-bom = "4.10.0"
15 |
16 | [libraries]
17 | core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
18 | junit = { group = "junit", name = "junit", version.ref = "junit" }
19 | androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
20 | espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
21 | lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" }
22 | lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version = "2.6.1" }
23 | lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version = "2.6.1" }
24 | activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
25 | compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
26 | ui = { group = "androidx.compose.ui", name = "ui" }
27 | ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
28 | ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
29 | ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
30 | ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
31 | ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
32 | material3 = { group = "androidx.compose.material3", name = "material3" }
33 | navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version = "2.6.0" }
34 | paging-compose = { group = "androidx.paging", name = "paging-compose", version = "3.2.0-rc01" }
35 | activity-ktx = { group = "androidx.activity", name = "activity-ktx", version = "1.7.2" }
36 | constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version = "1.0.1" }
37 |
38 | kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" }
39 | kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" }
40 | kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version = "1.7.1" }
41 | kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.5.1" }
42 | kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version = "0.4.0" }
43 |
44 | orbit-compose = { group = "org.orbit-mvi", name = "orbit-compose", version.ref = "orbit" }
45 | orbit-viewmodel = { group = "org.orbit-mvi", name = "orbit-viewmodel", version.ref = "orbit" }
46 |
47 | hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
48 | hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
49 | hilt-androidx-compiler = { module = "androidx.hilt:hilt-compiler", version = "1.0.0" }
50 | hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.0.0" }
51 | javax-inject = { module = "javax.inject:javax.inject", version = "1" }
52 |
53 | retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
54 | retrofit-serialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version = "1.0.0" }
55 |
56 | okhttp-bom = { group = "com.squareup.okhttp3", name = "okhttp-bom", version.ref = "okhttp-bom" }
57 | okhttp = { module = "com.squareup.okhttp3:okhttp" }
58 | okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor" }
59 |
60 | coil-compose = { module = "io.coil-kt:coil-compose", version = "2.3.0" }
61 |
62 | logger = { group = "com.orhanobut", name = "logger", version = "2.2.0" }
63 |
64 | [plugins]
65 | androidApplication = { id = "com.android.application", version.ref = "agp" }
66 | kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
67 | kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
68 | kotlinKapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
69 | hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
70 | kotlinParcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JayDev-Lee/GithubBrowserSample/d15102b9dd36bc7c61189076ffca9e801feffdd9/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Jul 25 14:51:45 KST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
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 |
--------------------------------------------------------------------------------
/remote/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/remote/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("kotlin")
3 | alias(libs.plugins.kotlinSerialization)
4 | }
5 |
6 | dependencies {
7 | api(project(":data"))
8 |
9 | implementation(libs.kotlin.stdlib)
10 | implementation(libs.kotlinx.coroutines.android)
11 |
12 | implementation(libs.retrofit)
13 | implementation(libs.kotlinx.serialization.json)
14 |
15 | implementation(platform(libs.okhttp.bom))
16 | implementation(libs.okhttp)
17 | implementation(libs.okhttp.logging.interceptor)
18 | }
--------------------------------------------------------------------------------
/remote/src/main/java/com/jaydev/github/remote/GithubRemoteImpl.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.remote
2 |
3 | import com.jaydev.github.model.source.GithubRemote
4 | import com.jaydev.github.remote.mapper.ForkEntityMapper
5 | import com.jaydev.github.remote.mapper.RepoEntityMapper
6 | import com.jaydev.github.remote.mapper.UserEntityMapper
7 | import kotlinx.coroutines.flow.flow
8 | import kotlinx.coroutines.flow.map
9 |
10 | class GithubRemoteImpl(
11 | private val githubService: GithubService,
12 | private val userEntityMapper: UserEntityMapper,
13 | private val repoEntityMapper: RepoEntityMapper,
14 | private val forkEntityMapper: ForkEntityMapper
15 | ) : GithubRemote {
16 | override fun getForks(userName: String, id: String) = flow {
17 | emit(
18 | githubService.getForks(
19 | userName = userName,
20 | repoName = id
21 | )
22 | )
23 | }.map { forks ->
24 | forks.map {
25 | forkEntityMapper.mapFromRemote(it)
26 | }
27 | }
28 |
29 | override fun getRepositories(userName: String) = flow {
30 | emit(
31 | githubService.getRepositories(userName)
32 | )
33 | }.map { repositories ->
34 | repositories.map {
35 | repoEntityMapper.mapFromRemote(it)
36 | }
37 | }
38 |
39 | override fun getRepository(userName: String, id: String) = flow {
40 | emit(
41 | githubService.getRepository(
42 | userName = userName,
43 | repoName = id
44 | )
45 | )
46 | }.map {
47 | repoEntityMapper.mapFromRemote(it)
48 | }
49 |
50 | override fun getUser(userName: String) = flow {
51 | emit(
52 | githubService.getUser(userName)
53 | )
54 | }.map {
55 | userEntityMapper.mapFromRemote(it)
56 | }
57 | }
--------------------------------------------------------------------------------
/remote/src/main/java/com/jaydev/github/remote/GithubService.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.remote
2 |
3 | import com.jaydev.github.remote.model.ForkModel
4 | import com.jaydev.github.remote.model.RepoModel
5 | import com.jaydev.github.remote.model.UserModel
6 | import retrofit2.http.*
7 |
8 | interface GithubService {
9 | @GET("/users/{userName}")
10 | suspend fun getUser(
11 | @Path("userName") userName: String
12 | ): UserModel
13 |
14 | @GET("/users/{userName}/repos")
15 | suspend fun getRepositories(
16 | @Path("userName") userName: String
17 | ): List
18 |
19 | @GET("/repos/{userName}/{repo}")
20 | suspend fun getRepository(
21 | @Path("userName") userName: String,
22 | @Path("repo") repoName: String
23 | ): RepoModel
24 |
25 | @GET("/repos/{userName}/{repo}/forks")
26 | suspend fun getForks(
27 | @Path("userName") userName: String,
28 | @Path("repo") repoName: String
29 | ): List
30 | }
--------------------------------------------------------------------------------
/remote/src/main/java/com/jaydev/github/remote/mapper/EntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.remote.mapper
2 |
3 | interface EntityMapper {
4 | fun mapFromRemote(model: MODEL): ENTITY
5 | }
6 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/jaydev/github/remote/mapper/ForkEntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.remote.mapper
2 |
3 | import com.jaydev.github.domain.entity.Fork
4 | import com.jaydev.github.remote.model.ForkModel
5 |
6 | class ForkEntityMapper(
7 | private val userMapper: UserEntityMapper
8 | ) : EntityMapper {
9 | override fun mapFromRemote(model: ForkModel) = Fork(
10 | model.name,
11 | model.full_name,
12 | userMapper.mapFromRemote(model.owner)
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/jaydev/github/remote/mapper/RepoEntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.remote.mapper
2 |
3 | import com.jaydev.github.domain.entity.Repo
4 | import com.jaydev.github.remote.model.RepoModel
5 |
6 | class RepoEntityMapper : EntityMapper {
7 | override fun mapFromRemote(model: RepoModel) = Repo(
8 | model.name,
9 | model.description ?: "",
10 | model.stargazers_count
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/jaydev/github/remote/mapper/UserEntityMapper.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.remote.mapper
2 |
3 | import com.jaydev.github.domain.entity.User
4 | import com.jaydev.github.remote.model.UserModel
5 |
6 |
7 | class UserEntityMapper : EntityMapper {
8 | override fun mapFromRemote(model: UserModel) = User(model.login, model.avatar_url)
9 | }
10 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/jaydev/github/remote/model/ForkModel.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.remote.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class ForkModel(
7 | val name: String,
8 | val full_name: String,
9 | val owner: UserModel
10 | )
11 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/jaydev/github/remote/model/RepoModel.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.remote.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class RepoModel(
7 | val name: String,
8 | val description: String?,
9 | val stargazers_count: String
10 | )
11 |
--------------------------------------------------------------------------------
/remote/src/main/java/com/jaydev/github/remote/model/UserModel.kt:
--------------------------------------------------------------------------------
1 | package com.jaydev.github.remote.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class UserModel(
7 | val login: String,
8 | val avatar_url: String,
9 | )
10 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 |
16 | rootProject.name = "GithubBrowserSample"
17 | include(":app")
18 | include(":data")
19 | include(":domain")
20 | include(":remote")
--------------------------------------------------------------------------------