kotlin-inject icon indicating copy to clipboard operation
kotlin-inject copied to clipboard

How to create ViewModel in Jetpack Compose?

Open Monabr opened this issue 1 year ago • 2 comments

Hello.

I'm trying to understand the documentation and can't find any important details.

How to create and pass a ViewModel in Jetpack Compose for Android?

The example I found suggests injecting the entire compose screen creation function directly into the component. But is that right? What about memory leaks and everything else? For me, it seems like some kind of reference to the view in the business logic of the application.

Also, passing parameters to such function is not sufficiently disclosed.

Monabr avatar Jul 26 '24 21:07 Monabr

How to create and pass a ViewModel in Jetpack Compose for Android?

Is https://github.com/evant/kotlin-inject/blob/main/docs/android.md#viewmodels what you are looking at? The idea is you inject the function to create the viewmodel then call it in viewModel {} to give it the right lifetime.

The example I found suggests injecting the entire compose screen creation function directly into the component

To inject into something it needs to be part of the component graph yah. I believe in the doc I linked it shows injecting into the composable but you could inject it into the fragment/activity instead if you want to do it that way, or create the component in the activity/fragment access the viewmodel creation function from it and pass it as an argument yourself.

What about memory leaks and everything else?

Not sure I understand, could you provide an example where it appears to leak memory?

For me, it seems like some kind of reference to the view in the business logic of the application.

When using compose your top-level composable function is often glue to wire your UI to your business logic, which is what fragments were traditionally used for. So I think it does make sense for them to be part of your graph.

Also, passing parameters to such function is not sufficiently disclosed.

Passing paramaters is done used assisted injection, the link shows an example using a SavedStateHandle which is generally what you want to use with a ViewModel, but you can also pass your own argument if you want. You'd do something like:

@Inject
class MyViewModel(@Assisted private val myArg: Arg) : ViewModel()
...
@Component fun MyComponent(createMyViewModel: (Arg) -> MyViewModel) {
    val viewModel = viewModel { createMyViewModel(arg) }
}

evant avatar Jul 26 '24 22:07 evant

Hi, I implemented it as follows. For your reference.

I defined a factory class for NavGraphBuilder (Jetpack AndroidX Navigation) and distributed the ViewModel from there to Composable.

interface AppRouteFactory {

    fun NavGraphBuilder.create(
        navController: NavController,
        modifier: Modifier,
    )
}

Example: HomeScreen with HomeViewModel

@Inject
class HomeRouteFactory(
    private val viewModelFactory: () -> HomeViewModel,
) : AppRouteFactory {
    override fun NavGraphBuilder.create(navController: NavController, modifier: Modifier) {
        composable(route = "HOME_ROUTE") { _ ->
            val viewModel = viewModel { viewModelFactory() }
            /* @Composable HomeScreen here */
        }
    }
}

Then I distributed this from a MainActivity Component(kotlin-inject) via AppContent.

interface AppContent {

    @Composable
    fun Content(
        modifier: Modifier,
    )
}

@Inject
class AppContentImpl(
    private val routeFactories: Set<AppRouteFactory>,
) : AppContent {

    @Composable
    override fun Content(
        modifier: Modifier,
    ) {
        NavHost(
            navController = rememberNavController(),
            startDestination = "HOME_ROUTE",
            modifier = modifier,
        ) {
            routeFactories.forEach { routeFactory ->
                with(routeFactory) {
                    [email protected](
                        navController = navController,
                        modifier = Modifier,
                    )
                }
            }
        }
    }
}

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val applicationComponent = /* Call your ApplicationComponent. */
        val component = MainActivityComponent::class.create(applicationComponent)

        setContent {
            component.appContent.Content(
                modifier = Modifier,
            )
        }
    }
}

Here is the MainActivity Component.

@Scope
annotation class MainActivityScope

@MainActivityScope
@Component
abstract class MainActivityComponent(
    @Component val applicationComponent: ApplicationComponent,
) : UiComponent

interface UiComponent  {

    val appContent: AppContent

    @MainActivityScope
    @Provides
    fun bindAppContent(bind: AppContentImpl): AppContent = bind

    @IntoSet
    @MainActivityScope
    @Provides
    fun bindHomeRouteFactory(bind: HomeRouteFactory): AppRouteFactory = bind
}

Full code is here https://github.com/oikvpqya/qiita-kotlin-inject-sample My Article of kotlin-inject (Japanese article, sorry not English) https://qiita.com/yuya2011/items/c3baea9a2fc4a6fce970

oikvpqya avatar Aug 02 '24 00:08 oikvpqya