A fork of Simple Time Tracker with WearOS support. (Now integrated upstream)
0

Configure Feed

Select the types of activity you want to include in your feed.

add wear loading state

razeeman (Mar 3, 2024, 2:15 PM +0300) 441ddde2 524c0479

+115 -91
+85 -39
wear/src/main/java/com/example/util/simpletimetracker/presentation/components/ActivitiesList.kt
··· 5 5 */ 6 6 package com.example.util.simpletimetracker.presentation.components 7 7 8 + import androidx.annotation.StringRes 8 9 import androidx.compose.foundation.layout.padding 9 10 import androidx.compose.runtime.Composable 10 11 import androidx.compose.ui.Modifier 11 - import androidx.compose.ui.platform.LocalContext 12 12 import androidx.compose.ui.tooling.preview.Preview 13 13 import androidx.compose.ui.unit.dp 14 + import androidx.wear.compose.material.ScalingLazyListScope 14 15 import androidx.wear.compose.material.Text 15 16 import androidx.wear.tooling.preview.devices.WearDevices 16 17 import com.example.util.simpletimetracker.R 17 18 import com.example.util.simpletimetracker.presentation.layout.ScaffoldedScrollingColumn 19 + import com.example.util.simpletimetracker.presentation.screens.ActivitiesViewModel 18 20 import com.example.util.simpletimetracker.presentation.utils.getString 19 21 import com.example.util.simpletimetracker.wear_api.WearActivity 20 22 import com.example.util.simpletimetracker.wear_api.WearCurrentActivity 21 23 24 + sealed interface ActivitiesListState { 25 + object Loading : ActivitiesListState 26 + 27 + data class Empty( 28 + @StringRes val messageResId: Int, 29 + ) : ActivitiesListState 30 + 31 + data class Content( 32 + val activities: List<WearActivity>, 33 + val currentActivities: List<WearCurrentActivity>, 34 + ) : ActivitiesListState 35 + } 36 + 22 37 @Composable 23 38 fun ActivitiesList( 24 - activities: List<WearActivity>, 25 - currentActivities: List<WearCurrentActivity>, 26 - onSelectActivity: (activity: WearActivity) -> Unit, 27 - onEnableActivity: (activity: WearActivity) -> Unit, 28 - onDisableActivity: (activity: WearActivity) -> Unit, 29 - onRefresh: () -> Unit, 39 + state: ActivitiesListState, 40 + onSelectActivity: (activity: WearActivity) -> Unit = {}, 41 + onEnableActivity: (activity: WearActivity) -> Unit = {}, 42 + onDisableActivity: (activity: WearActivity) -> Unit = {}, 43 + onRefresh: () -> Unit = {}, 30 44 ) { 31 45 ScaffoldedScrollingColumn { 32 - if (activities.isEmpty()) { 33 - item { 34 - Text( 35 - getString(R.string.no_activities), 36 - modifier = Modifier.padding(8.dp), 46 + when (state) { 47 + is ActivitiesListState.Loading -> { 48 + renderLoading() 49 + } 50 + is ActivitiesListState.Empty -> { 51 + renderEmpty(state) 52 + renderRefreshButton(onRefresh) 53 + } 54 + is ActivitiesListState.Content -> { 55 + renderContent( 56 + state = state, 57 + onSelectActivity = onSelectActivity, 58 + onEnableActivity = onEnableActivity, 59 + onDisableActivity = onDisableActivity, 37 60 ) 38 - } 39 - } else { 40 - for (activity in activities) { 41 - val currentActivity = currentActivities.filter { it.id == activity.id }.getOrNull(0) 42 - item(key = activity.id) { 43 - ActivityChip( 44 - activity = activity, 45 - startedAt = currentActivity?.startedAt, 46 - tags = currentActivity?.tags.orEmpty(), 47 - onClick = { onSelectActivity(activity) }, 48 - onToggleOn = { onEnableActivity(activity) }, 49 - onToggleOff = { onDisableActivity(activity) }, 50 - ) 51 - } 61 + renderRefreshButton(onRefresh) 52 62 } 53 63 } 64 + } 65 + } 54 66 55 - item { RefreshButton(onClick = onRefresh) } 67 + private fun renderLoading() { 68 + // Show nothing until data is loded. 69 + } 70 + 71 + private fun ScalingLazyListScope.renderEmpty( 72 + state: ActivitiesListState.Empty, 73 + ) { 74 + item { 75 + Text( 76 + getString(state.messageResId), 77 + modifier = Modifier.padding(8.dp), 78 + ) 56 79 } 57 80 } 58 81 82 + private fun ScalingLazyListScope.renderContent( 83 + state: ActivitiesListState.Content, 84 + onSelectActivity: (activity: WearActivity) -> Unit, 85 + onEnableActivity: (activity: WearActivity) -> Unit, 86 + onDisableActivity: (activity: WearActivity) -> Unit, 87 + ) { 88 + for (activity in state.activities) { 89 + val currentActivity = state.currentActivities 90 + .filter { it.id == activity.id } 91 + .getOrNull(0) 92 + 93 + item(key = activity.id) { 94 + ActivityChip( 95 + activity = activity, 96 + startedAt = currentActivity?.startedAt, 97 + tags = currentActivity?.tags.orEmpty(), 98 + onClick = { onSelectActivity(activity) }, 99 + onToggleOn = { onEnableActivity(activity) }, 100 + onToggleOff = { onDisableActivity(activity) }, 101 + ) 102 + } 103 + } 104 + } 105 + 106 + private fun ScalingLazyListScope.renderRefreshButton( 107 + onRefresh: () -> Unit, 108 + ) { 109 + item { RefreshButton(onClick = onRefresh) } 110 + } 111 + 59 112 @Preview(device = WearDevices.LARGE_ROUND) 60 113 @Composable 61 114 private fun NoActivities() { 62 115 ActivitiesList( 63 - activities = emptyList(), 64 - currentActivities = emptyList(), 65 - onSelectActivity = { /* `it` is the selected activity */ }, 66 - onEnableActivity = { /* `it` is the enabled activity */ }, 67 - onDisableActivity = { /* `it` is the disabled activity */ }, 68 - onRefresh = { /* What to do when requesting a refresh */ }, 116 + state = ActivitiesListState.Empty(R.string.no_activities), 69 117 ) 70 118 } 71 119 ··· 80 128 WearCurrentActivity(id = 4321, startedAt = 1708241427000L, tags = emptyList()), 81 129 ) 82 130 ActivitiesList( 83 - activities = activities, 84 - currentActivities = currents, 85 - onSelectActivity = { /* `it` is the selected activity */ }, 86 - onEnableActivity = { /* `it` is the enabled activity */ }, 87 - onDisableActivity = { /* `it` is the disabled activity */ }, 88 - onRefresh = { /* What to do when requesting a refresh */ }, 131 + state = ActivitiesListState.Content( 132 + activities = activities, 133 + currentActivities = currents, 134 + ), 89 135 ) 90 136 }
+7
wear/src/main/java/com/example/util/simpletimetracker/presentation/components/TagList.kt
··· 22 22 23 23 sealed interface TagListState { 24 24 25 + object Loading : TagListState 26 + 25 27 data class Empty( 26 28 @StringRes val messageResId: Int, 27 29 ) : TagListState ··· 58 60 ) { 59 61 ScaffoldedScrollingColumn { 60 62 when (state) { 63 + is TagListState.Loading -> renderLoadingState() 61 64 is TagListState.Empty -> renderEmptyState( 62 65 state = state, 63 66 ) ··· 68 71 ) 69 72 } 70 73 } 74 + } 75 + 76 + private fun renderLoadingState() { 77 + // Show nothing until data is loded. 71 78 } 72 79 73 80 private fun ScalingLazyListScope.renderEmptyState(
+2 -3
wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/ActivitiesScreen.kt
··· 28 28 } 29 29 30 30 ActivitiesList( 31 - activities = state.activities, 32 - currentActivities = state.currentActivities, 31 + state = state, 33 32 onSelectActivity = viewModel::onSelectActivity, 34 33 onEnableActivity = viewModel::startActivityWithoutTags, 35 34 onDisableActivity = viewModel::stopActivity, 36 - onRefresh = viewModel::refresh, 35 + onRefresh = viewModel::loadData, 37 36 ) 38 37 }
+16 -23
wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/ActivitiesViewModel.kt
··· 8 8 import android.util.Log 9 9 import androidx.lifecycle.ViewModel 10 10 import androidx.lifecycle.viewModelScope 11 + import com.example.util.simpletimetracker.R 12 + import com.example.util.simpletimetracker.presentation.components.ActivitiesListState 11 13 import com.example.util.simpletimetracker.presentation.data.WearRPCClient 12 14 import com.example.util.simpletimetracker.presentation.mediators.CurrentActivitiesMediator 13 15 import com.example.util.simpletimetracker.presentation.mediators.StartActivityMediator 14 16 import com.example.util.simpletimetracker.wear_api.WearActivity 15 - import com.example.util.simpletimetracker.wear_api.WearCurrentActivity 16 17 import dagger.hilt.android.lifecycle.HiltViewModel 17 18 import kotlinx.coroutines.channels.BufferOverflow 18 19 import kotlinx.coroutines.flow.MutableSharedFlow ··· 27 28 private val currentActivitiesMediator: CurrentActivitiesMediator, 28 29 ) : ViewModel() { 29 30 30 - val state = MutableStateFlow(State.Empty) 31 + val state: MutableStateFlow<ActivitiesListState> = MutableStateFlow(ActivitiesListState.Loading) 31 32 val effects = MutableSharedFlow<Effect>( 32 33 extraBufferCapacity = 1, 33 34 onBufferOverflow = BufferOverflow.DROP_OLDEST, ··· 37 38 38 39 fun init() { 39 40 if (isInitialized) return 40 - refresh() 41 + loadData() 41 42 isInitialized = true 42 43 } 43 44 ··· 45 46 Log.d("ActivitiesScreen", "Starting ${wearActivity.name} (#${wearActivity.id}) without tags") 46 47 viewModelScope.launch { 47 48 currentActivitiesMediator.start(wearActivity.id) 48 - refresh() 49 + loadData() 49 50 } 50 51 } 51 52 ··· 53 54 Log.d("ActivitiesScreen", "Stopping ${wearActivity.name} (#${wearActivity.id})") 54 55 viewModelScope.launch { 55 56 currentActivitiesMediator.stop(wearActivity.id) 56 - refresh() 57 + loadData() 57 58 } 58 59 } 59 60 ··· 67 68 } 68 69 } 69 70 70 - fun refresh() { 71 + fun loadData() { 71 72 viewModelScope.launch { 72 - val newState = State( 73 - activities = rpc.queryActivities(), 74 - currentActivities = rpc.queryCurrentActivities(), 75 - ) 73 + val activities = rpc.queryActivities() 74 + val newState = if (activities.isEmpty()) { 75 + ActivitiesListState.Empty(R.string.no_activities) 76 + } else { 77 + ActivitiesListState.Content( 78 + activities = activities, 79 + currentActivities = rpc.queryCurrentActivities(), 80 + ) 81 + } 76 82 state.value = newState 77 83 } 78 84 } ··· 81 87 viewModelScope.launch { 82 88 Log.d("ActivitiesScreen", "Starting ${wearActivity.name} (#${wearActivity.id}) with tags") 83 89 effects.emit(Effect.OnRequestTagSelection(wearActivity.id)) 84 - } 85 - } 86 - 87 - data class State( 88 - val activities: List<WearActivity>, 89 - val currentActivities: List<WearCurrentActivity>, 90 - ) { 91 - 92 - companion object { 93 - val Empty = State( 94 - activities = emptyList(), 95 - currentActivities = emptyList(), 96 - ) 97 90 } 98 91 } 99 92
+1 -1
wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/TagsScreen.kt
··· 29 29 } 30 30 31 31 TagList( 32 - state = state.listState, 32 + state = state, 33 33 onButtonClick = viewModel::onButtonClick, 34 34 onToggleClick = viewModel::onToggleClick 35 35 )
+2 -5
wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/TagsViewDataMapper.kt
··· 20 20 tags: List<WearTag>, 21 21 selectedTags: List<WearTag>, 22 22 settings: WearSettings, 23 - ): TagsViewModel.State { 23 + ): TagListState { 24 24 val listState = if (tags.isEmpty()) { 25 25 mapEmptyState() 26 26 } else { ··· 31 31 ) 32 32 } 33 33 34 - return TagsViewModel.State( 35 - listState = listState, 36 - settings = settings, 37 - ) 34 + return listState 38 35 } 39 36 40 37 private fun mapEmptyState(): TagListState.Empty {
+2 -20
wear/src/main/java/com/example/util/simpletimetracker/presentation/screens/TagsViewModel.kt
··· 7 7 8 8 import androidx.lifecycle.ViewModel 9 9 import androidx.lifecycle.viewModelScope 10 - import com.example.util.simpletimetracker.R 11 10 import com.example.util.simpletimetracker.presentation.components.TagListState 12 11 import com.example.util.simpletimetracker.presentation.data.WearRPCClient 13 12 import com.example.util.simpletimetracker.presentation.mediators.CurrentActivitiesMediator ··· 27 26 private val tagsViewDataMapper: TagsViewDataMapper, 28 27 ) : ViewModel() { 29 28 30 - val state = MutableStateFlow(State.Empty) 29 + val state: MutableStateFlow<TagListState> = MutableStateFlow(TagListState.Loading) 31 30 val effects = MutableSharedFlow<Effect>( 32 31 extraBufferCapacity = 1, 33 32 onBufferOverflow = BufferOverflow.DROP_OLDEST, ··· 100 99 effects.emit(Effect.OnComplete) 101 100 } 102 101 103 - private fun mapState(): State { 102 + private fun mapState(): TagListState { 104 103 return tagsViewDataMapper.mapState( 105 104 tags = tags, 106 105 selectedTags = selectedTags, 107 106 settings = settings, 108 107 ) 109 - } 110 - 111 - data class State( 112 - val listState: TagListState, 113 - val settings: WearSettings, 114 - ) { 115 - companion object { 116 - val Empty = State( 117 - listState = TagListState.Empty(R.string.no_tags), 118 - settings = WearSettings( 119 - allowMultitasking = false, 120 - showRecordTagSelection = false, 121 - recordTagSelectionCloseAfterOne = false, 122 - recordTagSelectionEvenForGeneralTags = false, 123 - ), 124 - ) 125 - } 126 108 } 127 109 128 110 sealed interface Effect {