0

Consider the following scenario. I have two fragments, FormFragment and SelectionFragment that are hosted by MainActivity. I have a SharedViewModel that I plan on using for data communication between the fragments.

FormFragment has a spinner, tapping on which opens up the SelectionFragment.

SelectionFragment consists of a RecyclerView that has a list of Item objects.

SharedViewModel has two LiveDatas, LiveData<Item> selectedItem and LiveData<List<Item>> items

Use case:

  1. On app launch, user lands on the FormFragment which has a spinner with no selected item
  2. When user taps on the spinner, we navigate to SelectionFragment
  3. SelectionFragment gets the list of items from the SharedViewModel and populates the RecyclerView
  4. When user taps on an item, we update the selectedItem in the SharedViewModel and pop the SelectionFragment from the back stack to go back to the FormFragment
  5. FormFragment spinner now shows the selected item
  6. User can select a different item by performing the same process

Problem:

After completing steps 1 to 5 above, when we try to choose another item by performing Step 1 again, the observer inside SelectionFragment immediately fires (since selectedItem is not null), and pops the fragment from the back stack. This is obviously not the desired behaviour since we want the user to be able to select a different item at will.

How can I properly observe the selectedItem inside SelectionFragment so it doesn't cause its observer to fire immediately?

class FormFragment extends Fragment {

    @BindView(R.id.spinner) protected Spinner spinner;

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        SharedViewModel viewModel = ViewModelProviders.of(requireActivity()).get(SharedViewModel.class);

        viewModel.getSelectedItem().observe(getViewLifecycleOwner(), selectedItem -> {
            spinner.setSelected(selectedItem);
        });

        spinner.setOnClickListener(v -> {
            // navigate to SelectionFragment
            requireFragmentManager().beginTransaction().replace(R.id.container, new SelectionFragment()).commit();
        });
    }
}

class SelectionFragment extends Fragment {

    @BindView(R.id.recycler_view) protected RecyclerView recyclerView;

    private ItemsAdapter adapter = new ItemsAdapter();

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        SharedViewModel viewModel = ViewModelProviders.of(requireActivity()).get(SharedViewModel.class);

        viewModel.getItems().observe(getViewLifecycleOwner(), items -> {
            // load items into recycler view
            adapter.submitList(new ArrayList<>(items));
        });

        viewModel.getSelectedItem().observe(getViewLifecycleOwner(), selectedItem -> {
            // pop fragment from back stack
            requireFragmentManager().popBackStackImmediate();
        });

        adapter.setOnItemClickedListener(item -> {
            // update selected item in view model when user taps on an item in the list
            viewModel.setSelectedItem(item);
        });

        recyclerView.setAdapter(adapter);
    }
}

class SharedViewModel extends ViewModel {

    private final MutableLiveData<Item> selectedItem = new MutableLiveData<>();
    private Repository repository;

    // repository is injected (unimportant to the problem)
    SharedViewModel(Repository repository) {
        this.repository = repository;
    }

    LiveData<List<Item>> getItems() {
        return repository.getItems();
    }

    LiveData<Item> getSelectedItem() {
        return selectedItem;
    }

    void setSelectedItem(Item item) {
        selectedItem.setValue(item);
    }
}
Bazinga
  • 489
  • 1
  • 5
  • 16

1 Answers1

0

In your FormFragment, after you read the data from the SelectionFragment, set it to null in the LIveData object.

Alternatively, you could wrap your objects in a Consumable, something like this

class ConsumableValue<T>(private val data: T) {

    private var consumed = false

    @UiThread
    fun consume(block: ConsumableValue<T>.(T) -> Unit) {
        val wasConsumed = consumed
        consumed = true
        if (!wasConsumed) {
            this.block(data)
        }
    }

    @UiThread
    fun ConsumableValue<T>.release() {
        consumed = false
    }
}
Francesc
  • 25,014
  • 10
  • 66
  • 84