Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

LiveData observer is being triggered multiple times using Navigation Component

Scenario: I have two fragments named FirstFragment and UnitFragment. I go from FirstFragment to UnitFragment to select a unit to come back to FirstFragmet using navController.popBackStack(); and send unit data to FirstFragment which is observing unit data.

This is my onViewCreated of FirstFragment:

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    if (viewModel == null) { // Lazy Initialization
        ApiService apiService = ApiServiceProvider.getInstance();
        AddNewWareViewModelFactory addNewWareViewModelFactory = new AddNewWareViewModelFactory(apiService);
        viewModel = new ViewModelProvider(this, addNewWareViewModelFactory).get(AddWareViewModel.class);
    }

    Log.i(TAG, "OnViewCreated -----> Called");
    viewModel.callNewWare(parentCode);
    viewModel.getNewWareResponse().observe(getViewLifecycleOwner(),
            resObject -> Log.i(TAG, "API Response LiveData Count -----> " + count++)); // Started From Zero

    NavHostFragment navHostFragment = (NavHostFragment) requireActivity()
            .getSupportFragmentManager()
            .findFragmentById(R.id.container);

    binding.button.setOnClickListener(v -> {
        if (navHostFragment != null) {
            NavController navController = navHostFragment.getNavController();
            navController.navigate(FirstFragmentDirections.actionFirstFragmentToUnitFragment());
        }
    });


    if (navHostFragment != null) {
        NavController navController = navHostFragment.getNavController();
        NavBackStackEntry navBackStackEntry = navController.getCurrentBackStackEntry();
        if (navBackStackEntry != null) {
            SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle();
            MutableLiveData<Unit> unitLiveData = savedStateHandle.getLiveData("unit_data");
            unitLiveData.observe(getViewLifecycleOwner(), unit -> binding.tvUnit.setText(unit.getTitle()));
        }
    }
}

This is the LogCat result:

--- Go to FirstFragment for first time ---
I/FirstFragment: OnViewCreated -----> Called
I/FirstFragment: API Response LiveData Count -----> 0
--- Button clicked to go to UnitFragment to select a unit ---
I/UnitFragment: Selected Unit -----> Meter
--- Come back to FirstFragment ---
I/FirstFragment: OnViewCreated -----> Called
I/FirstFragment: API Response LiveData Count -----> 1
I/FirstFragment: API Response LiveData Count -----> 2

As you see in LogCat result, every time I click the button and go to UnitFragment and come back to FirstFragment the onViewCreated will call again and API LiveDataObserver will be triggered TWO times!!!

I know onViewCreated will call again because Navigation Component replaces the fragments instead of adding them. but I don't know why LiveData observer is being triggered two times.

I read this post but he seems to have ignored Navigation Component.

I need a solution to ...

  1. Avoid calling onViewCreated codes again.
  2. Avoid triggering LiveData observer again.
like image 454
Alireza Noorali Avatar asked May 18 '21 07:05

Alireza Noorali


People also ask

Why LiveData Observer is being triggered twice for a newly attached Observer?

The observers method void onChanged(@Nullable T t) is called twice. That's fine. The first time it is called upon startup. The second time it is called as soon as Room has loaded the data.

In which method should you observe a LiveData object?

Attach the Observer object to the LiveData object using the observe() method. The observe() method takes a LifecycleOwner object. This subscribes the Observer object to the LiveData object so that it is notified of changes. You usually attach the Observer object in a UI controller, such as an activity or fragment.

How do I stop observe LiveData in fragment?

You should manually call removeObserver(Observer) to stop observing this LiveData. While LiveData has one of such observers, it will be considered as active.


Video Answer


1 Answers

Unfortunately, this is not the answer to your problems:

I need a solution to ...

Avoid calling onViewCreated codes again.

Avoid triggering LiveData observer again.

I'm trying to explain about navigation and its behaviors or correct some misunderstandings. These issues have different reasons and Avoid calling onViewCreated codes again. is a devious way.

I know onViewCreated will call again because Navigation Component replaces the fragments instead of adding them.

As you know, replacing fragments when they were added in back-stack, just detaching old fragment from fragmentManager. It means the old fragment's view will destroy.

And will create its view when you pop UnitFragment from the back-stack.

So don't call any API call in onViewCreated because it may call multiple times (in configuration changes, destroying fragment, etc...)

It's better to use onCreate for initializing non-view-related components (ViewModel + API calls). It reduces Lazy(!?) Initialization check.

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    ApiService apiService = ApiServiceProvider.getInstance();
    AddNewWareViewModelFactory addNewWareViewModelFactory = new AddNewWareViewModelFactory(apiService);
    viewModel = new ViewModelProvider(this /*owner*/, addNewWareViewModelFactory).get(AddWareViewModel.class);

    viewModel.callNewWare(parentCode);
}

And start to observe them in onViewCreated. Also, you should consume unit_data from navBackStackEntry when you got it.

if (navHostFragment != null) {
    NavController navController = navHostFragment.getNavController();
    NavBackStackEntry navBackStackEntry = navController.getCurrentBackStackEntry();
    if (navBackStackEntry != null) {
        SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle();
        MutableLiveData<Unit> unitLiveData = savedStateHandle.getLiveData("unit_data");
        unitLiveData.observe(getViewLifecycleOwner(), unit -> {
            savedStateHandle.remove("unit_data");       // add this line
            return binding.tvUnit.setText(unit.getTitle());
        });
    }
}

like image 160
beigirad Avatar answered Oct 22 '22 11:10

beigirad