StartActivityForResult called twice using Room, Coroutine and ViewModel - android

I have problem when I come back in the activity from another.
In my activity I use a ViewModel and, when the activity is created I call challengeDayViewModel.daysForChallenge and I observe the changes. When data are ready I set them on the adapter. In the adapter when I click on item I call challengeDayViewModel.getChallengeDay(dayId) and observe for the result. When it is ready I start activity for result passing a property of the object retrieved. In the CountdownActivity I do some things and when I come back, in the onActivityResult method I update my data in DB using
challengeDayViewModel.setDayDone(selectedDayId) and the problem is here. Commenting the row where I update the data in DB everything work perfectly but if I add the row for the update, the update is done but after he re-run the rown startActivityForResult(intent, ActivityRequestCode.COUNTDOWN_CODE) opening again the CountdownActivity (I see in debug)
class ChallengeDayActivity: AppCompatActivity() {
private var selectedDayId: Long = 0
private lateinit var adapter: ChallengeDayAdapter
private val challengeDayViewModel: ChallengeDayViewModel by lazy {
ViewModelProvider.AndroidViewModelFactory.getInstance(application).create(ChallengeDayViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_challenge_day)
val recyclerView = challenge_day_recyclerview
adapter = ChallengeDayAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = GridLayoutManager(this, 3)
challengeDayViewModel.daysForChallenge.observe(this, Observer { challengeDayList ->
challengeDayList.let {
adapter.setChallengeDay(challengeDayList)
adapter.notifyDataSetChanged()
}
})
adapter.goToCountdown = { dayId ->
selectedDayId = dayId
challengeDayViewModel.getChallengeDay(dayId).observe(this, Observer { challengeDay ->
val intent = Intent(this, CountdownActivity::class.java).apply {
putExtra("seconds", challengeDay.seconds)
}
startActivityForResult(intent, ActivityRequestCode.COUNTDOWN_CODE)
})
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == ActivityRequestCode.COUNTDOWN_CODE && resultCode == Activity.RESULT_OK) {
challengeDayViewModel.setDayDone(selectedDayId)
Snackbar.make(coordinator, "BUBU", Snackbar.LENGTH_LONG).show()
}
}
}
My viewmodel is
class ChallengeDayViewModel: ViewModel() {
private val challengeDayRepository: ChallengeDayRepository
val daysForChallenge: LiveData<List<ChallengeDay>>
init {
val challengeDayDao = database.challengeDayDao()
challengeDayRepository = ChallengeDayRepository(challengeDayDao)
daysForChallenge = challengeDayRepository.getChallengeDayForStandardChallenge()
}
fun setDayDone(challengeDayId: Long) = viewModelScope.launch(Dispatchers.IO) {
challengeDayRepository.setDayDone(challengeDayId)
}
fun getChallengeDay(challengeDayId: Long): LiveData<ChallengeDay> = challengeDayRepository.getChallengeDay(challengeDayId)
}
My DAO
#Dao
interface ChallengeDayDao {
#Query("SELECT * FROM CHALLENGE_DAY WHERE CHD_ID_PK = :challengeDayId")
fun getChallengeDay(challengeDayId: Long): LiveData<ChallengeDay>
#Query("SELECT * FROM CHALLENGE_DAY WHERE CHG_ID_FK = :challengeId")
fun getChallengeDayForChallenge(challengeId: Long): LiveData<List<ChallengeDay>>
#Query("SELECT CHD.* FROM CHALLENGE_DAY CHD JOIN CHALLENGE CHG ON CHD.CHG_ID_FK = CHG.CHG_ID_PK WHERE CHG.CHG_STANDARD = 1")
fun getStandardChallengeDayForChallenge(): LiveData<List<ChallengeDay>>
#Query("UPDATE CHALLENGE_DAY SET CHD_DONE = 1 WHERE CHD_ID_PK = :challengeDayId")
suspend fun setDayDone(challengeDayId: Long)
#Insert
suspend fun insert(challengeDay: ChallengeDay)
#Insert
suspend fun insertAll(challengeDay: List<ChallengeDay>): List<Long>
}
In CountdownActivity for coming back I have this code
alert("Test message", "Test title") {
okButton {
setResult(Activity.RESULT_OK)
finish()
}
}.show()
Here the code for Repository
class ChallengeDayRepository(private val challengeDayDao: ChallengeDayDao) {
#WorkerThread
fun getChallengeDay(challengeDayId: Long): LiveData<ChallengeDay> = challengeDayDao.getChallengeDay(challengeDayId)
#WorkerThread
fun getAllChallengeDaysForChallenge(challengeId: Long): LiveData<List<ChallengeDay>> = challengeDayDao.getChallengeDayForChallenge(challengeId)
#WorkerThread
fun getChallengeDayForStandardChallenge(): LiveData<List<ChallengeDay>> = challengeDayDao.getStandardChallengeDayForChallenge()
#WorkerThread
suspend fun setDayDone(challengeDayId: Long) = challengeDayDao.setDayDone(challengeDayId)
#WorkerThread
suspend fun insert(challengeDay: ChallengeDay) = challengeDayDao.insert(challengeDay)
#WorkerThread
suspend fun insertAll(challengeDay: List<ChallengeDay>): List<Long> = challengeDayDao.insertAll(challengeDay)
}

Same question has already been asked in: this thread
Check answers in that post.
One of solutions to prevent observe duplication, would be also to use SingleEvent LiveData: - source article can be found here, meaning that you consume data only once.
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}

Inspired by this thread I write this that resolve my issue.
fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer<T>) {
observe(lifecycleOwner, object: Observer<T> {
override fun onChanged(t: T?) {
observer.onChanged(t)
removeObserver(this)
}
})
}

Related

How to implement multiple queries with room database?

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

How to get an id of an added object from an Activity(VIEW) in a database(MODEL) through VIEWMODEL | Android, MVVM

I have a ShoppingList App that is built on MVVM architecture Android.
I did not make it, but I followed a tutorial on Youtube.
This is the image of the app(1/2) where the shopping list is shown. The bottom right side is a button for adding new elements of the list.
This is the second view(2/2) where the dialog appears to enter a name of our element and amount of it. Here we have cancel button and add button.
The problem is when I click the ADD button on the Dialog Box I do not know how to get an ID of this added item to the recycler view on my VIEW and to make it appear via the TOAST command on my main Activity.
The question is - How to get an ID of a new added element to my shopping list and show it on my MainActivity(ShoppingActivity) VIEW when I click the ADD button?
If you need additional information ask me out immediately! I will provide you anything you need.
Code is provided here:
ShoppingActivity(View)
class ShoppingActivity : AppCompatActivity(), KodeinAware {
override val kodein by kodein()
private val factory: ShoppingViewModelFactory by instance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_shopping)
// View Model is being created out of other classes to set changes to View
val viewModel = ViewModelProviders.of(this, factory).get(ShoppingViewModel::class.java)
// Adapters and Recycler View
val adapter = ShoppingItemAdapter(listOf(), viewModel)
rvShoppingItems.layoutManager = LinearLayoutManager(this)
rvShoppingItems.adapter = adapter
// ViewModel makes changes to the Activity
viewModel.getAllShoppingItems().observe(this, Observer {
adapter.items = it
adapter.notifyDataSetChanged()
})
fab.setOnClickListener {
AddShoppingItemDialog(this ,
object: AddDialogListener{
override fun onAddButtonClicked(item: ShoppingItem) {
viewModel.upsert(item)
showToast(viewModel.getID(item).toString().toInt())
}
}).show()
}
}
fun showToast(id: Int) {
Toast.makeText(this#ShoppingActivity, "ID записи: $id", Toast.LENGTH_LONG).show()
}}
ShoppingViewModel(ViewModel)
class ShoppingViewModel(private val repository: ShoppingRepository): ViewModel() {
fun upsert(item: ShoppingItem) = CoroutineScope(Dispatchers.IO).launch {
repository.upsert(item)
}
fun delete(item: ShoppingItem) = CoroutineScope(Dispatchers.IO).launch {
repository.delete( item)
}
fun getID(item: ShoppingItem) = repository.getID(item)
fun getAllShoppingItems() = repository.getAllShoppingItems()
}
AddShoppingItemDialog(the logic of showing Dialog info)
class AddShoppingItemDialog(context: Context, var addDialogListener: AddDialogListener): AppCompatDialog(context) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_add_shopping_item)
tvAdd.setOnClickListener {
val name = etName.text.toString()
val amount = etAmount.text.toString()
if(name.isEmpty()) {
Toast.makeText(context, "Please enter the name", Toast.LENGTH_LONG).show()
return#setOnClickListener
}
if(amount.isEmpty()) {
Toast.makeText(context, "Please enter the amount", Toast.LENGTH_LONG).show()
return#setOnClickListener
}
val item = ShoppingItem(name, amount.toInt())
// We need to
addDialogListener.onAddButtonClicked(item)
dismiss()
}
tvCancel.setOnClickListener {
cancel()
}
}}
Repository
class ShoppingRepository(private val db: ShoppingDatabase) {
suspend fun upsert(item: ShoppingItem) = db.getShoppingDao().upsert(item)
suspend fun delete(item: ShoppingItem) = db.getShoppingDao().delete(item)
fun getID(item: ShoppingItem) = db.getShoppingDao().getID(item)
fun getAllShoppingItems() = db.getShoppingDao().getAllShoppingItems()}
ShoppingDAO
#Dao
interface ShoppingDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(item: ShoppingItem) : Long
#Delete
suspend fun delete(item: ShoppingItem)
#Query("SELECT * FROM shopping_items WHERE id = $CURRENT_POSITION")
fun getID(item: ShoppingItem): LiveData<Int>
#Query("SELECT * FROM shopping_items")
fun getAllShoppingItems(): LiveData<List<ShoppingItem>>
}
ShoppingItem
const val CURRENT_POSITION = 0
#Entity(tableName = "shopping_items")
data class ShoppingItem(
#ColumnInfo(name = "item_name")
var name: String,
#ColumnInfo(name = "item_amount")
var amount: Int
) {
#PrimaryKey(autoGenerate = true)
var id: Int? = CURRENT_POSITION
}
AddDialogListener
interface AddDialogListener {
fun onAddButtonClicked(item: ShoppingItem)
}
App View with added items
Since insert/upsert operations on Database are suspend functions, observe returned id in view model
In ShoppingViewModel
private var _itemId : Long = MutableLiveData<Long>()
val itemId : LiveData<Long>
get() = _itemId
fun upsert(item: ShoppingItem) = CoroutineScope(Dispatchers.IO).launch {
val id = repository.upsert(item)
_itemId.postValue(id)
}
In ShoppingActivity,
viewModel.itemId.observe(this, Observer {id ->
showToast(id)
})
Please let me know if you need to know more details.
You need to use SingleLiveEvent to observe the value only once.
Add this SingleLiveEvent class
/**
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
* navigation and Snackbar messages.
*
*
* This avoids a common problem with events: on configuration change (like rotation) an update
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
* explicit call to setValue() or call().
*
*
* Note that only one observer is going to be notified of changes.
*/
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
#MainThread
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
#MainThread
fun call() {
value = null
}
companion object {
private val TAG = "SingleLiveEvent"
}
}
In ShoppingViewModel
Replace this
private var _itemId : Long = MutableLiveData<Long>()
val itemId : LiveData<Long>
get() = _itemId
with
private var _itemId : Long = SingleLiveEvent<Long>()
val itemId : LiveData<Long>
get() = _itemId
You can use Event wrapper when there are more than one observer.
For more details:
https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150

How to observe LiveData<PagedList> with android paging in kotlin

I can't observe the LiveData<PagedList> change in activity, but the UI is updated(the list has grown in activty).
I can only observe it once when the livedata is initialized.
when the paging library call loadAfter method, the ui is updated, but didn't call pageList.observe{}
Firstly, I put the process of data request into the Kotlin Coroutines, I can't observe the data change, then I used asynchronous requests instead.It still didn't work.
Here is my code:
PlayActivity main code
private val commentAdapter =
object : BasePagedAdapter(diffCallback, this) {
// just bind recycleview item and corresponding view model. etc.
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_play)
binding.vm = vm
binding.lifecycleOwner = this
val workId = intent.getLongExtra(WORK_ID, 0)
vm.listComment(workId)
play_rv_comment.adapter = commentAdapter
/* herer is the problem*/
vm.commentList.observe(this, Observer {
/*only log once when called loadInitial*/
LogUtils.e("observe", it)
commentAdapter.submitList(it)
})
PlayViewModel
class PlayViewModel : BaseViewModel() {
var workId: Long = 0
// the data which I want to observe
lateinit var commentList: LiveData<PagedList<WorkComment>>
private val commentPageSize = 15
fun listComment(workId: Long) {
// init by DataSource.Factory in android paging library
commentList = BaseDataSourceFactory(workId).toLiveData(commentPageSize)
}
DataSource.Factory in Android paging
class BaseDataSourceFactory(
val workId: Long
) :
DataSource.Factory<Long, WorkComment>() {
override fun create(): DataSource<Long, WorkComment> {
return object : PageKeyedDataSource<Long, WorkComment>() {
override fun loadInitial(
params: LoadInitialParams<Long>,
callback: LoadInitialCallback<Long, WorkComment>
) {
try {
val res = RetrofitUtil.getInstanceWithJwt().create(WorkCommentApi::class.java)
.listComment(
workId, 1, params.requestedLoadSize
)
res.enqueue(object : retrofit2.Callback<TResult> {
override fun onFailure(call: Call<TResult>, t: Throwable) {
}
override fun onResponse(call: Call<TResult>, response: Response<TResult>) {
callback.onResult(
response.body()!!.toList(WorkComment::class.java),
null, 2)
}
})
} catch (e: SocketTimeoutException) {
ToastUtils.showShort("请稍候重试")
} catch (e: Exception) {
LogUtils.e(e.localizedMessage)
}
}
// called many times, but I can't observe the PagedList change
override fun loadAfter(
params: LoadParams<Long>,
callback: LoadCallback<Long, WorkComment>
) {
val res = RetrofitUtil.getInstanceWithJwt().create(WorkCommentApi::class.java)
.listComment(
workId, 1, params.requestedLoadSize
)
res.enqueue(object : retrofit2.Callback<TResult> {
override fun onFailure(call: Call<TResult>, t: Throwable) {
}
override fun onResponse(call: Call<TResult>, response: Response<TResult>) {
callback.onResult(
response.body()!!.toList(WorkComment::class.java),
params.key + 1
)
}
})
}
override fun loadBefore(
params: LoadParams<Long>,
callback: LoadCallback<Long, WorkComment>
) {
}
}
}
}
Retrofit Api
interface WorkCommentApi {
/**
* list comment
*/
#GET("public/work/comment")
fun listComment(#Query("workId") workId: Long, #Query("current") current: Long, #Query("size") size: Int): Call<TResult>
}
I want to know what should I do to observe the LiveData<PagedList> change
This is happening because each time you call vm.listComment(workId), the object you first bound in activity is killed and new object is created.
You can use Transformations with MediatorLiveData.
Activity:
viewModel.logout().observe(this, Observer {
// do here
})
ViewModel:
class RepackViewModel(app: Application) : BaseViewModel(app) {
// IMPORTANT - Mediator
val logout = MediatorLiveData<PagedList<WorkComment>>()
fun logout() : LiveData<PagedList<WorkComment>> = logout
init {
// IMPORTANT - passes repo update to activity
logout.addSource(repo.getLogoutResponse()) { logout.postValue(it) }
}
}
Repository:
class BaseRepository(val app: Application) {
private val logout = MutableLiveData<PagedList<WorkComment>>()
fun getLogoutResponse(): LiveData<PagedList<WorkComment>> = logout
override fun create(): DataSource<Long, WorkComment> {
//when you get your data
logout.value = // your value
}
You need to have your work id be mutable data to be observed by the transformation. so whenever you update your work, id, it will fetch comments. Like Thus...
ViewModel:
val workIdMutableLiveData: MutableLiveData<Int> = MutableLiveData(workId)
//This performs the meat of the work to display the items in the recyclerview
var commentsList = Transformations.switchMap(workIdMutableLiveData) { workId ->
val config = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(pagingLimit)
.build()
val pagedListBuilder = LivePagedListBuilder<Long, WorkComment>(BaseDataSourceFactory(workId), config)
pagedListBuilder.build()
}
Then in your activity, observe
yourViewModel.commentsList.observe(this, Observer { list ->
list ?: return#Observer
adapter.submitList(list)
yourRecyclerView.adapter = adapter
})
Whenever you update the workIdMutableLiveData by doing a
workIdMutableLiveData.postValue(workId)
...The recyclerview will update. Your recyclerview must inherit from PagedListAdapter.
After testing, I knew list couldn't be observed when it has inner data change, like add(), remove(). etc.
It only be observed when its reference has been changed, like create or assignment operation:
list.value = null
So I couldn't observe the data change of LiveData<List>

Observe not called with LiveData<PagedList>

I'm using the PagedList architectural components using a Room database and I am having trouble returning results to the observe method.
Here is my Dao:
#Dao
interface WorkPackageStorageDao {
#Query("SELECT * from workpackages where id = :id")
fun getById(id: String): Workpackage
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(workpackage: Workpackage)
#Query("Select * from workpackages WHERE id LIKE '%' || :searchString || '%' order by :orderBy")
fun searchWorkpackages(searchString : String, orderBy : String) : DataSource.Factory<Int, Workpackage>
#Query("SELECT * FROM workpackages")
fun searchWorkPackgesTest() : List<Workpackage>
#Query("Select * from workpackages WHERE id LIKE '%' || :searchString || '%' order by :orderBy")
fun searchWorkPackgesTestQuery(searchString : String, orderBy : String) : List<Workpackage>
#Query("DELETE from workpackages")
fun deleteAll()
}
My repository:
fun getAllWorkPackagesTestQuery() : List<Workpackage> {
return workpackagesDao.searchWorkPackgesTestQuery("",SortedBy.WorkPackageNumber.type)
}
fun getAllWorkPackages() : DataSource.Factory<Int, Workpackage> {
return getSortedAndSearchedWorkPackages("",
SortedBy.WorkPackageNumber
)
}
fun getSortedAndSearchedWorkPackages(searchString : String, sortBy: SortedBy) : DataSource.Factory<Int, Workpackage> {
return workpackagesDao.searchWorkpackages(searchString,sortBy.type)
}
Here is the method in my view model:
suspend fun fetchWorkPackagesInitial(
workWeek: Configurations.AppWeek,
pagingLimit: Int
) {
coroutineScope {
withContext(Dispatchers.IO) {
val factory: DataSource.Factory<Int, Workpackage> =
workPackageRepository.getAllWorkPackages()
val pagedListBuilder =
LivePagedListBuilder<Int, Workpackage>(factory, pagingLimit)
workPackagesList = pagedListBuilder.build()
val list = workPackageRepository.getAllWorkPackagesTestQuery() //27 Items returned, query is fine.
}
}
}
Here is my fragment:
mainViewModel.week.observe(this, Observer {
it ?: return#Observer
launch { workPackagesViewModel.fetchWorkPackagesInitial(it, PAGING_LIMIT) }
})
//Observe never called.
workPackagesViewModel.workPackagesList?.observe(this, Observer { wpList ->
wpList ?: return#Observer
adapter = WorkPackagesRecyclerAdapter(this)
adapter.submitList(wpList)
binding.workPackagesRecyclerView.adapter = adapter
adapter.notifyDataSetChanged()
})
As a test to my query, I've implemented:
val list = workPackageRepository.getAllWorkPackagesTestQuery()
which returns 27 items, so query is fine. Am I setting up the the Dao wrong, the LivePagedListBuilder wrong? Why is observe not called?
You're not getting items because it's a PagedList. You need to trigger the load in order to obtain pages.
That is why giving the PagedList to a PagedListAdapter via submitList will eventually load the data.
You also don't need to manually invoke adapter.notifyDataSetChanged() when you're using a PagedListAdapter, because the DiffUtil will handle that internally.
However, you should definitely be retrieving the DataSource.Factory and the LivePagedListBuilder and the PagedList on the UI thread (Dispatcher.MAIN), because threading is handled by the Paging lib's Room integration internally. Observing (and invoking getItem( on an unloaded element) will trigger the load, and the load will be executed asynchronously by the DataSource out of the box.
The way to use Paging is like this:
class MyViewModel(
private val workPackageStorageDao: WorkPackageStorageDao
): ViewModel() {
private val searchQuery: MutableLiveData<String> = MutableLiveData("")
val workPackages: LiveData<PagedList<WorkPackage>> = Transformations.switchMap(searchQuery) { searchText ->
val factory = workPackageStorageDao.searchWorkPackages(searchText, SortedBy.WorkPackageNumber)
val pagedListBuilder = LivePagedListBuilder<Int, WorkPackage>(factory, pagingLimit)
pagedListBuilder.build()
}
}
Then in Fragment:
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ... setup RecyclerView, etc
viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java, viewModelFactory)
viewModel.workPackages.observe(viewLifecycleOwner, Observer { pagedList ->
adapter.submitList(pagedList)
})
}
And in adapter:
class MyAdapter: PagedListAdapter<WorkPackage, MyAdapter.ViewHolder>(WorkPackage.ITEM_CALLBACK) {
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
val workPackage = getItem(position)
if(workPackage != null) {
holder.bind(workPackage)
}
}
}
Long story short, you don't need coroutines here. Use the Paging Library and LiveData, and it will load on the correct threads.

Observe Data from Room using View model in Kotlin

I am creating a social media app and I am following MVVM Pattern
I am stuck at observing data from DB . As far I know is the Dao methods have to be executed on the background thread. But I am not able to implement viewmodel from Repository Class.I will paste the code ,Can anyone give me some help?
Dao
#Dao
interface FeedDao{
//Post feed item
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(feed:Feed):Long
//Update feed item
#Update(onConflict = OnConflictStrategy.IGNORE)
fun update(feed: Feed):Int
//Get feed from user friend's
#Query("SELECT * from feed")
fun getAllFeed():LiveData<List<Feed>>
}
Repository
class FeedRepository private constructor(private val feedDao: FeedDao) {
companion object {
#Volatile private var instance:FeedRepository? = null
private val apiClient = ApiClient.getApiClient().create(UserClient::class.java)
fun getInstance(feedDao: FeedDao)= instance?: synchronized(this){
instance ?: FeedRepository(feedDao).also { instance=it }
}
}
private fun getFeedResponse() {
//TODO:Check to change parameter values after adding Paging Functionality
val call: Call<List<Feed>> = apiClient.getFeed(20, 0)
call.enqueue(object :Callback<List<Feed>>{
override fun onFailure(call: Call<List<Feed>>?, t: Throwable?) {
Log.e("FeedRepository","Feed Callback Failure")
Timber.d(t?.message)
}
override fun onResponse(call: Call<List<Feed>>?, response: Response<List<Feed>>?) {
if(response!!.isSuccessful){
Timber.i("Successful Response -> Adding to DB")
addResponseTODB(response.body()!!)
}else{
when(response.code()){
400 -> Timber.d("Not Found 400")
500 -> Timber.d("Not logged in or Server broken")
}
}
}
})
}
private fun addResponseTODB(items:List<Feed>){
Timber.d("Response --> DB Started")
object : AsyncTask<List<Feed>,Void,Boolean>(){
override fun doInBackground(vararg params: List<Feed>?): Boolean {
var needsUpdate:Boolean = false
for(item in params[0]!!.iterator()){
var inserted = feedDao.insert(item)
if(inserted.equals(-1)){
var updated = feedDao.update(item)
if(updated > 0){
needsUpdate = true
}
}else{
needsUpdate = true
}
}
return needsUpdate
}
override fun onPostExecute(result: Boolean?) {
if (result!!){
loadFromDB()
}
}
}.execute(items)
}
private fun loadFromDB(){
Timber.d("loadFromDB")
object :AsyncTask<Void,Void,LiveData<List<Feed>>>(){
override fun doInBackground(vararg params: Void?): LiveData<List<Feed>>? {
return feedDao.getAllFeed()
}
override fun onPostExecute(result: LiveData<List<Feed>>?) {
}
}.execute()
}
public fun fetchFeed(){
Timber.d("fetchFeed")
loadFromDB()
getFeedResponse()
}
}
ViewModel
class FeedViewModel private constructor(private val
feedDao:FeedDao):ViewModel(){
// TODO: Implement the ViewModel
private val feedRepository= FeedRepository.getInstance(feedDao)
public val feed = MediatorLiveData<List<Feed>>()
val value = MutableLiveData<Int>()
init {
feed.addSource(feed,feed::setValue)
}
companion object {
private const val NO_FEED = -1
}
fun getFeedLD() = feed
}
First, you don't need to insert all Feed objects one by one in the Database. You can create a method inside FeedDao which to insert List<Feed>
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(feed: List<Feed>):Long
Than, it is not necessary to call loadFromDB() inside the onPostExecute() method, because your fun getAllFeed(): LiveData<List<Feed>>() method from #FeedDao returns LiveData and you can add Observer to it and every time when the data inside the Database is changes the Observer will be triggered.
And for your Network/Database requests you always can use NetworkBoundResource https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/NetworkBoundResource.kt
Hope I helped somehow :)

Categories

Resources