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.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,18 +105,14 @@ 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())
|
||||
|
||||
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 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
|
||||
|
@ -127,18 +122,11 @@ class MainModel(application: Application): AndroidViewModel(application) {
|
|||
if (containerId != null) {
|
||||
fragmentIds.add(containerId)
|
||||
|
||||
emit(Screen.FragmentScreen(it, it.toolbarIcons, it.toolbarOptions, it, containerId))
|
||||
}
|
||||
|
||||
true
|
||||
} else false
|
||||
})
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
|
||||
scope.cancel()
|
||||
emit(Screen.FragmentScreen(it as State, it.toolbarIcons, it.toolbarOptions, it, containerId))
|
||||
}
|
||||
}
|
||||
}
|
||||
).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)
|
||||
}
|
|
@ -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
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue