[Huawei Developers] Freemind R:
More articles like this one, you can visit HUAWEI Developer Forum and Medium.​

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 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:

import com.huawei.hms.maps.*

import com.google.android.gms.maps.*

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.

[IMG="https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/023/288/729/5190358000023288729.20200528181208.43728211170800647509226688196803:50510623061427:2800:71CD4D1FA47D23DF740FAA5642C2DD306A1203D4A25C4FF2844EFD535EFE5627.png"]https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/023/288/729/5190358000023288729.20200528181208.43728211170800647509226688196803:50510623061427:2800:71CD4D1FA47D23DF740FAA5642C2DD306A1203D4A25C4FF2844EFD535EFE5627.png[/IMG]

[IMG="https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/023/288/729/5190358000023288729.20200528181316.51224804603668884471265207983508:50510623061427:2800:5697A6BEF6F46E14323634A52F69D813BEF626986AA71C46B7043F74A5C8D122.png"]https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/023/288/729/5190358000023288729.20200528181316.51224804603668884471265207983508:50510623061427:2800:5697A6BEF6F46E14323634A52F69D813BEF626986AA71C46B7043F74A5C8D122.png[/IMG]

[IMG="https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/023/288/729/5190358000023288729.20200528181621.98830251350245671827800636421218:50510623070416:2800:2B29E87CDC00402D63C46EC12EA02E4223A16F0C03437A3DD04EBF7B2E9DF1C3.png"]https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/023/288/729/5190358000023288729.20200528181621.98830251350245671827800636421218:50510623070416:2800:2B29E87CDC00402D63C46EC12EA02E4223A16F0C03437A3DD04EBF7B2E9DF1C3.png[/IMG]

[IMG="https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/023/288/729/5190358000023288729.20200528181726.51298723360727393950247428425664:50510623070416:2800:069DF1970CC2363CC76CB54A1FE0579EF9F7270795D846748BC8818AEC1D9757.png"]https://communityfile-dre.op.hicloud.com/FileServer/getFile/cmtybbs/023/288/729/5190358000023288729.20200528181726.51298723360727393950247428425664:50510623070416:2800:069DF1970CC2363CC76CB54A1FE0579EF9F7270795D846748BC8818AEC1D9757.png[/IMG]

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:

inline class Latitude(val value: Float)
inline class Longitude(val value: Float)

data class LatLng(
val latitude: Latitude,
val longitude: Longitude
)

data class Point(
val latitude: Latitude,
val longitude: Longitude,
val altitude: Float? = null,
val name: String? = null
) {
val latLng: LatLng
get() = LatLng(latitude, longitude)
}

data class RouteInfo(
val routePoints: List<Point> = emptyList(),
val wayPoints: List<Point> = emptyList()
)

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:

import com.ulusoy.allaboutmaps.domain.entities.LatLng
import com.mapbox.mapboxsdk.geometry.LatLng as MapboxLatLng
import com.huawei.hms.maps.model.LatLng as HuaweiLatLng
import com.google.android.gms.maps.model.LatLng as GoogleLatLang

fun LatLng.toMapboxLatLng() = MapboxLatLng(
latitude.value.toDouble(),
longitude.value.toDouble()
)

fun LatLng.toHuaweiLatLng() = HuaweiLatLng(
latitude.value.toDouble(),
longitude.value.toDouble()
)

fun LatLng.toGoogleLatLng() = GoogleLatLang(
latitude.value.toDouble(),
longitude.value.toDouble()
)

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:

class GetRouteInfoUseCase
@Inject constructor(
private val routeInfoRepository: RouteInfoRepository
) {
suspend operator fun invoke(): RouteInfo {
return routeInfoRepository.getRouteInfo()
}
}

interface RouteInfoRepository {
suspend fun getRouteInfo(): RouteInfo
}

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.

class RouteInfoDataRepository
@Inject constructor(
@Named("GPX_DATA_SOURCE")
private val gpxFileDatasource: RouteInfoDatasource
) : RouteInfoRepository {
override suspend fun getRouteInfo(): RouteInfo {
return gpxFileDatasource.parseGpxFile()
}
}

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

class GpxFileDatasource
@Inject constructor(
private val gpxFileParser: GpxFileParser
): RouteInfoDatasource {
override suspend fun parseGpxFile(): RouteInfo {
return gpxFileParser.parseGpxFile()
}
}

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:

interface GpxFileParser {
suspend fun parseGpxFile(): RouteInfo
}

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.

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

Domain module forwards the request to data source.

Datasource orchestrates the data in the repository class

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.

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

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).

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:

class RouteInfoDataRepository
@Inject constructor(
@Named("GPX_DATA_SOURCE")
private val gpxFileDatasource: RouteInfoDatasource,
@Named("REMOTE_DATA_SOURCE")
private val remoteDatasource: RouteInfoDatasource,
@Named("DATABASE_SOURCE")
private val localDatasource: RouteInfoDatasource
) : RouteInfoRepository {
override suspend fun getRouteInfo(): RouteInfo? {
var routeInfo = remoteDatasource.parseGpxFile()
if (routeInfo == null) {
Timber.d("Route info is not available in remote source, now trying local database")
routeInfo = localDatasource.parseGpxFile()
if (routeInfo == null) {
Timber.d("Route info is not available in local database. Let's hope we have a gpx file in the app resource folder")
gpxFileDatasource.parseGpxFile()
}
}
return routeInfo
}
}

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

For full content, you can visit HUAWEI Developer Forum.​