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.
Join the DZone community and get the full member experience.Join For Free
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:
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:
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 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.
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:
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:
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:
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 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.
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
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:
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:
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.
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.
2) Create Custom MapView instances which extends the corresponding map provider's MapView and implements AllAboutMapView interface:
3) Fragment layouts includes these custom views:
4) The base class includes the common code.
5) Finally our Fragment classes... Note how small these classes are! We appreciate the minimalism here.
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.
Published at DZone with permission of Irene Li. See the original article here.
Opinions expressed by DZone contributors are their own.