Why we use Kotlin Multiplatform and Redux

This blog post comes with a free app. Furthermore, it helps you to get rid of your trash. No pun intended.

Kai Widmer
dreipol

--

Co-authors of the article are Julia Strasser & Samuel Bichsel. Visit www.dreipol.ch to learn more about us.

Native Apps still are the way to go if it comes to performance, offline functionality, and last but not least providing the look and feel of Android and iOS.

However, in the case of business logic, data persistence, network calls, and even some UI logic — maintaining two different code bases have disadvantages. Maintainability is just one of them. JetBrains provides an ideal solution for us with Kotlin Multiplatform to share code between iOS and Android and still build fully native apps.

Since we at dreipol prefer to use the Redux architecture (see here) we decided to build our architecture stack for mobile apps on Kotlin Multiplatform and Redux-Kotlin.

The goal of this blog post is to present an implementation of such an app with a small but complete example to you. There is a public GitHub repository of our example app which this post covers. We’re often linking directly to the repo rather than adding a lot of sample code to this article.

Now, let’s explore why sharing is caring.

Our example app

As an example, the team chose an existing application from the App Store, which has the potential for a redesign. In addition to new developments, redesigning applications are also part of dreipol’s daily business. To cover the redesign would go beyond the scope of this article though.

The decision was made for the recycling app of the city of Zurich. The main function of our sample app is to inform users about the upcoming collection dates. It can be configured with the users’ zip code and their preferred disposal types. The app starts with a one-time onboarding for configuration purposes. Once completed, there is a tab bar with three sections as known from many apps. The provided disposal data is taken from an open API with which we can present the networking architecture. For this project, we are using a mono repository. This includes a shared module and both apps, Android, and iOS. The main reason for this decision is the use of SQLDelight as a database. At the time of creating the project, SQLDelight wasn’t compatible with a multi-repo setup.

Project setup & architecture

Coming up next: the basic project setup. With the help of Redux and being forced to encapsulate the views, we were able to extract a lot of UI logic and even the navigation/routing into the shared multiplatform module. Naturally, the complete logic for networking and business logic is only part of the shared module and only written once.

Redux and presenter

The diagram below demonstrates how Redux is embedded in our app and manages its state. The app-state contains the different view-states, the navigation state, and the app settings (for postal code and notifications).

The presenter for each view is defined in the shared module, too. The rendering of the view itself and the dispatching of the action events happens in the native parts. This ensures a natural decoupling of the views and the view-models. The MVP (model-view-presenter) pattern will not be discussed in detail, but you’ll find a lot of great literature online. Just use the whole expression to search, not only MVP.

The navigators are designed as a composite. That keeps the navigation logic clean and delegates the actual handling of the navigation state to sub navigators.

Views

We use the settings screen of the example app to illustrate the functionality of the presenters and views.

Shared

The settingsViewState contains all information needed to render the settings view. It also contains the subViewStates for detailed views (like postal code, notifications, …).

Each presenter is subscribing to the view state and calling the render function of the view if the view state changes.

Android & iOS

The view is implemented in the platform-specific code by a fragment for Android and by a ViewController for iOS. The list with the settings items is created in the implementation and fed with data in the render function.

Android: SettingsFragment.kt
iOS: SettingsViewController.swift

Navigation

Shared

The navigationState contains the stack of screens as history and the navigation direction to the last screen. The last screen in the list is the screen that is currently displayed.

Screens are defined as enum classes or in the case of onboarding as a data class with a step property that defines what step of the onboarding the actual screen is representing.

The NavigationAction enum contains all actions which are used for triggering a navigation change.

In the navigationReducer, the new navigationState is calculated from the dispatched action and the current state. As you can see below, in the case of BACK, the last screen in the list is removed. In the case of SETTINGS, the settings-screen will be added and in the case of ONBOARDING_NEXT, a new onboarding screen with the calculated step is added.

The navigator itself is an interface that provides a function to subscribe to the navigation state. With this, the navigator can be implemented by one or multiple navigators who take care of navigating to the current screen.

Android & iOS

On both platforms, the navigators are mapping the screens to the corresponding destinations. Also, the system’s default back-functionality gets intercepted and the NavigationAction BACK is dispatched to keep the navigationState in sync.

In the iOS implementation, we decided to go with the coordinator pattern. Each navigator has a corresponding coordinator which does the iOS-specific lifting. So the «general coordinator», which is an instance variable in the Appdelegate, contains the OnboardingCoordinator and the MainCoordinator. While the OnboardingCoordinator is responsible for handling the UIPageViewController in the onboarding, the MainCoordinator only handles the presentation of the detail view controllers. The MainViewController which is a UITabBarController shows its view controllers directly and only updates the navigationState.

On Android, the «general navigator» is implemented by the MainActivity. Furthermore, there are two sub navigators: One is the OnboardingNavigatorFragment which navigates through the onboarding using a viewPager to allow swiping. The other one is the MainFragment which contains the BottomTabbarNavigation.

Android: MainActivity.kt
iOS: NavigationCoordinator.swift

Database and networking

As mentioned we are using SQLDelight as a database and for networking ktor client. Asynchronous database queries and network calls are implemented as actions in the form of thunks.

Thunks receive the current app state and will then dispatch one or multiple actions to load the according to data into the state. They can be dispatched anywhere and are executed by middleware in the store. You can find thunks in the example app here.

A thunk in itself is nothing else as a function. While dispatching a thunk works fine in the Android part, directly dispatching it wasn’t possible in iOS. The underlying problem is, that through type erasure the Kotlin function dispatched in Swift wasn’t identified as such back in the shared module. The solution was to create a wrapper class called ThunkAction with a field called thunk of type thunk.

Testing

The use of Redux gives us a nice decoupled code which improves the overall testability of the apps. Since the navigation is part of the app state it is easy to test the app navigation programmatically to make sure after dispatching specific action the navigation state was updated correctly.

An example can be found here.

dreimultiplatform, dreidroid and dreikit

Besides our existing open source utils libraries dreidroid (Android) and dreikit (iOS), we added dreimultiplatform to the family, which is open source and ready for you to use 🎉. It contains a bunch of useful stuff that will help you create apps with Kotlin Multiplatform and Redux.

Summary

We have watched Kotlin Multiplatform coming a long way and we have not regretted to give it a try. In practice, the development of an app for the two platforms is neither faster nor slower than doing it natively at first. But we are much more efficient when it comes to bug fixing or guaranteeing the same behavior on both platforms. Furthermore, it stimulates the exchange between our Android and iOS Devs. We are convinced to speed up our productivity with more experience.

From a UX perspective, we also keep the ability to implement some UI behavior differently on both platforms, for example, confirmation dialogues. This is important to convey the look and feel of the iOS and Android platforms with the app to the user.

Most importantly we are really happy we developed a nice and futureproof toolset to use our platform-specific UI know-how in our future app projects.

Thanks for reading. We are keen to hear your opinions and questions.

. . .

Feel free to like this post, share it, or follow Kai, Samuel, Julia and dreipol on Social Media:

www.dreipol.ch

Kai, medium.com/kai.widmer

Samuel, twitter.com/the_melbic
medium.com/samuel.bichsel

Julia, medium.com/julia.strasser

twitter.com/dreipol
blog.dreipol.ch

--

--