Use paged data on Android Wear

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

@ -22,6 +22,8 @@ class WearService(
private val headerFormatter: HeaderFormatter, private val headerFormatter: HeaderFormatter,
) : WearServiceGrpcKt.WearServiceCoroutineImplBase() { ) : WearServiceGrpcKt.WearServiceCoroutineImplBase() {
override suspend fun getTasks(request: GetTasksRequest): Tasks { override suspend fun getTasks(request: GetTasksRequest): Tasks {
val position = request.position
val limit = request.limit.takeIf { it > 0 } ?: Int.MAX_VALUE
val filter = MyTasksFilter.create() val filter = MyTasksFilter.create()
val payload = SectionedDataSource( val payload = SectionedDataSource(
tasks = taskDao.fetchTasks(preferences, filter), tasks = taskDao.fetchTasks(preferences, filter),
@ -34,13 +36,16 @@ class WearService(
) )
return Tasks.newBuilder() return Tasks.newBuilder()
.addAllItems( .addAllItems(
payload.map { item -> payload
.subList(position, position + limit)
.map { item ->
when (item) { when (item) {
is UiItem.Header -> is UiItem.Header ->
GrpcProto.UiItem.newBuilder() GrpcProto.UiItem.newBuilder()
.setType(GrpcProto.UiItemType.Header) .setType(GrpcProto.UiItemType.Header)
.setTitle(headerFormatter.headerString(item.value)) .setTitle(headerFormatter.headerString(item.value))
.build() .build()
is UiItem.Task -> is UiItem.Task ->
GrpcProto.UiItem.newBuilder() GrpcProto.UiItem.newBuilder()
.setType(GrpcProto.UiItemType.Task) .setType(GrpcProto.UiItemType.Task)

@ -87,6 +87,7 @@ androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-ru
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycle" } androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
androidx-paging-compose = { module = "androidx.paging:paging-compose", version = "3.3.2" }
androidx-preference = { module = "androidx.preference:preference", version.ref = "preference" } androidx-preference = { module = "androidx.preference:preference", version.ref = "preference" }
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
androidx-room = { module = "androidx.room:room-runtime", version.ref = "room" } androidx-room = { module = "androidx.room:room-runtime", version.ref = "room" }

@ -73,7 +73,7 @@ class SectionedDataSource(
} }
override fun subList(fromIndex: Int, toIndex: Int): List<UiItem> { override fun subList(fromIndex: Int, toIndex: Int): List<UiItem> {
TODO("Not yet implemented") return iterator().asSequence().drop(fromIndex).take(toIndex - fromIndex).toList()
} }
override fun lastIndexOf(element: UiItem): Int { override fun lastIndexOf(element: UiItem): Int {

@ -27,7 +27,10 @@ message LastUpdate {
uint64 now = 1; uint64 now = 1;
} }
message GetTasksRequest {} message GetTasksRequest {
uint32 position = 1;
uint32 limit = 2;
}
message CompleteTaskRequest { message CompleteTaskRequest {
uint64 id = 1; uint64 id = 1;
bool completed = 2; bool completed = 2;

@ -46,6 +46,7 @@ dependencies {
implementation(projects.kmp) implementation(projects.kmp)
implementation(libs.play.services.wearable) implementation(libs.play.services.wearable)
implementation(platform(libs.androidx.compose)) implementation(platform(libs.androidx.compose))
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.ui) implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.ui.tooling.preview)

@ -19,10 +19,10 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.navArgument import androidx.navigation.navArgument
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.wear.compose.material.MaterialTheme import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text import androidx.wear.compose.material.Text
import androidx.wear.compose.material.TimeText import androidx.wear.compose.material.TimeText
@ -57,9 +57,8 @@ class MainActivity : ComponentActivity() {
) { ) {
composable("task_list") { navBackStackEntry -> composable("task_list") { navBackStackEntry ->
val viewModel: TaskListViewModel = viewModel(navBackStackEntry) val viewModel: TaskListViewModel = viewModel(navBackStackEntry)
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
TaskListScreen( TaskListScreen(
uiItems = uiState.tasks.itemsList, uiItems = viewModel.uiItems.collectAsLazyPagingItems(),
onComplete = { viewModel.completeTask(it) }, onComplete = { viewModel.completeTask(it) },
onClick = { navController.navigate("task_edit/$it") }, onClick = { navController.navigate("task_edit/$it") },
) )

@ -0,0 +1,36 @@
package org.tasks.presentation
import android.util.Log
import androidx.paging.PagingSource
import androidx.paging.PagingState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class MyPagingSource<T : Any>(
private val fetch: suspend (position: Int, limit: Int) -> List<T>?,
) : PagingSource<Int, T>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
val position = params.key ?: 0
val limit = params.loadSize
return try {
val items = withContext (Dispatchers.IO) {
fetch(position, limit) ?: emptyList()
}
LoadResult.Page(
data = items,
prevKey = if (position <= 0) null else position - limit,
nextKey = if (items.isEmpty()) null else position + limit
)
} catch (e: Exception) {
Log.e("MyPagingSource", "${e.message}\n${e.stackTrace}")
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, T>): Int {
return ((state.anchorPosition ?: 0) - state.config.initialLoadSize / 2).coerceAtLeast(0)
}
}

@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems
import androidx.wear.compose.material.Button import androidx.wear.compose.material.Button
import androidx.wear.compose.material.ButtonDefaults import androidx.wear.compose.material.ButtonDefaults
import androidx.wear.compose.material.Card import androidx.wear.compose.material.Card
@ -33,18 +34,21 @@ import org.tasks.kmp.org.tasks.themes.ColorProvider
@OptIn(ExperimentalHorologistApi::class) @OptIn(ExperimentalHorologistApi::class)
@Composable @Composable
fun TaskListScreen( fun TaskListScreen(
uiItems: List<GrpcProto.UiItem>, uiItems: LazyPagingItems<GrpcProto.UiItem>,
onComplete: (Long) -> Unit, onComplete: (Long) -> Unit,
onClick: (Long) -> Unit, onClick: (Long) -> Unit,
) { ) {
val columnState = rememberResponsiveColumnState() val columnState = rememberResponsiveColumnState()
ScreenScaffold(scrollState = columnState) { ScreenScaffold(
scrollState = columnState,
positionIndicator = {},
) {
ScalingLazyColumn( ScalingLazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
columnState = columnState, columnState = columnState,
) { ) {
items(uiItems.size) { index -> items(uiItems.itemCount) { index ->
val item = uiItems[index] val item = uiItems[index] ?: return@items
key(item.id) { key(item.id) {
when (item.type) { when (item.type) {
GrpcProto.UiItemType.Task -> GrpcProto.UiItemType.Task ->

@ -3,56 +3,66 @@ package org.tasks.presentation.screens
import android.app.Application import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
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
import com.google.android.horologist.data.WearDataLayerRegistry import com.google.android.horologist.data.WearDataLayerRegistry
import com.google.android.horologist.datalayer.grpc.GrpcExtensions.grpcClient import com.google.android.horologist.datalayer.grpc.GrpcExtensions.grpcClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.GrpcProto import org.tasks.GrpcProto
import org.tasks.GrpcProto.LastUpdate import org.tasks.GrpcProto.LastUpdate
import org.tasks.GrpcProto.Tasks import org.tasks.GrpcProto.UiItem
import org.tasks.WearServiceGrpcKt import org.tasks.WearServiceGrpcKt
import org.tasks.presentation.MyPagingSource
import org.tasks.wear.LastUpdateSerializer import org.tasks.wear.LastUpdateSerializer
data class TaskListScreenState(
val error: String? = null,
val tasks: Tasks = Tasks.getDefaultInstance(),
)
@OptIn(ExperimentalHorologistApi::class) @OptIn(ExperimentalHorologistApi::class)
class TaskListViewModel( class TaskListViewModel(
application: Application application: Application
) : AndroidViewModel(application) { ) : AndroidViewModel(application) {
val uiState: MutableStateFlow<TaskListScreenState> = MutableStateFlow(TaskListScreenState()) private var pagingSource: MyPagingSource<UiItem>? = null
private val scope = CoroutineScope(Dispatchers.IO) val uiItems: Flow<PagingData<UiItem>> = Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = {
MyPagingSource { position, limit ->
wearService
.getTasks(
GrpcProto
.GetTasksRequest
.newBuilder()
.setPosition(position)
.setLimit(limit)
.build()
)
.itemsList
}
.also { pagingSource = it }
}
)
.flow
private val wearDataLayerRegistry = WearDataLayerRegistry.fromContext( private val wearDataLayerRegistry = WearDataLayerRegistry.fromContext(
application = application, application = application,
coroutineScope = scope, coroutineScope = viewModelScope,
).apply { ).apply {
registerSerializer(LastUpdateSerializer) registerSerializer(LastUpdateSerializer)
} }
private val wearService : WearServiceGrpcKt.WearServiceCoroutineStub = wearDataLayerRegistry.grpcClient( private val wearService : WearServiceGrpcKt.WearServiceCoroutineStub = wearDataLayerRegistry.grpcClient(
nodeId = TargetNodeId.PairedPhone, nodeId = TargetNodeId.PairedPhone,
coroutineScope = scope, coroutineScope = viewModelScope,
) { ) {
WearServiceGrpcKt.WearServiceCoroutineStub(it) WearServiceGrpcKt.WearServiceCoroutineStub(it)
} }
private val lastUpdate: Flow<LastUpdate> = wearDataLayerRegistry.protoFlow(TargetNodeId.PairedPhone)
init { init {
lastUpdate wearDataLayerRegistry
.onEach { .protoFlow<LastUpdate>(TargetNodeId.PairedPhone)
val tasks = wearService.getTasks(GrpcProto.GetTasksRequest.getDefaultInstance()) .onEach { pagingSource?.invalidate() }
uiState.update { it.copy(tasks = tasks) }
}
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }

Loading…
Cancel
Save