Hello, our Kotlin Multiplatform project is modularized, where one feature has at lest 2 gradle modules: an API module that contains the interfaces and data structures, and an Implementation module that contains the implementation details for the API module.
Currently we are using Kotlin / Native wrappers akin to this:
class FlowWrapper<T>(
private val scope: CoroutineScope,
private val flow: Flow<T>,
) {
init {
freeze()
}
fun subscribe(
onEach: (item: T) -> Unit,
onThrow: (error: Throwable) -> Unit,
): Job = subscribe(onEach, onThrow, {})
fun subscribe(
onEach: (item: T) -> Unit,
onThrow: (error: Throwable) -> Unit,
onComplete: () -> Unit,
): Job = flow
.onEach { onEach(it.freeze()) }
.catch { onThrow(it.freeze()) }
.onCompletion { onComplete() }
.launchIn(scope)
.freeze()
}
link
class GetTodoCountIos(
private val getTodoCount: GetTodoCount
){
fun invoke(): FlowWrapper<TodoCount> =
FlowWrapper(
scope = CoroutineScope(SupervisorJob() + Dispatchers.Main),
flow = getTodoCount()
)
}
Where GetTodoCount is an interface, and in swift this wrapper is used as getTodoCount:
link
getTodoCount.invoke().subscribe { count in
if (count != nil){
self.count = count as! TodoCount
}
} onThrow: { KotlinThrowable in
}
Removing the wrapper
When trying this library out I was unable to use the interface abstraction on iOS. I removed the GetTodoCountIos wrapper and tried to use the interface directly:
link
let future = createPublisher(for: getTodoCount.invokeNative())
let cancellable = future.sink { completion in
print("Received completion: \(completion)")
} receiveValue: { count in
print("Received value: \(count)")
if (count != nil){
self.count = count as! TodoCount
}
}
But it fails with the following error:
2022-02-04 08:31:38.374976+0100 KaMPKitiOS[1700:34628] -[Shared_kobjcc0 invokeNative]: unrecognized selector sent to instance 0x60000194d290
2022-02-04 08:31:38.419835+0100 KaMPKitiOS[1700:34628] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Shared_kobjcc0 invokeNative]: unrecognized selector sent to instance 0x60000194d290'
It does generate the correct native function, but the function crashes the app:
__attribute__((swift_name("GetTodoCount")))
@protocol SharedGetTodoCount
@required
- (id<SharedFlow>)invoke __attribute__((swift_name("invoke()")));
- (SharedKotlinUnit *(^(^)(SharedKotlinUnit *(^)(SharedTodoCount *, SharedKotlinUnit *), SharedKotlinUnit *(^)(NSError * _Nullable, SharedKotlinUnit *)))(void))invokeNative __attribute__((swift_name("invokeNative()")));
@end;
I'm not sure if this is caused by the fact that the interface does not know what the underlying implementation is or it is related to something else.
Light weight wrapper by interface delegation
Another solution I tried was using interface delegation, the wrapper is still written by hand but it's pretty concise and does not require updates when changing the interface:
link
class GetTodoCountIos(
private val getTodoCount: GetTodoCount
) : GetTodoCount by getTodoCount
link
But it also fails, but with a different error:
Terminating app due to uncaught exception 'NSGenericException', reason: '[SharedGetTodoCountIos invokeNative] can't be overridden: it is final'
The generated header file:
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("GetTodoCountIos")))
@interface SharedGetTodoCountIos : SharedBase <SharedGetTodoCount>
- (instancetype)initWithGetTodoCount:(id<SharedGetTodoCount>)getTodoCount __attribute__((swift_name("init(getTodoCount:)"))) __attribute__((objc_designated_initializer));
- (id<SharedFlow>)invoke __attribute__((swift_name("invoke()")));
@end;
Maybe it would be possible to extend it using an extension function?
Normal wrapper
The only solution that did not crash was creating the wrapper and calling the methods on the interface:
link
class GetTodoCountIos(
private val getTodoCount: GetTodoCount
){
fun invoke(): Flow<TodoCount> = getTodoCount()
}
With the following header:
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("GetTodoCountIos")))
@interface SharedGetTodoCountIos : SharedBase
- (instancetype)initWithGetTodoCount:(id<SharedGetTodoCount>)getTodoCount __attribute__((swift_name("init(getTodoCount:)"))) __attribute__((objc_designated_initializer));
- (id<SharedFlow>)invoke __attribute__((swift_name("invoke()")));
- (SharedKotlinUnit *(^(^)(SharedKotlinUnit *(^)(SharedTodoCount *, SharedKotlinUnit *), SharedKotlinUnit *(^)(NSError * _Nullable, SharedKotlinUnit *)))(void))invokeNative __attribute__((swift_name("invokeNative()")));
@end;
However this also didn't work as expected, because the flow is never collected (onEach, onComplete never called).
The repository to reproduce this is available here: