Fix broken view pager tabs implementation
- Fragments were being recreated from scratch (losing their state) every time some configuration change occurred (e.g. screen rotation). - Use `FragmentStatePagerAdapter` instead, as it is built to work with them and manage their states.
This commit is contained in:
parent
7e311e5567
commit
58a626dedb
2 changed files with 48 additions and 40 deletions
|
@ -1,5 +1,6 @@
|
||||||
package org.schabi.newpipe.fragments;
|
package org.schabi.newpipe.fragments;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
@ -15,7 +16,7 @@ import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentPagerAdapter;
|
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
|
|
||||||
import com.google.android.material.tabs.TabLayout;
|
import com.google.android.material.tabs.TabLayout;
|
||||||
|
@ -52,32 +53,19 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
|
|
||||||
destroyOldFragments();
|
|
||||||
|
|
||||||
tabsManager = TabsManager.getManager(activity);
|
tabsManager = TabsManager.getManager(activity);
|
||||||
tabsManager.setSavedTabsListener(() -> {
|
tabsManager.setSavedTabsListener(() -> {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "TabsManager.SavedTabsChangeListener: onTabsChanged called, isResumed = " + isResumed());
|
Log.d(TAG, "TabsManager.SavedTabsChangeListener: onTabsChanged called, isResumed = " + isResumed());
|
||||||
}
|
}
|
||||||
if (isResumed()) {
|
if (isResumed()) {
|
||||||
updateTabs();
|
setupTabs();
|
||||||
} else {
|
} else {
|
||||||
hasTabsChanged = true;
|
hasTabsChanged = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void destroyOldFragments() {
|
|
||||||
for (Fragment fragment : getChildFragmentManager().getFragments()) {
|
|
||||||
if (fragment != null) {
|
|
||||||
getChildFragmentManager()
|
|
||||||
.beginTransaction()
|
|
||||||
.remove(fragment)
|
|
||||||
.commitNowAllowingStateLoss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
return inflater.inflate(R.layout.fragment_main, container, false);
|
return inflater.inflate(R.layout.fragment_main, container, false);
|
||||||
|
@ -90,23 +78,17 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
tabLayout = rootView.findViewById(R.id.main_tab_layout);
|
tabLayout = rootView.findViewById(R.id.main_tab_layout);
|
||||||
viewPager = rootView.findViewById(R.id.pager);
|
viewPager = rootView.findViewById(R.id.pager);
|
||||||
|
|
||||||
/* Nested fragment, use child fragment here to maintain backstack in view pager. */
|
|
||||||
pagerAdapter = new SelectedTabsPagerAdapter(getChildFragmentManager());
|
|
||||||
viewPager.setAdapter(pagerAdapter);
|
|
||||||
|
|
||||||
tabLayout.setupWithViewPager(viewPager);
|
tabLayout.setupWithViewPager(viewPager);
|
||||||
tabLayout.addOnTabSelectedListener(this);
|
tabLayout.addOnTabSelectedListener(this);
|
||||||
updateTabs();
|
|
||||||
|
setupTabs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
if (hasTabsChanged) {
|
if (hasTabsChanged) setupTabs();
|
||||||
hasTabsChanged = false;
|
|
||||||
updateTabs();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -153,14 +135,21 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
// Tabs
|
// Tabs
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
public void updateTabs() {
|
public void setupTabs() {
|
||||||
tabsList.clear();
|
tabsList.clear();
|
||||||
tabsList.addAll(tabsManager.getTabs());
|
tabsList.addAll(tabsManager.getTabs());
|
||||||
pagerAdapter.notifyDataSetChanged();
|
|
||||||
|
|
||||||
viewPager.setOffscreenPageLimit(pagerAdapter.getCount());
|
if (pagerAdapter == null || !pagerAdapter.sameTabs(tabsList)) {
|
||||||
|
pagerAdapter = new SelectedTabsPagerAdapter(requireContext(), getChildFragmentManager(), tabsList);
|
||||||
|
}
|
||||||
|
// Clear previous tabs/fragments and set new adapter
|
||||||
|
viewPager.setAdapter(pagerAdapter);
|
||||||
|
viewPager.setOffscreenPageLimit(tabsList.size());
|
||||||
|
|
||||||
updateTabsIconAndDescription();
|
updateTabsIconAndDescription();
|
||||||
updateCurrentTitle();
|
updateCurrentTitle();
|
||||||
|
|
||||||
|
hasTabsChanged = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTabsIconAndDescription() {
|
private void updateTabsIconAndDescription() {
|
||||||
|
@ -194,26 +183,30 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
updateCurrentTitle();
|
updateCurrentTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SelectedTabsPagerAdapter extends FragmentPagerAdapter {
|
private static class SelectedTabsPagerAdapter extends FragmentStatePagerAdapter {
|
||||||
|
private final Context context;
|
||||||
|
private final List<Tab> internalTabsList;
|
||||||
|
|
||||||
private SelectedTabsPagerAdapter(FragmentManager fragmentManager) {
|
private SelectedTabsPagerAdapter(Context context, FragmentManager fragmentManager, List<Tab> tabsList) {
|
||||||
super(fragmentManager);
|
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||||
|
this.context = context;
|
||||||
|
this.internalTabsList = new ArrayList<>(tabsList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Fragment getItem(int position) {
|
public Fragment getItem(int position) {
|
||||||
final Tab tab = tabsList.get(position);
|
final Tab tab = internalTabsList.get(position);
|
||||||
|
|
||||||
Throwable throwable = null;
|
Throwable throwable = null;
|
||||||
Fragment fragment = null;
|
Fragment fragment = null;
|
||||||
try {
|
try {
|
||||||
fragment = tab.getFragment(requireContext());
|
fragment = tab.getFragment(context);
|
||||||
} catch (ExtractionException e) {
|
} catch (ExtractionException e) {
|
||||||
throwable = e;
|
throwable = e;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
ErrorActivity.reportError(activity, throwable, activity.getClass(), null,
|
ErrorActivity.reportError(context, throwable, null, null,
|
||||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
|
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
|
||||||
return new BlankFragment();
|
return new BlankFragment();
|
||||||
}
|
}
|
||||||
|
@ -234,15 +227,11 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCount() {
|
public int getCount() {
|
||||||
return tabsList.size();
|
return internalTabsList.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public boolean sameTabs(List<Tab> tabsToCompare) {
|
||||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
return internalTabsList.equals(tabsToCompare);
|
||||||
getChildFragmentManager()
|
|
||||||
.beginTransaction()
|
|
||||||
.remove((Fragment) object)
|
|
||||||
.commitNowAllowingStateLoss();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,8 @@ import org.schabi.newpipe.util.KioskTranslator;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public abstract class Tab {
|
public abstract class Tab {
|
||||||
Tab() {
|
Tab() {
|
||||||
}
|
}
|
||||||
|
@ -47,6 +49,8 @@ public abstract class Tab {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object obj) {
|
public boolean equals(Object obj) {
|
||||||
|
if (obj == this) return true;
|
||||||
|
|
||||||
return obj instanceof Tab && obj.getClass().equals(this.getClass())
|
return obj instanceof Tab && obj.getClass().equals(this.getClass())
|
||||||
&& ((Tab) obj).getTabId() == this.getTabId();
|
&& ((Tab) obj).getTabId() == this.getTabId();
|
||||||
}
|
}
|
||||||
|
@ -340,6 +344,13 @@ public abstract class Tab {
|
||||||
kioskId = jsonObject.getString(JSON_KIOSK_ID_KEY, "<no-id>");
|
kioskId = jsonObject.getString(JSON_KIOSK_ID_KEY, "<no-id>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
return super.equals(obj) &&
|
||||||
|
kioskServiceId == ((KioskTab) obj).kioskServiceId
|
||||||
|
&& Objects.equals(kioskId, ((KioskTab) obj).kioskId);
|
||||||
|
}
|
||||||
|
|
||||||
public int getKioskServiceId() {
|
public int getKioskServiceId() {
|
||||||
return kioskServiceId;
|
return kioskServiceId;
|
||||||
}
|
}
|
||||||
|
@ -409,6 +420,14 @@ public abstract class Tab {
|
||||||
channelName = jsonObject.getString(JSON_CHANNEL_NAME_KEY, "<no-name>");
|
channelName = jsonObject.getString(JSON_CHANNEL_NAME_KEY, "<no-name>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
return super.equals(obj) &&
|
||||||
|
channelServiceId == ((ChannelTab) obj).channelServiceId
|
||||||
|
&& Objects.equals(channelUrl, ((ChannelTab) obj).channelUrl)
|
||||||
|
&& Objects.equals(channelName, ((ChannelTab) obj).channelName);
|
||||||
|
}
|
||||||
|
|
||||||
public int getChannelServiceId() {
|
public int getChannelServiceId() {
|
||||||
return channelServiceId;
|
return channelServiceId;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue