Skip to content
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

How to create ViewModel in Jetpack Compose? #418

Open
Monabr opened this issue Jul 26, 2024 · 2 comments
Open

How to create ViewModel in Jetpack Compose? #418

Monabr opened this issue Jul 26, 2024 · 2 comments
Labels
question Further information is requested

Comments

@Monabr
Copy link

Monabr commented Jul 26, 2024

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.

@evant evant added the question Further information is requested label Jul 26, 2024
@evant
Copy link
Owner

evant commented Jul 26, 2024

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) }
}

@oikvpqya
Copy link

oikvpqya commented Aug 2, 2024

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) {
                    this@NavHost.create(
                        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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants