11

I am using Jetpack Compose and noticed that the preview is not shown. I read articles like this, but it seems my problem has a different root cause. Even I added defaults to all parameters in the compose function like this:

@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
@ExperimentalFoundationApi
@Preview
fun VolumeSettingsScreen(
    speech: SpeechHelper = SpeechHelper(), // my class that converts text to speech
    viewModel: VolumeSettingsViewModel = hiltViewModel(), // using Hilt to inject ViewModels
    navController: NavHostController = rememberNavController() // Compose Navigation component
) {
    MyAppTheme {
        Box(
             ...
        )
    }
}

When I rollbacked some changes I realized that the @Preview does not support the viewModels regardless of whether they are injected with Hilt or not.

Any Idea how this could be fixed?

2

3 Answers 3

9

Have you considered having a structure where you have a Screen and the actual Content separated like this?

// data class
data class AccountData(val accountInfo: Any?)

// composable "Screen", where you define contexts, viewModels, hoisted states, etc
@Composable
fun AccountScreen(viewModel: AccountViewModel = hiltViewModel()) {

    val accountData = viewModel.accountDataState.collectAsState()

    AccountContent(accountData = accountData) {
        // click callback
    }
}

//your actual composable that hosts your child composable widget/components
@Composable
fun AccountContent(
    accountData: AccountData,
    clickCallback: () ->
) {
   ...
}

where you can have a preview for the Content like this?

@Preview
@Composable
fun AccountContentPreview() {

    // create some mock AccountData
    val mockData = AccountData(…)
    AccountContent(accountData = mockData) {
         // I'm not expecting some actual ViewModel calls here, instead I'll just manipulate the mock data
    }
}

this way, all components that aren't needed to be configured by the actual content composable are separated, taking you off from headaches configuring a preview.

Just an added note and could be off-topic, I just noticed you have a parameter like this,

speech: SpeechHelper = SpeechHelper()

you might consider utilizing compositionLocalProvider (if needed), that could clean up your parameters.

Sign up to request clarification or add additional context in comments.

8 Comments

Thank you for the answer, In Mobin Yardim's comment, a similar approach is suggested. But I think it is a workaround, not a solution to the problem. Because the Preview of the AccountScreen still will be broken because of the ViewModel injections
I just read the link he provided, yes its a very similar suggestion, but If I may ask, view model injections such as..?
I mean when you have this: AccountScreen(viewModel: AccountViewModel = hiltViewModel()), the preview is not working.
Thanks for the comment, I got it. Thank you as for the compositionLocalProvider will check it as well
Thanks for the explanation, I see that we should use callbacks to handle user interactions (e.g. clicks, gestures and etc. )sooner or later :D.
|
4

I managed to visualize the preview of the screen, by wrapping the ViewModels's functions into data classes, like this:

@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
@ExperimentalFoundationApi
@Preview
fun VolumeSettingsScreen(
    modifier: Modifier = Modifier,
    speechCallbacks: SpeechCallbacks = SpeechCallbacks(),
    navigationCallbacks: NavigationCallbacks = NavigationCallbacks(),
    viewModelCallbacks: VolumeSettingsScreenCallbacks = VolumeSettingsScreenCallbacks()
) {
    MyAppheme {
        Box(
             ...
        )
    }
}

I passed not the ViewModel directly in the compose but needed functions in a Data class for example, like this:

data class VolumeSettingsScreenCallbacks(
    val uiState: Flow<BaseUiState?> = flowOf(null),
    val onValueUpSelected: () -> Boolean = { false },
    val onValueDownSelected: () -> Boolean = { false },
    val doOnBoarding: (String) -> Unit = {},
    val onScreenCloseRequest: (String) -> Unit = {} 
)

I made a method that generates those callbacks in the ViewModel, like this:

@HiltViewModel
class VolumeSettingsViewModel @Inject constructor() : BaseViewModel() {

    fun createViewModelCallbacks(): VolumeSettingsScreenCallbacks =
        VolumeSettingsScreenCallbacks(
            uiState = uiState,
            onValueUpSelected = ::onValueUpSelected,
            onValueDownSelected = ::onValueDownSelected,
            doOnBoarding = ::doOnBoarding,
            onScreenCloseRequest = ::onScreenCloseRequest
        )

 ....
}

In the NavHost I hoisted the creation of the ViewModel like this:

    @Composable
    @ExperimentalFoundationApi
    fun MyAppNavHost(
        speech: SpeechHelper,
        navController: NavHostController,
        startDestination: String = HOME.route,
    ): Unit = NavHost(
        navController = navController,
        startDestination = startDestination,
    ) {
        ...
    
        composable(route = Destination.VOLUME_SETTINGS.route) {
            hiltViewModel<VolumeSettingsViewModel>().run {
                VolumeSettingsScreen(
                    modifier = keyEventModifier,
                    speechCallbacks = speech.createCallback() // my function,
                    navigation callbacks = navController.createCallbacks(), //it is mine extension function                  
                    viewModelCallbacks = createViewModelCallbacks()
                )
            }
        }
    
        ...
    }

It is a bit complicated, but it works :D. I will be glad if there are some comets for improvements.

Comments

4

I found a solution that enables seeing preview that is optimal during development, but not so much in production code.

Make your viewmodel param in your compose nullable:

@Composable
fun VolumeSettingsScreen(
    viewModel: VolumeSettingsViewModel? = hiltViewModel(), ...)

Then, in your preview just pass a null param:

@Preview(showBackground = true)
@Composable
fun PreviewVolumeSettingsScreen() {
   VolumeSettingsScreen(null, ....)
}
    

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.