Skip to content

Commit

Permalink
Merge pull request #1875 from InsertKoinIO/3.6.0_compose_viewmodel
Browse files Browse the repository at this point in the history
3.6.0 compose viewmodel - KMP
  • Loading branch information
arnaudgiuliani authored May 17, 2024
2 parents a2af6e9 + 854d9d7 commit dc6f348
Show file tree
Hide file tree
Showing 12 changed files with 863 additions and 11 deletions.
52 changes: 52 additions & 0 deletions projects/compose/koin-compose-viewmodel/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.compose)
}

val koinComposeVersion: String by project
version = koinComposeVersion

kotlin {
jvm {
withJava()
}

js(IR) {
nodejs()
browser()
binaries.executable()
}

wasmJs {
binaries.executable()
nodejs()
}

iosX64()
iosArm64()
iosSimulatorArm64()
macosX64()
macosArm64()

sourceSets {
commonMain.dependencies {
api(project(":compose:koin-compose"))
api(libs.compose.viewmodel)
api(libs.compose.navigation)
}
}
}

rootProject.the<NodeJsRootExtension>().apply {
nodeVersion = "21.0.0-v8-canary202309143a48826a08"
nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary"
}

tasks.withType<org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask>().configureEach {
args.add("--ignore-engines")
}

apply(from = file("../../gradle/publish.gradle.kts"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2017-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("DeprecatedCallableAddReplaceWith")

package org.koin.compose.viewmodel

import androidx.compose.runtime.Composable
import androidx.lifecycle.*
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import org.koin.compose.currentKoinScope
import org.koin.core.annotation.KoinExperimentalAPI
import org.koin.core.annotation.KoinInternalApi
import org.koin.core.parameter.ParametersDefinition
import org.koin.core.qualifier.Qualifier
import org.koin.core.scope.Scope

/*
Ported directly from Android side. Waiting more feedback
*/

/**
* Resolve ViewModel instance with Navigation NavBackStackEntry as extras parameters
*
* @param qualifier
* @param parameters
*
* @author Arnaud Giuliani
*/
@OptIn(KoinInternalApi::class)
@KoinExperimentalAPI
@Composable
inline fun <reified T : ViewModel> koinNavViewModel(
qualifier: Qualifier? = null,
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
key: String? = null,
extras: CreationExtras = defaultNavExtras(viewModelStoreOwner),
scope: Scope = currentKoinScope(),
noinline parameters: ParametersDefinition? = null,
): T {
return resolveViewModel(
T::class, viewModelStoreOwner.viewModelStore, key, extras, qualifier, scope, parameters
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2017-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("DeprecatedCallableAddReplaceWith")

package org.koin.compose.viewmodel

import androidx.compose.runtime.Composable
import androidx.core.bundle.Bundle
import androidx.lifecycle.*
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.MutableCreationExtras
import androidx.navigation.NavBackStackEntry
import androidx.savedstate.SavedStateRegistryOwner
import org.koin.core.annotation.KoinInternalApi

/**
* Resolve ViewModel instance
*
* @param qualifier
* @param parameters
*
* @author Arnaud Giuliani
*/
@OptIn(KoinInternalApi::class)
@Composable
fun defaultNavExtras(viewModelStoreOwner: ViewModelStoreOwner): CreationExtras = when {
//TODO To be fully verified
viewModelStoreOwner is NavBackStackEntry && viewModelStoreOwner.arguments != null -> viewModelStoreOwner.arguments?.toExtras(viewModelStoreOwner) ?: CreationExtras.Empty
viewModelStoreOwner is HasDefaultViewModelProviderFactory -> viewModelStoreOwner.defaultViewModelCreationExtras
else -> CreationExtras.Empty
}

/**
* Convert current Bundle to CreationExtras
* @param viewModelStoreOwner
*/
@KoinInternalApi
fun Bundle.toExtras(viewModelStoreOwner: ViewModelStoreOwner): CreationExtras? {
return if (keySet().isEmpty()) null
else {
runCatching {
MutableCreationExtras().also { extras ->
extras[DEFAULT_ARGS_KEY] = this
extras[VIEW_MODEL_STORE_OWNER_KEY] = viewModelStoreOwner
extras[SAVED_STATE_REGISTRY_OWNER_KEY] = viewModelStoreOwner as SavedStateRegistryOwner
}
}.getOrNull()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2017-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("DeprecatedCallableAddReplaceWith")

package org.koin.compose.viewmodel

import androidx.compose.runtime.Composable
import androidx.lifecycle.*
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import org.koin.compose.currentKoinScope
import org.koin.core.annotation.KoinExperimentalAPI
import org.koin.core.annotation.KoinInternalApi
import org.koin.core.parameter.ParametersDefinition
import org.koin.core.qualifier.Qualifier
import org.koin.core.scope.Scope

/*
Ported directly from Android side. Waiting more feedback
*/

/**
* Resolve ViewModel instance
*
* @param qualifier
* @param parameters
*
* @author Arnaud Giuliani
*/

@OptIn(KoinInternalApi::class)
@KoinExperimentalAPI
@Composable
inline fun <reified T : ViewModel> koinViewModel(
qualifier: Qualifier? = null,
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
key: String? = null,
extras: CreationExtras = defaultExtras(viewModelStoreOwner),
scope: Scope = currentKoinScope(),
noinline parameters: ParametersDefinition? = null,
): T {
return resolveViewModel(
T::class, viewModelStoreOwner.viewModelStore, key, extras, qualifier, scope, parameters
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright 2017-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.koin.compose.viewmodel

import androidx.compose.runtime.Composable
import androidx.lifecycle.HasDefaultViewModelProviderFactory
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.viewmodel.CreationExtras
import org.koin.core.annotation.KoinInternalApi
import org.koin.core.parameter.ParametersDefinition
import org.koin.core.parameter.ParametersHolder
import org.koin.core.qualifier.Qualifier
import org.koin.core.scope.Scope
import org.koin.mp.KoinPlatformTools
import kotlin.reflect.KClass

@KoinInternalApi
fun <T : ViewModel> resolveViewModel(
vmClass: KClass<T>,
viewModelStore: ViewModelStore,
key: String?,
extras: CreationExtras,
qualifier: Qualifier?,
scope: Scope,
parameters: (() -> ParametersHolder)?
): T {
val factory = KoinViewModelFactory(vmClass, scope, qualifier, parameters)
val provider = ViewModelProvider.create(viewModelStore, factory, extras)
val vmKey = getViewModelKey(qualifier, key, KoinPlatformTools.getClassName(vmClass))
return when {
vmKey != null -> provider[vmKey, vmClass]
else -> provider[vmClass]
}
}

@KoinInternalApi
internal fun getViewModelKey(qualifier: Qualifier? = null, key: String? = null, className: String? = null): String? {
return when {
key != null -> key
qualifier != null -> qualifier.value + (className?.let { "_$className" } ?: "")
else -> null
}
}

class KoinViewModelFactory(
private val kClass: KClass<out ViewModel>,
private val scope: Scope,
private val qualifier: Qualifier? = null,
private val params: ParametersDefinition? = null
) : ViewModelProvider.Factory {

//TODO Should handle Extras/Bundle injection here (see Android side)
override fun <T : ViewModel> create(modelClass: KClass<T>, extras: CreationExtras): T {
return scope.get(kClass, qualifier,params)
}
}

/**
* Resolve ViewModel instance
*
* @param qualifier
* @param parameters
*
* @author Arnaud Giuliani
*/
@Composable
fun defaultExtras(viewModelStoreOwner: ViewModelStoreOwner): CreationExtras = when {
viewModelStoreOwner is HasDefaultViewModelProviderFactory -> viewModelStoreOwner.defaultViewModelCreationExtras
else -> CreationExtras.Empty
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2017-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.koin.compose.viewmodel.dsl

import androidx.lifecycle.ViewModel
import org.koin.core.definition.Definition
import org.koin.core.definition.KoinDefinition
import org.koin.core.module.Module
import org.koin.core.qualifier.Qualifier

//TODO Ported from Android side

/**
* ViewModel DSL Extension
* Allow to declare a ViewModel - be later inject into Activity/Fragment with dedicated injector
*
* @author Arnaud Giuliani
*
* @param qualifier - definition qualifier
* @param definition - allow definition override
*/
inline fun <reified T : ViewModel> Module.viewModel(
qualifier: Qualifier? = null,
noinline definition: Definition<T>
): KoinDefinition<T> {
return factory(qualifier, definition)
}
Loading

0 comments on commit dc6f348

Please sign in to comment.