I'm trying to handle a LiveData value (profilePicture: Bitmap) within a BindingAdapter function but the first value is always null. The binding adapter is a ViewSwitcher with ProgressBar and ImageView.

Getting profile picture from firebase like this:

    val downloadPictureResult = MutableLiveData<Bitmap>()
    // ...
   fun downloadProfilePic(): LiveData<Bitmap?> {
        val imageRef = storage.getReference("images/$uid/profile/profile_picture.jpg")
        imageRef.getBytes(ONE_MEGABYTE).addOnCompleteListener { task ->
            if (task.isSuccessful) {
                //...
                downloadPictureResult.value =  responseBitmap
                //...
            } else {
                downloadPictureResult.value = null
                Log.d(TAG, task.exception?.localizedMessage)
            }
        }
        return downloadPictureResult;
    }

Because the first value is null and the second the expected bitmap object, the view.showNext()is called two times. But it's more important for me to understand why the firstvalue is null because the setProfilePicture method will have some more logic.

BindingAdapter looks like this.

fun setProfilePicture(view: ViewSwitcher, profilePicture: Bitmap?) {
 Log.d("PPSS", profilePicture.toString())
    val imageView: ImageView = view[1] as ImageView
    imageView.setImageDrawable(view.context.getDrawable(R.drawable.account_circle_24dp))

    profilePicture.let { picture ->
        if (picture != null) {
            val rounded = RoundedBitmapDrawableFactory.create(view.resources, picture)
            rounded.isCircular = true
            imageView.setImageDrawable(rounded)
            view.showNext()
        } else {
            view.showNext()
        }
    }

Log:

2019-04-13 17:53:01.658 11158-11158/... D/PPSS: null
2019-04-13 17:53:02.891 11158-11158/... D/PPSS: [email protected]

3 Answers

3
Julio E. Rodríguez Cabañas On

When you define a LiveData, its initial value will be null even when its type is not nullable:

val downloadPictureResult = MutableLiveData<Bitmap>()
// Here, downloadPictureResult.value is null

In your case, the value of the LiveData returned by downloadProfilePic() will be null until the picture is downloaded, which will happen asynchronously inside the callback addOnCompleteListener:

fun downloadProfilePic(): LiveData<Bitmap?> {
    ...
    imageRef.getBytes(ONE_MEGABYTE).addOnCompleteListener { task ->
        ...
        // This happens asynchronously, likely after downloadProfilePic()
        // has already returned
        downloadPictureResult.value =  responseBitmap
        ...
    }

    return downloadPictureResult;
}

That's why the first value that gets passed to your adapter is null, because downloadPictureResult.value is still null at the time when downloadPictureResult is returned by downloadProfilePic() for the first time.

1
Jeel Vankhede On

Let's understand it step by step about how LiveData works in your case (In generally):

  1. Your method downloadProfilePic() returns LiveData of nullable Bitmap as return type. Method contains asynchronous code of addOnCompleteListener meaning that it's execution would be happen even after LiveData value has been returned.

Here:

fun downloadProfilePic(): LiveData<Bitmap?> {
    val imageRef = storage.getReference("images/$uid/profile/profile_picture.jpg")
    imageRef.getBytes(ONE_MEGABYTE).addOnCompleteListener { task ->
        if (task.isSuccessful) {
            //...
            downloadPictureResult.value =  responseBitmap
            //...
        } else {
            downloadPictureResult.value = null
            Log.d(TAG, task.exception?.localizedMessage)
        }
    }
    return downloadPictureResult;
}
  1. You're returning downloadPictureResult as result of method which is initialized globally as val downloadPictureResult = MutableLiveData<Bitmap>() mutable livedata, so that we can modify it afterwards. And that happens inside callback of addOnCompleteListener.

  2. Now presumably, when your view (Activity/Fragment) gets loaded and hence you've added your live data from ViewModel as DataBinding it gets initial value whatever is there in LiveData and then observes it further.

  3. So here, you've initialized LiveData with null value of Bitmap during this code val downloadPictureResult = MutableLiveData<Bitmap>() it returns null value for very first time.

  4. And then callback happens from addOnCompleteListener method and value is assigned livedata as downloadPictureResult.value = responseBitmap or null eventually if error.

That's why there are two calls for LiveData in your BindingAdapter method setProfilePicture().

Note: Simple hack you could do if you don't want two callback is making your callback synchronous instead of asynchronous.

1
Brian Kostadinov Shalon Isaac On

As the other answers have told you, the first value will be always null. However, you can get the value when it's ready using an observer and then unsubscribe from it when you have your image.

val pic = downloadProfilePic()
pic.observe(owner, object : Observer<BitMap?> {
    override fun onChanged(b: BitMap?){
        if(b != null){
            setProfilePicture(yourView, b)
            pic.removeObserver(this)
        }
    }
})