Simplifying TomTom Maps Integration with a Jetpack Compose Wrapper

Ezichi Amarachi
4 min readMay 3, 2024

In today’s mobile world, location-based services and map functionalities have become essential components of many mobile applications. TomTom, a leading provider of mapping solutions, provides a comprehensive Maps SDK for Android developers to integrate maps into their apps with functionalities like location tracking, search, routing, etc.

Building Android user interfaces has evolved since the introduction of Jetpack Compose. The declarative UI framework allows developers describe UI elements in a clean and concise manner. This article introduces a custom Jetpack Compose wrapper for TomTom Maps display API to simplify the map implementation within your Jetpack Compose UIs.

Let’s get Started 🙂

You should check out the official documentation for guidelines on how to obtain an API Key and setup the Map SDK in android, but for the scope of this article, we’ll only be focusing on implementing the Maps Display API.

First things first, add the map display dependency to your build.gradle.ktx file:

dependencies {

implementation("com.tomtom.sdk.maps:map-display:1.2.0")

}

The API provides a MapFragment class, which is a Fragment wrapper for the MapView. Traditionally, an instance of MapFragment should be created in the onCreate() method of an activity or the onCreateView() method of a fragment. Using Jetpack Compose, one way to go would be to create an instance of the fragment separately. Ideally, you need an xml view to act as a container to hold the fragment since Jetpack Compose doesn’t directly manipulate the view. Create an xml file for the view:

<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/map_container"
android:layout_width="match_parent"
android:layout_height="match_parent">

</androidx.fragment.app.FragmentContainerView>

Now, create a new Fragment as MapFragment and add it to the container using the FragmentManager class.

private fun createTomTomMapFragment(
fragmentManager: FragmentManager,
mapOptions: MapOptions,
) {
val fragment = fragmentManager
.findFragmentById(R.id.map_container) as MapFragment?
val newFragment = MapFragment.newInstance(mapOptions)
fragmentManager.beginTransaction()
.add(newFragment, "map_fragment")
.commitNow()
}

We can further improve createTomTomMapFragment() by including a callback to notify the caller when the fragment is ready, and also checking that there isn’t already an existing fragment before creating a new one:

private fun createTomTomMapFragment(
fragmentManager: FragmentManager,
mapOptions: MapOptions,
onFragmentReady: (MapFragment) -> Unit
) {
val fragment = fragmentManager.findFragmentById(R.id.map_container) as MapFragment?
val newFragment = fragment ?: MapFragment.newInstance(mapOptions)
fragmentManager.beginTransaction()
.add(newFragment, "map_fragment")
.commitNow()
onFragmentReady(newFragment)
}

onFragmentReady parameter receives the ready fragment to be used in the composable. Let’s create the composable!

@Composable
fun TomTomMapView(
modifier: Modifier = Modifier
) {
var mapFragment by remember {
mutableStateOf<MapFragment?>(null)
}
var isMapFragmentReady by remember {
mutableStateOf(false)
}
val context = LocalContext.current
val fragmentManager = (context as FragmentActivity).supportFragmentManager

val mapOptions = MapOptions(
mapKey = "Add_your_API_KEY",
// add more options
)

LaunchedEffect(Unit) {
createTomTomMapFragment(fragmentManager, mapOptions) { fragment ->
mapFragment = fragment
isMapFragmentReady = true
}
}

if (isMapFragmentReady) {
AndroidView(
factory = {
mapFragment?.requireView() ?: View(it)
},
update = {
// update fragment
}
)
}
}

We use LaunchedEffect to launch a coroutine that calls createTomTomMapFragment() asynchronously when the composable function is composed to avoid blocking the main thread. The fragment argument is an instance of MapFragment already created which is passed to the mapFragment variable. AndroidView composable allows for integration of a View with a composable: the factory block handles the creation of the layout to be composed and the update block is for performing updates on the already inflated layout. Calling requireView() on the mapFragment object essentially tells AndroidView to display the view, which in this case is an instance of MapFragment. isMapFragmentReady monitors the state of the fragment so that AndroidView is only created after the fragment is ready.

NB: In the AndroidView factory block, we use the Elvis operator to return an empty View instance in the event that mapFragment isn’t created successfully. For better user experience, you could inflate some dialog for the user to initiate recreation of the fragment.

The current state of TomTomMapView() composable works, but fails to handle the lifecycle of the MapFragment. If the composable is removed from composition, this could result in memory leaks because the fragment may not be cleaned up. We can use DisposableEffect to handle this by removing the fragment in the onDispose block. Here is the final state of TomTomMapView():

@Composable
fun TomTomMapView(
modifier: Modifier = Modifier
) {
var mapFragment by remember {
mutableStateOf<MapFragment?>(null)
}
var isMapFragmentReady by remember {
mutableStateOf(false)
}
val context = LocalContext.current
val fragmentManager = (context as FragmentActivity).supportFragmentManager

val mapOptions = MapOptions(
mapKey = "Add_your_API_KEY",
// add more options
)
LaunchedEffect(Unit) {
createTomTomMapFragment(fragmentManager, mapOptions) { fragment ->
mapFragment = fragment
isMapFragmentReady = true
}
}
if (isMapFragmentReady) {
AndroidView(
factory = {
mapFragment?.requireView() ?: View(it)
},
update = {
// update fragment
}
)
}

DisposableEffect(fragmentManager) {
onDispose {
fragmentManager.beginTransaction()
.remove(mapFragment!!)
.commitNow()
}
}
}

The TomTomMapView() composable can now be called from the setContent block of the Activity. It is important to note that because MapFragment is a Fragment class, we cast LocalContext to FragmentActivity to obtain the supportFragmentManager used in creating the fragment, therefore, the activity calling TomTomMapView() should be a subclass of FragmentActivity:

class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TomTomMapView()
}
}
}

Changes and updates to the Map can be made inside the update block of the AndroidView composable, changes like adding markers, map click listeners, etc. For example:

AndroidView(
factory = {
mapFragment?.requireView() ?: View(it)
},
update = {
mapFragment?.getMapAsync { tomtomMap ->
val markerOptions = MarkerOptions(
coordinate = GeoPoint(
latitude = 52.012665,
longitude = 4.355894
),
pinImage = ImageFactory.fromResource(R.drawable.pin_image),
pinIconImage = ImageFactory.fromResource(R.drawable.pin_icon),
)
tomtomMap.addMarker(markerOptions)

tomtomMap.addMarkerClickListener { }

tomtomMap.addMapClickListener { }

// and more
}
}
)

Go Wild!

And there you have it! Welcome to my unofficial Ted talk, cheers! 🤭

--

--

Ezichi Amarachi

Android Engineer with 5 years of experience in Native Android Development. Open for EU/UK based roles with relocation.