android-studiokotlinandroid-jetpack-composeandroid-jetpack-compose-preview

Jetpack Compose Preview Render Problem when casting LocalContext.current


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

Solution

  • 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.