Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

X © DMM.com ࣗݾ঺հ

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

X © DMM.com ΰʔϧ

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

X © DMM.com BHFOEB

Slide 7

Slide 7 text

X © DMM.com BHFOEB w ඪ४తͳ࢖͍ํ w ΞΠςϜͷΞοϓσʔτ͕Ͱ͖ͳ͍ʁ w 3PPNΛ࢖͏ w 3PPNΛ࢖Θͣʹ΍Δ w ͓ΘΓʹ ˞લఏͱͯ͠"1*࿈ܞͷ࿩Ͱ͢ɻ

Slide 8

Slide 8 text

X © DMM.com ඪ४తͳ࢖͍ํ

Slide 9

Slide 9 text

X © DMM.com ϖʔδϯά

Slide 10

Slide 10 text

X © DMM.com ࣮૷ͷྲྀΕ w %BUB4PVSDFΛ࣮૷͢Δ w %BUB4PVSDF'BDUPSZΛ࣮૷͢Δ w -JWF1BHFE-JTU#VJMEFSͰ-JWF%BUB1BHFE-JTU5ΛCVJME͢Δ w ͦΕΛPCTFSWFͯ͠1BHFE-JTU"EBQUFSʹTVCNJU-JTU͢Δ

Slide 11

Slide 11 text

X © DMM.com %BUB4PVSDFΛ࣮૷͢Δ internal class ItemDataSource() : PageKeyedDataSource() { override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { } override fun loadAfter(params: LoadParams, callback: LoadCallback) { } override fun loadBefore(params: LoadParams, callback: LoadCallback) { } }

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

X © DMM.com %BUB4PVSDF'BDUPSZΛ࣮૷͢Δ internal class ItemDataSourceFactory( private val itemRepository: ItemRepository ) : DataSource.Factory() { override fun create(): DataSource = ItemDataSource(itemRepository) }

Slide 16

Slide 16 text

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() }

Slide 17

Slide 17 text

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( this, R.layout.activity_item_list ) val adapter = ItemAdapter(this) binding.recyclerView.adapter = adapter viewModel.itemPagedList.observe( this, Observer { adapter.submitList(it) } ) } }

Slide 18

Slide 18 text

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( this, R.layout.activity_item_list ) val adapter = ItemAdapter(this) binding.recyclerView.adapter = adapter viewModel.itemPagedList.observe( this, Observer { adapter.submitList(it) } ) } }

Slide 19

Slide 19 text

X © DMM.com 1BHFE-JTU"EBQUFS internal class ItemAdapter(context: Context) : PagedListAdapter(ITEM_CALLBACK) { companion object { private val ITEM_CALLBACK = object : DiffUtil.ItemCallback() { 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) }

Slide 20

Slide 20 text

X © DMM.com 1BHFE-JTU"EBQUFS internal class ItemAdapter(context: Context) : PagedListAdapter(ITEM_CALLBACK) { companion object { private val ITEM_CALLBACK = object : DiffUtil.ItemCallback() { 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ʹ͢Δ͚ͩ

Slide 21

Slide 21 text

X © DMM.com ͜ΕͰ׬੒

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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ʹ৽͍͠σʔλΛอଘͯ͠ɺߋ৽Λ͔͚Δ

Slide 24

Slide 24 text

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( 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

Slide 25

Slide 25 text

X © DMM.com ͜ΕͰ׬੒ʁ

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

X © DMM.com ͳ͔ͥ dataSource?.invalidate() ८Γ८ͬͯ͜͜ʹ @Override protected PagedList 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; }

Slide 29

Slide 29 text

X © DMM.com @Override protected PagedList 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Λऔಘͯͦ͠ΕΛݩʹ࡞Γ௚͍ͯ͠Δ

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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; }

Slide 32

Slide 32 text

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ͳͷͰඞͣ࠷ॳͷϖʔδ͔ΒʹͳΔ

Slide 33

Slide 33 text

X © DMM.com 3PPNΛ࢖͏

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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͢Δ

Slide 36

Slide 36 text

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) @Query("DELETE FROM item") suspend fun deleteAll() @Query("SELECT * FROM item") fun selectAll(): DataSource.Factory } @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

Slide 37

Slide 37 text

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) @Query("DELETE FROM item") suspend fun deleteAll() @Query("SELECT * FROM item") fun selectAll(): DataSource.Factory } @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,

Slide 38

Slide 38 text

X © DMM.com #PVOEBSZ$BMMCBDLͰσʔλͷऔಘΛͯ͠%#ʹJOTFSU͢Δ internal class ItemBoundaryCallback( private val itemRepository: ItemRepository, private val inMemDatabase: InMemDatabase ) : PagedList.BoundaryCallback() { 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) } … } } …

Slide 39

Slide 39 text

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() }

Slide 40

Slide 40 text

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( this, R.layout.activity_item_list ) val adapter = ItemAdapter(this) binding.recyclerView.adapter = adapter viewModel.itemPagedList.observe( this, Observer { adapter.submitList(it) } ) } }

Slide 41

Slide 41 text

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)) } }

Slide 42

Slide 42 text

X © DMM.com ׬੒

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

X © DMM.com *O.FNPSZͰσʔλΛ؅ཧ͢Δ΋ͷΛ࡞Δ *UFN%BUB1SPWJEFS *UFN *UFN *UFN *UFN *UFN *UFN QBHF QBHF 3FQPTJUPSZ 1BHF୯Ґ *UFN,FZFE %BUB4PVSDF *UFN୯Ґ

Slide 46

Slide 46 text

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͢Δ

Slide 47

Slide 47 text

X © DMM.com *UFN%BUB1SPWJEFSΛ࣮૷͢Δ internal data class Page( val itemList: List, val isEndPage: Boolean ) internal class ItemDataProvider { val sourceLiveData = MutableLiveData>() private val _pageList = mutableListOf() val pageList: List get() = _pageList fun addPage(page: Page) { _pageList.add(page) sourceLiveData.value?.invalidate() } fun update(item: Item) { … sourceLiveData.value?.invalidate() } }

Slide 48

Slide 48 text

X © DMM.com *UFN#PVOEBSZ$BMMCBDLΛ࣮૷͢Δ internal class ItemBoundaryCallback( private val itemRepository: ItemRepository, private val itemDataProvider: ItemDataProvider ) : PagedList.BoundaryCallback() { 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) { … }

Slide 49

Slide 49 text

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) } }

Slide 50

Slide 50 text

X © DMM.com ׬੒

Slide 51

Slide 51 text

X © DMM.com ͓ΘΓʹ

Slide 52

Slide 52 text

X © DMM.com ͓ΘΓʹ w 1BHJOH͸ߋ৽͕ೖΓ࢝ΊΔͱਏ͍ w 3PPNͰ*O.FNPSZͳΒϚΠάϨʔγϣϯͱ͔΋ͳ͍ͷͰ͋Γ w ͜Ε͚ͩͷͨΊʹ3PPNΛೖΕΔͷ͸໨త͔Β֎ΕͯΔͷͰɺ3PPNͳ ͠Ͱ΍Δͷ΋͋Γ αϯϓϧίʔυ https://github.com/kgmyshin/paging-sample

Slide 53

Slide 53 text

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