Skip to content
This repository was archived by the owner on Dec 10, 2025. It is now read-only.

gabrielglbh/Route-Mapper

Repository files navigation

Taximeter Implementation Insights

Taximeter is a simple Android app that helps users simulate in real time car trips by selecting supplements (such as luggage), predefined routes and speed.

App was implemented in X days as I wanted to have fun with this... and I did! It has been a great challenge with beautiful outcomes and lessons.

Let's dive in!

Video Demo

In order for you to test the full app on your own, you will need to add a Google Maps API Key in the secrets.properties file called MAPS_API_KEY.

🎥 Sample Route

🎥 Sample Route - Landscape

🎥 Google Maps Custom Route

🎥 Google Maps Custom Route - Landscape

Architecture

In order to develop this app I tried to keep it simple and not to overdo or overkill the project. The desired architecture I went with is Domain Driven Development (DDD): it simplifies and abstracts the comunication between the view, the view model and the data layer. The view model serves as the controller that manages incoming third party data (such as LocationProvider and PriceConfiguration) and transforms it to domain models, agnostic of the raw data, to be provided to the view.

In other words:

  • Domain layer: Holds the classes and objects to be used by the view to show data to the user and the contracts of what the view model expects of having when retrieveing some data in any form. It directly communicates with the Presentation layer.

  • Infrastructure layer: Implements the contracts given by the repositories found in the Domain layer. It serves as the only layer to transfer data between the app and third party services, thus this layer defines all Data Transfer Objects (DTO) needed.

  • Presentation layer: Manages all composables in order to build the UI. This layer collaborates with the view model in order to retrieve domain-transformed data, abstracting itself from third party models.

One of the best features of this architecture is the separation of concerns (term you will read a lot in here): each layer is responsible of its bounds. If any third party service changes behaviour or I need to change provider, the domain and presentation layer will not be altered as they work with domain models and contracts that the infrastructure demands to those providers, making those layers agnositc and super easy to maintain.

Tools

Here is the list of tools that helped me build this app and architecture:

  • Retrofit: best library to professionally manage API calls.

  • Hilt/Dagger: the Dependency Injection (DI) library plagued with annotations. Once you get the basics, its pretty straightforward (that is if you want basic usage). All DI definition code is under the di folder.

  • Coroutines: best asynchronous manager for compose.

  • Google Maps: the well-known map API build for compose. That's a little cherry that I wanted to add to the project.

  • Mockk: framework for mocking everything in unit testing. There are several other like mockito but I prefer this one.

  • Mockito: framework for mocking everything in integration testing.

  • Turbine: framework for mocking exclusively StateFlow and Flow in unit testing.

  • Kakao: framework for testing composables in integration testing.

  • UiAutomator: framework for testing composables and views in integration testing.

  • Gemini: AI tool that helped me in various ways which I will describe later.

We will dive deep in some of those in the development journey!

Development Journey

I want to start by saying that you can easily see my journey in the various branches and commit history in GitLab. To not write extensive and tedious paragraphs, I will chronologically explain it making use of a bullet list, denoting the most important changes and lessons learned. After it, I will describe all premises I made in order to make the app and the AI tools guidance and usage.

  • First things first, when the code was handed over to me I looked at it and I updated all dependencies (also including ones that I was sure I was going to use: Hilt/Dagger and Coroutines) and changed the project basic structure to match the DDD architecture. Nothing fancy.

Now lets talk about some code:

  • Reviewing the unmodified PriceProvider class, I noticed that the API call to get the available tariff was performed manually instead of using Retrofit. I refactored all the structure of it, making a singleton instance of the TariffApi, defining the contract TariffRepository and implementing said repository in TariffRepositoryImpl by injecting the newly created API.

    • Far in development, I thought about not having any error handling on the API call, as it is stated that the JSON will not be modified nor altered structurally, but I did it anyways. Before the API returned a plain PriceConfigurationDTO and after the change it returned Response<JsonObject>, making it much more versatile when it comes to validating input and error handling.
  • Taking a look at the view model, it managed the StateFlow like the view model itself and anyone making use of those flows could modify them. I sense this as a sign of failure and intense debugging sessions, so following the ceparations of concerns, I modified the flows to only make them readable for the view and take the view model as the only figure to be allowed to modify them.

    • Later on, I changed my mind on how to handle the UI state with all these flows and I decided to make a generic UI state Resource that defines all UI possible states based on the definition of the problem: Loading, Loaded, Error and OngoingRoute. With all these states, the view only has to worry to show different composables in each state while the view model only has one flow (keeping the separation of concerns) that changes state depending on the retrieved data. Every state has its own set of parameters to support the view on painting whatever information it wants.

    • After this major UI state management refactor, I supported the idea of letting the view model do all the work for the view and just let it paint the screen... On a second thought, the screen should be in charge of managing its own state (like animations and forms) while the view model should only be the intermediary between the screen and the data (as declared before). So I refactored a little bit the Resource and branched it to TaximeterDetails for the luggage, route and configuration management in-view, keeping all other necessary parameters in the states. In this way, I followed an even better approach of the separations of concerns concept.

  • While programming and testing the algorithm, I discovered missing decimals when calculating the elapsed time between LocationPoints in milliseconds, as they are Long typed. Transforming them into seconds resulted in some decimals not adding up and thus money loss. Same with the kilometer calculation. I had to manually cast to Double to adjust the decimals and resolve the issue.

  • 🚀 After the basic app was finished, I showed it to my girlfriend and she proposed me to use custom routes, as she really liked the Google Maps Preview with the sample routes. So I thought about it and I finally implemented it. You can select any point in a Google Maps instance and manually create any route you want and test the pricing algorithm.

    • This was a challenge because I had to use the DirectionsAPI and transform it into something the pricing algorithm would understand: I got rid off all nullable fields at a domain-level and only gathered compulsory fields as I specifically wanted all Steps from the first Leg from the first Route (you can take a look at the API resonse here to really understand what I am saying).

      Later, the response was a list of Step with all LatLng steps already provided to start the route. In order to match it with the current implementation of the UI, I modified the repository-level response to a Flow (to match the LocationProvider's response) so every 250ms a new map point would be emitted to the UI to be shown.

      After that, I had to adapt the Step class into LocationTmpPoints for the algorithm to work: you can carefully see the extensive comment in DirectionsRepository to understand what I did to translate the steps in DirectionsRepositoryImpl.

      To add a little bit of substance to the new feature, I implemented the GeocodeAPI from Google Maps in order to retrieve the fomatted address of a given LocationTmpPoint to show the user from where the route started and where it ended while the trip is active.

    • Finishing up the project, I was going to add to the Future Plans section something like: Adjust the polylines of the Google Maps steps to adjust to the roads correctly. Reviewing the code of DirectionsRepositoryImpl I ended up discovering a flaw: I was emitting only the starting and ending point of each step instead of the whole decoded polyline that actually contains all the points for each step. I adjusted it, completely changing the conversion algorithm, and the Custom Route selection screen works perfectly fine like Google Maps!

  • Testing the code is always good to ensure the robustness of the layers and its functioning. I began the testing journey with the unit tests: proving the viewmodel and repository implementations are viable and good.

    • For the repository implementation, its a no brainer: test every possible case with the mock responses of the API.

    • Now, the view model has been altered a lot in the development process, as you have already read. I began the unit testing with all the StateFlow variables and with UI-only functions. I had to use @VisibleForTesting in some flows, something I had been eager to not use as it conflicts with the separation of concerns.

      After all the UI-state manager refactor, I had to refactor the tests of course, but the startRide test testing the LocationProvider flow was not succeeding. Finally, after some more tweaking I analyzed the view model and noticed the LocationProvider was, indeed, a third party data provider (what an oversight!): I had to abstract and inject it instead of calling it directly to match the architecture. This move was the key to successfully test the flow and properly apply the proposed architecture (with the help of Gemini and Turbine).

    • After all tests were developed, I decided to create a new library sharedTest in order to set shared mock objects and variables for the test accross unit and UI testing. Due to this change, LocationPoint, Route and ExecutionConfiguration (which are a business model / third party data) was secretly propagated across the app and thus, the sharedTest was not able to retrieve it, as it only knows domain models. I ended up creating LocationTmpPoint, SampleRoute and SimulationSpeed to transform them to domain models, which sharedTest can look up and use. I am stubborn, I know, but this perfectly aligns with the layered architecture and was another oversight by me that had not been tackled after a long time.

    • Testing Google Maps UI was not easy at glance because the Google Maps compose library is just a wrapper for a View and it depends on a completely different view tree. Because of that, Kakao cannot look up anything inside GoogleMapsComposable. After researching a bit, I had to use UiAutomator to carefully retrieve UiObjects with the contentDescription field instead of testTag (thanks to Ara Hakobyan).

  • Finally, after all testing was perfectly executed, I was able to implement the error handling from the API calls. Before this, I was under the assumption that if any error occurs while retrieving PriceConfiguration or DirectionDTO or GeocodingDTO I would provide a default value to not cause any error to the user. When this change applied, I was able to identify any failure with the DataResource generic class. This class would state the type of error from the API calls and transmit it to the view model and therefore to the view using the Resource.Error state. A new ErrorScreen popped to the user if any failure occurs at any time with the option to retry the operation.

Premises, Actions and New Features

Assumptions and new features made along the way to make the app fit the challenge and enhance UX and functionality:

  • Ride supplements cannot change when the ride begins (you cannot take luggage from a stranger in the street thrown at you). In the same way, you cannot alter the route nor the velocity.

  • The PriceConfiguration can change on the fly. I am retrieving it every second to check the value and update it consequently. I assume the JSON structure will not change: in any case we would have to monitor the API usage and cost to see if its doable every second or if we have to extend the delay.

  • You can stop the ride at any time, resetting the route (the driver magically delivers you to the starting point and he/she keeps the money).

  • I disabled the dynamicColor as I want my app to match business styles and not user defined ones. Most real apps (outside Google’s own suit) use a pre-defined non-alterable color scheme, and that’s what I will be using.

  • I added the ability to change route and execution configuration manually for better testing.

  • I added Google Maps view to show the current route in real time as any VTC app does. Furthermore, I added a new mode in order to test new routes aside from the samples given by LocationProvider: using the DirectionsAPI and GeocodeAPI from Google Maps the user is capable of manually select an starting point and ending point and see how much it will cost based on the processed route and with the current PriceConfiguration and supplements applied.

  • Available languages: spanish and english with accessibility concerns.

  • UI is updated for each point retrieved from the LocationProvider instead of waiting for all points to show the whole route and then animate the route. I tried to mimick Google Map's real time scenario of an ongoing route (without knowing where the route will end).

  • Two resource states has been defined: Resource, for view related state management and DataResource, a much simpler version for API calls state management. I could have defined one single state management generic class for the whole app, but I think this is better this way.

  • For the Polyline conversion/adaption algorithm in DirectionsRepositoryImpl I assumed that each LocationTmpPoint in the polyline has proportional time duration based of the size of the polyline itself. That is: if the polyline has 10 points and the step lasts for 10 minutes, each point has a duration of 1 minute, independently of the distance between the point before and after. In this way, in the UI you can see the minutes counter steadily increment while neglecting distance.

  • As a handy function I implemented a super basic gradle task to run all unit and integration tests with one click: runAllTests.

  • If you take a look at the journey in git, you will see I have always kept in mind orientation changes. The last thing I implemented was custom UI on the Google Maps Route Selection for landscape and portrait.

Use of AI - Gemini

I would be lying if I did not make use of any AI on the project. I used Gemini for this purposes:

  • Generation of color palette and typography to match purple colors.

  • Suggestion of variable naming for DTO / Domain models.

  • Partially commenting functions and variables.

  • Mostly I have used this as an overpowered Stack Overflow and Android documentation, asking a lot of questions about tests failing, mocking flows and injecting parametrized providers (LocationProvider).

    • I have to clarify that without Gemini I could not have abstracted the LocationProvider properly with Hilt. I gave a proper prompt to tell the problems I was having and it suggested me to use @Assisted parameters and @AssistedFactory to create a factory for the provider. Later I just had to inject it in the view model and with that, proper concern decoupling and testing could be done.

    • Another big thing is that it suggested me to use Turbine to test the LocationProvider flow, thing that I did not know how to do and ended up correcting the test and successfully passing the test.

Future Plans

  • In order to properly control the versions, down in the road I should implement a toml file for version control.

  • Download tiles from Google Maps in order to use them offline in routes in case internet does not work.

  • Google Maps API key management: its not ideal to save the api key to the BuildConfig as it is a plain text in the APK. I should use EncryptedSharedPreferences or store the API key in a remote storage and fetch it. For demo purposes, I will leave it in BuildConfig.

  • Implement custom annotations to preview all languages with @Preview.

  • Add location in the Custom Route selection screen to default the map's camera to it.

About

Taximeter Challenge Simple Project with Google Maps Integration

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages