Paging Library ~ アイテムの更新 ~

Paging Library ~ アイテムの更新 ~

Bonfire Android #5での発表資料です

3c4e24fd827c789cb67a9f759f057b06?s=128

Shinnosuke Kugimiya

August 19, 2019
Tweet

Transcript

  1. !LHNZTIJO #POpSF"OESPJE ΞΠςϜͷߋ৽ 1BHJOH-JCSBSZ

  2. X © DMM.com ࣗݾ঺հ

  3. X © DMM.com ࣗݾ঺հ w LHNZTIJOఝٶʢ͗͘Έ΍ʣ w "OESPJEΤϯδχΞ w ߹ಉձࣾ%..DPN

    w $50ࣨॴଐ
  4. X © DMM.com ΰʔϧ

  5. X © DMM.com ΰʔϧ w ϖʔδϯάͷͭΒΈϙΠϯτ͕Θ͔Δ w ϖʔδϯάͷͭΒΈϙΠϯτͷճආํ๏͕Θ͔Δ ˞લఏͱͯ͠"1*࿈ܞͷ࿩Ͱ͢ɻ

  6. X © DMM.com BHFOEB

  7. X © DMM.com BHFOEB w ඪ४తͳ࢖͍ํ w ΞΠςϜͷΞοϓσʔτ͕Ͱ͖ͳ͍ʁ w 3PPNΛ࢖͏

    w 3PPNΛ࢖Θͣʹ΍Δ w ͓ΘΓʹ ˞લఏͱͯ͠"1*࿈ܞͷ࿩Ͱ͢ɻ
  8. X © DMM.com ඪ४తͳ࢖͍ํ

  9. X © DMM.com ϖʔδϯά

  10. X © DMM.com ࣮૷ͷྲྀΕ w %BUB4PVSDFΛ࣮૷͢Δ w %BUB4PVSDF'BDUPSZΛ࣮૷͢Δ w -JWF1BHFE-JTU#VJMEFSͰ-JWF%BUB1BHFE-JTU5ΛCVJME͢Δ

    w ͦΕΛPCTFSWFͯ͠1BHFE-JTU"EBQUFSʹTVCNJU-JTU͢Δ
  11. X © DMM.com %BUB4PVSDFΛ࣮૷͢Δ internal class ItemDataSource() : PageKeyedDataSource<Int, Item>()

    { override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Item>) { } override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) { } override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) { } }
  12. X © DMM.com %BUB4PVSDFΛ࣮૷͢Δ internal class ItemDataSource( private val itemRepository:

    ItemRepository ) : PageKeyedDataSource<Int, Item>() { private val dataSourceScope = CoroutineScope(Main + Job()) override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Item>) { dataSourceScope.launch { callback.onResult( itemRepository.findAll(page = 0), null, 1 ) } } override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) { dataSourceScope.launch { callback.onResult( itemRepository.findAll(page = params.key), params.key + 1 ) } }
  13. X © DMM.com %BUB4PVSDFΛ࣮૷͢Δ internal class ItemDataSource( private val itemRepository:

    ItemRepository ) : PageKeyedDataSource<Int, Item>() { private val dataSourceScope = CoroutineScope(Main + Job()) override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Item>) { dataSourceScope.launch { callback.onResult( itemRepository.findAll(page = 0), null, 1 ) } } override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) { dataSourceScope.launch { callback.onResult( itemRepository.findAll(page = params.key), params.key + 1 ) } }
  14. X © DMM.com %BUB4PVSDFΛ࣮૷͢Δ internal class ItemDataSource( private val itemRepository:

    ItemRepository ) : PageKeyedDataSource<Int, Item>() { private val dataSourceScope = CoroutineScope(Main + Job()) override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Item>) { dataSourceScope.launch { callback.onResult( itemRepository.findAll(page = 0), null, 1 ) } } override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Item>) { dataSourceScope.launch { callback.onResult( itemRepository.findAll(page = params.key), params.key + 1 ) } }
  15. X © DMM.com %BUB4PVSDF'BDUPSZΛ࣮૷͢Δ internal class ItemDataSourceFactory( private val itemRepository:

    ItemRepository ) : DataSource.Factory<Int, Item>() { override fun create(): DataSource<Int, Item> = ItemDataSource(itemRepository) }
  16. X © DMM.com -JWF1BHFE-JTU#VJMEFSͰ-JWF%BUB1BHFE-JTU5ΛCVJME͢Δ internal class ItemViewModel( itemRepository: ItemRepository )

    : ViewModel() { val itemPagedList = LivePagedListBuilder( ItemDataSourceFactory(itemRepository), PagedList.Config.Builder() .setEnablePlaceholders(false) .setPageSize(30) .build() ).build() }
  17. X © DMM.com ͦΕΛPCTFSWFͯ͠1BHFE-JTU"EBQUFSʹTVCNJU-JTU͢Δ class ReadOnlyActivity : AppCompatActivity() { private

    val viewModel: ItemViewModel by viewModels { ItemViewModelFactory(ItemRepositoryImpl()) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView<ActivityItemListBinding>( this, R.layout.activity_item_list ) val adapter = ItemAdapter(this) binding.recyclerView.adapter = adapter viewModel.itemPagedList.observe( this, Observer { adapter.submitList(it) } ) } }
  18. X © DMM.com ͦΕΛPCTFSWFͯ͠1BHFE-JTU"EBQUFSʹTVCNJU-JTU͢Δ class ReadOnlyActivity : AppCompatActivity() { private

    val viewModel: ItemViewModel by viewModels { ItemViewModelFactory(ItemRepositoryImpl()) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView<ActivityItemListBinding>( this, R.layout.activity_item_list ) val adapter = ItemAdapter(this) binding.recyclerView.adapter = adapter viewModel.itemPagedList.observe( this, Observer { adapter.submitList(it) } ) } }
  19. X © DMM.com 1BHFE-JTU"EBQUFS internal class ItemAdapter(context: Context) : PagedListAdapter<Item,

    ItemViewHolder>(ITEM_CALLBACK) { companion object { private val ITEM_CALLBACK = object : DiffUtil.ItemCallback<Item>() { override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean = oldItem.id == newItem.id override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean = oldItem == newItem } } var onItemClickListener: OnItemClickListener? = null private val inflater = LayoutInflater.from(context) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder = ItemViewHolder.create(inflater, parent, false) override fun onBindViewHolder(holder: ItemViewHolder, position: Int) = holder.bind(getItem(position), onItemClickListener) }
  20. X © DMM.com 1BHFE-JTU"EBQUFS internal class ItemAdapter(context: Context) : PagedListAdapter<Item,

    ItemViewHolder>(ITEM_CALLBACK) { companion object { private val ITEM_CALLBACK = object : DiffUtil.ItemCallback<Item>() { override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean = oldItem.id == newItem.id override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean = oldItem == newItem } } var onItemClickListener: OnItemClickListener? = null private val inflater = LayoutInflater.from(context) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder = ItemViewHolder.create(inflater, parent, false) override fun onBindViewHolder(holder: ItemViewHolder, position: Int) = holder.bind(getItem(position), onItemClickListener) } ීஈ-JTU"EBQUFSΛ࢖ͬͯΔਓ͸ɺ -JTU"EBQUFSΛ1BHFE-JTU"EBQUFSʹ͢Δ͚ͩ
  21. X © DMM.com ͜ΕͰ׬੒

  22. X © DMM.com ΞΠςϜͷΞοϓσʔτ͕Ͱ͖ͳ͍ʁ

  23. X © DMM.com internal class ItemViewModel( private val itemRepository: ItemRepository

    ) : ViewModel() { private val dataSourceFactory = ItemDataSourceFactory(itemRepository) val itemPagedList = LivePagedListBuilder( dataSourceFactory, PagedList.Config.Builder().setEnablePlaceholders(false).setPageSize(30).build() ).build() fun check(item: Item): Job = viewModelScope.launch { itemRepository.store(item.copy(checked = true)) dataSourceFactory.sourceLiveData.value?.invalidate() } fun uncheck(item: Item): Job = viewModelScope.launch { itemRepository.store(item.copy(checked = false)) dataSourceFactory.sourceLiveData.value?.invalidate() } } %BUB4PVSDFJOWBMJEBUF͢Δͱσʔλͷ࠶औಘ͕૸Δ ߋ৽Ͱ͖ΔΑ͏ʹͯ͠ΈΔ JUFN3FQPTJUPSZʹ৽͍͠σʔλΛอଘͯ͠ɺߋ৽Λ͔͚Δ
  24. X © DMM.com ߋ৽Ͱ͖ΔΑ͏ʹͯ͠ΈΔ class NgActivity : AppCompatActivity() { private

    val viewModel: ItemViewModel by viewModels { ItemViewModelFactory(ItemRepositoryImpl()) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView<ActivityItemListBinding>( this, R.layout.activity_item_list ) val adapter = ItemAdapter(this).apply { onItemClickListener = object : OnItemClickListener { override fun onClick(item: Item) { if (item.checked) { viewModel.uncheck(item) } else { viewModel.check(item) } } } } binding.recyclerView.adapter = adapter
  25. X © DMM.com ͜ΕͰ׬੒ʁ

  26. X © DMM.com ݁Ռ w ߋ৽͸Ͱ͖͍ͯΔ w ͕ɺϦετ͕ઌ಄ʹ໭ͬͯ͠·͏

  27. X © DMM.com ͳ͔ͥ dataSource?.invalidate()

  28. X © DMM.com ͳ͔ͥ dataSource?.invalidate() ८Γ८ͬͯ͜͜ʹ @Override protected PagedList<Value> compute()

    { @Nullable Key initializeKey = initialLoadKey; if (mList != null) { initializeKey = (Key) mList.getLastKey(); } do { if (mDataSource != null) { mDataSource.removeInvalidatedCallback(mCallback); } mDataSource = dataSourceFactory.create(); mDataSource.addInvalidatedCallback(mCallback); mList = new PagedList.Builder<>(mDataSource, config) .setNotifyExecutor(notifyExecutor) .setFetchExecutor(fetchExecutor) .setBoundaryCallback(boundaryCallback) .setInitialKey(initializeKey) .build(); } while (mList.isDetached()); return mList; }
  29. X © DMM.com @Override protected PagedList<Value> compute() { @Nullable Key

    initializeKey = initialLoadKey; if (mList != null) { initializeKey = (Key) mList.getLastKey(); } do { if (mDataSource != null) { mDataSource.removeInvalidatedCallback(mCallback); } mDataSource = dataSourceFactory.create(); mDataSource.addInvalidatedCallback(mCallback); mList = new PagedList.Builder<>(mDataSource, config) .setNotifyExecutor(notifyExecutor) .setFetchExecutor(fetchExecutor) .setBoundaryCallback(boundaryCallback) .setInitialKey(initializeKey) .build(); } while (mList.isDetached()); return mList; } ͳ͔ͥ dataSource?.invalidate() ͕͜͜QPJOU JOJUJBMJ[F,FZΛऔಘͯͦ͠ΕΛݩʹ࡞Γ௚͍ͯ͠Δ
  30. X © DMM.com @Nullable Key initializeKey = initialLoadKey; if (mList

    != null) { initializeKey = (Key) mList.getLastKey(); } ͳ͔ͥ @Nullable @Override public Object getLastKey() { return mDataSource.getKey(mLastLoad, mLastItem); } ContiguousPagedList
  31. X © DMM.com @Nullable Key initializeKey = initialLoadKey; if (mList

    != null) { initializeKey = (Key) mList.getLastKey(); } ͳ͔ͥ public Object getLastKey() { return mDataSource.getKey(mLastLoad, mLastItem); } ContiguousPagedList PageKeyedDataSource final Key getKey(int position, Value item) { // don't attempt to persist keys, since we currently don't pass them to initial load return null; }
  32. X © DMM.com @Nullable Key initializeKey = initialLoadKey; if (mList

    != null) { initializeKey = (Key) mList.getLastKey(); } ͳ͔ͥ public Object getLastKey() { return mDataSource.getKey(mLastLoad, mLastItem); } ContiguousPagedList PageKeyedDataSource final Key getKey(int position, Value item) { // don't attempt to persist keys, since we currently don't pass them to initial load return null; } OVMMͳͷͰඞͣ࠷ॳͷϖʔδ͔ΒʹͳΔ
  33. X © DMM.com 3PPNΛ࢖͏

  34. X © DMM.com 3PPNΛ࢖͏ w 3PPN͸1BHJOHʹରԠ͍ͯ͠Δ w #PVOEBSZ$BMMCBDLͷதͰσʔλͷऔಘ3PPN΁ͷJOTFSUΛ͢Δ w ʢ*OWBMJEBUJPO5SBDLFSͱ͍͏ͷ͕͋ͬͯɺJOTFSU͢Δͱউखʹ

    %BUB4PVSDFJOWBMJEBUF͕૸Δʣ w ߋ৽͞ΕΔ
  35. X © DMM.com ࣮૷ͷྲྀΕ w %BUB4PVSDFΛ࣮૷͢Δ w %BUB4PVSDF'BDUPSZΛ࣮૷͢Δ w %BUBCBTF

    %BPΛ࣮૷͢Δ w #PVOEBSZ$BMMCBDLͰσʔλͷऔಘΛͯ͠%#ʹJOTFSU͢Δ w -JWF1BHFE-JTU#VJMEFSͰ-JWF%BUB1BHFE-JTU5ΛCVJME͢Δ w ͦΕΛPCTFSWFͯ͠1BHFE-JTU"EBQUFSʹTVCNJU-JTU͢Δ
  36. X © DMM.com %BUBCBTFɺ%BPΛ࣮૷͢Δ @Dao internal interface ItemDao { @Insert(onConflict

    = OnConflictStrategy.REPLACE) suspend fun upsert(items: ItemRecord) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun bulkUpsert(items: List<ItemRecord>) @Query("DELETE FROM item") suspend fun deleteAll() @Query("SELECT * FROM item") fun selectAll(): DataSource.Factory<Int, ItemRecord> } @Database( entities = [ItemRecord::class], version = 1, exportSchema = false ) internal abstract class InMemDatabase : RoomDatabase() { companion object { fun create(context: Context): InMemDatabase = Room.inMemoryDatabaseBuilder( context, InMemDatabase::class.java ).fallbackToDestructiveMigration().build() } abstract fun itemDao(): ItemDao } %BP %BUBCBTF
  37. X © DMM.com %BUBCBTFɺ%BPΛ࣮૷͢Δ @Dao internal interface ItemDao { @Insert(onConflict

    = OnConflictStrategy.REPLACE) suspend fun upsert(items: ItemRecord) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun bulkUpsert(items: List<ItemRecord>) @Query("DELETE FROM item") suspend fun deleteAll() @Query("SELECT * FROM item") fun selectAll(): DataSource.Factory<Int, ItemRecord> } @Database( entities = [ItemRecord::class], version = 1, exportSchema = false ) internal abstract class InMemDatabase : RoomDatabase() { companion object { fun create(context: Context): InMemDatabase = Room.inMemoryDatabaseBuilder( context, InMemDatabase::class.java ).fallbackToDestructiveMigration().build() } abstract fun itemDao(): ItemDao } %BP %BUBCBTF ϚΠάϨʔγϣϯͱ͔େมͳͷͰJO.FNPSZͰ0,
  38. X © DMM.com #PVOEBSZ$BMMCBDLͰσʔλͷऔಘΛͯ͠%#ʹJOTFSU͢Δ internal class ItemBoundaryCallback( private val itemRepository:

    ItemRepository, private val inMemDatabase: InMemDatabase ) : PagedList.BoundaryCallback<Item>() { private val boundaryCallbackScope = CoroutineScope(Main + Job()) override fun onZeroItemsLoaded() { super.onZeroItemsLoaded() if (pageList.isNotEmpty() && pageList[0].isEndPage) return boundaryCallbackScope.launch { val itemList = itemRepository.findAll(page = 0) if (itemList.isNotEmpty()) { bulkInsertToDatabase(itemList) } … } } …
  39. X © DMM.com -JWF1BHFE-JTU#VJMEFSͰ-JWF%BUB1BHFE-JTU5ΛCVJME͢Δ internal class ItemViewModel( private val itemRepository:

    ItemRepository, private val inMemDatabase: InMemDatabase ) : ViewModel() { val itemPagedList = LivePagedListBuilder( inMemDatabase.itemDao().selectAll().map { ItemConverter.convertToItem(it) }, PagedList.Config.Builder() .setEnablePlaceholders(false) .setPageSize(30) .build() ).setBoundaryCallback( ItemBoundaryCallback( itemRepository, inMemDatabase ) ).build() }
  40. X © DMM.com ͦΕΛPCTFSWFͯ͠1BHFE-JTU"EBQUFSʹTVCNJU-JTU͢Δ class RoomActivity : AppCompatActivity() { private

    val viewModel: ItemViewModel by viewModels { ItemViewModelFactory( ItemRepositoryImpl(), InMemDatabase.create(this) ) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView<ActivityItemListBinding>( this, R.layout.activity_item_list ) val adapter = ItemAdapter(this) binding.recyclerView.adapter = adapter viewModel.itemPagedList.observe( this, Observer { adapter.submitList(it) } ) } }
  41. X © DMM.com ߋ৽࣌͸%BPͷVQEBUFϝιουΛݺͿ internal class ItemViewModel( private val itemRepository:

    ItemRepository, private val inMemDatabase: InMemDatabase ) : ViewModel() { … fun check(item: Item): Job = viewModelScope.launch { val newItem = item.copy(checked = true) itemRepository.store(newItem) inMemDatabase.itemDao().upsert(ItemConverter.convertToRecord(newItem)) } fun uncheck(item: Item): Job = viewModelScope.launch { val newItem = item.copy(checked = false) itemRepository.store(newItem) inMemDatabase.itemDao().upsert(ItemConverter.convertToRecord(newItem)) } }
  42. X © DMM.com ׬੒

  43. X © DMM.com 1BHJOHͷͨΊʹ%BUBCBTFΛಋೖʜʁ w ӬଓԽ͢ΔΘ͚Ͱ΋ͳ͍ͷʹӬଓԽͷͨΊͷ΋ͷΛ࢖ͬͯྑ͍ͷ͔ʜ w *O.FNPSZ͔ͩΒϚΠάϨʔγϣϯ͕େมͱ͔͸ͳ͍ w 3PPNΛ࢖Θͳ͍ํ๏΋͋Δ

  44. X © DMM.com 3PPNΛ࢖Θͣʹ΍Δ

  45. X © DMM.com *O.FNPSZͰσʔλΛ؅ཧ͢Δ΋ͷΛ࡞Δ *UFN%BUB1SPWJEFS *UFN *UFN *UFN *UFN *UFN

    *UFN QBHF QBHF 3FQPTJUPSZ 1BHF୯Ґ *UFN,FZFE %BUB4PVSDF *UFN୯Ґ
  46. X © DMM.com ࣮૷ͷྲྀΕ w *UFN%BUB1SPWJEFSΛ࣮૷͢Δ w %BUB4PVSDFΛ࣮૷͢Δ w %BUB4PVSDF'BDUPSZΛ࣮૷͢Δ

    w #PVOEBSZ$BMMCBDLͰσʔλͷऔಘΛͯ͠*UFN%BUB1SPWJEFSʹBEE͢ Δ w -JWF1BHFE-JTU#VJMEFSͰ-JWF%BUB1BHFE-JTU5ΛCVJME͢Δ w ͦΕΛPCTFSWFͯ͠1BHFE-JTU"EBQUFSʹTVCNJU-JTU͢Δ
  47. X © DMM.com *UFN%BUB1SPWJEFSΛ࣮૷͢Δ internal data class Page( val itemList:

    List<Item>, val isEndPage: Boolean ) internal class ItemDataProvider { val sourceLiveData = MutableLiveData<DataSource<ItemId, Item>>() private val _pageList = mutableListOf<Page>() val pageList: List<Page> get() = _pageList fun addPage(page: Page) { _pageList.add(page) sourceLiveData.value?.invalidate() } fun update(item: Item) { … sourceLiveData.value?.invalidate() } }
  48. X © DMM.com *UFN#PVOEBSZ$BMMCBDLΛ࣮૷͢Δ internal class ItemBoundaryCallback( private val itemRepository:

    ItemRepository, private val itemDataProvider: ItemDataProvider ) : PagedList.BoundaryCallback<Item>() { private val boundaryCallbackScope = CoroutineScope(Dispatchers.Main + Job()) override fun onZeroItemsLoaded() { super.onZeroItemsLoaded() if (itemDataProvider.pageList.isNotEmpty() && itemDataProvider.pageList[0].isEndPage) return boundaryCallbackScope.launch { val itemList = itemRepository.findAll(page = 0) itemDataProvider.addPage( page = Page( itemList = itemList, isEndPage = itemList.isEmpty() ) ) } } override fun onItemAtEndLoaded(itemAtEnd: Item) { … }
  49. X © DMM.com *UFN%BUB1SPWJEFSͷVQEBUFϝιουΛݺͿ internal class ItemViewModel( private val itemRepository:

    ItemRepository, private val itemDataProvider: ItemDataProvider ) : ViewModel() { … fun check(item: Item): Job = viewModelScope.launch { val newItem = item.copy(checked = true) itemRepository.store(newItem) itemDataProvider.update(newItem) } fun uncheck(item: Item): Job = viewModelScope.launch { val newItem = item.copy(checked = false) itemRepository.store(newItem) itemDataProvider.update(newItem) } }
  50. X © DMM.com ׬੒

  51. X © DMM.com ͓ΘΓʹ

  52. X © DMM.com ͓ΘΓʹ w 1BHJOH͸ߋ৽͕ೖΓ࢝ΊΔͱਏ͍ w 3PPNͰ*O.FNPSZͳΒϚΠάϨʔγϣϯͱ͔΋ͳ͍ͷͰ͋Γ w ͜Ε͚ͩͷͨΊʹ3PPNΛೖΕΔͷ͸໨త͔Β֎ΕͯΔͷͰɺ3PPNͳ

    ͠Ͱ΍Δͷ΋͋Γ αϯϓϧίʔυ https://github.com/kgmyshin/paging-sample
  53. X © DMM.com αϯϓϧίʔυ͋Γ㽂 w ϦʔυΦϯϦʔύλʔϯʢVQEBUFͳ͠ʣ w ୯७ʹVQEBUF͚ͩͰ͖ΔΑ͏ʹͯ͠Έͨύλʔϯʢઌ಄ʹ͍͘Α͏ʹ ͳͬͯ͠·͏ࣦഊύλʔϯʣ w

    3PPN࢖༻ύλʔϯ w 3PPNෆ࢖༻ύλʔϯ https://github.com/kgmyshin/paging-sample