Android Stack at Kapten
As one of our first blog post about Android at Kapten, we wanted to present how we are doing our best to build strong and modern applications. Before diving into technical discussions, here’s the history of our rider and driver apps on Android.
The rider App was started in 2012 with no specific architecture, and Fragments and Activities handled everything with no unit or UI tests. The driver app was developed in the same way in mid-2015 in only three months. There was a lack of architecture, no unit tests, but some UI tests were added at the end of the development. This can be explained by the context of the hyper-growth of the company.
Late 2015, the rider app started to evolve: designing it with MVP and adding more and more unit tests to our presenters. On the other side, the driver app architecture didn’t change that much.
The third iteration of the rider app started mid-2016 by embracing clean architecture concepts and reactive programming. SOLID principles, dependency injection, unit testing, continuous deployment, PR review… all these keys concepts started to be mandatory.
Meanwhile, the driver app was under freeze state, waiting for us to decide if we had to rewrite it using react native or not. Six months later, we concluded that keeping up with native development was the smarter choice. So we restarted working on it to catch up with the changes made on the rider app.
Here we are today; all new features are fully unit tested, designed with clean architecture concepts with reactive programming, we started adding UI tests at scale and all of this 100% in Kotlin. Indeed, we have fully embrace Kotlin for both apps. We are now more than 73% in Kotlin for the rider app and up to 92% for the driver app (in March 2018, we were around 27% for both). We also started to share more code gradually between the rider and the driver app by developing our own libraries to fit our needs.
Apps & In-House Libraries
Swipr
Our main in-house library to handle the Android lifecycle, built with MVP pattern first, then we moved to the MVVM pattern. Following the years, significant updates were made to improve it, for example, integration of RxJava at some point. These breaking changes made us using three iterations of the lib in our Apps:
Swipr-Legacy
- Swipr: Born with version 3 of the app in 2016, it’s written in Java with MVP pattern in mind, handling Activities and Fragments lifecycle. Implementation is quite basic but was helping to avoid boilerplate
- Swipr2: Close to the previous version, the difference is the introduction of RxJava1 for the presenter and the business logic (called Interactor in our code base)
Swipr (Third iteration)
Started less than two years ago, it’s a rebuilt from scratch to our core library to address numerous issues. It has been rewritten in Kotlin and with RxJava2. We needed to improve the definition of responsibility for several architectural elements. For example, we moved to the MVVM pattern, and we defined Input and Output used by the view model (Inputs are observers, and Outputs are observables). One new necessary change is the will to stop using Fragments and Activities. Indeed, we want to use as much as possible Views, which are wrapped around a class we called ViewHolder in Swipr.
Nucleus
One year ago, we started to change how we used to develop some of our Interactor. What we wanted is to get something closer to Redux, being able to handle a state which can be listened to by our presenters (or other interactors). We did the first iteration of a state machine in early 2018, it was beneficial, but it generated boilerplate with each implementation. That’s why we decided to create a library that can be used across both applications and to reduce boilerplate. At the beginning of 2019, our first steady version (ready for production) of Nucleus was born
The mains goals are :
- Isolate side effects
- Have an unidirectional data flow
- Have a single source of truth
- Be easily composable
- Be easily testable
An observer of action can be called to ask for a specific sequence, then the reducer inside the Interactor will handle the action and create a new Pair of State and Command. A State represents the status of the business logic; it can be a simple enum class or a more complex data class. A Command is a representation of a side effect from the action.
An example is better than precept, so here is a simple case of an interactor using Nucleus :
FluxDelux
A simple logger inspired by the existing Kapten iOS library. This project aims to provide a smooth and extensible logger entirely written in Kotlin. It is composed of four components:
- A Destination which sends logs somewhere (ex: Console, LogCat, ELK …)
- A Formatter which determines how your logs will look like
- A Processor which enhances your log with custom data (ex: device info, user info etc)
- A TriggerStrategy which determines when your log have to be send (ex: immediately, when your app goes to background, every twenty seconds …)
We are aiming to open-source this library; when ready, we will write a blog post about it. Stay tuned!
Translations
In early 2018, when we initiated the process to launch new European cities, one of our main focus was to simplify the translations process. We decided to use PhraseApp to manage all the translations and started to look for a helpful solution for handling plurals and placeholders. After searching for an existing solution, we decided to build our own as it was simple and easier to maintain. We based our format on i18next, which was already used by the backend.
You can learn more about the internationalisation of our app on Android here.
Pratik
This library contains utils and extension functions used by both driver and rider apps. It includes utils for Android, RxJava, dates, and so on … For the non-french people, Pratik is a joke on “pratique,” a french word, which means “convenient.” Yes, the French are funny too.
Architecture
We built our apps inspired by Clean Architecture from Uncle Bob, which has one primary objective: the separation of concerns. I’m not going to discuss it any further, as I recommend to read the original blog post here for the none initiates. He also released an excellent book, which is much more detailed than the blog post.
Data layer
A mix of Retrofit, OkHttp, RxJava combined with the repository pattern is the recipe of our data layer. The logic of the repository is in charge of calls between the API, the database, or the SharedPreferences. In the end, we map Entity models to data class, exposed as reactive streams that are going to be consumed by the business logic.
Domain layer
Handled by Interactors, that received the inputs from the view and then consumed them in the state machine, to generate new states and possibly associated Nucleus Commands. For example, those can query a repository to fetch (or update, create …) data that could be used for the view.
Presentation layer
The presenter will map the Interactor’s state to a consumable object by the UI (Localized Strings, formatted date, colors, drawables …), exposed as outputs to the UI, making Views as dumb as possible. And, as said earlier, the view inputs (user interactions) are forwarded to Interactors.
A slice of code
Continuous deployment
We built our CI on top of Fastlane, by developing a suite of Ruby scripts. From now on, our lanes are running on Bitrise; as we chose to develop our scripts using Fastlane, we are CI independent. We have four main lanes:
Pulling translations
A script is running every night, which pulls our translations stored in PhraseApp, and creates a Pull Request on Github if changes are detected. The following day, a developer checks it. If the CI is passing, it is merged, or else a manual fix has to be done.
Feature checkup
Triggered by Bitrise when a PR on Github is submitted or a new commit is pushed to an existing PR. The goal is to run unit tests, code coverage, and static code analysis using JaCoCo and SonarQube. If one of those tasks failed, the merge is blocked. Otherwise, after a successful merge, an APK is built to be uploaded on Amazon S3, to be tested and validated by the QA team.
Integration checkup
Launched after a merge on the master, this lane is very close to the previous one. In fact, we run the unit test, and an APK is built to be uploaded on Amazon S3. The version builds on master is used internally for testing purposes on staging being iso-prod.
Beta
The only lane triggered manually, after running all sanity checks. It’s an automatisation of our internal beta release process: bumping app version, updating the changelog, creating a PR with a tag, and uploading a new beta on Fabric.
Release Process
On Thursdays, a beta is built and deployed with Fabric for Kapten employees. The Play Store release is on Wednesday, every two weeks. Every release will be tested internally before hitting production; this is an essential step of the release process as this is the last chance to catch any critical bugs before it ends up on our clients’ devices.
On top of that, the beta version, which will be a public release, goes through a sanity check. Meaning QA engineers run some regression tests before submitting the application to Fabric.
Part of the journey is the end
Quality is one of our fundamental values at Kapten. Therefore we are always eager to improve the way we build the best user experience possible for our apps. Over the years a lot of things evolved to face the hyper-growth of the business, we think we did an excellent job by balancing between the need of the product to change and the evolution of the Android ecosystem.
As we are continuously growing, opening new cities, and adding new features, we are always looking for talents to join our team, don’t hesitate to contact us.