Jetpack Compose Preview Render Problem when casting LocalContext.current

8.3k views Asked by At
Android Studio Chipmunk 2021.2.1; 
Compose Version = '1.1.1'; 
Gradle  Version 7.4.2; 
Kotlin 1.6.10;

Up to one point, everything was working. Then this error appeared and the preview stopped working when I try to call "LocalContext.current" and make "context.applicationContext as Application" both in this project and in another one. Where it used to work with "LocalContext.current"

Tried on different versions of Compose, kotlin, gradle.

Render problem

java.lang.ClassCastException: class com.android.layoutlib.bridge.android.BridgeContext cannot be cast to class android.app.Application (com.android.layoutlib.bridge.android.BridgeContext and android.app.Application are in unnamed module of loader com.intellij.ide.plugins.cl.PluginClassLoader @3a848149)   at com.client.personalfinance.screens.ComposableSingletons$AccountScreenKt$lambda-2$1.invoke(AccountScreen.kt:136)   at com.client.personalfinance.screens.ComposableSingletons$AccountScreenKt$lambda-2$1.invoke(AccountScreen.kt:133)   at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)   at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)   at androidx.compose.material.MaterialTheme_androidKt.PlatformMaterialTheme(MaterialTheme.android.kt:23)   at androidx.compose.material.MaterialThemeKt$MaterialTheme$1$1.invoke(MaterialTheme.kt:82)   at androidx.compose.material.MaterialThemeKt$MaterialTheme$1$1.invoke(MaterialTheme.kt:81)   at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)   at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)   at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228)   at androidx.compose.material.TextKt.ProvideTextStyle(Text.kt:265

@Preview(showBackground = true) 
@Composable fun PrevAccountScreen() {
    val context  = LocalContext.current
    val mViewModel: MainViewModel =
             viewModel(factory = MainVeiwModelFactory(context.applicationContext as Application))
    AccountScreen(navController = rememberNavController(), viewModel = mViewModel)
 }
5

There are 5 answers

1
Chantell Osejo On BEST ANSWER

I found the best way to get a Preview to work when you need to access something that's Android lifecycle-specific, e.g. Application, Activity, FragmentManager, ViewModel, etc, is to create an implementation of that interface that does nothing.

An example using FragmentManager:

@Composable
@OptIn(ExperimentalAnimationApi::class)
fun MyFragmentView(
    fragmentManager: FragmentManager
) {
    Button(modifier = Modifier.align(Alignment.End),
           onClick = {         
              MyDialogFragment().show(fragmentManager, "MyDialogTag")
           }
    ) {

        Text(text = "Open Dialog")
    }

}

Preview function:

object PreviewFragmentManager: FragmentManager()

@Preview
@Composable
fun MyFragmentViewPreview() {
    MyFragmentView(
        fragmentManager = PreviewFragmentManager
    )
}

Now your Preview function will render.

You can do the same thing with ViewModel - just make your ViewModel extend an interface.

import androidx.compose.runtime.*
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.flow.StateFlow

interface MyViewModel {
   val state: StateFlow<SomeState>
   fun doSomething(input: String)
}

class MyViewModelImpl: MyViewModel, ViewModel() {
   // implement interface's required values/functions
}

object PreviewViewModel: MyViewModel() 

@Composable
fun MyView(viewModel: MyViewModel = viewModel<MyViewModelImpl>()) {
   // UI building goes here
}

@Composable
@Preview
fun MyViewPreview() {
    MyView(viewModel = PreviewViewModel)
}

In your case, I would suggest doing the steps for ViewModel outlined above, and not messing around with LocalContext whatsoever in your preview.

2
Alexander On

This is a wrong line viewModel(factory = MainVeiwModelFactory(context.applicationContext as Application)

You can see your "as" is incorrect. Try to create an empty ViewModel without context. It solves the problem with a preview

0
Jolan DAUMAS On

You can't render a preview composable if you try to inject a ViewModel.

There is plenty of way to avoid this problem. But for me, the easiest and clearest way to do this is simply to NOT RENDER a composable who have a viewModel.

To do that you can simply extract the content of your MyView composable to another composable called MyViewContent.

The MyView composable will declare all states that the MyViewContent will need (he will be statefull) and the MyViewContent composable will have those values as parameter (he will be stateless).

This way you ensure to respect the state hoisting pattern because you have the major part of your UI that will be stateless thanks to MyViewContent

0
vitidev On

As I understand

Let's imagine that your screen is divided into 2 sections, which have 2 subsections, and so on.

First you do this

@Composable
fun MainScreen(viewModel: ...){
    Row {
       LeftSection(viewModel)
       RightSection(viewModel)
    }
}

@Composable
fun LeftSection(viewModel: ...){
    Row {
       LeftSubSection1(viewModel)
       LeftSubSection2(viewModel)
    }
}

RightSection looks similar

But it's impossible to render preview. So you can do like this (Pass simple stable types as parameters "top-down") if you want render whole screen

@Composable
fun MainScreen(viewModel: ...){
    MainContent(viewModel.prop1, viewmodel.prop2, viewmodel.prop3, viewmodel.someAction, viewmodel.someSubSectionAction, viewmodel.someSubSectionAction2)
}

fun MainContent(prop1: Int, prop2: Int, prop3: Int, onSomeAction: ()-> Unit, onSomeSubsectionAction: ()->Unit, onSomeSubsectionAction2: ()->Unit ){
    Row {
       LeftSection(prop1, prop2, someAction, someSubSectionAction...)
       RightSection(prop2, prop3, someAction, someSubSectionAction2...)
    }
}

@Composable
fun LeftSection(prop1: Int, prop2: Int, onSomeAction: ()-> Unit, onSomeSubsectionAction: ()->Unit ){
    Row {
       LeftSubSection1(prop1, onSomeSubsectionAction, ...)
       LeftSubSection2(prop2, onSomeSubsectionAction, ...)
    }
}

RightSection looks similar

And get a nightmare due to the need to pass a huge number of parameters, some of which are not needed by this composable, but are needed by the nested

So you can pass as parameters everything that can be created in the preview (not only primitive types), but organize it like this

@Composable
fun MainScreen(viewModel: ...) {
    MainContent(
        leftContent = {
            LeftSection(
                prop = viewModel.prop1,
                onSomeAction = { (viewModel.onSomeAction()) },
                subSection1 = { 
                    LeftSubSection1(viewModel.prop2) 
                },
                subSection2 = { 
                    LeftSubSection1(onSomeSubsectionAction = { viewModel.onSomeSubsectionAction() }) 
                }
            )
        },
        rightContent = {
            RightSection(
                prop = viewModel.prop2,
                onSomeAction = { (viewModel.onSomeAction2()) },
                subSection1 = { 
                    RightSubSection1(viewModel.prop2) 
                },
                subSection2 = { 
                    RightSubSection1(onSomeSubsectionAction = { viewModel.onSomeSubsectionAction2() }))
                }
        })
}

@Composable
private fun MainContent(
    leftContent: @Composable () -> Unit,
    rightContent: @Composable () -> Unit,
) {
    Row {
        leftContent()
        rightContent()
    }
}

@Composable
fun LeftSection(
    prop: Int,
    onSomeAction: () -> Unit,
    subSection1: @Composable () -> Unit,
    subSection2: @Composable () -> Unit
) {
    // use prop and onSomeAction
    Row {
        subSection1()
        subSection2()
    }    
}

RightSection looks similar

As you can see, viewmodel is located only in statefull MainScreen, and you can render MainContent and other composable segments

0
Mirhack On

If you need preview of the function, that uses context you need to use safe cast. Here's how I do it when inject httpClient to Coil.

val context = LocalContext.current
val appApi = remember { (context.applicationContext as? AppApiProvider)?.provideAppApi() }
val imageLoader = remember {
    ImageLoader.Builder(context)
        .apply { if (appApi != null) okHttpClient(UiComponent.get(appApi).httpClient) }
        ...
        .build()