Firebase database query design - android

I have basically a list of entries that many users can read (but can't write). These items show up sorted in the app based on a unique integer that each entry has. I'd like to add a way to allow each individual user to favorite some x number of these items, making those x items appear first in the list. Is there a way I can achieve this using firebase's querying without having to duplicate the list for each user?

Rather than using FirebaseRecyclerAdapter and just passing in the query (which is what I was previously doing), I had to manually manage the results and use a RecyclerView.Adapter instead. This is the code that I ended up with. I'll clean it up a bit so it actually replaces items if they're already there and update and whatnot, but this is a quick summary of the necessary approach:
val data = ArrayList<SomeData>
val favorites = ArrayList<Favorite>
val dataWithFavorites = ArrayList<SomeData>
private val dbRef : DatabaseReference by lazy { FirebaseDatabase.getInstance().reference }
private val dataQuery : Query by lazy { dbRef.child("data").orderByChild("rank") }
private val favoritesQuery : Query by lazy { dbRef.child("users").child("$someUserId").child("favorites").orderByChild("name") }
init {
dataQuery.addValueEventListener(object : ValueEventListener {
override fun onCancelled(p0: DatabaseError?) { }
override fun onDataChange(p0: DataSnapshot?) {
data.clear()
p0!!.children.mapTo(dataList) { it.getValue(SomeData::class.java)!! }
updateEntries()
}
})
favoritesQuery.addValueEventListener(object : ValueEventListener {
override fun onCancelled(p0: DatabaseError?) { }
override fun onDataChange(p0: DataSnapshot?) {
favorites.clear()
p0!!.children.mapTo(dataList) { it.getValue(Favorite::class.java)!! }
updateEntries()
}
})
}
private fun updateEntries() {
if (data.isEmpty()) {
return
}
val favoritesStrings = favorites.map { (id) -> id }
val favoriteData = data
.filter { favoritesStrings.contains(it.id) }
.sortedBy { it.name }
.onEach { it.isFavorite = true }
dataWithFavorites.clear()
dataWithFavorites.addAll(data)
dataWithFavorites.removeAll(favoriteData)
dataWithFavorites.forEach { it.isFavorite = false }
dataWithFavorites.addAll(0, favoriteData)
recyclerView.adapter?.notifyDataSetChanged()
}

Related

How to read asynchronous data from real-time database using android Kotlin?

Here is my code to read asynchronous data from a real-time database using android Kotlin:
class suDetails : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_su_details)
su_image.setOnClickListener {
readData(object : MyCallback {
override fun onCallback(imageUrl: String?) {
if (imageUrl != null) {
val imageViewer = Intent(baseContext, suDetails::class.java)
imageViewer.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
imageViewer.putExtra("su_image", imageUrl)
startActivity(imageViewer)
}
}
})
}
}
fun readData(myCallback: MyCallback) {
val su_resource =intent
val su_res = su_resource.getStringExtra("su_userid")
val suRef = FirebaseDatabase.getInstance().getReference().child("Users").child(su_res!!)
suRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
if(dataSnapshot.exists()){
su_layout.visibility = View.VISIBLE
val userData = dataSnapshot.getValue(profile_model::class.java)
val imageUrl = userData!!.getImageUrl()
Picasso.get().load(imageUrl).placeholder(R.drawable.ic_baseline_image_200).into(su_image)
su_name.text = userData.getnameOfsu()
Toast.makeText(baseContext, imageUrl, Toast.LENGTH_LONG).show()
myCallback.onCallback(imageUrl)
}
}
override fun onCancelled(error: DatabaseError) {
}
})
}
interface MyCallback {
fun onCallback(value: String?)
}
}
I have referred to other questions to read asynchronous data from a real-time database but when I tried the solution I am not able to show any data in my ImageView and textView. I am getting only the blank screen.
The New code after the answer of Tyler V:
class suDetails : AppCompatActivity() {
private var currentImageUrl: String = ""
private var su_res: String = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_su_details)
su_res = intent.getStringExtra("su_userid").toString()
// get views
val su_name = findViewById<TextView>(R.id.su_name)
val su_image = findViewById<ImageView>(R.id.su_image)
// onClick launches another activity - if the image
// hasn't loaded yet nothing happens
su_image.setOnClickListener { viewCurrentImage() }
// start the async loading right away - once it is loaded the
// su_layout view will be visible and the view data
// will be populated. It might be good to show a progress bar
// while it's loading
readData()
}
fun readData() {
println("LOG: called readData")
Toast.makeText(baseContext, su_res, Toast.LENGTH_LONG).show()
println("LOG: getting data for ${su_res}")
val suRef = FirebaseDatabase.getInstance()
.getReference()
.child("Users")
.child(su_res)
suRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
if (dataSnapshot.exists()) {
println("LOG: data snapshot exists")
su_layout.visibility = View.VISIBLE
val userData = dataSnapshot.getValue(profile_model::class.java)
currentImageUrl = userData?.getImageUrl() ?: ""
su_name.text = userData?.getnameOfsu() ?: ""
println("LOG: Got user data ${currentImageUrl}")
if (currentImageUrl.isNotEmpty()) {
Picasso.get()
.load(currentImageUrl)
.placeholder(R.drawable.ic_baseline_image_200)
.into(su_image)
}
} else {
println("LOG: user not found in database")
}
}
override fun onCancelled(error: DatabaseError) {
println("LOG: cancelled")
}
})
}
private fun viewCurrentImage() {
if (currentImageUrl.isEmpty()) return
Toast.makeText(baseContext, currentImageUrl, Toast.LENGTH_LONG).show()
val imageViewer = Intent(baseContext, ImageViewer::class.java)
imageViewer.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
imageViewer.putExtra("su_image", currentImageUrl)
startActivity(imageViewer)
}
}
The top answer to this related question shows you how to make callbacks, but that doesn't really answer the question of how to use the async data, and isn't really helpful or relevant to this type of problem.
I don't see anything specifically wrong with your callback - but it silently swallows a number of possible error cases (e.g. if the user doesn't exist). The example below has some print statements that should help determine better what is happening.
A cleaner approach than the extra callback interface is to make a separate method to handle the async result. Here is a cleaned up example of how that might look - with some pseudo-code where parts of your example were missing. To help debug, you should get in the habit of using log or print statements if you don't understand what parts of the code are running, or if something doesn't look the way you expect it to.
private var currentImageUrl: String = ""
private var userId: String = ""
private lateinit var su_name: TextView
private lateinit var su_image : ImageView
private lateinit var su_layout : ConstraintLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_su_details)
// get views
su_name = findViewById<TextView>(R.id.su_name)
su_image = findViewById<ImageView>(R.id.su_image)
su_layout = findViewById<ConstraintLayout>(R.id.su_layout)
su_layout.visibility = View.INVISIBLE
// get user id from intent
userId = intent.getStringExtra("su_userid").orEmpty()
// TODO: Handle what to do if userId is empty here!
if( userId.isEmpty() ) {
finish()
}
// onClick launches another activity - if the image
// hasn't loaded yet nothing happens
su_image.setOnClickListener { viewCurrentImage() }
// start the async loading right away - once it is loaded the
// su_layout view will be visible and the view data
// will be populated. It might be good to show a progress bar
// while it's loading
startLoading()
}
private fun startLoading() {
println("LOG: getting data for ${userId}")
val suRef = FirebaseDatabase.getInstance()
.getReference()
.child("Users")
.child(userId)
suRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
if(dataSnapshot.exists()) {
println("LOG: data snapshot exists")
val userData = dataSnapshot.getValue(profile_model::class.java)
showData(userData)
}
else {
println("LOG: user not found in database")
}
}
override fun onCancelled(error: DatabaseError) {
println("LOG: cancelled")
}
})
}
private fun showData(userData: profile_model?) {
su_layout.visibility = View.VISIBLE
currentImageUrl = userData?.getImageUrl() ?: ""
su_name.text = userData?.getnameOfsu() ?: "Error"
println("LOG: Got user data ${currentImageUrl}")
if( currentImageUrl.isNotEmpty() ) {
Picasso.get()
.load(currentImageUrl)
.placeholder(R.drawable.ic_baseline_image_200)
.into(su_image)
}
}
private fun viewCurrentImage() {
if( currentImageUrl.isEmpty() ) return
val imageViewer = Intent(this, suDetails::class.java)
imageViewer.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
imageViewer.putExtra("su_image", currentImageUrl)
startActivity(imageViewer)
}

MutableList remaining empty (?) even though i'm populating it

I'm trying to populate a mutable list so that I can use it for a recycler view. Unfortunately, although (i think) I'm populating the list, it's still remaining empty and the recycler view is not working (and I imagine it's because of the list issue). Please see below for the code:
private val newList: MutableList<NewListModel> = mutableListOf()
private val oldList = retrieveOldList()
private fun retrieveAndPopulate() {
for (i in 0 until oldList.size){
val oldItem = oldList[i]
val itemOne = oldItem.itemOne
val itemTwo = oldItem.itemTwo
val itemThree = oldItem.itemThree
val itemFour = oldItem.itemFour
val newItemData =
NewListModel(
itemOne, itemTwo, itemThree, itemFour
)
newList.add(newItemData)
Log.d(
"RetrieveData",
"${newItemData.itemOne} has been added to the list."
)
}
}
The class below is for the "NewListModel"
#Keep
#IgnoreExtraProperties
data class NewListModel (
var itemOne: String ?= null,
var itemTwo: String ?= null,
var itemThree: String ?= null,
var itemFour: String ?= null,
)
Below is how i try to populate the "oldList"
fun retrieveData(): MutableList<OldListModel> {
val list: MutableList<OldListModel> = mutableListOf()
val ref = FirebaseDatabase.getInstance().getReference("/storage")
ref.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.exists()) {
ref.get()
.addOnSuccessListener {
for (listItem in snapshot.children) {
val listItem = snapshot.getValue(OldListModel::class.java)
if (listItem != null) {
list.add(listItem)
}
}
}
} else {
Log.d(
"Data",
"Retrieving data was unsuccessful."
)
}
}
override fun onCancelled(error: DatabaseError) {
}
})
return list
}
It's probably worth mentioning that I'm getting the data from one mutable list and adding it to another. Any help is much appreciated
(below is how i try to populate the recycler view)
val newList = retrieveAndPopulate()
val recyclerView = findViewById<View>(R.id.recyclerView) as RecyclerView
val layoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
val adapterAdapter = AdapterAdapter(newList)
recyclerView.adapter = adapterAdapter
Your problem is that you think you're running code sequentially when it's running asynchronously. See the numbered comments from your function to trace the order of execution:
fun retrieveData(): MutableList<OldListModel> {
// 1. Here you create a list
val list: MutableList<OldListModel> = mutableListOf()
val ref = FirebaseDatabase.getInstance().getReference("/storage")
// 2. Here a listener is added that will let you know LATER when the data is ready
ref.addValueEventListener(object : ValueEventListener {
// 4. LATER the data changed will get called
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.exists()) {
ref.get()
.addOnSuccessListener {
// 5. EVEN LATER this listener is called with data
for (listItem in snapshot.children) {
val listItem = snapshot.getValue(OldListModel::class.java)
// 6. FINALLY - you add to a list that has long since stopped being relevant
if (listItem != null) {
list.add(listItem)
}
}
}
} else {
Log.d(
"Data",
"Retrieving data was unsuccessful."
)
}
}
override fun onCancelled(error: DatabaseError) {
}
})
return list // 3. Here you return the EMPTY list that was created
}
A solution - though likely not the best solution is to update your list once the callbacks complete:
private val theList = mutableListOf<YourDataModelType>
fun retrieveData(): { // No longer returning anything
// Remove this, no longer returning anything
// val list: MutableList<OldListModel> = mutableListOf()
val ref = FirebaseDatabase.getInstance().getReference("/storage")
ref.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.exists()) {
ref.get()
.addOnSuccessListener {
theList.clear() // Clear out existing data
for (listItem in snapshot.children) {
val listItem = snapshot.getValue(OldListModel::class.java)
if (listItem != null) {
theList.add(listItem)
}
}
// Since you're using Kotlin, you could use a map,
// but that's unrelated to this issue
// val list = snapshot.children.map { getValue(...) }.filterNotNull()
// Now that we have a full list here, update:
updateAdapterWithNewData(theList)
}
} else {
Log.d(
"Data",
"Retrieving data was unsuccessful."
)
}
}
override fun onCancelled(error: DatabaseError) {
}
})
}
Where updateAdapterWithNewData is a function you write to do as it says.
Please read up on asynchronous programming and make sure you understand how the code is flowing when using callbacks / listeners in frameworks like Firebase.
The issue that I had was with regards to not using callbacks while trying to access data in an onSuccessListener. This was causing the list not to be updated at all.
After a day of scrolling on the internet, I finally found these solutions:
Solution: link
Extension to the solution: link
This solved my problem and I hope it solves yours too!

Best practice to implement MVVM in fragment using Bottom navigation android kotlin

I am implementing the MVVM in fragments using the bottom navigation with firebase. But it's not working in my case. I searched for many solutions but didn't solve my problem.
I implemented viewModel in the fragment and apply observer to it. In the ViewModel class, call the method(getting data from firebase) with return type LiveData from the repository.
I'm using these dependencies
dependencies
// Fragment Navigation using JetPack
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
Repository.kt
// Get emergency requests
fun getEmergencyRequests(): LiveData<MutableList<EmergencyRequests>> {
val mutableData = MutableLiveData<MutableList<EmergencyRequests>>()
val requestListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
if (dataSnapshot.exists() && dataSnapshot.hasChildren()) {
// Get Post object and use the values to update the UI
val listData = mutableListOf<EmergencyRequests>()
for (snapshot in dataSnapshot.children) {
val model = snapshot.getValue(EmergencyRequests::class.java)
model?.let {
model.requestId = snapshot.ref.key.toString()
listData.add(it)
}
}
listData.sortByDescending { it.timestamp }
mutableData.value = listData
}
}
override fun onCancelled(error: DatabaseError) {
Log.e("EMERGENCIES_FAIL", error.message)
}
}
emergencyRequestRef?.addValueEventListener(requestListener)
return mutableData
}
ViewModel.kt
private val repository: HomeRepository = HomeRepository()
fun getRequests(): LiveData<MutableList<EmergencyRequests>> {
repository.getEmergencyRequests()
val mutableData = MutableLiveData<MutableList<EmergencyRequests>>()
repository.getEmergencyRequests().observeForever {
mutableData.value = it
}
return mutableData
}
Fragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Initialize
initialization()
// Recyclerview
binding.recyclerView.layoutManager = LinearLayoutManager(mContext)
adapter = EmergenciesAdapter(mContext, object : OnRequestClick {
override fun onClick(model: EmergencyRequests) {
}
})
binding.recyclerView.adapter = adapter
// Get emergencies
// getEmergencies()
viewModel = ViewModelProvider(mContext).get(HomeViewModel::class.java)
observer()
}
private fun observer() {
viewModel.getRequests().observe(viewLifecycleOwner, {
adapter.setDataList(it)
adapter.notifyDataSetChanged()
})
}
Not sure if this will fix it for you, but if you are using bottom navigation view and want fragments to maintain a backstack for the visited destinations, then with this version of Navigation dependency you can get to save the backstacks automatically, use these Navigation dependencies:
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.0-alpha01'
implementation 'androidx.navigation:navigation-ui-ktx:2.4.0-alpha01'
UPDATE
In your repository, are you able to get the data back? since the function might be returning before it gets data from firebase. I would suggest refactoring it a bit like this:
Repository.kt
private val _data: MutableLiveData<MutableList<EmergencyRequests>> = MutableLiveData()
val data: LiveData<MutableList<EmergencyRequests>> get() = _data
// Get emergency requests
fun getEmergencyRequests() {
val requestListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
if (dataSnapshot.exists() && dataSnapshot.hasChildren()) {
// Get Post object and use the values to update the UI
val listData = mutableListOf<EmergencyRequests>()
for (snapshot in dataSnapshot.children) {
val model = snapshot.getValue(EmergencyRequests::class.java)
model?.let {
model.requestId = snapshot.ref.key.toString()
listData.add(it)
}
}
listData.sortByDescending { it.timestamp }
_data.value = listData
}
}
override fun onCancelled(error: DatabaseError) {
Log.e("EMERGENCIES_FAIL", error.message)
}
}
emergencyRequestRef?.addValueEventListener(requestListener)
}
ViewModel.kt
private val repository: HomeRepository = HomeRepository()
fun getRequests(): LiveData<MutableList<EmergencyRequests>> {
repository.getEmergencyRequests()
return repository.data
}

How to sort by the first letter of a word in Livedata Recyclerview?

I have dataclass retrofit from json and a LiveData.I'm sending the adapter list with observe.How to sort by the first letter of a word in observe?
private fun getAllTeams(){
viewModel.getMyGroupMembers().observe(viewLifecycleOwner, Observer {
it.sortedBy {
it.name
}
it.forEach {
it.name.toCharArray().sorted().joinToString("")
}
Log.d("sort",it.toString())
scoreAdapter.submitList(it)
scoreAdapter.notifyDataSetChanged()
})
}
I tried something but it dosent work
class ScoreViewModel(private val scoreApi: ScoreApi) : ViewModel() {
#ExperimentalCoroutinesApi
private val myGroupMembers = MutableLiveData<List<DataClass>>()
#ExperimentalCoroutinesApi
fun getMyGroupMembers(): LiveData<List<DataClass>> = myGroupMembers
fun getAllGroups(){
viewModelScope.launch(Dispatchers.IO) {
val response = scoreApi.getAllGroups()
updateUi(response)
}
}
#ExperimentalCoroutinesApi
private fun updateUi(update: List<DataClass>) {
viewModelScope.launch(Dispatchers.Main) {
myGroupMembers.value = update
}
}
sortedBy method is used when you want to make a copy of your list. Its usually used when your main List is not Mutable:
val sortedList = it.sortedBy { it.name }
If you want to sort in-place without copying it you can use sortBy extension. This will only work if your list is Mutable.
it.sortBy { it.name }

Return data from Repository to ViewModel without LiveData

I'm just trying to find an answer how to pass the data from Repository to ViewModel without extra dependencies like RxJava. The LiveData seems as a not good solution here because I don't need to proceed it in my Presentation, only in ViewModel and it's not a good practice to use observeForever.
The code is simple: I use Firebase example trying to pass data with Flow but can't use it within a listener (Suspension functions can be called only within coroutine body error):
Repository
fun fetchFirebaseFlow(): Flow<List<MyData>?> = flow {
var ret: List<MyData>? = null
firebaseDb.child("data").addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val data = dataSnapshot.getValue<List<MyData>>()
emit(data) // Error. How to return the data here?
}
override fun onCancelled(databaseError: DatabaseError) {
emit(databaseError) // Error. How to return the data here?
}
})
// emit(ret) // Useless here
}
ViewModel
private suspend fun fetchFirebase() {
repo.fetchFirebaseFlow().collect { data ->
if (!data.isNullOrEmpty()) {
// Add data to something
} else {
// Something else
}
}
You can use callbackFlow
#ExperimentalCoroutinesApi
fun fetchFirebaseFlow(): Flow<List<String>?> = callbackFlow {
val listener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val data = dataSnapshot.getValue<List<MyData>>()
offer(data)
}
override fun onCancelled(databaseError: DatabaseError) {
}
}
val ref =firebaseDb.child("data")
reef.addListenerForSingleValueEvent(listener)
awaitClose{
//remove listener here
ref.removeEventListener(listener)
}
}
ObservableField is like LiveData but not lifecycle-aware and may be used instead of creating an Observable object.
{
val data = repo.getObservable()
val cb = object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(observable: Observable, i: Int) {
observable.removeOnPropertyChangedCallback(this)
val neededData = (observable as ObservableField<*>).get()
}
}
data.addOnPropertyChangedCallback(cb)
}
fun getObservable(): ObservableField<List<MyData>> {
val ret = ObservableField<List<MyData>>()
firebaseDb.child("events").addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
ret.set(dataSnapshot.getValue<List<MyData>>())
}
override fun onCancelled(databaseError: DatabaseError) {
ret.set(null)
}
})
return ret
}
It is also possible to use suspendCancellableCoroutine for a single result. Thanks to Kotlin forum.

Categories

Resources