For about a week now I've been working on adding coroutines to my Room database. I'm using Googles ViewModels with recyclers that need to update as the database is modified.
I finally got something apparently working but I am new to Android, and Kotlin (6 months) and so I know enough to know I don't know enough and would like to get feedback on any design issues. I wasn't able to find a simple example that fit my needs but was able to piece this together from what I did find on stackoverflow, Google, and YouTube.
Main Fragment:
class MainFragment : Fragment()
{
...
private fun recyclerSetup()
{
adapter = MainListAdapter(R.layout.main_list_item)
val recyclerView: RecyclerView? = view?.findViewById(R.id.main_list_recycler)
recyclerView?.layoutManager = LinearLayoutManager(context)
recyclerView?.adapter = adapter
}
private fun addObservers()
{
Log.i(TAG, MyUtils.header0(this, object {}))
viewModel.reminderEntries?.observe(viewLifecycleOwner) { reminderEntries ->
adapter?.setReminderEntryList(reminderEntries)
}
}
...
}
*** Edit #1 ***
(Added Adapter Code)
Adapter:
class MainListAdapter(private val productItemLayout:Int):RecyclerView.Adapter<MainListAdapter.ViewHolder>()
{
private var reminderEntryList:List<ReminderEntry>?=null
override fun onBindViewHolder(holder: ViewHolder, listPosition: Int) {
val itemQuestion = holder.itemQuestion
val itemTime = holder.itemCorrectAnswer
reminderEntryList.let {
val reminderEntry: ReminderEntry = it!![listPosition]
itemQuestion.text = reminderEntry.question
itemTime.text = reminderEntry.correctAnser
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder
{
val view = LayoutInflater.from(parent.context).inflate(productItemLayout, parent, false)
return ViewHolder(view)
}
fun setReminderEntryList(reminderEntries:List<ReminderEntry>)
{
reminderEntryList = reminderEntries
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return if(reminderEntryList==null) 0 else reminderEntryList!!.size
}
class ViewHolder(itemView:View):RecyclerView.ViewHolder(itemView)
{
var itemQuestion:TextView = itemView.findViewById(R.id.question)
var itemCorrectAnswer:TextView = itemView.findViewById(R.id.correctAnswer)
}
}
ViewModel:
class MainViewModel(application: Application) : AndroidViewModel(application)
{
val repository: Repository = Repository(application)
val debugLogEntries = MutableLiveData<List<DebugLogEntry>>()
// ========================================================================
fun insertReminderLogEntry(reminderEntry: ReminderEntry)
{
viewModelScope.launch {
repository.insertReminderEntry(reminderEntry)
}
}
fun deleteAllReminderEntries()
{
viewModelScope.launch {
repository.deleteAllReminderEntries()
}
}
}
Repository:
class Repository(application: Application)
{
var reminderEntries: LiveData<List<ReminderEntry>>?
private var reminderEntryDao: ReminderEntryDao?
init
{
val db: MindfulnessDatabase? = MindfulnessDatabase.getDatabase(application)
reminderEntryDao = db?.reminderEntryDao()
reminderEntries = reminderEntryDao?.findAllReminderEntries()
}
// =============================================================================================
suspend fun insertReminderEntry(newDebugLogEntry: ReminderEntry)
{
reminderEntryDao?.insertReminderEntry(newDebugLogEntry)
}
suspend fun findAllReminderEntries(): LiveData<List<ReminderEntry>>?
{
return reminderEntryDao?.findAllReminderEntries()
}
suspend fun deleteAllReminderEntries()
{
reminderEntryDao?.deleteAllReminderEntries()
}
}
Dao:
#Dao
interface ReminderEntryDao
{
#Query("SELECT * FROM bluetooth_event_table")
fun findAllReminderEntries():LiveData<List<ReminderEntry>>
#Insert
#Transaction
suspend fun insertReminderEntry(reminderEntry: ReminderEntry?)
#Query("DELETE FROM bluetooth_event_table")
suspend fun deleteAllReminderEntries()
}
I'm still quite new to stackoverflow so if anything in my question needs clarification please ask in the comments.
Related
It's first time using Room Data while also using MVVM pattern. The aim is that I want my data to appeard on the RecyclerList but it's doesn't shut down nor shows me any error it's just appears empty.
Here is my Database class:
#Database(entities = [Plant::class, Plant_Category::class], version = 1)
abstract class PlantDatabase:RoomDatabase() {
abstract fun plantDao(): PlantOperations
abstract fun plantCategoryDao(): PlantCategoryOperations
companion object {
private var INSTANCE: PlantDatabase? = null
fun getDatabase(context: Context): PlantDatabase {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.applicationContext,
PlantDatabase::class.java, DB_NAME // contains directory of sqlite database
)
.fallbackToDestructiveMigration()
.build()
}
return INSTANCE!!
}
}
}
My dao class:
#Dao
interface PlantOperations {
#Query("SELECT * FROM Plant")
fun getAll(): Flow<List<Plant>>
#Insert
fun insertPlant( plant: Plant)
#Delete
fun delete(plant:Plant)
#Update
fun updatePlant(plant:Plant)}
This is my repository class:
class PlantRepository(application:Application){
private var allPlants = MutableLiveData<List<Plant>>()
private val plantDAO = PlantDatabase.getDatabase(application).plantDao()
init {
CoroutineScope(Dispatchers.IO).launch {
val plantData = plantDAO.getAll()
plantData.collect{
allPlants.postValue(it)
}
}
}
fun getAllPlants(): MutableLiveData<List<Plant>> {
return allPlants
}
}
My Viewmodel class:
class PlantViewModel(
application: Application
): AndroidViewModel(application) {
private var repository = PlantRepository(application)
private var _allPlants = repository.getAllPlants()
val allPlants: MutableLiveData<List<Plant>>
get() = _allPlants
}
My Recycler in Fragment:
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?): View? {
lateinit var photoAdapter: Photo_Adapter
lateinit var plantViewModel: PlantViewModel
val view: View = inflater.inflate(R.layout.fragment_edit__form, container, false)
val fab = view.findViewById(R.id.floatingActionButton) as FloatingActionButton
val recyclerView = view.findViewById(R.id.recyclerView) as RecyclerView
recyclerView.layoutManager = GridLayoutManager(context, 2)
photoAdapter = Photo_Adapter(context)
recyclerView.adapter = photoAdapter
plantViewModel = ViewModelProvider(this).get(PlantViewModel::class.java)
plantViewModel.allPlants.observe(viewLifecycleOwner, androidx.lifecycle.Observer {
photoAdapter.setDataList(it)
})
// photoAdapter.setDataList(dataList)
//Floating button that opens the Form in order to add plant
fab?.setOnClickListener {
val intent = Intent(view.context, Edit_Form::class.java)
startActivity(intent);
}
return view
}
This is my adapter class:
class Photo_Adapter(var context: Context?) : RecyclerView.Adapter<Photo_Adapter.ViewHolder>() {
var dataList = emptyList<Plant>()
internal fun setDataList(dataList: List<Plant>) {
this.dataList = dataList
notifyDataSetChanged()
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// Get the data model based on position
var data = dataList[position]
holder.title.text = data.name
holder.desc.text = data.type.toString()
holder.image.setImageResource(data.image)
holder.relativeLayout.setOnClickListener { view -> //Toast.makeText(view.getContext(),"click on item: "+model.getTitle(),Toast.LENGTH_LONG).show();
val intent = Intent(view.context, PlantDetails::class.java)
intent.putExtra("plant_name", data.name)
intent.putExtra("plant_image",data.image)
intent.putExtra("plant_type", data.type.type)
intent.putExtra("plant_water", data.type.water_time)
intent.putExtra("plant_details", data.type.details)
view.context.startActivity(intent)
}
}
// Provide a direct reference to each of the views with data items
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var image: ImageView
var title: TextView
var desc: TextView
var relativeLayout: CardView
init {
image = itemView.findViewById(R.id.image)
title = itemView.findViewById(R.id.title)
desc = itemView.findViewById(R.id.desc)
relativeLayout = itemView.findViewById<View>(R.id.relativeLayout) as CardView
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Photo_Adapter.ViewHolder {
// Inflate the custom layout
var view = LayoutInflater.from(parent.context).inflate(R.layout.photo_layout, parent, false)
return ViewHolder(view)
}
// total count of items in the list
override fun getItemCount() = dataList.size
}
Perhaps I forgot to add something? In Anyway I will be grateful for your help.
You should not observe data on your repository, your activity/view should observe this data. Take a look at this:
First, add this dependency to your gradle:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
Then in your repository
class PlantRepository(application:Application){
private val plantDAO = PlantDatabase.getDatabase(application).plantDao()
fun getAllPlants(): Flow<List<Plant>> = plantDAO.getAll()
}
In your view model:
class PlantViewModel(
application: Application
): AndroidViewModel(application) {
private var repository = PlantRepository(application)
val allPlants = repository.getAllPlants()
.flowOn(Dispatchers.IO)
.asLiveData()
}
And your activity is fine, but check your adapter, make sure that you're notifing your adapter that your list has changed.
You can also work (collect) flows on your view, but this depends on you
I have an app which uses Room Database to show data in recycleview. It works fine when i load data seperately from different tables. But i want to show data from both tables in a single recycleview with multiple viewtypes, i know how to combine tables in room but it's not working. I get empty cards in recycleview when i load the data. Here is what i have tried so far.
My Adapter Class
class CategoriesAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
private const val TYPE_CATEGORIES = 0
private const val TYPE_ARTICLES = 1
}
private val items: MutableList<Any> by lazy {
ArrayList<Any>()
}
fun setItems(list: List<Any>) {
items.addAll(list)
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int {
return if (items[position] is Categories) TYPE_CATEGORIES else TYPE_ARTICLES
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
TYPE_CATEGORIES -> CategoriesViewHolder.create(viewGroup)
else -> ArticlesViewHolder.create(viewGroup)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is CategoriesViewHolder -> {
if (items[position] is Categories)
holder.bind(items[position] as Categories)
}
is ArticlesViewHolder -> {
if (items[position] is Articles)
holder.bind(items[position] as Articles)
}
}
}
override fun getItemCount(): Int {
return items.size
}
}
class CategoriesViewHolder (parent: View) : RecyclerView.ViewHolder(parent) {
val textView: TextView = parent.findViewById(R.id.categories_textView)
fun bind(category: Categories) {
textView.text = category.categoryName
}
companion object {
fun create(parent: ViewGroup): CategoriesViewHolder {
return CategoriesViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.categories_item_layout, parent, false))
}
}
}
class ArticlesViewHolder (parent: View) : RecyclerView.ViewHolder(parent) {
val textView: TextView = parent.findViewById(R.id.titleText)
fun bind(articles : Articles) {
textView.text = articles.articleName
}
companion object {
fun create(parent: ViewGroup): ArticlesViewHolder {
return ArticlesViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.article_item_layout, parent, false))
}
}
}
this is how i set data from my activity
val db = AppDatabase.getDatabase(applicationContext)
dao = db.articleDao()
val recyclerView = findViewById<RecyclerView>(R.id.categories_recycle_view)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = CategoriesAdapter()
adapter.setItems(dao.getAllArticlesAndCategories())
Can anyone help.
P.s i'm new to kotlin
Instead of
adapter.setItems(dao.getAllArticlesAndCategories())
Use live data observer to avoid processing on main thread and debug in observe function of live data to confirm you are receiving correct data from DB.
calling code one line of code is missing
val db = AppDatabase.getDatabase(applicationContext)
dao = db.articleDao()
val recyclerView = findViewById<RecyclerView>(R.id.categories_recycle_view)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = CategoriesAdapter()
adapter.setItems(dao.getAllArticlesAndCategories())
it should be:
val db = AppDatabase.getDatabase(applicationContext)
dao = db.articleDao()
val recyclerView = findViewById<RecyclerView>(R.id.categories_recycle_view)
recyclerView.layoutManager = LinearLayoutManager(this)
adapter=CategoriesAdapter()
adapter.setItems(dao.getAllArticlesAndCategories())
recyclerView.adapter = adapter
I would like to thank for the question and the code
Im creating an android app using room database with MVVM pattern, the problem is that i cant use multiple queries when fetching data. I can fetch data once, but then i cant do it anymore.
DAO interface:
#Dao
interface StockDao {
#Insert
suspend fun insert(stock:Stock)
#Update
suspend fun update(stock:Stock)
#Delete
suspend fun delete(stock:Stock)
#Query("DELETE FROM stock_table")
suspend fun deleteAll()
#Query("SELECT * FROM stock_table")
fun selectAll():Flow<List<Stock>>
#Query("SELECT * FROM stock_table WHERE isFinished = 0")
fun selectAllUnfinished(): Flow<List<Stock>>
#Query("SELECT * FROM stock_table WHERE isFinished = 1")
fun selectAllFinished():Flow<List<Stock>>
#Query("SELECT * FROM stock_table ORDER BY totalSpent DESC")
fun selectAllOrderByDesc():Flow<List<Stock>>
#Query("SELECT * FROM stock_table ORDER BY totalSpent ASC")
fun selectAllOrderByAsc():Flow<List<Stock>>
}
Repository:
class StockRepository(private val stockDao: StockDao) {
private lateinit var allStock: Flow<List<Stock>>
suspend fun insert(stock: Stock) {
stockDao.insert(stock)
}
suspend fun update(stock: Stock) {
stockDao.update(stock)
}
suspend fun delete(stock: Stock) {
stockDao.delete(stock)
}
suspend fun deleteAll() {
stockDao.deleteAll()
}
fun selectAll(): Flow<List<Stock>> {
allStock = stockDao.selectAll()
return allStock
}
fun selectAllOrderByDesc(): Flow<List<Stock>> {
allStock = stockDao.selectAllOrderByAsc()
return allStock
}
fun selectAllOrderByAsc(): Flow<List<Stock>> {
allStock = stockDao.selectAllOrderByAsc()
return allStock
}
fun selectAllFinished(): Flow<List<Stock>> {
allStock = stockDao.selectAllFinished()
return allStock
}
fun selectAllUnfinished(): Flow<List<Stock>> {
allStock = stockDao.selectAllUnfinished()
return allStock
}
}
Viewmodel class:
class StockViewModel(private val repo: StockRepository) : ViewModel() {
companion object {
const val ALL = 0
const val ORDER_BY_DESC = 1
const val ORDER_BY_ASC = 2
const val FINISHED = 3
const val UNFINISHED = 4
}
var allStocks = repo.selectAll().asLiveData()
fun insert(stock: Stock) = viewModelScope.launch {
repo.insert(stock)
}
fun update(stock: Stock) = viewModelScope.launch {
repo.update(stock)
}
fun delete(stock: Stock) = viewModelScope.launch {
repo.delete(stock)
}
fun deleteAll() = viewModelScope.launch {
repo.deleteAll()
}
fun selectAllStockWithFilter(filter: Int): LiveData<List<Stock>> {
when (filter) {
ALL -> allStocks = repo.selectAll().asLiveData()
ORDER_BY_DESC -> allStocks = repo.selectAllOrderByDesc().asLiveData()
ORDER_BY_ASC -> allStocks = repo.selectAllOrderByAsc().asLiveData()
FINISHED -> allStocks = repo.selectAllFinished().asLiveData()
UNFINISHED -> allStocks = repo.selectAllUnfinished().asLiveData()
}
return allStocks
}
class StockViewModelFactory(private val repo: StockRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(StockViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return StockViewModel(repo) as T
}
throw IllegalArgumentException("Unknown viewModel class")
}
}
}
Application class:
class FinanceApplication :Application(){
private val database by lazy { FinanceDatabase.getInstance(this)}
val stockRepository by lazy { StockRepository(database.stockDao()) }
}
Activity using this viewmodel :
class StocksActivity : AppCompatActivity() {
//Layout components
private lateinit var binder: ActivityStocksBinding
private lateinit var recyclerView: RecyclerView
private lateinit var recyclerViewAdapter: StockAdapter
//ViewModel
private val viewModel: StockViewModel by viewModels {
StockViewModel.StockViewModelFactory((application as FinanceApplication).stockRepository)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binder = ActivityStocksBinding.inflate(layoutInflater)
setContentView(binder.root)
fetchStocks()
}
private fun fetchStocks() {
viewModel.allStocks.observe(this) {
recyclerViewAdapter.submitList(it)
}
}
private fun initRecyclerViewLayout() {
val recyclerViewLayoutBinder = binder.includedLayout
recyclerView = recyclerViewLayoutBinder.stocksRecyclerView
recyclerViewAdapter = StockAdapter(this)
recyclerView.adapter = recyclerViewAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.setHasFixedSize(true)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_stock_toolbar, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_stock_toolbar_filter_all -> viewModel.selectAllStockWithFilter(StockViewModel.ALL)
R.id.menu_stock_toolbar_filter_maior_menor -> viewModel.selectAllStockWithFilter(StockViewModel.ORDER_BY_DESC)
R.id.menu_stock_toolbar_filter_menor_maior -> viewModel.selectAllStockWithFilter(StockViewModel.ORDER_BY_ASC)
R.id.menu_stock_toolbar_filter_finalized -> viewModel.selectAllStockWithFilter(StockViewModel.FINISHED)
R.id.menu_stock_toolbar_filter_opened -> viewModel.selectAllStockWithFilter(StockViewModel.UNFINISHED)
}
return true
//NOTHIN HAPPENS AFTER CHOOSING ONE
}
}
When i enter the activity, all the data is fetched normally, but when i click on a menu item to apply some filter on it, nothing happens, the data doesnt change. How can i fix this?
allStocks may seem dynamic because it's LiveData, but remember that it's still a reference to an object in memory. When StocksActivity is created, it observes allStocks in it's initial state. For the sake of simplicity, let's say allStocks is pointing to an object in memory with the address of "A". When selectAllStockWithFilter() is eventually invoked, the allStocks handle is updated to point to a new instance of LiveData living in memory at address "B". The problem you're facing is that StocksActivity is still observing "A". Nothing communicated that the allStocks handle itself has been changed.
One way to resolve this would be to change allStocks into an instance of MutableLiveData. Subsequently, whenever the contents of this allStocks should be updated, instead of reassigning allStocks, you would update it's internal "value". This allows the ViewModel to pump new/updated values through the same LiveData object instance that StocksActivity is observing.
Something like this:
class StockViewModel(private val repo: StockRepository) : ViewModel() {
...
val allStocks = MutableLiveData<List<Stock>>().apply { value = repo.selectAll() }
...
fun selectAllStockWithFilter(filter: Int) {
when (filter) {
ALL -> allStocks.postValue(repo.selectAll())
ORDER_BY_DESC -> allStocks.postValue(repo.selectAllOrderByDesc())
ORDER_BY_ASC -> allStocks.postValue(repo.selectAllOrderByAsc())
FINISHED -> allStocks.postValue(repo.selectAllFinished())
UNFINISHED -> allStocks.postValue(repo.selectAllUnfinished())
}
}
...
}
I have Room Entity Class "Symptom" with name of Symptom and id of it.
#Entity(tableName = "symptoms")
data class Symptom(
#PrimaryKey #NonNull val id: Int,
val name: String) {
override fun toString(): String {
return "Symptom $id: $name"
}
}
I'm getting it in the following classses:
SymptomDao
#Dao
interface SymptomDao {
#Query("SELECT * FROM symptoms WHERE id=:id LIMIT 1")
fun getSymptom(id: Int): Symptom
#Query("SELECT * FROM symptoms")
fun getAllSymptoms(): LiveData<List<Symptom>>
}
SymptomRepository
class SymptomRepository(private val symptomDao: SymptomDao) {
fun getSymptom(id: Int) = symptomDao.getSymptom(id)
fun getAllSymptoms() = symptomDao.getAllSymptoms()
}
SymptomsViewModel
class SymptomsViewModel(symptomRepository: SymptomRepository): ViewModel() {
private val symptomsList = symptomRepository.getAllSymptoms()
private val symptomsItemsList: MutableLiveData<List<SymptomItem>> = MutableLiveData()
fun getAllSymptoms(): LiveData<List<Symptom>> {
return symptomsList
}
fun getAllSymptomsItems(): LiveData<List<SymptomItem>> {
return symptomsItemsList
}
}
I have RecyclerView with list of SymptomItem with Checkboxes to remember which Symptoms of a list users chooses:
data class SymptomItem(
val symptom: Symptom,
var checked: Boolean = false)
Question
My question is how can I get LiveData<List<SymptomItem>> by LiveData<List<Symptom>>? I have just started learning MVVM and I can't find a simply answer how to do that. I have already tried to fill this list in various ways, but It loses checked variable every time I rotate my phone. I'll be grateful for any hints.
You'll need to store which items are checked by storing their Ids in a List within the ViewModel. Then you'll have combine the list of your Symptom objects and the list of which items are checked, and generate the list of SymptomItem objects.
I'm going to use Kotlin Flow to achieve this.
#Dao
interface SymptomDao {
#Query("SELECT * FROM symptoms")
fun flowAllSymptoms(): Flow<List<Symptom>>
}
class SymptomRepository(private val symptomDao: SymptomDao) {
fun flowAllSymptoms() = symptomDao.flowAllSymptoms()
}
class SymptomsViewModel(
private val symptomRepository: SymptomRepository
) : ViewModel() {
private val symptomsListFlow = symptomRepository.flowAllSymptoms()
private val symptomsItemsList: MutableLiveData<List<SymptomItem>> = MutableLiveData()
private var checkedIdsFlow = MutableStateFlow(emptyList<Int>())
init {
viewModelScope.launch {
collectSymptomsItems()
}
}
private suspend fun collectSymptomsItems() =
flowSymptomsItems().collect { symptomsItems ->
symptomsItemsList.postValue(symptomsItems)
}
private fun flowSymptomsItems() =
symptomsListFlow
.combine(checkedIdsFlow) { list, checkedIds ->
list.map { SymptomItem(it, checkedIds.contains(it.id)) }
}
fun checkItem(id: Int) {
(checkedIdsFlow.value as MutableList<Int>).add(id)
checkedIdsFlow.value = checkedIdsFlow.value
}
fun uncheckItem(id: Int) {
(checkedIdsFlow.value as MutableList<Int>).remove(id)
checkedIdsFlow.value = checkedIdsFlow.value
}
fun getSymptomsItems(): LiveData<List<SymptomItem>> {
return symptomsItemsList
}
}
In your Fragment, observe getSymptomsItems() and update your adapter data.
The code is not tested, you may have to make small adjustments to make it compile.
I am trying to practice the android architecture components Paging
Local + Remote Datasource with Room, MVVM and LiveData
When i first time scroll the list(get remote data), it get into loop by onItemAtEndLoaded in PagedList.BoundaryCallback, but it scroll smooth when open the activity next time (get local data)
Here is my github link here!
Can anyone take a look and help me how to fix it, Thanks!
Activity
class PagingActivity : AppCompatActivity() {
lateinit var viewModel: PagingViewModel
lateinit var adapter: PagingAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_paging)
val factory = PagingViewModelFactory(PagingRepository(), application)
viewModel = ViewModelProviders.of(this,factory).get(PagingViewModel::class.java)
adapter = PagingAdapter()
recyclerView.adapter = adapter
viewModel.pagedListLiveData.observe(this, Observer {
adapter.submitList(it)
})
}
}
ViewModel
class PagingViewModel(repository: PagingRepository, application: Application) :
AndroidViewModel(application) {
val pagedListLiveData = repository.getDataItem(application)
}
Repository
class PagingRepository : PagingRepositoryCallback {
private lateinit var localDataSource: DataSource.Factory<Int, DataItem>
override fun getDataItem(application: Application): LiveData<PagedList<DataItem>> {
val pagedListLiveData: LiveData<PagedList<DataItem>> by lazy {
localDataSource = DataItemDbHelper(application).getRoomDataItemDao().getAllDataItem()
val config = PagedList.Config.Builder()
.setPageSize(25)
.setEnablePlaceholders(false)
.build()
LivePagedListBuilder(localDataSource, config)
.setBoundaryCallback(PagingBoundaryCallback(application))
.build()
}
return pagedListLiveData
}
}
interface PagingRepositoryCallback {
fun getDataItem(application: Application): LiveData<PagedList<DataItem>>
}
BoundaryCallback
class PagingBoundaryCallback(context: Context) :
PagedList.BoundaryCallback<DataItem>() {
private var page = 2
private val api = AllPlayerApi.api
private val dao = DataItemDbHelper(context).getRoomDataItemDao()
override fun onZeroItemsLoaded() {
super.onZeroItemsLoaded()
api.getAllPlayer().enqueue(createWebserviceCallback())
}
override fun onItemAtEndLoaded(itemAtEnd: DataItem) {
super.onItemAtEndLoaded(itemAtEnd)
api.getAllPlayer(page).clone().enqueue(createWebserviceCallback())
}
private fun createWebserviceCallback(): Callback<AllPlayerData> {
return object : Callback<AllPlayerData> {
override fun onFailure(call: Call<AllPlayerData>?, t: Throwable?) {
Log.d("Huang", " get player fail ")
}
override fun onResponse(call: Call<AllPlayerData>?, response: Response<AllPlayerData>) {
Log.d("Huang", " onResponse " + page)
response.body()!!.data!!.forEach {
it.imageUrl = "https://pdc.princeton.edu/sites/pdc/files/events/new-nba-logo-1.png"
}
insertItemsIntoDb(response)
page++
}
}
}
private fun insertItemsIntoDb(response: Response<AllPlayerData>) {
GlobalScope.launch {
response.body()!!.data!!.forEach {
dao.insert(it)
}
}
}
}
Logic for, If onItemAtEndLoaded get the same itemAtEnd , then do nothing.
var lastItemAtEnd:DataItem? = null
override fun onItemAtEndLoaded(itemAtEnd: DataItem) {
lastItemAtEnd?.timestamp?.apply{
if(itemAtEnd.timestamp==this){
return;
}
}
super.onItemAtEndLoaded(itemAtEnd)
api.getAllPlayer(page).clone().enqueue(createWebserviceCallback())
}
As your page size is 25 so Pagelist config should have setInitialLoadSizeHint as 25 for avoiding looping/unnecessary call of onItemAtEndLoaded method
val config = PagedList.Config.Builder()
.setPageSize(25)
.setInitialLoadSizeHint(25) //same as your page size
.setEnablePlaceholders(false)
.build()
I know it's been long but i just post the solution in case someone need.
you should register an observer for your adapter and listen for onItemRangeInserted event and if the start position of item range is zero just simply scroll adapter to zero position, this make your RecyclerView on first load stay in zero position and by the way you should set setPrefetchDistance value smaller than the setInitialLoadSizeHint.
This is the Java code for adapters observer
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
if(positionStart == 0)
recyclerView.scrollToPosition(positionStart);
}
});