{{announcement.body}}
{{announcement.title}}

All About Maps — Episode 1: Showing Routes From GPX Files on Maps

DZone 's Guide to

All About Maps — Episode 1: Showing Routes From GPX Files on Maps

In this project, I aim to demonstrate how we can implement the same map related use cases with different map providers in one codebase.

· Web Dev Zone ·
Free Resource

All About Maps

Let's talk about maps. I started an open-source project called All About Maps (https://github.com/ulusoyca/AllAboutMaps). In this project, I aim to demonstrate how we can implement the same map related use cases with different map providers in one codebase. We will use Mapbox Maps, Google Maps, and Huawei HMS Map Kit. This project uses the following libraries and patterns:

  • MVVM pattern with Android Jetpack Libraries

  • Kotlin Coroutines for asynchronous operations

  • Dagger2 Dependency Injection

  • Android Clean Architecture

Note: The codebase changes by time. You can always find the latest code in develop branch. The code when this article is written can be seen by choosing the tag: episode_1-parse-gpx:

https://github.com/ulusoyca/AllAboutMaps/tree/episode_1-parse-gpx/

Motivation

Why do we need maps in our apps? What are the features a developer would expect from a map SDK? Let's try to list some:

  • Showing a coordinate on a map with camera options (zoom, tilt, latitude, longitude, bearing)
  • Adding symbols, photos, polylines, polygons to map
  • Handle user gestures (click, pinch, move events)
  • Showing maps with different map styles (Outdoor, Hybrid, Satallite, Winter, Dark etc.)
  • Data visualization (heatmaps, charts, clusters, time-lapse effect)
  • Offline map visualization (providing map tiles without network connectivity)
  • Generate snapshot image of a bounded region

We can probably add more items but I believe this is the list of features which all map provider companies would most likely provide. Knowing that we can achieve the same tasks with different map providers, we should not create huge dependencies to any specific provider in our codebase. When a product owner (PO) tells to developers to switch from Google Maps to Mapbox Maps, or Huawei Maps, developers should never see it as a big deal. It is software development. Business as usual.

One would probably think why a PO would want to switch from one map provider to another. In many cases, the reason is not the technical details. For example, Google Play Services may not be available in some devices or regions like China. Another case is when a company X which has a subscription to Mapbox, acquires company Y which uses Google Maps. In this case the transition to one provider is more efficient. Change in the terms of services, and pricing might be other motivations.

We need competition in the market! Let's switch easily when needed but how dependencies make things worse? Problematic dependencies in the codebase are usually created by developing software like there is no tomorrow. It is not always developers' fault. Tight schedules, anti-refactoring minded teams, unorganized plannings may cause careless coding and then eventually to technical depts. In this project, I aim to show how we can encapsulate the import lines below belonging to three different map providers to minimum number of classes with minimum lines:

Java
 




x


 
1
import com.huawei.hms.maps.*
2
import com.google.android.gms.maps.*
3
import com.mapbox.mapboxsdk.maps.*



It should be noted that the way of achieving this in this post is just one proposal. There are always alternative and better ways of implementations. In the end, as software developers, we should deliver our tasks time-efficiently, without over-engineering.

About the Project

In the home page of the project you will see the list of tutorials. Since this is the first blog post, there is only one item for now. To make our life easier with RecyclerViews, I use Epoxy library by Airbnb in the project. Once you click the buttons in the card, it will take to the detail page. Using bottom sheet we can switch between map providers. Note that Huawei Map Kit requires a Huawei mobile phone.

In this first blog post, we will parse the GPX file of 120 km route of Cappadocia Ultra Trail race and show the route and check points (food stations) on map. I finished this race in 23 hours 45 mins and you can also read my experience here (https://link.medium.com/uWmrWLAzR6). GPX is an open standart which contains route points that constructs a polyline and waypoints which are the attraction location. In this case, the waypoints represents the food and aid stations in the race. We will show the route with a polyline and waypoints with markers on map.

Architecture

Architecture is definitely not an overrated concept. Since the early days of Android, we have been seeking for the best architectural patterns that suits with Android development. We have heard of MVC, MVP, MVVM, MVI and many other patterns will emerge. The change and adaptation to a new pattern is inevitable by time. We should keep in mind some basic and commonly accepted concepts like SOLID principles, seperation of concerns, maintainability, readibility, testablity etc. so that we can switch to between patterns easily when needed. 

Nowadays, widely accepted architecture in Android community is modularization with Clean Architecture. If you have time to invest more, I would strongly suggest Joe Birch's clean architecture tutorials. As Joe suggests in his tutorials, we do not have to apply every rule line by line but instead we take whatever we feel like is needed. Here is my take and how I modularized the All About Maps app:

Note that dependency injection with Dagger2 is the core of this implementation. If you are not familiar with the concept, I strongly suggest you to read the best Dagger2 tutorial in the wild Dagger2 world by Nimrod Dayan.

Domain Module

Many of us are excited to start implementation with UI to see the results immediately but we should patiently build our blocks. We shall start with the domain module since we will put our business logic and define the entities and user interactions there. 

First question: What entities do we need for a Map app? 

We don't have to put every entity at once. Since our first tutorial is about drawing polylines and symbols we will need the following data:

  • LatLng class which holds Latitude and Longitude

  • Point which represents a geo-coordinate.

  • RouteInfo that holds points to be used to draw route and waypoints

Let's see the implementations:

Java
 




x
19


 
1
inline class Latitude(val value: Float)
2
inline class Longitude(val value: Float)
3
data class LatLng(
4
    val latitude: Latitude,
5
    val longitude: Longitude
6
)
7
data class Point(
8
    val latitude: Latitude,
9
    val longitude: Longitude,
10
    val altitude: Float? = null,
11
    val name: String? = null
12
) {
13
    val latLng: LatLng
14
        get() = LatLng(latitude, longitude)
15
}
16
data class RouteInfo(
17
    val routePoints: List<Point> = emptyList(),
18
    val wayPoints: List<Point> = emptyList()
19
)



I could have used Float primitive type for Latitude and Longitude fields. However, I strongly suggest you to take advantage of Kotlin inline classes. In my relatively long career of working on maps, I spent hours on issues caused by mistakenly using longitude for latitude values. 

Note that LatLng class is available in all Map SDKs. However, all the modules below the domain layer should use only our own LatLng to prevent the dependency to map SDKs in those modules. In the app layer we can map our LatLng class to corresponding classes:

Java
 




xxxxxxxxxx
1
19


 
1
import com.ulusoy.allaboutmaps.domain.entities.LatLng
2
import com.mapbox.mapboxsdk.geometry.LatLng as MapboxLatLng
3
import  com.huawei.hms.maps.model.LatLng as HuaweiLatLng
4
import com.google.android.gms.maps.model.LatLng as GoogleLatLang
5
 
          
6
fun LatLng.toMapboxLatLng() = MapboxLatLng(
7
    latitude.value.toDouble(),
8
    longitude.value.toDouble()
9
)
10
 
          
11
fun LatLng.toHuaweiLatLng() = HuaweiLatLng(
12
    latitude.value.toDouble(),
13
    longitude.value.toDouble()
14
)
15
 
          
16
fun LatLng.toGoogleLatLng() = GoogleLatLang(
17
    latitude.value.toDouble(),
18
    longitude.value.toDouble()
19
)



Second question: What actions user can trigger? 

Domain module contains the uses cases (interactors) that an application can perform to achieve goals based on user interactions. The code in this module is less likely to change compared to other modules. Business is business. For example, this application has one job for now: showing the route info with a polyline and markers. It can get the route info from a web server, a database or in this case from application resource file which is a GPX file. Neither the app module nor the domain module doesn't care where the route points and waypoints are retrieved from. It is not their concern. The concerns are seperated. 

Lets see the use case definition in our domain module:

Java
 




xxxxxxxxxx
1
11


 
1
class GetRouteInfoUseCase
2
@Inject constructor(
3
    private val routeInfoRepository: RouteInfoRepository
4
) {
5
    suspend operator fun invoke(): RouteInfo {
6
        return routeInfoRepository.getRouteInfo()
7
    }
8
}
9
interface RouteInfoRepository {
10
    suspend fun getRouteInfo(): RouteInfo
11
}



RouteInfoRepository is an interface that lives in the domain module and it is a contract between domain and datasource modules. Its concrete implementation lives in the datasource module.

Datasource Module

Datasource module is an abstraction world. Life here is based on interfaces. The domain module communicates with datasource module through the repository interface, then datasource module orchestrates the data flow in repository class and returns the final value. 

Here, the domain module asks for the route info. Datasource module decides what to return after retrieving data from different data sources. For the sake of simplicity, in this case we have only one datasource: GPX parser. The route info is extracted from a GPX file. We don't know where, and how. Let's see the code:

Here is the concrete implementation of RouteInfoRepository interface. Route info datasource is injected as constructor parameter to this class.

Java
 




xxxxxxxxxx
1


 
1
class RouteInfoDataRepository
2
@Inject constructor(
3
    @Named("GPX_DATA_SOURCE")
4
    private val gpxFileDatasource: RouteInfoDatasource
5
) : RouteInfoRepository {
6
    override suspend fun getRouteInfo(): RouteInfo {
7
        return gpxFileDatasource.parseGpxFile()
8
    }
9
}



Here is our one only route info data source: GpxFileDataSource. It still doesn't know how to get the data from gpx file. However, it knows where to get the data from thanks to contract GpxFileParser

Java
 




xxxxxxxxxx
1


 
1
class GpxFileDatasource
2
@Inject constructor(
3
    private val gpxFileParser: GpxFileParser
4
): RouteInfoDatasource {
5
    override suspend fun parseGpxFile(): RouteInfo {
6
        return gpxFileParser.parseGpxFile()
7
    }
8
}



What is a GPX file? How is it parsed? Where is the file located? Datasource doesn't care about these details. It only knows that the concrete implementation of GpxFileParser will return the RouteInfo. Here is the contract between the datasource and the concrete implementation: 

Java
 




xxxxxxxxxx
1


 
1
interface GpxFileParser {
2
    suspend fun parseGpxFile(): RouteInfo
3
}



Is it already too confusing with too many abstractions around? Is it overengineering? You might be right and choose to have less abstractions when you have one datasource like in this case. However, in real world, we have multiple datasources. Data is all around us. It may come from web server, from database or from our connected devices such as wearables. The benefit here is when things get more complicated with multiple datasources. Let's think about this complicated scenario. 

  1. App asks for the route info through a use case class.

  2. Domain module forwards the request to data source.

  3. Datasource orchestrates the data in the repository class

  4. It first asks to Web servers (remote data source) to get the route info. However, the user is offline. Thus, the remote data source is not available.

  5. Then it checks what we have locally by first checking if the route info is available in database or not.

  6. It is not available in database but we have a gpx file in our resource folder (I know it doesn't make sense but to give an example).

  7. The repository class asks GPX parser to parse the file and return the desired RouteInfo data.

Too complicated? It would be this much easier to implement this code in repository class based on the scenario:

Java
 




xxxxxxxxxx
1
22


 
1
class RouteInfoDataRepository
2
@Inject constructor(
3
    @Named("GPX_DATA_SOURCE")
4
    private val gpxFileDatasource: RouteInfoDatasource,
5
    @Named("REMOTE_DATA_SOURCE")
6
    private val remoteDatasource: RouteInfoDatasource,
7
    @Named("DATABASE_SOURCE")
8
    private val localDatasource: RouteInfoDatasource
9
) : RouteInfoRepository {
10
    override suspend fun getRouteInfo(): RouteInfo? {
11
        var routeInfo = remoteDatasource.parseGpxFile()
12
        if (routeInfo == null) {
13
            Timber.d("Route info is not available in remote source, now trying local database")
14
            routeInfo = localDatasource.parseGpxFile()
15
            if (routeInfo == null) {
16
                Timber.d("Route info is not available in local database. Let's hope we have a gpx file in the app resource folder")
17
                gpxFileDatasource.parseGpxFile()
18
            }
19
        }
20
        return routeInfo
21
    }
22
}



Thanks to Kotlin coroutines we can write these asynchronous operations sequentially.

GPX File Parser

This concrete datasource module returns us the route info parsed from a GPX file. The implementation details are straightforward thanks to the 3rd party library AndroidGpxParser. It does the job and returns the route info according to the contract GpxFileParser. Here you can find the implementation details in the LocalGpxFileParser class. I must say that the most cahllenging part of the clean architecture for me is finding good names for interfacases and classes that implement those interfaces. I just hate to add Impl suffix to class names. I wouldn't name the implementation of GpxFileParser as LocalGpxFileParserImpl. It is up to you but considered as a bad practise in the community.

App layer

We have gone through 3 modules, and still have not started implementing any UI code. How do we make sure that what we have done up to this point actually will work? The answer is testing. We should write readable, maintainable and testable code. Thanks to the seperated concerns, classes in each module can be tested with unit tests. 

Up until now we have never written code specific to Google Maps, Huawei Maps or Mapbox Maps. Remember that our goal is minimizing the dependency to these SDKs. Here is how we implement the app layer:

RouteInfoGoogleFragment, RouteInfoHuaweiFragment, and RouteInfoMapboxFragment each shows maps with polyline and markers. We include the common code in BaseRouteInfoMapFragment which has no import file from map SDKs. The SDK dependent code lives is CustomView which extend MapView class of corresponding SDKs and implements our own AllAboutMapView interface. This is how we do it:

1) Define an interface which has functions that each mapview class of map providers needs to implement.

Java
 




xxxxxxxxxx
1
12


 
1
interface AllAboutMapView {
2
    fun onMapViewCreate(savedInstanceState: Bundle?)
3
    fun onMapViewStart()
4
    fun onMapViewStop()
5
    fun onMapViewResume()
6
    fun onMapViewPause()
7
    fun onMapViewDestroy()
8
    fun onMapViewSaveInstanceState(savedInstanceState: Bundle?)
9
    fun onMapViewLowMemory()
10
    fun drawPolyline(latLngs: List<LatLng>, @ColorRes mapLineColor: Int)
11
    fun drawMarker(latLng: LatLng, icon: Bitmap, name: String?)
12
}



2) Create Custom MapView instances which extends the corresponding map provider's MapView and implements AllAboutMapView interface:

Java
 




xxxxxxxxxx
1
137


 
1
class GoogleMapView
2
@JvmOverloads constructor(
3
    context: Context,
4
    attrs: AttributeSet? = null,
5
    defStyleAttr: Int = 0
6
) : MapView(context, attrs, defStyleAttr), AllAboutMapView {
7
 
          
8
    private var map: GoogleMap? = null
9
 
          
10
    override fun onMapViewCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) }
11
    override fun onMapViewStart() { super.onStart() }
12
    override fun onMapViewStop() { super.onStop() }
13
    override fun onMapViewResume() { super.onResume() }
14
    override fun onMapViewPause() { super.onPause() }
15
    override fun onMapViewDestroy() { super.onDestroy() }
16
    override fun onMapViewSaveInstanceState(savedInstanceState: Bundle?) { super.onSaveInstanceState() }
17
    override fun onMapViewLowMemory() { super.onLowMemory() }
18
 
          
19
    fun onMapReady(map: GoogleMap) {
20
        this.map = map
21
        val mapStyleOptions =
22
            MapStyleOptions.loadRawResourceStyle(context, R.raw.google_maps_dark_style)
23
        map.setMapStyle(mapStyleOptions)
24
    }
25
 
          
26
    override fun drawPolyline(latLngs: List<LatLng>, @ColorRes mapLineColor: Int) {
27
        map?.addPolyline(
28
            PolylineOptions()
29
                .color(mapLineColor)
30
                .jointType(JointType.ROUND)
31
                .width(resources.getDimension(R.dimen.google_route_line_width_cut))
32
                .addAll(latLngs.map { it.toGoogleLatLng() })
33
        )
34
    }
35
 
          
36
    override fun drawMarker(latLng: LatLng, icon: Bitmap, name: String?) {
37
        var markerOptions = MarkerOptions()
38
            .icon(BitmapDescriptorFactory.fromBitmap(icon))
39
            .position(latLng.toGoogleLatLng())
40
        markerOptions = name?.run { markerOptions.title(this) }
41
        map?.addMarker(markerOptions)?.showInfoWindow()
42
    }
43
}
44
class HuaweiMapView
45
@JvmOverloads constructor(
46
    context: Context,
47
    attrs: AttributeSet? = null,
48
    defStyleAttr: Int = 0
49
) : MapView(context, attrs, defStyleAttr), AllAboutMapView {
50
 
          
51
    private var map: HuaweiMap? = null
52
 
          
53
    override fun onMapViewCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) }
54
    override fun onMapViewStart() { super.onStart() }
55
    override fun onMapViewStop() { super.onStop() }
56
    override fun onMapViewResume() { super.onResume() }
57
    override fun onMapViewPause() { super.onPause() }
58
    override fun onMapViewDestroy() { super.onDestroy() }
59
    override fun onMapViewSaveInstanceState(savedInstanceState: Bundle?) { super.onSaveInstanceState() }
60
    override fun onMapViewLowMemory() { super.onLowMemory() }
61
 
          
62
    fun onMapReady(map: HuaweiMap) {
63
        this.map = map
64
        val mapStyleOptions =
65
            MapStyleOptions.loadRawResourceStyle(context, R.raw.huawei_maps_dark_style)
66
        map.setMapStyle(mapStyleOptions)
67
    }
68
 
          
69
    override fun drawPolyline(latLngs: List<LatLng>, mapLineColor: Int) {
70
        map?.addPolyline(
71
            PolylineOptions()
72
                .color(mapLineColor)
73
                .jointType(JointType.ROUND)
74
                .width(resources.getDimension(R.dimen.huawei_route_line_width_cut))
75
                .addAll(latLngs.map { it.toHuaweiLatLng() })
76
        )
77
    }
78
 
          
79
    override fun drawMarker(latLng: LatLng, icon: Bitmap, name: String?) {
80
        var markerOptions = MarkerOptions()
81
            .icon(BitmapDescriptorFactory.fromBitmap(icon))
82
            .position(latLng.toHuaweiLatLng())
83
        markerOptions = name?.run { markerOptions.title(this) }
84
        map?.addMarker(markerOptions)?.showInfoWindow()
85
    }
86
}
87
private const val CHECK_POINT_IMAGE_ID = "CHECK_POINT_IMAGE_ID"
88
 
          
89
class MapboxMapView
90
@JvmOverloads constructor(
91
    context: Context,
92
    attrs: AttributeSet? = null,
93
    defStyleAttr: Int = 0
94
) : MapView(context, attrs, defStyleAttr), AllAboutMapView {
95
 
          
96
    private var lineManager: LineManager? = null
97
    private var symbolManager: SymbolManager? = null
98
    private var map: MapboxMap? = null
99
 
          
100
    override fun onMapViewCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) }
101
    override fun onMapViewStart() { super.onStart() }
102
    override fun onMapViewStop() { super.onStop() }
103
    override fun onMapViewResume() { super.onResume() }
104
    override fun onMapViewPause() { super.onPause() }
105
    override fun onMapViewDestroy() { super.onDestroy() }
106
    override fun onMapViewSaveInstanceState(savedInstanceState: Bundle?) { super.onSaveInstanceState() }
107
    override fun onMapViewLowMemory() { super.onLowMemory() }
108
 
          
109
    fun onMapReady(mapboxMap: MapboxMap, markerIcon: Bitmap) {
110
        map = mapboxMap.apply {
111
            setStyle(Style.Builder().fromUri(context.getString(R.string.mapbox_dark_map_style))) {
112
                it.addImage(CHECK_POINT_IMAGE_ID, markerIcon)
113
                lineManager = LineManager(this@MapboxMapView, mapboxMap, it)
114
                symbolManager = SymbolManager(this@MapboxMapView, mapboxMap, it)
115
            }
116
        }
117
    }
118
 
          
119
    override fun drawPolyline(latLngs: List<LatLng>, @ColorInt mapLineColor: Int) {
120
        val lineOptions = LineOptions().withLineJoin(Property.LINE_JOIN_ROUND)
121
            .withLineColor(ColorUtils.colorToRgbaString(mapLineColor))
122
            .withLineWidth(resources.getDimension(R.dimen.mapbox_route_line_width_cut))
123
            .withLatLngs(latLngs.map { it.toMapboxLatLng() })
124
        lineManager?.create(lineOptions)
125
    }
126
 
          
127
    override fun drawMarker(latLng: LatLng, icon: Bitmap, name: String?) {
128
        var symbolOptions = SymbolOptions()
129
            .withIconImage(CHECK_POINT_IMAGE_ID)
130
            .withLatLng(latLng.toMapboxLatLng())
131
            .withIconColor("#FFFFFF")
132
            .withTextColor("#FFFFFF")
133
            .withTextOffset(arrayOf(1f, 1f))
134
        symbolOptions = name?.run { symbolOptions.withTextField(name) }
135
        symbolManager?.create(symbolOptions)
136
    }
137
}



3) Fragment layouts includes these custom views:

Java
 




xxxxxxxxxx
1
28


 
1
<com.ulusoy.allaboutmaps.main.ui.GoogleMapView xmlns:android="http://schemas.android.com/apk/res/android"
2
    xmlns:map="http://schemas.android.com/apk/res-auto"
3
    android:id="@+id/mapView"
4
    android:layout_width="match_parent"
5
    android:layout_height="match_parent"
6
    map:cameraTargetLat="38.631155965849757"
7
    map:cameraTargetLng="34.909409992396832"
8
    map:cameraZoom="11.0"
9
    map:mapType="normal" />
10
<com.ulusoy.allaboutmaps.main.ui.HuaweiMapView xmlns:android="http://schemas.android.com/apk/res/android"
11
    xmlns:map="http://schemas.android.com/apk/res-auto"
12
    android:id="@+id/mapView"
13
    android:layout_width="match_parent"
14
    android:layout_height="match_parent"
15
    map:cameraTargetLat="38.631155965849757"
16
    map:cameraTargetLng="34.909409992396832"
17
    map:cameraZoom="11.0"
18
    map:mapType="normal" />
19
<com.ulusoy.allaboutmaps.main.ui.MapboxMapView xmlns:android="http://schemas.android.com/apk/res/android"
20
    xmlns:mapbox="http://schemas.android.com/apk/res-auto"
21
    android:id="@+id/mapView"
22
    android:layout_width="match_parent"
23
    android:layout_height="match_parent"
24
    mapbox:mapbox_compassAnimationEnabled="false"
25
    mapbox:mapbox_cameraTargetLat="38.631155965849757"
26
    mapbox:mapbox_cameraTargetLng="34.909409992396832"
27
    mapbox:mapbox_cameraZoom="10.0"
28
    mapbox:mapbox_uiAttribution="false" />



4) The base class includes the common code.

Java
 




xxxxxxxxxx
1
64


 
1
abstract class BaseRouteInfoMapFragment : DaggerFragment() {
2
 
          
3
    @Inject
4
    lateinit var viewModelFactory: ViewModelProvider.Factory
5
 
          
6
    val foodStationIcon: Bitmap by lazy {
7
        ContextCompat.getDrawable(requireContext(), R.drawable.ic_food_white)!!.toBitmap()
8
    }
9
 
          
10
    private val mapLineColor: Int by lazy {
11
        ContextCompat.getColor(requireContext(), R.color.map_route_cut_line_color)
12
    }
13
 
          
14
    private val viewModel: RouteInfoViewModel by viewModels { viewModelFactory }
15
 
          
16
    protected lateinit var mapView: AllAboutMapView
17
 
          
18
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
19
        super.onViewCreated(view, savedInstanceState)
20
        mapView.onMapViewCreate(savedInstanceState)
21
        with(viewModel) {
22
            routePoints.observe(viewLifecycleOwner, Observer { routePoints ->
23
                mapView.drawPolyline(routePoints.map { it.latLng }, mapLineColor)
24
            })
25
            waypoints.observe(viewLifecycleOwner, Observer { waypoints ->
26
                waypoints.forEach { mapView.drawMarker(it.latLng, foodStationIcon, it.name) }
27
            })
28
        }
29
    }
30
 
          
31
    protected fun onMapStyleLoaded() {
32
        viewModel.getRouteInfo()
33
    }
34
 
          
35
    override fun onResume() {
36
        super.onResume()
37
        mapView.onMapViewResume()
38
    }
39
 
          
40
    override fun onPause() {
41
        super.onPause()
42
        mapView.onMapViewPause()
43
    }
44
 
          
45
    override fun onStart() {
46
        super.onStart()
47
        mapView.onMapViewStart()
48
    }
49
 
          
50
    override fun onStop() {
51
        super.onStop()
52
        mapView.onMapViewStop()
53
    }
54
 
          
55
    override fun onDestroyView() {
56
        super.onDestroyView()
57
        mapView.onMapViewDestroy()
58
    }
59
 
          
60
    override fun onSaveInstanceState(outState: Bundle) {
61
        super.onSaveInstanceState(outState)
62
        mapView.onMapViewSaveInstanceState(outState)
63
    }
64
}



5) Finally our Fragment classes... Note how small these classes are! We appreciate the minimalism here.

Java
 




xxxxxxxxxx
1
69


 
1
class RouteInfoGoogleFragment : BaseRouteInfoMapFragment() {
2
 
          
3
    private lateinit var binding: FragmentRouteInfoGoogleBinding
4
 
          
5
    override fun onCreateView(
6
        inflater: LayoutInflater,
7
        container: ViewGroup?,
8
        savedInstanceState: Bundle?
9
    ): View? {
10
        super.onCreateView(inflater, container, savedInstanceState)
11
        binding = FragmentRouteInfoGoogleBinding.inflate(inflater, container, false)
12
        return binding.root
13
    }
14
 
          
15
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
16
        mapView = binding.mapView
17
        super.onViewCreated(view, savedInstanceState)
18
        binding.mapView.getMapAsync { mapboxMap ->
19
            binding.mapView.onMapReady(mapboxMap)
20
            onMapStyleLoaded()
21
        }
22
    }
23
}
24
class RouteInfoHuaweiFragment : BaseRouteInfoMapFragment() {
25
 
          
26
    private lateinit var binding: FragmentRouteInfoHuaweiBinding
27
 
          
28
    override fun onCreateView(
29
        inflater: LayoutInflater,
30
        container: ViewGroup?,
31
        savedInstanceState: Bundle?
32
    ): View? {
33
        super.onCreateView(inflater, container, savedInstanceState)
34
        binding = FragmentRouteInfoHuaweiBinding.inflate(inflater, container, false)
35
        return binding.root
36
    }
37
 
          
38
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
39
        mapView = binding.mapView
40
        super.onViewCreated(view, savedInstanceState)
41
        binding.mapView.getMapAsync { mapboxMap ->
42
            binding.mapView.onMapReady(mapboxMap)
43
            onMapStyleLoaded()
44
        }
45
    }
46
}
47
class RouteInfoMapboxFragment : BaseRouteInfoMapFragment() {
48
 
          
49
    private lateinit var binding: FragmentRouteInfoMapboxBinding
50
 
          
51
    override fun onCreateView(
52
        inflater: LayoutInflater,
53
        container: ViewGroup?,
54
        savedInstanceState: Bundle?
55
    ): View? {
56
        super.onCreateView(inflater, container, savedInstanceState)
57
        binding = FragmentRouteInfoMapboxBinding.inflate(inflater, container, false)
58
        return binding.root
59
    }
60
 
          
61
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
62
        mapView = binding.mapView
63
        super.onViewCreated(view, savedInstanceState)
64
        binding.mapView.getMapAsync { mapboxMap ->
65
            binding.mapView.onMapReady(mapboxMap, foodStationIcon)
66
            onMapStyleLoaded()
67
        }
68
    }
69
}



Conclusion

Software engineering is fun when you accept the challenges and come up with solutions. Let's code with good practises and never afraid of embracing latest and commonly accepted patterns and libraries! In this article, I demonstrated how we can encapsulate the dependencies coming from different map providers namely Google Maps, Mapbox Maps, and Huawei Maps so that we can switch between providers when needed in our projects.

Topics:
integration, java, kotlin, opensource, web dev

Published at DZone with permission of Irene Li . See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}