如何防止创建相同片段的 2 个实例?

Posted

技术标签:

【中文标题】如何防止创建相同片段的 2 个实例?【英文标题】:How to prevent creating 2 instances of same fragment? 【发布时间】:2021-11-12 03:39:36 【问题描述】:

我有一个包含 MasterFragment 的活动 (MainActivity),其中包含一个 Viewpager,其中 FragmentA 和 FragmentB 处于纵向屏幕方向。

在横向模式下,viewpager 仅包含分屏左侧的 FragmentA,右侧包含 FragmentB。

所以基本上 FragmentB 在横向模式下移动到 viewpager 的右侧。

虽然 FragmentB 每次轮换只显示一次,但轮换后会同时创建两个实例。

问题是 FragmentB 实际上是一张地图,我需要防止同时创建 2 个实例。我需要在创建下一个实例之前销毁第一个实例。

FragmentStateManager 在 MainActivity 中调用 setContentView 时会重新创建 FragmentB。

如何防止这种情况发生?

一种解决方案是在 MainActivity 中使用 super.onCreate(null),但这显然是矫枉过正。

如何防止在 ViewPager2 中重新创建片段?

另一种解决方案是使用重新创建的片段实例并将其从 viewpager 移动到 framlayout,反之亦然。我该如何移动它?

MasterFragment.java

public class MasterFragment extends Fragment

    NewPagerAdapter mSectionsPagerAdapter;
    ViewPager2 mViewPager;
    boolean mSplitView;

    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) 
    
        View view = inflater.inflate(R.layout.masterfragment, container, false);

        if (isLandScape())
        
            mSplitView = true;
            getChildFragmentManager().beginTransaction().replace(R.id.container, new FragmentB(), FragmentB.TAG).commit();
        
        else if (isLandScape())
        
            LinearLayout masterlayout = view.findViewById(R.id.masterlayout);
            masterlayout.removeViewAt(1);
        

        mViewPager = view.findViewById(R.id.pager);
        mSectionsPagerAdapter = new NewPagerAdapter(getChildFragmentManager(), getLifecycle());

        mViewPager.setOffscreenPageLimit(7);
        mViewPager.setAdapter(mSectionsPagerAdapter);

        return view;
    

    public boolean isLandScape()
    
        int orientation = getResources().getConfiguration().orientation;
        return orientation == Configuration.ORIENTATION_LANDSCAPE;
    

    public boolean backOnePage()
    
        if(mViewPager == null)
            return false;

        int page = mViewPager.getCurrentItem();

        if(page > 0)
        
            mViewPager.setCurrentItem(page - 1);
            return true;
        

        return false;
    

    public void viewFragmentB()
    
        if(!mSplitView)
            mViewPager.setCurrentItem(1);
    

    public class NewPagerAdapter extends FragmentStateAdapter
    
        public NewPagerAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle)
        
            super(fragmentManager, lifecycle);
        

        @NonNull
        @Override
        public Fragment createFragment(int position)
        
            if(position == 0)
                return new FragmentA();

            return new FragmentB();
        

        @Override
        public int getItemCount()
        
            return mSplitView ? 1 : 2;
        
    

ma​​sterfragment.xml(纵向)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_
        android:layout_
        android:background="#ff000000"
        android:orientation="vertical">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/pager"
        android:layout_
        android:layout_
        android:layout_weight="1.0"
    />
        
</LinearLayout>

ma​​sterfragment.xml(横向)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/masterlayout"
    android:layout_
    android:layout_
    android:layout_alignParentEnd="true"
    android:background="#ff000000">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/pager"
        android:layout_
        android:layout_
        android:layout_weight="0.5"
    />

    <FrameLayout
        android:id="@+id/container"
        android:layout_
        android:layout_
        android:layout_weight="0.5"/>

</LinearLayout>

MainActivity.java

public class MainActivity extends FragmentActivity

    MasterFragment mMasterFragment;

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

        requestWindowFeature(Window.FEATURE_NO_TITLE);

        setContentView(R.layout.main);

        Fragment fragment = savedInstanceState != null
                ? getSupportFragmentManager().getFragment(savedInstanceState, "MasterFragment")
                : null;

        mMasterFragment = fragment instanceof MasterFragment
                ? (MasterFragment)fragment
                : (MasterFragment)getSupportFragmentManager().findFragmentById(R.id.masterfragment);
    
    
    @Override
    public void onBackPressed() 
    
        if(mMasterFragment != null && mMasterFragment.backOnePage())
            return;

        super.finish();
    

片段A

public class FragmentA extends Fragment
   
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) 
    
        View view = inflater.inflate(R.layout.fragment_home, container, false);

        view.findViewById(R.id.title).setOnClickListener(new View.OnClickListener()
        
            @Override
            public void onClick(View view)
            
                MasterFragment.getInstance().viewFragmentB();
            
        );

        return view;
    

片段B

public class FragmentB extends Fragment

    public static String TAG = "OrderFragment";
    static int COUNTER;

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

        COUNTER++; // COUNTER BECOMES 2

        return view;
    

    @Override
    public void onDestroy()
    
        COUNTER--;

        super.onDestroy();
    

ma​​in.xml

<fragment xmlns:android="http://schemas.android.com/apk/res/android" 
    android:name="com.mobile.MasterFragment"
    android:id="@+id/masterfragment"
    android:layout_
    android:layout_/>

【问题讨论】:

可以分享一下主布局吗? 我添加了 main.xml 【参考方案1】:

我认为你的问题是你总是在做 new FragmentB()new Fragment...() 而不是检查它是否已经在 fragmentManager 中。

你必须做类似的事情(请原谅我的 kotlin 伪代码


var fragmentB = fragmentManager.findFragmentByTag("FragB")

if fragmentB == null  
   fragmentB = // create new instance
 

fragmentManager.replace(..., fragmentB, "FragB") //use the same tag you'll use later to search for it



【讨论】:

ViewPager 使用的 'FragmentStateAdapter' 根据其在 viewpager 中的位置,将 FragmentB 添加到带有标签“f0”、“f1”的 FragmentManager。我试图在旋转到横向时找到该实例并使用它而不是新实例,但片段确实出现了(右半窗格为黑色)。 我的意思是“没有出现” 我认为解决方案是在创建另一个实例之前从片段管理器中删除片段 我已经有一段时间没有使用带有这些适配器的 VPager,所以没有办法覆盖标签吗? 那将是非常骇人听闻的。我认为不可能重用片段,我宁愿确保 FragmentStateManager 不会重新创建 FragmentB【参考方案2】:

您需要在要保留的片段中使用setRetainInstance,而不是 Activity 配置更改。

Source

编辑

感谢@martin 指出setRetainInstance 已被弃用。

根据我的理解,防止片段重新创建超过配置更改的要求是无法实现的。我建议按照Android Doc's 建议通过视图模型实例维护片段状态

【讨论】:

setRetainInstance 已被标记为已弃用,这不是一个好主意。 (在 API 级别 28 中已弃用) setRetainInstance 已被弃用。我不是试图保留同一个实例,而是试图防止同时创建 2 个实例(onCreateView 被调用 2 次,而两者之间没有 onDestroy) FragmentA 一次只创建一次,尽管它在 viewpager 中以纵向和横向的形式出现。 原因是,在横向旋转期间,您将在 FrameLayout 容器中添加一个新的 FragmentB 实例来显示它。同时,您的 MasterFragment 再次被重新创建,隐式调用 FragmentB 两次。 恐怕我们无法阻止重新创建某个片段。我宁愿将 Fragment 状态保存在 viewmodel 实例中并在重新创建时恢复它。我同意@Martin + 这个android-review.googlesource.com/c/platform/frameworks/support/+/…【参考方案3】:

看来我需要回答我自己的问题。

正如一些人提到的,Android 平台不直接支持这一点。

一些可能性是:

    MainActivity 中的 super.onCreate(null)(这将禁用所有状态恢复)

    编写自定义viewpager/adapter,省略处于保存状态的片段(大量工作)

    旋转后,将片段从 Viewpager 移到 Framelayout 或从 Framelayout 移出,但这需要客户 viewpager/adapter 允许在不破坏片段视图的情况下移除片段视图(请参阅移动视图 Android Fragment - move from one View to another?),这可能是很多工作

【讨论】:

以上是关于如何防止创建相同片段的 2 个实例?的主要内容,如果未能解决你的问题,请参考以下文章

如何防止在方向更改时重新创建片段寻呼机中的片段?

Android - 多个视图或运行时片段替换​​?

如何防止片段在活动重新创建时触发 onCreate onCreateView

UIScrollView addSubView:如何防止重新添加相同的 UILabel 实例?

Back Stack 未维护 - 如何防止 Fragment 被破坏?

如何在ViewPager中重用Fragment实例?