Skip to content

update(vm): use Flow instead of LiveData #2

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 1 commit into from
May 14, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .idea/codeStyles/Project.xml

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

1 change: 0 additions & 1 deletion .idea/codeStyles/codeStyleConfig.xml

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

7 changes: 2 additions & 5 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,6 @@ dependencies {
// lifecycleScope
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha02'

// Extensions for LiveData
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-alpha02'

// retrofit2
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation 'com.squareup.retrofit2:converter-moshi:2.8.1'
Expand All @@ -65,8 +62,8 @@ dependencies {
implementation 'com.squareup.moshi:moshi-kotlin:1.9.2'

// coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6'

// koin
implementation 'org.koin:koin-androidx-viewmodel:2.1.5'
Expand Down
27 changes: 0 additions & 27 deletions app/src/main/java/com/hoc/flowmvi/Event.kt

This file was deleted.

18 changes: 12 additions & 6 deletions app/src/main/java/com/hoc/flowmvi/ui/add/AddActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import android.util.Log
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isInvisible
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import com.hoc.flowmvi.clicks
import com.hoc.flowmvi.databinding.ActivityAddBinding
Expand Down Expand Up @@ -41,11 +40,18 @@ class AddActivity : AppCompatActivity(), View {

private fun bindVM() {
// observe view model
addVM.viewState.observe(this, Observer { render(it ?: return@Observer) })
addVM.singleEvent.observe(
this,
Observer { handleSingleEvent(it?.getContentIfNotHandled() ?: return@Observer) }
)
lifecycleScope.launchWhenStarted {
addVM.viewState
.onEach { render(it) }
.catch { }
.collect()
}
lifecycleScope.launchWhenStarted {
addVM.singleEvent
.onEach { handleSingleEvent(it) }
.catch { }
.collect()
}

// pass view intent to view model
intents()
Expand Down
43 changes: 22 additions & 21 deletions app/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.hoc.flowmvi.ui.add

import androidx.core.util.PatternsCompat
import androidx.lifecycle.*
import com.hoc.flowmvi.Event
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.hoc.flowmvi.domain.entity.User
import com.hoc.flowmvi.domain.usecase.AddUserUseCase
import com.hoc.flowmvi.flatMapFirst
Expand All @@ -17,42 +17,43 @@ import kotlinx.coroutines.flow.*
@FlowPreview
@ExperimentalCoroutinesApi
class AddVM(private val addUser: AddUserUseCase) : ViewModel() {
private val initialVS = ViewState.initial()
private val _eventChannel = BroadcastChannel<SingleEvent>(capacity = Channel.BUFFERED)
private val _intentChannel = BroadcastChannel<ViewIntent>(capacity = Channel.BUFFERED)

private val _viewStateD = MutableLiveData<ViewState>().apply { value = initialVS }
val viewState: LiveData<ViewState> = _viewStateD.distinctUntilChanged()
val viewState: StateFlow<ViewState>

private val _eventD = MutableLiveData<Event<SingleEvent>>()
val singleEvent: LiveData<Event<SingleEvent>> get() = _eventD
val singleEvent: Flow<SingleEvent>

private val _intentChannel = BroadcastChannel<ViewIntent>(capacity = Channel.BUFFERED)
suspend fun processIntent(intent: ViewIntent) = _intentChannel.send(intent)

init {
val initialVS = ViewState.initial()

viewState = MutableStateFlow(initialVS)
singleEvent = _eventChannel.asFlow()

_intentChannel
.asFlow()
.toPartialStateChangesFlow()
.sendSingleEvent()
.scan(initialVS) { state, change -> change.reduce(state) }
.onEach { _viewStateD.value = it }
.onEach { viewState.value = it }
.catch { }
.launchIn(viewModelScope)
}


private fun Flow<PartialStateChange>.sendSingleEvent(): Flow<PartialStateChange> {
return onEach { change ->
_eventD.value = Event(
when (change) {
is PartialStateChange.ErrorsChanged -> return@onEach
PartialStateChange.AddUser.Loading -> return@onEach
is PartialStateChange.AddUser.AddUserSuccess -> SingleEvent.AddUserSuccess(change.user)
is PartialStateChange.AddUser.AddUserFailure -> SingleEvent.AddUserFailure(
change.user,
change.throwable
)
}
)
val event = when (change) {
is PartialStateChange.ErrorsChanged -> return@onEach
PartialStateChange.AddUser.Loading -> return@onEach
is PartialStateChange.AddUser.AddUserSuccess -> SingleEvent.AddUserSuccess(change.user)
is PartialStateChange.AddUser.AddUserFailure -> SingleEvent.AddUserFailure(
change.user,
change.throwable
)
}
_eventChannel.send(event)
}
}

Expand Down
20 changes: 13 additions & 7 deletions app/src/main/java/com/hoc/flowmvi/ui/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
Expand Down Expand Up @@ -66,7 +65,7 @@ class MainActivity : AppCompatActivity(), View {

ItemTouchHelper(
SwipeLeftToDeleteCallback(context) cb@{ position ->
val userItem = mainVM.viewState.value?.userItems?.get(position) ?: return@cb
val userItem = mainVM.viewState.value.userItems[position]
removeChannel.offer(userItem)
}
).attachToRecyclerView(this)
Expand All @@ -75,11 +74,18 @@ class MainActivity : AppCompatActivity(), View {

private fun bindVM() {
// observe view model
mainVM.viewState.observe(this, Observer { render(it ?: return@Observer) })
mainVM.singleEvent.observe(
this,
Observer { handleSingleEvent(it?.getContentIfNotHandled() ?: return@Observer) }
)
lifecycleScope.launchWhenStarted {
mainVM.viewState
.onEach { render(it) }
.catch { }
.collect()
}
lifecycleScope.launchWhenStarted {
mainVM.singleEvent
.onEach { handleSingleEvent(it) }
.catch { }
.collect()
}

// pass view intent to view model
intents()
Expand Down
64 changes: 31 additions & 33 deletions app/src/main/java/com/hoc/flowmvi/ui/main/MainVM.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.hoc.flowmvi.ui.main

import android.util.Log
import androidx.lifecycle.*
import com.hoc.flowmvi.Event
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.hoc.flowmvi.domain.usecase.GetUsersUseCase
import com.hoc.flowmvi.domain.usecase.RefreshGetUsersUseCase
import com.hoc.flowmvi.domain.usecase.RemoveUserUseCase
Expand All @@ -14,75 +14,73 @@ import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*

@Suppress("USELESS_CAST")
@FlowPreview
@ExperimentalCoroutinesApi
class MainVM(
private val getUsersUseCase: GetUsersUseCase,
private val refreshGetUsersUseCase: RefreshGetUsersUseCase,
private val removeUserUseCase: RemoveUserUseCase
private val refreshGetUsers: RefreshGetUsersUseCase,
private val removeUser: RemoveUserUseCase,
) : ViewModel() {
private val initialVS = ViewState.initial()
private val _eventChannel = BroadcastChannel<SingleEvent>(capacity = Channel.BUFFERED)
private val _intentChannel = BroadcastChannel<ViewIntent>(capacity = Channel.CONFLATED)

private val _viewStateD = MutableLiveData<ViewState>()
.apply { value = initialVS }
val viewState: LiveData<ViewState> = _viewStateD.distinctUntilChanged()
val viewState: StateFlow<ViewState>

private val _eventD = MutableLiveData<Event<SingleEvent>>()
val singleEvent: LiveData<Event<SingleEvent>> get() = _eventD
val singleEvent: Flow<SingleEvent>

private val _intentChannel = BroadcastChannel<ViewIntent>(capacity = Channel.CONFLATED)
suspend fun processIntent(intent: ViewIntent) = _intentChannel.send(intent)

init {
val intentFlow = _intentChannel.asFlow()
val initialVS = ViewState.initial()

viewState = MutableStateFlow(initialVS)
singleEvent = _eventChannel.asFlow()

val intentFlow = _intentChannel.asFlow()
merge(
intentFlow.filterIsInstance<ViewIntent.Initial>().take(1),
intentFlow.filterNot { it is ViewIntent.Initial }
)
.toPartialChangeFlow()
.sendSingleEvent()
.scan(initialVS) { vs, change -> change.reduce(vs) }
.onEach { _viewStateD.value = it }
.onEach { viewState.value = it }
.catch { }
.launchIn(viewModelScope)
}

private fun Flow<PartialChange>.sendSingleEvent(): Flow<PartialChange> {
return onEach {
_eventD.value = when (it) {
is PartialChange.GetUser.Error -> Event(SingleEvent.GetUsersError(it.error))
is PartialChange.Refresh.Success -> Event(SingleEvent.Refresh.Success)
is PartialChange.Refresh.Failure -> Event(SingleEvent.Refresh.Failure(it.error))
is PartialChange.RemoveUser.Success -> Event(SingleEvent.RemoveUser.Success(it.user))
is PartialChange.RemoveUser.Failure -> Event(
SingleEvent.RemoveUser.Failure(
user = it.user,
error = it.error
)
val event = when (it) {
is PartialChange.GetUser.Error -> SingleEvent.GetUsersError(it.error)
is PartialChange.Refresh.Success -> SingleEvent.Refresh.Success
is PartialChange.Refresh.Failure -> SingleEvent.Refresh.Failure(it.error)
is PartialChange.RemoveUser.Success -> SingleEvent.RemoveUser.Success(it.user)
is PartialChange.RemoveUser.Failure -> SingleEvent.RemoveUser.Failure(
user = it.user,
error = it.error,
)
PartialChange.GetUser.Loading -> return@onEach
is PartialChange.GetUser.Data -> return@onEach
PartialChange.Refresh.Loading -> return@onEach
}
_eventChannel.send(event)
}
}

private fun <T> Flow<T>.toPartialChangeFlow(): Flow<PartialChange> {
private fun Flow<ViewIntent>.toPartialChangeFlow(): Flow<PartialChange> {
val getUserChanges = getUsersUseCase()
.onEach { Log.d("###", "[MAIN_VM] Emit users.size=${it.size}") }
.map {
val items = it.map(::UserItem)
@Suppress("USELESS_CAST")
PartialChange.GetUser.Data(items) as PartialChange.GetUser
}
.onStart { emit(PartialChange.GetUser.Loading) }
.catch { emit(PartialChange.GetUser.Error(it)) }

val refreshChanges = flow { emit(refreshGetUsersUseCase()) }
.map {
@Suppress("USELESS_CAST")
PartialChange.Refresh.Success as PartialChange.Refresh
}
val refreshChanges = flow { emit(refreshGetUsers()) }
.map { PartialChange.Refresh.Success as PartialChange.Refresh }
.onStart { emit(PartialChange.Refresh.Loading) }
.catch { emit(PartialChange.Refresh.Failure(it)) }

Expand All @@ -91,11 +89,11 @@ class MainVM(
.logIntent()
.flatMapConcat { getUserChanges },
filterIsInstance<ViewIntent.Refresh>()
.filter { _viewStateD.value?.let { !it.isLoading && it.error === null } ?: false }
.filter { viewState.value.let { !it.isLoading && it.error === null } }
.logIntent()
.flatMapFirst { refreshChanges },
filterIsInstance<ViewIntent.Retry>()
.filter { _viewStateD.value?.error != null }
.filter { viewState.value.error != null }
.logIntent()
.flatMapFirst { getUserChanges },
filterIsInstance<ViewIntent.RemoveUser>()
Expand All @@ -104,7 +102,7 @@ class MainVM(
.flatMapMerge { userItem ->
flow {
try {
removeUserUseCase(userItem.toDomain())
removeUser(userItem.toDomain())
emit(PartialChange.RemoveUser.Success(userItem))
} catch (e: Exception) {
emit(PartialChange.RemoveUser.Failure(userItem, e))
Expand Down