Blog Infos
Author
Published
Topics
,
Author
Published

One of my 2021 new year’s resolutions was to dive in into Kotlin Multiplatform Mobile (KMM). I strongly believe that KMM is the only¹ framework created for native mobile developers, because it’s build upon the philosophy of becoming a code-sharing tool. This makes it possible to share platform independent logic, while preserving the platform specificone, in other words, UI/UX best practices.

My idea was to start by creating small proof-of-concept projects, gradually incrementing their complexity while trying to reach as close as I could to my “real life” problems.

The goal of this article is not to become another comparison between cross-platform frameworks — there’re a bunch of good articles already out there and my choice is made — , my goal it’s to share how we, as a team (me & Charles Prado), solved a particular problem regarding network responses.

Teamwork

If one were to ask me what changed considerably in my daily coding life — as an Android developer — when using KMM, I would say: teamwork.

The team has to keep in mind that for now, iOS is the weakest link, in a way that all the shared code must be validated by both platforms before merging it into main.
It should not be assumed that it will work just by evaluating on Android. We should also keep in mind that there may be slight differences in architectural patterns thus, a team reassessment of the best approach to follow, should be considered. KMM came to impose teamwork, so, it is in our best interest that it flows in the best possible way for everyone.

That being said, let’s illustrate what I mean with some code.

Android & Shared modules

In one of my previous articles, I’ve shown how we could take advantage of a sealed class to better illustrate use cases when consuming data from our repositories/managers:

Hier noch ein Link

For androidApp and shared modules, the approach remains the same:

sealed class UserStatus<out T : Any> {
class Success(val data: UserData) : UserStatus<UserData>()
object NoAccount : UserStatus<Unit>()
sealed class Error(val e: Exception) : UserStatus<Nothing>() {
class Generic(e: Exception) : Error(e)
class Error1(e: Exception) : Error(e)
class Error2(e: Exception) : Error(e)
class Error3(e: Exception) : Error(e)
}
}
view raw UserStatus.kt hosted with ❤ by GitHub

But does it work on iosApp just because it works on androidApp? Let’s find out.

iOS module

I won’t go into details about how Kotlin/Native (K/N) works, but briefly, gradle will generate Objective-C code. As of today² , K/N does not interop with Swift directly, only via Objective-C so we need to check first if Objective-C knows what a sealed class is and also if it supports Generics on classes. Short answer for the second: it does.

The following code is from the shared module:

suspend fun createReservation(): ReservationResult<Unit> {
return when (val response = api.createReservation(...)) {
is ApiResult.Success -> ReservationResult.Success
is ApiResult.Error -> ReservationResult.Error(response.exception)
}
}
sealed class ReservationResult<out T : Any> {
object Success : ReservationResult<Unit>()
data class Error(val exception: Throwable) : ReservationResult<Nothing>()
}

K/N will translate:

suspend fun createReservation(): ReservationResult<Unit> { ... }
view raw Suspend.kt hosted with ❤ by GitHub

into:

- (void)createReservation:completionHandler:(void (^)(SharedReservationsManagerdReservationResult<SharedKotlinUnit *> * _Nullable, NSError * _Nullable))completionHandler __attribute__((swift_name("createReservation(completionHandler:)")));
view raw Suspend.h hosted with ❤ by GitHub

In Swift it will be accessible as:

func createReservation(completionHandler: @escaping (SharedReservationsManagerReservationResult<SharedKotlinUnit>?, Error?) -> Void)

Job Offers

Job Offers


    Senior Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now

    Mobile Engineer

    OLX Group
    Remote, Portugal, Spain, Romania, Poland
    • Full Time
    apply now

    Kotlin Multiplatform Mobile Developer

    Touchlab
    Remote
    • Full Time
    apply now
Load more listings

OUR VIDEO RECOMMENDATION

,

Building a real-time collaboration tool using CRDTs and Kotlin Multiplatform

A quick introduction to conflict-free replicated data types (CRDTs) and how you can use these to build a real-time collaborative tool, where multiple clients can make edits to the same data without conflicts, even while…
Watch Video

Building a real-time collaboration tool using CRDTs and Kotlin Multiplatform

Anders Järleberg & Carlo Rapisarda
Lead Android Developer & iOS Tech Lead
Bontouch

Building a real-time collaboration tool using CRDTs and Kotlin Multiplatform

Anders Järleberg ...
Lead Android Develop ...
Bontouch

Building a real-time collaboration tool using CRDTs and Kotlin Multiplatform

Anders Järleber ...
Lead Android Developer & ...
Bontouch

Jobs

Thus, on iosApp we would expect to consume it like:

static func createReservation() -> Future<Void, Error> {
return Future() { promise in
manager.createReservation() { response, error in
if response != nil {
promise(Result.success(()))
} else if let error = error {
promise(Result.failure(error.toAppError()))
}
}
}
}
extension KotlinThrowable {
// Creates a LocalizedError using the error message returned from KotlinThrowable
func toAppError() -> AppError {
AppError.withText(self.message ?? "")
}
}

But for some reason — we still don’t understand —, the Error always returns null ?.

Let’s investigate the translated sealed class:

__attribute__((swift_name("ReservationsManagerReservationResult")))
@interface SharedReservationsManagerReservationResult<__covariant T> : SharedBase
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
@end;
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("ReservationsManagerReservationResultError")))
@interface SharedReservationsManagerReservationResultError : SharedReservationsManagerReservationResult<SharedKotlinNothing *>
- (instancetype)initWithException:(SharedKotlinThrowable *)exception __attribute__((swift_name("init(exception:)"))) __attribute__((objc_designated_initializer));
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable));
+ (instancetype)new __attribute__((unavailable));
- (SharedKotlinThrowable *)component1 __attribute__((swift_name("component1()")));
- (SharedReservationsManagerReservationResultError *)doCopyException:(SharedKotlinThrowable *)exception __attribute__((swift_name("doCopy(exception:)")));
- (BOOL)isEqual:(id _Nullable)other __attribute__((swift_name("isEqual(_:)")));
- (NSUInteger)hash __attribute__((swift_name("hash()")));
- (NSString *)description __attribute__((swift_name("description()")));
@property (readonly) SharedKotlinThrowable *exception __attribute__((swift_name("exception")));
@end;
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("ReservationsManagerReservationResultSuccess")))
@interface SharedReservationsManagerReservationResultSuccess : SharedReservationsManagerReservationResult<SharedKotlinUnit *>
+ (instancetype)alloc __attribute__((unavailable));
+ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable));
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable));
+ (instancetype)new __attribute__((unavailable));
+ (instancetype)success __attribute__((swift_name("init()")));
@end;

We have three interfaces but what quickly caught our attention was the fact that the completionHandler(Data?, Error) is using the ReservationResult as Data instead of using ReservationResultSuccess. OK, with this in mind, we’ve tried this:

static func createReservation() -> Future<Void, Error> {
return Future() { promise in
manager.createReservation() { data, error in
if let success = data as? ReservationsManagerReservationResultSuccess {
promise(Result.success(()))
} else if let error = data as? ReservationsManagerReservationResultError {
promise(Result.failure(error.toAppError()))
}
}
}
}

 

But unfortunately the compiler will not allow the ReservationResultErrorcast (we yet fail to understand why). Not only this invalidates the previous cast approach but it won’t allow either switch over out sealed class.

Resuming: completionHandler will use the ReservationResult interface as Data and apparently will ignore ReservationResultSuccess and ReservationResultError headers. Let’s try to find a solution for this.

Solution (workaround)

Trying to solve this problem I’ve created the following abstraction:

abstract class ManagerResult<out V : Any, out E : Throwable> {
open val data: V? = null
open val exception: E? = null
val isSuccess: Boolean get() = !isFailure
val isFailure: Boolean get() = exception != null
}

My intention was to change the less I could on androidApp and shared modules while helping out iosApp. All the sealed class results were changed to:

//void example
sealed class ReservationResult<out T : Any> : ManagerResult<T, Throwable>() {
object Success : ReservationResult<Unit>()
data class Error(override val exception: Throwable) : ReservationResult<Nothing>()
}
//value example
sealed class ReservationListResult<out T : Any> : ManagerResult<T, Throwable>() {
data class Success(override val data: List<Event>) : ReservationListResult<List<Event>>()
data class Error(override val exception: Throwable) : ReservationListResult<Nothing>()
}
view raw Results.kt hosted with ❤ by GitHub

iosApp calls changed to:

//void example
static func createReservation() -> Future<Void, Error> {
return Future() { promise in
manager.createReservation() { data, error in
if data?.isSuccess == true {
promise(Result.success(()))
} else if let error = data?.exception {
promise(Result.failure(error.toAppError()))
}
}
}
}
//value example
static func fetchReservations() -> Future<[shared.Event], Error> {
return Future() { promise in
manager.reservations() { data, _ in
if let success = data?.data {
promise(Result.success(success as! [shared.Event]))
} else if let error = data?.exception {
promise(Result.failure(error.toAppError()))
}
}
}
}

androidApp kept unchanged. Done.

Conclusion

We don’t believe this is a final solution because we have the dead weight of Error, and Data shouldn’t behave as both, but this illustrates a common situation where apparently the first approach was OK, since it was working for androidApp, but in reality it was broken for iosApp. Once again, in my opinion, KMM is all about great teamwork, because only then we can overcome situations like this, while also avoiding part of the team feel disregard leaving room to become angry against the framework. Keeping this in mind, KMM has huge potential to become a top pick for future projects.

Feel free to reach me out and Charles Prado on Kotlinlang’s slack if you want to discuss this further, we would appreciate it.

 

I hope you find this article useful, thanks for reading.

Featured on Kotlin Weekly #256 ?

[1]: When compared to Flutter, ReactNative and Xamarin.

[2]: Kotlin 1.5.10 | Roadmap

Thanks to Charles Prado.


Tags: Kotlin Multiplatform, Android, iOS, Android App Development, AndroidDev

 

View original article at:


Originally published: June 25, 2021

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Kotlin Multiplatform despite all of its benefits sometimes has its own challenges. One of…
READ MORE
blog
I develop a small multi-platform(android, desktop) project for finding cars at an auction using…
READ MORE
blog
Kotlin Multiplatform Mobile (or simply Multiplatform Mobile) is a new SDK from JetBrains that…
READ MORE
blog
We will review a framework for building KMM apps with the objective of not…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu