ViewModel onCleared() called on screen rotation - android

I am using Hilt to inject a ViewModel into a fragment. But on screen rotation, ViewModel.onCleared() is called. Is this expected behaviour? I always thought ViewModel survives screen rotation.
Due to this, the ViewModel is recreated which I verified by comparing the ViewModel hash code on screen rotation.
Here is my fragment code:
#AndroidEntryPoint
class DashboardFragment : BaseFragment() {
private val dashboardViewModel: DashboardViewModel by viewModels()
private var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = DataBindingUtil.inflate(
inflater, R.layout.fragment_dashboard, container, false
)
binding.viewModel = dashboardViewModel
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.adapter = adapter
setObservers()
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
}
and the ViewModel looks like this:
#HiltViewModel
class DashboardViewModel #Inject constructor() : ViewModel() {
init {
//Some code
}
override fun onCleared() {
super.onCleared()
Timber.e("cleared")
}
}

Maybe you missed .addToBackStack when you run this fragment.
But most importantly: make sure that the fragment is not recreated again when you rotate the screen, for example, using
if (savedInstanceState == null) {
...
}
inside of onCreate, onCreateView and so on

This is not what should be happening with a ViewModel. As you said they are designed to survive recreation due to an orientation change.
What seems to be happening here is that you're recreating your Fragment whenever the screen is rotated, in onCreate of an activity or something similar. This then causes the ViewModel to get cleared and re-instantiated.

Related

Best practices for Fragments + ViewBinding

From a Google Codelab (can't remember which one), they adviced doing the following for fragments:
class MyFragment : Fragment() {
private var _binding: MyFragmentBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = MyFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
And then accessing the views with e.g. binding.button1.
Is there a specific reason for doing it like this, with _binding and binding? Are there better methods? Perhaps an extension for Fragments - like a BaseFragment - to avoid code duplication.
It's not recommended to use BaseFragment or BaseActivity or BaseViewModel... it will just add boilerplate code to your project.
For binding you can just use it like this:
Declaration:
private var binding: MyFragmentBinding? = null
onCreateView:
binding = MyFragmentBinding.inflate(inflater, container, false)
binding?.root
Usage:
binding?.button...
binding?.text...
binding?.cardView...
onDestroyView:
binding = null
And everything is going to work just fine but we use the null check a lot (?) and it's making the code messy and we need to get a lot of null checks if we need something from a certain view, so we are sure that between onCreateView and onDestroyView, the binding is not null so we have _binding and binding:
private var _binding: MyFragmentBinding? = null
private val binding get() = _binding!!
We make _binding mutable with var so we can give it a value, and we make it nullable so we can clear it later.
And we have binding that have a custom getter so that means that each time we call binding it's going to return the latest value from _binding and force that it's not null with !!.
Now we seperate our variables, we have _binding to initialize and clear our binding, and we have binding that is immutable and not nullable to use it only for accessing views without null check ?
See this question for some answers about the reason why binding needs to be nullable in a fragment.
See this answer of mine where I linked some articles about the problems with BaseFragments. You can usually achieve the code reuse without the drawbacks of inheritance by using extension properties and functions.
Here is an example of a property delegate that takes care of releasing the ViewBinding reference when necessary and rebuilding it when necessary. If you use this, all you need is a single binding property. Example is from the article about this tool.
class FirstFragment: Fragment(R.layout.first_fragment) {
private val binding by viewBinding(FirstFragmentBinding::bind)
override fun onViewCreated(view: View, bundle: Bundle?) {
super.onViewCreated(view, bundle)
binding.buttonPressMe.onClick {
showToast("Hello binding!")
}
}
I just saw that CommonsWare has adressed this issue in this post.
Here is the parent class:
abstract class ViewBindingFragment<Binding : ViewBinding>(
private val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> Binding
) : Fragment() {
private var binding: Binding? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
return bindingInflater(inflater, container, false).apply { binding = this }.root
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
protected fun requireBinding(): Binding = binding
?: throw IllegalStateException("You used the binding before onCreateView() or after onDestroyView()")
protected fun useBinding(bindingUse: (Binding) -> Unit) {
bindingUse(requireBinding())
}
}
He then subclasses ViewBindingFragment like so:
class ListFragment :
ViewBindingFragment<TodoRosterBinding>(TodoRosterBinding::inflate) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
useBinding { binding ->
binding.items.layoutManager = LinearLayoutManager(context)
}
}
}
Though I am not sure it will eventually lead to less code, if useBinding { binding -> } needs to be called in several functions.

viewBinding not making any changes inside fragment

My Goal
I am trying to access the widget that was created inside my fragment using viewBinding.
What I have done / Info about my app
The language I am using is kotlin.
I have already added the code below into gradle:
buildFeatures{
dataBinding = true
viewBinding = true
}
I have tested binding.aTextView.setText("Code working.") inside my main activity and it works.
What's the problem
I have tested the setText code inside activity and it works. The problem right now is the same code when I move into the fragment it wouldn't work. And I am sure that the code has been executed as I putted a toast above it and the toast executed successfully which mean it should have at least reached that point before but not sure due to what reason there wasn't any changes.
My mainActivity Code:
class MainProgramActivity : AppCompatActivity() {
lateinit var binding: ActivityMainProgramBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainProgramBinding.inflate(layoutInflater)
setContentView(binding.root)
replaceFragment(FragmentMainPage())
}
private fun replaceFragment(fragment: Fragment){
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragmentContainerView,fragment)
fragmentTransaction.commit()
}
}
My fragment code:
class FragmentMainPage : Fragment(R.layout.fragment_main_page) {
lateinit var binding: FragmentMainPageBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Toast.makeText(getActivity(),"Text!",Toast.LENGTH_SHORT).show();
binding = FragmentMainPageBinding.inflate(layoutInflater)
binding.aTextView.setText("Code working") //<-- I want this code to make changes towards the textView
return super.onCreateView(inflater, container, savedInstanceState)
}
}
The aTextView itself is empty at the beginning, the expected result will be the aTextView to show "Code working".
I see two problems with your code. First, exactly what Michael pointed out. You're returning the super method when you should be returning the View you just created (binding.root). Second, you're currenly leaking your fragment. When you viewbind a fragment, you are supposed to set the variable to null in onDestroyView(), as per defined in the documentation.
class FragmentMainPage : Fragment(R.layout.fragment_main_page) {
private var _binding: FragmentMainPageBinding? = null
private val binding get() = _binding!! // non-null variable in order to avoid having safe calls everywhere
// create the view through binding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentMainPageBinding.inflate(layoutInflater, container, false)
return binding.root
}
// view already created, do whatever with it
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.aTextView.setText("Code working")
}
// clear the binding in order to avoid memory leaks
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

Unable to observe livedata set in viewmodel from activity

I am a newbie Android developer, and I am trying to observe a boolean set in the ViewModel from its parent's activity. I can observe its initial state as soon as the app launches, but any change applied later on doesn't seem to trigger the observer (i.e. when I switch the fragments).
Here is the code for my ViewModel:
class MyMusicViewModel : ViewModel() {
private var _MyMusicViewOn = MutableLiveData<Boolean>()
val MyMusicViewOn: LiveData<Boolean> get() = _MyMusicViewOn
init {
Timber.i("MyMusicViewModel Init Called!")
setMyMusicView(true)
}
override fun onCleared() {
super.onCleared()
Timber.i("MyMusicViewModel Cleared!")
setMyMusicView(false)
}
fun setMyMusicView(setter: Boolean) {
Timber.i("MyMusicViewModel setter called! %s", setter)
_MyMusicViewOn.value = setter
}
}
And here is its parent's activity:
class FullscreenActivity : AppCompatActivity() {
private val viewModel: MyMusicViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.MyMusicViewOn.observe(this, Observer { MyMusicViewOn ->
Timber.i("Observer called for MyMusicViewOn %s", MyMusicViewOn)
})
}
}
And in case you wanna see the ViewModel's related fragment, here it is:
class MyMusicFragment : Fragment() {
private lateinit var viewModel: MyMusicViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val binding = DataBindingUtil.inflate<FragmentMyMusicBinding>(
inflater,
R.layout.fragment_my_music,
container,
false
)
viewModel = ViewModelProvider(this).get(MyMusicViewModel::class.java)
return binding.root
}
override fun onResume() {
super.onResume()
Timber.i("MyMusicViewFragment resumed!")
viewModel.setMyMusicView(true)
}
}
What I am trying to accomplish is to observe the onResume(), onCleared() and init{} functions whenever they are called by changing the status of the MyMusicViewOn MutableLiveData Boolean. What I don't understand is why that boolean doesn't trigger the observer set in the parent activity whenever it changes.
Thankyou in advance for any thoughts!
All the best,
Fab.
I'm guessing that however you are populating that viewModel property in your Fragment, you are not using the Activity's ViewModel instance. The easiest way to get the same instance that the Activity is using would be to use the activityViewModels delegate:
private val viewModel: MyMusicViewModel by activityViewModels()

Android:SaveState, Fragments and ViewModel: what am I doing wrong?

I have the single activity with several fragments on top, as Google recommends. In one fragment I wish to place a switch, and I wish to still know it's state when I come back from other fragments. Example: I am in fragment one, then I turn on the switch, navigate to fragment two or three, go back to fragment one and I wish to load that fragment with that switch in the on position as I left it.
I have tried to copy the examples provided by google advocates, just to see the code to fail hard and do nothing.
/////////////////////////////////////////////////////////////////
//Inside the first fragment:
class myFragment : Fragment() {
companion object {
fun newInstance() = myFragment()
}
private lateinit var viewModel: myViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.my_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
**viewModel = ViewModelProvider(this, SavedStateVMFactory(this)).get(myViewModel::class.java)
//Here I was hoping to read the state when I come back.
switch_on_off.isChecked = viewModel.getSwRoundTimerInit()**
subscribeToLiveData() //To read liveData
switch_on_off.setOnCheckedChangeListener { _, isChecked ->
viewModel.setOnOff(isChecked)
}
}//End of onActivityCreated
//other code...
/////////////////////////////////////////////////////////////////////////
//On the fragment ViewModel
class myViewModel(private val **mState: SavedStateHandle**) : ViewModel() {
//SavedStateHandle Keys to save and restore states in the App
private val swStateKey = "SW_STATE_KEY"
private var otherSwitch:Boolean //other internal states.
//Init for the other internal states
init {
otherSwitch = false
}
fun getSwRoundTimerInit():Boolean{
val state = mState[swStateKey] ?: "false"
return state.toBoolean()
}
fun setOnOff(swValue:Boolean){
mState.set(swStateKey, swValue.toString())
}
}
This does not work. It always loads the default (off) value, as if the savedState is null all the time.
change
//fragment scope
viewModel = ViewModelProvider(thisSavedStateVMFactory(this)).get(myViewModel::class.java)
to
//activity scope
viewModel = activity?.let { ViewModelProviders.of(it,SavedStateVMFactory(this)).get(myViewModel::class.java) }
https://developer.android.com/topic/libraries/architecture/viewmodel#sharing

How to unbind viewModel from activity when it's destroying

I have my miewModel, which I'm injecting to my fragment throw ViewModelProviders.of(activity, viewModelFactory).get(MyViewModel::class.java).
It's work fine on first time of fragment creation, but if I will close my fragment, then I'will get an error "layout must not be null" which points on some of my layouts which I'm using in my fragment.
As I understood, this ishue happend because databinding still have some links to my fragment. So how to unbind it?
class MyFragment: Fragment(), Injectable {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity?.window?.changeStatusBarColor(this.requireContext(), R.color.yellow_status_bar)
val binding: MyLayoutBinding = DataBindingUtil.inflate(inflater, R.layout.my_layout, container, false)
binding.viewModel = viewModel
viewModel.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
}
}
ViewModelModule:
#Binds
#IntoMap
#ViewModelKey(MyViewModel::class)
internal abstract fun bindMyViewModel(myViewModel: MyViewModel): ViewModel
To bound ViewModel life cycle to fragment you need to call:
val vm = ViewModelProviders.of(fragnemt, viewModelFactory)[MyViewModel::class.java]
instead of:
val vm = ViewModelProviders.of(activity, viewModelFactory)[MyViewModel::class.java]
Don't forget to release resources in ViewModel.onCleared()

Categories

Resources