airbnb / mavericks

Mavericks: Android on Autopilot
https://airbnb.io/mavericks/
Apache License 2.0
5.85k stars 498 forks source link

Android Studio design fails on Composable is not hosted in a ComponentActivity! #631

Closed eboudrant closed 8 months ago

eboudrant commented 2 years ago

I have a composable that access to the mavericks view model :

@Preview
@Composable
fun MyComposable() {
    val viewModel: MyViewModel = mavericksViewModel()
    ....
}

As soon as I launch the design tab in Android Studio I get error, stack trace being :

java.lang.IllegalStateException: Composable is not hosted in a ComponentActivity!
    at com...MyComposable.MyComposable(MyComposable.kt:373)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

I can't find anything online mentioning this error so I'm opening this issue.

Using AS Electric Eel / Compose 1.2 beta / Mavericks 2.6.0

AS Composable is not hosted in a ComponentActivity error

Thanks

eboudrant commented 2 years ago

Ok, my understanding is you can pass the view model to the composable and build a view model / state for the preview composable.

@Preview
@Composable
fun MyComposablePreview() {
    Mavericks.initialize(debugMode = true)
    MyComposable(
        MyViewModel(MyState())
    )
}

@Composable
fun MyComposable(viewModel: MyViewModel ) {
    ....
}

Is it the recommended way?

elihart commented 2 years ago

Passing the ViewModel like that can work, but the viewmodel might have a bunch of other code and application/dagger references that will not work in the preview either. The approach I am taking so far is to actually use mockk (or mockito) to mock out the viewmodel. With this approach I can 1) specify the exact state I want to test with 2) enforce that the UI only invokes unidirectional functions on the ViewModel 3) avoid issues with dagger/application objection in previews. This also allows the same composables to work in Paparazzi for screenshot testing. (Another option could be to have a composable layer where you pass just the State object and viewmodel callbacks as lambda parameters - would be more tidy and explicit, but also a bit of a pain to manage and scale.)

I have set up some scaffolding to help mock the viewmodel and setup the state to preview automatically for each viewmodel, and we have a new pattern for declaring mocks (as opposed to the Fragment based mock system). We're still building it out and validating the design, but hopefully we can share more later this year.

safa007 commented 1 year ago

@elihart I'm wondering if this approach has evolved on your end and if there's a better way to access view model/state or whether or not you're hoisting state.

Our team is running into the same issue as above where if we reference the view model from a smaller composable function the Compose Preview doesn't work. We have the option of hoisting state, but we run into the risk of passing too many parameters down to the smaller Composables.

Could you also clarify your approach with using mockk to mock out the view model? I'm not understanding how it works.

BBogucki commented 1 year ago

@safa007 do you know if another solution exists? As @elihart said, in my case, where viewmodel has a lot of parameters, mocking them would be a nightmare

Dwite commented 8 months ago

@elihart @gpeal Do you have any suggestion or proposals to share how to resolve this issue?

elihart commented 8 months ago

Our solution is to mock the viewmodel; with that you can set the coroutine scope and state that you want returned by the viewmodel. the compose UI should only be accessing the state flow from the viewmodel, so there should be nothing else to mock. The ui will call functions on the viewmodel for event handling, but those functions can be set to be no-ops via the mocking (eg, Mockk has the relaxUnitFun option).

You aren't going to get a functional viewmodel in a compose preview.