Skip to content
This repository was archived by the owner on Jan 5, 2023. It is now read-only.

Commit 549a69e

Browse files
JoseAlcerrecaGerrit Code Review
authored andcommitted
Merge changes I84c9b9af,I387740d8 into main
* changes: Migrates Snackbar manager to expose a StateFlow Migrates Schedule to Flow
2 parents 7b2ca49 + eb8b394 commit 549a69e

File tree

7 files changed

+316
-307
lines changed

7 files changed

+316
-307
lines changed

mobile/src/main/java/com/google/samples/apps/iosched/ui/SnackbarMessageFragmentExtensions.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,14 @@ fun Fragment.setUpSnackbar(
8585
setupSnackbarManager(snackbarMessageManager, fadingSnackbar, actionClickListener)
8686
}
8787

88-
private fun Fragment.setupSnackbarManager(
88+
fun Fragment.setupSnackbarManager(
8989
snackbarMessageManager: SnackbarMessageManager,
9090
fadingSnackbar: FadingSnackbar,
9191
actionClickListener: () -> Unit
9292
) {
93-
snackbarMessageManager.observeNextMessage().observe(
94-
viewLifecycleOwner,
95-
EventObserver { message ->
93+
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
94+
snackbarMessageManager.currentSnackbar.collect { message ->
95+
if (message == null) { return@collect }
9696
val messageText = HtmlCompat.fromHtml(
9797
requireContext().getString(message.messageId, message.session?.title),
9898
FROM_HTML_MODE_LEGACY
@@ -107,8 +107,10 @@ private fun Fragment.setupSnackbarManager(
107107
},
108108
// When the snackbar is dismissed, ping the snackbar message manager in case there
109109
// are pending messages.
110-
dismissListener = { snackbarMessageManager.loadNextMessage() }
110+
dismissListener = {
111+
snackbarMessageManager.removeMessageAndLoadNext(message)
112+
}
111113
)
112114
}
113-
)
115+
}
114116
}

mobile/src/main/java/com/google/samples/apps/iosched/ui/messages/SnackbarMessageManager.kt

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
package com.google.samples.apps.iosched.ui.messages
1818

1919
import androidx.annotation.VisibleForTesting
20-
import androidx.lifecycle.MutableLiveData
2120
import com.google.samples.apps.iosched.R
2221
import com.google.samples.apps.iosched.shared.data.prefs.PreferenceStorage
23-
import com.google.samples.apps.iosched.shared.result.Event
2422
import com.google.samples.apps.iosched.ui.SnackbarMessage
2523
import com.google.samples.apps.iosched.ui.messages.SnackbarMessageManager.Companion.MAX_ITEMS
24+
import kotlinx.coroutines.flow.MutableStateFlow
25+
import kotlinx.coroutines.flow.StateFlow
26+
import timber.log.Timber
2627
import javax.inject.Inject
2728
import javax.inject.Singleton
2829

@@ -47,48 +48,46 @@ open class SnackbarMessageManager @Inject constructor(
4748
const val MAX_ITEMS = 10
4849
}
4950

50-
private val messages = mutableListOf<Event<SnackbarMessage>>()
51+
private val messages = mutableListOf<SnackbarMessage>()
5152

52-
private val result = MutableLiveData<Event<SnackbarMessage>>()
53+
private val _currentSnackbar = MutableStateFlow<SnackbarMessage?>(null)
54+
val currentSnackbar: StateFlow<SnackbarMessage?> = _currentSnackbar
5355

5456
fun addMessage(msg: SnackbarMessage) {
55-
if (isSnackbarShouldBeIgnored(msg)) {
57+
if (shouldSnackbarBeIgnored(msg)) {
5658
return
5759
}
58-
// If the new message is about the same change as a pending one, keep the new one. (rare)
59-
val sameRequestId = messages.filter {
60-
it.peekContent().requestChangeId == msg.requestChangeId && !it.hasBeenHandled
61-
}
62-
if (sameRequestId.isNotEmpty()) {
63-
messages.removeAll(sameRequestId)
64-
}
65-
66-
// If the new message is about a change that was already notified, ignore it.
67-
val alreadyHandledWithSameId = messages.filter {
68-
it.peekContent().requestChangeId == msg.requestChangeId && it.hasBeenHandled
60+
// Limit amount of pending messages
61+
if (messages.size > MAX_ITEMS) {
62+
Timber.e("Too many Snackbar messages. Message id: ${msg.messageId}")
63+
return
6964
}
70-
71-
// Only add the message if it hasn't been handled before
72-
if (alreadyHandledWithSameId.isEmpty()) {
73-
messages.add(Event(msg))
74-
loadNextMessage()
65+
// If the new message is about the same change as a pending one, keep the old one. (rare)
66+
val sameRequestId = messages.find {
67+
it.requestChangeId == msg.requestChangeId
7568
}
76-
77-
// Remove old messages
78-
if (messages.size > MAX_ITEMS) {
79-
messages.retainAll(messages.drop(messages.size - MAX_ITEMS))
69+
if (sameRequestId == null) {
70+
messages.add(msg)
8071
}
72+
loadNext()
8173
}
8274

83-
fun loadNextMessage() {
84-
result.postValue(messages.firstOrNull { !it.hasBeenHandled })
75+
private fun loadNext() {
76+
if (_currentSnackbar.value == null) {
77+
_currentSnackbar.value = messages.firstOrNull()
78+
}
8579
}
8680

87-
fun observeNextMessage(): MutableLiveData<Event<SnackbarMessage>> {
88-
return result
81+
fun removeMessageAndLoadNext(shownMsg: SnackbarMessage?) {
82+
messages.removeAll { it == shownMsg }
83+
if (_currentSnackbar.value == shownMsg) {
84+
_currentSnackbar.value = null
85+
}
86+
loadNext()
8987
}
9088

91-
private fun isSnackbarShouldBeIgnored(msg: SnackbarMessage): Boolean {
89+
private fun shouldSnackbarBeIgnored(msg: SnackbarMessage): Boolean {
90+
// TODO: This should call a suspend fun (migrate to datastore)
9291
return preferenceStorage.observableSnackbarIsStopped.value == true &&
9392
msg.actionId == R.string.dont_show
9493
}

mobile/src/main/java/com/google/samples/apps/iosched/ui/schedule/ScheduleFragment.kt

Lines changed: 31 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import android.widget.Toast
2424
import androidx.core.view.updatePaddingRelative
2525
import androidx.fragment.app.activityViewModels
2626
import androidx.fragment.app.viewModels
27-
import androidx.lifecycle.Observer
27+
import androidx.lifecycle.lifecycleScope
2828
import androidx.navigation.fragment.findNavController
2929
import androidx.recyclerview.widget.DefaultItemAnimator
3030
import androidx.recyclerview.widget.LinearLayoutManager
@@ -47,8 +47,12 @@ import com.google.samples.apps.iosched.ui.messages.SnackbarMessageManager
4747
import com.google.samples.apps.iosched.ui.prefs.SnackbarPreferenceViewModel
4848
import com.google.samples.apps.iosched.ui.schedule.ScheduleFragmentDirections.Companion.toSearch
4949
import com.google.samples.apps.iosched.ui.schedule.ScheduleFragmentDirections.Companion.toSessionDetail
50+
import com.google.samples.apps.iosched.ui.schedule.ScheduleNavigationAction.NavigateToSession
51+
import com.google.samples.apps.iosched.ui.schedule.ScheduleNavigationAction.NavigateToSignInDialogAction
52+
import com.google.samples.apps.iosched.ui.schedule.ScheduleNavigationAction.NavigateToSignOutDialogAction
53+
import com.google.samples.apps.iosched.ui.schedule.ScheduleNavigationAction.ShowScheduleUiHints
5054
import com.google.samples.apps.iosched.ui.sessioncommon.SessionsAdapter
51-
import com.google.samples.apps.iosched.ui.setUpSnackbar
55+
import com.google.samples.apps.iosched.ui.setupSnackbarManager
5256
import com.google.samples.apps.iosched.ui.signin.NotificationsPreferenceDialogFragment
5357
import com.google.samples.apps.iosched.ui.signin.NotificationsPreferenceDialogFragment.Companion.DIALOG_NOTIFICATIONS_PREFERENCE
5458
import com.google.samples.apps.iosched.ui.signin.SignInDialogFragment
@@ -62,6 +66,7 @@ import com.google.samples.apps.iosched.widget.BubbleDecoration
6266
import com.google.samples.apps.iosched.widget.FadingSnackbar
6367
import com.google.samples.apps.iosched.widget.JumpSmoothScroller
6468
import dagger.hilt.android.AndroidEntryPoint
69+
import kotlinx.coroutines.flow.collect
6570
import javax.inject.Inject
6671
import javax.inject.Named
6772

@@ -115,7 +120,7 @@ class ScheduleFragment : MainNavigationFragment() {
115120
inflater: LayoutInflater,
116121
container: ViewGroup?,
117122
savedInstanceState: Bundle?
118-
): View? {
123+
): View {
119124
binding = FragmentScheduleBinding.inflate(inflater, container, false).apply {
120125
lifecycleOwner = viewLifecycleOwner
121126
viewModel = scheduleViewModel
@@ -146,8 +151,8 @@ class ScheduleFragment : MainNavigationFragment() {
146151
}
147152

148153
// Snackbar configuration
149-
setUpSnackbar(
150-
scheduleViewModel.snackBarMessage, snackbar, snackbarMessageManager,
154+
setupSnackbarManager(
155+
snackbarMessageManager, snackbar,
151156
actionClickListener = {
152157
snackbarPrefsViewModel.onStopClicked()
153158
}
@@ -193,18 +198,14 @@ class ScheduleFragment : MainNavigationFragment() {
193198
dayIndicatorRecyclerView.adapter = dayIndicatorAdapter
194199

195200
// Start observing ViewModels
196-
scheduleViewModel.scheduleUiData.observe(
197-
viewLifecycleOwner,
198-
Observer {
199-
it ?: return@Observer
200-
updateScheduleUi(it)
201-
}
202-
)
201+
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
202+
scheduleViewModel.scheduleUiData.collect { updateScheduleUi(it) }
203+
}
203204

204205
// During conference, scroll to current event.
205-
scheduleViewModel.scrollToEvent.observe(
206-
viewLifecycleOwner,
207-
EventObserver { scrollEvent ->
206+
207+
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
208+
scheduleViewModel.scrollToEvent.collect { scrollEvent ->
208209
if (scrollEvent.targetPosition != -1) {
209210
scheduleRecyclerView.run {
210211
post {
@@ -219,36 +220,19 @@ class ScheduleFragment : MainNavigationFragment() {
219220
}
220221
}
221222
}
222-
)
223-
224-
scheduleViewModel.navigateToSessionAction.observe(
225-
viewLifecycleOwner,
226-
EventObserver { sessionId ->
227-
openSessionDetail(sessionId)
228-
}
229-
)
230-
231-
scheduleViewModel.navigateToSignInDialogAction.observe(
232-
viewLifecycleOwner,
233-
EventObserver {
234-
openSignInDialog()
235-
}
236-
)
237-
238-
scheduleViewModel.navigateToSignOutDialogAction.observe(
239-
viewLifecycleOwner,
240-
EventObserver {
241-
openSignOutDialog()
242-
}
243-
)
244-
scheduleViewModel.scheduleUiHintsShown.observe(
245-
viewLifecycleOwner,
246-
EventObserver {
247-
if (!it) {
248-
openScheduleUiHintsDialog()
223+
}
224+
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
225+
scheduleViewModel.navigationActions.collect {
226+
when (it) {
227+
is NavigateToSession -> openSessionDetail(it.sessionId)
228+
is NavigateToSignInDialogAction -> openSignInDialog()
229+
is NavigateToSignOutDialogAction -> openSignOutDialog()
230+
is ShowScheduleUiHints -> openScheduleUiHintsDialog()
249231
}
250232
}
251-
)
233+
}
234+
235+
// TODO: Migrate to StateFlow
252236
scheduleViewModel.shouldShowNotificationsPrefAction.observe(
253237
viewLifecycleOwner,
254238
EventObserver {
@@ -259,13 +243,11 @@ class ScheduleFragment : MainNavigationFragment() {
259243
)
260244

261245
// Show an error message
262-
scheduleViewModel.errorMessage.observe(
263-
viewLifecycleOwner,
264-
EventObserver { errorMsg ->
265-
// TODO: Change once there's a way to show errors to the user
266-
Toast.makeText(this.context, errorMsg, Toast.LENGTH_LONG).show()
246+
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
247+
scheduleViewModel.errorMessage.collect { errorMsg ->
248+
Toast.makeText(context, errorMsg, Toast.LENGTH_LONG).show()
267249
}
268-
)
250+
}
269251

270252
if (savedInstanceState == null) {
271253
// VM outlives the UI, so reset this flag when a new Schedule page is shown
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2021 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.samples.apps.iosched.ui.schedule
18+
19+
import com.google.samples.apps.iosched.model.SessionId
20+
21+
sealed class ScheduleNavigationAction {
22+
object NavigateToSignInDialogAction : ScheduleNavigationAction()
23+
object NavigateToSignOutDialogAction : ScheduleNavigationAction()
24+
class NavigateToSession(val sessionId: SessionId) : ScheduleNavigationAction()
25+
object ShowScheduleUiHints : ScheduleNavigationAction()
26+
}

0 commit comments

Comments
 (0)