Android Wear paging improvements

pull/3070/head
Alex Baker 1 year ago
parent dd6ba730e9
commit a8977f02fe

@ -35,6 +35,7 @@ class WearService(
completedAtBottom = preferences.completedTasksAtBottom, completedAtBottom = preferences.completedTasksAtBottom,
) )
return Tasks.newBuilder() return Tasks.newBuilder()
.setTotalItems(payload.size)
.addAllItems( .addAllItems(
payload payload
.subList(position, position + limit) .subList(position, position + limit)
@ -42,6 +43,7 @@ class WearService(
when (item) { when (item) {
is UiItem.Header -> is UiItem.Header ->
GrpcProto.UiItem.newBuilder() GrpcProto.UiItem.newBuilder()
.setId(item.value)
.setType(GrpcProto.UiItemType.Header) .setType(GrpcProto.UiItemType.Header)
.setTitle(headerFormatter.headerString(item.value)) .setTitle(headerFormatter.headerString(item.value))
.build() .build()

@ -20,7 +20,8 @@ message UiItem {
} }
message Tasks { message Tasks {
repeated UiItem items = 1; uint32 totalItems = 1;
repeated UiItem items = 2;
} }
message LastUpdate { message LastUpdate {

@ -51,15 +51,16 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.background(MaterialTheme.colors.background), modifier = Modifier.background(MaterialTheme.colors.background),
) { ) {
val navController = rememberSwipeDismissableNavController() val navController = rememberSwipeDismissableNavController()
val taskListViewModel: TaskListViewModel = viewModel()
val taskListItems = taskListViewModel.uiItems.collectAsLazyPagingItems()
SwipeDismissableNavHost( SwipeDismissableNavHost(
startDestination = "task_list", startDestination = "task_list",
navController = navController, navController = navController,
) { ) {
composable("task_list") { navBackStackEntry -> composable("task_list") {
val viewModel: TaskListViewModel = viewModel(navBackStackEntry)
TaskListScreen( TaskListScreen(
uiItems = viewModel.uiItems.collectAsLazyPagingItems(), uiItems = taskListItems,
onComplete = { viewModel.completeTask(it) }, onComplete = { taskListViewModel.completeTask(it) },
onClick = { navController.navigate("task_edit/$it") }, onClick = { navController.navigate("task_edit/$it") },
) )
} }

@ -1,36 +1,85 @@
package org.tasks.presentation package org.tasks.presentation
import android.util.Log
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.paging.PagingSource.LoadParams
import androidx.paging.PagingSource.LoadParams.Append
import androidx.paging.PagingSource.LoadParams.Prepend
import androidx.paging.PagingSource.LoadParams.Refresh
import androidx.paging.PagingState import androidx.paging.PagingState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext private const val INITIAL_ITEM_COUNT = -1
class MyPagingSource<T : Any>( class MyPagingSource<T : Any>(
private val fetch: suspend (position: Int, limit: Int) -> List<T>?, private val fetch: suspend (position: Int, limit: Int) -> Pair<Int, List<T>>,
) : PagingSource<Int, T>() { ) : PagingSource<Int, T>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> { private var itemCount = INITIAL_ITEM_COUNT
val position = params.key ?: 0
val limit = params.loadSize
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
return try { return try {
val items = withContext (Dispatchers.IO) { val key = params.key ?: 0
fetch(position, limit) ?: emptyList() val limit = getLimit(params, key)
val offset = getOffset(params, key, itemCount)
val (newCount, data) = fetch(offset, limit)
if (itemCount == INITIAL_ITEM_COUNT) {
itemCount = newCount
} }
val nextPosToLoad = offset + data.size
LoadResult.Page( val nextKey =
data = items, if (data.isEmpty() || data.size < limit || nextPosToLoad >= itemCount) {
prevKey = if (position <= 0) null else position - limit, null
nextKey = if (items.isEmpty()) null else position + limit } else {
nextPosToLoad
}
val prevKey = if (offset <= 0 || data.isEmpty()) null else offset
return LoadResult.Page(
data = data,
prevKey = prevKey,
nextKey = nextKey,
itemsBefore = offset,
itemsAfter = maxOf(0, itemCount - nextPosToLoad)
) )
} catch (e: Exception) { } catch (e: Exception) {
Log.e("MyPagingSource", "${e.message}\n${e.stackTrace}")
LoadResult.Error(e) LoadResult.Error(e)
} }
} }
override fun getRefreshKey(state: PagingState<Int, T>): Int { override fun getRefreshKey(state: PagingState<Int, T>): Int? = state.getClippedRefreshKey()
return ((state.anchorPosition ?: 0) - state.config.initialLoadSize / 2).coerceAtLeast(0) }
private fun <Value : Any> PagingState<Int, Value>.getClippedRefreshKey(): Int? {
return when (val anchorPosition = anchorPosition) {
null -> null
else -> maxOf(0, anchorPosition - (config.initialLoadSize / 2))
}
}
fun getLimit(params: LoadParams<Int>, key: Int): Int {
return when (params) {
is Prepend ->
if (key < params.loadSize) {
key
} else {
params.loadSize
}
else -> params.loadSize
}
}
fun getOffset(params: LoadParams<Int>, key: Int, itemCount: Int): Int {
return when (params) {
is Prepend ->
if (key < params.loadSize) {
0
} else {
key - params.loadSize
}
is Append -> key
is Refresh ->
if (itemCount != INITIAL_ITEM_COUNT && key >= itemCount) {
maxOf(0, itemCount - params.loadSize)
} else {
key
}
} }
} }

@ -10,7 +10,6 @@ import androidx.compose.material.icons.outlined.CheckBox
import androidx.compose.material.icons.outlined.CheckBoxOutlineBlank import androidx.compose.material.icons.outlined.CheckBoxOutlineBlank
import androidx.compose.material.icons.outlined.Repeat import androidx.compose.material.icons.outlined.Repeat
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -28,6 +27,7 @@ import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.compose.layout.ScalingLazyColumn import com.google.android.horologist.compose.layout.ScalingLazyColumn
import com.google.android.horologist.compose.layout.ScreenScaffold import com.google.android.horologist.compose.layout.ScreenScaffold
import com.google.android.horologist.compose.layout.rememberResponsiveColumnState import com.google.android.horologist.compose.layout.rememberResponsiveColumnState
import com.google.android.horologist.compose.paging.items
import org.tasks.GrpcProto import org.tasks.GrpcProto
import org.tasks.kmp.org.tasks.themes.ColorProvider import org.tasks.kmp.org.tasks.themes.ColorProvider
@ -41,19 +41,28 @@ fun TaskListScreen(
val columnState = rememberResponsiveColumnState() val columnState = rememberResponsiveColumnState()
ScreenScaffold( ScreenScaffold(
scrollState = columnState, scrollState = columnState,
positionIndicator = {},
) { ) {
ScalingLazyColumn( ScalingLazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
columnState = columnState, columnState = columnState,
) { ) {
items(uiItems.itemCount) { index -> items(
val item = uiItems[index] ?: return@items items = uiItems,
key(item.id) { key = { item -> "${item.type}_${item.id}" },
) { item ->
if (item == null) {
TaskCard(
task = GrpcProto.UiItem.getDefaultInstance(),
showCheckbox = false,
onComplete = {},
onClick = {},
)
} else {
when (item.type) { when (item.type) {
GrpcProto.UiItemType.Task -> GrpcProto.UiItemType.Task ->
TaskCard( TaskCard(
task = item, task = item,
showCheckbox = true,
onComplete = { onComplete(item.id) }, onComplete = { onComplete(item.id) },
onClick = { onClick(item.id) }, onClick = { onClick(item.id) },
) )
@ -87,6 +96,7 @@ fun GroupSeparator(
@Composable @Composable
fun TaskCard( fun TaskCard(
task: GrpcProto.UiItem, task: GrpcProto.UiItem,
showCheckbox: Boolean,
onComplete: () -> Unit, onComplete: () -> Unit,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
@ -98,6 +108,7 @@ fun TaskCard(
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
if (showCheckbox) {
Button( Button(
onClick = onComplete, onClick = onComplete,
colors = ButtonDefaults.iconButtonColors(), colors = ButtonDefaults.iconButtonColors(),
@ -118,6 +129,7 @@ fun TaskCard(
contentDescription = null, contentDescription = null,
) )
} }
}
Text( Text(
text = task.title, text = task.title,
modifier = Modifier.padding(vertical = 12.dp) modifier = Modifier.padding(vertical = 12.dp)

@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
import androidx.paging.Pager import androidx.paging.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.data.ProtoDataStoreHelper.protoFlow import com.google.android.horologist.data.ProtoDataStoreHelper.protoFlow
import com.google.android.horologist.data.TargetNodeId import com.google.android.horologist.data.TargetNodeId
@ -40,12 +41,14 @@ class TaskListViewModel(
.setLimit(limit) .setLimit(limit)
.build() .build()
) )
.itemsList .let { Pair(it.totalItems, it.itemsList) }
} }
.also { pagingSource = it } .also { pagingSource = it }
} }
) )
.flow .flow
.cachedIn(viewModelScope)
private val wearDataLayerRegistry = WearDataLayerRegistry.fromContext( private val wearDataLayerRegistry = WearDataLayerRegistry.fromContext(
application = application, application = application,
coroutineScope = viewModelScope, coroutineScope = viewModelScope,

Loading…
Cancel
Save