Bonfire Android #5での発表資料です
!LHNZTIJO#POpSF"OESPJEΞΠςϜͷߋ৽1BHJOH-JCSBSZ
View Slide
X© DMM.comࣗݾհ
X© DMM.comࣗݾհw LHNZTIJOఝٶʢ͗͘Έʣw "OESPJEΤϯδχΞw ߹ಉձࣾ%..DPNw $50ࣨॴଐ
X© DMM.comΰʔϧ
X© DMM.comΰʔϧw ϖʔδϯάͷͭΒΈϙΠϯτ͕Θ͔Δw ϖʔδϯάͷͭΒΈϙΠϯτͷճආํ๏͕Θ͔Δ˞લఏͱͯ͠"1*࿈ܞͷͰ͢ɻ
X© DMM.comBHFOEB
X© DMM.comBHFOEBw ඪ४తͳ͍ํw ΞΠςϜͷΞοϓσʔτ͕Ͱ͖ͳ͍ʁw 3PPNΛ͏w 3PPNΛΘͣʹΔw ͓ΘΓʹ˞લఏͱͯ͠"1*࿈ܞͷͰ͢ɻ
X© DMM.comඪ४తͳ͍ํ
X© DMM.comϖʔδϯά
X© DMM.com࣮ͷྲྀΕw %BUB4PVSDFΛ࣮͢Δw %BUB4PVSDF'BDUPSZΛ࣮͢Δw -JWF1BHFE-JTU#VJMEFSͰ-JWF%BUB1BHFE-JTU5ΛCVJME͢Δw ͦΕΛPCTFSWFͯ͠1BHFE-JTU"EBQUFSʹTVCNJU-JTU͢Δ
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) {}}
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)}}
X© DMM.com%BUB4PVSDF'BDUPSZΛ࣮͢Δinternal class ItemDataSourceFactory(private val itemRepository: ItemRepository) : DataSource.Factory() {override fun create(): DataSource = ItemDataSource(itemRepository)}
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()}
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 = adapterviewModel.itemPagedList.observe(this,Observer {adapter.submitList(it)})}}
X© DMM.com1BHFE-JTU"EBQUFSinternal 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.idoverride fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean = oldItem == newItem}}var onItemClickListener: OnItemClickListener? = nullprivate 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)}
X© DMM.com1BHFE-JTU"EBQUFSinternal 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.idoverride fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean = oldItem == newItem}}var onItemClickListener: OnItemClickListener? = nullprivate 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ʹ͢Δ͚ͩ
X© DMM.com͜ΕͰ
X© DMM.comΞΠςϜͷΞοϓσʔτ͕Ͱ͖ͳ͍ʁ
X© DMM.cominternal 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ʹ৽͍͠σʔλΛอଘͯ͠ɺߋ৽Λ͔͚Δ
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
X© DMM.com͜ΕͰʁ
X© DMM.com݁Ռw ߋ৽Ͱ͖͍ͯΔw ͕ɺϦετ͕ઌ಄ʹͬͯ͠·͏
X© DMM.comͳ͔ͥdataSource?.invalidate()
X© DMM.comͳ͔ͥdataSource?.invalidate()८Γ८ͬͯ͜͜ʹ@Overrideprotected 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;}
X© DMM.com@Overrideprotected 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()͕͜͜QPJOUJOJUJBMJ[F,FZΛऔಘͯͦ͠ΕΛݩʹ࡞Γ͍ͯ͠Δ
X© DMM.com@Nullable Key initializeKey = initialLoadKey;if (mList != null) {initializeKey = (Key) mList.getLastKey();}ͳ͔ͥ@Nullable@Overridepublic Object getLastKey() {return mDataSource.getKey(mLastLoad, mLastItem);}ContiguousPagedList
X© DMM.com@Nullable Key initializeKey = initialLoadKey;if (mList != null) {initializeKey = (Key) mList.getLastKey();}ͳ͔ͥpublic Object getLastKey() {return mDataSource.getKey(mLastLoad, mLastItem);}ContiguousPagedListPageKeyedDataSourcefinal Key getKey(int position, Value item) {// don't attempt to persist keys, since we currently don't pass them to initial loadreturn null;}
X© DMM.com@Nullable Key initializeKey = initialLoadKey;if (mList != null) {initializeKey = (Key) mList.getLastKey();}ͳ͔ͥpublic Object getLastKey() {return mDataSource.getKey(mLastLoad, mLastItem);}ContiguousPagedListPageKeyedDataSourcefinal Key getKey(int position, Value item) {// don't attempt to persist keys, since we currently don't pass them to initial loadreturn null;}OVMMͳͷͰඞͣ࠷ॳͷϖʔδ͔ΒʹͳΔ
X© DMM.com3PPNΛ͏
X© DMM.com3PPNΛ͏w 3PPN1BHJOHʹରԠ͍ͯ͠Δw #PVOEBSZ$BMMCBDLͷதͰσʔλͷऔಘ3PPNͷJOTFSUΛ͢Δw ʢ*OWBMJEBUJPO5SBDLFSͱ͍͏ͷ͕͋ͬͯɺJOTFSU͢Δͱউखʹ%BUB4PVSDFJOWBMJEBUF͕Δʣw ߋ৽͞ΕΔ
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͢Δ
X© DMM.com%BUBCBTFɺ%BPΛ࣮͢Δ@Daointernal 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
X© DMM.com%BUBCBTFɺ%BPΛ࣮͢Δ@Daointernal 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,
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) returnboundaryCallbackScope.launch {val itemList = itemRepository.findAll(page = 0)if (itemList.isNotEmpty()) {bulkInsertToDatabase(itemList)}…}}…
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()}
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 = adapterviewModel.itemPagedList.observe(this,Observer {adapter.submitList(it)})}}
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))}}
X© DMM.com
X© DMM.com1BHJOHͷͨΊʹ%BUBCBTFΛಋೖʜʁw ӬଓԽ͢ΔΘ͚Ͱͳ͍ͷʹӬଓԽͷͨΊͷͷΛͬͯྑ͍ͷ͔ʜw *O.FNPSZ͔ͩΒϚΠάϨʔγϣϯ͕େมͱ͔ͳ͍w 3PPNΛΘͳ͍ํ๏͋Δ
X© DMM.com3PPNΛΘͣʹΔ
X© DMM.com*O.FNPSZͰσʔλΛཧ͢ΔͷΛ࡞Δ*UFN%BUB1SPWJEFS*UFN*UFN*UFN*UFN*UFN*UFNQBHFQBHF3FQPTJUPSZ1BHF୯Ґ*UFN,FZFE%BUB4PVSDF*UFN୯Ґ
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͢Δ
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: Listget() = _pageListfun addPage(page: Page) {_pageList.add(page)sourceLiveData.value?.invalidate()}fun update(item: Item) {…sourceLiveData.value?.invalidate()}}
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) returnboundaryCallbackScope.launch {val itemList = itemRepository.findAll(page = 0)itemDataProvider.addPage(page = Page(itemList = itemList,isEndPage = itemList.isEmpty()))}}override fun onItemAtEndLoaded(itemAtEnd: Item) { … }
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)}}
X© DMM.com͓ΘΓʹ
X© DMM.com͓ΘΓʹw 1BHJOHߋ৽͕ೖΓ࢝ΊΔͱਏ͍w 3PPNͰ*O.FNPSZͳΒϚΠάϨʔγϣϯͱ͔ͳ͍ͷͰ͋Γw ͜Ε͚ͩͷͨΊʹ3PPNΛೖΕΔͷత͔Β֎ΕͯΔͷͰɺ3PPNͳ͠ͰΔͷ͋Γαϯϓϧίʔυ https://github.com/kgmyshin/paging-sample
X© DMM.comαϯϓϧίʔυ͋Γ㽂w ϦʔυΦϯϦʔύλʔϯʢVQEBUFͳ͠ʣw ୯७ʹVQEBUF͚ͩͰ͖ΔΑ͏ʹͯ͠Έͨύλʔϯʢઌ಄ʹ͍͘Α͏ʹͳͬͯ͠·͏ࣦഊύλʔϯʣw 3PPN༻ύλʔϯw 3PPNෆ༻ύλʔϯhttps://github.com/kgmyshin/paging-sample