Как получить предварительный просмотр в составных функциях, которые зависят от модели представления?

#android #android-jetpack-compose #android-viewmodel

Вопрос:

Описание проблемы

Я хотел бы иметь предварительный просмотр моей HomeScreen составной функции в моей HomeScreenPrevieiw функции предварительного просмотра. Однако это невозможно сделать, потому что я получаю следующую ошибку:

 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() (во втором фрагменте кода) и устранить зависимость от модели представления.

2. Это действительно хорошая идея, однако это могло бы стать проблемой, если бы у меня HomeScreen было больше зависимостей, связанных с HomeViewModel . Или, в идеале, экрану не нужно беспокоиться о модели представления, которая «связана» с ним?

3. Джим Спроч из Google пару раз заявлял, что вам следует свести к минимуму количество составных элементов, в которые вы передаете ViewModel . В более общем плане, @Preview предназначен для «листовых» композитных материалов, а не для полноэкранного режима. Для перевода в классические шаблоны Android вы можете просматривать пользовательские представления более легко, чем фрагменты или действия. Для этого существуют обходные пути, такие как реализация интерфейса viewmodel, возможность компоновки зависит от интерфейса и использование одноразовой реализации интерфейса для @Preview .

Ответ №1:

Это как раз одна из причин, по которой модель представления передается со значением по умолчанию. В предварительном просмотре вы можете передать тестовый объект:

 @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 хранилищем вместо хранилища, все зависит от ваших потребностей. Узнайте больше о тестировании рукояти

p.s. Я не уверен, что это сработает, так как у меня нет некоторых ваших занятий, но вы должны были уловить идею.

Комментарии:

1. Я использую рукоять, и когда мне нравится val viewModel = hiltViewModel<ProductsListViewModel>() метод предварительного просмотра, он все равно говорит ViewModels creation is not supported in Preview

2. @Dr. jacky Как я уже сказал в своем ответе, из предварительного просмотра вы должны пройти FakeCityRepository() , а не звонить hiltViewModel

Ответ №2:

Привет, как @Philip Духов объяснил, что его ответ правильный и в идеале должен быть сделан таким образом.

Но я хотел бы предложить обходной путь, потому что для этого требуется много настроек, таких как подделки и ручное создание промежуточных объектов.

Вы можете настроить предварительный просмотр на эмуляторе, используя пользовательскую конфигурацию запуска и используя определенное действие в качестве предварительного просмотра с аннотацией @AndroidEntryPoint.

Вы можете следовать подробному руководству со снимком экрана и внутренностями из блога, который я опубликовал здесь

Или просто вы можете

введите описание изображения здесь

введите описание изображения здесь

введите описание изображения здесь

Деятельность должна иметь

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

вам нужно вручную скопировать и вставить предварительный просмотр, составляемый в setContent{..} HiltPreviewActivity.

Запустите с панели инструментов, а не с ярлыка предварительного просмотра, проверьте руководство для получения подробной информации о режиме.

введите описание изображения здесь