:wave: everyone! I’m looking for architectural bes...
# multiplatform
a
👋 everyone! I’m looking for architectural best practices when it comes to KMM. I did a quick search in the channel but came up short. Here’s my situation: I have an existing iOS app and I’m trying out KMM. Ideally, the new version of the app. with KMM, would be available both for Android and iOS, but for the existing iOS users it would be an under the hood change, they wouldn’t lose any of their current content. I started building a
MigratorService
in
iosMain
, including all the necessary platform-specific models. I had to implement these models because I wanted to take this opportunity to revisit all the existing models and improve their structure which makes the native models not match 1-1 with the new ones. But now I’m at a crossroads since I have no way to call the
MigratorService
from
commonMain
. So I either: • Move all the code I’ve done in
iosMain
to
commonMain
. This has the potential risk of exposing old iOS related code to Android and the potential scenario where an Android app might try to run the migration. It also makes code ergonomics a bit more complicated, since I have a
User
common model and an old
User
iOS model. • Create an expect
MigratorService
in commonMain, with the actual implementation being on
iosMain
. But this approach implies also adding a complete
no-op
version of
MigratorService
in
androidMain
. Which is exposing an API that doesn’t really do what is expected. Which one would be considered the best practice in KMM and why? Or is there another alternative to tackle these situations?
k
Why would you need to call
MigratorService
from
commonMain
if it has no equivalent in Android? There’s no “best practice” here as far as I understand your situation. You need to code common models and both platforms can use them, or you have some iOS specific code in
iosMain
. You can have a common interface that is implemented by iOS and be a no-op on Android. While that may not seem ideal, I don’t understand your code or problem in the abstract, so it’s not feasible to say if that is or isn’t a good idea (I would for sure not do expect/actual for that kind of thing, however).
a
currently my iOS app has a stored JSON file with the user model, for persistence reasons, let’s call this
NativeUser
. Since I will want to share the user model with android and iOS and take this opportunity to do a bit of refactoring, I created in
commonMain
a
User
model. In order to bring the the information from the current version of the app, in
iOSMain
I replicated a class for the user, the
NativeUser
model and created a
MigrationService
that loads the file from the bundle, decodes it into a
NativeUser
and then I migrate the data to fit a
User
object and save it in the new location, marking the migration complete. This
MigrationService
should a property of a
ModuleManager
(for dependency injection purposes) defined in
commonMain
. When
ModuleManager
receives the call to
performMigration
, it ideally calls the method
performMigration
of the
MigrationService
. But since all this is happening in
commonMain
and my implementation of the
MigrationService
is in
iOSMain
I’m not sure how I can make this happen without defining an expect/actual
MigrationService
or simply moving everything to
commonMain
(which isn’t ideal because I’d lose access to Foundation specific things like
NSBundle
and
NSDate
, but can be worked around).
k
Well, I don’t really understand why
MigrationService
needs to be a properly of
ModuleManager
. Were I building this, generally speaking, you always have an entry point for each platform that gets called when the app starts to init the shared Kotlin. In there I’d probably check if the migration needed to happen, and it would be
MigrationService
calling into something from commonMain to do the initial migration. However…
If you want to architect it the way you have it laid out, it’s still pretty simple (to me). Create an interface
MigrationServiceIntf
(for lack of a better name), with one method (performMigration). Android and iOS implement their own version, and you DI those. Android’s indeed does nothing, but if you need to have everything injected in commonMain, that’s the simplest plan. Again, expect/actual for something like this is inflexible and I’d not use that. That is for sure my best practice advice here.
a
thanks so much for the feedback, it’s really insightful. 🙌 As to why I have it laid out like this, I already built the
PersistenceService
as part of the
ModuleManager
, that way once I get the
NativeUser
object transformed into a
User
one, I can simply fire it to the persistence service, to do its thing. The persistence service is not something that is visible from outside the
ModuleManager
, as it’s merely an implementation detail. your approach is quite interesting, I guess I’d just have to create a
Copy code
class MigrationService: MigrationServiceIntf {}
both on
androidMain
and
iOSMain
and everything should work. One last thing, why do you say expect/actual is inflexible and not the right approach to handle this? I fear I might be using it wrong now 😬
k
Well, I think when people first see KMP expect/actual stick out as the way to have platform specific code. Also, most tutorials seem to start with those. My first 6 months or so I put expect/actual everywhere, then spent the time after that ripping it out. For things like service objects, expect/actual gets you nothing that an interface can’t do, but it locks you into 1 specific implementation. You may want to have a test implementation, for example. You can’t. Platform-specific versions may need different parameters (lots of stuff on Android needs Context, for example), which means you can’t use common constructors for your expect/actual classes. If you can’t use common constructors, you need to call a platform-specific constructor, and at that point, expect/actual kind of loses all value.
It really depends on the situation, though.
I will use them for factory functions, and there are cases where they make mapping onto similar platform-specific object graphs easier (don’t need big delegate chains)
This is one of those talks where I crammed too many things into one session, but it covers some of this (it’s from 2019, so I would probably change a good amount of this, but still useful) https://vimeo.com/371460823
a
🙇 thanks so much, this makes so much sense. I will watch the talk ASAP