Я разрабатывал приложение, в котором пытаюсь внедрить некоторые новые технологии, такие как Jetpack Compose. И в целом, это отличный инструмент, за исключением того, что он имеет жесткую систему предварительной визуализации (@Preview
), чем обычные файлы дизайна xml.
Моя проблема возникает, когда я пытаюсь создать @Preview
компонента, который представляет разные строки, куда я загружаю данные для восстановления из сети.
В моем случае я сделал это:
@Preview(
name = "ListScreenPreview ",
showSystemUi = true,
showBackground = true,
device = Devices.NEXUS_9)
@Composable
fun myPokemonRowPreview(
@PreviewParameter(PokemonListScreenProvider::class) pokemonMokData: PokedexListModel
) {
PokedexEntry(
model = pokemonMokData,
navController = rememberNavController(),
viewModel = hiltViewModel())
}
class PokemonListScreenProvider: PreviewParameterProvider<PokedexListModel> {
override val values: Sequence<PokedexListModel> = sequenceOf(
PokedexListModel(
pokemonName = "Cacamon",
number = 0,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png"
),
PokedexListModel(
pokemonName = "Tontaro",
number = 73,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png"
)
)
}
Чтобы представить этот @Composable:
@Composable
fun PokemonListScreen(
navController: NavController,
viewModel: PokemonListViewModel
) {
Surface(
color = MaterialTheme.colors.background,
modifier = Modifier.fillMaxSize()
)
{
Column {
Spacer(modifier = Modifier.height(20.dp))
Image(
painter = painterResource(id = R.drawable.ic_international_pok_mon_logo),
contentDescription = "Pokemon",
modifier = Modifier
.fillMaxWidth()
.align(CenterHorizontally)
)
SearchBar(
hint = "Search...",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
viewModel.searchPokemonList(it)
}
Spacer(modifier = Modifier.height(16.dp))
PokemonList(navController = navController,
viewModel = viewModel)
}
}
}
@Composable
fun SearchBar(
modifier: Modifier = Modifier,
hint: String = " ",
onSearch: (String) -> Unit = { }
) {
var text by remember {
mutableStateOf("")
}
var isHintDisplayed by remember {
mutableStateOf(hint != "")
}
Box(modifier = modifier) {
BasicTextField(value = text,
onValueChange = {
text = it
onSearch(it)
},
maxLines = 1,
singleLine = true,
textStyle = TextStyle(color = Color.Black),
modifier = Modifier
.fillMaxWidth()
.shadow(5.dp, CircleShape)
.background(Color.White, CircleShape)
.padding(horizontal = 20.dp, vertical = 12.dp)
.onFocusChanged {
isHintDisplayed = !it.isFocused
}
)
if (isHintDisplayed) {
Text(
text = hint,
color = Color.LightGray,
modifier = Modifier
.padding(horizontal = 20.dp, vertical = 12.dp)
)
}
}
}
@Composable
fun PokemonList(
navController: NavController,
viewModel: PokemonListViewModel
) {
val pokemonList by remember { viewModel.pokemonList }
val endReached by remember { viewModel.endReached }
val loadError by remember { viewModel.loadError }
val isLoading by remember { viewModel.isLoading }
val isSearching by remember { viewModel.isSearching }
LazyColumn(contentPadding = PaddingValues(16.dp)) {
val itemCount = if (pokemonList.size % 2 == 0) {
pokemonList.size / 2
} else {
pokemonList.size / 2 + 1
}
items(itemCount) {
if (it >= itemCount - 1 && !endReached && !isLoading && !isSearching) {
viewModel.loadPokemonPaginated()
}
PokedexRow(rowIndex = it, models = pokemonList, navController = navController, viewModel = viewModel)
}
}
Box(
contentAlignment = Center,
modifier = Modifier.fillMaxSize()
) {
if (isLoading) {
CircularProgressIndicator(color = MaterialTheme.colors.primary)
}
if (loadError.isNotEmpty()) {
RetrySection(error = loadError) {
viewModel.loadPokemonPaginated()
}
}
}
}
@SuppressLint("LogNotTimber")
@Composable
fun PokedexEntry(
model: PokedexListModel,
navController: NavController,
modifier: Modifier = Modifier,
viewModel: PokemonListViewModel
) {
val defaultDominantColor = MaterialTheme.colors.surface
var dominantColor by remember {
mutableStateOf(defaultDominantColor)
}
Box(
contentAlignment = Center,
modifier = modifier
.shadow(5.dp, RoundedCornerShape(10.dp))
.clip(RoundedCornerShape(10.dp))
.aspectRatio(1f)
.background(
Brush.verticalGradient(
listOf(dominantColor, defaultDominantColor)
)
)
.clickable {
navController.navigate(
"pokemon_detail_screen/${dominantColor.toArgb()}/${model.pokemonName}/${model.number}"
)
}
) {
Column {
CoilImage(
imageRequest = ImageRequest.Builder(LocalContext.current)
.data(model.imageUrl)
.target {
viewModel.calcDominantColor(it) { color ->
dominantColor = color
}
}.build(),
imageLoader = ImageLoader.Builder(LocalContext.current)
.availableMemoryPercentage(0.25)
.crossfade(true)
.build(),
contentDescription = model.pokemonName,
modifier = Modifier
.size(120.dp)
.align(CenterHorizontally),
loading = {
ConstraintLayout(
modifier = Modifier.fillMaxSize()
) {
val indicator = createRef()
CircularProgressIndicator(
//Set constrains dynamically
modifier = Modifier.constrainAs(indicator) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
)
}
},
// shows an error text message when request failed.
failure = {
Text(text = "image request failed.")
}
)
Log.d("pokemonlist", model.imageUrl)
Text(
text = model.pokemonName,
fontFamily = RobotoCondensed,
fontSize = 20.sp,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
@Composable
fun PokedexRow(
rowIndex: Int,
models: List<PokedexListModel>,
navController: NavController,
viewModel: PokemonListViewModel
) {
Column {
Row {
PokedexEntry(
model = models[rowIndex * 2],
navController = navController,
modifier = Modifier.weight(1f),
viewModel = viewModel
)
Spacer(modifier = Modifier.width(16.dp))
if (models.size >= rowIndex * 2 + 2) {
PokedexEntry(
model = models[rowIndex * 2 + 1],
navController = navController,
modifier = Modifier.weight(1f),
viewModel = viewModel
)
} else {
Spacer(modifier = Modifier.weight(1f))
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
@Composable
fun RetrySection(
error: String,
onRetry: () -> Unit,
) {
Column() {
Text(error, color = Color.Red, fontSize = 18.sp)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { onRetry() },
modifier = Modifier.align(CenterHorizontally)
) {
Text(text = "Retry")
}
}
}
Я пытаюсь аннотировать с помощью @Nullable navController и модели представления PokemonListScreen @Composable, но тоже не работает. Я все еще вижу пустой экран:
Поэтому я пытаюсь найти документацию Jetpack, но это просто определение довольно простых Composables.
Так что, если у вас есть дополнительные знания об этом и вы можете помочь, заранее спасибо!
Основная проблема заключается в том, что если я хочу предварительно просмотреть этот @Composable, хотя я сделал @Nullable для параметра модели представления, что, я думаю, здесь проблема, AS все еще требует инициализации. Потому что я предполагаю, что правильный способ передать аргумент в предварительный просмотр — это аннотация @PreviewArgument.
[РЕДАКТИРОВАТЬ]
После некоторого копания я обнаружил, что AS возвращает следующую ошибку на экране предварительного просмотра:
Итак, в любом случае, чтобы избежать ошибки модели просмотра ??
[РЕШЕНИЕ]
Наконец, примените следующее решение, которое работает, потому что причина проблемы связана с тем, что у Hilt есть некоторые несовместимости с превью Jetpack Compose:
@SuppressLint("UnrememberedMutableState")
@Preview(
name = "ListScreenPreview",
showSystemUi = true,
showBackground = true,
device = Devices.PIXEL)
@Composable
fun MyPokemonRowPreview(
@PreviewParameter(PokemonListScreenProvider::class) pokemonMokData: PokedexListModel
) {
JetpackComposePokedexTheme {
PokedexRow(
rowIndex = 0,
models = PokemonListScreenProvider().values.toList(),
navController = rememberNavController(),
viewModel = PokemonListViewModelMock(
0, mutableStateOf(""), mutableStateOf(value = false),
mutableStateOf(false), mutableStateOf(listOf(pokemonMokData))
)
)
}
}
class PokemonListScreenProvider: PreviewParameterProvider<PokedexListModel> {
override val values: Sequence<PokedexListModel> = sequenceOf(
PokedexListModel(
pokemonName = "Machasaurio",
number = 0,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png"
),
PokedexListModel(
pokemonName = "Tontaro",
number = 73,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png"
)
)
}
PokemonListViewModelInterface
interface PokemonListViewModelInterface {
var curPage : Int
var loadError: MutableState<String>
var isLoading: MutableState<Boolean>
var endReached: MutableState<Boolean>
var pokemonList: MutableState<List<PokedexListModel>>
fun searchPokemonList(query: String)
fun loadPokemonPaginated()
fun calcDominantColor(drawable: Drawable, onFinish: (Color) -> Unit)
}
PokemonListViewModelMock
class PokemonListViewModelMock (
override var curPage: Int,
override var loadError: MutableState<String>,
override var isLoading: MutableState<Boolean>,
override var endReached: MutableState<Boolean>,
override var pokemonList: MutableState<List<PokedexListModel>>
): PokemonListViewModelInterface{
override fun searchPokemonList(query: String) {
TODO("Not yet implemented")
}
override fun loadPokemonPaginated() {
TODO("Not yet implemented")
}
override fun calcDominantColor(drawable: Drawable, onFinish: (Color) -> Unit) {
TODO("Not yet implemented")
}
}
Фактический предварительный просмотр выглядит следующим образом, и хотя изображение не отображается, оно отображается правильно:
Спасибо за ваши комментарии, но во избежание этого, почему вы должны использовать аннотацию @PreviewParamater в случае, если вы хотите просмотреть какой-то компонент, которому нужны некоторые данные, верно? Итак, как мне изменить свой код, чтобы он работал?
Глядя на ваш код, похоже, что он должен работать, но если это не так, попробуйте сделать недействительным кеш или что-то в этом роде. Возможно, проблема связана с составным компилятором.
@Jeel Vankhede пытался, но не работает
Я не уверен в глубине этого приложения, но потенциальной идеей было бы кодировать интерфейс, а не реализацию.
То есть создайте интерфейс со всеми функциями, которые вам нужны (которые могут уже существовать в вашей ViewModel), реализуйте его PokemonListViewModel
и создайте еще один фиктивный класс, который также его реализует. Передайте макет в предварительный просмотр и оставьте реальную реализацию с PokemonListViewModel
interface PokeListViewModel {
...
// your other val's
val isLoading: Boolean
fun searchPokemonList(pokemon: String)
fun loadPokemonPaginated()
// your other functions
...
}
Как только вы создадите свой интерфейс, вы можете просто обновить свои составные объекты, чтобы они ожидали, например, объект, который «является» PokeListViewModel
.
Надеюсь, это поможет
Спасибо, я попробовал ваше решение, и оно работает, хотя изображение не загружается в предварительный просмотр. Я объясняю решение в последнем редактировании
Вы можете создать другой компонуемый объект, который вызывает логику модели представления через лямбда-функции вместо использования самой модели представления. Извлеките свой uiState в отдельный класс, чтобы его можно было использовать как StateFlow
в вашей модели представления, что, в свою очередь, можно наблюдать из компонуемого.
@Composable
fun PokemonListScreen(
navController: NavController,
viewModel: PokemonListViewModel
) {
/*
rememberStateWithLifecyle is an extension function based on
https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
*/
val uiState by rememberStateWithLifecycle(viewModel.uiState)
PokemonListScreen(
uiState = uiState,
onLoadPokemons = viewModel::loadPokemons,
onSearchPokemon = {viewModel.searchPokemon(it)},
onCalculateDominantColor = {viewModel.calcDominantColor(it)},
onNavigate = {route -> navController.navigate(route, null, null)},
)
}
@Composable
private fun PokemonListScreen(
uiState: PokemonUiState,
onLoadPokemons:()->Unit,
onSearchPokemon: (String) -> Unit,
onCalculateDominantColor: (Drawable) -> Color,
onNavigate:(String)->Unit,
) {
}
@HiltViewModel
class PokemonListViewModel @Inject constructor(/*your datasources*/) {
private val loading = MutableStateFlow(false)
private val loadError = MutableStateFlow(false)
private val endReached = MutableStateFlow(false)
private val searching = MutableStateFlow(false)
private val pokemons = MutableStateFlow<Pokemon?>(null)
val uiState: StateFlow<PokemonUiState> = combine(
loading,
loadError,
endReached,
searching,
pokemons
) { loading, error, endReached, searching, pokemons ->
PokemonUiState(
isLoading = loading,
loadError = error,
endReached = endReached,
isSearching = searching,
pokemonList = pokemons,
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = PokemonUiState.Empty,
)
}
data class PokemonUiState(
val pokemonList: List<Pokemon> = emptyList(),
val endReached: Boolean = false,
val loadError: Boolean = false,
val isLoading: Boolean = false,
val isSearching: Boolean = false,
) {
companion object {
val Empty = PokemonUiState()
}
}
Обычный подход к отображению предварительного просмотра предполагает, что предварительный просмотр не должен зависеть от каких-либо дополнительных параметров в аргументах метода, в то время как компонуемый для предварительного просмотра также должен быть подъемным состоянием.