├── .gitignore ├── README.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── AppConfig.kt │ └── Dependencies.kt ├── data ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── gun │ │ └── data │ │ ├── database │ │ ├── MarvelDao.kt │ │ └── MarvelDatabase.kt │ │ ├── datasource │ │ ├── local │ │ │ ├── MarvelLocalDataSource.kt │ │ │ └── MarvelLocalDataSourceImpl.kt │ │ └── remote │ │ │ ├── MarvelRemoteDataSource.kt │ │ │ ├── MarvelRemoteDataSourceImpl.kt │ │ │ └── MarvelRemotePagingDataSourceImpl.kt │ │ ├── di │ │ ├── ApiModule.kt │ │ ├── DataSourceModule.kt │ │ ├── DatabaseModule.kt │ │ ├── NetworkModule.kt │ │ └── RepositoryModule.kt │ │ ├── entity │ │ └── response │ │ │ ├── local │ │ │ └── FavoriteEntity.kt │ │ │ └── remote │ │ │ ├── character │ │ │ ├── CharactersDto.kt │ │ │ ├── Comics.kt │ │ │ ├── Data.kt │ │ │ ├── Events.kt │ │ │ ├── Item.kt │ │ │ ├── Result.kt │ │ │ ├── Series.kt │ │ │ ├── Stories.kt │ │ │ ├── Thumbnail.kt │ │ │ └── Url.kt │ │ │ ├── comic │ │ │ ├── CharacterList.kt │ │ │ ├── CharacterSummary.kt │ │ │ ├── Characters.kt │ │ │ ├── ComicDto.kt │ │ │ ├── ComicSummary.kt │ │ │ ├── Creators.kt │ │ │ ├── Data.kt │ │ │ ├── Date.kt │ │ │ ├── EventSummary.kt │ │ │ ├── Events.kt │ │ │ ├── Item.kt │ │ │ ├── Price.kt │ │ │ ├── Result.kt │ │ │ ├── Series.kt │ │ │ ├── Stories.kt │ │ │ ├── TextObject.kt │ │ │ ├── Thumbnail.kt │ │ │ └── Url.kt │ │ │ ├── creator │ │ │ ├── Comics.kt │ │ │ ├── CreatorDto.kt │ │ │ ├── Data.kt │ │ │ ├── EventSummary.kt │ │ │ ├── Events.kt │ │ │ ├── Item.kt │ │ │ ├── Result.kt │ │ │ ├── Series.kt │ │ │ ├── Stories.kt │ │ │ ├── Thumbnail.kt │ │ │ └── Url.kt │ │ │ ├── event │ │ │ ├── Characters.kt │ │ │ ├── Comics.kt │ │ │ ├── Creators.kt │ │ │ ├── Data.kt │ │ │ ├── EventDto.kt │ │ │ ├── Item.kt │ │ │ ├── Next.kt │ │ │ ├── Previous.kt │ │ │ ├── Result.kt │ │ │ ├── Series.kt │ │ │ ├── Stories.kt │ │ │ ├── Thumbnail.kt │ │ │ └── Url.kt │ │ │ └── series │ │ │ ├── Characters.kt │ │ │ ├── Comics.kt │ │ │ ├── Creators.kt │ │ │ ├── Data.kt │ │ │ ├── EventSummary.kt │ │ │ ├── Events.kt │ │ │ ├── Item.kt │ │ │ ├── Next.kt │ │ │ ├── Result.kt │ │ │ ├── SeriesDto.kt │ │ │ ├── Stories.kt │ │ │ ├── Thumbnail.kt │ │ │ └── Url.kt │ │ ├── mapper │ │ ├── CharacterMapper.kt │ │ ├── ComicMapper.kt │ │ ├── CreatorMapper.kt │ │ ├── EventMapper.kt │ │ ├── FavoriteMapper.kt │ │ └── SeriesMapper.kt │ │ ├── network │ │ ├── MarvelApi.kt │ │ └── PrettyLog.kt │ │ └── repository │ │ └── MarvelRepositoryImpl.kt │ └── test │ └── java │ └── com │ └── gun │ └── data │ └── ExampleUnitTest.kt ├── domain ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── gun │ │ └── domain │ │ ├── common │ │ ├── Constants.kt │ │ ├── ContentType.kt │ │ └── StringExtension.kt │ │ ├── model │ │ ├── Character.kt │ │ ├── Comic.kt │ │ ├── Creator.kt │ │ ├── Event.kt │ │ ├── Series.kt │ │ ├── SimpleInfo.kt │ │ ├── detail │ │ │ └── ContentDetail.kt │ │ ├── favorite │ │ │ └── Favorite.kt │ │ ├── home │ │ │ └── HomeList.kt │ │ ├── mapper │ │ │ ├── ContentDetailMapper.kt │ │ │ └── FavoriteMapper.kt │ │ └── search │ │ │ ├── PagingModel.kt │ │ │ └── SearchResult.kt │ │ ├── repository │ │ └── MarvelRepository.kt │ │ ├── usecase │ │ ├── DeleteUseCase.kt │ │ ├── GetUseCase.kt │ │ ├── InsertUseCase.kt │ │ ├── detail │ │ │ └── GetDetailDataUseCaseImpl.kt │ │ ├── favorite │ │ │ ├── DeleteFavoriteUseCaseImpl.kt │ │ │ ├── GetFavoriteListUseCaseImpl.kt │ │ │ └── InsertFavoriteUseCaseImpl.kt │ │ ├── home │ │ │ └── GetHomeDataUseCaseImpl.kt │ │ └── search │ │ │ └── GetSearchDataUseCaseImpl.kt │ │ └── util │ │ └── DateParser.kt │ └── test │ └── java │ └── com │ └── gun │ └── domain │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── presentation ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── gun │ │ └── presentation │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── gun │ │ │ └── presentation │ │ │ ├── common │ │ │ ├── BaseApplication.kt │ │ │ ├── BaseBottomSheetDialog.kt │ │ │ ├── BaseFragment.kt │ │ │ ├── BaseItemCallback.kt │ │ │ ├── BaseListAdapter.kt │ │ │ ├── BasePagingAdapter.kt │ │ │ ├── BaseViewHolder.kt │ │ │ ├── BaseViewModel.kt │ │ │ ├── ImageLoadListener.kt │ │ │ ├── ItemClickListener.kt │ │ │ ├── binding │ │ │ │ └── GlideBindingAdapters.kt │ │ │ ├── extenstion │ │ │ │ └── _Activity.kt │ │ │ └── util │ │ │ │ └── BlurUtil.kt │ │ │ ├── di │ │ │ └── UseCaseModule.kt │ │ │ └── ui │ │ │ ├── common │ │ │ ├── CustomBadResultView.kt │ │ │ ├── LoadingStateAdapter.kt │ │ │ ├── PagingLoadStateListener.kt │ │ │ └── dialog │ │ │ │ ├── FilterBottomSheetDialog.kt │ │ │ │ └── FilterBottomSheetRecyclerAdapter.kt │ │ │ ├── detail │ │ │ ├── DetailFragment.kt │ │ │ ├── DetailItemView.kt │ │ │ ├── DetailUiModelState.kt │ │ │ └── DetailViewModel.kt │ │ │ ├── favorite │ │ │ ├── FavoriteChangedListener.kt │ │ │ ├── FavoriteFragment.kt │ │ │ ├── FavoriteViewModel.kt │ │ │ ├── list │ │ │ │ └── FavoriteRecyclerAdapter.kt │ │ │ ├── model │ │ │ │ ├── FavoriteUiFilterState.kt │ │ │ │ ├── FavoriteUiModelState.kt │ │ │ │ ├── FilterItem.kt │ │ │ │ └── SearchUiEvent.kt │ │ │ └── util │ │ │ │ └── FilterUtils.kt │ │ │ ├── home │ │ │ ├── HomeFragment.kt │ │ │ ├── HomeUiModelState.kt │ │ │ ├── HomeViewModel.kt │ │ │ ├── banner │ │ │ │ ├── HomeBannerAdapter.kt │ │ │ │ └── HomeBannerFragment.kt │ │ │ ├── list │ │ │ │ ├── HomeMainRecyclerAdapter.kt │ │ │ │ └── HomeSubRecyclerAdapter.kt │ │ │ └── model │ │ │ │ ├── HomeListItem.kt │ │ │ │ ├── HomeUiModel.kt │ │ │ │ ├── HomeUiSubModel.kt │ │ │ │ └── mapper │ │ │ │ └── HomeUiModelMapper.kt │ │ │ ├── main │ │ │ └── MainActivity.kt │ │ │ └── search │ │ │ ├── SearchFragment.kt │ │ │ ├── SearchPageMoveEvent.kt │ │ │ ├── SearchUiEvent.kt │ │ │ ├── SearchUiModel.kt │ │ │ ├── SearchViewModel.kt │ │ │ └── result │ │ │ ├── SearchResultFragment.kt │ │ │ ├── SearchResultPagerAdapter.kt │ │ │ └── SearchResultRecyclerAdapter.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xxxhdpi │ │ ├── ic_error_banner.jpeg │ │ ├── ic_error_list_item.jpg │ │ ├── ic_marvel_studios.png │ │ ├── test_thumbnail_landscape_xlarge.jpg │ │ └── test_thumbnail_portrait_xlarge.jpg │ │ ├── drawable │ │ ├── ic_arrow_right.xml │ │ ├── ic_check.xml │ │ ├── ic_favorite_default.xml │ │ ├── ic_filter.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_menu_favorite_default.xml │ │ ├── ic_menu_home_default.xml │ │ ├── ic_menu_search_default.xml │ │ ├── ic_mood_bad.xml │ │ ├── selector_bottom_menu_color.xml │ │ ├── selector_search_category_round.xml │ │ ├── shape_bottom_gradient.xml │ │ ├── shape_rounded_bottom_sheet_dialog.xml │ │ ├── shape_search_category_round_default.xml │ │ ├── shape_search_category_round_selected.xml │ │ ├── shape_search_edit_round.xml │ │ └── shape_top_gradient.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── dialog_bottom_sheet_item_select.xml │ │ ├── fragment_detail.xml │ │ ├── fragment_favorite.xml │ │ ├── fragment_home.xml │ │ ├── fragment_home_banner.xml │ │ ├── fragment_search.xml │ │ ├── fragment_search_result.xml │ │ ├── holder_bottom_sheet_list_item.xml │ │ ├── holder_favorite_list_item.xml │ │ ├── holder_home_list.xml │ │ ├── holder_home_list_item.xml │ │ ├── holder_paging_loading.xml │ │ ├── holder_search_result_item.xml │ │ ├── layout_detail_shimmer.xml │ │ ├── layout_favorite_shimmer.xml │ │ ├── layout_home_shimmer.xml │ │ ├── layout_home_title.xml │ │ ├── layout_search_guide.xml │ │ ├── layout_shimmer_favorite_list.xml │ │ ├── layout_shimmer_home_banner.xml │ │ ├── layout_shimmer_home_list.xml │ │ ├── view_custom_bad_result.xml │ │ ├── view_detail_contents.xml │ │ └── view_detail_contents_item.xml │ │ ├── menu │ │ └── bottom_nav_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-anydpi-v33 │ │ └── ic_launcher.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ ├── ic_launcher_round.webp │ │ └── ic_marvel.png │ │ ├── navigation │ │ └── nav_graph.xml │ │ ├── values-night │ │ └── themes.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── fragment_detail_scene.xml │ └── test │ └── java │ └── com │ └── gun │ └── presentation │ ├── MainDispatcherRule.kt │ ├── fake │ ├── data │ │ ├── FakeCharacterGenerator.kt │ │ ├── FakeComicGenerator.kt │ │ ├── FakeContentDetailGenerator.kt │ │ ├── FakeCreatorGenerator.kt │ │ ├── FakeDtoGenerator.kt │ │ ├── FakeEventGenerator.kt │ │ ├── FakeFavoriteGenerator.kt │ │ ├── FakeHomeListGenerator.kt │ │ ├── FakeSearchResultGenerator.kt │ │ └── FakeSeriesGenerator.kt │ └── usecase │ │ ├── FakeDeleteFavoriteUseCaseImpl.kt │ │ ├── FakeGetDetailDataUseCaseImpl.kt │ │ ├── FakeGetFavoriteDataUseCaseImpl.kt │ │ ├── FakeGetHomeDataUseCaseImpl.kt │ │ └── FakeInsertFavoriteUseCaseImpl.kt │ ├── test │ ├── TestDiffCallback.kt │ ├── TestListCallback.kt │ └── TestPagingDataConsumer.kt │ └── ui │ ├── detail │ └── DetailViewModelTest.kt │ ├── favorite │ └── FavoriteViewModelTest.kt │ ├── home │ └── HomeViewModelTest.kt │ └── search │ └── SearchViewModelTest.kt └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.apk 2 | *.ap_ 3 | *.aab 4 | 5 | *.dex 6 | 7 | *.class 8 | 9 | bin/ 10 | gen/ 11 | out/ 12 | 13 | .gradle 14 | .gradle/ 15 | build/ 16 | 17 | .signing/ 18 | 19 | local.properties 20 | 21 | proguard/ 22 | 23 | *.log 24 | 25 | /*/build/ 26 | /*/local.properties 27 | /*/out 28 | /*/*/build 29 | /*/*/production 30 | captures/ 31 | .navigation/ 32 | *.ipr 33 | *~ 34 | *.swp 35 | 36 | *.jks 37 | *.keystore 38 | 39 | gen-external-apklibs 40 | 41 | .externalNativeBuild 42 | 43 | obj/ 44 | 45 | *.iml 46 | *.iws 47 | /out/ 48 | 49 | .idea/caches/ 50 | .idea/libraries/ 51 | .idea/shelf/ 52 | .idea/workspace.xml 53 | .idea/tasks.xml 54 | .idea/.name 55 | .idea/compiler.xml 56 | .idea/copyright/profiles_settings.xml 57 | .idea/encodings.xml 58 | .idea/misc.xml 59 | .idea/modules.xml 60 | .idea/scopes/scope_settings.xml 61 | .idea/dictionaries 62 | .idea/vcs.xml 63 | .idea/jsLibraryMappings.xml 64 | .idea/datasources.xml 65 | .idea/dataSources.ids 66 | .idea/sqlDataSources.xml 67 | .idea/dynamic.xml 68 | .idea/uiDesigner.xml 69 | .idea/assetWizardSettings.xml 70 | .idea/gradle.xml 71 | .idea/jarRepositories.xml 72 | .idea/navEditor.xml 73 | 74 | .classpath 75 | .project 76 | .cproject 77 | .settings/ 78 | 79 | .mtj.tmp/ 80 | 81 | *.war 82 | *.ear 83 | 84 | hs_err_pid* 85 | 86 | .idea_modules/ 87 | 88 | atlassian-ide-plugin.xml 89 | 90 | .idea/mongoSettings.xml 91 | 92 | com_crashlytics_export_strings.xml 93 | crashlytics.properties 94 | crashlytics-build.properties 95 | fabric.properties 96 | 97 | !/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") version ("7.3.1") apply false 3 | id("com.android.library") version ("7.3.1") apply false 4 | id("org.jetbrains.kotlin.android") version ("1.8.0") apply false 5 | 6 | // AAC Navigation 7 | id("androidx.navigation.safeargs.kotlin") version (Dependencies.VERSION_ANDROIDX_NAVIGATION) apply false 8 | 9 | // Hilt 10 | id("com.google.dagger.hilt.android") version (Dependencies.VERSION_HILT) apply false 11 | } 12 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` // enable the Kotlin-DSL 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/AppConfig.kt: -------------------------------------------------------------------------------- 1 | object AppConfig { 2 | const val NAME_SPACE = "com.gun.mvvm_cleanarchitecture" 3 | const val COMPILE_SDK = 33 4 | const val MIN_SDK = 24 5 | const val TARGET_SDK = 33 6 | const val TEST_INSTRUMENTATION_RUNNER = "androidx.test.runner.AndroidJUnitRunner" 7 | const val PROGUARD_OPTIMIZE = "proguard-android-optimize.txt" 8 | const val PROGUARD = "proguard-rules.pro" 9 | 10 | const val API_PRIVATE_KEY = "MARVEL_API_PRIVATE_KEY" 11 | const val API_PUBLIC_KEY = "MARVEL_API_PUBLIC_KEY" 12 | } -------------------------------------------------------------------------------- /data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties 2 | 3 | plugins { 4 | id("com.android.library") 5 | id("kotlin-android") 6 | id("kotlin-kapt") // Hilt 7 | } 8 | 9 | android { 10 | compileSdk = AppConfig.COMPILE_SDK 11 | 12 | defaultConfig { 13 | minSdk = AppConfig.MIN_SDK 14 | targetSdk = AppConfig.TARGET_SDK 15 | 16 | testInstrumentationRunner = AppConfig.TEST_INSTRUMENTATION_RUNNER 17 | 18 | buildConfigField("String", AppConfig.API_PUBLIC_KEY, getApiKey(AppConfig.API_PUBLIC_KEY)) 19 | buildConfigField("String", AppConfig.API_PRIVATE_KEY, getApiKey(AppConfig.API_PRIVATE_KEY)) 20 | } 21 | 22 | buildTypes { 23 | getByName("release") { 24 | isMinifyEnabled = false 25 | proguardFiles(getDefaultProguardFile(AppConfig.PROGUARD_OPTIMIZE), AppConfig.PROGUARD) 26 | } 27 | } 28 | 29 | compileOptions { 30 | sourceCompatibility = JavaVersion.VERSION_11 31 | targetCompatibility = JavaVersion.VERSION_11 32 | } 33 | 34 | kotlinOptions { 35 | jvmTarget = JavaVersion.VERSION_11.toString() 36 | } 37 | } 38 | 39 | dependencies { 40 | implementation(project(":domain")) 41 | 42 | implementation(Dependencies.Google.MATERIAL) 43 | 44 | testImplementation(Dependencies.Test.JUNIT) 45 | androidTestImplementation(Dependencies.Test.JUNIT_ANDROID) 46 | androidTestImplementation(Dependencies.Test.ESPRESSO) 47 | 48 | implementation(Dependencies.AndroidX.CORE) 49 | implementation(Dependencies.AndroidX.APP_COMPAT) 50 | 51 | // Hilt 52 | implementation(Dependencies.Google.HILT) 53 | kapt(Dependencies.Google.HILT_COMPILER) 54 | 55 | // Retrofit 56 | implementation(Dependencies.Retrofit.RETROFIT) 57 | implementation(Dependencies.Retrofit.RETROFIT_CONVERTOR) 58 | 59 | // OkHttp 60 | implementation(Dependencies.OkHttp.OKHTTP) 61 | implementation(Dependencies.OkHttp.OKHTTP_INTERCEPTOR) 62 | 63 | // Gson 64 | implementation(Dependencies.Gson.GSON) 65 | 66 | // Paging 67 | implementation(Dependencies.AndroidX.PAGING_COMMON) 68 | 69 | // Room 70 | implementation(Dependencies.AndroidX.ROOM) 71 | kapt(Dependencies.AndroidX.ROOM_COMPILER) 72 | implementation(Dependencies.AndroidX.ROOM_KTX) 73 | } 74 | 75 | // Hilt 76 | kapt { 77 | correctErrorTypes = true 78 | } 79 | 80 | fun getApiKey(propertyKey: String): String { 81 | return gradleLocalProperties(rootDir).getProperty(propertyKey) 82 | } -------------------------------------------------------------------------------- /data/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.kts.kts. 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 -------------------------------------------------------------------------------- /data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/database/MarvelDao.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.database 2 | 3 | import androidx.room.* 4 | import com.gun.data.entity.response.local.FavoriteEntity 5 | 6 | @Dao 7 | interface MarvelDao { 8 | 9 | @Query("SELECT * FROM favorite_table") 10 | suspend fun getFavoriteList(): List 11 | 12 | @Query("SELECT * FROM favorite_table WHERE content_type LIKE :contentType") 13 | suspend fun getFavoriteListByContentType(contentType: String): List 14 | 15 | @Insert(onConflict = OnConflictStrategy.REPLACE) 16 | suspend fun insertFavorite(favoriteEntity: FavoriteEntity): Long // return rowId (-1 is error) 17 | 18 | @Delete 19 | suspend fun deleteFavorite(favoriteEntity: FavoriteEntity): Int // return success row count (0 is error) 20 | 21 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/database/MarvelDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.gun.data.entity.response.local.FavoriteEntity 6 | 7 | @Database(entities = [FavoriteEntity::class], version = 1) 8 | abstract class MarvelDatabase: RoomDatabase() { 9 | abstract fun getFavoriteDao(): MarvelDao 10 | 11 | companion object { 12 | const val DATABASE_NAME = "marvel_favorite.db" 13 | } 14 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/datasource/local/MarvelLocalDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.datasource.local 2 | 3 | import com.gun.data.entity.response.local.FavoriteEntity 4 | import com.gun.domain.common.ContentType 5 | 6 | interface MarvelLocalDataSource { 7 | suspend fun getFavoriteList(contentType: ContentType?): Result> 8 | 9 | suspend fun insertFavorite(favoriteEntity: FavoriteEntity): Result 10 | 11 | suspend fun deleteFavorite(favoriteEntity: FavoriteEntity): Result 12 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/datasource/local/MarvelLocalDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.datasource.local 2 | 3 | import com.gun.data.database.MarvelDao 4 | import com.gun.data.entity.response.local.FavoriteEntity 5 | import com.gun.domain.common.ContentType 6 | import com.gun.domain.common.name 7 | 8 | class MarvelLocalDataSourceImpl(private val marvelDao: MarvelDao): MarvelLocalDataSource { 9 | override suspend fun getFavoriteList(contentType: ContentType?): Result> = try { 10 | val result = if (contentType == null ) { 11 | marvelDao.getFavoriteList() 12 | } else { 13 | marvelDao.getFavoriteListByContentType(contentType.name()) 14 | } 15 | 16 | Result.success(result) 17 | } catch (e: Exception) { 18 | Result.failure(e) 19 | } 20 | 21 | override suspend fun insertFavorite(favoriteEntity: FavoriteEntity): Result = try { 22 | val result = marvelDao.insertFavorite(favoriteEntity) 23 | 24 | if (result >= 0L) { 25 | Result.success(favoriteEntity) 26 | } else { 27 | Result.failure(IllegalArgumentException()) 28 | } 29 | } catch (e: Exception) { 30 | Result.failure(e) 31 | } 32 | 33 | override suspend fun deleteFavorite(favoriteEntity: FavoriteEntity): Result = try { 34 | val result = marvelDao.deleteFavorite(favoriteEntity) 35 | 36 | if (result > 0) { 37 | Result.success(favoriteEntity) 38 | } else { 39 | Result.failure(IllegalArgumentException()) 40 | } 41 | } catch (e: Exception) { 42 | Result.failure(e) 43 | } 44 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/datasource/remote/MarvelRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.datasource.remote 2 | 3 | import com.gun.data.entity.response.remote.character.CharactersDto 4 | import com.gun.data.entity.response.remote.comic.ComicDto 5 | import com.gun.data.entity.response.remote.creator.CreatorDto 6 | import com.gun.data.entity.response.remote.event.EventDto 7 | import com.gun.data.entity.response.remote.series.SeriesDto 8 | 9 | interface MarvelRemoteDataSource { 10 | 11 | suspend fun getCharacter(characterId: Int): Result 12 | 13 | suspend fun getCharacterList(offset: Int, limit: Int): Result 14 | 15 | suspend fun getComic(comicId: Int): Result 16 | 17 | suspend fun getComicList(offset: Int, limit: Int): Result 18 | 19 | suspend fun getCreator(creatorId: Int): Result 20 | 21 | suspend fun getCreatorList(offset: Int, limit: Int): Result 22 | 23 | suspend fun getEvent(eventId: Int): Result 24 | 25 | suspend fun getEventList(offset: Int, limit: Int): Result 26 | 27 | suspend fun getSeries(seriesId: Int): Result 28 | 29 | suspend fun getSeriesList(offset: Int, limit: Int): Result 30 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/datasource/remote/MarvelRemotePagingDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.datasource.remote 2 | 3 | import androidx.paging.PagingSource 4 | import androidx.paging.PagingState 5 | import com.gun.data.mapper.toPagingModelOfSearch 6 | import com.gun.data.network.MarvelApi 7 | import com.gun.domain.common.* 8 | import com.gun.domain.model.search.PagingModel 9 | import com.gun.domain.model.search.SearchResult 10 | 11 | class MarvelRemotePagingDataSourceImpl( 12 | private val marvelApi: MarvelApi 13 | ) : PagingSource() { 14 | 15 | private var contentType: ContentType = CharacterType 16 | 17 | private lateinit var query: String 18 | 19 | fun setContentType(contentType: ContentType) { 20 | this.contentType = contentType 21 | } 22 | 23 | fun setQuery(query: String) { 24 | this.query = query 25 | } 26 | 27 | override val keyReuseSupported: Boolean = true 28 | 29 | override suspend fun load(params: LoadParams): LoadResult { 30 | return try { 31 | val current = params.key ?: 0 32 | 33 | val response = when(contentType) { 34 | is SeriesType -> marvelApi.getSeriesList(current, 20, query).toPagingModelOfSearch() 35 | is ComicType -> marvelApi.getComicList(current, 20, query).toPagingModelOfSearch() 36 | is EventType -> marvelApi.getEventList(current, 20, query).toPagingModelOfSearch() 37 | is CharacterType -> marvelApi.getCharacterList(current, 20, query).toPagingModelOfSearch() 38 | is CreatorType -> marvelApi.getCreatorList(current, 20, query).toPagingModelOfSearch() 39 | } 40 | 41 | LoadResult.Page( 42 | data = response.list?: emptyList(), 43 | prevKey = null, 44 | nextKey = nextKey(response) 45 | ) 46 | } catch (e: Exception) { 47 | LoadResult.Error(e) 48 | } 49 | } 50 | 51 | override fun getRefreshKey(state: PagingState): Int? { 52 | return null 53 | } 54 | 55 | private fun nextKey(response: PagingModel?): Int? { 56 | if (response == null || response.list.isNullOrEmpty()) { 57 | return null 58 | } 59 | 60 | val offset = response.offset 61 | val limit = response.limit 62 | val total = response.total 63 | 64 | val nextKey = if (total - (offset + limit) > 0) { 65 | offset + limit 66 | } else { 67 | null 68 | } 69 | 70 | return nextKey 71 | } 72 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/di/ApiModule.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.di 2 | 3 | import com.gun.data.network.MarvelApi 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 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | class ApiModule { 14 | @Singleton 15 | @Provides 16 | fun provideMarvelApi(retrofit: Retrofit): MarvelApi { 17 | return retrofit.create(MarvelApi::class.java) 18 | } 19 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/di/DataSourceModule.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.di 2 | 3 | import androidx.paging.PagingSource 4 | import com.gun.data.database.MarvelDao 5 | import com.gun.data.datasource.* 6 | import com.gun.data.datasource.local.MarvelLocalDataSource 7 | import com.gun.data.datasource.local.MarvelLocalDataSourceImpl 8 | import com.gun.data.datasource.remote.MarvelRemoteDataSource 9 | import com.gun.data.datasource.remote.MarvelRemoteDataSourceImpl 10 | import com.gun.data.network.MarvelApi 11 | import com.gun.data.datasource.remote.MarvelRemotePagingDataSourceImpl 12 | import com.gun.domain.model.search.SearchResult 13 | import dagger.Module 14 | import dagger.Provides 15 | import dagger.hilt.InstallIn 16 | import dagger.hilt.components.SingletonComponent 17 | import javax.inject.Singleton 18 | 19 | @Module 20 | @InstallIn(SingletonComponent::class) 21 | object DataSourceModule { 22 | 23 | @Provides 24 | @Singleton 25 | fun provideMarvelRemoteDataSource(marvelApi: MarvelApi): MarvelRemoteDataSource { 26 | return MarvelRemoteDataSourceImpl(marvelApi) 27 | } 28 | 29 | @Provides 30 | @Singleton 31 | fun provideMarvelRemotePagingDataSource(marvelApi: MarvelApi) : PagingSource { 32 | return MarvelRemotePagingDataSourceImpl(marvelApi) 33 | } 34 | 35 | @Provides 36 | @Singleton 37 | fun provideMarvelLocalDataSource(marvelDao: MarvelDao): MarvelLocalDataSource { 38 | return MarvelLocalDataSourceImpl(marvelDao) 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/di/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.di 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import com.gun.data.database.MarvelDatabase 6 | import com.gun.data.database.MarvelDao 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 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | class DatabaseModule { 17 | 18 | @Singleton 19 | @Provides 20 | fun provideAppDataBase(@ApplicationContext appContext: Context) : MarvelDatabase { 21 | return Room.databaseBuilder( 22 | appContext, 23 | MarvelDatabase::class.java, 24 | MarvelDatabase.DATABASE_NAME 25 | ).build() 26 | } 27 | 28 | @Singleton 29 | @Provides 30 | fun providesDao(database: MarvelDatabase) : MarvelDao { 31 | return database.getFavoriteDao() 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/di/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.di 2 | 3 | import com.gun.data.BuildConfig 4 | import com.gun.data.network.PrettyLogger 5 | import com.gun.domain.common.Constants 6 | import com.gun.domain.common.toMd5 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | import okhttp3.OkHttpClient 12 | import okhttp3.logging.HttpLoggingInterceptor 13 | import retrofit2.Retrofit 14 | import retrofit2.converter.gson.GsonConverterFactory 15 | import javax.inject.Singleton 16 | 17 | private const val PARAM_API_KEY = "apikey" 18 | private const val PARAM_TS = "ts" 19 | private const val PARAM_HASH = "hash" 20 | 21 | @Module 22 | @InstallIn(SingletonComponent::class) 23 | class NetworkModule { 24 | 25 | @Singleton 26 | @Provides 27 | fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit { 28 | return Retrofit.Builder() 29 | .client(okHttpClient) 30 | .baseUrl(Constants.BASE_URL) 31 | .addConverterFactory(GsonConverterFactory.create()) 32 | .build() 33 | } 34 | 35 | @Singleton 36 | @Provides 37 | fun provideOkHttp(): OkHttpClient { 38 | val loggingInterceptor = HttpLoggingInterceptor(PrettyLogger()).setLevel(HttpLoggingInterceptor.Level.BODY) 39 | 40 | return OkHttpClient.Builder() 41 | .addInterceptor(loggingInterceptor) 42 | .addInterceptor { chain -> 43 | val ts = System.currentTimeMillis().toString() 44 | val hash = (ts + BuildConfig.MARVEL_API_PRIVATE_KEY + BuildConfig.MARVEL_API_PUBLIC_KEY).toMd5() 45 | 46 | val url = chain 47 | .request() 48 | .url 49 | .newBuilder() 50 | .addQueryParameter(PARAM_API_KEY, BuildConfig.MARVEL_API_PUBLIC_KEY) 51 | .addQueryParameter(PARAM_TS, ts) 52 | .addQueryParameter(PARAM_HASH, hash) 53 | .build() 54 | chain.proceed(chain.request().newBuilder().url(url).build()) 55 | } 56 | .build() 57 | } 58 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.di 2 | 3 | import androidx.paging.PagingSource 4 | import com.gun.data.datasource.* 5 | import com.gun.data.datasource.local.MarvelLocalDataSource 6 | import com.gun.data.datasource.remote.MarvelRemoteDataSource 7 | import com.gun.data.repository.* 8 | import com.gun.domain.model.search.SearchResult 9 | import com.gun.domain.repository.* 10 | import dagger.Module 11 | import dagger.Provides 12 | import dagger.hilt.InstallIn 13 | import dagger.hilt.components.SingletonComponent 14 | import javax.inject.Singleton 15 | 16 | @Module 17 | @InstallIn(SingletonComponent::class) 18 | object RepositoryModule { 19 | 20 | @Provides 21 | @Singleton 22 | fun provideMarvelRepository( 23 | marvelLocalDataSource: MarvelLocalDataSource, 24 | marvelRemoteDataSource: MarvelRemoteDataSource, 25 | marvelRemotePagingDataSource: PagingSource 26 | ): MarvelRepository { 27 | return MarvelRepositoryImpl(marvelLocalDataSource, marvelRemoteDataSource, marvelRemotePagingDataSource) 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/local/FavoriteEntity.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.local 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "favorite_table") 8 | data class FavoriteEntity( 9 | @PrimaryKey val id: Int, 10 | @ColumnInfo(name = "name") val name: String, 11 | @ColumnInfo(name = "thumbnail_path") val thumbnailPath: String, 12 | @ColumnInfo(name = "thumbnail_extension") val thumbnailExtension: String, 13 | @ColumnInfo(name = "content_type") val contentType: String 14 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/character/CharactersDto.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.character 2 | 3 | data class CharactersDto( 4 | val code: Int, 5 | val status: String, 6 | val copyright: String, 7 | val attributionText: String, 8 | val attributionHTML: String, 9 | val etag: String, 10 | val data: Data 11 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/character/Comics.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.character 2 | 3 | data class Comics( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/character/Data.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.character 2 | 3 | data class Data( 4 | val count: Int, 5 | val limit: Int, 6 | val offset: Int, 7 | val results: List, 8 | val total: Int 9 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/character/Events.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.character 2 | 3 | data class Events( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/character/Item.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.character 2 | 3 | data class Item( 4 | val name: String, 5 | val resourceURI: String, 6 | val type: String 7 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/character/Result.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.character 2 | 3 | import android.text.TextUtils 4 | 5 | data class Result( 6 | val comics: Comics, 7 | val description: String?, 8 | val events: Events, 9 | val id: Int, 10 | val modified: String, 11 | val name: String, 12 | val resourceURI: String, 13 | val series: Series, 14 | val stories: Stories, 15 | val thumbnail: Thumbnail, 16 | val urls: List 17 | ) { 18 | fun getDetailUrl(): String { 19 | var detailUrl = "" 20 | 21 | if (urls.isNullOrEmpty()) { 22 | return detailUrl 23 | } 24 | 25 | for (url in urls) { 26 | if (TextUtils.equals("detail", url.type)) { 27 | detailUrl = url.url 28 | break 29 | } 30 | } 31 | 32 | return detailUrl 33 | } 34 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/character/Series.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.character 2 | 3 | data class Series( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/character/Stories.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.character 2 | 3 | data class Stories( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/character/Thumbnail.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.character 2 | 3 | data class Thumbnail( 4 | val extension: String, 5 | val path: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/character/Url.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.character 2 | 3 | data class Url( 4 | val type: String, 5 | val url: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/CharacterList.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class CharacterList( 4 | val available: Int, 5 | val returned: Int, 6 | val collectionURI: String, 7 | val items: List 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/CharacterSummary.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class CharacterSummary( 4 | val resourceURI: String, 5 | val name: String, 6 | val role: String 7 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/Characters.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class Characters( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/ComicDto.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class ComicDto( 4 | val attributionHTML: String, 5 | val attributionText: String, 6 | val code: Int, 7 | val copyright: String, 8 | val data: Data, 9 | val etag: String, 10 | val status: String 11 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/ComicSummary.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class ComicSummary( 4 | val resourceURI: String, val name: String 5 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/Creators.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class Creators( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/Data.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class Data( 4 | val count: Int, 5 | val limit: Int, 6 | val offset: Int, 7 | val results: List, 8 | val total: Int 9 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/Date.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class Date( 4 | val date: String, 5 | val type: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/EventSummary.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class EventSummary( 4 | val resourceURI: String, 5 | val name: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/Events.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class Events( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/Item.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class Item( 4 | val name: String, 5 | val resourceURI: String, 6 | val role: String, 7 | val type: String 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/Price.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class Price( 4 | val price: Float, 5 | val type: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/Result.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | import android.text.TextUtils 4 | 5 | data class Result( 6 | val characters: Characters, 7 | val collectedIssues: List, 8 | val collections: List, 9 | val creators: Creators, 10 | val dates: List, 11 | val description: String?, 12 | val diamondCode: String, 13 | val digitalId: Int, 14 | val ean: String, 15 | val events: Events, 16 | val format: String, 17 | val id: Int, 18 | val images: List, 19 | val isbn: String, 20 | val issn: String, 21 | val issueNumber: Int, 22 | val modified: String, 23 | val pageCount: Int, 24 | val prices: List, 25 | val resourceURI: String, 26 | val series: Series, 27 | val stories: Stories, 28 | val textObjects: List, 29 | val thumbnail: Thumbnail, 30 | val title: String, 31 | val upc: String, 32 | val urls: List, 33 | val variantDescription: String, 34 | val variants: List 35 | ) { 36 | fun getDetailUrl(): String { 37 | var detailUrl = "" 38 | 39 | if (urls.isNullOrEmpty()) { 40 | return detailUrl 41 | } 42 | 43 | for (url in urls) { 44 | if (TextUtils.equals("detail", url.type)) { 45 | detailUrl = url.url 46 | break 47 | } 48 | } 49 | 50 | return detailUrl 51 | } 52 | 53 | fun getAvailableDescription(): String { 54 | var desc = description ?: "" 55 | 56 | if (!desc.isNullOrEmpty()) { 57 | return desc 58 | } 59 | 60 | try { 61 | desc = textObjects.first { !it.text.isNullOrEmpty() }.text 62 | } catch (e: java.util.NoSuchElementException) { 63 | println("getAvailableDescription() - ${e.message}") 64 | } 65 | 66 | return desc 67 | } 68 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/Series.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class Series( 4 | val name: String, 5 | val resourceURI: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/Stories.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class Stories( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/TextObject.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class TextObject( 4 | val type: String, 5 | val language: String, 6 | val text: String 7 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/Thumbnail.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class Thumbnail( 4 | val extension: String, 5 | val path: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/comic/Url.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.comic 2 | 3 | data class Url( 4 | val type: String, 5 | val url: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/creator/Comics.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.creator 2 | 3 | data class Comics( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/creator/CreatorDto.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.creator 2 | 3 | data class CreatorDto( 4 | val attributionHTML: String, 5 | val attributionText: String, 6 | val code: Int, 7 | val copyright: String, 8 | val data: Data, 9 | val etag: String, 10 | val status: String 11 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/creator/Data.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.creator 2 | 3 | data class Data( 4 | val count: Int, 5 | val limit: Int, 6 | val offset: Int, 7 | val results: List, 8 | val total: Int 9 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/creator/EventSummary.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.creator 2 | 3 | data class EventSummary( 4 | val resourceURI: String, 5 | val name: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/creator/Events.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.creator 2 | 3 | data class Events( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/creator/Item.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.creator 2 | 3 | data class Item( 4 | val name: String, 5 | val resourceURI: String, 6 | val type: String 7 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/creator/Result.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.creator 2 | 3 | import android.text.TextUtils 4 | 5 | data class Result( 6 | val comics: Comics, 7 | val events: Events, 8 | val firstName: String, 9 | val fullName: String, 10 | val id: Int, 11 | val lastName: String, 12 | val middleName: String, 13 | val modified: String, 14 | val resourceURI: String, 15 | val series: Series, 16 | val stories: Stories, 17 | val suffix: String, 18 | val thumbnail: Thumbnail, 19 | val urls: List 20 | ) { 21 | fun getDetailUrl(): String { 22 | var detailUrl = "" 23 | 24 | if (urls.isNullOrEmpty()) { 25 | return detailUrl 26 | } 27 | 28 | for (url in urls) { 29 | if (TextUtils.equals("detail", url.type)) { 30 | detailUrl = url.url 31 | break 32 | } 33 | } 34 | 35 | return detailUrl 36 | } 37 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/creator/Series.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.creator 2 | 3 | data class Series( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/creator/Stories.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.creator 2 | 3 | data class Stories( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/creator/Thumbnail.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.creator 2 | 3 | data class Thumbnail( 4 | val extension: String, 5 | val path: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/creator/Url.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.creator 2 | 3 | data class Url( 4 | val type: String, 5 | val url: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/event/Characters.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.event 2 | 3 | data class Characters( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/event/Comics.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.event 2 | 3 | data class Comics( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/event/Creators.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.event 2 | 3 | data class Creators( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/event/Data.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.event 2 | 3 | data class Data( 4 | val count: Int, 5 | val limit: Int, 6 | val offset: Int, 7 | val results: List, 8 | val total: Int 9 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/event/EventDto.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.event 2 | 3 | data class EventDto( 4 | val attributionHTML: String, 5 | val attributionText: String, 6 | val code: Int, 7 | val copyright: String, 8 | val data: Data, 9 | val etag: String, 10 | val status: String 11 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/event/Item.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.event 2 | 3 | data class Item( 4 | val name: String, 5 | val resourceURI: String, 6 | val role: String, 7 | val type: String 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/event/Next.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.event 2 | 3 | data class Next( 4 | val name: String, 5 | val resourceURI: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/event/Previous.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.event 2 | 3 | data class Previous( 4 | val name: String, 5 | val resourceURI: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/event/Result.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.event 2 | 3 | import android.text.TextUtils 4 | 5 | data class Result( 6 | val characters: Characters, 7 | val comics: Comics, 8 | val creators: Creators, 9 | val description: String?, 10 | val end: String?, 11 | val id: Int, 12 | val modified: String, 13 | val next: Next, 14 | val previous: Previous, 15 | val resourceURI: String, 16 | val series: Series, 17 | val start: String?, 18 | val stories: Stories, 19 | val thumbnail: Thumbnail, 20 | val title: String, 21 | val urls: List 22 | ) { 23 | fun getDetailUrl(): String { 24 | var detailUrl = "" 25 | 26 | if (urls.isNullOrEmpty()) { 27 | return detailUrl 28 | } 29 | 30 | for (url in urls) { 31 | if (TextUtils.equals("detail", url.type)) { 32 | detailUrl = url.url 33 | break 34 | } 35 | } 36 | 37 | return detailUrl 38 | } 39 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/event/Series.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.event 2 | 3 | data class Series( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/event/Stories.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.event 2 | 3 | data class Stories( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/event/Thumbnail.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.event 2 | 3 | data class Thumbnail( 4 | val extension: String, 5 | val path: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/event/Url.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.event 2 | 3 | data class Url( 4 | val type: String, 5 | val url: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/series/Characters.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.series 2 | 3 | data class Characters( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/series/Comics.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.series 2 | 3 | data class Comics( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/series/Creators.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.series 2 | 3 | data class Creators( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/series/Data.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.series 2 | 3 | data class Data( 4 | val count: Int, 5 | val limit: Int, 6 | val offset: Int, 7 | val results: List, 8 | val total: Int 9 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/series/EventSummary.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.series 2 | 3 | data class EventSummary( 4 | val resourceURI: String, 5 | val name: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/series/Events.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.series 2 | 3 | data class Events( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/series/Item.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.series 2 | 3 | data class Item( 4 | val name: String, 5 | val resourceURI: String, 6 | val role: String, 7 | val type: String 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/series/Next.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.series 2 | 3 | data class Next( 4 | val name: String, 5 | val resourceURI: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/series/Result.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.series 2 | 3 | import android.text.TextUtils 4 | 5 | data class Result( 6 | val characters: Characters, 7 | val comics: Comics, 8 | val creators: Creators, 9 | val description: String?, 10 | val endYear: Int, 11 | val events: Events, 12 | val id: Int, 13 | val modified: String, 14 | val next: Next, 15 | val previous: Any, 16 | val rating: String, 17 | val resourceURI: String, 18 | val startYear: Int, 19 | val stories: Stories, 20 | val thumbnail: Thumbnail, 21 | val title: String, 22 | val type: String, 23 | val urls: List 24 | ) { 25 | fun getDetailUrl(): String { 26 | var detailUrl = "" 27 | 28 | if (urls.isNullOrEmpty()) { 29 | return detailUrl 30 | } 31 | 32 | for (url in urls) { 33 | if (TextUtils.equals("detail", url.type)) { 34 | detailUrl = url.url 35 | break 36 | } 37 | } 38 | 39 | return detailUrl 40 | } 41 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/series/SeriesDto.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.series 2 | 3 | data class SeriesDto( 4 | val attributionHTML: String, 5 | val attributionText: String, 6 | val code: Int, 7 | val copyright: String, 8 | val data: Data, 9 | val etag: String, 10 | val status: String 11 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/series/Stories.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.series 2 | 3 | data class Stories( 4 | val available: Int, 5 | val collectionURI: String, 6 | val items: List, 7 | val returned: Int 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/series/Thumbnail.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.series 2 | 3 | data class Thumbnail( 4 | val extension: String, 5 | val path: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/entity/response/remote/series/Url.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.entity.response.remote.series 2 | 3 | data class Url( 4 | val type: String, 5 | val url: String 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/mapper/CharacterMapper.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.mapper 2 | 3 | import com.gun.data.entity.response.remote.character.CharactersDto 4 | import com.gun.domain.model.Character 5 | import com.gun.domain.model.SimpleInfo 6 | import com.gun.domain.model.search.PagingModel 7 | import com.gun.domain.model.search.SearchResult 8 | 9 | fun CharactersDto.toDomainModel(): List { 10 | return data.results.map { result -> 11 | Character( 12 | id = result.id, 13 | name = result.name, 14 | description = result.description ?: "", 15 | thumbnailPath = result.thumbnail.path, 16 | thumbnailExtension = result.thumbnail.extension, 17 | detailUrl = result.getDetailUrl(), 18 | copyright = copyright, 19 | attributionText = attributionText, 20 | comicInfoList = result.comics.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 21 | seriesInfoList = result.series.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 22 | storyInfoList = result.stories.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 23 | eventInfoList = result.stories.items.map { SimpleInfo(it.resourceURI, it.name, "") } 24 | ) 25 | } 26 | } 27 | 28 | fun CharactersDto.toPagingModelOfSearch(): PagingModel { 29 | val searchResultList = data.results.map { result -> 30 | SearchResult( 31 | id = result.id, 32 | name = result.name, 33 | thumbnailPath = result.thumbnail.path, 34 | thumbnailExtension = result.thumbnail.extension, 35 | modified = result.modified 36 | ) 37 | } 38 | 39 | return with(data) { 40 | PagingModel( 41 | offset = offset, 42 | limit = limit, 43 | total = total, 44 | count = count, 45 | list = searchResultList 46 | ) 47 | } 48 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/mapper/ComicMapper.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.mapper 2 | 3 | import com.gun.data.entity.response.remote.comic.ComicDto 4 | import com.gun.domain.model.Comic 5 | import com.gun.domain.model.SimpleInfo 6 | import com.gun.domain.model.search.PagingModel 7 | import com.gun.domain.model.search.SearchResult 8 | 9 | fun ComicDto.toDomainModel(): List { 10 | return data.results.map { result -> 11 | Comic( 12 | id = result.id, 13 | title = result.title, 14 | description = result.getAvailableDescription(), 15 | format = result.format, 16 | thumbnailPath = result.thumbnail.path, 17 | thumbnailExtension = result.thumbnail.extension, 18 | detailUrl = result.getDetailUrl(), 19 | copyright = copyright, 20 | attributionText = attributionText, 21 | seriesInfoList = mutableListOf(SimpleInfo(result.series.resourceURI, result.series.name, "")), 22 | creatorInfoList = result.creators.items.map { SimpleInfo(it.resourceURI, it.name, it.role) }, 23 | characterInfoList = result.characters.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 24 | storyInfoList = result.stories.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 25 | eventInfoList = result.stories.items.map { SimpleInfo(it.resourceURI, it.name, "") } 26 | ) 27 | } 28 | } 29 | 30 | fun ComicDto.toPagingModelOfSearch(): PagingModel { 31 | val searchResultList = data.results.map { result -> 32 | SearchResult( 33 | id = result.id, 34 | name = result.title, 35 | thumbnailPath = result.thumbnail.path, 36 | thumbnailExtension = result.thumbnail.extension, 37 | modified = result.modified 38 | ) 39 | } 40 | 41 | return with(data) { 42 | PagingModel( 43 | offset = offset, 44 | limit = limit, 45 | total = total, 46 | count = count, 47 | list = searchResultList 48 | ) 49 | } 50 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/mapper/CreatorMapper.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.mapper 2 | 3 | import com.gun.data.entity.response.remote.creator.CreatorDto 4 | import com.gun.domain.model.Creator 5 | import com.gun.domain.model.SimpleInfo 6 | import com.gun.domain.model.search.PagingModel 7 | import com.gun.domain.model.search.SearchResult 8 | 9 | fun CreatorDto.toDomainModel(): List { 10 | return data.results.map { result -> 11 | Creator( 12 | id = result.id, 13 | fullName = result.fullName, 14 | thumbnailPath = result.thumbnail.path, 15 | thumbnailExtension = result.thumbnail.extension, 16 | detailUrl = result.getDetailUrl(), 17 | copyright = copyright, 18 | attributionText = attributionText, 19 | comicInfoList = result.comics.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 20 | seriesInfoList = result.series.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 21 | storyInfoList = result.stories.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 22 | eventInfoList = result.events.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 23 | ) 24 | } 25 | } 26 | 27 | fun CreatorDto.toPagingModelOfSearch(): PagingModel { 28 | val searchResultList = data.results.map { result -> 29 | SearchResult( 30 | id = result.id, 31 | name = result.fullName, 32 | thumbnailPath = result.thumbnail.path, 33 | thumbnailExtension = result.thumbnail.extension, 34 | modified = result.modified 35 | ) 36 | } 37 | 38 | return with(data) { 39 | PagingModel( 40 | offset = offset, 41 | limit = limit, 42 | total = total, 43 | count = count, 44 | list = searchResultList 45 | ) 46 | } 47 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/mapper/EventMapper.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.mapper 2 | 3 | import com.gun.data.entity.response.remote.event.EventDto 4 | import com.gun.domain.model.Event 5 | import com.gun.domain.model.SimpleInfo 6 | import com.gun.domain.model.search.PagingModel 7 | import com.gun.domain.model.search.SearchResult 8 | 9 | fun EventDto.toDomainModel(): List { 10 | return data.results.map { result -> 11 | Event( 12 | id = result.id, 13 | title = result.title, 14 | description = result.description ?: "", 15 | start = result.start ?: "", 16 | end = result.end ?: "", 17 | thumbnailPath = result.thumbnail.path, 18 | thumbnailExtension = result.thumbnail.extension, 19 | detailUrl = result.getDetailUrl(), 20 | copyright = copyright, 21 | attributionText = attributionText, 22 | creatorInfoList = result.creators.items.map { SimpleInfo(it.resourceURI, it.name, it.role) }, 23 | characterInfoList = result.characters.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 24 | storyInfoList = result.stories.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 25 | comicInfoList = result.comics.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 26 | seriesInfoList = result.comics.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 27 | ) 28 | } 29 | } 30 | 31 | fun EventDto.toPagingModelOfSearch(): PagingModel { 32 | val searchResultList = data.results.map { result -> 33 | SearchResult( 34 | id = result.id, 35 | name = result.title, 36 | thumbnailPath = result.thumbnail.path, 37 | thumbnailExtension = result.thumbnail.extension, 38 | modified = result.modified 39 | ) 40 | } 41 | 42 | return with(data) { 43 | PagingModel( 44 | offset = offset, 45 | limit = limit, 46 | total = total, 47 | count = count, 48 | list = searchResultList 49 | ) 50 | } 51 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/mapper/FavoriteMapper.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.mapper 2 | 3 | import com.gun.data.entity.response.local.FavoriteEntity 4 | import com.gun.domain.common.name 5 | import com.gun.domain.common.parseToContentType 6 | import com.gun.domain.model.favorite.Favorite 7 | 8 | fun FavoriteEntity.toDomainModel(): Favorite { 9 | return Favorite( 10 | id = id, 11 | name = name, 12 | thumbnailPath = thumbnailPath, 13 | thumbnailExtension = thumbnailExtension, 14 | contentType = contentType.parseToContentType() 15 | ) 16 | } 17 | 18 | fun Favorite.toEntity(): FavoriteEntity { 19 | return FavoriteEntity( 20 | id = id, 21 | name = name, 22 | thumbnailPath = thumbnailPath, 23 | thumbnailExtension = thumbnailExtension, 24 | contentType = contentType.name() 25 | ) 26 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/mapper/SeriesMapper.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.mapper 2 | 3 | import com.gun.data.entity.response.remote.series.SeriesDto 4 | import com.gun.domain.model.Series 5 | import com.gun.domain.model.SimpleInfo 6 | import com.gun.domain.model.search.PagingModel 7 | import com.gun.domain.model.search.SearchResult 8 | 9 | fun SeriesDto.toDomainModel(): List { 10 | return data.results.map { result -> 11 | Series( 12 | id = result.id, 13 | title = result.title, 14 | description = result.description ?: "", 15 | startYear = result.startYear, 16 | endYear = result.endYear, 17 | rating = result.rating, 18 | thumbnailPath = result.thumbnail.path, 19 | thumbnailExtension = result.thumbnail.extension, 20 | detailUrl = result.getDetailUrl(), 21 | copyright = copyright, 22 | attributionText = attributionText, 23 | creatorInfoList = result.creators.items.map { SimpleInfo(it.resourceURI, it.name, it.role) }, 24 | characterInfoList = result.characters.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 25 | storyInfoList = result.stories.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 26 | comicInfoList = result.comics.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 27 | eventInfoList = result.events.items.map { SimpleInfo(it.resourceURI, it.name, "") }, 28 | ) 29 | } 30 | } 31 | 32 | fun SeriesDto.toPagingModelOfSearch(): PagingModel { 33 | val searchResultList = data.results.map { result -> 34 | SearchResult( 35 | id = result.id, 36 | name = result.title, 37 | thumbnailPath = result.thumbnail.path, 38 | thumbnailExtension = result.thumbnail.extension, 39 | modified = result.modified 40 | ) 41 | } 42 | 43 | return with(data) { 44 | PagingModel( 45 | offset = offset, 46 | limit = limit, 47 | total = total, 48 | count = count, 49 | list = searchResultList 50 | ) 51 | } 52 | } -------------------------------------------------------------------------------- /data/src/main/java/com/gun/data/network/PrettyLog.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data.network 2 | 3 | import android.util.Log 4 | import com.google.gson.GsonBuilder 5 | import com.google.gson.JsonParser 6 | import com.gun.domain.common.Constants.TAG 7 | import okhttp3.logging.HttpLoggingInterceptor 8 | 9 | internal class PrettyLogger : HttpLoggingInterceptor.Logger { 10 | private val mGson = GsonBuilder().setPrettyPrinting().create() 11 | 12 | override fun log(message: String) { 13 | val trimMessage = message.trim { it <= ' ' } 14 | if (trimMessage.startsWith("{") && trimMessage.endsWith("}") 15 | || trimMessage.startsWith("[") && trimMessage.endsWith("]") 16 | ) { 17 | try { 18 | val prettyJson = mGson.toJson(JsonParser.parseString(message)) 19 | Log.d(TAG, prettyJson, null) 20 | } catch (e: Exception) { 21 | Log.d(TAG, message, e) 22 | } 23 | } else { 24 | Log.d(TAG, message, null) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /data/src/test/java/com/gun/data/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.gun.data 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /domain/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("kotlin") 3 | } 4 | 5 | dependencies { 6 | testImplementation(Dependencies.Test.JUNIT) 7 | implementation("javax.inject:javax.inject:1") 8 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0") 9 | 10 | // Paging 11 | implementation(Dependencies.AndroidX.PAGING_COMMON) 12 | } -------------------------------------------------------------------------------- /domain/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.kts.kts. 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 -------------------------------------------------------------------------------- /domain/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/common/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.common 2 | 3 | object Constants { 4 | const val BASE_URL = "https://gateway.marvel.com:443/" 5 | const val TAG = "MVVM_CleanArchitecture" 6 | 7 | const val TYPE_CHARACTER = "character" 8 | const val TYPE_COMIC = "comic" 9 | const val TYPE_CREATOR = "creator" 10 | const val TYPE_EVENT = "event" 11 | const val TYPE_SERIES = "series" 12 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/common/ContentType.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.common 2 | 3 | import java.io.Serializable 4 | 5 | sealed class ContentType : Serializable 6 | 7 | object CharacterType: ContentType() 8 | object ComicType: ContentType() 9 | object SeriesType: ContentType() 10 | object EventType: ContentType() 11 | object CreatorType: ContentType() 12 | 13 | fun ContentType.name(): String { 14 | return this.javaClass.simpleName 15 | } 16 | 17 | fun String.parseToContentType(): ContentType { 18 | return when(this) { 19 | CharacterType.name() -> CharacterType 20 | ComicType.name() -> ComicType 21 | SeriesType.name() -> SeriesType 22 | EventType.name() -> EventType 23 | CreatorType.name() -> CreatorType 24 | else -> throw IllegalArgumentException("InvalidType. type : $this") 25 | } 26 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/common/StringExtension.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.common 2 | 3 | import java.math.BigInteger 4 | import java.security.MessageDigest 5 | 6 | fun String.toMd5(): String { 7 | val md = MessageDigest.getInstance("MD5") 8 | return BigInteger(1, md.digest(this.toByteArray())).toString(16).padStart(32, '0') 9 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/model/Character.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.model 2 | 3 | data class Character( 4 | val id: Int, 5 | val name: String, 6 | val description: String, 7 | val thumbnailPath: String, 8 | val thumbnailExtension: String, 9 | val detailUrl: String, 10 | val copyright: String, 11 | val attributionText: String, 12 | val comicInfoList : List, 13 | val seriesInfoList : List, 14 | val storyInfoList : List, 15 | val eventInfoList : List 16 | 17 | ) -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/model/Comic.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.model 2 | 3 | data class Comic( 4 | val id: Int, 5 | val title: String, 6 | val description: String, 7 | val format : String, 8 | val thumbnailPath: String, 9 | val thumbnailExtension: String, 10 | val detailUrl: String, 11 | val copyright: String, 12 | val attributionText: String, 13 | val seriesInfoList : List, 14 | val creatorInfoList : List, 15 | val characterInfoList : List, 16 | val storyInfoList : List, 17 | val eventInfoList : List 18 | ) -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/model/Creator.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.model 2 | 3 | data class Creator( 4 | val id: Int, 5 | val fullName: String, 6 | val thumbnailPath: String, 7 | val thumbnailExtension: String, 8 | val detailUrl: String, 9 | val copyright: String, 10 | val attributionText: String, 11 | val comicInfoList : List, 12 | val seriesInfoList : List, 13 | val storyInfoList : List, 14 | val eventInfoList : List 15 | ) -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/model/Event.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.model 2 | 3 | data class Event( 4 | val id: Int, 5 | val title: String, 6 | val start : String, 7 | val end : String, 8 | val description: String, 9 | val thumbnailPath: String, 10 | val thumbnailExtension: String, 11 | val detailUrl: String, 12 | val copyright: String, 13 | val attributionText: String, 14 | val creatorInfoList : List, 15 | val characterInfoList : List, 16 | val storyInfoList : List, 17 | val comicInfoList : List, 18 | val seriesInfoList : List, 19 | ) -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/model/Series.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.model 2 | 3 | data class Series( 4 | val id: Int, 5 | val title: String, 6 | val description: String, 7 | val startYear: Int, 8 | val endYear: Int, 9 | val rating: String, 10 | val thumbnailPath: String, 11 | val thumbnailExtension: String, 12 | val detailUrl: String, 13 | val copyright: String, 14 | val attributionText: String, 15 | val creatorInfoList : List, 16 | val characterInfoList : List, 17 | val storyInfoList : List, 18 | val comicInfoList : List, 19 | val eventInfoList : List, 20 | ) -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/model/SimpleInfo.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.model 2 | 3 | data class SimpleInfo( 4 | val resourceURI: String, 5 | val name: String, 6 | val role: String 7 | ) -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/model/detail/ContentDetail.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.model.detail 2 | 3 | import com.gun.domain.model.SimpleInfo 4 | 5 | private const val DETAIL_THUMBNAIL_IMAGE_SIZE = "portrait_xlarge" 6 | 7 | data class ContentDetail( 8 | val id: Int, 9 | val name: String, 10 | val description: String, 11 | val format: String, 12 | val thumbnailPath: String, 13 | val thumbnailExtension: String, 14 | val detailUrl: String, 15 | val copyright: String, 16 | val attributionText: String, 17 | val characterInfoList: List, 18 | val comicInfoList: List, 19 | val seriesInfoList: List, 20 | val storyInfoList: List, 21 | val eventInfoList: List, 22 | val creatorInfoList: List 23 | ) { 24 | fun getThumbnailUrl(): String { 25 | return "${thumbnailPath}/$DETAIL_THUMBNAIL_IMAGE_SIZE.${thumbnailExtension}" 26 | } 27 | 28 | fun joinStringCharacters() = characterInfoList.joinToString(", ") { 29 | it.name 30 | } 31 | 32 | fun joinStringComics() = comicInfoList.joinToString(", ") { 33 | it.name 34 | } 35 | 36 | fun joinStringSeries() = seriesInfoList.joinToString(", ") { 37 | it.name 38 | } 39 | 40 | fun joinStringStories() = storyInfoList.joinToString(", ") { 41 | it.name 42 | } 43 | 44 | fun joinStringEvent() = eventInfoList.joinToString(", ") { 45 | it.name 46 | } 47 | 48 | fun joinStringCreator() = creatorInfoList.joinToString(", ") { 49 | it.name 50 | } 51 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/model/favorite/Favorite.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.model.favorite 2 | 3 | import com.gun.domain.common.* 4 | 5 | private const val HOME_LIST_ITEM_IMAGE_SIZE = "portrait_xlarge" 6 | 7 | data class Favorite( 8 | val id: Int, 9 | val name: String, 10 | val thumbnailPath: String, 11 | val thumbnailExtension: String, 12 | val contentType: ContentType 13 | ) { 14 | 15 | fun getListItemThumbnailUrl(): String { 16 | return "${thumbnailPath}/${HOME_LIST_ITEM_IMAGE_SIZE}.${thumbnailExtension}" 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/model/home/HomeList.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.model.home 2 | 3 | import com.gun.domain.model.* 4 | 5 | data class HomeList( 6 | val characterList: List?, 7 | val comicList: List?, 8 | val creatorList: List?, 9 | val eventList: List?, 10 | val seriesList: List? 11 | ) -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/model/mapper/FavoriteMapper.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.model.mapper 2 | 3 | import com.gun.domain.common.ContentType 4 | import com.gun.domain.model.detail.ContentDetail 5 | import com.gun.domain.model.favorite.Favorite 6 | import com.gun.domain.model.search.SearchResult 7 | 8 | fun SearchResult.parseFavorite(contentType: ContentType): Favorite { 9 | return Favorite( 10 | id = id, 11 | name = name, 12 | thumbnailPath = thumbnailPath, 13 | thumbnailExtension = thumbnailExtension, 14 | contentType = contentType 15 | ) 16 | } 17 | 18 | fun ContentDetail.parseFavorite(contentType: ContentType): Favorite { 19 | return Favorite( 20 | id = id, 21 | name = name, 22 | thumbnailPath = thumbnailPath, 23 | thumbnailExtension = thumbnailExtension, 24 | contentType = contentType 25 | ) 26 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/model/search/PagingModel.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.model.search 2 | 3 | data class PagingModel( 4 | val offset: Int, 5 | val limit: Int, 6 | val total: Int, 7 | val count: Int, 8 | var list: List? 9 | ) -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/model/search/SearchResult.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.model.search 2 | 3 | import com.gun.domain.util.DateParser 4 | 5 | private const val SEARCH_ITEM_THUMBNAIL_IMAGE_SIZE = "landscape_xlarge" 6 | 7 | data class SearchResult( 8 | val id: Int, 9 | val name: String, 10 | val thumbnailPath: String, 11 | val thumbnailExtension: String, 12 | val modified: String?, 13 | ) { 14 | fun getSearchItemThumbnailUrl(): String { 15 | return "${thumbnailPath}/$SEARCH_ITEM_THUMBNAIL_IMAGE_SIZE.${thumbnailExtension}" 16 | } 17 | 18 | fun getModifiedByDisplayFormat(): String { 19 | return DateParser.parseToDateFormatString(modified, DateParser.SEARCH_DATE_FORMAT) ?: "Unknown" 20 | } 21 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/repository/MarvelRepository.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.repository 2 | 3 | import androidx.paging.Pager 4 | import androidx.paging.PagingConfig 5 | import com.gun.domain.common.ContentType 6 | import com.gun.domain.model.* 7 | import com.gun.domain.model.favorite.Favorite 8 | import com.gun.domain.model.search.SearchResult 9 | import kotlinx.coroutines.flow.Flow 10 | 11 | interface MarvelRepository { 12 | 13 | fun getSearchResult(query: String, contentType: ContentType, pagingConfig: PagingConfig): Pager 14 | 15 | fun getCharacter(characterId: Int): Flow>> 16 | 17 | fun getCharacterList(offset: Int, limit: Int): Flow>> 18 | 19 | fun getComic(comicId: Int): Flow>> 20 | 21 | fun getComicList(offset: Int, limit: Int): Flow>> 22 | 23 | fun getCreator(creatorId: Int): Flow>> 24 | 25 | fun getCreatorList(offset: Int, limit: Int): Flow>> 26 | 27 | fun getEvent(eventId: Int): Flow>> 28 | 29 | fun getEventList(offset: Int, limit: Int): Flow>> 30 | 31 | fun getSeries(seriesId: Int): Flow>> 32 | 33 | fun getSeriesList(offset: Int, limit: Int): Flow>> 34 | 35 | fun getFavoriteList(contentType: ContentType?): Flow>> 36 | 37 | fun insertFavorite(favorite: Favorite): Flow> 38 | 39 | fun deleteFavorite(favorite: Favorite): Flow> 40 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/usecase/DeleteUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.usecase 2 | 3 | import com.gun.domain.model.favorite.Favorite 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface DeleteUseCase { 7 | 8 | interface DeleteFavoriteUseCase { 9 | operator fun invoke(favorite: Favorite): Flow> 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/usecase/GetUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.usecase 2 | 3 | import androidx.paging.PagingConfig 4 | import androidx.paging.PagingData 5 | import com.gun.domain.common.ContentType 6 | import com.gun.domain.model.detail.ContentDetail 7 | import com.gun.domain.model.favorite.Favorite 8 | import com.gun.domain.model.home.HomeList 9 | import com.gun.domain.model.search.SearchResult 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlin.time.Duration 12 | 13 | interface GetUseCase { 14 | 15 | interface GetHomeDataUseCase { 16 | operator fun invoke(offset: Int, limit: Int): Flow> 17 | } 18 | 19 | interface GetDetailDataUseCase { 20 | operator fun invoke(contentId: Int, contentType: ContentType): Flow> 21 | } 22 | 23 | interface GetSearchDataUseCase { 24 | operator fun invoke(param: GetSearchParams): Flow> 25 | 26 | data class GetSearchParams( 27 | val query: String, 28 | val pagingConfig: PagingConfig, 29 | val contentType: ContentType 30 | ) 31 | } 32 | 33 | interface GetFavoriteUseCase { 34 | operator fun invoke(contentType: ContentType?, shimmerDuration: Duration? = null): Flow>> 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/usecase/InsertUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.usecase 2 | 3 | import com.gun.domain.model.favorite.Favorite 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | interface InsertUseCase { 7 | 8 | interface InsertFavoriteUseCase { 9 | operator fun invoke(favorite: Favorite): Flow> 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/usecase/detail/GetDetailDataUseCaseImpl.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.usecase.detail 2 | 3 | import com.gun.domain.common.* 4 | import com.gun.domain.model.detail.ContentDetail 5 | import com.gun.domain.model.mapper.toContentDetail 6 | import com.gun.domain.repository.* 7 | import com.gun.domain.usecase.GetUseCase 8 | import kotlinx.coroutines.delay 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.flow 11 | import kotlinx.coroutines.flow.single 12 | import javax.inject.Inject 13 | import javax.inject.Singleton 14 | 15 | @Singleton 16 | class GetDetailDataUseCaseImpl @Inject constructor( 17 | private val marvelRepository: MarvelRepository 18 | ) : GetUseCase.GetDetailDataUseCase { 19 | 20 | override fun invoke(contentId: Int, contentType: ContentType): Flow> = 21 | flow { 22 | 23 | // For ShimmerEffect Showing 24 | delay(500) 25 | 26 | if (0 >= contentId) { 27 | emit(Result.failure(IllegalArgumentException())) 28 | return@flow 29 | } 30 | 31 | val result: Result 32 | 33 | when (contentType) { 34 | is CharacterType -> { 35 | result = marvelRepository.getCharacter(contentId).single() 36 | .map { it.first().toContentDetail() } 37 | } 38 | is ComicType -> { 39 | result = marvelRepository.getComic(contentId).single() 40 | .map { it.first().toContentDetail() } 41 | } 42 | is SeriesType -> { 43 | result = marvelRepository.getSeries(contentId).single() 44 | .map { it.first().toContentDetail() } 45 | } 46 | is EventType -> { 47 | result = marvelRepository.getEvent(contentId).single() 48 | .map { it.first().toContentDetail() } 49 | } 50 | is CreatorType -> { 51 | result = marvelRepository.getCreator(contentId).single() 52 | .map { it.first().toContentDetail() } 53 | } 54 | } 55 | 56 | emit(result) 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/usecase/favorite/DeleteFavoriteUseCaseImpl.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.usecase.favorite 2 | 3 | import com.gun.domain.model.favorite.Favorite 4 | import com.gun.domain.repository.MarvelRepository 5 | import com.gun.domain.usecase.DeleteUseCase 6 | import kotlinx.coroutines.flow.Flow 7 | import javax.inject.Inject 8 | import javax.inject.Singleton 9 | 10 | @Singleton 11 | class DeleteFavoriteUseCaseImpl @Inject constructor( 12 | private val marvelRepository: MarvelRepository 13 | ): DeleteUseCase.DeleteFavoriteUseCase { 14 | 15 | override fun invoke(favorite: Favorite): Flow> { 16 | return marvelRepository.deleteFavorite(favorite) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/usecase/favorite/GetFavoriteListUseCaseImpl.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.usecase.favorite 2 | 3 | import com.gun.domain.common.ContentType 4 | import com.gun.domain.model.favorite.Favorite 5 | import com.gun.domain.repository.MarvelRepository 6 | import com.gun.domain.usecase.GetUseCase 7 | import kotlinx.coroutines.delay 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.flow 10 | import kotlinx.coroutines.flow.single 11 | import javax.inject.Inject 12 | import javax.inject.Singleton 13 | import kotlin.time.Duration 14 | 15 | @Singleton 16 | class GetFavoriteListUseCaseImpl @Inject constructor( 17 | private val marvelRepository: MarvelRepository 18 | ) : GetUseCase.GetFavoriteUseCase { 19 | 20 | override fun invoke(contentType: ContentType?, shimmerDuration: Duration?): Flow>> = flow { 21 | // For ShimmerEffect Showing 22 | val delayMs = shimmerDuration?.inWholeMilliseconds ?: 0 23 | if (delayMs > 0) delay(delayMs) 24 | 25 | val result = marvelRepository.getFavoriteList(contentType).single() 26 | 27 | if (result.isFailure) { 28 | emit(result) 29 | return@flow 30 | } 31 | 32 | val favoriteList = if (contentType == null) { 33 | result.getOrNull() 34 | } else { 35 | result.getOrNull()?.filter { it.contentType == contentType } 36 | } 37 | 38 | emit(Result.success(favoriteList ?: emptyList())) 39 | } 40 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/usecase/favorite/InsertFavoriteUseCaseImpl.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.usecase.favorite 2 | 3 | import com.gun.domain.model.favorite.Favorite 4 | import com.gun.domain.repository.MarvelRepository 5 | import com.gun.domain.usecase.InsertUseCase 6 | import kotlinx.coroutines.flow.Flow 7 | import javax.inject.Inject 8 | import javax.inject.Singleton 9 | 10 | @Singleton 11 | class InsertFavoriteUseCaseImpl @Inject constructor( 12 | private val marvelRepository: MarvelRepository 13 | ): InsertUseCase.InsertFavoriteUseCase { 14 | 15 | override fun invoke(favorite: Favorite): Flow> { 16 | return marvelRepository.insertFavorite(favorite) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/usecase/home/GetHomeDataUseCaseImpl.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.usecase.home 2 | 3 | import com.gun.domain.model.Character 4 | import com.gun.domain.model.Comic 5 | import com.gun.domain.model.Creator 6 | import com.gun.domain.model.Event 7 | import com.gun.domain.model.Series 8 | import com.gun.domain.model.home.HomeList 9 | import com.gun.domain.repository.* 10 | import com.gun.domain.usecase.GetUseCase 11 | import kotlinx.coroutines.flow.* 12 | import javax.inject.Inject 13 | 14 | class GetHomeDataUseCaseImpl @Inject constructor( 15 | private val marvelRepository: MarvelRepository 16 | ) : GetUseCase.GetHomeDataUseCase { 17 | 18 | override fun invoke(offset: Int, limit: Int): Flow> = flow { 19 | if (limit == 0) { 20 | emit(Result.failure(IllegalArgumentException())) 21 | return@flow 22 | } 23 | 24 | combine( 25 | marvelRepository.getCharacterList(offset, limit), 26 | marvelRepository.getComicList(offset, limit), 27 | marvelRepository.getCreatorList(offset, limit), 28 | marvelRepository.getEventList(offset, limit), 29 | marvelRepository.getSeriesList(offset, limit) 30 | ) { characterResult: Result>, 31 | comicResult: Result>, 32 | creatorResult: Result>, 33 | eventResult: Result>, 34 | seriesResult: Result> -> 35 | { 36 | if (characterResult.isFailure && comicResult.isFailure && creatorResult.isFailure && eventResult.isFailure && seriesResult.isFailure) { 37 | Result.failure(NoSuchElementException()) 38 | } else { 39 | Result.success( 40 | HomeList( 41 | characterResult.getOrNull(), 42 | comicResult.getOrNull(), 43 | creatorResult.getOrNull(), 44 | eventResult.getOrNull(), 45 | seriesResult.getOrNull() 46 | ) 47 | ) 48 | } 49 | } 50 | }.collect { 51 | emit(it.asFlow().single()) 52 | } 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/usecase/search/GetSearchDataUseCaseImpl.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.usecase.search 2 | 3 | import com.gun.domain.repository.MarvelRepository 4 | import com.gun.domain.usecase.GetUseCase 5 | import javax.inject.Inject 6 | import javax.inject.Singleton 7 | 8 | @Singleton 9 | class GetSearchDataUseCaseImpl @Inject constructor( 10 | private val marvelRepository: MarvelRepository 11 | ) : GetUseCase.GetSearchDataUseCase { 12 | 13 | override fun invoke(param: GetUseCase.GetSearchDataUseCase.GetSearchParams) = 14 | marvelRepository.getSearchResult( 15 | param.query, 16 | param.contentType, 17 | param.pagingConfig 18 | ).flow 19 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/gun/domain/util/DateParser.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain.util 2 | 3 | import java.text.ParseException 4 | import java.text.SimpleDateFormat 5 | import java.util.* 6 | 7 | 8 | object DateParser { 9 | @Suppress("ConstantLocale") 10 | private val MARVEL_API_RESPONSE_DATE_FORMAT = SimpleDateFormat("yyyy-mm-dd", Locale.getDefault()) 11 | 12 | @Suppress("SimpleDateFormat") 13 | val SEARCH_DATE_FORMAT = SimpleDateFormat("yyyy.mm.dd") 14 | 15 | fun parseToDateFormatString(inputDateString: String?, outputDateStringFormat: SimpleDateFormat): String? { 16 | var result: String? = null 17 | 18 | try { 19 | val date = MARVEL_API_RESPONSE_DATE_FORMAT.parse(inputDateString) 20 | result = outputDateStringFormat.format(date) 21 | println("parseToDateFormatString() - result : $result") 22 | } catch (parseException: ParseException) { 23 | println("parseToDateFormatString() - parseException : ${parseException.message}") 24 | } catch (nullPointerException: NullPointerException) { 25 | println("parseToDateFormatString() - NullPointerException : ${nullPointerException.message}") 26 | } 27 | 28 | return result 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /domain/src/test/java/com/gun/domain/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.gun.domain 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /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 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon May 01 23:52:38 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /presentation/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /presentation/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.kts. 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 -------------------------------------------------------------------------------- /presentation/src/androidTest/java/com/gun/presentation/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.gun.mvvm_cleanarchitecture", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /presentation/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/common/BaseApplication.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.common 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class BaseApplication: Application() -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/common/BaseBottomSheetDialog.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.common 2 | 3 | import android.app.Activity 4 | import com.google.android.material.bottomsheet.BottomSheetDialog 5 | 6 | abstract class BottomSheetItem { 7 | abstract var isSelected: Boolean 8 | abstract val name: String 9 | } 10 | 11 | abstract class BaseBottomSheetDialog( 12 | activity: Activity, 13 | ) : BottomSheetDialog(activity) { 14 | 15 | abstract fun submitData(dataList: List): BaseBottomSheetDialog 16 | 17 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/common/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.common 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.view.animation.AlphaAnimation 6 | import androidx.fragment.app.Fragment 7 | import com.gun.presentation.ui.main.MainActivity 8 | 9 | abstract class BaseFragment: Fragment() { 10 | 11 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 12 | super.onViewCreated(view, savedInstanceState) 13 | setWindowLayoutNoLimit(false) 14 | } 15 | 16 | fun setWindowLayoutNoLimit(needExpand: Boolean) { 17 | (requireActivity() as MainActivity).setWindowLayoutNoLimit(needExpand) 18 | } 19 | 20 | fun setFadeAnimation(rootView: View, vararg viewIds: Int) { 21 | val anim = AlphaAnimation(0.0f, 1.0f) 22 | anim.duration = 600L 23 | 24 | for (viewId in viewIds) { 25 | rootView.findViewById(viewId).startAnimation(anim) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/common/BaseItemCallback.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.common 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.recyclerview.widget.DiffUtil 5 | 6 | class BaseItemCallback : DiffUtil.ItemCallback() { 7 | 8 | override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.toString() == newItem.toString() 9 | 10 | @SuppressLint("DiffUtilEquals") 11 | override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem == newItem 12 | } 13 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/common/BaseListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.common 2 | 3 | import androidx.recyclerview.widget.ListAdapter 4 | 5 | abstract class BaseListAdapter( 6 | var itemClickListener: ItemClickListener? = null 7 | ) : ListAdapter(BaseItemCallback()) 8 | 9 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/common/BasePagingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.common 2 | 3 | import androidx.paging.PagingDataAdapter 4 | 5 | abstract class BasePagingAdapter( 6 | var itemClickListener: ItemClickListener? = null 7 | ) : PagingDataAdapter(BaseItemCallback()) 8 | 9 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/common/BaseViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.common 2 | 3 | import android.view.View 4 | import androidx.recyclerview.widget.RecyclerView 5 | 6 | open class BaseViewHolder(view: View) : RecyclerView.ViewHolder(view) -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/common/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.common 2 | 3 | import androidx.lifecycle.ViewModel 4 | import kotlinx.coroutines.flow.MutableSharedFlow 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.flow.asSharedFlow 7 | import kotlinx.coroutines.flow.asStateFlow 8 | 9 | abstract class BaseViewModel : ViewModel() { 10 | protected val _messageSharedFlow = MutableSharedFlow() 11 | val messageSharedFlow = _messageSharedFlow.asSharedFlow() 12 | 13 | protected val _loadingStateFlow = MutableStateFlow(0) 14 | val loadingStateFlow = _loadingStateFlow.asStateFlow() 15 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/common/ImageLoadListener.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.common 2 | 3 | import android.graphics.Bitmap 4 | import android.widget.ImageView 5 | 6 | interface ImageLoadListener { 7 | fun onImageLoadedWithBitmap(imageView: ImageView, bitmap: Bitmap) 8 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/common/ItemClickListener.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.common 2 | 3 | interface ItemClickListener { 4 | fun onClickItem(data : T) 5 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/common/binding/GlideBindingAdapters.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.common.binding 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.drawable.Drawable 5 | import android.widget.ImageView 6 | import androidx.databinding.BindingAdapter 7 | import com.bumptech.glide.Glide 8 | import com.bumptech.glide.annotation.GlideModule 9 | import com.bumptech.glide.load.engine.DiskCacheStrategy 10 | import com.bumptech.glide.module.AppGlideModule 11 | import com.bumptech.glide.request.target.CustomTarget 12 | import com.bumptech.glide.request.transition.Transition 13 | import com.gun.presentation.common.ImageLoadListener 14 | 15 | @GlideModule 16 | class GlideAppModule : AppGlideModule() 17 | 18 | @BindingAdapter(value = ["imageUrl", "imageError", "imageListener"], requireAll = false) 19 | fun loadImage(imageView: ImageView, url: String?, error: Drawable, listener: ImageLoadListener?) { 20 | if (url.isNullOrEmpty()) return 21 | 22 | if (listener == null) { 23 | loadImageWithoutListener(imageView, url, error) 24 | } else { 25 | loadImageWithListener(imageView, url, error, listener) 26 | } 27 | } 28 | 29 | private fun loadImageWithoutListener(imageView: ImageView, url: String?, error: Drawable) { 30 | Glide.with(imageView.context) 31 | .load(url) 32 | .sizeMultiplier(0.5f) 33 | .error(error) 34 | .diskCacheStrategy(DiskCacheStrategy.ALL) 35 | .into(imageView) 36 | } 37 | 38 | private fun loadImageWithListener(imageView: ImageView, url: String?, error: Drawable, listener: ImageLoadListener) { 39 | Glide.with(imageView.context) 40 | .asBitmap() 41 | .load(url) 42 | .sizeMultiplier(0.5f) 43 | .error(error) 44 | .diskCacheStrategy(DiskCacheStrategy.ALL) 45 | .into(object : CustomTarget() { 46 | override fun onResourceReady(resource: Bitmap, transition: Transition?) { 47 | imageView.setImageBitmap(resource) 48 | listener.onImageLoadedWithBitmap(imageView, resource) 49 | } 50 | 51 | override fun onLoadCleared(placeholder: Drawable?) { 52 | 53 | } 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/common/extenstion/_Activity.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.common.extenstion 2 | 3 | import android.app.Activity 4 | import android.content.res.Configuration 5 | import android.os.Build 6 | import android.view.WindowManager 7 | import androidx.core.view.WindowCompat 8 | 9 | fun Activity.setStatusBarTransparent() { 10 | window.apply { 11 | setFlags( 12 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, 13 | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, 14 | ) 15 | } 16 | 17 | if(Build.VERSION.SDK_INT >= 30) { 18 | WindowCompat.setDecorFitsSystemWindows(window, false) 19 | } 20 | } 21 | 22 | fun Activity.setStatusBarOrigin() { 23 | window.apply { 24 | clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) 25 | } 26 | 27 | if(Build.VERSION.SDK_INT >= 30) { 28 | WindowCompat.setDecorFitsSystemWindows(window, false) 29 | } 30 | } 31 | 32 | fun Activity.getBottomSoftKeyHeight(): Int { 33 | var navigationBarHeight = 0 34 | 35 | val resName = if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { 36 | "navigation_bar_height" 37 | } else { 38 | "navigation_bar_height_landscape" 39 | } 40 | 41 | val resId = resources.getIdentifier(resName, "dimen", "android") 42 | 43 | if (resId > 0) { 44 | navigationBarHeight = resources.getDimensionPixelSize(resId) 45 | } 46 | 47 | return navigationBarHeight 48 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/common/util/BlurUtil.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.common.util 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.RenderEffect 6 | import android.graphics.Shader 7 | import android.os.Build 8 | import android.widget.ImageView 9 | import androidx.renderscript.Allocation 10 | import androidx.renderscript.Element 11 | import androidx.renderscript.RenderScript 12 | import androidx.renderscript.ScriptIntrinsicBlur 13 | 14 | object BlurUtil { 15 | // RenderEffect Blur Radius 최대값은 Float 범위에 해당하지만, RenderScript Blur Radius 최대값은 25f 이므로, 16 | // 임의로 RenderEffect Blur Radius 최대값을 RenderScript Blur Radius 최대값과 동일하게 보이는 160f 로 설정 17 | private const val MAX_BLUR_FOR_RENDER_EFFECT = 160f 18 | private const val MAX_BLUR_FOR_RENDER_SCRIPT = 25f 19 | 20 | fun blurToDetailBackground( 21 | context: Context?, 22 | imageView: ImageView, 23 | bitmap: Bitmap, 24 | @androidx.annotation.IntRange(from = 0, to = 100) blurPercent: Int = 0 25 | ) { 26 | if (blurPercent == 0) return 27 | 28 | val radius = calculateBlurRadius(blurPercent) 29 | 30 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 31 | val renderEffect = RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.DECAL) 32 | 33 | imageView.setRenderEffect(renderEffect) 34 | } else { 35 | val rs: RenderScript = RenderScript.create(context) 36 | val intrinsicBlur: ScriptIntrinsicBlur = 37 | ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)) 38 | val tmpIn: Allocation = Allocation.createFromBitmap(rs, bitmap) 39 | val tmpOut: Allocation = Allocation.createFromBitmap(rs, bitmap) 40 | intrinsicBlur.setRadius(radius) 41 | intrinsicBlur.setInput(tmpIn) 42 | intrinsicBlur.forEach(tmpOut) 43 | tmpOut.copyTo(bitmap) 44 | 45 | imageView.setImageBitmap(bitmap) 46 | } 47 | } 48 | 49 | private fun calculateBlurRadius( 50 | @androidx.annotation.IntRange(from = 0, to = 100) blurPercent: Int = 0 51 | ): Float { 52 | val radius = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 53 | MAX_BLUR_FOR_RENDER_EFFECT 54 | } else { 55 | MAX_BLUR_FOR_RENDER_SCRIPT 56 | } 57 | 58 | return blurPercent / 100f * radius 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/di/UseCaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.di 2 | 3 | import com.gun.domain.usecase.* 4 | import com.gun.domain.usecase.detail.GetDetailDataUseCaseImpl 5 | import com.gun.domain.usecase.favorite.DeleteFavoriteUseCaseImpl 6 | import com.gun.domain.usecase.favorite.GetFavoriteListUseCaseImpl 7 | import com.gun.domain.usecase.favorite.InsertFavoriteUseCaseImpl 8 | import com.gun.domain.usecase.home.GetHomeDataUseCaseImpl 9 | import com.gun.domain.usecase.search.GetSearchDataUseCaseImpl 10 | import dagger.Binds 11 | import dagger.Module 12 | import dagger.hilt.InstallIn 13 | import dagger.hilt.components.SingletonComponent 14 | import javax.inject.Singleton 15 | 16 | @Module 17 | @InstallIn(SingletonComponent::class) 18 | abstract class UseCaseModule { 19 | 20 | @Binds 21 | @Singleton 22 | abstract fun bindGetHomeDataUseCase( 23 | getHomeDataUseCaseImpl: GetHomeDataUseCaseImpl 24 | ): GetUseCase.GetHomeDataUseCase 25 | 26 | @Binds 27 | @Singleton 28 | abstract fun bindGetDetailDataUseCase( 29 | getDetailDataUseCaseImpl: GetDetailDataUseCaseImpl 30 | ): GetUseCase.GetDetailDataUseCase 31 | 32 | @Binds 33 | @Singleton 34 | abstract fun bindGetSearchDataUseCase( 35 | getSearchDataUseCaseImpl: GetSearchDataUseCaseImpl 36 | ): GetUseCase.GetSearchDataUseCase 37 | 38 | @Binds 39 | @Singleton 40 | abstract fun bindGetFavoriteUseCase( 41 | getFavoriteListUseCaseImpl: GetFavoriteListUseCaseImpl 42 | ): GetUseCase.GetFavoriteUseCase 43 | 44 | @Binds 45 | @Singleton 46 | abstract fun bindInsertFavoriteUseCase( 47 | insertFavoriteUseCaseImplImpl: InsertFavoriteUseCaseImpl 48 | ): InsertUseCase.InsertFavoriteUseCase 49 | 50 | @Binds 51 | @Singleton 52 | abstract fun bindDeleteFavoriteUseCase( 53 | deleteFavoriteUseCaseImpl: DeleteFavoriteUseCaseImpl 54 | ): DeleteUseCase.DeleteFavoriteUseCase 55 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/common/CustomBadResultView.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.common 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.widget.FrameLayout 8 | import androidx.core.view.doOnPreDraw 9 | import com.gun.mvvm_cleanarchitecture.R 10 | import com.gun.mvvm_cleanarchitecture.databinding.ViewCustomBadResultBinding 11 | 12 | sealed class BadResultType 13 | object ResultErrorType: BadResultType() 14 | object ResultEmptyType: BadResultType() 15 | object NoneType: BadResultType() 16 | 17 | class CustomBadResultView @JvmOverloads constructor( 18 | context: Context, 19 | attrs: AttributeSet? = null, 20 | defStyleAttr: Int = 0 21 | ) : FrameLayout(context, attrs, defStyleAttr) { 22 | 23 | private lateinit var binding: ViewCustomBadResultBinding 24 | 25 | init { 26 | if (!isInEditMode) { 27 | binding = ViewCustomBadResultBinding.inflate(LayoutInflater.from(context), this, false) 28 | addView(binding.root) 29 | } 30 | } 31 | 32 | fun show(badResultType: BadResultType) { 33 | with(binding) { 34 | doOnPreDraw { 35 | tvTitle.text = getTitleFromBadResultType(badResultType) 36 | tvMessage.text = getMessageFromBadResultType(badResultType) 37 | btnRetry.visibility = if (badResultType == ResultEmptyType) View.GONE else View.VISIBLE 38 | visibility = View.VISIBLE 39 | } 40 | } 41 | } 42 | 43 | fun hide() { 44 | visibility = View.GONE 45 | } 46 | 47 | fun setRetryClickListener(clickListener: OnClickListener) { 48 | binding.btnRetry.setOnClickListener(clickListener) 49 | } 50 | 51 | private fun getTitleFromBadResultType(badResultType: BadResultType) = when(badResultType) { 52 | is ResultErrorType -> context.getString(R.string.title_error_exception) 53 | is ResultEmptyType -> context.getString(R.string.title_error_result_not_found) 54 | is NoneType -> null 55 | } 56 | 57 | private fun getMessageFromBadResultType(badResultType: BadResultType) = when(badResultType) { 58 | is ResultErrorType -> context.getString(R.string.msg_error_exception) 59 | is ResultEmptyType -> context.getString(R.string.msg_error_result_not_found) 60 | is NoneType -> null 61 | } 62 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/common/LoadingStateAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.common 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.paging.LoadState 6 | import androidx.paging.LoadStateAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.gun.mvvm_cleanarchitecture.databinding.HolderPagingLoadingBinding 9 | 10 | class LoadingStateAdapter : LoadStateAdapter() { 11 | 12 | override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadingStateViewHolder { 13 | val view = HolderPagingLoadingBinding.inflate(LayoutInflater.from(parent.context), parent, false) 14 | return LoadingStateViewHolder(view) 15 | } 16 | 17 | override fun onBindViewHolder(holder: LoadingStateViewHolder, loadState: LoadState) { 18 | holder.bind(loadState) 19 | } 20 | 21 | inner class LoadingStateViewHolder( 22 | private val binding: HolderPagingLoadingBinding 23 | ) : RecyclerView.ViewHolder(binding.root) { 24 | fun bind(loadState: LoadState) { 25 | binding.isLoading = loadState is LoadState.Loading 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/common/PagingLoadStateListener.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.common 2 | 3 | import androidx.paging.CombinedLoadStates 4 | 5 | interface PagingLoadStateListener { 6 | fun onLoad(loadState: CombinedLoadStates, adapterItemCount: Int) 7 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/common/dialog/FilterBottomSheetDialog.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.common.dialog 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import com.gun.mvvm_cleanarchitecture.databinding.DialogBottomSheetItemSelectBinding 7 | import com.gun.presentation.common.BaseBottomSheetDialog 8 | import com.gun.presentation.common.ItemClickListener 9 | import com.gun.presentation.ui.favorite.model.FilterItem 10 | 11 | class FilterBottomSheetDialog( 12 | val activity: Activity, 13 | ) : BaseBottomSheetDialog(activity), ItemClickListener { 14 | 15 | private lateinit var binding: DialogBottomSheetItemSelectBinding 16 | 17 | private var listener: ItemClickListener? = null 18 | 19 | private val filterBottomSheetRecyclerAdapter = FilterBottomSheetRecyclerAdapter(this) 20 | 21 | fun setItemClickListener(listener: ItemClickListener): FilterBottomSheetDialog { 22 | this.listener = listener 23 | return this 24 | } 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | initLayout() 29 | } 30 | 31 | private fun initLayout() { 32 | binding = DialogBottomSheetItemSelectBinding.inflate(LayoutInflater.from(context)) 33 | setContentView(binding.root) 34 | binding.recyclerView.adapter = filterBottomSheetRecyclerAdapter 35 | } 36 | 37 | override fun submitData(dataList: List): FilterBottomSheetDialog { 38 | filterBottomSheetRecyclerAdapter.submitList(dataList) 39 | return this 40 | } 41 | 42 | override fun onClickItem(data: FilterItem) { 43 | listener?.onClickItem(data) 44 | dismiss() 45 | } 46 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/common/dialog/FilterBottomSheetRecyclerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.common.dialog 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import com.gun.mvvm_cleanarchitecture.databinding.HolderBottomSheetListItemBinding 6 | import com.gun.presentation.common.BaseListAdapter 7 | import com.gun.presentation.common.BaseViewHolder 8 | import com.gun.presentation.common.ItemClickListener 9 | import com.gun.presentation.ui.favorite.model.FilterItem 10 | 11 | class FilterBottomSheetRecyclerAdapter( 12 | itemClickListener: ItemClickListener? = null, 13 | ) : BaseListAdapter(itemClickListener) { 14 | 15 | private lateinit var selectedItem: FilterItem 16 | 17 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 18 | val view = HolderBottomSheetListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) 19 | return ViewHolder(view) 20 | } 21 | 22 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 23 | val data = getItem(holder.bindingAdapterPosition) 24 | if (data.isSelected) selectedItem = data 25 | 26 | holder.setData(data) 27 | } 28 | 29 | inner class ViewHolder( 30 | val binding: HolderBottomSheetListItemBinding 31 | ) : BaseViewHolder(binding.root), ItemClickListener { 32 | 33 | fun setData(data: FilterItem) { 34 | binding.data = data 35 | binding.onClickListener = this 36 | } 37 | 38 | override fun onClickItem(data: FilterItem) { 39 | val oldSelectedIndex = currentList.indexOf(selectedItem) 40 | val currentSelectedIndex = bindingAdapterPosition 41 | 42 | currentList[oldSelectedIndex].isSelected = false 43 | currentList[currentSelectedIndex].isSelected = true 44 | 45 | notifyItemChanged(oldSelectedIndex) 46 | notifyItemChanged(currentSelectedIndex) 47 | 48 | itemClickListener?.onClickItem(data) 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/detail/DetailUiModelState.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.detail 2 | 3 | import com.gun.domain.model.detail.ContentDetail 4 | 5 | sealed class DetailUiModelState { 6 | 7 | object Nothing : DetailUiModelState() 8 | 9 | data class ShowData(val data: ContentDetail) : DetailUiModelState() 10 | 11 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/favorite/FavoriteChangedListener.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.favorite 2 | 3 | interface FavoriteChangedListener { 4 | fun onFavoriteChange(data : T, isChecked: Boolean) 5 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/favorite/list/FavoriteRecyclerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.favorite.list 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import com.gun.domain.model.favorite.Favorite 6 | import com.gun.mvvm_cleanarchitecture.databinding.HolderFavoriteListItemBinding 7 | import com.gun.presentation.common.BaseListAdapter 8 | import com.gun.presentation.common.BaseViewHolder 9 | import com.gun.presentation.common.ItemClickListener 10 | 11 | class FavoriteRecyclerAdapter( 12 | itemClickListener: ItemClickListener? = null 13 | ) : BaseListAdapter(itemClickListener) { 14 | 15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 16 | val view = HolderFavoriteListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) 17 | return ViewHolder(view) 18 | } 19 | 20 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 21 | val data = getItem(holder.bindingAdapterPosition) 22 | holder.setData(data) 23 | } 24 | 25 | inner class ViewHolder(val binding: HolderFavoriteListItemBinding) : BaseViewHolder(binding.root) { 26 | fun setData(data: Favorite) { 27 | binding.data = data 28 | binding.onClickListener = itemClickListener 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/favorite/model/FavoriteUiFilterState.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.favorite.model 2 | 3 | import com.gun.domain.common.* 4 | 5 | sealed class FavoriteUiFilterState { 6 | object All: FavoriteUiFilterState() 7 | object Series: FavoriteUiFilterState() 8 | object Comic: FavoriteUiFilterState() 9 | object Event: FavoriteUiFilterState() 10 | object Character: FavoriteUiFilterState() 11 | object Creator: FavoriteUiFilterState() 12 | 13 | fun name(): String { 14 | return this.javaClass.simpleName 15 | } 16 | 17 | fun parseToContentType(): ContentType? = when (this) { 18 | is All -> null 19 | is Series -> SeriesType 20 | is Comic -> ComicType 21 | is Event -> EventType 22 | is Character -> CharacterType 23 | is Creator -> CreatorType 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/favorite/model/FavoriteUiModelState.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.favorite.model 2 | 3 | import com.gun.domain.model.favorite.Favorite 4 | 5 | sealed class FavoriteUiModelState { 6 | object Nothing : FavoriteUiModelState() 7 | 8 | data class ShowData(val data: List) : FavoriteUiModelState() 9 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/favorite/model/FilterItem.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.favorite.model 2 | 3 | import com.gun.presentation.common.BottomSheetItem 4 | 5 | data class FilterItem( 6 | override val name: String, 7 | override var isSelected: Boolean, 8 | val favoriteUiFilterState: FavoriteUiFilterState 9 | ) : BottomSheetItem() -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/favorite/model/SearchUiEvent.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.favorite.model 2 | 3 | import com.gun.presentation.ui.common.BadResultType 4 | 5 | sealed class FavoriteUiEvent { 6 | 7 | data class ShowBadResult(val badResultType: BadResultType) : FavoriteUiEvent() 8 | 9 | object HideBadResult : FavoriteUiEvent() 10 | 11 | object ShowLoading : FavoriteUiEvent() 12 | 13 | object HideLoading : FavoriteUiEvent() 14 | 15 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/favorite/util/FilterUtils.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.favorite.util 2 | 3 | import android.content.res.Resources 4 | import com.gun.mvvm_cleanarchitecture.R 5 | import com.gun.presentation.ui.favorite.model.FavoriteUiFilterState 6 | import com.gun.presentation.ui.favorite.model.FilterItem 7 | 8 | object FilterUtils { 9 | 10 | fun getFilterItemList(resources: Resources, favoriteFilter: FavoriteUiFilterState): List { 11 | return resources.getStringArray(R.array.favorite_filter_items) 12 | .toList() 13 | .map { 14 | val isSelectedItem = it == favoriteFilter.name() 15 | FilterItem(it, isSelectedItem, parseToFilterType(resources, it)) 16 | } 17 | } 18 | 19 | fun parseToFilterType(resources: Resources, str: String) = when (str) { 20 | resources.getString(R.string.label_all) -> FavoriteUiFilterState.All 21 | resources.getString(R.string.label_series) -> FavoriteUiFilterState.Series 22 | resources.getString(R.string.label_comic) -> FavoriteUiFilterState.Comic 23 | resources.getString(R.string.label_event) -> FavoriteUiFilterState.Event 24 | resources.getString(R.string.label_character) -> FavoriteUiFilterState.Character 25 | resources.getString(R.string.label_creator) -> FavoriteUiFilterState.Creator 26 | else -> throw IllegalArgumentException() 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/home/HomeUiModelState.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.home 2 | 3 | import com.gun.presentation.ui.home.model.HomeUiModel 4 | 5 | sealed class HomeUiModelState { 6 | object Nothing : HomeUiModelState() 7 | 8 | data class ShowData(val data: HomeUiModel) : HomeUiModelState() 9 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.home 2 | 3 | import androidx.lifecycle.viewModelScope 4 | import com.gun.domain.common.EventType 5 | import com.gun.domain.usecase.GetUseCase 6 | import com.gun.presentation.common.BaseViewModel 7 | import com.gun.presentation.ui.home.model.HomeListItem 8 | import com.gun.presentation.ui.home.model.HomeUiModel 9 | import com.gun.presentation.ui.home.model.mapper.toUiModel 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.flow.* 12 | import kotlinx.coroutines.launch 13 | import javax.inject.Inject 14 | 15 | const val HOME_LIST_OFFSET = 0 16 | const val HOME_LIST_LIMIT = 30 17 | const val HOME_BANNER_COUNT = 5 18 | 19 | @HiltViewModel 20 | class HomeViewModel @Inject constructor( 21 | private val getHomeListDataUseCase: GetUseCase.GetHomeDataUseCase, 22 | ) : BaseViewModel() { 23 | 24 | private val _homeUiDataStateFlow: MutableStateFlow = 25 | MutableStateFlow(HomeUiModelState.Nothing) 26 | val homeUiStateFlow = _homeUiDataStateFlow.asStateFlow() 27 | 28 | init { 29 | getHomeListData(HOME_LIST_OFFSET, HOME_LIST_LIMIT) 30 | } 31 | 32 | fun getHomeListData(offset: Int, limit: Int) { 33 | viewModelScope.launch { 34 | getHomeListDataUseCase(offset, limit) 35 | .onStart { 36 | _loadingStateFlow.update { it.plus(1) } 37 | }.onCompletion { 38 | _loadingStateFlow.update { it.minus(1) } 39 | }.catch { 40 | _messageSharedFlow.emit(it.message ?: "Error") 41 | it.printStackTrace() 42 | }.collectLatest { result -> 43 | result.onSuccess { homeList -> 44 | _homeUiDataStateFlow.value = HomeUiModelState.ShowData(homeList.toUiModel()) 45 | }.onFailure { 46 | _messageSharedFlow.emit(it.message ?: "Error") 47 | } 48 | } 49 | } 50 | } 51 | 52 | fun getFilterHomeBannerModel(homeUiModel: HomeUiModel): List? { 53 | return homeUiModel 54 | .fromUiModelType(EventType) 55 | .filterThumbnailAvailable() 56 | .sliceHomeListItem(HOME_BANNER_COUNT) 57 | } 58 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/home/banner/HomeBannerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.home.banner 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.fragment.app.Fragment 5 | import androidx.fragment.app.FragmentManager 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.viewpager2.adapter.FragmentStateAdapter 8 | import androidx.viewpager2.adapter.FragmentViewHolder 9 | import com.gun.presentation.common.ItemClickListener 10 | import com.gun.presentation.ui.home.model.HomeListItem 11 | 12 | class HomeBannerAdapter( 13 | fragmentManager: FragmentManager, 14 | lifecycle: Lifecycle, 15 | private val itemClickListener: ItemClickListener? = null 16 | ) : FragmentStateAdapter(fragmentManager, lifecycle) { 17 | 18 | private val fragmentList: MutableList = mutableListOf() 19 | private val dataList: MutableList = mutableListOf() 20 | 21 | override fun getItemCount(): Int { 22 | return fragmentList.size 23 | } 24 | 25 | override fun createFragment(position: Int): Fragment { 26 | return fragmentList[position] 27 | } 28 | 29 | override fun onBindViewHolder( 30 | holder: FragmentViewHolder, 31 | position: Int, 32 | payloads: MutableList 33 | ) { 34 | super.onBindViewHolder(holder, position, payloads) 35 | 36 | holder.itemView.setOnClickListener { 37 | itemClickListener?.onClickItem((dataList[holder.adapterPosition])) 38 | } 39 | } 40 | 41 | @SuppressLint("NotifyDataSetChanged") 42 | fun replaceData(dataList: List) { 43 | val bannerFragmentList = dataList.map { HomeBannerFragment.newInstance(it) } 44 | this.fragmentList.clear() 45 | this.fragmentList.addAll(bannerFragmentList) 46 | 47 | this.dataList.clear() 48 | this.dataList.addAll(dataList) 49 | 50 | notifyDataSetChanged() 51 | } 52 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/home/banner/HomeBannerFragment.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.home.banner 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import com.gun.mvvm_cleanarchitecture.databinding.FragmentHomeBannerBinding 10 | import com.gun.presentation.ui.home.model.HomeListItem 11 | 12 | private const val KEY_HOME_BANNER_DATA = "key_home_banner_data" 13 | 14 | class HomeBannerFragment : Fragment() { 15 | private lateinit var binding: FragmentHomeBannerBinding 16 | 17 | private lateinit var data: HomeListItem 18 | 19 | companion object { 20 | fun newInstance(event: HomeListItem): HomeBannerFragment { 21 | val bundle = Bundle() 22 | bundle.putParcelable(KEY_HOME_BANNER_DATA, event) 23 | 24 | val fragment = HomeBannerFragment() 25 | fragment.arguments = bundle 26 | return fragment 27 | } 28 | } 29 | 30 | override fun onCreateView( 31 | inflater: LayoutInflater, 32 | container: ViewGroup?, 33 | savedInstanceState: Bundle? 34 | ): View { 35 | binding = FragmentHomeBannerBinding.inflate(inflater, container, false) 36 | binding.lifecycleOwner = viewLifecycleOwner 37 | 38 | data = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 39 | requireArguments().getParcelable(KEY_HOME_BANNER_DATA, HomeListItem::class.java)!! 40 | } else { 41 | (requireArguments().getParcelable(KEY_HOME_BANNER_DATA) as? HomeListItem)!! 42 | } 43 | 44 | return binding.root 45 | } 46 | 47 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 48 | super.onViewCreated(view, savedInstanceState) 49 | with(binding) { 50 | lifecycleOwner = viewLifecycleOwner 51 | data = this@HomeBannerFragment.data 52 | } 53 | } 54 | 55 | 56 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/home/list/HomeMainRecyclerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.home.list 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import com.gun.domain.common.* 7 | import com.gun.mvvm_cleanarchitecture.R 8 | import com.gun.mvvm_cleanarchitecture.databinding.HolderHomeListBinding 9 | import com.gun.presentation.common.BaseListAdapter 10 | import com.gun.presentation.common.BaseViewHolder 11 | import com.gun.presentation.common.ItemClickListener 12 | import com.gun.presentation.ui.home.model.* 13 | 14 | class HomeMainRecyclerAdapter( 15 | private val context: Context, 16 | private val homeUiModel: HomeUiModel, 17 | itemClickListener: ItemClickListener? = null 18 | ) : BaseListAdapter(itemClickListener) { 19 | 20 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 21 | val view = HolderHomeListBinding.inflate(LayoutInflater.from(parent.context), parent, false) 22 | return ViewHolder(view) 23 | } 24 | 25 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 26 | val data = homeUiModel.homeUiSubModelList[holder.bindingAdapterPosition] 27 | holder.setData(data) 28 | } 29 | 30 | override fun getItemCount(): Int { 31 | return homeUiModel.homeUiSubModelList.size 32 | } 33 | 34 | inner class ViewHolder(val binding: HolderHomeListBinding) : BaseViewHolder(binding.root) { 35 | fun setData(data: HomeUiSubModel) { 36 | with(binding) { 37 | tvName.text = getNameFromUiModelType(data.contentType) 38 | val homeSubRecyclerAdapter = HomeSubRecyclerAdapter(itemClickListener) 39 | binding.recyclerView.adapter = homeSubRecyclerAdapter 40 | 41 | homeSubRecyclerAdapter.submitList(data.homeListItem) 42 | } 43 | } 44 | } 45 | 46 | private fun getNameFromUiModelType(contentType: ContentType) = with(context) { 47 | when(contentType) { 48 | is CharacterType -> getString(R.string.label_character) 49 | is ComicType -> getString(R.string.label_comic) 50 | is CreatorType -> getString(R.string.label_creator) 51 | is EventType -> getString(R.string.label_event) 52 | is SeriesType -> getString(R.string.label_series) 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/home/list/HomeSubRecyclerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.home.list 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import com.gun.mvvm_cleanarchitecture.databinding.HolderHomeListItemBinding 6 | import com.gun.presentation.common.BaseListAdapter 7 | import com.gun.presentation.common.BaseViewHolder 8 | import com.gun.presentation.common.ItemClickListener 9 | import com.gun.presentation.ui.home.model.HomeListItem 10 | 11 | class HomeSubRecyclerAdapter( 12 | itemClickListener: ItemClickListener? = null 13 | ) : BaseListAdapter(itemClickListener) { 14 | 15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 16 | val view = HolderHomeListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) 17 | return ViewHolder(view) 18 | } 19 | 20 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 21 | val data = getItem(holder.bindingAdapterPosition) 22 | holder.setData(data) 23 | } 24 | 25 | inner class ViewHolder(val binding: HolderHomeListItemBinding) : BaseViewHolder(binding.root) { 26 | fun setData(data: HomeListItem) { 27 | binding.data = data 28 | 29 | itemView.setOnClickListener { 30 | itemClickListener?.onClickItem(data) 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/home/model/HomeListItem.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.home.model 2 | 3 | import android.os.Parcelable 4 | import com.gun.domain.common.Constants.TYPE_CHARACTER 5 | import com.gun.domain.common.Constants.TYPE_COMIC 6 | import com.gun.domain.common.Constants.TYPE_CREATOR 7 | import com.gun.domain.common.Constants.TYPE_EVENT 8 | import com.gun.domain.common.Constants.TYPE_SERIES 9 | import com.gun.domain.common.ContentType 10 | import com.gun.domain.common.CharacterType 11 | import com.gun.domain.common.ComicType 12 | import com.gun.domain.common.CreatorType 13 | import com.gun.domain.common.EventType 14 | import com.gun.domain.common.SeriesType 15 | import kotlinx.parcelize.Parcelize 16 | 17 | private const val HOME_LIST_ITEM_IMAGE_SIZE = "portrait_xlarge" 18 | private const val HOME_BANNER_ITEM_IMAGE_SIZE = "landscape_xlarge" 19 | 20 | @Parcelize 21 | data class HomeListItem ( 22 | val id: Int, 23 | val name: String, 24 | private val thumbnailPath: String, 25 | private val thumbnailExtension: String, 26 | private val type: String, 27 | ) : Parcelable { 28 | 29 | // Parcelize 에서 sealed 클래스 미지원으로 속성을 감추고 메서드로 대체 30 | fun getContentType(): ContentType { 31 | return when(type) { 32 | TYPE_CHARACTER -> CharacterType 33 | TYPE_COMIC -> ComicType 34 | TYPE_CREATOR -> CreatorType 35 | TYPE_EVENT -> EventType 36 | TYPE_SERIES -> SeriesType 37 | else -> throw IllegalStateException("InvalidType. type : $type") 38 | } 39 | } 40 | 41 | fun getListItemThumbnailUrl(): String { 42 | return "${thumbnailPath}/${HOME_LIST_ITEM_IMAGE_SIZE}.${thumbnailExtension}" 43 | } 44 | 45 | fun getBannerItemThumbnailUrl(): String { 46 | return "${thumbnailPath}/${HOME_BANNER_ITEM_IMAGE_SIZE}.${thumbnailExtension}" 47 | } 48 | 49 | fun isThumbnailAvailable(): Boolean { 50 | return thumbnailPath.isNotEmpty() && 51 | !thumbnailPath.contains("image_not_available") && 52 | thumbnailExtension.isNotEmpty() 53 | } 54 | 55 | companion object { 56 | fun thumbnailAvailableComparator() = Comparator { a, b -> 57 | a.isThumbnailAvailable().not().compareTo(b.isThumbnailAvailable().not()) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/home/model/HomeUiModel.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.home.model 2 | 3 | import com.gun.domain.common.ContentType 4 | 5 | data class HomeUiModel( 6 | val homeUiSubModelList: List 7 | ) { 8 | fun fromUiModelType(contentType: ContentType): HomeUiSubModel { 9 | return homeUiSubModelList.first { it.contentType == contentType } 10 | } 11 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/home/model/HomeUiSubModel.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.home.model 2 | 3 | import com.gun.domain.common.ContentType 4 | 5 | data class HomeUiSubModel( 6 | val contentType: ContentType, 7 | var homeListItem: List?, 8 | ) { 9 | fun filterThumbnailAvailable() = HomeUiSubModel( 10 | contentType, 11 | homeListItem?.filter { it.isThumbnailAvailable() } 12 | ) 13 | 14 | fun sliceHomeListItem(count: Int): List? { 15 | if (homeListItem.isNullOrEmpty() || count > homeListItem!!.size) { 16 | return homeListItem 17 | } 18 | 19 | return homeListItem!!.slice(0 until 5) 20 | } 21 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/home/model/mapper/HomeUiModelMapper.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.home.model.mapper 2 | 3 | import com.gun.domain.common.* 4 | import com.gun.domain.common.Constants.TYPE_CHARACTER 5 | import com.gun.domain.common.Constants.TYPE_COMIC 6 | import com.gun.domain.common.Constants.TYPE_CREATOR 7 | import com.gun.domain.common.Constants.TYPE_EVENT 8 | import com.gun.domain.common.Constants.TYPE_SERIES 9 | import com.gun.domain.model.home.HomeList 10 | import com.gun.presentation.ui.home.model.HomeListItem 11 | import com.gun.presentation.ui.home.model.HomeUiModel 12 | import com.gun.presentation.ui.home.model.HomeUiSubModel 13 | 14 | fun HomeList.toUiModel(): HomeUiModel { 15 | val parsedCharacterList = characterList?.map { 16 | HomeListItem(it.id, it.name, it.thumbnailPath, it.thumbnailExtension, TYPE_CHARACTER) 17 | }?.sortedWith(HomeListItem.thumbnailAvailableComparator()) 18 | 19 | val parsedComicList = comicList?.map { 20 | HomeListItem(it.id, it.title, it.thumbnailPath, it.thumbnailExtension, TYPE_COMIC) 21 | }?.sortedWith(HomeListItem.thumbnailAvailableComparator()) 22 | 23 | val parsedCreatorList = creatorList?.map { 24 | HomeListItem(it.id, it.fullName, it.thumbnailPath, it.thumbnailExtension, TYPE_CREATOR) 25 | }?.sortedWith(HomeListItem.thumbnailAvailableComparator()) 26 | 27 | val parsedEventList = eventList?.map { 28 | HomeListItem(it.id, it.title, it.thumbnailPath, it.thumbnailExtension, TYPE_EVENT) 29 | }?.sortedWith(HomeListItem.thumbnailAvailableComparator()) 30 | 31 | val parsedSeriesList = seriesList?.map { 32 | HomeListItem(it.id, it.title, it.thumbnailPath, it.thumbnailExtension, TYPE_SERIES) 33 | }?.sortedWith(HomeListItem.thumbnailAvailableComparator()) 34 | 35 | return HomeUiModel( 36 | mutableListOf( 37 | HomeUiSubModel(contentType = SeriesType, homeListItem = parsedSeriesList), 38 | HomeUiSubModel(contentType = ComicType, homeListItem = parsedComicList), 39 | HomeUiSubModel(contentType = EventType, homeListItem = parsedEventList), 40 | HomeUiSubModel(contentType = CharacterType, homeListItem = parsedCharacterList), 41 | HomeUiSubModel(contentType = CreatorType, homeListItem = parsedCreatorList) 42 | ) 43 | ) 44 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.main 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.constraintlayout.widget.ConstraintLayout 6 | import androidx.core.view.updateLayoutParams 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.navigation.fragment.NavHostFragment 9 | import androidx.navigation.fragment.findNavController 10 | import androidx.navigation.ui.NavigationUI 11 | import androidx.navigation.ui.setupWithNavController 12 | import com.gun.mvvm_cleanarchitecture.R 13 | import com.gun.mvvm_cleanarchitecture.databinding.ActivityMainBinding 14 | import com.gun.presentation.common.extenstion.getBottomSoftKeyHeight 15 | import com.gun.presentation.common.extenstion.setStatusBarOrigin 16 | import com.gun.presentation.common.extenstion.setStatusBarTransparent 17 | import dagger.hilt.android.AndroidEntryPoint 18 | 19 | @AndroidEntryPoint 20 | class MainActivity : AppCompatActivity() { 21 | private val binding by lazy { DataBindingUtil.setContentView(this, R.layout.activity_main) } 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | binding.lifecycleOwner = this 26 | setNavigation() 27 | } 28 | 29 | private fun setNavigation() { 30 | with(binding) { 31 | val navHostFragment = supportFragmentManager 32 | .findFragmentById(navHostFragment.id) as NavHostFragment 33 | val navController = navHostFragment.findNavController() 34 | 35 | bottomNavigation.setupWithNavController(navController) 36 | 37 | bottomNavigation.setOnItemSelectedListener { item -> 38 | NavigationUI.onNavDestinationSelected(item, navController) 39 | // popBackStack 메서드를 호출하여 이동하려는 대상이 백 스택에 존재할 시, 해당 대상까지 사이의 스택을 지우고 가져온다. 40 | navController.popBackStack(item.itemId, inclusive = false) 41 | } 42 | } 43 | } 44 | 45 | /** 46 | * StatusBar/Navigation(Bottom) 영역까지 확장 (투명 상태바) 47 | * 48 | * @param needExpand - true : 확장 49 | * false : 축소 (원복) 50 | * */ 51 | fun setWindowLayoutNoLimit(needExpand: Boolean) { 52 | if (needExpand) { 53 | setStatusBarTransparent() 54 | } else { 55 | setStatusBarOrigin() 56 | } 57 | 58 | binding.bottomNavigation.updateLayoutParams { 59 | bottomMargin = if (needExpand) getBottomSoftKeyHeight() else 0 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/search/SearchPageMoveEvent.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.search 2 | 3 | import com.gun.domain.common.ContentType 4 | 5 | sealed class SearchPageMoveEvent { 6 | 7 | data class MoveToDetail(val contentId: Int, val contentType: ContentType) : SearchPageMoveEvent() 8 | 9 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/search/SearchUiEvent.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.search 2 | 3 | import com.gun.presentation.ui.common.BadResultType 4 | 5 | sealed class SearchUiEvent { 6 | 7 | data class ShowBadResult(val badResultType: BadResultType) : SearchUiEvent() 8 | 9 | object HideBadResult : SearchUiEvent() 10 | 11 | object ShowLoading : SearchUiEvent() 12 | 13 | object HideLoading : SearchUiEvent() 14 | 15 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/search/SearchUiModel.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.search 2 | 3 | import androidx.paging.PagingData 4 | import com.gun.domain.common.ContentType 5 | import com.gun.domain.model.search.SearchResult 6 | 7 | sealed class SearchUiModel { 8 | 9 | object Initialize : SearchUiModel() 10 | 11 | object Clear : SearchUiModel() 12 | 13 | data class ShowData(val contentType: ContentType, val data: PagingData) : SearchUiModel() 14 | 15 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/search/result/SearchResultPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.search.result 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentManager 5 | import androidx.lifecycle.Lifecycle 6 | import androidx.viewpager2.adapter.FragmentStateAdapter 7 | import com.google.android.material.tabs.TabLayout 8 | import com.google.android.material.tabs.TabLayoutMediator 9 | 10 | class SearchResultPagerAdapter( 11 | fragmentManager: FragmentManager, 12 | lifecycle: Lifecycle, 13 | private val dataList: List, 14 | ) : FragmentStateAdapter(fragmentManager, lifecycle), TabLayoutMediator.TabConfigurationStrategy { 15 | 16 | private val fragmentList: List 17 | 18 | init { 19 | fragmentList = dataList.map { SearchResultFragment.newInstance(it) } 20 | } 21 | 22 | override fun getItemCount(): Int { 23 | return fragmentList.size 24 | } 25 | 26 | override fun createFragment(position: Int): Fragment { 27 | return fragmentList[position] 28 | } 29 | 30 | override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { 31 | tab.text = dataList[position] 32 | } 33 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/gun/presentation/ui/search/result/SearchResultRecyclerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.ui.search.result 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import com.gun.mvvm_cleanarchitecture.databinding.HolderSearchResultItemBinding 7 | import com.gun.presentation.common.BasePagingAdapter 8 | import com.gun.presentation.common.BaseViewHolder 9 | import com.gun.presentation.common.ItemClickListener 10 | import com.gun.domain.model.search.SearchResult 11 | import com.gun.presentation.ui.favorite.FavoriteChangedListener 12 | 13 | class SearchResultRecyclerAdapter( 14 | itemClickListener: ItemClickListener? = null, 15 | val favoriteChangedListener: FavoriteChangedListener 16 | ) : BasePagingAdapter(itemClickListener) { 17 | 18 | private var favoriteIdList: List = mutableListOf() 19 | 20 | @SuppressLint("NotifyDataSetChanged") 21 | fun setFavoriteIdList(favoriteIdList: List) { 22 | this.favoriteIdList = favoriteIdList 23 | notifyDataSetChanged() 24 | } 25 | 26 | override fun onCreateViewHolder( 27 | parent: ViewGroup, 28 | viewType: Int 29 | ): SearchResultRecyclerAdapter.ViewHolder { 30 | val binding = HolderSearchResultItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) 31 | return ViewHolder(binding) 32 | } 33 | 34 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 35 | getItem(holder.bindingAdapterPosition)?.let { 36 | holder.setData(it) 37 | } 38 | } 39 | 40 | inner class ViewHolder(val binding: HolderSearchResultItemBinding) : BaseViewHolder(binding.root) { 41 | fun setData(data: SearchResult) { 42 | binding.data = data 43 | binding.isFavorite = favoriteIdList.contains(data.id) 44 | 45 | binding.root.setOnClickListener { 46 | itemClickListener?.onClickItem(data) 47 | } 48 | 49 | binding.checkBoxFavorite.setOnClickListener { 50 | favoriteChangedListener.onFavoriteChange(data, binding.checkBoxFavorite.isChecked) 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable-xxxhdpi/ic_error_banner.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/presentation/src/main/res/drawable-xxxhdpi/ic_error_banner.jpeg -------------------------------------------------------------------------------- /presentation/src/main/res/drawable-xxxhdpi/ic_error_list_item.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/presentation/src/main/res/drawable-xxxhdpi/ic_error_list_item.jpg -------------------------------------------------------------------------------- /presentation/src/main/res/drawable-xxxhdpi/ic_marvel_studios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/presentation/src/main/res/drawable-xxxhdpi/ic_marvel_studios.png -------------------------------------------------------------------------------- /presentation/src/main/res/drawable-xxxhdpi/test_thumbnail_landscape_xlarge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/presentation/src/main/res/drawable-xxxhdpi/test_thumbnail_landscape_xlarge.jpg -------------------------------------------------------------------------------- /presentation/src/main/res/drawable-xxxhdpi/test_thumbnail_portrait_xlarge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/presentation/src/main/res/drawable-xxxhdpi/test_thumbnail_portrait_xlarge.jpg -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_arrow_right.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_check.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_favorite_default.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_filter.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_menu_favorite_default.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_menu_home_default.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_menu_search_default.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/ic_mood_bad.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/selector_bottom_menu_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/selector_search_category_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/shape_bottom_gradient.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/shape_rounded_bottom_sheet_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/shape_search_category_round_default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/shape_search_category_round_selected.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/shape_search_edit_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /presentation/src/main/res/drawable/shape_top_gradient.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 19 | 20 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/dialog_bottom_sheet_item_select.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 22 | 23 | 30 | 31 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/fragment_home_banner.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 11 | 12 | 13 | 15 | 16 | 22 | 23 | 33 | 34 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/fragment_search_result.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 10 | 20 | 21 | 29 | 30 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/holder_bottom_sheet_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 19 | 20 | 21 | 25 | 26 | 36 | 37 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/holder_favorite_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 17 | 18 | 19 | 23 | 24 | 31 | 32 | 38 | 39 | 40 | 41 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/holder_home_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 18 | 19 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/holder_home_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 27 | 28 | 34 | 35 | 36 | 37 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/holder_paging_loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 20 | 21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/layout_favorite_shimmer.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/layout_home_shimmer.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/layout_home_title.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/layout_shimmer_home_banner.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 26 | 27 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/view_detail_contents.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 19 | 20 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /presentation/src/main/res/layout/view_detail_contents_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 20 | 21 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /presentation/src/main/res/menu/bottom_nav_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | 20 | 21 | -------------------------------------------------------------------------------- /presentation/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /presentation/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /presentation/src/main/res/mipmap-anydpi-v33/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /presentation/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/presentation/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /presentation/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/presentation/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /presentation/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/presentation/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /presentation/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/presentation/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /presentation/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/presentation/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /presentation/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/presentation/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /presentation/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/presentation/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /presentation/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/presentation/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /presentation/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /presentation/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /presentation/src/main/res/mipmap-xxxhdpi/ic_marvel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gun-HelloWorld/Android_MVVM_CleanArchitecture/899803ccad4c26086b446ad15f8bb0acc7fe16e4/presentation/src/main/res/mipmap-xxxhdpi/ic_marvel.png -------------------------------------------------------------------------------- /presentation/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /presentation/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | #FF5A5A 12 | #5D5D5D 13 | #C4C4C5 14 | #C4C4C5 15 | #212122 16 | #90000000 17 | @android:color/transparent 18 | #3a3a3a 19 | 20 | #393A3B 21 | 22 | #FF3636 23 | #C4C4C5 24 | 25 | #EAEAEA 26 | 27 | #4d000000 28 | 29 | #909091 30 | #CC3a3a3a 31 | -------------------------------------------------------------------------------- /presentation/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | 25 | 28 | 29 | 32 | 33 | 40 | -------------------------------------------------------------------------------- /presentation/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /presentation/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/MainDispatcherRule.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.TestDispatcher 6 | import kotlinx.coroutines.test.UnconfinedTestDispatcher 7 | import kotlinx.coroutines.test.resetMain 8 | import kotlinx.coroutines.test.setMain 9 | import org.junit.rules.TestWatcher 10 | import org.junit.runner.Description 11 | 12 | @OptIn(ExperimentalCoroutinesApi::class) 13 | class MainDispatcherRule( 14 | private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() 15 | ) : TestWatcher() { 16 | override fun starting(description: Description) { 17 | Dispatchers.setMain(testDispatcher) 18 | } 19 | 20 | override fun finished(description: Description) { 21 | Dispatchers.resetMain() 22 | } 23 | } -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/fake/data/FakeCharacterGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.fake.data 2 | 3 | import com.gun.domain.model.Character 4 | import com.gun.domain.model.SimpleInfo 5 | 6 | object FakeCharacterGenerator { 7 | 8 | fun generate(id: Int): Character { 9 | return Character( 10 | id = id, 11 | name = "CharacterName$id", 12 | description = "CharacterDesc$id", 13 | thumbnailPath = "CharacterThumbnailPath$id", 14 | thumbnailExtension = "CharacterThumbnailExtension$id", 15 | detailUrl = "CharacterDetailUrl$id", 16 | copyright = "CharacterCopyright$id", 17 | attributionText = "CharacterAttributionText$id", 18 | comicInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "")), 19 | seriesInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "")), 20 | storyInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "")), 21 | eventInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "")), 22 | ) 23 | } 24 | 25 | fun generate(offset: Int, limit: Int): List { 26 | val fakeCharacterList = mutableListOf() 27 | 28 | for (i in offset until limit) { 29 | fakeCharacterList.add( 30 | Character( 31 | id = i, 32 | name = "CharacterName$i", 33 | description = "CharacterDesc$i", 34 | thumbnailPath = "CharacterThumbnailPath$i", 35 | thumbnailExtension = "CharacterThumbnailExtension$i", 36 | detailUrl = "CharacterDetailUrl$i", 37 | copyright = "CharacterCopyright$i", 38 | attributionText = "CharacterAttributionText$i", 39 | comicInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "")), 40 | seriesInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "")), 41 | storyInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "")), 42 | eventInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "")), 43 | ) 44 | ) 45 | } 46 | 47 | return fakeCharacterList 48 | } 49 | } -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/fake/data/FakeComicGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.fake.data 2 | 3 | import com.gun.domain.model.Comic 4 | import com.gun.domain.model.SimpleInfo 5 | 6 | object FakeComicGenerator { 7 | 8 | fun generate(id: Int): Comic { 9 | return Comic( 10 | id = id, 11 | title = "TestComicTitle$id", 12 | description = "TestComicDesc$id", 13 | format = "TestComicFormat$id", 14 | thumbnailPath = "TestComicThumbnailPath$id", 15 | thumbnailExtension = "TestComicThumbnailExtension$id", 16 | detailUrl = "TestComicDetailUrl$id", 17 | copyright = "TestComicCopyright$id", 18 | attributionText = "TestComicAttributionText$id", 19 | seriesInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "")), 20 | creatorInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "Role$id")), 21 | characterInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "")), 22 | storyInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "")), 23 | eventInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "")) 24 | ) 25 | } 26 | 27 | fun generate(offset: Int, limit: Int): List { 28 | val fakeComicList = mutableListOf() 29 | 30 | for (i in offset until limit) { 31 | fakeComicList.add( 32 | Comic( 33 | id = i, 34 | title = "ComicTitle$i", 35 | description = "ComicDesc$i", 36 | format = "ComicFormat$i", 37 | thumbnailPath = "ComicThumbnailPath$i", 38 | thumbnailExtension = "ComicThumbnailExtension$i", 39 | detailUrl = "ComicDetailUrl$i", 40 | copyright = "ComicCopyright$i", 41 | attributionText = "ComicAttributionText$i", 42 | seriesInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "")), 43 | creatorInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "Role$i")), 44 | characterInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "")), 45 | storyInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "")), 46 | eventInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "")) 47 | ) 48 | ) 49 | } 50 | 51 | return fakeComicList 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/fake/data/FakeContentDetailGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.fake.data 2 | 3 | import com.gun.domain.common.* 4 | import com.gun.domain.model.mapper.toContentDetail 5 | 6 | object FakeContentDetailGenerator { 7 | 8 | fun generate(id: Int, contentType: ContentType) = when (contentType) { 9 | is CharacterType -> { 10 | FakeCharacterGenerator.generate(id).toContentDetail() 11 | } 12 | is ComicType -> { 13 | FakeComicGenerator.generate(id).toContentDetail() 14 | } 15 | is SeriesType -> { 16 | FakeSeriesGenerator.generate(id).toContentDetail() 17 | } 18 | is EventType -> { 19 | FakeEventGenerator.generate(id).toContentDetail() 20 | } 21 | is CreatorType -> { 22 | FakeCreatorGenerator.generate(id).toContentDetail() 23 | } 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/fake/data/FakeCreatorGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.fake.data 2 | 3 | import com.gun.domain.model.Creator 4 | import com.gun.domain.model.SimpleInfo 5 | 6 | object FakeCreatorGenerator { 7 | 8 | fun generate(id: Int): Creator { 9 | return Creator( 10 | id = id, 11 | fullName = "CreatorFullName$id", 12 | thumbnailPath = "CreatorThumbnailPath$id", 13 | thumbnailExtension = "CreatorThumbnailExtension$id", 14 | detailUrl = "CreatorDetailUrl$id", 15 | copyright = "CreatorCopyright$id", 16 | attributionText = "CreatorAttributionText$id", 17 | comicInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "")), 18 | seriesInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "")), 19 | storyInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "")), 20 | eventInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "")) 21 | ) 22 | } 23 | 24 | fun generate(offset: Int, limit: Int): List { 25 | val fakeCreatorList = mutableListOf() 26 | 27 | for (i in offset until limit) { 28 | fakeCreatorList.add( 29 | Creator( 30 | id = i, 31 | fullName = "CreatorFullName$i", 32 | thumbnailPath = "CreatorThumbnailPath$i", 33 | thumbnailExtension = "CreatorThumbnailExtension$i", 34 | detailUrl = "CreatorDetailUrl$i", 35 | copyright = "CreatorCopyright$i", 36 | attributionText = "CreatorAttributionText$i", 37 | comicInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "")), 38 | seriesInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "")), 39 | storyInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "")), 40 | eventInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "")) 41 | ) 42 | ) 43 | } 44 | 45 | return fakeCreatorList 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/fake/data/FakeFavoriteGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.fake.data 2 | 3 | import com.gun.domain.common.* 4 | import com.gun.domain.model.favorite.Favorite 5 | 6 | object FakeFavoriteGenerator { 7 | 8 | private val availableContentType = mutableListOf( 9 | SeriesType, 10 | ComicType, 11 | EventType, 12 | CharacterType, 13 | CreatorType 14 | ) 15 | 16 | fun generate(contentType: ContentType?, count: Int): List { 17 | val fakeFavoriteList = mutableListOf() 18 | 19 | for (i in 1 until count) { 20 | fakeFavoriteList.add(Favorite( 21 | id = i, 22 | name = "name$i", 23 | thumbnailPath = "thumbnailPath$i", 24 | thumbnailExtension = "thumbnailExtension$i", 25 | contentType = contentType ?: availableContentType.random() 26 | )) 27 | } 28 | 29 | return fakeFavoriteList 30 | } 31 | 32 | fun generateOnOfEachByFilterAll(): List { 33 | val fakeFavoriteList = mutableListOf() 34 | 35 | for (i in 0 until availableContentType.size) { 36 | fakeFavoriteList.add( 37 | Favorite( 38 | id = i, 39 | name = "name$i", 40 | thumbnailPath = "thumbnailPath$i", 41 | thumbnailExtension = "thumbnailExtension$i", 42 | contentType = availableContentType[i] 43 | ) 44 | ) 45 | } 46 | 47 | return fakeFavoriteList 48 | } 49 | } -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/fake/data/FakeHomeListGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.fake.data 2 | 3 | import com.gun.domain.model.home.HomeList 4 | 5 | object FakeHomeListGenerator { 6 | 7 | fun generate(offset: Int, limit: Int) = HomeList( 8 | characterList = FakeCharacterGenerator.generate(offset, limit), 9 | comicList = FakeComicGenerator.generate(offset, limit), 10 | creatorList = FakeCreatorGenerator.generate(offset, limit), 11 | eventList = FakeEventGenerator.generate(offset, limit), 12 | seriesList = FakeSeriesGenerator.generate(offset, limit 13 | ) 14 | ) 15 | 16 | } -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/fake/data/FakeSearchResultGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.fake.data 2 | 3 | import com.gun.domain.model.search.SearchResult 4 | 5 | object FakeSearchResultGenerator { 6 | 7 | fun generate(id: Int): SearchResult { 8 | return SearchResult( 9 | id = id, 10 | name = "name$id", 11 | thumbnailPath = "thumbnailPath$id", 12 | thumbnailExtension = "thumbnailExtension$id", 13 | modified = "modified$id" 14 | ) 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/fake/data/FakeSeriesGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.fake.data 2 | 3 | import com.gun.domain.model.Series 4 | import com.gun.domain.model.SimpleInfo 5 | 6 | object FakeSeriesGenerator { 7 | 8 | fun generate(id: Int): Series { 9 | return Series( 10 | id = id, 11 | title = "SeriesTitle$id", 12 | description = "SeriesDescription$id", 13 | startYear = id, 14 | endYear = id, 15 | rating = "SeriesRating$id", 16 | thumbnailPath = "SeriesThumbnailPath$id", 17 | thumbnailExtension = "SeriesThumbnailExtension$id", 18 | detailUrl = "SeriesDetailUrl$id", 19 | copyright = "SeriesCopyright$id", 20 | attributionText = "SeriesAttributionText$id", 21 | creatorInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "Role$id")), 22 | characterInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "")), 23 | storyInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "")), 24 | comicInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "")), 25 | eventInfoList = listOf(SimpleInfo("Uri$id", "Name$id", "")) 26 | ) 27 | } 28 | 29 | fun generate(offset: Int, limit: Int): List { 30 | val fakeSeriesList = mutableListOf() 31 | 32 | for (i in offset until limit) { 33 | fakeSeriesList.add( 34 | Series( 35 | id = i, 36 | title = "SeriesTitle$i", 37 | description = "SeriesDescription$i", 38 | startYear = i, 39 | endYear = i, 40 | rating = "SeriesRating$i", 41 | thumbnailPath = "SeriesThumbnailPath$i", 42 | thumbnailExtension = "SeriesThumbnailExtension$i", 43 | detailUrl = "SeriesDetailUrl$i", 44 | copyright = "SeriesCopyright$i", 45 | attributionText = "SeriesAttributionText$i", 46 | creatorInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "Role$i")), 47 | characterInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "")), 48 | storyInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "")), 49 | comicInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "")), 50 | eventInfoList = listOf(SimpleInfo("Uri$i", "Name$i", "")) 51 | ) 52 | ) 53 | } 54 | 55 | return fakeSeriesList 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/fake/usecase/FakeDeleteFavoriteUseCaseImpl.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.fake.usecase 2 | 3 | import com.gun.domain.model.favorite.Favorite 4 | import com.gun.domain.usecase.DeleteUseCase 5 | import kotlinx.coroutines.flow.MutableSharedFlow 6 | 7 | class FakeDeleteFavoriteUseCaseImpl : DeleteUseCase.DeleteFavoriteUseCase { 8 | private val fakeFlow = MutableSharedFlow>() 9 | suspend fun emit(value: Result) = fakeFlow.emit(value) 10 | 11 | override fun invoke(favorite: Favorite) = fakeFlow 12 | } -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/fake/usecase/FakeGetDetailDataUseCaseImpl.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.fake.usecase 2 | 3 | import com.gun.domain.common.ContentType 4 | import com.gun.domain.model.detail.ContentDetail 5 | import com.gun.domain.usecase.GetUseCase 6 | import kotlinx.coroutines.flow.MutableSharedFlow 7 | 8 | class FakeGetDetailDataUseCaseImpl : GetUseCase.GetDetailDataUseCase { 9 | private val fakeFlow = MutableSharedFlow>() 10 | suspend fun emit(value: Result) = fakeFlow.emit(value) 11 | 12 | override fun invoke(contentId: Int, contentType: ContentType) = fakeFlow 13 | } -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/fake/usecase/FakeGetFavoriteDataUseCaseImpl.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.fake.usecase 2 | 3 | import com.gun.domain.common.ContentType 4 | import com.gun.domain.model.favorite.Favorite 5 | import com.gun.domain.usecase.GetUseCase 6 | import kotlinx.coroutines.flow.MutableSharedFlow 7 | import kotlin.time.Duration 8 | 9 | class FakeGetFavoriteDataUseCaseImpl : GetUseCase.GetFavoriteUseCase { 10 | private val fakeFlow = MutableSharedFlow>>() 11 | suspend fun emit(value: Result>) = fakeFlow.emit(value) 12 | 13 | override fun invoke(contentType: ContentType?, shimmerDuration: Duration?) = fakeFlow 14 | } -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/fake/usecase/FakeGetHomeDataUseCaseImpl.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.fake.usecase 2 | 3 | import com.gun.domain.model.home.HomeList 4 | import com.gun.domain.usecase.GetUseCase 5 | import kotlinx.coroutines.flow.MutableSharedFlow 6 | 7 | class FakeGetHomeDataUseCaseImpl : GetUseCase.GetHomeDataUseCase { 8 | private val fakeFlow = MutableSharedFlow>() 9 | suspend fun emit(value: Result) = fakeFlow.emit(value) 10 | 11 | override operator fun invoke(offset: Int, limit: Int) = fakeFlow 12 | } -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/fake/usecase/FakeInsertFavoriteUseCaseImpl.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.fake.usecase 2 | 3 | import com.gun.domain.model.favorite.Favorite 4 | import com.gun.domain.usecase.InsertUseCase 5 | import kotlinx.coroutines.flow.MutableSharedFlow 6 | 7 | class FakeInsertFavoriteUseCaseImpl : InsertUseCase.InsertFavoriteUseCase { 8 | private val fakeFlow = MutableSharedFlow>() 9 | suspend fun emit(value: Result) = fakeFlow.emit(value) 10 | 11 | override fun invoke(favorite: Favorite) = fakeFlow 12 | } -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/test/TestDiffCallback.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.test 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | 5 | class TestDiffCallback : DiffUtil.ItemCallback() { 6 | override fun areItemsTheSame(oldItem: T & Any, newItem: T & Any): Boolean { 7 | return oldItem == newItem 8 | } 9 | 10 | override fun areContentsTheSame(oldItem: T & Any, newItem: T & Any): Boolean { 11 | return oldItem == newItem 12 | } 13 | } -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/test/TestListCallback.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.test 2 | 3 | import androidx.recyclerview.widget.ListUpdateCallback 4 | 5 | class TestListCallback : ListUpdateCallback { 6 | override fun onChanged(position: Int, count: Int, payload: Any?) {} 7 | override fun onMoved(fromPosition: Int, toPosition: Int) {} 8 | override fun onInserted(position: Int, count: Int) {} 9 | override fun onRemoved(position: Int, count: Int) {} 10 | } 11 | -------------------------------------------------------------------------------- /presentation/src/test/java/com/gun/presentation/test/TestPagingDataConsumer.kt: -------------------------------------------------------------------------------- 1 | package com.gun.presentation.test 2 | 3 | import androidx.paging.AsyncPagingDataDiffer 4 | import androidx.paging.PagingData 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.launch 7 | import kotlinx.coroutines.test.advanceUntilIdle 8 | import kotlinx.coroutines.test.runTest 9 | 10 | class TestPagingDataConsumer

{ 11 | 12 | /** 13 | * PagingData 는 값을 추출할 수 없으므로, PagingData 를 RecyclerView 에 매핑하는 헬퍼 클래스 사용 14 | * */ 15 | val testPagingDataDiffer = AsyncPagingDataDiffer

( 16 | diffCallback = TestDiffCallback(), 17 | updateCallback = TestListCallback(), 18 | workerDispatcher = Dispatchers.Main, 19 | ) 20 | 21 | /** 22 | * `PagingData`를 `PagingDataDiffer`에 Submit 23 | * 24 | * - PagingDataAdapter(`Paging`용 RecyclerViewAdapter) 에 데이터를 전달하는 것과 같은 역할을 하며 25 | * PagingDiffer 로 데이터 전달 시, 성공/실패 데이터에 따라 userViewModel.loadStateListener 로 상태 수신되어 에러, 로딩 상태를 업데이트 한다. 26 | * 27 | * - 직접적으로 확인할 수 없는 PagingData 를 `PagingDataDiffer`에 데이터 Submit 후, 28 | * PagingDataDiffer.snapshot() 호출을 통해 세부 데이터를 확인 할 수 있다. 29 | * */ 30 | fun submitPagingDataToDiffer(pagingData: PagingData

) = runTest { 31 | val job = launch { 32 | testPagingDataDiffer.submitData(pagingData) 33 | } 34 | 35 | // 대기열에 남은 항목이 없을 때까지 스케줄러에서 다른 코루틴을 모두 실행 36 | advanceUntilIdle() 37 | 38 | job.cancel() 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /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 = "Android_MVVM_CleanArchitecture" 17 | 18 | include( 19 | ":presentation", 20 | ":domain", 21 | ":data" 22 | ) 23 | --------------------------------------------------------------------------------