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:
Mauricio Colli 2019-10-19 21:31:15 -03:00
parent 7e311e5567
commit 58a626dedb
No known key found for this signature in database
GPG key ID: F200BFD6F29DDD85
2 changed files with 48 additions and 40 deletions

View file

@ -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();
} }
} }
} }

View file

@ -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;
} }