$30 off During Our Annual Pro Sale. View Details »

Jetpack ComposeとPaging3で実現する Endless Grid Design

akkie76
July 21, 2023

Jetpack ComposeとPaging3で実現する Endless Grid Design

「potatotips #83 iOS/Android開発Tips共有会」で発表した資料になります。

akkie76

July 21, 2023
Tweet

More Decks by akkie76

Other Decks in Technology

Transcript

  1. #potatotips #83
    ©2023 RAKUS Co., Ltd.
    Jetpack ComposeとPaging3で実現する
    Endless Grid Design
    potatotips #83 iOS/Android開発Tips共有会
    @akkiee76

    View Slide

  2. #potatotips #83
    Akihiko Sato
    株式会社ラクス 楽楽精算 Lead Engineer
    コールドブリューコーヒー☕ / パン作り🍞 / あんバターフランス🥐
    @akkiee76
    自己紹介

    View Slide

  3. #potatotips #83
    ページングライブラリ概要
    大規模なデータセットからデータのページを
    ローカルストレージやネットワーク経由で読み込んで
    表示することができるライブラリです

    View Slide

  4. #potatotips #83
    ページングライブラリの主な機能
    ● ページングデータに対するメモリ内キャッシュ
    ● 組み込みのリクエスト重複排除
    ● 読込まれたデータの一番下までスクロールすると、データが自動的にリクエストされる構
    成が可能
    ● 更新機能や再試行機能などエラー処理の組込みをサポート
    ● Kotlinコルーチンとフロー、LiveData、RxJava に対応

    View Slide

  5. #potatotips #83
    ページングライブラリの主な機能
    ● ページングデータに対するメモリ内キャッシュ
    ● 組み込みのリクエスト重複排除
    ● 読込まれたデータの一番下までスクロールすると、データが自動的にリクエストされる構
    成が可能
    ● 更新機能や再試行機能などエラー処理の組込みをサポート
    ● Kotlinコルーチンとフロー、LiveData、RxJava に対応
    リソースを効率的にしたページングデータの操作が可能に

    View Slide

  6. #potatotips #83
    サンプルアプリ
    主に使用しているライブラリなど
    ● paging 3
    ● paging-compose 3
    ● coil
    ● Unsplash Image API
    https://github.com/akkie76/Paging-App-Sample

    View Slide

  7. #potatotips #83
    ページングライブラリの主なクラス
    ● PagingData
    ● Pager
    ● PagingSource
    ● PagingConfig
    ● LazyPagingItems

    View Slide

  8. #potatotips #83
    ページングライブラリの主なクラス
    ● PagingData
    ○ ページングによりロードされたデータクラス
    ● Pager
    ● PagingSource
    ● PagingConfig
    ● LazyPagingItems

    View Slide

  9. #potatotips #83
    ページングライブラリの主なクラス
    ● PagingData
    ● Pager
    ○ PagingDataのリアクティブストリームを行うクラス
    ● PagingSource
    ● PagingConfig
    ● LazyPagingItems

    View Slide

  10. #potatotips #83
    ページングライブラリの主なクラス
    ● PagingData
    ● Pager
    ● PagingSource
    ○ 表示するデータソースを取得するクラス
    ● PagingConfig
    ● LazyPagingItems

    View Slide

  11. #potatotips #83
    ページングライブラリの主なクラス
    ● PagingData
    ● Pager
    ● PagingSource
    ● PagingConfig
    ○ PagingSource で取得するデータをロードする際の設定クラス
    ● LazyPagingItems

    View Slide

  12. #potatotips #83
    ページングライブラリの主なクラス
    ● PagingData
    ● Pager
    ● PagingSource
    ● PagingConfig
    ● LazyPagingItems
    ○ PagingData のフローから取得したデータを表示するためのクラス

    View Slide

  13. #potatotips #83
    アーキテクチャ

    View Slide

  14. #potatotips #83
    Repository

    View Slide

  15. #potatotips #83
    Repository
    class UnsplashSearchRepositoryImpl @Inject constructor(
    private val service: UnsplashService
    ): UnsplashSearchRepository {
    override fun searchPhotos(query: String): Flow> {
    return Pager(
    config = PagingConfig(enablePlaceholders = false, pageSize = PAGE_SIZE),
    pagingSourceFactory = { UnsplashPagingSource(
    service, query) }
    ).flow
    }
    companion object {
    private const val PAGE_SIZE = 25
    }
    }

    View Slide

  16. #potatotips #83
    Repository
    class UnsplashSearchRepositoryImpl @Inject constructor(
    private val service: UnsplashService
    ): UnsplashSearchRepository {
    override fun searchPhotos(query: String): Flow> {
    return Pager(
    config = PagingConfig(enablePlaceholders = false, pageSize = PAGE_SIZE),
    pagingSourceFactory = { UnsplashPagingSource(
    service, query) }
    ).flow
    }
    companion object {
    private const val PAGE_SIZE = 25
    }
    }

    View Slide

  17. #potatotips #83
    Repository
    class UnsplashPagingSource(
    private val service: UnsplashService,
    private val query: String
    ) : PagingSource() {
    override suspend fun load(params: LoadParams): LoadResult {
    val page = params.key ?: UNSPLASH_STARTING_PAGE_INDEX
    return try {
    val response = service.searchPhotos(query, page, params.loadSize)
    val photos = response.results
    LoadResult.Page(
    data = photos,
    prevKey = if (page == UNSPLASH_STARTING_PAGE_INDEX) null else page - 1,
    nextKey = if (page == response.totalPages) null else page + 1
    )
    } catch (exception: Exception) {
    LoadResult.Error(exception)
    }
    }

    View Slide

  18. #potatotips #83
    Repository
    class UnsplashPagingSource(
    private val service: UnsplashService,
    private val query: String
    ) : PagingSource() {
    override suspend fun load(params: LoadParams): LoadResult {
    val page = params.key ?: UNSPLASH_STARTING_PAGE_INDEX
    return try {
    val response = service.searchPhotos(query, page, params.loadSize)
    val photos = response.results
    LoadResult.Page(
    data = photos,
    prevKey = if (page == UNSPLASH_STARTING_PAGE_INDEX) null else page - 1,
    nextKey = if (page == response.totalPages) null else page + 1
    )
    } catch (exception: Exception) {
    LoadResult.Error(exception)
    }
    }

    View Slide

  19. #potatotips #83
    ViewModel

    View Slide

  20. #potatotips #83
    ViewModel
    @HiltViewModel
    class MainViewModel @Inject constructor(
    searchRepository: UnsplashSearchRepository
    ): ViewModel() {
    val photos = searchRepository.searchPhotos(
    QUERY).cachedIn(viewModelScope)
    companion object {
    private const val QUERY = "fruit"
    }
    }

    View Slide

  21. #potatotips #83
    ViewModel
    @HiltViewModel
    class MainViewModel @Inject constructor(
    searchRepository: UnsplashSearchRepository
    ): ViewModel() {
    val photos = searchRepository.searchPhotos(
    QUERY).cachedIn(viewModelScope)
    companion object {
    private const val QUERY = "fruit"
    }
    }

    View Slide

  22. #potatotips #83
    UI Layer

    View Slide

  23. #potatotips #83
    UI Layer
    @Composable
    fun MainScreen(viewModel: MainViewModel) {
    Scaffold { padding ->
    val lazyPagingItems = viewModel.photos.collectAsLazyPagingItems()
    LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 128.dp)
    ) {
    items(
    count = lazyPagingItems.itemCount,
    key = lazyPagingItems.itemKey()
    ) { index ->
    val photo = lazyPagingItems[index] ?: return@items
    AsyncImage(
    model = photo.urls.small,
    contentDescription = null,
    modifier = Modifier.fillMaxWidth().height(128.dp),
    contentScale = ContentScale.FillWidth
    )
    }
    }

    View Slide

  24. #potatotips #83
    UI Layer
    @Composable
    fun MainScreen(viewModel: MainViewModel) {
    Scaffold { padding ->
    val lazyPagingItems = viewModel.photos.collectAsLazyPagingItems()
    LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 128.dp)
    ) {
    items(
    count = lazyPagingItems.itemCount,
    key = lazyPagingItems.itemKey()
    ) { index ->
    val photo = lazyPagingItems[index] ?: return@items
    AsyncImage(
    model = photo.urls.small,
    contentDescription = null,
    modifier = Modifier.fillMaxWidth().height(128.dp),
    contentScale = ContentScale.FillWidth
    )
    }
    }

    View Slide

  25. #potatotips #83
    UI Layer(CircularProgress)
    when (lazyPagingItems.loadState.refresh) {
    is LoadState.Loading -> {
    Column(
    modifier = Modifier.fillMaxSize(),
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.Center,
    ) {
    Text(
    modifier = Modifier.padding(8.dp),
    text = "Loading"
    )
    CircularProgressIndicator(
    modifier = Modifier.padding(top = 8.dp),
    color = Color.LightGray
    )
    }
    }
    else -> {}
    }

    View Slide

  26. #potatotips #83
    UI Layer(CircularProgress)
    when (lazyPagingItems.loadState.refresh) {
    is LoadState.Loading -> {
    Column(
    modifier = Modifier.fillMaxSize(),
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.Center,
    ) {
    Text(
    modifier = Modifier.padding(8.dp),
    text = "Loading"
    )
    CircularProgressIndicator(
    modifier = Modifier.padding(top = 8.dp),
    color = Color.LightGray
    )
    }
    }
    else -> {}
    }

    View Slide

  27. #potatotips #83
    まとめ
    ページングライブラリを使うと、
    シンプルな実装でリソースを効率化したエンドレススクロールが実現可能です。
    画像読み込みライブラリも合わせて使うことで、
    デザインの幅も広げることも可能です。

    View Slide

  28. #potatotips #83
    Thank you !

    View Slide