Jetpack Compose Paging Sample

November 7, 2021

Dependecies

dependencies {
    implementation "androidx.paging:paging-compose:1.0.0-alpha14"
}
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.paging.*
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import kotlinx.coroutines.flow.Flow
import kotlin.random.Random

data class Member(val name: String, val age: Int)

class MemberRepository(val totalCount: Int, val pageSize: Int) {

    fun getMembers(page: Int): List<Member> {
        val random = Random(page)

        val startIndex = (page - 1) * pageSize + 1
        var endIndex = startIndex + pageSize - 1
        if (endIndex > totalCount) {
            endIndex = totalCount
        }

        return  (startIndex..endIndex).map { index ->
            Member(name = "Member #${index}", age = random.nextInt(1, 99))
        }
    }
}

class MemberSource(private val repo: MemberRepository) : PagingSource<Int, Member>() {
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Member> {
        val page = params.key ?: 1

        return LoadResult.Page(
            data = repo.getMembers(page),
            prevKey = if (page == 1) null else page - 1,
            nextKey = if (page * repo.pageSize < repo.totalCount) page + 1 else null
        )
    }

    override fun getRefreshKey(state: PagingState<Int, Member>): Int? {
        return state.anchorPosition
    }

}

ViewModel

class MemberViewModel: ViewModel() {
    fun fetchMemberData(): Flow<PagingData<Member>> {
      val repo = MemberRepository(totalCount = 101, pageSize = 10)

      return Pager(PagingConfig(pageSize = repo.pageSize)) {
          MemberSource(repo)
      }.flow
    }
}

Usage

@Composable
fun MemberScreen(viewModel: MemberViewModel = viewModel()) {
    val lazyMembers = viewModel.fetchMemberData().collectAsLazyPagingItems()

    LazyColumn() {
        items(lazyMembers) { member ->
            if (member != null) {
                Text("${member.name}, age=${member.age}")
            }
        }
    }
}

@Preview
@Composable
fun MemberScreenPreview() {
    MemberScreen()
}

Show Loading Indicator & Error/Retry Handling

@Composable
fun MemberScreen() {
    val lazyMembers = fetchMemberData().collectAsLazyPagingItems()

    LazyColumn() {
        items(lazyMembers) { member ->
            if (member != null) {
                Text("${member.name}, age=${member.age}")
            }
        }

        val refreshState = lazyMembers.loadState.refresh
        if (refreshState is LoadState.Loading)
            item {
                Center(modifier = Modifier.fillParentMaxSize()) {
                    Text("Initial data fetch ...")
                    CircularProgressIndicator()
                }
            }
        else if (refreshState is LoadState.Error)
            item {
                Center(modifier = Modifier.fillParentMaxSize()) {
                    val error = refreshState.error

                    Text("Initial Data Error: ${error.localizedMessage}")
                    Button(onClick = { lazyMembers.retry() }) {
                        Text("Retry")
                    }
                }
            }

        val appendState = lazyMembers.loadState.append
        if (appendState is LoadState.Loading)
            item {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Text("Subsequent data fetch ...")
                    CircularProgressIndicator()
                }
            }
        else if (appendState is LoadState.Error)
            item {
                val error = appendState.error
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Text("Subsequent data Error: ${error.localizedMessage}")
                    Button(onClick = { lazyMembers.retry() }) {
                        Text("Retry")
                    }
                }
            }
    }
}

Simulate delay and error

class MemberRepository(val totalCount: Int, val pageSize: Int) {

    suspend fun getMembers(page: Int): List<Member> {
        val random = Random(page)

        val startIndex = (page - 1) * pageSize + 1
        var endIndex = startIndex + pageSize - 1
        if (endIndex > totalCount) {
            endIndex = totalCount
        }

        delay(1000)
        if (Random.nextBoolean())
            throw Exception("Test Error")

        return  (startIndex..endIndex).map { index ->
            Member(name = "Member #${index}", age = random.nextInt(1, 99))
        }
    }
}


class MemberSource(private val repo: MemberRepository) : PagingSource<Int, Member>() {
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Member> {
        val page = params.key ?: 1

        return try {
            LoadResult.Page(
                data = repo.getMembers(page),
                prevKey = if (page == 1) null else page - 1,
                nextKey = if (page * repo.pageSize < repo.totalCount) page + 1 else null
            )
        }
        catch (e: Exception) {
            LoadResult.Error(e)
        }

    }

    override fun getRefreshKey(state: PagingState<Int, Member>): Int? {
        return state.anchorPosition
    }

}

References

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.