Skip to content

Commit 1e0c3b1

Browse files
Reduce boilerplate code when using composables (#3027)
* Add Compose support to AbstractFragment * Add `createComposeView` to create a ComposeView with app theme * Add `addComposableToRoot` to add a composable to a fragment's view * Refactor: Replace ComposeView with createComposeView * Replaced the ComposeView with a `createComposeView()` method in `AbstractFragment`. * Removed unnecessary `AppTheme` composables. * Minor refactor of dialogs. * Refactor: Extract common logic for setting Compose content * Introduce `setComposableContent` extension function for `ComposeView` to encapsulate setting the content with `AppTheme`. * Use `setComposableContent` instead of manually calling `setContent` with `AppTheme` in `OfflineAreasFragment`, `SyncStatusFragment`, and `AbstractTaskFragment`. * Update `createComposeView` in `AbstractFragment` to use `setComposableContent`. * Refactor: Replace custom dialogs with `ConfirmationDialog` composable - Removes `SignOutConfirmationDialog` & `LocationPermissionDialog` composables. - Replaces the custom dialog implementation with the generic `ConfirmationDialog` - Removes unused imports & code. - Use `setComposableContent` instead of `apply` to add compose view. - Removes `PermissionDeniedDialog` & replace with `ConfirmationDialog` - Removes unused `AppTheme` composable * Refactor composable dialog rendering * Rename `addComposableToRoot` to `renderComposableDialog` for better clarity. * Add helper methods `createComposeView` and `setComposableContent` in `AbstractFragment` to improve the readability. * Update all usages of `addComposableToRoot` to `renderComposableDialog`. * Refactor: Extract compose view creation logic to utility class * Refactor: Move state management out of Composable in HomeScreenFragment The changes move the `showUserDetailsDialog` and `showSignOutDialog` state variables and related functions outside of the Composable function in `HomeScreenFragment`. This fixes an issue where navigation clicks would stop working after the first time when adding a Compose view dynamically to the fragment. The state is now managed in the fragment class itself. The Composable still renders the dialogs based on these state variables. * Refactor: Simplify instructions and confirmation dialogs * Simplify `InstructionsDialog` to manage its own state, eliminating the need for external state management. * Simplify `InstructionsDialog` usages in `DrawAreaTaskFragment` and `DropPinTaskFragment`. * Simplify `ConfirmationDialog` to manage its own state, eliminating the need for external state management. * Add a preview to `InstructionsDialog`. * Refactor: Remove unnecessary MutableState from DataSharingTermsDialog * Simplify DataSharingTermsDialog by removing the MutableState parameter. * The dialog now uses an internal mutable state to manage its visibility. * Remove `showDataSharingTermsDialog` from `DataSharingTermsDialog` parameters * Remove `showDataSharingTermsDialog` from `DataSharingTermsDialog` usage in tests. * Remove `showDataSharingTermsDialog` from `HomeScreenMapContainerFragment`. * Fix unit tests * Fix import order
1 parent feae70b commit 1e0c3b1

24 files changed

+365
-552
lines changed

app/src/main/java/com/google/android/ground/MainActivity.kt

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,12 @@ package com.google.android.ground
1818
import android.app.AlertDialog
1919
import android.content.Intent
2020
import android.os.Bundle
21-
import android.view.ViewGroup
2221
import androidx.activity.OnBackPressedCallback
2322
import androidx.appcompat.app.AppCompatDelegate
2423
import androidx.compose.runtime.getValue
2524
import androidx.compose.runtime.mutableStateOf
2625
import androidx.compose.runtime.remember
2726
import androidx.compose.runtime.setValue
28-
import androidx.compose.ui.platform.ComposeView
2927
import androidx.core.view.WindowInsetsCompat
3028
import androidx.lifecycle.lifecycleScope
3129
import androidx.navigation.NavDirections
@@ -40,7 +38,7 @@ import com.google.android.ground.ui.common.modalSpinner
4038
import com.google.android.ground.ui.home.HomeScreenFragmentDirections
4139
import com.google.android.ground.ui.signin.SignInFragmentDirections
4240
import com.google.android.ground.ui.surveyselector.SurveySelectorFragmentDirections
43-
import com.google.android.ground.ui.theme.AppTheme
41+
import com.google.android.ground.util.renderComposableDialog
4442
import dagger.hilt.android.AndroidEntryPoint
4543
import javax.inject.Inject
4644
import kotlinx.coroutines.flow.filterNotNull
@@ -84,9 +82,7 @@ class MainActivity : AbstractActivity() {
8482

8583
viewModel = viewModelFactory[this, MainViewModel::class.java]
8684

87-
lifecycleScope.launch {
88-
viewModel.navigationRequests.filterNotNull().collect { updateUi(binding.root, it) }
89-
}
85+
lifecycleScope.launch { viewModel.navigationRequests.filterNotNull().collect { updateUi(it) } }
9086

9187
onBackPressedDispatcher.addCallback(
9288
this,
@@ -101,10 +97,10 @@ class MainActivity : AbstractActivity() {
10197
)
10298
}
10399

104-
private fun updateUi(viewGroup: ViewGroup, uiState: MainUiState) {
100+
private fun updateUi(uiState: MainUiState) {
105101
when (uiState) {
106102
MainUiState.OnPermissionDenied -> {
107-
showPermissionDeniedDialog(viewGroup)
103+
showPermissionDeniedDialog()
108104
}
109105
MainUiState.OnUserSignedOut -> {
110106
navigateTo(SignInFragmentDirections.showSignInScreen())
@@ -128,32 +124,25 @@ class MainActivity : AbstractActivity() {
128124
}
129125
}
130126

131-
private fun showPermissionDeniedDialog(viewGroup: ViewGroup) {
132-
viewGroup.addView(
133-
ComposeView(this).apply {
134-
setContent {
135-
AppTheme {
136-
var showDialog by remember { mutableStateOf(true) }
137-
if (showDialog) {
138-
PermissionDeniedDialog(
139-
// TODO: Read url from
140-
// Firestore config/properties/signUpUrl
141-
// Issue URL: https://github.com/google/ground-android/issues/2402
142-
BuildConfig.SIGNUP_FORM_LINK,
143-
onSignOut = {
144-
showDialog = false
145-
userRepository.signOut()
146-
},
147-
onCloseApp = {
148-
showDialog = false
149-
finish()
150-
},
151-
)
152-
}
153-
}
154-
}
127+
private fun showPermissionDeniedDialog() {
128+
renderComposableDialog {
129+
var showDialog by remember { mutableStateOf(true) }
130+
if (showDialog) {
131+
PermissionDeniedDialog(
132+
// TODO: Read url from Firestore config/properties/signUpUrl
133+
// Issue URL: https://github.com/google/ground-android/issues/2402
134+
BuildConfig.SIGNUP_FORM_LINK,
135+
onSignOut = {
136+
showDialog = false
137+
userRepository.signOut()
138+
},
139+
onCloseApp = {
140+
showDialog = false
141+
finish()
142+
},
143+
)
155144
}
156-
)
145+
}
157146
}
158147

159148
override fun onWindowInsetChanged(insets: WindowInsetsCompat) {

app/src/main/java/com/google/android/ground/ui/compose/ConfirmationDialog.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,18 @@ fun ConfirmationDialog(
3636
@StringRes confirmButtonText: Int,
3737
onConfirmClicked: () -> Unit,
3838
) {
39-
val showRemoveWarningDialog = remember { mutableStateOf(true) }
39+
val showDialog = remember { mutableStateOf(true) }
4040

4141
fun onConfirm() {
42-
showRemoveWarningDialog.value = false
42+
showDialog.value = false
4343
onConfirmClicked()
4444
}
4545

4646
fun onDismiss() {
47-
showRemoveWarningDialog.value = false
47+
showDialog.value = false
4848
}
4949

50-
if (showRemoveWarningDialog.value) {
50+
if (showDialog.value) {
5151
AlertDialog(
5252
onDismissRequest = { onDismiss() },
5353
title = { Text(text = stringResource(title)) },

app/src/main/java/com/google/android/ground/ui/datacollection/DataCollectionFragment.kt

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import android.view.ViewGroup
2323
import android.widget.ProgressBar
2424
import androidx.compose.runtime.mutableStateOf
2525
import androidx.compose.runtime.remember
26-
import androidx.compose.ui.platform.ComposeView
2726
import androidx.constraintlayout.widget.Guideline
2827
import androidx.core.view.WindowInsetsCompat
2928
import androidx.core.view.doOnLayout
@@ -39,7 +38,7 @@ import com.google.android.ground.ui.common.AbstractFragment
3938
import com.google.android.ground.ui.common.BackPressListener
4039
import com.google.android.ground.ui.compose.ConfirmationDialog
4140
import com.google.android.ground.ui.home.HomeScreenFragmentDirections
42-
import com.google.android.ground.ui.theme.AppTheme
41+
import com.google.android.ground.util.renderComposableDialog
4342
import dagger.hilt.android.AndroidEntryPoint
4443
import javax.inject.Inject
4544
import kotlinx.coroutines.Dispatchers
@@ -156,23 +155,17 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener {
156155

157156
private fun onTaskSubmitted() {
158157
// Display a confirmation dialog and move to home screen after that.
159-
(view as ViewGroup).addView(
160-
ComposeView(requireContext()).apply {
161-
setContent {
162-
val openAlertDialog = remember { mutableStateOf(true) }
163-
when {
164-
openAlertDialog.value -> {
165-
AppTheme {
166-
DataSubmissionConfirmationDialog {
167-
openAlertDialog.value = false
168-
findNavController().navigate(HomeScreenFragmentDirections.showHomeScreen())
169-
}
170-
}
171-
}
158+
renderComposableDialog {
159+
val openAlertDialog = remember { mutableStateOf(true) }
160+
when {
161+
openAlertDialog.value -> {
162+
DataSubmissionConfirmationDialog {
163+
openAlertDialog.value = false
164+
findNavController().navigate(HomeScreenFragmentDirections.showHomeScreen())
172165
}
173166
}
174167
}
175-
)
168+
}
176169
}
177170

178171
private fun updateProgressBar(taskPosition: TaskPosition, shouldAnimate: Boolean) {
@@ -202,30 +195,20 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener {
202195
}
203196

204197
private fun showExitWarningDialog() {
205-
showConfirmationDialog {
206-
isNavigatingUp = true
207-
viewModel.clearDraft()
208-
findNavController().navigateUp()
198+
renderComposableDialog {
199+
ConfirmationDialog(
200+
title = R.string.data_collection_cancellation_title,
201+
description = R.string.data_collection_cancellation_description,
202+
confirmButtonText = R.string.data_collection_cancellation_confirm_button,
203+
onConfirmClicked = {
204+
isNavigatingUp = true
205+
viewModel.clearDraft()
206+
findNavController().navigateUp()
207+
},
208+
)
209209
}
210210
}
211211

212-
private fun showConfirmationDialog(onConfirm: () -> Unit) {
213-
(view as ViewGroup).addView(
214-
ComposeView(requireContext()).apply {
215-
setContent {
216-
AppTheme {
217-
ConfirmationDialog(
218-
title = R.string.data_collection_cancellation_title,
219-
description = R.string.data_collection_cancellation_description,
220-
confirmButtonText = R.string.data_collection_cancellation_confirm_button,
221-
onConfirmClicked = { onConfirm() },
222-
)
223-
}
224-
}
225-
}
226-
)
227-
}
228-
229212
private companion object {
230213
private const val PROGRESS_SCALE = 100
231214
}

app/src/main/java/com/google/android/ground/ui/datacollection/components/InstructionsDialog.kt

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,31 +22,51 @@ import androidx.compose.material3.Icon
2222
import androidx.compose.material3.OutlinedButton
2323
import androidx.compose.material3.Text
2424
import androidx.compose.runtime.Composable
25+
import androidx.compose.runtime.mutableStateOf
26+
import androidx.compose.runtime.remember
2527
import androidx.compose.ui.Modifier
2628
import androidx.compose.ui.graphics.vector.ImageVector
2729
import androidx.compose.ui.res.stringResource
2830
import androidx.compose.ui.res.vectorResource
31+
import androidx.compose.ui.tooling.preview.Preview
2932
import androidx.compose.ui.unit.dp
3033
import androidx.compose.ui.unit.sp
34+
import com.google.android.ground.ExcludeFromJacocoGeneratedReport
3135
import com.google.android.ground.R
36+
import com.google.android.ground.ui.theme.AppTheme
3237

3338
@Composable
34-
fun InstructionsDialog(iconId: Int, stringId: Int, onDismissRequest: () -> Unit) {
35-
AlertDialog(
36-
icon = {
37-
Icon(
38-
imageVector = ImageVector.vectorResource(id = iconId),
39-
contentDescription = "",
40-
modifier = Modifier.width(48.dp).height(48.dp),
41-
)
42-
},
43-
title = { Text(text = stringResource(stringId), fontSize = 18.sp) },
44-
onDismissRequest = {}, // Prevent dismissing the dialog by clicking outside
45-
confirmButton = {}, // Hide confirm button
46-
dismissButton = {
47-
OutlinedButton(onClick = { onDismissRequest() }) {
48-
Text(text = stringResource(R.string.close))
49-
}
50-
},
51-
)
39+
fun InstructionsDialog(iconId: Int, stringId: Int) {
40+
val showDialog = remember { mutableStateOf(true) }
41+
if (showDialog.value) {
42+
AlertDialog(
43+
icon = {
44+
Icon(
45+
imageVector = ImageVector.vectorResource(id = iconId),
46+
contentDescription = "",
47+
modifier = Modifier.width(48.dp).height(48.dp),
48+
)
49+
},
50+
title = { Text(text = stringResource(stringId), fontSize = 18.sp) },
51+
onDismissRequest = {}, // Prevent dismissing the dialog by clicking outside
52+
confirmButton = {}, // Hide confirm button
53+
dismissButton = {
54+
OutlinedButton(onClick = { showDialog.value = false }) {
55+
Text(text = stringResource(R.string.close))
56+
}
57+
},
58+
)
59+
}
60+
}
61+
62+
@Composable
63+
@Preview
64+
@ExcludeFromJacocoGeneratedReport
65+
fun PreviewInstructionsDialog() {
66+
AppTheme {
67+
InstructionsDialog(
68+
iconId = R.drawable.touch_app_24,
69+
stringId = R.string.draw_area_task_instruction,
70+
)
71+
}
5272
}

app/src/main/java/com/google/android/ground/ui/datacollection/tasks/AbstractTaskFragment.kt

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import androidx.compose.runtime.mutableStateOf
2929
import androidx.compose.runtime.saveable.rememberSaveable
3030
import androidx.compose.runtime.setValue
3131
import androidx.compose.ui.Modifier
32-
import androidx.compose.ui.platform.ComposeView
3332
import androidx.compose.ui.unit.dp
3433
import androidx.core.view.doOnAttach
3534
import androidx.hilt.navigation.fragment.hiltNavGraphViewModels
@@ -45,7 +44,8 @@ import com.google.android.ground.ui.datacollection.components.ButtonAction
4544
import com.google.android.ground.ui.datacollection.components.LoiNameDialog
4645
import com.google.android.ground.ui.datacollection.components.TaskButton
4746
import com.google.android.ground.ui.datacollection.components.TaskView
48-
import com.google.android.ground.ui.theme.AppTheme
47+
import com.google.android.ground.util.renderComposableDialog
48+
import com.google.android.ground.util.setComposableContent
4949
import kotlin.properties.Delegates
5050
import kotlinx.coroutines.launch
5151
import org.jetbrains.annotations.TestOnly
@@ -219,19 +219,15 @@ abstract class AbstractTaskFragment<T : AbstractTaskViewModel> : AbstractFragmen
219219

220220
/** Adds the action buttons to the UI. */
221221
private fun renderButtons() {
222-
taskView.actionButtonsContainer.composeView.apply {
223-
setContent {
224-
AppTheme {
225-
Row(
226-
horizontalArrangement = Arrangement.SpaceBetween,
227-
modifier = Modifier.fillMaxWidth().padding(8.dp),
228-
) {
229-
// TODO: Previous button should always be positioned to the left of the screen.
230-
// Rest buttons should be aligned to the right side of the screen.
231-
// Issue URL: https://github.com/google/ground-android/issues/2417
232-
buttonDataList.sortedBy { it.index }.forEach { (_, button) -> button.CreateButton() }
233-
}
234-
}
222+
taskView.actionButtonsContainer.composeView.setComposableContent {
223+
Row(
224+
horizontalArrangement = Arrangement.SpaceBetween,
225+
modifier = Modifier.fillMaxWidth().padding(8.dp),
226+
) {
227+
// TODO: Previous button should always be positioned to the left of the screen.
228+
// Rest buttons should be aligned to the right side of the screen.
229+
// Issue URL: https://github.com/google/ground-android/issues/2417
230+
buttonDataList.sortedBy { it.index }.forEach { (_, button) -> button.CreateButton() }
235231
}
236232
}
237233
}
@@ -245,18 +241,12 @@ abstract class AbstractTaskFragment<T : AbstractTaskViewModel> : AbstractFragmen
245241

246242
private fun launchLoiNameDialog() {
247243
dataCollectionViewModel.loiNameDialogOpen.value = true
248-
(view as ViewGroup).addView(
249-
ComposeView(requireContext()).apply {
250-
setContent {
251-
AppTheme {
252-
// The LOI NameDialog should call `handleLoiNameSet()` to continue to the next task.
253-
ShowLoiNameDialog(dataCollectionViewModel.loiName.value ?: "") {
254-
handleLoiNameSet(loiName = it)
255-
}
256-
}
257-
}
244+
renderComposableDialog {
245+
// The LOI NameDialog should call `handleLoiNameSet()` to continue to the next task.
246+
ShowLoiNameDialog(dataCollectionViewModel.loiName.value ?: "") {
247+
handleLoiNameSet(loiName = it)
258248
}
259-
)
249+
}
260250
}
261251

262252
@Composable

0 commit comments

Comments
 (0)