Upgrade to Pro — share decks privately, control downloads, hide ads and more …

AndroidX Lifecycle’s Path to Multiplatform

AndroidX Lifecycle’s Path to Multiplatform

We’ve recently converted the AndroidX Lifecycle libraries (ViewModel, Lifecycle Runtime, and Compose support) to Kotlin multi-platform (KMP). Join this session to learn more about how this process went, what the real-world challenges of maintaining API backward compatibility are, what lessons we learned from working around KMP limitations, and insights to guide you in migrating your own libraries to KMP.

- Presented at Droidcon Berlin, Lisbon and London in 2024.
- Available on YouTube: https://www.youtube.com/watch?v=k1PIzEIO6jo

Marcello Galhardo

July 04, 2024
Tweet

More Decks by Marcello Galhardo

Other Decks in Programming

Transcript

  1. What will you learn today? Android Support for Kotlin Multiplatform

    (KMP) 01 Timeline for Lifecycle KMP 02 KMP API Challenges 03
  2. We have made the source code compatible with common source

    sets. But it requires integration. KMP Compatibility KMP Support A B
  3. We have made the source code compatible with common source

    sets. But it requires integration. KMP Compatibility Same as "KMP Compatibility", and it works out-of-the-box. The dream world where everything seamlessly works. KMP Support A B
  4. January February March April May Lifecycle Runtime Compose KMP Lifecycle

    common and runtime KMP Lifecycle KMP Stable Release Collaborate with JetBrains ViewModel KMP ViewModel Compose KMP Timeline for Lifecycle KMP Starting line! Finishing line! Navigation KMP 2024
  5. Finishing line! Navigation KMP Lifecycle Runtime Compose KMP Lifecycle common

    and runtime KMP Lifecycle KMP Stable Release ViewModel KMP ViewModel Compose KMP January February March April May Starting line! Timeline for Lifecycle KMP Collaborate with JetBrains 2024
  6. Finishing line! Navigation KMP Collaborate with JetBrains Starting line! Lifecycle

    Runtime Compose KMP Lifecycle common and runtime KMP Lifecycle KMP Stable Release January February March April May Timeline for Lifecycle KMP ViewModel KMP ViewModel Compose KMP 2024 Navigation Dependencies
  7. Events iOS Desktop Web ON_CREATED init init ON_START viewWillAppear willEnterForeground

    windowDeiconified ON_RESUME didBecomeActive windowGainedFocus focus ON_PAUSE willResignActive windowLostFocus blur ON_STOP viewDidDisappear didEnterBackground windowIconified ON_DESTROY didLeaveWindowHierar chy dispose
  8. Events iOS Desktop Web ON_CREATED init init ON_START viewWillAppear willEnterForeground

    windowDeiconified ON_RESUME didBecomeActive windowGainedFocus focus ON_PAUSE willResignActive windowLostFocus blur ON_STOP viewDidDisappear didEnterBackground windowIconified ON_DESTROY didLeaveWindowHierar chy dispose
  9. Events iOS Desktop Web ON_CREATED init init ON_START viewWillAppear willEnterForeground

    windowDeiconified ON_RESUME didBecomeActive windowGainedFocus focus ON_PAUSE willResignActive windowLostFocus blur ON_STOP viewDidDisappear didEnterBackground windowIconified ON_DESTROY didLeaveWindowHierar chy dispose
  10. Events iOS Desktop Web ON_CREATED init init ON_START viewWillAppear willEnterForeground

    windowDeiconified ON_RESUME didBecomeActive windowGainedFocus focus ON_PAUSE willResignActive windowLostFocus blur ON_STOP viewDidDisappear didEnterBackground windowIconified ON_DESTROY didLeaveWindowHierar chy dispose
  11. // Activity or Fragment val myViewModel by viewModels { MyViewModel.Factory

    } // Composable val myViewModel = viewModel { MyViewModel() } // Implementation Details ViewModelProvider(viewModelStore, factory, extras).get(key, MyViewModel::class)
  12. // Activity or Fragment val myViewModel by viewModels { MyViewModel.Factory

    } // Composable val myViewModel = viewModel { MyViewModel() } // Implementation Details ViewModelProvider(viewModelStore, factory, extras).get(key, MyViewModel::class)
  13. // Activity or Fragment val myViewModel by viewModels { MyViewModel.Factory

    } // Composable val myViewModel = viewModel { MyViewModel() } // Implementation Details ViewModelProvider(viewModelStore, factory, extras).get(key, MyViewModel::class)
  14. // Activity or Fragment val myViewModel by viewModels { MyViewModel.Factory

    } // Composable val myViewModel = viewModel { MyViewModel() } // Implementation Details ViewModelProvider(viewModelStore, factory, extras).get(key, MyViewModel::class)
  15. open class ViewModelProvider { @JvmOverloads constructor(store: ViewModelStore, factory: Factory, extras:

    CreationExtras = Empty) fun <T : ViewModel> get(key: String, modelClass: Class<T>): T interface Factory { fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T } open class NewInstanceFactory : Factory {...} open class AndroidViewModelFactory : NewInstanceFactory() {...} companion object {...} }
  16. open class ViewModelProvider { @JvmOverloads constructor(store: ViewModelStore, factory: Factory, extras:

    CreationExtras = Empty) fun <T : ViewModel> get(key: String, modelClass: Class<T>): T interface Factory { fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T } open class NewInstanceFactory : Factory {...} open class AndroidViewModelFactory : NewInstanceFactory() {...} companion object {...} }
  17. open class ViewModelProvider { @JvmOverloads constructor(store: ViewModelStore, factory: Factory, extras:

    CreationExtras = Empty) fun <T : ViewModel> get(key: String, modelClass: Class<T>): T interface Factory { fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T } open class NewInstanceFactory : Factory {...} open class AndroidViewModelFactory : NewInstanceFactory() {...} companion object {...} }
  18. open class ViewModelProvider { @JvmOverloads constructor(store: ViewModelStore, factory: Factory, extras:

    CreationExtras = Empty) fun <T : ViewModel> get(key: String, modelClass: Class<T>): T interface Factory { fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T } open class NewInstanceFactory : Factory {...} open class AndroidViewModelFactory : NewInstanceFactory() {...} companion object {...} }
  19. open class ViewModelProvider { @JvmOverloads constructor(store: ViewModelStore, factory: Factory, extras:

    CreationExtras = Empty) fun <T : ViewModel> get(key: String, modelClass: Class<T>): T interface Factory { fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T } open class NewInstanceFactory : Factory {...} open class AndroidViewModelFactory : NewInstanceFactory() {...} companion object {...} }
  20. open class ViewModelProvider { @JvmOverloads constructor(store: ViewModelStore, factory: Factory, extras:

    CreationExtras = Empty) fun <T : ViewModel> get(key: String, modelClass: Class<T>): T interface Factory { fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T } open class NewInstanceFactory : Factory {...} open class AndroidViewModelFactory : NewInstanceFactory() {...} companion object {...} }
  21. open class ViewModelProvider { @JvmOverloads constructor(store: ViewModelStore, factory: Factory, extras:

    CreationExtras = Empty) fun <T : ViewModel> get(key: String, modelClass: Class<T>): T interface Factory { fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T } open class NewInstanceFactory : Factory {...} open class AndroidViewModelFactory : NewInstanceFactory() {...} companion object {...} }
  22. $ ./gradlew checkApi - ctor public ViewModelProvider( androidx.lifecycle.ViewModelStore store, androidx.lifecycle.ViewModelProvider.Factory

    factory, optional androidx.lifecycle.viewmodel.CreationExtras defaultCreationExtras ); --- + ctor public ViewModelProvider( androidx.lifecycle.ViewModelStore store, androidx.lifecycle.ViewModelProvider.Factory factory, androidx.lifecycle.viewmodel.CreationExtras defaultCreationExtras );
  23. $ ./gradlew checkApi - ctor public ViewModelProvider( androidx.lifecycle.ViewModelStore store, androidx.lifecycle.ViewModelProvider.Factory

    factory, optional androidx.lifecycle.viewmodel.CreationExtras defaultCreationExtras ); --- + ctor public ViewModelProvider( androidx.lifecycle.ViewModelStore store, androidx.lifecycle.ViewModelProvider.Factory factory, androidx.lifecycle.viewmodel.CreationExtras defaultCreationExtras );
  24. @JvmOverloads public ViewModelProvider(@NotNull ViewModelStore store, @NotNull Factory factory) @JvmOverloads public

    ViewModelProvider(@NotNull ViewModelStore store, @NotNull Factory factory, @NotNull CreationExtras defaultCreationExtras) // $FF: synthetic method public ViewModelProvider(ViewModelStore var1, Factory var2, CreationExtras var3, int var4, DefaultConstructorMarker var5)
  25. @JvmOverloads public ViewModelProvider(@NotNull ViewModelStore store, @NotNull Factory factory) @JvmOverloads public

    ViewModelProvider(@NotNull ViewModelStore store, @NotNull Factory factory, @NotNull CreationExtras defaultCreationExtras) // $FF: synthetic method public ViewModelProvider(ViewModelStore var1, Factory var2, CreationExtras var3, int var4, DefaultConstructorMarker var5)
  26. @JvmOverloads public ViewModelProvider(@NotNull ViewModelStore store, @NotNull Factory factory) @JvmOverloads public

    ViewModelProvider(@NotNull ViewModelStore store, @NotNull Factory factory, @NotNull CreationExtras defaultCreationExtras) // $FF: synthetic method public ViewModelProvider(ViewModelStore var1, Factory var2, CreationExtras var3, int var4, DefaultConstructorMarker var5)
  27. KT-65945 JvmOverloads in common code in expected declarations does not

    generate Synthetic DefaultConstructorMarker constructor. https://youtrack.jetbrains.com/issue/KT-65945
  28. open class ViewModelProvider { @JvmOverloads constructor(store: ViewModelStore, factory: Factory, extras:

    CreationExtras = Empty) companion object { @JvmStatic fun create( store: ViewModelStore, factory: Factory, extras: CreationExtras, ): ViewModelProvider = ViewModelProvider(store, factory, extras) } }
  29. open class ViewModelProvider { fun <T : ViewModel> get(key: String,

    modelClass: Class<T>): T = get(key, modelClass.kotlin) fun <T : ViewModel> get(key: String, modelClass: KClass<T>): T }
  30. Issue-328275985 error: reference to get is ambiguous when(viewModelProvider.get(any(), any())) .thenReturn(viewModel)

    both methods match: • <T#1>get(String,KClass<T#1>) • <T#2>get(String,Class<T#2>) https://issuetracker.google.com/328275985
  31. open class ViewModelProvider { interface Factory { fun <T :

    ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T } }
  32. open class ViewModelProvider { interface Factory { fun <T :

    ViewModel> create(modelClass: java.lang.Class<T>, extras: CreationExtras): T } }
  33. open class ViewModelProvider { interface Factory { fun <T :

    ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T { throw UnsupportedOperationException() } fun <T : ViewModel> create(modelClass: KClass<T>, extras: CreationExtras): T { return create(modelClass.java, extras) } } }
  34. open class ViewModelProvider { open class NewInstanceFactory : Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T { return try { modelClass.getDeclaredConstructor().newInstance() } catch (e: NoSuchMethodException) { throw RuntimeException("Cannot create an instance of $modelClass", e) } catch (e: InstantiationException) { throw RuntimeException("Cannot create an instance of $modelClass", e) } catch (e: IllegalAccessException) { throw RuntimeException("Cannot create an instance of $modelClass", e) } } } }
  35. expect open class ViewModelProvider { @JvmOverloads constructor(store: ViewModelStore, factory: Factory,

    extras: CreationExtras) fun <T : ViewModel> get(key: String, modelClass: Class<T>): T interface Factory { fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T } open class NewInstanceFactory : Factory {...} open class AndroidViewModelFactory : NewInstanceFactory() {...} companion object {...} }
  36. expect open class ViewModelProvider { @JvmOverloads constructor(store: ViewModelStore, factory: Factory,

    extras: CreationExtras) fun <T : ViewModel> get(key: String, modelClass: Class<T>): T interface Factory { fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T } open class NewInstanceFactory : Factory {...} open class AndroidViewModelFactory : NewInstanceFactory() {...} companion object {...} }
  37. actual open class ViewModelProvider { @JvmOverloads constructor(store: ViewModelStore, factory: Factory,

    extras: CreationExtras) fun <T : ViewModel> get(key: String, modelClass: KClass<T>): T }
  38. actual open class ViewModelProvider private constructor( private val impl: ViewModelProviderImpl,

    ) { @JvmOverloads actual constructor( store: ViewModelStore, factory: Factory, defaultCreationExtras: CreationExtras = CreationExtras.Empty, ) : this(ViewModelProviderImpl(store, factory, defaultCreationExtras)) actual fun <T : ViewModel> get(modelClass: KClass<T>): T = impl.getViewModel(modelClass) }
  39. internal class ViewModelProviderImpl(...) { @Suppress("UNCHECKED_CAST") internal fun <T : ViewModel>

    getViewModel( modelClass: KClass<T>, key: String = ViewModelProviders.getDefaultKey(modelClass), ): T { val viewModel = store[key] if (modelClass.isInstance(viewModel)) { // ... return viewModel as T } val modelExtras = MutableCreationExtras(defaultExtras) modelExtras[ViewModelProviders.ViewModelKey] = key return factory.create(modelClass, modelExtras) .also { vm -> store.put(key, vm) } } }
  40. internal class ViewModelProviderImpl(...) { @Suppress("UNCHECKED_CAST") internal fun <T : ViewModel>

    getViewModel( modelClass: KClass<T>, key: String = ViewModelProviders.getDefaultKey(modelClass), ): T { val viewModel = store[key] if (modelClass.isInstance(viewModel)) { // ... return viewModel as T } val modelExtras = MutableCreationExtras(defaultExtras) modelExtras[ViewModelProviders.ViewModelKey] = key return factory.create(modelClass, modelExtras) .also { vm -> store.put(key, vm) } } }
  41. Issue-341792251 The migration (...) reverted a 2 year old fix

    to an issue that had to do with desugaring for compileOnly dependencies in a library project. Fixed on Lifecycle 2.8.2 https://issuetracker.google.com/341792251
  42. internal class ViewModelProviderImpl(...) { @Suppress("UNCHECKED_CAST") internal fun <T : ViewModel>

    getViewModel( modelClass: KClass<T>, key: String = ViewModelProviders.getDefaultKey(modelClass), ): T { val viewModel = store[key] if (modelClass.isInstance(viewModel)) { // ... return viewModel as T } val modelExtras = MutableCreationExtras(defaultExtras) modelExtras[ViewModelProviders.ViewModelKey] = key return factory.create(modelClass, modelExtras) .also { vm -> store.put(key, vm) } } }
  43. internal class ViewModelProviderImpl(...) { @Suppress("UNCHECKED_CAST") internal fun <T : ViewModel>

    getViewModel( modelClass: KClass<T>, key: String = ViewModelProviders.getDefaultKey(modelClass), ): T { val viewModel = store[key] if (modelClass.isInstance(viewModel)) { // ... return viewModel as T } val modelExtras = MutableCreationExtras(defaultExtras) modelExtras[ViewModelProviders.ViewModelKey] = key return createViewModel(factory, modelClass, modelExtras) .also { vm -> store.put(key, vm) } } }
  44. internal actual fun <VM : ViewModel> createViewModel( factory: ViewModelProvider.Factory, modelClass:

    KClass<VM>, extras: CreationExtras ): VM = factory.create(modelClass, extras)
  45. internal actual fun <VM : ViewModel> createViewModel( factory: ViewModelProvider.Factory, modelClass:

    KClass<VM>, extras: CreationExtras ): VM { // Android targets using `compileOnly` dependencies may encounter AGP desugaring // issues where `Factory.create` throws an `AbstractMethodError`. This is resolved by an // Android-specific implementation that first attempts all `ViewModelProvider.Factory.create` // method overloads before allowing the exception to propagate. // (See b/230454566 and b/341792251 for more details). return try { factory.create(modelClass, extras) } catch (e: AbstractMethodError) { try { factory.create(modelClass.java, extras) } catch (e: AbstractMethodError) { factory.create(modelClass.java) } } }
  46. Learnings Desugaring and minification may cause unexpected side effects. Expect

    classes can not have default implementations. Mix and match expect/actual with common code for reusability.
  47. abstract class ViewModel { constructor() protected open fun onCleared() internal

    fun clear() fun addCloseable(closeable: Closeable) fun addCloseable(key: String, closeable: Closeable) fun <T : Closeable> getCloseable(key: String): T? } val ViewModel.viewModelScope: CoroutineScope
  48. abstract class ViewModel { constructor() protected open fun onCleared() internal

    fun clear() fun addCloseable(closeable: Closeable) fun addCloseable(key: String, closeable: Closeable) fun <T : Closeable> getCloseable(key: String): T? } val ViewModel.viewModelScope: CoroutineScope
  49. abstract class ViewModel { constructor() protected open fun onCleared() internal

    fun clear() fun addCloseable(closeable: Closeable) fun addCloseable(key: String, closeable: Closeable) fun <T : Closeable> getCloseable(key: String): T? } val ViewModel.viewModelScope: CoroutineScope
  50. abstract class ViewModel { fun addCloseable(closeable: Closeable) fun addCloseable(key: String,

    closeable: Closeable) fun <T : Closeable> getCloseable(key: String): T? }
  51. abstract class ViewModel { fun addCloseable(closeable: java.io.Closeable) fun addCloseable(key: String,

    closeable: java.io.Closeable) fun <T : java.io.Closeable> getCloseable(key: String): T? }
  52. abstract class ViewModel { @Deprecated("Replaced by `AutoCloseable` overload.", DeprecationLevel.HIDDEN) fun

    addCloseable(closeable: java.io.Closeable) fun addCloseable(closeable: kotlin.AutoCloseable) fun addCloseable(key: String, closeable: kotlin.AutoCloseable) fun <T : kotlin.AutoCloseable> getCloseable(key: String): T? }
  53. abstract class ViewModel { @Deprecated("Replaced by `AutoCloseable` overload.", DeprecationLevel.HIDDEN) fun

    addCloseable(closeable: java.io.Closeable) fun addCloseable(closeable: kotlin.AutoCloseable) fun addCloseable(key: String, closeable: kotlin.AutoCloseable) fun <T : kotlin.AutoCloseable> getCloseable(key: String): T? }
  54. actual abstract class ViewModel { private val impl: ViewModelImpl =

    ViewModelImpl() actual fun addCloseable(key: String, closeable: AutoCloseable) { impl.addCloseable(key, closeable) } actual fun <T : AutoCloseable> getCloseable(key: String): T? = impl.getCloseable(key) }
  55. internal class ViewModelImpl { private val lock = SynchronizedObject() private

    val keyToCloseables = mutableMapOf<String, AutoCloseable>() private var isCleared = false fun addCloseable(key: String, closeable: AutoCloseable) { // Although no logic should be done after user calls onCleared(), we will // ensure that if it has already been called, the closeable attempting to // be added will be closed immediately to ensure there will be no leaks. if (isCleared) { closeWithRuntimeException(closeable) return } val oldCloseable = synchronized(lock) { keyToCloseables.put(key, closeable) } closeWithRuntimeException(oldCloseable) } /** @see [ViewModel.getCloseable] */ fun <T : AutoCloseable> getCloseable(key: String): T? = @Suppress("UNCHECKED_CAST") synchronized(lock) { keyToCloseables[key] as T? } }
  56. internal class ViewModelImpl { private val lock = SynchronizedObject() private

    val keyToCloseables = mutableMapOf<String, AutoCloseable>() private var isCleared = false fun addCloseable(key: String, closeable: AutoCloseable) { // Although no logic should be done after user calls onCleared(), we will // ensure that if it has already been called, the closeable attempting to // be added will be closed immediately to ensure there will be no leaks. if (isCleared) { closeWithRuntimeException(closeable) return } val oldCloseable = synchronized(lock) { keyToCloseables.put(key, closeable) } closeWithRuntimeException(oldCloseable) } /** @see [ViewModel.getCloseable] */ fun <T : AutoCloseable> getCloseable(key: String): T? = @Suppress("UNCHECKED_CAST") synchronized(lock) { keyToCloseables[key] as T? } }
  57. // common.SynchronizedObject.kt internal expect class SynchronizedObject() internal inline fun <T>

    synchronized(lock: SynchronizedObject, crossinline action: () -> T): T { contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) } return synchronizedImpl(lock, action) } internal expect inline fun <T> synchronizedImpl( lock: SynchronizedObject, crossinline action: () -> T, ): T
  58. // common.SynchronizedObject.kt internal expect class SynchronizedObject() internal inline fun <T>

    synchronized(lock: SynchronizedObject, crossinline action: () -> T): T { contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) } return synchronizedImpl(lock, action) } internal expect inline fun <T> synchronizedImpl( lock: SynchronizedObject, crossinline action: () -> T, ): T
  59. KT-29963 It's not possible to specify a contract for an

    expect function. https://youtrack.jetbrains.com/issue/KT-29963
  60. // jvm.SynchronizedObject.kt internal actual class SynchronizedObject actual constructor() internal actual

    inline fun <T> synchronizedImpl( lock: SynchronizedObject, crossinline action: () -> T ): T = kotlin.synchronized(lock, action)
  61. // native.SynchronizedObject.kt internal expect val PTHREAD_MUTEX_RECURSIVE: Int internal actual class

    SynchronizedObject actual constructor() {...} internal actual inline fun <T> synchronizedImpl( lock: SynchronizedObject, crossinline action: () -> T ): T { lock.lock() return try { action() } finally { lock.unlock() } }
  62. // native.SynchronizedObject.kt internal expect val PTHREAD_MUTEX_RECURSIVE: Int internal actual class

    SynchronizedObject actual constructor() { private val resource = Resource() private val cleaner = createCleaner(resource, Resource::dispose) fun lock() {...} fun unlock() {...} @OptIn(ExperimentalForeignApi::class) private class Resource {...} }
  63. // native.SynchronizedObject.kt internal expect val PTHREAD_MUTEX_RECURSIVE: Int internal actual class

    SynchronizedObject actual constructor() { private val resource = Resource() @OptIn(ExperimentalStdlibApi::class) private val cleaner = createCleaner(resource, Resource::dispose) fun lock() {...} fun unlock() {...} @OptIn(ExperimentalForeignApi::class) private class Resource {...} }
  64. // native.SynchronizedObject.kt internal expect val PTHREAD_MUTEX_RECURSIVE: Int // Lock.darwin.kt internal

    actual val PTHREAD_MUTEX_RECURSIVE: Int = platform.posix.PTHREAD_MUTEX_RECURSIVE // Lock.linux.kt internal actual val PTHREAD_MUTEX_RECURSIVE: Int = platform.posix.PTHREAD_MUTEX_RECURSIVE.toInt()
  65. val ViewModel.viewModelScope: CoroutineScope get() = synchronized(VIEW_MODEL_SCOPE_LOCK) { var scope =

    getCloseable(VIEW_MODEL_SCOPE_KEY) if (scope == null) { scope = CloseableCoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) addCloseable(VIEW_MODEL_SCOPE_KEY, scope) } scope } class CloseableCoroutineScope constructor( override val coroutineContext: CoroutineContext, ) : AutoCloseable, CoroutineScope { override fun close() = coroutineContext.cancel() }
  66. val ViewModel.viewModelScope: CoroutineScope get() = synchronized(VIEW_MODEL_SCOPE_LOCK) { var scope =

    getCloseable(VIEW_MODEL_SCOPE_KEY) if (scope == null) { scope = CloseableCoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) addCloseable(VIEW_MODEL_SCOPE_KEY, scope) } scope } class CloseableCoroutineScope constructor( override val coroutineContext: CoroutineContext, ) : AutoCloseable, CoroutineScope { override fun close() = coroutineContext.cancel() }
  67. val ViewModel.viewModelScope: CoroutineScope get() = synchronized(VIEW_MODEL_SCOPE_LOCK) { var scope =

    getCloseable(VIEW_MODEL_SCOPE_KEY) if (scope == null) { scope = CloseableCoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) addCloseable(VIEW_MODEL_SCOPE_KEY, scope) } scope } class CloseableCoroutineScope constructor( override val coroutineContext: CoroutineContext, ) : AutoCloseable, CoroutineScope { override fun close() = coroutineContext.cancel() }
  68. val ViewModel.viewModelScope: CoroutineScope get() = synchronized(VIEW_MODEL_SCOPE_LOCK) { var scope =

    getCloseable(VIEW_MODEL_SCOPE_KEY) if (scope == null) { scope = CloseableCoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) addCloseable(VIEW_MODEL_SCOPE_KEY, scope) } scope } class CloseableCoroutineScope constructor( override val coroutineContext: CoroutineContext, ) : AutoCloseable, CoroutineScope { override fun close() = coroutineContext.cancel() }
  69. val ViewModel.viewModelScope: CoroutineScope get() = synchronized(VIEW_MODEL_SCOPE_LOCK) { var scope =

    getCloseable(VIEW_MODEL_SCOPE_KEY) if (scope == null) { scope = createViewModelScope() addCloseable(VIEW_MODEL_SCOPE_KEY, scope) } scope } class CloseableCoroutineScope constructor( override val coroutineContext: CoroutineContext, ) : AutoCloseable, CoroutineScope { override fun close() = coroutineContext.cancel() }
  70. fun createViewModelScope(): CloseableCoroutineScope { val dispatcher = try { //

    In platforms where `Dispatchers.Main` is not available, Kotlin Multiplatform // will throw an exception (the specific exception type may depend on the platform). // Since there's no direct functional alternative, we use `EmptyCoroutineContext` to // ensure that a coroutine launched within this scope will run in the same context // as the caller. Dispatchers.Main.immediate } catch (_: NotImplementedError) { // In Native environments where `Dispatchers.Main` might not exist: EmptyCoroutineContext } catch (_: IllegalStateException) { // In JVM Desktop environments where `Dispatchers.Main` might not exist: EmptyCoroutineContext } return CloseableCoroutineScope(coroutineContext = dispatcher + SupervisorJob()) }
  71. fun createViewModelScope(): CloseableCoroutineScope { val dispatcher = try { //

    In platforms where `Dispatchers.Main` is not available, Kotlin Multiplatform // will throw an exception (the specific exception type may depend on the platform). // Since there's no direct functional alternative, we use `EmptyCoroutineContext` to // ensure that a coroutine launched within this scope will run in the same context // as the caller. Dispatchers.Main.immediate } catch (_: NotImplementedError) { // In Native environments where `Dispatchers.Main` might not exist: EmptyCoroutineContext } catch (_: IllegalStateException) { // In JVM Desktop environments where `Dispatchers.Main` might not exist: EmptyCoroutineContext } return CloseableCoroutineScope(coroutineContext = dispatcher + SupervisorJob()) }
  72. fun createViewModelScope(): CloseableCoroutineScope { val dispatcher = try { //

    In platforms where `Dispatchers.Main` is not available, Kotlin Multiplatform // will throw an exception (the specific exception type may depend on the platform). // Since there's no direct functional alternative, we use `EmptyCoroutineContext` to // ensure that a coroutine launched within this scope will run in the same context // as the caller. Dispatchers.Main.immediate } catch (_: NotImplementedError) { // In Native environments where `Dispatchers.Main` might not exist: EmptyCoroutineContext } catch (_: IllegalStateException) { // In JVM Desktop environments where `Dispatchers.Main` might not exist: EmptyCoroutineContext } return CloseableCoroutineScope(coroutineContext = dispatcher + SupervisorJob()) }
  73. fun createViewModelScope(): CloseableCoroutineScope { val dispatcher = try { //

    In platforms where `Dispatchers.Main` is not available, Kotlin Multiplatform // will throw an exception (the specific exception type may depend on the platform). // Since there's no direct functional alternative, we use `EmptyCoroutineContext` to // ensure that a coroutine launched within this scope will run in the same context // as the caller. Dispatchers.Main.immediate } catch (_: NotImplementedError) { // In Native environments where `Dispatchers.Main` might not exist: EmptyCoroutineContext } catch (_: IllegalStateException) { // In JVM Desktop environments where `Dispatchers.Main` might not exist: EmptyCoroutineContext } return CloseableCoroutineScope(coroutineContext = dispatcher + SupervisorJob()) }
  74. fun createViewModelScope(): CloseableCoroutineScope { val dispatcher = try { //

    In platforms where `Dispatchers.Main` is not available, Kotlin Multiplatform // will throw an exception (the specific exception type may depend on the platform). // Since there's no direct functional alternative, we use `EmptyCoroutineContext` to // ensure that a coroutine launched within this scope will run in the same context // as the caller. Dispatchers.Main.immediate } catch (_: NotImplementedError) { // In Native environments where `Dispatchers.Main` might not exist: EmptyCoroutineContext } catch (_: IllegalStateException) { // In JVM Desktop environments where `Dispatchers.Main` might not exist: EmptyCoroutineContext } return CloseableCoroutineScope(coroutineContext = dispatcher + SupervisorJob()) }
  75. Three key takeaways Minimal changes vs Best possible common API

    Reuse your knowledge Kotlin Multiplatform is Powerful