Skip to content

Commit 9575ea5

Browse files
author
Manuel Vivo
authored
Migrate project to use StateFlow and latest guidance (#854)
1 parent 335ec14 commit 9575ea5

File tree

39 files changed

+874
-1074
lines changed

39 files changed

+874
-1074
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ In this branch you'll find:
99
* User Interface built with **[Jetpack Compose](https://developer.android.com/jetpack/compose)**
1010
* A single-activity architecture, using **[Navigation Compose](https://developer.android.com/jetpack/compose/navigation)**.
1111
* A presentation layer that contains a Compose screen (View) and a **ViewModel** per screen (or feature).
12-
* Reactive UIs using **[LiveData](https://developer.android.com/topic/libraries/architecture/livedata)** and **[coroutines](https://kotlinlang.org/docs/coroutines-overview.html)** for asynchronous operations.
12+
* Reactive UIs using **[Flow](https://developer.android.com/kotlin/flow)** and **[coroutines](https://kotlinlang.org/docs/coroutines-overview.html)** for asynchronous operations.
1313
* A **data layer** with a repository and two data sources (local using Room and a fake remote).
1414
* Two **product flavors**, `mock` and `prod`, [to ease development and testing](https://android-developers.googleblog.com/2015/12/leveraging-product-flavors-in-android.html).
1515
* A collection of unit, integration and e2e **tests**, including "shared" tests that can be run on emulator/device or Robolectric.
@@ -24,6 +24,7 @@ This project hosts each sample app in separate repository branches. For more inf
2424
| ------------- | ------------- |
2525
| [main](https://github.com/googlesamples/android-architecture/tree/main) | This branch |
2626
| [hilt](https://github.com/googlesamples/android-architecture/tree/hilt) | A simple Hilt setup that removes the two flavors and service locator (not using Compose yet) |
27+
| [livedata](https://github.com/googlesamples/android-architecture/tree/livedata) | Uses LiveData instead of StateFlow as the data stream solution |
2728
| [usecases](https://github.com/googlesamples/android-architecture/tree/usecases) | Adds a new domain layer that uses UseCases for business logic (not using Compose yet) |
2829
| [views](https://github.com/googlesamples/android-architecture/tree/views) | Uses Views instead of Jetpack Compose to render UI elements on the screen |
2930

app/build.gradle

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,6 @@ android {
8080
compose true
8181
}
8282

83-
dataBinding {
84-
enabled = true
85-
enabledForTests = true
86-
}
87-
8883
compileOptions {
8984
sourceCompatibility = 1.8
9085
targetCompatibility = 1.8
@@ -111,27 +106,23 @@ android {
111106
apiLevel = 30
112107
// You can also specify "google" if you require Google Play Services.
113108
systemImageSource = "aosp-atd"
114-
abi = "x86"
115109
}
116110
pixel2api30(com.android.build.api.dsl.ManagedVirtualDevice) {
117111
// Use device profiles you typically see in Android Studio
118112
device = "Pixel 2"
119113
apiLevel = 30
120114
// You can also specify "google" if you require Google Play Services.
121115
systemImageSource = "aosp"
122-
abi = "x86"
123116
}
124117
pixel2api27(com.android.build.api.dsl.ManagedVirtualDevice) {
125118
device = "Pixel 2"
126119
apiLevel = 27
127120
systemImageSource = "aosp"
128-
abi = "x86"
129121
}
130122
nexus9api29(com.android.build.api.dsl.ManagedVirtualDevice) {
131123
device = "Nexus 9"
132124
apiLevel = 29
133125
systemImageSource = "aosp"
134-
abi = "x86"
135126
}
136127
}
137128
}
@@ -155,15 +146,14 @@ dependencies {
155146
implementation "androidx.room:room-runtime:$roomVersion"
156147
kapt "androidx.room:room-compiler:$roomVersion"
157148
implementation "androidx.room:room-ktx:$roomVersion"
149+
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$archLifecycleVersion"
158150
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$archLifecycleVersion"
159-
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$archLifecycleVersion"
160151

161152
// Jetpack Compose
162153
implementation "androidx.activity:activity-compose:$activityComposeVersion"
163154
implementation "androidx.compose.material:material:$composeVersion"
164155
implementation "androidx.compose.animation:animation:$composeVersion"
165156
implementation "androidx.compose.ui:ui-tooling-preview:$composeVersion"
166-
implementation "androidx.compose.runtime:runtime-livedata:$composeVersion"
167157
implementation "androidx.navigation:navigation-compose:$navigationVersion"
168158
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$archLifecycleVersion"
169159
implementation "com.google.accompanist:accompanist-appcompat-theme:$accompanistVersion"

app/src/androidTestMock/java/com/example/android/architecture/blueprints/todoapp/tasks/TasksScreenTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ class TasksScreenTest {
256256
TasksScreen(
257257
viewModel = TasksViewModel(repository, SavedStateHandle()),
258258
userMessage = R.string.successfully_added_task_message,
259+
onUserMessageDisplayed = { },
259260
onAddTask = { },
260261
onTaskClick = { },
261262
openDrawer = { }

app/src/main/java/com/example/android/architecture/blueprints/todoapp/TodoNavGraph.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.compose.runtime.getValue
2525
import androidx.compose.runtime.remember
2626
import androidx.compose.runtime.rememberCoroutineScope
2727
import androidx.compose.ui.Modifier
28+
import androidx.lifecycle.viewmodel.compose.viewModel
2829
import androidx.navigation.NavHostController
2930
import androidx.navigation.NavType
3031
import androidx.navigation.compose.NavHost
@@ -36,10 +37,13 @@ import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.
3637
import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.TITLE_ARG
3738
import com.example.android.architecture.blueprints.todoapp.TodoDestinationsArgs.USER_MESSAGE_ARG
3839
import com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskScreen
40+
import com.example.android.architecture.blueprints.todoapp.addedittask.AddEditTaskViewModel
3941
import com.example.android.architecture.blueprints.todoapp.statistics.StatisticsScreen
4042
import com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetailScreen
43+
import com.example.android.architecture.blueprints.todoapp.taskdetail.TaskDetailViewModel
4144
import com.example.android.architecture.blueprints.todoapp.tasks.TasksScreen
4245
import com.example.android.architecture.blueprints.todoapp.util.AppModalDrawer
46+
import com.example.android.architecture.blueprints.todoapp.util.getViewModelFactory
4347
import kotlinx.coroutines.CoroutineScope
4448
import kotlinx.coroutines.launch
4549

@@ -71,6 +75,7 @@ fun TodoNavGraph(
7175
AppModalDrawer(drawerState, currentRoute, navActions) {
7276
TasksScreen(
7377
userMessage = entry.arguments?.getInt(USER_MESSAGE_ARG)!!,
78+
onUserMessageDisplayed = { entry.arguments?.putInt(USER_MESSAGE_ARG, 0) },
7479
onAddTask = { navActions.navigateToAddEditTask(R.string.add_task, null) },
7580
onTaskClick = { task -> navActions.navigateToTaskDetail(task.id) },
7681
openDrawer = { coroutineScope.launch { drawerState.open() } }
@@ -90,9 +95,12 @@ fun TodoNavGraph(
9095
)
9196
) { entry ->
9297
val taskId = entry.arguments?.getString(TASK_ID_ARG)
98+
val viewModel: AddEditTaskViewModel = viewModel(
99+
factory = getViewModelFactory(entry.arguments)
100+
)
93101
AddEditTaskScreen(
102+
viewModel = viewModel,
94103
topBarTitle = entry.arguments?.getInt(TITLE_ARG)!!,
95-
taskId = taskId,
96104
onTaskUpdate = {
97105
navActions.navigateToTasks(
98106
if (taskId == null) ADD_EDIT_RESULT_OK else EDIT_RESULT_OK
@@ -102,8 +110,11 @@ fun TodoNavGraph(
102110
)
103111
}
104112
composable(TodoDestinations.TASK_DETAIL_ROUTE) { entry ->
113+
val viewModel: TaskDetailViewModel = viewModel(
114+
factory = getViewModelFactory(entry.arguments)
115+
)
105116
TaskDetailScreen(
106-
taskId = entry.arguments?.getString(TASK_ID_ARG)!!,
117+
viewModel = viewModel,
107118
onEditTask = { taskId ->
108119
navActions.navigateToAddEditTask(R.string.edit_task, taskId)
109120
},

app/src/main/java/com/example/android/architecture/blueprints/todoapp/ViewModelFactory.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ class ViewModelFactory constructor(
4545
isAssignableFrom(StatisticsViewModel::class.java) ->
4646
StatisticsViewModel(tasksRepository)
4747
isAssignableFrom(TaskDetailViewModel::class.java) ->
48-
TaskDetailViewModel(tasksRepository)
48+
TaskDetailViewModel(tasksRepository, handle)
4949
isAssignableFrom(AddEditTaskViewModel::class.java) ->
50-
AddEditTaskViewModel(tasksRepository)
50+
AddEditTaskViewModel(tasksRepository, handle)
5151
isAssignableFrom(TasksViewModel::class.java) ->
5252
TasksViewModel(tasksRepository, handle)
5353
else ->

app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskScreen.kt

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,58 +29,72 @@ import androidx.compose.material.Icon
2929
import androidx.compose.material.MaterialTheme
3030
import androidx.compose.material.OutlinedTextField
3131
import androidx.compose.material.Scaffold
32+
import androidx.compose.material.ScaffoldState
3233
import androidx.compose.material.Text
3334
import androidx.compose.material.TextFieldDefaults
3435
import androidx.compose.material.icons.Icons
3536
import androidx.compose.material.icons.filled.Done
37+
import androidx.compose.material.rememberScaffoldState
3638
import androidx.compose.runtime.Composable
39+
import androidx.compose.runtime.LaunchedEffect
3740
import androidx.compose.runtime.getValue
38-
import androidx.compose.runtime.livedata.observeAsState
3941
import androidx.compose.ui.Modifier
4042
import androidx.compose.ui.graphics.Color
4143
import androidx.compose.ui.res.dimensionResource
4244
import androidx.compose.ui.res.stringResource
4345
import androidx.compose.ui.text.font.FontWeight
4446
import androidx.compose.ui.unit.dp
45-
import androidx.lifecycle.viewmodel.compose.viewModel
4647
import com.example.android.architecture.blueprints.todoapp.R
4748
import com.example.android.architecture.blueprints.todoapp.util.AddEditTaskTopAppBar
48-
import com.example.android.architecture.blueprints.todoapp.util.getViewModelFactory
49+
import com.example.android.architecture.blueprints.todoapp.util.collectAsStateWithLifecycle
4950
import com.google.accompanist.swiperefresh.SwipeRefresh
5051
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
5152

5253
@Composable
5354
fun AddEditTaskScreen(
55+
viewModel: AddEditTaskViewModel,
5456
@StringRes topBarTitle: Int,
55-
taskId: String?,
5657
onTaskUpdate: () -> Unit,
5758
onBack: () -> Unit,
5859
modifier: Modifier = Modifier,
59-
viewModel: AddEditTaskViewModel = viewModel(factory = getViewModelFactory()),
60-
state: AddEditTaskState = rememberAddEditTaskState(taskId, viewModel, onTaskUpdate)
60+
scaffoldState: ScaffoldState = rememberScaffoldState()
6161
) {
6262
Scaffold(
6363
modifier = modifier.fillMaxSize(),
64-
scaffoldState = state.scaffoldState,
64+
scaffoldState = scaffoldState,
6565
topBar = { AddEditTaskTopAppBar(topBarTitle, onBack) },
6666
floatingActionButton = {
67-
FloatingActionButton(onClick = state::onFabClick) {
67+
FloatingActionButton(onClick = viewModel::saveTask) {
6868
Icon(Icons.Filled.Done, stringResource(id = R.string.cd_save_task))
6969
}
7070
}
7171
) { paddingValues ->
72-
val loading by viewModel.dataLoading.observeAsState(initial = false)
73-
val title by viewModel.title.observeAsState(initial = "")
74-
val description by viewModel.description.observeAsState(initial = "")
72+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
7573

7674
AddEditTaskContent(
77-
loading = loading,
78-
title = title,
79-
description = description,
80-
onTitleChanged = state::onTitleChanged,
81-
onDescriptionChanged = state::onDescriptionChanged,
75+
loading = uiState.isLoading,
76+
title = uiState.title,
77+
description = uiState.description,
78+
onTitleChanged = viewModel::updateTitle,
79+
onDescriptionChanged = viewModel::updateDescription,
8280
modifier = Modifier.padding(paddingValues)
8381
)
82+
83+
// Check if the task is saved and call onTaskUpdate event
84+
LaunchedEffect(uiState.isTaskSaved) {
85+
if (uiState.isTaskSaved) {
86+
onTaskUpdate()
87+
}
88+
}
89+
90+
// Check for user messages to display on the screen
91+
uiState.userMessage?.let { userMessage ->
92+
val snackbarText = stringResource(userMessage)
93+
LaunchedEffect(scaffoldState, viewModel, userMessage, snackbarText) {
94+
scaffoldState.snackbarHostState.showSnackbar(snackbarText)
95+
viewModel.snackbarMessageShown()
96+
}
97+
}
8498
}
8599
}
86100

app/src/main/java/com/example/android/architecture/blueprints/todoapp/addedittask/AddEditTaskState.kt

Lines changed: 0 additions & 106 deletions
This file was deleted.

0 commit comments

Comments
 (0)