Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for akkiee76 akkiee76
July 21, 2023

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

以下のイベントで発表した資料になります。

YUMEMI.grow Mobile #9
https://yumemi.connpass.com/event/302462/

potatotips #83 iOS/Android開発Tips共有会
https://potatotips.connpass.com/event/287244/

Avatar for akkiee76

akkiee76

July 21, 2023
Tweet

More Decks by akkiee76

Other Decks in Technology

Transcript

  1. YUMEMI.grow Mobile #9 ページングライブラリの主な機能 • ページングデータに対するメモリ内キャッシュ • リクエスト重複排除 • 画面スクロールに連動した際に自動的なリクエスト

    • ページングの状態管理 • Coroutine、Flow、LiveData、RxJava に対応 リソースを効率的にしたページングデータの操作を実現 5
  2. YUMEMI.grow Mobile #9 ページングライブラリの主なコンポーネント • PagingData • Pager • PagingSource

    ◦ 特定のページクエリのデータチャンクを読み込むためのクラス • PagingConfig • LazyPagingItems 9
  3. YUMEMI.grow Mobile #9 ページングライブラリの主なコンポーネント • PagingData • Pager • PagingSource

    • PagingConfig ◦ ページング動作を決定するパラメータを定義するクラス • LazyPagingItems 10
  4. YUMEMI.grow Mobile #9 ページングライブラリの主なコンポーネント • PagingData • Pager • PagingSource

    • PagingConfig • LazyPagingItems ◦ PagingData のフローから取得したデータを表示するためのクラス 11
  5. YUMEMI.grow Mobile #9 サンプルアプリ 主に使用しているライブラリなど • paging 3 • paging-compose

    3 • coil • Unsplash Image API https://github.com/akkie76/Paging-App-Sample 12
  6. YUMEMI.grow Mobile #9 Repository class UnsplashSearchRepositoryImpl @Inject constructor( private val

    service: UnsplashService ): UnsplashSearchRepository { override fun searchPhotos(query: String): Flow<PagingData<UnsplashPhoto>> { return Pager( config = PagingConfig(enablePlaceholders = false, pageSize = PAGE_SIZE), pagingSourceFactory = { UnsplashPagingSource(service, query) } ).flow } companion object { private const val PAGE_SIZE = 25 } } 15
  7. YUMEMI.grow Mobile #9 Repository class UnsplashPagingSource( private val service: UnsplashService,

    private val query: String ) : PagingSource<Int, UnsplashPhoto>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, UnsplashPhoto> { 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) } } 16
  8. YUMEMI.grow Mobile #9 ViewModel @HiltViewModel class MainViewModel @Inject constructor( searchRepository:

    UnsplashSearchRepository ): ViewModel() { val photos = searchRepository.searchPhotos(QUERY).cachedIn(viewModelScope) companion object { private const val QUERY = "fruit" } } 18
  9. YUMEMI.grow Mobile #9 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 ) } } 20
  10. YUMEMI.grow Mobile #9 ページングライブラリの状態管理 CombinedLoadStates 型で「読み込み状態」にアクセスすることが可能 • LoadStates.append ◦ ユーザーの現在位置より後に取得されるアイテムの

    LoadState • LoadStates.prepend ◦ ユーザーの現在位置より前に取得されるアイテムの LoadState • LoadStates.refresh ◦ 初期読み込みの LoadState 22
  11. YUMEMI.grow Mobile #9 ページングライブラリの状態管理 LoadState から取得できる状態 • LoadState.Loading ◦ アイテムを読み込み中

    • LoadState.NotLoading ◦ アイテムを読み込んでいない(初期状態も含まれる) • LoadState.Error ◦ 読み込みエラーが発生 23
  12. YUMEMI.grow Mobile #9 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 -> {} } 24