https://kotlinlang.org logo
Title
a

Archie

12/16/2021, 2:11 PM
Hi guys another question, I am working on making a library work in KMM. Using Android library and Ios Library and trying to pipe them into a a common code. My question is how do you guys deal with platform specific things? for example, when Android context is needed to create an instance of a class while the same class in ios needs a ios specific thing? Is there a way to work around this? Thanks in advance 🤔
m

MarkRS

12/16/2021, 3:17 PM
Hah! That's my question too 😟 Currently what I'm doing is declaring the relevant variables as "Any" in the common code and then casting them in the platform specific code. Not sure if that's the best approach, though.
a

Archie

12/16/2021, 3:21 PM
Hmm… 🤔 I’m quite hesitant to do that. I hope there is a better way.
Btw thanks for sharing! ❤️
👍 1
m

MarkRS

12/16/2021, 3:23 PM
Why hesitant? And what alternative d'you think there might be? It's a genuine question. I also felt/feel slightly uncomfortable about it, but having thought a little (I'm no expert), I don't see a better way. Would love to hear if there is, though
a

Archie

12/16/2021, 3:23 PM
I am actually leaning toward keeping the initialization of class/instances that requires platform specific code to be declared on each side of the platform instead and only pipe the common things in the common code, say for example “Configs” or the likes. But I’m really not sure as well.
I hesitate everytime I have to deal with
Any
. I might be wrong though. I would avoid it if possible.
m

MarkRS

12/16/2021, 3:25 PM
Yes, I started thinking that way, but I want/need (?) to bring platform specific types into my core calculations, which pushed me in this direction
a

Archie

12/16/2021, 3:26 PM
hmmm… I think I haven’t gotten to that point yet? Quite inexperience still I guess. But I am hoping typealiasing/interface declaration would help. I think we should wait of others opinion. They might have something in mind.
👍 1
r

russhwolf

12/16/2021, 4:50 PM
I usually favor platform-specific injection of platform-specific things. You can see that in KaMPKit where we bind a Context to the Android side of the Koin graph, which gets used in some of the other Android-specific dependencies. If you do it this way then the Context never needs to be part of the common API
m

MarkRS

12/16/2021, 4:55 PM
That works using platform specific things for constructing other things... can it would work for storing platform specific things? A common class can't "expect" some things and define others, can it?
r

russhwolf

12/16/2021, 4:59 PM
not via expect/actual keywords, but you can do something like
interface CommonFoo
with
AndroidFoo
and
IosFoo
implementations. If you initialize in platform code you can pass a Context to Android but just reference it as
CommonFoo
in common without knowing about Context.
:kotlin-intensifies: 1
k

kpgalligan

12/16/2021, 5:19 PM
Agree with Russell and above "I am actually leaning toward keeping the initialization of class/instances that requires platform specific code to be declared on each side of the platform". This is a common thing going back for years just on Android, in my view. You need that Context, but unless you do something like set a static global in Application.onCreate, getting the context is rough. So far, in general, I'd say the same issue isn't as common on iOS.
👍 1
We have landed almost always on initializing the core "app context" on app start, and the initialization is slightly different for Android and iOS. We are doing a lot of work on "internal SDKs" for orgs, but it's really crystalized a lot of the thinking around just how shared code for apps should be inited. There's a single "startup" entry point for each platform, which currently for our apps starts a Koin instance and you just do whatever platform-specific factory work you need from there. We have an internal SDK playground calld "DogSDK", and on Android you start it with
fun startDogSDK(context: Context): DogSDK
, and for iOS it's
fun startDogSDK():DogSDK
. Then we have an expect function that makes platform-specific factories, which will have access to that
Context
. For example, to create a
Settings
instance we need the
Context
, so
single<Settings> {
        val sp: SharedPreferences = get<Context>().getSharedPreferences("DogSDKSettings", Context.MODE_PRIVATE)
        AndroidSettings(sp)
    }
:kotlin-intensifies: 1
a

Archie

12/16/2021, 5:25 PM
@russhwolf, I was just looking at
MultiplatformSettings
and if I understand it correctly this is exactly what you are doing in there as well. I am trying to copy your pattern 😄
This is a common thing going back for years just on Android, in my view. You need that Context, but unless you do something like set a static global in Application.onCreate, getting the context is rough. So far, in general, I’d say the same issue isn’t as common on iOS.
I totally agree with this.
k

kpgalligan

12/16/2021, 5:27 PM
Sounds complicated, but it's not so bad, and it's not unbounded. It's basically Android's
Context
that makes things weird. Anyway, need to publish some docs on our current "best practice" around this, but we see the same pattern over and over.
:kotlin-intensifies: 2
r

russhwolf

12/16/2021, 5:28 PM
Yeah Multiplatform Settings was the first place I had to deal with this but the same things come up every time you set up something new
💯 1
a

Archie

12/16/2021, 5:29 PM
Sounds complicated, but it’s not so bad, and it’s not unbounded. It’s basically Android’s 
Context
 that makes things weird. Anyway, need to publish some docs on our current “best practice” around this, but we see the same pattern over and over.
@kpgalligan Looking forward for the post. Thank you very much! ❤️
k

kpgalligan

12/16/2021, 5:30 PM
SQLdelight? Platform-specific config. Write local files? Same. You can't avoid it, but you can use a repeatable pattern to make it manageable.
r

russhwolf

12/16/2021, 5:30 PM
If you can think of it as a dependency injection problem and not a KMP-specific thing, it's easier to see how the patterns you're already used to from Android (or whatever else) can apply
👆 3
a

Archie

12/16/2021, 5:31 PM
f you can think of it as a dependency injection problem and not a KMP-specific thing, it’s easier to see how the patterns you’re already used to from Android (or whatever else) can apply
I am starting to realize that now.
SO the only thing that bugs me right now, is that there are initializations of classes where “appId” needs to be provided to initialize objects. Where the “appId” are the same regardless of the platform but the problem is that because I can’t define the init in the common code, I end up with code like this:
// Android
val androidInstance = LibraryClass(context, appId)

// Ios
val iosInstance = LibraryClass(appId)
I guess this is a just a very small thing anyway but having to reference to “appId” twice just bugs me.
r

russhwolf

12/16/2021, 5:54 PM
If you're using something like Koin, you could bind the appId or any other common dependency to your dependency graph and then it's a
get()
call instead. YMMV on whether that makes the dependency graph hard to follow. If you want to handroll it, you could have a common function like
CustomDependencyContainer.getAppId()
that you delegate to. Probably not worth it for appId, but might be nice if it's some other more complex dependency that you don't want to define initialization logic for twice.
:kotlin-intensifies: 1
a

Archie

12/16/2021, 6:03 PM
If you want to handroll it, you could have a common function like 
CustomDependencyContainer.getAppId()
 that you delegate to. Probably not worth it for appId, but might be nice if it’s some other more complex dependency that you don’t want to define initialization logic for twice.
This is interesting but wouldn’t that just replace:
// Android
val androidInstance = LibraryClass(context, get<CustomDependencyContainer>.getAppId())

// Ios
val iosInstance = LibraryClass(get<CustomDependencyContainer>.getAppId())
Did I understand it wrong?
r

russhwolf

12/16/2021, 6:04 PM
I meant that if you weren't using Koin, you could define your own function. If you're using Koin then use
get()
a

Archie

12/16/2021, 6:04 PM
I see. Yeah I totally understand. Thank you very much. ❤️
Hi guys, another question, how do you deal with scenarios where code is available on one platform but not on the other? Do you force pipe the methods regardless so the common code will have everything?
k

kpgalligan

12/16/2021, 6:29 PM
where code is available on one platform but not on the other
Example?
a

Archie

12/16/2021, 6:43 PM
The analytics library (MoEngange) I am working on defines a function:
setUserNotificationCategories(...)
for iOS but doesn’t have this on the Android side. There are other definitions as well other than this.
k

kpgalligan

12/16/2021, 6:44 PM
Where do you need to call it? I'd say there are a few options. expect extension function, maybe.
If you need to call that on startup, then it's part of your platform-specific init. If it's calling that later, then maybe have an expect function that calls that just on iOS. I tend not to have many expect/actual classes, and not too many total of any type, but expect/actual functions can be pretty useful in specific cases.
👍 1
a

Archie

12/16/2021, 6:48 PM
For the usage, I have no idea yet. That is actually also one of the things I am thinking about. Where lets say, I add the analytics code on the common code, but there are also platform specific logs I have to do as well. How would it all be structured where all common is define but also the flexibility of adding platform specific logs. For the expect/actual approach, would I just not implement it on the android side? since no equivalent function would be available?
p

Paul Woitaschek

12/19/2021, 8:03 AM
Yes or a no op Implementation it applicable.
👍 1
a

Archie

12/25/2021, 6:28 AM
Hi Guys, Thank you for all the inputs really really helpful. May I ask for another advice. I have a situation where I need a class to be available in common but they have platform specific implementation. If I just do:
// Common
expect class SomeClass

// Android
actual typealias SomeClass = SomeClassAndroid

// iOS
actual typealias SomeClass = SomeClassIos
but the problem is that I cannot access the properties/functions in the
SomeClass
when using it inside common…
// common
val someClass = SomeClass()
someClass.someFunction(...) // I don/t have this definition in common, ERROR!
I thought of two things to do. 1. Declare an interface
SomeClassInterface
, declare the properties and functions in this interface and create a platform specific implementation wrapping the original class:
// common
interface SomeClassInterface {
   fun someFunction(...)
}

// Android
class SomeAndroidWrapperClass : SomeClassInterface {
    private val someClass = SomeClassAndroid()

    override fun someFunction(...) {
         someClass.someFunction(...)
    }
}

// iOS
class SomeIosWrapperClass : SomeClassInterface {
    private val someClass = SomeClassIos()

    override fun someFunction(...) {
         someClass.someFunction(...)
    }
}
2. Another approach I thoguh of is to keep the original typealias and simply declare the functions/properties as expect extension functions
// Common
expect class SomeClass

expect fun SomeClass.someFunction(...)

// Android
actual typealias SomeClass = SomeClassAndroid

actual fun SomeClass.someFunction(...) = someFunction(...)

// iOS
actual typealias SomeClass = SomeClassIos

actual fun SomeClass.someFunction(...) = someFunction(...)
My Question is whether, there is a better approach than these two? or if there is none, which of the two approach would be better to go through with? Thanks in advance guys. Really appreciate all the help.
p

Paul Woitaschek

12/25/2021, 7:22 AM
It’s a complicated question with no easy solution. The question is mainly: In which context are you and do you control all the classes? If it’s in the context of a library and you don’t own the `actual class`es, create a dedicated class and handle delegation to the platform types. The reason for this is simple: You have no control over the platform classes and when they for example add a property / function that conflicts with one of your declared ones, this will break your multiplatform code. If it’s your internal code, its up to your taste. I usually go with extension functions as it’s just easier to implement. For example here is our uuid:
public expect class UUID

@Serializer(forClass = UUID::class)
public object UUIDSerializer : KSerializer<UUID> {

  override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("com.yazio.shared.uuid.UUIDSerializer", PrimitiveKind.STRING)

  override fun serialize(encoder: Encoder, value: UUID) {
    encoder.encodeString(value.value)
  }

  override fun deserialize(decoder: Decoder): UUID {
    val stringValue = decoder.decodeString()
    return uuid(stringValue)
  }
}

/**
 * @throws [kotlin.IllegalArgumentException] if the value is no valid uuid
 */
public expect fun uuid(value: String): UUID

public expect val UUID.value: String

public expect fun randomUUID(): UUID

public expect fun String.asUUIDorNull(): UUID?
On jvm / droid it’s a typealias:
public actual typealias UUID = java.util.UUID

public actual fun uuid(value: String): UUID {
  return UUID.fromString(value)
}

public actual val UUID.value: String get() = toString()

public actual fun randomUUID(): UUID {
  return UUID.randomUUID()
}

public actual fun String.asUUIDorNull(): UUID? {
  return try {
    uuid(this)
  } catch (ignored: IllegalArgumentException) {
    null
  }
}
And on native:
public actual data class UUID(val uuid: String) {

  init {
    freeze()
  }
}

public actual fun uuid(value: String): UUID {
  return requireNotNull(value.asUUIDorNull()) {
    "Could not parse $value"
  }
}

public actual val UUID.value: String get() = uuid

public actual fun randomUUID(): UUID {
  return NSUUID().toUUID()
}

public actual fun String.asUUIDorNull(): UUID? {
  return try {
    NSUUID(this)
  } catch (e: NullPointerException) {
    // Kotlin does not support optional initializers.
    null
  }?.toUUID()
}

private fun NSUUID.toUUID(): UUID {
  return UUID(UUIDString.lowercase())
}
And this example highlights another important aspect. The native one could also be a typealias to NSUUID. And it actually was. However having native platform types inside your kotlin classes turns out to be actually pretty expensive in terms of performance. We had this UUID in one of our data intensive paths and it caused real performance issues. We had classes that had this uuid as a property. And this lead to it’s equals / hashCode implementations calling through to the objc-world. By refactoring the uuid to a real kotlin class the performance of that critical path went up by a factor of 2.5.