For my pet app, I have a composable that looks like this:
#Composable
fun PokeBrowser(model: PokeBrowserViewModel) {
val pokemonDataState by model.pokemonDataList.observeAsState()
pokemonDataState?.let {
val staticState = pokemonDataState ?: listOf()
LazyColumn() {
itemsIndexed(staticState) { i, p ->
if (i == staticState.lastIndex) {
model.loadPokemonData()
}
val pokeImagePainter = rememberImagePainter(
data = p.imageUrl
)
Row(
horizontalArrangement = Arrangement.Start,
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
) {
Image(
painter = pokeImagePainter,
contentDescription = "Pokemon name"
)
Text(text = p.name)
}
}
}
}
}
I populate the lazy load of data in this way:
private fun getPokemonData(nextPokeList: List<GetPokemonListResponse.PokemonUrl>) {
viewModelScope.launch {
val requests = nextPokeList.map { urlData ->
async {
pokeClient.getPokemonData(urlData.url)
}
}
val responses = requests.awaitAll()
val newPokemonDataList = _pokemonDataList.value as ArrayList
newPokemonDataList.addAll(
pokeClientMapper.mapPokeDataResponseToDomainModel(responses)
)
_pokemonDataList.postValue(newPokemonDataList)
}
}
I can see that more data is coming in on this line: _pokemonDataList.postValue(newPokemonDataList)
But, the composable does not update. Am I missing something?
Try this:
val pokemonDataState by model.pokemonDataList.observeAsState().value
I suspected that to postValue correctly I needed a brand new collection pointer. So, I rewrote the code in this way, which fixed the problem:
private fun getPokemonData(nextPokeList: List<GetPokemonListResponse.PokemonUrl>) {
viewModelScope.launch {
val requests = nextPokeList.map { urlData ->
async {
pokeClient.getPokemonData(urlData.url)
}
}
val responses = requests.awaitAll()
val oldPokemonData = _pokemonDataList.value as ArrayList
val newPokemonData = ArrayList<Pokemon>(oldPokemonData.size + responses.size)
newPokemonData.addAll(oldPokemonData.toList())
newPokemonData.addAll(pokeClientMapper.mapPokeDataResponseToDomainModel(responses))
_pokemonDataList.postValue(newPokemonData)
}
}
Related
I'm using the Paging 3 library with Jetpack Compose and have just implemented swipe to dismiss on some paged data (using the Material library's SwipeToDismiss composable).
Once a swipe action has completed, I call a method in my ViewModel to send an update to the server (either to mark a message as read or to delete a message). Once this action has taken place, I obviously need to refresh the paging data.
My current approach is to have a call back from my ViewModel function which will then handle the refresh on the LazyPagingItems, but this feels wrong.
Is there a better approach?
My ViewModel basically looks like:
#HiltViewModel
class MessageListViewModel #Inject constructor(
private val repository: Repository
): ViewModel() {
companion object {
private const val TAG = "MessageListViewModel"
}
val messages : Flow<PagingData<Message>> = Pager(
PagingConfig(
enablePlaceholders = false,
)
) {
MessagePagingSource(repository)
}.flow.cachedIn(viewModelScope)
fun markRead(guid: String, onComplete: () -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
try {
repository.markMessageRead(guid)
onComplete()
} catch (e: Throwable) {
Log.e(TAG, "Error marking message read: $guid", e)
}
}
}
}
And in my Composable for the message list, it looks a bit like the following:
#Composable
fun MessageListScreen(
vm: MessageListViewModel = viewModel(),
) {
val messages: LazyPagingItems<MessageSummary> = vm.messages.collectAsLazyPagingItems()
val refreshState = rememberSwipeRefreshState(
isRefreshing = messages.loadState.refresh is LoadState.Loading,
)
Scaffold(
topBar = {
SmallTopAppBar (
title = {
Text(stringResource(R.string.message_list_title))
},
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
) {
SwipeRefresh(
state = refreshState,
onRefresh = {
messages.refresh()
},
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
) {
items(
items = messages,
key = { it.guid }
) { message ->
message?.let {
MessageRow(
onMarkRead = {
vm.markRead(message.guid) {
messages.refresh()
}
},
)
}
}
}
}
}
}
}
As I say, this does work, it just doesn't quite feel like the cleanest approach.
I'm fairly new to working with flows, so I don't know if there's some other trick I'm missing...
I ended up implementing something like this:
View Model:
class MessageListViewModel #Inject constructor(
private val repository: Repository,
): ViewModel() {
sealed class UiAction {
class MarkReadError(val error: Throwable): UiAction()
class MarkedRead(val id: Long): UiAction()
}
private val _uiActions = MutableSharedFlow<UiAction>()
val uiActions = _uiActions.asSharedFlow()
.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
)
fun markRead(id: Long) {
viewModelScope.launch(Dispatchers.IO) {
try {
repository.markMessageRead(id)
_uiActions.emit(UiAction.MarkedRead(id))
} catch (e: Throwable) {
Log.e(TAG, "Error marking message read: $id", e)
_uiActions.emit(UiAction.MarkReadError(e))
}
}
}
}
View:
#Composable
fun MessageListScreen(
vm: MessageListViewModel = viewModel(),
onMarkReadFailed: (String) -> Unit,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val messages: LazyPagingItems<Message> = vm.messages.collectAsLazyPagingItems()
val refreshState = rememberSwipeRefreshState(
isRefreshing = messages.loadState.refresh is LoadState.Loading,
)
LaunchedEffect(lifecycleOwner) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.uiActions.collectLatest {
when (it) {
is MessageListViewModel.UiAction.MarkReadError -> {
val msg = it.error.localizedMessage ?: it.error.message
val message = if (!msg.isNullOrEmpty()) {
context.getString(R.string.error_unknown_error_with_message, msg)
} else {
context.getString(R.string.error_unknown_error_without_message)
}
onMarkReadFailed(message)
}
is MessageListViewModel.UiAction.MarkedRead -> {
messages.refresh()
}
}
}
}
}
SwipeRefresh(
state = refreshState,
onRefresh = {
messages.refresh()
},
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
state = listState,
) {
items(
items = messages,
key = { it.id }
) { message ->
message?.let {
MessageRow(
onMarkRead = {
vm.markRead(message.id)
},
)
}
FadedDivider()
}
messages.apply {
when (loadState.append) {
is LoadState.Loading -> {
item {
LoadingRow(R.string.messages_loading)
}
}
else -> {}
}
}
}
}
}
I am new in Compose Navigation. I have Button and when I clicked, I called the function in Viewmodel and trigger loading event with using StateFlow. So I called the next screen through navigation and calling loading spinner. I used delay(5000) to show spinner more before getting data but spinner is loading after the data is loaded. Can someone guide me.
MainActivityViewModel.kt
class MainActivityViewModel(private val resultRepository: ResultRepository) : ViewModel() {
val stateResultFetchState = MutableStateFlow<ResultFetchState>(ResultFetchState.OnEmpty)
fun getSportResult() {
viewModelScope.launch {
stateResultFetchState.value = ResultFetchState.IsLoading
val result = resultRepository.getSportResult()
delay(5000)
result.handleResult(
onSuccess = { response ->
if (response != null) {
stateResultFetchState.value = ResultFetchState.OnSuccess(response)
} else {
stateResultFetchState.value = ResultFetchState.OnEmpty
}
},
onError = {
stateResultFetchState.value =
ResultFetchState.OnError(it.errorResponse?.errorMessage)
}
)
}
}
}
SetupMainActivityView.kt
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun SetupMainActivityView(
viewModel: MainActivityViewModel = koinViewModel(),
navigateToNext: () -> Unit,
) {
Scaffold(topBar = {
TopAppBar(
title = { Text(text = stringResource(id = R.string.app_name)) },
backgroundColor = getBackgroundColor(),
elevation = 0.dp
)
}, content = { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.background(getBackgroundColor())
.padding(padding),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = {
viewModel.getSportResult()
}) {
Text(text = stringResource(id = R.string.get_result))
}
}
})
when (val state = viewModel.stateResultFetchState.collectAsState().value) {
is ResultFetchState.OnSuccess -> {}
is ResultFetchState.IsLoading -> {
navigateToNext()
}
is ResultFetchState.OnError -> {}
is ResultFetchState.OnEmpty -> {}
}
}
My whole project link. Can someone guide me how can I show loading spinner after loading the next screen. Thanks
UPDATE
NavigationGraph.kt
#Composable
internal fun NavigationGraph() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = ScreenRoute.Home.route) {
composable(ScreenRoute.Home.route) {
SetupMainActivityView{
navController.navigate(ScreenRoute.Result.route)
}
}
composable(ScreenRoute.Result.route) {
ResultScreen()
}
}
}
ResultScreen.kt
#Composable
fun ResultScreen() {
CircularProgressIndicator()
}
please check my repository if you need more code. I added my github link above. Thanks
I can't see your code handling the Spinner. Anyway, a general idea to handle these kinda situations is
val state = remember{mutableStateOf<ResultFetchState>(ResultFetchState.EMPTY)}
if(state == ResultFetchState.LOADING){
//show spinner
Spinner()
}
...
state.value = viewModel.stateResultFetchState.collectAsState().value
I have debugged the app and I saw that the data in UIState changes when I try to add or remove the item, especially the isAdded field. However, even though the isAdded changes, the AddableItem does not recompose. Additionally, when I try to sort items, or try to write a query THAT WILL NOT SEND ANY API REQUEST, JUST CHANGES THE STRING IN TEXTFIELD, the UI recomposes. So UI reacts to changes in UIState. I have searched for similar issues but cannot find anything. I believe that the framework must recompose when the pointer of the filed changes, however, it does not. Any idea why this happens or solve that?
This is the viewModel:
#HiltViewModel
class AddableItemScreenViewModel#Inject constructor(
val getAddableItemsUseCase: GetItems,
val getItemsFromRoomUseCase: GetRoomItems,
val updateItemCase: UpdateItem,
savedStateHandle: SavedStateHandle) : ViewModel() {
private val _uiState = mutableStateOf(UIState())
val uiState: State<UIState> = _uiState
private val _title = mutableStateOf("")
val title: State<String> = _title
private var getItemsJob: Job? = null
init {
savedStateHandle.get<String>(NavigationConstants.TITLE)?.let { title ->
_title.value = title
}
savedStateHandle.get<Int>(NavigationConstants.ID)?.let { id ->
getItems(id = id.toString())
}
}
fun onEvent(event: ItemEvent) {
when(event) {
is ItemEvent.UpdateEvent -> {
val modelToUpdate = UpdateModel(
id = event.source.id,
isAdded = event.source.isAdded,
name = event.source.name,
index = event.source.index
)
updateUseCase(modelToUpdate).launchIn(viewModelScope)
}
is ItemEvent.QueryChangeEvent -> {
_uiState.value = _uiState.value.copy(
searchQuery = event.newQuery
)
}
is ItemEvent.SortEvent -> {
val curSortType = _uiState.value.sortType
_uiState.value = _uiState.value.copy(
sortType = if(curSortType == SortType.AS_IT_IS)
SortType.ALPHA_NUMERIC
else
SortType.AS_IT_IS
)
}
}
}
private fun getItems(id: String) {
getItemsJob?.cancel()
getItemsJob = getItemsUseCase(id)
.combine(
getItemsFromRoomUseCase()
){ itemsApiResult, roomData ->
when (itemsApiResult) {
is Resource.Success -> {
val data = itemsApiResult.data.toMutableList()
// Look the api result, if the item is added on room, make it added, else make it not added. This ensures API call is done once and every state change happens because of room.
for(i in data.indices) {
val source = data[i]
val itemInRoomData = roomData.find { it.id == source.id }
data[i] = data[i].copy(
isAdded = itemInRoomData != null
)
}
_uiState.value = _uiState.value.copy(
data = data,
isLoading = false,
error = "",
)
}
is Resource.Error -> {
_uiState.value = UIState(
data = emptyList(),
isLoading = false,
error = itemsApiResult.message,
)
}
is Resource.Loading -> {
_uiState.value = UIState(
data = emptyList(),
isLoading = true,
error = "",
)
}
}
}.launchIn(viewModelScope)
}
}
This it the composable:
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun AddableItemsScreen(
itemsViewModel: AddableItemScreenViewModel = hiltViewModel()
) {
val state = itemsViewModel.uiState.value
val controller = LocalNavigationManager.current
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
val mainScrollState = rememberLazyListState()
val focusRequester = remember { FocusRequester() }
// Screen UI
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.BackgroundColor)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
focusManager.clearFocus()
},
) {
LazyColumn(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
state = mainScrollState,
) {
item {
WhiteSpacer(
whiteSpacePx = 200,
direction = SpacerDirections.VERTICAL
)
}
if (state.isLoading) {
item {
ProgressIndicator()
}
}
if (state.error.isNotEmpty() && state.error.isNotBlank()) {
item {
ErrorText()
}
}
if (state.data.isNotEmpty()) {
val data = if (state.sortType == SortType.ALPHA_NUMERIC)
state.data.sortedBy { it.name }
else
state.data
data.forEach { source ->
if((state.searchQuery.isEmpty() && state.searchQuery.isBlank()) ||
(source.name != null && source.name.contains(state.searchQuery, ignoreCase = true))) {
item {
AddableItem(
modifier = Modifier
.padding(
vertical = dimManager.heightPxToDp(20)
),
text = source.name ?: "",
isAdded = source.isAdded ?: false,
onItemPressed = {
controller.navigate(
Screens.ItemPreviewScreen.route +
"?title=${source.name}" +
"&id=${source.categoryId}" +
"&isAdded=${source.isAdded}"
)
},
onAddPressed = {
itemsViewModel.onEvent(ItemEvent.UpdateEvent(source))
}
)
}
}
}
}
}
Column(
modifier = Modifier
.align(Alignment.TopStart)
.background(
MaterialTheme.colors.BackgroundColor
),
) {
ItemsScreenAppBar(
title = itemsViewModel.title.value,
onSortPressed = {
itemsViewModel.onEvent(ItemEvent.SortEvent)
}
) {
controller.popBackStack()
}
SearchBar(
query = state.searchQuery,
focusRequester = focusRequester,
placeholder = itemsViewModel.title.value,
onDeletePressed = {
itemsViewModel.onEvent(ItemEvent.QueryChangeEvent(""))
},
onValueChanged = {
itemsViewModel.onEvent(ItemEvent.QueryChangeEvent(it))
},
onSearch = {
keyboardController!!.hide()
}
)
WhiteSpacer(
whiteSpacePx = 4,
direction = SpacerDirections.VERTICAL
)
}
}
}
And finally this is the UIState:
data class UIState(
val data: List<ItemModel> = emptyList(),
val isLoading: Boolean = false,
val error: String = "",
val searchQuery: String = "",
val sortType: SortType = SortType.AS_IT_IS,
)
#Parcelize
data class ItemModel (
val id: Int? = null,
var isAdded: Boolean? = null,
val name: String? = null,
val index: Int? = null,
#SerializedName("someSerializedNameForApi")
var id: Int? = null
): Parcelable
Finally, I have a similar issue with almost the same viewModel with the same UI structure. The UI contains an Add All button and when everything is added, it turns to Remove All. I also hold the state of the button in UIState for that screen. When I try to add all items or remove all items, the UI recomposes. But when I try to add or remove a single item, the recomposition does not happen as same as the published code above. Additionally, when I remove one item when everything is added on that screen, the state of the button does change but stops to react when I try to add more. I can also share that code if you people want. I still do not understand why the UI recomposes when I try to sort or try to add-remove all on both screens but does not recompose when the data changes, even though I change the pointer address of the list.
Thanks for any help.
I could not believe that the answer can be so simple but here are the solutions:
For the posted screen, I just changed _uiState.value = _uiState.value.copy(...) to _uiState.value = UIState(...copy and replace everything with old value...) as
_uiState.value = UIState(
data = data,
isLoading = false,
error = "",
searchQuery = _uiState.value.searchQuery,
sortType = _uiState.value.sortType
)
For the second screen, I was just double changing the isAdded value by sending the data directly without copying. As the api call changes the isAdded value again, and the read from room flow changes it again, the state were changed twice.
However, I still wonder why compose didn't recompose when I changed the memory location of data in UIState.
I am developing an application in which I show a list of potatoes, retrieving the data from Firestore.
I have added a swipe action to refresh the data. With the code that I show below, the data is updating fine, the call is made to Firestore and it is updated showing new values in case they exist, or stopping showing values that no longer exist.
The problem is that when I swipe the potato list screen remains blank, empty, and when the call to Firestore ends, they are shown again. That is, there are a couple of seconds that the screen goes blank.
Is there a possibility that this will not happen? This effect is somewhat ugly
ViewModel:
#HiltViewModel
class PotatoesViewModel #Inject constructor(
private val getPotatoesDataUseCase: GetPotatoesData
) : ViewModel() {
private val _state = mutableStateOf(PotatoesState())
val state: State<PotatoesState> = _state
private val _isRefreshing = MutableStateFlow(false)
val isRefreshing: StateFlow<Boolean>
get() = _isRefreshing.asStateFlow()
init {
getPotatoes()
}
private fun getPotatoes() {
getPotatoesDataUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = PotatoesState(potatoes = result.data?.potatoes ?: emptyList())
}
is Resource.Error -> {
_state.value = PotatoesState(
error = result.message ?: "An unexpected error occurred"
)
}
is Resource.Loading -> {
_state.value = PotatoesState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
fun refresh() {
viewModelScope.launch {
_isRefreshing.emit(true)
getIncidents()
_isRefreshing.emit(false)
}
}
}
Screen:
#Composable
fun PotatoesDataScreen(
navController: NavController,
viewModel: PotatoesViewModel = hiltViewModel()
) {
val state = viewModel.state.value
val isRefreshing by viewModel.isRefreshing.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
stringResource(R.string.app_name),
fontWeight = FontWeight.Bold
)
},
backgroundColor = Primary,
contentColor = Color.White
)
},
content = {
Box(modifier = Modifier.fillMaxSize()) {
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = { viewModel.refresh() }
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 8.dp)
) {
items(state.potatoes) { potato ->
PotatoCard(
potato = potato
)
}
}
}
}
}
)
}
PotatoState:
data class PotatoesState(
val isLoading: Boolean = false,
val potatoes: List<Potato> = emptyList(),
val error: String = ""
)
When the list screen is blank, this is the time when the Api call is made.
When your call is made and response is still not received, this is also when the list is blank.
You pass a new Object of PotatoesState to the mutableState every time you:
receive a response,
get an error, (with Potatoes = emptyList())
or state is loading. (with Potatoes = emptyList())
UI is updated according to the MutableState you named _state.
If you want to keep the same data until you get a new response, then you need to update the current state.value: MutableState<PotatoesState> object only when you get a new response (AKA, is Resource.success).
Alternatively, you can implement a Loading Spinner, and show it when you start your Api Request, until isLoading is false.
EDIT: Addition of suggested code.
So that how I would declare PotatoesState class:
class PotatoesState(
var isLoading: Boolean = false,
var potatoes: List<Potato> = emptyList(),
var error: String = ""
)
And that's what I would write in the CallBack function:
when (result) {
is Resource.Success -> {
if (result.data != null) {
_state.value = mutableStateOf().apply {
addAll(_state.value.apply { it ->
it.potatoes = result.data!!.potatoes
it.isLoading = false
it.error = ""
})
}
}
}
is Resource.Error -> {
_state.value = mutableStateOf().apply {
addAll(_state.value.apply { it ->
it.error =
result.message ?:
"An unexpected error occurred"
it.isLoading = false
})
}
}
is Resource.Loading -> {
_state.value = mutableStateOf().apply {
addAll(_state.value.apply { it ->
it.isLoading = true
})
}
}
}
I am trying to do pagination in my application. First, I'm fetching 20 item from Api (limit) and every time i scroll down to the bottom of the screen, it increase this number by 20 (nextPage()). However, when this function is called, the screen goes to the top, but I want it to continue where it left off. How can I do that?
Here is my code:
CharacterListScreen:
#Composable
fun CharacterListScreen(
characterListViewModel: CharacterListViewModel = hiltViewModel()
) {
val state = characterListViewModel.state.value
val limit = characterListViewModel.limit.value
Box(modifier = Modifier.fillMaxSize()) {
val listState = rememberLazyListState()
LazyColumn(modifier = Modifier.fillMaxSize(), state = listState) {
itemsIndexed(state.characters) { index, character ->
characterListViewModel.onChangeRecipeScrollPosition(index)
if ((index + 1) >= limit) {
characterListViewModel.nextPage()
}
CharacterListItem(character = character)
}
}
if (state.error.isNotBlank()) {
Text(
text = state.error,
color = MaterialTheme.colors.error,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.align(Alignment.Center)
)
}
if (state.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
}
CharacterListViewModel:
#HiltViewModel
class CharacterListViewModel #Inject constructor(
private val characterRepository: CharacterRepository
) : ViewModel() {
val state = mutableStateOf(CharacterListState())
val limit = mutableStateOf(20)
var recipeListScrollPosition = 0
init {
getCharacters(limit.value, Constants.HEADER)
}
private fun getCharacters(limit : Int, header : String) {
characterRepository.getCharacters(limit, header).onEach { result ->
when(result) {
is Resource.Success -> {
state.value = CharacterListState(characters = result.data ?: emptyList())
}
is Resource.Error -> {
state.value = CharacterListState(error = result.message ?: "Unexpected Error")
}
is Resource.Loading -> {
state.value = CharacterListState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
private fun incrementLimit() {
limit.value = limit.value + 20
}
fun onChangeRecipeScrollPosition(position: Int){
recipeListScrollPosition = position
}
fun nextPage() {
if((recipeListScrollPosition + 1) >= limit.value) {
incrementLimit()
characterRepository.getCharacters(limit.value, Constants.HEADER).onEach {result ->
when(result) {
is Resource.Success -> {
state.value = CharacterListState(characters = result.data ?: emptyList())
}
is Resource.Error -> {
state.value = CharacterListState(error = result.message ?: "Unexpected Error")
}
is Resource.Loading -> {
state.value = CharacterListState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
}
}
CharacterListState:
data class CharacterListState(
val isLoading : Boolean = false,
var characters : List<Character> = emptyList(),
val error : String = ""
)
I think the issue here is that you are creating CharacterListState(isLoading = true) while loading. This creates an object with empty list of elements. So compose renders an empty LazyColumn here which resets the scroll state. The easy solution for that could be state.value = state.value.copy(isLoading = true). Then, while loading, the item list can be preserved (and so is the scroll state)
Not sure if you are using the LazyListState correctly. In your viewmodel, create an instance of LazyListState:
val lazyListState: LazyListState = LazyListState()
Pass that into your composable and use it as follows:
#Composable
fun CharacterListScreen(
characterListViewModel: CharacterListViewModel = hiltViewModel()
) {
val limit = characterListViewModel.limit.value
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxSize(), state = characterListViewModel.lazyListState) {
itemsIndexed(state.characters) { index, character ->
}
}
}
}