From 65d3fe67cf9eed7a7f9faa693f29078345b06951 Mon Sep 17 00:00:00 2001 From: Joseph Hale Date: Sun, 18 Feb 2024 01:06:17 -0700 Subject: [PATCH] feat(wear): Add navigation layer This is the last big commit for creating the initial, guiding project structure. Replaces the monolithic `MainActivity` with a variety of narrowly focused Composables which assemble nicely into a full application. In particular, this commit shows off the ability to press an activity which opens a new screen with chips for that activity's tags. A major milestone towards #5 --- wear/build.gradle.kts | 7 +- .../presentation/MainActivity.kt | 141 +----------------- .../simpletimetracker/presentation/README.md | 16 ++ .../presentation/components/ActivitiesList.kt | 61 ++++++++ .../presentation/components/README.md | 13 ++ .../presentation/components/RefreshButton.kt | 44 ++++++ .../presentation/components/TagChip.kt | 20 +++ .../presentation/components/TagList.kt | 21 +++ .../presentation/layout/README.md | 9 ++ .../layout/ScaffoldedScrollingColumn.kt | 18 +++ .../presentation/layout/Scaffolding.kt | 36 +++++ .../presentation/layout/ScrollingColumn.kt | 45 ++++++ .../presentation/navigation/MainNavigator.kt | 34 +++++ .../presentation/navigation/README.md | 10 ++ .../presentation/remember/README.md | 16 ++ .../presentation/remember/activities.kt | 50 +++++++ .../remember/currentActivities.kt | 50 +++++++ .../presentation/remember/tags.kt | 51 +++++++ .../presentation/screens/ActivitiesScreen.kt | 37 +++++ .../presentation/screens/README.md | 11 ++ .../presentation/screens/TagsScreen.kt | 21 +++ 21 files changed, 571 insertions(+), 140 deletions(-) create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/README.md create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivitiesList.kt create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/components/README.md create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/components/RefreshButton.kt create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagChip.kt create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagList.kt create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/layout/README.md create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/layout/ScaffoldedScrollingColumn.kt create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/layout/Scaffolding.kt create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/layout/ScrollingColumn.kt create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/navigation/MainNavigator.kt create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/navigation/README.md create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/README.md create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/activities.kt create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/currentActivities.kt create 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/ActivitiesScreen.kt create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/README.md create mode 100644 wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/TagsScreen.kt diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index 1dca9ed1..039e0156 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -58,14 +58,19 @@ android { } dependencies { + var compose_version = "1.3.1" var wear_compose_version = "1.1.0" - // Custom Dependencies + // Runtime Dependencies + implementation("androidx.wear.compose:compose-navigation:$wear_compose_version") implementation("com.google.android.horologist:horologist-compose-layout:0.2.7") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") implementation("androidx.compose.material:material-icons-core:1.6.1") implementation(project(":wearrpc")) + // Dev Dependencies + implementation("androidx.wear:wear-tooling-preview:1.0.0") + // Default Dependencies implementation("androidx.core:core-ktx:1.7.0") implementation("com.google.android.gms:play-services-wearable:17.1.0") diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/MainActivity.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/MainActivity.kt index 55935ee6..5a8add39 100644 --- a/wear/src/main/java/com/example/util/simpletimetracker/presentation/MainActivity.kt +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/MainActivity.kt @@ -6,52 +6,11 @@ package com.example.util.simpletimetracker.presentation import android.os.Bundle -import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Refresh 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.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import androidx.wear.compose.material.Icon -import androidx.wear.compose.material.MaterialTheme -import androidx.wear.compose.material.OutlinedButton -import androidx.wear.compose.material.PositionIndicator -import androidx.wear.compose.material.Scaffold -import androidx.wear.compose.material.ScalingLazyColumn -import androidx.wear.compose.material.ScalingLazyListState -import androidx.wear.compose.material.TimeText -import androidx.wear.compose.material.Vignette -import androidx.wear.compose.material.VignettePosition -import androidx.wear.compose.material.rememberScalingLazyListState -import androidx.wear.compose.material.scrollAway +import com.example.util.simpletimetracker.presentation.navigation.MainNavigator import com.example.util.simpletimetracker.presentation.theme.SimpleTimeTrackerForWearOSTheme -import com.example.util.simpletimetracker.wearrpc.Activity -import com.example.util.simpletimetracker.wearrpc.ContextMessenger -import com.example.util.simpletimetracker.wearrpc.CurrentActivity -import com.example.util.simpletimetracker.wearrpc.WearRPCClient -import com.google.android.horologist.compose.focus.rememberActiveFocusRequester -import com.google.android.horologist.compose.navscaffold.ExperimentalHorologistComposeLayoutApi -import com.google.android.horologist.compose.rotaryinput.rememberRotaryHapticFeedback -import com.google.android.horologist.compose.rotaryinput.rotaryWithScroll -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -65,103 +24,7 @@ class MainActivity : ComponentActivity() { @Composable fun WearApp() { SimpleTimeTrackerForWearOSTheme { - val scrollState = rememberScalingLazyListState() - Scaffold( - timeText = { - TimeText(modifier = Modifier.scrollAway(scrollState)) - }, - vignette = { - Vignette(vignettePosition = VignettePosition.TopAndBottom) - }, - positionIndicator = { - PositionIndicator(scalingLazyListState = scrollState) - }, - ) { - ActivityList(scrollState) - } - } -} - - -@OptIn(ExperimentalHorologistComposeLayoutApi::class) -@Composable -fun ActivityList(scrollState: ScalingLazyListState = rememberScalingLazyListState()) { - val context = LocalContext.current - val composableScope = rememberCoroutineScope() - val rpc = WearRPCClient(ContextMessenger(context)) - - var activities: Array by remember { mutableStateOf(arrayOf()) } - var activitiesQueryCount by remember { mutableIntStateOf(0) } - LaunchedEffect( - key1 = activitiesQueryCount, - block = { - async(Dispatchers.Default) { - activities = rpc.queryActivities() - } - }, - ) - - var currentActivities: Array by remember { mutableStateOf(arrayOf()) } - var currentActivitiesQueryCount by remember { mutableIntStateOf(0) } - LaunchedEffect( - key1 = activitiesQueryCount, - block = { - async(Dispatchers.Default) { - currentActivities = rpc.queryCurrentActivities() - } - }, - ) - - val focusRequester = rememberActiveFocusRequester() - val rotaryHapticFeedback = rememberRotaryHapticFeedback() - ScalingLazyColumn( - modifier = Modifier - .rotaryWithScroll(focusRequester, scrollState, rotaryHapticFeedback) - .fillMaxSize() - .background(MaterialTheme.colors.background) - .selectableGroup(), - contentPadding = PaddingValues(10.dp), - verticalArrangement = Arrangement.Center, - state = scrollState, - ) { - for (activity in activities) { - val currentActivity = currentActivities.filter { it.id == activity.id }.getOrNull(0) - item { - ActivityChip( - activity, - startedAt = currentActivity?.startedAt, - tags = currentActivity?.tags ?: arrayOf(), - onClick = { - composableScope.launch(Dispatchers.Default) { - val tags = rpc.queryTagsForActivity(activity.id) - composableScope.launch(Dispatchers.Main) { - Toast.makeText( - context, - "`${activity.name}` (id: ${activity.id}) has tags [${ - tags.joinToString(",") { it.name } - }]", - Toast.LENGTH_SHORT, - ).show() - } - } - }, - ) - } - } - item { - OutlinedButton( - onClick = { - activitiesQueryCount++ - currentActivitiesQueryCount++ - Toast.makeText(context, "Refreshing activities", Toast.LENGTH_SHORT).show() - }, - content = { - Icon(Icons.Rounded.Refresh, contentDescription = "Refresh Activities List") - }, - modifier = Modifier.padding(all = 8.dp), - ) - } + MainNavigator() } } - diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/README.md b/wear/src/main/java/com/example/util/simpletimetracker/presentation/README.md new file mode 100644 index 00000000..b349890b --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/README.md @@ -0,0 +1,16 @@ + +# Presentation + +The UI for the app. Responsible for everything the user sees and interacts with. + +## MainActivity + +The `MainActivity` is what gets launched when the user opens the app on his/her +smartwatch. The `MainActivity` typically immediately delegates to a `navigation` +component which then manages which `screen` is first loaded and shown to the +user. + +Each `screen` fetches the data it needs (typically using a `remember` function) +then renders a `layout` containing one or more `components` for the user to interact with. \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivitiesList.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivitiesList.kt new file mode 100644 index 00000000..888b286d --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivitiesList.kt @@ -0,0 +1,61 @@ + +/* + * 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.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import androidx.wear.tooling.preview.devices.WearDevices +import com.example.util.simpletimetracker.presentation.ActivityChip +import com.example.util.simpletimetracker.presentation.layout.ScaffoldedScrollingColumn +import com.example.util.simpletimetracker.wearrpc.Activity +import com.example.util.simpletimetracker.wearrpc.CurrentActivity + +@Composable +fun ActivitiesList( + activities: Array, + currentActivities: Array, + onSelectActivity: (activity: Activity) -> Unit, + onRefresh: () -> Unit, +) { + ScaffoldedScrollingColumn { + for (activity in activities) { + val currentActivity = currentActivities.filter { it.id == activity.id }.getOrNull(0) + item { + ActivityChip( + activity, + startedAt = currentActivity?.startedAt, + tags = currentActivity?.tags ?: arrayOf(), + onClick = { onSelectActivity(activity) }, + ) + } + } + item { + RefreshButton( + onClick = onRefresh, + contentDescription = "Refresh Activities List", + ) + } + } +} + +@Preview(device = WearDevices.LARGE_ROUND) +@Composable +private fun Preview() { + val activities = arrayOf( + Activity(1234, "Chores", "๐Ÿงน", "#FA0000"), + Activity(4321, "Sleep", "๐Ÿ›๏ธ", "#0000FA"), + ) + val currents = arrayOf( + CurrentActivity(id = 4321, startedAt = 1708241427000L, tags = arrayOf()) + ) + ActivitiesList( + activities, + currentActivities = currents, + onSelectActivity = { /* `it` is the selected activity */ }, + onRefresh = { /* What to do when requesting a refresh */ } + ) +} \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/README.md b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/README.md new file mode 100644 index 00000000..29f408fa --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/README.md @@ -0,0 +1,13 @@ + +# Components + +Components are "view-only" chunks of UI. In other words, the UI they render +depends only on the values of the parameters passed to them -- no reading data +from any sort of state, local or remote. + +These properties make components extraordinarily easy to test, particularly by +adding `@Preview` versions within the source code file. + - Note `@Preview` versions also render *immediately* in the preview pane, + dramatically improving development velocity. \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/RefreshButton.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/RefreshButton.kt new file mode 100644 index 00000000..76b8a944 --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/RefreshButton.kt @@ -0,0 +1,44 @@ +/* + * 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.components + +import android.widget.Toast +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material.Icon +import androidx.wear.compose.material.OutlinedButton + +@Composable +fun RefreshButton(onClick: () -> Unit, contentDescription: String) { + val context = LocalContext.current + + OutlinedButton( + onClick = { + // Idea: Instead of a toast, spin the button while the callback is executing... + Toast.makeText(context, "Refreshing", Toast.LENGTH_SHORT).show() + onClick() + }, + content = { + Icon(Icons.Rounded.Refresh, contentDescription = contentDescription) + }, + modifier = Modifier.padding(all = 8.dp), + ) +} + +@Preview +@Composable +private fun Preview() { + RefreshButton( + onClick = { /* Log.i("Preview", "Refresh Button clicked!") */ }, + contentDescription = "Refreshing...", + ) +} \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagChip.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagChip.kt new file mode 100644 index 00000000..cf20bd0f --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagChip.kt @@ -0,0 +1,20 @@ +/* + * 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.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.style.TextOverflow +import androidx.wear.compose.material.Chip +import androidx.wear.compose.material.Text +import com.example.util.simpletimetracker.wearrpc.Tag + +@Composable +fun TagChip(tag: Tag, onClick: () -> Unit) { + Chip( + onClick = onClick, + label = { Text(tag.name, maxLines = 1, overflow = TextOverflow.Ellipsis) }, + ) +} \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagList.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagList.kt new file mode 100644 index 00000000..34d4eef5 --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagList.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.components + +import androidx.compose.runtime.Composable +import com.example.util.simpletimetracker.presentation.layout.ScaffoldedScrollingColumn +import com.example.util.simpletimetracker.wearrpc.Tag + +@Composable +fun TagList(tags: Array, onSelectTag: (tag: Tag) -> Unit) { + ScaffoldedScrollingColumn { + for (tag in tags) { + item { + TagChip(tag) { onSelectTag(tag) } + } + } + } +} \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/layout/README.md b/wear/src/main/java/com/example/util/simpletimetracker/presentation/layout/README.md new file mode 100644 index 00000000..38c19add --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/layout/README.md @@ -0,0 +1,9 @@ + +# Layout + +Structural composables which organize their content composables for painting on +the screen. + +Typically focus on how to position multiple components relative to each other. \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/layout/ScaffoldedScrollingColumn.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/layout/ScaffoldedScrollingColumn.kt new file mode 100644 index 00000000..75fc9ea8 --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/layout/ScaffoldedScrollingColumn.kt @@ -0,0 +1,18 @@ +/* + * 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.layout + +import androidx.compose.runtime.Composable +import androidx.wear.compose.material.ScalingLazyListScope +import androidx.wear.compose.material.rememberScalingLazyListState + +@Composable +fun ScaffoldedScrollingColumn(content: ScalingLazyListScope.() -> Unit) { + val scrollState = rememberScalingLazyListState() + Scaffolding(scrollState) { + ScrollingColumn(scrollState, content) + } +} \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/layout/Scaffolding.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/layout/Scaffolding.kt new file mode 100644 index 00000000..6cc169f5 --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/layout/Scaffolding.kt @@ -0,0 +1,36 @@ +/* + * 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.layout + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.wear.compose.material.PositionIndicator +import androidx.wear.compose.material.Scaffold +import androidx.wear.compose.material.ScalingLazyListState +import androidx.wear.compose.material.TimeText +import androidx.wear.compose.material.Vignette +import androidx.wear.compose.material.VignettePosition +import androidx.wear.compose.material.rememberScalingLazyListState +import androidx.wear.compose.material.scrollAway + +@Composable +fun Scaffolding( + scrollState: ScalingLazyListState = rememberScalingLazyListState(), + content: @Composable () -> Unit, +) { + Scaffold( + timeText = { + TimeText(modifier = Modifier.scrollAway(scrollState)) + }, + vignette = { + Vignette(vignettePosition = VignettePosition.TopAndBottom) + }, + positionIndicator = { + PositionIndicator(scalingLazyListState = scrollState) + }, + content = content, + ) +} \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/layout/ScrollingColumn.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/layout/ScrollingColumn.kt new file mode 100644 index 00000000..724666aa --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/layout/ScrollingColumn.kt @@ -0,0 +1,45 @@ +/* + * 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.layout + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.wear.compose.material.MaterialTheme +import androidx.wear.compose.material.ScalingLazyColumn +import androidx.wear.compose.material.ScalingLazyListScope +import androidx.wear.compose.material.ScalingLazyListState +import androidx.wear.compose.material.rememberScalingLazyListState +import com.google.android.horologist.compose.focus.rememberActiveFocusRequester +import com.google.android.horologist.compose.navscaffold.ExperimentalHorologistComposeLayoutApi +import com.google.android.horologist.compose.rotaryinput.rememberRotaryHapticFeedback +import com.google.android.horologist.compose.rotaryinput.rotaryWithScroll + +@OptIn(ExperimentalHorologistComposeLayoutApi::class) +@Composable +fun ScrollingColumn( + scrollState: ScalingLazyListState = rememberScalingLazyListState(), + content: ScalingLazyListScope.() -> Unit, +) { + val focusRequester = rememberActiveFocusRequester() + val rotaryHapticFeedback = rememberRotaryHapticFeedback() + ScalingLazyColumn( + modifier = Modifier + .rotaryWithScroll(focusRequester, scrollState, rotaryHapticFeedback) + .fillMaxSize() + .background(MaterialTheme.colors.background) + .selectableGroup(), + contentPadding = PaddingValues(10.dp), + verticalArrangement = Arrangement.Center, + state = scrollState, + content = content, + ) +} \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/navigation/MainNavigator.kt b/wear/src/main/java/com/example/util/simpletimetracker/presentation/navigation/MainNavigator.kt new file mode 100644 index 00000000..65b27906 --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/navigation/MainNavigator.kt @@ -0,0 +1,34 @@ +/* + * 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.navigation + +import android.util.Log +import androidx.compose.runtime.Composable +import androidx.wear.compose.navigation.SwipeDismissableNavHost +import androidx.wear.compose.navigation.composable +import androidx.wear.compose.navigation.rememberSwipeDismissableNavController +import com.example.util.simpletimetracker.presentation.screens.ActivitiesScreen +import com.example.util.simpletimetracker.presentation.screens.TagsScreen + +@Composable +fun MainNavigator() { + val navController = rememberSwipeDismissableNavController() + SwipeDismissableNavHost( + navController = navController, + startDestination = "activities" + ) { + composable("activities") { + ActivitiesScreen(onSelectActivity = { id -> + navController.navigate("activities/$id/tags") + }) + } + composable("activities/{id}/tags") { + TagsScreen(activityId = it.arguments?.getString("id")?.toLong()!!, onSelectTag = { + navController.navigate("activities") + }) + } + } +} \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/navigation/README.md b/wear/src/main/java/com/example/util/simpletimetracker/presentation/navigation/README.md new file mode 100644 index 00000000..c09ea6c2 --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/navigation/README.md @@ -0,0 +1,10 @@ + +# Navigation + +Navigation is responsible for moving users between screens of the app. + +Typically, each screen will take one or more listener lambdas as parameters, to +which Navigation will provide implementations which move the user from one +screen to another. \ No newline at end of file diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/README.md b/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/README.md new file mode 100644 index 00000000..82f9c7d0 --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/README.md @@ -0,0 +1,16 @@ + +# Remember Functions + +A package of `remember` functions for usage in other Composables. + +> If you have worked with ReactJS, this package will remind you of +[React Hooks](https://react.dev/reference/react/hooks). + +Essentially, these `remember` functions are reusable chunks of logic that can be +plugged into and shared between multiple Composables. + +They contain zero UI/rendering code. Rather they focus on wrapping reasonably +complex chunks of logic (e.g. fetching data from a remote source) for easy +consumption by a UI element. \ No newline at end of file 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 new file mode 100644 index 00000000..a2f12d82 --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/activities.kt @@ -0,0 +1,50 @@ +/* + * 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.wearrpc.Activity +import com.example.util.simpletimetracker.wearrpc.WearRPCClient +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 rpc = /* create a WearRPCClient instance */ + * val (activities, refresh) = rememberActivities(rpc) + * ``` + * + * 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(rpc: WearRPCClient): Pair, () -> Unit> { + var activities: Array by remember { mutableStateOf(arrayOf()) } + var activitiesQueryCount by remember { mutableIntStateOf(0) } + LaunchedEffect( + key1 = activitiesQueryCount, + block = { + 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 new file mode 100644 index 00000000..afabf369 --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/currentActivities.kt @@ -0,0 +1,50 @@ +/* + * 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.wearrpc.CurrentActivity +import com.example.util.simpletimetracker.wearrpc.WearRPCClient +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 rpc = /* create a WearRPCClient instance */ + * val (currents, refresh) = rememberCurrentActivities(rpc) + * ``` + * + * 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. + * + * `refresh` is a function you can call to forcibly re-request the array of current activities + * from the phone. + */ +@Composable +fun rememberCurrentActivities(rpc: WearRPCClient): Pair, () -> Unit> { + var currentActivities: Array by remember { mutableStateOf(arrayOf()) } + var currentActivitiesQueryCount by remember { mutableIntStateOf(0) } + LaunchedEffect( + key1 = currentActivitiesQueryCount, + block = { + async(Dispatchers.Default) { + currentActivities = rpc.queryCurrentActivities() + } + }, + ) + return Pair(currentActivities) { currentActivitiesQueryCount++ } +} \ 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 new file mode 100644 index 00000000..82838957 --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/remember/tags.kt @@ -0,0 +1,51 @@ +/* + * 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.wearrpc.Tag +import com.example.util.simpletimetracker.wearrpc.WearRPCClient +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 rpc = /* create a WearRPCClient instance */ + * val (tags, refresh) = rememberTags(rpc, 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(rpc: WearRPCClient, activityId: Long): Pair, () -> Unit> { + var tags: Array by remember { mutableStateOf(arrayOf()) } + var tagsQueryCount by remember { mutableIntStateOf(0) } + LaunchedEffect( + key1 = tagsQueryCount, + block = { + 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 new file mode 100644 index 00000000..430a2dbe --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/ActivitiesScreen.kt @@ -0,0 +1,37 @@ +/* + * 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.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import com.example.util.simpletimetracker.presentation.components.ActivitiesList +import com.example.util.simpletimetracker.presentation.remember.rememberActivities +import com.example.util.simpletimetracker.presentation.remember.rememberCurrentActivities +import com.example.util.simpletimetracker.wearrpc.ContextMessenger +import com.example.util.simpletimetracker.wearrpc.WearRPCClient + + +@Composable +fun ActivitiesScreen(onSelectActivity: (activityId: Long) -> Unit) { + val rpc = WearRPCClient(ContextMessenger(LocalContext.current)) + val (activities, refreshActivities) = rememberActivities(rpc) + val (currentActivities, refreshCurrentActivities) = rememberCurrentActivities(rpc) + + ActivitiesList( + activities, + currentActivities, + onSelectActivity = { + onSelectActivity(it.id) + }, + onRefresh = { + refreshActivities() + refreshCurrentActivities() + }, + ) +} + + + diff --git a/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/README.md b/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/README.md new file mode 100644 index 00000000..a39f159d --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/README.md @@ -0,0 +1,11 @@ + +# Screens + +Full-page, interactive user-facing experiences. + +Screens are typically assemble one or more component composables into a UI that +a user finds useful. + - Fetch relevant data, which is then passed to a component for rendering. + - Configure listeners and reactions to button taps/events. \ 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 new file mode 100644 index 00000000..1e7902f3 --- /dev/null +++ b/wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/TagsScreen.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.screens + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import com.example.util.simpletimetracker.presentation.components.TagList +import com.example.util.simpletimetracker.presentation.remember.rememberTags +import com.example.util.simpletimetracker.wearrpc.ContextMessenger +import com.example.util.simpletimetracker.wearrpc.WearRPCClient + +@Composable +fun TagsScreen(activityId: Long, onSelectTag: () -> Unit) { + val rpc = WearRPCClient(ContextMessenger(LocalContext.current)) + val (tags) = rememberTags(rpc, activityId) + + TagList(tags, onSelectTag = { onSelectTag() } ) +} \ No newline at end of file -- 2.51.2