So, hi, everyone. Welcome back to the next talk. Today we have on stage Anna La Bellarte
and Paolo Rattolo from Nextome talking about cutting multiplatform for Android and iOS library
developers, I guess it was. Now, yeah, gonna talk about it. Awesome. Thank you. Yes, I'm Paolo
and it's a very pleasure to be here with you. We come from Italy. We come from a small company
in the southern Italy and we decided to introduce cutting multiplatform in our code about a year
ago. We did this because for us it was easier to develop and also we wanted to share as much code
as possible. We make libraries so we didn't have the part of UI to transpose in the multiplatform
and this is our journey in the multiplatform world. Now, we've seen during this conference that we
cut in multiplatform mobile. We can develop a library that targets both Android and iOS
by just writing a single code base in Kotlin. Now, if you look at what happens when we distribute
the jar inside an Android library, we can say that the process is pretty straightforward.
Now, for an Android developer, the language is the same. The IDE that uses is the same and most
of the library that we can use are the same. So, we can't tell the difference between a library
that was made with Kotlin multiplatform mobile and one made with the native tooling. Now,
things are not quite the same when we talk about the iOS part. So, if we distribute the framework,
the process is not so straightforward. And the main of the problem arises because the code is
converted for an objective C and then most of the time we'll be using side projects that
has Rift as the main language. Now, for a Rift developer point of view, sometimes what can happen
is that they can find the API that we expose is just strange and this is just the base case scenario.
Other times, the app may just crash and this is due to some differences that there are between
the two platforms that aren't automatically translated for us by the compiler. So, we will see
during this talk what we can do to what happens when we distribute the framework and what we can
do to make libraries enjoyable also for the iOS part. Now, let's start with a simple example.
So, in this case, we talk about coroutines. So, inside our common code, we can use coroutine.
So, we can have a function like this, which is a suspend function because it performs some
operation with the network and some interaction with a persistent layer. Now, on Android, we
don't have many issues because if we have a coroutine scope, we can launch the coroutine.
But what happens on iOS when we don't have the coroutine library? Now, this function gets converted
by two alternatives. The first one which uses a completion handler, which is basically a callback
that gets called either when we have a result or when, in this case, the to-do variable would be
populated or when we have an error. So, in this case, I will have the error variable populated.
Now, if we don't want to use the callback because it can become cumbersome when we have
different functions, one inside the other, we can use the second alternative which uses
sync and await, but we have to target at least iOS 13. Now, if we go back on Android, we launch
the coroutine inside the scope, and this means that if I don't need the job anymore, what I can do
is just cancel the scope and then also the job gets cancelled. By default, I don't have this
power on iOS. So, what we can do is try to use a library which is called Coru.
Yes. We fixed that problem with Coru that is actually a library inspired by a blog post of
TouchLab. So, thanks to TouchLab for this. Does it work? Basically, you have to include, of course,
in the common dependency of your code. And using this library, it basically introduces you to a
new annotation that is too native class. So, with that annotation, you can specify a name for a
class that will be generated just in the iOS implementation of your code. So, if you have a
look at the generated class, we can see that it is basically a wrapper of our original repository.
So, you have two parameters that are passed. We have a wrapper that is the original repository,
and we have a scope provider. We'll see what a scope provider is later. And all the methods of
that generated class are the same methods that you have in the original repository, but the result
type, list of to-do, is wrapped in another object. So, if we try to use this on iOS, the code that's
get generated is something like that. We have two callbacks actually now, but I can see, for
example, other two problems with that code. First of all, we are exposing a coroutine scope for
iOS developers, and maybe iOS developers are not familiar with that concept of scope, like
coroutine developers are. And also, we have that object that now is an SRA, and it is not a list
of to-do anymore. That is because we are wrapped in a list of to-do in another object that accepts
generics. And the Objective C translation of the Kotlin code cannot do that for now, so it gets
simplified to an SRA. To solve this, we go back in the common code, and we use another function
of that library, that is, launch on scope. So, we can define a scope in common code and tell
the library to run all our coroutines in that scope. So, the scope will not be provided by
iOS developers anymore. Also, for the second problem, it's just a workaround. We can define a
data class, we can define a typology, or something to hide the fact that we are using a list of
something. So, if we try to generate a new code for this, this is the final code that is much
more readable and usable. And, of course, now we can dismiss the job if we are not interested in
the result anymore. What about flows? We'll talk about coroutines. What about flows? This is an
example of a simple flow that emits integers. Of course, again, on Android, it is simple, you
have a scope, you can start collecting values. As with the code that gets generated, it is
something like that. We still have a collect function to collect the values of the flow,
but we have to pass this, that is a flow collector, so we have to implement the function emit,
do something with that value that gets emitted, and then when we are done, call the
completion handler so we can receive the next value. Also, notice that we don't have the type
that we are collecting from the flow, we just have any. I can prove this. First of all,
we tried to make that collector generic, so we can use it in more parts of our code,
and so we exposed another callback. Yes, make it generic. So we exposed another callback,
and we actually casted the value as the one that we wanted, but we found that this is not
good enough. Also, because the highest developer has to do this in his code, we are not doing it
in common, so every time he has to use our library, he has to define this. So we decided to fix it
in common code, and again, we used this, that is a common flow. There is a class found actually in
the Kotlin Conf app, and this class wraps up flow and basically emits all the values of the flow
and returns a crossable object, so you can dismiss the flow when you don't want to listen to it
anymore. So again, we return now a common flow using the extension function, and on iOS again,
now we have a much more readable code that we can also cancel if we want.
Now, we mentioned before that sometimes the app may crash because of the differences between the
two platforms, and one great example of this is our exception handling are handled in the languages,
because Kotlin only works with unchecked exception, while Swift only works with the
unchecked one, and now we will see what this means and what happens. So if I have, we are
bringing back the function from the coroutine that we saw before, so in this case, because in
Kotlin, I don't have to mark explicitly each throwing function, I can wrap it inside a
tri-catch, so if something happens, I will receive the error inside the callback. Now,
if we bring back also the first alternative that we saw before with coroutine, what I expect is
to have the error in the function, but if I launch the application, it actually crashes,
and this is because in Swift, I have to mark explicitly each throwing function, so the fix is
actually quite easy, because there is an annotation that we can use, which is called
throws exception. So by doing this, just in the common code, and we don't have to make any changes
inside the Swift implementation, so in this case, I will receive the error in the callback,
and this works also with non-suspending functions, so if I have this function and mark it
throws and exception, once I compile the code, the generated function in Swift will be marked as
throwing, so this time will be the compiler to force us to handle the exception. Now,
another API that is not quite Swift-friendly is the one of sealed classes. Now, on Kotlin, we
can restrict the concept of inheritance by using sealed classes and sealed interfaces,
so when we use them inside our Android code, we can just make something like this, because,
okay, something like this, because we know for sure that those three, so data, error, and loading
in this case are the only cases that we have to handle, but on iOS, actually it gets converted
by just using the concept of inheritance, and so when I have to handle the, in this case, the
status, I have to define also the case, a default case, which I know for sure that will never be
called, and on Swift, we actually have a concept which is similar to the concept of sealed classes,
which is the concept of enum, so what we want is to map the sealed classes with enum, and to do so,
we can use a library, which is called, it's quite dark, but Merkur keys, Swift, and in this case,
using this library, it automatically detects any sealed classes and sealed interfaces,
and generates, in this case, will be UI state KS, and it just takes the status as input, and it
is actually an enum that I can use, so for a Swift developer, this is much easier to use,
because I don't have to define a default case anymore.
So, if you're writing code that is platform-specific from 400, for example, you probably will need
a context to access some system functionalities. What happens in the library ecosystem? So,
you may expose an API like this that gets the context from the user of the library,
but of course you don't have to do this on iOS, because you don't need a context on iOS.
How you can improve that, how you can unify those APIs, we try to use Jetpack app startup for this,
because if you include the app startup in your library, basically you will be able to
get a context that is injected by the operating system, and maybe save it.