#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 = {})
}
Поскольку у вас есть репозиторий, вы можете сделать то же самое, что и для тестирования модели представления.
- Создайте интерфейс для
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)
}
- Реализуйте его для
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)
}
- Создать
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.
Запустите с панели инструментов, а не с ярлыка предварительного просмотра, проверьте руководство для получения подробной информации о режиме.