Search code examples
androidbottomnavigationviewandroid-architecture-components

How to prevent BottomNavigationView animation from stuttering


(First of all: no, it's not a duplicate of mentioned question :P read, then push the button.)

I'm using the BottomNavigationView in one of my apps, loading Fragments with lists, which get their data from a ViewModel/LiveData/Dao. When selecting a Fragment via the BNV, it seems its animation somehow fights for UI-Thread time with the Fragment loading, causing it only to finish completely after the lists are displayed - which confuses me. I was under the impression, that LiveData calls are being handled async by default?

Stuttering gif

Is this a known thing?

ViewModel

public class ScheduleViewModel extends ViewModel {
    private final LiveData<List<ScheduleInfo>> arrivals;
    private final LiveData<List<ScheduleInfo>> departures;

    public ScheduleViewModel() {
        arrivals = SigmoDb.schedule().getArrivals();
        departures = SigmoDb.schedule().getDepartures();
    }

    public LiveData<List<ScheduleInfo>> getArrivals() {
        return arrivals;
    }

    public LiveData<List<ScheduleInfo>> getDepartures() {
        return departures;
    }
}

Fragment

public class ArrivalsFragment extends MainFragment {
    private ScheduleDetailsAdapter adapter;
    private ScheduleViewModel viewModel;

    private final Observer<List<ScheduleInfo>> arrivalsObserver = new Observer<List<ScheduleInfo>>() {
        @Override
        public void onChanged(@Nullable List<ScheduleInfo> infoList) {
            adapter.setData(infoList);
        }
    };

    public static ArrivalsFragment newInstance() {
        return new ArrivalsFragment();
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        adapter = new ScheduleDetailsAdapter(getActivity());
        // using the parent fragment as LifeCycleOwner, since both its
        // child Fragments use the same ViewModel
        Fragment parent = getParentFragment();
        if (parent == null) {
            parent = this;
        }
        viewModel = ViewModelProviders.of(parent).get(ScheduleViewModel.class);
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        reObserveViewModel();
    }

    // remove and re-add observer until Google fixes the multiple observer issue
    // TODO: remove when Google fixes the issue
    // https://github.com/googlesamples/android-architecture-components/issues/47
    private void reObserveViewModel() {
        viewModel.getArrivals().removeObservers(this);
        viewModel.getArrivals().observe(this, arrivalsObserver);
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_arrivals_departures, container, false);

        RecyclerView recyclerView = view.findViewById(R.id.rv_schedule_details);
        LinearLayoutManager llm = new LinearLayoutManager(this.getContext());
        recyclerView.setLayoutManager(llm);
        recyclerView.setAdapter(adapter);
        return view;
    }
}

For info: I timestamped the ViewModel's constructor start and end (to rule out those calls somehow being on the UI thread - takes 1 millisecond).

Narrowed down the issue

After Robin Davies' answer, I tried the Android Profiler and although I get some GC events now and then, I don't get them all the time with the stuttering being there every single time. However, delaying setting of the adapter data in the observer by 100ms seems to let the BNV animation complete when switching to the ArrivalsFragment:

No stuttering gif

All I did was changing

private final Observer<List<ScheduleInfo>> arrivalsObserver = new Observer<List<ScheduleInfo>>() {
    @Override
    public void onChanged(@Nullable List<ScheduleInfo> infoList) {
        adapter.setData(infoList);
    }
};

to

private final Observer<List<ScheduleInfo>> arrivalsObserver = new Observer<List<ScheduleInfo>>() {
    @Override
    public void onChanged(@Nullable final List<ScheduleInfo> infoList) {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                adapter.setData(infoList);
            }
        }, 100);
    }
};

So it seems that this part of your answer

Also if you post list results back to the foreground thread and populate the adapter while the animation is running, that will force a layout pass that will interfere with animation.

is the one I was struggling with in my particular case. While I'm a bit disappointed having to revert to using delays to make the animation fluent, I'm happy to have found the culprit and thank you very much for your help :)


Solution

  • Yes this is common enough problem.

    Assuming that you've already moved heavy processing onto an background threads...

    If you are doing really heavy lifting on the background thread, you can trigger garbage collects, which can block the foreground thread long enough to cause stuttering. Also if you post list results back to the foreground thread and populate the adapter while the animation is running, that will force a layout pass that will interfere with animation.

    Try using the CPU usage/profiling tools to see what exactly is holding up the foreground thread.

    Solutions to consider would be to postpone population of the fragments until the animation is finished. Or pre-populate the fragment. Or maybe block the background thread while animation is running (perhaps). Or postpone the animation until the fragment is populated and laid out (which gets potentially unpleasant). If the problem isn't caused by a garbage collect, you could delay creation/population of the adapter until the animation is finished.