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.ui.main.ActivityViewModel
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.main.OverviewHandling
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.ReceiveChannel
import kotlinx.coroutines.flow.*
@ -106,39 +105,28 @@ class MainModel(application: Application): AndroidViewModel(application) {
val state = MutableStateFlow(State.LaunchState as State)
var fragmentIds = mutableSetOf<Int>()
val screen: Flow<Screen> = flow {
while (true) {
val scope = CoroutineScope(viewModelScope.coroutineContext + Job())
val screen: Flow<Screen> = state.splitConflated(
Case.simple<_, _, State.LaunchState> { LaunchHandling.processLaunchState(state, logic) },
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) {
is State.LaunchState -> LaunchHandling.processLaunchState(state, logic)
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) {
fragmentIds.add(containerId)
if (containerId != null) {
fragmentIds.add(containerId)
emit(Screen.FragmentScreen(it, it.toolbarIcons, it.toolbarOptions, it, containerId))
}
true
} else false
})
else -> throw IllegalStateException()
emit(Screen.FragmentScreen(it as State, it.toolbarIcons, it.toolbarOptions, it, containerId))
}
}
scope.cancel()
}
}
).shareIn(viewModelScope, SharingStarted.WhileSubscribed(1000), 1)
fun execute(command: UpdateStateCommand) {
command.applyTo(state)
@ -147,4 +135,6 @@ class MainModel(application: Application): AndroidViewModel(application) {
fun reportAuthenticationScreenClosed() {
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
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.livedata.waitUntilValueMatches
import io.timelimit.android.logic.AppLogic
import io.timelimit.android.ui.model.Screen
import io.timelimit.android.ui.model.State
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flow
object LaunchHandling {
suspend fun processLaunchState(
fun processLaunchState(
state: MutableStateFlow<State>,
logic: AppLogic
) {
): Flow<Screen> = flow {
val oldValue = state.value
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.data.model.UserType
import io.timelimit.android.extensions.whileTrue
import io.timelimit.android.logic.AppLogic
import io.timelimit.android.ui.model.BackStackItem
import io.timelimit.android.ui.model.Screen
import io.timelimit.android.ui.model.State
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.*
object ManageChildHandling {
fun processState(
logic: AppLogic,
stateLive: MutableStateFlow<State>
) = flow {
while (true) when (stateLive.value) {
is State.ManageChild.Main -> emitAll(processMainState(logic, stateLive))
is State.ManageChild.Apps -> emitAll(processAppsState(logic, stateLive))
else -> break
}
}
state: Flow<State.ManageChild>,
updateState: ((State.ManageChild) -> State) -> Unit
) = state.splitConflated(
Case.simple<_, _, State.ManageChild.Main> { processMainState(logic, it, updateMethod(updateState)) },
Case.simple<_, _, State.ManageChild.Apps> { processAppsState(logic, it, updateMethod(updateState)) }
)
private fun processMainState(
logic: AppLogic,
stateLive: MutableStateFlow<State>
stateLive: Flow<State.ManageChild.Main>,
updateState: ((State.ManageChild.Main) -> State) -> Unit
): Flow<Screen> {
val hasMatchingStateLive = stateLive.map { it is State.ManageChild.Main }
val matchingState = stateLive.filterIsInstance<State.ManageChild.Main>()
val screenLive = matchingState.transformLatest { state ->
return stateLive.transformLatest { state ->
val userLive = logic.database.user().getUserByIdFlow(state.childId)
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(
state,
state.toolbarIcons,
@ -59,27 +56,23 @@ object ManageChildHandling {
listOf(
BackStackItem(
Title.StringResource(R.string.main_tab_overview)
) { stateLive.compareAndSet(state, state.previousOverview) }
) { updateState { state.previousOverview } }
)
))
})
}
return hasMatchingStateLive.whileTrue { screenLive }
}
private fun processAppsState(
logic: AppLogic,
stateLive: MutableStateFlow<State>
stateLive: Flow<State.ManageChild.Apps>,
updateState: ((State.ManageChild.Apps) -> State) -> Unit
): Flow<Screen> {
val hasMatchingStateLive = stateLive.map { it is State.ManageChild.Apps }
val matchingState = stateLive.filterIsInstance<State.ManageChild.Apps>()
val screenLive = matchingState.transformLatest { state ->
return stateLive.transformLatest { state ->
val userLive = logic.database.user().getUserByIdFlow(state.childId)
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(
state,
state.toolbarIcons,
@ -89,15 +82,13 @@ object ManageChildHandling {
listOf(
BackStackItem(
Title.StringResource(R.string.main_tab_overview)
) { stateLive.compareAndSet(state, state.previousChild.previousOverview) },
) { updateState { state.previousChild.previousOverview } },
BackStackItem(
Title.Plain(user.name)
) { stateLive.compareAndSet(state, state.previousChild) }
) { updateState { state.previousChild } }
)
))
})
}
return hasMatchingStateLive.whileTrue { screenLive }
}
}