Home > Software engineering >  How to make a @Preview in JetpackCompose, when the component depends of some data provide by ViewMod
How to make a @Preview in JetpackCompose, when the component depends of some data provide by ViewMod

Time:04-05

I was developing an App where I try to implement some new technologies, as Jetpack Compose. And in general, it's a great tool, except the fact that it has hard pre-visualize system (@Preview) thn the regular xml design files.

My problem comes when I try to create a @Preview of the component which represent the different rows, where I load my data recover from network.

In my case I made this:

@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"

        )
    )
}

To represent this @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")
        }
    }
}

I try to annotate with the @Nullable navController and viewmodel of the PokemonListScreen @Composable, but doesn't work either. I'm still seeing an empty screen:

enter image description here

So I try to search into the Jetpack documentation but, it's just defining quite simple Composables.

So if you have some more knowledge about it and can help, thanks in advance !

The main problem is if I wanna Preview that @Composable, although I made @Nullable to the viewmodel parameter, which I guess it's the problem here, AS still demand to initialize. Because I guess the right way to pass argument to a preview is by @PreviewArgument annotation.

[EDIT]

After some digging, I found AS is returning the following error under the Preview Screen:

enter image description here

So, there anyway to avoid viewmodel error??

CodePudding user response:

I'm not sure of the depth of this application, but a potential idea would be to code to an interface and not an implementation.

That is, create an interface with all of the functions you need (that may already exist in your ViewModel), have your PokemonListViewModel implement it, and create another mock class that implements it as well. Pass the mock into your preview and leave the real implementation with PokemonListViewModel

interface PokeListViewModel {
  ...
  // your other val's
  val isLoading: Boolean
  fun searchPokemonList(pokemon: String)
  fun loadPokemonPaginated()
  // your other functions
  ...
}
  

Once you create your interface you can simply update your composables to be expecting an object that "is a" PokeListViewModel, for example.

Hopefully this helps

CodePudding user response:

You could create another composable which invokes the viewmodel logic via lambda functions instead of using the viewmodel itself. Extract your uiState to a separate class, so it can be used as a StateFlow in your viewmodel, which in turn can be observed from the composable.

@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,
        onl oadPokemons = viewModel::loadPokemons,
        onSearchPokemon = {viewModel.searchPokemon(it)},
        onCalculateDominantColor = {viewModel.calcDominantColor(it)},
        onNavigate = {route -> navController.navigate(route, null, null)},
    )
}

@Composable
private fun PokemonListScreen(
        uiState: PokemonUiState,
        onl oadPokemons:()->Unit,
        onSearchPokemon: (String) -> Unit,
        onCalculateDominantColor: (/*Type*/) -> 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()
    }
}
  • Related