AppCompatActivity, ViewModel and Data Binding - android

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)

Related

How to prevent data duplication caused by LiveData observation in Fragment?

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.

Lifecycle scoping in a Fragment

I'm having a hard time understand what scopes to use for view models and live data when using fragments. Here is my ViewModel:
class MyViewModel: ViewModel() {
var myLiveData = MutableLiveData<WrappedResult<DataResponse>>()
private val repository = MyRespository()
private var job: Job? = null
fun getData(symbol: String) {
job = viewModelScope.launch(Dispatchers.IO) {
try {
val response = repository.getData(symbol)
withContext(Dispatchers.Main) {
myLiveData.value = WrappedResult.Success(response)
}
} catch(e: Exception) {
withContext(Dispatchers.Main) {
myLiveData.value = WrappedResult.Failure(e)
}
}
}
}
}
I can create the view model in the fragment using (where "this" is the fragment):
viewModel = new ViewModelProvider(this).get(MyViewModel.class);
However, I can observe the LiveData with two options:
viewModel.getMyLiveData.observe(this...
or
viewModel.getMyLiveData.observe(getViewLifecycleOwner()...
It would appear that the job I create in the view model is going to be scoped to the fragment's lifecycle (through viewModelScope) and not the fragment's view lifecycle, but I have a choice between these two for the live data.
I could use some guidance and what the best practice is here. Also, does any of this matter whether the fragment has retained instance or not? Currently the fragment has setRetainInstance(true). Finally, from everything I've read I shouldn't need to clear the observer in the fragment or override onCleared when things are setup this way. Is that correct?
refer the doc of view model
https://developer.android.com/topic/libraries/architecture/viewmodel?gclid=Cj0KCQjwtZH7BRDzARIsAGjbK2blIS5rGzBxBdX6HpB5PMKgpUQHvdKXbwrt-ukTnWkpax1otMk4sm4aAuzPEALw_wcB&gclsrc=aw.ds#lifecycle
Viewmodel will only gets destoyed once the activity is finished.As the fragments are on the top of acitivity, the lifecycle of fragment will not affect the Viewmodel.The data will be persisted there on the viewmodel. So you can write a method to reset the data in viewmodel while you are entering in to oncreate of fragment.
In Fragment, OnCreate :
getViewModel.init()
on ViewModel
fun init() {
// clear all varialbes/datas/ etc here
}

How can I initialize an androidx ViewModel from parcelable data?

In my Android app, I pass custom data (UByteArray) from one activity to another using the parcelable interface.
I am using this data inside multiple fragments, so I rewrote the data class to extend androidx ViewModel and expose LiveData properties to the fragments. Now the UI updates are a lot nicer, but I think I am using it wrong because I overwrite all ViewModel values inside onCreate.
Now my question: What do I need to change to initialize the ViewModel only once?
The following is my current code (abbreviated and renamed for this question):
class ActivityB : AppCompatActivity() {
private val bData: ViewModelB by viewModels()
// ...
override fun onCreate(savedInstanceState: Bundle?) {
// ...
intent.getParcelableExtra<ViewModelB>("id")?.let {
Log.e(TAG, "Found parceled bData $it")
// This seems like a very stupid way to do it, is there a better one?
bData.copyAll(it)
}
}
}
I saw that it is possible to inject SavedState into the ViewModelB constructor, but I don't have a saved state until now, and the data needs to be passed only once.
Should I change the initialization of tagData with by viewModels() to = ViewModelB(intent)?
Or do I need to extend the ViewModelFactory somehow?
Any tip here would be really appreciated, thanks.
I saw that it is possible to inject SavedState into the ViewModelB constructor, but I don't have a saved state until now, and the data needs to be passed only once.
The official solution would be to provide a SavedStateHandle that is initialized with the defaultArgs as the intent.extras of your Activity.
For that, you need to provide an AbstractSavedStateViewModelFactory implementation, OR use SavedStateViewModelFactory (in which case you must define the right constructor in order to have it instantiated via reflection).
class ActivityB : AppCompatActivity() {
private val bData: ViewModelB by viewModels {
SavedStateViewModelFactory(application, this, intent.extras)
}
// ...
override fun onCreate(savedInstanceState: Bundle?) {
// ...
// intent.getParcelableExtra<ViewModelB>("id")?.let {
// Log.e(TAG, "Found parceled bData $it")
}
}
Then in your ViewModel
#Keep
class ViewModelB(val savedStateHandle: SavedStateHandle): ViewModel() {
val uByteData = savedStateHandle.get<UByteArray>("id")
}
Or so. The "id" key must match the same key as is in the intent extras.
Since you have a ViewModel which implements Parcelable, you can get your ViewModelB instance directly from the Intent extra.
The Intent which is used for starting ActivityB may not be != null at the time when ActivityB is instantiated, but you can use
lateinit var bData: ViewModelB
Then in onCreate()
bData = if(intent.hasExtra("id")) intent.getParcelableExtra<ViewModelB>("id") else ViewModelProvider(this).get(ViewModelB::class.java)

Reacting to activity lifecycle in ViewModel

I'm trying to create an app which will use MVVM architecture and there's one thing I quite don't understand.
Official Android docs say that's not a good idea to reference activity context in ViewModel's (as ViewModel may outlive activity) so I've started to wonder about usecase when I want to execute some action when my activity is resumed.
I know ViewModel's shouldn't do business logic themselves but even if I use some service class (let's say GPSService which has to start and pauseeach time activity is resumed on paused), and inside this service I react to activity onResume (using Lifecycle observer) I will still reference this activity from ViewModel as I'm referencing service which holds reference to activity being observed, this may cause activity leak (correct me if I'm wrong).
So my question is, how to react to activity or fragment lifecycle in MVVM architecture?
If you need to have a ViewModel be lifecycle aware, then you can have it implement LifeCycleObserver and override life cycle events as necessary. Example,
public class MyModel extends ViewModel implements
LifecycleObserver {
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
protected void onLifeCycleStop() {
// do something
}
}
In the activity or fragment then you can add the view model to the activity life cycle owner.
public class MyActivity extends AppCompatActivity {
protected MyModel mMyModel;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mMyModel = ViewModelProviders
.of(this)
.get(MyModel.class);
getLifecycle().addObserver(mMyModel);
}
}
I know ViewModel's shouldn't do business logic themselves
Yes, you're right. ViewModel should not contain business logic but
it should contain UI related logic. So basically, API calls or Some
location related stuffs should be avoided in ViewModel logic.
So what if you wanna make some scenario which can react to any activity lifecycle? I'll suggest you to use LifecycleObserver.
Why?, Because LifecycleObserver will provide you callbacks once it's LifecycleOwner will change it's state.
What is LifecycleOwner here? In our case it may be Activity/Fragment.
So, how you can achieve this?
Let's say you want to make location requests during resume & pause period of any activity.
So, for that you can create a class called LocationUpdates as LifecycleObserver like below:
class LocationUpdates : LifecycleObserver {
constructor(){
// some basic location related initialization here
}
#OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun connectListener() {
// this method will respond to resume event of our Lifecycle owner (activity/fragment in our case)
// So let's get location here and provide callback
}
#OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun disconnectListener() {
// this method will respond to pause event of our Lifecycle owner (activity/fragment in our case)
// So let's stop receiveing location updates here and remove callback
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) // Optional if you want to cleanup references
fun cleanUp() {
// this method will respond to destroy event of our Lifecycle owner (activity/fragment in our case)
// Clean up code here
}
}
Now from your activity, you can directly make your LocationUpdates, and receive callback.
class MyActivity : AppCompatActivity() {
private lateinit var mLocationUpdates: LocationUpdates
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//Initialize your LifecycleObserver here & assign it to this activity's lifecycle
lifecycle.addObserver(mLocationUpdates)
}
}
You can refer to how to handle Lifecycle & Codelabs example.
Edit:
If you want to have ViewModel for that job, consider this:
class MyViewModel : ViewModel {
private lateinit var mLocationUpdates: LocationUpdates
constructor() : super() {
// initialize LocationUpdates here
}
// Assign our LifecyclerObserver to LifecycleOwner
fun addLocationUpdates(lifecycle: Lifecycle){
lifecycle.addObserver(mLocationUpdates)
}
//Optional, we really don't need this.
fun removeLocationUpdates(lifecycle: Lifecycle){
lifecycle.removeObserver(mLocationUpdates)
}
}
If your LocationUpdates depends upon Context, consider using AndroidViewModel.
We can now observe our location updates # any activity/fragment using LiveData, and assign our LifecycleObserver like below:
class MyActivity : AppCompatActivity() {
private val viewModel: MyViewModel by lazy {
return#lazy ViewModelProviders.of(this#MyActivity).get(MyViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.addLocationUpdates(lifecycle)
}
}
Please note: there's still lot to cover but making this answer as short as possible. So, if you're still confused about something related then please feel free to ask me in comment. I will edit my answer.
with java 8 LifecycleObserver has been deprecated. According to the [docs][1] it is not recommended to use this class as it uses reflection.
Rather the docs recommend using DefaultLifecycleObserver. To do that, extend your ViewModel class with DefaultLifecycleObserver like:
class MyViewModel : ViewModel(), DefaultLifecycleObserver {//implement default lifecycle observer
override fun onCreate(owner: LifecycleOwner) {//override lifecycle events
super.onCreate(owner)
}
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
}
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
}
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
}
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
}
}
and get all the lifecycle event callbacks in your viewmodel by registering your viewmodel as lifecycle event observer in your view class (e.g. Activity class) like:
class MyActivity : AppCompatActivity() {
private val myViewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
lifecycle.addObserver(splashViewModel)//registering observer
...
}
}
its just and update to the answer by #farid_z with kotlin and new sdk.
[1]: https://developer.android.com/reference/androidx/lifecycle/LifecycleObserver

Android LiveData Observer not active after first update

I'm trying out a basic implementation of Architecture Component's Live Data with Kotlin like this:
class MarketFragment : LifecycleFragment(){
......
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel=ViewModelProviders.of(this).get(MarketViewModel::class.java)
viewModel.book?.observe(this, Observer { book-> //updateUI })
....
My ViewModel class is created like this:
class MarketViewModel : ViewModel()
{
var book: MutableLiveData<Book>? =MutableLiveData()
var repository: Repository?= Repository()
init {
update("parameter")
}
fun update(s: String)
{
book=repository?.getBook(s)
}
}
And My Repository:
fun getBook(bookSymbol:String):MutableLiveData<Book>
{
val book=MutableLiveData<Book>()
......
call . enqueue (object : Callback<Book> {
override fun onResponse(call: Call<Book>?, response: Response<Book>?) {
book.value=response?.body()
}
.....
})
return book
}
}
And all of this works great and UI is updated as it should but only for the first time. If i try to make manual calls to update the viewModel from a UI action, the retrofit call still works as expected but the new data is not sent to the Observer in the Fragment:
//this doesn't work:
viewModel.update("string")
//This returns false:
viewModel.book.hasActiveObservers()
Is it the expected behaviour for an Observer to become inactive after the first trigger?
You are creating a new MutableLiveData instance each time you are calling getBooks
Hence your observer is not observing the right LiveData anymore.
To solve this
Your ViewModel should keep only one (immutable) LiveData instance
That immutable LiveData instance could either be:
A MediatorLiveData, which source is the repository's LiveData
A transformation of the repository's LiveData
That implies the repository method getBooks is only called once on initialization of the ViewModel, and that either refreshes itself OR you have to call another method on the repository to trigger a refresh.

Categories

Resources