如何在依赖于视图模型的可组合函数中获得预览?

11

问题描述

我想在HomeScreenPreview预览函数中显示HomeScreen组合函数的预览。但是,由于我遇到了以下错误,所以无法实现:

java.lang.IllegalStateException: ViewModels creation is not supported in Preview
    at androidx.compose.ui.tooling.ComposeViewAdapter$FakeViewModelStoreOwner$1.getViewModelStore(ComposeViewAdapter.kt:709)
    at androidx.lifecycle.ViewModelProvider.<init>(ViewModelProvider.kt:105)
    at androidx.lifecycle.viewmodel.compose.ViewModelKt.get(ViewModel.kt:82)
    at androidx.lifecycle.viewmodel.compose.ViewModelKt.viewModel(ViewModel.kt:72)
    at com.example.crud.ui.screens.home.HomeScreenKt.HomeScreen(HomeScreen.kt:53)
    at com.example.crud.ui.screens.home.HomeScreenKt.HomeScreenPreview(HomeScreen.kt:43)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    ...

我的代码

这是我的HomeScreen代码:

@Composable
fun HomeScreen(
    viewModel: HomeViewModel = hiltViewModel(),
    navigateToDetailsAction: () -> Unit,
    openCardDetailsAction: (Int) -> Unit
) {
    val cities = viewModel.cities.observeAsState(listOf())
    Scaffold(
        topBar = { HomeAppBar() },
        floatingActionButton = { HomeFab(navigateToDetailsAction) }
    ) {
        HomeContent(cities) { id -> openCardDetailsAction(id) }
    }
}

这是我的预览功能的代码:

@Preview
@Composable
private fun HomeScreenPreview() {
    HomeScreen(navigateToDetailsAction = {}, openCardDetailsAction = {})
}

我的视图模型:

@HiltViewModel
class HomeViewModel @Inject constructor(repository: CityRepository) : ViewModel() {
    val cities: LiveData<List<City>> = repository.allCities.asLiveData()
}

代码库:

@ViewModelScoped
class CityRepository @Inject constructor(appDatabase: AppDatabase) {
    private val dao by lazy { appDatabase.getCityDao() }

    val allCities by lazy { dao.getAllCities() }

    suspend fun addCity(city: City) = dao.insert(city)

    suspend fun updateCity(city: City) = dao.update(city)

    suspend fun deleteCity(city: City) = dao.delete(city)

    suspend fun getCityById(id: Int) = dao.getCityById(id)

}

应用程序数据库:

@Database(entities = [City::class], version = 2, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
    abstract fun getCityDao() : CityDao
}

我的失败尝试

我认为可能是因为将视图模型作为 HomeScreen 的默认参数传递导致了问题,所以我决定这样做:

@Composable
fun HomeScreen(
    navigateToDetailsAction: () -> Unit,
    openCardDetailsAction: (Int) -> Unit
) {
    val viewModel: HomeViewModel = hiltViewModel()
    val cities = viewModel.cities.observeAsState(listOf())
    Scaffold(
        topBar = { HomeAppBar() },
        floatingActionButton = { HomeFab(navigateToDetailsAction) }
    ) {
        HomeContent(cities) { id -> openCardDetailsAction(id) }
    }
}

但它仍然不起作用(我一直得到相同的错误),而且对于测试也不好,因为它会阻止我使用模拟的视图模型来测试HomeScreen


1
你可以将 cities 传递到 HomeScreen()(在第二个代码片段中)并消除对视图模型的依赖。 - CommonsWare
1
这是一个非常好的想法,但如果我的 HomeScreenHomeViewModel 有更多的依赖关系,那么这可能会成为一个问题。或者,理想情况下,一个屏幕不需要担心与之“相关”的视图模型? - Pierre Vieira
2
谷歌的Jim Sproch在多个场合表示,您应该将传递给ViewModel的Composeable数量最小化。更一般地说,@Preview是为“叶子”Composeable而设计的,而不是全屏幕。要转换为经典的Android模式,您可以更方便地预览自定义视图,而不是片段或活动。有一些解决方法,例如使ViewModel实现接口,使Composeable依赖于接口,并使用接口的一次性实现进行@Preview。 - CommonsWare
2个回答

10

这正是为什么视图模型要带有默认值的原因之一。在预览中,你可以传递一个测试对象:

@Preview
@Composable
private fun HomeScreenPreview() {
    val viewModel = HomeViewModel()
    // setup viewModel as you need it to be in the preview
    HomeScreen(viewModel = viewModel, navigateToDetailsAction = {}, openCardDetailsAction = {})
}

既然您有一个存储库,您可以执行与测试视图模型相同的操作。

  1. CityRepository创建接口
interface CityRepositoryI {
    val allCities: List<City>

    suspend fun addCity(city: City)
    suspend fun updateCity(city: City)
    suspend fun deleteCity(city: City)
    suspend fun getCityById(id: Int)
}
  1. 将其实现到CityRepository中:
@ViewModelScoped
class CityRepository @Inject constructor(appDatabase: AppDatabase) : CityRepositoryI {
    private val dao by lazy { appDatabase.getCityDao() }

    override val allCities by lazy { dao.getAllCities() }

    override suspend fun addCity(city: City) = dao.insert(city)

    override suspend fun updateCity(city: City) = dao.update(city)

    override suspend fun deleteCity(city: City) = dao.delete(city)

    override suspend fun getCityById(id: Int) = dao.getCityById(id)
}
  1. 为了测试目的,创建 FakeCityRepository
class FakeCityRepository : CityRepositoryI {
    // predefined cities for testing
    val cities = listOf(
        City(1)
    ).toMutableStateList()

    override val allCities by lazy { cities }

    override suspend fun addCity(city: City) {
        cities.add(city)
    }

    override suspend fun updateCity(city: City){
        val index = cities.indexOfFirst { it.id == city.id }
        cities[index] = city
    }

    override suspend fun deleteCity(city: City) {
        cities.removeAll { it.id == city.id }
    }

    override suspend fun getCityById(id: Int) = cities.first { it.id == id }
}

所以你可以将其传递到你的视图模型中:HomeViewModel(FakeCityRepository())

如果需要,你也可以使用AppDatabase来代替存储库。了解更多关于Hilt测试的内容。

p.s. 我不确定是否能够构建成功,因为我没有你的某些类,但你应该已经理解了这个想法。


我使用 Hilt,当我在预览方法中执行 val viewModel = hiltViewModel<ProductsListViewModel>() 时,它仍然显示 ViewModels creation is not supported in Preview - Dr.jacky
4
正如我在答案中所说的,从预览中,您应该传递 FakeCityRepository() 而不是调用 hiltViewModel - Phil Dukhov

4

大家好,就像@Philip Dukhov在他的回答中所解释的那样是正确的,理想情况下应该以这种方式完成。

但我想提供一种解决方法,因为它需要许多设置,例如伪造对象和手动创建中间对象。

您可以使用自定义运行配置,在模拟器上使用特定的Activity作为PreviewActivity,并使用@AndroidEntryPoint注释来使预览工作。

您可以从我发布的博客这里详细了解屏幕截图和内部信息

或者简单地说,您可以

enter image description here

enter image description here

enter image description here

Activity需要具备以下功能:

@AndroidEntryPoint
class HiltPreviewActivity : AppCompatActivity() {
....
}

你需要手动将预览元素复制并粘贴到 HiltPreviewActivity setContent {..} 中。

从工具栏运行,不要从预览快捷方式运行,请查看指南获取更多详细信息。

输入图片说明


1
警告:活动必须导出或包含一个意图过滤器。如何解决?对我无效。 - Chathuranga Shan Jayarathna
@ChathurangaShanJayarathna 请尝试在“启动选项”中检查“跳过活动验证”,然后它将完美运行。 - fobidlim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接