Configurar o ViewModel para KMP

O ViewModel do AndroidX serve como uma ponte, estabelecendo um contrato claro entre sua lógica de negócios compartilhada e seus componentes de IU. Esse padrão ajuda a garantir a consistência dos dados em todas as plataformas, além de permitir que as interfaces sejam personalizadas para a aparência distinta de cada plataforma. Você pode continuar desenvolvendo sua interface com o Jetpack Compose no Android e o SwiftUI no iOS.

Leia mais sobre os benefícios de usar o ViewModel e todos os recursos na documentação principal do ViewModel.

Configurar dependências

Para configurar o ViewModel do KMP no projeto, defina a dependência no arquivo libs.versions.toml:

[versions]
androidx-viewmodel = 2.11.0

[libraries]
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-viewmodel" }

Em seguida, adicione o artefato ao arquivo build.gradle.kts do módulo KMP e declare a dependência como api, porque ela será exportada para o framework binário:

// You need the "api" dependency declaration here if you want better access to the classes from Swift code.
commonMain.dependencies {
  api(libs.androidx.lifecycle.viewmodel)
}

Exportar APIs do ViewModel para acesso pelo Swift

Por padrão, as bibliotecas adicionadas à base de código não são exportadas automaticamente para o framework binário. Se as APIs não forem exportadas, elas vão estar disponíveis no framework binário apenas se você as usar no código compartilhado (do conjunto de origem iosMain ou commonMain). Nesse caso, as APIs conteriam o prefixo do pacote. Por exemplo, uma classe ViewModel estaria disponível como uma classe Lifecycle_viewmodelViewModel. Confira como exportar dependências para binários para mais informações sobre como exportar dependências.

Para melhorar a experiência, exporte a dependência do ViewModel para o framework binário usando a configuração export no arquivo build.gradle.kts em que você define o framework binário do iOS. Isso torna as APIs do ViewModel acessíveis diretamente do código Swift, assim como no código Kotlin:

listOf(
  iosX64(),
  iosArm64(),
  iosSimulatorArm64(),
).forEach {
  it.binaries.framework {
    // Add this line to all the targets you want to export this dependency
    export(libs.androidx.lifecycle.viewmodel)
    baseName = "shared"
  }
}

(Opcional) Como usar viewModelScope na JVM para computador

Ao executar corrotinas em um ViewModel, a propriedade viewModelScope é vinculada ao Dispatchers.Main.immediate, que pode não estar disponível no computador por padrão. Para que funcione corretamente, adicione a dependência kotlinx-coroutines-swing ao projeto:

// Optional if you use JVM Desktop
desktopMain.dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:[KotlinX Coroutines version]")
}

Consulte a documentação do Dispatchers.Main para saber mais.

Usar o ViewModel de commonMain ou androidMain

Não há um requisito específico para usar a classe ViewModel no commonMain compartilhado nem no sourceSet androidMain. A única consideração é que não é possível usar APIs específicas da plataforma, e você precisa abstraí-las. Por exemplo, se você estiver usando um Application do Android como um parâmetro de construtor do ViewModel, será necessário migrar dessa API, abstraindo-a.

Para mais informações sobre como usar código específico da plataforma, consulte Código específico da plataforma no Kotlin Multiplatform.

Por exemplo, no snippet abaixo, há uma classe ViewModel com a fábrica dela, definida em commonMain:

// commonMain/MainViewModel.kt

class MainViewModel(
    private val repository: DataRepository,
) : ViewModel() { /* some logic */ }

// ViewModelFactory that retrieves the data repository for your app.
val mainViewModelFactory = viewModelFactory {
    initializer {
        MainViewModel(repository = getDataRepository())
    }
}

fun getDataRepository(): DataRepository = DataRepository()

Em seguida, no código da interface, recupere o ViewModel como de costume:

// androidApp/ui/MainScreen.kt

@Composable
fun MainScreen(
    viewModel: MainViewModel = viewModel(
        factory = mainViewModelFactory,
    ),
) {
// observe the viewModel state
}

Usar o ViewModel do SwiftUI

No Android, o ciclo de vida do ViewModel é processado automaticamente e tem como escopo um ComponentActivity, Fragment, NavBackStackEntry (Navigation 2) ou rememberViewModelStoreNavEntryDecorator (Navigation 3). No entanto, o SwiftUI no iOS não tem um equivalente integrado para o ViewModel do AndroidX.

Para compartilhar o ViewModel com o app SwiftUI, é necessário adicionar um código de configuração.

Criar uma função para ajudar com genéricos

A instanciação de uma instância genérica do ViewModel usa um recurso de reflexão de referência de classe no Android. Como os genéricos do Objective-C não são compatíveis com todos os recursos do Kotlin ou do Swift, não é possível recuperar diretamente um ViewModel de um tipo genérico do Swift.

Para ajudar com esse problema, crie uma função auxiliar que use ObjCClass em vez do tipo genérico e, em seguida, use getOriginalKotlinClass para recuperar a classe ViewModel a ser instanciada:

// iosMain/ViewModelResolver.ios.kt

/**
 *   This function allows retrieving any ViewModel from Swift Code with generics. We only get
 *   [ObjCClass] type for the [modelClass], because the interop between Kotlin and Swift code
 *   doesn't preserve the generic class, but we can retrieve the original KClass in Kotlin.
 */
@BetaInteropApi
@Throws(IllegalArgumentException::class)
fun ViewModelStore.resolveViewModel(
    modelClass: ObjCClass,
    factory: ViewModelProvider.Factory,
    key: String?,
    extras: CreationExtras? = null,
): ViewModel {
    @Suppress("UNCHECKED_CAST")
    val vmClass = getOriginalKotlinClass(modelClass) as? KClass<ViewModel>
    require(vmClass != null) { "The modelClass parameter must be a ViewModel type." }

    val provider = ViewModelProvider.Companion.create(this, factory, extras ?: CreationExtras.Empty)
    return key?.let { provider[key, vmClass] } ?: provider[vmClass]
}

Em seguida, quando quiser chamar a função do Swift, escreva uma função genérica do tipo T : ViewModel e use T.self, que pode transmitir o ObjCClass para a função resolveViewModel.

Conectar o escopo do ViewModel ao ciclo de vida do SwiftUI

A próxima etapa é criar um IosViewModelStoreOwner que implemente as interfaces (protocolos) ObservableObject e ViewModelStoreOwner. O motivo do ObservableObject é poder usar essa classe como um @StateObject no código do SwiftUI:

// iosApp/IosViewModelStoreOwner.swift

class IosViewModelStoreOwner: ObservableObject, ViewModelStoreOwner {

    let viewModelStore = ViewModelStore()

    /// This function allows retrieving the androidx ViewModel from the store.
    /// It uses the utilify function to pass the generic type T to shared code
    func viewModel<T: ViewModel>(
        key: String? = nil,
        factory: ViewModelProviderFactory,
        extras: CreationExtras? = nil
    ) -> T {
        do {
            return try viewModelStore.resolveViewModel(
                modelClass: T.self,
                factory: factory,
                key: key,
                extras: extras
            ) as! T
        } catch {
            fatalError("Failed to create ViewModel of type \(T.self)")
        }
    }

    /// This is called when this class is used as a `@StateObject`
    deinit {
        viewModelStore.clear()
    }
}

Esse proprietário permite recuperar vários tipos de ViewModel, de forma semelhante ao Android. O ciclo de vida desses ViewModels é limpo quando a tela que usa o IosViewModelStoreOwner é desinicializada e chama deinit. Saiba mais sobre a desinicialização na documentação oficial.

Neste ponto, basta instanciar o IosViewModelStoreOwner como um @StateObject em uma visualização do SwiftUI e chamar a função viewModel para recuperar um ViewModel:

// iosApp/ContentView.swift

struct ContentView: View {

    /// Use the store owner as a StateObject to allow retrieving ViewModels and scoping it to this screen.
    @StateObject private var viewModelStoreOwner = IosViewModelStoreOwner()

    var body: some View {
        /// Retrieves the `MainViewModel` instance using the `viewModelStoreOwner`.
        /// The `MainViewModel.Factory` and `creationExtras` are provided to enable dependency injection
        /// and proper initialization of the ViewModel with its required `AppContainer`.
        let mainViewModel: MainViewModel = viewModelStoreOwner.viewModel(
            factory: MainViewModelKt.mainViewModelFactory
        )
        // ...
        // .. the rest of the SwiftUI code
    }
}

Indisponível no Kotlin Multiplatform

Algumas das APIs disponíveis no Android não estão disponíveis no Kotlin Multiplatform.

Integração com o Hilt

Como o Hilt não está disponível para projetos Kotlin Multiplatform, não é possível usar diretamente ViewModels com a anotação @HiltViewModel no sourceSet commonMain. Nesse caso, você precisa usar um framework de DI alternativo, como Koin, kotlin-inject, Metro ou Kodein. Encontre todos os frameworks de DI que funcionam com o Kotlin Multiplatform em klibs.io (link em inglês).

Observar fluxos no SwiftUI

Não há suporte direto para observar fluxos de corrotinas no SwiftUI. No entanto, é possível usar a biblioteca KMP-NativeCoroutines ou SKIE para permitir esse recurso.