getViewLifecycleOwner() in DialogFragment leads to crash
Asked Answered
I

6

37

I use DialogFragment (onCreateDialog) and ViewModel for it. But, when I try to pass getViewLifecycleOwner() to the LiveData::observe method I get the error below:

java.lang.IllegalStateException: Can't access the Fragment View's LifecycleOwner when getView() is null i.e., before onCreateView() or after onDestroyView().

Is it possible to use getViewLifecycleOwner() inside a DialogFragment?

Indoaryan answered 19/2, 2019 at 12:15 Comment(1)
In onCreateDialog Dialog is creating not yet created . Try in onViewCreated(). I myself not much used LifecycleOwner.Auroraauroral
R
19

This is the official recommendation for DialogFragments:

Note: When subscribing to lifecycle-aware components such as LiveData, you should never use viewLifecycleOwner as the LifecycleOwner in a DialogFragment that uses Dialogs. Instead, use the DialogFragment itself, or if you're using Jetpack Navigation, use the NavBackStackEntry.

So you can just observe things as normal, but instead of viewLifecycleOwner you pass this, or the current backstack entry (e.g. findNavController().currentBackStackEntry). No need to override onCreateView just to force a viewLifecycleOwner to be created or anything!

Rockribbed answered 21/6, 2021 at 18:7 Comment(0)
E
16

This happens because of how the DialogFragment is created. If you use onCreateDialog() than a slightly different lifecycle is used for this type of Fragment. The onCreateView() will not be used, thus the viewLifecycleOwner for this Fragment won't be initialized.

As a workaround for this, you can use the Fragment instance as the owner for the observer: .observe(this, Observer {...}. Although you will get a warning for using this instead of the viewLifecycleOwner.

Esmerolda answered 28/2, 2020 at 6:54 Comment(2)
I seem to get no warning using this (MyDialogFragment). Is this approach preferable to using either getActivity() or requireActivity()?Waldman
@Waldman As long as you don't show the DialogFragment multiple times with the same fragment instance. If you do, observers will be subscribed on a second time and you might have weird issues due to double observation. That's because by using this as the lifecycle owner, observers are only removed when onDestroy is called, and it isn't called by simply dismissing the dialog.Maura
C
8

Your case is slightly different but I think the concept is kind of the same. Just use this.getActivity() in your Dialog Class and pass it as LifeCycleOwner. I had the same problem because I used LiveData and Retrofit and LiveData needs a reference. The DialogFragment sets its LifeCycleOwner at some point but it is not at any of the methods mentioned above. By using the getActivity() you can use your observer as early as in onCreateDialog method. Here is some portion of my code that at first caused some issue when I tried to pass a null referenced this.getViewLifecycleOwner() instead of the activity.

@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
       FragmentActivity activity = this.getActivity();
       binding = DialogSelectIssuesBinding.inflate(LayoutInflater.from(getContext()));

       RetroRepository.
            getDefault().
            getAllIssues().
            observe(this.getActivity(), listLiveDataResponse -> {
                //ToDo Check for errors and Bind the data here 
            });


       AlertDialog alertDialog = new AlertDialog.Builder(activity)
                            .setView(binding.getRoot())
                            .setTitle("Please select issues from the list below:")
                            .setNegativeButton("CANCEL", null)
                            .setPositiveButton("ADD", null)
                            .create();
       alertDialog.setCanceledOnTouchOutside(false);
       return alertDialog;
}
Childbed answered 18/7, 2019 at 23:39 Comment(1)
What if fragment is destroyed then recreated, but the activity isn't? Old observers still reference the old fragment, and you got yourself a memory leak! The whole point of viewLifecycleOwner is to remove the observers when the fragment is destroyed.Maura
J
7

this happens because the lifecycle of DialogFragment is different from Fragment; onCreateDialog is called before onCreateView, so viewLifecycleOwner is unavailable... I worked around the issue by:

  • implemeting onCreateView instead of onCreateDialog
    • can access viewLifecycleOwner from within onCreateView
    • view returned from onCreateView is put into a dialog for us by DialogFragment...
    • you will need to create your own buttons, and titles in the dialog...

supplemental code:

class TextInputDialogFragment : DialogFragment() {

    ...

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View? {
        val viewBinding = FragmentDialogTextInputBinding.inflate(layoutInflater, container, false)

        val titleText = params.title.localize(requireContext())
        viewBinding.toolbar.isVisible = titleText.isNotBlank()
        if (titleText.isNotBlank()) {
            viewBinding.toolbar.title = titleText
        }

        viewBinding.recyclerview.adapter = ListItemAdapter(
            viewLifecycleOwner, requireContext().app.nowFactory, viewModel.fields
        )

        viewBinding.buttonAffirm.setOnClickListener {
            listener.onOkPressed(viewModel.userInputtedText.value)
            dismiss()
        }

        viewBinding.buttonReject.setOnClickListener {
            dismiss()
        }

        viewModel.enablePositiveButton.observe(viewLifecycleOwner) { isEnabled ->
            viewBinding.buttonAffirm.isEnabled = isEnabled
        }

        return viewBinding.root
    }

    ...
}

the layout file used

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:minHeight="?attr/actionBarSize"
            tools:title="Title" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerview"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

        <LinearLayout
            style="?buttonBarStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:clickable="false"
            android:gravity="end"
            android:orientation="horizontal"
            android:padding="@dimen/min_touch_target_spacing_half">

            <Button
                android:id="@+id/button_reject"
                style="?buttonBarButtonStyle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/min_touch_target_spacing_half"
                android:text="@android:string/cancel" />

            <Button
                android:id="@+id/button_affirm"
                style="?buttonBarButtonStyle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="@dimen/min_touch_target_spacing_half"
                android:text="@android:string/ok" />

        </LinearLayout>

    </LinearLayout>

</layout>
Jasperjaspers answered 23/7, 2020 at 7:42 Comment(0)
D
0

My solution was a little bit wacky...

My component was using the getViewLifecycleOwnerLiveData() .... so:

private final MyLifeCycleOwner owner = new MyLifeCycleOwner();

private final MutableLiveData<LifecycleOwner> result = new MutableLiveData<>();

@NonNull
@Override
public LiveData<LifecycleOwner> getViewLifecycleOwnerLiveData() {
    return result;
}

@Override
public void onDestroyView() {
    super.onDestroyView();
    owner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
    result.setValue(null);
}

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    result.setValue(owner);
    owner.getLifecycle();
    owner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
    return super.onCreateView(inflater, container, savedInstanceState);
}

Because FragmentViewLifecycleOwner is package private ... that's the reason for the MyLifeCycleOwner class.

I was not gonna change my components because of some mismanagement on the android architecture...

Deceit answered 19/11, 2020 at 19:51 Comment(0)
B
0

Because DialogFragment pops over the view of a parent fragment it can use its lifecycle owner. Thus code will look like this:

parentFragment?.viewLifecycleOwner?.let {
    binding.lifecycleOwner = it
}
Badoglio answered 20/7, 2021 at 15:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.