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

Commit 2ddf3f2

Browse files
JoseAlcerrecaGerrit Code Review
authored andcommitted
Merge "Migrates Map screen to Flows" into main
2 parents 91e7d21 + d388ee1 commit 2ddf3f2

File tree

6 files changed

+99
-93
lines changed

6 files changed

+99
-93
lines changed

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

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import androidx.core.widget.NestedScrollView
3636
import androidx.fragment.app.DialogFragment
3737
import androidx.fragment.app.activityViewModels
3838
import androidx.fragment.app.viewModels
39+
import androidx.lifecycle.Lifecycle
3940
import androidx.lifecycle.lifecycleScope
4041
import com.google.android.gms.maps.MapView
4142
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -48,6 +49,7 @@ import com.google.samples.apps.iosched.ui.MainActivityViewModel
4849
import com.google.samples.apps.iosched.ui.MainNavigationFragment
4950
import com.google.samples.apps.iosched.ui.signin.setupProfileMenuItem
5051
import com.google.samples.apps.iosched.util.doOnApplyWindowInsets
52+
import com.google.samples.apps.iosched.util.launchAndRepeatWithViewLifecycle
5153
import com.google.samples.apps.iosched.util.slideOffsetToAlpha
5254
import com.google.samples.apps.iosched.widget.BottomSheetBehavior
5355
import com.google.samples.apps.iosched.widget.BottomSheetBehavior.BottomSheetCallback
@@ -129,14 +131,14 @@ class MapFragment : MainNavigationFragment() {
129131
}
130132

131133
if (savedInstanceState == null) {
132-
MapFragmentArgs.fromBundle(arguments ?: Bundle.EMPTY).run {
133-
if (!featureId.isNullOrEmpty()) {
134-
viewModel.requestHighlightFeature(featureId)
134+
MapFragmentArgs.fromBundle(arguments ?: Bundle.EMPTY).let { it ->
135+
if (!it.featureId.isNullOrEmpty()) {
136+
viewModel.requestHighlightFeature(it.featureId)
135137
}
136138

137139
val variant = when {
138-
mapVariant != null -> MapVariant.valueOf(mapVariant)
139-
startTime > 0L -> MapVariant.forTime(Instant.ofEpochMilli(startTime))
140+
it.mapVariant != null -> MapVariant.valueOf(it.mapVariant)
141+
it.startTime > 0L -> MapVariant.forTime(Instant.ofEpochMilli(it.startTime))
140142
else -> MapVariant.forTime()
141143
}
142144
viewModel.setMapVariant(variant)
@@ -265,7 +267,7 @@ class MapFragment : MainNavigationFragment() {
265267
}
266268

267269
// Initialize MapView
268-
lifecycleScope.launchWhenCreated {
270+
launchAndRepeatWithViewLifecycle(Lifecycle.State.CREATED) {
269271
mapView.awaitMap().apply {
270272
setOnMapClickListener { viewModel.dismissFeatureDetails() }
271273
setOnCameraMoveListener {
@@ -275,22 +277,46 @@ class MapFragment : MainNavigationFragment() {
275277
}
276278
}
277279

278-
// Observe ViewModel data
279-
viewModel.mapVariant.observe(viewLifecycleOwner) {
280-
lifecycleScope.launchWhenCreated {
281-
mapView.awaitMap().apply {
282-
clear()
283-
viewModel.loadMapFeatures(this)
280+
launchAndRepeatWithViewLifecycle {
281+
launch {
282+
// Observe ViewModel data
283+
viewModel.mapVariant.collect {
284+
mapView.awaitMap().apply {
285+
clear()
286+
viewModel.loadMapFeatures(this)
287+
}
284288
}
285289
}
286-
}
287290

288-
viewModel.geoJsonLayer.observe(viewLifecycleOwner) {
289-
updateMarkers(it ?: return@observe)
290-
}
291+
// Set the center of the map's camera. Call this every time the user selects a marker.
292+
launch {
293+
viewModel.mapCenterEvent.collect { update ->
294+
mapView.getMapAsync {
295+
it.animateCamera(update)
296+
}
297+
}
298+
}
299+
300+
launch {
301+
viewModel.bottomSheetStateEvent.collect { event ->
302+
BottomSheetBehavior.from(binding.bottomSheet).state = event
303+
}
304+
}
291305

292-
viewModel.selectedMarkerInfo.observe(viewLifecycleOwner) {
293-
updateInfoSheet(it ?: return@observe)
306+
launch {
307+
viewModel.geoJsonLayer.collect {
308+
it?.let {
309+
updateMarkers(it)
310+
}
311+
}
312+
}
313+
launch {
314+
viewModel.selectedMarkerInfo.collect {
315+
it?.let {
316+
updateInfoSheet(it)
317+
}
318+
}
319+
}
294320
}
295321

296322
analyticsHelper.sendScreenView("Map", requireActivity())

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ import android.view.WindowManager
2727
import androidx.appcompat.app.AppCompatDialogFragment
2828
import androidx.core.view.updateLayoutParams
2929
import androidx.fragment.app.viewModels
30-
import androidx.lifecycle.Observer
3130
import androidx.recyclerview.widget.RecyclerView
3231
import com.google.samples.apps.iosched.R
32+
import com.google.samples.apps.iosched.util.launchAndRepeatWithViewLifecycle
3333
import dagger.hilt.android.AndroidEntryPoint
34+
import kotlinx.coroutines.flow.collect
3435

3536
@AndroidEntryPoint
3637
class MapVariantSelectionDialogFragment : AppCompatDialogFragment() {
@@ -57,12 +58,11 @@ class MapVariantSelectionDialogFragment : AppCompatDialogFragment() {
5758
adapter = MapVariantAdapter(::selectMapVariant)
5859
view.findViewById<RecyclerView>(R.id.map_variant_list).adapter = adapter
5960

60-
mapViewModel.mapVariant.observe(
61-
viewLifecycleOwner,
62-
Observer {
61+
launchAndRepeatWithViewLifecycle {
62+
mapViewModel.mapVariant.collect {
6363
adapter.currentSelection = it
6464
}
65-
)
65+
}
6666
}
6767

6868
@SuppressLint("RtlHardcoded")

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

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,14 @@
1616

1717
package com.google.samples.apps.iosched.ui.map
1818

19-
import android.view.View
2019
import androidx.annotation.DimenRes
2120
import androidx.annotation.RawRes
2221
import androidx.databinding.BindingAdapter
23-
import com.google.android.gms.maps.CameraUpdate
2422
import com.google.android.gms.maps.MapView
2523
import com.google.android.gms.maps.model.LatLngBounds
2624
import com.google.android.gms.maps.model.MapStyleOptions
2725
import com.google.android.gms.maps.model.TileOverlayOptions
28-
import com.google.samples.apps.iosched.shared.result.Event
2926
import com.google.samples.apps.iosched.util.getFloatUsingCompat
30-
import com.google.samples.apps.iosched.widget.BottomSheetBehavior
3127

3228
@BindingAdapter("mapStyle")
3329
fun mapStyle(mapView: MapView, @RawRes resId: Int) {
@@ -50,17 +46,6 @@ fun mapViewport(mapView: MapView, bounds: LatLngBounds?) {
5046
}
5147
}
5248

53-
/**
54-
* Sets the center of the map's camera. Call this every time the user selects a marker.
55-
*/
56-
@BindingAdapter("mapCenter")
57-
fun mapCenter(mapView: MapView, event: Event<CameraUpdate>?) {
58-
val update = event?.getContentIfNotHandled() ?: return
59-
mapView.getMapAsync {
60-
it.animateCamera(update)
61-
}
62-
}
63-
6449
/**
6550
* Sets the minimum zoom level of the map (how far out the user is allowed to zoom).
6651
*/
@@ -106,9 +91,3 @@ fun mapTileProvider(mapView: MapView, mapVariant: MapVariant?) {
10691
}
10792
}
10893
}
109-
110-
@BindingAdapter("bottomSheetState")
111-
fun bottomSheetState(view: View, event: Event<Int>?) {
112-
val state = event?.getContentIfNotHandled() ?: return
113-
BottomSheetBehavior.from(view).state = state
114-
}

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

Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@
1616

1717
package com.google.samples.apps.iosched.ui.map
1818

19-
import androidx.lifecycle.LiveData
20-
import androidx.lifecycle.MediatorLiveData
21-
import androidx.lifecycle.MutableLiveData
22-
import androidx.lifecycle.Transformations
2319
import androidx.lifecycle.ViewModel
2420
import androidx.lifecycle.viewModelScope
2521
import com.google.android.gms.maps.CameraUpdate
@@ -34,14 +30,22 @@ import com.google.samples.apps.iosched.shared.analytics.AnalyticsActions
3430
import com.google.samples.apps.iosched.shared.analytics.AnalyticsHelper
3531
import com.google.samples.apps.iosched.shared.domain.prefs.MyLocationOptedInUseCase
3632
import com.google.samples.apps.iosched.shared.domain.prefs.OptIntoMyLocationUseCase
37-
import com.google.samples.apps.iosched.shared.result.Event
3833
import com.google.samples.apps.iosched.shared.result.successOr
3934
import com.google.samples.apps.iosched.shared.result.updateOnSuccess
35+
import com.google.samples.apps.iosched.shared.util.tryOffer
4036
import com.google.samples.apps.iosched.ui.signin.SignInViewModelDelegate
37+
import com.google.samples.apps.iosched.util.WhileViewSubscribed
4138
import com.google.samples.apps.iosched.widget.BottomSheetBehavior
4239
import dagger.hilt.android.lifecycle.HiltViewModel
40+
import kotlinx.coroutines.channels.Channel
4341
import kotlinx.coroutines.flow.MutableStateFlow
42+
import kotlinx.coroutines.flow.StateFlow
43+
import kotlinx.coroutines.flow.collect
4444
import kotlinx.coroutines.flow.combine
45+
import kotlinx.coroutines.flow.map
46+
import kotlinx.coroutines.flow.onEach
47+
import kotlinx.coroutines.flow.receiveAsFlow
48+
import kotlinx.coroutines.flow.stateIn
4549
import kotlinx.coroutines.launch
4650
import javax.inject.Inject
4751

@@ -62,18 +66,24 @@ class MapViewModel @Inject constructor(
6266
BuildConfig.MAP_VIEWPORT_BOUND_NE
6367
)
6468

65-
private val _mapVariant = MutableLiveData<MapVariant>()
66-
val mapVariant = Transformations.distinctUntilChanged(_mapVariant)
69+
private val _mapVariant = MutableStateFlow<MapVariant?>(null)
70+
val mapVariant: StateFlow<MapVariant?> = _mapVariant
6771

68-
private val _mapCenterEvent = MutableLiveData<Event<CameraUpdate>>()
69-
val mapCenterEvent: LiveData<Event<CameraUpdate>>
70-
get() = _mapCenterEvent
72+
private val _mapCenterEvent = Channel<CameraUpdate>(Channel.CONFLATED)
73+
val mapCenterEvent = _mapCenterEvent.receiveAsFlow()
7174

72-
private val loadGeoJsonResult = MutableLiveData<GeoJsonData>()
75+
private val loadGeoJsonResult = MutableStateFlow<GeoJsonData?>(null)
7376

74-
private val _geoJsonLayer = MediatorLiveData<GeoJsonLayer?>()
75-
val geoJsonLayer: LiveData<GeoJsonLayer?>
76-
get() = _geoJsonLayer
77+
val geoJsonLayer: StateFlow<GeoJsonLayer?> = loadGeoJsonResult
78+
.onEach { data ->
79+
if (data != null) {
80+
// TODO: Remove side effects and make these reactive
81+
hasLoadedFeatures = true
82+
setMapFeatures(data.featureMap)
83+
}
84+
}.map { data ->
85+
data?.geoJsonLayer
86+
}.stateIn(viewModelScope, WhileViewSubscribed, null)
7787

7888
private val featureLookup: MutableMap<String, GeoJsonFeature> = mutableMapOf()
7989
private var hasLoadedFeatures = false
@@ -82,14 +92,23 @@ class MapViewModel @Inject constructor(
8292
private val focusZoomLevel = BuildConfig.MAP_CAMERA_FOCUS_ZOOM
8393
private var currentZoomLevel = 16 // min zoom level supported
8494

85-
private val _bottomSheetStateEvent = MediatorLiveData<Event<Int>>()
86-
val bottomSheetStateEvent: LiveData<Event<Int>>
87-
get() = _bottomSheetStateEvent
88-
private val _selectedMarkerInfo = MutableLiveData<MarkerInfo?>()
89-
val selectedMarkerInfo: LiveData<MarkerInfo?>
90-
get() = _selectedMarkerInfo
95+
private val _bottomSheetStateEvent = Channel<Int>(Channel.CONFLATED)
96+
val bottomSheetStateEvent = _bottomSheetStateEvent.receiveAsFlow()
9197

92-
private val myLocationOptedIn = MutableStateFlow<Boolean>(false)
98+
init {
99+
viewModelScope.launch {
100+
mapVariant.collect {
101+
// When the map variant changes, the selected feature might not be present in the
102+
// new variant, so hide the feature detail.
103+
dismissFeatureDetails()
104+
}
105+
}
106+
}
107+
108+
private val _selectedMarkerInfo = MutableStateFlow<MarkerInfo?>(null)
109+
val selectedMarkerInfo: StateFlow<MarkerInfo?> = _selectedMarkerInfo
110+
111+
private val myLocationOptedIn = MutableStateFlow(false)
93112

94113
val showMyLocationOption = userInfo.combine(myLocationOptedIn) { info, optedIn ->
95114
// Show the button to enable "My Location" when the user is an on-site attendee and he/she
@@ -101,17 +120,6 @@ class MapViewModel @Inject constructor(
101120
viewModelScope.launch {
102121
myLocationOptedIn.value = myLocationOptedInUseCase(Unit).successOr(false)
103122
}
104-
_geoJsonLayer.addSource(loadGeoJsonResult) { data ->
105-
hasLoadedFeatures = true
106-
setMapFeatures(data.featureMap)
107-
_geoJsonLayer.value = data.geoJsonLayer
108-
}
109-
110-
// When the map variant changes, the selected feature might not be present in the new
111-
// variant, so hide the feature detail.
112-
_bottomSheetStateEvent.addSource(mapVariant) {
113-
dismissFeatureDetails()
114-
}
115123
}
116124

117125
fun optIntoMyLocation(optIn: Boolean = true) {
@@ -128,7 +136,7 @@ class MapViewModel @Inject constructor(
128136
// The geo json layer is tied to the GoogleMap, so we should release it.
129137
hasLoadedFeatures = false
130138
featureLookup.clear()
131-
_geoJsonLayer.value = null
139+
loadGeoJsonResult.value = null
132140
}
133141

134142
fun loadMapFeatures(googleMap: GoogleMap) {
@@ -144,7 +152,7 @@ class MapViewModel @Inject constructor(
144152
private fun setMapFeatures(features: Map<String, GeoJsonFeature>) {
145153
featureLookup.clear()
146154
featureLookup.putAll(features)
147-
updateFeaturesVisiblity(currentZoomLevel.toFloat())
155+
updateFeaturesVisibility(currentZoomLevel.toFloat())
148156
// if we have a pending request to highlight a feature, resolve it now
149157
val featureId = requestedFeatureId ?: return
150158
requestedFeatureId = null
@@ -156,11 +164,11 @@ class MapViewModel @Inject constructor(
156164
val zoomInt = zoom.toInt()
157165
if (currentZoomLevel != zoomInt) {
158166
currentZoomLevel = zoomInt
159-
updateFeaturesVisiblity(zoom)
167+
updateFeaturesVisibility(zoom)
160168
}
161169
}
162170

163-
private fun updateFeaturesVisiblity(zoom: Float) {
171+
private fun updateFeaturesVisibility(zoom: Float) {
164172
// Don't hide the marker if it's currently being focused on by the user
165173
val selectedId = selectedMarkerInfo.value?.id
166174
featureLookup.values.forEach { feature ->
@@ -185,7 +193,7 @@ class MapViewModel @Inject constructor(
185193
val geometry = feature.geometry as? GeoJsonPoint ?: return
186194
// center map on the requested feature.
187195
val update = CameraUpdateFactory.newLatLngZoom(geometry.coordinates, focusZoomLevel)
188-
_mapCenterEvent.value = Event(update)
196+
_mapCenterEvent.tryOffer(update)
189197

190198
// publish feature data
191199
val title = feature.getProperty("title")
@@ -198,14 +206,14 @@ class MapViewModel @Inject constructor(
198206
)
199207

200208
// bring bottom sheet into view
201-
_bottomSheetStateEvent.value = Event(BottomSheetBehavior.STATE_COLLAPSED)
209+
_bottomSheetStateEvent.tryOffer(BottomSheetBehavior.STATE_COLLAPSED)
202210

203211
// Analytics
204212
analyticsHelper.logUiEvent(title, AnalyticsActions.MAP_MARKER_SELECT)
205213
}
206214

207215
fun dismissFeatureDetails() {
208-
_bottomSheetStateEvent.value = Event(BottomSheetBehavior.STATE_HIDDEN)
216+
_bottomSheetStateEvent.tryOffer(BottomSheetBehavior.STATE_HIDDEN)
209217
_selectedMarkerInfo.value = null
210218
}
211219

mobile/src/main/res/layout/fragment_map.xml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@
7070
app:cameraZoom="@dimen/map_camera_zoom"
7171
app:isIndoorEnabled="@{false}"
7272
app:isMapToolbarEnabled="@{false}"
73-
app:mapCenter="@{viewModel.mapCenterEvent}"
7473
app:mapMaxZoom="@{R.dimen.map_viewport_max_zoom}"
7574
app:mapMinZoom="@{R.dimen.map_viewport_min_zoom}"
7675
app:mapStyle="@{viewModel.mapVariant.styleResId}"
@@ -106,8 +105,7 @@
106105
android:elevation="@dimen/bottom_sheet_elevation"
107106
app:layout_behavior="com.google.samples.apps.iosched.widget.BottomSheetBehavior"
108107
app:behavior_hideable="true"
109-
app:behavior_peekHeight="@dimen/map_bottom_sheet_peek_height"
110-
app:bottomSheetState="@{viewModel.bottomSheetStateEvent}">
108+
app:behavior_peekHeight="@dimen/map_bottom_sheet_peek_height">
111109

112110
<androidx.constraintlayout.widget.Guideline
113111
android:id="@+id/guide_peek_height"

0 commit comments

Comments
 (0)