3

I am currently playing around to get a hang of the fragment's lifecycle in relation to ViewModel and LiveData.

I have 2 fragments, fragmentA and fragmentB. I add the Observer in the onCreate method of each fragment.

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    sharedViewModel = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
    sharedViewModel.getText().observe(this, new Observer<CharSequence>() {
        @Override
        public void onChanged(CharSequence charSequence) {
            editText.setText(charSequence);
        }
    });
}

Each fragment has a button that changes LiveData in the shared ViewModel

public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    [...]

    buttonOk.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            sharedViewModel.setText(editText.getText());
        }
    });

    [...]
}

SharedViewModel:

public class SharedViewModel extends ViewModel {
    private MutableLiveData<CharSequence> text = new MutableLiveData<>();

    public void setText(CharSequence input) {
        text.setValue(input);
    }

    public LiveData<CharSequence> getText() {
    return text;
    }
}

When I click a button, I replace the fragment for the other one.

public class MainActivity extends AppCompatActivity {
    Fragment fragmentA = new FragmentA();
    Fragment fragmentB = new FragmentB();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.container_a, fragmentA)
                    .commit();
        }
    }

    public void switchToA(View v) {
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.container, fragmentA)
                .commit();
    }

    public void switchToB(View v) {
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.container, fragmentB)
                .commit();
    }
}

Replace causes the fragment to be fully destroyed and run through it's onCreate method again the next time it is added. I can confirm that onCreate is called for each fragment placed onto the screen and the Observer is added. But once I replaced a fragment and re-added it, it completely stops getting any updates in onChanged. Even the ones it sent itself. onChanged is just not triggered anymore. I don't understand why.

Edit:

I actually found out that the followig if check in the LiveData class returns the 2nd time I try to add the Observer (after replacing the fragment for the first one):

@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
    assertMainThread("observe");
    if (owner.getLifecycle().getCurrentState() == DESTROYED) {
        // ignore
        return;
    }

Hence, the Observer is not added anymore. Why does getCurrentState() return DESTROYED when I try to re-add the fragment?

In short: the Observer is removed when the fragment is removed, but it doesn't add another Observer the next time the fragment is added.

Florian Walther
  • 6,237
  • 5
  • 46
  • 104

1 Answers1

5

As per the Lifecycle.State.DESTROYED documentation:

After this event, this Lifecycle will not dispatch any more events.

I.e., DESTROYED is a terminal state and once it is destroyed, that Lifecycle will always be destroyed.

This means there are two correct ways to do what you want:

  1. Create a new Fragment instance each time you call switchToA or switchToB. Since all the state is destroyed when you remove a Fragment, you aren't gaining anything by reusing Fragment instances.

  2. Don't use replace, but instead use attach() and detach() (i.e., attach the one you want to display, detach the one you want to hide). Fragments keep their state when detached (they aren't destroyed), so re-attaching it will move it back to resumed.

ianhanniballake
  • 191,609
  • 30
  • 470
  • 443
  • Thank you. So I guess with a ViewModel there is no need in reusing a Fragment instance that was removed anymore, since we update the new instance with the most recent data anyways? – Florian Walther Nov 03 '18 at 17:01
  • When you destroy a Fragment, its ViewModel is destroyed as well, so that's certainly a downside of option #1. If you go with option #1, it may make more sense for these to be Activity scoped ViewModels which wouldn't be destroyed when the Fragment is destroyed – ianhanniballake Nov 03 '18 at 18:28
  • Right, I actually use an activity scoped ViewModel in the question. But once a fragment went through onDestroy, we always have to create a new instance like you explain in `1.`, right? – Florian Walther Nov 03 '18 at 18:41
  • Ah, missed that you were using an Activity scoped ViewModel. In that case, yes option 1 would work perfectly. – ianhanniballake Nov 03 '18 at 18:43
  • But is this a bug? I feel like if the fragment went through onCreate again, it's lifecycle should be "revived" – Florian Walther Nov 03 '18 at 18:55
  • Yeah, I think creating a new Lifecycle instance makes a lot of sense if you want to [file a bug](https://issuetracker.google.com/issues/new?component=460964). – ianhanniballake Nov 03 '18 at 19:38
  • Actually, I checked it further with some log messages, and `getCurrentLifecyle` returns `RESUMED` after adding the first fragment back to the container. This is so confusing. – Florian Walther Nov 03 '18 at 22:58
  • You should check the Lifecycle.State in onCreate() since that's when you are registering your Observer – ianhanniballake Nov 03 '18 at 23:09
  • Good idea! The first time I add the fragment, it has the state INITIALIZED in onCreate. After removing and adding it, it has DESTROYED in onCreate, but CREATED in onCreateView. Does this mean onCreate is too early to add an Observer when "reviving" the fragment? Unless you create a new instance with new Fragment() – Florian Walther Nov 04 '18 at 08:44
  • Seems like a bug to me - it should be consistent whether the first time through or second. Using a new Fragment is a good workaround for right now – ianhanniballake Nov 04 '18 at 15:10
  • Gotcha. Thank you, that was a nice little quest! – Florian Walther Nov 04 '18 at 15:19
  • Please do file a bug – ianhanniballake Nov 04 '18 at 15:31