首页   

FragmentFactory:构建Fragment的好帮手

鸿洋  · android  · 2 年前

之前我一直没关注过这个类,直到有一次我们一位同学下发一个热修 patch,里面新增了一个 Fragment,然后线上开始出现非常少量的 crash,在堆栈中我看到了 FragmentFactory...


crash 的原因是,当当前Activity 在后台被销毁,重回此 Activity 时 Fragment会被恢复,恢复时会通过FragmentFactory重建,但是恢复过程中会指定使用应用的 classloader,而我们patch 中新增的 fragment 需要通过热修的 classloader 才能加载,于是...恩。


不过我们可以通过自定义一个 FragmentFactory,在构建时,手动使用热修的 classloader 去创建 fragment 对象。


一个小插曲,分享结束,看作者的文章吧。




1
FragmentFactory的意义?


关于Fragment的使用约定


有Fragment使用经验的人都知道,Fragment必须有有一个空参的构造函数,否则编译时会出现一下错误:


This fragment should provide a default constructor (a public constructor with no arguments)


但即使添加了空参的构造器,如果定义了任何带参数构造器,仍然会被亲切的提醒:


Avoid non-default constructors in fragments: use a default constructor plus Fragment#setArguments(Bundle) instead [ValidFragment]


可见 Android 对于 Framgent携带构造参数唯恐避之不及。


当系统发生 Configuration Change 时(例如横竖屏旋转等)Fragment会恢复重建,此时系统不知道该选择哪个构造函数,所以系统与开发者约定,统一使用默认的空参构造函数构建,然后通过setArgments设置初始化值。


以往的处理方式:静态工厂


为此,一个常见做法是通过静态方法,避免使用非空构造函数。


如下,静态方法getInstance(String str) 中,先空参构造Fragment,然后通过setArgments初始化。


public class MainFragment extends BaseFragment {

   private static final String MY_ARG = "my_arg";
   private String arg = "";

   public static MainFragment getInstance(String str) {
       MainFragment fragment = new MainFragment();
       Bundle bundle = new Bundle();
       bundle.putString(MY_ARG, str);
       fragment.setArguments(bundle);
       return fragment;
   }

   @Override
   public void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       if (getArguments() != null) {
           arg = getArguments().getString(MY_ARG);
       }
   }
}

后续便可以使用此静态方法构建Fragment了。


MainFragment fragment = MainFragment.getInstance("Hello world!!");


Fragment恢复重建过程中,系统会调用静态方法Fragment.instantiate(在onCreateonActivityCreated之间)。


@NonNull
public static Fragment instantiate(@NonNull Context context, @NonNull String fname,
       @Nullable Bundle args) {
   try {
       Class extends Fragment> clazz = FragmentFactory.loadFragmentClass(
               context.getClassLoader(), fname);
       Fragment f = clazz.getConstructor().newInstance();
       if (args != null) {
           args.setClassLoader(f.getClass().getClassLoader());
           f.setArguments(args);
       }
       return f;
   } catch (java.lang.InstantiationException e) {


我们先前通过setArguments传递的bundle(随着onSaveInstanceState保存),会被系统传递给instantiate,以协助fragment的恢复重建。


新的处理方案:FragmentFactory


以上关于Fragment 空参构造函数的约定,随着 androidx.fragment:fragment-1.1.0-alpha01 的发布成为了历史。


新版本中 Fragment.instantiate已经被@Deprecated,推荐使用FragmentManager.getFragmentFactory和FragmentFactory.instantiate (ClassLoader, String)替代。FragmentFactory 允许开发者按照需要自由定义构造函数,摆脱了必须使用空参构造的束缚。


2
FragmentFactory如何使用?


假设我们的MainFragment需要两个参数,那么使用FragmentFactory如何构造呢?


定义FragmentFactory


首先,需要定义自己的FragmentFactory。主要是重写instantiate方法,注意跟以前比,已经不支持传入Bundle args作为参数了。即使你想使用bundle传参,也推荐在这里手动setArgument,而非借助系统的设置。


class MyFragmentFactory extends FragmentFactory {

   private final AnyArg anyArg1;
   private final AnyArg anyArg2;

   public MyFragmentFactory(AnyArg arg1, AnyArg arg2) {
       this.anyArg1 = arg1;
       this.anyArg2 = arg2;
   }

   @NonNull
   @Override
   public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {
       Class extends Fragment> clazz = loadFragmentClass(classLoader, className);
       if (clazz == MainFragment.class) {
          return new MainFragment(anyArg1, anyArg2);
       } else {
           return super.instantiate(classLoader, className);
       }
   }
}


有了FragmentFactory的加持 Framgent直接使用构造函数传参即可:


protected MainFragment(AnyArg arg1, AnyArg arg2) {
    this.arg1 = arg1;
    this.arg2 = arg2;
}


设置Factory


接下来需要在Activity的onCreate中为 FragmentManager 设置此 Factory:


MyFragmentFactory fragmentFactory = new MyFragmentFactory( someObject1,  someObject2);

@Override
public void onCreate(Bundle savedInstanceState) {
    getSupportFragmentManager().setFragmentFactory(fragmentFactory);
    super.onCreate(savedInstanceState);

   FragmentManager fragmentManager = getSupportFragmentManager();
   FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction()
        .replace(
            R.id.fragment_container,
            MainFragment.class);
   if (addToBackStack) {
     fragmentTransaction.addToBackStack(tag);
   }
   fragmentTransaction.commit();

}

后续 FragmentManager 在创建/恢复 fragment 时,会使用此 factory 创建实例。


需要特别注意的是,setFragmentFactory一定要在super.onCreate之前调用,因为在super.onCreate中会进行fragment的重建是需要被使用到。


3
应用场景:构造函数设置LayoutId


androidx.annotation:annotation-1.1.0-alpha01 引入了@ContentView 注解用来为Fragment 设置默认布局文件,但时隔不久,androidx.fragment:fragment-1.1.0-alpha05 起,@ContentView 从class注解变为构造函数注解,fragment多了一个带参数的构造函数:支持使用构造函设置LayoutId:


/**
 * Alternate constructor that can be used to provide a default layout
 * that will be inflated by {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.
 *
 * @see #Fragment()
 * @see #onCreateView(LayoutInflater, ViewGroup, Bundle)
 */

@ContentView //以前是用在Class上的注解
public Fragment(@LayoutRes int contentLayoutId) {
    this();
    mContentLayoutId = contentLayoutId;
}

Note:Activity自 androidx.activity:activity-1.0.0-alpha06 起也支持通过构造函数设置LayoutId。


构造函数中将传入的LayoutId存于mContentLayoutId,onCreateView中根据 mConentLayoutId 自动创建 ContentView :


@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
        @Nullable Bundle savedInstanceState) {
    if (mContentLayoutId != 0) {
        return inflater.inflate(mContentLayoutId, container, false);
    }
    return null;
}

也就是说使用构造函数设置LayoutId 就无需重写 onCreateView 了。


Fragment( contentLayoutId ) + FragmentFactory


你也许会问这跟 FragmentFactory 有什么关系呢?


因为使用了构造函数设置 mContentLayoutId,当 ConfigurationChange 发生时,默认调用无参构造函数进行 fragment 的恢复重建,mContentLayoutId 信息会丢失,onCreateView 无法正常创建视图。


因此当使用构造函数设置 LayoutId 时,如果要考虑恢复重建的场景,必须配套设置一个 FragmentFactory。可能是踩坑的人太多了,在 1.1.0 之后的 doc注释中特别强调了这一点:


You must set a custom FragmentFactory if you want to use a non-default constructor to ensure that your constructor is called when the fragment is re-instantiated.


总结


相对与以往 setArguments 传参方式,FragmentFactory 中允许开发者直接通过构造函数传参、创建 Fragment。灵活的构造函数带来很多潜在收益,比如通过构造函数设置 LayoutId 可以为 Fragment 提供动态化的布局能力。此外,FragmentFactory 在 dagger、koin 等DI框架的使用场景中也能发挥很大的作用。




最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读


新技术又又又又又又叒来了?
LeakCanary 新版 2.x ,你应该知道的知识点
玩转Android AOP  ,这3个案例你需要掌握!


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

推荐文章
机器学习研究组订阅  ·  ACL2020论文精选  ·  3 年前  
© 2022 51好读
删除内容请联系邮箱 2879853325@qq.com