Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ViewPager2 preloaing next fragment even when offsetlimit is the default(0)

I am facing this rather strange problem with viewPager2. I did some reading and found out that viewPager2 has a default offset limit of 0 which is perfect for my application. I'm using it with a tab layout and I have 3 fragments (Home, Profile, Notification). When the activity loads and the first fragment(Home) loads, I can see in my logcat that the next fragment(Profile) is not loaded, as expected. But when I click on the profile tab something strange happens, the next tab(Notification) is preloaded. The methods onAttach,onCreate,onCreateView,onActivityCreated,onStart for the Notification tab is called. Why is this doing so and how to I fix this ? Logcat Screenshot I have attached a screen shot of my logcat here. Thankyou in advance.

like image 827
Tanzim Chowdhury Avatar asked Feb 16 '20 18:02

Tanzim Chowdhury


People also ask

What the difference between ViewPager and ViewPager2?

ViewPager2 is an improved version of the ViewPager library that offers enhanced functionality and addresses common difficulties with using ViewPager . If your app already uses ViewPager , read this page to learn more about migrating to ViewPager2 .

What does ViewPager2 do?

ViewPager2 supports paging through a modifiable collection of fragments, calling notifyDatasetChanged() to update the UI when the underlying collection changes. This means that your app can dynamically modify the fragment collection at runtime, and ViewPager2 will correctly display the modified collection.

What is ViewPager2 in android?

ViewPager2 uses FragmentStateAdapter objects as a supply for new pages to display, so the FragmentStateAdapter will use the fragment class that you created earlier. Create an activity that does the following things: Sets the content view to be the layout with the ViewPager2 .


1 Answers

I Assume you mean OFFSCREEN_PAGE_LIMIT_DEFAULT not offsetlimit as you are talking about a preloading of fragments problem.

And the default value is -1 not zero https://developer.android.com/reference/androidx/viewpager2/widget/ViewPager2.html#OFFSCREEN_PAGE_LIMIT_DEFAULT

and the default means

Value to indicate that the default caching mechanism of RecyclerView should be used instead of explicitly prefetch and retain pages to either side of the current page.

As this is a performance optimisation of the recyclerview, I would say it's not a guarantee that it won't preload your fragments, it's just left to the caching mechanism of the recyclerview to decide.

There are a number of factors that can affect the recyclerview's caching mechanism.

If preloading of your fragment is a problem because you have dynamic data in it that you only want to be loaded when the page is shown then it would be better to move your fragment to use a "lazy loading" method i.e. only load the data when it is shown.

I had a similar problem with the original viewpager and solved it with "lazy loading". If the timing of loading of your dynamic data is the problem then update the question and then I can outline a possible solution.

Update:3

It seems that Viewpager2 actually works correctly with the Fragments lifecycle unlike the original Viewpager thus you can call updateView() as shown in update2 example from the Fragments onResume method without having to use the pageSelected callback via the Adapter to trigger the update of the View.

Update:

I believe the actual cause from looking at the viewpager2 code is that selecting the Tab does a fake drag with smooth scrolling and smooth scrolling adds the selected item +1 to the cache, if you swipe from Tab0 to Tab1 yourself it does not create Tab2

ViewPager2 is a bit different and "lazy loading" method I used for the original ViewPager does not totally fit but there is a slightly different way to do the same.

The main idea with the original ViewPager was to update the view ONLY when a page was selected using a onPageChangeListener but ViewPager2 uses a callback instead

So add the following after you have created the ViewPager2 (in the Activity onCreate usually)

        viewPager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
            @Override
            public void onPageSelected(int position) {
                super.onPageSelected(position);
                // Tell the recyclerview that position 2 has changed when selected
                // Thus it recreates it updating the dynamic data
                if (position == 2) {
                    // the adapter for the ViewPager needs to a member of the Activity class so accessible here
                    adapter.notifyItemChanged(position);
                }
            }
        });

This is simpler but has a minor drawback that the dynamic data is loaded when it is preloaded and then again when it is actually displayed.

Update2: A more efficient addition to first method more similar to my original approach

This is a full working example as it is easier to explain.

The main idea is in your fragment with the dynamic data that you ONLY want to load when it is displayed is to create an empty "placeholder" view item and you don't fill it with data in the Fragments onViewCreated, in this example it is a second textview with no text but could be a recyclerview with zero objects or any other type of view.

In your Fragment you then create a method to update the "placeholder" with the data (in this case the method is called updateView() which sets the textview text to the current date and time)

Then in your Fragment Adapter you store a reference to each fragment it creates in a ArrayList (this allows you get the fragment back) and you then create an updateFragment() method in the adapter that uses the position to get the Fragment to be able to call the updateView() on it.

Finally again you use onPageSelected to call the updateFragment with the position you want to dynamically update.

So textview1 shows the data and time the fragement was created and textview2 is only shown on the third Tab and has a date and time on when page was selected. Note that texview1 on "Tab 2" and "Tab 3" is the same time when you click on the "Tab 2" headers to change tabs (this is the problem in the question)

MainActivity.java

package com.test.viewpager2;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.viewpager2.widget.ViewPager2;

import android.os.Bundle;

import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;

public class MainActivity extends AppCompatActivity {

    TabLayout tabLayout;
    ViewPager2 viewPager2;
    ViewPager2Adapter adapter;

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

        viewPager2 = findViewById(R.id.viewpager2);
        tabLayout = findViewById(R.id.tabLayout);

        viewPager2.setAdapter(createCardAdapter());

        new TabLayoutMediator(tabLayout, viewPager2,
                new TabLayoutMediator.TabConfigurationStrategy() {
                    @Override public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
                        tab.setText("Tab " + (position + 1));
                    }
                }).attach();

        viewPager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
            @Override
            public void onPageSelected(int position) {
                super.onPageSelected(position);
                // Tell the recyclerview that position 2 has changed when selected
                // Thus it recreates it updating the dynamic data
                if (position == 2) {
                    adapter.updateFragment(position);
                }
            }
        });
    }

    private ViewPager2Adapter createCardAdapter() {
        adapter = new ViewPager2Adapter(this);
        return adapter;
    }

}

ViewPager2Adapter.java

package com.test.viewpager2;

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;

import java.util.ArrayList;

public class ViewPager2Adapter extends FragmentStateAdapter {

    private static final int numOfTabs = 3;
    private ArrayList<Fragment> fragments = new ArrayList<>();

    public ViewPager2Adapter(@NonNull FragmentActivity fragmentActivity) {
        super(fragmentActivity);
    }

    @NonNull @Override public Fragment createFragment(int position){
        Fragment fragment = TextFragment.newInstance(position);
        fragments.add(fragment);
        return fragment;
    }

    @Override
    public int getItemCount(){
        return numOfTabs;
    }

    public void updateFragment(int position){
        Fragment fragment = fragments.get(position);
        // Check fragment type to make sure it is one we know has an updateView Method
        if (fragment instanceof TextFragment){
            TextFragment textFragment = (TextFragment) fragment;
            textFragment.updateView();
        }
    }
}

TextFragment.java

package com.test.viewpager2;

import android.content.Context;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;

public class TextFragment extends Fragment {
    // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
    private static final String ARG_PARAM1 = "param1";

    private int mParam1;
    private View view;


    public TextFragment() {
        // Required empty public constructor
    }

    public static TextFragment newInstance(int param1) {
        TextFragment fragment = new TextFragment();
        Bundle args = new Bundle();
        args.putInt(ARG_PARAM1, param1);
        fragment.setArguments(args);
        Log.d("Frag", "newInstance");
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mParam1 = getArguments().getInt(ARG_PARAM1);
        }
        Log.d("Frag", "onCreate");
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        Log.d("Frag", "onCreateView");
        view = inflater.inflate(R.layout.fragment_text, container, false);

        return view;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        Bundle args = getArguments();
        TextView textView1 = view.findViewById(R.id.textview1);
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss.sss", Locale.US);
        String dt = df.format(Calendar.getInstance().getTime());
        textView1.setText(dt);
    }


    public void updateView(){
        Log.d("Frag", "updateView");
        TextView textView2 = view.findViewById(R.id.textview2);
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss.sss", Locale.US);
        String dt = df.format(Calendar.getInstance().getTime());
        textView2.setText(dt);
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Log.d("Frag", "onAttach");
    }

    @Override
    public void onDetach() {
        super.onDetach();
        Log.d("Frag", "onDetach");
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tabLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewpager2"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".TextFragment">

    <TextView
        android:id="@+id/textview1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textview2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/textview1" />/>

</androidx.constraintlayout.widget.ConstraintLayout>
like image 64
Andrew Avatar answered Nov 08 '22 23:11

Andrew