Observe Data from Room using View model in Kotlin - android

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

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 save data to database after fetching data from server using room in kotlin coroutines?

I am developing news app I want to save data to database after fetching data from server using room but I am just confused I have followed tutorial but their logics is different how can I save data properly so that after getting data from server and I can save data to database?
below my SportNewsDatabase.kt
#Database(entities = [Article::class], version = 1, exportSchema = false)
abstract class SportNewsDatabase : RoomDatabase() {
abstract fun sportNewsDao(): SportNewsDao
companion object {
private var instance: SportNewsDatabase? = null
fun getInstance( context: Context): SportNewsDatabase? {
if (instance == null) {
synchronized(SportNewsDatabase::class.java) {
instance = Room.databaseBuilder(context.applicationContext, SportNewsDatabase::class.java, "article_database")
.fallbackToDestructiveMigration()
.build()
}
}
return instance
}
}
}
below SportNewsDao.kt
#Dao
interface SportNewsDao {
#Query("SELECT * FROM article")
fun getAllData(): LiveData<List<Article>>
#Insert
suspend fun addAll(article: List<Article>)
#Update
suspend fun updateArticle(article: Article)
#Delete
suspend fun deleteArticle(article: Article)
}
below my repository class
interface NewsRepository {
// Suspend is used to await the result from Deferred
suspend fun getNewsList(): UseCaseResult<List<Article>>
var sportNewsDao: SportNewsDao
}
fun insertArticle(article: List<Article>){
val insertArticles
}
#Suppress("UNCHECKED_CAST")
class NewsRepositoryImpl(private val sportsNewsApi: SportNewsInterface) : NewsRepository {
override suspend fun getNewsList(): UseCaseResult<List<Article>> {
return try {
val result = sportsNewsApi.getNewsAsync().body()!!.articles
UseCaseResult.Success(result)
} catch (ex: Exception) {
UseCaseResult.Error(ex)
}
}
}
below MainViewModel.kt where I am using Kotlin coroutines
#Suppress("UNCHECKED_CAST")
class MainViewModel(val newsRepository: NewsRepository) : ViewModel(), CoroutineScope {
// Coroutine's background job
val job = Job()
// Define default thread for Coroutine as Main and add job
override val coroutineContext: CoroutineContext = Dispatchers.Main + job
val showLoading = MutableLiveData<Boolean>()
val sportList = MutableLiveData<List<Article>>()
val showError = SingleLiveEvent<String>()
fun loadNews() {
// Show progressBar during the operation on the MAIN (default) thread
showLoading.value = true
// launch the Coroutine
launch {
// Switching from MAIN to IO thread for API operation
// Update our data list with the new one from API
val result = withContext(Dispatchers.IO) {
newsRepository?.getNewsList()
}
// Hide progressBar once the operation is done on the MAIN (default) thread
showLoading.value = false
when (result) {
is UseCaseResult.Success<*> -> {
sportList.value = result.data as List<Article>
}
is Error -> showError.value = result.message
}
}
}
override fun onCleared() {
super.onCleared()
// Clear our job when the linked activity is destroyed to avoid memory leaks
job.cancel()
}
}
#sashabeliy
Hi, i think i have the solution of your problem but i am not a expert in mvvm and coroutine so if someone have a better solution i take it :)
For my app i launch the the download in the main thread and only when i get the response i use Dispatchers.IO for insert in Room the data received.
In ViewModel
fun downLoadPDV(last_update : String) : LiveData<List<PDV>> {
return repositoryPDV.downloadPDV(last_update)
}
In Repository
fun downloadPDV(last_update : String ): LiveData<List<PDV>> {
val mutableLiveData: MutableLiveData<List<PDV>> = MutableLiveData()
val apiService: ApiInterface = RetrofitClient.getRetrofitClient().create(ApiInterface::class.java)
apiService.CallListPDV(last_update)?.enqueue(object :
Callback<List<PDV>> {
override fun onResponse(
call: Call<List<PDV>>,
response: Response<List<PDV>>
) {
Log.e(ContentValues.TAG, "Download PDV Success => $response")
if (response.isSuccessful && response.body() != null) {
mutableLiveData.setValue(response.body());
GlobalScope.launch(Dispatchers.IO) {
for (pdv in response.body()!!) {
insertPDV(pdv)
}
}
}
}
override fun onFailure(call: Call<List<PDV>>, t: Throwable) {
Log.e(ContentValues.TAG, "[Error] Download PDV Fail --> $call")
}
})
return mutableLiveData
}
I return the Data download for update the UI and inform the user of how many items was downloaded

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>

StartActivityForResult called twice using Room, Coroutine and ViewModel

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

Categories

Resources