Adrian Pascu
Adrian Pascu

Reputation: 1039

Android: Clear DialogFragment ViewModel

I am creating a dialog extending BottomSheetDialogFragment. Inside this dialog I have a ViewModel to hold the state of the fragment (it has some inputs inside). Some of the properties of the ViewModel are LiveData. I've noticed that when dismissing the dialog (either calling dismiss() or tapping outside the dialog) and reopening it, the values of the LiveData properties persist.

So as a solution, I thought to override the onDismiss() event to manually clear the data inside the ViewModel:

 override fun onDismiss(dialog: DialogInterface) {
//        Clear the viewmodel
        viewModel.clear()
        viewModelStore.clear()

        super.onDismiss(dialog)
//        Unsubscribe from observer
        viewModel.getContact().removeObservers(viewLifecycleOwner)

    }

viewModel.clear() is a function in my ViewModel that resets the data inside to default values.

Apparently, this wasn't enough either, since even after resetting the data inside the ViewModel before the dismissal, when reopening the dialog, the ViewModel hold the data that it held before calling clear()

Since the properties that are not LiveData do reset, I believe there is a specific way of reseting LiveData value, but I have no idea how to do it. Can anyone help me out? Thanks in advance!

Upvotes: 0

Views: 2620

Answers (2)

Jenea Vranceanu
Jenea Vranceanu

Reputation: 4694

Adrian kindly provided me with a sample of his project. This answer is based on the results of debugging the sample project.

The problem causing the issue you have is actually located in your MainActivity class. You instantiate AddReminderFragment in onCreate method of your activity and when FAB is pressed you display the instance of this fragment.

Everything seems good, except - you always present the same instance of AddReminderFragment. It means when you present this fragment for the first time you get brand new AddReminderViewModel that is lazy loaded using by viewModels().

Initially, I thought the lazy loading by viewModels() was causing this issue because it uses the fragment itself as the view model store and it caches created view model in case you will want to reuse it later. It was not the case.

How to fix the issue?

Never store references to activities and fragments or avoid as much as you can and be careful when you have any. It can potentially lead to a memory leak.

Shortly described what I did:

  1. Removed private lateinit var modal: AddReminderFragment variable from MainActivity;
  2. Removed ITimePipupCallback from MainActivity;
  3. Removed onAttach(context: Context) from TimePopupFragment;
  4. Implemented fun setCallback(callback: ITimePipupCallback): TimePopupFragment in TimePopupFragment;
  5. Before showing an instance of TimePopupFragment from AddReminderFragment passed in a callback ITimePipupCallback instance. As if it was a callback to a button click of an AlertDialog;
  6. Removed onTimePicked and onDayPicked methods from AddReminderFragment as they are no longer needed.

Updated MainActivity:

class MainActivity : AppCompatActivity() {

    companion object {
        const val AddReminderModalTag = "add_reminder_modal"
    }

    private lateinit var binding: MainActivityBinding
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = MainActivityBinding.inflate(layoutInflater)
        binding.showModalHandler = this
        setContentView(binding.root)
    }

    fun showAddReminderModal(view: View) {
        AddReminderFragment.newInstance().show(supportFragmentManager, AddReminderModalTag)
    }
}

Updates made to the AddReminderFragment:

class AddReminderFragment : BottomSheetDialogFragment() {
    ...
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        ...
        
        //Bind the add time button
        binding.addTimeButton.setOnClickListener {
            TimePopupFragment
                .newInstance(viewModel.getReminderType().value!!)
                .setCallback(object : ITimePipupCallback {
                    override fun onTimePicked(hour: Int, minute: Int) {
                        viewModel.addTimeToRemind(hour, minute)
                    }

                    override fun onDayPicked(day: Int) {
                        viewModel.addTimeToRemind(day)
                    }
                })
                .show(
                    parentFragmentManager,
                    TIME_PICKER_TAG
                )
        }

        ...

        return binding.root
    }

    ...
}

Updates made to TimePopupFragment:

class TimePopupFragment : DialogFragment() {
    ...
    private lateinit var callback: ITimePipupCallback
    
    // Removed `onAttach` method
    fun setCallback(callback: ITimePipupCallback): TimePopupFragment {
        this.callback = callback
        return this
    }

    ...

}

You can safely remove override fun onDismiss(dialog: DialogInterface) from AddReminderFragment.

Upvotes: 2

Snehil
Snehil

Reputation: 45

Make sure that you are using fragment's view model. You should not clear the view model by posting values to live data postValue(T value) as it is unnecessary.

Use the following view model,

class MyViewModel : ViewModel() {

    private val users: MutableLiveData<List<User>> by lazy {
        MutableLiveData().also {
            loadUsers()
            }
        }

    fun getUsers(): LiveData<List<User>> {
        return users
    }

    private fun loadUsers() {
        // Do an asynchronous operation to fetch users.
    }
}

In DialogFragment, initialize the view model by

ViewModelProviders.of(<FRAGMENTT>).get(MyViewModel.class);

If you are using ktx then initialize the view model by

// Get a reference to the ViewModel scoped to this Fragment
val viewModel by viewModels<MyViewModel>()

Upvotes: 0

Related Questions