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)
}
Related
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!
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.
private fun getUserInfo() {
val userkey = FirebaseAuth.getInstance().uid ?: ""
val ref = FirebaseDatabase.getInstance().getReference("/users/$userkey")
ref.addListenerForSingleValueEvent(object: ValueEventListener {
override fun onDataChange(p0: DataSnapshot) {
val adapter = GroupAdapter<ViewHolder>()
p0.children.forEach {
Log.d("getUserInfo", it.toString())
val user = it.getValue(User::class.java)
if (user != null) {
adapter.add(UserItem(user))
}
}
UserInfo_RCView.adapter = adapter
}
override fun onCancelled(p0: DatabaseError) {
}
})
}
}
class UserItem(val user: User): Item<ViewHolder>() {
override fun bind(viewHolder: ViewHolder, position: Int) {
viewHolder.itemView.UsernameSettings_txt.text = user.username
Picasso.get().load(user.profileImageUrl).into(viewHolder.itemView.ProfileImage_View)
}
override fun getLayout(): Int {
return R.layout.userinfo
}
}
I wanted to make a snapshot from my current user like this, every time I put the $userkey in my ref value the app crashes with this error:
2018-11-23 22:04:48.414 3871-3871/my.app E/RecyclerView: No adapter attached; skipping layout
2018-11-23 22:04:49.797 3871-3871/my.app E/AndroidRuntime: FATAL EXCEPTION: main
Process: nobrand.ljb.musicshare, PID: 3871
com.google.firebase.database.DatabaseException: Can't convert object of type java.lang.String to type myapp.User
When I didn't put the $userkey in my ref value. The app functions as normal put displays all users as expected, but as I said I don't want that.
I'll be thankful for every answer and suggestion!
If you directly access the correct child node, the loop in your code is not needed anymore.
This means there are two simple ways to do this:
Query by key
Remove the loop
I'd recommend removing the loop as there's really no need for it anymore, but wanted to show both options.
Query by key
val ref = FirebaseDatabase.getInstance().getReference("/users")
ref.orderByKey().equalTo(userkey).addListenerForSingleValueEvent(object: ValueEventListener {
override fun onDataChange(p0: DataSnapshot) {
val adapter = GroupAdapter<ViewHolder>()
p0.children.forEach {
Log.d("getUserInfo", it.toString())
val user = it.getValue(User::class.java)
if (user != null) {
adapter.add(UserItem(user))
}
}
UserInfo_RCView.adapter = adapter
}
override fun onCancelled(p0: DatabaseError) {
throw p0.toException();
}
})
Remove the loop
val ref = FirebaseDatabase.getInstance().getReference("/users/$userkey")
ref.addListenerForSingleValueEvent(object: ValueEventListener {
override fun onDataChange(p0: DataSnapshot) {
val adapter = GroupAdapter<ViewHolder>()
val user = p0.getValue(User::class.java)
if (user != null) {
adapter.add(UserItem(user))
}
UserInfo_RCView.adapter = adapter
}
override fun onCancelled(p0: DatabaseError) {
throw p0.toException();
}
})
How to determine size of data returned before setting adapter?
How to use emptyview with paging library?
How to set emptyview if pagedlist returns null or no data?
Update[24/04/19]:
I just found out that the library already provide us a way to listen to empty initial load, using PagedList.BoundaryCallback<YourItem>.
*Note that my old answer is still a valid alternative.
val livedPageList = LivePagedListBuilder(sourceFactory, config)
.setBoundaryCallback(object: PagedList.BoundaryCallback<YourItem>() {
override fun onZeroItemsLoaded() {
super.onZeroItemsLoaded()
// Handle empty initial load here
}
override fun onItemAtEndLoaded(itemAtEnd: YourItem) {
super.onItemAtEndLoaded(itemAtEnd)
// Here you can listen to last item on list
}
override fun onItemAtFrontLoaded(itemAtFront: YourItem) {
super.onItemAtFrontLoaded(itemAtFront)
// Here you can listen to first item on list
}
})
.build()
Original Answer:
Based on this class on google sample Network State. Modify it to handle empty content in initialLoad.
#Suppress("DataClassPrivateConstructor")
data class NetworkState private constructor(
val status: Status,
val msg: String? = null
) {
enum class Status {
RUNNING,
SUCCESS_LOADED, // New
SUCCESS_EMPTY, // New
FAILED
}
companion object {
val EMPTY = NetworkState(Status.SUCCESS_EMPTY) // New
val LOADED = NetworkState(Status.SUCCESS_LOADED) // New
val LOADING = NetworkState(Status.RUNNING)
fun error(msg: String?) = NetworkState(Status.FAILED, msg)
}
}
Usage as follow:
class DataSource: PageKeyedDataSource<Long, Item>() {
val initialLoad: MutableLiveData<NetworkState> = MutableLiveData()
override fun loadInitial(params: LoadInitialParams<Long>, callback: LoadInitialCallback<Long, Item>) {
initialLoad.postValue(NetworkState.LOADING)
apiCallSource.subscribe({ items ->
if (items.isEmpty()) {
initialLoad.postValue(NetworkState.EMPTY)
} else {
initialLoad.postValue(NetworkState.LOADED)
}
}, { error ->
// handle error
})
}
}
And this is how the activity handle it:
class activity: AppCompatActivity() {
val viewModel = // init viewmodel
override fun onCreate(savedInstanceState: Bundle?) {
viewModel.refreshState.observe(this, Observer { networkState ->
if (it == NetworkState.LOADING) {
// Show loading
} else {
// Hide loading
if (it.status == NetworkState.Status.SUCCESS_EMPTY) {
// Show empty state for initial load
}
}
}
}
}
For more details on how to connect DataSource with Activity, see this sample
Simply add a listener or callback function to your DataSourceFactory and your DataSource and call it if the list in loadInitial is empty:
class DataSourceFactory(
private val dataObservable: Observable<List<Data>>,
private val onEmptyAction: () -> Unit
) : DataSource.Factory<Int, Data >() {
override fun create(): DataSource {
return DataSource(observable, onEmptyAction)
}
}
class DataSource(
private val observable: Observable<List<Data>>,
private val onEmptyAction: () -> Unit
) : ItemKeyedDataSource<Int, Data>() {
private val data = mutableListOf<Data>()
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Data>) {
observable
.subscribe({ data ->
if (data.isEmpty()) {
// Inform someone that this list is empty from the
// beginning to be able to show an empty page
onEmptyAction()
}
// rest of your code & logic
}, { Timber.e(it) })
}
}
In your fragment/activity you are observing network state:
viewModel.getNetworkState1()?.observe(this, Observer {
// here you can handle you empty view
setEmptyView()
})
like this:
private fun setNoTransactionsLayout() {
if(viewModel.listIsEmpty()) {
yourTextView.visibility = View.VISIBLE
} else {
yourTextView.visibility = View.GONE
}
}
And in view model you have this function:
fun listIsEmpty(): Boolean {
return yourPagedList?.value?.isEmpty() ?: true
}
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()
}