From f8213ca1a6ba76fa2181294e46c4f62880c62f9b Mon Sep 17 00:00:00 2001 From: razeeman Date: Sat, 2 Mar 2024 21:50:06 +0300 Subject: [PATCH] switch wear architecture to viewModels --- .../example/util/simpletimetracker/Deps.kt | 2 + .../util/simpletimetracker/Versions.kt | 1 + .../{WidgetModule.kt => WearModule.kt} | 2 +- wear/build.gradle.kts | 1 + wear/src/main/AndroidManifest.xml | 1 + .../presentation/MainActivity.kt | 14 +-- .../simpletimetracker/presentation/WearApp.kt | 12 +++ .../presentation/components/TagChip.kt | 1 - .../presentation/components/TagList.kt | 3 +- .../presentation/data/Messenger.kt | 12 ++- .../presentation/di/WearModule.kt | 21 +++++ .../mediators/CurrentActivitiesMediator.kt | 21 +++-- .../mediators/StartActivityMediator.kt | 30 ++++-- .../presentation/remember/activities.kt | 46 ---------- .../remember/currentActivities.kt | 49 ---------- .../presentation/remember/rpcClient.kt | 12 --- .../presentation/remember/settings.kt | 44 --------- .../presentation/remember/tags.kt | 47 ---------- .../presentation/screens/ActivitiesScreen.kt | 74 ++++----------- .../screens/ActivitiesViewModel.kt | 91 +++++++++++++++++++ .../presentation/screens/TagsScreen.kt | 51 +++++------ .../presentation/screens/TagsViewModel.kt | 80 ++++++++++++++++ .../presentation/utils/Extensions.kt | 24 +++++ 23 files changed, 327 insertions(+), 312 deletions(-) rename features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/{WidgetModule.kt => WearModule.kt} (96%) create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/WearApp.kt create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/di/WearModule.kt delete mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/activities.kt delete mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/currentActivities.kt delete mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/rpcClient.kt delete mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/settings.kt delete mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/tags.kt create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/ActivitiesViewModel.kt create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/TagsViewModel.kt create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/utils/Extensions.kt diff --git a/buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Deps.kt b/buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Deps.kt index 0afccdfb..9b37be5c 100644 --- a/buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Deps.kt +++ b/buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Deps.kt @@ -83,6 +83,8 @@ object Deps { "androidx.wear:wear-tooling-preview:${Versions.wear_compose_tooling_preview}" const val horologist = "com.google.android.horologist:horologist-compose-layout:${Versions.horologist}" + const val hilt = + "androidx.hilt:hilt-navigation-compose:${Versions.compose_hilt}" const val uiTooling = "androidx.compose.ui:ui-tooling:${Versions.compose_version}" } diff --git a/buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Versions.kt b/buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Versions.kt index 7541865a..831a1ab1 100644 --- a/buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Versions.kt +++ b/buildSrc/src/main/kotlin/com/example/util/simpletimetracker/Versions.kt @@ -36,6 +36,7 @@ object Versions { const val wear_compose_tooling_preview = "1.0.0" const val horologist = "0.2.7" const val compose_kotlin_compiler = "1.4.0-alpha02" + const val compose_hilt = "1.0.0" const val junit = "4.13" const val junitUi = "1.1.4" diff --git a/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WidgetModule.kt b/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearModule.kt similarity index 96% rename from features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WidgetModule.kt rename to features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearModule.kt index 263f7be7..95b62496 100644 --- a/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WidgetModule.kt +++ b/features/feature_wear/src/main/java/com/example/util/simpletimetracker/feature_wear/WearModule.kt @@ -13,7 +13,7 @@ import dagger.hilt.components.SingletonComponent @Module @InstallIn(SingletonComponent::class) -interface WidgetModule { +interface WearModule { @Binds fun WearCommunicationInteractor.bindWearCommunicationInteractor(): WearCommunicationAPI diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index 0a677d81..f77e9814 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -70,6 +70,7 @@ dependencies { implementation(Deps.Compose.wearFoundation) implementation(Deps.Compose.wearToolingPreview) implementation(Deps.Compose.horologist) + implementation(Deps.Compose.hilt) debugImplementation(Deps.Compose.uiTooling) kapt(Deps.Kapt.dagger) diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 931b401d..c6d0410d 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ () val bestNode = findNearestNode(capability) @@ -57,7 +63,7 @@ class ContextMessenger @Inject constructor( deferred.cancel(CancellationException(logMessage)) } - return deferred.await() + deferred.await() } private fun findNearestNode(capability: String): Node? { diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/di/WearModule.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/di/WearModule.kt new file mode 100644 index 00000000..0805c423 --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/di/WearModule.kt @@ -0,0 +1,21 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.example.util.simpletimetracker.presentation.di + +import com.example.util.simpletimetracker.presentation.data.ContextMessenger +import com.example.util.simpletimetracker.presentation.data.Messenger +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface WearModule { + + @Binds + fun ContextMessenger.bindMessenger(): Messenger +} \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/mediators/CurrentActivitiesMediator.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/mediators/CurrentActivitiesMediator.kt index 0860b542..21809f73 100644 --- a/wear/src/main/java/com/example/util/simpletimetracker/presentation/mediators/CurrentActivitiesMediator.kt +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/mediators/CurrentActivitiesMediator.kt @@ -1,24 +1,26 @@ package com.example.util.simpletimetracker.presentation.mediators +import com.example.util.simpletimetracker.presentation.data.WearRPCClient import com.example.util.simpletimetracker.wear_api.WearCurrentActivity import com.example.util.simpletimetracker.wear_api.WearSettings import com.example.util.simpletimetracker.wear_api.WearTag -import com.example.util.simpletimetracker.presentation.data.WearRPCClient +import javax.inject.Inject -class CurrentActivitiesMediator( +class CurrentActivitiesMediator @Inject constructor( private val rpc: WearRPCClient, - private val currents: List, ) { - suspend fun start(activityId: Long) { - start(activityId, emptyList()) - } - suspend fun start(activityId: Long, tags: List) { + + suspend fun start( + activityId: Long, + tags: List = emptyList(), + ) { val newCurrent = WearCurrentActivity( id = activityId, startedAt = System.currentTimeMillis(), - tags = tags + tags = tags, ) if (settings().allowMultitasking) { + val currents = rpc.queryCurrentActivities() this.rpc.setCurrentActivities(currents.plus(newCurrent)) } else { this.rpc.setCurrentActivities(listOf(newCurrent)) @@ -26,11 +28,12 @@ class CurrentActivitiesMediator( } suspend fun stop(currentId: Long) { + val currents = rpc.queryCurrentActivities() val remaining = currents.filter { it.id != currentId } this.rpc.setCurrentActivities(remaining) } + private suspend fun settings(): WearSettings { return this.rpc.querySettings() } - } \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/mediators/StartActivityMediator.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/mediators/StartActivityMediator.kt index 158776c6..0c3e841c 100644 --- a/wear/src/main/java/com/example/util/simpletimetracker/presentation/mediators/StartActivityMediator.kt +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/mediators/StartActivityMediator.kt @@ -5,25 +5,39 @@ */ package com.example.util.simpletimetracker.presentation.mediators +import com.example.util.simpletimetracker.presentation.data.WearRPCClient import com.example.util.simpletimetracker.wear_api.WearActivity import com.example.util.simpletimetracker.wear_api.WearSettings -import com.example.util.simpletimetracker.wear_api.WearCommunicationAPI +import javax.inject.Inject -class StartActivityMediator( - private val api: WearCommunicationAPI, - private val onRequestStartActivity: suspend (activity: WearActivity) -> Unit, - private val onRequestTagSelection: suspend (activity: WearActivity) -> Unit, +class StartActivityMediator @Inject constructor( + private val api: WearRPCClient, ) { - suspend fun requestStart(activity: WearActivity) { + + suspend fun requestStart( + activity: WearActivity, + onRequestStartActivity: suspend (activity: WearActivity) -> Unit, + onRequestTagSelection: suspend (activity: WearActivity) -> Unit, + ) { val settings = api.querySettings() if (settings.showRecordTagSelection) { - requestTagSelectionIfNeeded(activity, settings) + requestTagSelectionIfNeeded( + activity = activity, + settings = settings, + onRequestStartActivity = onRequestStartActivity, + onRequestTagSelection = onRequestTagSelection, + ) } else { onRequestStartActivity(activity) } } - private suspend fun requestTagSelectionIfNeeded(activity: WearActivity, settings: WearSettings) { + private suspend fun requestTagSelectionIfNeeded( + activity: WearActivity, + settings: WearSettings, + onRequestStartActivity: suspend (activity: WearActivity) -> Unit, + onRequestTagSelection: suspend (activity: WearActivity) -> Unit, + ) { val tags = api.queryTagsForActivity(activity.id) val generalTags = tags.filter { it.isGeneral } val nonGeneralTags = tags.filter { !it.isGeneral } diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/activities.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/activities.kt deleted file mode 100644 index c918aa14..00000000 --- a/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/activities.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package com.example.util.simpletimetracker.presentation.remember - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import com.example.util.simpletimetracker.wear_api.WearActivity -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async - -/** - * Handles asynchronous retrieval of the available activities in Simple Time Tracker on the - * connected phone. - * - * Usage: - * ``` - * val (activities, refresh) = rememberActivities() - * ``` - * - * Initially, `activities` will be an empty array. Once the actual activities are received - * from the phone, `activities` will *automatically* update to that array and the encapsulating - * Composable will *automatically* re-render. - * - * `refresh` is a function you can call to forcibly re-request the array of available activities - * from the phone. - */ -@Composable -fun rememberActivities(): Pair, () -> Unit> { - val rpc = rememberRPCClient() - var activities: List by remember { mutableStateOf(emptyList()) } - var activitiesQueryCount by remember { mutableIntStateOf(0) } - LaunchedEffect(activitiesQueryCount) { - async(Dispatchers.Default) { - activities = rpc.queryActivities() - } - } - return Pair(activities) { activitiesQueryCount++ } -} \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/currentActivities.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/currentActivities.kt deleted file mode 100644 index 7f2c2742..00000000 --- a/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/currentActivities.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package com.example.util.simpletimetracker.presentation.remember - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import com.example.util.simpletimetracker.wear_api.WearCurrentActivity -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async - -/** - * Handles asynchronous retrieval of the currently running activities in Simple Time Tracker on the - * connected phone. - * - * Usage: - * ``` - * val (currents, setCurrents, refresh) = rememberCurrentActivities() - * ``` - * - * Initially, `currents` will be an empty array. Once the actual current activities are received - * from the phone, `currents` will *automatically* update to that array and the encapsulating - * Composable will *automatically* re-render. - * - * `setCurrents` is a function you can call to set the array of current activities on the phone. - * - * `refresh` is a function you can call to forcibly re-request the array of current activities - * from the phone. - */ -@Composable -fun rememberCurrentActivities(): Pair, () -> Unit> { - val rpc = rememberRPCClient() - var currentActivities: List by remember { mutableStateOf(emptyList()) } - var currentActivitiesQueryCount by remember { mutableIntStateOf(0) } - val queryCurrentActivities = { currentActivitiesQueryCount++ } - LaunchedEffect(currentActivitiesQueryCount) { - async(Dispatchers.Default) { - currentActivities = rpc.queryCurrentActivities() - } - } - return currentActivities to { queryCurrentActivities() } -} \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/rpcClient.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/rpcClient.kt deleted file mode 100644 index f14ea04b..00000000 --- a/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/rpcClient.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.util.simpletimetracker.presentation.remember - -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import com.example.util.simpletimetracker.presentation.data.ContextMessenger -import com.example.util.simpletimetracker.presentation.data.WearRPCClient - -@Composable -fun rememberRPCClient(): WearRPCClient { - val context = LocalContext.current - return WearRPCClient(ContextMessenger(context)) -} \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/settings.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/settings.kt deleted file mode 100644 index 3d67292f..00000000 --- a/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/settings.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package com.example.util.simpletimetracker.presentation.remember - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import com.example.util.simpletimetracker.wear_api.WearSettings -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async - -/** - * Handles asynchronous retrieval of the settings for Simple Time Tracker on the connected phone. - * - * Usage: - * ``` - * val (settings, refresh) = rememberSettings() - * ``` - * - * Initially, `settings` will be `null`. Once the actual settings are received - * from the phone, `settings` will *automatically* update to the object and the encapsulating - * Composable will *automatically* re-render. - * - * `refresh` is a function you can call to forcibly re-request the settings from the phone. - */ -@Composable -fun rememberSettings(): Pair Unit> { - var rpc = rememberRPCClient() - var settings: WearSettings? by remember { mutableStateOf(null) } - var settingsQueryCount by remember { mutableIntStateOf(0) } - LaunchedEffect(settingsQueryCount) { - async(Dispatchers.Default) { - settings = rpc.querySettings() - } - } - return Pair(settings) { settingsQueryCount++ } -} \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/tags.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/tags.kt deleted file mode 100644 index 1b89a37d..00000000 --- a/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/tags.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ -package com.example.util.simpletimetracker.presentation.remember - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import com.example.util.simpletimetracker.wear_api.WearTag -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async - -/** - * Handles asynchronous retrieval of the available tags for a specific activity in - * Simple Time Tracker on the connected phone. - * - * Usage: - * ``` - * val activityId: Long = /* obtain the ID (e.g. as a parameter) */ - * val (tags, refresh) = rememberTags(activityId) - * ``` - * - * Initially, `tags` will be an empty array. Once the actual tags are received - * from the phone, `tags` will *automatically* update to that array and the encapsulating - * Composable will *automatically* re-render. - * - * `refresh` is a function you can call to forcibly re-request the array of available tags - * from the phone. - */ -@Composable -fun rememberTags(activityId: Long): Pair, () -> Unit> { - val rpc = rememberRPCClient() - var tags: List by remember { mutableStateOf(emptyList()) } - var tagsQueryCount by remember { mutableIntStateOf(0) } - LaunchedEffect(tagsQueryCount) { - async(Dispatchers.Default) { - tags = rpc.queryTagsForActivity(activityId) - } - } - return Pair(tags) { tagsQueryCount++ } -} \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/ActivitiesScreen.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/ActivitiesScreen.kt index 068d26b9..0e2d014a 100644 --- a/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/ActivitiesScreen.kt +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/ActivitiesScreen.kt @@ -5,73 +5,39 @@ */ package com.example.util.simpletimetracker.presentation.screens -import android.util.Log import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.hilt.navigation.compose.hiltViewModel import com.example.util.simpletimetracker.presentation.components.ActivitiesList import com.example.util.simpletimetracker.presentation.components.CreditsButton -import com.example.util.simpletimetracker.presentation.mediators.CurrentActivitiesMediator -import com.example.util.simpletimetracker.presentation.remember.rememberActivities -import com.example.util.simpletimetracker.presentation.remember.rememberCurrentActivities -import com.example.util.simpletimetracker.presentation.remember.rememberRPCClient -import com.example.util.simpletimetracker.wear_api.WearActivity -import com.example.util.simpletimetracker.presentation.mediators.StartActivityMediator -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.example.util.simpletimetracker.presentation.screens.ActivitiesViewModel.Effect +import com.example.util.simpletimetracker.presentation.utils.collectEffects +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach @Composable fun ActivitiesScreen( onRequestTagSelection: (activityId: Long) -> Unit, onRequestCredits: () -> Unit, ) { - val coroutineScope = rememberCoroutineScope() - val rpc = rememberRPCClient() - val (activities, refreshActivities) = rememberActivities() - val (currentActivities, refreshCurrentActivities) = rememberCurrentActivities() - val refresh = { - refreshActivities() - refreshCurrentActivities() - } - val currentActivitiesMediator = CurrentActivitiesMediator(rpc, currentActivities) - val startActivityWithoutTags: (WearActivity) -> Unit = { - Log.d("ActivitiesScreen", "Starting ${it.name} (#${it.id}) without tags") - coroutineScope.launch(Dispatchers.Default) { - currentActivitiesMediator.start(it.id) - refresh() - } - } - val startActivityWithTags: (WearActivity) -> Unit = { - coroutineScope.launch(Dispatchers.Main) { - Log.d("ActivitiesScreen", "Starting ${it.name} (#${it.id}) with tags") - onRequestTagSelection(it.id) - } - } - val stopActivity: (WearActivity) -> Unit = { - Log.d("ActivitiesScreen", "Stopping ${it.name} (#${it.id})") - coroutineScope.launch(Dispatchers.Default) { - currentActivitiesMediator.stop(it.id) - refresh() + val viewModel = hiltViewModel() + viewModel.init() + val state = viewModel.state.collectAsState() + + viewModel.effects.collectEffects(key = viewModel) { + when (it) { + is Effect.OnRequestTagSelection -> onRequestTagSelection(it.activityId) } } - val startActivitiesMediator = StartActivityMediator( - rpc, - onRequestStartActivity = startActivityWithoutTags, - onRequestTagSelection = startActivityWithTags, - ) - - ActivitiesList( - activities = activities, - currentActivities = currentActivities, - onSelectActivity = { - coroutineScope.launch(Dispatchers.Default) { - startActivitiesMediator.requestStart(it) - } - }, - onEnableActivity = startActivityWithoutTags, - onDisableActivity = stopActivity, - onRefresh = refresh, + activities = state.value.activities, + currentActivities = state.value.currentActivities, + onSelectActivity = viewModel::onSelectActivity, + onEnableActivity = viewModel::startActivityWithoutTags, + onDisableActivity = viewModel::stopActivity, + onRefresh = viewModel::refresh, footer = { CreditsButton(onClick = onRequestCredits) }, ) } diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/ActivitiesViewModel.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/ActivitiesViewModel.kt new file mode 100644 index 00000000..f108619f --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/ActivitiesViewModel.kt @@ -0,0 +1,91 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.example.util.simpletimetracker.presentation.screens + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.util.simpletimetracker.presentation.data.WearRPCClient +import com.example.util.simpletimetracker.presentation.mediators.CurrentActivitiesMediator +import com.example.util.simpletimetracker.presentation.mediators.StartActivityMediator +import com.example.util.simpletimetracker.wear_api.WearActivity +import com.example.util.simpletimetracker.wear_api.WearCurrentActivity +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ActivitiesViewModel @Inject constructor( + private val rpc: WearRPCClient, + private val startActivitiesMediator: StartActivityMediator, + private val currentActivitiesMediator: CurrentActivitiesMediator, +) : ViewModel() { + + val state = MutableStateFlow(State()) + val effects = MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + fun init() { + refresh() + } + + fun startActivityWithoutTags(wearActivity: WearActivity) { + Log.d("ActivitiesScreen", "Starting ${wearActivity.name} (#${wearActivity.id}) without tags") + viewModelScope.launch { + currentActivitiesMediator.start(wearActivity.id) + refresh() + } + } + + fun stopActivity(wearActivity: WearActivity) { + Log.d("ActivitiesScreen", "Stopping ${wearActivity.name} (#${wearActivity.id})") + viewModelScope.launch { + currentActivitiesMediator.stop(wearActivity.id) + refresh() + } + } + + fun onSelectActivity(wearActivity: WearActivity) { + viewModelScope.launch { + startActivitiesMediator.requestStart( + activity = wearActivity, + onRequestStartActivity = ::startActivityWithoutTags, + onRequestTagSelection = ::startActivityWithTags, + ) + } + } + + fun refresh() { + viewModelScope.launch { + val newState = State( + activities = rpc.queryActivities(), + currentActivities = rpc.queryCurrentActivities(), + ) + state.emit(newState) + } + } + + private fun startActivityWithTags(wearActivity: WearActivity) { + viewModelScope.launch { + Log.d("ActivitiesScreen", "Starting ${wearActivity.name} (#${wearActivity.id}) with tags") + effects.emit(Effect.OnRequestTagSelection(wearActivity.id)) + } + } + + data class State( + val activities: List = emptyList(), + val currentActivities: List = emptyList(), + ) + + sealed interface Effect { + data class OnRequestTagSelection(val activityId: Long) : Effect + } +} \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/TagsScreen.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/TagsScreen.kt index 59a9ead8..a4cfe382 100644 --- a/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/TagsScreen.kt +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/TagsScreen.kt @@ -6,38 +6,33 @@ package com.example.util.simpletimetracker.presentation.screens import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.hilt.navigation.compose.hiltViewModel import com.example.util.simpletimetracker.presentation.components.TagList -import com.example.util.simpletimetracker.presentation.components.TagSelectionMode -import com.example.util.simpletimetracker.presentation.mediators.CurrentActivitiesMediator -import com.example.util.simpletimetracker.presentation.remember.rememberCurrentActivities -import com.example.util.simpletimetracker.presentation.remember.rememberRPCClient -import com.example.util.simpletimetracker.presentation.remember.rememberSettings -import com.example.util.simpletimetracker.presentation.remember.rememberTags -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.example.util.simpletimetracker.presentation.screens.TagsViewModel.Effect +import com.example.util.simpletimetracker.presentation.utils.collectEffects +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach @Composable -fun TagsScreen(activityId: Long, onComplete: () -> Unit) { - val coroutineScope = rememberCoroutineScope() - val rpc = rememberRPCClient() - val (settings) = rememberSettings() - val (tags) = rememberTags(activityId) - val (currentActivities) = rememberCurrentActivities() - val currentActivitiesMediator = CurrentActivitiesMediator(rpc, currentActivities) +fun TagsScreen( + activityId: Long, + onComplete: () -> Unit, +) { + val viewModel = hiltViewModel() + viewModel.init(activityId) + val state = viewModel.state.collectAsState() + + viewModel.effects.collectEffects(key = viewModel) { + when (it) { + is Effect.OnComplete -> onComplete() + } + } TagList( - tags, - mode = if (settings?.recordTagSelectionCloseAfterOne != false) { - TagSelectionMode.SINGLE - } else { - TagSelectionMode.MULTI - }, - onSelectionComplete = { - coroutineScope.launch(Dispatchers.Default) { - currentActivitiesMediator.start(activityId, it) - coroutineScope.launch(Dispatchers.Main) { onComplete() } - } - }, + tags = state.value.tags, + mode = state.value.mode, + onSelectionComplete = { viewModel.onSelectionComplete(it) }, ) } diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/TagsViewModel.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/TagsViewModel.kt new file mode 100644 index 00000000..139b7847 --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/TagsViewModel.kt @@ -0,0 +1,80 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.example.util.simpletimetracker.presentation.screens + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.util.simpletimetracker.presentation.components.TagSelectionMode +import com.example.util.simpletimetracker.presentation.data.WearRPCClient +import com.example.util.simpletimetracker.presentation.mediators.CurrentActivitiesMediator +import com.example.util.simpletimetracker.wear_api.WearSettings +import com.example.util.simpletimetracker.wear_api.WearTag +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class TagsViewModel @Inject constructor( + private val rpc: WearRPCClient, + private val currentActivitiesMediator: CurrentActivitiesMediator, +) : ViewModel() { + + val state = MutableStateFlow(State()) + val effects = MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + private var activityId: Long? = null + + fun init(activityId: Long) { + this.activityId = activityId + refresh() + } + + fun onSelectionComplete(tags: List) { + viewModelScope.launch { + val activityId = this@TagsViewModel.activityId ?: return@launch + currentActivitiesMediator.start(activityId, tags) + effects.emit(Effect.OnComplete) + } + } + + private fun refresh() { + viewModelScope.launch { + val activityId = this@TagsViewModel.activityId ?: return@launch + val settings = rpc.querySettings() + val newState = State( + tags = rpc.queryTagsForActivity(activityId), + mode = if (settings.recordTagSelectionCloseAfterOne) { + TagSelectionMode.SINGLE + } else { + TagSelectionMode.MULTI + }, + settings = settings, + ) + state.emit(newState) + } + } + + data class State( + val tags: List = emptyList(), + val mode: TagSelectionMode = TagSelectionMode.SINGLE, + val settings: WearSettings = WearSettings( + allowMultitasking = false, + showRecordTagSelection = false, + recordTagSelectionCloseAfterOne = false, + recordTagSelectionEvenForGeneralTags = false, + ), + ) + + sealed interface Effect { + object OnComplete : Effect + } +} \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/utils/Extensions.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/utils/Extensions.kt new file mode 100644 index 00000000..bc86f903 --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/utils/Extensions.kt @@ -0,0 +1,24 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package com.example.util.simpletimetracker.presentation.utils + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach + +@SuppressLint("ComposableNaming") +@Composable +fun Flow.collectEffects( + key: Any?, + action: (T) -> Unit, +) { + LaunchedEffect(key) { + onEach { action(it) }.collect() + } +} \ No newline at end of file -- 2.51.2