mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 17:59:51 +02:00
Use splitConflated
This commit is contained in:
parent
b48a985e43
commit
a33cdb34e4
4 changed files with 165 additions and 62 deletions
|
@ -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,18 +105,14 @@ 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)) },
|
||||||
when (val initialState = state.value) {
|
Case.simple<_, _, State.ManageChild.Apps> { state -> ManageChildHandling.processState(logic, state, updateMethod(::updateState)) },
|
||||||
is State.LaunchState -> LaunchHandling.processLaunchState(state, logic)
|
Case.simple<_, _, State.DiagnoseScreen.DeviceOwner> { DeviceOwnerHandling.processState(logic, scope, authenticationModelApi, state) },
|
||||||
is State.Overview -> emitAll(OverviewHandling.processState(logic, scope, activityCommandInternal, authenticationModelApi, state))
|
Case.simple<_, _, FragmentState> { state ->
|
||||||
is State.ManageChild.Main -> emitAll(ManageChildHandling.processState(logic, state))
|
state.transform {
|
||||||
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 {
|
val containerId = it.containerId ?: run {
|
||||||
(viewIdPool - fragmentIds).firstOrNull()?.also { id ->
|
(viewIdPool - fragmentIds).firstOrNull()?.also { id ->
|
||||||
it.containerId = id
|
it.containerId = id
|
||||||
|
@ -127,18 +122,11 @@ class MainModel(application: Application): AndroidViewModel(application) {
|
||||||
if (containerId != null) {
|
if (containerId != null) {
|
||||||
fragmentIds.add(containerId)
|
fragmentIds.add(containerId)
|
||||||
|
|
||||||
emit(Screen.FragmentScreen(it, it.toolbarIcons, it.toolbarOptions, it, containerId))
|
emit(Screen.FragmentScreen(it as State, 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)
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue