Skip to content

Commit d73a3a3

Browse files
committed
Separate UI-specific state from view model
1 parent bca043e commit d73a3a3

File tree

7 files changed

+110
-43
lines changed

7 files changed

+110
-43
lines changed

app/src/main/java/app/grapheneos/camera/ktx/SnackbarHostState.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ suspend fun SnackbarHostState.showOrReplaceSnackbar(
1010
duration: SnackbarDuration =
1111
if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite
1212
) {
13-
currentSnackbarData?.dismiss()
13+
dismissSnackBarIfVisible()
1414
showSnackbar(
1515
message = message,
1616
actionLabel = actionLabel,
1717
withDismissAction = withDismissAction,
1818
duration = duration
1919
)
20+
}
21+
22+
fun SnackbarHostState.dismissSnackBarIfVisible() {
23+
currentSnackbarData?.dismiss()
2024
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package app.grapheneos.camera.ui.composable.component
2+
3+
import androidx.compose.material3.SnackbarHostState
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.LaunchedEffect
6+
import app.grapheneos.camera.ktx.dismissSnackBarIfVisible
7+
import app.grapheneos.camera.ktx.showOrReplaceSnackbar
8+
import app.grapheneos.camera.ui.composable.model.NoDataSnackBarMessage
9+
import app.grapheneos.camera.ui.composable.model.SnackBarMessage
10+
11+
@Composable
12+
fun SnackBarMessageHandler(
13+
snackBarHostState: SnackbarHostState,
14+
snackBarMessage: SnackBarMessage
15+
) {
16+
LaunchedEffect(snackBarMessage) {
17+
if (snackBarMessage == NoDataSnackBarMessage) {
18+
snackBarHostState.dismissSnackBarIfVisible()
19+
} else {
20+
snackBarHostState.showOrReplaceSnackbar(snackBarMessage.message)
21+
}
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package app.grapheneos.camera.ui.composable.model
2+
3+
import java.util.UUID
4+
5+
data class SnackBarMessage(
6+
val message: String,
7+
val id: UUID = UUID.randomUUID()
8+
)
9+
10+
val NoDataSnackBarMessage = SnackBarMessage("")

app/src/main/java/app/grapheneos/camera/ui/composable/screen/ui/ExtendedGalleryScreen.kt

+13-1
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ import androidx.compose.material3.IconButton
2727
import androidx.compose.material3.MaterialTheme
2828
import androidx.compose.material3.Scaffold
2929
import androidx.compose.material3.SnackbarHost
30+
import androidx.compose.material3.SnackbarHostState
3031
import androidx.compose.material3.Text
3132
import androidx.compose.runtime.Composable
33+
import androidx.compose.runtime.remember
3234

3335
import androidx.compose.ui.Alignment
3436
import androidx.compose.ui.Modifier
@@ -43,6 +45,7 @@ import app.grapheneos.camera.CapturedItem
4345
import app.grapheneos.camera.R
4446
import app.grapheneos.camera.ktx.header
4547
import app.grapheneos.camera.ktx.requestDeviceUnlock
48+
import app.grapheneos.camera.ui.composable.component.SnackBarMessageHandler
4649
import app.grapheneos.camera.ui.composable.component.dialog.MultipleFileDeletionDialog
4750
import app.grapheneos.camera.ui.composable.component.mediapreview.SQUARE_MEDIA_PREVIEW_SIZE
4851
import app.grapheneos.camera.ui.composable.component.mediapreview.SquareMediaPreview
@@ -62,6 +65,15 @@ fun ExtendedGalleryScreen(
6265
ExtendedGalleryViewModel(context)
6366
}
6467

68+
val snackBarHostState = remember {
69+
SnackbarHostState()
70+
}
71+
72+
SnackBarMessageHandler(
73+
snackBarHostState = snackBarHostState,
74+
snackBarMessage = viewModel.snackBarMessage,
75+
)
76+
6577
BackHandler {
6678
if (viewModel.selectMode) {
6779
viewModel.exitSelectionMode()
@@ -83,7 +95,7 @@ fun ExtendedGalleryScreen(
8395

8496
Scaffold (
8597
snackbarHost = {
86-
SnackbarHost(viewModel.snackBarHostState)
98+
SnackbarHost(snackBarHostState)
8799
},
88100

89101
topBar = {

app/src/main/java/app/grapheneos/camera/ui/composable/screen/ui/GalleryScreen.kt

+22-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxSize
1616
import androidx.compose.foundation.layout.padding
1717
import androidx.compose.foundation.layout.wrapContentHeight
1818
import androidx.compose.foundation.pager.HorizontalPager
19+
import androidx.compose.foundation.pager.rememberPagerState
1920

2021
import androidx.compose.foundation.shape.CircleShape
2122
import androidx.compose.material.icons.Icons
@@ -24,6 +25,7 @@ import androidx.compose.material3.FloatingActionButton
2425
import androidx.compose.material3.Icon
2526
import androidx.compose.material3.Scaffold
2627
import androidx.compose.material3.SnackbarHost
28+
import androidx.compose.material3.SnackbarHostState
2729
import androidx.compose.material3.Text
2830

2931
import androidx.compose.runtime.Composable
@@ -63,6 +65,7 @@ import me.saket.telephoto.zoomable.zoomable
6365

6466
import app.grapheneos.camera.ITEM_TYPE_IMAGE
6567
import app.grapheneos.camera.R
68+
import app.grapheneos.camera.ui.composable.component.SnackBarMessageHandler
6669
import app.grapheneos.camera.ui.composable.component.tooltip.QuickTooltip
6770
import app.grapheneos.camera.ui.composable.component.tooltip.QuickTooltipVerticalDirection
6871

@@ -90,10 +93,23 @@ fun GalleryScreen(
9093

9194
val coroutineScope = rememberCoroutineScope()
9295

96+
val snackBarHostState = remember {
97+
SnackbarHostState()
98+
}
99+
93100
val viewModel = viewModel {
94101
GalleryViewModel(context)
95102
}
96103

104+
val pagerState = rememberPagerState {
105+
viewModel.capturedItems.size
106+
}
107+
108+
SnackBarMessageHandler(
109+
snackBarHostState = snackBarHostState,
110+
snackBarMessage = viewModel.snackBarMessage
111+
)
112+
97113
val backgroundColor by animateColorAsState(
98114
label = "background_color_animation",
99115
targetValue = if (viewModel.inFocusMode) Color.Black else AppColor.BackgroundColor,
@@ -111,7 +127,7 @@ fun GalleryScreen(
111127

112128
val focusIndex = viewModel.capturedItems.indexOf(focusItem)
113129
if (focusIndex != -1) {
114-
viewModel.pagerState.scrollToPage(focusIndex)
130+
pagerState.scrollToPage(focusIndex)
115131
}
116132
}
117133

@@ -121,8 +137,9 @@ fun GalleryScreen(
121137
}
122138

123139
// Update the current focus item when the user slides between pages
124-
LaunchedEffect(viewModel.pagerState.currentPage) {
125-
val page = viewModel.pagerState.currentPage
140+
LaunchedEffect(pagerState.currentPage) {
141+
val page = pagerState.currentPage
142+
viewModel.currentPage = page
126143
if (page < viewModel.capturedItems.size) {
127144
viewModel.focusItem = viewModel.capturedItems[page]
128145
}
@@ -169,7 +186,7 @@ fun GalleryScreen(
169186
Scaffold(
170187
containerColor = backgroundColor,
171188

172-
snackbarHost = { SnackbarHost(hostState = viewModel.snackbarHostState) },
189+
snackbarHost = { SnackbarHost(hostState = snackBarHostState) },
173190

174191
floatingActionButton = {
175192
AnimatedVisibility(
@@ -239,7 +256,7 @@ fun GalleryScreen(
239256
} else {
240257
if (viewModel.hasCapturedItems) {
241258
HorizontalPager(
242-
state = viewModel.pagerState,
259+
state = pagerState,
243260
userScrollEnabled = !viewModel.isZoomedIn,
244261
beyondViewportPageCount = 1,
245262
modifier = Modifier

app/src/main/java/app/grapheneos/camera/ui/composable/screen/viewmodel/ExtendedGalleryViewModel.kt

+16-19
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import androidx.compose.runtime.mutableStateOf
99
import androidx.compose.runtime.setValue
1010
import androidx.compose.runtime.snapshots.SnapshotStateList
1111
import androidx.lifecycle.ViewModel
12-
import androidx.compose.material3.SnackbarHostState
1312
import androidx.lifecycle.viewModelScope
1413

1514
import app.grapheneos.camera.CapturedItem
1615
import app.grapheneos.camera.R
1716
import app.grapheneos.camera.ktx.isDeviceLocked
18-
import app.grapheneos.camera.ktx.showOrReplaceSnackbar
17+
import app.grapheneos.camera.ui.composable.model.NoDataSnackBarMessage
18+
import app.grapheneos.camera.ui.composable.model.SnackBarMessage
1919
import app.grapheneos.camera.util.getMimeTypeForItems
2020
import kotlinx.coroutines.Dispatchers
2121
import kotlinx.coroutines.async
@@ -58,14 +58,21 @@ class ExtendedGalleryViewModel(context: Context) : ViewModel() {
5858

5959
var isDeletionDialogVisible by mutableStateOf(false)
6060

61-
val snackBarHostState = SnackbarHostState()
61+
var snackBarMessage by mutableStateOf<SnackBarMessage>(NoDataSnackBarMessage)
62+
private set
63+
64+
fun showSnackBar(message: String) {
65+
snackBarMessage = SnackBarMessage(message)
66+
}
67+
68+
fun hideSnackBar() {
69+
snackBarMessage = NoDataSnackBarMessage
70+
}
6271

6372
fun showDeletionDialog(context: Context) {
6473
viewModelScope.launch {
6574
if (selectedItems.isEmpty()) {
66-
snackBarHostState.showOrReplaceSnackbar(
67-
context.getString(R.string.select_an_item_request)
68-
)
75+
showSnackBar(context.getString(R.string.select_an_item_request))
6976
return@launch
7077
}
7178
isDeletionDialogVisible = true
@@ -165,16 +172,12 @@ class ExtendedGalleryViewModel(context: Context) : ViewModel() {
165172
fun shareSelectedItems(context: Context) {
166173
viewModelScope.launch {
167174
if (context.isDeviceLocked()) {
168-
snackBarHostState.showOrReplaceSnackbar(
169-
context.getString(R.string.sharing_not_allowed)
170-
)
175+
showSnackBar(context.getString(R.string.sharing_not_allowed))
171176
return@launch
172177
}
173178

174179
if (selectedItems.isEmpty()) {
175-
snackBarHostState.showOrReplaceSnackbar(
176-
context.getString(R.string.select_an_item_request)
177-
)
180+
showSnackBar(context.getString(R.string.select_an_item_request))
178181
return@launch
179182
}
180183

@@ -218,13 +221,7 @@ class ExtendedGalleryViewModel(context: Context) : ViewModel() {
218221
}
219222

220223
if (failedDeletions != 0) {
221-
snackBarHostState.showOrReplaceSnackbar(
222-
context.getString(
223-
R.string.failed_multiple_deletion_message,
224-
failedDeletions,
225-
selectedItemsSize
226-
)
227-
)
224+
showSnackBar(context.getString(R.string.failed_multiple_deletion_message, failedDeletions, selectedItemsSize))
228225
}
229226
}
230227
}

app/src/main/java/app/grapheneos/camera/ui/composable/screen/viewmodel/GalleryViewModel.kt

+21-17
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import android.content.Intent
66

77
import android.util.Log
88
import android.widget.Toast
9-
import androidx.compose.foundation.pager.PagerState
10-
import androidx.compose.material3.SnackbarHostState
119
import androidx.compose.runtime.derivedStateOf
1210
import androidx.compose.runtime.getValue
1311
import androidx.compose.runtime.mutableStateOf
@@ -21,8 +19,9 @@ import androidx.lifecycle.viewModelScope
2119
import app.grapheneos.camera.CapturedItem
2220
import app.grapheneos.camera.R
2321
import app.grapheneos.camera.ktx.isDeviceLocked
24-
import app.grapheneos.camera.ktx.showOrReplaceSnackbar
2522
import app.grapheneos.camera.ui.composable.model.MediaItemDetails
23+
import app.grapheneos.camera.ui.composable.model.NoDataSnackBarMessage
24+
import app.grapheneos.camera.ui.composable.model.SnackBarMessage
2625
import kotlinx.coroutines.launch
2726

2827
private const val TAG = "GalleryViewModel"
@@ -39,8 +38,10 @@ class GalleryViewModel(context: Context) : ViewModel() {
3938

4039
var deletionItem by mutableStateOf<CapturedItem?>(null)
4140

41+
var currentPage = 0
42+
4243
private val currentItem: CapturedItem
43-
get() = capturedItems[pagerState.currentPage]
44+
get() = capturedItems[currentPage]
4445

4546
private val capturedItemsViewModel = CapturedItemsRepository.get(context)
4647

@@ -54,18 +55,21 @@ class GalleryViewModel(context: Context) : ViewModel() {
5455
capturedItemsViewModel.isLoading
5556
}
5657

57-
val snackbarHostState = SnackbarHostState()
58+
var snackBarMessage by mutableStateOf<SnackBarMessage>(NoDataSnackBarMessage)
59+
private set
5860

59-
val pagerState = PagerState(
60-
pageCount = {
61-
capturedItems.size
62-
}
63-
)
61+
fun showSnackBar(message: String) {
62+
snackBarMessage = SnackBarMessage(message)
63+
}
64+
65+
fun hideSnackBar() {
66+
snackBarMessage = NoDataSnackBarMessage
67+
}
6468

6569
fun displayMediaInfo(context: Context, item: CapturedItem = currentItem) {
6670
viewModelScope.launch {
6771
if (!hasCapturedItems) {
68-
snackbarHostState.showOrReplaceSnackbar(
72+
showSnackBar(
6973
context.getString(R.string.unable_to_obtain_file_details)
7074
)
7175
return@launch
@@ -76,7 +80,7 @@ class GalleryViewModel(context: Context) : ViewModel() {
7680
} catch (e: Exception) {
7781
Log.i(TAG, "Unable to obtain file details for MediaInfoDialog")
7882
e.printStackTrace()
79-
snackbarHostState.showOrReplaceSnackbar(
83+
showSnackBar(
8084
context.getString(R.string.unable_to_obtain_file_details)
8185
)
8286
}
@@ -112,12 +116,12 @@ class GalleryViewModel(context: Context) : ViewModel() {
112116
.show()
113117
onLastItemDeletion()
114118
} else {
115-
snackbarHostState.showOrReplaceSnackbar(
119+
showSnackBar(
116120
context.getString(R.string.deleted_successfully)
117121
)
118122
}
119123
} else {
120-
snackbarHostState.showOrReplaceSnackbar(
124+
showSnackBar(
121125
context.getString(R.string.deleting_unexpected_error)
122126
)
123127
}
@@ -138,7 +142,7 @@ class GalleryViewModel(context: Context) : ViewModel() {
138142
) {
139143
viewModelScope.launch {
140144
if (context.isDeviceLocked()) {
141-
snackbarHostState.showOrReplaceSnackbar(
145+
showSnackBar(
142146
context.getString(R.string.edit_not_allowed)
143147
)
144148
return@launch
@@ -166,7 +170,7 @@ class GalleryViewModel(context: Context) : ViewModel() {
166170
try {
167171
context.startActivity(editIntent)
168172
} catch (ignored: ActivityNotFoundException) {
169-
snackbarHostState.showOrReplaceSnackbar(
173+
showSnackBar(
170174
context.getString(R.string.no_editor_app_error)
171175
)
172176
}
@@ -180,7 +184,7 @@ class GalleryViewModel(context: Context) : ViewModel() {
180184
) {
181185
viewModelScope.launch {
182186
if (context.isDeviceLocked()) {
183-
snackbarHostState.showOrReplaceSnackbar(
187+
showSnackBar(
184188
context.getString(R.string.sharing_not_allowed)
185189
)
186190
return@launch

0 commit comments

Comments
 (0)