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

How PayPay Flea Market Migrated from Paging 2 to Paging 3

How PayPay Flea Market Migrated from Paging 2 to Paging 3

Rei Nagahama (Yahoo! JAPAN / Flea Market Div, YAHUOKU! Services Group, Commerce Group / Android App Engineer)

https://tech-verse.me/ja/sessions/145
https://tech-verse.me/en/sessions/145
https://tech-verse.me/ko/sessions/145

Tech-Verse2022

November 18, 2022
Tweet

More Decks by Tech-Verse2022

Other Decks in Technology

Transcript

  1. Self Introduction - Rei Nagahama Twitter: Fel1Tech - Joined Yahoo

    Japan Corporation in 2021 as a new graduate - In charge of Android application development for PayPay Flea Market - In the middle of migration from Paging2 to Paging3
  2. PayPay Flea Market - Specialization in flea market - Payment

    through PayPay - Proposals and consultation for prices - Linking with Yahoo Auction!
  3. Small Data Small Data Large Data What is Paging3? Official

    library for easy implementation of paging process
  4. Why is it necessary to migrate? What is Paging3? -

    Coroutines Flow becomes available - Paging2 was deprecated at the same time when Paging3 was released in May 2021 - Paging3 provides loading status
  5. Why the migration at this time? What is Paging3? -

    Unclear if Paging3 is stable in operation - No research done on the migration - No implementation experience with other services ?
  6. How did we migrate? What is Paging3? 1.Migrate BoundaryCallback to

    RemoteMediator 2.Migrate PagedListAdapter to PagingDataAdapter 3.Migration of processing around State to loadStateFlow Official Documents:https://developer.android.com/topic/libraries/architecture/paging/v3-migration
  7. Migrate BoundaryCallback to RemoteMediator How did we migrate? class ItemBoundaryCallback(

    … ) : PagedList.BoundaryCallback<Item>() return LivePagedListBuilder(items, PAGE_SIZE) .setBoundaryCallback(boundaryCallback) .build() class ItemRemoteMediator( … ) : RemoteMediator<Int, Item>() return Pager(…).flow.cachedIn(scope)
  8. Migrate PagedListAdapter to PagingDataAdapter How did we migrate? class ItemPagedListAdapter(

    … ) : PagedListAdapter<Item, ItemVH>(diffCallback) class ItemPagingDataAdapter( … ) : PagingDataAdapter<Item, ItemVH>(diffCallback)
  9. Migration of processing around State to loadStateFlow How did we

    migrate? itemState.postValue(Loading) repository.getItems() when (itemState) { Loading -> binding.loading. visibility = VISIBLE … } lifecycleScope.launch { adapter.loadStateFlow.collectLatest { state -> if(state == LoadState.Loading) { binding.loading.visibility = VISIBLE } } }
  10. Points Noted at the Time of Migration What is Paging3?

    - Summarized and shared the method of migration to Paging3 - Migrated from the screens having minimal impact on services - Migrated the original implementation relevant to State and performing refactoring at the same time
  11. Points Noted at the Time of Migration What is Paging3?

    - Summarized and shared the method of migration to Paging3 - Migrated from the screens having minimal impact on services - Migrated the original implementation relevant to State and performing refactoring at the same time
  12. Migrated from the screens having minimal impact on services Points

    Noted at the Time of Migration - Migrated from the screens that do not affect primary functions - Migration may cause malfunction - Saved the know-how through gradual migration
  13. Points Noted at the Time of Migration What is Paging3?

    - Summarized and shared the method of migration to Paging3 - Migrated from the screens having minimal impact on services - Migrated the original implementation relevant to State and performing refactoring at the same time
  14. Points Noted at the Time of Migration What is Paging3?

    - Summarized and shared the method of migration to Paging3 - Migrated from the screens having minimal impact on services - Migrated the original implementation relevant to State and performing refactoring at the same time
  15. Migrated the original implementation of State management and DB and

    performed refactoring at the same time Points Noted at the Time of Migration - Efficient refactoring - Refactoring, which requires a lot of labor is difficult - Simultaneous confirmation that Paging, State, and DB behavior is correct
  16. Knowledge gained in migration - Original implementation around DataBase that

    caused problems in migration - Fatal code that can occur easily but generates infinite API calls - Advantages and considerations for migrating State to LoadStateFlow
  17. How did we migrate? What is Paging3? 1.Migrate BoundaryCallback to

    RemoteMediator 2.Migrate PagedListAdapter to PagingDataAdapter 3.Migration of processing around State to loadStateFlow Official Documents:https://developer.android.com/topic/libraries/architecture/paging/v3-migration
  18. Migrate BoundaryCallback to RemoteMediator How did we migrate? class ItemBoundaryCallback(

    … ) : PagedList.BoundaryCallback<Item>() return LivePagedListBuilder(items, PAGE_SIZE) .setBoundaryCallback(boundaryCallback) .build() class ItemRemoteMediator( … ) : RemoteMediator<Int, Item>() return Pager(…).flow.cachedIn(scope)
  19. Migrate PagedListAdapter to PagingDataAdapter How did we migrate? class ItemPagedListAdapter(

    … ) : PagedListAdapter<Item, ItemVH>(diffCallback) class ItemPagingDataAdapter( … ) : PagingDataAdapter<Item, ItemVH>(diffCallback)
  20. Migration of processing around State to loadStateFlow How did we

    migrate? itemState.postValue(Loading) repository.getItems() when (itemState) { Loading -> binding.loading. visibility = VISIBLE … } lifecycleScope.launch { adapter.loadStateFlow.collectLatest { state -> if(state == LoadState.Loading) { binding.loading.visibility = VISIBLE } } }
  21. Knowledge gained in migration - Original implementation around DataBase that

    caused problems in migration - Fatal code that can occur easily but generates infinite API calls - Advantages and considerations for migrating State to LoadStateFlow
  22. Fatal code that can occur easily but generates infinite API

    calls - It may occur because of the load function of RemoteMediator - Service may go down due to high server load Knowledge gained in migration
  23. Fatal code that can occur easily but generates infinite API

    calls override suspend fun load(loadType: LoadType, state: PagingState<Int, Item>): MediatorResult { return try { withContext(Dispatchers.IO) { val response = fetchAndRegisterData() val endOfPaginationReached = response.data.isZeroMatch MediatorResult.Success(endOfPaginationReached) } } catch (e: Exception) { MediatorResult.Error(e) } } NG
  24. Fatal code that can occur easily but generates infinite API

    calls override suspend fun load(loadType: LoadType, state: PagingState<Int, Item>): MediatorResult { return try { withContext(Dispatchers.IO) { if (loadType == LoadType.PREPEND) return MediatorResult.Success(true) val response = fetchAndRegisterData() val endOfPaginationReached = response.data.isEmpty() MediatorResult.Success(endOfPaginationReached) } } catch (e: Exception) { MediatorResult.Error(e) } } OK
  25. Fatal code that can occur easily but generates infinite API

    calls override suspend fun load(loadType: LoadType, state: PagingState<Int, Item>): MediatorResult { return try { withContext(Dispatchers.IO) { if (loadType == LoadType.PREPEND) return MediatorResult.Success(true) val response = fetchAndRegisterData() val endOfPaginationReached = response.data.isEmpty() MediatorResult.Success(endOfPaginationReached) } } catch (e: Exception) { MediatorResult.Error(e) } } OK
  26. Fatal code that can occur easily but generates infinite API

    calls override suspend fun load(loadType: LoadType, state: PagingState<Int, Item>): MediatorResult { return try { withContext(Dispatchers.IO) { if (loadType == LoadType.PREPEND) return MediatorResult.Success(true) val response = fetchAndRegisterData() val endOfPaginationReached = response.data.isEmpty() MediatorResult.Success(endOfPaginationReached) } } catch (e: Exception) { MediatorResult.Error(e) } } OK Upward reading is Since there is no need for Success(true)
  27. Fatal code that can occur easily but generates infinite API

    calls override suspend fun load(loadType: LoadType, state: PagingState<Int, Item>): MediatorResult { return try { withContext(Dispatchers.IO) { if (loadType == LoadType.PREPEND) return MediatorResult.Success(true) val response = fetchAndRegisterData() val endOfPaginationReached = response.data.isEmpty() MediatorResult.Success(endOfPaginationReached) } } catch (e: Exception) { MediatorResult.Error(e) } } OK Upward reading is Since there is no need for Success(true) Why is it necessary? 写真:アフロ
  28. Fatal code that can occur easily but generates infinite API

    calls override suspend fun load(loadType: LoadType, state: PagingState<Int, Item>): MediatorResult { return try { withContext(Dispatchers.IO) { if (loadType == LoadType.PREPEND) return MediatorResult.Success(true) val response = fetchAndRegisterData() val endOfPaginationReached = response.data.isEmpty() MediatorResult.Success(endOfPaginationReached) } } catch (e: Exception) { MediatorResult.Error(e) } } OK Upward reading is Since there is no need for Success(true) Let’s have a look at the loading order 写真:アフロ
  29. Fatal code that can occur easily but generates infinite API

    calls override suspend fun load(loadType: LoadType, state : PagingState<Int, Item>): MediatorResult { Log.d(“RemoteMediator”, “LoadType: ${loadType.name}”) MediatorResult.Success(true) }
  30. Fatal code that can occur easily but generates infinite API

    calls 13:41:34:046 RemoteMediator: LoadType: REFRESH ← initial loading 13:41:34:411 RemoteMediator: LoadType: PREPEND ← Upward loading 13:41:34:531 RemoteMediator: LoadType: APPEND ← Downward loading 13:41:34:652 RemoteMediator: LoadType: APPEND 13:41:34:657 RemoteMediator: LoadType: APPEND ・ ・ ・ OK
  31. Fatal code that can occur easily but generates infinite API

    calls 13:41:34:046 RemoteMediator: LoadType: REFRESH ← initial loading 13:41:34:411 RemoteMediator: LoadType: PREPEND ← Upward loading 13:41:34:531 RemoteMediator: LoadType: PREPEND 13:41:34:652 RemoteMediator: LoadType: PREPEND 13:41:34:657 RemoteMediator: LoadType: PREPEND ・ ・ ・ Judgment of upward loading in the internal part NG
  32. Fatal code that can occur easily but generates infinite API

    calls Judgment of initial state without scrolling is Judgment of upper end
  33. API Call Scroll position is maintained as superscript Judgment of

    upper end Internal part has upward loading Data is added below
  34. API Call Scroll position is maintained as superscript Judgment of

    upper end Internal part has upward loading Data is added below
  35. API Call Scroll position is maintained as superscript Judgment of

    upper end Internal part has upward loading Data is added below
  36. API Call Scroll position is maintained as superscript Judgment of

    upper end Internal part has upward loading Data is added below
  37. API Call Scroll position is maintained as superscript Judgment of

    upper end Internal part has upward loading Data is added below Until all the data is collected 写真:アフロ
  38. Fatal code that can occur easily but generates infinite API

    calls override suspend fun load(loadType: LoadType, state : PagingState<Int, Item>): MediatorResult { return try { withContext(Dispatchers.IO) { if (loadType == LoadType.PREPEND) return MediatorResult.Success(true) val response = fetchAndRegisterData() val endOfPaginationReached = response.data.isZeroMatch MediatorResult.Success(endOfPaginationReached) } } catch (e: Exception) { MediatorResult.Error(e) } } OK As PREPEND is not required at the time of downward loading, Success(true) is returned
  39. Knowledge gained in migration - Original implementation around DataBase that

    caused problems in migration - Fatal code that can occur easily but generates infinite API calls - Advantages and considerations for migrating State to LoadStateFlow
  40. Original Implementation around the DataBase that Caused Problems in Migration

    - Information relevant to State can be acquired using loadStateListener - Leaving implementation of State for both DataBase and loadStateListener aside is not a good option as the implementation will be reviewed again in the future - Information about State and information about products were saved together in DataBase
  41. Original implementation around DataBase that caused problems in migration @Entity

    data class Item( @PrimaryKey(autoGenerate = true) val id: Int, val title: String, val empty: Boolean = false ) Flag whether or not to display an indication of missing data
  42. Original implementation around DataBase that caused problems in migration @Entity

    data class Item( @PrimaryKey(autoGenerate = true) val id: Int, val title: String, val empty: Boolean = false ) Converted to an object indicating that there was no data and Item and State were separated out.
  43. Original implementation around DataBase that caused problems in migration 1.Separating

    the processes related to display such as State from DB 2.Aggregate State to Fragment 3.Migrate BoundaryCallback and other data acquisition processes 4.Managing the State on the basis of the loadStateFlow of PagingDataAdapter
  44. Original implementation around DataBase that caused problems in migration 1.Separating

    the processes related to display such as State from DB 2.Aggregate State to Fragment 3.Migrate BoundaryCallback and other data acquisition processes 4.Managing the State on the basis of the loadStateFlow of PagingDataAdapter
  45. Original implementation around DataBase that caused problems in migration @Entity

    data class Item( @PrimaryKey(autoGenerate = true) val id: Int, val title: String // val empty: Boolean = false )
  46. Original implementation around DataBase that caused problems in migration 1.Separating

    the processes related to display such as State from DB 2.Aggregate State to Fragment 3.Migrate BoundaryCallback and other data acquisition processes 4.Managing the State on the basis of the loadStateFlow of PagingDataAdapter
  47. sealed class VH (view: View) : RecyclerView.ViewHolder(view) { class ItemViewHolder(view:

    View) : VH(view) class ZeroMatchViewHolder(view: View) :VH(view) } <ProgressBar android:id="@+id/loading" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center” /> Original implementation around DataBase that caused problems in migration
  48. Original implementation around DataBase that caused problems in migration 1.Separating

    the processes related to display such as State from DB 2.Aggregate State to Fragment 3.Migrate BoundaryCallback and other data acquisition processes 4.Managing the State on the basis of the loadStateFlow of PagingDataAdapter
  49. Original implementation around DataBase that caused problems in migration 1.Separating

    the processes related to display such as State from DB 2.Aggregate State to Fragment 3.Migrate BoundaryCallback and other data acquisition processes 4.Managing the State on the basis of the loadStateFlow of PagingDataAdapter
  50. itemAdapter.loadStateFlow.collectLatest { state -> when { state == LoadState.Loading ->

    binding.loading.visibility = VISIBLE … } } Original implementation around DataBase that caused problems in migration
  51. itemAdapter.loadStateFlow.collectLatest { state -> when { state == LoadState.Loading ->

    binding.loading.visibility = VISIBLE … } } Original implementation around DataBase that caused problems in migration Perform corresponding processing based on the LoadState status
  52. Knowledge gained in migration - Original implementation around DataBase that

    caused problems in migration - Fatal code that can occur easily but generates infinite API calls - Advantages and considerations for migrating State to LoadStateFlow
  53. Advantages and Considerations for Migrating State to LoadStateFlow - Logic

    around State can be gathered into Fragment - As the loading information is provided by the officials, self-implementation is not required
  54. Advantages and Considerations for Migrating State to LoadStateFlow - gathered

    into FragmentaIt becomes complicated when display is split on the basis of loading information of multiple APIs - LoadState is flowing in large quantities, so it is necessary to devise a way to narrow it down
  55. It becomes complicated when display is split on the basis

    of loading information of multiple APIs. NotLoading Loading Error CombinedLoadStates Refresh Append Prepend LoadState
  56. It becomes complicated when display is split on the basis

    of loading information of multiple APIs. NotLoading Loading Error Refresh Append Prepend itemAdapter.loadStateFlow.first().refresh == LoadState.Loading CombinedLoadStates LoadState
  57. It becomes complicated when display is split on the basis

    of loading information of multiple APIs. lifecycleScope.launch { itemAdapter.loadStateFlow.combine(otherAdapter.loadStateFlow) { item, other -> if (item.refresh == LoadState.Loading && other.refresh == LoadState.Loading) { binding.loading.visibility = VISIBLE } } }
  58. itemAdapter.loadStateFlow.collectLatest { state -> Log.d(”LoadState”, ”${ state.toString() }” ) }

    LoadState is flowing in large quantities, so it is necessary to devise a way to narrow it down
  59. LoadState is flowing in large quantities, so it is necessary

    to devise a way to narrow it down 13:41:34:046 LoadState: LoadType.Loading 13:41:34:411 LoadState: LoadType.Loading 13:41:34:531 LoadState: LoadType.Loading 13:41:34:652 LoadState: LoadType.Loading 13:41:34:657 LoadState: LoadType.Loading ・ ・ ・
  60. itemAdapter.loadStateFlow.map { it.refresh }.distinctUntilChanged().collectLatest { state -> Log.d(”LoadState”, ”${ state.toString()

    }” ) } LoadState is flowing in large quantities, so it is necessary to devise a way to narrow it down
  61. itemAdapter.loadStateFlow.map { it.refresh }.distinctUntilChanged().collectLatest { state -> Log.d(”LoadState”, ”${ state.toString()

    }” ) } LoadState is flowing in large quantities, so it is necessary to devise a way to narrow it down
  62. itemAdapter.loadStateFlow.map { it.refresh }.distinctUntilChanged().collectLatest { state -> Log.d(”LoadState”, ”${ state.toString()

    }” ) } LoadState is flowing in large quantities, so it is necessary to devise a way to narrow it down
  63. LoadState: LoadType.Loading LoadState: LoadType.NotLoading ・ ・ ・ ・ LoadState is

    flowing in large quantities, so it is necessary to devise a way to narrow it down
  64. Summary - By migrating, you will obtain various benefits such

    as the benefits from new features, affinity with Coroutines Flow, provision of LoadState - We hope it will be useful in your projects! - I have introduced the knowledge gained in the current migration in PayPay Flea Market