I test LiveData like this.
// MainActivity.kt
class MainActivity : AppCompatActivity() {
val testViewModel: TestViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
testViewModel.testLiveData.value = true
testViewModel.testLiveData.observe(this) {
println("Hello")
}
}
}
// TestViewModel.kt
class TestViewModel : ViewModel(){
val testLiveData = MutableLiveData<Boolean>()
}
I think.... (livedata).observe mean start observe about liveData value change.
I don't think the value changed before the observer is set is not observable.
But, it print hello....
Am I misunderstood about live data observers?
When you start observing a LiveData, if the LiveData has a value already, it will replay that value to the observer immediately. This is by design, because LiveData is typically in a ViewModel that outlives the views. For example, when the screen rotates, all the views are recreated and observations begin again. All the views will be updated with the latest values of the LiveData. If it didn't behave this way, then when the screen rotates and Activities/Fragments are recreated, they would just sit there and have nothing to observe, defeating the purpose of using a ViewModel to retain state that outlives views.
Related
I'm subscribed to an observable in my Fragment, the observable listens for some user input from three different sources.
The main issue is that once I navigate to another Fragment and return to the one with the subscription, the data is duplicated as the observable is handled twice.
What is the correct way to handle a situation like this?
I've migrated my application to a Single-Activity and before it, the subscription was made in the activity without any problem.
Here is my Fragment code:
#AndroidEntryPoint
class ProductsFragment : Fragment() {
#Inject
lateinit var sharedPreferences: SharedPreferences
private var _binding: FragmentProductsBinding? = null
private val binding get() = _binding!!
private val viewModel: ProductsViewModel by viewModels()
private val scanner: CodeReaderViewModel by activityViewModels()
private fun observeBarcode() {
scanner.barcode.observe(viewLifecycleOwner) { barcode ->
if (barcode.isNotEmpty()) {
if (binding.searchView.isIconified) {
addProduct(barcode) // here if the fragment is resumed from a backstack the data is duplicated.
}
if (!binding.searchView.isIconified) {
binding.searchView.setQuery(barcode, true)
}
}
}
}
private fun addProduct(barcode: String) {
if (barcode.isEmpty()) {
return
}
viewModel.insert(barcode)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.start(args.documentId)
if (args.documentType == "Etichette") {
binding.cvLabels.visibility = View.VISIBLE
}
initUI()
observe()
}
private fun observe() {
observeBarcode()
observeProducts()
observeLoading()
observeLast()
}
}
Unfortunately, LiveData is a terribly bad idea (the way it was designed), Google insisted till they kinda phased it out (but not really since it's still there) that "it's just a value holder"...
Anyway... not to rant too much, the solution you have to use can be:
Use The "SingleLiveEvent" (method is officially "deprecated now" but... you can read more about it here).
Follow the "official guidelines" and use a Flow instead, as described in the official guideline for handling UI Events.
Update: Using StateFlow
The way to collect the flow is, for e.g. in a Fragment:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { // or RESUMED
viewModel.yourFlow.collectLatest { ... } // or collect { ... }
}
}
For that in your ViewModel you'd expose something like:
Warning: Pseudo-Code
// Imagine your state is represented in this sealed class
sealed class State {
object Idle: State
object Loading: State
data class Success(val name: String): State
data class Failure(val reason: String): State
}
// You need an initial state
private val _yourFlow = MutableStateFlow(State.Idle)
val yourFlow: StateFlow<State> = _yourFlow
Then you can emit using
_yourFlow.emit(State.Loading)
Every time you call
scanner.barcode.observe(viewLifecycleOwner){
}
You are creating a new anonymous observer. So every new call to observe will add another observer that will get onChanged callbacks. You could move this observer out to be a property. With this solution observe won't register new observers.
Try
class property
val observer = Observer<String> { onChanged() }
inside your method
scanner.barcode.observe(viewLifecycleOwner, observer)
Alternatively you could keep your observe code as is but move it to a Fragment's callback that only gets called once fex. onCreate(). onCreate gets called only once per fragment instance whereas onViewCreated gets called every time the fragment's view is created.
I have 2 fragments with a single activity. Fragment A and Fragment B. I did some tasks and then If I move from FragmentA to FragmentB and came back by pressing the back button. In that case, the state is not remaining as before, instead, I'm getting a fresh View like the first time. Why?
Actually, I have a recyclerview and custom toolbar. If I scroll some item and then move to fragment B and came back, I should be in the same state.
Please note: This question might be asked a number of times, but my coding scenario is different.
I'm using dagger hilt, kotlin, corutine, viewmodel, live data, recyclerview asynclist differ, and nav component.
Code:
FragmentA
private val viewModel: ProductViewModel by viewModels<ProductViewModel>()
onviewcreated
viewModel.getProducts.observe(viewLifecycleOwner, Observer { response ->
stopShimmerLayout()
response.let { productResponse ->
productAdapter.differ.submitList(productResponse)
}
setActionListener()
})
ViewModel class
class ProductViewModel #ViewModelInject constructor(
private val repository: ProductRepository,
) : ViewModel() {
val getProducts = liveData(Dispatchers.IO) {
emit(repository.getProducts())
}
}
LiveData will always give you its current value upon loading the view again. If you dont want that then might I suggest moving to Kotlin Flow or MutableStateFlow
Update
If you updated your adapter data (or repository) which you should have done when when something changes like your button then called getProducts again to trigger the livedata observer to update your recyclerview you should get the data upon returning.
Here is an example of how I would do it
Example:
ViewModel
class ProductViewModel #ViewModelInject constructor(
private val repository: ProductRepository,
) : ViewModel() {
val productsLiveData: MutableLiveData<Products> = MutableLiveData<Products>()
fun getProductsAsync() {
val data = repository.getProducts()
productsLiveData.postValue(data)
}
fun updateDataAsync(){
//Update your repository data
//Call get products again to propagate the data change to the productsLiveData and recycler view
getProductsAsync()
}
}
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?){
viewModel.productsLiveData.observe(viewLifecycleOwner, Observer{
//Update recycler view adapter here
//Anytime you either call getProductsAsync() or updateDataAsync() from your viewmodel this will get called and you should update the adapter
productAdapter.differ.submitList(productResponse) { setActionListener() })
}
viewModel.getProductsAsync()
}
Obviously you will need to update the code to fit what you have but this is the basic flow I would do so that whenever you load the fragment it will always have the latest correct data
How I can use shared viewModel with fragments without activity?
Like in code but in place of requireActivity() use ParentFragment. In this case when ParentFragment will destroyed, SharedViewModel is cleared, but when I provide SharedViewModel from activity, it not cleared when ParentFragment destroyed.
And I use Navigation Components, which mean that I can`t set tag for fragment and then use findFragmentByTag()
class ParentFragment:Fragment{
override fun onCreate(savedInstanceState: Bundle?) {
var viewModel = ViewModelProviders.of(requireActivity()).get(SharedViewModel::class)
}
}
class ChildFragment:Fragmnet{
override fun onCreate(savedInstanceState: Bundle?) {
var viewModel = ViewModelProviders.of(requireActivity()).get(SharedViewModel::class)
}
}
You can try scoped-vm - it allows you to request ViewModel for scope identified by a String key. Scope lives till the last fragment that requested ViewModel gets destroyed, then ViewModel gets cleared.
You can use this code to obtain SharedViewModel both in ParentFragment and ChildFragment.
ScopedViewModelProviders
.forScope(this, "scope")
.of(requireActivity())
.get(SharedViewModel::class.java)
See you can initialize viewModel in all fragments which you want to share viewmodel, and use Rx with viewModel, your all process in these fragments will keep running until you want to cancel it,you can call viewModel.oncleard() from Activity or Fragment.
public override fun onCleared() {
super.onCleared()
compositeDisposable.dispose()
// or cancel any process
}
If you do not know ViewModel Scope, please check this image
According to the section "Share data between fragments" at https://developer.android.com/topic/libraries/architecture/viewmodel we are told that creating a ViewModel in the activity scope and sharing that amongst the fragments is the way to go.
This is the Fragment which sets the value in the ViewModel
class MasterFragment : Fragment() {
private lateinit var itemSelector: Selector
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
itemSelector.setOnClickListener { item ->
// Update the UI
}
}
}
This is the detail fragment which uses the property set
class DetailFragment : Fragment() {
private lateinit var model: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = activity?.run {
ViewModelProviders.of(this).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
model.selected.observe(this, Observer<Item> { item ->
// Update the UI
})
}
}
This is the ViewModel
class SharedViewModel : ViewModel() {
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
}
My question is simple. Assuming the MasterFragment set a value in the ViewModel on a button click, how would we recover that value when accessing it AFTER THE SYSTEM HAS KILLED OUR APPLICATION AND RESTARTED IT ?.
Our DetailFragment will not be seeing the value since we were setting it on the button click in the MasterFragment. To understand the question better, consider we have Fragment A, B, C, and D and they share a ViewModel which has a value Fragment A B and C together computed and placed it in ViewModel for Fragment D to access.
Now when the system kills and recreates our application Fragment D won't have that value available.
OnSaveInstance also won't be able to help out much without resorting to dirty code. For simple situations, yes , but like the one in which FragmentA B and C together are making a value, in that situation, OnSaveInstance would be problematic.
OnSaveInstance should have been inside the ViewModel but alas I don't think that's the case. Any ideas?
ViewModel objects are scoped to the Lifecycle passed to the ViewModelProvider when getting the ViewModel. The ViewModel remains in memory until the Lifecycle it's scoped to goes away permanently: in the case of an activity, when it finishes, while in the case of a fragment, when it's detached.
You can check it here
My question is simple. Assuming the MasterFragment set a value in the ViewModel on a buttonClick , how would we recover that value when accessing it AFTER THE SYSTEM HAS KILLED OUR APPLICATION AND RESTARTED IT ?.
You can't recover the value if the application is killed by the user or system or restarted.
To solve your purpose of accumulating data from Activity A, B and C and display it in Activity D even though the application is killed or restarted, you can choose any 1 method from the following:
1. SharedPreference
2. Local Database Room or SQLite
3. Store data in a file
I recommend you to use SharedPreference for small data and Room for Large and Complex data.
In a nutshell, ViewModel stores data temporary to survive orientation change(no need to write code of onSaveInstanceState and onRestoreInstanceState) and share data between Activities and Fragments. Data will be lost if the activity is destroyed or fragment is detached.
If you still want to get stored value after app reset or killed you need to save data to SharedPreferences or internal SqLite database and restore it after app start.
For those using Kotlin out there try the following approach:
Add the androidx ViewModel and LiveData libraries to your gradle file
Call your viewmodel inside the fragment like this:
class MainFragment : Fragment() {
private lateinit var viewModel: ViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// kotlin does not have a getActivity() built in method instead we use activity, which is null-safe
activity?.let {
viemModel = ViewModelProvider(it).get(SharedViewModel::class.java)
}
}
}
Trying to figure out the Google's recent tools and concepts: LifecycleActivity, ViewModel and Data Binding.
So imagine there is a FooActivity that extends AppCompatActivity(to be able to use Support library) and implements LifecycleOwner interface (from reference: required to use LiveData):
FooActivity.kt:
class FooActivity: AppCompatActivity(), LifecycleObserver {
...
We set the binding:
..
private val mBinding: by lazy {
DataBindingUtil.setContentView<ActivityFooBinding>(this, R.layout.activity_foo)
}
..
We set activity's ViewModel, and attach our barObserver (which should observe changes of bar inside a ViewModel, which is just a List of Strings):
..
val viewModel = ViewModelProviders.of(this).get(FooViewModel::class.java)
viewModel.getBars()?.observe(this, barObserver)
viewModel.init() // changes bar, should trigger Observer's onChanged
..
And finally we define barObserver:
..
class BarObserver : Observer<List<String>> {
override fun onChanged(p0: List<String>?) {
Log.d("Say", "changed:")
}
}
barObserver = BarObserver()
..
Questions:
Why does Observers onChange never triggered?
Should one use LifeCycleOwner``getLifecycle instead of using Observer?
Any other thoughts?
Edit:
As the reference states:
LiveData is a data holder class that can be observed within a given lifecycle. This means that an Observer can be added in a pair with a LifecycleOwner, and this observer will be notified about modifications of the wrapped data only if the paired LifecycleOwner is in active state. LifecycleOwner is considered as active, if its state is STARTED or RESUMED. An observer added via observeForever(Observer) is considered as always active and thus will be always notified about modifications. For those observers, you should manually call removeObserver(Observer).
Changing:
enter code hereviewModel.getBars()?.observe(this, barObserver)
to:
viewModel.getBars()?.observeForever(barObserver)
didn't to the trick. onChange still never triggered.
Edit2:
FooActivity.kt:
replaced with:
class FooActivity: AppCompatActivity(), LifecycleRegistryOwner{
override fun getLifecycle(): LifecycleRegistry {
var lifecycle = LifecycleRegistry(this)
lifecycle.addObserver(barObserver)
return lifecycle
}
FooViewModel:
..
private var mBar = MutableLiveData<List<String>>()
..
mBar.value = listValueOf("1", "2", "3")
..
fun getBar(): LiveData<List<String>>? = mBar
..
If you are not extending LifecycleActivity then your activity should implement LifecycleRegistryOwner instead of LifecycleObserver. Check out the docs.
And then you can pass it to liveData.observe(lifecycleOwner, observer)