Use paged data on Android Wear

pull/3024/merge
Alex Baker 1 day ago
parent 31cbe8fbab
commit dd6ba730e9

@ -22,6 +22,8 @@ class WearService(
private val headerFormatter: HeaderFormatter,
) : WearServiceGrpcKt.WearServiceCoroutineImplBase() {
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 payload = SectionedDataSource(
tasks = taskDao.fetchTasks(preferences, filter),
@ -34,27 +36,30 @@ class WearService(
)
return Tasks.newBuilder()
.addAllItems(
payload.map { item ->
when (item) {
is UiItem.Header ->
GrpcProto.UiItem.newBuilder()
.setType(GrpcProto.UiItemType.Header)
.setTitle(headerFormatter.headerString(item.value))
.build()
is UiItem.Task ->
GrpcProto.UiItem.newBuilder()
.setType(GrpcProto.UiItemType.Task)
.setId(item.task.id)
.setPriority(item.task.priority)
.setCompleted(item.task.isCompleted)
.apply {
if (item.task.title != null) {
setTitle(item.task.title)
payload
.subList(position, position + limit)
.map { item ->
when (item) {
is UiItem.Header ->
GrpcProto.UiItem.newBuilder()
.setType(GrpcProto.UiItemType.Header)
.setTitle(headerFormatter.headerString(item.value))
.build()
is UiItem.Task ->
GrpcProto.UiItem.newBuilder()
.setType(GrpcProto.UiItemType.Task)
.setId(item.task.id)
.setPriority(item.task.priority)
.setCompleted(item.task.isCompleted)
.apply {
if (item.task.title != null) {
setTitle(item.task.title)
}
}
}
.setRepeating(item.task.task.isRecurring)
.build()
}
.setRepeating(item.task.task.isRecurring)
.build()
}
}
)
.build()

@ -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-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
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-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
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> {
TODO("Not yet implemented")
return iterator().asSequence().drop(fromIndex).take(toIndex - fromIndex).toList()
}
override fun lastIndexOf(element: UiItem): Int {

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

@ -46,6 +46,7 @@ dependencies {
implementation(projects.kmp)
implementation(libs.play.services.wearable)
implementation(platform(libs.androidx.compose))
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
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.tooling.preview.Preview
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavType
import androidx.navigation.navArgument
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.TimeText
@ -57,9 +57,8 @@ class MainActivity : ComponentActivity() {
) {
composable("task_list") { navBackStackEntry ->
val viewModel: TaskListViewModel = viewModel(navBackStackEntry)
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
TaskListScreen(
uiItems = uiState.tasks.itemsList,
uiItems = viewModel.uiItems.collectAsLazyPagingItems(),
onComplete = { viewModel.completeTask(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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems
import androidx.wear.compose.material.Button
import androidx.wear.compose.material.ButtonDefaults
import androidx.wear.compose.material.Card
@ -33,18 +34,21 @@ import org.tasks.kmp.org.tasks.themes.ColorProvider
@OptIn(ExperimentalHorologistApi::class)
@Composable
fun TaskListScreen(
uiItems: List<GrpcProto.UiItem>,
uiItems: LazyPagingItems<GrpcProto.UiItem>,
onComplete: (Long) -> Unit,
onClick: (Long) -> Unit,
) {
val columnState = rememberResponsiveColumnState()
ScreenScaffold(scrollState = columnState) {
ScreenScaffold(
scrollState = columnState,
positionIndicator = {},
) {
ScalingLazyColumn(
modifier = Modifier.fillMaxSize(),
columnState = columnState,
) {
items(uiItems.size) { index ->
val item = uiItems[index]
items(uiItems.itemCount) { index ->
val item = uiItems[index] ?: return@items
key(item.id) {
when (item.type) {
GrpcProto.UiItemType.Task ->

@ -3,60 +3,70 @@ package org.tasks.presentation.screens
import android.app.Application
import androidx.lifecycle.AndroidViewModel
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.data.ProtoDataStoreHelper.protoFlow
import com.google.android.horologist.data.TargetNodeId
import com.google.android.horologist.data.WearDataLayerRegistry
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.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.tasks.GrpcProto
import org.tasks.GrpcProto.LastUpdate
import org.tasks.GrpcProto.Tasks
import org.tasks.GrpcProto.UiItem
import org.tasks.WearServiceGrpcKt
import org.tasks.presentation.MyPagingSource
import org.tasks.wear.LastUpdateSerializer
data class TaskListScreenState(
val error: String? = null,
val tasks: Tasks = Tasks.getDefaultInstance(),
)
@OptIn(ExperimentalHorologistApi::class)
class TaskListViewModel(
application: Application
) : AndroidViewModel(application) {
val uiState: MutableStateFlow<TaskListScreenState> = MutableStateFlow(TaskListScreenState())
private val scope = CoroutineScope(Dispatchers.IO)
private var pagingSource: MyPagingSource<UiItem>? = null
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(
application = application,
coroutineScope = scope,
coroutineScope = viewModelScope,
).apply {
registerSerializer(LastUpdateSerializer)
}
private val wearService : WearServiceGrpcKt.WearServiceCoroutineStub = wearDataLayerRegistry.grpcClient(
nodeId = TargetNodeId.PairedPhone,
coroutineScope = scope,
coroutineScope = viewModelScope,
) {
WearServiceGrpcKt.WearServiceCoroutineStub(it)
}
private val lastUpdate: Flow<LastUpdate> = wearDataLayerRegistry.protoFlow(TargetNodeId.PairedPhone)
init {
lastUpdate
.onEach {
val tasks = wearService.getTasks(GrpcProto.GetTasksRequest.getDefaultInstance())
uiState.update { it.copy(tasks = tasks) }
}
wearDataLayerRegistry
.protoFlow<LastUpdate>(TargetNodeId.PairedPhone)
.onEach { pagingSource?.invalidate() }
.launchIn(viewModelScope)
}
fun completeTask(it: Long) = viewModelScope.launch {
wearService.completeTask(GrpcProto.CompleteTaskRequest.newBuilder().setId(it).setCompleted(true).build())
}
}
}

Loading…
Cancel
Save