From 625776609d4896adcf578bf1693d13c9865758d0 Mon Sep 17 00:00:00 2001 From: Melihcan Aksoy Date: Fri, 26 Jul 2019 13:38:51 +0200 Subject: [PATCH] Milestones/ms1 (#16) * Closes #11 * Closes #13 * Closes #17 * Closes #18 * Closes #19 * Closes #6 * Closes #3 * Closes #12 * Closes #15 --- .circleci/config.yml | 6 +- .gitignore | 1 + .gradletasknamecache | 3130 +++++++++++++++++ app/build.gradle | 2 +- .../melih/rocketscience/di/AppComponent.kt | 7 +- .../com/melih/rocketscience/di/AppModule.kt | 26 - build.gradle | 12 +- core/jacoco.exec | Bin 0 -> 934 bytes .../melih/core/base/lifecycle/BaseActivity.kt | 2 - .../melih/core/base/lifecycle/BaseFragment.kt | 2 - .../core/base/paging/BasePagingDataSource.kt | 140 + .../core/base/paging/BasePagingFactory.kt | 42 + ...istAdapter.kt => BasePagingListAdapter.kt} | 12 +- .../base/viewmodel/BasePagingViewModel.kt | 70 + .../core/base/viewmodel/BaseViewModel.kt | 26 +- .../kotlin/com/melih/core/di/CoreComponent.kt | 6 +- .../kotlin/com/melih/core/di/CoreModule.kt | 13 +- .../melih/core/extensions/UtilityExtension.kt | 21 + .../com/melih/core/utils/SnackbarBehaviour.kt | 1 + .../com/melih/core/BaseTestWithMainThread.kt | 14 +- .../com/melih/core/base/BaseViewModelTest.kt | 23 +- .../core/paging/BasePagingDataSourceTest.kt | 166 + .../core/paging/BasePagingFactoryTest.kt | 33 + docs/dependency_hierarchy.png | Bin 0 -> 38740 bytes docs/module_graph.png | Bin 0 -> 39188 bytes features/detail/build.gradle | 3 - features/detail/jacoco.exec | Bin 0 -> 2651 bytes .../com/melih/detail/di/DetailContributor.kt | 6 +- .../com/melih/detail/di/DetailModule.kt | 24 + .../melih/detail/di/modules/DetailBinds.kt | 2 - .../melih/detail/di/modules/DetailProvides.kt | 21 + .../detail/di/scopes/DetailFragmentScope.kt | 7 + .../com/melih/detail/di/scopes/DetailScope.kt | 7 + .../com/melih/detail/ui/DetailActivity.kt | 6 +- .../com/melih/detail/ui/DetailFragment.kt | 10 - .../com/melih/detail/ui/DetailViewModel.kt | 22 +- .../melih/detail/BaseTestWithMainThread.kt | 6 +- .../com/melih/detail/DetailViewModelTest.kt | 8 +- features/{list => launches}/.gitignore | 0 features/{list => launches}/build.gradle | 5 +- .../{list => launches}/consumer-rules.pro | 0 features/launches/jacoco.exec | Bin 0 -> 2302 bytes .../{list => launches}/proguard-rules.pro | 0 .../sampledata/launches.json | 0 .../src/main/AndroidManifest.xml | 0 .../com/melih/list/di/LaunchesContributor.kt | 4 +- .../melih/list/di/LaunchesFeatureModule.kt | 24 + .../melih/list/di/modules/LaunchesBinds.kt | 4 +- .../melih/list/di/modules/LaunchesProvides.kt | 24 + .../list/di/scopes/LaunchesFragmentScope.kt | 7 + .../com/melih/list/di/scopes/LaunchesScope.kt | 7 + .../com/melih/list/ui/LaunchesActivity.kt | 2 - .../com/melih/list/ui/LaunchesFragment.kt | 97 + .../list/ui/adapters}/LaunchesAdapter.kt | 12 +- .../list/ui/paging/LaunchesPagingSource.kt | 26 + .../ui/paging/LaunchesPagingSourceFactory.kt | 14 + .../com/melih/list/ui/vm/LaunchesViewModel.kt | 45 + .../src/main/res/anim/item_enter.xml | 0 .../src/main/res/anim/layout_item_enter.xml | 0 .../src/main/res/layout/activity_launches.xml | 0 .../src/main/res/layout/fragment_launches.xml | 2 +- .../src/main/res/layout/row_launch.xml | 2 +- .../src/main/res/menu/menu_rocket_list.xml | 0 .../src/main/res/navigation/nav_launches.xml | 0 .../src/main/res/values/integers.xml | 0 .../src/main/res/values/strings.xml | 0 .../com/melih/list/BaseTestWithMainThread.kt | 5 +- .../melih/list/di/modules/LaunchesProvides.kt | 16 - .../com/melih/list/ui/LaunchesFragment.kt | 100 - .../com/melih/list/ui/LaunchesViewModel.kt | 38 - .../com/melih/list/LaunchesViewModelTest.kt | 33 - repository/build.gradle | 1 + .../kotlin/com/melih/repository/Repository.kt | 2 +- .../melih/repository/entities/LaunchEntity.kt | 2 + .../repository/entities/LaunchesEntity.kt | 3 + .../repository/entities/LocationEntity.kt | 3 + .../repository/entities/MissionEntity.kt | 2 + .../melih/repository/entities/RocketEntity.kt | 2 + .../interactors/GetLaunchDetails.kt | 36 +- .../repository/interactors/GetLaunches.kt | 37 +- .../interactors/base/BaseInteractor.kt | 15 +- .../repository/interactors/base/Reason.kt | 19 +- .../repository/interactors/base/Result.kt | 78 +- .../com/melih/repository/network/Api.kt | 12 +- .../com/melih/repository/network/ApiImpl.kt | 23 +- .../persistence/LaunchesDatabase.kt | 13 +- .../converters/LocationConverter.kt | 3 +- .../persistence/converters/RocketConverter.kt | 3 +- .../repository/persistence/dao/LaunchesDao.kt | 21 +- .../melih/repository/sources/NetworkSource.kt | 38 +- .../repository/sources/PersistenceSource.kt | 24 +- .../melih/repository/sources/SourceManager.kt | 55 - .../interactors/base/BaseInteractorTest.kt | 16 +- .../repository/interactors/base/ResultTest.kt | 16 +- .../repository/sources/NetworkSourceTest.kt | 56 +- .../sources/PersistanceSourceTest.kt | 53 +- .../repository/sources/SourceManagerTest.kt | 141 - scripts/default_dependencies.gradle | 55 + scripts/dependencies.gradle | 5 +- scripts/feature_module.gradle | 1 + scripts/flavors.gradle | 1 + scripts/sources.gradle | 1 + settings.gradle | 2 +- 103 files changed, 4367 insertions(+), 716 deletions(-) create mode 100644 .gradletasknamecache delete mode 100644 app/src/main/kotlin/com/melih/rocketscience/di/AppModule.kt create mode 100644 core/jacoco.exec create mode 100644 core/src/main/kotlin/com/melih/core/base/paging/BasePagingDataSource.kt create mode 100644 core/src/main/kotlin/com/melih/core/base/paging/BasePagingFactory.kt rename core/src/main/kotlin/com/melih/core/base/recycler/{BaseListAdapter.kt => BasePagingListAdapter.kt} (89%) create mode 100644 core/src/main/kotlin/com/melih/core/base/viewmodel/BasePagingViewModel.kt create mode 100644 core/src/test/kotlin/com/melih/core/paging/BasePagingDataSourceTest.kt create mode 100644 core/src/test/kotlin/com/melih/core/paging/BasePagingFactoryTest.kt create mode 100644 docs/dependency_hierarchy.png create mode 100644 docs/module_graph.png create mode 100644 features/detail/jacoco.exec create mode 100644 features/detail/src/main/kotlin/com/melih/detail/di/DetailModule.kt create mode 100644 features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailProvides.kt create mode 100644 features/detail/src/main/kotlin/com/melih/detail/di/scopes/DetailFragmentScope.kt create mode 100644 features/detail/src/main/kotlin/com/melih/detail/di/scopes/DetailScope.kt rename features/{list => launches}/.gitignore (100%) rename features/{list => launches}/build.gradle (68%) rename features/{list => launches}/consumer-rules.pro (100%) create mode 100644 features/launches/jacoco.exec rename features/{list => launches}/proguard-rules.pro (100%) rename features/{list => launches}/sampledata/launches.json (100%) rename features/{list => launches}/src/main/AndroidManifest.xml (100%) rename features/{list => launches}/src/main/kotlin/com/melih/list/di/LaunchesContributor.kt (79%) create mode 100644 features/launches/src/main/kotlin/com/melih/list/di/LaunchesFeatureModule.kt rename features/{list => launches}/src/main/kotlin/com/melih/list/di/modules/LaunchesBinds.kt (76%) create mode 100644 features/launches/src/main/kotlin/com/melih/list/di/modules/LaunchesProvides.kt create mode 100644 features/launches/src/main/kotlin/com/melih/list/di/scopes/LaunchesFragmentScope.kt create mode 100644 features/launches/src/main/kotlin/com/melih/list/di/scopes/LaunchesScope.kt rename features/{list => launches}/src/main/kotlin/com/melih/list/ui/LaunchesActivity.kt (89%) create mode 100644 features/launches/src/main/kotlin/com/melih/list/ui/LaunchesFragment.kt rename features/{list/src/main/kotlin/com/melih/list/ui => launches/src/main/kotlin/com/melih/list/ui/adapters}/LaunchesAdapter.kt (73%) create mode 100644 features/launches/src/main/kotlin/com/melih/list/ui/paging/LaunchesPagingSource.kt create mode 100644 features/launches/src/main/kotlin/com/melih/list/ui/paging/LaunchesPagingSourceFactory.kt create mode 100644 features/launches/src/main/kotlin/com/melih/list/ui/vm/LaunchesViewModel.kt rename features/{list => launches}/src/main/res/anim/item_enter.xml (100%) rename features/{list => launches}/src/main/res/anim/layout_item_enter.xml (100%) rename features/{list => launches}/src/main/res/layout/activity_launches.xml (100%) rename features/{list => launches}/src/main/res/layout/fragment_launches.xml (96%) rename features/{list => launches}/src/main/res/layout/row_launch.xml (98%) rename features/{list => launches}/src/main/res/menu/menu_rocket_list.xml (100%) rename features/{list => launches}/src/main/res/navigation/nav_launches.xml (100%) rename features/{list => launches}/src/main/res/values/integers.xml (100%) rename features/{list => launches}/src/main/res/values/strings.xml (100%) rename features/{list => launches}/src/test/kotlin/com/melih/list/BaseTestWithMainThread.kt (87%) delete mode 100644 features/list/src/main/kotlin/com/melih/list/di/modules/LaunchesProvides.kt delete mode 100644 features/list/src/main/kotlin/com/melih/list/ui/LaunchesFragment.kt delete mode 100644 features/list/src/main/kotlin/com/melih/list/ui/LaunchesViewModel.kt delete mode 100644 features/list/src/test/kotlin/com/melih/list/LaunchesViewModelTest.kt delete mode 100644 repository/src/main/kotlin/com/melih/repository/sources/SourceManager.kt delete mode 100644 repository/src/test/kotlin/com/melih/repository/sources/SourceManagerTest.kt diff --git a/.circleci/config.yml b/.circleci/config.yml index 11b3e90..f174524 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,6 +6,7 @@ jobs: - image: circleci/android:api-28 environment: JVM_OPTS: -Xmx3200m + CODECOV_TOKEN: "cd1376e1-2cfd-49a2-9f25-03ef69056b4d" steps: - checkout - restore_cache: @@ -32,7 +33,10 @@ jobs: # Tests - run: name: Tests - command: fastlane test_all + command: | + fastlane test_all + ./gradlew jacocoTestReport + bash <(curl -s https://codecov.io/bash) - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ path: build/reports/tests - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ diff --git a/.gitignore b/.gitignore index 84d7b28..d1bd31e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /build /captures .externalNativeBuild +/projectFilesBackup # Project reports /reports diff --git a/.gradletasknamecache b/.gradletasknamecache new file mode 100644 index 0000000..aeab705 --- /dev/null +++ b/.gradletasknamecache @@ -0,0 +1,3130 @@ +app:androidDependencies +core:androidDependencies +features:detail:androidDependencies +features:launches:androidDependencies +repository:androidDependencies +app:signingReport +core:signingReport +features:detail:signingReport +features:launches:signingReport +repository:signingReport +app:sourceSets +core:sourceSets +features:detail:sourceSets +features:launches:sourceSets +repository:sourceSets +app:assemble +core:assemble +features:detail:assemble +features:launches:assemble +repository:assemble +app:assembleAndroidTest +core:assembleAndroidTest +features:detail:assembleAndroidTest +features:launches:assembleAndroidTest +repository:assembleAndroidTest +app:build +core:build +features:detail:build +features:launches:build +repository:build +app:buildDependents +core:buildDependents +features:detail:buildDependents +features:launches:buildDependents +repository:buildDependents +app:buildNeeded +core:buildNeeded +features:detail:buildNeeded +features:launches:buildNeeded +repository:buildNeeded +app:bundle +app:clean +core:clean +features:detail:clean +features:launches:clean +repository:clean +app:cleanBuildCache +core:cleanBuildCache +features:detail:cleanBuildCache +features:launches:cleanBuildCache +repository:cleanBuildCache +app:compileDebugAndroidTestSources +core:compileDebugAndroidTestSources +features:detail:compileDebugAndroidTestSources +features:launches:compileDebugAndroidTestSources +repository:compileDebugAndroidTestSources +app:compileDebugSources +core:compileDebugSources +features:detail:compileDebugSources +features:launches:compileDebugSources +repository:compileDebugSources +app:compileDebugUnitTestSources +core:compileDebugUnitTestSources +features:detail:compileDebugUnitTestSources +features:launches:compileDebugUnitTestSources +repository:compileDebugUnitTestSources +app:compileDevSources +core:compileDevSources +features:detail:compileDevSources +features:launches:compileDevSources +repository:compileDevSources +app:compileDevUnitTestSources +core:compileDevUnitTestSources +features:detail:compileDevUnitTestSources +features:launches:compileDevUnitTestSources +repository:compileDevUnitTestSources +app:compileReleaseSources +core:compileReleaseSources +features:detail:compileReleaseSources +features:launches:compileReleaseSources +repository:compileReleaseSources +app:compileReleaseUnitTestSources +core:compileReleaseUnitTestSources +features:detail:compileReleaseUnitTestSources +features:launches:compileReleaseUnitTestSources +repository:compileReleaseUnitTestSources +core:extractDebugAnnotations +features:detail:extractDebugAnnotations +features:launches:extractDebugAnnotations +repository:extractDebugAnnotations +core:extractDevAnnotations +features:detail:extractDevAnnotations +features:launches:extractDevAnnotations +repository:extractDevAnnotations +core:extractReleaseAnnotations +features:detail:extractReleaseAnnotations +features:launches:extractReleaseAnnotations +repository:extractReleaseAnnotations +init +wrapper +app:lintFix +core:lintFix +features:detail:lintFix +features:launches:lintFix +repository:lintFix +dokka +app:dokka +core:dokka +features:detail:dokka +features:launches:dokka +repository:dokka +buildEnvironment +app:buildEnvironment +core:buildEnvironment +features:buildEnvironment +features:detail:buildEnvironment +features:launches:buildEnvironment +repository:buildEnvironment +components +app:components +core:components +features:components +features:detail:components +features:launches:components +repository:components +dependencies +app:dependencies +core:dependencies +features:dependencies +features:detail:dependencies +features:launches:dependencies +repository:dependencies +dependencyInsight +app:dependencyInsight +core:dependencyInsight +features:dependencyInsight +features:detail:dependencyInsight +features:launches:dependencyInsight +repository:dependencyInsight +dependentComponents +app:dependentComponents +core:dependentComponents +features:dependentComponents +features:detail:dependentComponents +features:launches:dependentComponents +repository:dependentComponents +help +app:help +core:help +features:help +features:detail:help +features:launches:help +repository:help +model +app:model +core:model +features:model +features:detail:model +features:launches:model +repository:model +projects +app:projects +core:projects +features:projects +features:detail:projects +features:launches:projects +repository:projects +properties +app:properties +core:properties +features:properties +features:detail:properties +features:launches:properties +repository:properties +tasks +app:tasks +core:tasks +features:tasks +features:detail:tasks +features:launches:tasks +repository:tasks +app:installDebug +app:installDebugAndroidTest +core:installDebugAndroidTest +features:detail:installDebugAndroidTest +features:launches:installDebugAndroidTest +repository:installDebugAndroidTest +app:installDev +app:uninstallAll +core:uninstallAll +features:detail:uninstallAll +features:launches:uninstallAll +repository:uninstallAll +app:uninstallDebug +app:uninstallDebugAndroidTest +core:uninstallDebugAndroidTest +features:detail:uninstallDebugAndroidTest +features:launches:uninstallDebugAndroidTest +repository:uninstallDebugAndroidTest +app:uninstallDev +app:uninstallRelease +app:jacocoTestReport +core:jacocoTestReport +features:detail:jacocoTestReport +features:launches:jacocoTestReport +repository:jacocoTestReport +app:jacocoTestReportDebug +core:jacocoTestReportDebug +features:detail:jacocoTestReportDebug +features:launches:jacocoTestReportDebug +repository:jacocoTestReportDebug +app:jacocoTestReportDev +core:jacocoTestReportDev +features:detail:jacocoTestReportDev +features:launches:jacocoTestReportDev +repository:jacocoTestReportDev +app:jacocoTestReportRelease +core:jacocoTestReportRelease +features:detail:jacocoTestReportRelease +features:launches:jacocoTestReportRelease +repository:jacocoTestReportRelease +app:check +core:check +features:detail:check +features:launches:check +repository:check +app:connectedAndroidTest +core:connectedAndroidTest +features:detail:connectedAndroidTest +features:launches:connectedAndroidTest +repository:connectedAndroidTest +app:connectedCheck +core:connectedCheck +features:detail:connectedCheck +features:launches:connectedCheck +repository:connectedCheck +app:connectedDebugAndroidTest +core:connectedDebugAndroidTest +features:detail:connectedDebugAndroidTest +features:launches:connectedDebugAndroidTest +repository:connectedDebugAndroidTest +app:createDebugCoverageReport +core:createDebugCoverageReport +features:detail:createDebugCoverageReport +features:launches:createDebugCoverageReport +repository:createDebugCoverageReport +app:createDevCoverageReport +core:createDevCoverageReport +features:detail:createDevCoverageReport +features:launches:createDevCoverageReport +repository:createDevCoverageReport +detekt +app:detekt +core:detekt +features:detail:detekt +features:launches:detekt +repository:detekt +detektBaseline +app:detektBaseline +core:detektBaseline +features:detail:detektBaseline +features:launches:detektBaseline +repository:detektBaseline +detektGenerateConfig +app:detektGenerateConfig +core:detektGenerateConfig +features:detail:detektGenerateConfig +features:launches:detektGenerateConfig +repository:detektGenerateConfig +detektIdeaFormat +app:detektIdeaFormat +core:detektIdeaFormat +features:detail:detektIdeaFormat +features:launches:detektIdeaFormat +repository:detektIdeaFormat +detektIdeaInspect +app:detektIdeaInspect +core:detektIdeaInspect +features:detail:detektIdeaInspect +features:launches:detektIdeaInspect +repository:detektIdeaInspect +app:deviceAndroidTest +core:deviceAndroidTest +features:detail:deviceAndroidTest +features:launches:deviceAndroidTest +repository:deviceAndroidTest +app:deviceCheck +core:deviceCheck +features:detail:deviceCheck +features:launches:deviceCheck +repository:deviceCheck +app:lint +core:lint +features:detail:lint +features:launches:lint +repository:lint +app:lintDebug +core:lintDebug +features:detail:lintDebug +features:launches:lintDebug +repository:lintDebug +app:lintDev +core:lintDev +features:detail:lintDev +features:launches:lintDev +repository:lintDev +app:lintRelease +core:lintRelease +features:detail:lintRelease +features:launches:lintRelease +repository:lintRelease +app:lintVitalRelease +app:test +core:test +features:detail:test +features:launches:test +repository:test +app:testDebugUnitTest +core:testDebugUnitTest +features:detail:testDebugUnitTest +features:launches:testDebugUnitTest +repository:testDebugUnitTest +app:testDevUnitTest +core:testDevUnitTest +features:detail:testDevUnitTest +features:launches:testDevUnitTest +repository:testDevUnitTest +app:testReleaseUnitTest +core:testReleaseUnitTest +features:detail:testReleaseUnitTest +features:launches:testReleaseUnitTest +repository:testReleaseUnitTest +app:assembleDebug +core:assembleDebug +features:detail:assembleDebug +features:launches:assembleDebug +repository:assembleDebug +app:assembleDebugAndroidTest +core:assembleDebugAndroidTest +features:detail:assembleDebugAndroidTest +features:launches:assembleDebugAndroidTest +repository:assembleDebugAndroidTest +app:assembleDebugUnitTest +core:assembleDebugUnitTest +features:detail:assembleDebugUnitTest +features:launches:assembleDebugUnitTest +repository:assembleDebugUnitTest +app:assembleDev +core:assembleDev +features:detail:assembleDev +features:launches:assembleDev +repository:assembleDev +app:assembleDevUnitTest +core:assembleDevUnitTest +features:detail:assembleDevUnitTest +features:launches:assembleDevUnitTest +repository:assembleDevUnitTest +app:assembleRelease +core:assembleRelease +features:detail:assembleRelease +features:launches:assembleRelease +repository:assembleRelease +app:assembleReleaseUnitTest +core:assembleReleaseUnitTest +features:detail:assembleReleaseUnitTest +features:launches:assembleReleaseUnitTest +repository:assembleReleaseUnitTest +app:buildDebugPreBundle +app:buildDevPreBundle +app:buildReleasePreBundle +app:bundleDebug +core:bundleDebugAar +features:detail:bundleDebugAar +features:launches:bundleDebugAar +repository:bundleDebugAar +app:bundleDebugAndroidTestClasses +app:bundleDebugAndroidTestResources +core:bundleDebugAndroidTestResources +features:detail:bundleDebugAndroidTestResources +features:launches:bundleDebugAndroidTestResources +repository:bundleDebugAndroidTestResources +app:bundleDebugClasses +app:bundleDebugResources +app:bundleDebugUnitTestClasses +app:bundleDev +core:bundleDevAar +features:detail:bundleDevAar +features:launches:bundleDevAar +repository:bundleDevAar +app:bundleDevClasses +app:bundleDevResources +app:bundleDevUnitTestClasses +core:bundleLibCompileDebug +features:detail:bundleLibCompileDebug +features:launches:bundleLibCompileDebug +repository:bundleLibCompileDebug +core:bundleLibCompileDebugAndroidTest +features:detail:bundleLibCompileDebugAndroidTest +features:launches:bundleLibCompileDebugAndroidTest +repository:bundleLibCompileDebugAndroidTest +core:bundleLibCompileDebugUnitTest +features:detail:bundleLibCompileDebugUnitTest +features:launches:bundleLibCompileDebugUnitTest +repository:bundleLibCompileDebugUnitTest +core:bundleLibCompileDev +features:detail:bundleLibCompileDev +features:launches:bundleLibCompileDev +repository:bundleLibCompileDev +core:bundleLibCompileDevUnitTest +features:detail:bundleLibCompileDevUnitTest +features:launches:bundleLibCompileDevUnitTest +repository:bundleLibCompileDevUnitTest +core:bundleLibCompileRelease +features:detail:bundleLibCompileRelease +features:launches:bundleLibCompileRelease +repository:bundleLibCompileRelease +core:bundleLibCompileReleaseUnitTest +features:detail:bundleLibCompileReleaseUnitTest +features:launches:bundleLibCompileReleaseUnitTest +repository:bundleLibCompileReleaseUnitTest +core:bundleLibResDebug +features:detail:bundleLibResDebug +features:launches:bundleLibResDebug +repository:bundleLibResDebug +core:bundleLibResDev +features:detail:bundleLibResDev +features:launches:bundleLibResDev +repository:bundleLibResDev +core:bundleLibResRelease +features:detail:bundleLibResRelease +features:launches:bundleLibResRelease +repository:bundleLibResRelease +core:bundleLibRuntimeDebug +features:detail:bundleLibRuntimeDebug +features:launches:bundleLibRuntimeDebug +repository:bundleLibRuntimeDebug +core:bundleLibRuntimeDev +features:detail:bundleLibRuntimeDev +features:launches:bundleLibRuntimeDev +repository:bundleLibRuntimeDev +core:bundleLibRuntimeRelease +features:detail:bundleLibRuntimeRelease +features:launches:bundleLibRuntimeRelease +repository:bundleLibRuntimeRelease +app:bundleRelease +core:bundleReleaseAar +features:detail:bundleReleaseAar +features:launches:bundleReleaseAar +repository:bundleReleaseAar +app:bundleReleaseClasses +app:bundleReleaseResources +app:bundleReleaseUnitTestClasses +app:checkDebugAndroidTestDuplicateClasses +core:checkDebugAndroidTestDuplicateClasses +features:detail:checkDebugAndroidTestDuplicateClasses +features:launches:checkDebugAndroidTestDuplicateClasses +repository:checkDebugAndroidTestDuplicateClasses +app:checkDebugDuplicateClasses +app:checkDebugManifest +core:checkDebugManifest +features:detail:checkDebugManifest +features:launches:checkDebugManifest +repository:checkDebugManifest +app:checkDevDuplicateClasses +app:checkDevManifest +core:checkDevManifest +features:detail:checkDevManifest +features:launches:checkDevManifest +repository:checkDevManifest +app:checkReleaseManifest +core:checkReleaseManifest +features:detail:checkReleaseManifest +features:launches:checkReleaseManifest +repository:checkReleaseManifest +clean +app:collectDebugDependencies +app:collectDevDependencies +app:collectReleaseDependencies +app:compileDebugAidl +core:compileDebugAidl +features:detail:compileDebugAidl +features:launches:compileDebugAidl +repository:compileDebugAidl +app:compileDebugAndroidTestAidl +core:compileDebugAndroidTestAidl +features:detail:compileDebugAndroidTestAidl +features:launches:compileDebugAndroidTestAidl +repository:compileDebugAndroidTestAidl +app:compileDebugAndroidTestJavaWithJavac +core:compileDebugAndroidTestJavaWithJavac +features:detail:compileDebugAndroidTestJavaWithJavac +features:launches:compileDebugAndroidTestJavaWithJavac +repository:compileDebugAndroidTestJavaWithJavac +app:compileDebugAndroidTestKotlin +core:compileDebugAndroidTestKotlin +features:detail:compileDebugAndroidTestKotlin +features:launches:compileDebugAndroidTestKotlin +repository:compileDebugAndroidTestKotlin +app:compileDebugAndroidTestRenderscript +core:compileDebugAndroidTestRenderscript +features:detail:compileDebugAndroidTestRenderscript +features:launches:compileDebugAndroidTestRenderscript +repository:compileDebugAndroidTestRenderscript +app:compileDebugAndroidTestShaders +core:compileDebugAndroidTestShaders +features:detail:compileDebugAndroidTestShaders +features:launches:compileDebugAndroidTestShaders +repository:compileDebugAndroidTestShaders +app:compileDebugJavaWithJavac +core:compileDebugJavaWithJavac +features:detail:compileDebugJavaWithJavac +features:launches:compileDebugJavaWithJavac +repository:compileDebugJavaWithJavac +app:compileDebugKotlin +core:compileDebugKotlin +features:detail:compileDebugKotlin +features:launches:compileDebugKotlin +repository:compileDebugKotlin +app:compileDebugRenderscript +core:compileDebugRenderscript +features:detail:compileDebugRenderscript +features:launches:compileDebugRenderscript +repository:compileDebugRenderscript +app:compileDebugShaders +core:compileDebugShaders +features:detail:compileDebugShaders +features:launches:compileDebugShaders +repository:compileDebugShaders +app:compileDebugUnitTestJavaWithJavac +core:compileDebugUnitTestJavaWithJavac +features:detail:compileDebugUnitTestJavaWithJavac +features:launches:compileDebugUnitTestJavaWithJavac +repository:compileDebugUnitTestJavaWithJavac +app:compileDebugUnitTestKotlin +core:compileDebugUnitTestKotlin +features:detail:compileDebugUnitTestKotlin +features:launches:compileDebugUnitTestKotlin +repository:compileDebugUnitTestKotlin +app:compileDevAidl +core:compileDevAidl +features:detail:compileDevAidl +features:launches:compileDevAidl +repository:compileDevAidl +app:compileDevJavaWithJavac +core:compileDevJavaWithJavac +features:detail:compileDevJavaWithJavac +features:launches:compileDevJavaWithJavac +repository:compileDevJavaWithJavac +app:compileDevKotlin +core:compileDevKotlin +features:detail:compileDevKotlin +features:launches:compileDevKotlin +repository:compileDevKotlin +app:compileDevRenderscript +core:compileDevRenderscript +features:detail:compileDevRenderscript +features:launches:compileDevRenderscript +repository:compileDevRenderscript +app:compileDevShaders +core:compileDevShaders +features:detail:compileDevShaders +features:launches:compileDevShaders +repository:compileDevShaders +app:compileDevUnitTestJavaWithJavac +core:compileDevUnitTestJavaWithJavac +features:detail:compileDevUnitTestJavaWithJavac +features:launches:compileDevUnitTestJavaWithJavac +repository:compileDevUnitTestJavaWithJavac +app:compileDevUnitTestKotlin +core:compileDevUnitTestKotlin +features:detail:compileDevUnitTestKotlin +features:launches:compileDevUnitTestKotlin +repository:compileDevUnitTestKotlin +app:compileLint +core:compileLint +features:detail:compileLint +features:launches:compileLint +repository:compileLint +app:compileReleaseAidl +core:compileReleaseAidl +features:detail:compileReleaseAidl +features:launches:compileReleaseAidl +repository:compileReleaseAidl +app:compileReleaseJavaWithJavac +core:compileReleaseJavaWithJavac +features:detail:compileReleaseJavaWithJavac +features:launches:compileReleaseJavaWithJavac +repository:compileReleaseJavaWithJavac +app:compileReleaseKotlin +core:compileReleaseKotlin +features:detail:compileReleaseKotlin +features:launches:compileReleaseKotlin +repository:compileReleaseKotlin +app:compileReleaseRenderscript +core:compileReleaseRenderscript +features:detail:compileReleaseRenderscript +features:launches:compileReleaseRenderscript +repository:compileReleaseRenderscript +app:compileReleaseShaders +core:compileReleaseShaders +features:detail:compileReleaseShaders +features:launches:compileReleaseShaders +repository:compileReleaseShaders +app:compileReleaseUnitTestJavaWithJavac +core:compileReleaseUnitTestJavaWithJavac +features:detail:compileReleaseUnitTestJavaWithJavac +features:launches:compileReleaseUnitTestJavaWithJavac +repository:compileReleaseUnitTestJavaWithJavac +app:compileReleaseUnitTestKotlin +core:compileReleaseUnitTestKotlin +features:detail:compileReleaseUnitTestKotlin +features:launches:compileReleaseUnitTestKotlin +repository:compileReleaseUnitTestKotlin +app:configureDebugDependencies +app:configureDevDependencies +app:configureReleaseDependencies +app:consumeConfigAttr +core:consumeConfigAttr +features:detail:consumeConfigAttr +features:launches:consumeConfigAttr +repository:consumeConfigAttr +app:createDebugAndroidTestCoverageReport +core:createDebugAndroidTestCoverageReport +features:detail:createDebugAndroidTestCoverageReport +features:launches:createDebugAndroidTestCoverageReport +repository:createDebugAndroidTestCoverageReport +app:createDebugCompatibleScreenManifests +app:createDevCompatibleScreenManifests +core:createFullJarDebug +features:detail:createFullJarDebug +features:launches:createFullJarDebug +repository:createFullJarDebug +core:createFullJarDev +features:detail:createFullJarDev +features:launches:createFullJarDev +repository:createFullJarDev +core:createFullJarRelease +features:detail:createFullJarRelease +features:launches:createFullJarRelease +repository:createFullJarRelease +app:createMockableJar +core:createMockableJar +features:detail:createMockableJar +features:launches:createMockableJar +repository:createMockableJar +app:createReleaseCompatibleScreenManifests +app:dataBindingExportBuildInfoDebug +core:dataBindingExportBuildInfoDebug +features:detail:dataBindingExportBuildInfoDebug +features:launches:dataBindingExportBuildInfoDebug +core:dataBindingExportBuildInfoDebugAndroidTest +features:detail:dataBindingExportBuildInfoDebugAndroidTest +features:launches:dataBindingExportBuildInfoDebugAndroidTest +app:dataBindingExportBuildInfoDev +core:dataBindingExportBuildInfoDev +features:detail:dataBindingExportBuildInfoDev +features:launches:dataBindingExportBuildInfoDev +app:dataBindingExportBuildInfoRelease +core:dataBindingExportBuildInfoRelease +features:detail:dataBindingExportBuildInfoRelease +features:launches:dataBindingExportBuildInfoRelease +app:dataBindingExportFeaturePackageIdsDebug +app:dataBindingExportFeaturePackageIdsDev +app:dataBindingExportFeaturePackageIdsRelease +app:dataBindingGenBaseClassesDebug +core:dataBindingGenBaseClassesDebug +features:detail:dataBindingGenBaseClassesDebug +features:launches:dataBindingGenBaseClassesDebug +core:dataBindingGenBaseClassesDebugAndroidTest +features:detail:dataBindingGenBaseClassesDebugAndroidTest +features:launches:dataBindingGenBaseClassesDebugAndroidTest +app:dataBindingGenBaseClassesDev +core:dataBindingGenBaseClassesDev +features:detail:dataBindingGenBaseClassesDev +features:launches:dataBindingGenBaseClassesDev +app:dataBindingGenBaseClassesRelease +core:dataBindingGenBaseClassesRelease +features:detail:dataBindingGenBaseClassesRelease +features:launches:dataBindingGenBaseClassesRelease +app:dataBindingMergeDependencyArtifactsDebug +core:dataBindingMergeDependencyArtifactsDebug +features:detail:dataBindingMergeDependencyArtifactsDebug +features:launches:dataBindingMergeDependencyArtifactsDebug +core:dataBindingMergeDependencyArtifactsDebugAndroidTest +features:detail:dataBindingMergeDependencyArtifactsDebugAndroidTest +features:launches:dataBindingMergeDependencyArtifactsDebugAndroidTest +app:dataBindingMergeDependencyArtifactsDev +core:dataBindingMergeDependencyArtifactsDev +features:detail:dataBindingMergeDependencyArtifactsDev +features:launches:dataBindingMergeDependencyArtifactsDev +app:dataBindingMergeDependencyArtifactsRelease +core:dataBindingMergeDependencyArtifactsRelease +features:detail:dataBindingMergeDependencyArtifactsRelease +features:launches:dataBindingMergeDependencyArtifactsRelease +app:dataBindingMergeGenClassesDebug +core:dataBindingMergeGenClassesDebug +features:detail:dataBindingMergeGenClassesDebug +features:launches:dataBindingMergeGenClassesDebug +core:dataBindingMergeGenClassesDebugAndroidTest +features:detail:dataBindingMergeGenClassesDebugAndroidTest +features:launches:dataBindingMergeGenClassesDebugAndroidTest +app:dataBindingMergeGenClassesDev +core:dataBindingMergeGenClassesDev +features:detail:dataBindingMergeGenClassesDev +features:launches:dataBindingMergeGenClassesDev +app:dataBindingMergeGenClassesRelease +core:dataBindingMergeGenClassesRelease +features:detail:dataBindingMergeGenClassesRelease +features:launches:dataBindingMergeGenClassesRelease +app:desugarDebugAndroidTestFileDependencies +core:desugarDebugAndroidTestFileDependencies +features:detail:desugarDebugAndroidTestFileDependencies +features:launches:desugarDebugAndroidTestFileDependencies +repository:desugarDebugAndroidTestFileDependencies +app:desugarDebugFileDependencies +app:desugarDevFileDependencies +core:dexDebug +features:detail:dexDebug +features:launches:dexDebug +repository:dexDebug +core:dexDev +features:detail:dexDev +features:launches:dexDev +repository:dexDev +core:dexRelease +features:detail:dexRelease +features:launches:dexRelease +repository:dexRelease +app:dummydebugUnitTest +core:dummydebugUnitTest +features:detail:dummydebugUnitTest +features:launches:dummydebugUnitTest +repository:dummydebugUnitTest +app:dummydevUnitTest +core:dummydevUnitTest +features:detail:dummydevUnitTest +features:launches:dummydevUnitTest +repository:dummydevUnitTest +app:dummyreleaseUnitTest +core:dummyreleaseUnitTest +features:detail:dummyreleaseUnitTest +features:launches:dummyreleaseUnitTest +repository:dummyreleaseUnitTest +app:extractApksForDebug +app:extractApksForDev +app:extractApksForRelease +app:extractProguardFiles +core:extractProguardFiles +features:detail:extractProguardFiles +features:launches:extractProguardFiles +repository:extractProguardFiles +app:generateDebugAndroidTestAssets +core:generateDebugAndroidTestAssets +features:detail:generateDebugAndroidTestAssets +features:launches:generateDebugAndroidTestAssets +repository:generateDebugAndroidTestAssets +app:generateDebugAndroidTestBuildConfig +core:generateDebugAndroidTestBuildConfig +features:detail:generateDebugAndroidTestBuildConfig +features:launches:generateDebugAndroidTestBuildConfig +repository:generateDebugAndroidTestBuildConfig +app:generateDebugAndroidTestResources +core:generateDebugAndroidTestResources +features:detail:generateDebugAndroidTestResources +features:launches:generateDebugAndroidTestResources +repository:generateDebugAndroidTestResources +app:generateDebugAndroidTestResValues +core:generateDebugAndroidTestResValues +features:detail:generateDebugAndroidTestResValues +features:launches:generateDebugAndroidTestResValues +repository:generateDebugAndroidTestResValues +app:generateDebugAndroidTestSources +core:generateDebugAndroidTestSources +features:detail:generateDebugAndroidTestSources +features:launches:generateDebugAndroidTestSources +repository:generateDebugAndroidTestSources +app:generateDebugAssets +core:generateDebugAssets +features:detail:generateDebugAssets +features:launches:generateDebugAssets +repository:generateDebugAssets +app:generateDebugBuildConfig +core:generateDebugBuildConfig +features:detail:generateDebugBuildConfig +features:launches:generateDebugBuildConfig +repository:generateDebugBuildConfig +app:generateDebugFeatureMetadata +app:generateDebugFeatureTransitiveDeps +app:generateDebugResources +core:generateDebugResources +features:detail:generateDebugResources +features:launches:generateDebugResources +repository:generateDebugResources +app:generateDebugResValues +core:generateDebugResValues +features:detail:generateDebugResValues +features:launches:generateDebugResValues +repository:generateDebugResValues +core:generateDebugRFile +features:detail:generateDebugRFile +features:launches:generateDebugRFile +repository:generateDebugRFile +app:generateDebugSources +core:generateDebugSources +features:detail:generateDebugSources +features:launches:generateDebugSources +repository:generateDebugSources +app:generateDebugUnitTestAssets +core:generateDebugUnitTestAssets +features:detail:generateDebugUnitTestAssets +features:launches:generateDebugUnitTestAssets +repository:generateDebugUnitTestAssets +app:generateDebugUnitTestResources +core:generateDebugUnitTestResources +features:detail:generateDebugUnitTestResources +features:launches:generateDebugUnitTestResources +repository:generateDebugUnitTestResources +app:generateDebugUnitTestSources +core:generateDebugUnitTestSources +features:detail:generateDebugUnitTestSources +features:launches:generateDebugUnitTestSources +repository:generateDebugUnitTestSources +app:generateDevAssets +core:generateDevAssets +features:detail:generateDevAssets +features:launches:generateDevAssets +repository:generateDevAssets +app:generateDevBuildConfig +core:generateDevBuildConfig +features:detail:generateDevBuildConfig +features:launches:generateDevBuildConfig +repository:generateDevBuildConfig +app:generateDevFeatureMetadata +app:generateDevFeatureTransitiveDeps +app:generateDevResources +core:generateDevResources +features:detail:generateDevResources +features:launches:generateDevResources +repository:generateDevResources +app:generateDevResValues +core:generateDevResValues +features:detail:generateDevResValues +features:launches:generateDevResValues +repository:generateDevResValues +core:generateDevRFile +features:detail:generateDevRFile +features:launches:generateDevRFile +repository:generateDevRFile +app:generateDevSources +core:generateDevSources +features:detail:generateDevSources +features:launches:generateDevSources +repository:generateDevSources +app:generateDevUnitTestAssets +core:generateDevUnitTestAssets +features:detail:generateDevUnitTestAssets +features:launches:generateDevUnitTestAssets +repository:generateDevUnitTestAssets +app:generateDevUnitTestResources +core:generateDevUnitTestResources +features:detail:generateDevUnitTestResources +features:launches:generateDevUnitTestResources +repository:generateDevUnitTestResources +app:generateDevUnitTestSources +core:generateDevUnitTestSources +features:detail:generateDevUnitTestSources +features:launches:generateDevUnitTestSources +repository:generateDevUnitTestSources +app:generateReleaseAssets +core:generateReleaseAssets +features:detail:generateReleaseAssets +features:launches:generateReleaseAssets +repository:generateReleaseAssets +app:generateReleaseBuildConfig +core:generateReleaseBuildConfig +features:detail:generateReleaseBuildConfig +features:launches:generateReleaseBuildConfig +repository:generateReleaseBuildConfig +app:generateReleaseFeatureMetadata +app:generateReleaseFeatureTransitiveDeps +core:generateReleaseLibraryProguardRules +features:detail:generateReleaseLibraryProguardRules +features:launches:generateReleaseLibraryProguardRules +repository:generateReleaseLibraryProguardRules +app:generateReleaseResources +core:generateReleaseResources +features:detail:generateReleaseResources +features:launches:generateReleaseResources +repository:generateReleaseResources +app:generateReleaseResValues +core:generateReleaseResValues +features:detail:generateReleaseResValues +features:launches:generateReleaseResValues +repository:generateReleaseResValues +core:generateReleaseRFile +features:detail:generateReleaseRFile +features:launches:generateReleaseRFile +repository:generateReleaseRFile +app:generateReleaseSources +core:generateReleaseSources +features:detail:generateReleaseSources +features:launches:generateReleaseSources +repository:generateReleaseSources +app:generateReleaseUnitTestAssets +core:generateReleaseUnitTestAssets +features:detail:generateReleaseUnitTestAssets +features:launches:generateReleaseUnitTestAssets +repository:generateReleaseUnitTestAssets +app:generateReleaseUnitTestResources +core:generateReleaseUnitTestResources +features:detail:generateReleaseUnitTestResources +features:launches:generateReleaseUnitTestResources +repository:generateReleaseUnitTestResources +app:generateReleaseUnitTestSources +core:generateReleaseUnitTestSources +features:detail:generateReleaseUnitTestSources +features:launches:generateReleaseUnitTestSources +repository:generateReleaseUnitTestSources +features:detail:generateSafeArgsDebug +features:detail:generateSafeArgsDev +features:detail:generateSafeArgsRelease +app:jacocoDebug +core:jacocoDebug +features:detail:jacocoDebug +features:launches:jacocoDebug +repository:jacocoDebug +app:jacocoDev +core:jacocoDev +features:detail:jacocoDev +features:launches:jacocoDev +repository:jacocoDev +app:javaPreCompileDebug +core:javaPreCompileDebug +features:detail:javaPreCompileDebug +features:launches:javaPreCompileDebug +repository:javaPreCompileDebug +app:javaPreCompileDebugAndroidTest +core:javaPreCompileDebugAndroidTest +features:detail:javaPreCompileDebugAndroidTest +features:launches:javaPreCompileDebugAndroidTest +repository:javaPreCompileDebugAndroidTest +app:javaPreCompileDebugUnitTest +core:javaPreCompileDebugUnitTest +features:detail:javaPreCompileDebugUnitTest +features:launches:javaPreCompileDebugUnitTest +repository:javaPreCompileDebugUnitTest +app:javaPreCompileDev +core:javaPreCompileDev +features:detail:javaPreCompileDev +features:launches:javaPreCompileDev +repository:javaPreCompileDev +app:javaPreCompileDevUnitTest +core:javaPreCompileDevUnitTest +features:detail:javaPreCompileDevUnitTest +features:launches:javaPreCompileDevUnitTest +repository:javaPreCompileDevUnitTest +app:javaPreCompileRelease +core:javaPreCompileRelease +features:detail:javaPreCompileRelease +features:launches:javaPreCompileRelease +repository:javaPreCompileRelease +app:javaPreCompileReleaseUnitTest +core:javaPreCompileReleaseUnitTest +features:detail:javaPreCompileReleaseUnitTest +features:launches:javaPreCompileReleaseUnitTest +repository:javaPreCompileReleaseUnitTest +app:kaptDebugAndroidTestKotlin +core:kaptDebugAndroidTestKotlin +features:detail:kaptDebugAndroidTestKotlin +features:launches:kaptDebugAndroidTestKotlin +repository:kaptDebugAndroidTestKotlin +app:kaptDebugKotlin +core:kaptDebugKotlin +features:detail:kaptDebugKotlin +features:launches:kaptDebugKotlin +repository:kaptDebugKotlin +app:kaptDebugUnitTestKotlin +core:kaptDebugUnitTestKotlin +features:detail:kaptDebugUnitTestKotlin +features:launches:kaptDebugUnitTestKotlin +repository:kaptDebugUnitTestKotlin +app:kaptDevKotlin +core:kaptDevKotlin +features:detail:kaptDevKotlin +features:launches:kaptDevKotlin +repository:kaptDevKotlin +app:kaptDevUnitTestKotlin +core:kaptDevUnitTestKotlin +features:detail:kaptDevUnitTestKotlin +features:launches:kaptDevUnitTestKotlin +repository:kaptDevUnitTestKotlin +app:kaptGenerateStubsDebugAndroidTestKotlin +core:kaptGenerateStubsDebugAndroidTestKotlin +features:detail:kaptGenerateStubsDebugAndroidTestKotlin +features:launches:kaptGenerateStubsDebugAndroidTestKotlin +repository:kaptGenerateStubsDebugAndroidTestKotlin +app:kaptGenerateStubsDebugKotlin +core:kaptGenerateStubsDebugKotlin +features:detail:kaptGenerateStubsDebugKotlin +features:launches:kaptGenerateStubsDebugKotlin +repository:kaptGenerateStubsDebugKotlin +app:kaptGenerateStubsDebugUnitTestKotlin +core:kaptGenerateStubsDebugUnitTestKotlin +features:detail:kaptGenerateStubsDebugUnitTestKotlin +features:launches:kaptGenerateStubsDebugUnitTestKotlin +repository:kaptGenerateStubsDebugUnitTestKotlin +app:kaptGenerateStubsDevKotlin +core:kaptGenerateStubsDevKotlin +features:detail:kaptGenerateStubsDevKotlin +features:launches:kaptGenerateStubsDevKotlin +repository:kaptGenerateStubsDevKotlin +app:kaptGenerateStubsDevUnitTestKotlin +core:kaptGenerateStubsDevUnitTestKotlin +features:detail:kaptGenerateStubsDevUnitTestKotlin +features:launches:kaptGenerateStubsDevUnitTestKotlin +repository:kaptGenerateStubsDevUnitTestKotlin +app:kaptGenerateStubsReleaseKotlin +core:kaptGenerateStubsReleaseKotlin +features:detail:kaptGenerateStubsReleaseKotlin +features:launches:kaptGenerateStubsReleaseKotlin +repository:kaptGenerateStubsReleaseKotlin +app:kaptGenerateStubsReleaseUnitTestKotlin +core:kaptGenerateStubsReleaseUnitTestKotlin +features:detail:kaptGenerateStubsReleaseUnitTestKotlin +features:launches:kaptGenerateStubsReleaseUnitTestKotlin +repository:kaptGenerateStubsReleaseUnitTestKotlin +app:kaptReleaseKotlin +core:kaptReleaseKotlin +features:detail:kaptReleaseKotlin +features:launches:kaptReleaseKotlin +repository:kaptReleaseKotlin +app:kaptReleaseUnitTestKotlin +core:kaptReleaseUnitTestKotlin +features:detail:kaptReleaseUnitTestKotlin +features:launches:kaptReleaseUnitTestKotlin +repository:kaptReleaseUnitTestKotlin +app:mainApkListPersistenceDebug +app:mainApkListPersistenceDebugAndroidTest +core:mainApkListPersistenceDebugAndroidTest +features:detail:mainApkListPersistenceDebugAndroidTest +features:launches:mainApkListPersistenceDebugAndroidTest +repository:mainApkListPersistenceDebugAndroidTest +app:mainApkListPersistenceDev +app:mainApkListPersistenceRelease +app:makeApkFromBundleForDebug +app:makeApkFromBundleForDev +app:makeApkFromBundleForRelease +app:mergeDebugAndroidTestAssets +core:mergeDebugAndroidTestAssets +features:detail:mergeDebugAndroidTestAssets +features:launches:mergeDebugAndroidTestAssets +repository:mergeDebugAndroidTestAssets +app:mergeDebugAndroidTestGeneratedProguardFiles +core:mergeDebugAndroidTestGeneratedProguardFiles +features:detail:mergeDebugAndroidTestGeneratedProguardFiles +features:launches:mergeDebugAndroidTestGeneratedProguardFiles +repository:mergeDebugAndroidTestGeneratedProguardFiles +app:mergeDebugAndroidTestJavaResource +core:mergeDebugAndroidTestJavaResource +features:detail:mergeDebugAndroidTestJavaResource +features:launches:mergeDebugAndroidTestJavaResource +repository:mergeDebugAndroidTestJavaResource +app:mergeDebugAndroidTestJniLibFolders +core:mergeDebugAndroidTestJniLibFolders +features:detail:mergeDebugAndroidTestJniLibFolders +features:launches:mergeDebugAndroidTestJniLibFolders +repository:mergeDebugAndroidTestJniLibFolders +app:mergeDebugAndroidTestNativeLibs +core:mergeDebugAndroidTestNativeLibs +features:detail:mergeDebugAndroidTestNativeLibs +features:launches:mergeDebugAndroidTestNativeLibs +repository:mergeDebugAndroidTestNativeLibs +app:mergeDebugAndroidTestResources +core:mergeDebugAndroidTestResources +features:detail:mergeDebugAndroidTestResources +features:launches:mergeDebugAndroidTestResources +repository:mergeDebugAndroidTestResources +app:mergeDebugAndroidTestShaders +core:mergeDebugAndroidTestShaders +features:detail:mergeDebugAndroidTestShaders +features:launches:mergeDebugAndroidTestShaders +repository:mergeDebugAndroidTestShaders +app:mergeDebugAssets +core:mergeDebugAssets +features:detail:mergeDebugAssets +features:launches:mergeDebugAssets +repository:mergeDebugAssets +core:mergeDebugConsumerProguardFiles +features:detail:mergeDebugConsumerProguardFiles +features:launches:mergeDebugConsumerProguardFiles +repository:mergeDebugConsumerProguardFiles +app:mergeDebugGeneratedProguardFiles +core:mergeDebugGeneratedProguardFiles +features:detail:mergeDebugGeneratedProguardFiles +features:launches:mergeDebugGeneratedProguardFiles +repository:mergeDebugGeneratedProguardFiles +app:mergeDebugJavaResource +core:mergeDebugJavaResource +features:detail:mergeDebugJavaResource +features:launches:mergeDebugJavaResource +repository:mergeDebugJavaResource +app:mergeDebugJniLibFolders +core:mergeDebugJniLibFolders +features:detail:mergeDebugJniLibFolders +features:launches:mergeDebugJniLibFolders +repository:mergeDebugJniLibFolders +app:mergeDebugNativeLibs +core:mergeDebugNativeLibs +features:detail:mergeDebugNativeLibs +features:launches:mergeDebugNativeLibs +repository:mergeDebugNativeLibs +app:mergeDebugResources +core:mergeDebugResources +features:detail:mergeDebugResources +features:launches:mergeDebugResources +repository:mergeDebugResources +app:mergeDebugShaders +core:mergeDebugShaders +features:detail:mergeDebugShaders +features:launches:mergeDebugShaders +repository:mergeDebugShaders +app:mergeDevAssets +core:mergeDevAssets +features:detail:mergeDevAssets +features:launches:mergeDevAssets +repository:mergeDevAssets +core:mergeDevConsumerProguardFiles +features:detail:mergeDevConsumerProguardFiles +features:launches:mergeDevConsumerProguardFiles +repository:mergeDevConsumerProguardFiles +app:mergeDevGeneratedProguardFiles +core:mergeDevGeneratedProguardFiles +features:detail:mergeDevGeneratedProguardFiles +features:launches:mergeDevGeneratedProguardFiles +repository:mergeDevGeneratedProguardFiles +app:mergeDevJavaResource +core:mergeDevJavaResource +features:detail:mergeDevJavaResource +features:launches:mergeDevJavaResource +repository:mergeDevJavaResource +app:mergeDevJniLibFolders +core:mergeDevJniLibFolders +features:detail:mergeDevJniLibFolders +features:launches:mergeDevJniLibFolders +repository:mergeDevJniLibFolders +app:mergeDevNativeLibs +core:mergeDevNativeLibs +features:detail:mergeDevNativeLibs +features:launches:mergeDevNativeLibs +repository:mergeDevNativeLibs +app:mergeDevResources +core:mergeDevResources +features:detail:mergeDevResources +features:launches:mergeDevResources +repository:mergeDevResources +app:mergeDevShaders +core:mergeDevShaders +features:detail:mergeDevShaders +features:launches:mergeDevShaders +repository:mergeDevShaders +app:mergeExtDexDebug +app:mergeExtDexDebugAndroidTest +core:mergeExtDexDebugAndroidTest +features:detail:mergeExtDexDebugAndroidTest +features:launches:mergeExtDexDebugAndroidTest +repository:mergeExtDexDebugAndroidTest +app:mergeExtDexDev +app:mergeLibDexDebug +app:mergeLibDexDebugAndroidTest +core:mergeLibDexDebugAndroidTest +features:detail:mergeLibDexDebugAndroidTest +features:launches:mergeLibDexDebugAndroidTest +repository:mergeLibDexDebugAndroidTest +app:mergeLibDexDev +app:mergeProjectDexDebug +app:mergeProjectDexDebugAndroidTest +core:mergeProjectDexDebugAndroidTest +features:detail:mergeProjectDexDebugAndroidTest +features:launches:mergeProjectDexDebugAndroidTest +repository:mergeProjectDexDebugAndroidTest +app:mergeProjectDexDev +app:mergeReleaseAssets +core:mergeReleaseAssets +features:detail:mergeReleaseAssets +features:launches:mergeReleaseAssets +repository:mergeReleaseAssets +core:mergeReleaseConsumerProguardFiles +features:detail:mergeReleaseConsumerProguardFiles +features:launches:mergeReleaseConsumerProguardFiles +repository:mergeReleaseConsumerProguardFiles +app:mergeReleaseGeneratedProguardFiles +core:mergeReleaseGeneratedProguardFiles +features:detail:mergeReleaseGeneratedProguardFiles +features:launches:mergeReleaseGeneratedProguardFiles +repository:mergeReleaseGeneratedProguardFiles +app:mergeReleaseJavaResource +core:mergeReleaseJavaResource +features:detail:mergeReleaseJavaResource +features:launches:mergeReleaseJavaResource +repository:mergeReleaseJavaResource +app:mergeReleaseJniLibFolders +core:mergeReleaseJniLibFolders +features:detail:mergeReleaseJniLibFolders +features:launches:mergeReleaseJniLibFolders +repository:mergeReleaseJniLibFolders +app:mergeReleaseNativeLibs +core:mergeReleaseNativeLibs +features:detail:mergeReleaseNativeLibs +features:launches:mergeReleaseNativeLibs +repository:mergeReleaseNativeLibs +app:mergeReleaseResources +core:mergeReleaseResources +features:detail:mergeReleaseResources +features:launches:mergeReleaseResources +repository:mergeReleaseResources +app:mergeReleaseShaders +core:mergeReleaseShaders +features:detail:mergeReleaseShaders +features:launches:mergeReleaseShaders +repository:mergeReleaseShaders +app:packageDebug +app:packageDebugAndroidTest +core:packageDebugAndroidTest +features:detail:packageDebugAndroidTest +features:launches:packageDebugAndroidTest +repository:packageDebugAndroidTest +core:packageDebugAssets +features:detail:packageDebugAssets +features:launches:packageDebugAssets +repository:packageDebugAssets +app:packageDebugBundle +core:packageDebugRenderscript +features:detail:packageDebugRenderscript +features:launches:packageDebugRenderscript +repository:packageDebugRenderscript +core:packageDebugResources +features:detail:packageDebugResources +features:launches:packageDebugResources +repository:packageDebugResources +app:packageDebugUniversalApk +app:packageDev +core:packageDevAssets +features:detail:packageDevAssets +features:launches:packageDevAssets +repository:packageDevAssets +app:packageDevBundle +core:packageDevRenderscript +features:detail:packageDevRenderscript +features:launches:packageDevRenderscript +repository:packageDevRenderscript +core:packageDevResources +features:detail:packageDevResources +features:launches:packageDevResources +repository:packageDevResources +app:packageDevUniversalApk +app:packageRelease +core:packageReleaseAssets +features:detail:packageReleaseAssets +features:launches:packageReleaseAssets +repository:packageReleaseAssets +app:packageReleaseBundle +core:packageReleaseRenderscript +features:detail:packageReleaseRenderscript +features:launches:packageReleaseRenderscript +repository:packageReleaseRenderscript +core:packageReleaseResources +features:detail:packageReleaseResources +features:launches:packageReleaseResources +repository:packageReleaseResources +app:packageReleaseUniversalApk +core:parseDebugLibraryResources +features:detail:parseDebugLibraryResources +features:launches:parseDebugLibraryResources +repository:parseDebugLibraryResources +core:parseDevLibraryResources +features:detail:parseDevLibraryResources +features:launches:parseDevLibraryResources +repository:parseDevLibraryResources +core:parseReleaseLibraryResources +features:detail:parseReleaseLibraryResources +features:launches:parseReleaseLibraryResources +repository:parseReleaseLibraryResources +app:preBuild +core:preBuild +features:detail:preBuild +features:launches:preBuild +repository:preBuild +app:preDebugAndroidTestBuild +core:preDebugAndroidTestBuild +features:detail:preDebugAndroidTestBuild +features:launches:preDebugAndroidTestBuild +repository:preDebugAndroidTestBuild +app:preDebugBuild +core:preDebugBuild +features:detail:preDebugBuild +features:launches:preDebugBuild +repository:preDebugBuild +app:preDebugUnitTestBuild +core:preDebugUnitTestBuild +features:detail:preDebugUnitTestBuild +features:launches:preDebugUnitTestBuild +repository:preDebugUnitTestBuild +app:preDevBuild +core:preDevBuild +features:detail:preDevBuild +features:launches:preDevBuild +repository:preDevBuild +app:preDevUnitTestBuild +core:preDevUnitTestBuild +features:detail:preDevUnitTestBuild +features:launches:preDevUnitTestBuild +repository:preDevUnitTestBuild +prepareKotlinBuildScriptModel +app:prepareKotlinBuildScriptModel +core:prepareKotlinBuildScriptModel +features:prepareKotlinBuildScriptModel +features:detail:prepareKotlinBuildScriptModel +features:launches:prepareKotlinBuildScriptModel +repository:prepareKotlinBuildScriptModel +app:prepareLintJar +core:prepareLintJar +features:detail:prepareLintJar +features:launches:prepareLintJar +repository:prepareLintJar +app:prepareLintJarForPublish +core:prepareLintJarForPublish +features:detail:prepareLintJarForPublish +features:launches:prepareLintJarForPublish +repository:prepareLintJarForPublish +app:preReleaseBuild +core:preReleaseBuild +features:detail:preReleaseBuild +features:launches:preReleaseBuild +repository:preReleaseBuild +app:preReleaseUnitTestBuild +core:preReleaseUnitTestBuild +features:detail:preReleaseUnitTestBuild +features:launches:preReleaseUnitTestBuild +repository:preReleaseUnitTestBuild +app:processDebugAndroidTestJavaRes +core:processDebugAndroidTestJavaRes +features:detail:processDebugAndroidTestJavaRes +features:launches:processDebugAndroidTestJavaRes +repository:processDebugAndroidTestJavaRes +app:processDebugAndroidTestManifest +core:processDebugAndroidTestManifest +features:detail:processDebugAndroidTestManifest +features:launches:processDebugAndroidTestManifest +repository:processDebugAndroidTestManifest +app:processDebugAndroidTestResources +core:processDebugAndroidTestResources +features:detail:processDebugAndroidTestResources +features:launches:processDebugAndroidTestResources +repository:processDebugAndroidTestResources +app:processDebugJavaRes +core:processDebugJavaRes +features:detail:processDebugJavaRes +features:launches:processDebugJavaRes +repository:processDebugJavaRes +app:processDebugManifest +core:processDebugManifest +features:detail:processDebugManifest +features:launches:processDebugManifest +repository:processDebugManifest +app:processDebugResources +app:processDebugUnitTestJavaRes +core:processDebugUnitTestJavaRes +features:detail:processDebugUnitTestJavaRes +features:launches:processDebugUnitTestJavaRes +repository:processDebugUnitTestJavaRes +app:processDevJavaRes +core:processDevJavaRes +features:detail:processDevJavaRes +features:launches:processDevJavaRes +repository:processDevJavaRes +app:processDevManifest +core:processDevManifest +features:detail:processDevManifest +features:launches:processDevManifest +repository:processDevManifest +app:processDevResources +app:processDevUnitTestJavaRes +core:processDevUnitTestJavaRes +features:detail:processDevUnitTestJavaRes +features:launches:processDevUnitTestJavaRes +repository:processDevUnitTestJavaRes +app:processReleaseJavaRes +core:processReleaseJavaRes +features:detail:processReleaseJavaRes +features:launches:processReleaseJavaRes +repository:processReleaseJavaRes +app:processReleaseManifest +core:processReleaseManifest +features:detail:processReleaseManifest +features:launches:processReleaseManifest +repository:processReleaseManifest +app:processReleaseResources +app:processReleaseUnitTestJavaRes +core:processReleaseUnitTestJavaRes +features:detail:processReleaseUnitTestJavaRes +features:launches:processReleaseUnitTestJavaRes +repository:processReleaseUnitTestJavaRes +projectDependencyGraph +removeReports +app:reportBuildArtifactsDebug +core:reportBuildArtifactsDebug +features:detail:reportBuildArtifactsDebug +features:launches:reportBuildArtifactsDebug +repository:reportBuildArtifactsDebug +app:reportBuildArtifactsDev +core:reportBuildArtifactsDev +features:detail:reportBuildArtifactsDev +features:launches:reportBuildArtifactsDev +repository:reportBuildArtifactsDev +app:reportBuildArtifactsRelease +core:reportBuildArtifactsRelease +features:detail:reportBuildArtifactsRelease +features:launches:reportBuildArtifactsRelease +repository:reportBuildArtifactsRelease +app:reportSourceSetTransformAndroidTest +core:reportSourceSetTransformAndroidTest +features:detail:reportSourceSetTransformAndroidTest +features:launches:reportSourceSetTransformAndroidTest +repository:reportSourceSetTransformAndroidTest +app:reportSourceSetTransformAndroidTestDebug +core:reportSourceSetTransformAndroidTestDebug +features:detail:reportSourceSetTransformAndroidTestDebug +features:launches:reportSourceSetTransformAndroidTestDebug +repository:reportSourceSetTransformAndroidTestDebug +app:reportSourceSetTransformDebug +core:reportSourceSetTransformDebug +features:detail:reportSourceSetTransformDebug +features:launches:reportSourceSetTransformDebug +repository:reportSourceSetTransformDebug +app:reportSourceSetTransformDev +core:reportSourceSetTransformDev +features:detail:reportSourceSetTransformDev +features:launches:reportSourceSetTransformDev +repository:reportSourceSetTransformDev +app:reportSourceSetTransformMain +core:reportSourceSetTransformMain +features:detail:reportSourceSetTransformMain +features:launches:reportSourceSetTransformMain +repository:reportSourceSetTransformMain +app:reportSourceSetTransformRelease +core:reportSourceSetTransformRelease +features:detail:reportSourceSetTransformRelease +features:launches:reportSourceSetTransformRelease +repository:reportSourceSetTransformRelease +app:reportSourceSetTransformTest +core:reportSourceSetTransformTest +features:detail:reportSourceSetTransformTest +features:launches:reportSourceSetTransformTest +repository:reportSourceSetTransformTest +app:reportSourceSetTransformTestDebug +core:reportSourceSetTransformTestDebug +features:detail:reportSourceSetTransformTestDebug +features:launches:reportSourceSetTransformTestDebug +repository:reportSourceSetTransformTestDebug +app:reportSourceSetTransformTestDev +core:reportSourceSetTransformTestDev +features:detail:reportSourceSetTransformTestDev +features:launches:reportSourceSetTransformTestDev +repository:reportSourceSetTransformTestDev +app:reportSourceSetTransformTestRelease +core:reportSourceSetTransformTestRelease +features:detail:reportSourceSetTransformTestRelease +features:launches:reportSourceSetTransformTestRelease +repository:reportSourceSetTransformTestRelease +app:resolveConfigAttr +core:resolveConfigAttr +features:detail:resolveConfigAttr +features:launches:resolveConfigAttr +repository:resolveConfigAttr +app:signDebugBundle +app:signDevBundle +app:signingConfigWriterDebug +app:signingConfigWriterDebugAndroidTest +core:signingConfigWriterDebugAndroidTest +features:detail:signingConfigWriterDebugAndroidTest +features:launches:signingConfigWriterDebugAndroidTest +repository:signingConfigWriterDebugAndroidTest +app:signingConfigWriterDev +app:signingConfigWriterRelease +app:signReleaseBundle +app:stripDebugDebugSymbols +core:stripDebugDebugSymbols +features:detail:stripDebugDebugSymbols +features:launches:stripDebugDebugSymbols +repository:stripDebugDebugSymbols +app:stripDevDebugSymbols +core:stripDevDebugSymbols +features:detail:stripDevDebugSymbols +features:launches:stripDevDebugSymbols +repository:stripDevDebugSymbols +app:stripReleaseDebugSymbols +core:stripReleaseDebugSymbols +features:detail:stripReleaseDebugSymbols +features:launches:stripReleaseDebugSymbols +repository:stripReleaseDebugSymbols +app:transformClassesAndResourcesWithR8ForRelease +core:transformClassesAndResourcesWithR8ForRelease +features:detail:transformClassesAndResourcesWithR8ForRelease +features:launches:transformClassesAndResourcesWithR8ForRelease +repository:transformClassesAndResourcesWithR8ForRelease +core:transformClassesAndResourcesWithSyncLibJarsForDebug +features:detail:transformClassesAndResourcesWithSyncLibJarsForDebug +features:launches:transformClassesAndResourcesWithSyncLibJarsForDebug +repository:transformClassesAndResourcesWithSyncLibJarsForDebug +core:transformClassesAndResourcesWithSyncLibJarsForDev +features:detail:transformClassesAndResourcesWithSyncLibJarsForDev +features:launches:transformClassesAndResourcesWithSyncLibJarsForDev +repository:transformClassesAndResourcesWithSyncLibJarsForDev +core:transformClassesAndResourcesWithSyncLibJarsForRelease +features:detail:transformClassesAndResourcesWithSyncLibJarsForRelease +features:launches:transformClassesAndResourcesWithSyncLibJarsForRelease +repository:transformClassesAndResourcesWithSyncLibJarsForRelease +app:transformClassesWithDexBuilderForDebug +app:transformClassesWithDexBuilderForDebugAndroidTest +core:transformClassesWithDexBuilderForDebugAndroidTest +features:detail:transformClassesWithDexBuilderForDebugAndroidTest +features:launches:transformClassesWithDexBuilderForDebugAndroidTest +repository:transformClassesWithDexBuilderForDebugAndroidTest +app:transformClassesWithDexBuilderForDev +core:transformNativeLibsWithIntermediateJniLibsForDebug +features:detail:transformNativeLibsWithIntermediateJniLibsForDebug +features:launches:transformNativeLibsWithIntermediateJniLibsForDebug +repository:transformNativeLibsWithIntermediateJniLibsForDebug +core:transformNativeLibsWithIntermediateJniLibsForDev +features:detail:transformNativeLibsWithIntermediateJniLibsForDev +features:launches:transformNativeLibsWithIntermediateJniLibsForDev +repository:transformNativeLibsWithIntermediateJniLibsForDev +core:transformNativeLibsWithIntermediateJniLibsForRelease +features:detail:transformNativeLibsWithIntermediateJniLibsForRelease +features:launches:transformNativeLibsWithIntermediateJniLibsForRelease +repository:transformNativeLibsWithIntermediateJniLibsForRelease +core:transformNativeLibsWithSyncJniLibsForDebug +features:detail:transformNativeLibsWithSyncJniLibsForDebug +features:launches:transformNativeLibsWithSyncJniLibsForDebug +repository:transformNativeLibsWithSyncJniLibsForDebug +core:transformNativeLibsWithSyncJniLibsForDev +features:detail:transformNativeLibsWithSyncJniLibsForDev +features:launches:transformNativeLibsWithSyncJniLibsForDev +repository:transformNativeLibsWithSyncJniLibsForDev +core:transformNativeLibsWithSyncJniLibsForRelease +features:detail:transformNativeLibsWithSyncJniLibsForRelease +features:launches:transformNativeLibsWithSyncJniLibsForRelease +repository:transformNativeLibsWithSyncJniLibsForRelease +app:validateSigningDebug +app:validateSigningDebugAndroidTest +core:validateSigningDebugAndroidTest +features:detail:validateSigningDebugAndroidTest +features:launches:validateSigningDebugAndroidTest +repository:validateSigningDebugAndroidTest +app:validateSigningDev +core:verifyReleaseResources +features:detail:verifyReleaseResources +features:launches:verifyReleaseResources +repository:verifyReleaseResources +app:writeDebugApplicationId +app:writeDebugModuleMetadata +app:writeDevApplicationId +app:writeDevModuleMetadata +app:writeReleaseApplicationId +app:writeReleaseModuleMetadata +androidDependencies +androidDependencies +androidDependencies +androidDependencies +androidDependencies +signingReport +signingReport +signingReport +signingReport +signingReport +sourceSets +sourceSets +sourceSets +sourceSets +sourceSets +assemble +assemble +assemble +assemble +assemble +assembleAndroidTest +assembleAndroidTest +assembleAndroidTest +assembleAndroidTest +assembleAndroidTest +build +build +build +build +build +buildDependents +buildDependents +buildDependents +buildDependents +buildDependents +buildNeeded +buildNeeded +buildNeeded +buildNeeded +buildNeeded +bundle +clean +clean +clean +clean +clean +cleanBuildCache +cleanBuildCache +cleanBuildCache +cleanBuildCache +cleanBuildCache +compileDebugAndroidTestSources +compileDebugAndroidTestSources +compileDebugAndroidTestSources +compileDebugAndroidTestSources +compileDebugAndroidTestSources +compileDebugSources +compileDebugSources +compileDebugSources +compileDebugSources +compileDebugSources +compileDebugUnitTestSources +compileDebugUnitTestSources +compileDebugUnitTestSources +compileDebugUnitTestSources +compileDebugUnitTestSources +compileDevSources +compileDevSources +compileDevSources +compileDevSources +compileDevSources +compileDevUnitTestSources +compileDevUnitTestSources +compileDevUnitTestSources +compileDevUnitTestSources +compileDevUnitTestSources +compileReleaseSources +compileReleaseSources +compileReleaseSources +compileReleaseSources +compileReleaseSources +compileReleaseUnitTestSources +compileReleaseUnitTestSources +compileReleaseUnitTestSources +compileReleaseUnitTestSources +compileReleaseUnitTestSources +extractDebugAnnotations +extractDebugAnnotations +extractDebugAnnotations +extractDebugAnnotations +extractDevAnnotations +extractDevAnnotations +extractDevAnnotations +extractDevAnnotations +extractReleaseAnnotations +extractReleaseAnnotations +extractReleaseAnnotations +extractReleaseAnnotations +init +wrapper +lintFix +lintFix +lintFix +lintFix +lintFix +dokka +dokka +dokka +dokka +dokka +dokka +buildEnvironment +buildEnvironment +buildEnvironment +buildEnvironment +buildEnvironment +buildEnvironment +buildEnvironment +components +components +components +components +components +components +components +dependencies +dependencies +dependencies +dependencies +dependencies +dependencies +dependencies +dependencyInsight +dependencyInsight +dependencyInsight +dependencyInsight +dependencyInsight +dependencyInsight +dependencyInsight +dependentComponents +dependentComponents +dependentComponents +dependentComponents +dependentComponents +dependentComponents +dependentComponents +help +help +help +help +help +help +help +model +model +model +model +model +model +model +projects +projects +projects +projects +projects +projects +projects +properties +properties +properties +properties +properties +properties +properties +tasks +tasks +tasks +tasks +tasks +tasks +tasks +installDebug +installDebugAndroidTest +installDebugAndroidTest +installDebugAndroidTest +installDebugAndroidTest +installDebugAndroidTest +installDev +uninstallAll +uninstallAll +uninstallAll +uninstallAll +uninstallAll +uninstallDebug +uninstallDebugAndroidTest +uninstallDebugAndroidTest +uninstallDebugAndroidTest +uninstallDebugAndroidTest +uninstallDebugAndroidTest +uninstallDev +uninstallRelease +jacocoTestReport +jacocoTestReport +jacocoTestReport +jacocoTestReport +jacocoTestReport +jacocoTestReportDebug +jacocoTestReportDebug +jacocoTestReportDebug +jacocoTestReportDebug +jacocoTestReportDebug +jacocoTestReportDev +jacocoTestReportDev +jacocoTestReportDev +jacocoTestReportDev +jacocoTestReportDev +jacocoTestReportRelease +jacocoTestReportRelease +jacocoTestReportRelease +jacocoTestReportRelease +jacocoTestReportRelease +check +check +check +check +check +connectedAndroidTest +connectedAndroidTest +connectedAndroidTest +connectedAndroidTest +connectedAndroidTest +connectedCheck +connectedCheck +connectedCheck +connectedCheck +connectedCheck +connectedDebugAndroidTest +connectedDebugAndroidTest +connectedDebugAndroidTest +connectedDebugAndroidTest +connectedDebugAndroidTest +createDebugCoverageReport +createDebugCoverageReport +createDebugCoverageReport +createDebugCoverageReport +createDebugCoverageReport +createDevCoverageReport +createDevCoverageReport +createDevCoverageReport +createDevCoverageReport +createDevCoverageReport +detekt +detekt +detekt +detekt +detekt +detekt +detektBaseline +detektBaseline +detektBaseline +detektBaseline +detektBaseline +detektBaseline +detektGenerateConfig +detektGenerateConfig +detektGenerateConfig +detektGenerateConfig +detektGenerateConfig +detektGenerateConfig +detektIdeaFormat +detektIdeaFormat +detektIdeaFormat +detektIdeaFormat +detektIdeaFormat +detektIdeaFormat +detektIdeaInspect +detektIdeaInspect +detektIdeaInspect +detektIdeaInspect +detektIdeaInspect +detektIdeaInspect +deviceAndroidTest +deviceAndroidTest +deviceAndroidTest +deviceAndroidTest +deviceAndroidTest +deviceCheck +deviceCheck +deviceCheck +deviceCheck +deviceCheck +lint +lint +lint +lint +lint +lintDebug +lintDebug +lintDebug +lintDebug +lintDebug +lintDev +lintDev +lintDev +lintDev +lintDev +lintRelease +lintRelease +lintRelease +lintRelease +lintRelease +lintVitalRelease +test +test +test +test +test +testDebugUnitTest +testDebugUnitTest +testDebugUnitTest +testDebugUnitTest +testDebugUnitTest +testDevUnitTest +testDevUnitTest +testDevUnitTest +testDevUnitTest +testDevUnitTest +testReleaseUnitTest +testReleaseUnitTest +testReleaseUnitTest +testReleaseUnitTest +testReleaseUnitTest +assembleDebug +assembleDebug +assembleDebug +assembleDebug +assembleDebug +assembleDebugAndroidTest +assembleDebugAndroidTest +assembleDebugAndroidTest +assembleDebugAndroidTest +assembleDebugAndroidTest +assembleDebugUnitTest +assembleDebugUnitTest +assembleDebugUnitTest +assembleDebugUnitTest +assembleDebugUnitTest +assembleDev +assembleDev +assembleDev +assembleDev +assembleDev +assembleDevUnitTest +assembleDevUnitTest +assembleDevUnitTest +assembleDevUnitTest +assembleDevUnitTest +assembleRelease +assembleRelease +assembleRelease +assembleRelease +assembleRelease +assembleReleaseUnitTest +assembleReleaseUnitTest +assembleReleaseUnitTest +assembleReleaseUnitTest +assembleReleaseUnitTest +buildDebugPreBundle +buildDevPreBundle +buildReleasePreBundle +bundleDebug +bundleDebugAar +bundleDebugAar +bundleDebugAar +bundleDebugAar +bundleDebugAndroidTestClasses +bundleDebugAndroidTestResources +bundleDebugAndroidTestResources +bundleDebugAndroidTestResources +bundleDebugAndroidTestResources +bundleDebugAndroidTestResources +bundleDebugClasses +bundleDebugResources +bundleDebugUnitTestClasses +bundleDev +bundleDevAar +bundleDevAar +bundleDevAar +bundleDevAar +bundleDevClasses +bundleDevResources +bundleDevUnitTestClasses +bundleLibCompileDebug +bundleLibCompileDebug +bundleLibCompileDebug +bundleLibCompileDebug +bundleLibCompileDebugAndroidTest +bundleLibCompileDebugAndroidTest +bundleLibCompileDebugAndroidTest +bundleLibCompileDebugAndroidTest +bundleLibCompileDebugUnitTest +bundleLibCompileDebugUnitTest +bundleLibCompileDebugUnitTest +bundleLibCompileDebugUnitTest +bundleLibCompileDev +bundleLibCompileDev +bundleLibCompileDev +bundleLibCompileDev +bundleLibCompileDevUnitTest +bundleLibCompileDevUnitTest +bundleLibCompileDevUnitTest +bundleLibCompileDevUnitTest +bundleLibCompileRelease +bundleLibCompileRelease +bundleLibCompileRelease +bundleLibCompileRelease +bundleLibCompileReleaseUnitTest +bundleLibCompileReleaseUnitTest +bundleLibCompileReleaseUnitTest +bundleLibCompileReleaseUnitTest +bundleLibResDebug +bundleLibResDebug +bundleLibResDebug +bundleLibResDebug +bundleLibResDev +bundleLibResDev +bundleLibResDev +bundleLibResDev +bundleLibResRelease +bundleLibResRelease +bundleLibResRelease +bundleLibResRelease +bundleLibRuntimeDebug +bundleLibRuntimeDebug +bundleLibRuntimeDebug +bundleLibRuntimeDebug +bundleLibRuntimeDev +bundleLibRuntimeDev +bundleLibRuntimeDev +bundleLibRuntimeDev +bundleLibRuntimeRelease +bundleLibRuntimeRelease +bundleLibRuntimeRelease +bundleLibRuntimeRelease +bundleRelease +bundleReleaseAar +bundleReleaseAar +bundleReleaseAar +bundleReleaseAar +bundleReleaseClasses +bundleReleaseResources +bundleReleaseUnitTestClasses +checkDebugAndroidTestDuplicateClasses +checkDebugAndroidTestDuplicateClasses +checkDebugAndroidTestDuplicateClasses +checkDebugAndroidTestDuplicateClasses +checkDebugAndroidTestDuplicateClasses +checkDebugDuplicateClasses +checkDebugManifest +checkDebugManifest +checkDebugManifest +checkDebugManifest +checkDebugManifest +checkDevDuplicateClasses +checkDevManifest +checkDevManifest +checkDevManifest +checkDevManifest +checkDevManifest +checkReleaseManifest +checkReleaseManifest +checkReleaseManifest +checkReleaseManifest +checkReleaseManifest +clean +collectDebugDependencies +collectDevDependencies +collectReleaseDependencies +compileDebugAidl +compileDebugAidl +compileDebugAidl +compileDebugAidl +compileDebugAidl +compileDebugAndroidTestAidl +compileDebugAndroidTestAidl +compileDebugAndroidTestAidl +compileDebugAndroidTestAidl +compileDebugAndroidTestAidl +compileDebugAndroidTestJavaWithJavac +compileDebugAndroidTestJavaWithJavac +compileDebugAndroidTestJavaWithJavac +compileDebugAndroidTestJavaWithJavac +compileDebugAndroidTestJavaWithJavac +compileDebugAndroidTestKotlin +compileDebugAndroidTestKotlin +compileDebugAndroidTestKotlin +compileDebugAndroidTestKotlin +compileDebugAndroidTestKotlin +compileDebugAndroidTestRenderscript +compileDebugAndroidTestRenderscript +compileDebugAndroidTestRenderscript +compileDebugAndroidTestRenderscript +compileDebugAndroidTestRenderscript +compileDebugAndroidTestShaders +compileDebugAndroidTestShaders +compileDebugAndroidTestShaders +compileDebugAndroidTestShaders +compileDebugAndroidTestShaders +compileDebugJavaWithJavac +compileDebugJavaWithJavac +compileDebugJavaWithJavac +compileDebugJavaWithJavac +compileDebugJavaWithJavac +compileDebugKotlin +compileDebugKotlin +compileDebugKotlin +compileDebugKotlin +compileDebugKotlin +compileDebugRenderscript +compileDebugRenderscript +compileDebugRenderscript +compileDebugRenderscript +compileDebugRenderscript +compileDebugShaders +compileDebugShaders +compileDebugShaders +compileDebugShaders +compileDebugShaders +compileDebugUnitTestJavaWithJavac +compileDebugUnitTestJavaWithJavac +compileDebugUnitTestJavaWithJavac +compileDebugUnitTestJavaWithJavac +compileDebugUnitTestJavaWithJavac +compileDebugUnitTestKotlin +compileDebugUnitTestKotlin +compileDebugUnitTestKotlin +compileDebugUnitTestKotlin +compileDebugUnitTestKotlin +compileDevAidl +compileDevAidl +compileDevAidl +compileDevAidl +compileDevAidl +compileDevJavaWithJavac +compileDevJavaWithJavac +compileDevJavaWithJavac +compileDevJavaWithJavac +compileDevJavaWithJavac +compileDevKotlin +compileDevKotlin +compileDevKotlin +compileDevKotlin +compileDevKotlin +compileDevRenderscript +compileDevRenderscript +compileDevRenderscript +compileDevRenderscript +compileDevRenderscript +compileDevShaders +compileDevShaders +compileDevShaders +compileDevShaders +compileDevShaders +compileDevUnitTestJavaWithJavac +compileDevUnitTestJavaWithJavac +compileDevUnitTestJavaWithJavac +compileDevUnitTestJavaWithJavac +compileDevUnitTestJavaWithJavac +compileDevUnitTestKotlin +compileDevUnitTestKotlin +compileDevUnitTestKotlin +compileDevUnitTestKotlin +compileDevUnitTestKotlin +compileLint +compileLint +compileLint +compileLint +compileLint +compileReleaseAidl +compileReleaseAidl +compileReleaseAidl +compileReleaseAidl +compileReleaseAidl +compileReleaseJavaWithJavac +compileReleaseJavaWithJavac +compileReleaseJavaWithJavac +compileReleaseJavaWithJavac +compileReleaseJavaWithJavac +compileReleaseKotlin +compileReleaseKotlin +compileReleaseKotlin +compileReleaseKotlin +compileReleaseKotlin +compileReleaseRenderscript +compileReleaseRenderscript +compileReleaseRenderscript +compileReleaseRenderscript +compileReleaseRenderscript +compileReleaseShaders +compileReleaseShaders +compileReleaseShaders +compileReleaseShaders +compileReleaseShaders +compileReleaseUnitTestJavaWithJavac +compileReleaseUnitTestJavaWithJavac +compileReleaseUnitTestJavaWithJavac +compileReleaseUnitTestJavaWithJavac +compileReleaseUnitTestJavaWithJavac +compileReleaseUnitTestKotlin +compileReleaseUnitTestKotlin +compileReleaseUnitTestKotlin +compileReleaseUnitTestKotlin +compileReleaseUnitTestKotlin +configureDebugDependencies +configureDevDependencies +configureReleaseDependencies +consumeConfigAttr +consumeConfigAttr +consumeConfigAttr +consumeConfigAttr +consumeConfigAttr +createDebugAndroidTestCoverageReport +createDebugAndroidTestCoverageReport +createDebugAndroidTestCoverageReport +createDebugAndroidTestCoverageReport +createDebugAndroidTestCoverageReport +createDebugCompatibleScreenManifests +createDevCompatibleScreenManifests +createFullJarDebug +createFullJarDebug +createFullJarDebug +createFullJarDebug +createFullJarDev +createFullJarDev +createFullJarDev +createFullJarDev +createFullJarRelease +createFullJarRelease +createFullJarRelease +createFullJarRelease +createMockableJar +createMockableJar +createMockableJar +createMockableJar +createMockableJar +createReleaseCompatibleScreenManifests +dataBindingExportBuildInfoDebug +dataBindingExportBuildInfoDebug +dataBindingExportBuildInfoDebug +dataBindingExportBuildInfoDebug +dataBindingExportBuildInfoDebugAndroidTest +dataBindingExportBuildInfoDebugAndroidTest +dataBindingExportBuildInfoDebugAndroidTest +dataBindingExportBuildInfoDev +dataBindingExportBuildInfoDev +dataBindingExportBuildInfoDev +dataBindingExportBuildInfoDev +dataBindingExportBuildInfoRelease +dataBindingExportBuildInfoRelease +dataBindingExportBuildInfoRelease +dataBindingExportBuildInfoRelease +dataBindingExportFeaturePackageIdsDebug +dataBindingExportFeaturePackageIdsDev +dataBindingExportFeaturePackageIdsRelease +dataBindingGenBaseClassesDebug +dataBindingGenBaseClassesDebug +dataBindingGenBaseClassesDebug +dataBindingGenBaseClassesDebug +dataBindingGenBaseClassesDebugAndroidTest +dataBindingGenBaseClassesDebugAndroidTest +dataBindingGenBaseClassesDebugAndroidTest +dataBindingGenBaseClassesDev +dataBindingGenBaseClassesDev +dataBindingGenBaseClassesDev +dataBindingGenBaseClassesDev +dataBindingGenBaseClassesRelease +dataBindingGenBaseClassesRelease +dataBindingGenBaseClassesRelease +dataBindingGenBaseClassesRelease +dataBindingMergeDependencyArtifactsDebug +dataBindingMergeDependencyArtifactsDebug +dataBindingMergeDependencyArtifactsDebug +dataBindingMergeDependencyArtifactsDebug +dataBindingMergeDependencyArtifactsDebugAndroidTest +dataBindingMergeDependencyArtifactsDebugAndroidTest +dataBindingMergeDependencyArtifactsDebugAndroidTest +dataBindingMergeDependencyArtifactsDev +dataBindingMergeDependencyArtifactsDev +dataBindingMergeDependencyArtifactsDev +dataBindingMergeDependencyArtifactsDev +dataBindingMergeDependencyArtifactsRelease +dataBindingMergeDependencyArtifactsRelease +dataBindingMergeDependencyArtifactsRelease +dataBindingMergeDependencyArtifactsRelease +dataBindingMergeGenClassesDebug +dataBindingMergeGenClassesDebug +dataBindingMergeGenClassesDebug +dataBindingMergeGenClassesDebug +dataBindingMergeGenClassesDebugAndroidTest +dataBindingMergeGenClassesDebugAndroidTest +dataBindingMergeGenClassesDebugAndroidTest +dataBindingMergeGenClassesDev +dataBindingMergeGenClassesDev +dataBindingMergeGenClassesDev +dataBindingMergeGenClassesDev +dataBindingMergeGenClassesRelease +dataBindingMergeGenClassesRelease +dataBindingMergeGenClassesRelease +dataBindingMergeGenClassesRelease +desugarDebugAndroidTestFileDependencies +desugarDebugAndroidTestFileDependencies +desugarDebugAndroidTestFileDependencies +desugarDebugAndroidTestFileDependencies +desugarDebugAndroidTestFileDependencies +desugarDebugFileDependencies +desugarDevFileDependencies +dexDebug +dexDebug +dexDebug +dexDebug +dexDev +dexDev +dexDev +dexDev +dexRelease +dexRelease +dexRelease +dexRelease +dummydebugUnitTest +dummydebugUnitTest +dummydebugUnitTest +dummydebugUnitTest +dummydebugUnitTest +dummydevUnitTest +dummydevUnitTest +dummydevUnitTest +dummydevUnitTest +dummydevUnitTest +dummyreleaseUnitTest +dummyreleaseUnitTest +dummyreleaseUnitTest +dummyreleaseUnitTest +dummyreleaseUnitTest +extractApksForDebug +extractApksForDev +extractApksForRelease +extractProguardFiles +extractProguardFiles +extractProguardFiles +extractProguardFiles +extractProguardFiles +generateDebugAndroidTestAssets +generateDebugAndroidTestAssets +generateDebugAndroidTestAssets +generateDebugAndroidTestAssets +generateDebugAndroidTestAssets +generateDebugAndroidTestBuildConfig +generateDebugAndroidTestBuildConfig +generateDebugAndroidTestBuildConfig +generateDebugAndroidTestBuildConfig +generateDebugAndroidTestBuildConfig +generateDebugAndroidTestResources +generateDebugAndroidTestResources +generateDebugAndroidTestResources +generateDebugAndroidTestResources +generateDebugAndroidTestResources +generateDebugAndroidTestResValues +generateDebugAndroidTestResValues +generateDebugAndroidTestResValues +generateDebugAndroidTestResValues +generateDebugAndroidTestResValues +generateDebugAndroidTestSources +generateDebugAndroidTestSources +generateDebugAndroidTestSources +generateDebugAndroidTestSources +generateDebugAndroidTestSources +generateDebugAssets +generateDebugAssets +generateDebugAssets +generateDebugAssets +generateDebugAssets +generateDebugBuildConfig +generateDebugBuildConfig +generateDebugBuildConfig +generateDebugBuildConfig +generateDebugBuildConfig +generateDebugFeatureMetadata +generateDebugFeatureTransitiveDeps +generateDebugResources +generateDebugResources +generateDebugResources +generateDebugResources +generateDebugResources +generateDebugResValues +generateDebugResValues +generateDebugResValues +generateDebugResValues +generateDebugResValues +generateDebugRFile +generateDebugRFile +generateDebugRFile +generateDebugRFile +generateDebugSources +generateDebugSources +generateDebugSources +generateDebugSources +generateDebugSources +generateDebugUnitTestAssets +generateDebugUnitTestAssets +generateDebugUnitTestAssets +generateDebugUnitTestAssets +generateDebugUnitTestAssets +generateDebugUnitTestResources +generateDebugUnitTestResources +generateDebugUnitTestResources +generateDebugUnitTestResources +generateDebugUnitTestResources +generateDebugUnitTestSources +generateDebugUnitTestSources +generateDebugUnitTestSources +generateDebugUnitTestSources +generateDebugUnitTestSources +generateDevAssets +generateDevAssets +generateDevAssets +generateDevAssets +generateDevAssets +generateDevBuildConfig +generateDevBuildConfig +generateDevBuildConfig +generateDevBuildConfig +generateDevBuildConfig +generateDevFeatureMetadata +generateDevFeatureTransitiveDeps +generateDevResources +generateDevResources +generateDevResources +generateDevResources +generateDevResources +generateDevResValues +generateDevResValues +generateDevResValues +generateDevResValues +generateDevResValues +generateDevRFile +generateDevRFile +generateDevRFile +generateDevRFile +generateDevSources +generateDevSources +generateDevSources +generateDevSources +generateDevSources +generateDevUnitTestAssets +generateDevUnitTestAssets +generateDevUnitTestAssets +generateDevUnitTestAssets +generateDevUnitTestAssets +generateDevUnitTestResources +generateDevUnitTestResources +generateDevUnitTestResources +generateDevUnitTestResources +generateDevUnitTestResources +generateDevUnitTestSources +generateDevUnitTestSources +generateDevUnitTestSources +generateDevUnitTestSources +generateDevUnitTestSources +generateReleaseAssets +generateReleaseAssets +generateReleaseAssets +generateReleaseAssets +generateReleaseAssets +generateReleaseBuildConfig +generateReleaseBuildConfig +generateReleaseBuildConfig +generateReleaseBuildConfig +generateReleaseBuildConfig +generateReleaseFeatureMetadata +generateReleaseFeatureTransitiveDeps +generateReleaseLibraryProguardRules +generateReleaseLibraryProguardRules +generateReleaseLibraryProguardRules +generateReleaseLibraryProguardRules +generateReleaseResources +generateReleaseResources +generateReleaseResources +generateReleaseResources +generateReleaseResources +generateReleaseResValues +generateReleaseResValues +generateReleaseResValues +generateReleaseResValues +generateReleaseResValues +generateReleaseRFile +generateReleaseRFile +generateReleaseRFile +generateReleaseRFile +generateReleaseSources +generateReleaseSources +generateReleaseSources +generateReleaseSources +generateReleaseSources +generateReleaseUnitTestAssets +generateReleaseUnitTestAssets +generateReleaseUnitTestAssets +generateReleaseUnitTestAssets +generateReleaseUnitTestAssets +generateReleaseUnitTestResources +generateReleaseUnitTestResources +generateReleaseUnitTestResources +generateReleaseUnitTestResources +generateReleaseUnitTestResources +generateReleaseUnitTestSources +generateReleaseUnitTestSources +generateReleaseUnitTestSources +generateReleaseUnitTestSources +generateReleaseUnitTestSources +generateSafeArgsDebug +generateSafeArgsDev +generateSafeArgsRelease +jacocoDebug +jacocoDebug +jacocoDebug +jacocoDebug +jacocoDebug +jacocoDev +jacocoDev +jacocoDev +jacocoDev +jacocoDev +javaPreCompileDebug +javaPreCompileDebug +javaPreCompileDebug +javaPreCompileDebug +javaPreCompileDebug +javaPreCompileDebugAndroidTest +javaPreCompileDebugAndroidTest +javaPreCompileDebugAndroidTest +javaPreCompileDebugAndroidTest +javaPreCompileDebugAndroidTest +javaPreCompileDebugUnitTest +javaPreCompileDebugUnitTest +javaPreCompileDebugUnitTest +javaPreCompileDebugUnitTest +javaPreCompileDebugUnitTest +javaPreCompileDev +javaPreCompileDev +javaPreCompileDev +javaPreCompileDev +javaPreCompileDev +javaPreCompileDevUnitTest +javaPreCompileDevUnitTest +javaPreCompileDevUnitTest +javaPreCompileDevUnitTest +javaPreCompileDevUnitTest +javaPreCompileRelease +javaPreCompileRelease +javaPreCompileRelease +javaPreCompileRelease +javaPreCompileRelease +javaPreCompileReleaseUnitTest +javaPreCompileReleaseUnitTest +javaPreCompileReleaseUnitTest +javaPreCompileReleaseUnitTest +javaPreCompileReleaseUnitTest +kaptDebugAndroidTestKotlin +kaptDebugAndroidTestKotlin +kaptDebugAndroidTestKotlin +kaptDebugAndroidTestKotlin +kaptDebugAndroidTestKotlin +kaptDebugKotlin +kaptDebugKotlin +kaptDebugKotlin +kaptDebugKotlin +kaptDebugKotlin +kaptDebugUnitTestKotlin +kaptDebugUnitTestKotlin +kaptDebugUnitTestKotlin +kaptDebugUnitTestKotlin +kaptDebugUnitTestKotlin +kaptDevKotlin +kaptDevKotlin +kaptDevKotlin +kaptDevKotlin +kaptDevKotlin +kaptDevUnitTestKotlin +kaptDevUnitTestKotlin +kaptDevUnitTestKotlin +kaptDevUnitTestKotlin +kaptDevUnitTestKotlin +kaptGenerateStubsDebugAndroidTestKotlin +kaptGenerateStubsDebugAndroidTestKotlin +kaptGenerateStubsDebugAndroidTestKotlin +kaptGenerateStubsDebugAndroidTestKotlin +kaptGenerateStubsDebugAndroidTestKotlin +kaptGenerateStubsDebugKotlin +kaptGenerateStubsDebugKotlin +kaptGenerateStubsDebugKotlin +kaptGenerateStubsDebugKotlin +kaptGenerateStubsDebugKotlin +kaptGenerateStubsDebugUnitTestKotlin +kaptGenerateStubsDebugUnitTestKotlin +kaptGenerateStubsDebugUnitTestKotlin +kaptGenerateStubsDebugUnitTestKotlin +kaptGenerateStubsDebugUnitTestKotlin +kaptGenerateStubsDevKotlin +kaptGenerateStubsDevKotlin +kaptGenerateStubsDevKotlin +kaptGenerateStubsDevKotlin +kaptGenerateStubsDevKotlin +kaptGenerateStubsDevUnitTestKotlin +kaptGenerateStubsDevUnitTestKotlin +kaptGenerateStubsDevUnitTestKotlin +kaptGenerateStubsDevUnitTestKotlin +kaptGenerateStubsDevUnitTestKotlin +kaptGenerateStubsReleaseKotlin +kaptGenerateStubsReleaseKotlin +kaptGenerateStubsReleaseKotlin +kaptGenerateStubsReleaseKotlin +kaptGenerateStubsReleaseKotlin +kaptGenerateStubsReleaseUnitTestKotlin +kaptGenerateStubsReleaseUnitTestKotlin +kaptGenerateStubsReleaseUnitTestKotlin +kaptGenerateStubsReleaseUnitTestKotlin +kaptGenerateStubsReleaseUnitTestKotlin +kaptReleaseKotlin +kaptReleaseKotlin +kaptReleaseKotlin +kaptReleaseKotlin +kaptReleaseKotlin +kaptReleaseUnitTestKotlin +kaptReleaseUnitTestKotlin +kaptReleaseUnitTestKotlin +kaptReleaseUnitTestKotlin +kaptReleaseUnitTestKotlin +mainApkListPersistenceDebug +mainApkListPersistenceDebugAndroidTest +mainApkListPersistenceDebugAndroidTest +mainApkListPersistenceDebugAndroidTest +mainApkListPersistenceDebugAndroidTest +mainApkListPersistenceDebugAndroidTest +mainApkListPersistenceDev +mainApkListPersistenceRelease +makeApkFromBundleForDebug +makeApkFromBundleForDev +makeApkFromBundleForRelease +mergeDebugAndroidTestAssets +mergeDebugAndroidTestAssets +mergeDebugAndroidTestAssets +mergeDebugAndroidTestAssets +mergeDebugAndroidTestAssets +mergeDebugAndroidTestGeneratedProguardFiles +mergeDebugAndroidTestGeneratedProguardFiles +mergeDebugAndroidTestGeneratedProguardFiles +mergeDebugAndroidTestGeneratedProguardFiles +mergeDebugAndroidTestGeneratedProguardFiles +mergeDebugAndroidTestJavaResource +mergeDebugAndroidTestJavaResource +mergeDebugAndroidTestJavaResource +mergeDebugAndroidTestJavaResource +mergeDebugAndroidTestJavaResource +mergeDebugAndroidTestJniLibFolders +mergeDebugAndroidTestJniLibFolders +mergeDebugAndroidTestJniLibFolders +mergeDebugAndroidTestJniLibFolders +mergeDebugAndroidTestJniLibFolders +mergeDebugAndroidTestNativeLibs +mergeDebugAndroidTestNativeLibs +mergeDebugAndroidTestNativeLibs +mergeDebugAndroidTestNativeLibs +mergeDebugAndroidTestNativeLibs +mergeDebugAndroidTestResources +mergeDebugAndroidTestResources +mergeDebugAndroidTestResources +mergeDebugAndroidTestResources +mergeDebugAndroidTestResources +mergeDebugAndroidTestShaders +mergeDebugAndroidTestShaders +mergeDebugAndroidTestShaders +mergeDebugAndroidTestShaders +mergeDebugAndroidTestShaders +mergeDebugAssets +mergeDebugAssets +mergeDebugAssets +mergeDebugAssets +mergeDebugAssets +mergeDebugConsumerProguardFiles +mergeDebugConsumerProguardFiles +mergeDebugConsumerProguardFiles +mergeDebugConsumerProguardFiles +mergeDebugGeneratedProguardFiles +mergeDebugGeneratedProguardFiles +mergeDebugGeneratedProguardFiles +mergeDebugGeneratedProguardFiles +mergeDebugGeneratedProguardFiles +mergeDebugJavaResource +mergeDebugJavaResource +mergeDebugJavaResource +mergeDebugJavaResource +mergeDebugJavaResource +mergeDebugJniLibFolders +mergeDebugJniLibFolders +mergeDebugJniLibFolders +mergeDebugJniLibFolders +mergeDebugJniLibFolders +mergeDebugNativeLibs +mergeDebugNativeLibs +mergeDebugNativeLibs +mergeDebugNativeLibs +mergeDebugNativeLibs +mergeDebugResources +mergeDebugResources +mergeDebugResources +mergeDebugResources +mergeDebugResources +mergeDebugShaders +mergeDebugShaders +mergeDebugShaders +mergeDebugShaders +mergeDebugShaders +mergeDevAssets +mergeDevAssets +mergeDevAssets +mergeDevAssets +mergeDevAssets +mergeDevConsumerProguardFiles +mergeDevConsumerProguardFiles +mergeDevConsumerProguardFiles +mergeDevConsumerProguardFiles +mergeDevGeneratedProguardFiles +mergeDevGeneratedProguardFiles +mergeDevGeneratedProguardFiles +mergeDevGeneratedProguardFiles +mergeDevGeneratedProguardFiles +mergeDevJavaResource +mergeDevJavaResource +mergeDevJavaResource +mergeDevJavaResource +mergeDevJavaResource +mergeDevJniLibFolders +mergeDevJniLibFolders +mergeDevJniLibFolders +mergeDevJniLibFolders +mergeDevJniLibFolders +mergeDevNativeLibs +mergeDevNativeLibs +mergeDevNativeLibs +mergeDevNativeLibs +mergeDevNativeLibs +mergeDevResources +mergeDevResources +mergeDevResources +mergeDevResources +mergeDevResources +mergeDevShaders +mergeDevShaders +mergeDevShaders +mergeDevShaders +mergeDevShaders +mergeExtDexDebug +mergeExtDexDebugAndroidTest +mergeExtDexDebugAndroidTest +mergeExtDexDebugAndroidTest +mergeExtDexDebugAndroidTest +mergeExtDexDebugAndroidTest +mergeExtDexDev +mergeLibDexDebug +mergeLibDexDebugAndroidTest +mergeLibDexDebugAndroidTest +mergeLibDexDebugAndroidTest +mergeLibDexDebugAndroidTest +mergeLibDexDebugAndroidTest +mergeLibDexDev +mergeProjectDexDebug +mergeProjectDexDebugAndroidTest +mergeProjectDexDebugAndroidTest +mergeProjectDexDebugAndroidTest +mergeProjectDexDebugAndroidTest +mergeProjectDexDebugAndroidTest +mergeProjectDexDev +mergeReleaseAssets +mergeReleaseAssets +mergeReleaseAssets +mergeReleaseAssets +mergeReleaseAssets +mergeReleaseConsumerProguardFiles +mergeReleaseConsumerProguardFiles +mergeReleaseConsumerProguardFiles +mergeReleaseConsumerProguardFiles +mergeReleaseGeneratedProguardFiles +mergeReleaseGeneratedProguardFiles +mergeReleaseGeneratedProguardFiles +mergeReleaseGeneratedProguardFiles +mergeReleaseGeneratedProguardFiles +mergeReleaseJavaResource +mergeReleaseJavaResource +mergeReleaseJavaResource +mergeReleaseJavaResource +mergeReleaseJavaResource +mergeReleaseJniLibFolders +mergeReleaseJniLibFolders +mergeReleaseJniLibFolders +mergeReleaseJniLibFolders +mergeReleaseJniLibFolders +mergeReleaseNativeLibs +mergeReleaseNativeLibs +mergeReleaseNativeLibs +mergeReleaseNativeLibs +mergeReleaseNativeLibs +mergeReleaseResources +mergeReleaseResources +mergeReleaseResources +mergeReleaseResources +mergeReleaseResources +mergeReleaseShaders +mergeReleaseShaders +mergeReleaseShaders +mergeReleaseShaders +mergeReleaseShaders +packageDebug +packageDebugAndroidTest +packageDebugAndroidTest +packageDebugAndroidTest +packageDebugAndroidTest +packageDebugAndroidTest +packageDebugAssets +packageDebugAssets +packageDebugAssets +packageDebugAssets +packageDebugBundle +packageDebugRenderscript +packageDebugRenderscript +packageDebugRenderscript +packageDebugRenderscript +packageDebugResources +packageDebugResources +packageDebugResources +packageDebugResources +packageDebugUniversalApk +packageDev +packageDevAssets +packageDevAssets +packageDevAssets +packageDevAssets +packageDevBundle +packageDevRenderscript +packageDevRenderscript +packageDevRenderscript +packageDevRenderscript +packageDevResources +packageDevResources +packageDevResources +packageDevResources +packageDevUniversalApk +packageRelease +packageReleaseAssets +packageReleaseAssets +packageReleaseAssets +packageReleaseAssets +packageReleaseBundle +packageReleaseRenderscript +packageReleaseRenderscript +packageReleaseRenderscript +packageReleaseRenderscript +packageReleaseResources +packageReleaseResources +packageReleaseResources +packageReleaseResources +packageReleaseUniversalApk +parseDebugLibraryResources +parseDebugLibraryResources +parseDebugLibraryResources +parseDebugLibraryResources +parseDevLibraryResources +parseDevLibraryResources +parseDevLibraryResources +parseDevLibraryResources +parseReleaseLibraryResources +parseReleaseLibraryResources +parseReleaseLibraryResources +parseReleaseLibraryResources +preBuild +preBuild +preBuild +preBuild +preBuild +preDebugAndroidTestBuild +preDebugAndroidTestBuild +preDebugAndroidTestBuild +preDebugAndroidTestBuild +preDebugAndroidTestBuild +preDebugBuild +preDebugBuild +preDebugBuild +preDebugBuild +preDebugBuild +preDebugUnitTestBuild +preDebugUnitTestBuild +preDebugUnitTestBuild +preDebugUnitTestBuild +preDebugUnitTestBuild +preDevBuild +preDevBuild +preDevBuild +preDevBuild +preDevBuild +preDevUnitTestBuild +preDevUnitTestBuild +preDevUnitTestBuild +preDevUnitTestBuild +preDevUnitTestBuild +prepareKotlinBuildScriptModel +prepareKotlinBuildScriptModel +prepareKotlinBuildScriptModel +prepareKotlinBuildScriptModel +prepareKotlinBuildScriptModel +prepareKotlinBuildScriptModel +prepareKotlinBuildScriptModel +prepareLintJar +prepareLintJar +prepareLintJar +prepareLintJar +prepareLintJar +prepareLintJarForPublish +prepareLintJarForPublish +prepareLintJarForPublish +prepareLintJarForPublish +prepareLintJarForPublish +preReleaseBuild +preReleaseBuild +preReleaseBuild +preReleaseBuild +preReleaseBuild +preReleaseUnitTestBuild +preReleaseUnitTestBuild +preReleaseUnitTestBuild +preReleaseUnitTestBuild +preReleaseUnitTestBuild +processDebugAndroidTestJavaRes +processDebugAndroidTestJavaRes +processDebugAndroidTestJavaRes +processDebugAndroidTestJavaRes +processDebugAndroidTestJavaRes +processDebugAndroidTestManifest +processDebugAndroidTestManifest +processDebugAndroidTestManifest +processDebugAndroidTestManifest +processDebugAndroidTestManifest +processDebugAndroidTestResources +processDebugAndroidTestResources +processDebugAndroidTestResources +processDebugAndroidTestResources +processDebugAndroidTestResources +processDebugJavaRes +processDebugJavaRes +processDebugJavaRes +processDebugJavaRes +processDebugJavaRes +processDebugManifest +processDebugManifest +processDebugManifest +processDebugManifest +processDebugManifest +processDebugResources +processDebugUnitTestJavaRes +processDebugUnitTestJavaRes +processDebugUnitTestJavaRes +processDebugUnitTestJavaRes +processDebugUnitTestJavaRes +processDevJavaRes +processDevJavaRes +processDevJavaRes +processDevJavaRes +processDevJavaRes +processDevManifest +processDevManifest +processDevManifest +processDevManifest +processDevManifest +processDevResources +processDevUnitTestJavaRes +processDevUnitTestJavaRes +processDevUnitTestJavaRes +processDevUnitTestJavaRes +processDevUnitTestJavaRes +processReleaseJavaRes +processReleaseJavaRes +processReleaseJavaRes +processReleaseJavaRes +processReleaseJavaRes +processReleaseManifest +processReleaseManifest +processReleaseManifest +processReleaseManifest +processReleaseManifest +processReleaseResources +processReleaseUnitTestJavaRes +processReleaseUnitTestJavaRes +processReleaseUnitTestJavaRes +processReleaseUnitTestJavaRes +processReleaseUnitTestJavaRes +projectDependencyGraph +removeReports +reportBuildArtifactsDebug +reportBuildArtifactsDebug +reportBuildArtifactsDebug +reportBuildArtifactsDebug +reportBuildArtifactsDebug +reportBuildArtifactsDev +reportBuildArtifactsDev +reportBuildArtifactsDev +reportBuildArtifactsDev +reportBuildArtifactsDev +reportBuildArtifactsRelease +reportBuildArtifactsRelease +reportBuildArtifactsRelease +reportBuildArtifactsRelease +reportBuildArtifactsRelease +reportSourceSetTransformAndroidTest +reportSourceSetTransformAndroidTest +reportSourceSetTransformAndroidTest +reportSourceSetTransformAndroidTest +reportSourceSetTransformAndroidTest +reportSourceSetTransformAndroidTestDebug +reportSourceSetTransformAndroidTestDebug +reportSourceSetTransformAndroidTestDebug +reportSourceSetTransformAndroidTestDebug +reportSourceSetTransformAndroidTestDebug +reportSourceSetTransformDebug +reportSourceSetTransformDebug +reportSourceSetTransformDebug +reportSourceSetTransformDebug +reportSourceSetTransformDebug +reportSourceSetTransformDev +reportSourceSetTransformDev +reportSourceSetTransformDev +reportSourceSetTransformDev +reportSourceSetTransformDev +reportSourceSetTransformMain +reportSourceSetTransformMain +reportSourceSetTransformMain +reportSourceSetTransformMain +reportSourceSetTransformMain +reportSourceSetTransformRelease +reportSourceSetTransformRelease +reportSourceSetTransformRelease +reportSourceSetTransformRelease +reportSourceSetTransformRelease +reportSourceSetTransformTest +reportSourceSetTransformTest +reportSourceSetTransformTest +reportSourceSetTransformTest +reportSourceSetTransformTest +reportSourceSetTransformTestDebug +reportSourceSetTransformTestDebug +reportSourceSetTransformTestDebug +reportSourceSetTransformTestDebug +reportSourceSetTransformTestDebug +reportSourceSetTransformTestDev +reportSourceSetTransformTestDev +reportSourceSetTransformTestDev +reportSourceSetTransformTestDev +reportSourceSetTransformTestDev +reportSourceSetTransformTestRelease +reportSourceSetTransformTestRelease +reportSourceSetTransformTestRelease +reportSourceSetTransformTestRelease +reportSourceSetTransformTestRelease +resolveConfigAttr +resolveConfigAttr +resolveConfigAttr +resolveConfigAttr +resolveConfigAttr +signDebugBundle +signDevBundle +signingConfigWriterDebug +signingConfigWriterDebugAndroidTest +signingConfigWriterDebugAndroidTest +signingConfigWriterDebugAndroidTest +signingConfigWriterDebugAndroidTest +signingConfigWriterDebugAndroidTest +signingConfigWriterDev +signingConfigWriterRelease +signReleaseBundle +stripDebugDebugSymbols +stripDebugDebugSymbols +stripDebugDebugSymbols +stripDebugDebugSymbols +stripDebugDebugSymbols +stripDevDebugSymbols +stripDevDebugSymbols +stripDevDebugSymbols +stripDevDebugSymbols +stripDevDebugSymbols +stripReleaseDebugSymbols +stripReleaseDebugSymbols +stripReleaseDebugSymbols +stripReleaseDebugSymbols +stripReleaseDebugSymbols +transformClassesAndResourcesWithR8ForRelease +transformClassesAndResourcesWithR8ForRelease +transformClassesAndResourcesWithR8ForRelease +transformClassesAndResourcesWithR8ForRelease +transformClassesAndResourcesWithR8ForRelease +transformClassesAndResourcesWithSyncLibJarsForDebug +transformClassesAndResourcesWithSyncLibJarsForDebug +transformClassesAndResourcesWithSyncLibJarsForDebug +transformClassesAndResourcesWithSyncLibJarsForDebug +transformClassesAndResourcesWithSyncLibJarsForDev +transformClassesAndResourcesWithSyncLibJarsForDev +transformClassesAndResourcesWithSyncLibJarsForDev +transformClassesAndResourcesWithSyncLibJarsForDev +transformClassesAndResourcesWithSyncLibJarsForRelease +transformClassesAndResourcesWithSyncLibJarsForRelease +transformClassesAndResourcesWithSyncLibJarsForRelease +transformClassesAndResourcesWithSyncLibJarsForRelease +transformClassesWithDexBuilderForDebug +transformClassesWithDexBuilderForDebugAndroidTest +transformClassesWithDexBuilderForDebugAndroidTest +transformClassesWithDexBuilderForDebugAndroidTest +transformClassesWithDexBuilderForDebugAndroidTest +transformClassesWithDexBuilderForDebugAndroidTest +transformClassesWithDexBuilderForDev +transformNativeLibsWithIntermediateJniLibsForDebug +transformNativeLibsWithIntermediateJniLibsForDebug +transformNativeLibsWithIntermediateJniLibsForDebug +transformNativeLibsWithIntermediateJniLibsForDebug +transformNativeLibsWithIntermediateJniLibsForDev +transformNativeLibsWithIntermediateJniLibsForDev +transformNativeLibsWithIntermediateJniLibsForDev +transformNativeLibsWithIntermediateJniLibsForDev +transformNativeLibsWithIntermediateJniLibsForRelease +transformNativeLibsWithIntermediateJniLibsForRelease +transformNativeLibsWithIntermediateJniLibsForRelease +transformNativeLibsWithIntermediateJniLibsForRelease +transformNativeLibsWithSyncJniLibsForDebug +transformNativeLibsWithSyncJniLibsForDebug +transformNativeLibsWithSyncJniLibsForDebug +transformNativeLibsWithSyncJniLibsForDebug +transformNativeLibsWithSyncJniLibsForDev +transformNativeLibsWithSyncJniLibsForDev +transformNativeLibsWithSyncJniLibsForDev +transformNativeLibsWithSyncJniLibsForDev +transformNativeLibsWithSyncJniLibsForRelease +transformNativeLibsWithSyncJniLibsForRelease +transformNativeLibsWithSyncJniLibsForRelease +transformNativeLibsWithSyncJniLibsForRelease +validateSigningDebug +validateSigningDebugAndroidTest +validateSigningDebugAndroidTest +validateSigningDebugAndroidTest +validateSigningDebugAndroidTest +validateSigningDebugAndroidTest +validateSigningDev +verifyReleaseResources +verifyReleaseResources +verifyReleaseResources +verifyReleaseResources +writeDebugApplicationId +writeDebugModuleMetadata +writeDevApplicationId +writeDevModuleMetadata +writeReleaseApplicationId +writeReleaseModuleMetadata diff --git a/app/build.gradle b/app/build.gradle index b99eac8..abf4201 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,7 +23,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':core') - implementation project(':features:list') + implementation project(':features:launches') implementation project(':features:detail') implementation libraries.coroutines diff --git a/app/src/main/kotlin/com/melih/rocketscience/di/AppComponent.kt b/app/src/main/kotlin/com/melih/rocketscience/di/AppComponent.kt index 3ed9830..e120675 100644 --- a/app/src/main/kotlin/com/melih/rocketscience/di/AppComponent.kt +++ b/app/src/main/kotlin/com/melih/rocketscience/di/AppComponent.kt @@ -1,6 +1,8 @@ package com.melih.rocketscience.di import com.melih.core.di.CoreComponent +import com.melih.detail.di.DetailModule +import com.melih.list.di.LaunchesFeatureModule import com.melih.rocketscience.App import dagger.Component import dagger.android.AndroidInjectionModule @@ -8,7 +10,10 @@ import dagger.android.AndroidInjector @AppScope @Component( - modules = [AndroidInjectionModule::class, AppModule::class], + modules = [AndroidInjectionModule::class, + LaunchesFeatureModule::class, + DetailModule::class], + dependencies = [CoreComponent::class] ) interface AppComponent : AndroidInjector { diff --git a/app/src/main/kotlin/com/melih/rocketscience/di/AppModule.kt b/app/src/main/kotlin/com/melih/rocketscience/di/AppModule.kt deleted file mode 100644 index c80bd10..0000000 --- a/app/src/main/kotlin/com/melih/rocketscience/di/AppModule.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.melih.rocketscience.di - -import com.melih.detail.di.DetailContributor -import com.melih.detail.ui.DetailActivity -import com.melih.list.di.LaunchesContributor -import com.melih.list.ui.LaunchesActivity -import dagger.Module -import dagger.android.ContributesAndroidInjector - -@Module -abstract class AppModule { - - @ContributesAndroidInjector( - modules = [ - LaunchesContributor::class - ] - ) - abstract fun launchesActivity(): LaunchesActivity - - @ContributesAndroidInjector( - modules = [ - DetailContributor::class - ] - ) - abstract fun detailActivity(): DetailActivity -} diff --git a/build.gradle b/build.gradle index 30e23e9..2f4bf76 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.40' + ext.kotlin_version = '1.3.41' ext.nav_version = '2.1.0-alpha06' repositories { @@ -9,10 +9,10 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0-beta05' + classpath 'com.android.tools.build:gradle:3.5.0-rc01' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.18" - classpath "de.mannodermaus.gradle.plugins:android-junit5:1.4.2.1" + classpath "de.mannodermaus.gradle.plugins:android-junit5:1.5.0.0" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -22,6 +22,7 @@ buildscript { plugins { id "io.gitlab.arturbosch.detekt" version "1.0.0-RC14" id "org.jetbrains.dokka" version "0.9.18" + id "jacoco" } allprojects { @@ -159,4 +160,9 @@ task projectDependencyGraph { } } +jacoco { + toolVersion = "0.8.4" + reportsDir = file("reports/jacoco") +} + apply from: "scripts/dependencies.gradle" diff --git a/core/jacoco.exec b/core/jacoco.exec new file mode 100644 index 0000000000000000000000000000000000000000..45a3fecf1b2c5f56d98a221e1182637b874b1f5a GIT binary patch literal 934 zcmZQPa6o`vfI-eTH77HpSl8DnK(D;mNUtO{S1&I|H^sm-#XL1Rg@J)FN00fq9E3j0 zB^Z2U{wL#_6&Vb=$@#hZxggE@MX3e(#hE4fMV0!Qc_pbuiOE2Av3^owajJfBX>xLE zaWNCKV1sI|14q#ZB?fIGb(SQSq%yM$=CUyf+cr2j5No4bVrEWh5lH7VM%xTt+aFB~ zI>gx+l$uzapU2E5C=%_s`SW8>0|q1FG`gqer50r-yA~DY7Xf|h!@lQ+?Vsj&;_WRi z%_#wD)c#lFaK>?l5rZ*t_6CE3NW~{VF(orEor#$dZQs=ZTFOL|Qm : DaggerAppCompatActivity() { protected lateinit var binding: T protected lateinit var navHostFragment: NavHostFragment - @ExperimentalCoroutinesApi override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseFragment.kt b/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseFragment.kt index 7dce574..4224e72 100644 --- a/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseFragment.kt +++ b/core/src/main/kotlin/com/melih/core/base/lifecycle/BaseFragment.kt @@ -12,7 +12,6 @@ import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import com.google.android.material.snackbar.Snackbar import com.melih.repository.interactors.base.Reason -import kotlinx.coroutines.ExperimentalCoroutinesApi /** * Parent of all fragments. @@ -41,7 +40,6 @@ abstract class BaseFragment : Fragment() { return binding.root } - @ExperimentalCoroutinesApi protected fun showSnackbarWithAction(reason: Reason, block: () -> Unit) { Snackbar.make( binding.root, diff --git a/core/src/main/kotlin/com/melih/core/base/paging/BasePagingDataSource.kt b/core/src/main/kotlin/com/melih/core/base/paging/BasePagingDataSource.kt new file mode 100644 index 0000000..47b6126 --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/base/paging/BasePagingDataSource.kt @@ -0,0 +1,140 @@ +package com.melih.core.base.paging + +import androidx.annotation.CallSuper +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.paging.PageKeyedDataSource +import com.melih.repository.interactors.base.Reason +import com.melih.repository.interactors.base.Result +import com.melih.repository.interactors.base.State +import com.melih.repository.interactors.base.onFailure +import com.melih.repository.interactors.base.onState +import com.melih.repository.interactors.base.onSuccess +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +const val INITIAL_PAGE = 0 + +/** + * Base class for all [pageKeyedDataSources][PageKeyedDataSource] in project. + * + * Purpose of this class is to ease handling of [result][Result]. It overrides [loadInitial], [loadAfter] and [loadBefore] + * so sources extends from base does not need to override them, they just need to provide way of loading data by overriding [loadDataForPage]. + * + * [handleState] & [handleFailure] updates corresponding [liveData][LiveData] objects [stateData] & [reasonData], + * which can be used with [androidx.lifecycle.Transformations] to observe changes on the source state & error. + * + * This source has it's own [coroutineScope][CoroutineScope] that's backed up by a [SupervisorJob] to handle networking operations. + * It's cancelled automatically when source factory [invalidates][invalidate] the source. + */ + +@UseExperimental(ExperimentalCoroutinesApi::class) +abstract class BasePagingDataSource : PageKeyedDataSource() { + + // region Abstractions + + abstract fun loadDataForPage(page: Int): Flow>> // Load next page(s) + // endregion + + // region Properties + + private val _stateData = MutableLiveData() + private val _reasonData = MutableLiveData() + private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + + /** + * Observe [stateData] to get notified of state of data + */ + val stateData: LiveData + get() = _stateData + + /** + * Observe [reasonData] to get notified if an error occurs + */ + val reasonData: LiveData + get() = _reasonData + // endregion + + // region Functions + + override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { + // Looping through channel as we'll receive any state, error or data here + loadDataForPage(INITIAL_PAGE) + .onEach { result -> + result.onState(::handleState) + .onFailure(::handleFailure) + .onSuccess { + // When we receive data without any failures, we transform it and return list, also what's the value for next page + callback.onResult( + it, + INITIAL_PAGE, + INITIAL_PAGE + 1 + ) + } + } + .launchIn(coroutineScope) + } + + override fun loadAfter(params: LoadParams, callback: LoadCallback) { + // Key for which page to load is in params + val page = params.key + + loadDataForPage(page) + .onEach { result -> + result + .onState(::handleState) + .onFailure(::handleFailure) + .onSuccess { + // When we receive data without any failures, we transform it and return list, also what's the value for next page + callback.onResult( + it, + page + 1 + ) + } + } + .launchIn(coroutineScope) + } + + /** + * This loads previous pages, we don't have a use for it yet, so it's a no-op override + */ + override fun loadBefore(params: LoadParams, callback: LoadCallback) { + // no-op + } + + /** + * Default state handler which assigns given [state] to [stateData] + * + * @param state state of operation + */ + @CallSuper + protected fun handleState(state: State) { + _stateData.value = state + } + + /** + * Default error handler which assign received [reason] to [reasonData] + * + * @param reason check [Reason] class for possible error types + */ + @CallSuper + protected fun handleFailure(reason: Reason) { + _reasonData.value = reason + } + + /** + * Canceling [coroutineScope] + */ + @CallSuper + override fun invalidate() { + coroutineScope.cancel() + super.invalidate() + } + // endregion +} diff --git a/core/src/main/kotlin/com/melih/core/base/paging/BasePagingFactory.kt b/core/src/main/kotlin/com/melih/core/base/paging/BasePagingFactory.kt new file mode 100644 index 0000000..34a6ea3 --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/base/paging/BasePagingFactory.kt @@ -0,0 +1,42 @@ +package com.melih.core.base.paging + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.paging.DataSource + +/** + * Base [factory][DataSource.Factory] class for any [dataSource][DataSource]s in project. + * + * It's purpose is to provide latest source so [basePagingViewModel][be.mediahuis.core.base.viewmodel.BasePagingViewModel] can obtain + * [stateData][BasePagingDataSource.stateData] and [reasonData][BasePagingDataSource.reasonData] from it. + * + * This is done under the hood by telling this factory how to create a source by overriding [createSource]. + * + * Purpose of this transmission is to encapuslate [basePagingDataSource][BasePagingDataSource]. + */ +abstract class BasePagingFactory : DataSource.Factory() { + + // region Abstractions + + abstract fun createSource(): BasePagingDataSource + // endregion + + // region Properties + + private val _currentSource = MutableLiveData>() + + val currentSource: LiveData> + get() = _currentSource + // endregion + + // region Functions + + override fun create(): DataSource = createSource().apply { _currentSource.postValue(this) } + + /** + * Invalidating the [currentSource] + * by calling [be.mediahuis.core.base.paging.BasePagingDataSource.invalidate] + */ + fun invalidateDataSource() = currentSource.value?.invalidate() + // endregion +} diff --git a/core/src/main/kotlin/com/melih/core/base/recycler/BaseListAdapter.kt b/core/src/main/kotlin/com/melih/core/base/recycler/BasePagingListAdapter.kt similarity index 89% rename from core/src/main/kotlin/com/melih/core/base/recycler/BaseListAdapter.kt rename to core/src/main/kotlin/com/melih/core/base/recycler/BasePagingListAdapter.kt index 8ec796b..e3ec4bc 100644 --- a/core/src/main/kotlin/com/melih/core/base/recycler/BaseListAdapter.kt +++ b/core/src/main/kotlin/com/melih/core/base/recycler/BasePagingListAdapter.kt @@ -3,8 +3,8 @@ package com.melih.core.base.recycler import android.view.LayoutInflater import android.view.ViewGroup import androidx.databinding.ViewDataBinding +import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView /** @@ -12,10 +12,10 @@ import androidx.recyclerview.widget.RecyclerView * * */ -abstract class BaseListAdapter( +abstract class BasePagingListAdapter( callback: DiffUtil.ItemCallback, - private val clickListener: (T) -> Unit -) : ListAdapter>(callback) { + private val clickListener: (T?) -> Unit +) : PagedListAdapter>(callback) { private var itemClickListener: ((T) -> Unit)? = null @@ -48,12 +48,12 @@ abstract class BaseListAdapter( override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { val item = getItem(position) + holder.itemView.setOnClickListener { clickListener(item) } holder.bind(item) - } } @@ -68,5 +68,5 @@ abstract class BaseViewHolder(binding: ViewDataBinding) : RecyclerView.ViewHo * @param item entity * @param position position from adapter */ - abstract fun bind(item: T) + abstract fun bind(item: T?) } diff --git a/core/src/main/kotlin/com/melih/core/base/viewmodel/BasePagingViewModel.kt b/core/src/main/kotlin/com/melih/core/base/viewmodel/BasePagingViewModel.kt new file mode 100644 index 0000000..57da05b --- /dev/null +++ b/core/src/main/kotlin/com/melih/core/base/viewmodel/BasePagingViewModel.kt @@ -0,0 +1,70 @@ +package com.melih.core.base.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations +import androidx.lifecycle.ViewModel +import androidx.paging.PagedList +import androidx.paging.toLiveData +import com.melih.core.base.paging.BasePagingFactory +import com.melih.repository.interactors.base.Reason +import com.melih.repository.interactors.base.State + +/** + * Base [ViewModel] for view models that will use [PagedList]. + * + * Since data handling is done via [be.mediahuis.core.base.paging.BasePagingDataSource], this view model doesn't need + * a [kotlinx.coroutines.channels.ReceiveChannel] and will not provide any default operations of data, but instead will + * provde [pagedList] which should be observed and submitted. + * + * If paging won't be used, use [BaseViewModel] instead. + */ +abstract class BasePagingViewModel : ViewModel() { + + // region Abstractions + + abstract val factory: BasePagingFactory + abstract val config: PagedList.Config + // endregion + + // region Properties + + /** + * Observe [stateData] to get notified of state of data + */ + val stateData: LiveData by lazy { + Transformations.switchMap(factory.currentSource) { + it.stateData + } + } + + /** + * Observe [errorData] to get notified if an error occurs + */ + val errorData: LiveData by lazy { + Transformations.switchMap(factory.currentSource) { + it.reasonData + } + } + + /** + * Observe [pagedList] to submit list it provides + */ + val pagedList: LiveData> by lazy { + factory.toLiveData(config) + } + // endregion + + // region Functions + + fun refresh() { + factory.currentSource.value?.invalidate() + } + + /** + * Retry loading data, incase there's difference between refresh and retry, should go here + */ + fun retry() { + factory.currentSource.value?.invalidate() + } + // endregion +} diff --git a/core/src/main/kotlin/com/melih/core/base/viewmodel/BaseViewModel.kt b/core/src/main/kotlin/com/melih/core/base/viewmodel/BaseViewModel.kt index d30406b..2e3246b 100644 --- a/core/src/main/kotlin/com/melih/core/base/viewmodel/BaseViewModel.kt +++ b/core/src/main/kotlin/com/melih/core/base/viewmodel/BaseViewModel.kt @@ -3,8 +3,10 @@ package com.melih.core.base.viewmodel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.melih.repository.interactors.base.Reason -import com.melih.repository.interactors.base.Result +import com.melih.repository.interactors.base.State +import kotlinx.coroutines.launch /** * Base [ViewModel] for view models that will process data. @@ -15,13 +17,19 @@ abstract class BaseViewModel : ViewModel() { // region Abstractions - abstract fun loadData() + abstract suspend fun loadData() // endregion + init { + viewModelScope.launch { + loadData() + } + } + // region Properties private val _successData = MutableLiveData() - private val _stateData = MutableLiveData() + private val _stateData = MutableLiveData() private val _errorData = MutableLiveData() /** @@ -33,7 +41,7 @@ abstract class BaseViewModel : ViewModel() { /** * Observe [stateData] to get notified of state of data */ - val stateData: LiveData + val stateData: LiveData get() = _stateData /** @@ -59,7 +67,7 @@ abstract class BaseViewModel : ViewModel() { * * @param state state of operation */ - protected fun handleState(state: Result.State) { + protected fun handleState(state: State) { _stateData.value = state } @@ -76,14 +84,18 @@ abstract class BaseViewModel : ViewModel() { * Reload data */ fun refresh() { - loadData() + viewModelScope.launch { + loadData() + } } /** * Retry loading data, incase there's difference between refresh and retry, should go here */ fun retry() { - loadData() + viewModelScope.launch { + loadData() + } } // endregion } diff --git a/core/src/main/kotlin/com/melih/core/di/CoreComponent.kt b/core/src/main/kotlin/com/melih/core/di/CoreComponent.kt index 859796c..bc49abc 100644 --- a/core/src/main/kotlin/com/melih/core/di/CoreComponent.kt +++ b/core/src/main/kotlin/com/melih/core/di/CoreComponent.kt @@ -1,8 +1,8 @@ package com.melih.core.di import android.app.Application +import android.content.Context import android.net.NetworkInfo -import com.melih.repository.persistence.LaunchesDatabase import dagger.BindsInstance import dagger.Component import javax.inject.Singleton @@ -11,9 +11,9 @@ import javax.inject.Singleton @Component(modules = [CoreModule::class]) interface CoreComponent { - fun getNetworkInfo(): NetworkInfo? + fun getAppContext(): Context - fun getLaunchesDatabase(): LaunchesDatabase + fun getNetworkInfo(): NetworkInfo? @Component.Factory interface Factory { diff --git a/core/src/main/kotlin/com/melih/core/di/CoreModule.kt b/core/src/main/kotlin/com/melih/core/di/CoreModule.kt index 1867c58..bc8aab2 100644 --- a/core/src/main/kotlin/com/melih/core/di/CoreModule.kt +++ b/core/src/main/kotlin/com/melih/core/di/CoreModule.kt @@ -4,23 +4,16 @@ import android.app.Application import android.content.Context import android.net.ConnectivityManager import android.net.NetworkInfo -import androidx.room.Room -import com.melih.repository.persistence.DB_NAME -import com.melih.repository.persistence.LaunchesDatabase import dagger.Module import dagger.Provides -import javax.inject.Singleton @Module class CoreModule { @Provides - fun provideNetworkInfo(app: Application): NetworkInfo? = - (app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo + fun proivdeAppContext(app: Application): Context = app.applicationContext @Provides - @Singleton - fun provideLaunchesDatabase(app: Application) = - Room.databaseBuilder(app.applicationContext, LaunchesDatabase::class.java, DB_NAME) - .build() + fun provideNetworkInfo(app: Application): NetworkInfo? = + (app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo } diff --git a/core/src/main/kotlin/com/melih/core/extensions/UtilityExtension.kt b/core/src/main/kotlin/com/melih/core/extensions/UtilityExtension.kt index 0b69ae2..c57840f 100644 --- a/core/src/main/kotlin/com/melih/core/extensions/UtilityExtension.kt +++ b/core/src/main/kotlin/com/melih/core/extensions/UtilityExtension.kt @@ -1,5 +1,6 @@ package com.melih.core.extensions +import android.view.MenuItem import androidx.appcompat.widget.SearchView import com.melih.core.utils.ClearFocusQueryTextListener @@ -8,4 +9,24 @@ import com.melih.core.utils.ClearFocusQueryTextListener */ fun CharSequence.containsIgnoreCase(other: CharSequence) = contains(other, true) +/** + * Adds [ClearFocusQueryTextListener] as [SearchView.OnQueryTextListener] + */ fun SearchView.setOnQueryChangedListener(block: (String?) -> Unit) = setOnQueryTextListener(ClearFocusQueryTextListener(this, block)) + +/** + * Shortening set menu item expands / collapses + */ +fun MenuItem.onExpandOrCollapse(onExpand: () -> Unit, onCollapse: () -> Unit) { + setOnActionExpandListener(object : MenuItem.OnActionExpandListener { + override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { + onCollapse() + return true + } + + override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + onExpand() + return true + } + }) +} diff --git a/core/src/main/kotlin/com/melih/core/utils/SnackbarBehaviour.kt b/core/src/main/kotlin/com/melih/core/utils/SnackbarBehaviour.kt index 1bd4a28..26291ae 100644 --- a/core/src/main/kotlin/com/melih/core/utils/SnackbarBehaviour.kt +++ b/core/src/main/kotlin/com/melih/core/utils/SnackbarBehaviour.kt @@ -9,6 +9,7 @@ import com.google.android.material.snackbar.Snackbar /** * Simple behaviour for pushing views when snackbar is animating so none of views will remain under snackbar */ +@Suppress("UNUSED_PARAMETER") class SnackbarBehaviour constructor( context: Context, attributeSet: AttributeSet diff --git a/core/src/test/kotlin/com/melih/core/BaseTestWithMainThread.kt b/core/src/test/kotlin/com/melih/core/BaseTestWithMainThread.kt index 203a012..d8d68fb 100644 --- a/core/src/test/kotlin/com/melih/core/BaseTestWithMainThread.kt +++ b/core/src/test/kotlin/com/melih/core/BaseTestWithMainThread.kt @@ -6,19 +6,19 @@ import androidx.lifecycle.LiveData import com.melih.core.observers.OneShotObserverWithLifecycle import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach -import java.util.concurrent.Executors import kotlin.coroutines.suspendCoroutine +@UseExperimental(ExperimentalCoroutinesApi::class) abstract class BaseTestWithMainThread { - private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + + private val dispatcher = TestCoroutineDispatcher() @BeforeEach - @ExperimentalCoroutinesApi fun setUp() { Dispatchers.setMain(dispatcher) ArchTaskExecutor.getInstance() @@ -32,12 +32,12 @@ abstract class BaseTestWithMainThread { } @AfterEach - @ExperimentalCoroutinesApi fun tearDown() { - Dispatchers.resetMain() - dispatcher.close() ArchTaskExecutor.getInstance() .setDelegate(null) + + Dispatchers.resetMain() + dispatcher.cleanupTestCoroutines() } } diff --git a/core/src/test/kotlin/com/melih/core/base/BaseViewModelTest.kt b/core/src/test/kotlin/com/melih/core/base/BaseViewModelTest.kt index 4436517..b195388 100644 --- a/core/src/test/kotlin/com/melih/core/base/BaseViewModelTest.kt +++ b/core/src/test/kotlin/com/melih/core/base/BaseViewModelTest.kt @@ -1,37 +1,32 @@ package com.melih.core.base +import com.melih.core.BaseTestWithMainThread import com.melih.core.base.viewmodel.BaseViewModel +import io.mockk.coVerify import io.mockk.spyk -import io.mockk.verify import org.junit.jupiter.api.Test -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class BaseViewModelTest { - - val baseVm = spyk(TestViewModel()) +class BaseViewModelTest : BaseTestWithMainThread() { @Test fun `refresh should invoke loadData`() { + val baseVm = spyk(TestViewModel()) baseVm.refresh() - verify(exactly = 1) { baseVm.loadData() } + coVerify(exactly = 1) { baseVm.loadData() } } @Test fun `retry should invoke loadData`() { + val baseVm = spyk(TestViewModel()) baseVm.retry() - verify(exactly = 1) { baseVm.loadData() } + coVerify(exactly = 1) { baseVm.loadData() } } } -class TestViewModel : BaseViewModel() { - override public fun loadData() { +class TestViewModel : BaseViewModel() { + override suspend fun loadData() { // no - op } - } diff --git a/core/src/test/kotlin/com/melih/core/paging/BasePagingDataSourceTest.kt b/core/src/test/kotlin/com/melih/core/paging/BasePagingDataSourceTest.kt new file mode 100644 index 0000000..5750ad1 --- /dev/null +++ b/core/src/test/kotlin/com/melih/core/paging/BasePagingDataSourceTest.kt @@ -0,0 +1,166 @@ +@file:UseExperimental(ExperimentalCoroutinesApi::class) + +package com.melih.core.paging + +import androidx.paging.PageKeyedDataSource +import com.melih.core.BaseTestWithMainThread +import com.melih.core.base.paging.BasePagingDataSource +import com.melih.core.testObserve +import com.melih.repository.interactors.base.Failure +import com.melih.repository.interactors.base.GenericError +import com.melih.repository.interactors.base.Result +import com.melih.repository.interactors.base.State +import com.melih.repository.interactors.base.Success +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.shouldBeInstanceOf +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class BasePagingDataSourceTest : BaseTestWithMainThread() { + + val source = spyk(TestSource()) + val failureSource = spyk(TestFailureSource()) + + val data = 10 + val errorMessage = "Generic Error" + + @Nested + inner class BasePagingSource { + + @Nested + inner class LoadInitial { + + @Test + + fun `should update state accordingly`() { + val params = mockk>(relaxed = true) + val callback = mockk>(relaxed = true) + + runBlocking { + + // Fake loading + source.loadInitial(params, callback) + + source.stateData.testObserve { + it shouldBeInstanceOf State.Loading::class + } + } + } + + @Test + + fun `should update error Error accordingly`() { + val params = PageKeyedDataSource.LoadInitialParams(10, false) + val callback = mockk>(relaxed = true) + + runBlocking { + + // Fake loading + failureSource.loadInitial(params, callback) + + failureSource.reasonData.testObserve { + it shouldBeInstanceOf GenericError::class + } + } + } + } + + @Nested + inner class LoadAfter { + + @Test + + fun `should update state accordingly`() { + val params = PageKeyedDataSource.LoadParams(2, 10) + val callback = mockk>(relaxed = true) + + runBlocking { + + // Fake loading + source.loadAfter(params, callback) + + source.stateData.testObserve { + it shouldBeInstanceOf State.Loading::class + } + } + } + + @Test + + fun `should update error Error accordingly`() { + val params = PageKeyedDataSource.LoadParams(2, 10) + val callback = mockk>(relaxed = true) + + runBlocking { + + // Fake loading + failureSource.loadAfter(params, callback) + + failureSource.reasonData.testObserve { + it shouldBeInstanceOf GenericError::class + } + } + } + } + + @Test + + fun `should use loadDataForPage in loadInitial and transform emmited value`() { + val params = mockk>(relaxed = true) + val callback = mockk>(relaxed = true) + + // Fake loading + source.loadInitial(params, callback) + + // Make sure load initial called only once + verify(exactly = 1) { source.loadDataForPage(any()) } + + // Notified callback + verify(exactly = 1) { callback.onResult(any(), any(), any()) } + } + + @Test + + fun `should use loadDataForPage in loadAfter and transform emmited value`() { + val params = PageKeyedDataSource.LoadParams(2, 10) + val callback = mockk>(relaxed = true) + + // Fake loading + source.loadAfter(params, callback) + + // Make sure load initial called only once + verify(exactly = 1) { source.loadDataForPage(any()) } + + // Notified callback + verify(exactly = 1) { callback.onResult(any(), any()) } + } + } + + inner class TestSource : BasePagingDataSource() { + + + val result = flow { + emit(State.Loading()) + emit(Success(listOf(data))) + } + + + override fun loadDataForPage(page: Int): Flow>> = result + } + + inner class TestFailureSource : BasePagingDataSource() { + + val result = flow { + emit(State.Loading()) + emit(Failure(GenericError())) + } + + override fun loadDataForPage(page: Int): Flow>> = result + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/com/melih/core/paging/BasePagingFactoryTest.kt b/core/src/test/kotlin/com/melih/core/paging/BasePagingFactoryTest.kt new file mode 100644 index 0000000..5829189 --- /dev/null +++ b/core/src/test/kotlin/com/melih/core/paging/BasePagingFactoryTest.kt @@ -0,0 +1,33 @@ +package com.melih.core.paging + +import com.melih.core.BaseTestWithMainThread +import com.melih.core.base.paging.BasePagingDataSource +import com.melih.core.base.paging.BasePagingFactory +import com.melih.core.testObserve +import io.mockk.mockk +import io.mockk.spyk +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.shouldEqual +import org.junit.jupiter.api.Test + +class BasePagingFactoryTest : BaseTestWithMainThread() { + + val factory = spyk(TestFactory()) + + @Test + fun `create should update current source when it creates a new one`() { + val source = factory.create() + + runBlocking { + factory.currentSource.testObserve { + it shouldEqual source + } + } + } + + inner class TestFactory : BasePagingFactory() { + + override fun createSource(): BasePagingDataSource = mockk(relaxed = true) + + } +} diff --git a/docs/dependency_hierarchy.png b/docs/dependency_hierarchy.png new file mode 100644 index 0000000000000000000000000000000000000000..3cd50453cfd29a907a0ccf575332423043f6d66a GIT binary patch literal 38740 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 Xahzc@B1BcNo(>_gx9f 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 Gp#K8|W6Pfa literal 0 HcmV?d00001 diff --git a/docs/module_graph.png b/docs/module_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..93bab7997822958c62652e946459c980b4d6a2c1 GIT binary patch literal 39188 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 literal 0 HcmV?d00001 diff --git a/features/detail/build.gradle b/features/detail/build.gradle index e380aa6..0b93486 100644 --- a/features/detail/build.gradle +++ b/features/detail/build.gradle @@ -15,8 +15,5 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - testImplementation testLibraries.jUnitApi - testImplementation testLibraries.mockk - testImplementation testLibraries.kluent testImplementation testLibraries.coroutinesTest } diff --git a/features/detail/jacoco.exec b/features/detail/jacoco.exec new file mode 100644 index 0000000000000000000000000000000000000000..b1922d8c8e4bfe1496e0078b4c30eb6f2ffe588c GIT binary patch literal 2651 zcmZQPa6o`vfI-eTH77HpSl8DnK(D;mNUtO{S1&I|*U;EB&A`GinSp^ZhpR)&8$uiU z3GO<}?o#qDuYDypq(S#AG15 zSl>Oh#3!*dFFC^{wIne!r&uK*u_!UOSg@IukrA(l&5X=UObrYS!5-a{t_-0Irw&^W zC!3iVr0F0?49 zmbR_TIk`$Bc^hTr9sWYgBE>8z#n>2JEy?k20mrZ$@41VD76(I_0x#9LQD!*>g{|O< z>?Vh)VH_Vw4RQh=PEIyAO-=)QSdaBh4=9H9SpTmdwjNHkFt@NUuuKGdlr1F)LYEKa GBn<#og|7wx literal 0 HcmV?d00001 diff --git a/features/detail/src/main/kotlin/com/melih/detail/di/DetailContributor.kt b/features/detail/src/main/kotlin/com/melih/detail/di/DetailContributor.kt index 22a2c54..ff9261e 100644 --- a/features/detail/src/main/kotlin/com/melih/detail/di/DetailContributor.kt +++ b/features/detail/src/main/kotlin/com/melih/detail/di/DetailContributor.kt @@ -1,7 +1,9 @@ package com.melih.detail.di import com.melih.detail.di.modules.DetailBinds +import com.melih.detail.di.modules.DetailProvides import com.melih.detail.ui.DetailFragment +import com.melih.list.di.scopes.DetailFragmentScope import dagger.Module import dagger.android.ContributesAndroidInjector @@ -15,9 +17,11 @@ abstract class DetailContributor { @ContributesAndroidInjector( modules = [ - DetailBinds::class + DetailBinds::class, + DetailProvides::class ] ) + @DetailFragmentScope abstract fun detailFragment(): DetailFragment // endregion } diff --git a/features/detail/src/main/kotlin/com/melih/detail/di/DetailModule.kt b/features/detail/src/main/kotlin/com/melih/detail/di/DetailModule.kt new file mode 100644 index 0000000..eab7f69 --- /dev/null +++ b/features/detail/src/main/kotlin/com/melih/detail/di/DetailModule.kt @@ -0,0 +1,24 @@ +package com.melih.detail.di + +import com.melih.detail.ui.DetailActivity +import com.melih.list.di.scopes.DetailScope +import dagger.Module +import dagger.android.ContributesAndroidInjector + +/** + * Contributes fragments & view models in this module + */ +@Module +abstract class DetailModule { + + // region Contributes + + @ContributesAndroidInjector( + modules = [ + DetailContributor::class + ] + ) + @DetailScope + abstract fun detailActivity(): DetailActivity + // endregion +} diff --git a/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailBinds.kt b/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailBinds.kt index 578d247..934faa2 100644 --- a/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailBinds.kt +++ b/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailBinds.kt @@ -6,7 +6,6 @@ import com.melih.detail.ui.DetailViewModel import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap -import kotlinx.coroutines.ExperimentalCoroutinesApi @Module abstract class DetailBinds { @@ -16,7 +15,6 @@ abstract class DetailBinds { @Binds @IntoMap @ViewModelKey(DetailViewModel::class) - @ExperimentalCoroutinesApi abstract fun detailViewModel(detailViewModel: DetailViewModel): ViewModel // endregion } diff --git a/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailProvides.kt b/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailProvides.kt new file mode 100644 index 0000000..3ee52f0 --- /dev/null +++ b/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailProvides.kt @@ -0,0 +1,21 @@ +package com.melih.detail.di.modules + +import androidx.navigation.fragment.navArgs +import com.melih.detail.ui.DetailFragment +import com.melih.detail.ui.DetailFragmentArgs +import com.melih.repository.interactors.GetLaunchDetails +import dagger.Module +import dagger.Provides + +@Module +class DetailProvides { + + /** + * Provides launch detail params + */ + @Provides + fun provideGetLaunchDetailParams(fragment: DetailFragment): GetLaunchDetails.Params { + val args: DetailFragmentArgs by fragment.navArgs() + return GetLaunchDetails.Params(args.launchId) + } +} diff --git a/features/detail/src/main/kotlin/com/melih/detail/di/scopes/DetailFragmentScope.kt b/features/detail/src/main/kotlin/com/melih/detail/di/scopes/DetailFragmentScope.kt new file mode 100644 index 0000000..c5a983b --- /dev/null +++ b/features/detail/src/main/kotlin/com/melih/detail/di/scopes/DetailFragmentScope.kt @@ -0,0 +1,7 @@ +package com.melih.list.di.scopes + +import javax.inject.Scope + +@Scope +@Retention(AnnotationRetention.BINARY) +annotation class DetailFragmentScope diff --git a/features/detail/src/main/kotlin/com/melih/detail/di/scopes/DetailScope.kt b/features/detail/src/main/kotlin/com/melih/detail/di/scopes/DetailScope.kt new file mode 100644 index 0000000..e264fde --- /dev/null +++ b/features/detail/src/main/kotlin/com/melih/detail/di/scopes/DetailScope.kt @@ -0,0 +1,7 @@ +package com.melih.list.di.scopes + +import javax.inject.Scope + +@Scope +@Retention(AnnotationRetention.BINARY) +annotation class DetailScope diff --git a/features/detail/src/main/kotlin/com/melih/detail/ui/DetailActivity.kt b/features/detail/src/main/kotlin/com/melih/detail/ui/DetailActivity.kt index 519c20e..0e7fbae 100644 --- a/features/detail/src/main/kotlin/com/melih/detail/ui/DetailActivity.kt +++ b/features/detail/src/main/kotlin/com/melih/detail/ui/DetailActivity.kt @@ -6,7 +6,6 @@ import com.melih.core.actions.EXTRA_LAUNCH_ID import com.melih.core.base.lifecycle.BaseActivity import com.melih.detail.R import com.melih.detail.databinding.DetailActivityBinding -import kotlinx.coroutines.ExperimentalCoroutinesApi const val INVALID_LAUNCH_ID = -1L @@ -14,13 +13,12 @@ class DetailActivity : BaseActivity() { // region Functions - @ExperimentalCoroutinesApi override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setSupportActionBar(binding.toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true); - supportActionBar?.setDisplayShowHomeEnabled(true); + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) } override fun getLayoutId(): Int = R.layout.activity_detail diff --git a/features/detail/src/main/kotlin/com/melih/detail/ui/DetailFragment.kt b/features/detail/src/main/kotlin/com/melih/detail/ui/DetailFragment.kt index c7bdfe6..b9a9ca5 100644 --- a/features/detail/src/main/kotlin/com/melih/detail/ui/DetailFragment.kt +++ b/features/detail/src/main/kotlin/com/melih/detail/ui/DetailFragment.kt @@ -3,38 +3,28 @@ package com.melih.detail.ui import android.os.Bundle import android.text.method.ScrollingMovementMethod import android.view.View -import androidx.navigation.fragment.navArgs import com.melih.core.base.lifecycle.BaseDaggerFragment import com.melih.core.extensions.createFor import com.melih.core.extensions.observe import com.melih.detail.R import com.melih.detail.databinding.DetailBinding -import kotlinx.coroutines.ExperimentalCoroutinesApi -import timber.log.Timber class DetailFragment : BaseDaggerFragment() { // region Properties - private val args: DetailFragmentArgs by navArgs() - - @ExperimentalCoroutinesApi private val viewModel: DetailViewModel get() = viewModelFactory.createFor(this) // endregion // region Functions - @ExperimentalCoroutinesApi override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.tvDescription.movementMethod = ScrollingMovementMethod() binding.viewModel = viewModel - viewModel.createParamsFor(args.launchId) - viewModel.loadData() - // Observing error to show toast with retry action observe(viewModel.errorData) { showSnackbarWithAction(it) { diff --git a/features/detail/src/main/kotlin/com/melih/detail/ui/DetailViewModel.kt b/features/detail/src/main/kotlin/com/melih/detail/ui/DetailViewModel.kt index ad17cbb..83c28dd 100644 --- a/features/detail/src/main/kotlin/com/melih/detail/ui/DetailViewModel.kt +++ b/features/detail/src/main/kotlin/com/melih/detail/ui/DetailViewModel.kt @@ -1,24 +1,20 @@ package com.melih.detail.ui import androidx.lifecycle.Transformations -import androidx.lifecycle.viewModelScope import com.melih.core.base.viewmodel.BaseViewModel import com.melih.repository.entities.LaunchEntity import com.melih.repository.interactors.GetLaunchDetails -import kotlinx.coroutines.ExperimentalCoroutinesApi +import com.melih.repository.interactors.base.handle import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.launch import javax.inject.Inject -@ExperimentalCoroutinesApi class DetailViewModel @Inject constructor( - private val getLaunchDetails: GetLaunchDetails + private val getLaunchDetails: GetLaunchDetails, + private val getLaunchDetailsParams: GetLaunchDetails.Params ) : BaseViewModel() { // region Properties - private var params = GetLaunchDetails.Params(INVALID_LAUNCH_ID) - val rocketName = Transformations.map(successData) { it.rocket.name } @@ -38,18 +34,12 @@ class DetailViewModel @Inject constructor( // region Functions - fun createParamsFor(id: Long) { - params = GetLaunchDetails.Params(id) - } - /** * Triggering interactor in view model scope */ - override fun loadData() { - viewModelScope.launch { - getLaunchDetails(params).collect { - it.handle(::handleState, ::handleFailure, ::handleSuccess) - } + override suspend fun loadData() { + getLaunchDetails(getLaunchDetailsParams).collect { + it.handle(::handleState, ::handleFailure, ::handleSuccess) } } // endregion diff --git a/features/detail/src/test/java/com/melih/detail/BaseTestWithMainThread.kt b/features/detail/src/test/java/com/melih/detail/BaseTestWithMainThread.kt index 94e3614..82056d4 100644 --- a/features/detail/src/test/java/com/melih/detail/BaseTestWithMainThread.kt +++ b/features/detail/src/test/java/com/melih/detail/BaseTestWithMainThread.kt @@ -1,4 +1,4 @@ -package com.melih.list +package com.melih.detail import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -8,19 +8,17 @@ import kotlinx.coroutines.test.setMain import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach +@UseExperimental(ExperimentalCoroutinesApi::class) abstract class BaseTestWithMainThread { - @ExperimentalCoroutinesApi protected val dispatcher = TestCoroutineDispatcher() @BeforeEach - @ExperimentalCoroutinesApi fun setUp() { Dispatchers.setMain(dispatcher) } @AfterEach - @ExperimentalCoroutinesApi fun tearDown() { Dispatchers.resetMain() dispatcher.cleanupTestCoroutines() diff --git a/features/detail/src/test/java/com/melih/detail/DetailViewModelTest.kt b/features/detail/src/test/java/com/melih/detail/DetailViewModelTest.kt index 0e6a4d4..69ad7c9 100644 --- a/features/detail/src/test/java/com/melih/detail/DetailViewModelTest.kt +++ b/features/detail/src/test/java/com/melih/detail/DetailViewModelTest.kt @@ -1,7 +1,6 @@ package com.melih.detail import com.melih.detail.ui.DetailViewModel -import com.melih.list.BaseTestWithMainThread import com.melih.repository.interactors.GetLaunchDetails import io.mockk.mockk import io.mockk.slot @@ -17,21 +16,20 @@ import org.junit.jupiter.api.Test * * See [testing documentation](http://d.android.com/tools/testing). */ +@UseExperimental(ExperimentalCoroutinesApi::class) class DetailViewModelTest : BaseTestWithMainThread() { private val getLaunchDetails: GetLaunchDetails = mockk(relaxed = true) + private val getLaunchDetailsParams = GetLaunchDetails.Params(1013) - @ExperimentalCoroutinesApi - private val viewModel = spyk(DetailViewModel(getLaunchDetails)) + private val viewModel = spyk(DetailViewModel(getLaunchDetails, getLaunchDetailsParams)) @Test - @ExperimentalCoroutinesApi fun `loadData should invoke getLauchDetails with provided params`() { dispatcher.runBlockingTest { val paramsSlot = slot() - viewModel.createParamsFor(1013) viewModel.loadData() // init should have called it already due to creation above diff --git a/features/list/.gitignore b/features/launches/.gitignore similarity index 100% rename from features/list/.gitignore rename to features/launches/.gitignore diff --git a/features/list/build.gradle b/features/launches/build.gradle similarity index 68% rename from features/list/build.gradle rename to features/launches/build.gradle index df7af5b..1128480 100644 --- a/features/list/build.gradle +++ b/features/launches/build.gradle @@ -7,8 +7,7 @@ apply from: "$rootProject.projectDir/scripts/feature_module.gradle" dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - testImplementation testLibraries.jUnitApi - testImplementation testLibraries.mockk - testImplementation testLibraries.kluent + implementation libraries.paging + testImplementation testLibraries.coroutinesTest } diff --git a/features/list/consumer-rules.pro b/features/launches/consumer-rules.pro similarity index 100% rename from features/list/consumer-rules.pro rename to features/launches/consumer-rules.pro diff --git a/features/launches/jacoco.exec b/features/launches/jacoco.exec new file mode 100644 index 0000000000000000000000000000000000000000..65d24583fde61e2f3fb3f8bbafd36c6c14f60818 GIT binary patch literal 2302 zcmZQPa6o`vfI-eTH77HpSl8DnK(D;mNUtO{S1&I|*U-}3FxfQCgn@xEhpUb8DTHQ^ z5ZrZ^$$4+tzXJ@~$@#hZxggE@$@xX8`bmk!srqG^spYx(DXBU7PC#xLkmn2H2{8N- zWa`ZMTX=irVFq0iG^!YKGcz;db!nP;Qc{vZS_;^O9hN!}+KPi*m+}Zcj!MwlQnd0m zgE7LTMX3e(#hE4fMV0!Qc_pbuiOE2AvA%n1iBDo_UUEiiu}VNFtIR%c=Fg}2z?@1aIKlmYfjy=_1QlNV;1O9V}n$43$Q1z|NaG`|6UlDo;6ELG&V>vFavw?21fvd=295i z$ygS9QEHZAVwP-Z1oq>thg=Z)F~hKo*yL2RH1kALQ-~*j=0ND*+Cw{HanzB?iHWHe zX{N~_Ps;I$+CpdvbwS=aAus$q7R6Ig%osbwx)>)NNFce0Be$9*RP&@*8l;+~7#To3 lsj(13YrUhOsuK+6^x#lncekb>oK|EvIZO@X_&{nn0RXUjAT|I1 literal 0 HcmV?d00001 diff --git a/features/list/proguard-rules.pro b/features/launches/proguard-rules.pro similarity index 100% rename from features/list/proguard-rules.pro rename to features/launches/proguard-rules.pro diff --git a/features/list/sampledata/launches.json b/features/launches/sampledata/launches.json similarity index 100% rename from features/list/sampledata/launches.json rename to features/launches/sampledata/launches.json diff --git a/features/list/src/main/AndroidManifest.xml b/features/launches/src/main/AndroidManifest.xml similarity index 100% rename from features/list/src/main/AndroidManifest.xml rename to features/launches/src/main/AndroidManifest.xml diff --git a/features/list/src/main/kotlin/com/melih/list/di/LaunchesContributor.kt b/features/launches/src/main/kotlin/com/melih/list/di/LaunchesContributor.kt similarity index 79% rename from features/list/src/main/kotlin/com/melih/list/di/LaunchesContributor.kt rename to features/launches/src/main/kotlin/com/melih/list/di/LaunchesContributor.kt index a3065ee..9ab2f11 100644 --- a/features/list/src/main/kotlin/com/melih/list/di/LaunchesContributor.kt +++ b/features/launches/src/main/kotlin/com/melih/list/di/LaunchesContributor.kt @@ -2,6 +2,7 @@ package com.melih.list.di import com.melih.list.di.modules.LaunchesBinds import com.melih.list.di.modules.LaunchesProvides +import com.melih.list.di.scopes.LaunchesFragmentScope import com.melih.list.ui.LaunchesFragment import dagger.Module import dagger.android.ContributesAndroidInjector @@ -20,6 +21,7 @@ abstract class LaunchesContributor { LaunchesBinds::class ] ) - abstract fun listFragment(): LaunchesFragment + @LaunchesFragmentScope + abstract fun launchesFragment(): LaunchesFragment // endregion } diff --git a/features/launches/src/main/kotlin/com/melih/list/di/LaunchesFeatureModule.kt b/features/launches/src/main/kotlin/com/melih/list/di/LaunchesFeatureModule.kt new file mode 100644 index 0000000..68ae1be --- /dev/null +++ b/features/launches/src/main/kotlin/com/melih/list/di/LaunchesFeatureModule.kt @@ -0,0 +1,24 @@ +package com.melih.list.di + +import com.melih.list.di.scopes.LaunchesScope +import com.melih.list.ui.LaunchesActivity +import dagger.Module +import dagger.android.ContributesAndroidInjector + +/** + * Contributes fragments & view models in this module + */ +@Module +abstract class LaunchesFeatureModule { + + // region Contributes + + @ContributesAndroidInjector( + modules = [ + LaunchesContributor::class + ] + ) + @LaunchesScope + abstract fun launchesActivity(): LaunchesActivity + // endregion +} diff --git a/features/list/src/main/kotlin/com/melih/list/di/modules/LaunchesBinds.kt b/features/launches/src/main/kotlin/com/melih/list/di/modules/LaunchesBinds.kt similarity index 76% rename from features/list/src/main/kotlin/com/melih/list/di/modules/LaunchesBinds.kt rename to features/launches/src/main/kotlin/com/melih/list/di/modules/LaunchesBinds.kt index 35a77a4..a3a66c2 100644 --- a/features/list/src/main/kotlin/com/melih/list/di/modules/LaunchesBinds.kt +++ b/features/launches/src/main/kotlin/com/melih/list/di/modules/LaunchesBinds.kt @@ -2,11 +2,10 @@ package com.melih.list.di.modules import androidx.lifecycle.ViewModel import com.melih.core.di.keys.ViewModelKey -import com.melih.list.ui.LaunchesViewModel +import com.melih.list.ui.vm.LaunchesViewModel import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap -import kotlinx.coroutines.ExperimentalCoroutinesApi @Module abstract class LaunchesBinds { @@ -16,7 +15,6 @@ abstract class LaunchesBinds { @Binds @IntoMap @ViewModelKey(LaunchesViewModel::class) - @ExperimentalCoroutinesApi abstract fun listViewModel(listViewModel: LaunchesViewModel): ViewModel // endregion } diff --git a/features/launches/src/main/kotlin/com/melih/list/di/modules/LaunchesProvides.kt b/features/launches/src/main/kotlin/com/melih/list/di/modules/LaunchesProvides.kt new file mode 100644 index 0000000..59cff00 --- /dev/null +++ b/features/launches/src/main/kotlin/com/melih/list/di/modules/LaunchesProvides.kt @@ -0,0 +1,24 @@ +package com.melih.list.di.modules + +import androidx.paging.Config +import com.melih.repository.interactors.DEFAULT_LAUNCHES_AMOUNT +import com.melih.repository.interactors.GetLaunches +import dagger.Module +import dagger.Provides + +@Module +class LaunchesProvides { + + /** + * Provides lauches, using default value of 15 + */ + @Provides + fun provideGetLaunchesParams() = GetLaunches.Params(page = 0) + + @Provides + fun getPagingConfig() = Config( + DEFAULT_LAUNCHES_AMOUNT, + prefetchDistance = 2, + enablePlaceholders = false + ) +} diff --git a/features/launches/src/main/kotlin/com/melih/list/di/scopes/LaunchesFragmentScope.kt b/features/launches/src/main/kotlin/com/melih/list/di/scopes/LaunchesFragmentScope.kt new file mode 100644 index 0000000..e5b898b --- /dev/null +++ b/features/launches/src/main/kotlin/com/melih/list/di/scopes/LaunchesFragmentScope.kt @@ -0,0 +1,7 @@ +package com.melih.list.di.scopes + +import javax.inject.Scope + +@Scope +@Retention(AnnotationRetention.BINARY) +annotation class LaunchesFragmentScope diff --git a/features/launches/src/main/kotlin/com/melih/list/di/scopes/LaunchesScope.kt b/features/launches/src/main/kotlin/com/melih/list/di/scopes/LaunchesScope.kt new file mode 100644 index 0000000..31b4887 --- /dev/null +++ b/features/launches/src/main/kotlin/com/melih/list/di/scopes/LaunchesScope.kt @@ -0,0 +1,7 @@ +package com.melih.list.di.scopes + +import javax.inject.Scope + +@Scope +@Retention(AnnotationRetention.BINARY) +annotation class LaunchesScope diff --git a/features/list/src/main/kotlin/com/melih/list/ui/LaunchesActivity.kt b/features/launches/src/main/kotlin/com/melih/list/ui/LaunchesActivity.kt similarity index 89% rename from features/list/src/main/kotlin/com/melih/list/ui/LaunchesActivity.kt rename to features/launches/src/main/kotlin/com/melih/list/ui/LaunchesActivity.kt index 3043883..529b4ae 100644 --- a/features/list/src/main/kotlin/com/melih/list/ui/LaunchesActivity.kt +++ b/features/launches/src/main/kotlin/com/melih/list/ui/LaunchesActivity.kt @@ -5,13 +5,11 @@ import androidx.navigation.fragment.NavHostFragment import com.melih.core.base.lifecycle.BaseActivity import com.melih.list.R import com.melih.list.databinding.LaunchesActivityBinding -import kotlinx.coroutines.ExperimentalCoroutinesApi class LaunchesActivity : BaseActivity() { // region Functions - @ExperimentalCoroutinesApi override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/features/launches/src/main/kotlin/com/melih/list/ui/LaunchesFragment.kt b/features/launches/src/main/kotlin/com/melih/list/ui/LaunchesFragment.kt new file mode 100644 index 0000000..8f27757 --- /dev/null +++ b/features/launches/src/main/kotlin/com/melih/list/ui/LaunchesFragment.kt @@ -0,0 +1,97 @@ +package com.melih.list.ui + +import android.os.Bundle +import android.view.View +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.melih.core.actions.Actions +import com.melih.core.base.lifecycle.BaseDaggerFragment +import com.melih.core.extensions.createFor +import com.melih.core.extensions.observe +import com.melih.list.R +import com.melih.list.databinding.ListBinding +import com.melih.list.ui.adapters.LaunchesAdapter +import com.melih.list.ui.vm.LaunchesViewModel +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.interactors.base.State + +class LaunchesFragment : BaseDaggerFragment(), SwipeRefreshLayout.OnRefreshListener { + + // region Properties + + private val viewModel: LaunchesViewModel + get() = viewModelFactory.createFor(this) + + private val launchesAdapter = LaunchesAdapter(::onItemSelected) + // endregion + + // region Functions + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + //setHasOptionsMenu(true) + + binding.rocketList.adapter = launchesAdapter + binding.swipeRefreshLayout.setOnRefreshListener(this) + + observeDataChanges() + } + + //override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + // inflater.inflate(R.menu.menu_rocket_list, menu) + // + // with(menu.findItem(R.id.search)) { + // onExpandOrCollapse(::onSearchExpand, ::onSearchCollapse) + // setSearchQueryListener(actionView as SearchView) + // } + // + // super.onCreateOptionsMenu(menu, inflater) + //} + + private fun observeDataChanges() { + + // Observing state to show loading + observe(viewModel.stateData) { + binding.swipeRefreshLayout.isRefreshing = it is State.Loading + } + + // Observing error to show toast with retry action + observe(viewModel.errorData) { + showSnackbarWithAction(it) { + viewModel.retry() + } + } + + observe(viewModel.pagedList) { + launchesAdapter.submitList(it) + } + + //observe(viewModel.filteredItems) { + // launchesAdapter.submitList(it) + //} + } + + private fun onItemSelected(item: LaunchEntity?) { + startActivity(Actions.openDetailFor(item?.id ?: -1L)) + } + + //private fun onSearchExpand() { + // binding.swipeRefreshLayout.isEnabled = false + //} + + //private fun onSearchCollapse() { + // binding.swipeRefreshLayout.isEnabled = true + //} + + //private fun setSearchQueryListener(searchView: SearchView) { + // searchView.setOnQueryChangedListener { + // viewModel.filterItemListBy(it) + // } + //} + + override fun onRefresh() { + viewModel.refresh() + } + + override fun getLayoutId(): Int = R.layout.fragment_launches + // endregion +} diff --git a/features/list/src/main/kotlin/com/melih/list/ui/LaunchesAdapter.kt b/features/launches/src/main/kotlin/com/melih/list/ui/adapters/LaunchesAdapter.kt similarity index 73% rename from features/list/src/main/kotlin/com/melih/list/ui/LaunchesAdapter.kt rename to features/launches/src/main/kotlin/com/melih/list/ui/adapters/LaunchesAdapter.kt index a47c3e5..1547ef5 100644 --- a/features/list/src/main/kotlin/com/melih/list/ui/LaunchesAdapter.kt +++ b/features/launches/src/main/kotlin/com/melih/list/ui/adapters/LaunchesAdapter.kt @@ -1,14 +1,14 @@ -package com.melih.list.ui +package com.melih.list.ui.adapters import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil -import com.melih.core.base.recycler.BaseListAdapter +import com.melih.core.base.recycler.BasePagingListAdapter import com.melih.core.base.recycler.BaseViewHolder import com.melih.list.databinding.LaunchRowBinding import com.melih.repository.entities.LaunchEntity -class LaunchesAdapter(itemClickListener: (LaunchEntity) -> Unit) : BaseListAdapter( +class LaunchesAdapter(itemClickListener: (LaunchEntity?) -> Unit) : BasePagingListAdapter( object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: LaunchEntity, newItem: LaunchEntity): Boolean = oldItem.id == newItem.id @@ -30,11 +30,11 @@ class LaunchesAdapter(itemClickListener: (LaunchEntity) -> Unit) : BaseListAdapt class LaunchesViewHolder(private val binding: LaunchRowBinding) : BaseViewHolder(binding) { - override fun bind(item: LaunchEntity) { + override fun bind(item: LaunchEntity?) { binding.entity = item - val missions = item.missions - binding.tvDescription.text = if (missions.isNotEmpty()) missions[0].description else "" + val missions = item?.missions + binding.tvDescription.text = if (!missions.isNullOrEmpty()) missions[0].description else "" binding.executePendingBindings() } diff --git a/features/launches/src/main/kotlin/com/melih/list/ui/paging/LaunchesPagingSource.kt b/features/launches/src/main/kotlin/com/melih/list/ui/paging/LaunchesPagingSource.kt new file mode 100644 index 0000000..9ec0bf8 --- /dev/null +++ b/features/launches/src/main/kotlin/com/melih/list/ui/paging/LaunchesPagingSource.kt @@ -0,0 +1,26 @@ +package com.melih.list.ui.paging + +import com.melih.core.base.paging.BasePagingDataSource +import com.melih.repository.entities.LaunchEntity +import com.melih.repository.interactors.GetLaunches +import com.melih.repository.interactors.base.Result +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class LaunchesPagingSource @Inject constructor( + private val getLaunches: GetLaunches, + private val getLaunchesParams: GetLaunches.Params +) : BasePagingDataSource() { + + //region Functions + + @UseExperimental(ExperimentalCoroutinesApi::class) + override fun loadDataForPage(page: Int): Flow>> = + getLaunches( + getLaunchesParams.copy( + page = page + ) + ) + //endregion +} diff --git a/features/launches/src/main/kotlin/com/melih/list/ui/paging/LaunchesPagingSourceFactory.kt b/features/launches/src/main/kotlin/com/melih/list/ui/paging/LaunchesPagingSourceFactory.kt new file mode 100644 index 0000000..d176076 --- /dev/null +++ b/features/launches/src/main/kotlin/com/melih/list/ui/paging/LaunchesPagingSourceFactory.kt @@ -0,0 +1,14 @@ +package com.melih.list.ui.paging + +import com.melih.core.base.paging.BasePagingDataSource +import com.melih.core.base.paging.BasePagingFactory +import com.melih.repository.entities.LaunchEntity +import javax.inject.Inject +import javax.inject.Provider + +class LaunchesPagingSourceFactory @Inject constructor( + private val sourceProvider: Provider +) : BasePagingFactory() { + + override fun createSource(): BasePagingDataSource = sourceProvider.get() +} diff --git a/features/launches/src/main/kotlin/com/melih/list/ui/vm/LaunchesViewModel.kt b/features/launches/src/main/kotlin/com/melih/list/ui/vm/LaunchesViewModel.kt new file mode 100644 index 0000000..4576d9c --- /dev/null +++ b/features/launches/src/main/kotlin/com/melih/list/ui/vm/LaunchesViewModel.kt @@ -0,0 +1,45 @@ +package com.melih.list.ui.vm + +import androidx.paging.PagedList +import com.melih.core.base.paging.BasePagingFactory +import com.melih.core.base.viewmodel.BasePagingViewModel +import com.melih.list.ui.paging.LaunchesPagingSourceFactory +import com.melih.repository.entities.LaunchEntity +import javax.inject.Inject + +class LaunchesViewModel @Inject constructor( + private val launchesPagingSourceFactory: LaunchesPagingSourceFactory, + private val launchesPagingConfig: PagedList.Config +) : BasePagingViewModel() { + + // region Properties + + override val factory: BasePagingFactory + get() = launchesPagingSourceFactory + + override val config: PagedList.Config + get() = launchesPagingConfig + + //private val _filteredItems = MediatorLiveData>() + + //val filteredItems: LiveData> + // get() = _filteredItems + // endregion + + //init { + // _filteredItems.addSource(pagedList, _filteredItems::setValue) + //} + + // region Functions + + //fun filterItemListBy(query: String?) { + // + // _filteredItems.value = if (!query.isNullOrBlank()) { + // pagedList.value + // ?.snapshot() as PagedList + // } else { + // pagedList.value + // } + //} + // endregion +} diff --git a/features/list/src/main/res/anim/item_enter.xml b/features/launches/src/main/res/anim/item_enter.xml similarity index 100% rename from features/list/src/main/res/anim/item_enter.xml rename to features/launches/src/main/res/anim/item_enter.xml diff --git a/features/list/src/main/res/anim/layout_item_enter.xml b/features/launches/src/main/res/anim/layout_item_enter.xml similarity index 100% rename from features/list/src/main/res/anim/layout_item_enter.xml rename to features/launches/src/main/res/anim/layout_item_enter.xml diff --git a/features/list/src/main/res/layout/activity_launches.xml b/features/launches/src/main/res/layout/activity_launches.xml similarity index 100% rename from features/list/src/main/res/layout/activity_launches.xml rename to features/launches/src/main/res/layout/activity_launches.xml diff --git a/features/list/src/main/res/layout/fragment_launches.xml b/features/launches/src/main/res/layout/fragment_launches.xml similarity index 96% rename from features/list/src/main/res/layout/fragment_launches.xml rename to features/launches/src/main/res/layout/fragment_launches.xml index c70a7c6..cac321d 100644 --- a/features/list/src/main/res/layout/fragment_launches.xml +++ b/features/launches/src/main/res/layout/fragment_launches.xml @@ -8,7 +8,7 @@ + type="com.melih.list.ui.vm.LaunchesViewModel" /> (), SwipeRefreshLayout.OnRefreshListener { - - // region Properties - - @ExperimentalCoroutinesApi - private val viewModel: LaunchesViewModel - get() = viewModelFactory.createFor(this) - - private val launchesAdapter = LaunchesAdapter(::onItemSelected) - private val itemList = mutableListOf() - // endregion - - // region Functions - - @ExperimentalCoroutinesApi - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setHasOptionsMenu(true) - - binding.rocketList.adapter = launchesAdapter - binding.swipeRefreshLayout.setOnRefreshListener(this) - - observeDataChanges() - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.menu_rocket_list, menu) - setSearchQueryListener((menu.findItem(R.id.search).actionView as SearchView)) - super.onCreateOptionsMenu(menu, inflater) - } - - @ExperimentalCoroutinesApi - private fun observeDataChanges() { - - // Observing state to show loading - observe(viewModel.stateData) { - binding.swipeRefreshLayout.isRefreshing = it is Result.State.Loading - } - - // Observing error to show toast with retry action - observe(viewModel.errorData) { - showSnackbarWithAction(it) { - viewModel.retry() - } - } - - observe(viewModel.successData) { - itemList.addAll(it) - launchesAdapter.submitList(itemList) - binding.rocketList.scheduleLayoutAnimation() - } - } - - private fun onItemSelected(item: LaunchEntity) { - startActivity(Actions.openDetailFor(item.id)) - } - - private fun setSearchQueryListener(searchView: SearchView) { - searchView.setOnQueryChangedListener { - filterItemListBy(it) - } - } - - private fun filterItemListBy(query: String?) = - if (!query.isNullOrBlank()) { - itemList.filter { - it.rocket.name.containsIgnoreCase(query) || it.missions.any { it.description.containsIgnoreCase(query) } - } - } else { - itemList - } - - @ExperimentalCoroutinesApi - override fun onRefresh() { - viewModel.refresh() - } - - override fun getLayoutId(): Int = R.layout.fragment_launches - // endregion -} diff --git a/features/list/src/main/kotlin/com/melih/list/ui/LaunchesViewModel.kt b/features/list/src/main/kotlin/com/melih/list/ui/LaunchesViewModel.kt deleted file mode 100644 index 4f05f86..0000000 --- a/features/list/src/main/kotlin/com/melih/list/ui/LaunchesViewModel.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.melih.list.ui - -import androidx.lifecycle.viewModelScope -import com.melih.core.base.viewmodel.BaseViewModel -import com.melih.repository.entities.LaunchEntity -import com.melih.repository.interactors.GetLaunches -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.launch -import javax.inject.Inject - -@ExperimentalCoroutinesApi -class LaunchesViewModel @Inject constructor( - private val getLaunches: GetLaunches, - private val getLaunchesParams: GetLaunches.Params -) : BaseViewModel>() { - - // region Initialization - - init { - loadData() - } - // endregion - - // region Functions - - /** - * Triggering interactor in view model scope - */ - override fun loadData() { - viewModelScope.launch { - getLaunches(getLaunchesParams).collect { - it.handle(::handleState, ::handleFailure, ::handleSuccess) - } - } - } - // endregion -} diff --git a/features/list/src/test/kotlin/com/melih/list/LaunchesViewModelTest.kt b/features/list/src/test/kotlin/com/melih/list/LaunchesViewModelTest.kt deleted file mode 100644 index 7a28959..0000000 --- a/features/list/src/test/kotlin/com/melih/list/LaunchesViewModelTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.melih.list - -import com.melih.list.ui.LaunchesViewModel -import com.melih.repository.interactors.GetLaunches -import io.mockk.mockk -import io.mockk.spyk -import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runBlockingTest -import org.junit.jupiter.api.Test - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class LaunchesViewModelTest : BaseTestWithMainThread() { - - val getLaunches: GetLaunches = mockk(relaxed = true) - val getLaunchesParams: GetLaunches.Params = mockk(relaxed = true) - - @Test - @ExperimentalCoroutinesApi - fun `loadData should invoke getLauches with provided params`() { - spyk(LaunchesViewModel(getLaunches, getLaunchesParams)) - - dispatcher.runBlockingTest { - - // init should have called it already due to creation above - verify(exactly = 1) { getLaunches(getLaunchesParams) } - } - } -} diff --git a/repository/build.gradle b/repository/build.gradle index 73ef37e..843f46d 100644 --- a/repository/build.gradle +++ b/repository/build.gradle @@ -25,6 +25,7 @@ dependencies { implementation libraries.okHttpLogger kapt annotationProcessors.roomCompiler + kapt annotationProcessors.moshi testImplementation testLibraries.coroutinesCore testImplementation testLibraries.coroutinesTest diff --git a/repository/src/main/kotlin/com/melih/repository/Repository.kt b/repository/src/main/kotlin/com/melih/repository/Repository.kt index 297ac78..82a9d41 100644 --- a/repository/src/main/kotlin/com/melih/repository/Repository.kt +++ b/repository/src/main/kotlin/com/melih/repository/Repository.kt @@ -8,6 +8,6 @@ import com.melih.repository.interactors.base.Result */ abstract class Repository { - internal abstract suspend fun getNextLaunches(count: Int): Result> + internal abstract suspend fun getNextLaunches(count: Int, page: Int): Result> internal abstract suspend fun getLaunchById(id: Long): Result } diff --git a/repository/src/main/kotlin/com/melih/repository/entities/LaunchEntity.kt b/repository/src/main/kotlin/com/melih/repository/entities/LaunchEntity.kt index c7c3f27..2a7bdbd 100644 --- a/repository/src/main/kotlin/com/melih/repository/entities/LaunchEntity.kt +++ b/repository/src/main/kotlin/com/melih/repository/entities/LaunchEntity.kt @@ -4,8 +4,10 @@ import androidx.room.Entity import androidx.room.PrimaryKey import com.melih.repository.DEFAULT_NAME import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass @Entity(tableName = "Launches") +@JsonClass(generateAdapter = true) data class LaunchEntity( @PrimaryKey val id: Long = 0L, val name: String = DEFAULT_NAME, diff --git a/repository/src/main/kotlin/com/melih/repository/entities/LaunchesEntity.kt b/repository/src/main/kotlin/com/melih/repository/entities/LaunchesEntity.kt index 5119207..eaa55df 100644 --- a/repository/src/main/kotlin/com/melih/repository/entities/LaunchesEntity.kt +++ b/repository/src/main/kotlin/com/melih/repository/entities/LaunchesEntity.kt @@ -1,5 +1,8 @@ package com.melih.repository.entities +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) data class LaunchesEntity( val id: Long = 0L, val launches: List = listOf(), diff --git a/repository/src/main/kotlin/com/melih/repository/entities/LocationEntity.kt b/repository/src/main/kotlin/com/melih/repository/entities/LocationEntity.kt index 13bf125..652c4d3 100644 --- a/repository/src/main/kotlin/com/melih/repository/entities/LocationEntity.kt +++ b/repository/src/main/kotlin/com/melih/repository/entities/LocationEntity.kt @@ -2,13 +2,16 @@ package com.melih.repository.entities import androidx.room.ColumnInfo import com.melih.repository.DEFAULT_NAME +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class LocationEntity( @ColumnInfo(name = "id_location") val id: Long = 0L, @ColumnInfo(name = "name_location") val name: String = DEFAULT_NAME, val pads: List = listOf(PadEntity()) ) +@JsonClass(generateAdapter = true) data class PadEntity( @ColumnInfo(name = "id_pad") val id: Long = 0L, @ColumnInfo(name = "name_pad") val name: String = DEFAULT_NAME, diff --git a/repository/src/main/kotlin/com/melih/repository/entities/MissionEntity.kt b/repository/src/main/kotlin/com/melih/repository/entities/MissionEntity.kt index ab641e8..971ec00 100644 --- a/repository/src/main/kotlin/com/melih/repository/entities/MissionEntity.kt +++ b/repository/src/main/kotlin/com/melih/repository/entities/MissionEntity.kt @@ -3,7 +3,9 @@ package com.melih.repository.entities import androidx.room.ColumnInfo import com.melih.repository.DEFAULT_NAME import com.melih.repository.EMPTY_STRING +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class MissionEntity( @ColumnInfo(name = "id_mission") val id: Long = 0L, @ColumnInfo(name = "name_mission") val name: String = DEFAULT_NAME, diff --git a/repository/src/main/kotlin/com/melih/repository/entities/RocketEntity.kt b/repository/src/main/kotlin/com/melih/repository/entities/RocketEntity.kt index 3f220e5..7fd8c3d 100644 --- a/repository/src/main/kotlin/com/melih/repository/entities/RocketEntity.kt +++ b/repository/src/main/kotlin/com/melih/repository/entities/RocketEntity.kt @@ -4,7 +4,9 @@ import androidx.room.ColumnInfo import com.melih.repository.DEFAULT_NAME import com.melih.repository.EMPTY_STRING import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +@JsonClass(generateAdapter = true) data class RocketEntity( @ColumnInfo(name = "id_rocket") val id: Long = 0L, @ColumnInfo(name = "name_rocket") val name: String = DEFAULT_NAME, diff --git a/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunchDetails.kt b/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunchDetails.kt index c38bffa..7bf13d5 100644 --- a/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunchDetails.kt +++ b/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunchDetails.kt @@ -2,9 +2,12 @@ package com.melih.repository.interactors import com.melih.repository.entities.LaunchEntity import com.melih.repository.interactors.base.BaseInteractor +import com.melih.repository.interactors.base.Failure import com.melih.repository.interactors.base.InteractorParameters import com.melih.repository.interactors.base.Result -import com.melih.repository.sources.SourceManager +import com.melih.repository.interactors.base.Success +import com.melih.repository.sources.NetworkSource +import com.melih.repository.sources.PersistenceSource import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.FlowCollector import javax.inject.Inject @@ -12,13 +15,32 @@ import javax.inject.Inject /** * Gets next given number of launches */ -class GetLaunchDetails @Inject constructor( - private val sourceManager: SourceManager -) : BaseInteractor() { +@UseExperimental(ExperimentalCoroutinesApi::class) +class GetLaunchDetails @Inject constructor() : BaseInteractor() { - @ExperimentalCoroutinesApi - override suspend fun run(collector: FlowCollector>, params: Params) { - collector.emit(sourceManager.getLaunchById(params.id)) + @field:Inject + internal lateinit var networkSource: NetworkSource + + @field:Inject + internal lateinit var persistenceSource: PersistenceSource + + override suspend fun FlowCollector>.run(params: Params) { + val result = persistenceSource.getLaunchById(params.id) + + if (result !is Success) { + when (val response = networkSource.getLaunchById(params.id)) { + // Save result and return again from persistence + is Success -> { + persistenceSource.saveLaunch(response.successData) + emit(persistenceSource.getLaunchById(params.id)) + } + + // Redirect failure as it is + is Failure -> emit(response) + } + } else { + emit(result) + } } data class Params( diff --git a/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunches.kt b/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunches.kt index ae4aa43..5cc80ec 100644 --- a/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunches.kt +++ b/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunches.kt @@ -4,24 +4,45 @@ import com.melih.repository.entities.LaunchEntity import com.melih.repository.interactors.base.BaseInteractor import com.melih.repository.interactors.base.InteractorParameters import com.melih.repository.interactors.base.Result -import com.melih.repository.sources.SourceManager +import com.melih.repository.interactors.base.Success +import com.melih.repository.sources.NetworkSource +import com.melih.repository.sources.PersistenceSource import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.FlowCollector import javax.inject.Inject +const val DEFAULT_LAUNCHES_AMOUNT = 15 + /** * Gets next given number of launches */ -class GetLaunches @Inject constructor( - private val sourceManager: SourceManager -) : BaseInteractor, GetLaunches.Params>() { +@UseExperimental(ExperimentalCoroutinesApi::class) +class GetLaunches @Inject constructor() : BaseInteractor, GetLaunches.Params>() { - @ExperimentalCoroutinesApi - override suspend fun run(collector: FlowCollector>>, params: Params) { - collector.emit(sourceManager.getNextLaunches(params.count)) + @field:Inject + internal lateinit var networkSource: NetworkSource + + @field:Inject + internal lateinit var persistenceSource: PersistenceSource + + override suspend fun FlowCollector>>.run(params: Params) { + + // Start network fetch - we're not handling state here to ommit them + networkSource + .getNextLaunches(params.count, params.page) + .also { + if (it is Success) { + persistenceSource.saveLaunches(it.successData) + emit(persistenceSource.getNextLaunches(params.count, params.page)) + } else { + emit(it) + emit(persistenceSource.getNextLaunches(params.count, params.page)) + } + } } data class Params( - val count: Int = 10 + val count: Int = DEFAULT_LAUNCHES_AMOUNT, + val page: Int ) : InteractorParameters } diff --git a/repository/src/main/kotlin/com/melih/repository/interactors/base/BaseInteractor.kt b/repository/src/main/kotlin/com/melih/repository/interactors/base/BaseInteractor.kt index e33ecfa..e431a2d 100644 --- a/repository/src/main/kotlin/com/melih/repository/interactors/base/BaseInteractor.kt +++ b/repository/src/main/kotlin/com/melih/repository/interactors/base/BaseInteractor.kt @@ -10,23 +10,22 @@ import kotlinx.coroutines.flow.flowOn /** * Base use case that wraps [suspending][suspend] [run] function with [flow][Flow] and returns it for later usage. */ +@UseExperimental(ExperimentalCoroutinesApi::class) abstract class BaseInteractor { // region Abstractions - @ExperimentalCoroutinesApi - protected abstract suspend fun run(collector: FlowCollector>, params: P) + protected abstract suspend fun FlowCollector>.run(params: P) // endregion // region Functions - @ExperimentalCoroutinesApi operator fun invoke(params: P) = - flow> { - emit(Result.State.Loading()) - run(this, params) - emit(Result.State.Loaded()) - }.flowOn(Dispatchers.IO) + flow> { + emit(State.Loading()) + run(params) + emit(State.Loaded()) + }.flowOn(Dispatchers.IO) // endregion } diff --git a/repository/src/main/kotlin/com/melih/repository/interactors/base/Reason.kt b/repository/src/main/kotlin/com/melih/repository/interactors/base/Reason.kt index 5431133..e14b96e 100644 --- a/repository/src/main/kotlin/com/melih/repository/interactors/base/Reason.kt +++ b/repository/src/main/kotlin/com/melih/repository/interactors/base/Reason.kt @@ -5,15 +5,14 @@ import com.melih.repository.R /** - * [Result.Failure] reasons + * [Failure] reasons */ -sealed class Reason(@StringRes val messageRes: Int) { +sealed class Reason(@StringRes val messageRes: Int) - class NetworkError : Reason(R.string.reason_network) - class EmptyResultError : Reason(R.string.reason_empty_body) - class GenericError : Reason(R.string.reason_generic) - class ResponseError : Reason(R.string.reason_response) - class TimeoutError : Reason(R.string.reason_timeout) - class PersistenceEmpty : Reason(R.string.reason_persistance_empty) - class NoNetworkPersistenceEmpty : Reason(R.string.reason_no_network_persistance_empty) -} +class NetworkError : Reason(R.string.reason_network) +class EmptyResultError : Reason(R.string.reason_empty_body) +class GenericError : Reason(R.string.reason_generic) +class ResponseError : Reason(R.string.reason_response) +class TimeoutError : Reason(R.string.reason_timeout) +class PersistenceEmpty : Reason(R.string.reason_persistance_empty) +class NoNetworkPersistenceEmpty : Reason(R.string.reason_no_network_persistance_empty) diff --git a/repository/src/main/kotlin/com/melih/repository/interactors/base/Result.kt b/repository/src/main/kotlin/com/melih/repository/interactors/base/Result.kt index c4d5306..a5684c7 100644 --- a/repository/src/main/kotlin/com/melih/repository/interactors/base/Result.kt +++ b/repository/src/main/kotlin/com/melih/repository/interactors/base/Result.kt @@ -1,45 +1,53 @@ package com.melih.repository.interactors.base +import kotlinx.coroutines.ExperimentalCoroutinesApi + /** * Result class that wraps any [Success], [Failure] or [State] that can be generated by any derivation of [BaseInteractor] */ -sealed class Result { +@UseExperimental(ExperimentalCoroutinesApi::class) +sealed class Result - // region Subclasses +// region Subclasses - class Success(val successData: T) : Result() - class Failure(val errorData: Reason) : Result() +class Success(val successData: T) : Result() +class Failure(val errorData: Reason) : Result() - sealed class State : Result() { - class Loading : State() - class Loaded : State() - } - // endregion - - // region Functions - - inline fun handle(stateBlock: (State) -> Unit, failureBlock: (Reason) -> Unit, successBlock: (T) -> Unit) { - when (this) { - is Success -> successBlock(successData) - is Failure -> failureBlock(errorData) - is State -> stateBlock(this) - } - } - - inline fun handleSuccess(successBlock: (T) -> Unit) { - if (this is Success) - successBlock(successData) - } - - inline fun handleFailure(errorBlock: (Reason) -> Unit) { - if (this is Failure) - errorBlock(errorData) - } - - inline fun handleState(stateBlock: (State) -> Unit) { - if (this is State) - stateBlock(this) - } - // endregion +sealed class State : Result() { + class Loading : State() + class Loaded : State() } +// endregion + +// region Extensions + +inline fun Result.handle(stateBlock: (State) -> Unit, failureBlock: (Reason) -> Unit, successBlock: (T) -> Unit) { + when (this) { + is Success -> successBlock(successData) + is Failure -> failureBlock(errorData) + is State -> stateBlock(this) + } +} + +inline fun Result.onSuccess(successBlock: (T) -> Unit): Result { + if (this is Success) + successBlock(successData) + + return this +} + +inline fun Result.onFailure(errorBlock: (Reason) -> Unit): Result { + if (this is Failure) + errorBlock(errorData) + + return this +} + +inline fun Result.onState(stateBlock: (State) -> Unit): Result { + if (this is State) + stateBlock(this) + + return this +} +// endregion diff --git a/repository/src/main/kotlin/com/melih/repository/network/Api.kt b/repository/src/main/kotlin/com/melih/repository/network/Api.kt index 5ce9cad..0b410d8 100644 --- a/repository/src/main/kotlin/com/melih/repository/network/Api.kt +++ b/repository/src/main/kotlin/com/melih/repository/network/Api.kt @@ -5,15 +5,21 @@ import com.melih.repository.entities.LaunchesEntity import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path +import retrofit2.http.Query /** * Retrofit interface for networking */ -interface Api { +internal interface Api { @GET("launch/next/{count}") - suspend fun getNextLaunches(@Path("count") count: Int): Response + suspend fun getNextLaunches( + @Path("count") count: Int, + @Query("offset") offset: Int + ): Response @GET("launch/{id}") - suspend fun getLaunchById(@Path("id") id: Long): Response + suspend fun getLaunchById( + @Path("id") id: Long + ): Response } diff --git a/repository/src/main/kotlin/com/melih/repository/network/ApiImpl.kt b/repository/src/main/kotlin/com/melih/repository/network/ApiImpl.kt index 3a03cac..85ca556 100644 --- a/repository/src/main/kotlin/com/melih/repository/network/ApiImpl.kt +++ b/repository/src/main/kotlin/com/melih/repository/network/ApiImpl.kt @@ -9,9 +9,12 @@ import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Response import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory +import java.util.concurrent.TimeUnit import javax.inject.Inject -class ApiImpl @Inject constructor() : Api { +internal const val TIMEOUT_DURATION = 7L + +internal class ApiImpl @Inject constructor() : Api { // region Properties @@ -23,9 +26,12 @@ class ApiImpl @Inject constructor() : Api { Retrofit.Builder() .client( OkHttpClient.Builder() + .connectTimeout(TIMEOUT_DURATION, TimeUnit.SECONDS) + .readTimeout(TIMEOUT_DURATION, TimeUnit.SECONDS) .addInterceptor( - HttpLoggingInterceptor() - .setLevel(HttpLoggingInterceptor.Level.BODY) + HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } ).build() ) .addConverterFactory(MoshiConverterFactory.create(moshi)) @@ -35,9 +41,14 @@ class ApiImpl @Inject constructor() : Api { } // endregion - override suspend fun getNextLaunches(count: Int): Response = - service.getNextLaunches(count) + override suspend fun getNextLaunches( + count: Int, + offset: Int + ): Response = + service.getNextLaunches(count, offset) - override suspend fun getLaunchById(id: Long): Response = + override suspend fun getLaunchById( + id: Long + ): Response = service.getLaunchById(id) } diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/LaunchesDatabase.kt b/repository/src/main/kotlin/com/melih/repository/persistence/LaunchesDatabase.kt index 304e2a8..7d5f139 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/LaunchesDatabase.kt +++ b/repository/src/main/kotlin/com/melih/repository/persistence/LaunchesDatabase.kt @@ -1,6 +1,8 @@ package com.melih.repository.persistence +import android.content.Context import androidx.room.Database +import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters import com.melih.repository.entities.LaunchEntity @@ -24,7 +26,14 @@ const val DB_NAME = "LaunchesDB" RocketConverter::class, MissionConverter::class ) -abstract class LaunchesDatabase : RoomDatabase() { +internal abstract class LaunchesDatabase : RoomDatabase() { - abstract val launchesDao: LaunchesDao + companion object { + + fun getInstance(ctx: Context) = + Room.databaseBuilder(ctx, LaunchesDatabase::class.java, DB_NAME) + .build() + } + + internal abstract val launchesDao: LaunchesDao } diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/LocationConverter.kt b/repository/src/main/kotlin/com/melih/repository/persistence/converters/LocationConverter.kt index a05da1b..6742ecb 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/converters/LocationConverter.kt +++ b/repository/src/main/kotlin/com/melih/repository/persistence/converters/LocationConverter.kt @@ -1,6 +1,7 @@ package com.melih.repository.persistence.converters import com.melih.repository.entities.LocationEntity +import com.melih.repository.entities.LocationEntityJsonAdapter import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi @@ -9,5 +10,5 @@ import com.squareup.moshi.Moshi */ class LocationConverter : BaseConverter() { override fun getAdapter(moshi: Moshi): JsonAdapter = - moshi.adapter(LocationEntity::class.java) + LocationEntityJsonAdapter(moshi) } diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/RocketConverter.kt b/repository/src/main/kotlin/com/melih/repository/persistence/converters/RocketConverter.kt index 980d64d..de2e3b8 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/converters/RocketConverter.kt +++ b/repository/src/main/kotlin/com/melih/repository/persistence/converters/RocketConverter.kt @@ -1,6 +1,7 @@ package com.melih.repository.persistence.converters import com.melih.repository.entities.RocketEntity +import com.melih.repository.entities.RocketEntityJsonAdapter import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi @@ -9,5 +10,5 @@ import com.squareup.moshi.Moshi */ class RocketConverter : BaseConverter() { override fun getAdapter(moshi: Moshi): JsonAdapter = - moshi.adapter(RocketEntity::class.java) + RocketEntityJsonAdapter(moshi) } diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/dao/LaunchesDao.kt b/repository/src/main/kotlin/com/melih/repository/persistence/dao/LaunchesDao.kt index 97f12bb..df4b014 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/dao/LaunchesDao.kt +++ b/repository/src/main/kotlin/com/melih/repository/persistence/dao/LaunchesDao.kt @@ -2,20 +2,20 @@ package com.melih.repository.persistence.dao import androidx.room.Dao import androidx.room.Insert +import androidx.room.OnConflictStrategy import androidx.room.Query -import androidx.room.Transaction import com.melih.repository.entities.LaunchEntity /** * DAO for list of [launches][LaunchEntity] */ @Dao -abstract class LaunchesDao { +internal abstract class LaunchesDao { // region Queries - @Query("SELECT * FROM Launches LIMIT :count") - abstract suspend fun getLaunches(count: Int): List + @Query("SELECT * FROM Launches ORDER BY launchStartTime DESC LIMIT :count OFFSET :page*:count") + abstract suspend fun getLaunches(count: Int, page: Int): List @Query("SELECT * FROM Launches WHERE id=:id LIMIT 1") abstract suspend fun getLaunchById(id: Long): LaunchEntity? @@ -26,19 +26,10 @@ abstract class LaunchesDao { // region Insertion - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) abstract suspend fun saveLaunches(launches: List) - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) abstract suspend fun saveLaunch(launch: LaunchEntity) // endregion - - // region Transactions - - @Transaction - open suspend fun updateLaunches(launches: List) { - nukeLaunches() - saveLaunches(launches) - } - // endregion } diff --git a/repository/src/main/kotlin/com/melih/repository/sources/NetworkSource.kt b/repository/src/main/kotlin/com/melih/repository/sources/NetworkSource.kt index b64cd5c..8b67b74 100644 --- a/repository/src/main/kotlin/com/melih/repository/sources/NetworkSource.kt +++ b/repository/src/main/kotlin/com/melih/repository/sources/NetworkSource.kt @@ -4,8 +4,15 @@ import android.net.NetworkInfo import com.melih.repository.DEFAULT_IMAGE_SIZE import com.melih.repository.Repository import com.melih.repository.entities.LaunchEntity +import com.melih.repository.interactors.DEFAULT_LAUNCHES_AMOUNT +import com.melih.repository.interactors.base.EmptyResultError +import com.melih.repository.interactors.base.Failure +import com.melih.repository.interactors.base.NetworkError import com.melih.repository.interactors.base.Reason +import com.melih.repository.interactors.base.ResponseError import com.melih.repository.interactors.base.Result +import com.melih.repository.interactors.base.Success +import com.melih.repository.interactors.base.TimeoutError import com.melih.repository.network.ApiImpl import retrofit2.Response import java.io.IOException @@ -14,9 +21,9 @@ import javax.inject.Provider /** * NetworkSource for fetching results using api and wrapping them as contracted in [repository][Repository], - * returning either [failure][Result.Failure] with proper [reason][Reason] or [success][Result.Success] with data + * returning either [failure][Failure] with proper [reason][Reason] or [success][Success] with data */ -class NetworkSource @Inject constructor( +internal class NetworkSource @Inject constructor( private val apiImpl: ApiImpl, private val networkInfoProvider: Provider ) : Repository() { @@ -31,8 +38,10 @@ class NetworkSource @Inject constructor( // region Functions - override suspend fun getNextLaunches(count: Int): Result> = - safeExecute(apiImpl::getNextLaunches, count) { entity -> + override suspend fun getNextLaunches(count: Int, page: Int): Result> = + safeExecute({ + apiImpl.getNextLaunches(count, page * DEFAULT_LAUNCHES_AMOUNT) + }) { entity -> entity.launches.map { launch -> if (!launch.rocket.imageURL.isNotBlank()) { launch.copy( @@ -50,7 +59,9 @@ class NetworkSource @Inject constructor( } override suspend fun getLaunchById(id: Long): Result = - safeExecute(apiImpl::getLaunchById, id) { + safeExecute({ + apiImpl.getLaunchById(id) + }) { if (!it.rocket.imageURL.isNotBlank()) { it.copy( rocket = it.rocket.copy( @@ -62,28 +73,27 @@ class NetworkSource @Inject constructor( } } - private suspend inline fun safeExecute( - block: suspend (param: P) -> Response, - param: P, + private suspend inline fun safeExecute( + block: suspend () -> Response, transform: (T) -> R ) = if (isNetworkConnected) { try { - block(param).extractResponseBody(transform) + block().extractResponseBody(transform) } catch (e: IOException) { - Result.Failure(Reason.TimeoutError()) + Failure(TimeoutError()) } } else { - Result.Failure(Reason.NetworkError()) + Failure(NetworkError()) } private inline fun Response.extractResponseBody(transform: (T) -> R) = if (isSuccessful) { body()?.let { - Result.Success(transform(it)) - } ?: Result.Failure(Reason.EmptyResultError()) + Success(transform(it)) + } ?: Failure(EmptyResultError()) } else { - Result.Failure(Reason.ResponseError()) + Failure(ResponseError()) } private fun transformImageUrl(imageUrl: String, supportedSizes: IntArray) = diff --git a/repository/src/main/kotlin/com/melih/repository/sources/PersistenceSource.kt b/repository/src/main/kotlin/com/melih/repository/sources/PersistenceSource.kt index 6d6c974..881e45d 100644 --- a/repository/src/main/kotlin/com/melih/repository/sources/PersistenceSource.kt +++ b/repository/src/main/kotlin/com/melih/repository/sources/PersistenceSource.kt @@ -1,28 +1,34 @@ package com.melih.repository.sources +import android.content.Context import com.melih.repository.Repository import com.melih.repository.entities.LaunchEntity +import com.melih.repository.interactors.base.Failure +import com.melih.repository.interactors.base.PersistenceEmpty import com.melih.repository.interactors.base.Reason import com.melih.repository.interactors.base.Result +import com.melih.repository.interactors.base.Success import com.melih.repository.persistence.LaunchesDatabase import javax.inject.Inject /** * Persistance source using Room database to save / read objects for SST - offline usage */ -class PersistenceSource @Inject constructor( - private val launchesDatabase: LaunchesDatabase +internal class PersistenceSource @Inject constructor( + ctx: Context ) : Repository() { // region Functions - override suspend fun getNextLaunches(count: Int): Result> = + private val launchesDatabase = LaunchesDatabase.getInstance(ctx) + + override suspend fun getNextLaunches(count: Int, page: Int): Result> = launchesDatabase .launchesDao - .getLaunches(count) + .getLaunches(count, page) .takeIf { it.isNotEmpty() } ?.run { - Result.Success(this) - } ?: Result.Failure(Reason.PersistenceEmpty()) + Success(this) + } ?: Failure(PersistenceEmpty()) override suspend fun getLaunchById(id: Long): Result = launchesDatabase @@ -30,11 +36,11 @@ class PersistenceSource @Inject constructor( .getLaunchById(id) .takeIf { it != null } ?.run { - Result.Success(this) - } ?: Result.Failure(Reason.PersistenceEmpty()) + Success(this) + } ?: Failure(PersistenceEmpty()) internal suspend fun saveLaunches(launches: List) { - launchesDatabase.launchesDao.updateLaunches(launches) + launchesDatabase.launchesDao.saveLaunches(launches) } internal suspend fun saveLaunch(launch: LaunchEntity) { diff --git a/repository/src/main/kotlin/com/melih/repository/sources/SourceManager.kt b/repository/src/main/kotlin/com/melih/repository/sources/SourceManager.kt deleted file mode 100644 index 999225b..0000000 --- a/repository/src/main/kotlin/com/melih/repository/sources/SourceManager.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.melih.repository.sources - -import com.melih.repository.Repository -import com.melih.repository.entities.LaunchEntity -import com.melih.repository.interactors.base.Reason -import com.melih.repository.interactors.base.Result -import javax.inject.Inject - -/** - * Manages SST by using network & persistance sources - */ -class SourceManager @Inject constructor( - private val networkSource: NetworkSource, - private val persistenceSource: PersistenceSource -) : Repository() { - - // region Functions - - override suspend fun getNextLaunches(count: Int): Result> { - networkSource - .getNextLaunches(count) - .takeIf { it is Result.Success } - ?.let { - persistenceSource.saveLaunches((it as Result.Success).successData) - } - - return persistenceSource - .getNextLaunches(count) - .takeIf { - it is Result.Success && it.successData.isNotEmpty() - } - ?: Result.Failure(Reason.NoNetworkPersistenceEmpty()) - } - - override suspend fun getLaunchById(id: Long): Result { - val result = - persistenceSource - .getLaunchById(id) - - return if (result is Result.Failure) { - networkSource - .getLaunchById(id) - .takeIf { it is Result.Success } - ?.let { - persistenceSource.saveLaunch((it as Result.Success).successData) - } - - persistenceSource - .getLaunchById(id) - } else { - result - } - } - // endregion -} diff --git a/repository/src/test/kotlin/com/melih/repository/interactors/base/BaseInteractorTest.kt b/repository/src/test/kotlin/com/melih/repository/interactors/base/BaseInteractorTest.kt index f1d6430..35d922c 100644 --- a/repository/src/test/kotlin/com/melih/repository/interactors/base/BaseInteractorTest.kt +++ b/repository/src/test/kotlin/com/melih/repository/interactors/base/BaseInteractorTest.kt @@ -12,14 +12,13 @@ import org.amshove.kluent.shouldEqualTo import org.junit.jupiter.api.Test import java.util.* - +@UseExperimental(ExperimentalCoroutinesApi::class) class BaseInteractorTest { val testInteractor = spyk(TestInteractor()) val testParams = TestParams() @Test - @ExperimentalCoroutinesApi fun `BaseInteractor should send states and items emmited by run`() { // Using run blocking due to threading problems in runBlockingTest // See https://github.com/Kotlin/kotlinx.coroutines/issues/1204 @@ -42,24 +41,23 @@ class BaseInteractorTest { resultDeque.size shouldEqualTo 3 // Verify first item is Loading state - resultDeque.poll() shouldBeInstanceOf Result.State.Loading::class + resultDeque.poll() shouldBeInstanceOf State.Loading::class // Verify second item is Success, with default value we set below in TestParams class resultDeque.poll().also { - it shouldBeInstanceOf Result.Success::class - (it as Result.Success).successData shouldEqualTo 10 + it shouldBeInstanceOf Success::class + (it as Success).successData shouldEqualTo 10 } // Verify last item is Loaded state - resultDeque.poll() shouldBeInstanceOf Result.State.Loaded::class + resultDeque.poll() shouldBeInstanceOf State.Loaded::class } } inner class TestInteractor : BaseInteractor() { - @ExperimentalCoroutinesApi - override suspend fun run(collector: FlowCollector>, params: TestParams) { - collector.emit(Result.Success(params.testValue)) + override suspend fun FlowCollector>.run(params: TestParams) { + emit(Success(params.testValue)) } } diff --git a/repository/src/test/kotlin/com/melih/repository/interactors/base/ResultTest.kt b/repository/src/test/kotlin/com/melih/repository/interactors/base/ResultTest.kt index 79cfc02..cc770df 100644 --- a/repository/src/test/kotlin/com/melih/repository/interactors/base/ResultTest.kt +++ b/repository/src/test/kotlin/com/melih/repository/interactors/base/ResultTest.kt @@ -12,11 +12,11 @@ class ResultTest { private val number = 10 - private val success = Result.Success(number) - private val failure = Result.Failure(Reason.GenericError()) - private val state = Result.State.Loading() + private val success = Success(number) + private val failure = Failure(GenericError()) + private val state = State.Loading() - private val emptyStateBlock = spyk({ _: Result.State -> }) + private val emptyStateBlock = spyk({ _: State -> }) private val emptyFailureBlock = spyk({ _: Reason -> }) private val emptySuccessBlock = spyk({ _: Int -> }) @@ -37,8 +37,8 @@ class ResultTest { @Test fun `Failure should only invoke failureBlock with correct error`() { val actualFailureBlock = spyk({ reason: Reason -> - reason shouldBeInstanceOf Reason.GenericError::class - (reason as Reason.GenericError).messageRes shouldEqualTo R.string.reason_generic + reason shouldBeInstanceOf GenericError::class + (reason as GenericError).messageRes shouldEqualTo R.string.reason_generic Unit }) @@ -51,8 +51,8 @@ class ResultTest { @Test fun `State should only invoke stateBlock with correct state`() { - val actualSuccessBlock = spyk({ state: Result.State -> - state shouldBeInstanceOf Result.State.Loading::class + val actualSuccessBlock = spyk({ state: State -> + state shouldBeInstanceOf State.Loading::class Unit }) diff --git a/repository/src/test/kotlin/com/melih/repository/sources/NetworkSourceTest.kt b/repository/src/test/kotlin/com/melih/repository/sources/NetworkSourceTest.kt index 55f9528..343aac6 100644 --- a/repository/src/test/kotlin/com/melih/repository/sources/NetworkSourceTest.kt +++ b/repository/src/test/kotlin/com/melih/repository/sources/NetworkSourceTest.kt @@ -4,8 +4,13 @@ import android.net.NetworkInfo import com.melih.repository.R import com.melih.repository.entities.LaunchEntity import com.melih.repository.entities.LaunchesEntity -import com.melih.repository.interactors.base.Reason -import com.melih.repository.interactors.base.Result +import com.melih.repository.interactors.base.EmptyResultError +import com.melih.repository.interactors.base.Failure +import com.melih.repository.interactors.base.NetworkError +import com.melih.repository.interactors.base.ResponseError +import com.melih.repository.interactors.base.Success +import com.melih.repository.interactors.base.onFailure +import com.melih.repository.interactors.base.onSuccess import com.melih.repository.network.ApiImpl import io.mockk.coEvery import io.mockk.every @@ -19,6 +24,7 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import javax.inject.Provider +@UseExperimental(ExperimentalCoroutinesApi::class) class NetworkSourceTest { private val apiImpl = mockk(relaxed = true) @@ -32,66 +38,62 @@ class NetworkSourceTest { inner class GetNextLaunches { @Test - @ExperimentalCoroutinesApi fun `should return network error when internet is not connected`() { every { networkInfoProvider.get().isConnected } returns false runBlockingTest { - val result = source.getNextLaunches(1) + val result = source.getNextLaunches(1, 0) - result shouldBeInstanceOf Result.Failure::class - result.handleFailure { - it shouldBeInstanceOf Reason.NetworkError::class + result shouldBeInstanceOf Failure::class + result.onFailure { + it shouldBeInstanceOf NetworkError::class } } } @Test - @ExperimentalCoroutinesApi fun `should return response error when it is not successful`() { every { networkInfoProvider.get().isConnected } returns true - coEvery { apiImpl.getNextLaunches(any()).isSuccessful } returns false + coEvery { apiImpl.getNextLaunches(any(), any()).isSuccessful } returns false runBlockingTest { - val result = source.getNextLaunches(1) + val result = source.getNextLaunches(1, 0) - result shouldBeInstanceOf Result.Failure::class - result.handleFailure { - it shouldBeInstanceOf Reason.ResponseError::class - (it as Reason.ResponseError).messageRes shouldEqualTo R.string.reason_response + result shouldBeInstanceOf Failure::class + result.onFailure { + it shouldBeInstanceOf ResponseError::class + (it as ResponseError).messageRes shouldEqualTo R.string.reason_response } } } @Test - @ExperimentalCoroutinesApi fun `should return empty result error when body is null`() { every { networkInfoProvider.get().isConnected } returns true - coEvery { apiImpl.getNextLaunches(any()).isSuccessful } returns true - coEvery { apiImpl.getNextLaunches(any()).body() } returns null + coEvery { apiImpl.getNextLaunches(any(), any()).isSuccessful } returns true + coEvery { apiImpl.getNextLaunches(any(), any()).body() } returns null runBlockingTest { - val result = source.getNextLaunches(1) + val result = source.getNextLaunches(1, 0) - result shouldBeInstanceOf Result.Failure::class - result.handleFailure { - it shouldBeInstanceOf Reason.EmptyResultError::class + result shouldBeInstanceOf Failure::class + result.onFailure { + it shouldBeInstanceOf EmptyResultError::class } } } @Test - @ExperimentalCoroutinesApi fun `should return success with data if execution is successful`() { every { networkInfoProvider.get().isConnected } returns true - coEvery { apiImpl.getNextLaunches(any()).isSuccessful } returns true - coEvery { apiImpl.getNextLaunches(any()).body() } returns LaunchesEntity(launches = listOf(LaunchEntity(id = 1013))) + coEvery { apiImpl.getNextLaunches(any(), any()).isSuccessful } returns true + coEvery { apiImpl.getNextLaunches(any(), any()).body() } returns LaunchesEntity(launches = listOf(LaunchEntity(id = 1013))) runBlockingTest { - val result = source.getNextLaunches(1) + val result = source.getNextLaunches(1, 0) - result shouldBeInstanceOf Result.Success::class - result.handleSuccess { + result shouldBeInstanceOf Success::class + result.onSuccess { it shouldBeInstanceOf List::class it.size shouldEqualTo 1 it[0].id shouldEqualTo 1013 diff --git a/repository/src/test/kotlin/com/melih/repository/sources/PersistanceSourceTest.kt b/repository/src/test/kotlin/com/melih/repository/sources/PersistanceSourceTest.kt index a423423..e40fa14 100644 --- a/repository/src/test/kotlin/com/melih/repository/sources/PersistanceSourceTest.kt +++ b/repository/src/test/kotlin/com/melih/repository/sources/PersistanceSourceTest.kt @@ -1,51 +1,68 @@ package com.melih.repository.sources +import android.content.Context import com.melih.repository.entities.LaunchEntity -import com.melih.repository.interactors.base.Reason -import com.melih.repository.interactors.base.Result +import com.melih.repository.interactors.base.Failure +import com.melih.repository.interactors.base.PersistenceEmpty +import com.melih.repository.interactors.base.Success +import com.melih.repository.interactors.base.onFailure +import com.melih.repository.interactors.base.onSuccess import com.melih.repository.persistence.LaunchesDatabase import io.mockk.coEvery +import io.mockk.every import io.mockk.mockk +import io.mockk.mockkObject import io.mockk.spyk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeInstanceOf import org.amshove.kluent.shouldEqualTo +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test class PersistanceSourceTest { + private val ctx = mockk(relaxed = true) private val dbImplementation = mockk(relaxed = true) - private val source = spyk(PersistenceSource(dbImplementation)) + private val source = spyk(PersistenceSource(ctx)) + + private val scope = CoroutineScope(Dispatchers.IO) + + @BeforeEach + fun setup() { + mockkObject(LaunchesDatabase) + every { LaunchesDatabase.getInstance(ctx) } returns dbImplementation + } @Nested inner class GetNextLaunches { @Test - @ExperimentalCoroutinesApi fun `should return persistance empty error when db is empty`() { - runBlockingTest { - coEvery { dbImplementation.launchesDao.getLaunches(any()) } returns emptyList() + coEvery { dbImplementation.launchesDao.getLaunches(any(), any()) } returns emptyList() - val result = source.getNextLaunches(10) - result shouldBeInstanceOf Result.Failure::class - result.handleFailure { - it shouldBeInstanceOf Reason.PersistenceEmpty::class + scope.launch { + val result = source.getNextLaunches(10, 0) + + result shouldBeInstanceOf Failure::class + result.onFailure { + it shouldBeInstanceOf PersistenceEmpty::class } } } @Test - @ExperimentalCoroutinesApi fun `should return success with data if db is not empty`() { - runBlockingTest { - coEvery { dbImplementation.launchesDao.getLaunches(any()) } returns listOf(LaunchEntity(id = 1013)) + coEvery { dbImplementation.launchesDao.getLaunches(any(), any()) } returns listOf(LaunchEntity(id = 1013)) - val result = source.getNextLaunches(10) - result shouldBeInstanceOf Result.Success::class - result.handleSuccess { + scope.launch { + val result = source.getNextLaunches(10, 0) + + result shouldBeInstanceOf Success::class + result.onSuccess { it.isEmpty() shouldBe false it.size shouldEqualTo 1 it[0].id shouldEqualTo 1013 diff --git a/repository/src/test/kotlin/com/melih/repository/sources/SourceManagerTest.kt b/repository/src/test/kotlin/com/melih/repository/sources/SourceManagerTest.kt deleted file mode 100644 index 8415a8f..0000000 --- a/repository/src/test/kotlin/com/melih/repository/sources/SourceManagerTest.kt +++ /dev/null @@ -1,141 +0,0 @@ -package com.melih.repository.sources - -import com.melih.repository.entities.LaunchEntity -import com.melih.repository.interactors.base.Reason -import com.melih.repository.interactors.base.Result -import io.mockk.called -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.coVerifyOrder -import io.mockk.mockk -import io.mockk.spyk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.runBlockingTest -import org.amshove.kluent.shouldBeInstanceOf -import org.amshove.kluent.shouldEqualTo -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test - -class SourceManagerTest { - - private val networkSource = mockk(relaxed = true) - private val persistenceSource = mockk(relaxed = true) - - private val sourceManager = spyk(SourceManager(networkSource, persistenceSource)) - - @Nested - inner class GetNextLaunches { - - @Test - @ExperimentalCoroutinesApi - fun `should try to fetch, save and return result from persistance`() { - runBlockingTest { - val amount = 10 - coEvery { networkSource.getNextLaunches(any()) } returns Result.Success(listOf(LaunchEntity(id = 1012))) - coEvery { persistenceSource.getNextLaunches(any()) } returns Result.Success(listOf(LaunchEntity(id = 1013))) - - val result = sourceManager.getNextLaunches(amount) - - coVerifyOrder { - networkSource.getNextLaunches(any()) - persistenceSource.saveLaunches(any()) - persistenceSource.getNextLaunches(any()) - } - - coVerify(exactly = 1) { networkSource.getNextLaunches(any()) } - coVerify(exactly = 1) { persistenceSource.saveLaunches(any()) } - coVerify(exactly = 1) { persistenceSource.getNextLaunches(any()) } - - result shouldBeInstanceOf Result.Success::class - result.handleSuccess { - it.size shouldEqualTo 1 - it[0].id shouldEqualTo 1013 - } - } - } - - @Test - @ExperimentalCoroutinesApi - fun `should not save response if fetching was failure and return result from persistance`() { - runBlockingTest { - val amount = 10 - coEvery { networkSource.getNextLaunches(any()) } returns Result.Failure(Reason.NetworkError()) - coEvery { persistenceSource.getNextLaunches(any()) } returns Result.Success(listOf(LaunchEntity(id = 1013))) - - val result = sourceManager.getNextLaunches(amount) - - coVerify { persistenceSource.saveLaunches(any()) wasNot called } - coVerify(exactly = 1) { persistenceSource.getNextLaunches(any()) } - - result shouldBeInstanceOf Result.Success::class - result.handleSuccess { - it.size shouldEqualTo 1 - it[0].id shouldEqualTo 1013 - } - } - } - - @Test - @ExperimentalCoroutinesApi - fun `should return failure if network and persistance fails`() { - runBlockingTest { - val amount = 10 - coEvery { networkSource.getNextLaunches(any()) } returns Result.Failure(Reason.NetworkError()) - coEvery { persistenceSource.getNextLaunches(any()) } returns Result.Failure(Reason.PersistenceEmpty()) - - val result = sourceManager.getNextLaunches(amount) - - coVerify { persistenceSource.saveLaunches(any()) wasNot called } - coVerify(exactly = 1) { persistenceSource.getNextLaunches(any()) } - - result shouldBeInstanceOf Result.Failure::class - result.handleFailure { - it shouldBeInstanceOf Reason.NoNetworkPersistenceEmpty::class - } - } - } - } - - @Nested - inner class GetLaunchDetails { - - @Test - fun `should return result from persistance immediately if it's found`() { - runBlocking { - coEvery { persistenceSource.getLaunchById(any()) } returns Result.Success(LaunchEntity(id = 1013)) - - val result = sourceManager.getLaunchById(1) - - coVerify { networkSource.getLaunchById(any()) wasNot called } - - result shouldBeInstanceOf Result.Success::class - result.handleSuccess { - it.id shouldEqualTo 1013 - } - } - } - - @Test - fun `should fetch result from network if it's not found in persistance`() { - runBlocking { - coEvery { - persistenceSource.getLaunchById(any()) - } returns Result.Failure(Reason.PersistenceEmpty()) andThen Result.Success(LaunchEntity(id = 1013)) - - coEvery { networkSource.getLaunchById(any()) } returns Result.Success(LaunchEntity(id = 1013)) - - val result = sourceManager.getLaunchById(1) - - coVerify(exactly = 1) { networkSource.getLaunchById(any()) } - coVerify(exactly = 1) { persistenceSource.saveLaunch(any()) } - coVerify(exactly = 2) { persistenceSource.getLaunchById(any()) } - - result shouldBeInstanceOf Result.Success::class - result.handleSuccess { - it.id shouldEqualTo 1013 - } - } - } - } -} diff --git a/scripts/default_dependencies.gradle b/scripts/default_dependencies.gradle index a6ae4c6..b9ee1c9 100644 --- a/scripts/default_dependencies.gradle +++ b/scripts/default_dependencies.gradle @@ -1,4 +1,5 @@ apply plugin: "de.mannodermaus.android-junit5" + apply from: "$rootProject.projectDir/scripts/detekt.gradle" apply from: "$rootProject.projectDir/scripts/dokka.gradle" @@ -17,3 +18,57 @@ dependencies { testRuntimeOnly testLibraries.jUnitEngine } + +apply plugin: 'jacoco' + +jacoco { + toolVersion = "0.8.1" + reportsDir = file("$rootProject.projectDir/reports/jacoco/$project.name") +} + +task jacocoTestReport(type: JacocoReport, dependsOn: "testDebugUnitTest") { + group = "Reporting" + description = "Generate Jacoco coverage reports for Debug build" + + reports { + xml.enabled = true + html.enabled = true + } + + // what to exclude from coverage report + // UI, "noise", generated classes, platform classes, etc. + def excludes = [ + '**/R.class', + '**/R$*.class', + '**/*$ViewInjector*.*', + '**/BuildConfig.*', + '**/Manifest*.*', + '**/*Test*.*', + 'android/**/*.*', + '**/*Fragment.*', + '**/*Activity.*' + ] + + // generated classes + getClassDirectories().setFrom( + fileTree( + dir: "$buildDir/intermediates/classes/debug", + excludes: excludes + ) + fileTree( + dir: "$buildDir/tmp/kotlin-classes/debug", + excludes: excludes + ) + ) + + // sources + getSourceDirectories().setFrom( + files([ + android.sourceSets.main.java.srcDirs, + "src/main/kotlin" + ]) + ) + + getExecutionData().setFrom( + files("$buildDir/jacoco/testDebugUnitTest.exec") + ) +} diff --git a/scripts/dependencies.gradle b/scripts/dependencies.gradle index 3bc483d..29a823e 100644 --- a/scripts/dependencies.gradle +++ b/scripts/dependencies.gradle @@ -8,7 +8,7 @@ ext { supportLibraryVersion : "28.0.0", appCompatVersion : "1.1.0-alpha04", lifecycleVersion : "2.2.0-alpha02", - fragmentVersion : "1.1.0-beta01", + fragmentVersion : "1.2.0-alpha01", workManagerVersion : "2.1.0-alpha03", constraintLayoutVesion: "2.0.0-beta1", cardViewVersion : "1.0.0", @@ -22,7 +22,7 @@ ext { retrofitVersion : "2.6.0", picassoVersion : "2.71828", moshiVersion : "1.8.0", - coroutinesVersion : "1.3.0-M1", + coroutinesVersion : "1.3.0-M2", leakCanaryVersion : "2.0-alpha-2", timberVersion : "4.7.1", jUnitVersion : "5.5.0", @@ -127,6 +127,7 @@ ext { "com.google.dagger:dagger-compiler:${versions.daggerVersion}", "com.google.dagger:dagger-android-processor:${versions.daggerVersion}" ], + moshi : "com.squareup.moshi:moshi-kotlin-codegen:${versions.moshiVersion}" ] testLibraries = [ diff --git a/scripts/feature_module.gradle b/scripts/feature_module.gradle index 146c113..e6ceeb1 100644 --- a/scripts/feature_module.gradle +++ b/scripts/feature_module.gradle @@ -11,6 +11,7 @@ dependencies { implementation libraries.fragment implementation libraries.lifecycle + implementation libraries.liveDataKTX implementation libraries.navigation implementation libraries.constraintLayout } diff --git a/scripts/flavors.gradle b/scripts/flavors.gradle index 49de32e..496ab22 100644 --- a/scripts/flavors.gradle +++ b/scripts/flavors.gradle @@ -8,6 +8,7 @@ android { debug { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + testCoverageEnabled = true } dev { diff --git a/scripts/sources.gradle b/scripts/sources.gradle index d6a4f00..dcbd8fc 100644 --- a/scripts/sources.gradle +++ b/scripts/sources.gradle @@ -12,5 +12,6 @@ android { kotlinOptions { jvmTarget = '1.8' + freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental" } } diff --git a/settings.gradle b/settings.gradle index 6bb6ae3..7a766a4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ -include ':app', ':repository', ':core', ':features:list', ':features:detail' +include ':app', ':repository', ':core', ':features:launches', ':features:detail' rootProject.name = 'Rocket Science'