diff --git a/.editorconfig b/.editorconfig
index 4bda7f7f..098208df 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,14 +1,19 @@
-root = true
-
+root=true
[*]
-indent_size = 2
-end_of_line = lf
-charset = utf-8
-trim_trailing_whitespace = true
-insert_final_newline = true
-
+indent_size=2
+end_of_line=lf
+charset=utf-8
+trim_trailing_whitespace=true
+insert_final_newline=true
[*.{kt,kts}]
-ij_kotlin_imports_layout = *
-
+ij_kotlin_imports_layout=*
+ij_continuation_indent_size=4
+ktlint_standard_filename=disabled
+ktlint_standard_package-name=disabled
+ktlint_standard_property-naming=disabled
+ktlint_standard_function-naming=disabled
+ktlint_standard_no-empty-file=disabled
+filename=disabled
+ktlint_experimental=enabled
[*.xml]
-indent_size = 4
+indent_size=4
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 40cb3f3e..ac6fe9ec 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -22,7 +22,7 @@ jobs:
uses: actions/setup-java@v3
with:
distribution: 'zulu'
- java-version: '11'
+ java-version: '17'
- name: Cache gradle, wrapper and buildSrc
uses: actions/cache@v3
diff --git a/.github/workflows/gradle-versions-checker.yml b/.github/workflows/gradle-versions-checker.yml
index 1131a546..3f14ca2d 100644
--- a/.github/workflows/gradle-versions-checker.yml
+++ b/.github/workflows/gradle-versions-checker.yml
@@ -20,7 +20,7 @@ jobs:
uses: actions/setup-java@v3
with:
distribution: 'zulu'
- java-version: '11'
+ java-version: '17'
- name: Cache gradle, wrapper and buildSrc
uses: actions/cache@v3
diff --git a/.github/workflows/qodana.yml b/.github/workflows/qodana.yml
index 8f4f22a8..a20aa35c 100644
--- a/.github/workflows/qodana.yml
+++ b/.github/workflows/qodana.yml
@@ -15,14 +15,14 @@ jobs:
steps:
- uses: actions/checkout@v3
- - uses: JetBrains/qodana-action@v2022.3.4
- with:
- linter: jetbrains/qodana-jvm-android:latest
- fail-threshold: 10
-
- - name: Deploy to GitHub Pages
- uses: peaceiris/actions-gh-pages@v3
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- publish_dir: ${{ runner.temp }}/qodana/results/report
- destination_dir: ./
+ # - name: Set up JDK
+ # uses: actions/setup-java@v3
+ # with:
+ # distribution: 'zulu'
+ # java-version: '11'
+
+ # - uses: JetBrains/qodana-action@v2023.2.6
+ # with:
+ # linter: jetbrains/qodana-jvm-android:latest
+ # fail-threshold: 10
+ # upload-result: true
diff --git a/.github/workflows/review-suggest.yml b/.github/workflows/review-suggest.yml
index 2ff9fc2c..32f92e64 100644
--- a/.github/workflows/review-suggest.yml
+++ b/.github/workflows/review-suggest.yml
@@ -18,7 +18,7 @@ jobs:
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
- java-version: '11'
+ java-version: '17'
- name: Cache gradle, wrapper and buildSrc
uses: actions/cache@v3
with:
diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml
index f5c0d600..b240674c 100644
--- a/.github/workflows/unit-test.yml
+++ b/.github/workflows/unit-test.yml
@@ -22,7 +22,7 @@ jobs:
uses: actions/setup-java@v3
with:
distribution: 'zulu'
- java-version: '11'
+ java-version: '17'
- name: Cache gradle, wrapper and buildSrc
uses: actions/cache@v3
@@ -38,10 +38,7 @@ jobs:
run: chmod +x ./gradlew
- name: Run Android Debug Unit Test
- run: ./gradlew jacocoTestReportDebug --warning-mode all --stacktrace
-
- - name: Run Java/Kotlin Unit Test
- run: ./gradlew jacocoTestReport --warning-mode all --stacktrace
+ run: ./gradlew :app:koverHtmlReport --warning-mode all --stacktrace
- name: Upload Test Report
uses: codecov/codecov-action@v3.1.4
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 17eb6a80..1cae6040 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -7,8 +7,6 @@
-
-
@@ -134,4 +132,4 @@
-
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 44afafaf..44e86047 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -2,17 +2,17 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index cb99164d..9561532f 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -7,7 +7,7 @@
-
+
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 57f05c9d..d9572549 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -4,4 +4,7 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 4673587a..0f4ea7c0 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,3 @@
-
@@ -22,7 +21,7 @@
-
+
diff --git a/README.md b/README.md
index c7b904b3..bc7ed35d 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
[](https://github.com/Kotlin-Android-Open-Source/MVI-Coroutines-Flow/actions/workflows/qodana.yml)
[](https://github.com/Kotlin-Android-Open-Source/MVI-Coroutines-Flow/actions/workflows/gradle-wrapper-validation.yml)
[](https://android-arsenal.com/api?level=21)
-[](http://kotlinlang.org)
+[](http://kotlinlang.org)
[](https://hits.seeyoufarm.com)
[](https://opensource.org/licenses/MIT)
[](https://gitter.im/Kotlin-Android-Open-Source/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index bede0b8c..cb704ca4 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,7 +1,7 @@
plugins {
androidApplication
kotlinAndroid
- jacoco
+ id("org.jetbrains.kotlinx.kover")
}
android {
@@ -25,7 +25,7 @@ android {
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
+ "proguard-rules.pro",
)
}
@@ -39,7 +39,11 @@ android {
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() }
- buildFeatures { viewBinding = true }
+
+ buildFeatures {
+ viewBinding = true
+ buildConfig = true
+ }
testOptions {
unitTests.isIncludeAndroidResources = true
@@ -52,9 +56,9 @@ dependencies {
fileTree(
mapOf(
"dir" to "libs",
- "include" to listOf("*.jar")
- )
- )
+ "include" to listOf("*.jar"),
+ ),
+ ),
)
implementation(domain)
@@ -77,10 +81,45 @@ dependencies {
testImplementation(deps.test.junit)
androidTestImplementation(deps.test.androidx.junit)
androidTestImplementation(deps.test.androidx.core)
- androidTestImplementation(deps.test.androidx.espresso.core)
+ androidTestImplementation(
+ deps
+ .test
+ .androidx
+ .espresso
+ .core,
+ )
addUnitTest()
testImplementation(testUtils)
testImplementation(deps.koin.testJunit4)
testImplementation(deps.koin.test)
}
+
+dependencies {
+ kover(project(":feature-main"))
+ kover(project(":feature-add"))
+ kover(project(":feature-search"))
+ kover(project(":domain"))
+ kover(project(":data"))
+ kover(project(":core"))
+ kover(project(":core-ui"))
+ kover(project(":mvi-base"))
+}
+
+koverReport {
+ // filters for all report types of all build variants
+ filters {
+ excludes {
+ classes(
+ "*.databinding.*",
+ "*.BuildConfig",
+ )
+ }
+ }
+
+ defaults {
+ // Tests, sources, classes, and compilation tasks of the 'debug' build variant will be included in the default report.
+ // Thus, information from the 'app1AppDebug' variant will be included in the default report for this project and any project that specifies this project as a dependency.
+ mergeWith("debug") // or the name of any build variant needed
+ }
+}
diff --git a/app/src/main/java/com/hoc/flowmvi/App.kt b/app/src/main/java/com/hoc/flowmvi/App.kt
index 6f8ba856..e5cee287 100644
--- a/app/src/main/java/com/hoc/flowmvi/App.kt
+++ b/app/src/main/java/com/hoc/flowmvi/App.kt
@@ -21,14 +21,15 @@ import org.koin.core.logger.Level
@ExperimentalStdlibApi
@ExperimentalTime
@JvmField
-val allModules = listOf(
- coreModule,
- dataModule,
- domainModule,
- mainModule,
- addModule,
- searchModule,
-)
+val allModules =
+ listOf(
+ coreModule,
+ dataModule,
+ domainModule,
+ mainModule,
+ addModule,
+ searchModule,
+ )
@Suppress("unused")
@ExperimentalStdlibApi
diff --git a/app/src/main/java/com/hoc/flowmvi/core/CoreModule.kt b/app/src/main/java/com/hoc/flowmvi/core/CoreModule.kt
index fa28128c..ed07e938 100644
--- a/app/src/main/java/com/hoc/flowmvi/core/CoreModule.kt
+++ b/app/src/main/java/com/hoc/flowmvi/core/CoreModule.kt
@@ -7,8 +7,9 @@ import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
@JvmField
-val coreModule = module {
- singleOf(::DefaultAppCoroutineDispatchers) { bind() }
+val coreModule =
+ module {
+ singleOf(::DefaultAppCoroutineDispatchers) { bind() }
- singleOf(::NavigatorImpl) { bind() }
-}
+ singleOf(::NavigatorImpl) { bind() }
+ }
diff --git a/app/src/main/java/com/hoc/flowmvi/core/NavigatorImpl.kt b/app/src/main/java/com/hoc/flowmvi/core/NavigatorImpl.kt
index 0bfd900e..9a31ebde 100644
--- a/app/src/main/java/com/hoc/flowmvi/core/NavigatorImpl.kt
+++ b/app/src/main/java/com/hoc/flowmvi/core/NavigatorImpl.kt
@@ -8,8 +8,7 @@ class NavigatorImpl(
private val add: IntentProviders.Add,
private val search: IntentProviders.Search,
) : Navigator {
- override fun Context.navigateToAdd() =
- startActivity(add.makeIntent(this))
+ override fun Context.navigateToAdd() = startActivity(add.makeIntent(this))
override fun Context.navigateToSearch() {
startActivity(search.makeIntent(this))
diff --git a/app/src/main/java/com/hoc/flowmvi/initializer/ViewBindingInitializer.kt b/app/src/main/java/com/hoc/flowmvi/initializer/ViewBindingInitializer.kt
index ea58b77e..12d58c73 100644
--- a/app/src/main/java/com/hoc/flowmvi/initializer/ViewBindingInitializer.kt
+++ b/app/src/main/java/com/hoc/flowmvi/initializer/ViewBindingInitializer.kt
@@ -18,6 +18,5 @@ class ViewBindingInitializer : Initializer {
Timber.d("ViewBindingInitializer...")
}
- override fun dependencies(): List>> =
- listOf(TimberInitializer::class.java)
+ override fun dependencies(): List>> = listOf(TimberInitializer::class.java)
}
diff --git a/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt b/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt
index 657b47b4..bc9dab30 100644
--- a/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt
+++ b/app/src/test/java/com/hoc/flowmvi/CheckModulesTest.kt
@@ -23,17 +23,18 @@ import org.koin.test.mock.MockProviderRule
@ExperimentalTime
class CheckModulesTest : AutoCloseKoinTest() {
@get:Rule
- val mockProvider = MockProviderRule.create { clazz ->
- when (clazz) {
- SavedStateHandle::class -> {
- mockk {
- every { get(any()) } returns null
- every { setSavedStateProvider(any(), any()) } just runs
+ val mockProvider =
+ MockProviderRule.create { clazz ->
+ when (clazz) {
+ SavedStateHandle::class -> {
+ mockk {
+ every { get(any()) } returns null
+ every { setSavedStateProvider(any(), any()) } just runs
+ }
}
+ else -> error("Unknown class: $clazz")
}
- else -> error("Unknown class: $clazz")
}
- }
@get:Rule
val coroutineRule = TestCoroutineDispatcherRule()
diff --git a/build.gradle.kts b/build.gradle.kts
index cb6a3532..75769eda 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -6,6 +6,11 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id("org.jetbrains.kotlinx.kover") version "0.7.3" apply false
+ id("com.diffplug.spotless") version "6.22.0" apply false
+}
+
buildscript {
repositories {
google()
@@ -14,24 +19,21 @@ buildscript {
maven(url = "https://oss.sonatype.org/content/repositories/snapshots")
}
dependencies {
- classpath("com.android.tools.build:gradle:7.4.2")
+ classpath("com.android.tools.build:gradle:8.1.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
- classpath("com.diffplug.spotless:spotless-plugin-gradle:6.18.0")
- classpath("dev.drewhamilton.poko:poko-gradle-plugin:0.13.0")
- classpath("org.jacoco:org.jacoco.core:0.8.10")
- classpath("com.vanniktech:gradle-android-junit-jacoco-plugin:0.17.0-SNAPSHOT")
+ classpath("com.diffplug.spotless:spotless-plugin-gradle:6.20.0")
+ classpath("dev.drewhamilton.poko:poko-gradle-plugin:0.15.0")
classpath("com.github.ben-manes:gradle-versions-plugin:0.46.0")
}
}
subprojects {
- apply(plugin = "com.diffplug.spotless")
- apply(plugin = "com.vanniktech.android.junit.jacoco")
-
apply(plugin = "com.github.ben-manes.versions")
fun isNonStable(version: String): Boolean {
- val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.toUpperCase().contains(it) }
+ val stableKeyword =
+ listOf("RELEASE", "FINAL", "GA")
+ .any { version.uppercase().contains(it) }
val regex = "^[0-9,.v-]+(-r)?$".toRegex()
val isStable = stableKeyword || regex.matches(version)
return !isStable
@@ -49,36 +51,45 @@ subprojects {
}
}
- configure {
- val EDITOR_CONFIG_KEYS: Set = hashSetOf(
- "ij_kotlin_imports_layout",
- "indent_size",
- "end_of_line",
- "charset",
- "disabled_rules"
- )
+ afterEvaluate {
+ tasks.withType {
+ maxParallelForks =
+ (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1).also {
+ println("Setting maxParallelForks to $it")
+ }
+ testLogging {
+ showExceptions = true
+ showCauses = true
+ showStackTraces = true
+ showStandardStreams = true
+ events =
+ EnumSet.of(
+ TestLogEvent.PASSED,
+ TestLogEvent.FAILED,
+ TestLogEvent.SKIPPED,
+ TestLogEvent.STANDARD_OUT,
+ TestLogEvent.STANDARD_ERROR,
+ )
+ exceptionFormat = TestExceptionFormat.FULL
+ }
+ }
+ }
+}
+allprojects {
+ tasks.withType {
+ kotlinOptions {
+ val version = JavaVersion.VERSION_11.toString()
+ jvmTarget = version
+ }
+ }
+
+ apply()
+ configure {
kotlin {
target("**/*.kt")
- // TODO this should all come from editorconfig https://github.com/diffplug/spotless/issues/142
- val data = mapOf(
- "indent_size" to "2",
- "ij_kotlin_imports_layout" to "*",
- "end_of_line" to "lf",
- "charset" to "utf-8",
- "disabled_rules" to arrayOf(
- "package-name",
- "trailing-comma",
- "filename",
- "experimental:type-parameter-list-spacing",
- ).joinToString(separator = ","),
- )
-
ktlint(ktlintVersion)
- .setUseExperimental(true)
- .userData(data.filterKeys { it !in EDITOR_CONFIG_KEYS })
- .editorConfigOverride(data.filterKeys { it in EDITOR_CONFIG_KEYS })
trimTrailingWhitespace()
indentWithSpaces()
@@ -96,79 +107,11 @@ subprojects {
kotlinGradle {
target("**/*.gradle.kts", "*.gradle.kts")
- val data = mapOf(
- "indent_size" to "2",
- "ij_kotlin_imports_layout" to "*",
- "end_of_line" to "lf",
- "charset" to "utf-8"
- )
ktlint(ktlintVersion)
- .setUseExperimental(true)
- .userData(data.filterKeys { it !in EDITOR_CONFIG_KEYS })
- .editorConfigOverride(data.filterKeys { it in EDITOR_CONFIG_KEYS })
trimTrailingWhitespace()
indentWithSpaces()
endWithNewline()
}
}
-
- configure {
- jacocoVersion = "0.8.8"
- includeNoLocationClasses = true
- includeInstrumentationCoverageInMergedReport = true
- csv.isEnabled = false
- xml.isEnabled = true
- html.isEnabled = true
- }
-
- afterEvaluate {
- tasks.withType {
- extensions
- .getByType()
- .run {
- isIncludeNoLocationClasses = true
- excludes = listOf("jdk.internal.*")
- }
-
- maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1).also {
- println("Setting maxParallelForks to $it")
- }
- testLogging {
- showExceptions = true
- showCauses = true
- showStackTraces = true
- showStandardStreams = true
- events = EnumSet.of(
- TestLogEvent.PASSED,
- TestLogEvent.FAILED,
- TestLogEvent.SKIPPED,
- TestLogEvent.STANDARD_OUT,
- TestLogEvent.STANDARD_ERROR
- )
- exceptionFormat = TestExceptionFormat.FULL
- }
- }
- }
-}
-
-allprojects {
- tasks.withType {
- kotlinOptions {
- val version = JavaVersion.VERSION_11.toString()
- jvmTarget = version
- languageVersion = "1.8"
- }
- }
-
- repositories {
- google()
- mavenCentral()
- maven(url = "https://jitpack.io")
- maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/")
- }
-}
-
-tasks.register("clean", Delete::class) {
- delete(rootProject.buildDir)
}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 26dc658c..4b60cdd3 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -1,8 +1,8 @@
repositories {
- mavenCentral()
+ mavenCentral()
}
plugins {
- `kotlin-dsl`
- `kotlin-dsl-precompiled-script-plugins`
-}
\ No newline at end of file
+ `kotlin-dsl`
+ `kotlin-dsl-precompiled-script-plugins`
+}
diff --git a/buildSrc/gradle/wrapper/gradle-wrapper.properties b/buildSrc/gradle/wrapper/gradle-wrapper.properties
index e8be595e..c30b486a 100644
--- a/buildSrc/gradle/wrapper/gradle-wrapper.properties
+++ b/buildSrc/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/buildSrc/src/main/kotlin/deps.kt b/buildSrc/src/main/kotlin/deps.kt
index 1e7c9a4b..80b55e34 100644
--- a/buildSrc/src/main/kotlin/deps.kt
+++ b/buildSrc/src/main/kotlin/deps.kt
@@ -6,17 +6,16 @@ import org.gradle.kotlin.dsl.project
import org.gradle.plugin.use.PluginDependenciesSpec
import org.gradle.plugin.use.PluginDependencySpec
-const val ktlintVersion = "0.46.1"
-const val kotlinVersion = "1.8.21"
+const val ktlintVersion = "1.0.0"
+const val kotlinVersion = "1.9.10"
object appConfig {
const val applicationId = "com.hoc.flowmvi"
- const val compileSdkVersion = 33
- const val buildToolsVersion = "33.0.1"
-
+ const val compileSdkVersion = 34
+ const val buildToolsVersion = "34.0.0"
const val minSdkVersion = 21
- const val targetSdkVersion = 33
+ const val targetSdkVersion = 34
private const val MAJOR = 2
private const val MINOR = 1
@@ -28,16 +27,16 @@ object appConfig {
object deps {
object androidx {
const val appCompat = "androidx.appcompat:appcompat:1.6.1"
- const val coreKtx = "androidx.core:core-ktx:1.10.1"
+ const val coreKtx = "androidx.core:core-ktx:1.12.0"
const val constraintLayout = "androidx.constraintlayout:constraintlayout:2.1.4"
- const val recyclerView = "androidx.recyclerview:recyclerview:1.3.0"
+ const val recyclerView = "androidx.recyclerview:recyclerview:1.3.1"
const val swipeRefreshLayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01"
const val material = "com.google.android.material:material:1.9.0"
const val startup = "androidx.startup:startup-runtime:1.1.1"
}
object lifecycle {
- private const val version = "2.6.1"
+ private const val version = "2.6.2"
const val viewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:$version" // viewModelScope
const val runtimeKtx = "androidx.lifecycle:lifecycle-runtime-ktx:$version" // lifecycleScope
@@ -49,11 +48,11 @@ object deps {
const val converterMoshi = "com.squareup.retrofit2:converter-moshi:2.9.0"
const val loggingInterceptor = "com.squareup.okhttp3:logging-interceptor:4.11.0"
const val moshiKotlin = "com.squareup.moshi:moshi-kotlin:1.15.0"
- const val leakCanary = "com.squareup.leakcanary:leakcanary-android:2.11"
+ const val leakCanary = "com.squareup.leakcanary:leakcanary-android:2.12"
}
object coroutines {
- private const val version = "1.7.1"
+ private const val version = "1.7.3"
const val core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"
const val android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version"
@@ -61,7 +60,7 @@ object deps {
}
object koin {
- private const val version = "3.4.0"
+ private const val version = "3.5.0"
const val core = "io.insert-koin:koin-core:$version"
const val android = "io.insert-koin:koin-android:$version"
@@ -69,13 +68,13 @@ object deps {
const val test = "io.insert-koin:koin-test:$version"
}
- const val coil = "io.coil-kt:coil:2.3.0"
+ const val coil = "io.coil-kt:coil:2.4.0"
const val viewBindingDelegate = "com.github.hoc081098:ViewBindingDelegate:1.4.0"
- const val flowExt = "io.github.hoc081098:FlowExt:0.6.1"
+ const val flowExt = "io.github.hoc081098:FlowExt:0.7.1"
const val timber = "com.jakewharton.timber:timber:5.0.1"
object arrow {
- private const val version = "1.2.0-RC"
+ private const val version = "1.2.1"
const val core = "io.arrow-kt:arrow-core:$version"
}
@@ -87,11 +86,11 @@ object deps {
const val junit = "androidx.test.ext:junit-ktx:1.1.5"
object espresso {
- const val core = "androidx.test.espresso:espresso-core:3.4.0"
+ const val core = "androidx.test.espresso:espresso-core:3.5.1"
}
}
- const val mockk = "io.mockk:mockk:1.13.5"
+ const val mockk = "io.mockk:mockk:1.13.8"
const val kotlinJUnit = "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion"
}
}
diff --git a/core-ui/build.gradle.kts b/core-ui/build.gradle.kts
index 86deb0ec..8329d652 100644
--- a/core-ui/build.gradle.kts
+++ b/core-ui/build.gradle.kts
@@ -1,6 +1,7 @@
plugins {
androidLib
kotlinAndroid
+ id("org.jetbrains.kotlinx.kover")
}
android {
@@ -10,7 +11,6 @@ android {
defaultConfig {
minSdk = appConfig.minSdkVersion
- targetSdk = appConfig.targetSdkVersion
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
@@ -21,7 +21,7 @@ android {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
+ "proguard-rules.pro",
)
}
}
@@ -36,6 +36,10 @@ android {
unitTests.isIncludeAndroidResources = true
unitTests.isReturnDefaultValues = true
}
+
+ buildFeatures {
+ buildConfig = true
+ }
}
dependencies {
diff --git a/core-ui/src/main/java/com/hoc/flowmvi/core_ui/CollectIn.kt b/core-ui/src/main/java/com/hoc/flowmvi/core_ui/CollectIn.kt
index 669e251d..973827b0 100644
--- a/core-ui/src/main/java/com/hoc/flowmvi/core_ui/CollectIn.kt
+++ b/core-ui/src/main/java/com/hoc/flowmvi/core_ui/CollectIn.kt
@@ -15,12 +15,13 @@ inline fun Flow.collectIn(
owner: LifecycleOwner,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
crossinline action: suspend (value: T) -> Unit,
-): Job = owner.lifecycleScope.launch {
- owner.repeatOnLifecycle(state = minActiveState) {
- Timber.d("Start collecting $owner $minActiveState...")
- collect { action(it) }
+): Job =
+ owner.lifecycleScope.launch {
+ owner.repeatOnLifecycle(state = minActiveState) {
+ Timber.d("Start collecting $owner $minActiveState...")
+ collect { action(it) }
+ }
}
-}
/**
* Launches a new coroutine and repeats `block` every time the Fragment's viewLifecycleOwner
@@ -31,8 +32,9 @@ inline fun Flow.collectInViewLifecycle(
fragment: Fragment,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
crossinline action: suspend (value: T) -> Unit,
-): Job = collectIn(
- owner = fragment.viewLifecycleOwner,
- minActiveState = minActiveState,
- action = action,
-)
+): Job =
+ collectIn(
+ owner = fragment.viewLifecycleOwner,
+ minActiveState = minActiveState,
+ action = action,
+ )
diff --git a/core-ui/src/main/java/com/hoc/flowmvi/core_ui/FlowBinding.kt b/core-ui/src/main/java/com/hoc/flowmvi/core_ui/FlowBinding.kt
index 44f910a6..3d1475fb 100644
--- a/core-ui/src/main/java/com/hoc/flowmvi/core_ui/FlowBinding.kt
+++ b/core-ui/src/main/java/com/hoc/flowmvi/core_ui/FlowBinding.kt
@@ -23,8 +23,8 @@ internal fun checkMainThread() {
}
@CheckResult
-fun EditText.firstChange(): Flow {
- return callbackFlow {
+fun EditText.firstChange(): Flow =
+ callbackFlow {
checkMainThread()
val listener = doOnTextChanged { _, _, _, _ -> trySend(Unit) }
@@ -35,27 +35,24 @@ fun EditText.firstChange(): Flow {
}
}
}.take(1)
-}
@CheckResult
-fun SwipeRefreshLayout.refreshes(): Flow {
- return callbackFlow {
+fun SwipeRefreshLayout.refreshes(): Flow =
+ callbackFlow {
checkMainThread()
setOnRefreshListener { trySend(Unit) }
awaitClose { setOnRefreshListener(null) }
}
-}
@CheckResult
-fun View.clicks(): Flow {
- return callbackFlow {
+fun View.clicks(): Flow =
+ callbackFlow {
checkMainThread()
setOnClickListener { trySend(it) }
awaitClose { setOnClickListener(null) }
}
-}
data class SearchViewQueryTextEvent(
val view: SearchView,
@@ -64,33 +61,35 @@ data class SearchViewQueryTextEvent(
)
@CheckResult
-fun SearchView.queryTextEvents(): Flow {
- return callbackFlow {
+fun SearchView.queryTextEvents(): Flow =
+ callbackFlow {
checkMainThread()
- setOnQueryTextListener(object : SearchView.OnQueryTextListener {
- override fun onQueryTextSubmit(query: String): Boolean {
- trySend(
- SearchViewQueryTextEvent(
- view = this@queryTextEvents,
- query = query,
- isSubmitted = true,
+ setOnQueryTextListener(
+ object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String): Boolean {
+ trySend(
+ SearchViewQueryTextEvent(
+ view = this@queryTextEvents,
+ query = query,
+ isSubmitted = true,
+ ),
)
- )
- return false
- }
+ return false
+ }
- override fun onQueryTextChange(newText: String): Boolean {
- trySend(
- SearchViewQueryTextEvent(
- view = this@queryTextEvents,
- query = newText,
- isSubmitted = false,
+ override fun onQueryTextChange(newText: String): Boolean {
+ trySend(
+ SearchViewQueryTextEvent(
+ view = this@queryTextEvents,
+ query = newText,
+ isSubmitted = false,
+ ),
)
- )
- return true
- }
- })
+ return true
+ }
+ },
+ )
awaitClose { setOnQueryTextListener(null) }
}.startWith {
@@ -100,14 +99,12 @@ fun SearchView.queryTextEvents(): Flow {
isSubmitted = false,
)
}
-}
@CheckResult
-fun EditText.textChanges(): Flow {
- return callbackFlow {
+fun EditText.textChanges(): Flow =
+ callbackFlow {
checkMainThread()
val listener = doOnTextChanged { text, _, _, _ -> trySend(text) }
awaitClose { removeTextChangedListener(listener) }
}.startWith { text }
-}
diff --git a/core-ui/src/main/java/com/hoc/flowmvi/core_ui/SwipeLeftToDeleteCallback.kt b/core-ui/src/main/java/com/hoc/flowmvi/core_ui/SwipeLeftToDeleteCallback.kt
index 39b5ad6b..6e38cd26 100644
--- a/core-ui/src/main/java/com/hoc/flowmvi/core_ui/SwipeLeftToDeleteCallback.kt
+++ b/core-ui/src/main/java/com/hoc/flowmvi/core_ui/SwipeLeftToDeleteCallback.kt
@@ -9,8 +9,10 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import kotlin.LazyThreadSafetyMode.NONE
-class SwipeLeftToDeleteCallback(context: Context, private val onSwipedCallback: (Int) -> Unit) :
- ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
+class SwipeLeftToDeleteCallback(
+ context: Context,
+ private val onSwipedCallback: (Int) -> Unit,
+) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
private val background: ColorDrawable by lazy(NONE) {
ColorDrawable(getColor(context, R.color.swipe_to_delete_background_color))
}
@@ -24,7 +26,10 @@ class SwipeLeftToDeleteCallback(context: Context, private val onSwipedCallback:
target: RecyclerView.ViewHolder,
) = false
- override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
+ override fun onSwiped(
+ viewHolder: RecyclerView.ViewHolder,
+ direction: Int,
+ ) {
val position = viewHolder.bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
onSwipedCallback(position)
@@ -57,7 +62,7 @@ class SwipeLeftToDeleteCallback(context: Context, private val onSwipedCallback:
itemView.right + dX.toInt() - 8,
itemView.top,
itemView.right,
- itemView.bottom
+ itemView.bottom,
)
}
else -> {
diff --git a/core-ui/src/main/java/com/hoc/flowmvi/core_ui/parcelable.kt b/core-ui/src/main/java/com/hoc/flowmvi/core_ui/parcelable.kt
new file mode 100644
index 00000000..0e8e3d88
--- /dev/null
+++ b/core-ui/src/main/java/com/hoc/flowmvi/core_ui/parcelable.kt
@@ -0,0 +1,30 @@
+package com.hoc.flowmvi.core_ui
+
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+
+/**
+ * https://stackoverflow.com/a/73311814/11191424
+ */
+inline fun Intent.parcelable(key: String): T? =
+ // TODO: Use `>`, because https://issuetracker.google.com/issues/240585930#comment6
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
+ getParcelableExtra(key, T::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ getParcelableExtra(key)
+ }
+
+/**
+ * https://stackoverflow.com/a/73311814/11191424
+ */
+inline fun Bundle.parcelable(key: String): T? =
+ // TODO: Use `>`, because https://issuetracker.google.com/issues/240585930#comment6
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
+ getParcelable(key, T::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ getParcelable(key)
+ }
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 169a4954..2b85a8b5 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -1,5 +1,6 @@
plugins {
kotlin
+ id("org.jetbrains.kotlinx.kover")
}
java {
diff --git a/core/src/main/java/com/hoc/flowmvi/core/EitherNes.kt b/core/src/main/java/com/hoc/flowmvi/core/EitherNes.kt
index 29c7a099..0cd43881 100644
--- a/core/src/main/java/com/hoc/flowmvi/core/EitherNes.kt
+++ b/core/src/main/java/com/hoc/flowmvi/core/EitherNes.kt
@@ -17,8 +17,7 @@ typealias EitherNes = Either, A>
inline fun A.rightNes(): EitherNes = this.right()
@Suppress("NOTHING_TO_INLINE")
-inline fun E.leftNes(): EitherNes =
- NonEmptySet.of(this).left()
+inline fun E.leftNes(): EitherNes = NonEmptySet.of(this).left()
@OptIn(ExperimentalContracts::class)
inline fun Either.Companion.zipOrAccumulateNonEmptySet(
@@ -39,7 +38,7 @@ inline fun Either.Companion.zipOrAccumulateNonEmptySet(
a.value,
b.value,
c.value,
- )
+ ),
)
} else {
Either.Left(
@@ -47,7 +46,7 @@ inline fun Either.Companion.zipOrAccumulateNonEmptySet(
if (a is Either.Left) this.addAll(a.value)
if (b is Either.Left) this.addAll(b.value)
if (c is Either.Left) this.addAll(c.value)
- }.toNonEmptySetOrNull()!!
+ }.toNonEmptySetOrNull()!!,
)
}
}
diff --git a/core/src/main/java/com/hoc/flowmvi/core/NonEmptySet.kt b/core/src/main/java/com/hoc/flowmvi/core/NonEmptySet.kt
index 336215bb..315af8b8 100644
--- a/core/src/main/java/com/hoc/flowmvi/core/NonEmptySet.kt
+++ b/core/src/main/java/com/hoc/flowmvi/core/NonEmptySet.kt
@@ -4,61 +4,67 @@ package com.hoc.flowmvi.core
* `NonEmptySet` is a data type used to model sets that guarantee to have at least one value.
*/
class NonEmptySet
-@Throws(IllegalArgumentException::class)
-private constructor(val set: Set) : AbstractSet() {
- init {
- require(set.isNotEmpty()) { "Set must not be empty" }
- require(set !is NonEmptySet) { "Set must not be NonEmptySet" }
- }
+ @Throws(IllegalArgumentException::class)
+ private constructor(
+ val set: Set,
+ ) : AbstractSet() {
+ init {
+ require(set.isNotEmpty()) { "Set must not be empty" }
+ require(set !is NonEmptySet) { "Set must not be NonEmptySet" }
+ }
+
+ override val size: Int get() = set.size
+
+ override fun iterator(): Iterator = set.iterator()
- override val size: Int get() = set.size
- override fun iterator(): Iterator = set.iterator()
- override fun isEmpty(): Boolean = false
+ override fun isEmpty(): Boolean = false
- operator fun plus(l: NonEmptySet<@UnsafeVariance T>): NonEmptySet =
- NonEmptySet(set + l.set)
+ operator fun plus(l: NonEmptySet<@UnsafeVariance T>): NonEmptySet = NonEmptySet(set + l.set)
- @Suppress("RedundantOverride")
- override fun equals(other: Any?): Boolean = super.equals(other)
+ @Suppress("RedundantOverride")
+ override fun equals(other: Any?): Boolean = super.equals(other)
- @Suppress("RedundantOverride")
- override fun hashCode(): Int = super.hashCode()
+ @Suppress("RedundantOverride")
+ override fun hashCode(): Int = super.hashCode()
- override fun toString(): String =
- "NonEmptySet(${set.joinToString()})"
+ override fun toString(): String = "NonEmptySet(${set.joinToString()})"
- companion object {
- /**
- * Creates a [NonEmptySet] from the given [Collection].
- * @return null if [this] is empty.
- */
- @JvmStatic
- fun Collection.toNonEmptySetOrNull(): NonEmptySet? =
- if (isEmpty()) null else NonEmptySet(toSet())
+ companion object {
+ /**
+ * Creates a [NonEmptySet] from the given [Collection].
+ * @return null if [this] is empty.
+ */
+ @JvmStatic
+ fun Collection.toNonEmptySetOrNull(): NonEmptySet? = if (isEmpty()) null else NonEmptySet(toSet())
- /**
- * Creates a [NonEmptySet] from the given [Set].
- * @return null if [this] is empty.
- */
- @JvmStatic
- fun Set.toNonEmptySetOrNull(): NonEmptySet? = (this as? NonEmptySet)
- ?: if (isEmpty()) null else NonEmptySet(this)
+ /**
+ * Creates a [NonEmptySet] from the given [Set].
+ * @return null if [this] is empty.
+ */
+ @JvmStatic
+ fun Set.toNonEmptySetOrNull(): NonEmptySet? =
+ (this as? NonEmptySet)
+ ?: if (isEmpty()) null else NonEmptySet(this)
- /**
- * Creates a [NonEmptySet] from the given values.
- */
- @JvmStatic
- fun of(element: T, vararg elements: T): NonEmptySet = NonEmptySet(
- buildSet(capacity = 1 + elements.size) {
- add(element)
- addAll(elements)
- }
- )
+ /**
+ * Creates a [NonEmptySet] from the given values.
+ */
+ @JvmStatic
+ fun of(
+ element: T,
+ vararg elements: T,
+ ): NonEmptySet =
+ NonEmptySet(
+ buildSet(capacity = 1 + elements.size) {
+ add(element)
+ addAll(elements)
+ },
+ )
- /**
- * Creates a [NonEmptySet] that contains only the specified [element].
- */
- @JvmStatic
- fun of(element: T): NonEmptySet = NonEmptySet(setOf(element))
+ /**
+ * Creates a [NonEmptySet] that contains only the specified [element].
+ */
+ @JvmStatic
+ fun of(element: T): NonEmptySet = NonEmptySet(setOf(element))
+ }
}
-}
diff --git a/core/src/main/java/com/hoc/flowmvi/core/selfReference.kt b/core/src/main/java/com/hoc/flowmvi/core/selfReference.kt
index fffed204..dd109ee5 100644
--- a/core/src/main/java/com/hoc/flowmvi/core/selfReference.kt
+++ b/core/src/main/java/com/hoc/flowmvi/core/selfReference.kt
@@ -8,8 +8,13 @@ import kotlin.reflect.KProperty
// Opt-in is required with the -language-version 1.8 compiler option.
// See https://kotlinlang.org/docs/inline-classes.html for more information.
@JvmInline
-value class SelfReference(val value: T) : ReadOnlyProperty {
- override fun getValue(thisRef: Any?, property: KProperty<*>): T = value
+value class SelfReference(
+ val value: T,
+) : ReadOnlyProperty {
+ override fun getValue(
+ thisRef: Any?,
+ property: KProperty<*>,
+ ): T = value
}
/**
diff --git a/core/src/test/java/com/hoc/flowmvi/core/NonEmptySetTest.kt b/core/src/test/java/com/hoc/flowmvi/core/NonEmptySetTest.kt
index c5597c44..040bfc12 100644
--- a/core/src/test/java/com/hoc/flowmvi/core/NonEmptySetTest.kt
+++ b/core/src/test/java/com/hoc/flowmvi/core/NonEmptySetTest.kt
@@ -18,7 +18,7 @@ class NonEmptySetTest {
assertEquals(
setOf(1),
assertNotNull(
- listOf(1).toNonEmptySetOrNull()
+ listOf(1).toNonEmptySetOrNull(),
),
)
@@ -32,7 +32,7 @@ class NonEmptySetTest {
assertEquals(
setOf(1),
assertNotNull(
- listOf(1, 1).toNonEmptySetOrNull()
+ listOf(1, 1).toNonEmptySetOrNull(),
),
)
}
@@ -47,22 +47,22 @@ class NonEmptySetTest {
assertEquals(
setOf(1),
assertNotNull(
- setOf(1).toNonEmptySetOrNull()
- )
+ setOf(1).toNonEmptySetOrNull(),
+ ),
)
assertEquals(
setOf(1, 2),
assertNotNull(
- setOf(1, 2).toNonEmptySetOrNull()
- )
+ setOf(1, 2).toNonEmptySetOrNull(),
+ ),
)
assertEquals(
setOf(1),
assertNotNull(
- setOf(1, 1).toNonEmptySetOrNull()
- )
+ setOf(1, 1).toNonEmptySetOrNull(),
+ ),
)
}
@@ -72,7 +72,7 @@ class NonEmptySetTest {
assertSame(
input,
- input.toNonEmptySetOrNull()
+ input.toNonEmptySetOrNull(),
)
}
diff --git a/coverage.gradle.kts b/coverage.gradle.kts
deleted file mode 100644
index aa6b4a14..00000000
--- a/coverage.gradle.kts
+++ /dev/null
@@ -1,36 +0,0 @@
-apply(plugin = "jacoco")
-
-tasks {
- val debugCoverageReport by registering(JacocoReport::class)
- debugCoverageReport {
- dependsOn("testDebugUnitTest")
-
- reports {
- xml.run {
- required.value(true)
- outputLocation.set(file("$buildDir/reports/jacoco/test/jacocoTestReport.xml"))
- }
- html.required.value(true)
- }
-
- val kotlinClasses = fileTree("$buildDir/tmp/kotlin-classes/debug")
- val coverageSourceDirs = arrayOf(
- "src/main/java",
- "src/debug/java"
- )
- val executionDataDirs = fileTree("$buildDir") {
- setIncludes(
- listOf(
- "jacoco/testDebugUnitTest.exec",
- "outputs/code_coverage/debugAndroidTest/connected/*.ec",
- "outputs/code-coverage/connected/*coverage.ec"
- )
- )
- }
-
- classDirectories.setFrom(files(kotlinClasses))
- sourceDirectories.setFrom(coverageSourceDirs)
- additionalSourceDirs.setFrom(files(coverageSourceDirs))
- executionData.setFrom(executionDataDirs)
- }
-}
diff --git a/data/build.gradle.kts b/data/build.gradle.kts
index 8c000e54..dd6ba394 100644
--- a/data/build.gradle.kts
+++ b/data/build.gradle.kts
@@ -2,6 +2,7 @@ plugins {
androidLib
kotlinAndroid
kotlinKapt
+ id("org.jetbrains.kotlinx.kover")
}
android {
@@ -11,7 +12,6 @@ android {
defaultConfig {
minSdk = appConfig.minSdkVersion
- targetSdk = appConfig.targetSdkVersion
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
@@ -22,7 +22,7 @@ android {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
+ "proguard-rules.pro",
)
}
}
@@ -37,6 +37,10 @@ android {
unitTests.isIncludeAndroidResources = true
unitTests.isReturnDefaultValues = true
}
+
+ buildFeatures {
+ buildConfig = true
+ }
}
dependencies {
diff --git a/data/src/main/java/com/hoc/flowmvi/data/DataModule.kt b/data/src/main/java/com/hoc/flowmvi/data/DataModule.kt
index 1effdef9..d7168731 100644
--- a/data/src/main/java/com/hoc/flowmvi/data/DataModule.kt
+++ b/data/src/main/java/com/hoc/flowmvi/data/DataModule.kt
@@ -30,65 +30,68 @@ internal val ERROR_RESPONSE_JSON_ADAPTER = named("ERROR_RESPONSE_JSON_ADAPTER")
@ExperimentalStdlibApi
@ExperimentalTime
@ExperimentalCoroutinesApi
-val dataModule = module {
- singleOf(UserApiService::invoke)
+val dataModule =
+ module {
+ singleOf(UserApiService::invoke)
- single {
- provideRetrofit(
- baseUrl = get(BASE_URL_QUALIFIER),
- moshi = get(),
- client = get()
- )
- }
+ single {
+ provideRetrofit(
+ baseUrl = get(BASE_URL_QUALIFIER),
+ moshi = get(),
+ client = get(),
+ )
+ }
- single { provideMoshi() }
+ single { provideMoshi() }
- single { provideOkHttpClient() }
+ single { provideOkHttpClient() }
- factory(BASE_URL_QUALIFIER) { "https://mvi-coroutines-flow-server.onrender.com/" }
+ factory(BASE_URL_QUALIFIER) { "https://mvi-coroutines-flow-server.onrender.com/" }
- factory { UserResponseToUserDomainMapper() }
+ factory { UserResponseToUserDomainMapper() }
- factory { UserDomainToUserBodyMapper() }
+ factory { UserDomainToUserBodyMapper() }
- factory(ERROR_RESPONSE_JSON_ADAPTER) { get().adapter() }
+ factory(ERROR_RESPONSE_JSON_ADAPTER) { get().adapter() }
- factory { UserErrorMapper(get(ERROR_RESPONSE_JSON_ADAPTER)) }
+ factory { UserErrorMapper(get(ERROR_RESPONSE_JSON_ADAPTER)) }
- single {
- UserRepositoryImpl(
- userApiService = get(),
- dispatchers = get(),
- responseToDomain = get(),
- domainToBody = get(),
- errorMapper = get(),
- )
+ single {
+ UserRepositoryImpl(
+ userApiService = get(),
+ dispatchers = get(),
+ responseToDomain = get(),
+ domainToBody = get(),
+ errorMapper = get(),
+ )
+ }
}
-}
-private fun provideMoshi(): Moshi {
- return Moshi
+private fun provideMoshi(): Moshi =
+ Moshi
.Builder()
.add(KotlinJsonAdapterFactory())
.build()
-}
-private fun provideRetrofit(baseUrl: String, moshi: Moshi, client: OkHttpClient): Retrofit {
- return Retrofit.Builder()
+private fun provideRetrofit(
+ baseUrl: String,
+ moshi: Moshi,
+ client: OkHttpClient,
+): Retrofit =
+ Retrofit
+ .Builder()
.client(client)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(baseUrl)
.build()
-}
-private fun provideOkHttpClient(): OkHttpClient {
- return OkHttpClient.Builder()
+private fun provideOkHttpClient(): OkHttpClient =
+ OkHttpClient
+ .Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.addInterceptor(
HttpLoggingInterceptor()
- .apply { level = if (BuildConfig.DEBUG) Level.BODY else Level.NONE }
- )
- .build()
-}
+ .apply { level = if (BuildConfig.DEBUG) Level.BODY else Level.NONE },
+ ).build()
diff --git a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt
index dbee5bcf..37f5009b 100644
--- a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt
+++ b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt
@@ -44,112 +44,133 @@ internal class UserRepositoryImpl(
private val domainToBody: Mapper,
private val errorMapper: Mapper,
) : UserRepository {
-
private sealed class Change {
- class Removed(val removed: User) : Change()
- class Refreshed(val user: List) : Change()
- class Added(val user: User) : Change()
+ class Removed(
+ val removed: User,
+ ) : Change()
+
+ class Refreshed(
+ val user: List,
+ ) : Change()
+
+ class Added(
+ val user: User,
+ ) : Change()
}
private val changesFlow = MutableSharedFlow(extraBufferCapacity = 64)
- private suspend inline fun sendChange(change: Change) = changesFlow.emit(change)
- private fun getUsersFromRemote(): Flow> = flowFromSuspend {
- Timber.d("[USER_REPO] getUsersFromRemote ...")
+ private suspend inline fun sendChange(change: Change) = changesFlow.emit(change)
- userApiService
- .getUsers()
- .map { response ->
- responseToDomain(response)
- .mapLeft(UserError::ValidationFailed)
- .onLeft { logError(it, "Map $response to user") }
- .getOrElse { throw it }
- }
- }
- .retryWithExponentialBackoff(
+ private fun getUsersFromRemote(): Flow> =
+ flowFromSuspend {
+ Timber.d("[USER_REPO] getUsersFromRemote ...")
+
+ userApiService
+ .getUsers()
+ .map { response ->
+ responseToDomain(response)
+ .mapLeft(UserError::ValidationFailed)
+ .onLeft { logError(it, "Map $response to user") }
+ .getOrElse { throw it }
+ }
+ }.retryWithExponentialBackoff(
maxAttempt = 2,
initialDelay = 500.milliseconds,
factor = 2.0,
) { it is IOException }
- override fun getUsers() = getUsersFromRemote()
- .flatMapConcat { initial ->
- changesFlow
- .onEach { Timber.d("[USER_REPO] Change=$it") }
- .scan(initial) { acc, change ->
- when (change) {
- is Change.Removed -> acc.filter { it.id != change.removed.id }
- is Change.Refreshed -> change.user
- is Change.Added -> acc + change.user
+ override fun getUsers() =
+ getUsersFromRemote()
+ .flatMapConcat { initial ->
+ changesFlow
+ .onEach { Timber.d("[USER_REPO] Change=$it") }
+ .scan(initial) { acc, change ->
+ when (change) {
+ is Change.Removed -> acc.filter { it.id != change.removed.id }
+ is Change.Refreshed -> change.user
+ is Change.Added -> acc + change.user
+ }
}
- }
- }
- .onEach { Timber.d("[USER_REPO] Emit users.size=${it.size} ") }
- .map { it.right().leftWiden>() }
- .catch {
- logError(it, "getUsers")
- emit(errorMapper(it).left())
- }
+ }.onEach { Timber.d("[USER_REPO] Emit users.size=${it.size} ") }
+ .map { it.right().leftWiden() }
+ .catch {
+ logError(it, "getUsers")
+ emit(errorMapper(it).left())
+ }
- override suspend fun refresh() = catchEither { getUsersFromRemote().first() }
- .onRight { sendChange(Change.Refreshed(it)) }
- .map { }
- .onLeft { logError(it, "refresh") }
- .mapLeft(errorMapper)
-
- override suspend fun remove(user: User) = either {
- withContext(dispatchers.io) {
- val response = catchEither { userApiService.remove(user.id) }
- .onLeft { logError(it, "remove user=$user") }
- .mapLeft(errorMapper)
- .bind()
-
- val deleted = responseToDomain(response)
- .mapLeft { UserError.ValidationFailed(it) }
- .onLeft { logError(it, "remove user=$user") }
- .bind()
-
- sendChange(Change.Removed(deleted))
+ override suspend fun refresh() =
+ catchEither { getUsersFromRemote().first() }
+ .onRight { sendChange(Change.Refreshed(it)) }
+ .map { }
+ .onLeft { logError(it, "refresh") }
+ .mapLeft(errorMapper)
+
+ override suspend fun remove(user: User) =
+ either {
+ withContext(dispatchers.io) {
+ val response =
+ catchEither { userApiService.remove(user.id) }
+ .onLeft { logError(it, "remove user=$user") }
+ .mapLeft(errorMapper)
+ .bind()
+
+ val deleted =
+ responseToDomain(response)
+ .mapLeft { UserError.ValidationFailed(it) }
+ .onLeft { logError(it, "remove user=$user") }
+ .bind()
+
+ sendChange(Change.Removed(deleted))
+ }
}
- }
- override suspend fun add(user: User) = either {
- withContext(dispatchers.io) {
- val response = catchEither { userApiService.add(domainToBody(user)) }
- .onLeft { logError(it, "add user=$user") }
- .mapLeft(errorMapper)
- .bind()
-
- val added = responseToDomain(response)
- .mapLeft { UserError.ValidationFailed(it) }
- .onLeft { logError(it, "add user=$user") }
- .bind()
-
- sendChange(Change.Added(added))
+ override suspend fun add(user: User) =
+ either {
+ withContext(dispatchers.io) {
+ val response =
+ catchEither { userApiService.add(domainToBody(user)) }
+ .onLeft { logError(it, "add user=$user") }
+ .mapLeft(errorMapper)
+ .bind()
+
+ val added =
+ responseToDomain(response)
+ .mapLeft { UserError.ValidationFailed(it) }
+ .onLeft { logError(it, "add user=$user") }
+ .bind()
+
+ sendChange(Change.Added(added))
+ }
}
- }
- override suspend fun search(query: String) = either {
- withContext(dispatchers.io) {
- val userResponses = catchEither { userApiService.search(query) }
- .onLeft { logError(it, "search query=$query") }
- .mapLeft(errorMapper)
- .bind()
-
- val users = userResponses.map { userResponse ->
- responseToDomain(userResponse)
- .mapLeft(UserError::ValidationFailed)
- .onLeft { logError(it, "search query=$query") }
- .bind()
- }
+ override suspend fun search(query: String) =
+ either {
+ withContext(dispatchers.io) {
+ val userResponses =
+ catchEither { userApiService.search(query) }
+ .onLeft { logError(it, "search query=$query") }
+ .mapLeft(errorMapper)
+ .bind()
+
+ val users =
+ userResponses.map { userResponse ->
+ responseToDomain(userResponse)
+ .mapLeft(UserError::ValidationFailed)
+ .onLeft { logError(it, "search query=$query") }
+ .bind()
+ }
- users
+ users
+ }
}
- }
private companion object {
@Suppress("NOTHING_TO_INLINE")
- private inline fun logError(t: Throwable, message: String) = Timber.tag(TAG).e(t, message)
+ private inline fun logError(
+ t: Throwable,
+ message: String,
+ ) = Timber.tag(TAG).e(t, message)
private val TAG = UserRepositoryImpl::class.java.simpleName
}
diff --git a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapper.kt b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapper.kt
index 088b3a50..de10474d 100644
--- a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapper.kt
+++ b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapper.kt
@@ -5,11 +5,10 @@ import com.hoc.flowmvi.data.remote.UserBody
import com.hoc.flowmvi.domain.model.User
internal class UserDomainToUserBodyMapper : Mapper {
- override fun invoke(domain: User): UserBody {
- return UserBody(
+ override fun invoke(domain: User): UserBody =
+ UserBody(
email = domain.email.value,
firstName = domain.firstName.value,
- lastName = domain.lastName.value
+ lastName = domain.lastName.value,
)
- }
}
diff --git a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt
index ef48d351..87c1e7ff 100644
--- a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt
+++ b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt
@@ -13,22 +13,25 @@ import java.net.UnknownHostException
import okhttp3.ResponseBody
import retrofit2.HttpException
-internal class UserErrorMapper(private val errorResponseJsonAdapter: JsonAdapter) :
- Mapper {
+internal class UserErrorMapper(
+ private val errorResponseJsonAdapter: JsonAdapter,
+) : Mapper {
override fun invoke(throwable: Throwable): UserError {
throwable.nonFatalOrThrow()
return runCatching {
when (throwable) {
is UserError -> throwable
- is IOException -> when (throwable) {
- is UnknownHostException -> UserError.NetworkError
- is SocketTimeoutException -> UserError.NetworkError
- is SocketException -> UserError.NetworkError
- else -> UserError.NetworkError
- }
+ is IOException ->
+ when (throwable) {
+ is UnknownHostException -> UserError.NetworkError
+ is SocketTimeoutException -> UserError.NetworkError
+ is SocketException -> UserError.NetworkError
+ else -> UserError.NetworkError
+ }
is HttpException ->
- throwable.response()!!
+ throwable
+ .response()!!
.takeUnless { it.isSuccessful }!!
.errorBody()!!
.use(ResponseBody::string)
@@ -49,9 +52,11 @@ internal class UserErrorMapper(private val errorResponseJsonAdapter: JsonAdapter
"internal-error" -> UserError.ServerError
"invalid-id" -> UserError.InvalidId(id = errorResponse.data as String)
"user-not-found" -> UserError.UserNotFound(id = errorResponse.data as String)
- "validation-failed" -> UserError.ValidationFailed(
- errors = UserValidationError.VALUES_SET // TODO(hoc081098): Map validation errors from server response
- )
+ "validation-failed" ->
+ UserError.ValidationFailed(
+ // TODO(hoc081098): Map validation errors from server response
+ errors = UserValidationError.VALUES_SET,
+ )
else -> UserError.Unexpected
}
}
diff --git a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt
index 0201a373..e20e880c 100644
--- a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt
+++ b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt
@@ -6,15 +6,13 @@ import com.hoc.flowmvi.data.remote.UserResponse
import com.hoc.flowmvi.domain.model.User
import com.hoc.flowmvi.domain.model.UserValidationError
-internal class UserResponseToUserDomainMapper :
- Mapper> {
- override fun invoke(response: UserResponse): EitherNes {
- return User.create(
+internal class UserResponseToUserDomainMapper : Mapper> {
+ override fun invoke(response: UserResponse): EitherNes =
+ User.create(
id = response.id,
avatar = response.avatar,
email = response.email,
firstName = response.firstName,
- lastName = response.lastName
+ lastName = response.lastName,
)
- }
}
diff --git a/data/src/main/java/com/hoc/flowmvi/data/remote/ErrorResponse.kt b/data/src/main/java/com/hoc/flowmvi/data/remote/ErrorResponse.kt
index 88c6bd4e..cccc3afe 100644
--- a/data/src/main/java/com/hoc/flowmvi/data/remote/ErrorResponse.kt
+++ b/data/src/main/java/com/hoc/flowmvi/data/remote/ErrorResponse.kt
@@ -1,3 +1,5 @@
+@file:Suppress("ktlint:standard:discouraged-comment-location")
+
package com.hoc.flowmvi.data.remote
import com.squareup.moshi.Json
diff --git a/data/src/main/java/com/hoc/flowmvi/data/remote/UserApiService.kt b/data/src/main/java/com/hoc/flowmvi/data/remote/UserApiService.kt
index 2c7cd3c7..659e996e 100644
--- a/data/src/main/java/com/hoc/flowmvi/data/remote/UserApiService.kt
+++ b/data/src/main/java/com/hoc/flowmvi/data/remote/UserApiService.kt
@@ -15,14 +15,20 @@ internal interface UserApiService {
suspend fun getUsers(): List
@DELETE("users/{id}")
- suspend fun remove(@Path("id") userId: String): UserResponse
+ suspend fun remove(
+ @Path("id") userId: String,
+ ): UserResponse
@Headers("Content-Type: application/json")
@POST("users")
- suspend fun add(@Body user: UserBody): UserResponse
+ suspend fun add(
+ @Body user: UserBody,
+ ): UserResponse
@GET("users/search")
- suspend fun search(@Query("q") query: String): List
+ suspend fun search(
+ @Query("q") query: String,
+ ): List
companion object {
operator fun invoke(retrofit: Retrofit) = retrofit.create()
diff --git a/data/src/main/java/com/hoc/flowmvi/data/remote/UserResponse.kt b/data/src/main/java/com/hoc/flowmvi/data/remote/UserResponse.kt
index 4ba27a48..0d49ef49 100644
--- a/data/src/main/java/com/hoc/flowmvi/data/remote/UserResponse.kt
+++ b/data/src/main/java/com/hoc/flowmvi/data/remote/UserResponse.kt
@@ -12,5 +12,5 @@ internal data class UserResponse(
@Json(name = "last_name")
val lastName: String,
@Json(name = "avatar")
- val avatar: String
+ val avatar: String,
)
diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt
index 9516cc59..115dd5cf 100644
--- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt
+++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt
@@ -32,22 +32,23 @@ import timber.log.Timber
@ExperimentalStdlibApi
class UserRepositoryImplRealAPITest : KoinTest {
@get:Rule
- val koinRuleTest = KoinTestRule.create {
- printLogger(Level.DEBUG)
+ val koinRuleTest =
+ KoinTestRule.create {
+ printLogger(Level.DEBUG)
- modules(
- dataModule,
- module {
- factory {
- object : AppCoroutineDispatchers {
- override val main: CoroutineDispatcher get() = Main
- override val io: CoroutineDispatcher get() = IO
- override val mainImmediate: CoroutineDispatcher get() = Main.immediate
+ modules(
+ dataModule,
+ module {
+ factory {
+ object : AppCoroutineDispatchers {
+ override val main: CoroutineDispatcher get() = Main
+ override val io: CoroutineDispatcher get() = IO
+ override val mainImmediate: CoroutineDispatcher get() = Main.immediate
+ }
}
- }
- }
- )
- }
+ },
+ )
+ }
@get:Rule
val timberRule = TimberRule()
@@ -55,16 +56,18 @@ class UserRepositoryImplRealAPITest : KoinTest {
private val userRepo by inject()
@Test
- fun getUsers() = runBlocking {
- kotlin.runCatching {
- val result = userRepo
- .getUsers()
- .first()
- assertTrue(result.isRight())
- assertTrue(result.rightValueOrThrow.isNotEmpty())
+ fun getUsers() =
+ runBlocking {
+ kotlin.runCatching {
+ val result =
+ userRepo
+ .getUsers()
+ .first()
+ assertTrue(result.isRight())
+ assertTrue(result.rightValueOrThrow.isNotEmpty())
+ }
+ Unit
}
- Unit
- }
}
class TimberRule : TestWatcher() {
@@ -84,28 +87,35 @@ class ConsoleTree : Timber.DebugTree() {
private val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
- override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
+ override fun log(
+ priority: Int,
+ tag: String?,
+ message: String,
+ t: Throwable?,
+ ) {
val dateTime = LocalDateTime.now().format(dateTimeFormatter)
- val priorityChar = when (priority) {
- Log.VERBOSE -> 'V'
- Log.DEBUG -> 'D'
- Log.INFO -> 'I'
- Log.WARN -> 'W'
- Log.ERROR -> 'E'
- Log.ASSERT -> 'A'
- else -> '?'
- }
+ val priorityChar =
+ when (priority) {
+ Log.VERBOSE -> 'V'
+ Log.DEBUG -> 'D'
+ Log.INFO -> 'I'
+ Log.WARN -> 'W'
+ Log.ERROR -> 'E'
+ Log.ASSERT -> 'A'
+ else -> '?'
+ }
println("$dateTime $priorityChar/$tag: $message")
}
override fun createStackElementTag(element: StackTraceElement): String {
val className = element.className
- val tag = if (anonymousClassPattern.containsMatchIn(className)) {
- anonymousClassPattern.replace(className, "")
- } else {
- className
- }
+ val tag =
+ if (anonymousClassPattern.containsMatchIn(className)) {
+ anonymousClassPattern.replace(className, "")
+ } else {
+ className
+ }
return tag.substringAfterLast('.')
}
}
diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt
index cccb7004..15172f3a 100644
--- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt
+++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt
@@ -42,59 +42,62 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Rule
-private val USER_BODY = UserBody(
- email = "email1@gmail.com",
- firstName = "first",
- lastName = "last",
-)
-
-private val USER_RESPONSES = listOf(
- UserResponse(
- id = "1",
+private val USER_BODY =
+ UserBody(
email = "email1@gmail.com",
firstName = "first",
lastName = "last",
- avatar = "avatar1",
- ),
- UserResponse(
- id = "2",
- email = "email2@gmail.com",
- firstName = "first",
- lastName = "last",
- avatar = "avatar2",
- ),
- UserResponse(
- id = "3",
- email = "email3@gmail.com",
- firstName = "first",
- lastName = "last",
- avatar = "avatar3",
- ),
-)
-
-private val USERS = listOf(
- User.create(
- id = "1",
- email = "email1@gmail.com",
- firstName = "first",
- lastName = "last",
- avatar = "avatar1",
- ),
- User.create(
- id = "2",
- email = "email2@gmail.com",
- firstName = "first",
- lastName = "last",
- avatar = "avatar2",
- ),
- User.create(
- id = "3",
- email = "email3@gmail.com",
- firstName = "first",
- lastName = "last",
- avatar = "avatar3",
- ),
-).map { it.rightValueOrThrow }
+ )
+
+private val USER_RESPONSES =
+ listOf(
+ UserResponse(
+ id = "1",
+ email = "email1@gmail.com",
+ firstName = "first",
+ lastName = "last",
+ avatar = "avatar1",
+ ),
+ UserResponse(
+ id = "2",
+ email = "email2@gmail.com",
+ firstName = "first",
+ lastName = "last",
+ avatar = "avatar2",
+ ),
+ UserResponse(
+ id = "3",
+ email = "email3@gmail.com",
+ firstName = "first",
+ lastName = "last",
+ avatar = "avatar3",
+ ),
+ )
+
+private val USERS =
+ listOf(
+ User.create(
+ id = "1",
+ email = "email1@gmail.com",
+ firstName = "first",
+ lastName = "last",
+ avatar = "avatar1",
+ ),
+ User.create(
+ id = "2",
+ email = "email2@gmail.com",
+ firstName = "first",
+ lastName = "last",
+ avatar = "avatar2",
+ ),
+ User.create(
+ id = "3",
+ email = "email3@gmail.com",
+ firstName = "first",
+ lastName = "last",
+ avatar = "avatar3",
+ ),
+ ).map { it.rightValueOrThrow }
private val VALID_NES_USERS = USERS.map(User::rightNes)
@@ -118,13 +121,14 @@ class UserRepositoryImplTest {
domainToBody = mockk()
errorMapper = mockk()
- repo = UserRepositoryImpl(
- userApiService = userApiService,
- dispatchers = TestAppCoroutineDispatchers(coroutineRule.testDispatcher),
- responseToDomain = responseToDomain,
- domainToBody = domainToBody,
- errorMapper = errorMapper
- )
+ repo =
+ UserRepositoryImpl(
+ userApiService = userApiService,
+ dispatchers = TestAppCoroutineDispatchers(coroutineRule.testDispatcher),
+ responseToDomain = responseToDomain,
+ domainToBody = domainToBody,
+ errorMapper = errorMapper,
+ )
}
@AfterTest
@@ -139,186 +143,198 @@ class UserRepositoryImplTest {
}
@Test
- fun test_refresh_withApiCallSuccess_returnsRight() = runTest {
- coEvery { userApiService.getUsers() } returns USER_RESPONSES
- every { responseToDomain(any()) } returnsMany VALID_NES_USERS
+ fun test_refresh_withApiCallSuccess_returnsRight() =
+ runTest {
+ coEvery { userApiService.getUsers() } returns USER_RESPONSES
+ every { responseToDomain(any()) } returnsMany VALID_NES_USERS
- val result = repo.refresh()
+ val result = repo.refresh()
- assertTrue(result.isRight())
- assertNotNull(result.getOrNull())
+ assertTrue(result.isRight())
+ assertNotNull(result.getOrNull())
- coVerify { userApiService.getUsers() }
- verifySequence {
- USER_RESPONSES.forEach {
- responseToDomain(it)
+ coVerify { userApiService.getUsers() }
+ verifySequence {
+ USER_RESPONSES.forEach {
+ responseToDomain(it)
+ }
}
}
- }
@Test
- fun test_refresh_withApiCallError_returnsLeft() = runTest {
- val ioException = IOException()
- coEvery { userApiService.getUsers() } throws ioException
- every { errorMapper(ofType()) } returns UserError.NetworkError
+ fun test_refresh_withApiCallError_returnsLeft() =
+ runTest {
+ val ioException = IOException()
+ coEvery { userApiService.getUsers() } throws ioException
+ every { errorMapper(ofType()) } returns UserError.NetworkError
- val result = repo.refresh()
+ val result = repo.refresh()
- assertTrue(result.isLeft())
- assertEquals(UserError.NetworkError, result.leftValueOrThrow)
- coVerify(exactly = 3) { userApiService.getUsers() } // retry 2 times
- verify(exactly = 1) { errorMapper(ofType()) }
- }
+ assertTrue(result.isLeft())
+ assertEquals(UserError.NetworkError, result.leftValueOrThrow)
+ coVerify(exactly = 3) { userApiService.getUsers() } // retry 2 times
+ verify(exactly = 1) { errorMapper(ofType()) }
+ }
@Test
- fun test_remove_withApiCallSuccess_returnsRight() = runTest {
- val user = USERS[0]
- val userResponse = USER_RESPONSES[0]
+ fun test_remove_withApiCallSuccess_returnsRight() =
+ runTest {
+ val user = USERS[0]
+ val userResponse = USER_RESPONSES[0]
- coEvery { userApiService.remove(user.id) } returns userResponse
- every { responseToDomain(userResponse) } returns user.rightNes()
+ coEvery { userApiService.remove(user.id) } returns userResponse
+ every { responseToDomain(userResponse) } returns user.rightNes()
- val result = repo.remove(user)
+ val result = repo.remove(user)
- assertTrue(result.isRight())
- assertNotNull(result.getOrNull())
+ assertTrue(result.isRight())
+ assertNotNull(result.getOrNull())
- coVerify { userApiService.remove(user.id) }
- coVerify { responseToDomain(userResponse) }
- }
+ coVerify { userApiService.remove(user.id) }
+ coVerify { responseToDomain(userResponse) }
+ }
@Test
- fun test_remove_withApiCallError_returnsLeft() = runTest {
- val user = USERS[0]
- coEvery { userApiService.remove(user.id) } throws IOException()
- every { errorMapper(ofType()) } returns UserError.NetworkError
+ fun test_remove_withApiCallError_returnsLeft() =
+ runTest {
+ val user = USERS[0]
+ coEvery { userApiService.remove(user.id) } throws IOException()
+ every { errorMapper(ofType()) } returns UserError.NetworkError
- val result = repo.remove(user)
+ val result = repo.remove(user)
- assertTrue(result.isLeft())
- assertEquals(UserError.NetworkError, result.leftValueOrThrow)
- coVerify(exactly = 1) { userApiService.remove(user.id) }
- verify(exactly = 1) { errorMapper(ofType()) }
- }
+ assertTrue(result.isLeft())
+ assertEquals(UserError.NetworkError, result.leftValueOrThrow)
+ coVerify(exactly = 1) { userApiService.remove(user.id) }
+ verify(exactly = 1) { errorMapper(ofType()) }
+ }
@Test
- fun test_add_withApiCallSuccess_returnsRight() = runTest {
- val user = USERS[0]
- val userResponse = USER_RESPONSES[0]
+ fun test_add_withApiCallSuccess_returnsRight() =
+ runTest {
+ val user = USERS[0]
+ val userResponse = USER_RESPONSES[0]
- coEvery { userApiService.add(USER_BODY) } returns userResponse
- every { domainToBody(user) } returns USER_BODY
- every { responseToDomain(userResponse) } returns user.rightNes()
+ coEvery { userApiService.add(USER_BODY) } returns userResponse
+ every { domainToBody(user) } returns USER_BODY
+ every { responseToDomain(userResponse) } returns user.rightNes()
- val result = repo.add(user)
+ val result = repo.add(user)
- assertTrue(result.isRight())
- assertNotNull(result.getOrNull())
+ assertTrue(result.isRight())
+ assertNotNull(result.getOrNull())
- coVerify { userApiService.add(USER_BODY) }
- verify { domainToBody(user) }
- coVerify { responseToDomain(userResponse) }
- }
+ coVerify { userApiService.add(USER_BODY) }
+ verify { domainToBody(user) }
+ coVerify { responseToDomain(userResponse) }
+ }
@Test
- fun test_add_withApiCallError_returnsLeft() = runTest {
- val user = USERS[0]
- coEvery { userApiService.add(USER_BODY) } throws IOException()
- every { domainToBody(user) } returns USER_BODY
- every { errorMapper(ofType()) } returns UserError.NetworkError
+ fun test_add_withApiCallError_returnsLeft() =
+ runTest {
+ val user = USERS[0]
+ coEvery { userApiService.add(USER_BODY) } throws IOException()
+ every { domainToBody(user) } returns USER_BODY
+ every { errorMapper(ofType()) } returns UserError.NetworkError
- val result = repo.add(user)
+ val result = repo.add(user)
- assertTrue(result.isLeft())
- assertEquals(UserError.NetworkError, result.leftValueOrThrow)
+ assertTrue(result.isLeft())
+ assertEquals(UserError.NetworkError, result.leftValueOrThrow)
- coVerify(exactly = 1) { userApiService.add(USER_BODY) }
- verify(exactly = 1) { domainToBody(user) }
- verify(exactly = 1) { errorMapper(ofType()) }
- }
+ coVerify(exactly = 1) { userApiService.add(USER_BODY) }
+ verify(exactly = 1) { domainToBody(user) }
+ verify(exactly = 1) { errorMapper(ofType()) }
+ }
@Test
- fun test_search_withApiCallSuccess_returnsRight() = runTest {
- val q = "hoc081098"
- coEvery { userApiService.search(q) } returns USER_RESPONSES
- every { responseToDomain(any()) } returnsMany VALID_NES_USERS
+ fun test_search_withApiCallSuccess_returnsRight() =
+ runTest {
+ val q = "hoc081098"
+ coEvery { userApiService.search(q) } returns USER_RESPONSES
+ every { responseToDomain(any()) } returnsMany VALID_NES_USERS
- val result = repo.search(q)
+ val result = repo.search(q)
- assertTrue(result.isRight())
- assertNotNull(result.getOrNull())
- assertContentEquals(USERS, result.rightValueOrThrow)
+ assertTrue(result.isRight())
+ assertNotNull(result.getOrNull())
+ assertContentEquals(USERS, result.rightValueOrThrow)
- coVerify { userApiService.search(q) }
- coVerifySequence {
- USER_RESPONSES.forEach {
- responseToDomain(it)
+ coVerify { userApiService.search(q) }
+ coVerifySequence {
+ USER_RESPONSES.forEach {
+ responseToDomain(it)
+ }
}
}
- }
@Test
- fun test_search_withApiCallError_returnsLeft() = runTest {
- val q = "hoc081098"
- coEvery { userApiService.search(q) } throws IOException()
- every { errorMapper(ofType()) } returns UserError.NetworkError
+ fun test_search_withApiCallError_returnsLeft() =
+ runTest {
+ val q = "hoc081098"
+ coEvery { userApiService.search(q) } throws IOException()
+ every { errorMapper(ofType()) } returns UserError.NetworkError
- val result = repo.search(q)
+ val result = repo.search(q)
- assertTrue(result.isLeft())
- assertEquals(UserError.NetworkError, result.leftValueOrThrow)
+ assertTrue(result.isLeft())
+ assertEquals(UserError.NetworkError, result.leftValueOrThrow)
- coVerify(exactly = 1) { userApiService.search(q) }
- verify(exactly = 1) { errorMapper(ofType()) }
- }
+ coVerify(exactly = 1) { userApiService.search(q) }
+ verify(exactly = 1) { errorMapper(ofType()) }
+ }
@Test
- fun test_getUsers_withApiCallSuccess_emitsInitial() = runTest {
- coEvery { userApiService.getUsers() } returns USER_RESPONSES
- every { responseToDomain(any()) } returnsMany VALID_NES_USERS
+ fun test_getUsers_withApiCallSuccess_emitsInitial() =
+ runTest {
+ coEvery { userApiService.getUsers() } returns USER_RESPONSES
+ every { responseToDomain(any()) } returnsMany VALID_NES_USERS
- val events = mutableListOf>>()
- val job = launch(start = CoroutineStart.UNDISPATCHED) {
- repo.getUsers().toList(events)
- }
- delay(5_000)
- job.cancel()
-
- assertEquals(1, events.size)
- val result = events.single()
- assertTrue(result.isRight())
- assertNotNull(result.getOrNull())
- assertEquals(USERS, result.rightValueOrThrow)
-
- coVerify { userApiService.getUsers() }
- verifySequence {
- USER_RESPONSES.forEach {
- responseToDomain(it)
+ val events = mutableListOf>>()
+ val job =
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ repo.getUsers().toList(events)
+ }
+ delay(5_000)
+ job.cancel()
+
+ assertEquals(1, events.size)
+ val result = events.single()
+ assertTrue(result.isRight())
+ assertNotNull(result.getOrNull())
+ assertEquals(USERS, result.rightValueOrThrow)
+
+ coVerify { userApiService.getUsers() }
+ verifySequence {
+ USER_RESPONSES.forEach {
+ responseToDomain(it)
+ }
}
}
- }
@Test
- fun test_getUsers_withApiCallError_rethrows() = runTest {
- coEvery { userApiService.getUsers() } throws IOException()
- every { errorMapper(ofType()) } returns UserError.NetworkError
+ fun test_getUsers_withApiCallError_rethrows() =
+ runTest {
+ coEvery { userApiService.getUsers() } throws IOException()
+ every { errorMapper(ofType()) } returns UserError.NetworkError
- val events = mutableListOf>>()
- val job = launch(start = CoroutineStart.UNDISPATCHED) {
- repo.getUsers().toList(events)
- }
- delay(20_000)
- job.cancel()
+ val events = mutableListOf>>()
+ val job =
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ repo.getUsers().toList(events)
+ }
+ delay(20_000)
+ job.cancel()
- assertEquals(1, events.size)
- val result = events.single()
- assertTrue(result.isLeft())
- assertNull(result.getOrNull())
- assertEquals(UserError.NetworkError, result.leftValueOrThrow)
+ assertEquals(1, events.size)
+ val result = events.single()
+ assertTrue(result.isLeft())
+ assertNull(result.getOrNull())
+ assertEquals(UserError.NetworkError, result.leftValueOrThrow)
- coVerify(exactly = 3) { userApiService.getUsers() } // retry 2 times.
- verify(exactly = 1) { errorMapper(ofType()) }
- }
+ coVerify(exactly = 3) { userApiService.getUsers() } // retry 2 times.
+ verify(exactly = 1) { errorMapper(ofType()) }
+ }
@Test
fun test_getUsers_withApiCallSuccess_emitsInitialAndUpdatedUsers() =
@@ -329,13 +345,15 @@ class UserRepositoryImplTest {
coEvery { userApiService.add(USER_BODY) } returns userResponse
coEvery { userApiService.remove(user.id) } returns userResponse
every { domainToBody(user) } returns USER_BODY
- USER_RESPONSES.zip(USERS)
+ USER_RESPONSES
+ .zip(USERS)
.forEach { (r, u) -> every { responseToDomain(r) } returns u.rightNes() }
val events = mutableListOf>>()
- val job = launch(start = CoroutineStart.UNDISPATCHED) {
- repo.getUsers().toList(events)
- }
+ val job =
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ repo.getUsers().toList(events)
+ }
repo.add(user)
repo.remove(user)
delay(120_000)
@@ -347,7 +365,7 @@ class UserRepositoryImplTest {
USERS.dropLast(1),
USERS,
USERS.dropLast(1),
- )
+ ),
)
coVerify { userApiService.getUsers() }
diff --git a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt
index 56586c13..44f0ac50 100644
--- a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt
+++ b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt
@@ -11,15 +11,17 @@ class UserDomainToUserBodyMapperTest {
@Test
fun test_UserDomainToUserBodyMapper() {
- val body = mapper(
- User.create(
- id = "id",
- email = "email@gmail.com",
- firstName = "first",
- lastName = "last",
- avatar = "avatar",
- ).rightValueOrThrow
- )
+ val body =
+ mapper(
+ User
+ .create(
+ id = "id",
+ email = "email@gmail.com",
+ firstName = "first",
+ lastName = "last",
+ avatar = "avatar",
+ ).rightValueOrThrow,
+ )
assertEquals(
UserBody(
@@ -27,7 +29,7 @@ class UserDomainToUserBodyMapperTest {
firstName = "first",
lastName = "last",
),
- body
+ body,
)
}
}
diff --git a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt
index 301fec7b..489308fd 100644
--- a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt
+++ b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt
@@ -23,27 +23,31 @@ import retrofit2.Response
@ExperimentalStdlibApi
class UserErrorMapperTest {
- private val moshi = Moshi
- .Builder()
- .add(KotlinJsonAdapterFactory())
- .build()
+ private val moshi =
+ Moshi
+ .Builder()
+ .add(KotlinJsonAdapterFactory())
+ .build()
private val errorResponseJsonAdapter = moshi.adapter()
private val errorMapper = UserErrorMapper(errorResponseJsonAdapter)
- private fun buildHttpException(error: String, data: Any?) =
- HttpException(
- Response.error(
- 400,
- errorResponseJsonAdapter.toJson(
+ private fun buildHttpException(
+ error: String,
+ data: Any?,
+ ) = HttpException(
+ Response.error(
+ 400,
+ errorResponseJsonAdapter
+ .toJson(
ErrorResponse(
statusCode = 400,
error = error,
message = "error=$error",
data = data,
- )
- ).toResponseBody("application/json".toMediaType())
- )
- )
+ ),
+ ).toResponseBody("application/json".toMediaType()),
+ ),
+ )
@Test
fun test_withUserError_returnsItself() {
@@ -94,8 +98,8 @@ class UserErrorMapperTest {
UserError.Unexpected,
errorMapper(
HttpException(
- Response.success(null)
- )
+ Response.success(null),
+ ),
),
)
@@ -105,9 +109,9 @@ class UserErrorMapperTest {
HttpException(
Response.error(
400,
- "{}".toResponseBody("application/json".toMediaType())
- )
- )
+ "{}".toResponseBody("application/json".toMediaType()),
+ ),
+ ),
),
)
@@ -117,18 +121,20 @@ class UserErrorMapperTest {
buildHttpException(
"hello",
mapOf(
- "1" to mapOf(
- "2" to 3,
- "3" to listOf("4", "5"),
- "6" to "7"
- ),
+ "1" to
+ mapOf(
+ "2" to 3,
+ "3" to listOf("4", "5"),
+ "6" to "7",
+ ),
"2" to null,
- "3" to listOf(
- hashMapOf("1" to "2"),
- hashMapOf("2" to "3"),
- )
+ "3" to
+ listOf(
+ hashMapOf("1" to "2"),
+ hashMapOf("2" to "3"),
+ ),
),
- )
+ ),
),
)
diff --git a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt
index 73f868fd..0ccaf4f3 100644
--- a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt
+++ b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt
@@ -14,39 +14,42 @@ class UserResponseToUserDomainMapperTest {
@Test
fun testUserDomainToUserResponseMapper_withValidResponse_returnsValid() {
- val validated = mapper(
- UserResponse(
- id = "id",
- email = "email@gmail.com",
- firstName = "first",
- lastName = "last",
- avatar = "avatar",
+ val validated =
+ mapper(
+ UserResponse(
+ id = "id",
+ email = "email@gmail.com",
+ firstName = "first",
+ lastName = "last",
+ avatar = "avatar",
+ ),
)
- )
assertTrue(validated.isRight())
assertEquals(
- User.create(
- id = "id",
- email = "email@gmail.com",
- firstName = "first",
- lastName = "last",
- avatar = "avatar",
- ).rightValueOrThrow,
+ User
+ .create(
+ id = "id",
+ email = "email@gmail.com",
+ firstName = "first",
+ lastName = "last",
+ avatar = "avatar",
+ ).rightValueOrThrow,
validated.rightValueOrThrow,
)
}
@Test
fun testUserDomainToUserResponseMapper_withInvalidResponse_returnsInvalid() {
- val validated = mapper(
- UserResponse(
- id = "id",
- email = "email@",
- firstName = "first",
- lastName = "last",
- avatar = "avatar",
+ val validated =
+ mapper(
+ UserResponse(
+ id = "id",
+ email = "email@",
+ firstName = "first",
+ lastName = "last",
+ avatar = "avatar",
+ ),
)
- )
assertTrue(validated.isLeft())
assertEquals(
UserValidationError.INVALID_EMAIL_ADDRESS,
diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts
index ab438cd5..6c023635 100644
--- a/domain/build.gradle.kts
+++ b/domain/build.gradle.kts
@@ -1,5 +1,6 @@
plugins {
kotlin
+ id("org.jetbrains.kotlinx.kover")
}
java {
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/DomainModule.kt b/domain/src/main/java/com/hoc/flowmvi/domain/DomainModule.kt
index d288eabb..e8aa9b27 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/DomainModule.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/DomainModule.kt
@@ -9,14 +9,15 @@ import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module
@JvmField
-val domainModule = module {
- factoryOf(::GetUsersUseCase)
+val domainModule =
+ module {
+ factoryOf(::GetUsersUseCase)
- factoryOf(::RefreshGetUsersUseCase)
+ factoryOf(::RefreshGetUsersUseCase)
- factoryOf(::RemoveUserUseCase)
+ factoryOf(::RemoveUserUseCase)
- factoryOf(::AddUserUseCase)
+ factoryOf(::AddUserUseCase)
- factoryOf(::SearchUsersUseCase)
-}
+ factoryOf(::SearchUsersUseCase)
+ }
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/Email.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/Email.kt
index 3100750d..7ab70246 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/model/Email.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/Email.kt
@@ -3,9 +3,10 @@ package com.hoc.flowmvi.domain.model
import com.hoc.flowmvi.core.EitherNes
@JvmInline
-value class Email private constructor(val value: String) {
+value class Email private constructor(
+ val value: String,
+) {
companion object {
- fun create(value: String?): EitherNes =
- validateEmail(value).map(::Email)
+ fun create(value: String?): EitherNes = validateEmail(value).map(::Email)
}
}
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/FirstName.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/FirstName.kt
index 13368849..b2d0bd71 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/model/FirstName.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/FirstName.kt
@@ -3,9 +3,10 @@ package com.hoc.flowmvi.domain.model
import com.hoc.flowmvi.core.EitherNes
@JvmInline
-value class FirstName private constructor(val value: String) {
+value class FirstName private constructor(
+ val value: String,
+) {
companion object {
- fun create(value: String?): EitherNes =
- validateFirstName(value).map(::FirstName)
+ fun create(value: String?): EitherNes = validateFirstName(value).map(::FirstName)
}
}
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/LastName.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/LastName.kt
index b88224da..5fe82e69 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/model/LastName.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/LastName.kt
@@ -3,9 +3,10 @@ package com.hoc.flowmvi.domain.model
import com.hoc.flowmvi.core.EitherNes
@JvmInline
-value class LastName private constructor(val value: String) {
+value class LastName private constructor(
+ val value: String,
+) {
companion object {
- fun create(value: String?): EitherNes =
- validateLastName(value).map(::LastName)
+ fun create(value: String?): EitherNes = validateLastName(value).map(::LastName)
}
}
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt
index 7a067970..a4b9c366 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt
@@ -19,19 +19,20 @@ data class User(
firstName: String?,
lastName: String?,
avatar: String,
- ): EitherNes = Either.zipOrAccumulateNonEmptySet(
- Email.create(email),
- FirstName.create(firstName),
- LastName.create(lastName)
- ) { e, f, l ->
- User(
- firstName = f,
- email = e,
- lastName = l,
- id = id,
- avatar = avatar
- )
- }
+ ): EitherNes =
+ Either.zipOrAccumulateNonEmptySet(
+ Email.create(email),
+ FirstName.create(firstName),
+ LastName.create(lastName),
+ ) { e, f, l ->
+ User(
+ firstName = f,
+ email = e,
+ lastName = l,
+ id = id,
+ avatar = avatar,
+ )
+ }
}
}
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt
index 26e1493d..d3c075b2 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt
@@ -4,9 +4,18 @@ import com.hoc.flowmvi.core.NonEmptySet
sealed class UserError : Throwable() {
object NetworkError : UserError()
- data class UserNotFound(val id: String) : UserError()
- data class InvalidId(val id: String) : UserError()
- data class ValidationFailed(val errors: NonEmptySet) : UserError()
+
+ data class UserNotFound(
+ val id: String,
+ ) : UserError()
+
+ data class InvalidId(
+ val id: String,
+ ) : UserError()
+
+ data class ValidationFailed(
+ val errors: NonEmptySet,
+ ) : UserError()
object ServerError : UserError()
object Unexpected : UserError()
}
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/UserValidationError.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/UserValidationError.kt
index 1aedce2e..623b6838 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/model/UserValidationError.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/UserValidationError.kt
@@ -8,7 +8,8 @@ import com.hoc.flowmvi.core.leftNes
enum class UserValidationError {
INVALID_EMAIL_ADDRESS,
TOO_SHORT_FIRST_NAME,
- TOO_SHORT_LAST_NAME;
+ TOO_SHORT_LAST_NAME,
+ ;
val asLeftNes: EitherNes = leftNes()
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/AddUserUseCase.kt b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/AddUserUseCase.kt
index 59eeb0e6..18d5dc3b 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/AddUserUseCase.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/AddUserUseCase.kt
@@ -5,6 +5,8 @@ import com.hoc.flowmvi.domain.model.User
import com.hoc.flowmvi.domain.model.UserError
import com.hoc.flowmvi.domain.repository.UserRepository
-class AddUserUseCase(private val userRepository: UserRepository) {
+class AddUserUseCase(
+ private val userRepository: UserRepository,
+) {
suspend operator fun invoke(user: User): Either = userRepository.add(user)
}
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/GetUsersUseCase.kt b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/GetUsersUseCase.kt
index 8fdb9de9..7ac40b8c 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/GetUsersUseCase.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/GetUsersUseCase.kt
@@ -6,6 +6,8 @@ import com.hoc.flowmvi.domain.model.UserError
import com.hoc.flowmvi.domain.repository.UserRepository
import kotlinx.coroutines.flow.Flow
-class GetUsersUseCase(private val userRepository: UserRepository) {
+class GetUsersUseCase(
+ private val userRepository: UserRepository,
+) {
operator fun invoke(): Flow>> = userRepository.getUsers()
}
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RefreshGetUsersUseCase.kt b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RefreshGetUsersUseCase.kt
index ae6e17a5..083ca1b9 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RefreshGetUsersUseCase.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RefreshGetUsersUseCase.kt
@@ -4,6 +4,8 @@ import arrow.core.Either
import com.hoc.flowmvi.domain.model.UserError
import com.hoc.flowmvi.domain.repository.UserRepository
-class RefreshGetUsersUseCase(private val userRepository: UserRepository) {
+class RefreshGetUsersUseCase(
+ private val userRepository: UserRepository,
+) {
suspend operator fun invoke(): Either = userRepository.refresh()
}
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RemoveUserUseCase.kt b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RemoveUserUseCase.kt
index fce13af0..f5e226d3 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RemoveUserUseCase.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/RemoveUserUseCase.kt
@@ -5,6 +5,8 @@ import com.hoc.flowmvi.domain.model.User
import com.hoc.flowmvi.domain.model.UserError
import com.hoc.flowmvi.domain.repository.UserRepository
-class RemoveUserUseCase(private val userRepository: UserRepository) {
+class RemoveUserUseCase(
+ private val userRepository: UserRepository,
+) {
suspend operator fun invoke(user: User): Either = userRepository.remove(user)
}
diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/SearchUsersUseCase.kt b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/SearchUsersUseCase.kt
index fd6713c0..10a64c03 100644
--- a/domain/src/main/java/com/hoc/flowmvi/domain/usecase/SearchUsersUseCase.kt
+++ b/domain/src/main/java/com/hoc/flowmvi/domain/usecase/SearchUsersUseCase.kt
@@ -5,7 +5,8 @@ import com.hoc.flowmvi.domain.model.User
import com.hoc.flowmvi.domain.model.UserError
import com.hoc.flowmvi.domain.repository.UserRepository
-class SearchUsersUseCase(private val userRepository: UserRepository) {
- suspend operator fun invoke(query: String): Either> =
- userRepository.search(query)
+class SearchUsersUseCase(
+ private val userRepository: UserRepository,
+) {
+ suspend operator fun invoke(query: String): Either> = userRepository.search(query)
}
diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt b/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt
index 7b75e88d..2a1a2597 100644
--- a/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt
+++ b/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt
@@ -29,29 +29,30 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Rule
-private val USERS = listOf(
- User.create(
- id = "1",
- email = "email1@gmail.com",
- firstName = "first1",
- lastName = "last1",
- avatar = "1.png"
- ),
- User.create(
- id = "2",
- email = "email1@gmail.com",
- firstName = "first2",
- lastName = "last2",
- avatar = "2.png"
- ),
- User.create(
- id = "3",
- email = "email1@gmail.com",
- firstName = "first3",
- lastName = "last3",
- avatar = "3.png"
- ),
-).map { it.rightValueOrThrow }
+private val USERS =
+ listOf(
+ User.create(
+ id = "1",
+ email = "email1@gmail.com",
+ firstName = "first1",
+ lastName = "last1",
+ avatar = "1.png",
+ ),
+ User.create(
+ id = "2",
+ email = "email1@gmail.com",
+ firstName = "first2",
+ lastName = "last2",
+ avatar = "2.png",
+ ),
+ User.create(
+ id = "3",
+ email = "email1@gmail.com",
+ firstName = "first3",
+ lastName = "last3",
+ avatar = "3.png",
+ ),
+ ).map { it.rightValueOrThrow }
@ExperimentalCoroutinesApi
class UseCaseTest {
@@ -85,105 +86,115 @@ class UseCaseTest {
}
@Test
- fun test_getUsersUseCase_whenSuccess_emitsUsers() = runTest {
- val usersRight = USERS.right()
- every { userRepository.getUsers() } returns flowOf(usersRight)
+ fun test_getUsersUseCase_whenSuccess_emitsUsers() =
+ runTest {
+ val usersRight = USERS.right()
+ every { userRepository.getUsers() } returns flowOf(usersRight)
- val result = getUsersUseCase()
+ val result = getUsersUseCase()
- verify { userRepository.getUsers() }
- assertEquals(usersRight, result.first())
- }
+ verify { userRepository.getUsers() }
+ assertEquals(usersRight, result.first())
+ }
@Test
- fun test_getUsersUseCase_whenError_throwsError() = runTest {
- every { userRepository.getUsers() } returns flowOf(errorLeft)
+ fun test_getUsersUseCase_whenError_throwsError() =
+ runTest {
+ every { userRepository.getUsers() } returns flowOf(errorLeft)
- val result = getUsersUseCase()
+ val result = getUsersUseCase()
- verify { userRepository.getUsers() }
- assertEquals(errorLeft, result.first())
- }
+ verify { userRepository.getUsers() }
+ assertEquals(errorLeft, result.first())
+ }
@Test
- fun test_refreshUseCase_whenSuccess_returnsUnit() = runTest {
- coEvery { userRepository.refresh() } returns Unit.right()
+ fun test_refreshUseCase_whenSuccess_returnsUnit() =
+ runTest {
+ coEvery { userRepository.refresh() } returns Unit.right()
- val result = refreshUseCase()
+ val result = refreshUseCase()
- coVerify { userRepository.refresh() }
- assertEquals(Unit.right(), result)
- }
+ coVerify { userRepository.refresh() }
+ assertEquals(Unit.right(), result)
+ }
@Test
- fun test_refreshUseCase_whenError_throwsError() = runTest {
- coEvery { userRepository.refresh() } returns errorLeft
+ fun test_refreshUseCase_whenError_throwsError() =
+ runTest {
+ coEvery { userRepository.refresh() } returns errorLeft
- val result = refreshUseCase()
+ val result = refreshUseCase()
- coVerify { userRepository.refresh() }
- assertEquals(errorLeft, result)
- }
+ coVerify { userRepository.refresh() }
+ assertEquals(errorLeft, result)
+ }
@Test
- fun test_removeUserUseCase_whenSuccess_returnsUnit() = runTest {
- coEvery { userRepository.remove(any()) } returns Unit.right()
+ fun test_removeUserUseCase_whenSuccess_returnsUnit() =
+ runTest {
+ coEvery { userRepository.remove(any()) } returns Unit.right()
- val result = removeUserUseCase(USERS[0])
+ val result = removeUserUseCase(USERS[0])
- coVerify { userRepository.remove(USERS[0]) }
- assertEquals(Unit.right(), result)
- }
+ coVerify { userRepository.remove(USERS[0]) }
+ assertEquals(Unit.right(), result)
+ }
@Test
- fun test_removeUserUseCase_whenError_throwsError() = runTest {
- coEvery { userRepository.remove(any()) } returns errorLeft
+ fun test_removeUserUseCase_whenError_throwsError() =
+ runTest {
+ coEvery { userRepository.remove(any()) } returns errorLeft
- val result = removeUserUseCase(USERS[0])
+ val result = removeUserUseCase(USERS[0])
- coVerify { userRepository.remove(USERS[0]) }
- assertEquals(errorLeft, result)
- }
+ coVerify { userRepository.remove(USERS[0]) }
+ assertEquals(errorLeft, result)
+ }
@Test
- fun test_addUserUseCase_whenSuccess_returnsUnit() = runTest {
- coEvery { userRepository.add(any()) } returns Unit.right()
+ fun test_addUserUseCase_whenSuccess_returnsUnit() =
+ runTest {
+ coEvery { userRepository.add(any()) } returns Unit.right()
- val result = addUserUseCase(USERS[0])
+ val result = addUserUseCase(USERS[0])
- coVerify { userRepository.add(USERS[0]) }
- assertEquals(Unit.right(), result)
- }
+ coVerify { userRepository.add(USERS[0]) }
+ assertEquals(Unit.right(), result)
+ }
@Test
- fun test_addUserUseCase_whenError_throwsError() = runTest {
- coEvery { userRepository.add(any()) } returns errorLeft
+ fun test_addUserUseCase_whenError_throwsError() =
+ runTest {
+ coEvery { userRepository.add(any()) } returns errorLeft
- val result = addUserUseCase(USERS[0])
+ val result = addUserUseCase(USERS[0])
- coVerify { userRepository.add(USERS[0]) }
- assertEquals(errorLeft, result)
- }
+ coVerify { userRepository.add(USERS[0]) }
+ assertEquals(errorLeft, result)
+ }
@Test
- fun test_searchUsersUseCase_whenSuccess_returnsUsers() = runTest {
- coEvery { userRepository.search(any()) } returns USERS.right()
+ fun test_searchUsersUseCase_whenSuccess_returnsUsers() =
+ runTest {
+ coEvery { userRepository.search(any()) } returns USERS.right()
- val query = "hoc081098"
- val result = searchUsersUseCase(query)
+ val query = "hoc081098"
+ val result = searchUsersUseCase(query)
- coVerify { userRepository.search(query) }
- assertEquals(USERS.right(), result)
- }
+ coVerify { userRepository.search(query) }
+ assertEquals(USERS.right(), result)
+ }
@Test
- fun test_searchUsersUseCase_whenError_throwsError() = runTest {
- coEvery { userRepository.search(any()) } returns errorLeft
+ fun test_searchUsersUseCase_whenError_throwsError() =
+ runTest {
+ coEvery { userRepository.search(any()) } returns errorLeft
- val query = "hoc081098"
- val result = searchUsersUseCase(query)
+ val query = "hoc081098"
+ val result = searchUsersUseCase(query)
- coVerify { userRepository.search(query) }
- assertEquals(errorLeft, result)
- }
+ coVerify { userRepository.search(query) }
+ assertEquals(errorLeft, result)
+ }
}
diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt b/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt
index 348ac36f..041df420 100644
--- a/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt
+++ b/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt
@@ -16,13 +16,14 @@ private const val AVATAR = "avatar"
class UserTest {
@Test
fun testCreateUser_withValidValues_returnsValid() {
- val validated = User.create(
- id = ID,
- email = VALID_EMAIL,
- firstName = VALID_NAME,
- lastName = VALID_NAME,
- avatar = AVATAR,
- )
+ val validated =
+ User.create(
+ id = ID,
+ email = VALID_EMAIL,
+ firstName = VALID_NAME,
+ lastName = VALID_NAME,
+ avatar = AVATAR,
+ )
assertTrue(validated.isRight())
validated.rightValueOrThrow.let { user ->
assertEquals(ID, user.id)
@@ -35,113 +36,120 @@ class UserTest {
@Test
fun testCreateUser_withInvalidEmail_returnsInvalid() {
- val validated = User.create(
- id = ID,
- email = "invalid email",
- firstName = VALID_NAME,
- lastName = VALID_NAME,
- avatar = AVATAR,
- )
+ val validated =
+ User.create(
+ id = ID,
+ email = "invalid email",
+ firstName = VALID_NAME,
+ lastName = VALID_NAME,
+ avatar = AVATAR,
+ )
assertTrue(validated.isLeft())
assertEquals(UserValidationError.INVALID_EMAIL_ADDRESS, validated.leftValueOrThrow.single())
}
@Test
fun testCreateUser_withInvalidFirstName_returnsInvalid() {
- val validated = User.create(
- id = ID,
- email = VALID_EMAIL,
- firstName = "h",
- lastName = VALID_NAME,
- avatar = AVATAR,
- )
+ val validated =
+ User.create(
+ id = ID,
+ email = VALID_EMAIL,
+ firstName = "h",
+ lastName = VALID_NAME,
+ avatar = AVATAR,
+ )
assertTrue(validated.isLeft())
assertEquals(UserValidationError.TOO_SHORT_FIRST_NAME, validated.leftValueOrThrow.single())
}
@Test
fun testCreateUser_withInvalidLastName_returnsInvalid() {
- val validated = User.create(
- id = ID,
- email = VALID_EMAIL,
- firstName = VALID_NAME,
- lastName = "h",
- avatar = AVATAR,
- )
+ val validated =
+ User.create(
+ id = ID,
+ email = VALID_EMAIL,
+ firstName = VALID_NAME,
+ lastName = "h",
+ avatar = AVATAR,
+ )
assertTrue(validated.isLeft())
assertEquals(UserValidationError.TOO_SHORT_LAST_NAME, validated.leftValueOrThrow.single())
}
@Test
fun testCreateUser_withInvalidEmailAndFirstName_returnsInvalid() {
- val validated = User.create(
- id = ID,
- email = "h",
- firstName = "h",
- lastName = VALID_NAME,
- avatar = AVATAR,
- )
+ val validated =
+ User.create(
+ id = ID,
+ email = "h",
+ firstName = "h",
+ lastName = VALID_NAME,
+ avatar = AVATAR,
+ )
assertTrue(validated.isLeft())
assertEquals(
setOf(
UserValidationError.INVALID_EMAIL_ADDRESS,
UserValidationError.TOO_SHORT_FIRST_NAME,
),
- validated.leftValueOrThrow.toSet()
+ validated.leftValueOrThrow.toSet(),
)
}
@Test
fun testCreateUser_withInvalidEmailAndLastName_returnsInvalid() {
- val validated = User.create(
- id = ID,
- email = "h",
- firstName = VALID_NAME,
- lastName = "h",
- avatar = AVATAR,
- )
+ val validated =
+ User.create(
+ id = ID,
+ email = "h",
+ firstName = VALID_NAME,
+ lastName = "h",
+ avatar = AVATAR,
+ )
assertTrue(validated.isLeft())
assertEquals(
setOf(
UserValidationError.INVALID_EMAIL_ADDRESS,
UserValidationError.TOO_SHORT_LAST_NAME,
),
- validated.leftValueOrThrow.toSet()
+ validated.leftValueOrThrow.toSet(),
)
}
@Test
fun testCreateUser_withInvalidFirstNameAndLastName_returnsInvalid() {
- val validated = User.create(
- id = ID,
- email = VALID_EMAIL,
- firstName = "h",
- lastName = "h",
- avatar = AVATAR,
- )
+ val validated =
+ User.create(
+ id = ID,
+ email = VALID_EMAIL,
+ firstName = "h",
+ lastName = "h",
+ avatar = AVATAR,
+ )
assertTrue(validated.isLeft())
assertEquals(
setOf(
UserValidationError.TOO_SHORT_FIRST_NAME,
UserValidationError.TOO_SHORT_LAST_NAME,
),
- validated.leftValueOrThrow.toSet()
+ validated.leftValueOrThrow.toSet(),
)
}
@Test
fun testCreateUser_withInvalidValues_returnsInvalid() {
- val validated = User.create(
- id = ID,
- email = "h",
- firstName = "h",
- lastName = "h",
- avatar = AVATAR,
- )
+ val validated =
+ User.create(
+ id = ID,
+ email = "h",
+ firstName = "h",
+ lastName = "h",
+ avatar = AVATAR,
+ )
assertTrue(validated.isLeft())
assertEquals(
UserValidationError.values().toSet(),
- validated.leftValueOrThrow.toSet()
+ validated.leftValueOrThrow.toSet(),
)
}
}
diff --git a/feature-add/build.gradle.kts b/feature-add/build.gradle.kts
index 4a8b504a..98319da3 100644
--- a/feature-add/build.gradle.kts
+++ b/feature-add/build.gradle.kts
@@ -2,6 +2,7 @@ plugins {
androidLib
kotlinAndroid
kotlinParcelize
+ id("org.jetbrains.kotlinx.kover")
}
android {
@@ -11,7 +12,6 @@ android {
defaultConfig {
minSdk = appConfig.minSdkVersion
- targetSdk = appConfig.targetSdkVersion
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
@@ -22,7 +22,7 @@ android {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
+ "proguard-rules.pro",
)
}
}
diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt
index 166c1ad5..eb6f2c6f 100644
--- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt
+++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt
@@ -21,21 +21,19 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
-import org.koin.androidx.viewmodel.ext.android.stateViewModel
+import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
@ExperimentalCoroutinesApi
-class AddActivity :
- AbstractMviActivity(R.layout.activity_add) {
- override val vm by stateViewModel()
+class AddActivity : AbstractMviActivity(R.layout.activity_add) {
+ override val vm by viewModel()
private val addBinding by viewBinding()
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
+ override fun onOptionsItemSelected(item: MenuItem): Boolean =
+ when (item.itemId) {
android.R.id.home -> true.also { finish() }
else -> super.onOptionsItemSelected(item)
}
- }
override fun handleSingleEvent(event: SingleEvent) {
Timber.d("Event=$event")
@@ -55,16 +53,25 @@ class AddActivity :
Timber.d("viewState=$viewState")
addBinding.emailEditText.setErrorIfChanged(viewState.emailChanged) {
- if (UserValidationError.INVALID_EMAIL_ADDRESS in viewState.errors) "Invalid email"
- else null
+ if (UserValidationError.INVALID_EMAIL_ADDRESS in viewState.errors) {
+ "Invalid email"
+ } else {
+ null
+ }
}
addBinding.firstNameEditText.setErrorIfChanged(viewState.firstNameChanged) {
- if (UserValidationError.TOO_SHORT_FIRST_NAME in viewState.errors) "Too short first name"
- else null
+ if (UserValidationError.TOO_SHORT_FIRST_NAME in viewState.errors) {
+ "Too short first name"
+ } else {
+ null
+ }
}
addBinding.lastNameEditText.setErrorIfChanged(viewState.lastNameChanged) {
- if (UserValidationError.TOO_SHORT_LAST_NAME in viewState.errors) "Too short last name"
- else null
+ if (UserValidationError.TOO_SHORT_LAST_NAME in viewState.errors) {
+ "Too short last name"
+ } else {
+ null
+ }
}
TransitionManager.endTransitions(addBinding.root)
@@ -73,7 +80,7 @@ class AddActivity :
AutoTransition()
.addTarget(addBinding.progressBar)
.addTarget(addBinding.addButton)
- .setDuration(200)
+ .setDuration(200),
)
addBinding.progressBar.isInvisible = !viewState.isLoading
addBinding.addButton.isInvisible = viewState.isLoading
@@ -91,41 +98,41 @@ class AddActivity :
}
}
- override fun viewIntents(): Flow = addBinding.run {
- merge(
- emailEditText
- .editText!!
- .textChanges()
- .map { ViewIntent.EmailChanged(it?.toString().orEmpty()) },
- firstNameEditText
- .editText!!
- .textChanges()
- .map { ViewIntent.FirstNameChanged(it?.toString().orEmpty()) },
- lastNameEditText
- .editText!!
- .textChanges()
- .map { ViewIntent.LastNameChanged(it?.toString().orEmpty()) },
- addButton
- .clicks()
- .map { ViewIntent.Submit },
- emailEditText
- .editText!!
- .firstChange()
- .mapTo(ViewIntent.EmailChangedFirstTime),
- firstNameEditText
- .editText!!
- .firstChange()
- .mapTo(ViewIntent.FirstNameChangedFirstTime),
- lastNameEditText
- .editText!!
- .firstChange()
- .mapTo(ViewIntent.LastNameChangedFirstTime),
- )
- }
+ override fun viewIntents(): Flow =
+ addBinding.run {
+ merge(
+ emailEditText
+ .editText!!
+ .textChanges()
+ .map { ViewIntent.EmailChanged(it?.toString().orEmpty()) },
+ firstNameEditText
+ .editText!!
+ .textChanges()
+ .map { ViewIntent.FirstNameChanged(it?.toString().orEmpty()) },
+ lastNameEditText
+ .editText!!
+ .textChanges()
+ .map { ViewIntent.LastNameChanged(it?.toString().orEmpty()) },
+ addButton
+ .clicks()
+ .map { ViewIntent.Submit },
+ emailEditText
+ .editText!!
+ .firstChange()
+ .mapTo(ViewIntent.EmailChangedFirstTime),
+ firstNameEditText
+ .editText!!
+ .firstChange()
+ .mapTo(ViewIntent.FirstNameChangedFirstTime),
+ lastNameEditText
+ .editText!!
+ .firstChange()
+ .mapTo(ViewIntent.LastNameChangedFirstTime),
+ )
+ }
internal class IntentProvider : IntentProviders.Add {
- override fun makeIntent(context: Context): Intent =
- Intent(context, AddActivity::class.java)
+ override fun makeIntent(context: Context): Intent = Intent(context, AddActivity::class.java)
}
}
diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt
index 49ec7c0a..17a36169 100644
--- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt
+++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt
@@ -5,6 +5,7 @@ import android.os.Parcelable
import androidx.core.os.bundleOf
import arrow.core.identity
import com.hoc.flowmvi.core.EitherNes
+import com.hoc.flowmvi.core_ui.parcelable
import com.hoc.flowmvi.domain.model.User
import com.hoc.flowmvi.domain.model.UserError
import com.hoc.flowmvi.domain.model.UserValidationError
@@ -26,36 +27,47 @@ data class ViewState(
val email: String,
val firstName: String,
val lastName: String,
-) : MviViewState, Parcelable {
+) : MviViewState,
+ Parcelable {
companion object {
private const val VIEW_STATE_KEY = "com.hoc.flowmvi.ui.add.StateSaver"
- fun initial() = ViewState(
- errors = UserValidationError.VALUES_SET,
- isLoading = false,
- emailChanged = false,
- firstNameChanged = false,
- lastNameChanged = false,
- email = "",
- firstName = "",
- lastName = "",
- )
+ fun initial() =
+ ViewState(
+ errors = UserValidationError.VALUES_SET,
+ isLoading = false,
+ emailChanged = false,
+ firstNameChanged = false,
+ lastNameChanged = false,
+ email = "",
+ firstName = "",
+ lastName = "",
+ )
}
class StateSaver : MviViewStateSaver {
override fun ViewState.toBundle() = bundleOf(VIEW_STATE_KEY to this)
- override fun restore(bundle: Bundle?) = bundle
- ?.getParcelable(VIEW_STATE_KEY)
- ?.copy(isLoading = false)
- ?: initial()
+ override fun restore(bundle: Bundle?) =
+ bundle
+ ?.parcelable(VIEW_STATE_KEY)
+ ?.copy(isLoading = false)
+ ?: initial()
}
}
sealed interface ViewIntent : MviIntent {
- data class EmailChanged(val email: String) : ViewIntent
- data class FirstNameChanged(val firstName: String) : ViewIntent
- data class LastNameChanged(val lastName: String) : ViewIntent
+ data class EmailChanged(
+ val email: String,
+ ) : ViewIntent
+
+ data class FirstNameChanged(
+ val firstName: String,
+ ) : ViewIntent
+
+ data class LastNameChanged(
+ val lastName: String,
+ ) : ViewIntent
object Submit : ViewIntent
@@ -73,29 +85,37 @@ internal sealed interface PartialStateChange {
val lastName: String,
val userEitherNes: EitherNes,
) : PartialStateChange {
- override fun reduce(viewState: ViewState): ViewState = viewState.copy(
- email = email,
- firstName = firstName,
- lastName = lastName,
- errors = userEitherNes.fold(
- ifLeft = ::identity,
- ifRight = { emptySet() },
- ),
- )
+ override fun reduce(viewState: ViewState): ViewState =
+ viewState.copy(
+ email = email,
+ firstName = firstName,
+ lastName = lastName,
+ errors =
+ userEitherNes.fold(
+ ifLeft = ::identity,
+ ifRight = { emptySet() },
+ ),
+ )
}
sealed interface AddUser : PartialStateChange {
object Loading : AddUser
- data class AddUserSuccess(val user: User) : AddUser
- data class AddUserFailure(val user: User, val error: UserError) : AddUser
- override fun reduce(viewState: ViewState): ViewState {
- return when (this) {
+ data class AddUserSuccess(
+ val user: User,
+ ) : AddUser
+
+ data class AddUserFailure(
+ val user: User,
+ val error: UserError,
+ ) : AddUser
+
+ override fun reduce(viewState: ViewState): ViewState =
+ when (this) {
Loading -> viewState.copy(isLoading = true)
is AddUserSuccess -> viewState.copy(isLoading = false)
is AddUserFailure -> viewState.copy(isLoading = false)
}
- }
}
sealed interface FirstChange : PartialStateChange {
@@ -103,26 +123,40 @@ internal sealed interface PartialStateChange {
object FirstNameChangedFirstTime : FirstChange
object LastNameChangedFirstTime : FirstChange
- override fun reduce(viewState: ViewState): ViewState {
- return when (this) {
+ override fun reduce(viewState: ViewState): ViewState =
+ when (this) {
EmailChangedFirstTime -> {
- if (viewState.emailChanged) viewState
- else viewState.copy(emailChanged = true)
+ if (viewState.emailChanged) {
+ viewState
+ } else {
+ viewState.copy(emailChanged = true)
+ }
}
FirstNameChangedFirstTime -> {
- if (viewState.firstNameChanged) viewState
- else viewState.copy(firstNameChanged = true)
+ if (viewState.firstNameChanged) {
+ viewState
+ } else {
+ viewState.copy(firstNameChanged = true)
+ }
}
LastNameChangedFirstTime -> {
- if (viewState.lastNameChanged) viewState
- else viewState.copy(lastNameChanged = true)
+ if (viewState.lastNameChanged) {
+ viewState
+ } else {
+ viewState.copy(lastNameChanged = true)
+ }
}
}
- }
}
}
sealed interface SingleEvent : MviSingleEvent {
- data class AddUserSuccess(val user: User) : SingleEvent
- data class AddUserFailure(val user: User, val error: UserError) : SingleEvent
+ data class AddUserSuccess(
+ val user: User,
+ ) : SingleEvent
+
+ data class AddUserFailure(
+ val user: User,
+ val error: UserError,
+ ) : SingleEvent
}
diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddModule.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddModule.kt
index 9f4c0ccc..63969f9c 100644
--- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddModule.kt
+++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddModule.kt
@@ -10,10 +10,11 @@ import org.koin.dsl.module
@JvmField
@ExperimentalCoroutinesApi
-val addModule = module {
- viewModelOf(::AddVM)
+val addModule =
+ module {
+ viewModelOf(::AddVM)
- singleOf(AddActivity::IntentProvider) { bind() }
+ singleOf(AddActivity::IntentProvider) { bind() }
- factoryOf(ViewState::StateSaver)
-}
+ factoryOf(ViewState::StateSaver)
+ }
diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt
index 21d0eb24..f81552be 100644
--- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt
+++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt
@@ -33,7 +33,6 @@ class AddVM(
savedStateHandle: SavedStateHandle,
stateSaver: ViewState.StateSaver,
) : AbstractMviViewModel() {
-
override val rawLogTag get() = "AddVM[${System.identityHashCode(this)}]"
override val viewState: StateFlow
@@ -42,14 +41,15 @@ class AddVM(
val initialVS = stateSaver.restore(savedStateHandle[VIEW_STATE_BUNDLE_KEY])
Timber.tag(logTag).d("initialVS=$initialVS")
- viewState = intentSharedFlow
- .debugLog("ViewIntent")
- .toPartialStateChangeFlow(initialVS)
- .debugLog("PartialStateChange")
- .onEach { sendEvent(it.toSingleEventOrNull() ?: return@onEach) }
- .scan(initialVS) { state, change -> change.reduce(state) }
- .debugLog("ViewState")
- .stateIn(viewModelScope, SharingStarted.Eagerly, initialVS)
+ viewState =
+ intentSharedFlow
+ .debugLog("ViewIntent")
+ .toPartialStateChangeFlow(initialVS)
+ .debugLog("PartialStateChange")
+ .onEach { sendEvent(it.toSingleEventOrNull() ?: return@onEach) }
+ .scan(initialVS) { state, change -> change.reduce(state) }
+ .debugLog("ViewState")
+ .stateIn(viewModelScope, SharingStarted.Eagerly, initialVS)
savedStateHandle.setSavedStateProvider(VIEW_STATE_BUNDLE_KEY) {
stateSaver.run { viewState.value.toBundle() }
@@ -57,39 +57,44 @@ class AddVM(
}
private fun SharedFlow.toPartialStateChangeFlow(initialVS: ViewState): Flow {
- val emailFlow = filterIsInstance()
- .map { it.email }
- .startWith(initialVS.email)
- .distinctUntilChanged()
-
- val firstNameFlow = filterIsInstance()
- .map { it.firstName }
- .startWith(initialVS.firstName)
- .distinctUntilChanged()
-
- val lastNameFlow = filterIsInstance()
- .map { it.lastName }
- .startWith(initialVS.lastName)
- .distinctUntilChanged()
-
- val userFormStateFlow = combine(
- emailFlow,
- firstNameFlow,
- lastNameFlow,
- ) { email, firstName, lastName ->
- PartialStateChange.UserFormState(
- email = email,
- firstName = firstName,
- lastName = lastName,
- userEitherNes = User.create(
+ val emailFlow =
+ filterIsInstance()
+ .map { it.email }
+ .startWith(initialVS.email)
+ .distinctUntilChanged()
+
+ val firstNameFlow =
+ filterIsInstance()
+ .map { it.firstName }
+ .startWith(initialVS.firstName)
+ .distinctUntilChanged()
+
+ val lastNameFlow =
+ filterIsInstance()
+ .map { it.lastName }
+ .startWith(initialVS.lastName)
+ .distinctUntilChanged()
+
+ val userFormStateFlow =
+ combine(
+ emailFlow,
+ firstNameFlow,
+ lastNameFlow,
+ ) { email, firstName, lastName ->
+ PartialStateChange.UserFormState(
email = email,
firstName = firstName,
lastName = lastName,
- id = "",
- avatar = "",
- ),
- )
- }.shareWhileSubscribed()
+ userEitherNes =
+ User.create(
+ email = email,
+ firstName = firstName,
+ lastName = lastName,
+ id = "",
+ avatar = "",
+ ),
+ )
+ }.shareWhileSubscribed()
return merge(
// user form state change
@@ -113,10 +118,12 @@ class AddVM(
.mapTo(PartialStateChange.FirstChange.FirstNameChangedFirstTime),
filterIsInstance()
.take(1)
- .mapTo(PartialStateChange.FirstChange.LastNameChangedFirstTime)
+ .mapTo(PartialStateChange.FirstChange.LastNameChangedFirstTime),
)
- private fun Flow.toAddUserChangeFlow(userFormFlow: SharedFlow): Flow =
+ private fun Flow.toAddUserChangeFlow(
+ userFormFlow: SharedFlow,
+ ): Flow =
withLatestFrom(userFormFlow) { _, userForm -> userForm.userEitherNes }
.debugLog("toAddUserChangeFlow::userValidatedNel")
.mapNotNull { it.getOrNull() }
@@ -125,28 +132,29 @@ class AddVM(
.map { result ->
result.fold(
ifLeft = { PartialStateChange.AddUser.AddUserFailure(user, it) },
- ifRight = { PartialStateChange.AddUser.AddUserSuccess(user) }
+ ifRight = { PartialStateChange.AddUser.AddUserSuccess(user) },
)
- }
- .startWith(PartialStateChange.AddUser.Loading)
+ }.startWith(PartialStateChange.AddUser.Loading)
}
//endregion
private companion object {
private const val VIEW_STATE_BUNDLE_KEY = "com.hoc.flowmvi.ui.add.view_state"
- private fun PartialStateChange.toSingleEventOrNull(): SingleEvent? = when (this) {
- is PartialStateChange.AddUser.AddUserSuccess -> SingleEvent.AddUserSuccess(user)
- is PartialStateChange.AddUser.AddUserFailure -> SingleEvent.AddUserFailure(
- user = user,
- error = error
- )
- PartialStateChange.FirstChange.EmailChangedFirstTime,
- PartialStateChange.FirstChange.FirstNameChangedFirstTime,
- PartialStateChange.FirstChange.LastNameChangedFirstTime,
- is PartialStateChange.UserFormState,
- PartialStateChange.AddUser.Loading,
- -> null
- }
+ private fun PartialStateChange.toSingleEventOrNull(): SingleEvent? =
+ when (this) {
+ is PartialStateChange.AddUser.AddUserSuccess -> SingleEvent.AddUserSuccess(user)
+ is PartialStateChange.AddUser.AddUserFailure ->
+ SingleEvent.AddUserFailure(
+ user = user,
+ error = error,
+ )
+ PartialStateChange.FirstChange.EmailChangedFirstTime,
+ PartialStateChange.FirstChange.FirstNameChangedFirstTime,
+ PartialStateChange.FirstChange.LastNameChangedFirstTime,
+ is PartialStateChange.UserFormState,
+ PartialStateChange.AddUser.Loading,
+ -> null
+ }
}
}
diff --git a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt
index 9e8bf1b0..c5315e19 100644
--- a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt
+++ b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt
@@ -39,11 +39,12 @@ class AddVMTest : BaseMviViewModelTest(R.layout.activity_main) {
+class MainActivity : AbstractMviActivity(R.layout.activity_main) {
override val vm by viewModel()
private val navigator by inject()
@@ -41,8 +40,8 @@ class MainActivity :
private val removeChannel = Channel(Channel.BUFFERED)
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
+ override fun onOptionsItemSelected(item: MenuItem): Boolean =
+ when (item.itemId) {
R.id.add_action -> {
navigator.run { navigateToAdd() }
true
@@ -53,10 +52,8 @@ class MainActivity :
}
else -> super.onOptionsItemSelected(item)
}
- }
- override fun onCreateOptionsMenu(menu: Menu) =
- menuInflater.inflate(R.menu.menu_main, menu).let { true }
+ override fun onCreateOptionsMenu(menu: Menu) = menuInflater.inflate(R.menu.menu_main, menu).let { true }
override fun setupViews() {
mainBinding.usersRecycler.run {
@@ -69,24 +66,25 @@ class MainActivity :
dividerInsetEnd = dpToPx(8f)
isLastItemDecorated = false
dividerThickness = dpToPx(0.8f)
- }
+ },
)
ItemTouchHelper(
SwipeLeftToDeleteCallback(context) cb@{ position ->
val userItem = vm.viewState.value.userItems[position]
removeChannel.trySend(userItem)
- }
+ },
).attachToRecyclerView(this)
}
}
- override fun viewIntents(): Flow = merge(
- flowOf(ViewIntent.Initial),
- mainBinding.swipeRefreshLayout.refreshes().map { ViewIntent.Refresh },
- mainBinding.retryButton.clicks().map { ViewIntent.Retry },
- removeChannel.consumeAsFlow().map { ViewIntent.RemoveUser(it) }
- )
+ override fun viewIntents(): Flow =
+ merge(
+ flowOf(ViewIntent.Initial),
+ mainBinding.swipeRefreshLayout.refreshes().map { ViewIntent.Refresh },
+ mainBinding.retryButton.clicks().map { ViewIntent.Retry },
+ removeChannel.consumeAsFlow().map { ViewIntent.RemoveUser(it) },
+ )
override fun handleSingleEvent(event: SingleEvent) {
Timber.d("handleSingleEvent $event")
@@ -109,16 +107,17 @@ class MainActivity :
mainBinding.run {
errorGroup.isVisible = viewState.error !== null
- errorMessageTextView.text = viewState.error?.let {
- when (it) {
- is UserError.InvalidId -> "Invalid id"
- UserError.NetworkError -> "Network error"
- UserError.ServerError -> "Server error"
- UserError.Unexpected -> "Unexpected error"
- is UserError.UserNotFound -> "User not found"
- is UserError.ValidationFailed -> "Validation failed"
+ errorMessageTextView.text =
+ viewState.error?.let {
+ when (it) {
+ is UserError.InvalidId -> "Invalid id"
+ UserError.NetworkError -> "Network error"
+ UserError.ServerError -> "Server error"
+ UserError.Unexpected -> "Unexpected error"
+ is UserError.UserNotFound -> "User not found"
+ is UserError.ValidationFailed -> "Validation failed"
+ }
}
- }
progressBar.isVisible = viewState.isLoading
diff --git a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt
index b6ed064c..9c8792e7 100644
--- a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt
+++ b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainContract.kt
@@ -14,7 +14,7 @@ data class UserItem(
val email: String,
val avatar: String,
val firstName: String,
- val lastName: String
+ val lastName: String,
) {
@get:MainThread
val fullName by lazy(NONE) { "$firstName $lastName" }
@@ -24,40 +24,46 @@ data class UserItem(
email = domain.email.value,
avatar = domain.avatar,
firstName = domain.firstName.value,
- lastName = domain.lastName.value
+ lastName = domain.lastName.value,
)
- fun toDomain(): Either = User.create(
- id = id,
- lastName = lastName,
- firstName = firstName,
- avatar = avatar,
- email = email
- ).mapLeft { UserError.ValidationFailed(it) }
+ fun toDomain(): Either =
+ User
+ .create(
+ id = id,
+ lastName = lastName,
+ firstName = firstName,
+ avatar = avatar,
+ email = email,
+ ).mapLeft { UserError.ValidationFailed(it) }
}
sealed interface ViewIntent : MviIntent {
object Initial : ViewIntent
object Refresh : ViewIntent
object Retry : ViewIntent
- data class RemoveUser(val user: UserItem) : ViewIntent
+
+ data class RemoveUser(
+ val user: UserItem,
+ ) : ViewIntent
}
data class ViewState(
val userItems: List,
val isLoading: Boolean,
val error: UserError?,
- val isRefreshing: Boolean
+ val isRefreshing: Boolean,
) : MviViewState {
inline val canRefresh get() = !isLoading && error === null
companion object {
- fun initial() = ViewState(
- userItems = emptyList(),
- isLoading = true,
- error = null,
- isRefreshing = false
- )
+ fun initial() =
+ ViewState(
+ userItems = emptyList(),
+ isLoading = true,
+ error = null,
+ isRefreshing = false,
+ )
}
}
@@ -65,46 +71,62 @@ internal sealed interface PartialStateChange {
fun reduce(viewState: ViewState): ViewState
sealed interface Users : PartialStateChange {
- override fun reduce(viewState: ViewState): ViewState {
- return when (this) {
- Loading -> viewState.copy(
- isLoading = true,
- error = null
- )
- is Data -> viewState.copy(
- isLoading = false,
- error = null,
- userItems = users
- )
- is Error -> viewState.copy(
- isLoading = false,
- error = error
- )
+ override fun reduce(viewState: ViewState): ViewState =
+ when (this) {
+ Loading ->
+ viewState.copy(
+ isLoading = true,
+ error = null,
+ )
+ is Data ->
+ viewState.copy(
+ isLoading = false,
+ error = null,
+ userItems = users,
+ )
+ is Error ->
+ viewState.copy(
+ isLoading = false,
+ error = error,
+ )
}
- }
object Loading : Users
- data class Data(val users: List) : Users
- data class Error(val error: UserError) : Users
+
+ data class Data(
+ val users: List,
+ ) : Users
+
+ data class Error(
+ val error: UserError,
+ ) : Users
}
sealed interface Refresh : PartialStateChange {
- override fun reduce(viewState: ViewState): ViewState {
- return when (this) {
+ override fun reduce(viewState: ViewState): ViewState =
+ when (this) {
is Success -> viewState.copy(isRefreshing = false)
is Failure -> viewState.copy(isRefreshing = false)
Loading -> viewState.copy(isRefreshing = true)
}
- }
object Loading : Refresh
object Success : Refresh
- data class Failure(val error: UserError) : Refresh
+
+ data class Failure(
+ val error: UserError,
+ ) : Refresh
}
sealed interface RemoveUser : PartialStateChange {
- data class Success(val user: UserItem) : RemoveUser
- data class Failure(val user: UserItem, val error: UserError) : RemoveUser
+ data class Success(
+ val user: UserItem,
+ ) : RemoveUser
+
+ data class Failure(
+ val user: UserItem,
+ val error: UserError,
+ ) : RemoveUser
override fun reduce(viewState: ViewState) = viewState
}
@@ -113,13 +135,21 @@ internal sealed interface PartialStateChange {
sealed interface SingleEvent : MviSingleEvent {
sealed interface Refresh : SingleEvent {
object Success : Refresh
- data class Failure(val error: UserError) : Refresh
+
+ data class Failure(
+ val error: UserError,
+ ) : Refresh
}
- data class GetUsersError(val error: UserError) : SingleEvent
+ data class GetUsersError(
+ val error: UserError,
+ ) : SingleEvent
sealed interface RemoveUser : SingleEvent {
- data class Success(val user: UserItem) : RemoveUser
+ data class Success(
+ val user: UserItem,
+ ) : RemoveUser
+
data class Failure(
val user: UserItem,
val error: UserError,
diff --git a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainModule.kt b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainModule.kt
index fbf88155..75440f84 100644
--- a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainModule.kt
+++ b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainModule.kt
@@ -8,6 +8,7 @@ import org.koin.dsl.module
@JvmField
@ExperimentalCoroutinesApi
@FlowPreview
-val mainModule = module {
- viewModelOf(::MainVM)
-}
+val mainModule =
+ module {
+ viewModelOf(::MainVM)
+ }
diff --git a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt
index 635ab10d..660f3944 100644
--- a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt
+++ b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt
@@ -37,7 +37,6 @@ class MainVM(
private val refreshGetUsers: RefreshGetUsersUseCase,
private val removeUser: RemoveUserUseCase,
) : AbstractMviViewModel() {
-
override val rawLogTag get() = "MainVM[${System.identityHashCode(this)}]"
override val viewState: StateFlow by selfReferenced {
@@ -56,49 +55,50 @@ class MainVM(
.stateIn(
viewModelScope,
SharingStarted.Eagerly,
- initialVS
+ initialVS,
)
}
- private fun SharedFlow.toPartialStateChangeFlow(): Flow = merge(
- // users change
+ private fun SharedFlow.toPartialStateChangeFlow(): Flow =
merge(
- filterIsInstance(),
- filterIsInstance()
- .filter { viewState.value.error != null },
- ).toUserChangeFlow(),
- // refresh change
- filterIsInstance()
- .toRefreshChangeFlow(),
- // remove user change
- filterIsInstance()
- .toRemoveUserChangeFlow()
- )
+ // users change
+ merge(
+ filterIsInstance(),
+ filterIsInstance()
+ .filter { viewState.value.error != null },
+ ).toUserChangeFlow(),
+ // refresh change
+ filterIsInstance()
+ .toRefreshChangeFlow(),
+ // remove user change
+ filterIsInstance()
+ .toRemoveUserChangeFlow(),
+ )
//region Processors
private fun Flow.toUserChangeFlow(): Flow {
- val userChanges = defer(getUsersUseCase::invoke)
- .onEach { either -> Timber.tag(logTag).d("Emit users.size=${either.map { it.size }}") }
- .map { result ->
- result.fold(
- ifLeft = { PartialStateChange.Users.Error(it) },
- ifRight = { PartialStateChange.Users.Data(it.map(::UserItem)) }
- )
- }
- .startWith(PartialStateChange.Users.Loading)
+ val userChanges =
+ defer(getUsersUseCase::invoke)
+ .onEach { either -> Timber.tag(logTag).d("Emit users.size=${either.map { it.size }}") }
+ .map { result ->
+ result.fold(
+ ifLeft = { PartialStateChange.Users.Error(it) },
+ ifRight = { PartialStateChange.Users.Data(it.map(::UserItem)) },
+ )
+ }.startWith(PartialStateChange.Users.Loading)
return flatMapLatest { userChanges }
}
private fun Flow.toRefreshChangeFlow(): Flow {
- val refreshChanges = flowFromSuspend(refreshGetUsers::invoke)
- .map { result ->
- result.fold(
- ifLeft = { PartialStateChange.Refresh.Failure(it) },
- ifRight = { PartialStateChange.Refresh.Success }
- )
- }
- .startWith(PartialStateChange.Refresh.Loading)
+ val refreshChanges =
+ flowFromSuspend(refreshGetUsers::invoke)
+ .map { result ->
+ result.fold(
+ ifLeft = { PartialStateChange.Refresh.Failure(it) },
+ ifRight = { PartialStateChange.Refresh.Success },
+ )
+ }.startWith(PartialStateChange.Refresh.Loading)
return filter { viewState.value.canRefresh }
.flatMapFirst { refreshChanges }
@@ -111,21 +111,21 @@ class MainVM(
userItem
.toDomain()
.flatMap { removeUser(it) }
+ }.map { result ->
+ result.fold(
+ ifLeft = { PartialStateChange.RemoveUser.Failure(userItem, it) },
+ ifRight = { PartialStateChange.RemoveUser.Success(userItem) },
+ )
}
- .map { result ->
- result.fold(
- ifLeft = { PartialStateChange.RemoveUser.Failure(userItem, it) },
- ifRight = { PartialStateChange.RemoveUser.Success(userItem) },
- )
- }
}
//endregion
private companion object {
- private fun SharedFlow.filtered(): Flow = merge(
- filterIsInstance().take(1),
- filterNot { it is ViewIntent.Initial }
- )
+ private fun SharedFlow.filtered(): Flow =
+ merge(
+ filterIsInstance().take(1),
+ filterNot { it is ViewIntent.Initial },
+ )
private fun PartialStateChange.toSingleEventOrNull(getViewState: () -> ViewState): SingleEvent? =
when (this) {
@@ -133,16 +133,17 @@ class MainVM(
is PartialStateChange.Refresh.Success -> SingleEvent.Refresh.Success
is PartialStateChange.Refresh.Failure -> SingleEvent.Refresh.Failure(error)
is PartialStateChange.RemoveUser.Success -> SingleEvent.RemoveUser.Success(user)
- is PartialStateChange.RemoveUser.Failure -> SingleEvent.RemoveUser.Failure(
- user = user,
- error = error,
- indexProducer = {
- getViewState()
- .userItems
- .indexOfFirst { it.id == user.id }
- .takeIf { it != -1 }
- }
- )
+ is PartialStateChange.RemoveUser.Failure ->
+ SingleEvent.RemoveUser.Failure(
+ user = user,
+ error = error,
+ indexProducer = {
+ getViewState()
+ .userItems
+ .indexOfFirst { it.id == user.id }
+ .takeIf { it != -1 }
+ },
+ )
PartialStateChange.Users.Loading,
is PartialStateChange.Users.Data,
PartialStateChange.Refresh.Loading,
diff --git a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/UserAdapter.kt b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/UserAdapter.kt
index b2130ee4..de8b2367 100644
--- a/feature-main/src/main/java/com/hoc/flowmvi/ui/main/UserAdapter.kt
+++ b/feature-main/src/main/java/com/hoc/flowmvi/ui/main/UserAdapter.kt
@@ -12,19 +12,30 @@ import com.hoc081098.viewbindingdelegate.inflateViewBinding
internal class UserAdapter :
ListAdapter(
object : DiffUtil.ItemCallback() {
- override fun areItemsTheSame(oldItem: UserItem, newItem: UserItem) =
- oldItem.id == newItem.id
+ override fun areItemsTheSame(
+ oldItem: UserItem,
+ newItem: UserItem,
+ ) = oldItem.id == newItem.id
- override fun areContentsTheSame(oldItem: UserItem, newItem: UserItem) = oldItem == newItem
- }
+ override fun areContentsTheSame(
+ oldItem: UserItem,
+ newItem: UserItem,
+ ) = oldItem == newItem
+ },
) {
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int,
+ ): VH = VH(parent inflateViewBinding false)
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
- VH(parent inflateViewBinding false)
-
- override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(getItem(position))
+ override fun onBindViewHolder(
+ holder: VH,
+ position: Int,
+ ) = holder.bind(getItem(position))
- class VH(private val binding: ItemRecyclerUserBinding) : RecyclerView.ViewHolder(binding.root) {
+ class VH(
+ private val binding: ItemRecyclerUserBinding,
+ ) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: UserItem) {
binding.run {
nameTextView.text = item.fullName
diff --git a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainContractTest.kt b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainContractTest.kt
index a2480d2f..35e94497 100644
--- a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainContractTest.kt
+++ b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainContractTest.kt
@@ -14,15 +14,15 @@ class MainContractTest {
email = "test@gmail.com",
avatar = "avatar.png",
firstName = "first",
- lastName = "last"
+ lastName = "last",
),
UserItem(
id = "0",
email = "test@gmail.com",
avatar = "avatar.png",
firstName = "first",
- lastName = "last"
- )
+ lastName = "last",
+ ),
)
}
@@ -34,15 +34,15 @@ class MainContractTest {
email = "test@gmail.com",
avatar = "avatar.png",
firstName = "first",
- lastName = "last"
+ lastName = "last",
).hashCode(),
UserItem(
id = "0",
email = "test@gmail.com",
avatar = "avatar.png",
firstName = "first",
- lastName = "last"
- ).hashCode()
+ lastName = "last",
+ ).hashCode(),
)
}
@@ -54,7 +54,7 @@ class MainContractTest {
email = "test@gmail.com",
avatar = "avatar.png",
firstName = "first",
- lastName = "last"
+ lastName = "last",
).fullName,
"first last",
)
@@ -63,19 +63,20 @@ class MainContractTest {
@Test
fun test_userItem_toDomain() {
assertEquals(
- User.create(
- id = "0",
- email = "test@gmail.com",
- firstName = "first",
- lastName = "last",
- avatar = "avatar.png",
- ).rightValueOrThrow,
+ User
+ .create(
+ id = "0",
+ email = "test@gmail.com",
+ firstName = "first",
+ lastName = "last",
+ avatar = "avatar.png",
+ ).rightValueOrThrow,
UserItem(
id = "0",
email = "test@gmail.com",
avatar = "avatar.png",
firstName = "first",
- lastName = "last"
+ lastName = "last",
).toDomain().rightValueOrThrow,
)
}
@@ -88,16 +89,18 @@ class MainContractTest {
email = "test@gmail.com",
avatar = "avatar.png",
firstName = "first",
- lastName = "last"
+ lastName = "last",
),
UserItem(
- domain = User.create(
- id = "0",
- email = "test@gmail.com",
- firstName = "first",
- lastName = "last",
- avatar = "avatar.png",
- ).rightValueOrThrow
+ domain =
+ User
+ .create(
+ id = "0",
+ email = "test@gmail.com",
+ firstName = "first",
+ lastName = "last",
+ avatar = "avatar.png",
+ ).rightValueOrThrow,
),
)
}
diff --git a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt
index 231c87c7..349fee5f 100644
--- a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt
+++ b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/MainVMTest.kt
@@ -32,10 +32,13 @@ import kotlinx.coroutines.flow.update
@ExperimentalTime
@ExperimentalCoroutinesApi
@FlowPreview
-class MainVMTest : BaseMviViewModelTest() {
+class MainVMTest :
+ BaseMviViewModelTest<
+ ViewIntent,
+ ViewState,
+ SingleEvent,
+ MainVM,
+ >() {
private lateinit var vm: MainVM
private lateinit var getUserUseCase: GetUsersUseCase
private lateinit var refreshGetUsersUseCase: RefreshGetUsersUseCase
@@ -48,11 +51,12 @@ class MainVMTest : BaseMviViewModelTest
+ val removed = assertIs(event)
+ assertEquals(item, removed.user)
+ assertEquals(userError, removed.error)
+ assertEquals(removed.indexProducer(), 0)
+ }.left(),
),
- ).mapRight(),
- expectedEvents = listOf(
- { event: SingleEvent ->
- val removed = assertIs(event)
- assertEquals(item, removed.user)
- assertEquals(userError, removed.error)
- assertEquals(removed.indexProducer(), 0)
- }.left(),
- ),
- preProcessingIntents = flowOf(ViewIntent.Initial)
+ preProcessingIntents = flowOf(ViewIntent.Initial),
) {
coVerify(exactly = 1) { getUserUseCase() }
coVerify(exactly = 1) { removeUser(user) }
diff --git a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/TestData.kt b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/TestData.kt
index 90730c48..2f639fea 100644
--- a/feature-main/src/test/java/com/hoc/flowmvi/ui/main/TestData.kt
+++ b/feature-main/src/test/java/com/hoc/flowmvi/ui/main/TestData.kt
@@ -3,28 +3,29 @@ package com.hoc.flowmvi.ui.main
import com.hoc.flowmvi.domain.model.User
import com.hoc.flowmvi.test_utils.rightValueOrThrow
-internal val USERS = listOf(
- User.create(
- id = "1",
- email = "email1@gmail.com",
- firstName = "first1",
- lastName = "last1",
- avatar = "1.png"
- ),
- User.create(
- id = "2",
- email = "email1@gmail.com",
- firstName = "first2",
- lastName = "last2",
- avatar = "2.png"
- ),
- User.create(
- id = "3",
- email = "email1@gmail.com",
- firstName = "first3",
- lastName = "last3",
- avatar = "3.png"
- ),
-).map { it.rightValueOrThrow }
+internal val USERS =
+ listOf(
+ User.create(
+ id = "1",
+ email = "email1@gmail.com",
+ firstName = "first1",
+ lastName = "last1",
+ avatar = "1.png",
+ ),
+ User.create(
+ id = "2",
+ email = "email1@gmail.com",
+ firstName = "first2",
+ lastName = "last2",
+ avatar = "2.png",
+ ),
+ User.create(
+ id = "3",
+ email = "email1@gmail.com",
+ firstName = "first3",
+ lastName = "last3",
+ avatar = "3.png",
+ ),
+ ).map { it.rightValueOrThrow }
internal val USER_ITEMS = USERS.map(::UserItem)
diff --git a/feature-search/build.gradle.kts b/feature-search/build.gradle.kts
index 70a3be7a..725f323d 100644
--- a/feature-search/build.gradle.kts
+++ b/feature-search/build.gradle.kts
@@ -2,6 +2,7 @@ plugins {
androidLib
kotlinAndroid
pokoPlugin
+ id("org.jetbrains.kotlinx.kover")
}
android {
@@ -11,7 +12,6 @@ android {
defaultConfig {
minSdk = appConfig.minSdkVersion
- targetSdk = appConfig.targetSdkVersion
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
@@ -22,7 +22,7 @@ android {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
+ "proguard-rules.pro",
)
}
}
diff --git a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchActivity.kt b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchActivity.kt
index 5d337019..e41ba60f 100644
--- a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchActivity.kt
+++ b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchActivity.kt
@@ -31,16 +31,15 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
-import org.koin.androidx.viewmodel.ext.android.stateViewModel
+import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
@ExperimentalCoroutinesApi
@FlowPreview
@ExperimentalTime
-class SearchActivity :
- AbstractMviActivity(R.layout.activity_search) {
+class SearchActivity : AbstractMviActivity(R.layout.activity_search) {
private val binding by viewBinding()
- override val vm by stateViewModel()
+ override val vm by viewModel()
private val searchViewQueryTextEventChannel = Channel()
private val searchAdapter = SearchAdapter()
@@ -66,34 +65,36 @@ class SearchActivity :
AutoTransition()
.addTarget(errorGroup)
.addTarget(progressBar)
- .setDuration(200)
+ .setDuration(200),
)
errorGroup.isVisible = viewState.error !== null
if (errorGroup.isVisible) {
- errorMessageTextView.text = viewState.error?.let {
- when (it) {
- is UserError.InvalidId -> "Invalid id"
- UserError.NetworkError -> "Network error"
- UserError.ServerError -> "Server error"
- UserError.Unexpected -> "Unexpected error"
- is UserError.UserNotFound -> "User not found"
- is UserError.ValidationFailed -> "Validation failed"
+ errorMessageTextView.text =
+ viewState.error?.let {
+ when (it) {
+ is UserError.InvalidId -> "Invalid id"
+ UserError.NetworkError -> "Network error"
+ UserError.ServerError -> "Server error"
+ UserError.Unexpected -> "Unexpected error"
+ is UserError.UserNotFound -> "User not found"
+ is UserError.ValidationFailed -> "Validation failed"
+ }
}
- }
}
progressBar.isVisible = viewState.isLoading
}
}
- override fun viewIntents(): Flow = merge(
- searchViewQueryTextEventChannel
- .consumeAsFlow()
- .onEach { Timber.d(">>> Query $it") }
- .map { ViewIntent.Search(it.query.toString()) },
- binding.retryButton.clicks().map { ViewIntent.Retry },
- )
+ override fun viewIntents(): Flow =
+ merge(
+ searchViewQueryTextEventChannel
+ .consumeAsFlow()
+ .onEach { Timber.d(">>> Query $it") }
+ .map { ViewIntent.Search(it.query.toString()) },
+ binding.retryButton.clicks().map { ViewIntent.Retry },
+ )
override fun setupViews() {
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
@@ -101,21 +102,21 @@ class SearchActivity :
binding.run {
usersRecycler.run {
setHasFixedSize(true)
- layoutManager = GridLayoutManager(
- context,
- if (context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) 3 else 4,
- )
+ layoutManager =
+ GridLayoutManager(
+ context,
+ if (context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) 3 else 4,
+ )
adapter = searchAdapter
}
}
}
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
+ override fun onOptionsItemSelected(item: MenuItem): Boolean =
+ when (item.itemId) {
android.R.id.home -> finish().let { true }
else -> super.onOptionsItemSelected(item)
}
- }
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_search, menu)
@@ -126,7 +127,9 @@ class SearchActivity :
queryHint = "Search user..."
Timber.d("onCreateOptionsMenu: originalQuery=${ vm.viewState.value.originalQuery}")
- vm.viewState.value
+ vm
+ .viewState
+ .value
.originalQuery
.takeIf { it.isNotBlank() }
?.let {
@@ -145,7 +148,6 @@ class SearchActivity :
}
internal class IntentProvider : IntentProviders.Search {
- override fun makeIntent(context: Context): Intent =
- Intent(context, SearchActivity::class.java)
+ override fun makeIntent(context: Context): Intent = Intent(context, SearchActivity::class.java)
}
}
diff --git a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchAdapter.kt b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchAdapter.kt
index 4996208e..d2358010 100644
--- a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchAdapter.kt
+++ b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchAdapter.kt
@@ -10,19 +10,30 @@ import com.hoc.flowmvi.ui.search.databinding.ItemRecyclerSearchUserBinding
import com.hoc081098.viewbindingdelegate.inflateViewBinding
internal class SearchAdapter :
- ListAdapter(object : DiffUtil.ItemCallback