I am working my through a new android application using Jetpack Compose (1.0.0-alpha08) and RxJava2 to manage the flow of data from my model (Realm 10 in this case. For a given screen, I have a view model that defines the data that will be subscribed to by the top level Compostable view. So, for example:
ViewModel...
class ListItemViewModel: ViewModel() {
val items: Flowable<Item>
get() {
val data1 = userRealm.where<Item1>()
.also(query).findAllAsync().asFlowable()
.onBackpressureLatest().doOnNext{System.out.println("Realm on Next")}
.observeOn(
Schedulers.single()
).filter{it.isLoaded}.map{ result ->
System.out.println("Maping Realm")
result
}.doOnSubscribe {System.out.println("Subscribe")}
val data2 == //same as above but with a different item
return Flowable.combineLatest(data1, data2, combineFunction)
.onBackpressureLatest()
.doOnNext{System.out.println("Hello")}
.doOnComplete {System.out.println("Complete")}
.subscribeOn(AndroidSchedulers.mainThread())
}
}
View
#Compostable
fun List(List<Item> items) {
val viewModel: ListItemViewModel = viewModel()
val list by viewModel.items.subscribeAsState(initial = listOf())
ItemList(list = list)
}
#Compostable
fun ItemList(List<Item> items {
LazyColumnFor(...) {
.......
}
}
Everything works as I would expect and the list renders on the screen as I want. However, what I assume would happen here is that the subscribe would only happen once and the Flowable would only push out new data as new data was emitted. As a result, I would only expect the various onNext methods to be triggered when new data was present in the stream, e.g. something changed in the realm db. As I am not adding/deleting any data to/from the Realm, once I have the first set of results, I would expect the flowable to go "silent".
However, when I run the above, the subscribe message related to the realm subscription is logged over and over. The same for the "Hello" and the other logging statements in the onNext methods. Also, if I add any logging in my combine function, I see those log statements in the same fashion as I see the "Hello" log. From this it seems like each time the List composable is being rendered, it resubscribes to the Flowable from my viewmodel and triggers the full process. As I said, I was expecting that this subscription would only happen once.
This is perhaps correct behaviour, but mentally, it feels like I am burning CPU cycles for no reason as my methods are being called over and over when no data has change. Am I setting things up correctly, or is there something flawed in how I have configured things?
I ultimately worked around the problem and took a hybrid approach where I used Realm/RXJava to handle the data flow and when things have changed, update a LiveData object.
View Model
private val internalItemList = MutableLiveData(listOf<Item>())
val itemList: LiveData<List<Item>> = internalItemList
//capture the subscription so you can dispose in onCleared()
val subscription = items.observeOn(AndroidSchedulers.mainThread()).subscribe {
this.internalItemList.value = it
}
View
val list by viewModel.itemList.observeAsState(listOf())
This is must less chatty and works as I want it to. Not sure if it is the correct way to do this, but it seems to be working
Related
I know this is a very documented topic, but I couldn't find a way to implement it in my project, even after spending hours trying to figure it out.
My root problem is that I have a RecyclerView with an Adapter whose content isn't updating as I'd like. I'm a beginner in Android, so I didn't implement any MVVM or such architecture, and my project only contains a repository, fetching data from Firebase Database, and passing it to a list of ShowModel, a copy of said list being used in my Adapter to display my shows (In order to filter/sort them without modifying the list with all shows).
However, when adding a show to the database from another Activity, my Adapter isn't displaying the newly added show (as detailed here)
I was told to use LiveData and ViewModel, but even though I started understanding how it works after spending time researching it, I don't fully get how I should use it in order to implement it in my project.
Currently I have the following classes:
The Adapter:
class ShowAdapter(private val context: MainActivity, private val layoutId: Int, private val textNoResult: TextView?) : RecyclerView.Adapter<ShowAdapter.ViewHolder>(), Filterable {
var displayList = ArrayList(showList)
class ViewHolder(view : View) : RecyclerView.ViewHolder(view){
val showName: TextView = view.findViewById(R.id.show_name)
val showMenuIcon: ImageView = view.findViewById(R.id.menu_icon)
}
#SuppressLint("NewApi")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
return ViewHolder(view)
}
#SuppressLint("NewApi", "WeekBasedYear")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentShow = displayList[position]
val index = holder.adapterPosition
holder.showName.text = currentShow.name
holder.itemView.setOnClickListener{ // Display show content
val intent = Intent(context, DetailsActivity::class.java)
intent.putExtra("position", index)
startActivity(context, intent, null)
}
holder.showMenuIcon.setOnClickListener{
val popupMenu = PopupMenu(context, it)
popupMenu.menuInflater.inflate(R.menu.show_management_menu, popupMenu.menu)
popupMenu.show()
popupMenu.setOnMenuItemClickListener {
when(it.itemId){
R.id.edit -> { // Edit show
val intent = Intent(context, AddShowActivity::class.java)
intent.putExtra("position", index)
startActivity(context, intent, null)
return#setOnMenuItemClickListener true
}
R.id.delete -> { // Delete show
val repo = ShowRepository()
repo.deleteShow(currentShow)
displayList.remove(currentShow)
notifyItemRemoved(index)
return#setOnMenuItemClickListener true
}
else -> false
}
}
}
}
override fun getItemCount(): Int = displayList.size
// Sorting/Filtering methods
}
The fragment displaying the adapter:
class HomeFragment : Fragment() {
private lateinit var context: MainActivity
private lateinit var verticalRecyclerView: RecyclerView
private lateinit var buttonAddShow: Button
private lateinit var showsAdapter: ShowAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
context = getContext() as MainActivity
buttonAddShow = view.findViewById(R.id.home_button_add_show)
buttonAddShow.setOnClickListener{ // Starts activity to add a show
startActivity(Intent(context, AddShowActivity::class.java))
}
verticalRecyclerView = view.findViewById(R.id.home_recycler_view)
showsAdapter = ShowAdapter(context, R.layout.item_show, null)
verticalRecyclerView.adapter = showsAdapter
return view
}
}
The MainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadFragment(HomeFragment())
}
private fun loadFragment(fragment: Fragment){
val repo = ShowRepository()
if(showsListener != null) databaseRef.removeEventListener(showsListener!!)
repo.updateData{
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragment)
transaction.addToBackStack(null)
if(supportFragmentManager.isStateSaved)transaction.commitAllowingStateLoss()
else transaction.commit()
}
}
}
The repository:
class ShowRepository {
object Singleton{
val databaseRef = FirebaseDatabase.getInstance().getReference("shows")
val showList = arrayListOf<ShowModel>()
var showsListener: ValueEventListener? = null
}
fun updateData(callback: () -> Unit){
showsListener = databaseRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
showList.clear()
for(ds in snapshot.children){
val show = ds.getValue(ShowModel::class.java)
if(show != null) showList.add(show)
}
callback()
}
override fun onCancelled(p0: DatabaseError) { }
})
}
fun insertShow(show: ShowModel){
databaseRef.child(show.id).setValue(show)
}
fun deleteShow(show: ShowModel){
databaseRef.child(show.id).removeValue()
}
}
From what I understand of LiveData and ViewModel, what I should do is creating a ShowViewModel containing a MutableLiveData<List<ShowModel>> containing the shows, and then observe it in my HomeFragment and update the adapter depending on the changes happening. However, everytime I start something to implement it, I encounter a situation where I'm lost and don't know what I should do, which leads me back to square one once again. I've been trying this for more than a week without progressing even a little bit, and that's why I'm here, hoping for some insight.
Sorry for the silly question and the absurd amount of informations, and hoping someone will be able to help me understand what I do wrong/should do.
(this ended up longer than I meant it to be - hope it's not too much! There's a lot to learn, but you don't have to make it super complicated at first)
Broadly, working backwards, it should go like this:
Adapter
displays whatever the Fragment tells it to (some kind of setData function that updates its internal list and refreshes)
passes events to the Fragment (deleteItem(item), showDetails(item) etc.) - don't have the Adapter doing things like starting Activites, that's not its responsibility
Fragment
grabs a reference to any ViewModels (only certain components like Fragments and Activities can actually "own" them)
observes any LiveData (or collects Flows if you're doing it that way) on the VM, and updates stuff in the UI in response
e.g. model.shows.observe(viewLifecycleOwner) { shows -> adapter.setData(shows) }
handles UI events and calls methods on the VM in response, e.g. click listeners, events from the Adapter
ViewModel
acts as a go-between for the UI (the Fragment) and the data layer (the repository)
exposes methods for handling events like deleting items, interacts with the data layer as required (e.g. calling the appropriate delete function)
exposes data state for the UI to observe, so it can react to changes/updates (e.g. a LiveData containing the current list of shows that the data layer has provided)
That's the basic setup - the VM exposes data which the UI layer observes and reacts to, by displaying it. The UI layer also produces events (usually down to user interaction) which are passed to the VM. You can read more about this general approach in this guide about app architecture - it's worth reading because not only is it recommended as a way to build apps, a lot of the components you use in modern Android are designed with this kind of approach in mind (like the reactive model of wiring stuff up).
You could handle the Adapter events like this:
// in your Adapter
var itemDeletedListener: ((Item) -> Unit)? = null
// when the delete event happens for an item
itemDeletedListener?.invoke(item)
// in your Fragment
adapter.itemDeletedListener = { viewModel.deleteItem(it) }
which is easier than implementing an interface, and lets you wire up your Adapter similar to doing setOnClickListener on a button. Notice we're passing the actual Item object here instead of a list index - generally this is easier to work with, you don't need to maintain multiple copies of a list just so you can look up an index given to you by something else. Passing a unique ID can make sense though, especially if you're working with a database! But usually the object itself is more useful and consistent
The data layer is the tricky bit - the ViewModel needs to communicate with that to get the current state. Say you delete an item - you then need to get the current, updated list of shows. You have three approaches:
Call the delete function, immediately after fetch the current data, and set it on the appropriate LiveData
This can work, but it's not very reactive - you're doing one action, then immediately doing another because you know your data is stale. It would be better if the new data just arrived automatically and you could react to that by pushing it out. The other issue is that calling the delete function might not have an immediate effect - if you fetch the current data, nothing might have changed yet. It's better if the data layer is responsible for announcing updates.
This is the simplest approach though, and probably a good start! You could run this task in a coroutine (viewModelScope.launch { // delete and fetch and update LiveData }) so any slowness doesn't block the current thread.
Have the data layer's functions return the current, updated data that results
Similar to above, you're just sort of pushing the fetching into the data layer. This requires all those functions to be written to return the current state, which could take a while! And depending on what data you want, this might be impossible - if you have an active query on some data, how does the function know what specific data to return?
Make the ViewModel observe the data it wants, so when the data layer updates, you get the results automatically
This is the recommended reactive approach - again it's that two-way idea. The VM calling a function on the data layer is completely separate from the VM receiving new data. One thing just happens as a natural consequence of the other, they don't need to be tied together. You just need to wire them up right!
How do you actually do that though? If you're working with something like Room, that's already baked in. Queries can return async data providers like LiveData or Flows - your VM just needs to observe those and expose the results, or just expose them directly. That way, when a table is updated, any queries (like the current shows) push a new value, and the observers receive it and do whatever they need to do, like telling the Adapter to display the data. It all Just Works once it's wired up.
Since you have your own repo, you need to expose your own data sources. You could have a currentShows LiveData or (probably preferably) the flow equivalent, StateFlow. When the repo initialises, and when any data is changed, it updates that currentShows data. Anything observing that (e.g. the VM, the Fragment through a LiveData/Flow that the VM exposes) will automatically get the new values. So broadly:
// Repo
// this setup is exactly the same as your typical LiveData, except you need an initial value
private val _currentShows = MutableStateFlow<List<Show>>(emptyList()) // or whatever default
val currentShows: StateFlow<List<Show>> = _currentShows
fun deleteItem(item: Item) {
// do the deletion
// get the updated show list
_currentShows.value = updatedShowList
}
// ViewModel
// one way of doing things - you have a lot of options! This literally just exposes
// the state from the data layer, and turns it into a LiveData (if you want that)
val currentShows = repo.currentShows.asLiveData()
// Fragment
// wire things up so you handle new data as it arrives
viewModel.currentShows.observe(viewLifecycleOwner) { shows -> adapter.setData(shows) }
That's basically it. I've skimmed over a lot because honestly, there's a lot to learn with this - especially about Flows and coroutines if you're not already familiar with those. But hopefully that gives you an overview of the general idea, and don't be afraid to take shortcuts (like just updating your data in the ViewModel by setting its LiveData values) while you're learning and getting the hang of it. Definitely give that app architecture guide a read, and also the guides for ViewModels and LiveData. It'll start to click when you get the general idea!
emit accepts the data class whereas emitSource accepts LiveData<T> ( T -> data ). Considering the following example :- I have two type of calls :-
suspend fun getData(): Data // returns directly data
and the other one ;
suspend fun getData(): LiveData<Data> // returns live data instead
For the first case i can use:-
liveData {
emit(LOADING)
emit(getData())
}
My question : Using the above method would solve my problem , WHY do we need emitSource(liveData) anyway ?
Any good use-case for using the emitSource method would make it clear !
As you mentioned, I don't think it solves anything in your stated problem, but I usually use it like this:
If I want to show cached data to the user from the db while I get fresh data from remote, with only emit it would look something like this:
liveData{
emit(db.getData())
val latest = webService.getLatestData()
db.insert(latest)
emit(db.getData())
}
But with emitSource it looks like this:
liveData{
emitSource(db.getData())
val latest = webService.getLatestData()
db.insert(latest)
}
Don't need to call emit again since the liveData already have a source.
From what I understand emit(someValue) is similar to myData.value = someValue whereas emitSource(someLiveValue) is similar to myData = someLiveValue. This means that you can use emit whenever you want to set a value once, but if you want to connect your live data to another live data value you use emit source. An example would be emitting live data from a call to room (using emitSource(someLiveData)) then performing a network query and emitting an error (using emit(someError)).
I found a real use-case which depicts the use of emitSource over emit which I have used many times in production now. :D The use-case:
Suppose u have some user data (User which has some fields like userId, userName ) returned by some ApiService.
The User Model:
data class User(var userId: String, var userName: String)
The userName is required by the view/activity to paint the UI. And the userId is used to make another API call which returns the UserData like profileImage , emailId.
The UserData Model:
data class UserData(var profileImage: String, var emailId: String)
This can be achieved internally using emitSource by wiring the two liveData in the ViewModel like:
User liveData -
val userLiveData: LiveData<User> = liveData {
emit(service.getUser())
}
UserData liveData -
val userDataLiveData: LiveData<UserData> = liveData {
emitSource(userLiveData.switchMap {
liveData {
emit(service.getUserData(it.userId))
}
})
}
So, in the activity / view one can ONLY call getUser() and the getUserData(userId) will be automatically triggered internally via switchMap.
You need not manually call the getUserData(id) by passing the id.
This is a simple example, imagine there is a chain of dependent-tasks which needs to be executed one after the other, each of which is observed in the activity. emitSource comes in handy
With emitSource() you can not only emit a single value, but attach your LiveData to another LiveData and start emitting from it. Anyway, each emit() or emitSource() call will remove the previously added source.
var someData = liveData {
val cachedData = dataRepository.getCachedData()
emit(cachedData)
val actualData = dataRepository.getData()
emitSource(actualData)
}
The activity that’s observing the someData object, will quickly receive the cached data on the device and update the UI. Then, the LiveData itself will take care of making the network request and replace the cached data with a new live stream of data, that will eventually trigger the Activity observer and update the UI with the updated info.
Source: Exploring new Coroutines and Lifecycle Architectural Components integration on Android
I will like share a example where we use "emit" and "emitsource" both to communicate from UI -> View Model -> Repository
Repository layer we use emit to send the values downstream :
suspend fun fetchNews(): Flow<Result<List<Article>>> {
val queryPath = QueryPath("tata", apikey = AppConstant.API_KEY)
return flow {
emit(
Result.success(
openNewsAPI.getResponse(
"everything",
queryPath.searchTitle,
queryPath.page,
queryPath.apikey
).articles
)
)
}.catch { exception ->
emit(Result.failure(RuntimeException(exception.message)));
}
}
ViewModel layer we use emitsource to pass the live data object to UI for subscriptions
val loader = MutableLiveData<Boolean>()
val newsListLiveData = liveData<Result<List<Article>>> {
loader.postValue(true)
emitSource(newRepo.fetchNews()
.onEach {
loader.postValue(false)
}
.asLiveData())
}
UI Layer - we observe the live data emitted by emitsource
viewModel.newsListLiveData.observe(viewLifecycleOwner, { result ->
val listArticle = result.getOrNull()
if (result.isSuccess && listArticle != null) {
setupList(binding.list, listArticle)
} else {
Toast.makeText(
appContext,
result.exceptionOrNull()?.message + "Error",
Toast.LENGTH_LONG
).show()
}
})
We convert Flow observable to LiveData in viewModel
I'm making an Android app using Kotlin with Firebase products. I have successful connections with Firestore and can successfully retrieve the data I want, but I am having difficulty displaying it within a RecyclerView.
When the application loads, and after a user has logged in, my Firestore queries use the UID of the user to get a list of their assignments. Using logs I can see that this occurs without issue as the home screen loads. Within the home screen fragment I have data binding for the RecyclerView and setup my ViewModel to have the fragment observe the returned Firestore data.
I believe it is a misunderstanding on my part on exactly how LiveData works because if I tap the bottom nav icon for the home screen to trigger a refresh of the UI then the list populates and I can use the app as desired. Therefore my observer/LiveData must not be setup properly as it is not automatically refreshing once the data has changed (null list to not null list).
As I'm new to programming I'm sure I've fallen into a number of pitfalls and done a few things incorrectly, but I've been searching through StackOverflow and YouTube for help on this issue for months now. Unfortunately I don't have all of the links saved to every video and every post.
I've tried tweaking the ViewModel and the Repository/Database class (singleton) to different effects and currently I'm at my best version with only a single tap required to refresh the UI. Previously it took multiple taps.
from the Database class
private val assignments = MutableLiveData<List<AssignmentModel>>()
private fun getUserAssignments(c: ClassModel) {
val assignmentQuery = assignmentRef.whereEqualTo("Class_ID", c.Class_ID)
assignmentQuery.addSnapshotListener { documents, _ ->
documents?.forEach { document ->
val a = document.toObject(AssignmentModel::class.java)
a.Assignment_ID = document.id
a.Class_Title = c.Title
a.Formatted_Date_Due = formatAssignmentDueDate(a)
assignmentMap[a.Assignment_ID] = a
}
}
}
fun getAssignments() : LiveData<List<AssignmentModel>> {
assignments.value = assignmentMap.values.toList().filter {
if (it.Date_Due != null) it.Date_Due!!.toDate() >= Calendar.getInstance().time else true }
.sortedBy { it.Date_Due }
return assignments
}
from the ViewModel
class AssignmentListViewModel internal constructor(private val myDatabase: Database) : ViewModel() {
private var _assignments: LiveData<List<AssignmentModel>>? = null
fun getAssignments() : LiveData<List<AssignmentModel>> {
var liveData = _assignments
if (liveData == null) {
liveData = myDatabase.getAssignments()
_assignments = liveData
}
return liveData
}
}
from the Fragment
class AssignmentList : Fragment() {
private lateinit var model: AssignmentListViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = AssignmentListBinding.inflate(inflater, container, false)
val factory = InjectorUtils.provideAssignmentListViewModelFactory()
model = ViewModelProvider(this, factory).get(AssignmentListViewModel::class.java)
val assignmentAdapter = AssignmentAdapter()
binding.assignmentRecycler.adapter = assignmentAdapter
updateUI(assignmentAdapter)
return binding.root
}
private fun updateUI(adapter: AssignmentAdapter) {
model.getAssignments().observe(this, Observer { assignments ->
if (assignments.isNotEmpty()) adapter.submitList(assignments)
})
}
}
Again, I expect the RecyclerView to populate automatically once the data from Firestore appears, but it doesn't. The screen remains empty until I tap the home screen button.
These snippets show the most recent changes I've made. Originally I had the Firestore query function returning the LiveData directly. I also had a much simpler ViewModel of something like fun getAssignments() = myDatabase.getAssignments().
Thanks for any and all help and advice.
When troubleshooting this issue, I'd recommend starting by looking at two things.
Take a look at where/when you're updating your LiveData
The goal is whenever the data in Firebase updates, your assignments LiveData updates your UI. Something like:
Firestore updates
Firestore triggers SnapshotListener
SnapshotListener updates LiveData
LiveData observer updates UI
So in your snapshot listener, you should be updating your LiveData, which is what I think you're missing. So it would be something like:
// Where you define your SnapshotListner
assignmentQuery.addSnapshotListener { documents, _ ->
// Process the data
documents?.forEach { document ->
val a = document.toObject(AssignmentModel::class.java)
a.Assignment_ID = document.id
a.Class_Title = c.Title
a.Formatted_Date_Due = formatAssignmentDueDate(a)
assignmentMap[a.Assignment_ID] = a
}
// Update your LiveData
assignments.value = assignmentMap.values.toList().filter {
if (it.Date_Due != null) it.Date_Due!!.toDate() >= Calendar.getInstance().time else true }
.sortedBy { it.Date_Due }
}
Now every time your Firestore updates, your LiveData will update and your UI should update.
Given the code change, getAssignments() can just return assignments. You can do this using a Kotlin backing property, covered here:
private val _assignments = MutableLiveData<List<AssignmentModel>>()
val assignments: LiveData<List<AssignmentModel>>
get() = _assignments
As for why it's not working at the moment, right now you call getAssignments() once on start up. This will filter an empty assignmentMap.values (I believe - might be worth checking), because when it's called, Firebase hasn't finished getting you any data. And when Firebase does get it's new data, it triggers the listener, but you don't update the LiveData.
Mind where you're setting up your listeners/observers
A tricky thing with LiveData observers and Firebase listeners is to make sure you only set them up once.
For your Firebase listener, you should be setting up the listener when you initialize your database and not every single time you call getUserAssignments. Then you wouldn't need all the null checking in the ViewModel, which essentially ensures that at least the ViewModel won't call getUserAssignments twice....but if you have other classes interacting with your database, they might call getUserAssignments multiple times and then you have tons of extra listeners.
Also, make sure you detach your listener.
One way to handle this is described in Doug Stevenson's talk Firebase and Android Jetpack: Fit Like a Glove - the talk includes a demo code here. The part that's related to this is how he handles LiveData -- notice how the class includes adding and removing the listener. The TL;DR is that he's using LiveData's lifecycle awareness to automatically do Firebase listener setup and cleanup. How that's done is a bit complicated, so I'd suggest watching the talk from here.
For your LiveData, setup/tear down looks correct since it's getting setup in onCreateView (and torn down automatically via the fact it's lifecycle aware). I might rename updateUI to something like setupUIObservation, since updateUI sounds like something you call multiple times. As with the Firebase listeners, you want to make sure you're not setting up the same LiveData observer more than once.
Hope that helps!
my app architecture, quite common:
Please explain me if I have list of Entities, for example
#Entity(tableName = TABLE_NAME)
class Item constructor(
#PrimaryKey(autoGenerate = false)
var id: Long = 0L,
#TypeConverters(ListToStringConverter::class)
var eventDescription: List<String> = emptyList(),
#TypeConverters(DateConverter::class)
var date: Date = Date(),
var rate: Int ?= null)
Picture explanation:
Currently I do (according to picture above):
mLiveData getLiveData from Repository
callbackrefreshFromDataBase()
mLiveData.addSource from LiveData of DataBase - what causes that Android View is updated quickly
callback refreshFromNetwork()
Rest updates DatabaseTable
Database insert causes that LiveData add pushes changes to the View
Formulation of the problem
What is the best practice for 5 step - a moment when new data comes and I have to replace old data with the freshest?
Currently I'm using RxJava, Room, Kotlin and I'm using in step 3 nested Rx.Single which is very ugly spagetti code.
like
disposable = usecase.getDataFromNetwork()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(onSuccess = {
val itemsList = it
dataBase.deleteAllItems()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy (onComplete = { dataBase.insertNewItems(itemsList)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(onComplete = {
// Process complete
}, onError = {
...
})
}
}, onError = {
...
})
}, onError = {
...
})
Ugly spagetti code.
QUESTIONS
Should I wipe all Table before inserting new Items? Livedata listen to the changes of List, so whenever anything changes (some item updated, some inserted, some removed) view will be updated
Under Circumstances where all my Streams are Single (reason: they dispose by itself when onComplete), how to chain them sequentially in oder to restrict one stream rule?
This depends if your fetched data will be similar or not. If you fresh data is normally completly different I would delete all elements and then insert new elements with only one method like following:
#Insert
public void insertItems(Item... items);
If your new data is similar and you don't care about their order I would compare all new items to the old ones to know for each item if you have to update, insert or delete item. At the end you call deleteItems(itemsToDelete), updateItems(itemsToUpdate) and insertItems(itemsToInsert). This three methods are similar as the one written above but with #Delete, #Update and #Insert.
If you care about the order I recommend you take a look at Android Paging Library and create a custom DataSource where you can tell to your live data when to update its values. Some links to start: this, this, this and this
First time implementing Room and Data Binding.
In my ViewModel:
var numberOfClubs = ObservableInt(0)
private var clubs: LiveData<List<Club>> = clubDao.getAllClubs()
I then try to get clubs.value after inserting into the Room database but am getting a KotlinNullPointerException every time.
fun addClubs() {
Observable.fromCallable({
clubDao.insertClubs(listOf(Club(null, "clubName"))
})
.subscribeOn(schedulerFacade.computation)
.subscribe(
{
numberOfClubs.set(clubs.value!!.size) <--- Kotlin NPE
},
{ error ->
logWrapper.e("MainViewModel", error.message!!)
clubError.set(true)
})
}
In my XML I'm calling: android:text="#{viewModel.numberOfClubs}".
As inserting into the Room database doesn't cause anything to be returned, I can't set numberOfClubs with the result. I assumed that clubs should track the changes?
Yes, as you figured out already LiveData is lifecycle aware. That means that it observe aslong as the livecycle owner is present.
Even if you've posted the solution already, you shouldnt use !! at all.
viewModel.clubs.observe(this, {
it?.let {
viewModel.numberOfClubs.set(it.size)
}
)
is the proper way doing a null check before assigning the size of your recieved list.
By the way, if you want to get the items size in your layout, you should use
text="#{viewModel.clubsList.size() + ``}"
and observe a list, not a list size using
val clubsList = ObservableArrayList<..>()
viewModel.clubs.observe(this, {
it?.let {
viewModel.clubsList.set(it)
}
)
Was a bit of a LiveData misunderstanding from myself. In order to get a LiveData object to begin emitting things is to .observe() it. As LiveData is lifecycle aware, .observe() is best called from within a Fragment or Activity as it has a reference to LifecycleOwner which must be the first parameter passed into .observe().
I'm perhaps not following best practices as I set an ObservableInt in the ViewModel from my Activity but I used:
viewModel.clubs
.observe(this, Observer { clubs ->
viewModel.numberOfClubs.set(clubs!!.size)
})