├── .circleci └── config.yml ├── .gitignore ├── .idea ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── dictionaries │ └── abdullah.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── keystores │ └── debug.keystore ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── mobiaxe │ │ └── wasd │ │ └── ExampleInstrumentedTest.kt │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── mobiaxe │ │ └── wasd │ │ ├── WASDApplication.kt │ │ ├── dashboard │ │ └── DashboardActivity.kt │ │ ├── koin │ │ └── Modules.kt │ │ ├── navigation │ │ ├── IntentLoader.kt │ │ └── Navigation.kt │ │ ├── splash │ │ ├── data │ │ │ ├── datasource │ │ │ │ └── SplashCachedDataSource.kt │ │ │ └── repository │ │ │ │ └── SplashDataRepository.kt │ │ ├── domain │ │ │ ├── datasource │ │ │ │ └── SplashCachedDataSourceImpl.kt │ │ │ ├── repository │ │ │ │ └── SplashRepository.kt │ │ │ └── usecase │ │ │ │ └── OnboardingPassed.kt │ │ └── presentation │ │ │ ├── SplashActivity.kt │ │ │ └── SplashViewModel.kt │ │ └── util │ │ └── BindingUtils.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_launcher_background.xml │ └── splash_screen.jpg │ ├── font │ ├── moon_bold_v2.otf │ ├── moon_light_v2.otf │ ├── panton_black.otf │ ├── panton_bold.otf │ ├── panton_light.otf │ └── panton_regular.otf │ ├── layout │ └── activity_dashboard.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── navigation │ └── nav_app_graph.xml │ ├── values-v28 │ └── styles.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ ├── styles.xml │ └── text_appearances.xml ├── build.gradle.kts ├── buildSrc ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── AndroidConfig.kt │ ├── CoreVersion.kt │ ├── GradleDependency.kt │ ├── LibraryDependency.kt │ ├── ModuleDependency.kt │ ├── extension │ ├── ProjectExt.kt │ └── RepositoryHandler.kt │ └── utils │ └── BuildUtils.kt ├── core ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── mobiaxe │ │ └── core │ │ └── ExampleInstrumentedTest.kt │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── mobiaxe │ │ └── core │ │ ├── cache │ │ ├── PreferencesHelper.kt │ │ ├── WASDCacheImpl.kt │ │ ├── WASDCachedDataStore.kt │ │ └── db │ │ │ ├── CachedWASDDao.kt │ │ │ ├── CoverConverter.kt │ │ │ ├── ListConverter.kt │ │ │ ├── WASDConstants.kt │ │ │ └── WASDDatabase.kt │ │ ├── data │ │ ├── Resource.kt │ │ ├── Status.kt │ │ └── WASDModel.kt │ │ ├── domain │ │ ├── SimpleUseCase.kt │ │ ├── SuspendUseCase.kt │ │ └── SynchronouosUseCase.kt │ │ ├── extension │ │ ├── ContextExt.kt │ │ ├── LifecycleExt.kt │ │ └── UtilExt.kt │ │ ├── koin │ │ └── Modules.kt │ │ ├── presentation │ │ ├── BaseActivity.kt │ │ ├── BaseFragment.kt │ │ ├── BaseViewModel.kt │ │ ├── DataBindingAdapter.kt │ │ └── ListItemViewModel.kt │ │ └── remote │ │ ├── IGDBRequest.kt │ │ ├── WASDRemoteDataStore.kt │ │ ├── WASDRemoteImpl.kt │ │ └── service │ │ ├── WASDService.kt │ │ └── WASDServiceFactory.kt │ └── res │ ├── layout │ └── list_item.xml │ └── values │ └── strings.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── home ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── mobiaxe │ │ └── home │ │ ├── data │ │ ├── SectionData.kt │ │ ├── datasource │ │ │ ├── GamesPagingDataSource.kt │ │ │ └── HomeRemoteDataSource.kt │ │ └── repository │ │ │ └── HomeDataRepository.kt │ │ ├── domain │ │ ├── datasource │ │ │ ├── GamesDataSourceFactory.kt │ │ │ └── HomeRemoteDataSourceImpl.kt │ │ ├── repository │ │ │ └── HomeRepository.kt │ │ └── usecase │ │ │ └── GetPopularGames.kt │ │ ├── koin │ │ └── Modules.kt │ │ ├── presentation │ │ ├── GameListAdapter.kt │ │ ├── HomeFragment.kt │ │ ├── HomeViewModel.kt │ │ ├── RecentlyReleasedListAdapter.kt │ │ └── SectionListDataAdapter.kt │ │ └── util │ │ └── BindingUtils.kt │ └── res │ ├── layout │ ├── fragment_home.xml │ ├── item_game.xml │ ├── item_network_state.xml │ ├── item_recently_released.xml │ └── item_section.xml │ └── values │ └── strings.xml ├── keystore.properties ├── misc ├── home.jpg ├── onboarding_1.jpg ├── onboarding_2.jpg ├── onboarding_3.jpg └── select_platform.jpg ├── onboarding ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── mobiaxe │ │ └── onboarding │ │ ├── data │ │ ├── OnboardingData.kt │ │ ├── datasource │ │ │ └── OnboardingCachedDataSource.kt │ │ └── repository │ │ │ └── OnboardingDataRepository.kt │ │ ├── domain │ │ ├── datasource │ │ │ └── OnboardingCachedDataSourceImpl.kt │ │ ├── repository │ │ │ └── OnboardingRepository.kt │ │ └── usecase │ │ │ └── SavePlatform.kt │ │ ├── koin │ │ └── Modules.kt │ │ ├── presentation │ │ ├── OnboardingActivity.kt │ │ ├── OnboardingAdapter.kt │ │ ├── OnboardingFragment.kt │ │ ├── OnboardingViewModel.kt │ │ ├── ParallaxPageTransformer.kt │ │ └── SelectPlatformFragment.kt │ │ └── util │ │ └── BindingUtils.kt │ └── res │ ├── color │ └── onboarding_continue_btn_color.xml │ ├── drawable-xxxhdpi │ ├── godofwar_onboarding_two.jpg │ ├── platform_background.jpg │ ├── rdr_onboarding_three.jpg │ └── witcher_onboarding_one.jpg │ ├── drawable │ ├── ic_nintendo.xml │ ├── ic_nintendo_colored.xml │ ├── ic_nintendo_white.xml │ ├── ic_onboarding_left.xml │ ├── ic_onboarding_right.xml │ ├── ic_pc_colored.xml │ ├── ic_pc_white.xml │ ├── ic_playstation.xml │ ├── ic_ps4_colored.xml │ ├── ic_ps4_white.xml │ ├── ic_tick.xml │ ├── ic_windows.xml │ ├── ic_xbox.xml │ ├── ic_xbox_colored.xml │ ├── ic_xbox_white.xml │ ├── tab_page_four_selector.xml │ ├── tab_page_one_selector.xml │ ├── tab_page_three_selector.xml │ └── tab_page_two_selector.xml │ ├── layout │ ├── activity_onboarding.xml │ ├── fragment_onboarding.xml │ └── fragment_platform.xml │ ├── values │ ├── arrays.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── onboarding_scene.xml └── settings.gradle.kts /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | android: circleci/android@0.2.0 5 | 6 | jobs: 7 | build: 8 | filters: 9 | branches: 10 | only: 11 | - master 12 | 13 | working_directory: ~/Modular 14 | 15 | docker: 16 | - image: circleci/android:api-29 17 | 18 | environment: 19 | GRADLE_OPTS: -Xmx4g -Dorg.gradle.daemon=false 20 | JVM_OPTS: -Xmx4g 21 | 22 | executor: android/android 23 | 24 | steps: 25 | - checkout 26 | - run: 27 | name: Build Debug 28 | command: ./gradlew assembleDebug 29 | - run: 30 | name: Build 31 | command: ./gradlew build 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | WASD -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | xmlns:android 17 | 18 | ^$ 19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | xmlns:.* 28 | 29 | ^$ 30 | 31 | 32 | BY_NAME 33 | 34 |
35 |
36 | 37 | 38 | 39 | .*:id 40 | 41 | http://schemas.android.com/apk/res/android 42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | .*:name 51 | 52 | http://schemas.android.com/apk/res/android 53 | 54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | name 62 | 63 | ^$ 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | style 73 | 74 | ^$ 75 | 76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | .* 84 | 85 | ^$ 86 | 87 | 88 | BY_NAME 89 | 90 |
91 |
92 | 93 | 94 | 95 | .* 96 | 97 | http://schemas.android.com/apk/res/android 98 | 99 | 100 | ANDROID_ATTRIBUTE_ORDER 101 | 102 |
103 |
104 | 105 | 106 | 107 | .* 108 | 109 | .* 110 | 111 | 112 | BY_NAME 113 | 114 |
115 |
116 |
117 |
118 | 119 | 121 |
122 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/dictionaries/abdullah.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | wasd 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project description 2 | [![CircleCI](https://circleci.com/gh/abalta/WASD-Modular.svg?style=shield&circle-token=8867ef8e7edeacd352f0090618f7c873e5346799)](https://circleci.com/gh/abalta/WASD-Modular) 3 | [![Kotlin Version](https://img.shields.io/badge/Kotlin-1.3.61-blue.svg)](https://kotlinlang.org) 4 | [![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21) 5 | [![Android Studio](https://img.shields.io/badge/Android%20Studio-3.6.1-informational.svg)](https://developer.android.com/studio) 6 | 7 | This project is open source part for my ongoing development. I present modern Android application architecture that is modular with dynamic feature. 8 | 9 | ## Project characteristics 10 | 11 | This project brings to table set of best practices, tools, and solutions: 12 | 13 | * Modern architecture (feature modules, Clean Architecture, Model-View-ViewModel) 14 | * [Android Jetpack](https://developer.android.com/jetpack) 15 | * CI pipeline 16 | * Dependency Injection with Koin 17 | * Material design 18 | 19 | ## Tech-stack 20 | 21 | * Libraries and Frameworks 22 | * [Kotlin](https://kotlinlang.org/) + [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) - perform background operations 23 | * [Koin](https://insert-koin.io/) - dependency injection 24 | * [Retrofit](https://square.github.io/retrofit/) - networking 25 | * [Jetpack](https://developer.android.com/jetpack) 26 | * [Navigation](https://developer.android.com/topic/libraries/architecture/navigation/) - deal with whole in-app navigation 27 | * [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) - notify views about database change 28 | * [Lifecycle](https://developer.android.com/topic/libraries/architecture/lifecycle) - perform action when lifecycle state changes 29 | * [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) - store and manage UI-related data in a lifecycle conscious way 30 | * [Room](https://developer.android.com/topic/libraries/architecture/room) - persistence library provides an abstraction layer over SQLite to allow for more robust database access while harnessing the full power of SQLite. 31 | * [Pagination](https://developer.android.com/topic/libraries/architecture/paging) - helps you load and display small chunks of data at a time. Loading partial data on demand reduces usage of network bandwidth and system resources. 32 | * [Data Binding](https://developer.android.com/topic/libraries/data-binding) - allows you to bind UI components in your layouts to data sources in your app using a declarative format rather than programmatically. 33 | * [Stetho](http://facebook.github.io/stetho/) - application debugging tool 34 | * Architecture 35 | * Clean Architecture (at module level) 36 | * MVVM Pattern 37 | * [Dynamic feature modules](https://developer.android.com/studio/projects/dynamic-delivery) 38 | * Gradle 39 | * [Gradle Kotlin DSL](https://docs.gradle.org/current/userguide/kotlin_dsl.html) 40 | 41 | ## Getting Started 42 | 43 | This project uses IGDB Api for fetching datas. You need to get Api key for services the app uses. 44 | 45 | - IGDB: https://api-docs.igdb.com/#about 46 | 47 | When you obtain the key, you can provide them to the app by putting the following in the `gradle.properties` project root file: 48 | 49 | ```properties 50 | #IGDB API KEY 51 | apiToken = 52 | ``` 53 | 54 | ## Architecture 55 | 56 | Each module has own Clean Architecture layers. 57 | 58 | - `:core` module only responsible for implement common dependencies, holds remote (Retrofit) and cache (Room) repositories. 59 | - `:onboarding` module is install-time delivery feature module, this module install when app is downloaded. (Uninstall this module is upcoming development progress.) 60 | - `:home` module is install-time delivery feature module, this module install when app is downloaded. 61 | - `:app` this is simple application module, this is responsible for navigate feature modules. 62 | 63 | ## Showroom - UI 64 | 65 | [OnboardingActivity](https://github.com/abalta/Kotlin-DynamicFeature-Clean/blob/master/onboarding/src/main/java/com/mobiaxe/onboarding/presentation/OnboardingActivity.kt) 66 | 67 | | Module | OnboardingFragments | ViewPager2 - TabLayout | MaterialAndroid | SelectPlatformFragment | 68 | |---------------|-----------------------------------------------|-----------------------------------------------|-----------------------------------------------|---------------------------------------------------------| 69 | | `:onboarding` | | | | | 70 | 71 | [DashboardActivity](https://github.com/abalta/Kotlin-DynamicFeature-Clean/blob/master/app/src/main/java/com/mobiaxe/wasd/dashboard/DashboardActivity.kt) 72 | 73 | | Module | HomeFragment - BottomNavigationView | Pagination - Horizontal RecyclerView | NavigationComponent | 74 | |---------------|-----------------------------------------------|-----------------------------------------------|-----------------------------------------------| 75 | | `:home` | | | | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import extension.getLocalProperty 2 | 3 | plugins { 4 | id(GradlePluginId.ANDROID_APPLICATION) 5 | id(GradlePluginId.KOTLIN_ANDROID) 6 | id(GradlePluginId.KOTLIN_ANDROID_EXTENSIONS) 7 | id(GradlePluginId.KOTLIN_KAPT) 8 | } 9 | 10 | android { 11 | compileSdkVersion(AndroidConfig.COMPILE_SDK_VERSION) 12 | 13 | defaultConfig { 14 | applicationId = AndroidConfig.APPLICATION_ID 15 | minSdkVersion(AndroidConfig.MIN_SDK_VERSION) 16 | targetSdkVersion(AndroidConfig.TARGET_SDK_VERSION) 17 | buildToolsVersion(AndroidConfig.BUILD_TOOLS_VERSION) 18 | 19 | versionCode = AndroidConfig.VERSION_CODE 20 | versionName = AndroidConfig.VERSION_NAME 21 | testInstrumentationRunner = AndroidConfig.TEST_INSTRUMENTATION_RUNNER 22 | } 23 | 24 | signingConfigs { 25 | create(BuildType.RELEASE) { 26 | keyAlias = getLocalProperty("keyAlias") 27 | keyPassword = getLocalProperty("keyPassword") 28 | storeFile = file(getLocalProperty("storeFile")) 29 | storePassword = getLocalProperty("storePassword") 30 | } 31 | getByName(BuildType.DEBUG) { 32 | keyAlias = SigningConfig.DEBUG.KEY_ALIAS 33 | keyPassword = SigningConfig.DEBUG.KEY_PASSWORD 34 | storeFile = file(SigningConfig.DEBUG.STORE_FILE) 35 | storePassword = SigningConfig.DEBUG.STORE_PASSWORD 36 | } 37 | } 38 | 39 | buildTypes { 40 | getByName(BuildType.DEBUG) { 41 | applicationIdSuffix = ".debug" 42 | versionNameSuffix = "-debug" 43 | isMinifyEnabled = false 44 | isDebuggable = true 45 | signingConfig = signingConfigs.getByName(BuildType.DEBUG) 46 | } 47 | 48 | getByName(BuildType.RELEASE) { 49 | isMinifyEnabled = true 50 | signingConfig = signingConfigs.getByName(BuildType.RELEASE) 51 | proguardFile(getDefaultProguardFile("proguard-android.txt")) 52 | proguardFile(file("proguard-rules.pro")) 53 | } 54 | } 55 | 56 | dataBinding { 57 | isEnabled = true 58 | } 59 | 60 | dynamicFeatures = mutableSetOf( 61 | ModuleDependency.FEATURE_ONBOARDING, 62 | ModuleDependency.FEATURE_HOME) 63 | } 64 | 65 | dependencies { 66 | api(project(ModuleDependency.CORE)) 67 | 68 | implementation(Library.NAVIGATION_FRAGMENT_KTX) 69 | implementation(Library.NAVIGATION_DYNAMIC_FEATURE) 70 | implementation(Library.NAVIGATION_UI_KTX) 71 | 72 | implementation(Library.STETHO) 73 | } 74 | -------------------------------------------------------------------------------- /app/keystores/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/keystores/debug.keystore -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.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 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/mobiaxe/wasd/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.wasd 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.mobiaxe.wasd", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobiaxe/wasd/WASDApplication.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.wasd 2 | 3 | import android.app.Application 4 | import com.facebook.stetho.Stetho 5 | import com.mobiaxe.core.koin.coreModule 6 | import com.mobiaxe.wasd.koin.repositoryModule 7 | import com.mobiaxe.wasd.koin.useCaseModule 8 | import com.mobiaxe.wasd.koin.viewModelModule 9 | import org.koin.android.ext.koin.androidContext 10 | import org.koin.core.context.startKoin 11 | 12 | class WASDApplication: Application() { 13 | 14 | override fun onCreate() { 15 | super.onCreate() 16 | startKoin { 17 | androidContext(this@WASDApplication) 18 | modules(listOf( 19 | coreModule, 20 | viewModelModule, 21 | useCaseModule, 22 | repositoryModule 23 | )) 24 | } 25 | Stetho.initializeWithDefaults(this) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobiaxe/wasd/dashboard/DashboardActivity.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.wasd.dashboard 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.databinding.DataBindingUtil 6 | import androidx.navigation.Navigation 7 | import androidx.navigation.ui.setupWithNavController 8 | import com.mobiaxe.wasd.R 9 | import com.mobiaxe.wasd.databinding.ActivityDashboardBinding 10 | 11 | class DashboardActivity : AppCompatActivity() { 12 | 13 | private var binding: ActivityDashboardBinding? = null 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | binding = DataBindingUtil.setContentView(this, R.layout.activity_dashboard) 18 | setupBottomNavigation() 19 | } 20 | 21 | private fun setupBottomNavigation() { 22 | binding?.let { 23 | Navigation.findNavController(this, R.id.nav_host_fragment).apply { 24 | it.bottomNav.setupWithNavController(this) 25 | } 26 | } 27 | } 28 | 29 | override fun onBackPressed() { 30 | finishAfterTransition() 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobiaxe/wasd/koin/Modules.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.wasd.koin 2 | 3 | import com.mobiaxe.wasd.splash.data.datasource.SplashCachedDataSource 4 | import com.mobiaxe.wasd.splash.data.repository.SplashDataRepository 5 | import com.mobiaxe.wasd.splash.domain.datasource.SplashCachedDataSourceImpl 6 | import com.mobiaxe.wasd.splash.domain.repository.SplashRepository 7 | import com.mobiaxe.wasd.splash.domain.usecase.OnboardingPassed 8 | import com.mobiaxe.wasd.splash.presentation.SplashViewModel 9 | import org.koin.androidx.viewmodel.dsl.viewModel 10 | import org.koin.dsl.module 11 | 12 | val viewModelModule = module { 13 | viewModel { SplashViewModel(get()) } 14 | } 15 | 16 | val useCaseModule = module { 17 | factory { OnboardingPassed(get()) } 18 | } 19 | 20 | val repositoryModule = module { 21 | factory { SplashDataRepository(get()) } 22 | factory { SplashCachedDataSourceImpl(get()) } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobiaxe/wasd/navigation/IntentLoader.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.wasd.navigation 2 | 3 | import android.content.Intent 4 | import com.mobiaxe.wasd.BuildConfig 5 | 6 | private fun intentTo(className: String): Intent = 7 | Intent(Intent.ACTION_VIEW).setClassName(BuildConfig.APPLICATION_ID, className) 8 | 9 | internal fun String.loadIntentOrNull(): Intent? = 10 | try { 11 | Class.forName(this).run { intentTo(this@loadIntentOrNull) } 12 | } catch (e: ClassNotFoundException) { 13 | null 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/mobiaxe/wasd/navigation/Navigation.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.wasd.navigation 2 | 3 | import android.content.Intent 4 | 5 | object Navigation { 6 | 7 | private const val ONBOARDING = "com.mobiaxe.onboarding.presentation.OnboardingActivity" 8 | private const val DASHBOARD = "com.mobiaxe.wasd.dashboard.DashboardActivity" 9 | 10 | fun navigateOnboardingOrDashboard(isNavigateDashboard: Boolean): Intent? = 11 | if (isNavigateDashboard) { 12 | DASHBOARD.loadIntentOrNull() 13 | } else { 14 | ONBOARDING.loadIntentOrNull() 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobiaxe/wasd/splash/data/datasource/SplashCachedDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.wasd.splash.data.datasource 2 | 3 | interface SplashCachedDataSource { 4 | 5 | fun isOnboardingShown(isShowOnboarding: Boolean) 6 | 7 | fun isOnboardingPassed(): Boolean 8 | 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobiaxe/wasd/splash/data/repository/SplashDataRepository.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.wasd.splash.data.repository 2 | 3 | import com.mobiaxe.wasd.splash.data.datasource.SplashCachedDataSource 4 | import com.mobiaxe.wasd.splash.domain.repository.SplashRepository 5 | 6 | class SplashDataRepository(private val splashCachedDataSource: SplashCachedDataSource): SplashRepository { 7 | 8 | override fun isOnboardingShow(isShown: Boolean) { 9 | splashCachedDataSource.isOnboardingShown(isShown) 10 | } 11 | 12 | override fun isOnboardingPass(): Boolean = 13 | splashCachedDataSource.isOnboardingPassed() 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobiaxe/wasd/splash/domain/datasource/SplashCachedDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.wasd.splash.domain.datasource 2 | 3 | import com.mobiaxe.core.cache.PreferencesHelper 4 | import com.mobiaxe.wasd.splash.data.datasource.SplashCachedDataSource 5 | 6 | class SplashCachedDataSourceImpl constructor(private val preferencesHelper: PreferencesHelper): SplashCachedDataSource { 7 | 8 | override fun isOnboardingShown(isShowOnboarding: Boolean) { 9 | preferencesHelper.onboardingShow = isShowOnboarding 10 | } 11 | 12 | override fun isOnboardingPassed(): Boolean = 13 | preferencesHelper.onboardingShow 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobiaxe/wasd/splash/domain/repository/SplashRepository.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.wasd.splash.domain.repository 2 | 3 | interface SplashRepository { 4 | 5 | fun isOnboardingShow(isShown: Boolean) 6 | 7 | fun isOnboardingPass(): Boolean 8 | 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobiaxe/wasd/splash/domain/usecase/OnboardingPassed.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.wasd.splash.domain.usecase 2 | 3 | import com.mobiaxe.core.domain.SynchronouosUseCase 4 | import com.mobiaxe.wasd.splash.domain.repository.SplashRepository 5 | 6 | class OnboardingPassed( 7 | private val splashRepository: SplashRepository 8 | ): SynchronouosUseCase { 9 | 10 | override fun execute(params: Boolean): Boolean { 11 | return if (splashRepository.isOnboardingPass()) { 12 | true 13 | } else { 14 | splashRepository.isOnboardingShow(params) 15 | params 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobiaxe/wasd/splash/presentation/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.wasd.splash.presentation 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.mobiaxe.core.extension.observe 6 | import com.mobiaxe.core.extension.start 7 | import com.mobiaxe.wasd.navigation.Navigation 8 | import org.koin.androidx.viewmodel.ext.android.getViewModel 9 | 10 | class SplashActivity: AppCompatActivity() { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | 15 | with(getViewModel()) { 16 | isOnboardingPassed() 17 | observe(showOnboardingLiveData) { 18 | start(Navigation.navigateOnboardingOrDashboard(it)) 19 | finish() 20 | } 21 | } 22 | 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobiaxe/wasd/splash/presentation/SplashViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.wasd.splash.presentation 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import com.mobiaxe.core.presentation.BaseViewModel 6 | import com.mobiaxe.wasd.splash.domain.usecase.OnboardingPassed 7 | 8 | class SplashViewModel( 9 | private val onboardingPassed: OnboardingPassed 10 | ): BaseViewModel() { 11 | private val _showOnboardingLiveData = MutableLiveData() 12 | val showOnboardingLiveData: LiveData = _showOnboardingLiveData 13 | 14 | fun isOnboardingPassed() { 15 | _showOnboardingLiveData.value = onboardingPassed.execute(false) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mobiaxe/wasd/util/BindingUtils.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.wasd.util 2 | 3 | import androidx.databinding.BindingAdapter 4 | import androidx.recyclerview.widget.RecyclerView 5 | import com.google.android.material.card.MaterialCardView 6 | import com.mobiaxe.core.presentation.DataBindingAdapter 7 | 8 | @BindingAdapter("app:isSelected") 9 | fun MaterialCardView.setSelected(isSelected: Boolean) { 10 | isChecked = isSelected 11 | isHovered = isSelected 12 | isPressed = isSelected 13 | } 14 | 15 | @BindingAdapter("app:recycledViewPool") 16 | fun RecyclerView.setRecyclerViewPoolBinding(pool: RecyclerView.RecycledViewPool) { 17 | setRecycledViewPool(pool) 18 | } 19 | 20 | @BindingAdapter("app:submitList") 21 | fun setRecyclerViewProperties(recyclerView: RecyclerView, items: List?) { 22 | if (recyclerView.adapter is DataBindingAdapter<*>) { 23 | items?.let { 24 | (recyclerView.adapter as DataBindingAdapter).submitList(it) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash_screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/drawable/splash_screen.jpg -------------------------------------------------------------------------------- /app/src/main/res/font/moon_bold_v2.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/font/moon_bold_v2.otf -------------------------------------------------------------------------------- /app/src/main/res/font/moon_light_v2.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/font/moon_light_v2.otf -------------------------------------------------------------------------------- /app/src/main/res/font/panton_black.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/font/panton_black.otf -------------------------------------------------------------------------------- /app/src/main/res/font/panton_bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/font/panton_bold.otf -------------------------------------------------------------------------------- /app/src/main/res/font/panton_light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/font/panton_light.otf -------------------------------------------------------------------------------- /app/src/main/res/font/panton_regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/font/panton_regular.otf -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_dashboard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 22 | 23 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_app_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values-v28/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | #FFFFFF 8 | #4DFFFFFF 9 | #222d45 10 | #99222d45 11 | #171414 12 | 13 | #07152A 14 | #0A2143 15 | #92B4C2 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 32sp 5 | 24sp 6 | 20sp 7 | 18sp 8 | 16sp 9 | 14sp 10 | 12sp 11 | 10sp 12 | 13 | 16dp 14 | 16dp 15 | 16 | 132dp 17 | 192dp 18 | 224dp 19 | 20 | 16dp 21 | 12dp 22 | 8dp 23 | 12dp 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | WASD 3 | Onboarding 4 | Home 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 14 | 15 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/text_appearances.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 15 | 16 | 19 | 20 | 21 | 22 | 25 | 26 | 29 | 30 | 33 | 34 | 35 | 36 | 39 | 40 | 43 | 44 | 47 | 48 | 49 | 50 | 53 | 54 | 57 | 58 | 61 | 62 | 63 | 64 | 67 | 68 | 71 | 72 | 75 | 76 | 77 | 78 | 81 | 82 | 85 | 86 | 89 | 90 | 91 | 92 | 93 | 94 | 97 | 98 | 101 | 102 | 105 | 106 | 109 | 110 | 113 | 114 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import extension.applyDefaults 2 | 3 | plugins { 4 | id(GradlePluginId.GRADLE_VERSION_PLUGIN) version GradlePluginVersion.GRADLE_VERSION_PLUGIN 5 | } 6 | 7 | buildscript { 8 | repositories { 9 | google() 10 | jcenter() 11 | } 12 | 13 | dependencies { 14 | classpath(GradleClasspath.GRADLE_PLUGIN) 15 | classpath(GradleClasspath.KOTLIN_GRADLE_PLUGIN) 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories.applyDefaults() 21 | } 22 | 23 | tasks { 24 | register("clean", Delete::class) { 25 | delete = setOf(rootProject.buildDir) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /buildSrc/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | /build 3 | /.gradle 4 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | jcenter() 7 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/AndroidConfig.kt: -------------------------------------------------------------------------------- 1 | object AndroidConfig { 2 | const val COMPILE_SDK_VERSION = 29 3 | const val MIN_SDK_VERSION = 21 4 | const val TARGET_SDK_VERSION = 29 5 | const val BUILD_TOOLS_VERSION = "29.0.2" 6 | 7 | const val TEST_INSTRUMENTATION_RUNNER = "androidx.test.runner.AndroidJUnitRunner" 8 | 9 | const val APPLICATION_ID = "com.mobiaxe.wasd" 10 | 11 | const val VERSION_CODE = 1 12 | const val VERSION_NAME = "1.0" 13 | } 14 | 15 | object BuildType { 16 | const val RELEASE = "release" 17 | const val DEBUG = "debug" 18 | } 19 | 20 | object SigningConfig { 21 | object DEBUG { 22 | const val KEY_ALIAS = "androiddebugkey" 23 | const val KEY_PASSWORD = "android" 24 | const val STORE_PASSWORD = "android" 25 | const val STORE_FILE = "keystores/debug.keystore" 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/CoreVersion.kt: -------------------------------------------------------------------------------- 1 | /** 2 | * These versions are covered across libraries and plugins 3 | */ 4 | object CoreVersion { 5 | const val KOTLIN = "1.3.70" 6 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/GradleDependency.kt: -------------------------------------------------------------------------------- 1 | object GradlePluginVersion { 2 | const val ANDROID_GRADLE = "3.5.3" 3 | const val GRADLE_VERSION_PLUGIN = "0.28.0" 4 | } 5 | 6 | object GradlePluginId { 7 | const val ANDROID_APPLICATION = "com.android.application" 8 | const val ANDROID_DYNAMIC_FEATURE = "com.android.dynamic-feature" 9 | const val ANDROID_LIBRARY = "com.android.library" 10 | const val KOTLIN_ANDROID = "org.jetbrains.kotlin.android" 11 | const val KOTLIN_ANDROID_EXTENSIONS = "org.jetbrains.kotlin.android.extensions" 12 | const val GRADLE_VERSION_PLUGIN = "com.github.ben-manes.versions" 13 | const val KOTLIN_KAPT = "kotlin-kapt" 14 | } 15 | 16 | object GradleClasspath { 17 | const val GRADLE_PLUGIN = "com.android.tools.build:gradle:${GradlePluginVersion.ANDROID_GRADLE}" 18 | const val KOTLIN_GRADLE_PLUGIN = "org.jetbrains.kotlin:kotlin-gradle-plugin:${CoreVersion.KOTLIN}" 19 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/LibraryDependency.kt: -------------------------------------------------------------------------------- 1 | private object Version { 2 | const val MATERIAL = "1.2.0-alpha05" 3 | const val LIFECYCLE = "2.2.0" 4 | const val CONSTRAINT = "2.0.0-beta4" 5 | const val KOIN = "2.1.4" 6 | const val GLIDE = "4.11.0" 7 | const val VIEWPAGER2 = "1.0.0" 8 | const val NAVIGATION = "2.3.0-alpha03" 9 | const val ROOM = "2.2.4" 10 | const val TIMBER = "4.7.1" 11 | const val STETHO = "1.5.1" 12 | const val RETROFIT = "2.7.1" 13 | const val PAGING = "2.1.1" 14 | const val COROUTINE = "1.3.4" 15 | const val LEAK_CANARY = "2.2" 16 | const val OKHTTP_LOGGING = "4.4.0" 17 | } 18 | 19 | object Library { 20 | const val KOTLIN = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${CoreVersion.KOTLIN}" 21 | const val VIEWMODEL = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Version.LIFECYCLE}" 22 | const val MATERIAL = "com.google.android.material:material:${Version.MATERIAL}" 23 | const val CONSTRAINT = "androidx.constraintlayout:constraintlayout:${Version.CONSTRAINT}" 24 | const val KOIN = "org.koin:koin-androidx-viewmodel:${Version.KOIN}" 25 | const val GLIDE = "com.github.bumptech.glide:glide:${Version.GLIDE}" 26 | const val VIEWPAGER2 = "androidx.viewpager2:viewpager2:${Version.VIEWPAGER2}" 27 | const val NAVIGATION_FRAGMENT_KTX = "androidx.navigation:navigation-fragment-ktx:${Version.NAVIGATION}" 28 | const val NAVIGATION_UI_KTX = "androidx.navigation:navigation-ui-ktx:${Version.NAVIGATION}" 29 | const val NAVIGATION_DYNAMIC_FEATURE = "androidx.navigation:navigation-dynamic-features-fragment:${Version.NAVIGATION}" 30 | const val ROOM = "androidx.room:room-runtime:${Version.ROOM}" 31 | const val ROOM_KTX = "androidx.room:room-ktx:${Version.ROOM}" 32 | const val TIMBER = "com.jakewharton.timber:timber:${Version.TIMBER}" 33 | const val STETHO = "com.facebook.stetho:stetho:${Version.STETHO}" 34 | const val STETHO_OKHTTP = "com.facebook.stetho:stetho-okhttp3:${Version.STETHO}" 35 | const val RETROFIT = "com.squareup.retrofit2:retrofit:${Version.RETROFIT}" 36 | const val RETROFIT_CONVERTER = "com.squareup.retrofit2:converter-gson:${Version.RETROFIT}" 37 | const val OKHTTP_LOGGING = "com.squareup.okhttp3:logging-interceptor:${Version.OKHTTP_LOGGING}" 38 | const val PAGING = "androidx.paging:paging-runtime-ktx:${Version.PAGING}" 39 | const val COROUTINE = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Version.COROUTINE}" 40 | const val LEAK_CANARY = "com.squareup.leakcanary:leakcanary-android:${Version.LEAK_CANARY}" 41 | 42 | const val GLIDE_COMPILER = "com.github.bumptech.glide:compiler:${Version.GLIDE}" 43 | const val ROOM_COMPILER = "androidx.room:room-compiler:${Version.ROOM}" 44 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/ModuleDependency.kt: -------------------------------------------------------------------------------- 1 | object ModuleDependency { 2 | const val APP = ":app" 3 | const val CORE = ":core" 4 | const val FEATURE_ONBOARDING = ":onboarding" 5 | const val FEATURE_HOME = ":home" 6 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/extension/ProjectExt.kt: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import org.gradle.api.Project 4 | import utils.getLocalProperty 5 | 6 | /** 7 | * Obtain property declared on `$projectRoot/keystore.properties` file. 8 | * 9 | * @param propertyName the name of declared property 10 | */ 11 | fun Project.getLocalProperty(propertyName: String): String { 12 | return getLocalProperty(propertyName, this) 13 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/extension/RepositoryHandler.kt: -------------------------------------------------------------------------------- 1 | package extension 2 | 3 | import org.gradle.api.artifacts.dsl.RepositoryHandler 4 | 5 | fun RepositoryHandler.applyDefaults() { 6 | google() 7 | jcenter() 8 | } 9 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/utils/BuildUtils.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import org.gradle.api.Project 4 | import java.util.Properties 5 | 6 | private const val LOCAL_PROPERTIES_FILE_NAME = "keystore.properties" 7 | 8 | /** 9 | * Util to obtain property declared on `$projectRoot/local.properties` file. 10 | * 11 | * @param propertyName the name of declared property 12 | * @param project the project reference 13 | * 14 | * @return the value of property name, otherwise throw [Exception] 15 | */ 16 | fun getLocalProperty(propertyName: String, project: Project): String { 17 | val localProperties = Properties().apply { 18 | val localPropertiesFile = project.rootProject.file(LOCAL_PROPERTIES_FILE_NAME) 19 | if (localPropertiesFile.exists()) { 20 | load(localPropertiesFile.inputStream()) 21 | } 22 | } 23 | 24 | return localProperties.getProperty(propertyName)?.let { 25 | it 26 | } ?: run { 27 | throw NoSuchFieldException("Not defined property: $propertyName") 28 | } 29 | } -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(GradlePluginId.ANDROID_LIBRARY) 3 | id(GradlePluginId.KOTLIN_ANDROID) 4 | id(GradlePluginId.KOTLIN_ANDROID_EXTENSIONS) 5 | id(GradlePluginId.KOTLIN_KAPT) 6 | } 7 | 8 | android { 9 | compileSdkVersion(AndroidConfig.COMPILE_SDK_VERSION) 10 | 11 | defaultConfig { 12 | minSdkVersion(AndroidConfig.MIN_SDK_VERSION) 13 | targetSdkVersion(AndroidConfig.TARGET_SDK_VERSION) 14 | testInstrumentationRunner = AndroidConfig.TEST_INSTRUMENTATION_RUNNER 15 | 16 | buildConfigFieldFromGradleProperty("apiBaseUrl") 17 | buildConfigFieldFromGradleProperty("apiToken") 18 | } 19 | 20 | dataBinding { 21 | isEnabled = true 22 | } 23 | 24 | kotlinOptions { 25 | jvmTarget = JavaVersion.VERSION_1_8.toString() 26 | } 27 | } 28 | 29 | dependencies { 30 | api(Library.KOTLIN) 31 | api(Library.MATERIAL) 32 | api(Library.VIEWPAGER2) 33 | api(Library.CONSTRAINT) 34 | api(Library.KOIN) 35 | api(Library.GLIDE) 36 | api(Library.PAGING) 37 | api(Library.COROUTINE) 38 | 39 | implementation(Library.RETROFIT) 40 | implementation(Library.RETROFIT_CONVERTER) 41 | implementation(Library.OKHTTP_LOGGING) 42 | implementation(Library.VIEWMODEL) 43 | implementation(Library.ROOM) 44 | implementation(Library.ROOM_KTX) 45 | implementation(Library.TIMBER) 46 | implementation(Library.STETHO_OKHTTP) 47 | 48 | debugImplementation(Library.LEAK_CANARY) 49 | 50 | kapt(Library.ROOM_COMPILER) 51 | } 52 | 53 | fun com.android.build.gradle.internal.dsl.BaseFlavor.buildConfigFieldFromGradleProperty(gradlePropertyName: String) { 54 | val propertyValue = project.properties[gradlePropertyName] as? String 55 | checkNotNull(propertyValue) { "Gradle property $gradlePropertyName is null" } 56 | 57 | val androidResourceName = "GRADLE_${gradlePropertyName.toSnakeCase()}".toUpperCase() 58 | buildConfigField("String", androidResourceName, propertyValue) 59 | } 60 | 61 | fun String.toSnakeCase() = this.split(Regex("(?=[A-Z])")).joinToString("_") { it.toLowerCase() } 62 | 63 | -------------------------------------------------------------------------------- /core/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/core/consumer-rules.pro -------------------------------------------------------------------------------- /core/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 22 | -------------------------------------------------------------------------------- /core/src/androidTest/java/com/mobiaxe/core/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core 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.mobiaxe.core.test", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/cache/PreferencesHelper.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.cache 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | 6 | /** 7 | * General Preferences Helper class, used for storing preference values using the Preference API 8 | */ 9 | open class PreferencesHelper(context: Context) { 10 | 11 | companion object { 12 | private const val PREF_WASD_PACKAGE_NAME = "com.mobiaxe.wasd" 13 | 14 | private const val PREF_KEY_LAST_CACHE = "last_cache" 15 | 16 | private const val PREF_KEY_ONBOARDING_SHOW = "onboarding_show" 17 | } 18 | 19 | private val wasdPref: SharedPreferences 20 | 21 | init { 22 | wasdPref = context.getSharedPreferences(PREF_WASD_PACKAGE_NAME, Context.MODE_PRIVATE) 23 | } 24 | 25 | /** 26 | * Store and retrieve the last time data was cached 27 | */ 28 | var lastCacheTime: Long 29 | get() = wasdPref.getLong(PREF_KEY_LAST_CACHE, 0) 30 | set(lastCache) = wasdPref.edit().putLong(PREF_KEY_LAST_CACHE, lastCache).apply() 31 | 32 | var onboardingShow: Boolean 33 | get() = wasdPref.getBoolean(PREF_KEY_ONBOARDING_SHOW, false) 34 | set(isOnboardingShown) = wasdPref.edit().putBoolean(PREF_KEY_ONBOARDING_SHOW, isOnboardingShown).apply() 35 | 36 | } 37 | -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/cache/WASDCacheImpl.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.cache 2 | 3 | import com.mobiaxe.core.cache.db.WASDDatabase 4 | import com.mobiaxe.core.data.Gamer 5 | 6 | class WASDCacheImpl(private val wasdDatabase: WASDDatabase): WASDCachedDataStore { 7 | 8 | override suspend fun savePlatform(platformId: String?) = 9 | wasdDatabase.cachedWASDDao().insertPlatformId(platformId?.let { Gamer(platform = it) }) 10 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/cache/WASDCachedDataStore.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.cache 2 | 3 | interface WASDCachedDataStore { 4 | 5 | suspend fun savePlatform(platformId: String?) 6 | 7 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/cache/db/CachedWASDDao.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.cache.db 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import com.mobiaxe.core.data.Gamer 7 | 8 | @Dao 9 | abstract class CachedWASDDao { 10 | 11 | @Insert(onConflict = OnConflictStrategy.REPLACE) 12 | abstract suspend fun insertPlatformId(platform: Gamer?) 13 | 14 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/cache/db/CoverConverter.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.cache.db 2 | 3 | import androidx.room.TypeConverter 4 | import com.google.gson.Gson 5 | import com.mobiaxe.core.data.Cover 6 | 7 | class CoverConverter { 8 | 9 | @TypeConverter 10 | fun coverToJson(value: Cover?): String { 11 | return Gson().toJson(value) 12 | } 13 | 14 | @TypeConverter 15 | fun jsonToCover(value: String): Cover? { 16 | return Gson().fromJson(value, Cover::class.java) ?: Cover(0, "", "", 0) 17 | } 18 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/cache/db/ListConverter.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.cache.db 2 | 3 | import androidx.room.TypeConverter 4 | import com.google.gson.Gson 5 | 6 | class ListConverter { 7 | 8 | @TypeConverter 9 | fun listToJson(value: List?): String { 10 | return Gson().toJson(value) 11 | } 12 | 13 | @TypeConverter 14 | fun jsonToList(value: String): List? { 15 | val objects = Gson().fromJson(value, Array::class.java) 16 | return if (objects == null) { 17 | emptyList() 18 | } else { 19 | objects as Array 20 | objects.toList() 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/cache/db/WASDConstants.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.cache.db 2 | 3 | object WASDConstants { 4 | 5 | const val TABLE_GAMER = "gamer" 6 | const val TABLE_POPULAR = "popular" 7 | 8 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/cache/db/WASDDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.cache.db 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | import com.mobiaxe.core.data.Gamer 8 | 9 | @Database(entities = [Gamer::class], version = 1) 10 | abstract class WASDDatabase: RoomDatabase() { 11 | 12 | abstract fun cachedWASDDao(): CachedWASDDao 13 | 14 | companion object { 15 | 16 | @Volatile 17 | private var INSTANCE: WASDDatabase? = null 18 | 19 | fun getInstance(context: Context): WASDDatabase = 20 | INSTANCE ?: synchronized(this) { 21 | INSTANCE ?: buildDatabase(context).also { INSTANCE = it } 22 | } 23 | 24 | private fun buildDatabase(context: Context) = 25 | Room.databaseBuilder(context.applicationContext, 26 | WASDDatabase::class.java, "WASD.db") 27 | .build() 28 | } 29 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/data/Resource.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.data 2 | 3 | data class Resource(val status: Status, val data: T?, val message: String?) { 4 | companion object { 5 | fun success(data: T? = null): Resource { 6 | return Resource(Status.SUCCESS, data, null) 7 | } 8 | 9 | fun error(msg: String): Resource { 10 | return Resource(Status.ERROR, null, msg) 11 | } 12 | 13 | fun loading(data: T? = null): Resource { 14 | return Resource(Status.LOADING, data, null) 15 | } 16 | 17 | fun empty(msg: String): Resource { 18 | return Resource(Status.EMPTY, null, msg) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/data/Status.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.data 2 | 3 | import android.view.View 4 | 5 | enum class Status(val visibility: Int) { 6 | SUCCESS(View.GONE), 7 | ERROR(View.GONE), 8 | LOADING(View.VISIBLE), 9 | EMPTY(View.GONE) 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/data/WASDModel.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.data 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import androidx.room.TypeConverters 7 | import com.google.gson.annotations.SerializedName 8 | import com.mobiaxe.core.cache.db.CoverConverter 9 | import com.mobiaxe.core.cache.db.ListConverter 10 | import com.mobiaxe.core.cache.db.WASDConstants 11 | import com.mobiaxe.core.presentation.ListItemViewModel 12 | import java.util.* 13 | 14 | @Entity(tableName = WASDConstants.TABLE_GAMER) 15 | data class Gamer( 16 | @PrimaryKey 17 | @ColumnInfo(name = "id") 18 | val id: String = UUID.randomUUID().toString(), 19 | @ColumnInfo(name = "platform") 20 | val platform: String 21 | ) 22 | 23 | @Entity(tableName = WASDConstants.TABLE_POPULAR) 24 | data class Game( 25 | @PrimaryKey @field:SerializedName("id") val id: Long, 26 | @TypeConverters(CoverConverter::class) @field:SerializedName("cover") val cover: Cover?, 27 | @field:SerializedName("name") val name: String, 28 | @field:SerializedName("popularity") val popularity: Double, 29 | @TypeConverters(ListConverter::class) val platforms: MutableList = mutableListOf() 30 | ): ListItemViewModel() 31 | 32 | data class Cover( 33 | @SerializedName("id") val id: Long, 34 | @SerializedName("image_id") val imageId: String, 35 | @SerializedName("url") val url: String, 36 | @SerializedName("game") val game: Long) 37 | 38 | data class ReleaseDate( 39 | @SerializedName("id") val id: Long, 40 | @SerializedName("game") val game: Game 41 | ): ListItemViewModel() -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/domain/SimpleUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.domain 2 | 3 | abstract class SimpleUseCase where Type : Any { 4 | abstract fun run(params: Params): Type 5 | operator fun invoke(params: Params): Type = run(params) 6 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/domain/SuspendUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.domain 2 | 3 | import kotlinx.coroutines.* 4 | import timber.log.Timber 5 | 6 | abstract class SuspendUseCase where Type : Any { 7 | 8 | abstract suspend fun run(params: Params): Type 9 | suspend operator fun invoke(params: Params): Type = run(params) 10 | 11 | private val viewModelJob = SupervisorJob() 12 | private val viewModelScope = CoroutineScope(Dispatchers.Main + viewModelJob) 13 | 14 | private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> 15 | handleErrors(throwable) 16 | } 17 | 18 | private fun handleErrors(error: Throwable?) { 19 | Timber.e(error, "Error") 20 | } 21 | 22 | fun execute(params: Params, onResult: (Type) -> Unit = {}) { 23 | viewModelScope.launch (exceptionHandler) { 24 | val result = withContext(Dispatchers.IO) { 25 | run(params) 26 | } 27 | onResult(result) 28 | } 29 | } 30 | 31 | fun cancel() { 32 | viewModelScope.coroutineContext.cancelChildren() 33 | } 34 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/domain/SynchronouosUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.domain 2 | 3 | interface SynchronouosUseCase { 4 | fun execute(params: Params): Results 5 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/extension/ContextExt.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.extension 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.widget.Toast 6 | 7 | fun Context.toast(message: String) = Toast.makeText(this, message, Toast.LENGTH_SHORT).show() 8 | 9 | fun Context.start(intent: Intent?) = intent?.let { startActivity(it) } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/extension/LifecycleExt.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.extension 2 | 3 | import androidx.lifecycle.LifecycleOwner 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.Observer 6 | 7 | fun LifecycleOwner.observe(liveData: LiveData?, observer: (T) -> Unit) { 8 | liveData?.observe(this, Observer(observer)) 9 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/extension/UtilExt.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.extension 2 | 3 | import android.content.res.Resources 4 | 5 | val Int.dp: Int 6 | get() = (this / Resources.getSystem().displayMetrics.density).toInt() 7 | val Int.px: Int 8 | get() = (this * Resources.getSystem().displayMetrics.density).toInt() -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/koin/Modules.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.koin 2 | 3 | import androidx.room.Room 4 | import com.mobiaxe.core.BuildConfig 5 | import com.mobiaxe.core.cache.PreferencesHelper 6 | import com.mobiaxe.core.cache.WASDCacheImpl 7 | import com.mobiaxe.core.cache.WASDCachedDataStore 8 | import com.mobiaxe.core.cache.db.WASDDatabase 9 | import com.mobiaxe.core.remote.WASDRemoteDataStore 10 | import com.mobiaxe.core.remote.WASDRemoteImpl 11 | import com.mobiaxe.core.remote.service.WASDServiceFactory 12 | import org.koin.android.ext.koin.androidContext 13 | import org.koin.dsl.module 14 | 15 | val coreModule = module { 16 | single { PreferencesHelper(androidContext()) } 17 | single { 18 | Room.databaseBuilder(androidContext(), 19 | WASDDatabase::class.java, "WASD.db") 20 | .build() 21 | } 22 | 23 | factory { WASDServiceFactory.makeWASDService(BuildConfig.DEBUG, BuildConfig.GRADLE_API_BASE_URL, BuildConfig.GRADLE_API_TOKEN) } 24 | factory { get().cachedWASDDao() } 25 | factory { WASDCacheImpl(get()) } 26 | factory { WASDRemoteImpl(get()) } 27 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/presentation/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.presentation 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.databinding.DataBindingUtil 6 | import androidx.databinding.ViewDataBinding 7 | import androidx.lifecycle.ViewModel 8 | 9 | abstract class BaseActivity: AppCompatActivity() { 10 | 11 | private lateinit var viewModel: V 12 | private lateinit var dataBinding: B 13 | 14 | /** 15 | * @return layout resource id 16 | */ 17 | abstract fun getLayoutId(): Int 18 | 19 | /** 20 | * Override for set view model 21 | * 22 | * @return view model instance 23 | */ 24 | abstract fun getViewModel(): Lazy 25 | 26 | /** 27 | * Override for bind view 28 | * 29 | * @param dataBinding instance 30 | */ 31 | abstract fun bindViewModel(dataBinding: B) 32 | 33 | abstract fun loadKoinModules() 34 | 35 | abstract fun unloadKoinModules() 36 | 37 | fun getBinding(): B = dataBinding 38 | fun vm() = getViewModel().value 39 | 40 | override fun onCreate(savedInstanceState: Bundle?) { 41 | super.onCreate(savedInstanceState) 42 | dataBinding = DataBindingUtil.setContentView(this, getLayoutId()) 43 | loadKoinModules() 44 | viewModel = getViewModel().value 45 | bindViewModel(dataBinding) 46 | } 47 | 48 | override fun onDestroy() { 49 | super.onDestroy() 50 | unloadKoinModules() 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/presentation/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.presentation 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.databinding.DataBindingUtil 8 | import androidx.databinding.ViewDataBinding 9 | import androidx.fragment.app.Fragment 10 | import androidx.lifecycle.ViewModel 11 | 12 | abstract class BaseFragment: Fragment() { 13 | 14 | private lateinit var viewModel: V 15 | private lateinit var dataBinding: B 16 | private lateinit var rootView: View 17 | 18 | /** 19 | * @return layout resource id 20 | */ 21 | abstract fun getLayoutId(): Int 22 | 23 | /** 24 | * Override for set view model 25 | * 26 | * @return view model instance 27 | */ 28 | abstract fun getViewModel(): Lazy 29 | 30 | /** 31 | * Override for bind view 32 | * 33 | * @param dataBinding instance 34 | */ 35 | abstract fun bindViewModel(dataBinding: B) 36 | 37 | abstract fun loadKoinModules() 38 | 39 | abstract fun unloadKoinModules() 40 | 41 | override fun onCreate(savedInstanceState: Bundle?) { 42 | super.onCreate(savedInstanceState) 43 | loadKoinModules() 44 | viewModel = getViewModel().value 45 | } 46 | 47 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 48 | dataBinding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false) 49 | rootView = dataBinding.root 50 | return rootView 51 | } 52 | 53 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 54 | super.onViewCreated(view, savedInstanceState) 55 | bindViewModel(dataBinding) 56 | } 57 | 58 | override fun onDestroyView() { 59 | super.onDestroyView() 60 | unloadKoinModules() 61 | } 62 | 63 | 64 | fun getBinding() = dataBinding 65 | 66 | fun vModel() = getViewModel().value 67 | 68 | 69 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/presentation/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.presentation 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.mobiaxe.core.domain.SuspendUseCase 5 | 6 | abstract class BaseViewModel(vararg suspendUseCase: SuspendUseCase<*, *>): ViewModel() { 7 | 8 | protected var useCaseList: MutableList> = mutableListOf() 9 | 10 | init { 11 | useCaseList.addAll(suspendUseCase) 12 | } 13 | 14 | override fun onCleared() { 15 | super.onCleared() 16 | useCaseList.forEach { 17 | it.cancel() 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/presentation/DataBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.presentation 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.databinding.DataBindingUtil 7 | import androidx.databinding.ViewDataBinding 8 | import androidx.recyclerview.widget.DiffUtil 9 | import androidx.recyclerview.widget.ListAdapter 10 | import androidx.recyclerview.widget.RecyclerView 11 | import com.mobiaxe.core.BR 12 | 13 | abstract class DataBindingAdapter(diffCallback: DiffUtil.ItemCallback) : 14 | ListAdapter>(diffCallback) { 15 | 16 | private var onListItemViewClickListener: OnListItemViewClickListener? = null 17 | 18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataBindingViewHolder { 19 | val layoutInflater = LayoutInflater.from(parent.context) 20 | val binding = DataBindingUtil.inflate(layoutInflater, viewType, parent, false) 21 | return DataBindingViewHolder(binding) 22 | } 23 | 24 | override fun onBindViewHolder(holder: DataBindingViewHolder, position: Int) = holder.bind(getItem(position)) 25 | 26 | class DataBindingViewHolder(private val binding: ViewDataBinding) : 27 | RecyclerView.ViewHolder(binding.root) { 28 | 29 | fun bind(item: T) { 30 | binding.setVariable(BR.item, item) 31 | binding.executePendingBindings() 32 | } 33 | } 34 | 35 | interface OnListItemViewClickListener{ 36 | fun onClick(view: View, position: Int) 37 | } 38 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/presentation/ListItemViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.presentation 2 | 3 | abstract class ListItemViewModel{ 4 | var adapterPosition: Int = -1 5 | var onListItemViewClickListener: DataBindingAdapter.OnListItemViewClickListener? = null 6 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/remote/IGDBRequest.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.remote 2 | 3 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 4 | import okhttp3.RequestBody 5 | import okhttp3.RequestBody.Companion.toRequestBody 6 | 7 | class IGDBRequest( 8 | val fields: String?, 9 | val sort: String?, 10 | val limit: Int?, 11 | val offset: Int?, 12 | val where: List?, 13 | val search: String? 14 | ) { 15 | 16 | companion object { 17 | private const val FIELDS = "f" 18 | private const val SORT = "s" 19 | private const val SEARCH = "search" 20 | private const val LIMIT = "l" 21 | private const val OFFSET = "o" 22 | private const val WHERE = "w" 23 | private const val COMMA = ";" 24 | private const val ASCENDING = "asc" 25 | private const val DESCENDING = "desc" 26 | private const val MEDIA_TYPE = "text/plain" 27 | } 28 | 29 | data class Builder( 30 | var fields: String? = null, 31 | var search: String? = null, 32 | var sort: String? = null, 33 | var limit: Int? = null, 34 | var offset: Int? = null, 35 | var where: List? = null, 36 | var isAscending: Boolean = false, 37 | var by: String? = null 38 | ) { 39 | 40 | fun search(query: String) = apply { this.search = query } 41 | 42 | fun fields(fields: String) = apply { this.fields = fields } 43 | 44 | fun sort(sort: String, isAscending: Boolean = false) = apply { 45 | this.sort = sort 46 | this.isAscending = isAscending 47 | } 48 | 49 | fun where(by: String) = apply { this.by = by } 50 | 51 | fun limit(limit: Int) = apply { this.limit = limit } 52 | 53 | fun offset(offset: Int) = apply { this.offset = offset } 54 | 55 | fun build(): RequestBody { 56 | 57 | val request = StringBuilder() 58 | 59 | if (search != null) { 60 | request.append("$SEARCH \"$search\"$COMMA") 61 | } 62 | 63 | if (fields != null) { 64 | request.append("$FIELDS $fields$COMMA") 65 | } 66 | 67 | if (sort != null) { 68 | if (isAscending) { 69 | request.append("$SORT $sort $ASCENDING$COMMA") 70 | } else { 71 | request.append("$SORT $sort $DESCENDING$COMMA") 72 | } 73 | } 74 | 75 | if (by != null) { 76 | request.append("$WHERE $by$COMMA") 77 | } 78 | 79 | if (limit != null) { 80 | request.append("$LIMIT $limit$COMMA") 81 | } 82 | 83 | if (offset != null) { 84 | request.append("$OFFSET $offset$COMMA") 85 | } 86 | 87 | return request.toString().toRequestBody(MEDIA_TYPE.toMediaTypeOrNull()) 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/remote/WASDRemoteDataStore.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.remote 2 | 3 | import com.mobiaxe.core.data.Game 4 | 5 | interface WASDRemoteDataStore { 6 | suspend fun getPopularGames(offset: Int, platforms: String): List 7 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/remote/WASDRemoteImpl.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.remote 2 | 3 | import com.mobiaxe.core.data.Game 4 | import com.mobiaxe.core.remote.service.WASDService 5 | 6 | class WASDRemoteImpl(private val wasdService: WASDService): WASDRemoteDataStore { 7 | 8 | companion object { 9 | private const val POPULAR_GAMES_FIELDS = "name,popularity,rating,cover.image_id,platforms" 10 | private const val POPULAR_GAMES_SORT = "popularity" 11 | private const val COVER_FIELDS = "image_id,url,game" 12 | private const val GAME = "game" 13 | private const val DATE = "date" 14 | private const val LIMIT = 50 15 | } 16 | 17 | private val whereGames: (String) -> String = {param -> "platforms = $param & category = 0"} 18 | 19 | override suspend fun getPopularGames(offset: Int, platforms: String): List { 20 | val request = IGDBRequest.Builder().fields(POPULAR_GAMES_FIELDS).sort(POPULAR_GAMES_SORT).where(whereGames(platforms)).limit(LIMIT).offset(offset).build() 21 | return wasdService.getGames(request) 22 | } 23 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/remote/service/WASDService.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.remote.service 2 | 3 | import com.mobiaxe.core.data.Game 4 | import okhttp3.RequestBody 5 | import retrofit2.http.Body 6 | import retrofit2.http.POST 7 | 8 | interface WASDService { 9 | 10 | @POST("games") 11 | suspend fun getGames(@Body request: RequestBody): List 12 | 13 | 14 | } -------------------------------------------------------------------------------- /core/src/main/java/com/mobiaxe/core/remote/service/WASDServiceFactory.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.core.remote.service 2 | 3 | import com.facebook.stetho.okhttp3.StethoInterceptor 4 | import com.google.gson.FieldNamingPolicy 5 | import com.google.gson.Gson 6 | import com.google.gson.GsonBuilder 7 | import okhttp3.Interceptor 8 | import okhttp3.OkHttpClient 9 | import okhttp3.logging.HttpLoggingInterceptor 10 | import retrofit2.Retrofit 11 | import retrofit2.converter.gson.GsonConverterFactory 12 | import java.util.concurrent.TimeUnit 13 | 14 | object WASDServiceFactory { 15 | fun makeWASDService(isDebug: Boolean, baseUrl: String, apiKey: String): WASDService { 16 | val retrofit = Retrofit.Builder() 17 | .baseUrl(baseUrl) 18 | .client(makeOkHttpClient( 19 | makeLoggingInterceptor(isDebug), apiKey)) 20 | .addConverterFactory(GsonConverterFactory.create(makeGson())) 21 | .build() 22 | return retrofit.create(WASDService::class.java) 23 | } 24 | 25 | private fun makeOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor, apiKey: String): OkHttpClient { 26 | return OkHttpClient.Builder() 27 | .addInterceptor(httpLoggingInterceptor) 28 | .addInterceptor(makeHeaderInterceptor(apiKey)) 29 | .addNetworkInterceptor(StethoInterceptor()) 30 | .connectTimeout(120, TimeUnit.SECONDS) 31 | .readTimeout(120, TimeUnit.SECONDS) 32 | .build() 33 | } 34 | 35 | private fun makeGson(): Gson { 36 | return GsonBuilder() 37 | .setLenient() 38 | .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") 39 | .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) 40 | .create() 41 | } 42 | 43 | private fun makeLoggingInterceptor(isDebug: Boolean): HttpLoggingInterceptor { 44 | val logging = HttpLoggingInterceptor() 45 | logging.level = if (isDebug) 46 | HttpLoggingInterceptor.Level.BODY 47 | else 48 | HttpLoggingInterceptor.Level.NONE 49 | return logging 50 | } 51 | 52 | private fun makeHeaderInterceptor(apiKey: String): Interceptor = 53 | Interceptor { chain -> 54 | val request = chain.request().newBuilder() 55 | request.addHeader("user-key", apiKey) 56 | chain.proceed(request.build()) 57 | } 58 | } -------------------------------------------------------------------------------- /core/src/main/res/layout/list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /core/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | core 3 | 4 | -------------------------------------------------------------------------------- /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 | 10 | # -------Gradle-------- 11 | org.gradle.daemon=true 12 | org.gradle.parallel=true 13 | org.gradle.caching=true 14 | org.gradle.jvmargs=-Xmx4g 15 | 16 | 17 | # When configured, Gradle will run in incubating parallel mode. 18 | # This option should only be used with decoupled projects. More details, visit 19 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 20 | # org.gradle.parallel=true 21 | # AndroidX package structure to make it clearer which packages are bundled with the 22 | # Android operating system, and which are packaged with your app's APK 23 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 24 | 25 | # -------AndroidX------- 26 | android.useAndroidX=true 27 | android.enableJetifier=true 28 | android.enableBuildCache=true 29 | 30 | # -------Incremental annotation processing------- 31 | kapt.incremental.apt=true 32 | kapt.use.worker.api=true 33 | kapt.include.compile.classpath=false 34 | 35 | 36 | # -------New code srinker------- 37 | android.enableR8=true 38 | android.enableR8.libraries=true 39 | 40 | # Kotlin code style for this project: "official" or "obsolete": 41 | kotlin.code.style=official 42 | 43 | # -------Build parameters-------- 44 | # Values may be overridden in CI using gradlew "-Pname=value" param 45 | apiBaseUrl="https://api-v3.igdb.com/" 46 | # Typically we shouldn't store token in public repository, however this is just a sample project, so 47 | # we can favour convenience (app can be compiled and launched after checkout) over security (each person who 48 | # checkouts the project must generate own api key and change app configuration before running it). 49 | # In real-live setup this key could be provided\overriden by CI. 50 | apiToken="INSERT YOUR API KEY" 51 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jan 28 01:46:46 EET 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /home/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /home/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(GradlePluginId.ANDROID_DYNAMIC_FEATURE) 3 | id(GradlePluginId.KOTLIN_ANDROID) 4 | id(GradlePluginId.KOTLIN_ANDROID_EXTENSIONS) 5 | id(GradlePluginId.KOTLIN_KAPT) 6 | } 7 | 8 | android { 9 | compileSdkVersion(AndroidConfig.COMPILE_SDK_VERSION) 10 | 11 | defaultConfig { 12 | applicationId = AndroidConfig.APPLICATION_ID 13 | minSdkVersion(AndroidConfig.MIN_SDK_VERSION) 14 | targetSdkVersion(AndroidConfig.TARGET_SDK_VERSION) 15 | buildToolsVersion(AndroidConfig.BUILD_TOOLS_VERSION) 16 | 17 | versionCode = AndroidConfig.VERSION_CODE 18 | versionName = AndroidConfig.VERSION_NAME 19 | testInstrumentationRunner = AndroidConfig.TEST_INSTRUMENTATION_RUNNER 20 | } 21 | 22 | dataBinding { 23 | isEnabled = true 24 | } 25 | 26 | } 27 | 28 | dependencies { 29 | implementation(project(ModuleDependency.APP)) 30 | } 31 | -------------------------------------------------------------------------------- /home/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /home/src/main/java/com/mobiaxe/home/data/SectionData.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.home.data 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | import com.mobiaxe.core.presentation.ListItemViewModel 5 | import com.mobiaxe.home.presentation.GameListAdapter 6 | import com.mobiaxe.home.presentation.RecentlyReleasedListAdapter 7 | 8 | data class SectionData( 9 | val headerTitle: Int, 10 | val gameListAdapter: GameListAdapter?, 11 | val recentlyReleasedListAdapter: RecentlyReleasedListAdapter?, 12 | val pool: RecyclerView.RecycledViewPool 13 | ): ListItemViewModel() -------------------------------------------------------------------------------- /home/src/main/java/com/mobiaxe/home/data/datasource/GamesPagingDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.home.data.datasource 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.paging.PageKeyedDataSource 6 | import com.mobiaxe.core.data.Game 7 | import com.mobiaxe.core.data.Status 8 | import com.mobiaxe.home.domain.usecase.GetPopularGames 9 | 10 | class GamesPagingDataSource(private val getPopularGames: GetPopularGames) : PageKeyedDataSource() { 11 | 12 | private val _networkState = MutableLiveData() 13 | val networkState: LiveData = _networkState 14 | 15 | override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { 16 | _networkState.postValue(Status.LOADING) 17 | getPopularGames.execute(0) { 18 | callback.onResult(it, null, 0) 19 | _networkState.postValue(Status.SUCCESS) 20 | } 21 | } 22 | 23 | override fun loadAfter(params: LoadParams, callback: LoadCallback) { 24 | _networkState.postValue(Status.LOADING) 25 | getPopularGames.execute(params.key + 20) { 26 | callback.onResult(it, params.key + 20) 27 | _networkState.postValue(Status.SUCCESS) 28 | } 29 | } 30 | 31 | override fun loadBefore(params: LoadParams, callback: LoadCallback) { 32 | 33 | } 34 | } -------------------------------------------------------------------------------- /home/src/main/java/com/mobiaxe/home/data/datasource/HomeRemoteDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.home.data.datasource 2 | 3 | import com.mobiaxe.core.data.Game 4 | 5 | interface HomeRemoteDataSource { 6 | suspend fun popularGamesFetched(offset: Int): List 7 | } -------------------------------------------------------------------------------- /home/src/main/java/com/mobiaxe/home/data/repository/HomeDataRepository.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.home.data.repository 2 | 3 | import com.mobiaxe.core.data.Game 4 | import com.mobiaxe.home.data.datasource.HomeRemoteDataSource 5 | import com.mobiaxe.home.domain.repository.HomeRepository 6 | 7 | class HomeDataRepository(private val homeRemoteDataSource: HomeRemoteDataSource): HomeRepository { 8 | override suspend fun getPopularGames(offset: Int): List = homeRemoteDataSource.popularGamesFetched(offset) 9 | } -------------------------------------------------------------------------------- /home/src/main/java/com/mobiaxe/home/domain/datasource/GamesDataSourceFactory.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.home.domain.datasource 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.paging.DataSource 5 | import com.mobiaxe.core.data.Game 6 | import com.mobiaxe.home.data.datasource.GamesPagingDataSource 7 | import com.mobiaxe.home.domain.usecase.GetPopularGames 8 | 9 | class GamesDataSourceFactory(private val getPopularGames: GetPopularGames): DataSource.Factory() { 10 | 11 | val gameDataSourceLiveData = MutableLiveData() 12 | 13 | override fun create(): DataSource { 14 | val dataSource = GamesPagingDataSource( 15 | getPopularGames 16 | ) 17 | gameDataSourceLiveData.postValue(dataSource) 18 | return dataSource 19 | } 20 | } -------------------------------------------------------------------------------- /home/src/main/java/com/mobiaxe/home/domain/datasource/HomeRemoteDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.home.domain.datasource 2 | 3 | import com.mobiaxe.core.data.Game 4 | import com.mobiaxe.core.remote.WASDRemoteDataStore 5 | import com.mobiaxe.home.data.datasource.HomeRemoteDataSource 6 | 7 | class HomeRemoteDataSourceImpl(private val wasdRemoteDataStore: WASDRemoteDataStore): HomeRemoteDataSource { 8 | override suspend fun popularGamesFetched(offset: Int): List = wasdRemoteDataStore.getPopularGames(offset, "(48, 6)") 9 | } -------------------------------------------------------------------------------- /home/src/main/java/com/mobiaxe/home/domain/repository/HomeRepository.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.home.domain.repository 2 | 3 | import com.mobiaxe.core.data.Game 4 | 5 | interface HomeRepository { 6 | suspend fun getPopularGames(offset: Int): List 7 | } -------------------------------------------------------------------------------- /home/src/main/java/com/mobiaxe/home/domain/usecase/GetPopularGames.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.home.domain.usecase 2 | 3 | import com.mobiaxe.core.data.Game 4 | import com.mobiaxe.core.domain.SuspendUseCase 5 | import com.mobiaxe.home.domain.repository.HomeRepository 6 | 7 | class GetPopularGames( 8 | private val homeRepository: HomeRepository 9 | ): SuspendUseCase, Int>() { 10 | override suspend fun run(params: Int): List = 11 | homeRepository.getPopularGames(params) 12 | } -------------------------------------------------------------------------------- /home/src/main/java/com/mobiaxe/home/koin/Modules.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.home.koin 2 | 3 | import com.mobiaxe.home.data.datasource.HomeRemoteDataSource 4 | import com.mobiaxe.home.data.repository.HomeDataRepository 5 | import com.mobiaxe.home.domain.datasource.HomeRemoteDataSourceImpl 6 | import com.mobiaxe.home.domain.repository.HomeRepository 7 | import com.mobiaxe.home.domain.usecase.GetPopularGames 8 | import com.mobiaxe.home.presentation.HomeFragment 9 | import com.mobiaxe.home.presentation.HomeViewModel 10 | import com.mobiaxe.home.presentation.SectionListDataAdapter 11 | import org.koin.androidx.viewmodel.dsl.viewModel 12 | import org.koin.core.context.loadKoinModules 13 | import org.koin.core.context.unloadKoinModules 14 | import org.koin.dsl.module 15 | 16 | fun loadModules() = loadKoinModules( 17 | listOf( 18 | viewModelModule, 19 | repositoryModule, 20 | useCaseModule, 21 | homeModule 22 | ) 23 | ) 24 | 25 | fun unloadModules() = unloadKoinModules( 26 | listOf( 27 | viewModelModule, 28 | repositoryModule, 29 | useCaseModule, 30 | homeModule 31 | ) 32 | ) 33 | 34 | val homeModule = module(override = true) { 35 | factory { SectionListDataAdapter() } 36 | } 37 | 38 | val viewModelModule = module { 39 | viewModel { HomeViewModel(get()) } 40 | } 41 | 42 | val repositoryModule = module { 43 | factory { HomeDataRepository(get()) } 44 | factory { HomeRemoteDataSourceImpl(get()) } 45 | } 46 | 47 | val useCaseModule = module { 48 | factory { GetPopularGames(get()) } 49 | } 50 | -------------------------------------------------------------------------------- /home/src/main/java/com/mobiaxe/home/presentation/GameListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.home.presentation 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.paging.PagedListAdapter 6 | import androidx.recyclerview.widget.DiffUtil 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.mobiaxe.core.data.Game 9 | import com.mobiaxe.core.data.Status 10 | import com.mobiaxe.home.databinding.ItemGameBinding 11 | import com.mobiaxe.home.databinding.ItemNetworkStateBinding 12 | import com.mobiaxe.home.BR 13 | 14 | class GameListAdapter : PagedListAdapter(DiffCallback()) { 15 | 16 | companion object { 17 | private const val DATA_VIEW_TYPE = 1 18 | private const val PROGRESS_VIEW_TYPE = 2 19 | } 20 | 21 | private var networkState: Status? = null 22 | 23 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 24 | val layoutInflater = LayoutInflater.from(parent.context) 25 | return if (viewType == PROGRESS_VIEW_TYPE) { 26 | NetworkStateItemViewHolder(ItemNetworkStateBinding.inflate(layoutInflater, parent, false)) 27 | } else { 28 | GameBindingViewHolder(ItemGameBinding.inflate(layoutInflater, parent, false)) 29 | } 30 | } 31 | 32 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 33 | if (holder is GameBindingViewHolder) { 34 | getItem(position)?.let { 35 | holder.bind(it) 36 | } 37 | } else { 38 | (holder as NetworkStateItemViewHolder).bind(networkState) 39 | } 40 | } 41 | 42 | override fun getItemViewType(position: Int): Int { 43 | return if (hasExtraRow() && position == itemCount - 1) { 44 | PROGRESS_VIEW_TYPE 45 | } else { 46 | DATA_VIEW_TYPE 47 | } 48 | } 49 | 50 | override fun getItemCount(): Int { 51 | return super.getItemCount() + if (hasExtraRow()) 1 else 0 52 | } 53 | 54 | private fun hasExtraRow() = networkState != null && networkState != Status.SUCCESS 55 | 56 | 57 | fun setNetworkState(newNetworkState: Status?) { 58 | val previousState = this.networkState 59 | val hadExtraRow = hasExtraRow() 60 | this.networkState = newNetworkState 61 | val hasExtraRow = hasExtraRow() 62 | if (hadExtraRow != hasExtraRow) { 63 | if (hadExtraRow) { 64 | notifyItemRemoved(super.getItemCount()) 65 | } else { 66 | notifyItemInserted(super.getItemCount()) 67 | } 68 | } else if (hasExtraRow && previousState != newNetworkState) { 69 | notifyItemChanged(itemCount - 1) 70 | } 71 | } 72 | 73 | class DiffCallback : DiffUtil.ItemCallback() { 74 | override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean = oldItem.id == newItem.id 75 | 76 | override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean = oldItem == newItem 77 | } 78 | 79 | class GameBindingViewHolder(private val binding: ItemGameBinding) : 80 | RecyclerView.ViewHolder(binding.root) { 81 | 82 | fun bind(item: Game) { 83 | binding.setVariable(BR.item, item) 84 | binding.executePendingBindings() 85 | } 86 | } 87 | 88 | class NetworkStateItemViewHolder(private val binding: ItemNetworkStateBinding) : 89 | RecyclerView.ViewHolder(binding.root) { 90 | 91 | fun bind(item: Status?) { 92 | binding.setVariable(BR.item, item) 93 | binding.executePendingBindings() 94 | } 95 | } 96 | 97 | } -------------------------------------------------------------------------------- /home/src/main/java/com/mobiaxe/home/presentation/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.home.presentation 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import com.mobiaxe.core.extension.observe 6 | import com.mobiaxe.core.presentation.BaseFragment 7 | import com.mobiaxe.home.R 8 | import com.mobiaxe.home.databinding.FragmentHomeBinding 9 | import com.mobiaxe.home.koin.loadModules 10 | import com.mobiaxe.home.koin.unloadModules 11 | import org.koin.android.ext.android.inject 12 | import org.koin.androidx.viewmodel.ext.android.viewModel 13 | 14 | class HomeFragment: BaseFragment() { 15 | 16 | private val sectionListDataAdapter: SectionListDataAdapter by inject() 17 | 18 | override fun getLayoutId(): Int = R.layout.fragment_home 19 | 20 | override fun getViewModel(): Lazy = viewModel() 21 | 22 | override fun bindViewModel(dataBinding: FragmentHomeBinding) { 23 | 24 | } 25 | 26 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 27 | super.onViewCreated(view, savedInstanceState) 28 | initListeners() 29 | initObservers() 30 | vModel().getDashboard() 31 | } 32 | 33 | private fun initListeners() { 34 | getBinding().recyclerHome.adapter = sectionListDataAdapter 35 | } 36 | 37 | private fun initObservers() { 38 | vModel().apply { 39 | observe(sectionListLiveData) { sectionDataList -> 40 | sectionListDataAdapter.submitList(sectionDataList) 41 | observe(popularGamesPagedList) { 42 | sectionDataList[0].gameListAdapter?.submitList(it) 43 | } 44 | observe(networkState) { 45 | sectionDataList[0].gameListAdapter?.setNetworkState(it) 46 | } 47 | } 48 | } 49 | } 50 | 51 | override fun loadKoinModules() { 52 | loadModules() 53 | } 54 | 55 | override fun unloadKoinModules() { 56 | unloadModules() 57 | } 58 | } -------------------------------------------------------------------------------- /home/src/main/java/com/mobiaxe/home/presentation/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.home.presentation 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.Transformations 6 | import androidx.paging.LivePagedListBuilder 7 | import androidx.paging.PagedList 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.mobiaxe.core.data.Game 10 | import com.mobiaxe.core.data.Status 11 | import com.mobiaxe.core.presentation.BaseViewModel 12 | import com.mobiaxe.home.R 13 | import com.mobiaxe.home.data.SectionData 14 | import com.mobiaxe.home.domain.datasource.GamesDataSourceFactory 15 | import com.mobiaxe.home.domain.usecase.GetPopularGames 16 | 17 | class HomeViewModel( 18 | private val getPopularGames: GetPopularGames 19 | ): BaseViewModel(getPopularGames) { 20 | 21 | private val _sectionListLiveData: MutableLiveData> = MutableLiveData() 22 | val sectionListLiveData: LiveData> = _sectionListLiveData 23 | 24 | lateinit var popularGamesPagedList: LiveData> 25 | 26 | var networkState: LiveData? = null 27 | 28 | fun getDashboard() { 29 | val popularGamesDataFactory = GamesDataSourceFactory(getPopularGames) 30 | 31 | val config = PagedList.Config.Builder() 32 | .setPageSize(50) 33 | .setPrefetchDistance(20) 34 | .setEnablePlaceholders(false) 35 | .build() 36 | 37 | popularGamesPagedList = LivePagedListBuilder(popularGamesDataFactory, 50).build() 38 | 39 | getSection(mutableListOf( 40 | SectionData(R.string.title_popular_games, GameListAdapter(), null, RecyclerView.RecycledViewPool()))) 41 | 42 | networkState = Transformations.switchMap(popularGamesDataFactory.gameDataSourceLiveData) { 43 | it.networkState 44 | } 45 | } 46 | 47 | private fun getSection(sectionDataList: MutableList) { 48 | _sectionListLiveData.value = sectionDataList 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /home/src/main/java/com/mobiaxe/home/presentation/RecentlyReleasedListAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.home.presentation 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.paging.PagedListAdapter 6 | import androidx.recyclerview.widget.DiffUtil 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.mobiaxe.core.data.ReleaseDate 9 | import com.mobiaxe.home.databinding.ItemRecentlyReleasedBinding 10 | import com.mobiaxe.home.BR 11 | 12 | class RecentlyReleasedListAdapter : PagedListAdapter(DiffCallback()) { 13 | 14 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 15 | val layoutInflater = LayoutInflater.from(parent.context) 16 | return GameBindingViewHolder(ItemRecentlyReleasedBinding.inflate(layoutInflater, parent, false)) 17 | } 18 | 19 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 20 | getItem(position)?.let { 21 | (holder as GameBindingViewHolder).bind(it) 22 | } 23 | 24 | } 25 | 26 | class DiffCallback : DiffUtil.ItemCallback() { 27 | override fun areItemsTheSame(oldItem: ReleaseDate, newItem: ReleaseDate): Boolean = false 28 | 29 | override fun areContentsTheSame(oldItem: ReleaseDate, newItem: ReleaseDate): Boolean = false 30 | } 31 | 32 | class GameBindingViewHolder(private val binding: ItemRecentlyReleasedBinding) : 33 | RecyclerView.ViewHolder(binding.root) { 34 | 35 | fun bind(item: ReleaseDate) { 36 | binding.setVariable(BR.item, item) 37 | binding.executePendingBindings() 38 | } 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /home/src/main/java/com/mobiaxe/home/presentation/SectionListDataAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.home.presentation 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import com.mobiaxe.core.presentation.DataBindingAdapter 5 | import com.mobiaxe.home.R 6 | import com.mobiaxe.home.data.SectionData 7 | 8 | class SectionListDataAdapter: DataBindingAdapter(DiffCallback()) { 9 | 10 | class DiffCallback: DiffUtil.ItemCallback() { 11 | override fun areItemsTheSame(oldItem: SectionData, newItem: SectionData): Boolean = false 12 | 13 | override fun areContentsTheSame(oldItem: SectionData, newItem: SectionData): Boolean = false 14 | } 15 | 16 | override fun getItemViewType(position: Int) = R.layout.item_section 17 | } -------------------------------------------------------------------------------- /home/src/main/java/com/mobiaxe/home/util/BindingUtils.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.home.util 2 | 3 | import android.widget.ImageView 4 | import androidx.databinding.BindingAdapter 5 | import com.bumptech.glide.Glide 6 | import com.bumptech.glide.request.RequestOptions 7 | import com.bumptech.glide.request.target.Target 8 | 9 | @BindingAdapter("app:coverBig") 10 | fun setCoverBig(imageView: ImageView, hash: String?) { 11 | val context = imageView.context 12 | Glide.with(context) 13 | .load("https://images.igdb.com/igdb/image/upload/t_cover_big/$hash.jpg") 14 | .apply(RequestOptions().override(Target.SIZE_ORIGINAL)) 15 | .into(imageView) 16 | } -------------------------------------------------------------------------------- /home/src/main/res/layout/fragment_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /home/src/main/res/layout/item_game.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 17 | 18 | 25 | 26 | 32 | 33 | 34 | 43 | 44 | -------------------------------------------------------------------------------- /home/src/main/res/layout/item_network_state.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 16 | 17 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /home/src/main/res/layout/item_recently_released.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 17 | 18 | 25 | 26 | 32 | 33 | 34 | 43 | 44 | -------------------------------------------------------------------------------- /home/src/main/res/layout/item_section.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 14 | 15 | 25 | 26 | 36 | 37 | -------------------------------------------------------------------------------- /home/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Popular Games 4 | -------------------------------------------------------------------------------- /keystore.properties: -------------------------------------------------------------------------------- 1 | ## This file is automatically generated by Android Studio. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file should *NOT* be checked into Version Control Systems, 5 | # as it contains information specific to your local configuration. 6 | # 7 | # Location of the SDK. This is only used by Gradle. 8 | # For customization when using a Version Control System, please read the 9 | # header note. 10 | storePassword=storePassword 11 | keyPassword=keyPassword 12 | keyAlias=keyAlias 13 | storeFile=keystores/your_keystore_file -------------------------------------------------------------------------------- /misc/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/misc/home.jpg -------------------------------------------------------------------------------- /misc/onboarding_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/misc/onboarding_1.jpg -------------------------------------------------------------------------------- /misc/onboarding_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/misc/onboarding_2.jpg -------------------------------------------------------------------------------- /misc/onboarding_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/misc/onboarding_3.jpg -------------------------------------------------------------------------------- /misc/select_platform.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/misc/select_platform.jpg -------------------------------------------------------------------------------- /onboarding/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /onboarding/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(GradlePluginId.ANDROID_DYNAMIC_FEATURE) 3 | id(GradlePluginId.KOTLIN_ANDROID) 4 | id(GradlePluginId.KOTLIN_ANDROID_EXTENSIONS) 5 | id(GradlePluginId.KOTLIN_KAPT) 6 | } 7 | 8 | android { 9 | compileSdkVersion(AndroidConfig.COMPILE_SDK_VERSION) 10 | 11 | defaultConfig { 12 | applicationId = AndroidConfig.APPLICATION_ID 13 | minSdkVersion(AndroidConfig.MIN_SDK_VERSION) 14 | targetSdkVersion(AndroidConfig.TARGET_SDK_VERSION) 15 | buildToolsVersion(AndroidConfig.BUILD_TOOLS_VERSION) 16 | 17 | versionCode = AndroidConfig.VERSION_CODE 18 | versionName = AndroidConfig.VERSION_NAME 19 | testInstrumentationRunner = AndroidConfig.TEST_INSTRUMENTATION_RUNNER 20 | } 21 | 22 | dataBinding { 23 | isEnabled = true 24 | } 25 | 26 | } 27 | 28 | dependencies { 29 | implementation(project(ModuleDependency.APP)) 30 | } 31 | -------------------------------------------------------------------------------- /onboarding/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /onboarding/src/main/java/com/mobiaxe/onboarding/data/OnboardingData.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.onboarding.data 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | 6 | @Parcelize 7 | data class OnboardingData( 8 | val title: String, 9 | val description: String, 10 | val background: Int, 11 | val position: Int 12 | ) : Parcelable -------------------------------------------------------------------------------- /onboarding/src/main/java/com/mobiaxe/onboarding/data/datasource/OnboardingCachedDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.onboarding.data.datasource 2 | 3 | interface OnboardingCachedDataSource { 4 | 5 | suspend fun platformSaved(platformId: String?) 6 | 7 | } -------------------------------------------------------------------------------- /onboarding/src/main/java/com/mobiaxe/onboarding/data/repository/OnboardingDataRepository.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.onboarding.data.repository 2 | 3 | import com.mobiaxe.onboarding.data.datasource.OnboardingCachedDataSource 4 | import com.mobiaxe.onboarding.domain.repository.OnboardingRepository 5 | 6 | class OnboardingDataRepository(private val onboardingCachedDataSource: OnboardingCachedDataSource): OnboardingRepository { 7 | 8 | override suspend fun savePlatform(platformId: String?) = 9 | onboardingCachedDataSource.platformSaved(platformId) 10 | } -------------------------------------------------------------------------------- /onboarding/src/main/java/com/mobiaxe/onboarding/domain/datasource/OnboardingCachedDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.onboarding.domain.datasource 2 | 3 | import com.mobiaxe.core.cache.WASDCachedDataStore 4 | import com.mobiaxe.onboarding.data.datasource.OnboardingCachedDataSource 5 | 6 | class OnboardingCachedDataSourceImpl(private val wasdCachedDataStore: WASDCachedDataStore): OnboardingCachedDataSource { 7 | 8 | override suspend fun platformSaved(platformId: String?) = 9 | wasdCachedDataStore.savePlatform(platformId) 10 | 11 | } -------------------------------------------------------------------------------- /onboarding/src/main/java/com/mobiaxe/onboarding/domain/repository/OnboardingRepository.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.onboarding.domain.repository 2 | 3 | 4 | interface OnboardingRepository { 5 | 6 | suspend fun savePlatform(platformId: String?) 7 | 8 | } -------------------------------------------------------------------------------- /onboarding/src/main/java/com/mobiaxe/onboarding/domain/usecase/SavePlatform.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.onboarding.domain.usecase 2 | 3 | import com.mobiaxe.core.domain.SuspendUseCase 4 | import com.mobiaxe.onboarding.domain.repository.OnboardingRepository 5 | 6 | class SavePlatform( 7 | private val onboardingRepository: OnboardingRepository): SuspendUseCase() { 8 | 9 | override suspend fun run(params: String) = onboardingRepository.savePlatform(params) 10 | 11 | } -------------------------------------------------------------------------------- /onboarding/src/main/java/com/mobiaxe/onboarding/koin/Modules.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.onboarding.koin 2 | 3 | import com.mobiaxe.onboarding.data.datasource.OnboardingCachedDataSource 4 | import com.mobiaxe.onboarding.data.repository.OnboardingDataRepository 5 | import com.mobiaxe.onboarding.domain.datasource.OnboardingCachedDataSourceImpl 6 | import com.mobiaxe.onboarding.domain.repository.OnboardingRepository 7 | import com.mobiaxe.onboarding.domain.usecase.SavePlatform 8 | import com.mobiaxe.onboarding.presentation.OnboardingActivity 9 | import com.mobiaxe.onboarding.presentation.OnboardingAdapter 10 | import com.mobiaxe.onboarding.presentation.OnboardingViewModel 11 | import com.mobiaxe.onboarding.presentation.ParallaxPageTransformer 12 | import org.koin.android.ext.koin.androidApplication 13 | import org.koin.androidx.viewmodel.dsl.viewModel 14 | import org.koin.core.context.loadKoinModules 15 | import org.koin.core.context.unloadKoinModules 16 | import org.koin.dsl.module 17 | 18 | fun loadModules() = loadKoinModules( 19 | listOf( 20 | viewModelModule, 21 | onboardingModule, 22 | repositoryModule, 23 | useCaseModule 24 | ) 25 | ) 26 | 27 | fun unloadModules() = unloadKoinModules( 28 | listOf( 29 | viewModelModule, 30 | onboardingModule, 31 | repositoryModule, 32 | useCaseModule 33 | ) 34 | ) 35 | 36 | val viewModelModule = module(override = true) { 37 | viewModel { OnboardingViewModel(androidApplication(), get(), get()) } 38 | } 39 | 40 | val onboardingModule = module(override = true) { 41 | factory { ParallaxPageTransformer() } 42 | factory { (activity: OnboardingActivity) -> OnboardingAdapter(activity) } 43 | factory { (activity: OnboardingActivity) -> activity.ViewPager2PageChangeCallback() } 44 | } 45 | 46 | val repositoryModule = module { 47 | factory { OnboardingDataRepository(get()) } 48 | factory { OnboardingCachedDataSourceImpl(get()) } 49 | } 50 | 51 | val useCaseModule = module { 52 | factory { SavePlatform(get()) } 53 | } 54 | -------------------------------------------------------------------------------- /onboarding/src/main/java/com/mobiaxe/onboarding/presentation/OnboardingActivity.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.onboarding.presentation 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.viewpager2.widget.ViewPager2 6 | import com.google.android.material.tabs.TabLayoutMediator 7 | import com.mobiaxe.core.presentation.BaseActivity 8 | import com.mobiaxe.onboarding.R 9 | import com.mobiaxe.onboarding.databinding.ActivityOnboardingBinding 10 | import com.mobiaxe.onboarding.koin.loadModules 11 | import com.mobiaxe.onboarding.koin.unloadModules 12 | import org.koin.android.ext.android.inject 13 | import org.koin.androidx.viewmodel.ext.android.viewModel 14 | import org.koin.core.parameter.parametersOf 15 | 16 | 17 | class OnboardingActivity : BaseActivity() { 18 | 19 | companion object { 20 | private const val IMAGE_PARALLAX = 2f 21 | private const val TITLE_PARALLAX_ENTER = -1.95f 22 | private const val TITLE_PARALLAX_EXIT = 0.5f 23 | } 24 | 25 | private val onboardingAdapter: OnboardingAdapter by inject { parametersOf(this) } 26 | 27 | private val mPageTransformer: ParallaxPageTransformer by inject() 28 | 29 | private val viewPage2Callback: ViewPager2PageChangeCallback by inject { parametersOf(this) } 30 | 31 | private val tabIconArray = arrayOf( 32 | R.drawable.tab_page_one_selector, 33 | R.drawable.tab_page_two_selector, 34 | R.drawable.tab_page_three_selector, 35 | R.drawable.tab_page_four_selector 36 | ) 37 | 38 | override fun onCreate(savedInstanceState: Bundle?) { 39 | super.onCreate(savedInstanceState) 40 | addFragments() 41 | setUpParallaxEffect() 42 | setUpTabIndicator() 43 | initListener() 44 | } 45 | 46 | override fun getLayoutId(): Int = R.layout.activity_onboarding 47 | 48 | override fun getViewModel(): Lazy = viewModel() 49 | 50 | override fun bindViewModel(dataBinding: ActivityOnboardingBinding) { 51 | 52 | } 53 | 54 | override fun onResume() { 55 | super.onResume() 56 | runBasicImmersive() 57 | } 58 | 59 | private fun runBasicImmersive() { 60 | val uiOptions: Int = window.decorView.systemUiVisibility 61 | var newUiOptions = uiOptions 62 | newUiOptions = newUiOptions xor View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 63 | newUiOptions = newUiOptions xor View.SYSTEM_UI_FLAG_FULLSCREEN 64 | newUiOptions = newUiOptions xor View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY 65 | window.decorView.systemUiVisibility = newUiOptions 66 | } 67 | 68 | private fun addFragments() { 69 | getViewModel().value.getOnboardingData().map { 70 | onboardingAdapter.addFragment(OnboardingFragment.newInstance(it)) 71 | } 72 | onboardingAdapter.addFragment(SelectPlatformFragment.newInstance()) 73 | } 74 | 75 | private fun setUpParallaxEffect() { 76 | getBinding().apply { 77 | pagerOnboarding.adapter = onboardingAdapter 78 | pagerOnboarding.registerOnPageChangeCallback(viewPage2Callback) 79 | 80 | val transformationImage = ParallaxPageTransformer().ParallaxTransformInformation( 81 | R.id.img_onboarding, 82 | IMAGE_PARALLAX, 83 | IMAGE_PARALLAX 84 | ) 85 | mPageTransformer.addViewToParallax(transformationImage) 86 | 87 | val transformationTitle = ParallaxPageTransformer().ParallaxTransformInformation( 88 | R.id.title_onboarding, 89 | TITLE_PARALLAX_ENTER, 90 | TITLE_PARALLAX_EXIT 91 | ) 92 | mPageTransformer.addViewToParallax(transformationTitle) 93 | 94 | val transformationDescription = ParallaxPageTransformer().ParallaxTransformInformation( 95 | R.id.description_onboarding, 96 | TITLE_PARALLAX_ENTER, 97 | TITLE_PARALLAX_EXIT 98 | ) 99 | mPageTransformer.addViewToParallax(transformationDescription) 100 | 101 | pagerOnboarding.setPageTransformer(mPageTransformer) 102 | 103 | TabLayoutMediator(pagerIndicator, pagerOnboarding) { _, _ -> 104 | }.attach() 105 | } 106 | } 107 | 108 | private fun setUpTabIndicator() { 109 | getBinding().pagerIndicator.apply { 110 | onboardingAdapter.getFragmentList().forEachIndexed { i, _ -> 111 | this.getTabAt(i)?.setIcon(tabIconArray[i]) 112 | } 113 | } 114 | } 115 | 116 | private fun initListener() { 117 | getBinding().apply { 118 | pagerRightBtn.setOnClickListener { 119 | if (!vm().lastPageObservable.get()) 120 | pagerOnboarding.setCurrentItem(pagerOnboarding.currentItem + 1, true) 121 | } 122 | 123 | pagerLeftBtn.setOnClickListener { 124 | if (!vm().lastPageObservable.get()) 125 | pagerOnboarding.setCurrentItem(pagerOnboarding.currentItem - 1, true) 126 | } 127 | } 128 | } 129 | 130 | override fun loadKoinModules() { 131 | loadModules() 132 | } 133 | 134 | override fun unloadKoinModules() { 135 | unloadModules() 136 | } 137 | 138 | override fun onDestroy() { 139 | super.onDestroy() 140 | getBinding().pagerOnboarding.unregisterOnPageChangeCallback(viewPage2Callback) 141 | } 142 | 143 | inner class ViewPager2PageChangeCallback : ViewPager2.OnPageChangeCallback() { 144 | override fun onPageScrolled( 145 | position: Int, 146 | positionOffset: Float, 147 | positionOffsetPixels: Int 148 | ) { 149 | if (onboardingAdapter.getFragmentList().size > 1) { 150 | val newProgress = 151 | (position + positionOffset) / (onboardingAdapter.getFragmentList().size - 1) 152 | getBinding().onboardingRoot.progress = newProgress 153 | } 154 | } 155 | 156 | override fun onPageSelected(position: Int) { 157 | vm().lastPageObservable.set(onboardingAdapter.getFragmentList().size.minus(1) == position) 158 | } 159 | } 160 | 161 | override fun onBackPressed() { 162 | finishAfterTransition() 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /onboarding/src/main/java/com/mobiaxe/onboarding/presentation/OnboardingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.onboarding.presentation 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentActivity 5 | import androidx.viewpager2.adapter.FragmentStateAdapter 6 | 7 | class OnboardingAdapter(context: FragmentActivity) : FragmentStateAdapter(context) { 8 | 9 | private var fragmentList = mutableListOf() 10 | 11 | 12 | fun getFragmentList() = fragmentList 13 | 14 | fun addFragment(fragment: Fragment) { 15 | fragmentList.add(fragment) 16 | } 17 | 18 | override fun getItemCount(): Int = fragmentList.size 19 | 20 | override fun createFragment(position: Int): Fragment = fragmentList[position] 21 | } -------------------------------------------------------------------------------- /onboarding/src/main/java/com/mobiaxe/onboarding/presentation/OnboardingFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.onboarding.presentation 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import com.mobiaxe.core.presentation.BaseFragment 8 | import com.mobiaxe.onboarding.R 9 | import com.mobiaxe.onboarding.data.OnboardingData 10 | import com.mobiaxe.onboarding.databinding.FragmentOnboardingBinding 11 | import org.koin.androidx.viewmodel.ext.android.viewModel 12 | 13 | class OnboardingFragment : BaseFragment() { 14 | 15 | private lateinit var onboardingData: OnboardingData 16 | 17 | companion object { 18 | 19 | private const val DATA = "data" 20 | 21 | fun newInstance(data: OnboardingData): OnboardingFragment { 22 | val fragment = OnboardingFragment() 23 | val b = Bundle() 24 | b.putParcelable(DATA, data) 25 | fragment.arguments = b 26 | return fragment 27 | } 28 | } 29 | 30 | override fun getLayoutId(): Int = R.layout.fragment_onboarding 31 | 32 | override fun getViewModel(): Lazy = viewModel() 33 | 34 | override fun bindViewModel(dataBinding: FragmentOnboardingBinding) { 35 | dataBinding.onboardingData = onboardingData 36 | dataBinding.executePendingBindings() 37 | } 38 | 39 | override fun onCreateView( 40 | inflater: LayoutInflater, 41 | container: ViewGroup?, 42 | savedInstanceState: Bundle? 43 | ): View? { 44 | val rootView = super.onCreateView(inflater, container, savedInstanceState) 45 | onboardingData = arguments?.get(DATA) as OnboardingData 46 | rootView?.tag = onboardingData.position 47 | return rootView 48 | } 49 | 50 | override fun loadKoinModules() { 51 | } 52 | 53 | override fun unloadKoinModules() { 54 | } 55 | } -------------------------------------------------------------------------------- /onboarding/src/main/java/com/mobiaxe/onboarding/presentation/OnboardingViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.onboarding.presentation 2 | 3 | import android.app.Application 4 | import androidx.databinding.ObservableBoolean 5 | import androidx.lifecycle.LiveData 6 | import androidx.lifecycle.MutableLiveData 7 | import com.mobiaxe.core.presentation.BaseViewModel 8 | import com.mobiaxe.onboarding.R 9 | import com.mobiaxe.onboarding.data.OnboardingData 10 | import com.mobiaxe.onboarding.domain.usecase.SavePlatform 11 | import com.mobiaxe.wasd.splash.domain.usecase.OnboardingPassed 12 | 13 | class OnboardingViewModel( 14 | private val appContext: Application, 15 | private val onboardingPassed: OnboardingPassed, 16 | private val savePlatform: SavePlatform 17 | ): BaseViewModel(savePlatform) { 18 | 19 | private val _passOnboardingLiveData = MutableLiveData() 20 | val passOnboardingLiveData: LiveData = _passOnboardingLiveData 21 | 22 | val lastPageObservable = ObservableBoolean(false) 23 | val checkWindowsObsv = ObservableBoolean(false) 24 | val checkPlaystationObsv = ObservableBoolean(false) 25 | val checkXboxObsv = ObservableBoolean(false) 26 | val checkNintendoObsv = ObservableBoolean(false) 27 | 28 | private val selectedPlatforms = mutableSetOf() 29 | 30 | companion object { 31 | private const val SWITCH = 130 32 | private const val PC = 6 33 | private const val XBOX = 49 34 | private const val PS4 = 48 35 | } 36 | 37 | fun isOnboardingPassed() { 38 | _passOnboardingLiveData.value = onboardingPassed.execute(true) 39 | } 40 | 41 | fun getOnboardingData(): List { 42 | val titleList = appContext.resources.getStringArray(R.array.title_onboarding) 43 | val descriptionList = appContext.resources.getStringArray(R.array.description_onboarding) 44 | val drawableList = mutableListOf( 45 | R.drawable.witcher_onboarding_one, 46 | R.drawable.godofwar_onboarding_two, 47 | R.drawable.rdr_onboarding_three) 48 | 49 | return (titleList.indices).map { i -> 50 | OnboardingData(titleList[i], descriptionList[i], drawableList[i], i) 51 | } 52 | } 53 | 54 | fun selectWindows() { 55 | if (!checkWindowsObsv.get()) { 56 | checkWindowsObsv.set(true) 57 | selectedPlatforms.add(PC) 58 | } else { 59 | checkWindowsObsv.set(false) 60 | selectedPlatforms.remove(PC) 61 | } 62 | 63 | } 64 | 65 | fun selectPlaystation() { 66 | if (!checkPlaystationObsv.get()) { 67 | checkPlaystationObsv.set(true) 68 | selectedPlatforms.add(PS4) 69 | } else { 70 | checkPlaystationObsv.set(false) 71 | selectedPlatforms.remove(PS4) 72 | } 73 | } 74 | 75 | fun selectXBox() { 76 | if (!checkXboxObsv.get()) { 77 | checkXboxObsv.set(true) 78 | selectedPlatforms.add(XBOX) 79 | } else { 80 | checkXboxObsv.set(false) 81 | selectedPlatforms.remove(XBOX) 82 | } 83 | } 84 | 85 | fun selectNintendo() { 86 | if (!checkNintendoObsv.get()) { 87 | checkNintendoObsv.set(true) 88 | selectedPlatforms.add(SWITCH) 89 | } else { 90 | checkNintendoObsv.set(false) 91 | selectedPlatforms.remove(SWITCH) 92 | } 93 | } 94 | 95 | fun savePlatform() { 96 | savePlatform.execute(selectedPlatforms.joinToString(prefix = "(", postfix = ")")) { 97 | isOnboardingPassed() 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /onboarding/src/main/java/com/mobiaxe/onboarding/presentation/ParallaxPageTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.onboarding.presentation 2 | 3 | import android.view.View 4 | import androidx.viewpager2.widget.ViewPager2 5 | 6 | class ParallaxPageTransformer : ViewPager2.PageTransformer { 7 | 8 | private val mViewsToParallax = mutableListOf() 9 | 10 | companion object { 11 | const val PARALLAX_EFFECT_DEFAULT = -101.1986f 12 | } 13 | 14 | fun addViewToParallax(viewInfo: ParallaxTransformInformation): ParallaxPageTransformer { 15 | mViewsToParallax.add(viewInfo) 16 | return this 17 | } 18 | 19 | override fun transformPage(page: View, position: Float) { 20 | val pageWidth: Int = page.width 21 | 22 | if (position < -1) { 23 | page.alpha = 1f 24 | } else if (position <= 1 && mViewsToParallax.isNotEmpty()) { 25 | for (parallaxTransformInformation in mViewsToParallax) { 26 | applyParallaxEffect( 27 | page, position, pageWidth, parallaxTransformInformation, 28 | position > 0 29 | ) 30 | } 31 | } else { 32 | page.alpha = 1f 33 | } 34 | } 35 | 36 | private fun applyParallaxEffect( 37 | page: View, 38 | position: Float, 39 | pageWidth: Int, 40 | information: ParallaxTransformInformation, 41 | isEnter: Boolean 42 | ) { 43 | if (information.isValid() && page.findViewById(information.resource) != null) { 44 | if (isEnter && information.isEnterDefault().not()) { 45 | page.findViewById(information.resource).translationX = -position * (pageWidth / information.parallaxEnterEffect) 46 | } else if (isEnter.not() && information.isExitDefault().not()) { 47 | page.findViewById(information.resource).translationX = -position * (pageWidth / information.parallaxExitEffect) 48 | } 49 | } 50 | } 51 | 52 | inner class ParallaxTransformInformation( 53 | val resource: Int, 54 | val parallaxEnterEffect: Float, 55 | val parallaxExitEffect: Float 56 | ) { 57 | 58 | fun isValid() = parallaxEnterEffect != 0f && parallaxExitEffect != 0f && resource != -1 59 | 60 | fun isEnterDefault() = parallaxEnterEffect == PARALLAX_EFFECT_DEFAULT 61 | 62 | fun isExitDefault() = parallaxExitEffect == PARALLAX_EFFECT_DEFAULT 63 | } 64 | } -------------------------------------------------------------------------------- /onboarding/src/main/java/com/mobiaxe/onboarding/presentation/SelectPlatformFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.onboarding.presentation 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import com.mobiaxe.core.extension.observe 6 | import com.mobiaxe.core.extension.start 7 | import com.mobiaxe.core.presentation.BaseFragment 8 | import com.mobiaxe.onboarding.R 9 | import com.mobiaxe.onboarding.databinding.FragmentPlatformBinding 10 | import com.mobiaxe.wasd.navigation.Navigation 11 | import org.koin.androidx.viewmodel.ext.android.viewModel 12 | 13 | class SelectPlatformFragment : BaseFragment() { 14 | 15 | companion object { 16 | 17 | fun newInstance(): SelectPlatformFragment { 18 | val fragment = SelectPlatformFragment() 19 | val b = Bundle() 20 | fragment.arguments = b 21 | return fragment 22 | } 23 | } 24 | 25 | override fun getLayoutId(): Int = R.layout.fragment_platform 26 | 27 | override fun getViewModel(): Lazy = viewModel() 28 | 29 | override fun bindViewModel(dataBinding: FragmentPlatformBinding) { 30 | dataBinding.viewModel = vModel() 31 | } 32 | 33 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 34 | super.onViewCreated(view, savedInstanceState) 35 | initListeners() 36 | initObservers() 37 | } 38 | 39 | private fun initObservers() { 40 | with(vModel()) { 41 | observe(passOnboardingLiveData) { 42 | requireActivity().start(Navigation.navigateOnboardingOrDashboard(it)) 43 | requireActivity().finish() 44 | } 45 | } 46 | } 47 | 48 | 49 | private fun initListeners() { 50 | getBinding().apply { 51 | btnWindows.setOnClickListener { 52 | vModel().selectWindows() 53 | } 54 | 55 | btnNintendo.setOnClickListener { 56 | vModel().selectNintendo() 57 | } 58 | 59 | btnPlaystation.setOnClickListener { 60 | vModel().selectPlaystation() 61 | } 62 | 63 | btnXbox.setOnClickListener { 64 | vModel().selectXBox() 65 | } 66 | 67 | btnContinue.setOnClickListener { 68 | vModel().savePlatform() 69 | } 70 | } 71 | } 72 | 73 | override fun loadKoinModules() { 74 | } 75 | 76 | override fun unloadKoinModules() { 77 | } 78 | } -------------------------------------------------------------------------------- /onboarding/src/main/java/com/mobiaxe/onboarding/util/BindingUtils.kt: -------------------------------------------------------------------------------- 1 | package com.mobiaxe.onboarding.util 2 | 3 | import android.graphics.Rect 4 | import android.view.TouchDelegate 5 | import android.view.View 6 | import android.widget.ImageView 7 | import androidx.databinding.BindingAdapter 8 | import com.bumptech.glide.Glide 9 | import com.bumptech.glide.request.RequestOptions 10 | import com.google.android.material.card.MaterialCardView 11 | import com.bumptech.glide.request.target.Target 12 | import com.mobiaxe.core.extension.px 13 | 14 | 15 | @BindingAdapter("isSelected") 16 | fun MaterialCardView.setSelected(isSelected: Boolean) { 17 | isChecked = isSelected 18 | isHovered = isSelected 19 | isPressed = isSelected 20 | } 21 | 22 | @BindingAdapter("imageDrawable") 23 | fun setImageDrawable(imageView: ImageView, res: Int?) { 24 | val context = imageView.context 25 | Glide.with(context).load(res).apply(RequestOptions().override(Target.SIZE_ORIGINAL)).into(imageView) 26 | } 27 | 28 | @BindingAdapter("increaseClickingArea") 29 | fun increaseClickingArea(view: View, extraSpace: Int) { 30 | val parent = view.parent as View 31 | parent.post { 32 | val touchableArea = Rect() 33 | view.getHitRect(touchableArea) 34 | touchableArea.top -= extraSpace.px 35 | touchableArea.bottom += extraSpace.px 36 | touchableArea.left -= extraSpace.px 37 | touchableArea.right += extraSpace.px 38 | parent.touchDelegate = TouchDelegate(touchableArea, view) 39 | } 40 | } -------------------------------------------------------------------------------- /onboarding/src/main/res/color/onboarding_continue_btn_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable-xxxhdpi/godofwar_onboarding_two.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/onboarding/src/main/res/drawable-xxxhdpi/godofwar_onboarding_two.jpg -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable-xxxhdpi/platform_background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/onboarding/src/main/res/drawable-xxxhdpi/platform_background.jpg -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable-xxxhdpi/rdr_onboarding_three.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/onboarding/src/main/res/drawable-xxxhdpi/rdr_onboarding_three.jpg -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable-xxxhdpi/witcher_onboarding_one.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abalta/Kotlin-DynamicFeature-Clean/98217b9dfd24438cb81b5b4208f7fd9679bf43f1/onboarding/src/main/res/drawable-xxxhdpi/witcher_onboarding_one.jpg -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/ic_nintendo.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 46 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/ic_nintendo_colored.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 43 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/ic_nintendo_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/ic_onboarding_left.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/ic_onboarding_right.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/ic_pc_colored.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 57 | 60 | 63 | 66 | 67 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/ic_pc_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/ic_playstation.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/ic_ps4_colored.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/ic_ps4_white.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/ic_tick.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/ic_windows.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/ic_xbox.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/ic_xbox_colored.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 57 | 58 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/ic_xbox_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 37 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/tab_page_four_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/tab_page_one_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/tab_page_three_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /onboarding/src/main/res/drawable/tab_page_two_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /onboarding/src/main/res/layout/activity_onboarding.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 22 | 23 | 33 | 34 | 44 | 45 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /onboarding/src/main/res/layout/fragment_onboarding.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 25 | 26 | 38 | 39 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /onboarding/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Discover 7 | Play 8 | Gamer 9 | 10 | 11 | 12 | 13 | 14 | Check out popular games.\nFollow upcoming. 15 | Make your favorite list.\nComment and like. 16 | Casual, Pro, Hardcore.\nWhat kind of gamer are you? 17 | 18 | 19 | -------------------------------------------------------------------------------- /onboarding/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Choose your platform 4 | Are the keyboard and mouse indispensable?\nOr are you a hardcore console player? 5 | PC 6 | Play Station 7 | Xbox 8 | Nintendo 9 | Continue 10 | 11 | -------------------------------------------------------------------------------- /onboarding/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /onboarding/src/main/res/xml/onboarding_scene.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 36 | 37 | 38 | 47 | 48 | 56 | 57 | 58 | 59 | 68 | 69 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include(":app") 2 | include(":core") 3 | include(":onboarding") 4 | include(":home") 5 | 6 | rootProject.name = "WASD" 7 | rootProject.buildFileName = "build.gradle.kts" 8 | 9 | --------------------------------------------------------------------------------