How can I get the value of a LivaData<String> at once in Android Studio?

Viewed 224

savedRecordFileName is a variable of LivaData<String>, I hope to get the value of savedRecordFileName at once in Code A.

You know that LiveData variable is lazy, maybe the value of savedRecordFileName is null in binding.btnStop.setOnClickListener { }, so the code in binding.btnStop.setOnClickListener { } will not be fired when the value of savedRecordFileName is null.

I hope that the code in binding.btnStop.setOnClickListener { } can always be fired, how can I do?

BTW, I think the Code B is not suitable because the value of savedRecordFileName maybe changed by other function.

Code B

binding.btnStop.setOnClickListener {
   mHomeViewModel.savedRecordFileName.observe(viewLifecycleOwner){
        val aMVoice = getDefaultMVoice(mContext,it)             
        mHomeViewModel.add(aMVoice)  
   }
}

Code A

class FragmentHome : Fragment() {

    private val mHomeViewModel by lazy {
        getViewModel {
            HomeViewModel(mActivity.application, provideRepository(mContext))
        }
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {        
       ... 
       binding.btnStop.setOnClickListener {
            mHomeViewModel.savedRecordFileName.value?.let{
                val aMVoice = getDefaultMVoice(mContext,it)             
                mHomeViewModel.add(aMVoice)                
            }
        }
        ...
        return binding.root
    }

}


class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
    val savedRecordFileName: LiveData<String> = mDBVoiceRepository.getTotalOfVoice().map {
        mApplication.getString(R.string.defaultName, (it+1).toString())
    }
}

class DBVoiceRepository private constructor(private val mDBVoiceDao: DBVoiceDao){
    fun getTotalOfVoice() = mDBVoiceDao.getTotalOfVoice()
}

@Dao
interface DBVoiceDao{
   @Query("SELECT count(id) FROM voice_table")
   fun getTotalOfVoice(): LiveData<Long>
}

Add Content

To Ridcully: Thanks!

I think your way "move all of that into the viewmodel class" is good !

I think it will be OK even if the filename is livedata in your Code C, right?

Code C

viewModelScope.launch(Dispatchers.IO) {
    filename = dao.getFilename() // without livedata.  I think it will be OK even if the filename is livedata
    voice = getDefaultVoice(...) // also do this in background
    add(voice)
    result.postValue(true)
}
3 Answers

Solution 1

A simple/tricky/dirty solution would be an empty observe in onViewCreated like:

mHomeViewModel.savedRecordFileName.observe(viewLifecycleOwner){ }

which causes the LiveData to be observed, so it will be queried by Room because it is active and hopefully when you click on the button, the LiveData is filled.



Solution 2

But my favorite approach would be a reactive one. I don't know if you want to take a reactive look toward your code or not, but you can use MediatorLiveData (or take advantage of a library like this) to solve this situation reactively. Let there be another LiveData in your VM named btnStopClicked and each click changes the value of that LiveData. So, your code may look like this:

class FragmentHome : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {        
       ... 
       binding.btnStop.setOnClickListener {
            mHomeViewModel.btnStopClicked.value = null //we don't care about the value
        }
        mHomeViewModel.addVoiceEvent.observe(viewLifecycleOwner){
            val aMVoice = getDefaultMVoice(mContext,it)             
            mHomeViewModel.add(aMVoice)   
        }
        ...
        return binding.root
    }

}


class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
 
    val savedRecordFileName: ... //whatever it was
    val btnStopClicked = MutableLiveData<Any>()

    val addVoiceEvent = savedRecordFileName
        .combineLatestWith(btnStopClicked){srfn, _ -> srfn}
        .toSingleEvent()
}

Also do not forget toSingleEvent because if you don't turn it into an event, it would get called unintentionally in some situations (look at this).

As you said, you should do Room queries in background to avoid blocking ui thread. But you shouldn't stop there. Do as much work as possible in a background thread. In your case, on button click, start running a background thread that does all the work: getting name from database (with direct query, which is possible now as you are running in background), getting the default voice, and adding it, to whatever it is you are adding it to.

You should also move all of that into the viewmodel class.

btnStop.setOnClickListener {
    viewmodel.stop()
}

In viewmodel something like this (java / pseudo code like):

void stop() {
    runinbackgroundthread {
        filename = dao.getFilename() // without livedata
        voice = getDefaultVoice(...) // also do this in background
        add(voice)
        // If you want the action to have a reault, observable by ui
        // use a MutableLiveData and set it here, via postValue()
        // as we are still in background thread.
        result.postValue(true)
    }
}

On how to actually implement my pseudo code runinbackgroundthread see some official guide here: https://developer.android.com/guide/background/threading

In short, with Kotlin coroutines it should go like this:

viewModelScope.launch(Dispatchers.IO) {
    filename = dao.getFilename() // without livedata
    voice = getDefaultVoice(...) // also do this in background
    add(voice)
    result.postValue(true)
}

TL;DR - Code A is better, with extra additions.

As you already know, LiveData is not guaranteed to have a non-null value right after instantiation. Ideally, as for best practice, it is good to always treat LiveData value as ever changing (i.e - assume direct value access twice will produce different values). This is intended behavior as LiveData is designed to be observable instead of you dealing with immediate value.

Hence, listen to the LiveData and update your UI accordingly. For instance, disable or hide the stop button when there's no data inside it.

mHomeViewModel.savedRecordFileName.observe(viewLifecycleOwner) {
    binding.btnStop.isEnabled = it != null
}
binding.btnStop.setOnClickListener {
    mHomeViewModel.savedRecordFileName.value?.also {
        val aMVoice = getDefaultMVoice(mContext,it)             
        mHomeViewModel.add(aMVoice)                
    }
}
Related