Skip to content

Improve retrieving ViewModels #80

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/Fruitties.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ on:
paths:
- 'Fruitties/**'
- '.github/workflows/Fruitties.yml'
pull_request:
paths:
- 'Fruitties/**'
- '.github/workflows/Fruitties.yml'
pull_request:
paths:
- 'Fruitties/**'
- '.github/workflows/Fruitties.yml'

concurrency:
group: build-${{ github.ref }}
Expand Down
2 changes: 1 addition & 1 deletion Fruitties/androidApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
android:dataExtractionRules="@xml/data_extraction_rules"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:name=".di.App">
android:name=".FruittiesAndroidApp">
<activity
android:name=".MainActivity"
android:exported="true">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
* limitations under the License.
*/

package com.example.fruitties.android.di
package com.example.fruitties.android

import android.app.Application
import androidx.compose.runtime.staticCompositionLocalOf
import com.example.fruitties.di.AppContainer
import com.example.fruitties.di.Factory

class App : Application() {
class FruittiesAndroidApp : Application() {
/** AppContainer instance used by the rest of classes to obtain dependencies */
lateinit var container: AppContainer

Expand All @@ -29,3 +30,11 @@ class App : Application() {
container = AppContainer(Factory(this))
}
}

/**
* Allows retrieving the AppContainer, which represents a DI graph everywhere from a composable.
* Because the [AppContainer] is effectively a singleton, we can use static composition local,
* because it won't change during the app execution.
*/
val LocalAppContainer =
staticCompositionLocalOf<AppContainer> { error("No AppContainer provided!") }
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavKey
Expand All @@ -35,6 +36,7 @@ import androidx.navigation3.ui.NavDisplay
import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator
import com.example.fruitties.android.ui.CartScreen
import com.example.fruitties.android.ui.FruittieScreen
import com.example.fruitties.android.ui.FruittiesTheme
import com.example.fruitties.android.ui.ListScreen
import kotlinx.serialization.Serializable

Expand All @@ -54,12 +56,16 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
) {
NavApp()
CompositionLocalProvider(
LocalAppContainer provides (this.applicationContext as FruittiesAndroidApp).container,
) {
FruittiesTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
) {
NavApp()
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,35 +50,23 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.fruitties.android.MyApplicationTheme
import com.example.fruitties.android.LocalAppContainer
import com.example.fruitties.android.R
import com.example.fruitties.android.di.App
import com.example.fruitties.model.CartItemDetails
import com.example.fruitties.model.Fruittie
import com.example.fruitties.viewmodel.CartUiState
import com.example.fruitties.viewmodel.CartViewModel
import com.example.fruitties.viewmodel.creationExtras

@Composable
fun CartScreen(onNavBarBack: () -> Unit) {
// Instantiate a ViewModel with a dependency on the AppContainer.
// To make ViewModel compatible with KMP, the ViewModel factory must
// create an instance without referencing the Android Application.
// Here we put the KMP-compatible AppContainer into the extras
// so it can be passed to the ViewModel factory.
val app = LocalContext.current.applicationContext as App

val viewModel: CartViewModel = viewModel(
factory = CartViewModel.Factory,
extras = creationExtras(app.container),
)

fun CartScreen(
onNavBarBack: () -> Unit,
viewModel: CartViewModel = viewModel(factory = LocalAppContainer.current.cartViewModelFactory),
) {
val cartState by viewModel.cartUiState.collectAsState()

CartScreen(
Expand Down Expand Up @@ -200,7 +188,7 @@ fun CartItem(
@Preview
@Composable
private fun CartScreenPreview() {
MyApplicationTheme {
FruittiesTheme {
CartScreen(
onNavBarBack = {},
cartState = CartUiState(
Expand Down Expand Up @@ -240,7 +228,7 @@ private fun CartScreenPreview() {
@Preview
@Composable
private fun CartItemPreview() {
MyApplicationTheme {
FruittiesTheme {
CartItem(
cartItem = CartItemDetails(
fruittie = Fruittie(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.fruitties.android.LocalAppContainer
import com.example.fruitties.android.R
import com.example.fruitties.android.di.App
import com.example.fruitties.model.Fruittie
import com.example.fruitties.viewmodel.FruittieViewModel
import com.example.fruitties.viewmodel.FruittieViewModel.Companion.FRUITTIE_ID_KEY
Expand All @@ -38,16 +37,14 @@ import com.example.fruitties.viewmodel.creationExtras
fun FruittieScreen(
fruittieId: Long,
onNavBarBack: () -> Unit,
) {
val app = LocalContext.current.applicationContext as App

val viewModel: FruittieViewModel = viewModel(
factory = FruittieViewModel.Factory,
extras = creationExtras(app.container) {
viewModel: FruittieViewModel = viewModel(
key = "fruittie_$fruittieId",
factory = LocalAppContainer.current.fruittieViewModelFactory,
extras = creationExtras {
set(FRUITTIE_ID_KEY, fruittieId)
},
)

),
) {
val state = viewModel.state.collectAsState().value

FruittieScreen(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.fruitties.android
package com.example.fruitties.android.ui

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.shape.RoundedCornerShape
Expand All @@ -31,7 +31,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

@Composable
fun MyApplicationTheme(
fun FruittiesTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,35 +45,25 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.fruitties.android.LocalAppContainer
import com.example.fruitties.android.R
import com.example.fruitties.android.di.App
import com.example.fruitties.model.Fruittie
import com.example.fruitties.viewmodel.MainViewModel
import com.example.fruitties.viewmodel.creationExtras

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ListScreen(
onClickViewCart: () -> Unit,
onFruittieClick: (Fruittie) -> Unit,
viewModel: MainViewModel = viewModel(
factory = LocalAppContainer.current.mainViewModelFactory,
),
) {
// Instantiate a ViewModel with a dependency on the AppContainer.
// To make ViewModel compatible with KMP, the ViewModel factory must
// create an instance without referencing the Android Application.
// Here we put the KMP-compatible AppContainer into the extras
// so it can be passed to the ViewModel factory.
val app = LocalContext.current.applicationContext as App
val viewModel: MainViewModel = viewModel(
factory = MainViewModel.Factory,
extras = creationExtras(app.container),
)

val uiState by viewModel.homeUiState.collectAsState()
val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(
Expand Down
2 changes: 1 addition & 1 deletion Fruitties/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ composeBom = "2025.06.01"
dataStore = "1.1.7"
kotlin = "2.2.0"
kotlinx-coroutines = "1.10.2"
kotlinxDatetime = "0.7.0-0.6.x-compat"
kotlinxDatetime = "0.7.0"
ksp = "2.2.0-2.0.2"
ktorVersion = "3.2.1"
pagingComposeAndroid = "3.3.6"
Expand Down
3 changes: 1 addition & 2 deletions Fruitties/iosApp/iosApp/ui/CartView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ struct CartView: View {
/// The `CartViewModel.Factory` and `creationExtras` are provided to enable dependency injection
/// and proper initialization of the ViewModel with its required `AppContainer`.
let cartViewModel: CartViewModel = viewModelStoreOwner.viewModel(
factory: CartViewModel.companion.Factory,
extras: creationExtras(appContainer: appContainer.value)
factory: appContainer.value.cartViewModelFactory
)

/// Observes the `cartUiState` `StateFlow` from the `CartViewModel` using SKIE's `Observing` utility.
Expand Down
3 changes: 1 addition & 2 deletions Fruitties/iosApp/iosApp/ui/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ struct ContentView: View {
/// 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: MainViewModel.companion.Factory,
extras: creationExtras(appContainer: appContainer.value)
factory: appContainer.value.mainViewModelFactory
)
NavigationStack {
Observing(mainViewModel.homeUiState) { homeUIState in
Expand Down
18 changes: 8 additions & 10 deletions Fruitties/iosApp/iosApp/ui/FruittieScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,14 @@ struct FruittieScreen: View {
var body: some View {
let fruittieViewModel: FruittieViewModel =
viewModelStoreOwner.viewModel(
factory: FruittieViewModel.companion.Factory,
extras: creationExtras(
appContainer: appContainer.value,
additional: { extras in
extras.set(
key: FruittieViewModel.companion.FRUITTIE_ID_KEY,
t: fruittie.id
)
}
)
key: "fruittie_\(fruittie.id)",
factory: appContainer.value.fruittieViewModelFactory,
extras: creationExtras { extras in
extras.set(
key: FruittieViewModel.companion.FRUITTIE_ID_KEY,
t: fruittie.id
)
}
)

Observing(fruittieViewModel.state) { uiState in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@
*/
package com.example.fruitties.di

import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.example.fruitties.DataRepository
import com.example.fruitties.viewmodel.CartViewModel
import com.example.fruitties.viewmodel.FruittieViewModel
import com.example.fruitties.viewmodel.FruittieViewModel.Companion.FRUITTIE_ID_KEY
import com.example.fruitties.viewmodel.MainViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
Expand All @@ -31,4 +37,26 @@ class AppContainer(
scope = CoroutineScope(Dispatchers.Default + SupervisorJob()),
)
}

val mainViewModelFactory = viewModelFactory {
initializer {
MainViewModel(repository = dataRepository)
}
}

val cartViewModelFactory = viewModelFactory {
initializer {
CartViewModel(repository = dataRepository)
}
}

val fruittieViewModelFactory = viewModelFactory {
initializer {
// this: CreationExtras
FruittieViewModel(
fruittieId = this[FRUITTIE_ID_KEY] ?: error("Expected fruittieId!"),
repository = dataRepository,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package com.example.fruitties.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import com.example.fruitties.DataRepository
Expand Down Expand Up @@ -62,12 +61,6 @@ class CartViewModel(
repository.removeFromCart(cartItem.fruittie)
}
}

companion object {
val Factory: ViewModelProvider.Factory = fruittiesViewModelFactory {
CartViewModel(repository = it.dataRepository)
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,6 @@ class FruittieViewModel(
}

companion object {
val Factory = fruittiesViewModelFactory {
FruittieViewModel(
fruittieId = get(FRUITTIE_ID_KEY) ?: error("Expected fruittieId!"),
repository = it.dataRepository,
)
}

val FRUITTIE_ID_KEY = CreationExtras.Key<Long>()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package com.example.fruitties.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import com.example.fruitties.DataRepository
Expand Down Expand Up @@ -59,12 +58,6 @@ class MainViewModel(
repository.addToCart(fruittie)
}
}

companion object {
val Factory: ViewModelProvider.Factory = fruittiesViewModelFactory {
MainViewModel(repository = it.dataRepository)
}
}
}

/**
Expand Down
Loading