Skip to content

Commit 29f251a

Browse files
committed
refactor: decouple map logic from MapScreen
This effort is for the KMP initiative. This commit refactors the map-related logic out of `MapScreen.kt` and into a new composable `SevMap.kt`. This improves separation of concerns and makes `MapScreen.kt` leaner. Key Changes: - Created `SevMap.kt` to encapsulate Google Maps interactions, camera movements, and marker rendering. - Moved map-specific state management (like `cameraPositionState`) and effects (like centering on selections or user location) into `SevMap.kt`. - `MapScreen.kt` now delegates map rendering and interaction to `SevMap.kt`. - Introduced `locationButtonClickFlow: SharedFlow<Unit>` to communicate location button clicks from `MapScreen.kt` to `SevMap.kt` for camera animation. - Added `onCameraPositionChanged: (LatLng) -> Unit` callback in `SevMap.kt` to inform `MapScreen.kt` about camera position changes, used for updating the location button's state. - Simplified `MapScreen.kt` by removing direct dependencies on map camera state and location service for camera movements. - Updated `LocationButton` to take `currentCameraPosition` and an `onLocationButtonClicked` callback instead of directly interacting with `CameraPositionState`. - Debug info in `MapScreen.kt` was simplified as camera details are now internal to `SevMap.kt`. - Preview for `MapScreen.kt` was updated to reflect the new structure, instantiating `MapScreen` directly.
1 parent f895c9c commit 29f251a

2 files changed

Lines changed: 133 additions & 94 deletions

File tree

app/src/main/java/com/sloy/sevibus/feature/map/MapScreen.kt

Lines changed: 44 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@ import androidx.compose.material3.Icon
1717
import androidx.compose.material3.SnackbarHostState
1818
import androidx.compose.material3.Text
1919
import androidx.compose.runtime.Composable
20-
import androidx.compose.runtime.LaunchedEffect
2120
import androidx.compose.runtime.getValue
21+
import androidx.compose.runtime.mutableStateOf
2222
import androidx.compose.runtime.remember
2323
import androidx.compose.runtime.rememberCoroutineScope
24+
import androidx.compose.runtime.setValue
25+
import kotlinx.coroutines.flow.MutableSharedFlow
2426
import androidx.compose.ui.Alignment
2527
import androidx.compose.ui.Modifier
2628
import androidx.compose.ui.graphics.graphicsLayer
2729
import androidx.compose.ui.platform.LocalContext
28-
import androidx.compose.ui.platform.LocalDensity
2930
import androidx.compose.ui.platform.LocalView
3031
import androidx.compose.ui.tooling.preview.Preview
3132
import androidx.compose.ui.unit.dp
@@ -36,19 +37,11 @@ import com.composables.core.BottomSheetState
3637
import com.google.accompanist.permissions.ExperimentalPermissionsApi
3738
import com.google.accompanist.permissions.PermissionState
3839
import com.google.accompanist.permissions.isGranted
39-
import com.google.android.gms.maps.CameraUpdate
40-
import com.google.android.gms.maps.CameraUpdateFactory
41-
import com.google.android.gms.maps.model.CameraPosition
42-
import com.google.maps.android.compose.CameraMoveStartedReason
43-
import com.google.maps.android.compose.CameraPositionState
44-
import com.google.maps.android.compose.rememberCameraPositionState
4540
import com.sloy.debugmenu.launcher.DebugMenuLauncher
46-
import com.sloy.sevibus.Stubs
41+
import com.google.android.gms.maps.model.LatLng
4742
import com.sloy.sevibus.domain.model.Stop
4843
import com.sloy.sevibus.domain.model.isInsideSevilla
49-
import com.sloy.sevibus.domain.model.toBounds
5044
import com.sloy.sevibus.domain.model.toLatLng
51-
import com.sloy.sevibus.domain.model.toLatLngBounds
5245
import com.sloy.sevibus.feature.debug.SevDebugMenu
5346
import com.sloy.sevibus.infrastructure.BuildVariant
5447
import com.sloy.sevibus.infrastructure.EventCollector
@@ -66,9 +59,7 @@ import com.sloy.sevibus.ui.components.CustomFab
6659
import com.sloy.sevibus.ui.preview.ScreenPreview
6760
import com.sloy.sevibus.ui.snackbar.LocalSnackbarHostState
6861
import com.sloy.sevibus.ui.theme.SevTheme
69-
import kotlinx.coroutines.delay
7062
import kotlinx.coroutines.launch
71-
import okhttp3.internal.format
7263
import org.koin.androidx.compose.koinViewModel
7364

7465

@@ -105,65 +96,13 @@ private fun MapScreen(
10596
onTrack: (SevEvent) -> Unit,
10697
snackbarHostState: SnackbarHostState,
10798
) {
108-
val locationService: LocationService = koinInjectOnUI() ?: NoopLocationService
10999
val locationPermissionState = rememberPermissionStateOnUI(Manifest.permission.ACCESS_FINE_LOCATION)
110-
val camera = rememberCameraPositionState {
111-
position = CameraPosition.fromLatLngZoom(Stubs.locationInitial.toLatLng(), ZoomLevel.Far.minimumLevel.toFloat())
112-
}
113-
LaunchedEffect(locationPermissionState?.status?.isGranted) {
114-
locationService.obtainCurrentLocation()?.toLatLng()?.let { userLocation ->
115-
if (userLocation.isInsideSevilla()) {
116-
camera.animate(CameraUpdateFactory.newLatLngZoom(userLocation, MY_LOCATION_ZOOM))
117-
}
118-
}
119-
}
120-
LaunchedEffect(camera.isMoving, camera.cameraMoveStartedReason) {
121-
if (camera.isMoving && camera.cameraMoveStartedReason == CameraMoveStartedReason.GESTURE) {
122-
onMapClick()
123-
}
124-
}
125-
126-
suspend fun centerMapOn(cameraUpdate: CameraUpdate?) {
127-
if (cameraUpdate == null) return
128-
// Wait until the sheet is in its final position
129-
delay(20)
130-
while (!sheetState.isIdle) {
131-
delay(10)
132-
}
133-
camera.animate(cameraUpdate, MAP_CAMERA_ANIMATION_DURATION)
134-
}
135-
136-
val density = LocalDensity.current
137-
val boundsPadding = with(density) { 40.dp.toPx() }.toInt()
138-
139-
when (state) {
140-
is MapScreenState.StopSelected -> {
141-
LaunchedEffect(state.selectedStop) {
142-
centerMapOn(CameraUpdateFactory.newLatLngZoom(state.selectedStop.position.toLatLng(), ZoomLevel.Close.minimumLevel.toFloat()) )
143-
}
144-
}
145-
146-
is MapScreenState.LineSelected -> {
147-
LaunchedEffect(state.lineStops) {
148-
centerMapOn(CameraUpdateFactory.newLatLngBounds(state.lineStops.toBounds().toLatLngBounds(), boundsPadding))
149-
}
150-
}
151-
152-
is MapScreenState.StopAndLineSelected -> {
153-
LaunchedEffect(state.selectedStop) {
154-
centerMapOn(CameraUpdateFactory.newLatLngBounds(state.selectedStops().toBounds().toLatLngBounds(), boundsPadding))
155-
}
156-
}
157-
158-
else -> {}
159-
}
160100

161101
MapUI(
102+
sheetState,
162103
state,
163-
camera,
164104
locationPermissionState,
165105
snackbarHostState,
166-
locationService,
167106
contentPadding,
168107
onStopSelected,
169108
onMapClick,
@@ -174,25 +113,27 @@ private fun MapScreen(
174113
@OptIn(ExperimentalPermissionsApi::class)
175114
@Composable
176115
private fun MapUI(
116+
sheetState: BottomSheetState,
177117
state: MapScreenState,
178-
cameraPositionState: CameraPositionState,
179118
locationPermissionState: PermissionState?,
180119
snackbarHostState: SnackbarHostState,
181-
locationService: LocationService,
182120
contentPadding: PaddingValues,
183121
onStopSelected: (stop: Stop) -> Unit,
184122
onMapClick: () -> Unit,
185123
onTrack: (SevEvent) -> Unit,
186124
) {
125+
val locationService: LocationService = koinInjectOnUI() ?: NoopLocationService
187126
val shakeAnim = remember { Animatable(0f) }
127+
val locationButtonClickFlow = remember { MutableSharedFlow<Unit>(extraBufferCapacity = 1) }
128+
var currentCameraPosition by remember { mutableStateOf<LatLng?>(null) }
188129

189130
Box(
190131
Modifier
191132
.fillMaxSize()
192133
.graphicsLayer { translationX = shakeAnim.value }) {
193134
if (FeatureFlags.showMapDebugInfo) {
194135
DebugInfo(
195-
state, cameraPositionState,
136+
state,
196137
Modifier
197138
.zIndex(1f)
198139
.padding(contentPadding)
@@ -208,25 +149,30 @@ private fun MapUI(
208149
DebugButton()
209150
LocationButton(
210151
locationPermissionState,
211-
cameraPositionState,
212152
snackbarHostState,
213153
locationService,
154+
currentCameraPosition,
214155
onReject = {
215156
shakeAnim.animateShake()
216157
},
217158
onTrack = onTrack,
159+
onLocationButtonClicked = {
160+
locationButtonClickFlow.tryEmit(Unit)
161+
}
218162
)
219163
}
220164

221-
222-
223165
SevMap(
224166
state = state,
225-
cameraPositionState = cameraPositionState,
226167
hasLocationPermission = locationPermissionState?.status?.isGranted ?: false,
227168
onStopSelected = onStopSelected,
228169
onMapClick = onMapClick,
229170
contentPadding = contentPadding,
171+
sheetState = sheetState,
172+
locationButtonClickFlow = locationButtonClickFlow,
173+
onCameraPositionChanged = { position ->
174+
currentCameraPosition = position
175+
},
230176
modifier = Modifier.zIndex(0f)
231177
)
232178
}
@@ -253,19 +199,23 @@ private fun DebugButton(modifier: Modifier = Modifier) {
253199
@Composable
254200
private fun LocationButton(
255201
locationPermissionState: PermissionState?,
256-
cameraPositionState: CameraPositionState,
257202
snackbarState: SnackbarHostState,
258203
locationService: LocationService,
204+
currentCameraPosition: LatLng?,
259205
onReject: suspend () -> Unit,
260206
onTrack: (SevEvent) -> Unit,
207+
onLocationButtonClicked: () -> Unit,
261208
modifier: Modifier = Modifier,
262209
) {
263210
val hasPermission = locationPermissionState?.status?.isGranted ?: false
264211
val scope = rememberCoroutineScope()
265212
val view = LocalView.current
266213
val lifecycle = LocalLifecycleOwner.current.lifecycle
267214
val location by locationService.requestLocationUpdates().collectAsStateWithLifecycle(null, lifecycle)
268-
val isCameraInCurrentPosition = location?.toLatLng()?.isApproximatelyEqualTo(cameraPositionState.position.target) == true
215+
216+
val isCameraInCurrentPosition = currentCameraPosition?.let { cameraPos ->
217+
location?.toLatLng()?.isApproximatelyEqualTo(cameraPos) == true
218+
} ?: false
269219

270220
val outsideSevillaMessenger = remember { OutsideSevillaMessenger() }
271221

@@ -279,7 +229,7 @@ private fun LocationButton(
279229
locationService.obtainCurrentLocation()?.toLatLng()?.let { userLocation ->
280230
if (userLocation.isInsideSevilla()) {
281231
onTrack(Clicks.LocationButtonClicked("inside-sevilla"))
282-
cameraPositionState.animate(CameraUpdateFactory.newLatLngZoom(userLocation, MY_LOCATION_ZOOM))
232+
onLocationButtonClicked()
283233
} else {
284234
onTrack(Clicks.LocationButtonClicked("outside-sevilla"))
285235
view.performHapticReject()
@@ -300,14 +250,11 @@ private fun LocationButton(
300250
) {
301251
Icon(if (isCameraInCurrentPosition) Icons.Filled.MyLocation else Icons.Filled.LocationSearching, "Search Location")
302252
}
303-
304-
305253
}
306254

307255
@Composable
308256
private fun DebugInfo(
309257
state: MapScreenState,
310-
cameraPositionState: CameraPositionState,
311258
modifier: Modifier = Modifier,
312259
) {
313260
Column(
@@ -316,31 +263,35 @@ private fun DebugInfo(
316263
.fillMaxSize()
317264
) {
318265
Text(state::class.simpleName!!)
319-
val zoom = cameraPositionState.position.zoom
320-
Text(format("%.2f", zoom) + " - " + ZoomLevel(zoom.toInt()))
266+
Text("Debug Info")
321267
}
322268
}
323269

324270

325-
private const val MY_LOCATION_ZOOM = 17f
326-
private const val MAP_CAMERA_ANIMATION_DURATION = 200
327-
328-
329271
@OptIn(ExperimentalPermissionsApi::class)
330272
@Preview
331273
@Composable
332274
private fun MapScreenPreview() {
333275
ScreenPreview {
334-
MapUI(
335-
state = MapScreenState.Initial,
276+
val scope = rememberCoroutineScope()
277+
val density = androidx.compose.ui.platform.LocalDensity.current
278+
MapScreen(
279+
sheetState = remember {
280+
com.composables.core.BottomSheetState(
281+
initialDetent = com.composables.core.SheetDetent.Hidden,
282+
detents = listOf(com.composables.core.SheetDetent.Hidden),
283+
coroutineScope = scope,
284+
animationSpec = androidx.compose.animation.core.spring(),
285+
velocityThreshold = { 125.dp.value },
286+
positionalThreshold = { 56.dp.value },
287+
decayAnimationSpec = androidx.compose.animation.core.exponentialDecay(),
288+
confirmDetentChange = { true },
289+
density = { density }
290+
)
291+
},
336292
contentPadding = PaddingValues(),
337-
cameraPositionState = remember { CameraPositionState() },
338-
snackbarHostState = SnackbarHostState(),
339-
locationPermissionState = null,
340-
locationService = NoopLocationService,
341293
onStopSelected = {},
342-
onMapClick = {},
343-
onTrack = {}
294+
onMapClick = {}
344295
)
345296
}
346297
}

0 commit comments

Comments
 (0)