├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── smarttoolfactory │ │ └── postdynamichilt │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── smarttoolfactory │ │ │ └── postdynamichilt │ │ │ ├── MainActivity.kt │ │ │ ├── PostApplication.kt │ │ │ ├── main │ │ │ ├── MainFragment.kt │ │ │ ├── MainFragmentBottomNav.kt │ │ │ └── MainFragmentViewPager2.kt │ │ │ └── ui │ │ │ ├── BottomNavigationFragmentStateAdapter.kt │ │ │ ├── DynamicBottomNavigationStateAdapter.kt │ │ │ └── DynamicFragmentStateAdapter.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── avatar_1_raster.png │ │ ├── avatar_2_raster.png │ │ ├── avatar_3_raster.png │ │ ├── avatar_4_raster.png │ │ ├── avatar_5_raster.png │ │ ├── avatar_6_raster.png │ │ ├── ic_baseline_account_circle_24.xml │ │ ├── ic_baseline_create_24.xml │ │ ├── ic_baseline_dashboard_24.xml │ │ ├── ic_baseline_favorite_24.xml │ │ ├── ic_baseline_home_24.xml │ │ ├── ic_baseline_notifications_24.xml │ │ ├── ic_baseline_personal_video_24.xml │ │ ├── ic_baseline_thumb_up_24.xml │ │ ├── ic_launcher_background.xml │ │ └── ic_outline_thumb_up_24.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── fragment_main.xml │ │ ├── fragment_main_bottom_nav.xml │ │ ├── fragment_main_viewpager2.xml │ │ ├── fragment_navhost.xml │ │ ├── fragment_navhost_account.xml │ │ ├── fragment_navhost_dashboard.xml │ │ ├── fragment_navhost_home.xml │ │ └── fragment_navhost_notification.xml │ │ ├── menu │ │ └── menu_bottom.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_graph_dfm_account_start.xml │ │ ├── nav_graph_dfm_dashboard_start.xml │ │ ├── nav_graph_dfm_home_start.xml │ │ ├── nav_graph_dfm_notification_start.xml │ │ ├── nav_graph_main.xml │ │ ├── nav_graph_main_bottom_nav.xml │ │ └── nav_graph_main_viewpager2.xml │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── smarttoolfactory │ └── postdynamichilt │ ├── ExampleUnitTest.kt │ └── postlist │ ├── PostListViewModelRxJava3Test.kt │ └── PostListViewModelTest.kt ├── build.gradle.kts ├── buildSrc ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── java │ ├── Dependencies.kt │ ├── Modules.kt │ ├── Plugins.kt │ ├── Version.kt │ └── extension │ └── DependencyHandlerExtension.kt ├── config └── detekt │ └── detekt.yml ├── features ├── account │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── smarttoolfactory │ │ │ └── account │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── smarttoolfactory │ │ │ │ └── account │ │ │ │ └── AccountFragment.kt │ │ └── res │ │ │ ├── layout │ │ │ └── fragment_account.xml │ │ │ └── navigation │ │ │ └── nav_graph_account.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── smarttoolfactory │ │ └── account │ │ └── ExampleUnitTest.kt ├── dashboard │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── smarttoolfactory │ │ │ └── dashboard │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── smarttoolfactory │ │ │ │ └── dashboard │ │ │ │ └── DashboardFragment.kt │ │ └── res │ │ │ ├── layout │ │ │ ├── fragment_dashboard.xml │ │ │ └── fragment_navhost_dashboard.xml │ │ │ └── navigation │ │ │ └── nav_graph_dashboard.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── smarttoolfactory │ │ └── dashboard │ │ └── ExampleUnitTest.kt ├── home │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── smarttoolfactory │ │ │ └── home │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── smarttoolfactory │ │ │ │ └── home │ │ │ │ ├── HomeFragment.kt │ │ │ │ ├── TestFragment.kt │ │ │ │ ├── adapter │ │ │ │ ├── HomeViewPager2FragmentStateAdapter.kt │ │ │ │ └── PostListAdapter.kt │ │ │ │ ├── di │ │ │ │ ├── PostListComponent.kt │ │ │ │ └── PostListModule.kt │ │ │ │ ├── postlist │ │ │ │ ├── PostListFlowFragment.kt │ │ │ │ ├── PostListRxJava3Fragment.kt │ │ │ │ └── PostListWithStatusFragment.kt │ │ │ │ ├── util │ │ │ │ └── ViewBindings.kt │ │ │ │ └── viewmodel │ │ │ │ ├── AbstractPostListVM.kt │ │ │ │ ├── PostListViewModelFlow.kt │ │ │ │ ├── PostListViewModelRxJava3.kt │ │ │ │ ├── PostStatusViewModel.kt │ │ │ │ └── ViewModelFactory.kt │ │ └── res │ │ │ ├── drawable │ │ │ ├── avatar_1_raster.png │ │ │ ├── avatar_2_raster.png │ │ │ ├── avatar_3_raster.png │ │ │ ├── avatar_4_raster.png │ │ │ ├── avatar_5_raster.png │ │ │ ├── avatar_6_raster.png │ │ │ ├── ic_baseline_thumb_up_24.xml │ │ │ ├── ic_launcher_background.xml │ │ │ └── ic_outline_thumb_up_24.xml │ │ │ ├── layout │ │ │ ├── fragment_home.xml │ │ │ ├── fragment_navhost_post_list.xml │ │ │ ├── fragment_navhost_post_list_flow.xml │ │ │ ├── fragment_navhost_post_list_rxjava3.xml │ │ │ ├── fragment_post_list.xml │ │ │ ├── fragment_test.xml │ │ │ ├── row_post.xml │ │ │ ├── row_post_grid.xml │ │ │ └── row_post_staggered.xml │ │ │ └── navigation │ │ │ ├── nav_graph_home.xml │ │ │ ├── nav_graph_post_list.xml │ │ │ ├── nav_graph_post_list_flow.xml │ │ │ └── nav_graph_post_list_rxjava3.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── smarttoolfactory │ │ └── home │ │ ├── ExampleUnitTest.kt │ │ └── postlist │ │ ├── PostListViewModelRxJava3Test.kt │ │ └── PostListViewModelTest.kt ├── notification │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── smarttoolfactory │ │ │ └── notification │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── smarttoolfactory │ │ │ │ └── notification │ │ │ │ └── NotificationFragment.kt │ │ └── res │ │ │ ├── layout │ │ │ └── fragment_notification.xml │ │ │ └── navigation │ │ │ └── nav_graph_notification.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── smarttoolfactory │ │ └── notification │ │ └── ExampleUnitTest.kt ├── post_detail │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── smarttoolfactory │ │ │ └── post_detail │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── smarttoolfactory │ │ │ │ └── post_detail │ │ │ │ ├── PostBinding.kt │ │ │ │ ├── PostDetailFragment.kt │ │ │ │ ├── PostDetailViewModel.kt │ │ │ │ └── di │ │ │ │ ├── PostDetailComponent.kt │ │ │ │ └── PostDetailModule.kt │ │ └── res │ │ │ ├── layout │ │ │ └── fragment_post_detail.xml │ │ │ └── navigation │ │ │ └── nav_graph_post_detail.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── smarttoolfactory │ │ └── post_detail │ │ └── ExampleUnitTest.kt └── search │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── smarttoolfactory │ │ └── search │ │ └── ExampleInstrumentedTest.kt │ ├── main │ └── AndroidManifest.xml │ └── test │ └── java │ └── com │ └── smarttoolfactory │ └── search │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── libraries ├── core │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── smarttoolfactory │ │ │ └── core │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── assets │ │ │ ├── construction_process.json │ │ │ └── under_construction.json │ │ └── java │ │ │ └── com │ │ │ └── smarttoolfactory │ │ │ └── core │ │ │ ├── CoreDependency.kt │ │ │ ├── di │ │ │ ├── CoreModule.kt │ │ │ ├── CoreModuleDependencies.kt │ │ │ ├── DataModule.kt │ │ │ └── scope │ │ │ │ └── FeatureScope.kt │ │ │ ├── error │ │ │ └── NavigationException.kt │ │ │ ├── ui │ │ │ ├── adapter │ │ │ │ ├── BaseListAdapter.kt │ │ │ │ └── NavigableFragmentStateAdapter.kt │ │ │ ├── fragment │ │ │ │ ├── BaseDataBindingFragment.kt │ │ │ │ ├── DynamicNavigationFragment.kt │ │ │ │ └── navhost │ │ │ │ │ ├── BaseDynamicNavHostFragment.kt │ │ │ │ │ ├── BaseNavHostFragment.kt │ │ │ │ │ └── NavHostContainerFragment.kt │ │ │ └── widget │ │ │ │ └── NestedScrollableHost.kt │ │ │ ├── util │ │ │ ├── Event.kt │ │ │ ├── FlowViewStateExtension.kt │ │ │ ├── LifecycleOwnerExtension.kt │ │ │ ├── NavHostExtension.kt │ │ │ ├── NavigationExtensions.kt │ │ │ └── RxJavaViewStateExtension.kt │ │ │ ├── viewmodel │ │ │ └── NavControllerViewModel.kt │ │ │ └── viewstate │ │ │ └── ViewState.kt │ │ └── test │ │ └── java │ │ └── com │ │ └── smarttoolfactory │ │ └── core │ │ └── ExampleUnitTest.kt ├── data │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ ├── schemas │ │ └── com.smarttoolfactory.data.db.PostDatabase │ │ │ └── 2.json │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── smarttoolfactory │ │ │ └── data │ │ │ ├── PostDaoCoroutinesCoroutinesFlowTest.kt │ │ │ ├── PostDaoCoroutinesRxJavaTest.kt │ │ │ └── PostDaoCoroutinesTestSuite.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── smarttoolfactory │ │ │ └── data │ │ │ ├── api │ │ │ └── PostApi.kt │ │ │ ├── db │ │ │ ├── PostDaoCoroutines.kt │ │ │ ├── PostDaoRxJava3.kt │ │ │ ├── PostDatabase.kt │ │ │ └── PostStatusDao.kt │ │ │ ├── di │ │ │ ├── DatabaseModule.kt │ │ │ └── NetworkModule.kt │ │ │ ├── mapper │ │ │ └── Mappers.kt │ │ │ ├── model │ │ │ ├── PostDTO.kt │ │ │ └── PostEntity.kt │ │ │ ├── repository │ │ │ ├── PostRepositoryCoroutines.kt │ │ │ ├── PostRepositoryImpl.kt │ │ │ └── PostStatusRepository.kt │ │ │ └── source │ │ │ ├── PostDataSource.kt │ │ │ ├── PostDataSourceImpl.kt │ │ │ └── PostStatusDataSource.kt │ │ └── test │ │ └── java │ │ └── com │ │ └── smarttoolfactory │ │ └── data │ │ ├── api │ │ ├── AbstractPostApiTest.kt │ │ ├── PostApiCoroutinesTest.kt │ │ └── PostApiRxJava3Test.kt │ │ ├── mapper │ │ └── DTOtoEntityMapperTest.kt │ │ ├── repository │ │ ├── PostRepositoryCoroutinesTest.kt │ │ └── PostRepositoryRxJava3Test.kt │ │ ├── source │ │ ├── PostDataSourceCoroutinesTest.kt │ │ └── PostDataSourceRxJava3Test.kt │ │ └── test_suite │ │ ├── JUnit5DataTestSuite.kt │ │ └── JUnit5TestSuite.java ├── domain │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── smarttoolfactory │ │ │ └── domain │ │ │ ├── base │ │ │ └── Disposable.kt │ │ │ ├── di │ │ │ ├── DomainModule.kt │ │ │ └── DomainModuleDependencies.kt │ │ │ ├── dispatcher │ │ │ └── UseCaseDispatchers.kt │ │ │ ├── error │ │ │ └── EmptyDataException.kt │ │ │ ├── mapper │ │ │ └── EntityToPostMapper.kt │ │ │ ├── model │ │ │ └── Post.kt │ │ │ ├── usecase │ │ │ ├── GetPostListUseCaseFlow.kt │ │ │ ├── GetPostListUseCaseRxJava3.kt │ │ │ └── GetPostsWithStatusUseCaseFlow.kt │ │ │ └── util │ │ │ ├── RxJavaExtensions.kt │ │ │ └── RxJavaLifeCycleExtensions.kt │ │ └── test │ │ └── java │ │ └── com │ │ └── smarttoolfactory │ │ └── domain │ │ ├── ExampleUnitTest.kt │ │ └── usecase │ │ ├── GetPostListUseCaseFlowTest.kt │ │ └── GetPostListUseCaseRxJava3Test.kt └── test-utils │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── smarttoolfactory │ │ └── test_utils │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── smarttoolfactory │ │ └── test_utils │ │ ├── TestConstans.kt │ │ ├── extension │ │ ├── RxImmediateSchedulerExtension.kt │ │ └── TestCoroutineExtension.kt │ │ ├── rule │ │ ├── MockWebServerRule.kt │ │ ├── RxImmediateSchedulerRule.kt │ │ └── TestCoroutineRule.kt │ │ ├── test_observer │ │ ├── FlowTestObserver.kt │ │ └── LiveDataTestObserver.kt │ │ └── util │ │ ├── LiveDataTestUtil.kt │ │ └── ReadResourceUtil.kt │ └── test │ ├── java │ └── com │ │ └── smarttoolfactory │ │ └── test_utils │ │ └── ExampleUnitTest.kt │ └── resources │ └── response.json ├── screenshots ├── dashboard.png ├── post_flow.png ├── post_rxjava3.png └── post_with_status.png ├── scripts └── git-hooks │ └── pre-commit └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/* 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MVVM Clean Architecture with RxJava3+Coroutines Flow, Static Code Analysis, Dagger Hilt, Dynamic Features 2 | 3 | [![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/) 4 | [![Kotlin Version](https://img.shields.io/badge/kotlin-1.4.0-blue.svg)](https://kotlinlang.org) 5 | [![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21) 6 | 7 | 8 | Sample project that build with MVVM clean architure and various cool tech stack including RxJava3 and Coroutines Flow, Dynamic Feature modules as base of BottomNavigationView or ViewPager2, with both OfflineFirst and OfflineLast approaches as database Single Source of Truth 9 | 10 | | Posts with Stautus | Flow | RxJava3 | Dashboard 11 | | ------------------|-------------| -----|--------------| 12 | | | | | | 13 | 14 | 15 | ### Overview 16 | * Gradle Kotlin DSL is used for setting up gradle files with ```buildSrc``` folder and extensions. 17 | * KtLint, Detekt, and Git Hooks is used for checking, and formatting code and validating code before commits. 18 | * Dagger Hilt, Dynamic Feature Modules with Navigation Components, ViewModel, Retrofit, Room, RxJava, Coroutines libraries adn dependencies are set up. 19 | * ```features``` and ```libraries``` folders are used to include android libraries and dynamic feature modules 20 | * In core module dagger hilt dependencies and ```@EntryPoint``` is created 21 | * Data module uses Retrofit and Room to provide Local and Remote data sources 22 | * Repository provides offline and remote fetch function with mapping and local save, delete and fetch functions 23 | 24 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /app/src/androidTest/java/com/smarttoolfactory/postdynamichilt/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.postdynamichilt 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import com.smarttoolfactory.domain.model.Post 6 | import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH 7 | import com.smarttoolfactory.test_utils.util.convertFromJsonToListOf 8 | import com.smarttoolfactory.test_utils.util.getResourceAsText 9 | import junit.framework.TestCase.assertEquals 10 | import org.junit.Test 11 | import org.junit.runner.RunWith 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * See [testing documentation](http://d.android.com/tools/testing). 17 | */ 18 | @RunWith(AndroidJUnit4::class) 19 | class ExampleInstrumentedTest { 20 | 21 | private val postList = 22 | convertFromJsonToListOf(getResourceAsText(RESPONSE_JSON_PATH))!! 23 | 24 | @Test 25 | fun testExtensionFunctions() { 26 | assertEquals(postList.size, 100) 27 | } 28 | @Test 29 | fun useAppContext() { 30 | // Context of the app under test. 31 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 32 | assertEquals("com.smarttoolfactory.postdynamichilt", appContext.packageName) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/postdynamichilt/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.postdynamichilt 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.databinding.DataBindingUtil 6 | import com.smarttoolfactory.postdynamichilt.databinding.ActivityMainBinding 7 | import dagger.hilt.android.AndroidEntryPoint 8 | 9 | @AndroidEntryPoint 10 | class MainActivity : AppCompatActivity() { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | val dataBinding: ActivityMainBinding = 15 | DataBindingUtil.setContentView(this, R.layout.activity_main) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/postdynamichilt/PostApplication.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.postdynamichilt 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class PostApplication : Application() { 8 | 9 | override fun onCreate() { 10 | super.onCreate() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/postdynamichilt/ui/BottomNavigationFragmentStateAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.postdynamichilt.ui 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentContainerView 5 | import androidx.fragment.app.FragmentManager 6 | import androidx.lifecycle.Lifecycle 7 | import com.smarttoolfactory.core.ui.adapter.NavigableFragmentStateAdapter 8 | import com.smarttoolfactory.core.ui.fragment.navhost.NavHostContainerFragment 9 | import com.smarttoolfactory.postdynamichilt.R 10 | 11 | /** 12 | * FragmentStateAdapter to contain ViewPager2 fragments inside another fragment which uses 13 | * wrapper layouts that contain [FragmentContainerView] 14 | * 15 | * * 🔥 Create FragmentStateAdapter with viewLifeCycleOwner instead of Fragment to make sure 16 | * that it lives between [Fragment.onCreateView] and [Fragment.onDestroyView] while [View] is alive 17 | * 18 | * * https://stackoverflow.com/questions/61779776/leak-canary-detects-memory-leaks-for-tablayout-with-viewpager2 19 | */ 20 | class BottomNavigationFragmentStateAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : 21 | NavigableFragmentStateAdapter(fragmentManager, lifecycle) { 22 | 23 | override fun getItemCount(): Int = 4 24 | 25 | override fun createFragment(position: Int): Fragment { 26 | return when (position) { 27 | 0 -> NavHostContainerFragment.createNavHostContainerFragment( 28 | R.layout.fragment_navhost_home, 29 | R.id.nested_nav_host_fragment_home 30 | ) 31 | 32 | // Vertical NavHost Post Fragment Container 33 | 1 -> NavHostContainerFragment.createNavHostContainerFragment( 34 | R.layout.fragment_navhost_dashboard, 35 | R.id.nested_nav_host_fragment_dashboard 36 | ) 37 | 38 | // Horizontal NavHost Post Fragment Container 39 | 2 -> NavHostContainerFragment.createNavHostContainerFragment( 40 | R.layout.fragment_navhost_notification, 41 | R.id.nested_nav_host_fragment_notification 42 | ) 43 | 44 | else -> NavHostContainerFragment.createNavHostContainerFragment( 45 | R.layout.fragment_navhost_account, 46 | R.id.nested_nav_host_fragment_account 47 | ) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/postdynamichilt/ui/DynamicBottomNavigationStateAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.postdynamichilt.ui 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentManager 5 | import androidx.lifecycle.Lifecycle 6 | import androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment 7 | import com.google.android.material.bottomnavigation.BottomNavigationView 8 | import com.smarttoolfactory.core.ui.adapter.NavigableFragmentStateAdapter 9 | import com.smarttoolfactory.core.ui.fragment.navhost.BaseDynamicNavHostFragment 10 | import com.smarttoolfactory.core.ui.fragment.navhost.BaseNavHostFragment 11 | import com.smarttoolfactory.postdynamichilt.R 12 | 13 | /** 14 | * ViewPager2 Adapter for changing tabs of BottomNavigationView with [BaseDynamicNavHostFragment] 15 | * which are extension of [DynamicNavHostFragment] that let's fragments from Dynamic Feature 16 | * Modules to be added as root of [BottomNavigationView] 17 | * 18 | */ 19 | class DynamicBottomNavigationStateAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : 20 | NavigableFragmentStateAdapter(fragmentManager, lifecycle) { 21 | 22 | override fun getItemCount(): Int = 4 23 | 24 | override fun createFragment(position: Int): Fragment { 25 | return when (position) { 26 | 27 | // Home nav graph 28 | 0 -> 29 | BaseNavHostFragment 30 | .createNavHostFragment(R.navigation.nav_graph_dfm_home_start) 31 | 32 | // Dashboard nav graph 33 | 1 -> 34 | BaseDynamicNavHostFragment 35 | .createDynamicNavHostFragment(R.navigation.nav_graph_dfm_dashboard_start) 36 | 37 | // Notification nav graph 38 | 2 -> 39 | BaseNavHostFragment 40 | .createNavHostFragment(R.navigation.nav_graph_dfm_notification_start) 41 | 42 | // Account nav graph 43 | else -> 44 | BaseNavHostFragment 45 | .createNavHostFragment(R.navigation.nav_graph_dfm_account_start) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/smarttoolfactory/postdynamichilt/ui/DynamicFragmentStateAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.postdynamichilt.ui 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentManager 5 | import androidx.lifecycle.Lifecycle 6 | import androidx.navigation.fragment.NavHostFragment 7 | import com.smarttoolfactory.core.ui.adapter.NavigableFragmentStateAdapter 8 | import com.smarttoolfactory.core.ui.fragment.navhost.BaseDynamicNavHostFragment 9 | import com.smarttoolfactory.core.ui.fragment.navhost.BaseNavHostFragment 10 | import com.smarttoolfactory.postdynamichilt.R 11 | 12 | // FIXME NOT working, solve the issue with Dynamic Fragments returning containerId 0 13 | 14 | /** 15 | * FragmentStateAdapter to add [BaseDynamicNavHostFragment]s without adding extra wrapper fragment. 16 | * This adapter uses [BaseDynamicNavHostFragment.createDynamicNavHostFragment] to create a 17 | * [NavHostFragment] using the navigation resource as parameter 18 | */ 19 | class DynamicFragmentStateAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : 20 | NavigableFragmentStateAdapter(fragmentManager, lifecycle) { 21 | 22 | override fun getItemCount(): Int = 4 23 | 24 | override fun createFragment(position: Int): Fragment { 25 | return when (position) { 26 | 27 | // Home nav graph 28 | 0 -> 29 | BaseDynamicNavHostFragment 30 | .createDynamicNavHostFragment(R.navigation.nav_graph_dfm_home_start) 31 | 32 | // Dashboard nav graph 33 | 1 -> 34 | BaseDynamicNavHostFragment 35 | .createDynamicNavHostFragment(R.navigation.nav_graph_dfm_dashboard_start) 36 | 37 | // Notification nav graph 38 | 2 -> 39 | BaseNavHostFragment 40 | .createNavHostFragment(R.navigation.nav_graph_dfm_notification_start) 41 | 42 | // Account nav graph 43 | else -> 44 | BaseNavHostFragment 45 | .createNavHostFragment(R.navigation.nav_graph_dfm_account_start) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/avatar_1_raster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/app/src/main/res/drawable/avatar_1_raster.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/avatar_2_raster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/app/src/main/res/drawable/avatar_2_raster.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/avatar_3_raster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/app/src/main/res/drawable/avatar_3_raster.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/avatar_4_raster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/app/src/main/res/drawable/avatar_4_raster.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/avatar_5_raster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/app/src/main/res/drawable/avatar_5_raster.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/avatar_6_raster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/app/src/main/res/drawable/avatar_6_raster.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_account_circle_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_create_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_dashboard_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_favorite_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_home_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_notifications_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_personal_video_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_thumb_up_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_outline_thumb_up_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 18 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_main_bottom_nav.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_main_viewpager2.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 14 | 15 | 19 | 20 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_navhost.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_navhost_account.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_navhost_dashboard.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_navhost_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_navhost_notification.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 13 | 14 | 18 | 19 | 23 | 24 | -------------------------------------------------------------------------------- /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/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph_dfm_account_start.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph_dfm_dashboard_start.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph_dfm_home_start.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph_dfm_notification_start.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 14 | 15 | 16 | 21 | 22 | 23 | 28 | 29 | 30 | 35 | 36 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph_main_bottom_nav.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 15 | 16 | 19 | 20 | 21 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph_main_viewpager2.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 15 | 16 | 19 | 20 | 21 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Poster 3 | Post Detail 4 | Search 5 | Dashboard 6 | Notification 7 | Account 8 | Home 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/test/java/com/smarttoolfactory/postdynamichilt/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.postdynamichilt 2 | 3 | import junit.framework.TestCase.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | 4 | val kotlin_version by extra("1.4.0") 5 | repositories { 6 | google() 7 | jcenter() 8 | maven { url = uri("https://plugins.gradle.org/m2/") } 9 | } 10 | 11 | dependencies { 12 | classpath(Plugins.CLASSPATH_GRADLE) 13 | classpath(kotlin("gradle-plugin", version = PluginVersion.KOTLIN_VERSION)) 14 | classpath(Plugins.CLASSPATH_DAGGER_HILT) 15 | classpath(Plugins.CLASSPATH_KTLINT) 16 | classpath(Plugins.CLASSPATH_NAV_SAFE_ARGS) 17 | } 18 | } 19 | 20 | plugins { 21 | id(Plugins.KTLINT) version PluginVersion.KTLINT_VERSION 22 | id(Plugins.DETEKT) version PluginVersion.DETEKT_VERSION 23 | } 24 | 25 | allprojects { 26 | repositories { 27 | google() 28 | jcenter() 29 | } 30 | } 31 | 32 | subprojects { 33 | 34 | tasks.withType { 35 | maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).takeIf { it > 0 } ?: 1 36 | } 37 | 38 | // KtLint 39 | apply(plugin = Plugins.KTLINT) // Version should be inherited from parent 40 | 41 | // KtLint Configurations 42 | ktlint { 43 | debug.set(true) 44 | verbose.set(true) 45 | android.set(true) 46 | outputToConsole.set(true) 47 | } 48 | 49 | // Detekt 50 | apply(plugin = Plugins.DETEKT) 51 | 52 | detekt { 53 | config = files("${project.rootDir}/config/detekt/detekt.yml") 54 | parallel = true 55 | 56 | reports { 57 | xml { 58 | enabled = true 59 | destination = file("${project.rootDir}/build/reports/detekt_report.xml") 60 | } 61 | html { 62 | enabled = true 63 | destination = file("${project.rootDir}/build/reports/detekt_report.html") 64 | } 65 | txt { 66 | enabled = true 67 | destination = file("${project.rootDir}/build/reports/detekt_report.txt") 68 | } 69 | } 70 | } 71 | } 72 | // JVM target applied to all Kotlin tasks across all sub-projects 73 | tasks.withType { 74 | kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() 75 | } 76 | 77 | tasks.register("clean", Delete::class) { 78 | delete(rootProject.buildDir) 79 | } 80 | -------------------------------------------------------------------------------- /buildSrc/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | /build 3 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.kotlin.dsl.`kotlin-dsl` 2 | 3 | plugins { 4 | `kotlin-dsl` 5 | } 6 | 7 | repositories { 8 | jcenter() 9 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/Modules.kt: -------------------------------------------------------------------------------- 1 | object Modules { 2 | 3 | const val APP = ":app" 4 | 5 | object AndroidLibrary { 6 | const val CORE = ":libraries:core" 7 | const val DATA = ":libraries:data" 8 | const val DOMAIN = ":libraries:domain" 9 | const val TEST_UTILS = ":libraries:test-utils" 10 | } 11 | 12 | /** 13 | * Dynamic Feature Modules 14 | */ 15 | object DynamicFeature { 16 | const val POST_DETAIL = ":features:post_detail" 17 | const val SEARCH = ":features:search" 18 | const val HOME = ":features:home" 19 | const val DASHBOARD = ":features:dashboard" 20 | const val NOTIFICATION = ":features:notification" 21 | const val ACCOUNT = ":features:account" 22 | } 23 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/Plugins.kt: -------------------------------------------------------------------------------- 1 | object Plugins { 2 | 3 | /* 4 | Project Level 5 | */ 6 | const val GRADLE = "com.android.tools.build:gradle" 7 | const val DETEKT = "io.gitlab.arturbosch.detekt" 8 | const val KTLINT = "org.jlleitschuh.gradle.ktlint" 9 | const val GIT_HOOKS = "plugins.git-hooks" 10 | 11 | const val CLASSPATH_GRADLE = "com.android.tools.build:gradle:${PluginVersion.GRADLE_VERSION}" 12 | const val CLASSPATH_KTLINT = 13 | "org.jlleitschuh.gradle:ktlint-gradle:${PluginVersion.KTLINT_VERSION}" 14 | const val CLASSPATH_DAGGER_HILT = 15 | "com.google.dagger:hilt-android-gradle-plugin:${Version.DAGGER_HILT_VERSION}" 16 | const val CLASSPATH_NAV_SAFE_ARGS = 17 | "androidx.navigation:navigation-safe-args-gradle-plugin:${PluginVersion.NAV_SAFE_ARGS_VERSION}" 18 | 19 | /* 20 | Module Level 21 | */ 22 | const val DAGGER_HILT_PLUGIN = "dagger.hilt.android.plugin" 23 | const val ANDROID_APPLICATION_PLUGIN = "com.android.application" 24 | const val ANDROID_DYNAMIC_FEATURE_PLUGIN = "com.android.dynamic-feature" 25 | const val ANDROID_LIBRARY_PLUGIN = "com.android.library" 26 | 27 | const val KOTLIN_ANDROID_PLUGIN = "kotlin-android" 28 | const val KOTLIN_ANDROID_EXTENSIONS_PLUGIN = "kotlin-android-extensions" 29 | const val KOTLIN_KAPT_PLUGIN = "kotlin-kapt" 30 | } 31 | -------------------------------------------------------------------------------- /features/account/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /features/account/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import extension.addBaseDynamicFeatureModuleDependencies 2 | import extension.addInstrumentationTestDependencies 3 | import extension.addUnitTestDependencies 4 | 5 | plugins { 6 | id(Plugins.ANDROID_DYNAMIC_FEATURE_PLUGIN) 7 | id(Plugins.KOTLIN_ANDROID_PLUGIN) 8 | id(Plugins.KOTLIN_ANDROID_EXTENSIONS_PLUGIN) 9 | id(Plugins.KOTLIN_KAPT_PLUGIN) 10 | id(Plugins.DAGGER_HILT_PLUGIN) 11 | } 12 | 13 | android { 14 | 15 | compileSdkVersion(AndroidVersion.COMPILE_SDK_VERSION) 16 | 17 | defaultConfig { 18 | 19 | applicationId = "com.smarttoolfactory.account" 20 | 21 | minSdkVersion(AndroidVersion.MIN_SDK_VERSION) 22 | targetSdkVersion(AndroidVersion.TARGET_SDK_VERSION) 23 | versionCode = AndroidVersion.VERSION_CODE 24 | versionName = AndroidVersion.VERSION_NAME 25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | 28 | buildTypes { 29 | getByName("release") { 30 | isMinifyEnabled = false 31 | proguardFiles( 32 | getDefaultProguardFile("proguard-android-optimize.txt"), 33 | "proguard-rules.pro" 34 | ) 35 | } 36 | } 37 | 38 | packagingOptions { 39 | exclude("META-INF/AL2.0") 40 | } 41 | 42 | dataBinding.isEnabled = true 43 | // android.buildFeatures.viewBinding = true 44 | 45 | compileOptions { 46 | sourceCompatibility = JavaVersion.VERSION_1_8 47 | targetCompatibility = JavaVersion.VERSION_1_8 48 | } 49 | kotlinOptions { 50 | jvmTarget = "1.8" 51 | } 52 | } 53 | 54 | dependencies { 55 | 56 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) 57 | 58 | implementation(project(Modules.APP)) 59 | implementation(project(Modules.AndroidLibrary.CORE)) 60 | implementation(project(Modules.AndroidLibrary.DOMAIN)) 61 | 62 | addBaseDynamicFeatureModuleDependencies() 63 | 64 | // Support and Widgets 65 | implementation(Deps.APPCOMPAT) 66 | implementation(Deps.MATERIAL) 67 | implementation(Deps.CONSTRAINT_LAYOUT) 68 | 69 | // Glide 70 | implementation(Deps.GLIDE) 71 | kapt(Deps.GLIDE_COMPILER) 72 | 73 | // Lottie 74 | implementation(Deps.LOTTIE) 75 | 76 | // Unit Tests 77 | addUnitTestDependencies() 78 | testImplementation(project(Modules.AndroidLibrary.TEST_UTILS)) 79 | 80 | // Instrumentation Tests 81 | addInstrumentationTestDependencies() 82 | androidTestImplementation(project(Modules.AndroidLibrary.TEST_UTILS)) 83 | } 84 | -------------------------------------------------------------------------------- /features/account/src/androidTest/java/com/smarttoolfactory/account/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.account 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.smarttoolfactory.account", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /features/account/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /features/account/src/main/java/com/smarttoolfactory/account/AccountFragment.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.account 2 | 3 | import com.smarttoolfactory.account.databinding.FragmentAccountBinding 4 | import com.smarttoolfactory.core.ui.fragment.DynamicNavigationFragment 5 | 6 | class AccountFragment : DynamicNavigationFragment() { 7 | 8 | override fun getLayoutRes(): Int = R.layout.fragment_account 9 | } 10 | -------------------------------------------------------------------------------- /features/account/src/main/res/layout/fragment_account.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 21 | 22 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /features/account/src/main/res/navigation/nav_graph_account.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /features/account/src/test/java/com/smarttoolfactory/account/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.account 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /features/dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /features/dashboard/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import extension.addBaseDynamicFeatureModuleDependencies 2 | import extension.addInstrumentationTestDependencies 3 | import extension.addUnitTestDependencies 4 | 5 | plugins { 6 | id(Plugins.ANDROID_DYNAMIC_FEATURE_PLUGIN) 7 | id(Plugins.KOTLIN_ANDROID_PLUGIN) 8 | id(Plugins.KOTLIN_ANDROID_EXTENSIONS_PLUGIN) 9 | id(Plugins.KOTLIN_KAPT_PLUGIN) 10 | id(Plugins.DAGGER_HILT_PLUGIN) 11 | } 12 | 13 | android { 14 | 15 | compileSdkVersion(AndroidVersion.COMPILE_SDK_VERSION) 16 | 17 | defaultConfig { 18 | 19 | applicationId = "com.smarttoolfactory.dashboard" 20 | 21 | minSdkVersion(AndroidVersion.MIN_SDK_VERSION) 22 | targetSdkVersion(AndroidVersion.TARGET_SDK_VERSION) 23 | versionCode = AndroidVersion.VERSION_CODE 24 | versionName = AndroidVersion.VERSION_NAME 25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | 28 | buildTypes { 29 | getByName("release") { 30 | isMinifyEnabled = false 31 | proguardFiles( 32 | getDefaultProguardFile("proguard-android-optimize.txt"), 33 | "proguard-rules.pro" 34 | ) 35 | } 36 | } 37 | 38 | packagingOptions { 39 | exclude("META-INF/AL2.0") 40 | } 41 | 42 | dataBinding.isEnabled = true 43 | // android.buildFeatures.viewBinding = true 44 | 45 | compileOptions { 46 | sourceCompatibility = JavaVersion.VERSION_1_8 47 | targetCompatibility = JavaVersion.VERSION_1_8 48 | } 49 | kotlinOptions { 50 | jvmTarget = "1.8" 51 | } 52 | } 53 | 54 | dependencies { 55 | 56 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) 57 | 58 | implementation(project(Modules.APP)) 59 | implementation(project(Modules.AndroidLibrary.CORE)) 60 | implementation(project(Modules.AndroidLibrary.DOMAIN)) 61 | 62 | addBaseDynamicFeatureModuleDependencies() 63 | 64 | // Support and Widgets 65 | implementation(Deps.APPCOMPAT) 66 | implementation(Deps.MATERIAL) 67 | implementation(Deps.CONSTRAINT_LAYOUT) 68 | 69 | // Glide 70 | implementation(Deps.GLIDE) 71 | kapt(Deps.GLIDE_COMPILER) 72 | 73 | // Lottie 74 | implementation(Deps.LOTTIE) 75 | 76 | // Unit Tests 77 | addUnitTestDependencies() 78 | testImplementation(project(Modules.AndroidLibrary.TEST_UTILS)) 79 | 80 | // Instrumentation Tests 81 | addInstrumentationTestDependencies() 82 | androidTestImplementation(project(Modules.AndroidLibrary.TEST_UTILS)) 83 | } 84 | -------------------------------------------------------------------------------- /features/dashboard/src/androidTest/java/com/smarttoolfactory/dashboard/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.dashboard 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.smarttoolfactory.dashboard", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /features/dashboard/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /features/dashboard/src/main/java/com/smarttoolfactory/dashboard/DashboardFragment.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.dashboard 2 | 3 | import com.smarttoolfactory.core.ui.fragment.DynamicNavigationFragment 4 | import com.smarttoolfactory.dashboard.databinding.FragmentDashboardBinding 5 | 6 | class DashboardFragment : DynamicNavigationFragment() { 7 | 8 | override fun getLayoutRes(): Int = R.layout.fragment_dashboard 9 | } 10 | -------------------------------------------------------------------------------- /features/dashboard/src/main/res/layout/fragment_dashboard.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 22 | 23 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /features/dashboard/src/main/res/layout/fragment_navhost_dashboard.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /features/dashboard/src/main/res/navigation/nav_graph_dashboard.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /features/dashboard/src/test/java/com/smarttoolfactory/dashboard/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.dashboard 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /features/home/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /features/home/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import extension.addBaseDynamicFeatureModuleDependencies 2 | import extension.addInstrumentationTestDependencies 3 | import extension.addUnitTestDependencies 4 | 5 | plugins { 6 | id(Plugins.ANDROID_DYNAMIC_FEATURE_PLUGIN) 7 | id(Plugins.KOTLIN_ANDROID_PLUGIN) 8 | id(Plugins.KOTLIN_ANDROID_EXTENSIONS_PLUGIN) 9 | id(Plugins.KOTLIN_KAPT_PLUGIN) 10 | id(Plugins.DAGGER_HILT_PLUGIN) 11 | } 12 | 13 | android { 14 | 15 | compileSdkVersion(AndroidVersion.COMPILE_SDK_VERSION) 16 | 17 | defaultConfig { 18 | 19 | applicationId = "com.smarttoolfactory.home" 20 | 21 | minSdkVersion(AndroidVersion.MIN_SDK_VERSION) 22 | targetSdkVersion(AndroidVersion.TARGET_SDK_VERSION) 23 | versionCode = AndroidVersion.VERSION_CODE 24 | versionName = AndroidVersion.VERSION_NAME 25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | 28 | buildTypes { 29 | getByName("release") { 30 | isMinifyEnabled = false 31 | proguardFiles( 32 | getDefaultProguardFile("proguard-android-optimize.txt"), 33 | "proguard-rules.pro" 34 | ) 35 | } 36 | } 37 | 38 | packagingOptions { 39 | exclude("META-INF/AL2.0") 40 | } 41 | 42 | dataBinding.isEnabled = true 43 | // android.buildFeatures.viewBinding = true 44 | 45 | compileOptions { 46 | sourceCompatibility = JavaVersion.VERSION_1_8 47 | targetCompatibility = JavaVersion.VERSION_1_8 48 | } 49 | kotlinOptions { 50 | jvmTarget = "1.8" 51 | } 52 | } 53 | 54 | dependencies { 55 | 56 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) 57 | 58 | implementation(project(Modules.APP)) 59 | implementation(project(Modules.AndroidLibrary.CORE)) 60 | implementation(project(Modules.AndroidLibrary.DOMAIN)) 61 | 62 | addBaseDynamicFeatureModuleDependencies() 63 | 64 | // Support and Widgets 65 | implementation(Deps.APPCOMPAT) 66 | implementation(Deps.MATERIAL) 67 | implementation(Deps.CONSTRAINT_LAYOUT) 68 | implementation(Deps.RECYCLER_VIEW) 69 | implementation(Deps.VIEWPAGER2) 70 | implementation(Deps.SWIPE_REFRESH_LAYOUT) 71 | 72 | // Glide 73 | implementation(Deps.GLIDE) 74 | kapt(Deps.GLIDE_COMPILER) 75 | 76 | // Unit Tests 77 | addUnitTestDependencies() 78 | testImplementation(project(Modules.AndroidLibrary.TEST_UTILS)) 79 | 80 | // Instrumentation Tests 81 | addInstrumentationTestDependencies() 82 | androidTestImplementation(project(Modules.AndroidLibrary.TEST_UTILS)) 83 | } 84 | -------------------------------------------------------------------------------- /features/home/src/androidTest/java/com/smarttoolfactory/home/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.home 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.smarttoolfactory.home", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /features/home/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /features/home/src/main/java/com/smarttoolfactory/home/TestFragment.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.home 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | 9 | class TestFragment : Fragment() { 10 | 11 | override fun onCreateView( 12 | inflater: LayoutInflater, 13 | container: ViewGroup?, 14 | savedInstanceState: Bundle? 15 | ): View? { 16 | return inflater.inflate(R.layout.fragment_test, container, false) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /features/home/src/main/java/com/smarttoolfactory/home/adapter/HomeViewPager2FragmentStateAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.home.adapter 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentManager 5 | import androidx.lifecycle.Lifecycle 6 | import com.smarttoolfactory.core.ui.adapter.NavigableFragmentStateAdapter 7 | import com.smarttoolfactory.core.ui.fragment.navhost.NavHostContainerFragment 8 | import com.smarttoolfactory.home.R 9 | 10 | /** 11 | * FragmentStateAdapter to contain ViewPager2 fragments inside another fragment. 12 | * 13 | * * 🔥 Create FragmentStateAdapter with viewLifeCycleOwner instead of Fragment to make sure 14 | * that it lives between [Fragment.onCreateView] and [Fragment.onDestroyView] while [View] is alive 15 | * 16 | * * https://stackoverflow.com/questions/61779776/leak-canary-detects-memory-leaks-for-tablayout-with-viewpager2 17 | */ 18 | class HomeViewPager2FragmentStateAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : 19 | NavigableFragmentStateAdapter(fragmentManager, lifecycle) { 20 | 21 | override fun getItemCount(): Int = 3 22 | 23 | override fun createFragment(position: Int): Fragment { 24 | 25 | return when (position) { 26 | 27 | // Post List with Status 28 | 0 -> NavHostContainerFragment.createNavHostContainerFragment( 29 | R.layout.fragment_navhost_post_list, 30 | R.id.nested_nav_host_fragment_post_list 31 | ) 32 | 33 | // Post List with Flow 34 | 1 -> NavHostContainerFragment.createNavHostContainerFragment( 35 | R.layout.fragment_navhost_post_list_flow, 36 | R.id.nested_nav_host_fragment_post_list 37 | ) 38 | 39 | // Post List with Rxjava3 40 | else -> NavHostContainerFragment.createNavHostContainerFragment( 41 | R.layout.fragment_navhost_post_list_rxjava3, 42 | R.id.nested_nav_host_fragment_post_list 43 | ) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /features/home/src/main/java/com/smarttoolfactory/home/di/PostListComponent.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.home.di 2 | 3 | import androidx.fragment.app.Fragment 4 | import com.smarttoolfactory.core.di.CoreModuleDependencies 5 | import com.smarttoolfactory.home.postlist.PostListFlowFragment 6 | import com.smarttoolfactory.home.postlist.PostListRxJava3Fragment 7 | import com.smarttoolfactory.home.postlist.PostListWithStatusFragment 8 | import dagger.BindsInstance 9 | import dagger.Component 10 | 11 | @Component( 12 | dependencies = [CoreModuleDependencies::class], 13 | modules = [PostListModule::class] 14 | ) 15 | interface PostListComponent { 16 | 17 | fun inject(postListWithStatusFragment: PostListWithStatusFragment) 18 | fun inject(postListFlowFragment: PostListFlowFragment) 19 | fun inject(postListRxJava3Fragment: PostListRxJava3Fragment) 20 | 21 | @Component.Factory 22 | interface Factory { 23 | fun create( 24 | dependentModule: CoreModuleDependencies, 25 | @BindsInstance fragment: Fragment 26 | ): PostListComponent 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /features/home/src/main/java/com/smarttoolfactory/home/di/PostListModule.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.home.di 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.smarttoolfactory.home.viewmodel.PostListFlowViewModelFactory 6 | import com.smarttoolfactory.home.viewmodel.PostListRxJava3ViewModelFactory 7 | import com.smarttoolfactory.home.viewmodel.PostListViewModelFlow 8 | import com.smarttoolfactory.home.viewmodel.PostListViewModelRxJava3 9 | import com.smarttoolfactory.home.viewmodel.PostStatusViewModel 10 | import com.smarttoolfactory.home.viewmodel.PostStatusViewModelFactory 11 | import dagger.Module 12 | import dagger.Provides 13 | import dagger.hilt.InstallIn 14 | import dagger.hilt.android.components.FragmentComponent 15 | import kotlinx.coroutines.CoroutineScope 16 | import kotlinx.coroutines.Dispatchers 17 | import kotlinx.coroutines.SupervisorJob 18 | 19 | @InstallIn(FragmentComponent::class) 20 | @Module 21 | class PostListModule { 22 | 23 | @Provides 24 | fun providePostListViewModelFlow(fragment: Fragment, factory: PostListFlowViewModelFactory) = 25 | ViewModelProvider(fragment, factory).get(PostListViewModelFlow::class.java) 26 | 27 | @Provides 28 | fun providePostListViewModelRxJava3( 29 | fragment: Fragment, 30 | factory: PostListRxJava3ViewModelFactory 31 | ) = 32 | ViewModelProvider(fragment, factory).get(PostListViewModelRxJava3::class.java) 33 | 34 | @Provides 35 | fun providePostListViewModel(fragment: Fragment, factory: PostStatusViewModelFactory) = 36 | ViewModelProvider(fragment, factory).get(PostStatusViewModel::class.java) 37 | 38 | @Provides 39 | fun provideCoroutineScope() = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) 40 | } 41 | -------------------------------------------------------------------------------- /features/home/src/main/java/com/smarttoolfactory/home/viewmodel/AbstractPostListVM.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.home.viewmodel 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.ViewModel 5 | import com.smarttoolfactory.core.util.Event 6 | import com.smarttoolfactory.core.viewstate.ViewState 7 | import com.smarttoolfactory.domain.model.Post 8 | 9 | abstract class AbstractPostListVM : ViewModel() { 10 | 11 | companion object { 12 | const val POST_LIST = "POST_LIST" 13 | const val POST_DETAIL = "POST_DETAIL" 14 | } 15 | 16 | abstract val goToDetailScreen: LiveData> 17 | 18 | abstract val postViewState: LiveData>> 19 | 20 | abstract fun getPosts() 21 | 22 | abstract fun refreshPosts() 23 | 24 | abstract fun onClick(post: Post) 25 | } 26 | -------------------------------------------------------------------------------- /features/home/src/main/java/com/smarttoolfactory/home/viewmodel/ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.home.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.smarttoolfactory.domain.usecase.GetPostListUseCaseFlow 6 | import com.smarttoolfactory.domain.usecase.GetPostListUseCaseRxJava3 7 | import com.smarttoolfactory.domain.usecase.GetPostsWithStatusUseCaseFlow 8 | import javax.inject.Inject 9 | import kotlinx.coroutines.CoroutineScope 10 | 11 | // TODO Add SavedStateHandle to ViewModelFactory 12 | 13 | class ViewModelFactory 14 | 15 | class PostListFlowViewModelFactory @Inject constructor( 16 | private val coroutineScope: CoroutineScope, 17 | private val getPostsUseCase: GetPostListUseCaseFlow 18 | ) : ViewModelProvider.Factory { 19 | 20 | @Suppress("UNCHECKED_CAST") 21 | override fun create(modelClass: Class): T { 22 | 23 | if (modelClass != PostListViewModelFlow::class.java) { 24 | throw IllegalArgumentException("Unknown ViewModel class") 25 | } 26 | 27 | return PostListViewModelFlow( 28 | coroutineScope, 29 | getPostsUseCase 30 | ) as T 31 | } 32 | } 33 | 34 | class PostListRxJava3ViewModelFactory @Inject constructor( 35 | private val getPostsUseCase: GetPostListUseCaseRxJava3 36 | ) : ViewModelProvider.Factory { 37 | 38 | @Suppress("UNCHECKED_CAST") 39 | override fun create(modelClass: Class): T { 40 | 41 | if (modelClass != PostListViewModelRxJava3::class.java) { 42 | throw IllegalArgumentException("Unknown ViewModel class") 43 | } 44 | 45 | return PostListViewModelRxJava3(getPostsUseCase) as T 46 | } 47 | } 48 | 49 | class PostStatusViewModelFactory @Inject constructor( 50 | private val coroutineScope: CoroutineScope, 51 | private val getPostsUseCase: GetPostsWithStatusUseCaseFlow 52 | ) : ViewModelProvider.Factory { 53 | 54 | @Suppress("UNCHECKED_CAST") 55 | override fun create(modelClass: Class): T { 56 | 57 | if (modelClass != PostStatusViewModel::class.java) { 58 | throw IllegalArgumentException("Unknown ViewModel class") 59 | } 60 | 61 | return PostStatusViewModel( 62 | coroutineScope, 63 | getPostsUseCase 64 | ) as T 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /features/home/src/main/res/drawable/avatar_1_raster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/features/home/src/main/res/drawable/avatar_1_raster.png -------------------------------------------------------------------------------- /features/home/src/main/res/drawable/avatar_2_raster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/features/home/src/main/res/drawable/avatar_2_raster.png -------------------------------------------------------------------------------- /features/home/src/main/res/drawable/avatar_3_raster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/features/home/src/main/res/drawable/avatar_3_raster.png -------------------------------------------------------------------------------- /features/home/src/main/res/drawable/avatar_4_raster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/features/home/src/main/res/drawable/avatar_4_raster.png -------------------------------------------------------------------------------- /features/home/src/main/res/drawable/avatar_5_raster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/features/home/src/main/res/drawable/avatar_5_raster.png -------------------------------------------------------------------------------- /features/home/src/main/res/drawable/avatar_6_raster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/features/home/src/main/res/drawable/avatar_6_raster.png -------------------------------------------------------------------------------- /features/home/src/main/res/drawable/ic_baseline_thumb_up_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /features/home/src/main/res/drawable/ic_outline_thumb_up_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /features/home/src/main/res/layout/fragment_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 14 | 15 | 19 | 20 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /features/home/src/main/res/layout/fragment_navhost_post_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /features/home/src/main/res/layout/fragment_navhost_post_list_flow.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /features/home/src/main/res/layout/fragment_navhost_post_list_rxjava3.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /features/home/src/main/res/layout/fragment_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /features/home/src/main/res/layout/row_post_grid.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 19 | 20 | 23 | 24 | 34 | 35 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /features/home/src/main/res/layout/row_post_staggered.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 20 | 21 | 24 | 25 | 34 | 35 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /features/home/src/main/res/navigation/nav_graph_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /features/home/src/main/res/navigation/nav_graph_post_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 17 | 18 | 19 | 20 | 25 | 26 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /features/home/src/main/res/navigation/nav_graph_post_list_flow.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 17 | 18 | 19 | 20 | 25 | 26 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /features/home/src/main/res/navigation/nav_graph_post_list_rxjava3.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 26 | 27 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /features/home/src/test/java/com/smarttoolfactory/home/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.home 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /features/notification/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /features/notification/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import extension.addBaseDynamicFeatureModuleDependencies 2 | import extension.addInstrumentationTestDependencies 3 | import extension.addUnitTestDependencies 4 | 5 | plugins { 6 | id(Plugins.ANDROID_DYNAMIC_FEATURE_PLUGIN) 7 | id(Plugins.KOTLIN_ANDROID_PLUGIN) 8 | id(Plugins.KOTLIN_ANDROID_EXTENSIONS_PLUGIN) 9 | id(Plugins.KOTLIN_KAPT_PLUGIN) 10 | id(Plugins.DAGGER_HILT_PLUGIN) 11 | } 12 | 13 | android { 14 | 15 | compileSdkVersion(AndroidVersion.COMPILE_SDK_VERSION) 16 | 17 | defaultConfig { 18 | 19 | applicationId = "com.smarttoolfactory.notification" 20 | 21 | minSdkVersion(AndroidVersion.MIN_SDK_VERSION) 22 | targetSdkVersion(AndroidVersion.TARGET_SDK_VERSION) 23 | versionCode = AndroidVersion.VERSION_CODE 24 | versionName = AndroidVersion.VERSION_NAME 25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | 28 | buildTypes { 29 | getByName("release") { 30 | isMinifyEnabled = false 31 | proguardFiles( 32 | getDefaultProguardFile("proguard-android-optimize.txt"), 33 | "proguard-rules.pro" 34 | ) 35 | } 36 | } 37 | 38 | packagingOptions { 39 | exclude("META-INF/AL2.0") 40 | } 41 | 42 | dataBinding.isEnabled = true 43 | // android.buildFeatures.viewBinding = true 44 | 45 | compileOptions { 46 | sourceCompatibility = JavaVersion.VERSION_1_8 47 | targetCompatibility = JavaVersion.VERSION_1_8 48 | } 49 | kotlinOptions { 50 | jvmTarget = "1.8" 51 | } 52 | } 53 | 54 | dependencies { 55 | 56 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) 57 | 58 | implementation(project(Modules.APP)) 59 | implementation(project(Modules.AndroidLibrary.CORE)) 60 | implementation(project(Modules.AndroidLibrary.DOMAIN)) 61 | 62 | addBaseDynamicFeatureModuleDependencies() 63 | 64 | // Support and Widgets 65 | implementation(Deps.APPCOMPAT) 66 | implementation(Deps.MATERIAL) 67 | implementation(Deps.CONSTRAINT_LAYOUT) 68 | 69 | // Glide 70 | implementation(Deps.GLIDE) 71 | kapt(Deps.GLIDE_COMPILER) 72 | 73 | // Lottie 74 | implementation(Deps.LOTTIE) 75 | 76 | // Unit Tests 77 | addUnitTestDependencies() 78 | testImplementation(project(Modules.AndroidLibrary.TEST_UTILS)) 79 | 80 | // Instrumentation Tests 81 | addInstrumentationTestDependencies() 82 | androidTestImplementation(project(Modules.AndroidLibrary.TEST_UTILS)) 83 | } 84 | -------------------------------------------------------------------------------- /features/notification/src/androidTest/java/com/smarttoolfactory/notification/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.notification 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.smarttoolfactory.notification", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /features/notification/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /features/notification/src/main/java/com/smarttoolfactory/notification/NotificationFragment.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.notification 2 | 3 | import com.smarttoolfactory.core.ui.fragment.DynamicNavigationFragment 4 | import com.smarttoolfactory.notification.databinding.FragmentNotificationBinding 5 | 6 | class NotificationFragment : DynamicNavigationFragment() { 7 | 8 | override fun getLayoutRes(): Int = R.layout.fragment_notification 9 | } 10 | -------------------------------------------------------------------------------- /features/notification/src/main/res/layout/fragment_notification.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 21 | 22 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /features/notification/src/main/res/navigation/nav_graph_notification.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /features/notification/src/test/java/com/smarttoolfactory/notification/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.notification 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /features/post_detail/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /features/post_detail/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import extension.addBaseDynamicFeatureModuleDependencies 2 | import extension.addInstrumentationTestDependencies 3 | import extension.addUnitTestDependencies 4 | 5 | plugins { 6 | id(Plugins.ANDROID_DYNAMIC_FEATURE_PLUGIN) 7 | id(Plugins.KOTLIN_ANDROID_PLUGIN) 8 | id(Plugins.KOTLIN_ANDROID_EXTENSIONS_PLUGIN) 9 | id(Plugins.KOTLIN_KAPT_PLUGIN) 10 | id(Plugins.DAGGER_HILT_PLUGIN) 11 | } 12 | 13 | android { 14 | 15 | compileSdkVersion(AndroidVersion.COMPILE_SDK_VERSION) 16 | 17 | defaultConfig { 18 | 19 | applicationId = "com.smarttoolfactory.post_detail" 20 | 21 | minSdkVersion(AndroidVersion.MIN_SDK_VERSION) 22 | targetSdkVersion(AndroidVersion.TARGET_SDK_VERSION) 23 | versionCode = AndroidVersion.VERSION_CODE 24 | versionName = AndroidVersion.VERSION_NAME 25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | 28 | buildTypes { 29 | getByName("release") { 30 | isMinifyEnabled = false 31 | proguardFiles( 32 | getDefaultProguardFile("proguard-android-optimize.txt"), 33 | "proguard-rules.pro" 34 | ) 35 | } 36 | } 37 | 38 | packagingOptions { 39 | exclude("META-INF/AL2.0") 40 | } 41 | 42 | dataBinding.isEnabled = true 43 | // android.buildFeatures.viewBinding = true 44 | 45 | compileOptions { 46 | sourceCompatibility = JavaVersion.VERSION_1_8 47 | targetCompatibility = JavaVersion.VERSION_1_8 48 | } 49 | kotlinOptions { 50 | jvmTarget = "1.8" 51 | } 52 | } 53 | 54 | dependencies { 55 | 56 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) 57 | 58 | implementation(project(Modules.APP)) 59 | implementation(project(Modules.AndroidLibrary.CORE)) 60 | implementation(project(Modules.AndroidLibrary.DOMAIN)) 61 | 62 | addBaseDynamicFeatureModuleDependencies() 63 | 64 | // Support and Widgets 65 | implementation(Deps.APPCOMPAT) 66 | implementation(Deps.MATERIAL) 67 | implementation(Deps.CONSTRAINT_LAYOUT) 68 | 69 | // Glide 70 | implementation(Deps.GLIDE) 71 | kapt(Deps.GLIDE_COMPILER) 72 | 73 | // Unit Tests 74 | addUnitTestDependencies() 75 | testImplementation(project(Modules.AndroidLibrary.TEST_UTILS)) 76 | 77 | // Instrumentation Tests 78 | addInstrumentationTestDependencies() 79 | androidTestImplementation(project(Modules.AndroidLibrary.TEST_UTILS)) 80 | } 81 | -------------------------------------------------------------------------------- /features/post_detail/src/androidTest/java/com/smarttoolfactory/post_detail/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.post_detail 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.smarttoolfactory.post_detail", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /features/post_detail/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /features/post_detail/src/main/java/com/smarttoolfactory/post_detail/PostBinding.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.post_detail 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.smarttoolfactory.postdynamichilt.R 8 | 9 | // TODO Same binding in app module not working in this feature module, why? 10 | @BindingAdapter("imageSource") 11 | fun setImageUrl(view: ImageView, userId: Int) { 12 | 13 | try { 14 | 15 | val drawableRes = when { 16 | userId % 6 == 0 -> { 17 | R.drawable.avatar_1_raster 18 | } 19 | userId % 6 == 1 -> { 20 | R.drawable.avatar_2_raster 21 | } 22 | userId % 6 == 2 -> { 23 | R.drawable.avatar_3_raster 24 | } 25 | userId % 6 == 3 -> { 26 | R.drawable.avatar_4_raster 27 | } 28 | userId % 6 == 4 -> { 29 | R.drawable.avatar_5_raster 30 | } 31 | else -> { 32 | R.drawable.avatar_6_raster 33 | } 34 | } 35 | 36 | val requestOptions = RequestOptions() 37 | requestOptions.placeholder(R.drawable.ic_launcher_background) 38 | 39 | Glide 40 | .with(view.context) 41 | .setDefaultRequestOptions(requestOptions) 42 | .load(drawableRes) 43 | .into(view) 44 | } catch (e: Exception) { 45 | e.printStackTrace() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /features/post_detail/src/main/java/com/smarttoolfactory/post_detail/PostDetailFragment.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.post_detail 2 | 3 | import android.os.Bundle 4 | import com.smarttoolfactory.core.di.CoreModuleDependencies 5 | import com.smarttoolfactory.core.ui.fragment.DynamicNavigationFragment 6 | import com.smarttoolfactory.domain.model.Post 7 | import com.smarttoolfactory.post_detail.databinding.FragmentPostDetailBinding 8 | import com.smarttoolfactory.post_detail.di.DaggerPostDetailComponent 9 | import dagger.hilt.android.EntryPointAccessors 10 | import javax.inject.Inject 11 | 12 | class PostDetailFragment : DynamicNavigationFragment() { 13 | 14 | @Inject 15 | lateinit var viewModel: PostDetailViewModel 16 | 17 | override fun getLayoutRes(): Int = R.layout.fragment_post_detail 18 | 19 | override fun bindViews() { 20 | // Get Post from navigation component arguments 21 | val post = arguments?.get("post") as Post 22 | dataBinding.item = post 23 | viewModel.updatePostStatus(post) 24 | } 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | initCoreDependentInjection() 28 | super.onCreate(savedInstanceState) 29 | } 30 | 31 | private fun initCoreDependentInjection() { 32 | 33 | val coreModuleDependencies = EntryPointAccessors.fromApplication( 34 | requireActivity().applicationContext, 35 | CoreModuleDependencies::class.java 36 | ) 37 | 38 | DaggerPostDetailComponent.factory().create( 39 | dependentModule = coreModuleDependencies, 40 | fragment = this 41 | ) 42 | .inject(this) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /features/post_detail/src/main/java/com/smarttoolfactory/post_detail/PostDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.post_detail 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.smarttoolfactory.domain.model.Post 6 | import com.smarttoolfactory.domain.usecase.GetPostsWithStatusUseCaseFlow 7 | import javax.inject.Inject 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.flow.launchIn 10 | 11 | class PostDetailViewModel @Inject constructor( 12 | private val coroutineScope: CoroutineScope, 13 | private val getPostsUseCase: GetPostsWithStatusUseCaseFlow 14 | ) : ViewModel() { 15 | 16 | fun updatePostStatus(post: Post) { 17 | post.displayCount++ 18 | getPostsUseCase.updatePostStatus(post) 19 | .launchIn(coroutineScope) 20 | } 21 | } 22 | 23 | class PostDetailViewModelFactory @Inject constructor( 24 | private val coroutineScope: CoroutineScope, 25 | private val getPostsUseCase: GetPostsWithStatusUseCaseFlow 26 | ) : ViewModelProvider.Factory { 27 | 28 | @Suppress("UNCHECKED_CAST") 29 | override fun create(modelClass: Class): T { 30 | if (modelClass != PostDetailViewModel::class.java) { 31 | throw IllegalArgumentException("Unknown ViewModel class") 32 | } 33 | return PostDetailViewModel( 34 | coroutineScope, 35 | getPostsUseCase 36 | ) as T 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /features/post_detail/src/main/java/com/smarttoolfactory/post_detail/di/PostDetailComponent.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.post_detail.di 2 | 3 | import androidx.fragment.app.Fragment 4 | import com.smarttoolfactory.core.di.CoreModuleDependencies 5 | import com.smarttoolfactory.post_detail.PostDetailFragment 6 | import dagger.BindsInstance 7 | import dagger.Component 8 | 9 | @Component( 10 | dependencies = [CoreModuleDependencies::class], 11 | modules = [PostDetailModule::class] 12 | ) 13 | interface PostDetailComponent { 14 | 15 | fun inject(postDetailFragment: PostDetailFragment) 16 | 17 | @Component.Factory 18 | interface Factory { 19 | fun create( 20 | dependentModule: CoreModuleDependencies, 21 | @BindsInstance fragment: Fragment 22 | ): PostDetailComponent 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /features/post_detail/src/main/java/com/smarttoolfactory/post_detail/di/PostDetailModule.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.post_detail.di 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.smarttoolfactory.post_detail.PostDetailViewModel 6 | import com.smarttoolfactory.post_detail.PostDetailViewModelFactory 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.android.components.FragmentComponent 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.SupervisorJob 14 | 15 | @InstallIn(FragmentComponent::class) 16 | @Module 17 | class PostDetailModule { 18 | 19 | @Provides 20 | fun providePostDetailViewModel(fragment: Fragment, factory: PostDetailViewModelFactory) = 21 | ViewModelProvider(fragment, factory).get(PostDetailViewModel::class.java) 22 | 23 | @Provides 24 | fun provideCoroutineScope() = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) 25 | } 26 | -------------------------------------------------------------------------------- /features/post_detail/src/main/res/navigation/nav_graph_post_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /features/post_detail/src/test/java/com/smarttoolfactory/post_detail/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.post_detail 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /features/search/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /features/search/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import extension.addBaseDynamicFeatureModuleDependencies 2 | import extension.addInstrumentationTestDependencies 3 | import extension.addUnitTestDependencies 4 | 5 | plugins { 6 | id(Plugins.ANDROID_DYNAMIC_FEATURE_PLUGIN) 7 | id(Plugins.KOTLIN_ANDROID_PLUGIN) 8 | id(Plugins.KOTLIN_ANDROID_EXTENSIONS_PLUGIN) 9 | id(Plugins.KOTLIN_KAPT_PLUGIN) 10 | id(Plugins.DAGGER_HILT_PLUGIN) 11 | } 12 | 13 | android { 14 | 15 | compileSdkVersion(AndroidVersion.COMPILE_SDK_VERSION) 16 | 17 | defaultConfig { 18 | 19 | applicationId = "com.smarttoolfactory.search" 20 | 21 | minSdkVersion(AndroidVersion.MIN_SDK_VERSION) 22 | targetSdkVersion(AndroidVersion.TARGET_SDK_VERSION) 23 | versionCode = AndroidVersion.VERSION_CODE 24 | versionName = AndroidVersion.VERSION_NAME 25 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | 28 | buildTypes { 29 | getByName("release") { 30 | isMinifyEnabled = false 31 | proguardFiles( 32 | getDefaultProguardFile("proguard-android-optimize.txt"), 33 | "proguard-rules.pro" 34 | ) 35 | } 36 | } 37 | 38 | packagingOptions { 39 | exclude("META-INF/AL2.0") 40 | } 41 | 42 | dataBinding.isEnabled = true 43 | // android.buildFeatures.viewBinding = true 44 | 45 | compileOptions { 46 | sourceCompatibility = JavaVersion.VERSION_1_8 47 | targetCompatibility = JavaVersion.VERSION_1_8 48 | } 49 | kotlinOptions { 50 | jvmTarget = "1.8" 51 | } 52 | } 53 | 54 | dependencies { 55 | 56 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) 57 | 58 | implementation(project(Modules.APP)) 59 | implementation(project(Modules.AndroidLibrary.CORE)) 60 | implementation(project(Modules.AndroidLibrary.DOMAIN)) 61 | 62 | addBaseDynamicFeatureModuleDependencies() 63 | 64 | // Support and Widgets 65 | implementation(Deps.APPCOMPAT) 66 | implementation(Deps.MATERIAL) 67 | implementation(Deps.CONSTRAINT_LAYOUT) 68 | 69 | // Glide 70 | implementation(Deps.GLIDE) 71 | kapt(Deps.GLIDE_COMPILER) 72 | 73 | // Unit Tests 74 | addUnitTestDependencies() 75 | testImplementation(project(Modules.AndroidLibrary.TEST_UTILS)) 76 | 77 | // Instrumentation Tests 78 | addInstrumentationTestDependencies() 79 | androidTestImplementation(project(Modules.AndroidLibrary.TEST_UTILS)) 80 | } 81 | -------------------------------------------------------------------------------- /features/search/src/androidTest/java/com/smarttoolfactory/search/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.search 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.smarttoolfactory.search", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /features/search/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /features/search/src/test/java/com/smarttoolfactory/search/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.search 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jul 31 10:27:25 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.5-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /libraries/core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /libraries/core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import extension.addCoreModuleDependencies 2 | import extension.addInstrumentationTestDependencies 3 | import extension.addUnitTestDependencies 4 | 5 | plugins { 6 | id(Plugins.ANDROID_LIBRARY_PLUGIN) 7 | id(Plugins.KOTLIN_ANDROID_PLUGIN) 8 | id(Plugins.KOTLIN_ANDROID_EXTENSIONS_PLUGIN) 9 | id(Plugins.KOTLIN_KAPT_PLUGIN) 10 | id(Plugins.DAGGER_HILT_PLUGIN) 11 | } 12 | 13 | android { 14 | 15 | compileSdkVersion(AndroidVersion.COMPILE_SDK_VERSION) 16 | defaultConfig { 17 | minSdkVersion(AndroidVersion.MIN_SDK_VERSION) 18 | targetSdkVersion(AndroidVersion.TARGET_SDK_VERSION) 19 | versionCode = AndroidVersion.VERSION_CODE 20 | versionName = AndroidVersion.VERSION_NAME 21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 22 | } 23 | 24 | buildTypes { 25 | getByName("release") { 26 | isMinifyEnabled = false 27 | proguardFiles( 28 | getDefaultProguardFile("proguard-android-optimize.txt"), 29 | "proguard-rules.pro" 30 | ) 31 | } 32 | } 33 | 34 | android.buildFeatures.dataBinding = true 35 | 36 | compileOptions { 37 | sourceCompatibility = JavaVersion.VERSION_1_8 38 | targetCompatibility = JavaVersion.VERSION_1_8 39 | } 40 | 41 | kotlinOptions { 42 | jvmTarget = "1.8" 43 | } 44 | 45 | testOptions { 46 | unitTests.isIncludeAndroidResources = true 47 | } 48 | } 49 | 50 | dependencies { 51 | 52 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) 53 | 54 | implementation(project(Modules.AndroidLibrary.DOMAIN)) 55 | implementation(project(Modules.AndroidLibrary.DATA)) 56 | 57 | addCoreModuleDependencies() 58 | 59 | addUnitTestDependencies() 60 | testImplementation(project(Modules.AndroidLibrary.TEST_UTILS)) 61 | 62 | addInstrumentationTestDependencies() 63 | androidTestImplementation(project(Modules.AndroidLibrary.TEST_UTILS)) 64 | } 65 | -------------------------------------------------------------------------------- /libraries/core/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/libraries/core/consumer-rules.pro -------------------------------------------------------------------------------- /libraries/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 -------------------------------------------------------------------------------- /libraries/core/src/androidTest/java/com/smarttoolfactory/core/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.core 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.smarttoolfactory.core.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /libraries/core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /libraries/core/src/main/java/com/smarttoolfactory/core/CoreDependency.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.core 2 | 3 | /** 4 | * MOCK DEPENDENCY for instructing how to inject it to a feature module from **core module**, delete it for production 5 | */ 6 | class CoreDependency 7 | -------------------------------------------------------------------------------- /libraries/core/src/main/java/com/smarttoolfactory/core/di/CoreModule.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.core.di 2 | 3 | import com.smarttoolfactory.core.CoreDependency 4 | import com.smarttoolfactory.domain.dispatcher.UseCaseDispatchers 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.components.ApplicationComponent 9 | import javax.inject.Singleton 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.SupervisorJob 13 | 14 | @InstallIn(ApplicationComponent::class) 15 | @Module(includes = [DataModule::class]) 16 | class CoreModule { 17 | 18 | @Provides 19 | fun provideCoreDependency() = CoreDependency() 20 | 21 | @Singleton 22 | @Provides 23 | fun provideCoroutineScope() = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) 24 | 25 | @Provides 26 | fun provideUseCaseDispatchers(): UseCaseDispatchers { 27 | return UseCaseDispatchers(Dispatchers.IO, Dispatchers.Default, Dispatchers.Main) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /libraries/core/src/main/java/com/smarttoolfactory/core/di/CoreModuleDependencies.kt: -------------------------------------------------------------------------------- 1 | 2 | package com.smarttoolfactory.core.di 3 | 4 | import com.smarttoolfactory.core.CoreDependency 5 | import com.smarttoolfactory.domain.usecase.GetPostListUseCaseFlow 6 | import com.smarttoolfactory.domain.usecase.GetPostListUseCaseRxJava3 7 | import com.smarttoolfactory.domain.usecase.GetPostsWithStatusUseCaseFlow 8 | import dagger.hilt.EntryPoint 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.android.components.ApplicationComponent 11 | 12 | /** 13 | * This component is required for adding component to Dynamic Feature Module dependencies 14 | */ 15 | @EntryPoint 16 | @InstallIn(ApplicationComponent::class) 17 | interface CoreModuleDependencies { 18 | 19 | /* 20 | Provision methods to provide dependencies to components that depend on this component 21 | */ 22 | fun coreDependency(): CoreDependency 23 | 24 | /* 25 | Provision methods to provide dependencies to components that depend on this component 26 | */ 27 | 28 | fun getPostListUseCaseFlow(): GetPostListUseCaseFlow 29 | fun getPostListUseCaseRxJava3(): GetPostListUseCaseRxJava3 30 | fun getPostsWithStatusUseCaseFlow(): GetPostsWithStatusUseCaseFlow 31 | } 32 | -------------------------------------------------------------------------------- /libraries/core/src/main/java/com/smarttoolfactory/core/di/scope/FeatureScope.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.core.di.scope 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @kotlin.annotation.Retention(AnnotationRetention.RUNTIME) 7 | annotation class FeatureScope 8 | -------------------------------------------------------------------------------- /libraries/core/src/main/java/com/smarttoolfactory/core/error/NavigationException.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.core.error 2 | 3 | class NavigationException(override val message: String?) : Exception(message) 4 | -------------------------------------------------------------------------------- /libraries/core/src/main/java/com/smarttoolfactory/core/ui/adapter/NavigableFragmentStateAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.core.ui.adapter 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentManager 5 | import androidx.fragment.app.commitNow 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.viewpager2.adapter.FragmentStateAdapter 8 | import androidx.viewpager2.adapter.FragmentStateAdapter.FragmentTransactionCallback.OnPostEventListener 9 | 10 | /** 11 | * FragmentStateAdapter to add ability to set primary navigation fragment 12 | * which let's active fragment to be navigable when back button is pressed using 13 | * [FragmentStateAdapter.FragmentTransactionCallback] to [ViewPager2]. 14 | * 15 | * * 🔥 Create FragmentStateAdapter with viewLifeCycleOwner instead of Fragment to make sure 16 | * that it lives between [Fragment.onCreateView] and [Fragment.onDestroyView] while [View] is alive 17 | * 18 | * * https://stackoverflow.com/questions/61779776/leak-canary-detects-memory-leaks-for-tablayout-with-viewpager2 19 | */ 20 | abstract class NavigableFragmentStateAdapter( 21 | fragmentManager: FragmentManager, 22 | lifecycle: Lifecycle 23 | ) : FragmentStateAdapter(fragmentManager, lifecycle) { 24 | 25 | private val fragmentTransactionCallback = 26 | object : FragmentStateAdapter.FragmentTransactionCallback() { 27 | override fun onFragmentMaxLifecyclePreUpdated( 28 | fragment: Fragment, 29 | maxLifecycleState: Lifecycle.State 30 | ) = if (maxLifecycleState == Lifecycle.State.RESUMED) { 31 | 32 | // This fragment is becoming the active Fragment - set it to 33 | // the primary navigation fragment in the OnPostEventListener 34 | OnPostEventListener { 35 | fragment.parentFragmentManager.commitNow { 36 | setPrimaryNavigationFragment(fragment) 37 | } 38 | } 39 | } else { 40 | super.onFragmentMaxLifecyclePreUpdated(fragment, maxLifecycleState) 41 | } 42 | } 43 | 44 | init { 45 | // Add a FragmentTransactionCallback to handle changing 46 | // the primary navigation fragment 47 | registerFragmentTransactionCallback() 48 | } 49 | 50 | fun registerFragmentTransactionCallback() { 51 | registerFragmentTransactionCallback(fragmentTransactionCallback) 52 | } 53 | 54 | fun unregisterFragmentTransactionCallback() { 55 | unregisterFragmentTransactionCallback(fragmentTransactionCallback) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /libraries/core/src/main/java/com/smarttoolfactory/core/util/Event.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.smarttoolfactory.core.util 17 | 18 | import androidx.lifecycle.Observer 19 | 20 | /** 21 | * Used as a wrapper for data that is exposed via a LiveData that represents an event. 22 | */ 23 | open class Event(private val content: T) { 24 | 25 | @Suppress("MemberVisibilityCanBePrivate") 26 | var hasBeenHandled = false 27 | private set // Allow external read but not write 28 | 29 | /** 30 | * Returns the content and prevents its use again. 31 | */ 32 | fun getContentIfNotHandled(): T? { 33 | return if (hasBeenHandled) { 34 | null 35 | } else { 36 | hasBeenHandled = true 37 | content 38 | } 39 | } 40 | 41 | /** 42 | * Returns the content, even if it's already been handled. 43 | */ 44 | fun peekContent(): T = content 45 | } 46 | 47 | /** 48 | * An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has 49 | * already been handled. 50 | * 51 | * [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled. 52 | */ 53 | class EventObserver(private val onEventUnhandledContent: (T) -> Unit) : Observer> { 54 | override fun onChanged(event: Event?) { 55 | event?.getContentIfNotHandled()?.let { 56 | onEventUnhandledContent(it) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /libraries/core/src/main/java/com/smarttoolfactory/core/util/FlowViewStateExtension.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.core.util 2 | 3 | import com.smarttoolfactory.core.viewstate.Status 4 | import com.smarttoolfactory.core.viewstate.ViewState 5 | import kotlinx.coroutines.CoroutineDispatcher 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.catch 9 | import kotlinx.coroutines.flow.emitAll 10 | import kotlinx.coroutines.flow.flowOf 11 | import kotlinx.coroutines.flow.flowOn 12 | import kotlinx.coroutines.flow.map 13 | 14 | fun Flow.convertToFlowViewState( 15 | dispatcher: CoroutineDispatcher = Dispatchers.Default 16 | ): Flow> { 17 | return this 18 | .map { postList -> ViewState(status = Status.SUCCESS, data = postList) } 19 | .catch { cause: Throwable -> emitAll(flowOf(ViewState(Status.ERROR, error = cause))) } 20 | .flowOn(dispatcher) 21 | } 22 | -------------------------------------------------------------------------------- /libraries/core/src/main/java/com/smarttoolfactory/core/util/LifecycleOwnerExtension.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.core.util 2 | 3 | import androidx.lifecycle.LifecycleOwner 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.Observer 6 | 7 | fun LifecycleOwner.observe(liveData: LiveData, predicate: (T) -> Unit) { 8 | liveData.observe(this, Observer { it?.let { predicate(it) } }) 9 | } 10 | -------------------------------------------------------------------------------- /libraries/core/src/main/java/com/smarttoolfactory/core/util/NavHostExtension.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.core.util 2 | 3 | import androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment 4 | import androidx.navigation.fragment.NavHostFragment 5 | import com.smarttoolfactory.core.ui.fragment.navhost.FieldProperty 6 | import com.smarttoolfactory.core.viewmodel.NavControllerViewModel 7 | 8 | var DynamicNavHostFragment.viewModel: NavControllerViewModel by FieldProperty { 9 | NavControllerViewModel() 10 | } 11 | 12 | var NavHostFragment.viewModel: NavControllerViewModel by FieldProperty { 13 | NavControllerViewModel() 14 | } 15 | -------------------------------------------------------------------------------- /libraries/core/src/main/java/com/smarttoolfactory/core/util/RxJavaViewStateExtension.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.core.util 2 | 3 | import com.smarttoolfactory.core.viewstate.Status 4 | import com.smarttoolfactory.core.viewstate.ViewState 5 | import io.reactivex.rxjava3.core.Flowable 6 | import io.reactivex.rxjava3.core.Observable 7 | import io.reactivex.rxjava3.core.Single 8 | 9 | fun Observable.convertToObservableViewState(): Observable> { 10 | return this 11 | .map { data -> 12 | ViewState(status = Status.SUCCESS, data = data) 13 | } 14 | .onErrorResumeNext { throwable: Throwable -> 15 | Observable.just(ViewState(status = Status.ERROR, error = throwable)) 16 | } 17 | } 18 | 19 | fun Observable.convertToObservableViewStateWithLoading(): Observable> { 20 | return this 21 | .map { data -> 22 | ViewState(status = Status.SUCCESS, data = data) 23 | } 24 | .onErrorResumeNext { throwable: Throwable -> 25 | Observable.just(ViewState(status = Status.ERROR, error = throwable)) 26 | } 27 | .startWith(Observable.just(ViewState(status = Status.LOADING))) 28 | } 29 | 30 | fun Single.convertFromSingleToObservableViewState(): Observable> { 31 | return this 32 | .toObservable() 33 | .convertToObservableViewState() 34 | } 35 | 36 | fun Single.convertFromSingleToObservableViewStateWithLoading(): Observable> { 37 | return this 38 | .toObservable() 39 | .convertToObservableViewStateWithLoading() 40 | } 41 | 42 | fun Single.convertToSingleViewState(): Single>? { 43 | return this 44 | .map { data -> 45 | ViewState(status = Status.SUCCESS, data = data) 46 | } 47 | .onErrorResumeNext { throwable: Throwable -> 48 | Single.just(ViewState(status = Status.ERROR, error = throwable)) 49 | } 50 | } 51 | 52 | fun Single.convertFromSingletToFlowableViewState(): Flowable>? { 53 | return this 54 | .map { data -> 55 | ViewState(status = Status.SUCCESS, data = data) 56 | } 57 | .onErrorResumeNext { throwable: Throwable -> 58 | Single.just(ViewState(status = Status.ERROR, error = throwable)) 59 | } 60 | .startWith(Single.just(ViewState(status = Status.LOADING))) 61 | } 62 | -------------------------------------------------------------------------------- /libraries/core/src/main/java/com/smarttoolfactory/core/viewmodel/NavControllerViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.core.viewmodel 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.ViewModel 5 | import androidx.navigation.NavController 6 | import com.smarttoolfactory.core.util.Event 7 | 8 | class NavControllerViewModel : ViewModel() { 9 | val currentNavController = MutableLiveData>() 10 | } 11 | -------------------------------------------------------------------------------- /libraries/core/src/main/java/com/smarttoolfactory/core/viewstate/ViewState.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.core.viewstate 2 | 3 | class ViewState( 4 | val status: Status, 5 | val data: T? = null, 6 | val error: Throwable? = null 7 | ) { 8 | 9 | fun isSuccess() = status == Status.SUCCESS 10 | 11 | fun isLoading() = status == Status.LOADING 12 | 13 | fun getErrorMessage() = error?.message 14 | 15 | fun shouldShowErrorMessage() = error != null && status == Status.ERROR 16 | } 17 | 18 | enum class Status { 19 | LOADING, 20 | SUCCESS, 21 | ERROR 22 | } 23 | -------------------------------------------------------------------------------- /libraries/core/src/test/java/com/smarttoolfactory/core/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.core 2 | 3 | import junit.framework.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libraries/data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /libraries/data/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/libraries/data/consumer-rules.pro -------------------------------------------------------------------------------- /libraries/data/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 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 -------------------------------------------------------------------------------- /libraries/data/src/androidTest/java/com/smarttoolfactory/data/PostDaoCoroutinesTestSuite.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data 2 | 3 | import org.junit.runner.RunWith 4 | import org.junit.runners.Suite 5 | 6 | // Runs all unit tests with JUnit4. 7 | @RunWith(Suite::class) 8 | @Suite.SuiteClasses( 9 | PostDaoCoroutinesCoroutinesFlowTest::class, 10 | PostDaoCoroutinesRxJavaTest::class 11 | ) 12 | class PostDaoCoroutinesTestSuite 13 | -------------------------------------------------------------------------------- /libraries/data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/smarttoolfactory/data/api/PostApi.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.api 2 | 3 | import com.smarttoolfactory.data.model.PostDTO 4 | import io.reactivex.rxjava3.core.Single 5 | import retrofit2.http.GET 6 | 7 | interface PostApi { 8 | 9 | @GET("/posts") 10 | suspend fun getPosts(): List 11 | } 12 | 13 | interface PostApiRxJava { 14 | @GET("/posts") 15 | fun getPostsSingle(): Single> 16 | } 17 | 18 | const val BASE_URL = "https://jsonplaceholder.typicode.com" 19 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/smarttoolfactory/data/db/PostDaoCoroutines.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.db 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import com.smarttoolfactory.data.model.PostEntity 9 | import kotlinx.coroutines.flow.Flow 10 | 11 | @Dao 12 | interface PostDaoCoroutines { 13 | 14 | /* 15 | ***** Coroutines ****** 16 | */ 17 | 18 | @Insert(onConflict = OnConflictStrategy.REPLACE) 19 | suspend fun insert(entity: PostEntity): Long 20 | 21 | @Insert(onConflict = OnConflictStrategy.REPLACE) 22 | suspend fun insert(entities: List): List 23 | 24 | @Delete 25 | suspend fun deletePost(entity: PostEntity): Int 26 | 27 | @Query("DELETE FROM post") 28 | suspend fun deleteAll() 29 | 30 | @Query("SELECT COUNT(*) FROM post") 31 | suspend fun getPostCount(): Int 32 | 33 | /** 34 | * Get posts from database. 35 | * 36 | * *If database is empty returns empty list [] 37 | */ 38 | @Query("SELECT * FROM post") 39 | suspend fun getPostList(): List 40 | 41 | /** 42 | * Get list of [PostEntity] that contains [keyword] in title or body of the post 43 | */ 44 | // @Query("SELECT * FROM post WHERE title LIKE '%' || :keyword || '%' OR body LIKE '%' || :keyword || '%'") 45 | // suspend fun searchPostsByKeyword(keyword: String): List 46 | // 47 | // /** 48 | // * Search [PostEntity] that belong to user with [posterId] 49 | // */ 50 | // @Query("SELECT * FROM post WHERE userId =:posterId") 51 | // suspend fun searchPostsByUser(posterId: Int) 52 | // 53 | // /** 54 | // * Get most displayed posts in descending order 55 | // */ 56 | // @Query("SELECT * FROM post ORDER BY displayCount DESC") 57 | // suspend fun getDisplayedMostPosts(): List 58 | 59 | /* 60 | ***** Flow ****** 61 | */ 62 | /** 63 | * Get [Flow] of [List] of [PostEntity] 64 | * 65 | * * If database is empty returns an empty list [] 66 | */ 67 | // Flow 68 | @Query("SELECT * FROM post") 69 | fun getPostListFlow(): Flow> 70 | } 71 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/smarttoolfactory/data/db/PostDaoRxJava3.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.db 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import androidx.room.Update 9 | import com.smarttoolfactory.data.model.PostEntity 10 | import io.reactivex.rxjava3.core.Completable 11 | import io.reactivex.rxjava3.core.Maybe 12 | import io.reactivex.rxjava3.core.Observable 13 | import io.reactivex.rxjava3.core.Single 14 | 15 | @Dao 16 | interface PostDaoRxJava3 { 17 | 18 | @Insert(onConflict = OnConflictStrategy.REPLACE) 19 | fun insert(postEntity: PostEntity): Completable 20 | 21 | @Insert(onConflict = OnConflictStrategy.REPLACE) 22 | fun insert(postEntityList: List): Completable 23 | 24 | @Delete 25 | fun deletePost(entity: PostEntity): Single 26 | 27 | @Query("DELETE FROM post") 28 | fun deleteAll(): Completable 29 | 30 | /** 31 | * Get number of posts in db 32 | */ 33 | @Query("SELECT COUNT(*) FROM post") 34 | fun getPostCount(): Maybe 35 | 36 | /** 37 | * Get list of [PostEntity]s to from database. 38 | */ 39 | @Query("SELECT * FROM post") 40 | fun getPostsSingle(): Single> 41 | 42 | /** 43 | * Get list of [PostEntity]s to from database. 44 | */ 45 | @Query("SELECT * FROM post") 46 | fun getPostsMaybe(): Maybe> 47 | 48 | /** 49 | * Get list of [PostEntity]s to from database. 50 | */ 51 | @Query("SELECT * FROM post") 52 | fun getPosts(): Observable> 53 | 54 | /** 55 | * Get list of [PostEntity] that contains [keyword] in title or body of the post 56 | */ 57 | // @Query("SELECT * FROM post WHERE title LIKE '%' || :keyword || '%' OR body LIKE '%' || :keyword || '%'") 58 | // suspend fun searchPostsByKeyword(keyword: String): List 59 | // 60 | // /** 61 | // * Search [PostEntity] that belong to user with [posterId] 62 | // */ 63 | // @Query("SELECT * FROM post WHERE userId =:posterId") 64 | // suspend fun searchPostsByUser(posterId: Int) 65 | // 66 | // /** 67 | // * Get most displayed posts in descending order 68 | // */ 69 | // @Query("SELECT * FROM post ORDER BY displayCount DESC") 70 | // suspend fun getDisplayedMostPosts(): List 71 | 72 | /** 73 | * Update a post's favorite or display count status. 74 | */ 75 | @Update 76 | fun updatePostFavoriteOrSelectStatus(postEntity: PostEntity) 77 | } 78 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/smarttoolfactory/data/db/PostDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.db 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.migration.Migration 6 | import androidx.sqlite.db.SupportSQLiteDatabase 7 | import com.smarttoolfactory.data.model.PostEntity 8 | import com.smarttoolfactory.data.model.PostStatus 9 | 10 | const val DATABASE_NAME = "post.db" 11 | 12 | @Database( 13 | entities = [PostEntity::class, PostStatus::class], 14 | version = 2, 15 | exportSchema = true 16 | ) 17 | abstract class PostDatabase : RoomDatabase() { 18 | abstract fun postDao(): PostDaoCoroutines 19 | abstract fun postDaoRxJava(): PostDaoRxJava3 20 | abstract fun postStatusDao(): PostStatusDao 21 | } 22 | 23 | val MIGRATION_1_2: Migration = object : Migration(1, 2) { 24 | 25 | override fun migrate(database: SupportSQLiteDatabase) { 26 | 27 | database.execSQL( 28 | "CREATE TABLE IF NOT EXISTS post_status " + 29 | "(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + 30 | "userAccountId INTEGER NOT NULL, " + 31 | "postId INTEGER NOT NULL, " + 32 | "displayCount INTEGER NOT NULL, " + 33 | "isFavorite INTEGER NOT NULL, " + 34 | "FOREIGN KEY(postId) REFERENCES post(id) " + 35 | "ON UPDATE NO ACTION ON DELETE NO ACTION )" 36 | ) 37 | 38 | database.execSQL( 39 | "CREATE INDEX IF NOT EXISTS index_post_status_postId " + 40 | "ON post_status (userAccountId, postId)" 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/smarttoolfactory/data/db/PostStatusDao.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.db 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Embedded 5 | import androidx.room.Insert 6 | import androidx.room.OnConflictStrategy 7 | import androidx.room.Query 8 | import androidx.room.Relation 9 | import androidx.room.Transaction 10 | import com.smarttoolfactory.data.model.PostAndStatus 11 | import com.smarttoolfactory.data.model.PostEntity 12 | import com.smarttoolfactory.data.model.PostStatus 13 | 14 | @Dao 15 | interface PostStatusDao { 16 | 17 | /** 18 | * Update a post's favorite or display count status. 19 | */ 20 | @Insert(onConflict = OnConflictStrategy.REPLACE) 21 | suspend fun insertOrUpdatePostStatus(postStatus: PostStatus) 22 | 23 | /** 24 | * Get status belong to current user for this [PostEntity] with [PostEntity.id] 25 | */ 26 | @Query("SELECT * FROM post_status WHERE userAccountId =:userId AND postId =:postId") 27 | suspend fun getUpdateStatus(userId: Int, postId: Int): PostStatus? 28 | 29 | /** 30 | * This method uses [Embedded] and [Relation] annotations to create a class that contains 31 | * [PostEntity] and [PostStatus] which is display count and like belong to this [PostEntity] 32 | * by a specific account that is currently logged in 33 | */ 34 | @Transaction 35 | @Query("SELECT * FROM post") 36 | suspend fun getPostWithStatus(): List 37 | } 38 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/smarttoolfactory/data/di/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.di 2 | 3 | import android.app.Application 4 | import androidx.room.Room 5 | import com.smarttoolfactory.data.db.DATABASE_NAME 6 | import com.smarttoolfactory.data.db.MIGRATION_1_2 7 | import com.smarttoolfactory.data.db.PostDaoCoroutines 8 | import com.smarttoolfactory.data.db.PostDaoRxJava3 9 | import com.smarttoolfactory.data.db.PostDatabase 10 | import com.smarttoolfactory.data.db.PostStatusDao 11 | import dagger.Module 12 | import dagger.Provides 13 | import dagger.hilt.InstallIn 14 | import dagger.hilt.android.components.ApplicationComponent 15 | import javax.inject.Singleton 16 | 17 | @InstallIn(ApplicationComponent::class) 18 | @Module 19 | class DatabaseModule { 20 | 21 | @Singleton 22 | @Provides 23 | fun provideDatabase(application: Application): PostDatabase { 24 | return Room.databaseBuilder( 25 | application, 26 | PostDatabase::class.java, 27 | DATABASE_NAME 28 | ) 29 | .addMigrations(MIGRATION_1_2) 30 | .build() 31 | } 32 | 33 | @Singleton 34 | @Provides 35 | fun providePostDao(appDatabase: PostDatabase): PostDaoCoroutines = appDatabase.postDao() 36 | 37 | @Provides 38 | fun providePostDaoRxJava3(appDatabase: PostDatabase): PostDaoRxJava3 = 39 | appDatabase.postDaoRxJava() 40 | 41 | @Provides 42 | fun providePostStatusDao(appDatabase: PostDatabase): PostStatusDao = appDatabase.postStatusDao() 43 | } 44 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/smarttoolfactory/data/mapper/Mappers.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.mapper 2 | 3 | import com.smarttoolfactory.data.model.PostDTO 4 | import com.smarttoolfactory.data.model.PostEntity 5 | 6 | /** 7 | * Mapper for transforming objects between REST and database or REST/db and domain 8 | * which are Non-nullable to Non-nullable 9 | */ 10 | interface Mapper { 11 | fun map(input: I): O 12 | } 13 | 14 | /** 15 | * Mapper for transforming objects between REST and database or REST/db and domain 16 | * as [List] which are Non-nullable to Non-nullable 17 | */ 18 | interface ListMapper : Mapper, List> 19 | 20 | class DTOtoEntityMapper : ListMapper { 21 | 22 | override fun map(input: List): List { 23 | return input.map { 24 | PostEntity(it.id, it.userId, it.title, it.body) 25 | } 26 | } 27 | } 28 | 29 | /** 30 | * Interface for marking models used for fetching data from REST 31 | */ 32 | interface IDataTransferObject 33 | 34 | /** 35 | * Interface for marking models used for fetching data from database 36 | */ 37 | interface IEntity 38 | 39 | /** 40 | * Interface for marking models used for presentation and ui 41 | */ 42 | interface IuiItem 43 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/smarttoolfactory/data/model/PostDTO.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.model 2 | 3 | data class PostDTO( 4 | val id: Int, 5 | val userId: Int, 6 | val title: String, 7 | val body: String 8 | ) 9 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/smarttoolfactory/data/model/PostEntity.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.model 2 | 3 | import androidx.room.Embedded 4 | import androidx.room.Entity 5 | import androidx.room.ForeignKey 6 | import androidx.room.Index 7 | import androidx.room.PrimaryKey 8 | import androidx.room.Relation 9 | 10 | @Entity(tableName = "post") 11 | data class PostEntity( 12 | @PrimaryKey 13 | val id: Int, 14 | val userId: Int, 15 | val title: String, 16 | val body: String 17 | ) 18 | 19 | /** 20 | * * Data class that contains [PostStatus] data. 21 | * [PostEntity.id] is in [PostEntity] class, [PostStatus.postId] is in [PostStatus] 22 | * both points to same value. 23 | * 24 | * * [PostStatus.id] is auto generated by insertion to table. 25 | * 26 | * * Index let's this table to be sorted by postId which makes all 27 | * rows with same postId to be found faster. 28 | * 29 | * * Status of the [PostEntity] with [PostEntity.id] or [PostStatus.postId] belong to current user 30 | * logged in with [PostStatus.userAccountId] or -1 if any user hasn't logged in 31 | */ 32 | @Entity( 33 | tableName = "post_status", 34 | indices = [Index(value = ["userAccountId", "postId"])], 35 | foreignKeys = [ 36 | ForeignKey( 37 | entity = PostEntity::class, 38 | parentColumns = ["id"], 39 | childColumns = ["postId"], 40 | onDelete = ForeignKey.NO_ACTION 41 | ) 42 | ] 43 | ) 44 | data class PostStatus( 45 | @PrimaryKey(autoGenerate = true) 46 | val id: Int = 0, 47 | val userAccountId: Int = -1, 48 | val postId: Int, 49 | val displayCount: Int = 0, 50 | val isFavorite: Boolean = false 51 | ) 52 | 53 | /** 54 | * @Embedded tag is for having nested entities that are contained inside another entity. For 55 | * instance Songs are embedded inside an Album. 56 | * 57 | * @Relation is for having relation between entities based on pairing one or more properties, 58 | * such as ids. For instance Person with id, having Pets that has userId that is exactly same 59 | * with each other. 60 | * 61 | * * ParentColumn name from [PostEntity] class is matched with entityColumn 62 | * from [PostStatus.postId] 63 | */ 64 | data class PostAndStatus( 65 | 66 | @Embedded 67 | val postEntity: PostEntity, 68 | 69 | // 🔥 'id' comes from Post, 'postId' comes from Post. Both are the same ids 70 | @Relation(parentColumn = "id", entityColumn = "postId") 71 | var postStatus: PostStatus? = null 72 | ) 73 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/smarttoolfactory/data/repository/PostRepositoryCoroutines.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.repository 2 | 3 | import com.smarttoolfactory.data.model.PostEntity 4 | import io.reactivex.rxjava3.core.Completable 5 | import io.reactivex.rxjava3.core.Single 6 | 7 | /** 8 | * This repository contains no data save, delete or fetch logic with Coroutines. 9 | * 10 | * All business logic for creating offline-first or offline-last approach is moved to UseCase 11 | */ 12 | interface PostRepositoryCoroutines { 13 | 14 | suspend fun fetchEntitiesFromRemote(): List 15 | 16 | suspend fun getPostEntitiesFromLocal(): List 17 | 18 | suspend fun savePostEntities(postEntities: List) 19 | 20 | suspend fun deletePostEntities() 21 | } 22 | 23 | /** 24 | * This repository contains no data save, delete or fetch logic with RxJava3. 25 | * 26 | * All business logic for creating offline-first or offline-last approach is moved to UseCase 27 | */ 28 | interface PostRepositoryRxJava3 { 29 | 30 | fun fetchEntitiesFromRemote(): Single> 31 | 32 | fun getPostEntitiesFromLocal(): Single> 33 | 34 | fun savePostEntities(postEntities: List): Completable 35 | 36 | fun deletePostEntities(): Completable 37 | } 38 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/smarttoolfactory/data/repository/PostStatusRepository.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.repository 2 | 3 | import com.smarttoolfactory.data.mapper.DTOtoEntityMapper 4 | import com.smarttoolfactory.data.model.PostAndStatus 5 | import com.smarttoolfactory.data.model.PostEntity 6 | import com.smarttoolfactory.data.model.PostStatus 7 | import com.smarttoolfactory.data.source.LocalPostStatusSource 8 | import com.smarttoolfactory.data.source.RemotePostDataSourceCoroutines 9 | import javax.inject.Inject 10 | 11 | interface PostStatusRepository { 12 | 13 | suspend fun fetchEntitiesFromRemote(): List 14 | 15 | suspend fun getPostWithStatusFromLocal(): List 16 | 17 | suspend fun updatePostStatus(postStatus: PostStatus) 18 | 19 | suspend fun getPostStatus(userAccountId: Int, postId: Int): PostStatus? 20 | 21 | suspend fun savePostEntities(postEntities: List) 22 | 23 | suspend fun deletePostEntities() 24 | } 25 | 26 | class PostStatusRepositoryImpl 27 | @Inject constructor( 28 | private val remotePostDataSource: RemotePostDataSourceCoroutines, 29 | private val localPostDataSource: LocalPostStatusSource, 30 | private val mapper: DTOtoEntityMapper 31 | ) : 32 | PostStatusRepository { 33 | 34 | override suspend fun fetchEntitiesFromRemote(): List { 35 | val postDTOList = remotePostDataSource.getPostDTOs() 36 | return mapper.map(postDTOList) 37 | } 38 | 39 | override suspend fun getPostWithStatusFromLocal(): List { 40 | return localPostDataSource.getPostWithStatus() 41 | } 42 | 43 | override suspend fun updatePostStatus(postStatus: PostStatus) { 44 | localPostDataSource.updatePostStatus(postStatus) 45 | } 46 | 47 | override suspend fun getPostStatus(userAccountId: Int, postId: Int): PostStatus? { 48 | return localPostDataSource.getPostStatus(userAccountId, postId) 49 | } 50 | 51 | override suspend fun savePostEntities(postEntities: List) { 52 | localPostDataSource.savePostEntities(postEntities) 53 | } 54 | 55 | override suspend fun deletePostEntities() { 56 | localPostDataSource.deletePostEntities() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/smarttoolfactory/data/source/PostDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.source 2 | 3 | import com.smarttoolfactory.data.model.PostDTO 4 | import com.smarttoolfactory.data.model.PostEntity 5 | import io.reactivex.rxjava3.core.Completable 6 | import io.reactivex.rxjava3.core.Single 7 | 8 | interface PostDataSource 9 | 10 | /* 11 | Coroutines 12 | */ 13 | interface RemotePostDataSourceCoroutines : PostDataSource { 14 | suspend fun getPostDTOs(): List 15 | } 16 | 17 | interface LocalPostDataSourceCoroutines : PostDataSource { 18 | suspend fun getPostEntities(): List 19 | suspend fun saveEntities(posts: List): List 20 | suspend fun deletePostEntities() 21 | } 22 | 23 | /* 24 | RxJava3 25 | */ 26 | interface RemotePostDataSourceRxJava3 : PostDataSource { 27 | fun getPostDTOs(): Single> 28 | } 29 | 30 | interface LocalPostDataSourceRxJava3 : PostDataSource { 31 | fun getPostEntities(): Single> 32 | fun saveEntities(posts: List): Completable 33 | fun deletePostEntities(): Completable 34 | } 35 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/smarttoolfactory/data/source/PostDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.source 2 | 3 | import com.smarttoolfactory.data.api.PostApi 4 | import com.smarttoolfactory.data.api.PostApiRxJava 5 | import com.smarttoolfactory.data.db.PostDaoCoroutines 6 | import com.smarttoolfactory.data.db.PostDaoRxJava3 7 | import com.smarttoolfactory.data.model.PostDTO 8 | import com.smarttoolfactory.data.model.PostEntity 9 | import io.reactivex.rxjava3.core.Completable 10 | import io.reactivex.rxjava3.core.Single 11 | import javax.inject.Inject 12 | 13 | /* 14 | *** Coroutines Implementations for PostDataSources *** 15 | */ 16 | class RemotePostDataSourceCoroutinesImpl @Inject constructor(private val postApi: PostApi) : 17 | RemotePostDataSourceCoroutines { 18 | 19 | override suspend fun getPostDTOs(): List { 20 | return postApi.getPosts() 21 | } 22 | } 23 | 24 | class LocalPostDataSourceCoroutinesImpl 25 | @Inject constructor(private val postDao: PostDaoCoroutines) : LocalPostDataSourceCoroutines { 26 | 27 | override suspend fun getPostEntities(): List { 28 | return postDao.getPostList() 29 | } 30 | 31 | override suspend fun saveEntities(posts: List): List { 32 | 33 | return postDao.insert(posts) 34 | } 35 | 36 | override suspend fun deletePostEntities() { 37 | postDao.deleteAll() 38 | } 39 | } 40 | 41 | /* 42 | *** RxJava3 Implementations for PostDataSources *** 43 | */ 44 | class RemoteDataSourceRxJava3Impl @Inject constructor(private val postApi: PostApiRxJava) : 45 | RemotePostDataSourceRxJava3 { 46 | 47 | override fun getPostDTOs(): Single> { 48 | return postApi.getPostsSingle() 49 | } 50 | } 51 | 52 | class LocalDataSourceRxJava3Impl @Inject constructor(private val postDaoRxJava3: PostDaoRxJava3) : 53 | LocalPostDataSourceRxJava3 { 54 | 55 | override fun getPostEntities(): Single> { 56 | return postDaoRxJava3.getPostsSingle() 57 | } 58 | 59 | override fun saveEntities(posts: List): Completable { 60 | return postDaoRxJava3.insert(posts) 61 | } 62 | 63 | override fun deletePostEntities(): Completable { 64 | return postDaoRxJava3.deleteAll() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /libraries/data/src/main/java/com/smarttoolfactory/data/source/PostStatusDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.source 2 | 3 | import com.smarttoolfactory.data.db.PostDaoCoroutines 4 | import com.smarttoolfactory.data.db.PostStatusDao 5 | import com.smarttoolfactory.data.model.PostAndStatus 6 | import com.smarttoolfactory.data.model.PostEntity 7 | import com.smarttoolfactory.data.model.PostStatus 8 | import javax.inject.Inject 9 | 10 | interface LocalPostStatusSource : PostDataSource { 11 | suspend fun getPostWithStatus(): List 12 | suspend fun updatePostStatus(postStatus: PostStatus) 13 | suspend fun getPostStatus(userAccountId: Int, postId: Int): PostStatus? 14 | suspend fun savePostEntities(postEntities: List) 15 | suspend fun deletePostEntities() 16 | } 17 | 18 | class LocalPostStatusSourceImpl @Inject constructor( 19 | private val postDaoCoroutines: PostDaoCoroutines, 20 | private val postStatusDao: PostStatusDao, 21 | ) : 22 | LocalPostStatusSource { 23 | 24 | override suspend fun getPostWithStatus(): List { 25 | return postStatusDao.getPostWithStatus() 26 | } 27 | 28 | override suspend fun updatePostStatus(postStatus: PostStatus) { 29 | postStatusDao.insertOrUpdatePostStatus(postStatus) 30 | } 31 | 32 | override suspend fun getPostStatus(userAccountId: Int, postId: Int): PostStatus? { 33 | return postStatusDao.getUpdateStatus(userAccountId, postId) 34 | } 35 | 36 | override suspend fun savePostEntities(postEntities: List) { 37 | postDaoCoroutines.insert(postEntities) 38 | } 39 | 40 | override suspend fun deletePostEntities() { 41 | postDaoCoroutines.deleteAll() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /libraries/data/src/test/java/com/smarttoolfactory/data/mapper/DTOtoEntityMapperTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.mapper 2 | 3 | import com.google.common.truth.Truth 4 | import com.smarttoolfactory.data.model.PostDTO 5 | import com.smarttoolfactory.data.model.PostEntity 6 | import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH 7 | import com.smarttoolfactory.test_utils.util.convertFromJsonToListOf 8 | import com.smarttoolfactory.test_utils.util.getResourceAsText 9 | import org.junit.jupiter.api.Test 10 | 11 | class DTOtoEntityMapperTest { 12 | 13 | private val postDTOList by lazy { 14 | convertFromJsonToListOf(getResourceAsText(RESPONSE_JSON_PATH))!! 15 | } 16 | 17 | private val postEntityList by lazy { 18 | convertFromJsonToListOf(getResourceAsText(RESPONSE_JSON_PATH))!! 19 | } 20 | 21 | @Test 22 | fun `given PostDTO is input, should return PostEntity`() { 23 | 24 | val mapper = DTOtoEntityMapper() 25 | 26 | // GIVEN 27 | val expected = postEntityList 28 | 29 | // WHEN 30 | val actual = mapper.map(postDTOList) 31 | 32 | // THEN 33 | Truth.assertThat(actual).containsExactlyElementsIn(expected) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /libraries/data/src/test/java/com/smarttoolfactory/data/test_suite/JUnit5DataTestSuite.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.test_suite 2 | 3 | import com.smarttoolfactory.data.mapper.DTOtoEntityMapperTest 4 | import com.smarttoolfactory.data.source.PostDataSourceCoroutinesTest 5 | import com.smarttoolfactory.data.source.PostDataSourceRxJava3Test 6 | import org.junit.runner.RunWith 7 | import org.junit.runners.Suite 8 | 9 | // FIXME Not working, FIX! https://github.com/junit-team/junit5/issues/1334 10 | @RunWith(Suite::class) 11 | @Suite.SuiteClasses( 12 | DTOtoEntityMapperTest::class, 13 | PostDataSourceCoroutinesTest::class, 14 | PostDataSourceRxJava3Test::class 15 | ) 16 | class JUnit5DataTestSuite 17 | -------------------------------------------------------------------------------- /libraries/data/src/test/java/com/smarttoolfactory/data/test_suite/JUnit5TestSuite.java: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.data.test_suite; 2 | 3 | import org.junit.runner.RunWith; 4 | 5 | //@RunWith(JUnitPlatform.class) 6 | //@SelectClasses({AssertionTest.class, AssumptionTest.class, ExceptionTest.class}) 7 | //public class JUnit5TestSuite { 8 | // 9 | //} -------------------------------------------------------------------------------- /libraries/domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /libraries/domain/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import extension.addInstrumentationTestDependencies 2 | import extension.addUnitTestDependencies 3 | 4 | plugins { 5 | id(Plugins.ANDROID_LIBRARY_PLUGIN) 6 | id(Plugins.KOTLIN_ANDROID_PLUGIN) 7 | id(Plugins.KOTLIN_ANDROID_EXTENSIONS_PLUGIN) 8 | id(Plugins.KOTLIN_KAPT_PLUGIN) 9 | id(Plugins.DAGGER_HILT_PLUGIN) 10 | } 11 | 12 | android { 13 | 14 | compileSdkVersion(AndroidVersion.COMPILE_SDK_VERSION) 15 | defaultConfig { 16 | minSdkVersion(AndroidVersion.MIN_SDK_VERSION) 17 | targetSdkVersion(AndroidVersion.TARGET_SDK_VERSION) 18 | versionCode = AndroidVersion.VERSION_CODE 19 | versionName = AndroidVersion.VERSION_NAME 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | } 22 | 23 | buildTypes { 24 | getByName("release") { 25 | isMinifyEnabled = false 26 | proguardFiles( 27 | getDefaultProguardFile("proguard-android-optimize.txt"), 28 | "proguard-rules.pro" 29 | ) 30 | } 31 | } 32 | compileOptions { 33 | sourceCompatibility = JavaVersion.VERSION_1_8 34 | targetCompatibility = JavaVersion.VERSION_1_8 35 | } 36 | kotlinOptions { 37 | jvmTarget = "1.8" 38 | } 39 | 40 | // sourceSets { 41 | // 42 | // val sharedTestDir = 43 | // "${project(Modules.AndroidLibrary.TEST_UTILS).projectDir}/src/test-shared/java" 44 | // 45 | // getByName("test") { 46 | // java.srcDir(sharedTestDir) 47 | // } 48 | // 49 | // getByName("androidTest") { 50 | // java.srcDir(sharedTestDir) 51 | // resources.srcDir("${project(Modules.AndroidLibrary.TEST_UTILS).projectDir}/src/test/resources") 52 | // } 53 | // } 54 | } 55 | 56 | dependencies { 57 | 58 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) 59 | 60 | implementation(project(Modules.AndroidLibrary.DATA)) 61 | 62 | implementation(Deps.KOTLIN) 63 | implementation(Deps.ANDROIDX_CORE_KTX) 64 | 65 | // Dagger 66 | implementation(Deps.DAGGER_HILT_ANDROID) 67 | kapt(Deps.DAGGER_HILT_COMPILER) 68 | 69 | // RxJava 70 | implementation(Deps.RX_JAVA3) 71 | // RxAndroid 72 | implementation(Deps.RX_JAVA3_ANDROID) 73 | 74 | // Coroutines 75 | implementation(Deps.COROUTINES_CORE) 76 | implementation(Deps.COROUTINES_ANDROID) 77 | 78 | testImplementation(project(Modules.AndroidLibrary.TEST_UTILS)) 79 | 80 | testImplementation(Deps.GSON) 81 | 82 | addUnitTestDependencies() 83 | addInstrumentationTestDependencies() 84 | } 85 | -------------------------------------------------------------------------------- /libraries/domain/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/libraries/domain/consumer-rules.pro -------------------------------------------------------------------------------- /libraries/domain/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 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 -------------------------------------------------------------------------------- /libraries/domain/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /libraries/domain/src/main/java/com/smarttoolfactory/domain/base/Disposable.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.domain.base 2 | 3 | /** 4 | * Interface for adding common behavior for any class that has items that need to be disposed 5 | * such as RxJava Observables or Coroutines jobs. 6 | */ 7 | interface Disposable { 8 | 9 | fun dispose() 10 | } 11 | -------------------------------------------------------------------------------- /libraries/domain/src/main/java/com/smarttoolfactory/domain/di/DomainModule.kt: -------------------------------------------------------------------------------- 1 | // package com.smarttoolfactory.domain.di 2 | // 3 | // import com.smarttoolfactory.domain.dispatcher.UseCaseDispatchers 4 | // import dagger.Module 5 | // import dagger.Provides 6 | // import dagger.hilt.InstallIn 7 | // import dagger.hilt.android.components.ApplicationComponent 8 | // import kotlinx.coroutines.Dispatchers 9 | // 10 | // @Module 11 | // @InstallIn(ApplicationComponent::class) 12 | // class DomainModule { 13 | // 14 | // @Provides 15 | // fun provideUseCaseDispatchers(): UseCaseDispatchers { 16 | // return UseCaseDispatchers(Dispatchers.IO, Dispatchers.Default, Dispatchers.Main) 17 | // } 18 | // } 19 | -------------------------------------------------------------------------------- /libraries/domain/src/main/java/com/smarttoolfactory/domain/di/DomainModuleDependencies.kt: -------------------------------------------------------------------------------- 1 | // package com.smarttoolfactory.domain.di 2 | // 3 | // import com.smarttoolfactory.data.repository.PostStatusRepository 4 | // import com.smarttoolfactory.domain.usecase.GetPostListUseCaseFlow 5 | // import com.smarttoolfactory.domain.usecase.GetPostListUseCaseRxJava3 6 | // import com.smarttoolfactory.domain.usecase.GetPostsWithStatusUseCaseFlow 7 | // import dagger.hilt.EntryPoint 8 | // import dagger.hilt.InstallIn 9 | // import dagger.hilt.android.components.ApplicationComponent 10 | // 11 | // @EntryPoint 12 | // @InstallIn(ApplicationComponent::class) 13 | // interface DomainModuleDependencies { 14 | // 15 | // /* 16 | // Provision methods to provide dependencies to components that depend on this component 17 | // */ 18 | // 19 | // fun postStatusRepository(): PostStatusRepository 20 | // 21 | // fun getPostListUseCaseFlow(): GetPostListUseCaseFlow 22 | // fun getPostListUseCaseRxJava3(): GetPostListUseCaseRxJava3 23 | // fun getPostsWithStatusUseCaseFlow(): GetPostsWithStatusUseCaseFlow 24 | // } 25 | -------------------------------------------------------------------------------- /libraries/domain/src/main/java/com/smarttoolfactory/domain/dispatcher/UseCaseDispatchers.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.domain.dispatcher 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | 6 | /** 7 | * Class for providing dispatcher for different thread operations. This class is useful 8 | * in tests to control every thread in one place. 9 | */ 10 | 11 | data class UseCaseDispatchers( 12 | val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, 13 | val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default, 14 | val mainDispatcher: CoroutineDispatcher = Dispatchers.Main 15 | ) 16 | -------------------------------------------------------------------------------- /libraries/domain/src/main/java/com/smarttoolfactory/domain/error/EmptyDataException.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.domain.error 2 | 3 | class EmptyDataException(message: String) : Exception(message) 4 | -------------------------------------------------------------------------------- /libraries/domain/src/main/java/com/smarttoolfactory/domain/mapper/EntityToPostMapper.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.domain.mapper 2 | 3 | import com.smarttoolfactory.data.mapper.ListMapper 4 | import com.smarttoolfactory.data.model.PostAndStatus 5 | import com.smarttoolfactory.data.model.PostEntity 6 | import com.smarttoolfactory.domain.model.Post 7 | import javax.inject.Inject 8 | import javax.inject.Singleton 9 | 10 | @Singleton 11 | class EntityToPostMapper @Inject constructor() : ListMapper { 12 | override fun map(input: List): List { 13 | return input.map { 14 | Post(it.id, it.userId, it.title, it.body) 15 | } 16 | } 17 | } 18 | 19 | class EntityToPostStatusMapper @Inject constructor() : ListMapper { 20 | override fun map(input: List): List { 21 | 22 | return input.map { 23 | 24 | val postEntity = it.postEntity 25 | val status = it.postStatus 26 | 27 | Post( 28 | postEntity.id, 29 | postEntity.userId, 30 | postEntity.title, 31 | postEntity.body, 32 | status?.displayCount ?: 0, 33 | status?.isFavorite ?: false 34 | ) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /libraries/domain/src/main/java/com/smarttoolfactory/domain/model/Post.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.domain.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | 6 | @Parcelize 7 | data class Post( 8 | val id: Int, 9 | val userId: Int, 10 | val title: String, 11 | val body: String, 12 | var displayCount: Int = 0, 13 | var isFavorite: Boolean = false 14 | ) : Parcelable 15 | -------------------------------------------------------------------------------- /libraries/domain/src/main/java/com/smarttoolfactory/domain/util/RxJavaExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.domain.util 2 | 3 | import io.reactivex.rxjava3.core.Observable 4 | import io.reactivex.rxjava3.core.Scheduler 5 | import io.reactivex.rxjava3.core.Single 6 | import io.reactivex.rxjava3.disposables.CompositeDisposable 7 | import io.reactivex.rxjava3.disposables.Disposable 8 | import io.reactivex.rxjava3.schedulers.Schedulers 9 | 10 | /** 11 | * 12 | * Extension Functions for RxJava2 13 | * @author Smart Tool Factory 14 | * 15 | */ 16 | fun Observable.listen( 17 | scheduleSubscribe: Scheduler, 18 | schedulerObserve: Scheduler 19 | ): Observable { 20 | return subscribeOn(scheduleSubscribe) 21 | .observeOn(schedulerObserve) 22 | } 23 | 24 | fun Observable.subscribeOnIoObserveOnComputation(): Observable { 25 | return listen(Schedulers.io(), Schedulers.computation()) 26 | } 27 | 28 | fun Single.listen( 29 | scheduleSubscribe: Scheduler, 30 | schedulerObserve: Scheduler 31 | ): Single { 32 | return subscribeOn(scheduleSubscribe) 33 | .observeOn(schedulerObserve) 34 | } 35 | 36 | fun Single.subscribeOnIoObserveOnComputation(): Single { 37 | return listen(Schedulers.io(), Schedulers.computation()) 38 | } 39 | 40 | fun Observable.observeResultOnIO( 41 | onNext: (T) -> Unit, 42 | onError: ((Throwable) -> Unit)? = null, 43 | onComplete: (() -> Unit)? = null 44 | ): Disposable { 45 | return subscribeOnIoObserveOnComputation() 46 | .subscribe( 47 | { 48 | onNext(it) 49 | }, 50 | { throwable -> 51 | onError?.apply { 52 | onError(throwable) 53 | } 54 | }, 55 | { 56 | onComplete?.apply { 57 | onComplete() 58 | } 59 | } 60 | ) 61 | } 62 | 63 | fun Disposable.addTo(disposables: CompositeDisposable) { 64 | disposables.add(this) 65 | } 66 | -------------------------------------------------------------------------------- /libraries/domain/src/test/java/com/smarttoolfactory/domain/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.domain 2 | 3 | import io.mockk.every 4 | import io.mockk.spyk 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | 8 | /** 9 | * Example local unit test, which will execute on the development machine (host). 10 | * 11 | * See [testing documentation](http://d.android.com/tools/testing). 12 | */ 13 | class ExampleUnitTest { 14 | 15 | private val mock = spyk(TestObject(), recordPrivateCalls = true) 16 | 17 | @Test 18 | fun addition_isCorrect() { 19 | 20 | every { mock["count"]() } returns 5 21 | 22 | assertEquals(4, 2 + 2) 23 | } 24 | } 25 | 26 | class TestObject() { 27 | 28 | private var name = "Hello" 29 | 30 | private var counter = 0 31 | 32 | private fun increaseAndCount(): Int { 33 | return ++counter 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /libraries/test-utils/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /libraries/test-utils/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.ANDROID_LIBRARY_PLUGIN) 3 | id(Plugins.KOTLIN_ANDROID_PLUGIN) 4 | id(Plugins.KOTLIN_ANDROID_EXTENSIONS_PLUGIN) 5 | id(Plugins.KOTLIN_KAPT_PLUGIN) 6 | } 7 | 8 | android { 9 | 10 | compileSdkVersion(AndroidVersion.COMPILE_SDK_VERSION) 11 | defaultConfig { 12 | minSdkVersion(AndroidVersion.MIN_SDK_VERSION) 13 | targetSdkVersion(AndroidVersion.TARGET_SDK_VERSION) 14 | versionCode = AndroidVersion.VERSION_CODE 15 | versionName = AndroidVersion.VERSION_NAME 16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | // sourceSets { 20 | // 21 | // val sharedTestDir = "src/test-shared/java" 22 | // 23 | // getByName("test") { 24 | // java.srcDir(sharedTestDir) 25 | // } 26 | // 27 | // getByName("androidTest") { 28 | // java.srcDir(sharedTestDir) 29 | // resources.srcDir("src/test/resources") 30 | // } 31 | // } 32 | 33 | buildTypes { 34 | getByName("release") { 35 | isMinifyEnabled = false 36 | proguardFiles( 37 | getDefaultProguardFile("proguard-android-optimize.txt"), 38 | "proguard-rules.pro" 39 | ) 40 | } 41 | } 42 | compileOptions { 43 | sourceCompatibility = JavaVersion.VERSION_1_8 44 | targetCompatibility = JavaVersion.VERSION_1_8 45 | } 46 | kotlinOptions { 47 | jvmTarget = "1.8" 48 | } 49 | testOptions { 50 | unitTests.isIncludeAndroidResources = true 51 | } 52 | } 53 | 54 | dependencies { 55 | 56 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) 57 | 58 | implementation(Deps.KOTLIN) 59 | implementation(Deps.ANDROIDX_CORE_KTX) 60 | 61 | implementation(Deps.RX_JAVA3) 62 | implementation(Deps.RX_JAVA3_ANDROID) 63 | implementation(Deps.COROUTINES_CORE) 64 | implementation(Deps.COROUTINES_ANDROID) 65 | 66 | // Lifecycle, LiveData, ViewModel 67 | implementation(Deps.LIFECYCLE_LIVEDATA_KTX) 68 | implementation(Deps.LIFECYCLE_VIEWMODEL_KTX) 69 | implementation(Deps.LIFECYCLE_EXTENSIONS) 70 | 71 | // implementation(TestDeps.ANDROIDX_CORE_KTX) 72 | // implementation(TestDeps.ANDROIDX_CORE_TESTING) 73 | 74 | implementation(TestDeps.JUNIT5_API) 75 | implementation(TestDeps.JUNIT5_ENGINE) 76 | 77 | // GSon 78 | implementation(Deps.GSON) 79 | 80 | // Coroutines Test 81 | implementation(TestDeps.COROUTINES_TEST) 82 | 83 | // MockWebServer 84 | implementation(TestDeps.MOCK_WEB_SERVER) 85 | } 86 | -------------------------------------------------------------------------------- /libraries/test-utils/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/libraries/test-utils/consumer-rules.pro -------------------------------------------------------------------------------- /libraries/test-utils/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 -------------------------------------------------------------------------------- /libraries/test-utils/src/androidTest/java/com/smarttoolfactory/test_utils/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | // package com.smarttoolfactory.test_utils 2 | // 3 | // import androidx.test.ext.junit.runners.AndroidJUnit4 4 | // import androidx.test.platform.app.InstrumentationRegistry 5 | // import org.junit.Test 6 | // import org.junit.runner.RunWith 7 | // 8 | // /** 9 | // * Instrumented test, which will execute on an Android device. 10 | // * 11 | // * See [testing documentation](http://d.android.com/tools/testing). 12 | // */ 13 | // @RunWith(AndroidJUnit4::class) 14 | // class ExampleInstrumentedTest { 15 | // @Test 16 | // fun useAppContext() { 17 | // // Context of the app under test. 18 | // val appContext = InstrumentationRegistry.getInstrumentation().targetContext 19 | // assertEquals("com.smarttoolfactory.test_shared.test", appContext.packageName) 20 | // } 21 | // } 22 | -------------------------------------------------------------------------------- /libraries/test-utils/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /libraries/test-utils/src/main/java/com/smarttoolfactory/test_utils/TestConstans.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.test_utils 2 | 3 | const val RESPONSE_JSON_PATH = "response.json" 4 | const val SERVER_INTERNAL_ERROR_MESSAGE = "Unexpected error occurred" 5 | -------------------------------------------------------------------------------- /libraries/test-utils/src/main/java/com/smarttoolfactory/test_utils/extension/RxImmediateSchedulerExtension.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.test_utils.extension 2 | 3 | import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins 4 | import io.reactivex.rxjava3.core.Scheduler 5 | import io.reactivex.rxjava3.internal.schedulers.ExecutorScheduler 6 | import io.reactivex.rxjava3.plugins.RxJavaPlugins 7 | import java.util.concurrent.Executor 8 | import org.junit.jupiter.api.extension.AfterEachCallback 9 | import org.junit.jupiter.api.extension.BeforeEachCallback 10 | import org.junit.jupiter.api.extension.ExtensionContext 11 | 12 | /** 13 | * LifeCycle 14 | * 15 | * * BeforeAllCallback 16 | * * BeforeAll 17 | * * BeforeEachCallback 18 | * * BeforeEach 19 | * * BeforeTestExecutionCallback 20 | * * Test 21 | * * AfterTestExecutionCallback 22 | * * AfterEach 23 | * * AfterEachCallback 24 | * * AfterAll 25 | * * AfterAllCallback 26 | */ 27 | class RxImmediateSchedulerExtension : BeforeEachCallback, AfterEachCallback { 28 | 29 | private val immediate = object : Scheduler() { 30 | 31 | override fun createWorker(): Worker { 32 | return ExecutorScheduler.ExecutorWorker(Executor { it.run() }, true, true) 33 | } 34 | } 35 | 36 | // private val immediate = Schedulers.trampoline() 37 | 38 | override fun beforeEach(context: ExtensionContext?) { 39 | RxJavaPlugins.setInitIoSchedulerHandler { immediate } 40 | RxJavaPlugins.setInitComputationSchedulerHandler { immediate } 41 | RxJavaPlugins.setInitNewThreadSchedulerHandler { immediate } 42 | RxJavaPlugins.setInitSingleSchedulerHandler { immediate } 43 | RxAndroidPlugins.setInitMainThreadSchedulerHandler { immediate } 44 | } 45 | 46 | override fun afterEach(context: ExtensionContext?) { 47 | RxJavaPlugins.reset() 48 | RxAndroidPlugins.reset() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /libraries/test-utils/src/main/java/com/smarttoolfactory/test_utils/extension/TestCoroutineExtension.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.test_utils.extension 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.test.TestCoroutineDispatcher 5 | import kotlinx.coroutines.test.TestCoroutineScope 6 | import kotlinx.coroutines.test.resetMain 7 | import kotlinx.coroutines.test.runBlockingTest 8 | import kotlinx.coroutines.test.setMain 9 | import org.junit.jupiter.api.extension.AfterEachCallback 10 | import org.junit.jupiter.api.extension.BeforeEachCallback 11 | import org.junit.jupiter.api.extension.ExtensionContext 12 | 13 | /** 14 | * LifeCycle 15 | * 16 | * * BeforeAllCallback 17 | * * BeforeAll 18 | * * BeforeEachCallback 19 | * * BeforeEach 20 | * * BeforeTestExecutionCallback 21 | * * Test 22 | * * AfterTestExecutionCallback 23 | * * AfterEach 24 | * * AfterEachCallback 25 | * * AfterAll 26 | * * AfterAllCallback 27 | */ 28 | class TestCoroutineExtension : BeforeEachCallback, AfterEachCallback { 29 | 30 | private val testCoroutineDispatcher = TestCoroutineDispatcher() 31 | 32 | val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher) 33 | 34 | override fun beforeEach(context: ExtensionContext?) { 35 | println("🚙 TestCoroutineExtension beforeEach()") 36 | Dispatchers.setMain(testCoroutineDispatcher) 37 | } 38 | 39 | override fun afterEach(context: ExtensionContext?) { 40 | 41 | println("🚗 TestCoroutineExtension afterEach()") 42 | 43 | Dispatchers.resetMain() 44 | try { 45 | testCoroutineScope.cleanupTestCoroutines() 46 | } catch (exception: Exception) { 47 | exception.printStackTrace() 48 | } 49 | } 50 | 51 | fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) = 52 | testCoroutineScope.runBlockingTest { block() } 53 | } 54 | -------------------------------------------------------------------------------- /libraries/test-utils/src/main/java/com/smarttoolfactory/test_utils/rule/MockWebServerRule.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.test_utils.rule 2 | 3 | import okhttp3.mockwebserver.MockWebServer 4 | import org.junit.rules.TestRule 5 | import org.junit.runner.Description 6 | import org.junit.runners.model.Statement 7 | 8 | /** 9 | * Test rule for JUnit4 to invoke actions which are 10 | * start [MockWebServer], 11 | * run the test , 12 | * and shut [MockWebServer] down after the test is run. 13 | */ 14 | class MockWebServerRule : TestRule { 15 | 16 | val mockWebServer = MockWebServer() 17 | 18 | override fun apply( 19 | base: Statement, 20 | description: Description 21 | ): Statement { 22 | 23 | return object : Statement() { 24 | 25 | @Throws(Throwable::class) 26 | override fun evaluate() { 27 | mockWebServer.start() 28 | base.evaluate() 29 | mockWebServer.shutdown() 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /libraries/test-utils/src/main/java/com/smarttoolfactory/test_utils/rule/RxImmediateSchedulerRule.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.test_utils.rule 2 | 3 | import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins 4 | import io.reactivex.rxjava3.core.Scheduler 5 | import io.reactivex.rxjava3.internal.schedulers.ExecutorScheduler 6 | import io.reactivex.rxjava3.plugins.RxJavaPlugins 7 | import java.util.concurrent.Executor 8 | import org.junit.rules.TestRule 9 | import org.junit.runner.Description 10 | import org.junit.runners.model.Statement 11 | 12 | class RxImmediateSchedulerRule : TestRule { 13 | 14 | private val immediate = object : Scheduler() { 15 | 16 | override fun createWorker(): Worker { 17 | return ExecutorScheduler.ExecutorWorker(Executor { it.run() }, true, true) 18 | } 19 | } 20 | 21 | // private val immediate = Schedulers.trampoline() 22 | 23 | override fun apply(base: Statement, description: Description): Statement { 24 | return object : Statement() { 25 | @Throws(Throwable::class) 26 | override fun evaluate() { 27 | RxJavaPlugins.setInitIoSchedulerHandler { immediate } 28 | RxJavaPlugins.setInitComputationSchedulerHandler { immediate } 29 | RxJavaPlugins.setInitNewThreadSchedulerHandler { immediate } 30 | RxJavaPlugins.setInitSingleSchedulerHandler { immediate } 31 | RxAndroidPlugins.setInitMainThreadSchedulerHandler { immediate } 32 | 33 | try { 34 | base.evaluate() 35 | } finally { 36 | RxJavaPlugins.reset() 37 | RxAndroidPlugins.reset() 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /libraries/test-utils/src/main/java/com/smarttoolfactory/test_utils/rule/TestCoroutineRule.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.test_utils.rule 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.TestCoroutineDispatcher 6 | import kotlinx.coroutines.test.TestCoroutineScope 7 | import kotlinx.coroutines.test.resetMain 8 | import kotlinx.coroutines.test.runBlockingTest 9 | import kotlinx.coroutines.test.setMain 10 | import org.junit.rules.TestRule 11 | import org.junit.runner.Description 12 | import org.junit.runners.model.Statement 13 | 14 | @ExperimentalCoroutinesApi 15 | class TestCoroutineRule : TestRule { 16 | 17 | private val testCoroutineDispatcher = TestCoroutineDispatcher() 18 | 19 | val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher) 20 | 21 | override fun apply(base: Statement, description: Description?) = object : Statement() { 22 | 23 | @Throws(Throwable::class) 24 | override fun evaluate() { 25 | 26 | Dispatchers.setMain(testCoroutineDispatcher) 27 | 28 | base.evaluate() 29 | 30 | Dispatchers.resetMain() 31 | try { 32 | testCoroutineScope.cleanupTestCoroutines() 33 | } catch (exception: Exception) { 34 | exception.printStackTrace() 35 | } 36 | } 37 | } 38 | 39 | fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) = 40 | testCoroutineScope.runBlockingTest { block() } 41 | } 42 | -------------------------------------------------------------------------------- /libraries/test-utils/src/main/java/com/smarttoolfactory/test_utils/util/LiveDataTestUtil.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.test_utils.util 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.Observer 5 | import java.util.concurrent.CountDownLatch 6 | import java.util.concurrent.TimeUnit 7 | import java.util.concurrent.TimeoutException 8 | 9 | /** 10 | * Gets the value of a [LiveData] or waits for it to have one, with a timeout. 11 | * 12 | * Use this extension from host-side (JVM) tests. It's recommended to use it alongside 13 | * `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously. 14 | */ 15 | fun LiveData.getOrAwaitValue( 16 | time: Long = 2, 17 | timeUnit: TimeUnit = TimeUnit.SECONDS, 18 | afterObserve: () -> Unit = {} 19 | ): T { 20 | 21 | var data: T? = null 22 | val latch = CountDownLatch(1) 23 | 24 | val observer = object : Observer { 25 | override fun onChanged(o: T?) { 26 | data = o 27 | latch.countDown() 28 | this@getOrAwaitValue.removeObserver(this) 29 | } 30 | } 31 | 32 | this.observeForever(observer) 33 | 34 | afterObserve.invoke() 35 | 36 | // Don't wait indefinitely if the LiveData is not set. 37 | if (!latch.await(time, timeUnit)) { 38 | this.removeObserver(observer) 39 | throw TimeoutException("LiveData value was never set.") 40 | } 41 | 42 | @Suppress("UNCHECKED_CAST") 43 | return data as T 44 | } 45 | -------------------------------------------------------------------------------- /libraries/test-utils/src/main/java/com/smarttoolfactory/test_utils/util/ReadResourceUtil.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.test_utils.util 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.GsonBuilder 5 | import com.google.gson.internal.LinkedTreeMap 6 | import com.google.gson.reflect.TypeToken 7 | 8 | /** 9 | * Use this method to get json files as string from resources folder to use in tests. 10 | */ 11 | fun getResourceAsText(path: String): String { 12 | return object {}.javaClass.classLoader!!.getResource(path)!!.readText() 13 | } 14 | 15 | inline fun Gson.fromJsonWithType(json: String): T? = 16 | fromJson(json, object : TypeToken() {}.type) 17 | 18 | /** 19 | * 20 | * Convert from json to item with type T 21 | * 22 | * * This function returns for some items as [LinkedTreeMap] 23 | */ 24 | inline fun convertToObjectFromJson(json: String): T? { 25 | return Gson().fromJsonWithType(json) 26 | } 27 | 28 | /** 29 | * 30 | * Convert from json to [List] of items with type T 31 | * 32 | * * This function returns for some items as [LinkedTreeMap] 33 | */ 34 | inline fun fromJsonToListOf(json: String): List { 35 | return GsonBuilder().create().fromJson(json, Array::class.java).asList() 36 | } 37 | 38 | fun Gson.mapFromLinkedTreeMap(map: Map?, type: Class): T? { 39 | if (map == null) return null 40 | 41 | val json = toJson(map) 42 | return fromJson(json, type) 43 | } 44 | 45 | inline fun convertFromJsonToListOf(json: String): List? { 46 | 47 | val gson = GsonBuilder().create() 48 | 49 | val itemList = fromJsonToListOf(json) 50 | 51 | if (itemList.first() !is LinkedTreeMap<*, *>) 52 | return itemList 53 | 54 | // Must use map here because the result is a list of LinkedTreeMaps 55 | val list: ArrayList>? = gson.fromJsonWithType(json) 56 | // handle type erasure 57 | 58 | return list?.mapNotNull { 59 | gson.mapFromLinkedTreeMap(it, T::class.java) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /libraries/test-utils/src/test/java/com/smarttoolfactory/test_utils/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.smarttoolfactory.test_utils 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /screenshots/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/screenshots/dashboard.png -------------------------------------------------------------------------------- /screenshots/post_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/screenshots/post_flow.png -------------------------------------------------------------------------------- /screenshots/post_rxjava3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/screenshots/post_rxjava3.png -------------------------------------------------------------------------------- /screenshots/post_with_status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmartToolFactory/Posts-MVVM-DaggerHilt-Dynamic-Feature-RxJava3-Flow-Sample/311d866ac918d64b6eb990f59d1cf9d1fa468c07/screenshots/post_with_status.png -------------------------------------------------------------------------------- /scripts/git-hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | echo "Running static analysis..." 5 | 6 | 7 | # Inspect code using KtLint, and Detekt 8 | 9 | # Run KtLint only 10 | #./gradlew app:ktlintCheck --daemon 11 | 12 | # Format code using KtLint, then run Detekt and KtLint static analysis 13 | ./gradlew app:ktlintFormat app:detekt app:ktlintCheck --daemon 14 | 15 | status=$? 16 | 17 | 18 | if [ "$status" = 0 ] ; then 19 | 20 | echo "Static analysis found no problems." 21 | 22 | exit 0 23 | 24 | else 25 | 26 | echo 1>&2 "Static analysis found violations it could not fix." 27 | 28 | exit 1 29 | 30 | fi 31 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include( 2 | ":app", 3 | ":libraries:core", 4 | ":libraries:data", 5 | ":libraries:domain", 6 | ":libraries:test-utils", 7 | ":features:home", 8 | ":features:post_detail", 9 | ":features:search", 10 | ":features:dashboard", 11 | ":features:notification", 12 | ":features:account" 13 | ) 14 | --------------------------------------------------------------------------------