├── .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 | [](https://ktlint.github.io/)
4 | [](https://kotlinlang.org)
5 | [](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 |
--------------------------------------------------------------------------------
/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