投射时 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
和其他可组合段
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
和其他可组合段