Quick question, what is your approach when exposin...
# multiplatform
h
Quick question, what is your approach when exposing suspending functions from kotlin to swift and you want to return either a succesful response or an exceptional case? Currently I am using this method signature:
Copy code
@Throws(Throwable::class) public suspend fun getSomething(): String
because in my experience returning sealed classes with Success / Failure types to swift (via objective c) does not work nicely with swift. This approach works OK with swift since the exceptions are checked, but when using this same method in android kotlin, there is no checked exceptions so its easy to forget to add a try catch around it. So I was wondering what your approach is.
a
Why don't you use the result class as a return type for your method. I found it better than throwing exceptions.
h
I just checked it, when using the Result class the type information is removed in the objective c code; the return type is no longer
Result<String>
, but
id
, that is not really usable from a swift perspective
One suggestion I can think of is making clear in the naming of the methods that they're throwing methods:
Copy code
@Throws(Throwable::class) public suspend fun getSomthingOrThrow(): String
j
You might try making the
Result
generic type explicitly an
NSObject
subclass. With
String
, maybe try expect/actual the type to
String
in Kotlin and
NSString
in ObjC.
h
How can I do that for the kotlin
Result
class? It is provided by the std and its an
@JvmInline value class
, so i think its not possible using the
Result
type.
j
Not for the
Result
type, but for its generic type. Something like:
Copy code
expect class MyResultType

// Android
actual typealias MyResultType = String

// iOS
actual typealias MyResultType = NSString

...
val result: Result<MyResultType> = ...
Of course you'll want
MyResultType
to behave like a
String
in common code, so the above code won't be all that's required. But might check if it will resolve the generic type in ObjC correctly first though.
h
String already is translated to NSString so i dont think that will solve the issue that the
Result
type itself is erased because its an inline value class? The
Result<T>
type of the kotlin std is unusable because inline classes are unsupported. (Result<T> is defined like so:
Copy code
@JvmInline
public value class Result<out T> @PublishedApi internal constructor(
j
I know it's translated automatically, but I'm wondering if maybe the generic type isn't happy with this automatic translation and needs an explicit
NSObject
subclass.
h
To be sure i tested it,
Copy code
public fun test(): Result<NSString> { TODO() }
translates to
Copy code
+ (id _Nullable)test __attribute__((swift_name("test()")));
j
Good to know. 👍
Result
is
@JvmInline
. So the native version shouldn't be inlined, right?
h
value classes are also inline classes
j
Ah, yes, it is a
value class
. You might try creating your own
Result
type to see if you can get it to work with a non-inlined class. As a last resort, you could even make it non-generic.
h
Yeah creating my own Result type does work, I was wondering how other people approached these issues. I wonder if creating your own Result class is better or worse than regular throwing functions, since then you have neither idiomic swift and not really idiomic kotlin as well (since its a duplicate type which conflicts with the result type). So im looking for solutions and some insight in whether those solutions worked well or if there are caveats
j
There are definitely examples in my code where I've had to create workarounds to get some things to work where there's still some rough edges with KMP or differences between Kotlin/JVM and Kotlin/Native. I would probably avoid relying on checked exceptions, mostly because they don't work well in Kotlin.
a
@Hylke Bron sorry for the half info, I wanted to tell you to create your own implementation. Was caught up. I found returning a result type is cleaner than an exception that I might forget to catch 😅 .
h
Did you also use a custom Result type when the method is suspending? Suspending functions are always translated into throwing functions in iOS (due to the CancelationException). That means that from a swift perspective, you would have 2 ways of getting an error out of the method, you still have to try catch the method call, and then you need to unpack the result type to see if it is an exceptional case that way. I got pushed back by my ios developer when trying something like that. so i ended up using throw for everything. How are your ios devs handling this? do they not care you have both a result type you need to unpack and a try catch you need to do?
a
Basically, we handled most exceptions that might happen during an operation in the shared code, then forward a cleaner error using our custom
Result
. Including
CancellationExceptions
. So our iOS guys only has to focus on the result type. It is tricky but it works so far for us. And we agreed on this approach together for both iOS and Android.
h
What does the method signature for suspending functions then look like? something like this?:
Copy code
public fun doSomething(completionBlock: (MyResult<String>) -> Unit)
a
Yes!!!
h
Ok, thanks for sharing your experience 🙂
a
Or better still you can have
Copy code
public fun doSomething(success: (Mydata) -> Unit, error: (Whatever) -> Unit)
Check out @russhwolf and @kpgalligan talk about making your code work on iOS

here▾

h
But you lose the ability to cancel the method (or do you return some handle to cancel the method?) and its not really idiomatic code for the android/kotlin side anymore imo
oh ill check that, thanks
a
You can fast forward to 12014
```But you lose the ability to cancel the method (or do you return some handle to cancel the method?) and its not really idiomatic code for the android/kotlin side anymore imo
```
Yep you can wrap an adapter around the shared code. It was discussed in the video
An adapter class that will expose a cancel method.
r
h
Interesting video (also the part about SPM, we've currently taken another (but similar?) approach at publishing our ios framework via SPM to make intermediate builds also easily available and it's part of our CI/CD pipeline) If i look at the example, i think the solution lies in using expect/actual to expose a different public API for Android and for iOS? For Android we can just expose the suspend funs, and for iOS we have to expose them as callback functions and return an cancellable interface. I think that's the best solution. I dont mind duplicating my public API interface, since that is the smallest part of my library
k
The expect/actual is only for the view model to hide the lifecycle complexity a bit. The api layers is a bit of a different topic. I talk about this some here (https://www.droidcon.com/2022/08/01/sdk-design-and-publishing-for-kotlin-multiplatform-mobile-2/), but it gets a little confusing. We used to structure the modules such that Android talks directly to a coroutines-aware api surface, then an iOS api surface that has callbacks and “wraps” the underlying coroutines-aware api. If you’re OK with “duplicating” the API for iOS, that’s ideal because you can directly control both the API surface and limit exposing things that don’t need to be exposed.
But, it’s extra work.
We’re currently working on some things that will directly augment the API surface so. a lot of that manual work could be handled with compiler plugins, etc, but no ETA.
h
Yeah its extra work, but also provides extra freedom. For example the string im returning actually is an URL, but since there is no shared type I just made it String. By separating the interfaces i can still use a string under water, but map it to an NSURL on the iOS side and something else for Android. Maybe those compiler plugins will make it even less cumbersome in the future, looking forward at looking at them
Thanks for the advice everyone 👍