Use splitConflated

This commit is contained in:
Jonas Lochmann 2023-02-27 01:00:00 +01:00
parent b48a985e43
commit a33cdb34e4
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
4 changed files with 165 additions and 62 deletions

View file

@ -24,12 +24,11 @@ import io.timelimit.android.data.model.UserType
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.model.diagnose.DeviceOwnerHandling import io.timelimit.android.ui.model.diagnose.DeviceOwnerHandling
import io.timelimit.android.ui.model.flow.Case
import io.timelimit.android.ui.model.flow.splitConflated
import io.timelimit.android.ui.model.launch.LaunchHandling import io.timelimit.android.ui.model.launch.LaunchHandling
import io.timelimit.android.ui.model.main.OverviewHandling import io.timelimit.android.ui.model.main.OverviewHandling
import io.timelimit.android.ui.model.managechild.ManageChildHandling import io.timelimit.android.ui.model.managechild.ManageChildHandling
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@ -106,39 +105,28 @@ class MainModel(application: Application): AndroidViewModel(application) {
val state = MutableStateFlow(State.LaunchState as State) val state = MutableStateFlow(State.LaunchState as State)
var fragmentIds = mutableSetOf<Int>() var fragmentIds = mutableSetOf<Int>()
val screen: Flow<Screen> = flow { val screen: Flow<Screen> = state.splitConflated(
while (true) { Case.simple<_, _, State.LaunchState> { LaunchHandling.processLaunchState(state, logic) },
val scope = CoroutineScope(viewModelScope.coroutineContext + Job()) Case.simple<_, _, State.Overview> { OverviewHandling.processState(logic, scope, activityCommandInternal, authenticationModelApi, state) },
Case.simple<_, _, State.ManageChild.Main> { state -> ManageChildHandling.processState(logic, state, updateMethod(::updateState)) },
Case.simple<_, _, State.ManageChild.Apps> { state -> ManageChildHandling.processState(logic, state, updateMethod(::updateState)) },
Case.simple<_, _, State.DiagnoseScreen.DeviceOwner> { DeviceOwnerHandling.processState(logic, scope, authenticationModelApi, state) },
Case.simple<_, _, FragmentState> { state ->
state.transform {
val containerId = it.containerId ?: run {
(viewIdPool - fragmentIds).firstOrNull()?.also { id ->
it.containerId = id
}
}
when (val initialState = state.value) { if (containerId != null) {
is State.LaunchState -> LaunchHandling.processLaunchState(state, logic) fragmentIds.add(containerId)
is State.Overview -> emitAll(OverviewHandling.processState(logic, scope, activityCommandInternal, authenticationModelApi, state))
is State.ManageChild.Main -> emitAll(ManageChildHandling.processState(logic, state))
is State.ManageChild.Apps -> emitAll(ManageChildHandling.processState(logic, state))
is State.DiagnoseScreen.DeviceOwner -> emitAll(DeviceOwnerHandling.processState(logic, scope, authenticationModelApi, state))
is FragmentState -> emitAll(state.transformWhile {
if (it is FragmentState && it::class.java === initialState::class.java) {
val containerId = it.containerId ?: run {
(viewIdPool - fragmentIds).firstOrNull()?.also { id ->
it.containerId = id
}
}
if (containerId != null) { emit(Screen.FragmentScreen(it as State, it.toolbarIcons, it.toolbarOptions, it, containerId))
fragmentIds.add(containerId) }
emit(Screen.FragmentScreen(it, it.toolbarIcons, it.toolbarOptions, it, containerId))
}
true
} else false
})
else -> throw IllegalStateException()
} }
scope.cancel()
} }
} ).shareIn(viewModelScope, SharingStarted.WhileSubscribed(1000), 1)
fun execute(command: UpdateStateCommand) { fun execute(command: UpdateStateCommand) {
command.applyTo(state) command.applyTo(state)
@ -147,4 +135,6 @@ class MainModel(application: Application): AndroidViewModel(application) {
fun reportAuthenticationScreenClosed() { fun reportAuthenticationScreenClosed() {
authenticationScreenClosed.tryEmit(Unit) authenticationScreenClosed.tryEmit(Unit)
} }
private fun updateState(method: (State) -> State): Unit = state.update(method)
} }

View file

@ -0,0 +1,104 @@
/*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.model.flow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
class CaseScope<LocalStateType>(
val scope: CoroutineScope,
val className: Class<LocalStateType>
) {
inline fun <SuperStateType, LocalStateType : SuperStateType> updateMethod(
crossinline parent: ((SuperStateType) -> SuperStateType) -> Unit
): ((LocalStateType) -> SuperStateType) -> Unit = { request ->
parent { oldState ->
if (scope.isActive && className.isInstance(oldState)) request(oldState as LocalStateType)
else oldState
}
}
}
class Case<T, R>(
val className: Class<out Any>,
val key: (T) -> Any?,
val producer: CaseScope<T>.(Flow<T>, Any?) -> Flow<R>
) {
companion object {
inline fun <T, R, reified C : Any> simple(
crossinline producer: CaseScope<C>.(Flow<C>) -> Flow<R>
) = Case<T, R>(
className = C::class.java,
key = {}
) { flow, _ -> producer(this as CaseScope<C>, flow as Flow<C>) }
inline fun <T, R, reified C : Any, K> withKey(
crossinline withKey: (C) -> K,
crossinline producer: CaseScope<C>.(K, Flow<C>) -> Flow<R>
) = Case<T, R>(
className = C::class.java,
key = { withKey(it as C) }
) { flow, key -> producer(this as CaseScope<C>, key as K, flow as Flow<C>) }
}
internal fun doesMatch(value: Any?): Boolean = className.isInstance(value)
}
fun <T, R> Flow<T>.splitConflated(vararg cases: Case<T, R>): Flow<R> {
val input = this
return channelFlow<R> {
val inputChannel = Channel<T>()
launch { input.collect { inputChannel.send(it) }; inputChannel.close() }
var value = inputChannel.receive()
while (true) {
val case = cases.first { it.doesMatch(value) }
val key = case.key(value)
val relayChannel = Channel<T>(Channel.CONFLATED)
val job = launch {
val scope = CaseScope<T>(this, case.className as Class<T>)
val inputFlow = flow { relayChannel.consumeEach { emit(it) } }
case.producer(scope, inputFlow, key).collect { send(it) }
}
try {
relayChannel.send(value)
while (true) {
value = inputChannel.receive()
val newCase = cases.first { it.doesMatch(value) }
if (case !== newCase) break
if (case.key(value) != key) break
relayChannel.send(value)
}
} finally {
relayChannel.cancel()
job.cancel()
}
}
}
}

View file

@ -1,3 +1,18 @@
/*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.model.launch package io.timelimit.android.ui.model.launch
import io.timelimit.android.async.Threads import io.timelimit.android.async.Threads
@ -5,14 +20,17 @@ import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.data.model.UserType import io.timelimit.android.data.model.UserType
import io.timelimit.android.livedata.waitUntilValueMatches import io.timelimit.android.livedata.waitUntilValueMatches
import io.timelimit.android.logic.AppLogic import io.timelimit.android.logic.AppLogic
import io.timelimit.android.ui.model.Screen
import io.timelimit.android.ui.model.State import io.timelimit.android.ui.model.State
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flow
object LaunchHandling { object LaunchHandling {
suspend fun processLaunchState( fun processLaunchState(
state: MutableStateFlow<State>, state: MutableStateFlow<State>,
logic: AppLogic logic: AppLogic
) { ): Flow<Screen> = flow {
val oldValue = state.value val oldValue = state.value
if (oldValue is State.LaunchState) { if (oldValue is State.LaunchState) {

View file

@ -17,38 +17,35 @@ package io.timelimit.android.ui.model.managechild
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.data.model.UserType import io.timelimit.android.data.model.UserType
import io.timelimit.android.extensions.whileTrue
import io.timelimit.android.logic.AppLogic import io.timelimit.android.logic.AppLogic
import io.timelimit.android.ui.model.BackStackItem import io.timelimit.android.ui.model.BackStackItem
import io.timelimit.android.ui.model.Screen import io.timelimit.android.ui.model.Screen
import io.timelimit.android.ui.model.State import io.timelimit.android.ui.model.State
import io.timelimit.android.ui.model.Title import io.timelimit.android.ui.model.Title
import io.timelimit.android.ui.model.flow.Case
import io.timelimit.android.ui.model.flow.splitConflated
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
object ManageChildHandling { object ManageChildHandling {
fun processState( fun processState(
logic: AppLogic, logic: AppLogic,
stateLive: MutableStateFlow<State> state: Flow<State.ManageChild>,
) = flow { updateState: ((State.ManageChild) -> State) -> Unit
while (true) when (stateLive.value) { ) = state.splitConflated(
is State.ManageChild.Main -> emitAll(processMainState(logic, stateLive)) Case.simple<_, _, State.ManageChild.Main> { processMainState(logic, it, updateMethod(updateState)) },
is State.ManageChild.Apps -> emitAll(processAppsState(logic, stateLive)) Case.simple<_, _, State.ManageChild.Apps> { processAppsState(logic, it, updateMethod(updateState)) }
else -> break )
}
}
private fun processMainState( private fun processMainState(
logic: AppLogic, logic: AppLogic,
stateLive: MutableStateFlow<State> stateLive: Flow<State.ManageChild.Main>,
updateState: ((State.ManageChild.Main) -> State) -> Unit
): Flow<Screen> { ): Flow<Screen> {
val hasMatchingStateLive = stateLive.map { it is State.ManageChild.Main } return stateLive.transformLatest { state ->
val matchingState = stateLive.filterIsInstance<State.ManageChild.Main>()
val screenLive = matchingState.transformLatest { state ->
val userLive = logic.database.user().getUserByIdFlow(state.childId) val userLive = logic.database.user().getUserByIdFlow(state.childId)
emitAll(userLive.transform {user -> emitAll(userLive.transform {user ->
if (user?.type != UserType.Child) stateLive.compareAndSet(state, state.previousOverview) if (user?.type != UserType.Child) updateState { state.previousOverview }
else emit(Screen.ManageChildScreen( else emit(Screen.ManageChildScreen(
state, state,
state.toolbarIcons, state.toolbarIcons,
@ -59,27 +56,23 @@ object ManageChildHandling {
listOf( listOf(
BackStackItem( BackStackItem(
Title.StringResource(R.string.main_tab_overview) Title.StringResource(R.string.main_tab_overview)
) { stateLive.compareAndSet(state, state.previousOverview) } ) { updateState { state.previousOverview } }
) )
)) ))
}) })
} }
return hasMatchingStateLive.whileTrue { screenLive }
} }
private fun processAppsState( private fun processAppsState(
logic: AppLogic, logic: AppLogic,
stateLive: MutableStateFlow<State> stateLive: Flow<State.ManageChild.Apps>,
updateState: ((State.ManageChild.Apps) -> State) -> Unit
): Flow<Screen> { ): Flow<Screen> {
val hasMatchingStateLive = stateLive.map { it is State.ManageChild.Apps } return stateLive.transformLatest { state ->
val matchingState = stateLive.filterIsInstance<State.ManageChild.Apps>()
val screenLive = matchingState.transformLatest { state ->
val userLive = logic.database.user().getUserByIdFlow(state.childId) val userLive = logic.database.user().getUserByIdFlow(state.childId)
emitAll(userLive.transform {user -> emitAll(userLive.transform {user ->
if (user?.type != UserType.Child) stateLive.compareAndSet(state, state.previousChild.previousOverview) if (user?.type != UserType.Child) updateState { state.previousChild.previousOverview }
else emit(Screen.ManageChildAppsScreen( else emit(Screen.ManageChildAppsScreen(
state, state,
state.toolbarIcons, state.toolbarIcons,
@ -89,15 +82,13 @@ object ManageChildHandling {
listOf( listOf(
BackStackItem( BackStackItem(
Title.StringResource(R.string.main_tab_overview) Title.StringResource(R.string.main_tab_overview)
) { stateLive.compareAndSet(state, state.previousChild.previousOverview) }, ) { updateState { state.previousChild.previousOverview } },
BackStackItem( BackStackItem(
Title.Plain(user.name) Title.Plain(user.name)
) { stateLive.compareAndSet(state, state.previousChild) } ) { updateState { state.previousChild } }
) )
)) ))
}) })
} }
return hasMatchingStateLive.whileTrue { screenLive }
} }
} }