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 os componentes de UI. Esse padrão ajuda a garantir a consistência dos dados em todas as plataformas, permitindo 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 a ViewModel do KMP no seu projeto, defina a dependência no arquivo libs.versions.toml:

[versions]
androidx-viewmodel = 2.9.3

[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 ViewModel para acesso do Swift

Por padrão, qualquer biblioteca adicionada à sua base de código não é exportada automaticamente para o framework binário. Se as APIs não forem exportadas, elas estarão disponíveis no framework binário somente 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 classe Lifecycle_viewmodelViewModel. Confira 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 do 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) Usar viewModelScope na área de trabalho da JVM

Ao executar corrotinas em um ViewModel, a propriedade viewModelScope é vinculada ao Dispatchers.Main.immediate, que pode não estar disponível na área de trabalho por padrão. Para que ele 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 mais detalhes.

Usar 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 você não pode usar APIs específicas da plataforma e precisa abstraí-las. Por exemplo, se você estiver usando um Application do Android como um parâmetro do construtor do ViewModel, será necessário migrar dessa API abstraindo-a.

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

Por exemplo, no snippet a seguir, 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 normalmente:

// androidApp/ui/MainScreen.kt

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

Usar ViewModel do SwiftUI

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

Para compartilhar a ViewModel com seu app SwiftUI, adicione um código de configuração.

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

A criação de uma instância genérica de ViewModel usa um recurso de reflexão de referência de classe no Android. Como os tipos 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 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 para o ObservableObject é poder usar essa classe como um @StateObject no código 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, assim como no 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
    }
}

Não disponí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 ViewModels diretamente com a anotação @HiltViewModel no sourceSet commonMain. Nesse caso, use uma estrutura de DI alternativa, como Koin, kotlin-inject, Metro ou Kodein. Encontre todos os frameworks de DI que funcionam com Kotlin Multiplatform em klibs.io.

Observar fluxos no SwiftUI

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