投射时 Jetpack Compose 预览渲染问题 LocalContext.current

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;

有一点,一切正常。然后出现此错误,当我尝试在该项目和另一个项目中调用“LocalContext.current”并将“context.applicationContext 作为应用程序”时,预览停止工作。它曾经与“LocalContext.current”一起工作的地方

尝试了不同版本的 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.invoke(AccountScreen.kt:136)   at com.client.personalfinance.screens.ComposableSingletons$AccountScreenKt$lambda-2.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.invoke(MaterialTheme.kt:82)   at androidx.compose.material.MaterialThemeKt$MaterialTheme.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)
 }

这是一条错误的线路 viewModel(factory = MainVeiwModelFactory(context.applicationContext as Application)

您可以看到您的“as”是不正确的。 尝试创建一个没有上下文的空 ViewModel。 它通过预览解决了问题

当您需要访问 Android lifecycle-specific 的内容时,我找到了使预览工作的最佳方式,例如应用程序、Activity、FragmentManager、ViewModel 等是创建该接口的一个不执行任何操作的实现。

一个使用 FragmentManager 的例子:

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

        Text(text = "Open Dialog")
    }

}

预览功能:

object PreviewFragmentManager: FragmentManager()

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

现在您的预览功能将呈现。

您可以对 ViewModel 做同样的事情 - 只需让您的 ViewModel 扩展一个接口。

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

对于您的情况,我建议执行上述 ViewModel 的步骤,不要在预览中乱用 LocalContext。

如果您尝试注入 ViewModel,则无法呈现预览可组合项。

有很多方法可以避免这个问题。但对我来说,最简单明了的方法就是不渲染具有 viewModel 的可组合项。

为此,您只需将 MyView 可组合项的内容提取到另一个名为 MyViewContent 的可组合项即可。

MyView 可组合项将声明 MyViewContent 需要的所有状态(他将是有状态的)并且 MyViewContent 可组合项将这些值作为参数(他将是无状态的)。

通过这种方式,您可以确保尊重 state hoisting pattern,因为由于 MyViewContent

,您的 UI 的主要部分将是无状态的

据我了解

假设您的屏幕分为 2 个部分,每个部分有 2 个子部分,依此类推。

首先你这样做

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

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

RightSection looks similar

但是渲染预览是不可能的。所以你可以这样做(传递简单的稳定类型作为参数“top-down”)如果你想渲染整个屏幕

@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

并且由于需要传递大量参数而陷入噩梦,其中一些参数不需要此可组合项,但嵌套

因此您可以将预览中创建的所有内容(不仅是原始类型)作为参数传递,但要像这样组织它

@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

如您所见,viewmodel 仅位于 statefull MainScreen,您可以渲染 MainContent 和其他可组合段