diff --git a/.circleci/config.yml b/.circleci/config.yml index edbe35c..e4542dd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -76,12 +76,60 @@ jobs: - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ path: features/detail/build/test-results - ## Repository + ## Abstractions - run: - name: Test Repository + name: Test Abstractions command: | - fastlane test_repository - ./gradlew repository:jacocoTestReport + fastlane test_abstractions + ./gradlew abstractions:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ + path: repository/build/reports/tests + - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ + path: repository/build/test-results + + ## Definitions + - run: + name: Test Definitions + command: | + fastlane test_definitions + ./gradlew data:definitions:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ + path: repository/build/reports/tests + - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ + path: repository/build/test-results + + ## Interactors + - run: + name: Test Interactors + command: | + fastlane test_interactors + ./gradlew data:interactors:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ + path: repository/build/reports/tests + - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ + path: repository/build/test-results + + ## Network + - run: + name: Test Network + command: | + fastlane test_network + ./gradlew data:network:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ + path: repository/build/reports/tests + - store_test_results: # for display in Test Summary: https://circleci.com/docs/2.0/collect-test-data/ + path: repository/build/test-results + + ## Persistence + - run: + name: Test Persistence + command: | + fastlane test_persistence + ./gradlew data:persistence:jacocoTestReport bash <(curl -s https://codecov.io/bash) - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ path: repository/build/reports/tests diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 0972801..13de4f6 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -55,8 +55,32 @@ jobs: ./gradlew features:detail:jacocoTestReport bash <(curl -s https://codecov.io/bash) - - name: Test Repository + - name: Test Abstractions run: | - fastlane test_repository - ./gradlew repository:jacocoTestReport + fastlane test_abstractions + ./gradlew abstractions:jacocoTestReport bash <(curl -s https://codecov.io/bash) + + - name: Test Definitions + run: | + fastlane test_definitions + ./gradlew data:definitions:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + + - name: Test Interactors + run: | + fastlane test_interactors + ./gradlew data:interactors:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + + - name: Test Network + run: | + fastlane test_network + ./gradlew data:network:jacocoTestReport + bash <(curl -s https://codecov.io/bash) + + - name: Test Persistence + run: | + fastlane test_persistence + ./gradlew data:persistence:jacocoTestReport + bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3a8ee31..4cdb0a4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /.idea .DS_Store /build +**/build /fastlane/README.md /fastlane/report.xml /captures diff --git a/Gemfile.lock b/Gemfile.lock index b9db40d..192c06b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,7 +5,7 @@ GEM addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) atomos (0.1.3) - babosa (1.0.2) + babosa (1.0.3) claide (1.0.3) colored (1.2) colored2 (3.1.2) @@ -18,7 +18,7 @@ GEM unf (>= 0.0.5, < 1.0.0) dotenv (2.7.5) emoji_regex (1.0.1) - excon (0.66.0) + excon (0.67.0) faraday (0.15.4) multipart-post (>= 1.2, < 3) faraday-cookie_jar (0.0.6) @@ -27,7 +27,7 @@ GEM faraday_middleware (0.13.1) faraday (>= 0.7.4, < 1.0) fastimage (2.1.7) - fastlane (2.131.0) + fastlane (2.133.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) babosa (>= 1.0.2, < 2.0.0) @@ -37,9 +37,9 @@ GEM dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 2.0) excon (>= 0.45.0, < 1.0.0) - faraday (~> 0.9) + faraday (< 0.16.0) faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 0.9) + faraday_middleware (< 0.16.0) fastimage (>= 2.1.0, < 3.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-api-client (>= 0.21.2, < 0.24.0) @@ -52,7 +52,7 @@ GEM multipart-post (~> 2.0.0) plist (>= 3.1.0, < 4.0.0) public_suffix (~> 2.0.0) - rubyzip (>= 1.2.2, < 2.0.0) + rubyzip (>= 1.3.0, < 2.0.0) security (= 0.1.3) simctl (~> 1.6.3) slack-notifier (>= 2.0.0, < 3.0.0) @@ -98,7 +98,7 @@ GEM memoist (0.16.0) mime-types (3.3) mime-types-data (~> 3.2015) - mime-types-data (3.2019.0904) + mime-types-data (3.2019.1009) mini_magick (4.9.5) multi_json (1.13.1) multi_xml (0.6.0) @@ -114,7 +114,7 @@ GEM uber (< 0.2.0) retriable (3.1.2) rouge (2.0.7) - rubyzip (1.2.4) + rubyzip (1.3.0) security (0.1.3) signet (0.11.0) addressable (~> 2.3) diff --git a/core/.gitignore b/abstractions/.gitignore similarity index 100% rename from core/.gitignore rename to abstractions/.gitignore diff --git a/abstractions/build.gradle b/abstractions/build.gradle new file mode 100644 index 0000000..86354c0 --- /dev/null +++ b/abstractions/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: "de.mannodermaus.android-junit5" + +apply from: "$rootProject.projectDir/scripts/module.gradle" + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation libraries.kotlin + + testImplementation testLibraries.jUnitApi + testImplementation testLibraries.mockk + testImplementation testLibraries.kluent + + testRuntimeOnly testLibraries.jUnitEngine +} diff --git a/repository/proguard-rules.pro b/abstractions/consumer-rules.pro similarity index 100% rename from repository/proguard-rules.pro rename to abstractions/consumer-rules.pro diff --git a/abstractions/proguard-rules.pro b/abstractions/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/abstractions/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/abstractions/src/main/AndroidManifest.xml b/abstractions/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c706bc8 --- /dev/null +++ b/abstractions/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + diff --git a/abstractions/src/main/kotlin/com/melih/abstractions/data/ViewEntity.kt b/abstractions/src/main/kotlin/com/melih/abstractions/data/ViewEntity.kt new file mode 100644 index 0000000..6d9668e --- /dev/null +++ b/abstractions/src/main/kotlin/com/melih/abstractions/data/ViewEntity.kt @@ -0,0 +1,3 @@ +package com.melih.abstractions.data + +interface ViewEntity diff --git a/abstractions/src/main/kotlin/com/melih/abstractions/deliverable/Reason.kt b/abstractions/src/main/kotlin/com/melih/abstractions/deliverable/Reason.kt new file mode 100644 index 0000000..030a02e --- /dev/null +++ b/abstractions/src/main/kotlin/com/melih/abstractions/deliverable/Reason.kt @@ -0,0 +1,6 @@ +package com.melih.abstractions.deliverable + +abstract class Reason : Throwable() { + + abstract val messageRes: Int +} diff --git a/repository/src/main/kotlin/com/melih/repository/interactors/base/Result.kt b/abstractions/src/main/kotlin/com/melih/abstractions/deliverable/Result.kt similarity index 70% rename from repository/src/main/kotlin/com/melih/repository/interactors/base/Result.kt rename to abstractions/src/main/kotlin/com/melih/abstractions/deliverable/Result.kt index e34eea7..f81e27e 100644 --- a/repository/src/main/kotlin/com/melih/repository/interactors/base/Result.kt +++ b/abstractions/src/main/kotlin/com/melih/abstractions/deliverable/Result.kt @@ -1,12 +1,9 @@ -package com.melih.repository.interactors.base - -import kotlinx.coroutines.ExperimentalCoroutinesApi +package com.melih.abstractions.deliverable /** - * Result class that wraps any [Success], [Failure] or [State] that can be generated by any derivation of [BaseInteractor] + * Result class that wraps any [Success], [Failure] or [State] */ -@UseExperimental(ExperimentalCoroutinesApi::class) sealed class Result //region Subclasses @@ -22,7 +19,11 @@ sealed class State : Result() { //region Extensions -inline fun Result.handle(stateBlock: (State) -> Unit, failureBlock: (Reason) -> Unit, successBlock: (T) -> Unit) { +inline fun Result.handle( + stateBlock: (State) -> Unit, + failureBlock: (Reason) -> Unit, + successBlock: (T) -> Unit +) { when (this) { is Success -> successBlock(successData) is Failure -> failureBlock(errorData) diff --git a/abstractions/src/main/kotlin/com/melih/abstractions/mapper/Mapper.kt b/abstractions/src/main/kotlin/com/melih/abstractions/mapper/Mapper.kt new file mode 100644 index 0000000..aac38d2 --- /dev/null +++ b/abstractions/src/main/kotlin/com/melih/abstractions/mapper/Mapper.kt @@ -0,0 +1,8 @@ +package com.melih.abstractions.mapper + +import com.melih.abstractions.data.ViewEntity + +abstract class Mapper { + + abstract fun convert(t: T): R +} diff --git a/repository/src/test/kotlin/com/melih/repository/interactors/base/ResultTest.kt b/abstractions/src/test/kotlin/com/melih/abstractions/ResultTest.kt similarity index 80% rename from repository/src/test/kotlin/com/melih/repository/interactors/base/ResultTest.kt rename to abstractions/src/test/kotlin/com/melih/abstractions/ResultTest.kt index cc770df..a21f935 100644 --- a/repository/src/test/kotlin/com/melih/repository/interactors/base/ResultTest.kt +++ b/abstractions/src/test/kotlin/com/melih/abstractions/ResultTest.kt @@ -1,6 +1,10 @@ -package com.melih.repository.interactors.base +package com.melih.abstractions -import com.melih.repository.R +import com.melih.abstractions.deliverable.Failure +import com.melih.abstractions.deliverable.Reason +import com.melih.abstractions.deliverable.State +import com.melih.abstractions.deliverable.Success +import com.melih.abstractions.deliverable.handle import io.mockk.called import io.mockk.spyk import io.mockk.verify @@ -8,12 +12,17 @@ import org.amshove.kluent.shouldBeInstanceOf import org.amshove.kluent.shouldEqualTo import org.junit.jupiter.api.Test + class ResultTest { private val number = 10 private val success = Success(number) - private val failure = Failure(GenericError()) + private val failure = Failure(object : Reason() { + override val messageRes: Int + get() = 10 + + }) private val state = State.Loading() private val emptyStateBlock = spyk({ _: State -> }) @@ -37,8 +46,7 @@ class ResultTest { @Test fun `Failure should only invoke failureBlock with correct error`() { val actualFailureBlock = spyk({ reason: Reason -> - reason shouldBeInstanceOf GenericError::class - (reason as GenericError).messageRes shouldEqualTo R.string.reason_generic + reason.messageRes shouldEqualTo 10 Unit }) diff --git a/app/build.gradle b/app/build.gradle index 4eb083f..4d1af18 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply from: "$rootProject.projectDir/scripts/default_android_config.gradle" +apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle" apply from: "$rootProject.projectDir/scripts/sources.gradle" android { @@ -31,10 +32,14 @@ dependencies { androidTestImplementation testLibraries.espresso - // These libraries required by dagger to create dependency graph, but not by app - compileOnly project(':repository') + // These libraries required by dagger to create dependency graph and application compilation, but not used by app + + compileOnly project(':abstractions') + compileOnly project(':data:interactors') + compileOnly project(':data:network') + compileOnly project(':data:definitions') + compileOnly libraries.retrofit - compileOnly libraries.room compileOnly libraries.paging compileOnly libraries.swipeRefreshLayout diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index da2a358..ce0ec64 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -21,8 +21,5 @@ @com.squareup.moshi.ToJson ; } --keepnames @kotlin.Metadata class com.myapp.packagename.model.** --keep class com.myapp.packagnename.model.** { *; } - # Keeping entities intact --keep class com.melih.repository.entities.** { *; } +-keep class com.melih.definitions.entities.** { *; } diff --git a/build.gradle b/build.gradle index 232dd0a..d709464 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = '1.3.50' - ext.nav_version = '2.2.0-alpha01' + ext.nav_version = '2.2.0-beta01' repositories { google() @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:3.5.1' 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.5.1.0" @@ -20,9 +20,9 @@ buildscript { } plugins { - id "io.gitlab.arturbosch.detekt" version "1.0.0" - id "org.jetbrains.dokka" version "0.9.18" - id "jacoco" + id 'io.gitlab.arturbosch.detekt' version '1.1.1' + id 'org.jetbrains.dokka' version '0.9.18' + id 'jacoco' } allprojects { @@ -97,7 +97,9 @@ task projectDependencyGraph { rootProjects.remove(dependency) def graphKey = new Tuple2(project, dependency) - def traits = dependencies.computeIfAbsent(graphKey) { new ArrayList() } + def traits = dependencies.computeIfAbsent(graphKey) { + new ArrayList() + } if (config.name.toLowerCase().endsWith('implementation')) { traits.add('style=dotted') diff --git a/core/build.gradle b/core/build.gradle index ef8cb83..0d9ae40 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply from: "$rootProject.projectDir/scripts/default_android_config.gradle" +apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle" apply from: "$rootProject.projectDir/scripts/sources.gradle" android { @@ -14,8 +15,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation project(':repository') - + implementation libraries.coroutines implementation libraries.fragment implementation libraries.paging implementation libraries.lifecycle 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 53e1d0a..368a89e 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 @@ -9,8 +9,8 @@ import androidx.databinding.DataBindingUtil import androidx.databinding.ViewDataBinding import androidx.fragment.app.Fragment import com.google.android.material.snackbar.Snackbar +import com.melih.abstractions.deliverable.Reason import com.melih.core.R -import com.melih.repository.interactors.base.Reason /** * Parent of all fragments. 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 index ff1ff69..a004f0e 100644 --- a/core/src/main/kotlin/com/melih/core/base/paging/BasePagingDataSource.kt +++ b/core/src/main/kotlin/com/melih/core/base/paging/BasePagingDataSource.kt @@ -4,12 +4,13 @@ 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 com.melih.abstractions.data.ViewEntity +import com.melih.abstractions.deliverable.Reason +import com.melih.abstractions.deliverable.Result +import com.melih.abstractions.deliverable.State +import com.melih.abstractions.deliverable.onFailure +import com.melih.abstractions.deliverable.onState +import com.melih.abstractions.deliverable.onSuccess import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -35,11 +36,11 @@ const val INITIAL_PAGE = 0 */ @UseExperimental(ExperimentalCoroutinesApi::class) -abstract class BasePagingDataSource : PageKeyedDataSource() { +abstract class BasePagingDataSource : PageKeyedDataSource() { //region Abstractions - abstract fun loadDataForPage(page: Int): Flow>> // Load next page(s) + abstract fun loadDataForPage(page: Int): Flow>> // Load next page(s) //endregion //region Properties @@ -63,7 +64,10 @@ abstract class BasePagingDataSource : PageKeyedDataSource() { //region Functions - override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { + 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 -> @@ -81,7 +85,7 @@ abstract class BasePagingDataSource : PageKeyedDataSource() { .launchIn(coroutineScope) } - override fun loadAfter(params: LoadParams, callback: LoadCallback) { + override fun loadAfter(params: LoadParams, callback: LoadCallback) { // Key for which page to load is in params val page = params.key @@ -104,7 +108,7 @@ abstract class BasePagingDataSource : PageKeyedDataSource() { /** * 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) { + override fun loadBefore(params: LoadParams, callback: LoadCallback) { // no-op } 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 index 1a51f1e..ddd12b6 100644 --- a/core/src/main/kotlin/com/melih/core/base/paging/BasePagingFactory.kt +++ b/core/src/main/kotlin/com/melih/core/base/paging/BasePagingFactory.kt @@ -3,6 +3,7 @@ package com.melih.core.base.paging import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.paging.DataSource +import com.melih.abstractions.data.ViewEntity /** * Base [factory][DataSource.Factory] class for any [dataSource][DataSource]s in project. @@ -14,7 +15,7 @@ import androidx.paging.DataSource * * Purpose of this transmission is to encapuslate [basePagingDataSource][BasePagingDataSource]. */ -abstract class BasePagingFactory : DataSource.Factory() { +abstract class BasePagingFactory : DataSource.Factory() { //region Abstractions 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 index 413a602..7a0caad 100644 --- a/core/src/main/kotlin/com/melih/core/base/viewmodel/BasePagingViewModel.kt +++ b/core/src/main/kotlin/com/melih/core/base/viewmodel/BasePagingViewModel.kt @@ -5,9 +5,10 @@ import androidx.lifecycle.Transformations.switchMap import androidx.lifecycle.ViewModel import androidx.paging.PagedList import androidx.paging.toLiveData +import com.melih.abstractions.data.ViewEntity +import com.melih.abstractions.deliverable.Reason +import com.melih.abstractions.deliverable.State 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]. @@ -18,7 +19,7 @@ import com.melih.repository.interactors.base.State * * If paging won't be used, use [BaseViewModel] instead. */ -abstract class BasePagingViewModel : ViewModel() { +abstract class BasePagingViewModel : ViewModel() { //region Abstractions 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 6e7c440..525a772 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 @@ -4,8 +4,8 @@ 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.State +import com.melih.abstractions.deliverable.Reason +import com.melih.abstractions.deliverable.State import kotlinx.coroutines.launch /** diff --git a/core/src/main/kotlin/com/melih/core/extensions/UtilityExtension.kt b/core/src/main/kotlin/com/melih/core/extensions/UtilityExtension.kt deleted file mode 100644 index c57840f..0000000 --- a/core/src/main/kotlin/com/melih/core/extensions/UtilityExtension.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.melih.core.extensions - -import android.view.MenuItem -import androidx.appcompat.widget.SearchView -import com.melih.core.utils.ClearFocusQueryTextListener - -/** - * Shorthand for [contains] with ignoreCase set [true] - */ -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/test/kotlin/com/melih/core/paging/BasePagingDataSourceTest.kt b/core/src/test/kotlin/com/melih/core/paging/BasePagingDataSourceTest.kt index 5750ad1..700bbfb 100644 --- a/core/src/test/kotlin/com/melih/core/paging/BasePagingDataSourceTest.kt +++ b/core/src/test/kotlin/com/melih/core/paging/BasePagingDataSourceTest.kt @@ -3,14 +3,15 @@ package com.melih.core.paging import androidx.paging.PageKeyedDataSource +import com.melih.abstractions.data.ViewEntity +import com.melih.abstractions.deliverable.Failure +import com.melih.abstractions.deliverable.Reason +import com.melih.abstractions.deliverable.Result +import com.melih.abstractions.deliverable.State +import com.melih.abstractions.deliverable.Success 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 @@ -28,7 +29,7 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { val failureSource = spyk(TestFailureSource()) val data = 10 - val errorMessage = "Generic Error" + val errorMessageResId = 1313 @Nested inner class BasePagingSource { @@ -37,10 +38,9 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { inner class LoadInitial { @Test - fun `should update state accordingly`() { val params = mockk>(relaxed = true) - val callback = mockk>(relaxed = true) + val callback = mockk>(relaxed = true) runBlocking { @@ -54,10 +54,9 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { } @Test - fun `should update error Error accordingly`() { val params = PageKeyedDataSource.LoadInitialParams(10, false) - val callback = mockk>(relaxed = true) + val callback = mockk>(relaxed = true) runBlocking { @@ -65,7 +64,7 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { failureSource.loadInitial(params, callback) failureSource.reasonData.testObserve { - it shouldBeInstanceOf GenericError::class + it shouldBeInstanceOf TestFailureReason::class } } } @@ -75,10 +74,9 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { inner class LoadAfter { @Test - fun `should update state accordingly`() { val params = PageKeyedDataSource.LoadParams(2, 10) - val callback = mockk>(relaxed = true) + val callback = mockk>(relaxed = true) runBlocking { @@ -92,10 +90,9 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { } @Test - fun `should update error Error accordingly`() { val params = PageKeyedDataSource.LoadParams(2, 10) - val callback = mockk>(relaxed = true) + val callback = mockk>(relaxed = true) runBlocking { @@ -103,17 +100,16 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { failureSource.loadAfter(params, callback) failureSource.reasonData.testObserve { - it shouldBeInstanceOf GenericError::class + it shouldBeInstanceOf TestFailureReason::class } } } } @Test - fun `should use loadDataForPage in loadInitial and transform emmited value`() { val params = mockk>(relaxed = true) - val callback = mockk>(relaxed = true) + val callback = mockk>(relaxed = true) // Fake loading source.loadInitial(params, callback) @@ -126,10 +122,9 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { } @Test - fun `should use loadDataForPage in loadAfter and transform emmited value`() { val params = PageKeyedDataSource.LoadParams(2, 10) - val callback = mockk>(relaxed = true) + val callback = mockk>(relaxed = true) // Fake loading source.loadAfter(params, callback) @@ -142,25 +137,27 @@ class BasePagingDataSourceTest : BaseTestWithMainThread() { } } - inner class TestSource : BasePagingDataSource() { - + inner class TestSource : BasePagingDataSource() { val result = flow { emit(State.Loading()) - emit(Success(listOf(data))) + emit(Success(listOf(TestViewEntity(data)))) } - - override fun loadDataForPage(page: Int): Flow>> = result + override fun loadDataForPage(page: Int): Flow>> = result } - inner class TestFailureSource : BasePagingDataSource() { + inner class TestFailureSource : BasePagingDataSource() { val result = flow { emit(State.Loading()) - emit(Failure(GenericError())) + emit(Failure(TestFailureReason(errorMessageResId))) } - override fun loadDataForPage(page: Int): Flow>> = result + override fun loadDataForPage(page: Int): Flow>> = result } -} \ No newline at end of file + + inner class TestViewEntity(data: Int) : ViewEntity + + inner class TestFailureReason(override val messageRes: Int) : Reason() +} diff --git a/core/src/test/kotlin/com/melih/core/paging/BasePagingFactoryTest.kt b/core/src/test/kotlin/com/melih/core/paging/BasePagingFactoryTest.kt index 5829189..cfb6145 100644 --- a/core/src/test/kotlin/com/melih/core/paging/BasePagingFactoryTest.kt +++ b/core/src/test/kotlin/com/melih/core/paging/BasePagingFactoryTest.kt @@ -1,5 +1,6 @@ package com.melih.core.paging +import com.melih.abstractions.data.ViewEntity import com.melih.core.BaseTestWithMainThread import com.melih.core.base.paging.BasePagingDataSource import com.melih.core.base.paging.BasePagingFactory @@ -25,9 +26,10 @@ class BasePagingFactoryTest : BaseTestWithMainThread() { } } - inner class TestFactory : BasePagingFactory() { - - override fun createSource(): BasePagingDataSource = mockk(relaxed = true) + inner class TestFactory : BasePagingFactory() { + override fun createSource(): BasePagingDataSource = mockk(relaxed = true) } + + inner class TestViewEntity : ViewEntity } diff --git a/data/definitions/build.gradle b/data/definitions/build.gradle new file mode 100644 index 0000000..b234e17 --- /dev/null +++ b/data/definitions/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +apply from: "$rootProject.projectDir/scripts/module.gradle" + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation project(':abstractions') + + implementation libraries.room + implementation libraries.moshiKotlin + + kapt annotationProcessors.roomCompiler + kapt annotationProcessors.moshi +} diff --git a/data/definitions/consumer-rules.pro b/data/definitions/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/data/definitions/proguard-rules.pro b/data/definitions/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/data/definitions/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/data/definitions/src/main/AndroidManifest.xml b/data/definitions/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8363ce0 --- /dev/null +++ b/data/definitions/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + diff --git a/repository/src/main/kotlin/com/melih/repository/Constants.kt b/data/definitions/src/main/kotlin/com/melih/definitions/Constants.kt similarity index 74% rename from repository/src/main/kotlin/com/melih/repository/Constants.kt rename to data/definitions/src/main/kotlin/com/melih/definitions/Constants.kt index 5d5e2dc..533f176 100644 --- a/repository/src/main/kotlin/com/melih/repository/Constants.kt +++ b/data/definitions/src/main/kotlin/com/melih/definitions/Constants.kt @@ -1,4 +1,4 @@ -package com.melih.repository +package com.melih.definitions internal const val DEFAULT_NAME = "Default name" internal const val EMPTY_STRING = "" diff --git a/data/definitions/src/main/kotlin/com/melih/definitions/Source.kt b/data/definitions/src/main/kotlin/com/melih/definitions/Source.kt new file mode 100644 index 0000000..289304f --- /dev/null +++ b/data/definitions/src/main/kotlin/com/melih/definitions/Source.kt @@ -0,0 +1,23 @@ +package com.melih.definitions + +import com.melih.abstractions.data.ViewEntity +import com.melih.abstractions.deliverable.Result +import com.melih.abstractions.mapper.Mapper +import com.melih.definitions.entities.LaunchEntity + +/** + * Contract for sources to seperate business logic from build and return type + */ +interface Source { + + //region Abstractions + + suspend fun getNextLaunches( + count: Int, + page: Int, + mapper: Mapper + ): Result> + + suspend fun getLaunchById(id: Long, mapper: Mapper): Result + //endregion +} diff --git a/repository/src/main/kotlin/com/melih/repository/entities/LaunchEntity.kt b/data/definitions/src/main/kotlin/com/melih/definitions/entities/LaunchEntity.kt similarity index 87% rename from repository/src/main/kotlin/com/melih/repository/entities/LaunchEntity.kt rename to data/definitions/src/main/kotlin/com/melih/definitions/entities/LaunchEntity.kt index 2a7bdbd..43779e8 100644 --- a/repository/src/main/kotlin/com/melih/repository/entities/LaunchEntity.kt +++ b/data/definitions/src/main/kotlin/com/melih/definitions/entities/LaunchEntity.kt @@ -1,8 +1,8 @@ -package com.melih.repository.entities +package com.melih.definitions.entities import androidx.room.Entity import androidx.room.PrimaryKey -import com.melih.repository.DEFAULT_NAME +import com.melih.definitions.DEFAULT_NAME import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/repository/src/main/kotlin/com/melih/repository/entities/LaunchesEntity.kt b/data/definitions/src/main/kotlin/com/melih/definitions/entities/LaunchesEntity.kt similarity index 86% rename from repository/src/main/kotlin/com/melih/repository/entities/LaunchesEntity.kt rename to data/definitions/src/main/kotlin/com/melih/definitions/entities/LaunchesEntity.kt index eaa55df..f6f3762 100644 --- a/repository/src/main/kotlin/com/melih/repository/entities/LaunchesEntity.kt +++ b/data/definitions/src/main/kotlin/com/melih/definitions/entities/LaunchesEntity.kt @@ -1,4 +1,4 @@ -package com.melih.repository.entities +package com.melih.definitions.entities import com.squareup.moshi.JsonClass diff --git a/repository/src/main/kotlin/com/melih/repository/entities/LocationEntity.kt b/data/definitions/src/main/kotlin/com/melih/definitions/entities/LocationEntity.kt similarity index 87% rename from repository/src/main/kotlin/com/melih/repository/entities/LocationEntity.kt rename to data/definitions/src/main/kotlin/com/melih/definitions/entities/LocationEntity.kt index 652c4d3..af217ef 100644 --- a/repository/src/main/kotlin/com/melih/repository/entities/LocationEntity.kt +++ b/data/definitions/src/main/kotlin/com/melih/definitions/entities/LocationEntity.kt @@ -1,7 +1,7 @@ -package com.melih.repository.entities +package com.melih.definitions.entities import androidx.room.ColumnInfo -import com.melih.repository.DEFAULT_NAME +import com.melih.definitions.DEFAULT_NAME import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) diff --git a/repository/src/main/kotlin/com/melih/repository/entities/MissionEntity.kt b/data/definitions/src/main/kotlin/com/melih/definitions/entities/MissionEntity.kt similarity index 73% rename from repository/src/main/kotlin/com/melih/repository/entities/MissionEntity.kt rename to data/definitions/src/main/kotlin/com/melih/definitions/entities/MissionEntity.kt index 971ec00..a8d1141 100644 --- a/repository/src/main/kotlin/com/melih/repository/entities/MissionEntity.kt +++ b/data/definitions/src/main/kotlin/com/melih/definitions/entities/MissionEntity.kt @@ -1,8 +1,8 @@ -package com.melih.repository.entities +package com.melih.definitions.entities import androidx.room.ColumnInfo -import com.melih.repository.DEFAULT_NAME -import com.melih.repository.EMPTY_STRING +import com.melih.definitions.DEFAULT_NAME +import com.melih.definitions.EMPTY_STRING import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) diff --git a/repository/src/main/kotlin/com/melih/repository/entities/RocketEntity.kt b/data/definitions/src/main/kotlin/com/melih/definitions/entities/RocketEntity.kt similarity index 78% rename from repository/src/main/kotlin/com/melih/repository/entities/RocketEntity.kt rename to data/definitions/src/main/kotlin/com/melih/definitions/entities/RocketEntity.kt index 7fd8c3d..1b3f01e 100644 --- a/repository/src/main/kotlin/com/melih/repository/entities/RocketEntity.kt +++ b/data/definitions/src/main/kotlin/com/melih/definitions/entities/RocketEntity.kt @@ -1,8 +1,8 @@ -package com.melih.repository.entities +package com.melih.definitions.entities import androidx.room.ColumnInfo -import com.melih.repository.DEFAULT_NAME -import com.melih.repository.EMPTY_STRING +import com.melih.definitions.DEFAULT_NAME +import com.melih.definitions.EMPTY_STRING import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/data/interactors/build.gradle b/data/interactors/build.gradle new file mode 100644 index 0000000..b09d7d2 --- /dev/null +++ b/data/interactors/build.gradle @@ -0,0 +1,22 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +apply from: "$rootProject.projectDir/scripts/module.gradle" +apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle" + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation project(':data:definitions') + implementation project(':data:network') + implementation project(':data:persistence') + + implementation libraries.coroutines + implementation libraries.retrofit + + testImplementation testLibraries.coroutinesCore + testImplementation testLibraries.coroutinesTest + + compileOnly libraries.room +} diff --git a/data/interactors/consumer-rules.pro b/data/interactors/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/data/interactors/proguard-rules.pro b/data/interactors/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/data/interactors/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/data/interactors/src/main/AndroidManifest.xml b/data/interactors/src/main/AndroidManifest.xml new file mode 100644 index 0000000..86a5aa4 --- /dev/null +++ b/data/interactors/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + diff --git a/data/interactors/src/main/kotlin/com/melih/interactors/GetLaunchDetails.kt b/data/interactors/src/main/kotlin/com/melih/interactors/GetLaunchDetails.kt new file mode 100644 index 0000000..50fef98 --- /dev/null +++ b/data/interactors/src/main/kotlin/com/melih/interactors/GetLaunchDetails.kt @@ -0,0 +1,40 @@ +package com.melih.interactors + +import com.melih.abstractions.data.ViewEntity +import com.melih.abstractions.deliverable.Result +import com.melih.abstractions.mapper.Mapper +import com.melih.definitions.entities.LaunchEntity +import com.melih.interactors.base.BaseInteractor +import com.melih.interactors.base.InteractorParameters +import com.melih.interactors.sources.LaunchesSource +import kotlinx.coroutines.flow.FlowCollector +import javax.inject.Inject + +/** + * Gets next given number of launches + */ +class GetLaunchDetails @Inject constructor( + private val mapper: @JvmSuppressWildcards Mapper +) : BaseInteractor() { + + //region Properties + + @Inject + internal lateinit var launchesSource: LaunchesSource + //endregion + + //region Functions + + override suspend fun FlowCollector>.run(params: Params) { + emit(launchesSource.getLaunchById(params.id, mapper)) + } + //endregion + + + //region Parameters + + data class Params( + val id: Long + ) : InteractorParameters + //endregion +} diff --git a/data/interactors/src/main/kotlin/com/melih/interactors/GetLaunches.kt b/data/interactors/src/main/kotlin/com/melih/interactors/GetLaunches.kt new file mode 100644 index 0000000..0f3e601 --- /dev/null +++ b/data/interactors/src/main/kotlin/com/melih/interactors/GetLaunches.kt @@ -0,0 +1,44 @@ +package com.melih.interactors + +import com.melih.abstractions.data.ViewEntity +import com.melih.abstractions.deliverable.Result +import com.melih.abstractions.mapper.Mapper +import com.melih.definitions.entities.LaunchEntity +import com.melih.interactors.base.BaseInteractor +import com.melih.interactors.base.InteractorParameters +import com.melih.interactors.sources.LaunchesSource +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 mapper: @JvmSuppressWildcards Mapper +) : BaseInteractor, GetLaunches.Params>() { + + //region Properties + + @Inject + internal lateinit var launchesSource: LaunchesSource + //endregion + + //region Functions + + override suspend fun FlowCollector>>.run(params: Params) { + + // Start network fetch - we're not handling state here to ommit them + emit( + launchesSource + .getNextLaunches(params.count, params.page, mapper) + ) + } + //endregion + + data class Params( + 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/data/interactors/src/main/kotlin/com/melih/interactors/base/BaseInteractor.kt similarity index 88% rename from repository/src/main/kotlin/com/melih/repository/interactors/base/BaseInteractor.kt rename to data/interactors/src/main/kotlin/com/melih/interactors/base/BaseInteractor.kt index bb7605c..ffcca7f 100644 --- a/repository/src/main/kotlin/com/melih/repository/interactors/base/BaseInteractor.kt +++ b/data/interactors/src/main/kotlin/com/melih/interactors/base/BaseInteractor.kt @@ -1,5 +1,7 @@ -package com.melih.repository.interactors.base +package com.melih.interactors.base +import com.melih.abstractions.deliverable.Result +import com.melih.abstractions.deliverable.State import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow diff --git a/data/interactors/src/main/kotlin/com/melih/interactors/error/InteractionErrorReason.kt b/data/interactors/src/main/kotlin/com/melih/interactors/error/InteractionErrorReason.kt new file mode 100644 index 0000000..3242f88 --- /dev/null +++ b/data/interactors/src/main/kotlin/com/melih/interactors/error/InteractionErrorReason.kt @@ -0,0 +1,17 @@ +package com.melih.interactors.error + +import androidx.annotation.StringRes +import com.melih.abstractions.deliverable.Reason +import com.melih.interactors.R + +sealed class InteractionErrorReason(@StringRes override val messageRes: Int) : Reason() + +class GenericError(@StringRes override val messageRes: Int = R.string.reason_generic) : InteractionErrorReason(messageRes) + +sealed class NetworkError(override val messageRes: Int) : InteractionErrorReason(messageRes) +class ConnectionError : NetworkError(R.string.reason_network) +class EmptyResultError : NetworkError(R.string.reason_empty_body) +class ResponseError : NetworkError(R.string.reason_response) +class TimeoutError : NetworkError(R.string.reason_timeout) + +class PersistenceEmptyError : InteractionErrorReason(R.string.reason_persistance_empty) diff --git a/data/interactors/src/main/kotlin/com/melih/interactors/sources/LaunchesSource.kt b/data/interactors/src/main/kotlin/com/melih/interactors/sources/LaunchesSource.kt new file mode 100644 index 0000000..ad73cce --- /dev/null +++ b/data/interactors/src/main/kotlin/com/melih/interactors/sources/LaunchesSource.kt @@ -0,0 +1,163 @@ +package com.melih.interactors.sources + +import android.content.Context +import android.net.NetworkInfo +import com.melih.abstractions.data.ViewEntity +import com.melih.abstractions.deliverable.Failure +import com.melih.abstractions.deliverable.Result +import com.melih.abstractions.deliverable.Success +import com.melih.abstractions.mapper.Mapper +import com.melih.definitions.Source +import com.melih.definitions.entities.LaunchEntity +import com.melih.interactors.DEFAULT_LAUNCHES_AMOUNT +import com.melih.interactors.error.ConnectionError +import com.melih.interactors.error.EmptyResultError +import com.melih.interactors.error.NetworkError +import com.melih.interactors.error.PersistenceEmptyError +import com.melih.interactors.error.ResponseError +import com.melih.interactors.error.TimeoutError +import com.melih.network.ApiImpl +import com.melih.persistence.LaunchesDatabase +import retrofit2.Response +import java.io.IOException +import javax.inject.Inject +import javax.inject.Provider + +private const val DEFAULT_IMAGE_SIZE = 480 + +internal class LaunchesSource @Inject constructor( + ctx: Context, + private val apiImpl: ApiImpl, + private val networkInfoProvider: Provider +) : Source { + + //region Properties + + private val launchesDatabase = LaunchesDatabase.getInstance(ctx) + + private val isNetworkConnected: Boolean + get() { + val networkInfo = networkInfoProvider.get() + return networkInfo != null && networkInfo.isConnected + } + //endregion + + //region Functions + + override suspend fun getNextLaunches( + count: Int, + page: Int, mapper: Mapper + ): Result> { + val networkResponse = safeExecute({ + apiImpl.getNextLaunches(count, page * DEFAULT_LAUNCHES_AMOUNT) + }) { entity -> + entity.launches + .map(::transformRocketImageUrl) + .saveLaunches() + .map(mapper::convert) + } + + return if (networkResponse is NetworkError) { + launchesDatabase + .launchesDao + .getLaunches(count, page) + .takeUnless { it.isNullOrEmpty() } + ?.run { + Success(map(mapper::convert)) + } ?: Failure(PersistenceEmptyError()) + } else { + networkResponse + } + } + + override suspend fun getLaunchById( + id: Long, + mapper: Mapper + ): Result { + return launchesDatabase + .launchesDao + .getLaunchById(id) + .takeIf { it != null } + ?.run { + Success(mapper.convert(this)) + } ?: loadLaunchFromNetwork(id, mapper) + } + + private suspend fun loadLaunchFromNetwork( + id: Long, + mapper: Mapper + ): Result = + safeExecute({ + apiImpl.getLaunchById(id) + }) { + mapper.convert( + transformRocketImageUrl(it) + .saveLaunch() + ) + } + + private suspend fun List.saveLaunches() = run { + launchesDatabase.launchesDao.saveLaunches(this) + this + } + + private suspend fun LaunchEntity.saveLaunch() = run { + launchesDatabase.launchesDao.saveLaunch(this) + this + } + + private inline fun safeExecute( + block: () -> Response, + transform: (T) -> R + ) = + if (isNetworkConnected) { + try { + block().extractResponseBody(transform) + } catch (e: IOException) { + Failure(TimeoutError()) + } + } else { + Failure(ConnectionError()) + } + + private inline fun Response.extractResponseBody(transform: (T) -> R) = + if (isSuccessful) { + body()?.let { + Success(transform(it)) + } ?: Failure(EmptyResultError()) + } else { + Failure(ResponseError()) + } + + private fun transformRocketImageUrl(launch: LaunchEntity) = + if (!launch.rocket.imageURL.isNotBlank()) { + launch.copy( + rocket = launch.rocket.copy( + imageURL = transformImageUrl( + launch.rocket.imageURL, + launch.rocket.imageSizes + ) + ) + ) + } else { + launch + } + + private fun transformImageUrl(imageUrl: String, supportedSizes: IntArray) = + try { + val urlSplit = imageUrl.split("_") + val url = urlSplit[0] + val format = urlSplit[1].split(".")[1] + + val requestedSize = if (!supportedSizes.contains(DEFAULT_IMAGE_SIZE)) { + supportedSizes.last { it < DEFAULT_IMAGE_SIZE } + } else { + DEFAULT_IMAGE_SIZE + } + + "${url}_$requestedSize.$format" + } catch (e: Exception) { + imageUrl + } + //endregion +} diff --git a/repository/src/main/res/values/strings.xml b/data/interactors/src/main/res/values/strings.xml similarity index 81% rename from repository/src/main/res/values/strings.xml rename to data/interactors/src/main/res/values/strings.xml index 92cdbc9..13e42df 100644 --- a/repository/src/main/res/values/strings.xml +++ b/data/interactors/src/main/res/values/strings.xml @@ -1,9 +1,8 @@ + Something went wrong + There are no saved launches Network error Response is empty - Something went wrong Woops, seems we got a server error Server timed out - There are no saved launches - Seems there are no data and network diff --git a/repository/src/test/kotlin/com/melih/repository/interactors/base/BaseInteractorTest.kt b/data/interactors/src/test/kotlin/com/melih/interactors/base/BaseInteractorTest.kt similarity index 91% rename from repository/src/test/kotlin/com/melih/repository/interactors/base/BaseInteractorTest.kt rename to data/interactors/src/test/kotlin/com/melih/interactors/base/BaseInteractorTest.kt index 35d922c..5505f1a 100644 --- a/repository/src/test/kotlin/com/melih/repository/interactors/base/BaseInteractorTest.kt +++ b/data/interactors/src/test/kotlin/com/melih/interactors/base/BaseInteractorTest.kt @@ -1,5 +1,8 @@ -package com.melih.repository.interactors.base +package com.melih.interactors.base +import com.melih.abstractions.deliverable.Result +import com.melih.abstractions.deliverable.State +import com.melih.abstractions.deliverable.Success import io.mockk.coVerify import io.mockk.spyk import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -10,7 +13,7 @@ import kotlinx.coroutines.runBlocking import org.amshove.kluent.shouldBeInstanceOf import org.amshove.kluent.shouldEqualTo import org.junit.jupiter.api.Test -import java.util.* +import java.util.ArrayDeque @UseExperimental(ExperimentalCoroutinesApi::class) class BaseInteractorTest { diff --git a/data/network/build.gradle b/data/network/build.gradle new file mode 100644 index 0000000..cc02d5c --- /dev/null +++ b/data/network/build.gradle @@ -0,0 +1,20 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +apply from: "$rootProject.projectDir/scripts/module.gradle" +apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle" + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation project(':data:definitions') + + implementation libraries.okHttpLogger + implementation libraries.moshiKotlin + implementation libraries.coroutines + implementation libraries.retrofit + + testImplementation testLibraries.coroutinesCore + testImplementation testLibraries.coroutinesTest +} diff --git a/data/network/consumer-rules.pro b/data/network/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/data/network/proguard-rules.pro b/data/network/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/data/network/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/data/network/src/main/AndroidManifest.xml b/data/network/src/main/AndroidManifest.xml new file mode 100644 index 0000000..32d8ca6 --- /dev/null +++ b/data/network/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + diff --git a/repository/src/main/kotlin/com/melih/repository/network/Api.kt b/data/network/src/main/kotlin/com/melih/network/api/Api.kt similarity index 78% rename from repository/src/main/kotlin/com/melih/repository/network/Api.kt rename to data/network/src/main/kotlin/com/melih/network/api/Api.kt index 2783559..bef517d 100644 --- a/repository/src/main/kotlin/com/melih/repository/network/Api.kt +++ b/data/network/src/main/kotlin/com/melih/network/api/Api.kt @@ -1,7 +1,7 @@ -package com.melih.repository.network +package com.melih.network -import com.melih.repository.entities.LaunchEntity -import com.melih.repository.entities.LaunchesEntity +import com.melih.definitions.entities.LaunchEntity +import com.melih.definitions.entities.LaunchesEntity import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path diff --git a/repository/src/main/kotlin/com/melih/repository/network/ApiImpl.kt b/data/network/src/main/kotlin/com/melih/network/api/ApiImpl.kt similarity index 88% rename from repository/src/main/kotlin/com/melih/repository/network/ApiImpl.kt rename to data/network/src/main/kotlin/com/melih/network/api/ApiImpl.kt index c56ffb8..ff195c6 100644 --- a/repository/src/main/kotlin/com/melih/repository/network/ApiImpl.kt +++ b/data/network/src/main/kotlin/com/melih/network/api/ApiImpl.kt @@ -1,7 +1,7 @@ -package com.melih.repository.network +package com.melih.network -import com.melih.repository.entities.LaunchEntity -import com.melih.repository.entities.LaunchesEntity +import com.melih.definitions.entities.LaunchEntity +import com.melih.definitions.entities.LaunchesEntity import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import okhttp3.OkHttpClient @@ -14,7 +14,7 @@ import javax.inject.Inject internal const val TIMEOUT_DURATION = 7L -internal class ApiImpl @Inject constructor() : Api { +class ApiImpl @Inject constructor() : Api { //region Properties diff --git a/repository/build.gradle b/data/persistence/build.gradle similarity index 82% rename from repository/build.gradle rename to data/persistence/build.gradle index 843f46d..fcc536e 100644 --- a/repository/build.gradle +++ b/data/persistence/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply from: "$rootProject.projectDir/scripts/module.gradle" +apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle" android { defaultConfig { @@ -17,15 +18,13 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation libraries.coroutines - implementation libraries.liveDataKTX - implementation libraries.retrofit - implementation libraries.room + implementation project(':data:definitions') + implementation libraries.moshiKotlin - implementation libraries.okHttpLogger + implementation libraries.coroutines + implementation libraries.room kapt annotationProcessors.roomCompiler - kapt annotationProcessors.moshi testImplementation testLibraries.coroutinesCore testImplementation testLibraries.coroutinesTest diff --git a/data/persistence/consumer-rules.pro b/data/persistence/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/data/persistence/proguard-rules.pro b/data/persistence/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/data/persistence/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/data/persistence/src/main/AndroidManifest.xml b/data/persistence/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4d3d76b --- /dev/null +++ b/data/persistence/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/LaunchesDatabase.kt b/data/persistence/src/main/kotlin/com/melih/persistence/LaunchesDatabase.kt similarity index 65% rename from repository/src/main/kotlin/com/melih/repository/persistence/LaunchesDatabase.kt rename to data/persistence/src/main/kotlin/com/melih/persistence/LaunchesDatabase.kt index 636dbab..da7a83c 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/LaunchesDatabase.kt +++ b/data/persistence/src/main/kotlin/com/melih/persistence/LaunchesDatabase.kt @@ -1,15 +1,15 @@ -package com.melih.repository.persistence +package com.melih.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 -import com.melih.repository.persistence.converters.LocationConverter -import com.melih.repository.persistence.converters.MissionConverter -import com.melih.repository.persistence.converters.RocketConverter -import com.melih.repository.persistence.dao.LaunchesDao +import com.melih.definitions.entities.LaunchEntity +import com.melih.persistence.converters.LocationConverter +import com.melih.persistence.converters.MissionConverter +import com.melih.persistence.converters.RocketConverter +import com.melih.persistence.dao.LaunchesDao const val DB_NAME = "LaunchesDB" @@ -26,7 +26,7 @@ const val DB_NAME = "LaunchesDB" RocketConverter::class, MissionConverter::class ) -internal abstract class LaunchesDatabase : RoomDatabase() { +abstract class LaunchesDatabase : RoomDatabase() { //region Companion @@ -48,6 +48,6 @@ internal abstract class LaunchesDatabase : RoomDatabase() { //region Abstractions - internal abstract val launchesDao: LaunchesDao + abstract val launchesDao: LaunchesDao //endregion } diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseConverter.kt b/data/persistence/src/main/kotlin/com/melih/persistence/converters/BaseConverter.kt similarity index 93% rename from repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseConverter.kt rename to data/persistence/src/main/kotlin/com/melih/persistence/converters/BaseConverter.kt index cc9d698..a0bb43f 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseConverter.kt +++ b/data/persistence/src/main/kotlin/com/melih/persistence/converters/BaseConverter.kt @@ -1,4 +1,4 @@ -package com.melih.repository.persistence.converters +package com.melih.persistence.converters import androidx.room.TypeConverter import com.squareup.moshi.JsonAdapter diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseListConverter.kt b/data/persistence/src/main/kotlin/com/melih/persistence/converters/BaseListConverter.kt similarity index 93% rename from repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseListConverter.kt rename to data/persistence/src/main/kotlin/com/melih/persistence/converters/BaseListConverter.kt index 7bdc189..df5fe73 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/converters/BaseListConverter.kt +++ b/data/persistence/src/main/kotlin/com/melih/persistence/converters/BaseListConverter.kt @@ -1,4 +1,4 @@ -package com.melih.repository.persistence.converters +package com.melih.persistence.converters import androidx.room.TypeConverter import com.squareup.moshi.JsonAdapter diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/LocationConverter.kt b/data/persistence/src/main/kotlin/com/melih/persistence/converters/LocationConverter.kt similarity index 63% rename from repository/src/main/kotlin/com/melih/repository/persistence/converters/LocationConverter.kt rename to data/persistence/src/main/kotlin/com/melih/persistence/converters/LocationConverter.kt index 399efb9..a4d375d 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/converters/LocationConverter.kt +++ b/data/persistence/src/main/kotlin/com/melih/persistence/converters/LocationConverter.kt @@ -1,7 +1,7 @@ -package com.melih.repository.persistence.converters +package com.melih.persistence.converters -import com.melih.repository.entities.LocationEntity -import com.melih.repository.entities.LocationEntityJsonAdapter +import com.melih.definitions.entities.LocationEntity +import com.melih.definitions.entities.LocationEntityJsonAdapter import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/MissionConverter.kt b/data/persistence/src/main/kotlin/com/melih/persistence/converters/MissionConverter.kt similarity index 81% rename from repository/src/main/kotlin/com/melih/repository/persistence/converters/MissionConverter.kt rename to data/persistence/src/main/kotlin/com/melih/persistence/converters/MissionConverter.kt index 1d25e9a..6a524e4 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/converters/MissionConverter.kt +++ b/data/persistence/src/main/kotlin/com/melih/persistence/converters/MissionConverter.kt @@ -1,6 +1,6 @@ -package com.melih.repository.persistence.converters +package com.melih.persistence.converters -import com.melih.repository.entities.MissionEntity +import com.melih.definitions.entities.MissionEntity import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/converters/RocketConverter.kt b/data/persistence/src/main/kotlin/com/melih/persistence/converters/RocketConverter.kt similarity index 63% rename from repository/src/main/kotlin/com/melih/repository/persistence/converters/RocketConverter.kt rename to data/persistence/src/main/kotlin/com/melih/persistence/converters/RocketConverter.kt index 66b3606..d98102b 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/converters/RocketConverter.kt +++ b/data/persistence/src/main/kotlin/com/melih/persistence/converters/RocketConverter.kt @@ -1,7 +1,7 @@ -package com.melih.repository.persistence.converters +package com.melih.persistence.converters -import com.melih.repository.entities.RocketEntity -import com.melih.repository.entities.RocketEntityJsonAdapter +import com.melih.definitions.entities.RocketEntity +import com.melih.definitions.entities.RocketEntityJsonAdapter import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi diff --git a/repository/src/main/kotlin/com/melih/repository/persistence/dao/LaunchesDao.kt b/data/persistence/src/main/kotlin/com/melih/persistence/dao/LaunchesDao.kt similarity index 86% rename from repository/src/main/kotlin/com/melih/repository/persistence/dao/LaunchesDao.kt rename to data/persistence/src/main/kotlin/com/melih/persistence/dao/LaunchesDao.kt index 2490cdd..7d14666 100644 --- a/repository/src/main/kotlin/com/melih/repository/persistence/dao/LaunchesDao.kt +++ b/data/persistence/src/main/kotlin/com/melih/persistence/dao/LaunchesDao.kt @@ -1,16 +1,16 @@ -package com.melih.repository.persistence.dao +package com.melih.persistence.dao import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import com.melih.repository.entities.LaunchEntity +import com.melih.definitions.entities.LaunchEntity /** * DAO for list of [launches][LaunchEntity] */ @Dao -internal abstract class LaunchesDao { +abstract class LaunchesDao { //region Queries diff --git a/docs/module_graph.png b/docs/module_graph.png index 7add4b6..eab7666 100644 Binary files a/docs/module_graph.png and b/docs/module_graph.png differ diff --git a/fastlane/Fastfile b/fastlane/Fastfile index ce5b3e9..43c06ce 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -50,9 +50,29 @@ platform :android do run_detail_tests() end - desc "Runs tests in repository module" - lane :test_repository do - run_repository_tests() + desc "Runs tests in abstraction module" + lane :test_abstractions do + run_abstractions_tests() + end + + desc "Runs tests in definitions module" + lane :test_definitions do + run_definitions_tests() + end + + desc "Runs tests in interactors module" + lane :test_interactors do + run_interactors_tests() + end + + desc "Runs tests in network module" + lane :test_network do + run_network_tests() + end + + desc "Runs tests in persistence module" + lane :test_persistence do + run_persistence_tests() end # ================ Gradle tasks ================ @@ -81,7 +101,23 @@ platform :android do gradle(task: "features:detail:test --continue") end - def run_repository_tests - gradle(task: "repository:test --continue") + def run_abstractions_tests + gradle(task: "abstractions:test --continue") + end + + def run_definitions_tests + gradle(task: "data:definitions:test --continue") + end + + def run_interactors_tests + gradle(task: "data:interactors:test --continue") + end + + def run_network_tests + gradle(task: "data:network:test --continue") + end + + def run_persistence_tests + gradle(task: "data:persistence:test --continue") end end diff --git a/features/detail/build.gradle b/features/detail/build.gradle index 43a02bd..cea8317 100644 --- a/features/detail/build.gradle +++ b/features/detail/build.gradle @@ -14,7 +14,5 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation project(':repository') - testImplementation testLibraries.coroutinesTest } diff --git a/features/detail/src/main/kotlin/com/melih/detail/data/LaunchDetailItem.kt b/features/detail/src/main/kotlin/com/melih/detail/data/LaunchDetailItem.kt new file mode 100644 index 0000000..e6676f4 --- /dev/null +++ b/features/detail/src/main/kotlin/com/melih/detail/data/LaunchDetailItem.kt @@ -0,0 +1,10 @@ +package com.melih.launches.data + +import com.melih.abstractions.data.ViewEntity + +data class LaunchDetailItem( + val id: Long, + val imageUrl: String, + val rocketName: String, + val missionDescription: String +) : ViewEntity diff --git a/features/detail/src/main/kotlin/com/melih/detail/data/LaunchDetailMapper.kt b/features/detail/src/main/kotlin/com/melih/detail/data/LaunchDetailMapper.kt new file mode 100644 index 0000000..32f61c0 --- /dev/null +++ b/features/detail/src/main/kotlin/com/melih/detail/data/LaunchDetailMapper.kt @@ -0,0 +1,18 @@ +package com.melih.launches.data + +import com.melih.abstractions.mapper.Mapper +import com.melih.definitions.entities.LaunchEntity +import javax.inject.Inject + +class LaunchDetailMapper @Inject constructor() : Mapper() { + + override fun convert(launchEntity: LaunchEntity) = + with(launchEntity) { + LaunchDetailItem( + id, + rocket.imageURL, + rocket.name, + if (!missions.isNullOrEmpty()) missions[0].description else "" + ) + } +} diff --git a/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailFragmentModule.kt b/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailFragmentModule.kt index c4a58a2..946e852 100644 --- a/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailFragmentModule.kt +++ b/features/detail/src/main/kotlin/com/melih/detail/di/modules/DetailFragmentModule.kt @@ -2,11 +2,15 @@ package com.melih.detail.di.modules import androidx.lifecycle.ViewModel import androidx.navigation.fragment.navArgs +import com.melih.abstractions.mapper.Mapper import com.melih.core.di.keys.ViewModelKey +import com.melih.definitions.entities.LaunchEntity import com.melih.detail.ui.DetailFragment import com.melih.detail.ui.DetailFragmentArgs import com.melih.detail.ui.DetailViewModel -import com.melih.repository.interactors.GetLaunchDetails +import com.melih.interactors.GetLaunchDetails +import com.melih.launches.data.LaunchDetailItem +import com.melih.launches.data.LaunchDetailMapper import dagger.Binds import dagger.Module import dagger.Provides @@ -21,6 +25,9 @@ abstract class DetailFragmentModule { @IntoMap @ViewModelKey(DetailViewModel::class) abstract fun detailViewModel(detailViewModel: DetailViewModel): ViewModel + + @Binds + abstract fun detailMapper(mapper: LaunchDetailMapper): Mapper //endregion @Module 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 f63ee29..8897dbc 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,34 +1,31 @@ package com.melih.detail.ui import androidx.lifecycle.Transformations.map +import com.melih.abstractions.deliverable.handle import com.melih.core.base.viewmodel.BaseViewModel -import com.melih.repository.entities.LaunchEntity -import com.melih.repository.interactors.GetLaunchDetails -import com.melih.repository.interactors.base.handle +import com.melih.interactors.GetLaunchDetails +import com.melih.launches.data.LaunchDetailItem +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import javax.inject.Inject class DetailViewModel @Inject constructor( - private val getLaunchDetails: GetLaunchDetails, + private val getLaunchDetails: GetLaunchDetails, private val getLaunchDetailsParams: GetLaunchDetails.Params -) : BaseViewModel() { +) : BaseViewModel() { //region Properties val rocketName = map(successData) { - it.rocket.name + it.rocketName } val description = map(successData) { - if (it.missions.isEmpty()) { - "" - } else { - it.missions[0].description - } + it.missionDescription } val imageUrl = map(successData) { - it.rocket.imageURL + it.imageUrl } //endregion diff --git a/features/detail/src/main/res/layout/fragment_detail.xml b/features/detail/src/main/res/layout/fragment_detail.xml index dde2a8f..e58cded 100644 --- a/features/detail/src/main/res/layout/fragment_detail.xml +++ b/features/detail/src/main/res/layout/fragment_detail.xml @@ -1,69 +1,69 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> - + - - + + - + - + - + - - + + 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 69ad7c9..d4cc572 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,8 @@ package com.melih.detail import com.melih.detail.ui.DetailViewModel -import com.melih.repository.interactors.GetLaunchDetails +import com.melih.interactors.GetLaunchDetails +import com.melih.launches.data.LaunchDetailItem import io.mockk.mockk import io.mockk.slot import io.mockk.spyk @@ -19,7 +20,7 @@ import org.junit.jupiter.api.Test @UseExperimental(ExperimentalCoroutinesApi::class) class DetailViewModelTest : BaseTestWithMainThread() { - private val getLaunchDetails: GetLaunchDetails = mockk(relaxed = true) + private val getLaunchDetails: GetLaunchDetails = mockk(relaxed = true) private val getLaunchDetailsParams = GetLaunchDetails.Params(1013) private val viewModel = spyk(DetailViewModel(getLaunchDetails, getLaunchDetailsParams)) diff --git a/features/launches/build.gradle b/features/launches/build.gradle index 78a3446..740d414 100644 --- a/features/launches/build.gradle +++ b/features/launches/build.gradle @@ -7,8 +7,6 @@ apply from: "$rootProject.projectDir/scripts/feature_module.gradle" dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation project(':repository') - implementation libraries.paging implementation libraries.swipeRefreshLayout diff --git a/features/launches/src/main/kotlin/com/melih/launches/data/LaunchItem.kt b/features/launches/src/main/kotlin/com/melih/launches/data/LaunchItem.kt new file mode 100644 index 0000000..8d50cb4 --- /dev/null +++ b/features/launches/src/main/kotlin/com/melih/launches/data/LaunchItem.kt @@ -0,0 +1,10 @@ +package com.melih.launches.data + +import com.melih.abstractions.data.ViewEntity + +data class LaunchItem( + val id: Long, + val imageUrl: String, + val rocketName: String, + val missionDescription: String +) : ViewEntity diff --git a/features/launches/src/main/kotlin/com/melih/launches/data/LaunchMapper.kt b/features/launches/src/main/kotlin/com/melih/launches/data/LaunchMapper.kt new file mode 100644 index 0000000..77aadd1 --- /dev/null +++ b/features/launches/src/main/kotlin/com/melih/launches/data/LaunchMapper.kt @@ -0,0 +1,18 @@ +package com.melih.launches.data + +import com.melih.abstractions.mapper.Mapper +import com.melih.definitions.entities.LaunchEntity +import javax.inject.Inject + +class LaunchMapper @Inject constructor() : Mapper() { + + override fun convert(launchEntity: LaunchEntity) = + with(launchEntity) { + LaunchItem( + id, + rocket.imageURL, + rocket.name, + if (!missions.isNullOrEmpty()) missions[0].description else "" + ) + } +} diff --git a/features/launches/src/main/kotlin/com/melih/launches/di/modules/LaunchesFragmentModule.kt b/features/launches/src/main/kotlin/com/melih/launches/di/modules/LaunchesFragmentModule.kt index 207eaf3..8841558 100644 --- a/features/launches/src/main/kotlin/com/melih/launches/di/modules/LaunchesFragmentModule.kt +++ b/features/launches/src/main/kotlin/com/melih/launches/di/modules/LaunchesFragmentModule.kt @@ -2,10 +2,14 @@ package com.melih.launches.di.modules import androidx.lifecycle.ViewModel import androidx.paging.Config +import com.melih.abstractions.mapper.Mapper import com.melih.core.di.keys.ViewModelKey +import com.melih.definitions.entities.LaunchEntity +import com.melih.interactors.DEFAULT_LAUNCHES_AMOUNT +import com.melih.interactors.GetLaunches +import com.melih.launches.data.LaunchItem +import com.melih.launches.data.LaunchMapper import com.melih.launches.ui.vm.LaunchesViewModel -import com.melih.repository.interactors.DEFAULT_LAUNCHES_AMOUNT -import com.melih.repository.interactors.GetLaunches import dagger.Binds import dagger.Module import dagger.Provides @@ -14,12 +18,15 @@ import dagger.multibindings.IntoMap @Module abstract class LaunchesFragmentModule { - //region ViewModels + //region Binds @Binds @IntoMap @ViewModelKey(LaunchesViewModel::class) - abstract fun listViewModel(listViewModel: LaunchesViewModel): ViewModel + abstract fun launchesViewModel(listViewModel: LaunchesViewModel): ViewModel + + @Binds + abstract fun launchMapper(mapper: LaunchMapper): Mapper //endregion @Module diff --git a/features/launches/src/main/kotlin/com/melih/launches/ui/LaunchesFragment.kt b/features/launches/src/main/kotlin/com/melih/launches/ui/LaunchesFragment.kt index e29d29c..e5e7a85 100644 --- a/features/launches/src/main/kotlin/com/melih/launches/ui/LaunchesFragment.kt +++ b/features/launches/src/main/kotlin/com/melih/launches/ui/LaunchesFragment.kt @@ -4,18 +4,18 @@ import android.os.Bundle import android.view.View import androidx.fragment.app.viewModels import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.melih.abstractions.deliverable.State import com.melih.core.actions.openDetail import com.melih.core.base.lifecycle.BaseDaggerFragment import com.melih.core.extensions.observe +import com.melih.interactors.error.PersistenceEmptyError import com.melih.launches.R -import com.melih.launches.databinding.ListBinding +import com.melih.launches.data.LaunchItem +import com.melih.launches.databinding.LaunchesBinding import com.melih.launches.ui.adapters.LaunchesAdapter import com.melih.launches.ui.vm.LaunchesViewModel -import com.melih.repository.entities.LaunchEntity -import com.melih.repository.interactors.base.PersistenceEmpty -import com.melih.repository.interactors.base.State -class LaunchesFragment : BaseDaggerFragment(), SwipeRefreshLayout.OnRefreshListener { +class LaunchesFragment : BaseDaggerFragment(), SwipeRefreshLayout.OnRefreshListener { //region Properties @@ -67,7 +67,7 @@ class LaunchesFragment : BaseDaggerFragment(), SwipeRefreshLayout.O // Observing error to show toast with retry action observe(viewModel.errorData) { - if (it !is PersistenceEmpty) { + if (it !is PersistenceEmptyError) { showSnackbarWithAction(it) { viewModel.retry() } @@ -79,7 +79,7 @@ class LaunchesFragment : BaseDaggerFragment(), SwipeRefreshLayout.O } } - private fun onItemSelected(item: LaunchEntity) { + private fun onItemSelected(item: LaunchItem) { openDetail(item.id) } diff --git a/features/launches/src/main/kotlin/com/melih/launches/ui/adapters/LaunchesAdapter.kt b/features/launches/src/main/kotlin/com/melih/launches/ui/adapters/LaunchesAdapter.kt index d9e88ec..7f8f423 100644 --- a/features/launches/src/main/kotlin/com/melih/launches/ui/adapters/LaunchesAdapter.kt +++ b/features/launches/src/main/kotlin/com/melih/launches/ui/adapters/LaunchesAdapter.kt @@ -5,10 +5,10 @@ import android.view.ViewGroup import com.melih.core.base.recycler.BasePagingListAdapter import com.melih.core.base.recycler.BaseViewHolder import com.melih.core.extensions.createDiffCallback +import com.melih.launches.data.LaunchItem import com.melih.launches.databinding.LaunchRowBinding -import com.melih.repository.entities.LaunchEntity -class LaunchesAdapter(itemClickListener: (LaunchEntity) -> Unit) : BasePagingListAdapter( +class LaunchesAdapter(itemClickListener: (LaunchItem) -> Unit) : BasePagingListAdapter( createDiffCallback { oldItem, newItem -> oldItem.id == newItem.id }, itemClickListener ) { @@ -19,21 +19,18 @@ class LaunchesAdapter(itemClickListener: (LaunchEntity) -> Unit) : BasePagingLis inflater: LayoutInflater, parent: ViewGroup, viewType: Int - ): BaseViewHolder = + ): BaseViewHolder = LaunchesViewHolder(LaunchRowBinding.inflate(inflater, parent, false)) //endregion } -class LaunchesViewHolder(private val binding: LaunchRowBinding) : BaseViewHolder(binding) { +class LaunchesViewHolder(private val binding: LaunchRowBinding) : + BaseViewHolder(binding) { //region Functions - override fun bind(item: LaunchEntity) { + override fun bind(item: LaunchItem) { binding.entity = item - - val missions = item.missions - binding.tvDescription.text = if (!missions.isNullOrEmpty()) missions[0].description else "" - binding.executePendingBindings() } //endregion diff --git a/features/launches/src/main/kotlin/com/melih/launches/ui/paging/LaunchesPagingSource.kt b/features/launches/src/main/kotlin/com/melih/launches/ui/paging/LaunchesPagingSource.kt index 6dc3056..7e1424b 100644 --- a/features/launches/src/main/kotlin/com/melih/launches/ui/paging/LaunchesPagingSource.kt +++ b/features/launches/src/main/kotlin/com/melih/launches/ui/paging/LaunchesPagingSource.kt @@ -1,25 +1,23 @@ package com.melih.launches.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 com.melih.interactors.GetLaunches +import com.melih.launches.data.LaunchItem import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow import javax.inject.Inject /** * Uses [GetLaunches] to get data for pagination */ class LaunchesPagingSource @Inject constructor( - private val getLaunches: GetLaunches, + private val getLaunches: GetLaunches, private val getLaunchesParams: GetLaunches.Params -) : BasePagingDataSource() { +) : BasePagingDataSource() { //region Functions @UseExperimental(ExperimentalCoroutinesApi::class) - override fun loadDataForPage(page: Int): Flow>> = + override fun loadDataForPage(page: Int) = getLaunches( getLaunchesParams.copy( page = page diff --git a/features/launches/src/main/kotlin/com/melih/launches/ui/paging/LaunchesPagingSourceFactory.kt b/features/launches/src/main/kotlin/com/melih/launches/ui/paging/LaunchesPagingSourceFactory.kt index 98b3485..7a6cfcf 100644 --- a/features/launches/src/main/kotlin/com/melih/launches/ui/paging/LaunchesPagingSourceFactory.kt +++ b/features/launches/src/main/kotlin/com/melih/launches/ui/paging/LaunchesPagingSourceFactory.kt @@ -2,16 +2,16 @@ package com.melih.launches.ui.paging import com.melih.core.base.paging.BasePagingDataSource import com.melih.core.base.paging.BasePagingFactory -import com.melih.repository.entities.LaunchEntity +import com.melih.launches.data.LaunchItem import javax.inject.Inject import javax.inject.Provider class LaunchesPagingSourceFactory @Inject constructor( private val sourceProvider: Provider -) : BasePagingFactory() { +) : BasePagingFactory() { //region Functions - override fun createSource(): BasePagingDataSource = sourceProvider.get() + override fun createSource(): BasePagingDataSource = sourceProvider.get() //endregion } diff --git a/features/launches/src/main/kotlin/com/melih/launches/ui/vm/LaunchesViewModel.kt b/features/launches/src/main/kotlin/com/melih/launches/ui/vm/LaunchesViewModel.kt index 39133a7..d026a54 100644 --- a/features/launches/src/main/kotlin/com/melih/launches/ui/vm/LaunchesViewModel.kt +++ b/features/launches/src/main/kotlin/com/melih/launches/ui/vm/LaunchesViewModel.kt @@ -3,18 +3,18 @@ package com.melih.launches.ui.vm import androidx.paging.PagedList import com.melih.core.base.paging.BasePagingFactory import com.melih.core.base.viewmodel.BasePagingViewModel +import com.melih.launches.data.LaunchItem import com.melih.launches.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() { +) : BasePagingViewModel() { //region Properties - override val factory: BasePagingFactory + override val factory: BasePagingFactory get() = launchesPagingSourceFactory override val config: PagedList.Config diff --git a/features/launches/src/main/res/layout/fragment_launches.xml b/features/launches/src/main/res/layout/fragment_launches.xml index f9f368d..967e940 100644 --- a/features/launches/src/main/res/layout/fragment_launches.xml +++ b/features/launches/src/main/res/layout/fragment_launches.xml @@ -4,7 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> - + + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> - + - - + + - + - + - + - + - - - + + + diff --git a/repository/.gitignore b/repository/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/repository/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/repository/src/main/AndroidManifest.xml b/repository/src/main/AndroidManifest.xml deleted file mode 100644 index efd4c29..0000000 --- a/repository/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/repository/src/main/kotlin/com/melih/repository/Repository.kt b/repository/src/main/kotlin/com/melih/repository/Repository.kt deleted file mode 100644 index af1e3d8..0000000 --- a/repository/src/main/kotlin/com/melih/repository/Repository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.melih.repository - -import com.melih.repository.entities.LaunchEntity -import com.melih.repository.interactors.base.Result - -/** - * Contract for sources to seperate low level business logic from build and return type - */ -abstract class Repository { - - //region Abstractions - - internal abstract suspend fun getNextLaunches(count: Int, page: Int): Result> - internal abstract suspend fun getLaunchById(id: Long): Result - //endregion -} diff --git a/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunchDetails.kt b/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunchDetails.kt deleted file mode 100644 index 5f9de0d..0000000 --- a/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunchDetails.kt +++ /dev/null @@ -1,55 +0,0 @@ -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.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 - -/** - * Gets next given number of launches - */ -@UseExperimental(ExperimentalCoroutinesApi::class) -class GetLaunchDetails @Inject constructor() : BaseInteractor() { - - //region Properties - - @field:Inject - internal lateinit var networkSource: NetworkSource - - @field:Inject - internal lateinit var persistenceSource: PersistenceSource - //endregion - - //region Functions - - 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) - } - } - //endregion - - data class Params( - val id: Long - ) : InteractorParameters -} diff --git a/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunches.kt b/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunches.kt deleted file mode 100644 index 775d8c9..0000000 --- a/repository/src/main/kotlin/com/melih/repository/interactors/GetLaunches.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.melih.repository.interactors - -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.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 - */ -@UseExperimental(ExperimentalCoroutinesApi::class) -class GetLaunches @Inject constructor() : BaseInteractor, GetLaunches.Params>() { - - //region Properties - - @field:Inject - internal lateinit var networkSource: NetworkSource - - @field:Inject - internal lateinit var persistenceSource: PersistenceSource - //endregion - - //region Functions - - 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)) - } - } - } - //endregion - - data class Params( - val count: Int = DEFAULT_LAUNCHES_AMOUNT, - val page: Int - ) : InteractorParameters -} 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 deleted file mode 100644 index fa14e9a..0000000 --- a/repository/src/main/kotlin/com/melih/repository/interactors/base/Reason.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.melih.repository.interactors.base - -import androidx.annotation.StringRes -import com.melih.repository.R - - -/** - * [Failure] reasons - */ -sealed class Reason(@StringRes val messageRes: Int) - -//region Subclasses - -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) -//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 deleted file mode 100644 index 785e0a2..0000000 --- a/repository/src/main/kotlin/com/melih/repository/sources/NetworkSource.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.melih.repository.sources - -import android.net.NetworkInfo -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 -import javax.inject.Inject -import javax.inject.Provider - -private const val DEFAULT_IMAGE_SIZE = 480 - -/** - * NetworkSource for fetching results using api and wrapping them as contracted in [repository][Repository], - * returning either [failure][Failure] with proper [reason][Reason] or [success][Success] with data - */ -internal class NetworkSource @Inject constructor( - private val apiImpl: ApiImpl, - private val networkInfoProvider: Provider -) : Repository() { - - //region Properties - - private val isNetworkConnected: Boolean - get() { - val networkInfo = networkInfoProvider.get() - return networkInfo != null && networkInfo.isConnected - } - //endregion - - //region Functions - - 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( - rocket = launch.rocket.copy( - imageURL = transformImageUrl( - launch.rocket.imageURL, - launch.rocket.imageSizes - ) - ) - ) - } else { - launch - } - } - } - - override suspend fun getLaunchById(id: Long): Result = - safeExecute({ - apiImpl.getLaunchById(id) - }) { - if (!it.rocket.imageURL.isNotBlank()) { - it.copy( - rocket = it.rocket.copy( - imageURL = transformImageUrl(it.rocket.imageURL, it.rocket.imageSizes) - ) - ) - } else { - it - } - } - - private inline fun safeExecute( - block: () -> Response, - transform: (T) -> R - ) = - if (isNetworkConnected) { - try { - block().extractResponseBody(transform) - } catch (e: IOException) { - Failure(TimeoutError()) - } - } else { - Failure(NetworkError()) - } - - private inline fun Response.extractResponseBody(transform: (T) -> R) = - if (isSuccessful) { - body()?.let { - Success(transform(it)) - } ?: Failure(EmptyResultError()) - } else { - Failure(ResponseError()) - } - - private fun transformImageUrl(imageUrl: String, supportedSizes: IntArray) = - try { - val urlSplit = imageUrl.split("_") - val url = urlSplit[0] - val format = urlSplit[1].split(".")[1] - - val requestedSize = if (!supportedSizes.contains(DEFAULT_IMAGE_SIZE)) { - supportedSizes.last { it < DEFAULT_IMAGE_SIZE } - } else { - DEFAULT_IMAGE_SIZE - } - - "${url}_$requestedSize.$format" - } catch (e: Exception) { - imageUrl - } - //endregion -} diff --git a/repository/src/main/kotlin/com/melih/repository/sources/PersistenceSource.kt b/repository/src/main/kotlin/com/melih/repository/sources/PersistenceSource.kt deleted file mode 100644 index c49881d..0000000 --- a/repository/src/main/kotlin/com/melih/repository/sources/PersistenceSource.kt +++ /dev/null @@ -1,50 +0,0 @@ -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.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 - */ -internal class PersistenceSource @Inject constructor( - ctx: Context -) : Repository() { - - //region Functions - - private val launchesDatabase = LaunchesDatabase.getInstance(ctx) - - override suspend fun getNextLaunches(count: Int, page: Int): Result> = - launchesDatabase - .launchesDao - .getLaunches(count, page) - .takeIf { it.isNotEmpty() } - ?.run { - Success(this) - } ?: Failure(PersistenceEmpty()) - - override suspend fun getLaunchById(id: Long): Result = - launchesDatabase - .launchesDao - .getLaunchById(id) - .takeIf { it != null } - ?.run { - Success(this) - } ?: Failure(PersistenceEmpty()) - - internal suspend fun saveLaunches(launches: List) { - launchesDatabase.launchesDao.saveLaunches(launches) - } - - internal suspend fun saveLaunch(launch: LaunchEntity) { - launchesDatabase.launchesDao.saveLaunch(launch) - } - //endregion -} diff --git a/repository/src/test/kotlin/com/melih/repository/sources/NetworkSourceTest.kt b/repository/src/test/kotlin/com/melih/repository/sources/NetworkSourceTest.kt deleted file mode 100644 index 343aac6..0000000 --- a/repository/src/test/kotlin/com/melih/repository/sources/NetworkSourceTest.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.melih.repository.sources - -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.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 -import io.mockk.mockk -import io.mockk.spyk -import kotlinx.coroutines.ExperimentalCoroutinesApi -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 -import javax.inject.Provider - -@UseExperimental(ExperimentalCoroutinesApi::class) -class NetworkSourceTest { - - private val apiImpl = mockk(relaxed = true) - private val networkInfoProvider = mockk>(relaxed = true) { - every { get() } returns mockk(relaxed = true) - } - - private val source = spyk(NetworkSource(apiImpl, networkInfoProvider)) - - @Nested - inner class GetNextLaunches { - - @Test - fun `should return network error when internet is not connected`() { - every { networkInfoProvider.get().isConnected } returns false - - runBlockingTest { - val result = source.getNextLaunches(1, 0) - - result shouldBeInstanceOf Failure::class - result.onFailure { - it shouldBeInstanceOf NetworkError::class - } - } - } - - @Test - fun `should return response error when it is not successful`() { - every { networkInfoProvider.get().isConnected } returns true - coEvery { apiImpl.getNextLaunches(any(), any()).isSuccessful } returns false - - runBlockingTest { - val result = source.getNextLaunches(1, 0) - - result shouldBeInstanceOf Failure::class - result.onFailure { - it shouldBeInstanceOf ResponseError::class - (it as ResponseError).messageRes shouldEqualTo R.string.reason_response - } - } - } - - @Test - fun `should return empty result error when body is null`() { - every { networkInfoProvider.get().isConnected } returns true - coEvery { apiImpl.getNextLaunches(any(), any()).isSuccessful } returns true - coEvery { apiImpl.getNextLaunches(any(), any()).body() } returns null - - runBlockingTest { - val result = source.getNextLaunches(1, 0) - - result shouldBeInstanceOf Failure::class - result.onFailure { - it shouldBeInstanceOf EmptyResultError::class - } - } - } - - @Test - fun `should return success with data if execution is successful`() { - every { networkInfoProvider.get().isConnected } returns true - 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, 0) - - 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 deleted file mode 100644 index e40fa14..0000000 --- a/repository/src/test/kotlin/com/melih/repository/sources/PersistanceSourceTest.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.melih.repository.sources - -import android.content.Context -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.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.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(ctx)) - - private val scope = CoroutineScope(Dispatchers.IO) - - @BeforeEach - fun setup() { - mockkObject(LaunchesDatabase) - every { LaunchesDatabase.getInstance(ctx) } returns dbImplementation - } - - @Nested - inner class GetNextLaunches { - - @Test - fun `should return persistance empty error when db is empty`() { - coEvery { dbImplementation.launchesDao.getLaunches(any(), any()) } returns emptyList() - - scope.launch { - val result = source.getNextLaunches(10, 0) - - result shouldBeInstanceOf Failure::class - result.onFailure { - it shouldBeInstanceOf PersistenceEmpty::class - } - } - } - - @Test - fun `should return success with data if db is not empty`() { - coEvery { dbImplementation.launchesDao.getLaunches(any(), any()) } returns listOf(LaunchEntity(id = 1013)) - - 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/scripts/detekt.gradle b/scripts/cq/detekt.gradle similarity index 100% rename from scripts/detekt.gradle rename to scripts/cq/detekt.gradle diff --git a/scripts/dokka.gradle b/scripts/cq/dokka.gradle similarity index 100% rename from scripts/dokka.gradle rename to scripts/cq/dokka.gradle diff --git a/scripts/cq/jacoco.gradle b/scripts/cq/jacoco.gradle new file mode 100644 index 0000000..0493b26 --- /dev/null +++ b/scripts/cq/jacoco.gradle @@ -0,0 +1,53 @@ +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") + ) +} \ No newline at end of file diff --git a/scripts/default_android_config.gradle b/scripts/default_android_config.gradle index 4546de2..c81a51b 100644 --- a/scripts/default_android_config.gradle +++ b/scripts/default_android_config.gradle @@ -1,4 +1,6 @@ -apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle" +apply from: "$rootProject.projectDir/scripts/cq/detekt.gradle" +apply from: "$rootProject.projectDir/scripts/cq/dokka.gradle" +apply from: "$rootProject.projectDir/scripts/cq/jacoco.gradle" android { compileSdkVersion versions.compileSdkVersion diff --git a/scripts/default_dependencies.gradle b/scripts/default_dependencies.gradle index b9ee1c9..9e6003a 100644 --- a/scripts/default_dependencies.gradle +++ b/scripts/default_dependencies.gradle @@ -1,11 +1,10 @@ apply plugin: "de.mannodermaus.android-junit5" -apply from: "$rootProject.projectDir/scripts/detekt.gradle" -apply from: "$rootProject.projectDir/scripts/dokka.gradle" - dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(':abstractions') + implementation libraries.kotlin implementation libraries.dagger implementation libraries.timber @@ -18,57 +17,3 @@ 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 21ff897..49b7aed 100644 --- a/scripts/dependencies.gradle +++ b/scripts/dependencies.gradle @@ -5,28 +5,28 @@ ext { compileSdkVersion : 29, targetSdkVersion : 29, buildToolsVersion : "29.0.2", - appCompatVersion : "1.1.0-rc01", - lifecycleVersion : "2.2.0-alpha03", - fragmentVersion : "1.2.0-alpha02", - workManagerVersion : "2.3.0-alpha01", - constraintLayoutVesion : "2.0.0-beta2", + appCompatVersion : "1.1.0", + lifecycleVersion : "2.2.0-alpha05", + fragmentVersion : "1.2.0-rc01", + workManagerVersion : "2.3.0-alpha03", + constraintLayoutVesion : "2.0.0-beta3", cardViewVersion : "1.0.0", - recyclerViewVersion : "1.1.0-beta03", + recyclerViewVersion : "1.1.0-rc01", pagingVersion : "2.1.0", - viewPagerVersion : "1.0.0-beta03", + viewPagerVersion : "1.0.0-rc01", materialVersion : "1.1.0-alpha09", - swipeRefreshLayoutVersion: "1.1.0-alpha02", + swipeRefreshLayoutVersion: "1.1.0-alpha03", collectionVersion : "1.1.0", - roomVersion : "2.2.0-rc01", - daggerVersion : "2.24", - okHttpVersion : "4.0.1", - retrofitVersion : "2.6.1", + roomVersion : "2.2.0", + daggerVersion : "2.25.2", + okHttpVersion : "4.2.1", + retrofitVersion : "2.6.2", picassoVersion : "2.71828", moshiVersion : "1.8.0", - coroutinesVersion : "1.3.0-RC2", - leakCanaryVersion : "2.0-beta-2", + coroutinesVersion : "1.3.2", + leakCanaryVersion : "2.0-beta-3", timberVersion : "4.7.1", - jUnitVersion : "5.5.1", + jUnitVersion : "5.5.2", espressoVersion : "3.2.0", mockkVersion : "1.9.3", kluentVersion : "1.53", diff --git a/scripts/feature_module.gradle b/scripts/feature_module.gradle index 981efc5..89eae3a 100644 --- a/scripts/feature_module.gradle +++ b/scripts/feature_module.gradle @@ -1,5 +1,7 @@ apply plugin: "androidx.navigation.safeargs" + apply from: "$rootProject.projectDir/scripts/module.gradle" +apply from: "$rootProject.projectDir/scripts/default_dependencies.gradle" android { dataBinding { @@ -9,6 +11,8 @@ android { dependencies { implementation project(':core') + implementation project(':data:interactors') + implementation project(':data:definitions') implementation libraries.fragment implementation libraries.lifecycle diff --git a/settings.gradle b/settings.gradle index 7a766a4..ad1f600 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,11 @@ -include ':app', ':repository', ':core', ':features:launches', ':features:detail' -rootProject.name = 'Rocket Science' +rootProject.name = 'RocketScience' + +include ':abstractions', + ':core', + 'app', + ':features:launches', + ':features:detail', + ':data:interactors', + ':data:network', + ':data:persistence', + ':data:definitions'