MVI in Kotlin Multiplatform — part 2 (2 of 3)

Arkadii Ivanov
Bumble Tech

--

This is the second in a series of three articles on MVI architectural pattern in Kotlin Multiplatform. In the article part 1 we reminded ourselves what MVI is and learned how to use it to write shared code using MVI. We defined simple abstractions like Store and View as well as some helper classes and used them to create a shared module. This module’s job is to load lists of image URLs from the network and to wire the business logic with UI. The UI is represented as an interface and is to be implemented natively in every platform. This is something we are going to cover in this article.

In this part, we are going to implement platform-specific parts of the module and integrate the module into iOS and Android applications. As before, I assume some expertise in Kotlin Multiplatform. I’m not going to cover any project configurations and other stuff unrelated to MVI in Kotlin Multiplatform.

The updated sample project is available in our GitHub: https://github.com/badoo/KmpMvi.

The plan

In the first article we defined the KittenDataSource interface in our shared Kotlin module. This data source is responsible for loading image urls from the network. So now it’s time to actually implement it for both iOS and Android. We will use the expect/actual feature of Kotlin Multiplatform. After that, we will integrate our shared Kittens module into iOS and Android apps. For iOS we will be using SwiftUI, and for Android we will use normal Android views.

So, the plan is as follows:

  • Implement KittenDataSource in Kotlin
    - For iOS
    - For Android
  • Integrate Kittens into an iOS app
    - Implement KittenView using SwiftUI
    - Integrate KittenComponent into a SwiftUI view
  • Integrate Kittens into an Android app
    - Implement KittenView using normal Android views
    - Integrate KittenComponent into a Fragment

Implementing KittenDataSource

Let’s first remember what this interface looks like:

And here is its factory expect function that we are going to implement:

They are all internal which means they are implementation details of the shared module. Using expect/actual feature we can access platform specific API.

KittenDataSource for iOS

Let’s first implement the data source for iOS. In order to access platform specific API, we need to put our code in the “iosCommonMain” source set. This custom source set is configured in such a way that it depends on the “commonMain” source set. Leaf source sets (“iosX64Main” and “iosArm64Main”) both depend on the “iosCommonMain” source set. You can find the complete configuration here.

Here is the implementation of the data source:

Using NSURLSession is a very popular way of loading data from the network in iOS. It is async under the hood so manual thread switching is unnecessary. We simply wrapped the call into Maybe and added some response, error and cancellation handling.

And here is the actual factory function:

Now we can compile our shared module against iOS targets: iosX64 and iosArm64.

KittenDataSource for Android

In order to access Android API we need to place our code in the “androidMain” source set. Here is the implementation:

For Android we are using HttpUrlConnection, again this is a popular way of loading data in Android. This API is blocking, so we are purposefully switching to a background thread using the “subscribeOn” operator.

Here the actual factory function is identical to that for iOS, except that being implemented in the “androidMain” source set it creates the Android specific implementation.

Now we can compile our shared module against Android.

Integrating Kittens into an iOS app

This is the hardest (and the most interesting) part of the article. Suppose that we built our module as described in the sample’s README. We also created a basic SwiftUI project and linked the Kittens Framework into it. Now it’s time to integrate the KittenComponent.

Implementing KittenView

Let’s start with the KittenView implementation. Let’s remind ourselves what its Kotlin interface looks like:

So, our KittenView accepts Models and produces Events. In order to render the Model in a SwiftUI view we need to write some kind of proxy:

The proxy conforms to both KittenView and ObservableObject protocols. The KittenViewModel is exposed via the @Published property “model” so our view will be able to subscribe to it. We used the AbstractMviView class we created earlier. Thanks to its design, it saved us from interacting with the Reaktive library. We can dispatch the UI events using its “dispatch” method.

Why are we keen to avoid the Reaktive library (or coroutines/Flow) in Swift? Because Kotlin-Swift interoperability has several limitations. e.g. generic parameters are not exported for interfaces (protocols), extension functions cannot be called directly on receivers, etc. Most of the limitations are because the Kotlin-Swift interoperability is performed via Objective-C (you can find all the limitations here). Also, because of the tricky Kotlin/Native memory model I believe it’s better to have as little Kotlin-iOS interaction as possible.

Now it’s time for the SwiftUI view itself. Let’s start by creating a skeleton:

We declared our SwiftUI view that depends on the KittenViewProxy. The @ObservedObject property wrapper type subscribes to the observable object (the proxy) and invalidates the view when the observable object changes.

Now let’s proceed with the view implementation:

The main part here is the content view. It takes the model from the proxy and displays either nothing, an error message, or a list of images.

The body may look like this:

It displays the content within the NavigationView and adds title, activity indicator (loader) and a refresh button.

Each time the model is changed, the view will automatically be updated. The activity indicator is displayed when the “isLoading” flag is set to true. When the refresh button is clicked the RefreshTriggered event is dispatched. The error message is displayed when the “isError” flag is set to true, otherwise the image list is displayed.

Integrating KittenComponent

Now we have the view, it’s time to use our KittenComponent. SwiftUI has nothing but views, so we will have to wrap both the KittenSwiftView and the KittenComponent into another SwiftUI view.

The SwiftUI view lifecycle has just two callbacks: onAppear and onDisappear. When the view is displayed its onAppear callback is called. When you navigate to another view or just close the current one, the onDisappear callback is called. There is no explicit callback to handle view destruction. So, we will use the “deinit” block that is called when objects are reclaimed.

Unfortunately, structs cannot have “deinit” blocks, so we have to wrap our KittenComponent into a class:

Lastly, let’s implement our main Kittens SwiftUI view:

The crucial part here is that both ComponentHolder and KittenViewProxy() are defined using @State property wrapper. Invalidated view structs are recreated on every UI update but @State properties will be preserved by SwiftUI across view struct instances.

Everything else here is trivial. We are using the KittenSwiftView. When the “onAppear” callback is called we are passing the KittenViewProxy (that conforms to the KittenView protocol) to the KittenComponent and starting the component. When the “onDisappear” callback is called we are calling component’s opposite callbacks. The KittenComponent continues to work even if you navigate to another view, until it is reclaimed from memory.

Here how the iOS app looks:

Integrating Kittens into an Android app

This is going to be much simpler than for iOS. Again, suppose we have created a basic Android app module. So, let’s start with the KittenView implementation.

The layout is nothing special. Simply SwipeRefreshLayout and the RecyclerView:

The KittenView implementation:

As in iOS, we are using the AbstractMviView class to simplify the implementation. The RefreshTriggered event is dispatched when swipe refresh is triggered. The error snackbar is displayed when there is an error. The KittenAdapter displays image urls and is updated every time the model changes. DiffUtil is used under the hood to prevent unnecessary UI updates. You can find the KittenAdapter implementation here.

Time to use the KittenComponent. In this article I’m going to use Androidx Fragments, but do check out our RIBs framework. It is a safer and more powerful alternative to Fragments.

The implementation is very simple. Just create the KittenComponent and call its callback at the appropriate time.

This is what the Android application looks like:

Conclusion

In this article we integrated the shared Kittens module both into iOS and Android applications. First, we implemented in Kotlin the internal KittensDataSource that is responsible for loading image URLs from the network. We used NSURLSession for iOS and HttpURLConnection for Android. Then we integrated the KittenComponent into the iOS project using SwiftUI and into the Android project using normal Android views.

In Android the integration of the KittenComponent was very simple. We created a simple layout with RecyclerView and SwypeRefreshLayout and implemented the KittenView interface by extending the AbstractMviView class. After that we used the KittenComponent in a Fragment, just created an insinstance and called its callbacks. Overall, the integration was very straightforward.

In iOS, it was a bit more complicated. Given the nature of SwiftUI we had to write some additional classes:

  • KittenViewProxy — this class is both the KittenView and the ObservableObject at the same time. It does not render the view model directly but exposes it via @Published property.
  • ComponentHolder — this class holds an instance of the KittenComponent and calls its onDestroy method when the holder itself is destroyed (reclaimed).

In the third (and last) article I will be demonstrating how to test the shared code and writing some unit and integration tests.

Stay tuned and follow me on Twitter!

--

--