Slide 1

Slide 1 text

Brian Gardner CashApp GoogleMap() {}

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Dependencies

Slide 5

Slide 5 text

/ / technically incorrect (the best kind) compose - maps = { group = “com.google.maps.android", name = “maps - compose", version = “2.11.4” } play - services - maps = { group = "com.google.android.gms", name = "play - services - maps", version = "18.1.0" }

Slide 6

Slide 6 text

… / / maps api key < / application> < / manifest>

Slide 7

Slide 7 text

Essential configuration 1. Map rendering 2. User interaction 3. Location to display

Slide 8

Slide 8 text

Map Rendering

Slide 9

Slide 9 text

public class MapProperties( )

Slide 10

Slide 10 text

public class MapProperties( public val isBuildingEnabled: Boolean = false, )

Slide 11

Slide 11 text

public class MapProperties( public val isBuildingEnabled: Boolean = false, public val isIndoorEnabled: Boolean = false, )

Slide 12

Slide 12 text

public class MapProperties( public val isBuildingEnabled: Boolean = false, public val isIndoorEnabled: Boolean = false, public val isMyLocationEnabled: Boolean = false, )

Slide 13

Slide 13 text

public class MapProperties( public val isBuildingEnabled: Boolean = false, public val isIndoorEnabled: Boolean = false, public val isMyLocationEnabled: Boolean = false, public val isTrafficEnabled: Boolean = false, )

Slide 14

Slide 14 text

public class MapProperties( public val isBuildingEnabled: Boolean = false, public val isIndoorEnabled: Boolean = false, public val isMyLocationEnabled: Boolean = false, public val isTrafficEnabled: Boolean = false, public val latLngBoundsForCameraTarget: LatLngBounds? = null, )

Slide 15

Slide 15 text

public class MapProperties( public val isBuildingEnabled: Boolean = false, public val isIndoorEnabled: Boolean = false, public val isMyLocationEnabled: Boolean = false, public val isTrafficEnabled: Boolean = false, public val latLngBoundsForCameraTarget: LatLngBounds? = null, public val mapStyleOptions: MapStyleOptions? = null, )

Slide 16

Slide 16 text

public class MapProperties( public val isBuildingEnabled: Boolean = false, public val isIndoorEnabled: Boolean = false, public val isMyLocationEnabled: Boolean = false, public val isTrafficEnabled: Boolean = false, public val latLngBoundsForCameraTarget: LatLngBounds? = null, public val mapStyleOptions: MapStyleOptions? = null, public val mapType: MapType = MapType.NORMAL, )

Slide 17

Slide 17 text

public class MapProperties( public val isBuildingEnabled: Boolean = false, public val isIndoorEnabled: Boolean = false, public val isMyLocationEnabled: Boolean = false, public val isTrafficEnabled: Boolean = false, public val latLngBoundsForCameraTarget: LatLngBounds? = null, public val mapStyleOptions: MapStyleOptions? = null, public val mapType: MapType = MapType.NORMAL, public val maxZoomPreference: Float = 21.0f, public val minZoomPreference: Float = 3.0f, )

Slide 18

Slide 18 text

val mapStyleOptions = if (isSystemInDarkTheme()) { MapStyleOptions.loadRawResourceStyle( context, R.raw.night_mode_style ) } else { null }

Slide 19

Slide 19 text

{ // https://mapstyle.withgoogle.com/ "featureType": "all", "elementType": "geometry", "stylers": [ { "color": "#242f3e" } ] }, { "featureType": "all", "elementType": "labels.text.stroke", "stylers": [ { "lightness": -80 } ] },

Slide 20

Slide 20 text

val mapProperties by remember(hasLocationPermission) { mutableStateOf( MapProperties( mapStyleOptions = mapStyleOptions, maxZoomPreference = ZOOM_MAX, minZoomPreference = ZOOM_MIN, isMyLocationEnabled = hasLocationPermission, ) ) }

Slide 21

Slide 21 text

User Interaction

Slide 22

Slide 22 text

public class MapUiSettings( )

Slide 23

Slide 23 text

public class MapUiSettings( public val compassEnabled: Boolean = true, )

Slide 24

Slide 24 text

public class MapUiSettings( public val compassEnabled: Boolean = true, public val indoorLevelPickerEnabled: Boolean = true, )

Slide 25

Slide 25 text

public class MapUiSettings( public val compassEnabled: Boolean = true, public val indoorLevelPickerEnabled: Boolean = true, public val mapToolbarEnabled: Boolean = true, )

Slide 26

Slide 26 text

public class MapUiSettings( public val compassEnabled: Boolean = true, public val indoorLevelPickerEnabled: Boolean = true, public val mapToolbarEnabled: Boolean = true, public val myLocationButtonEnabled: Boolean = true, )

Slide 27

Slide 27 text

public class MapUiSettings( public val compassEnabled: Boolean = true, public val indoorLevelPickerEnabled: Boolean = true, public val mapToolbarEnabled: Boolean = true, public val myLocationButtonEnabled: Boolean = true, public val rotationGesturesEnabled: Boolean = true, )

Slide 28

Slide 28 text

public class MapUiSettings( public val compassEnabled: Boolean = true, public val indoorLevelPickerEnabled: Boolean = true, public val mapToolbarEnabled: Boolean = true, public val myLocationButtonEnabled: Boolean = true, public val rotationGesturesEnabled: Boolean = true, public val scrollGesturesEnabled: Boolean = true, )

Slide 29

Slide 29 text

public class MapUiSettings( public val compassEnabled: Boolean = true, public val indoorLevelPickerEnabled: Boolean = true, public val mapToolbarEnabled: Boolean = true, public val myLocationButtonEnabled: Boolean = true, public val rotationGesturesEnabled: Boolean = true, public val scrollGesturesEnabled: Boolean = true, public val scrollGesturesEnabledDuringRotateOrZoom: Boolean = true, )

Slide 30

Slide 30 text

public class MapUiSettings( public val compassEnabled: Boolean = true, public val indoorLevelPickerEnabled: Boolean = true, public val mapToolbarEnabled: Boolean = true, public val myLocationButtonEnabled: Boolean = true, public val rotationGesturesEnabled: Boolean = true, public val scrollGesturesEnabled: Boolean = true, public val scrollGesturesEnabledDuringRotateOrZoom: Boolean = true, public val tiltGesturesEnabled: Boolean = true, )

Slide 31

Slide 31 text

public class MapUiSettings( public val compassEnabled: Boolean = true, public val indoorLevelPickerEnabled: Boolean = true, public val mapToolbarEnabled: Boolean = true, public val myLocationButtonEnabled: Boolean = true, public val rotationGesturesEnabled: Boolean = true, public val scrollGesturesEnabled: Boolean = true, public val scrollGesturesEnabledDuringRotateOrZoom: Boolean = true, public val tiltGesturesEnabled: Boolean = true, public val zoomControlsEnabled: Boolean = true, )

Slide 32

Slide 32 text

public class MapUiSettings( public val compassEnabled: Boolean = true, public val indoorLevelPickerEnabled: Boolean = true, public val mapToolbarEnabled: Boolean = true, public val myLocationButtonEnabled: Boolean = true, public val rotationGesturesEnabled: Boolean = true, public val scrollGesturesEnabled: Boolean = true, public val scrollGesturesEnabledDuringRotateOrZoom: Boolean = true, public val tiltGesturesEnabled: Boolean = true, public val zoomControlsEnabled: Boolean = true, public val zoomGesturesEnabled: Boolean = true, )

Slide 33

Slide 33 text

val mapUiSettings by remember { mutableStateOf( MapUiSettings( indoorLevelPickerEnabled = false, mapToolbarEnabled = false, myLocationButtonEnabled = false, rotationGesturesEnabled = false, tiltGesturesEnabled = false, zoomControlsEnabled = false, ), ) }

Slide 34

Slide 34 text

Location To Display

Slide 35

Slide 35 text

val cameraState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom( LatLng(location.lat, location.lng), location.zoom ) }

Slide 36

Slide 36 text

val cameraState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom( LatLng(location.lat, location.lng), location.zoom ) }

Slide 37

Slide 37 text

val cameraState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom( LatLng(location.lat, location.lng), location.zoom ) }

Slide 38

Slide 38 text

Putting it together

Slide 39

Slide 39 text

GoogleMap( modifier = modifier, cameraPositionState = cameraState, properties = mapProperties, uiSettings = mapUiSettings, ) { }

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

Updating Location

Slide 42

Slide 42 text

LaunchedEffect(location) { cameraState.move( CameraUpdateFactory.newCameraPosition( CameraPosition.fromLatLngZoom( LatLng(location.lat, location.lng), location.zoom ) ), ) }

Slide 43

Slide 43 text

LaunchedEffect(location) { cameraState.move( CameraUpdateFactory.newCameraPosition( CameraPosition.fromLatLngZoom( LatLng(location.lat, location.lng), location.zoom ) ), ) }

Slide 44

Slide 44 text

LaunchedEffect(location) { cameraState.move( CameraUpdateFactory.newCameraPosition( CameraPosition.fromLatLngZoom( LatLng(location.lat, location.lng), location.zoom ) ), ) }

Slide 45

Slide 45 text

LaunchedEffect(location) { cameraState.move( CameraUpdateFactory.newCameraPosition( CameraPosition.fromLatLngZoom( LatLng(location.lat, location.lng), location.zoom ) ), ) }

Slide 46

Slide 46 text

Reacting to new locations

Slide 47

Slide 47 text

LaunchedEffect(cameraState.isMoving) { if (!cameraState.isMoving) { / / notify presenter of new location } }

Slide 48

Slide 48 text

LaunchedEffect(cameraState.isMoving) { if (!cameraState.isMoving) { / / notify presenter of new location onEvent( OnCameraPositionChanged( lat = cameraState.position.target.latitude, lng = cameraState.position.target.longitude, zoom = cameraState.position.zoom ) ) } }

Slide 49

Slide 49 text

Markers

Slide 50

Slide 50 text

GoogleMap(…) { Marker( state = rememberMarkerState( key = location.token, position = LatLng(location.lat, location.lng) ), title = "Cash Money ATM" ) }

Slide 51

Slide 51 text

GoogleMap(…) { Marker( state = rememberMarkerState( key = location.token, position = LatLng(location.lat, location.lng) ), title = "Cash Money ATM" ) }

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

No content

Slide 54

Slide 54 text

Custom info window

Slide 55

Slide 55 text

MarkerInfoWindow( state = rememberMarkerState( key = location.token, position = LatLng(location.lat, location.lng) ), ) { Surface( modif i er = Modif i er.background(MaterialTheme.colorScheme.background) .padding(8.dp) ) { Column { Text(text = "Cash Money ATM") Text(text = "Withdraw or deposit here!") } } }

Slide 56

Slide 56 text

MarkerInfoWindow( state = rememberMarkerState( key = location.token, position = LatLng(location.lat, location.lng) ), ) { Surface( modif i er = Modif i er.background(MaterialTheme.colorScheme.background) .padding(8.dp) ) { Column { Text(text = "Cash Money ATM") Text(text = "Withdraw or deposit here!") } } }

Slide 57

Slide 57 text

MarkerInfoWindow( state = rememberMarkerState( key = location.token, position = LatLng(location.lat, location.lng) ), ) { Surface( modif i er = Modif i er.background(MaterialTheme.colorScheme.background) .padding(8.dp) ) { Column { Text(text = "Cash Money ATM") Text(text = "Withdraw or deposit here!") } } }

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

Custom marker icon

Slide 60

Slide 60 text

val resources = LocalContext.current.resources val iconBitmapDescriptor = remember { val iconBitmap = ResourcesCompat.getDrawable( resources, R.drawable.icon_marker, null ) ? . toBitmap() ! ! BitmapDescriptorFactory.fromBitmap(iconBitmap) } Marker( state = rememberMarkerState( key = location.token, position = LatLng(location.lat, location.lng) ), icon = iconBitmapDescriptor, )

Slide 61

Slide 61 text

val resources = LocalContext.current.resources val iconBitmapDescriptor = remember { val iconBitmap = ResourcesCompat.getDrawable( resources, R.drawable.icon_marker, null ) ? . toBitmap() ! ! BitmapDescriptorFactory.fromBitmap(iconBitmap) } Marker( state = rememberMarkerState( key = location.token, position = LatLng(location.lat, location.lng) ), icon = iconBitmapDescriptor, )

Slide 62

Slide 62 text

val resources = LocalContext.current.resources val iconBitmapDescriptor = remember { val iconBitmap = ResourcesCompat.getDrawable( resources, R.drawable.icon_marker, null ) ? . toBitmap() ! ! BitmapDescriptorFactory.fromBitmap(iconBitmap) } Marker( state = rememberMarkerState( key = location.token, position = LatLng(location.lat, location.lng) ), icon = iconBitmapDescriptor, )

Slide 63

Slide 63 text

val resources = LocalContext.current.resources val iconBitmapDescriptor = remember { val iconBitmap = ResourcesCompat.getDrawable( resources, R.drawable.icon_marker, null ) ? . toBitmap() ! ! BitmapDescriptorFactory.fromBitmap(iconBitmap) } Marker( state = rememberMarkerState( key = location.token, position = LatLng(location.lat, location.lng) ), icon = iconBitmapDescriptor, )

Slide 64

Slide 64 text

val resources = LocalContext.current.resources val iconBitmapDescriptor = remember { val iconBitmap = ResourcesCompat.getDrawable( resources, R.drawable.icon_marker, null ) ? . toBitmap() ! ! BitmapDescriptorFactory.fromBitmap(iconBitmap) } Marker( state = rememberMarkerState( key = location.token, position = LatLng(location.lat, location.lng) ), icon = iconBitmapDescriptor, )

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

Multiple markers

Slide 67

Slide 67 text

for (marker in markers) { Marker( state = rememberMarkerState( key = marker.token, position = LatLng(marker.lat, marker.lng) ), ) }

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

No content

Slide 70

Slide 70 text

Clusters

Slide 71

Slide 71 text

Options 1. Can use android-maps-utils library, but need access to GoogleMap instance • MapE ff ect { map: GoogleMap -> } provided in android-maps-utils library 2. Clustering support in maps-compose-util library

Slide 72

Slide 72 text

Don’t use MapEffect 1. Manually creating ClusterManager is a pain 2. Manual ClusterManager overrides Marker composable click handling 3. ClusterManager handles onCameraIdle callback 4. Having multiple types of markers or clusters requires MarkerManager

Slide 73

Slide 73 text

compose - maps - utils = { group = "com.google.maps.android", name = “maps - compose - utils“, version.ref = “2.11.4” }

Slide 74

Slide 74 text

Cluster Item

Slide 75

Slide 75 text

class MapClusterItem( val location: MapLocation ) : ClusterItem { override fun getPosition() = LatLng(location.lat, location.lng) override fun getTitle() = location.title override fun getSnippet() = location.description }

Slide 76

Slide 76 text

class MapClusterItem( val location: MapLocation ) : ClusterItem { override fun getPosition() = LatLng(location.lat, location.lng) override fun getTitle() = location.title override fun getSnippet() = location.description }

Slide 77

Slide 77 text

val clusterItems = remember(markers) { markers.map { MapClusterItem(it) } } GoogleMap(…){ Clustering(items = clusterItems) }

Slide 78

Slide 78 text

val clusterItems = remember(markers) { markers.map { MapClusterItem(it) } } GoogleMap(…){ Clustering(items = clusterItems) }

Slide 79

Slide 79 text

val clusterItems = remember(markers) { markers.map { MapClusterItem(it) } } GoogleMap(…){ Clustering(items = clusterItems) }

Slide 80

Slide 80 text

Before:

Slide 81

Slide 81 text

After:

Slide 82

Slide 82 text

Zoomed in:

Slide 83

Slide 83 text

Custom cluster UI

Slide 84

Slide 84 text

Clustering( items = clusterItems, clusterContent = { cluster - > CustomClusterContent(cluster) }, clusterItemContent = { mapClusterItem - > ClusterItemContent(mapClusterItem) } )

Slide 85

Slide 85 text

Clustering( items = clusterItems, clusterContent = { cluster - > CustomClusterContent(cluster) }, clusterItemContent = { mapClusterItem - > ClusterItemContent(mapClusterItem) } )

Slide 86

Slide 86 text

Clustering( items = clusterItems, clusterContent = { cluster - > CustomClusterContent(cluster) }, clusterItemContent = { mapClusterItem - > ClusterItemContent(mapClusterItem) } )

Slide 87

Slide 87 text

Caution! java.lang.IllegalStateException: Invalid applier Don’t use Marker within Clustering

Slide 88

Slide 88 text

Cluster UI events

Slide 89

Slide 89 text

Clustering( items = clusterItems, onClusterClick = { cluster - > true }, )

Slide 90

Slide 90 text

Clustering( items = clusterItems, onClusterClick = { cluster - > onEvent( OnClusterClicked( cluster.position.latitude, cluster.position.longitude, cluster.items.map { it.location } ) ) true }, )

Slide 91

Slide 91 text

Clustering( items = clusterItems, onClusterClick = { … }, onClusterItemClick = { mapClusterItem - > true }, )

Slide 92

Slide 92 text

Clustering( items = clusterItems, onClusterClick = { … }, onClusterItemClick = { mapClusterItem - > onEvent( OnClusterItemClicked( mapClusterItem.location.token ) ) true }, )

Slide 93

Slide 93 text

Issues

Slide 94

Slide 94 text

Paparazzi testing

Slide 95

Slide 95 text

Paparazzi testing // When in preview, early return a Box with // the received modifier preserving layout if (LocalInspectionMode.current) { Box(modifier = modifier) return }

Slide 96

Slide 96 text

Paparazzi testing paparazzi.snapshot { CompositionLocalProvider( LocalInspectionMode provides true ) { MainMap(…) } }

Slide 97

Slide 97 text

Paparazzi testing paparazzi.snapshot { CompositionLocalProvider( LocalInspectionMode provides true ) { MainMap(…) } }

Slide 98

Slide 98 text

Performance concerns

Slide 99

Slide 99 text

Performance concerns MapsInitializer.initialize( context, MapsInitializer.Renderer.LATEST ) {}

Slide 100

Slide 100 text

ANR Issue

Slide 101

Slide 101 text

Thanks! Brian Gardner CashApp @[email protected]