Skip to content

Commit

Permalink
Improve NodeDetailsScreen to show success/failure dialogs. (#1911)
Browse files Browse the repository at this point in the history
  • Loading branch information
luizgrp authored Dec 21, 2023
1 parent 6abd6f3 commit 4450c18
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,52 @@ package com.google.android.horologist.datalayer.sample.screens.nodesactions

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Done
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.wear.compose.material.CircularProgressIndicator
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.dialog.Dialog
import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
import com.google.android.horologist.compose.layout.ScalingLazyColumn
import com.google.android.horologist.compose.layout.ScalingLazyColumnState
import com.google.android.horologist.compose.layout.belowTimeTextPreview
import com.google.android.horologist.compose.material.Chip
import com.google.android.horologist.compose.material.Confirmation
import com.google.android.horologist.compose.material.Icon
import com.google.android.horologist.compose.material.util.DECORATIVE_ELEMENT_CONTENT_DESCRIPTION
import com.google.android.horologist.datalayer.sample.R
import com.google.android.horologist.images.base.paintable.ImageVectorPaintable

@Composable
fun NodeDetailsScreen(
columnState: ScalingLazyColumnState,
modifier: Modifier = Modifier,
viewModel: NodeDetailsViewModel = hiltViewModel(),
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()

NodeDetailsScreen(
nodeId = viewModel.nodeId,
state = state,
onStartCompanionClick = viewModel::onStartCompanionClick,
onInstallOnNodeClick = viewModel::onInstallOnNodeClick,
onStartRemoteOwnAppClick = viewModel::onStartRemoteOwnAppClick,
onDialogDismiss = viewModel::onDialogDismiss,
columnState = columnState,
modifier = modifier,
)
Expand All @@ -50,12 +72,18 @@ fun NodeDetailsScreen(
@Composable
fun NodeDetailsScreen(
nodeId: String,
state: NodeDetailsScreenState,
onStartCompanionClick: () -> Unit,
onInstallOnNodeClick: () -> Unit,
onStartRemoteOwnAppClick: () -> Unit,
onDialogDismiss: () -> Unit,
columnState: ScalingLazyColumnState,
modifier: Modifier = Modifier,
) {
var showSuccessDialog by rememberSaveable { mutableStateOf(false) }
var showFailureDialog by rememberSaveable { mutableStateOf(false) }
var errorCode by rememberSaveable { mutableStateOf("") }

ScalingLazyColumn(
columnState = columnState,
modifier = modifier.fillMaxSize(),
Expand All @@ -72,22 +100,101 @@ fun NodeDetailsScreen(
text = stringResource(id = R.string.node_details_id, nodeId),
)
}
item {
Chip(
label = stringResource(id = R.string.node_details_start_companion_chip_label),
onClick = onStartCompanionClick,
)
when (state) {
NodeDetailsScreenState.Idle -> {
item {
Chip(
label = stringResource(id = R.string.node_details_start_companion_chip_label),
onClick = onStartCompanionClick,
)
}
item {
Chip(
label = stringResource(id = R.string.node_details_install_on_node_chip_label),
onClick = onInstallOnNodeClick,
)
}
item {
Chip(
label = stringResource(id = R.string.node_details_start_remote_own_app_chip_label),
onClick = onStartRemoteOwnAppClick,
)
}
}

NodeDetailsScreenState.ActionRunning -> {
item {
CircularProgressIndicator(modifier = Modifier.padding(top = 10.dp))
}
}

NodeDetailsScreenState.ActionSucceeded -> {
item {
CircularProgressIndicator(modifier = Modifier.padding(top = 10.dp))
}

showSuccessDialog = true
}

is NodeDetailsScreenState.ActionFailed -> {
item {
CircularProgressIndicator(modifier = Modifier.padding(top = 10.dp))
}

showFailureDialog = true
errorCode = state.errorCode
}
}
item {
Chip(
label = stringResource(id = R.string.node_details_install_on_node_chip_label),
onClick = onInstallOnNodeClick,
}

Dialog(
showDialog = showSuccessDialog,
onDismissRequest = {
showSuccessDialog = false
onDialogDismiss()
},
) {
Confirmation(onTimeout = {
showSuccessDialog = false
onDialogDismiss()
}) {
Icon(
paintable = ImageVectorPaintable(imageVector = Icons.Default.Done),
contentDescription = DECORATIVE_ELEMENT_CONTENT_DESCRIPTION,
modifier = Modifier
.size(48.dp)
.align(Alignment.CenterHorizontally),
tint = Color.Green,
)
Text(
stringResource(id = R.string.node_details_success_dialog_message),
textAlign = TextAlign.Center,
)
}
item {
Chip(
label = stringResource(id = R.string.node_details_start_remote_own_app_chip_label),
onClick = onStartRemoteOwnAppClick,
}

Dialog(
showDialog = showFailureDialog,
onDismissRequest = {
showFailureDialog = false
onDialogDismiss()
},
) {
Confirmation(onTimeout = {
showFailureDialog = false
onDialogDismiss()
}) {
Icon(
paintable = ImageVectorPaintable(imageVector = Icons.Default.Close),
contentDescription = DECORATIVE_ELEMENT_CONTENT_DESCRIPTION,
modifier = Modifier
.size(48.dp)
.align(Alignment.CenterHorizontally),
tint = Color.Red,
)
Text(
stringResource(id = R.string.node_details_failure_dialog_message, errorCode),
textAlign = TextAlign.Center,
)
}
}
Expand All @@ -98,9 +205,11 @@ fun NodeDetailsScreen(
fun NodeDetailsScreenPreview() {
NodeDetailsScreen(
nodeId = "12345",
state = NodeDetailsScreenState.Idle,
onStartCompanionClick = { },
onInstallOnNodeClick = { },
onStartRemoteOwnAppClick = { },
onDialogDismiss = { },
columnState = belowTimeTextPreview(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ package com.google.android.horologist.datalayer.sample.screens.nodesactions
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.horologist.data.AppHelperResultCode
import com.google.android.horologist.datalayer.watch.WearDataLayerAppHelper
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

Expand All @@ -31,27 +34,65 @@ class NodeDetailsViewModel
savedStateHandle: SavedStateHandle,
private val wearDataLayerAppHelper: WearDataLayerAppHelper,
) : ViewModel() {
private val nodeDetailsScreenArgs: NodeDetailsScreenArgs =
NodeDetailsScreenArgs(savedStateHandle)
private val nodeDetailsScreenArgs: NodeDetailsScreenArgs = NodeDetailsScreenArgs(savedStateHandle)

val nodeId: String
get() = nodeDetailsScreenArgs.nodeId

private val _uiState = MutableStateFlow<NodeDetailsScreenState>(NodeDetailsScreenState.Idle)
public val uiState: StateFlow<NodeDetailsScreenState> = _uiState

fun onStartCompanionClick() {
viewModelScope.launch {
runActionAndHandleAppHelperResult {
wearDataLayerAppHelper.startCompanion(node = nodeId)
}
}

fun onInstallOnNodeClick() {
_uiState.value = NodeDetailsScreenState.ActionRunning
viewModelScope.launch {
wearDataLayerAppHelper.installOnNode(node = nodeId)
try {
wearDataLayerAppHelper.installOnNode(node = nodeId)

_uiState.value = NodeDetailsScreenState.ActionSucceeded
} catch (e: Exception) {
// This should be handled with AppHelperResultCode if API gets improved:
// https://github.com/google/horologist/issues/1902
_uiState.value = NodeDetailsScreenState.ActionFailed(errorCode = e::class.java.simpleName)
e.printStackTrace()
}
}
}

fun onStartRemoteOwnAppClick() {
viewModelScope.launch {
runActionAndHandleAppHelperResult {
wearDataLayerAppHelper.startRemoteOwnApp(node = nodeId)
}
}

fun onDialogDismiss() {
_uiState.value = NodeDetailsScreenState.Idle
}

private fun runActionAndHandleAppHelperResult(action: suspend () -> AppHelperResultCode) {
_uiState.value = NodeDetailsScreenState.ActionRunning
viewModelScope.launch {
when (val result = action()) {
AppHelperResultCode.APP_HELPER_RESULT_SUCCESS -> {
_uiState.value = NodeDetailsScreenState.ActionSucceeded
}

else -> {
_uiState.value = NodeDetailsScreenState.ActionFailed(errorCode = result.name)
}
}
}
}
}

sealed class NodeDetailsScreenState {
data object Idle : NodeDetailsScreenState()
data object ActionRunning : NodeDetailsScreenState()
data object ActionSucceeded : NodeDetailsScreenState()
data class ActionFailed(val errorCode: String) : NodeDetailsScreenState()
}
2 changes: 2 additions & 0 deletions datalayer/sample/wear/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,6 @@
<string name="node_details_start_companion_chip_label">Start companion</string>
<string name="node_details_install_on_node_chip_label">Install on node</string>
<string name="node_details_start_remote_own_app_chip_label">Start remote own app</string>
<string name="node_details_success_dialog_message">Success!</string>
<string name="node_details_failure_dialog_message">Failed: \n%1$s</string>
</resources>

0 comments on commit 4450c18

Please sign in to comment.