I have a LazyVerticalGrid with 2 cells.
LazyVerticalGrid(
cells = GridCells.Fixed(2),
content = {
items(moviePagingItems.itemCount) { index ->
val movie = moviePagingItems[index] ?: return#items
MovieItem(movie, Modifier.preferredHeight(320.dp))
}
renderLoading(moviePagingItems.loadState)
}
)
I am trying to show full width loading with LazyGridScope's fillParentMaxSize modifier.
fun LazyGridScope.renderLoading(loadState: CombinedLoadStates) {
when {
loadState.refresh is LoadState.Loading -> {
item {
LoadingColumn("Fetching movies", Modifier.fillParentMaxSize())
}
}
loadState.append is LoadState.Loading -> {
item {
LoadingRow(title = "Fetching more movies")
}
}
}
}
But since we have 2 cells, the loading can occupy half of the screen. Like this:
Is there a way my loading view can occupy full width?
Jetpack Compose 1.1.0-beta03 version includes horizontal span support for LazyVerticalGrid.
Here is the example usage:
private const val CELL_COUNT = 2
private val span: (LazyGridItemSpanScope) -> GridItemSpan = { GridItemSpan(CELL_COUNT) }
LazyVerticalGrid(
cells = GridCells.Fixed(CELL_COUNT),
content = {
items(moviePagingItems.itemCount) { index ->
val movie = moviePagingItems.peek(index) ?: return#items
Movie(movie)
}
renderLoading(moviePagingItems.loadState)
}
}
private fun LazyGridScope.renderLoading(loadState: CombinedLoadStates) {
if (loadState.append !is LoadState.Loading) return
item(span = span) {
val title = stringResource(R.string.fetching_more_movies)
LoadingRow(title = title)
}
}
Code examples of this answer can be found at: Jetflix/MoviesGrid.kt
LazyVerticalGrid has a span strategy built into items() and itemsIndexed()
#Composable
fun SpanLazyVerticalGrid(cols: Int, itemList: List<String>) {
val lazyGridState = rememberLazyGridState()
LazyVerticalGrid(
columns = GridCells.Fixed(cols),
state = lazyGridState
) {
items(itemList, span = { item ->
val lowercase = item.lowercase()
val span = if (lowercase.startsWith("a") || lowercase.lowercase().startsWith("b") || lowercase.lowercase().startsWith("d")) {
cols
}
else {
1
}
GridItemSpan(span)
}) { item ->
Box(modifier = Modifier
.fillMaxWidth()
.height(150.dp)
.padding(10.dp)
.background(Color.Black)
.padding(2.dp)
.background(Color.White)
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = item,
fontSize = 18.sp
)
}
}
}
}
'
val names = listOf("Alice", "Bob", "Cindy", "Doug", "Ernie", "Fred", "George", "Harris")
SpanLazyVerticalGrid(
cols = 3,
itemList = names
)
Try something like:
var cellState by remember { mutableStateOf(2) }
LazyVerticalGrid(
cells = GridCells.Fixed(cellState),
content = {
items(moviePagingItems.itemCount) { index ->
val movie = moviePagingItems[index] ?: return#items
MovieItem(movie, Modifier.preferredHeight(320.dp))
}
renderLoading(moviePagingItems.loadState) {
cellState = it
}
}
)
The renderLoading function:
fun LazyGridScope.renderLoading(loadState: CombinedLoadStates, span: (Int) -> Unit) {
when {
loadState.refresh is LoadState.Loading -> {
item {
LoadingColumn("Fetching movies", Modifier.fillParentMaxSize())
}
span(1)
}
...
else -> span(2)
}
}
I have created an issue for it: https://issuetracker.google.com/u/1/issues/176758183
Current workaround I have is to use LazyColumn and implement items or header.
override val content: #Composable () -> Unit = {
LazyColumn(
contentPadding = PaddingValues(8.dp),
content = {
items(colors.chunked(3), itemContent = {
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
val modifier = Modifier.weight(1f)
it.forEach {
ColorItem(modifier, it)
}
for (i in 1..(3 - it.size)) {
Spacer(modifier)
}
}
})
item {
Text(
text = stringResource(R.string.themed_colors),
style = MaterialTheme.typography.h3
)
}
items(themedColors.chunked(3), itemContent = {
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
val modifier = Modifier.weight(1f)
it.forEach {
ColorItem(modifier, it)
}
for (i in 1..(3 - it.size)) {
Spacer(modifier)
}
}
})
})
}
Related
I have implemented search functionality in my app which display result as a verticalGridView with pagination : https://github.com/alirezaeiii/TMDb-Compose
I have following logic for refresh load state that works as I wish :
#Composable
fun <T : TMDbItem> PagingScreen(
viewModel: BasePagingViewModel<T>,
onClick: (TMDbItem) -> Unit,
) {
val lazyTMDbItems = viewModel.pagingDataFlow.collectAsLazyPagingItems()
when (lazyTMDbItems.loadState.refresh) {
is LoadState.Loading -> {
TMDbProgressBar()
}
is LoadState.Error -> {
val message =
(lazyTMDbItems.loadState.refresh as? LoadState.Error)?.error?.message ?: return
lazyTMDbItems.apply {
ErrorScreen(
message = message,
modifier = Modifier.fillMaxSize(),
refresh = { retry() }
)
}
}
else -> {
LazyTMDbItemGrid(lazyTMDbItems, onClick)
}
}
}
In LazyTMDbItemGrid, I try to manage append load state as follow :
#Composable
private fun <T : TMDbItem> LazyTMDbItemGrid(
lazyTMDbItems: LazyPagingItems<T>,
onClick: (TMDbItem) -> Unit,
) {
LazyVerticalGrid(
columns = GridCells.Fixed(COLUMN_COUNT),
contentPadding = PaddingValues(
start = Dimens.GridSpacing,
end = Dimens.GridSpacing,
bottom = WindowInsets.navigationBars.getBottom(LocalDensity.current)
.toDp().dp.plus(
Dimens.GridSpacing
)
),
horizontalArrangement = Arrangement.spacedBy(
Dimens.GridSpacing,
Alignment.CenterHorizontally
),
content = {
repeat(COLUMN_COUNT) {
item {
Spacer(
Modifier.windowInsetsTopHeight(
WindowInsets.statusBars.add(WindowInsets(top = 56.dp))
)
)
}
}
items(lazyTMDbItems.itemCount) { index ->
val tmdbItem = lazyTMDbItems[index]
tmdbItem?.let {
TMDbItemContent(
it,
Modifier
.height(320.dp)
.padding(vertical = Dimens.GridSpacing),
onClick
)
}
}
lazyTMDbItems.apply {
when (loadState.append) {
is LoadState.Loading -> {
item(span = span) {
LoadingRow(modifier = Modifier.padding(vertical = Dimens.GridSpacing))
}
}
is LoadState.Error -> {
val message =
(loadState.append as? LoadState.Error)?.error?.message ?: return#apply
item(span = span) {
ErrorScreen(
message = message,
modifier = Modifier.padding(vertical = Dimens.GridSpacing),
refresh = { retry() })
}
}
else -> {}
}
}
})
}
The problem is when there is no result for search, or when result items is shorter than screen size, it displays LoadingRow. My expectation is when we are in this state, LoadingRow does not display, but how can I detect this state?
Correct me if I'm wrong but these should be dictated by the PagingSource.LoadResult.Page
Documentation :
Success result object for PagingSource.load. Params: data - Loaded
data prevKey - Key for previous page if more data can be loaded in
that direction, null otherwise. nextKey - Key for next page if more
data can be loaded in that direction, null otherwise.
So if you reached the pagination end (in either direction) :
PagingSource.LoadResult.Page(
data = loadedData,
prevKey = null,
nextKey = null)
I'm currently trying to show a list of EventType (custom class) objects as DropdownMenuItems in a DropdownMenu.
The code I'm trying is:
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
items(plannerViewModel.eventTypeList) { eventType ->
DropdownMenuItem(onClick = { /*TODO*/ }) {
TypeOfEvent(eventType.color, eventType.name, openDialog)
}
}
}
The problem is the items() function is not being recognized and I don't know how else it could be done.
items is for LazyColumn. It doesn't exist on DropdownMenu.
You can just use a for loop in this case
plannerViewModel.eventTypeList.forEach { eventType ->
DropdownMenuItem(...)
}
Below is syntax of Drop Down menu.There is no scope to add items in syntax.
#Composable
fun DropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(0.dp, 0.dp),
properties: PopupProperties = PopupProperties(focusable = true),
content: #Composable #ExtensionFunctionType ColumnScope.() -> Unit
): Unit
If you want to add multiple items than you can use for loop like below.
#Preview
#Composable
fun DropdownDemo() {
var expanded by remember { mutableStateOf(false) }
val items = listOf("A", "B", "C", "D", "E", "F")
val disabledValue = "B"
var selectedIndex by remember { mutableStateOf(0) }
Box(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) {
Text(items[selectedIndex],modifier = Modifier.fillMaxWidth().clickable(onClick = { expanded = true }).background(
Color.Gray))
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier.fillMaxWidth().background(
Color.Red)
) {
items.forEachIndexed { index, s ->
DropdownMenuItem(onClick = {
selectedIndex = index
expanded = false
}) {
val disabledText = if (s == disabledValue) {
" (Disabled)"
} else {
""
}
Text(text = s + disabledText)
}
}
}
}
}
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 ->
}
}
}
}
I'm new to Jetpack Compose. I am currently developing a chat application. I ask the user to choose an image from the gallery or take a picture from the camera. Then I save the file Uri to the database and then listen to the list of all messages. When this list is updated, this image is recomposing and it flashes.
Messages list in viewmodel:
private var _messages = MutableStateFlow<List<ChatUiMessage>>(mutableListOf())
val messages: StateFlow<List<ChatUiMessage>> = _messages
...
private fun observeMessages() {
viewModelScope.launch {
chatManager.observeMessagesFlow()
.flowOn(dispatcherIO)
.collect {
_messages.emit(it)
}
}
}
Main chat screen:
...
val messages by viewModel.messages.collectAsState(listOf())
...
val listState = rememberLazyListState()
LazyColumn(
modifier = modifier.fillMaxWidth(),
reverseLayout = true,
state = listState
) {
itemsIndexed(items = messages) { index, message ->
when (message) {
...
is ChatUiMessage.Image -> SentImageBlock(
message = message
)
...
}
}
}
My SentImageBlock:
#Composable
private fun SentImageBlock(message: ChatUiMessage.Image) {
val context = LocalContext.current
val bitmap: MutableState<Bitmap?> = rememberSaveable { mutableStateOf(null) }
bitmap.value ?: run {
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
bitmap.value = try {
when {
Build.VERSION.SDK_INT >= 28 -> {
val source = ImageDecoder.createSource(context.contentResolver, message.fileUriPath.toUri())
ImageDecoder.decodeBitmap(source)
}
else -> {
MediaStore.Images.Media.getBitmap(context.contentResolver, message.fileUriPath.toUri())
}
}
} catch (e: Exception) {
null
}
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.padding(end = 16.dp, top = 16.dp, bottom = 16.dp)
.heightIn(max = 200.dp, min = 200.dp)
) {
bitmap.value?.let {
Image(
bitmap = it.asImageBitmap(),
contentDescription = null,
modifier = Modifier
.wrapContentSize()
.align(Alignment.CenterEnd)
)
}
}
StandardText(text = message.sendFileStatus.toString())
StandardText(text = message.fileType.toString())
}
I have tried several things and either is the Image always blinking.
LazyColumn reuses views for items using key argument, and by default it's equal to item index. You can provide a correct key(something like message id) for views to be reused correctly:
val messages = listOf(1,2,3)
LazyColumn(
modifier = modifier.fillMaxWidth(),
reverseLayout = true,
state = listState
) {
itemsIndexed(
items = messages,
key = { index, message -> message.id }
) { index, message ->
when (message) {
...
is ChatUiMessage.Image -> SentImageBlock(
message = message
)
...
}
}
}
When make an xml endless list need create RecyclerView and add RecyclerViewOnScrollListener. How to do it in Jetpack Compose?
You can use androidx.ui.foundation.AdapterList for this.
It will only composes and lays out the currently visible items.
#Composable
fun CustomerListView(list: List<Customer>) {
AdapterList(data = list) { customer->
Text("name:${customer.name}")
}
}
You could use LazyColumnFor like explained here:
#Composable
fun LazyColumnForDemo() {
LazyColumnFor(items = listOf(
"A", "B", "C", "D"
) + ((0..100).map { it.toString() }),
modifier = Modifier,
itemContent = { item ->
Log.d("COMPOSE", "This get rendered $item")
when (item) {
"A" -> {
Text(text = item, style = TextStyle(fontSize = 80.sp))
}
"B" -> {
Button(onClick = {}) {
Text(text = item, style = TextStyle(fontSize = 80.sp))
}
}
"C" -> {
//Do Nothing
}
"D" -> {
Text(text = item)
}
else -> {
Text(text = item, style = TextStyle(fontSize = 80.sp))
}
}
})
}
Explained by google here: https://youtu.be/SMOhl9RK0BA 23:18
Similar to endless list in RecyclerView with some tunes:
#Parcelize
data class PagingController(
var loading: Boolean = false,
var itemsFromEndToLoad: Int = 5,
var lastLoadedItemsCount: Int = 0,
) : Parcelable {
fun reset() {
loading = false
itemsFromEndToLoad = 5
lastLoadedItemsCount = 0
}
}
#Composable
fun LazyGridState.endlessOnScrollListener(
pagingController: PagingController,
itemsCount: Int, // provide real collection size to not have collisions if list contains another view types
loadMore: () -> Unit
) {
if (!isScrollInProgress) return
val lastVisiblePosition = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
pagingController.run {
if (loading) {
if (itemsCount > lastLoadedItemsCount) {
loading = false
Timber.v("loaded, lastVisiblePosition: $lastVisiblePosition, itemsCount: $itemsCount")
lastLoadedItemsCount = itemsCount
}
} else {
if (itemsCount < lastLoadedItemsCount) {
Timber.v("list reset")
reset()
}
val needToLoad = lastVisiblePosition + itemsFromEndToLoad >= itemsCount
if (needToLoad) {
Timber.v("loading, lastVisiblePosition: $lastVisiblePosition, itemsCount: $itemsCount")
loading = true
loadMore()
}
}
}
}
val gridState = rememberLazyGridState()
LazyVerticalGrid(
...
state = gridState,
) { ...
}
val pagingController by rememberSaveable { mutableStateOf(PagingController()) }
gridState.endlessOnScrollListener(pagingController, dataList.size) {
... // loadNextPage
}