Skip to content

Refactors #285

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 13, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 0 additions & 11 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2019-2024 Kotlin Android Open Source
Copyright (c) 2019-2025 Kotlin Android Open Source

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
Binary file modified buildSrc/gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 0 additions & 2 deletions core-ui/src/main/java/com/hoc/flowmvi/core_ui/CollectIn.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import timber.log.Timber

inline fun <T> Flow<T>.collectIn(
owner: LifecycleOwner,
Expand All @@ -18,7 +17,6 @@ inline fun <T> Flow<T>.collectIn(
): Job =
owner.lifecycleScope.launch {
owner.repeatOnLifecycle(state = minActiveState) {
Timber.d("Start collecting $owner $minActiveState...")
collect { action(it) }
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package com.hoc.flowmvi.core_ui

import kotlin.coroutines.ContinuationInterceptor
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext
import timber.log.Timber

suspend fun debugCheckImmediateMainDispatcher() {
@OptIn(ExperimentalStdlibApi::class)
suspend inline fun debugCheckImmediateMainDispatcher() {
if (BuildConfig.DEBUG) {
val interceptor = currentCoroutineContext()[ContinuationInterceptor]
Timber.d("debugCheckImmediateMainDispatcher: interceptor=$interceptor")
val dispatcher = checkNotNull(currentCoroutineContext()[CoroutineDispatcher]) {
"Expected CoroutineDispatcher in current CoroutineContext but was null"
}.also { Timber.d("debugCheckImmediateMainDispatcher: dispatcher=$it") }

check(interceptor === Dispatchers.Main.immediate) {
"Expected ContinuationInterceptor to be Dispatchers.Main.immediate but was $interceptor"
check(
dispatcher === Dispatchers.Main.immediate ||
!dispatcher.isDispatchNeeded(Dispatchers.Main.immediate),
) {
"Expected ContinuationInterceptor to be Dispatchers.Main.immediate but was $dispatcher"
}
}
}
3 changes: 2 additions & 1 deletion core/src/main/java/com/hoc/flowmvi/core/selfReference.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import kotlin.reflect.KProperty
value class SelfReference<T>(
val value: T,
) : ReadOnlyProperty<Any?, T> {
override fun getValue(
@Suppress("OVERRIDE_BY_INLINE", "NOTHING_TO_INLINE")
override inline fun getValue(
thisRef: Any?,
property: KProperty<*>,
): T = value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ internal class UserRepositoryImpl(
.first()

@OptIn(FlowExtPreview::class)
override fun getUsers() =
override fun observeUsers() =
changesFlow
.onEach { Timber.d("[USER_REPO] Change=$it") }
.scanWith(::getUsersFromRemoteWithRetry) { acc, change ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ class UserRepositoryImplRealAPITest : KoinTest {
private val userRepo by inject<UserRepository>()

@Test
fun getUsers() =
fun observeUsers() =
runBlocking {
kotlin.runCatching {
val result =
userRepo
.getUsers()
.observeUsers()
.first()
assertTrue(result.isRight())
assertTrue(result.rightValueOrThrow.isNotEmpty())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,15 +285,15 @@ class UserRepositoryImplTest {
}

@Test
fun test_getUsers_withApiCallSuccess_emitsInitial() =
fun test_observeUsers_withApiCallSuccess_emitsInitial() =
runTest {
coEvery { userApiService.getUsers() } returns USER_RESPONSES
every { responseToDomain(any()) } returnsMany VALID_NES_USERS

val events = mutableListOf<Either<UserError, List<User>>>()
val job =
launch(start = CoroutineStart.UNDISPATCHED) {
repo.getUsers().toList(events)
repo.observeUsers().toList(events)
}
delay(5_000)
job.cancel()
Expand All @@ -313,15 +313,15 @@ class UserRepositoryImplTest {
}

@Test
fun test_getUsers_withApiCallError_rethrows() =
fun test_observeUsers_withApiCallError_rethrows() =
runTest {
coEvery { userApiService.getUsers() } throws IOException()
every { errorMapper(ofType<IOException>()) } returns UserError.NetworkError

val events = mutableListOf<Either<UserError, List<User>>>()
val job =
launch(start = CoroutineStart.UNDISPATCHED) {
repo.getUsers().toList(events)
repo.observeUsers().toList(events)
}
delay(20_000)
job.cancel()
Expand All @@ -337,7 +337,7 @@ class UserRepositoryImplTest {
}

@Test
fun test_getUsers_withApiCallSuccess_emitsInitialAndUpdatedUsers() =
fun test_observeUsers_withApiCallSuccess_emitsInitialAndUpdatedUsers() =
runTest {
val user = USERS.last()
val userResponse = USER_RESPONSES.last()
Expand All @@ -352,7 +352,7 @@ class UserRepositoryImplTest {
val events = mutableListOf<Either<UserError, List<User>>>()
val job =
launch(start = CoroutineStart.UNDISPATCHED) {
repo.getUsers().toList(events)
repo.observeUsers().toList(events)
}
repo.add(user)
repo.remove(user)
Expand Down
4 changes: 2 additions & 2 deletions domain/src/main/java/com/hoc/flowmvi/domain/DomainModule.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.hoc.flowmvi.domain

import com.hoc.flowmvi.domain.usecase.AddUserUseCase
import com.hoc.flowmvi.domain.usecase.GetUsersUseCase
import com.hoc.flowmvi.domain.usecase.ObserveUsersUseCase
import com.hoc.flowmvi.domain.usecase.RefreshGetUsersUseCase
import com.hoc.flowmvi.domain.usecase.RemoveUserUseCase
import com.hoc.flowmvi.domain.usecase.SearchUsersUseCase
Expand All @@ -11,7 +11,7 @@ import org.koin.dsl.module
@JvmField
val domainModule =
module {
factoryOf(::GetUsersUseCase)
factoryOf(::ObserveUsersUseCase)

factoryOf(::RefreshGetUsersUseCase)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.hoc.flowmvi.domain.model

import arrow.core.NonEmptySet

sealed class UserError : Throwable() {
sealed class UserError : RuntimeException() {
data object NetworkError : UserError() {
private fun readResolve(): Any = NetworkError
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.hoc.flowmvi.domain.model.UserError
import kotlinx.coroutines.flow.Flow

interface UserRepository {
fun getUsers(): Flow<Either<UserError, List<User>>>
fun observeUsers(): Flow<Either<UserError, List<User>>>

suspend fun refresh(): Either<UserError, Unit>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import com.hoc.flowmvi.domain.model.UserError
import com.hoc.flowmvi.domain.repository.UserRepository
import kotlinx.coroutines.flow.Flow

class GetUsersUseCase(
class ObserveUsersUseCase(
private val userRepository: UserRepository,
) {
operator fun invoke(): Flow<Either<UserError, List<User>>> = userRepository.getUsers()
operator fun invoke(): Flow<Either<UserError, List<User>>> = userRepository.observeUsers()
}
22 changes: 11 additions & 11 deletions domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.hoc.flowmvi.domain.model.User
import com.hoc.flowmvi.domain.model.UserError
import com.hoc.flowmvi.domain.repository.UserRepository
import com.hoc.flowmvi.domain.usecase.AddUserUseCase
import com.hoc.flowmvi.domain.usecase.GetUsersUseCase
import com.hoc.flowmvi.domain.usecase.ObserveUsersUseCase
import com.hoc.flowmvi.domain.usecase.RefreshGetUsersUseCase
import com.hoc.flowmvi.domain.usecase.RemoveUserUseCase
import com.hoc.flowmvi.domain.usecase.SearchUsersUseCase
Expand Down Expand Up @@ -60,7 +60,7 @@ class UseCaseTest {
val coroutineRule = TestCoroutineDispatcherRule()

private lateinit var userRepository: UserRepository
private lateinit var getUsersUseCase: GetUsersUseCase
private lateinit var observeUsersUseCase: ObserveUsersUseCase
private lateinit var refreshUseCase: RefreshGetUsersUseCase
private lateinit var removeUserUseCase: RemoveUserUseCase
private lateinit var addUserUseCase: AddUserUseCase
Expand All @@ -72,7 +72,7 @@ class UseCaseTest {
fun setup() {
userRepository = mockk()

getUsersUseCase = GetUsersUseCase(userRepository)
observeUsersUseCase = ObserveUsersUseCase(userRepository)
refreshUseCase = RefreshGetUsersUseCase(userRepository)
removeUserUseCase = RemoveUserUseCase(userRepository)
addUserUseCase = AddUserUseCase(userRepository)
Expand All @@ -86,25 +86,25 @@ class UseCaseTest {
}

@Test
fun test_getUsersUseCase_whenSuccess_emitsUsers() =
fun test_observeUsersUseCase_whenSuccess_emitsUsers() =
runTest {
val usersRight = USERS.right()
every { userRepository.getUsers() } returns flowOf(usersRight)
every { userRepository.observeUsers() } returns flowOf(usersRight)

val result = getUsersUseCase()
val result = observeUsersUseCase()

verify { userRepository.getUsers() }
verify { userRepository.observeUsers() }
assertEquals(usersRight, result.first())
}

@Test
fun test_getUsersUseCase_whenError_throwsError() =
fun test_observeUsersUseCase_whenError_throwsError() =
runTest {
every { userRepository.getUsers() } returns flowOf(errorLeft)
every { userRepository.observeUsers() } returns flowOf(errorLeft)

val result = getUsersUseCase()
val result = observeUsersUseCase()

verify { userRepository.getUsers() }
verify { userRepository.observeUsers() }
assertEquals(errorLeft, result.first())
}

Expand Down
6 changes: 3 additions & 3 deletions feature-main/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.hoc.flowmvi.ui.main
import androidx.lifecycle.viewModelScope
import arrow.core.flatMap
import com.hoc.flowmvi.core.selfReferenced
import com.hoc.flowmvi.domain.usecase.GetUsersUseCase
import com.hoc.flowmvi.domain.usecase.ObserveUsersUseCase
import com.hoc.flowmvi.domain.usecase.RefreshGetUsersUseCase
import com.hoc.flowmvi.domain.usecase.RemoveUserUseCase
import com.hoc.flowmvi.mvi_base.AbstractMviViewModel
Expand Down Expand Up @@ -33,7 +33,7 @@ import timber.log.Timber
@FlowPreview
@ExperimentalCoroutinesApi
class MainVM(
private val getUsersUseCase: GetUsersUseCase,
private val observeUsersUseCase: ObserveUsersUseCase,
private val refreshGetUsers: RefreshGetUsersUseCase,
private val removeUser: RemoveUserUseCase,
) : AbstractMviViewModel<ViewIntent, ViewState, SingleEvent>() {
Expand Down Expand Up @@ -78,7 +78,7 @@ class MainVM(
//region Processors
private fun Flow<ViewIntent>.toUserChangeFlow(): Flow<PartialStateChange.Users> {
val userChanges =
defer(getUsersUseCase::invoke)
defer { observeUsersUseCase() }
.onEach { either -> Timber.tag(logTag).d("Emit users.size=${either.map { it.size }}") }
.map { result ->
result.fold(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import arrow.core.left
import arrow.core.right
import com.hoc.flowmvi.domain.model.User
import com.hoc.flowmvi.domain.model.UserError
import com.hoc.flowmvi.domain.usecase.GetUsersUseCase
import com.hoc.flowmvi.domain.usecase.ObserveUsersUseCase
import com.hoc.flowmvi.domain.usecase.RefreshGetUsersUseCase
import com.hoc.flowmvi.domain.usecase.RemoveUserUseCase
import com.hoc.flowmvi.mvi_testing.BaseMviViewModelTest
Expand Down Expand Up @@ -40,7 +40,7 @@ class MainVMTest :
MainVM,
>() {
private lateinit var vm: MainVM
private lateinit var getUserUseCase: GetUsersUseCase
private lateinit var getUserUseCase: ObserveUsersUseCase
private lateinit var refreshGetUsersUseCase: RefreshGetUsersUseCase
private lateinit var removeUser: RemoveUserUseCase

Expand All @@ -53,7 +53,7 @@ class MainVMTest :

vm =
MainVM(
getUsersUseCase = getUserUseCase,
observeUsersUseCase = getUserUseCase,
refreshGetUsers = refreshGetUsersUseCase,
removeUser = removeUser,
)
Expand Down
Loading
Loading