借助工厂模式构建不同的 Fragment

还记的上篇文章 使用观察者模式解决单 Activity 与多个 Fragment 通信 中我使用了观察者模式暂时解决了 Activity 与多个 Fragment 之间的通信问题,最后的更新中我抽象了一个 Fragment 共同的基类:BaseFragment,在 BaseFragment 的构造方法中传入了 EventManager 也就是消息处理中心的实例,本来这样是没有问题的。直到今天,我升级了 AS 的 Gradle 的版本,然后重新编译项目的时候,报了一个错误:


为什么之前的时候没有发现这个错误吧,因为以前编译报错的时候,我一直是按快捷键 Alt + Enter 自动修正的,甚至有时候都没看清具体的错误描述信息是什么就被修正了。大部分情况下这些错误都可以被搞定的,主要还是以前碰到的都是类型转换之类的,看多了也就懒得再仔细看描述了。所以大概上次报错的时候我也是直接按了快捷键,结果就是会关闭关于这个错误的警告。但是这次我特意去看了一眼,然后 Google 了一下,明白了这个错误是什么警告是什么意思。

以前使用 Fragment 的时候,如果需要传入某个参数,经常就是给 Fragment 加一个构造方法吧参数传进去,有时候提示如果有了有参的构造方法,那么还需要添加无参的默认构造方法,而我也会顺手价格无参构造方法。但这次加了之后还是报错,而且还是之前的提示,因为一直提到 setArguments 这个方法,说如果要传递参数,最好使用这个方法。看了网络上各大博客的解释,就是说如果 Fragment 异常停止了,系统会自动重新创建 Fragment 的实例,但是并不会调用有参的构造方法,而是调用默认的无参构造方法,而 Fragment 内部会维护一个 Bundle 类型的变量,名字就叫 mArguments ,在 Fragment 重建的某个时期,会自动将 mArguments set 到新的实例上,可以看看 Fragment 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
try {
Class<?> clazz = sClassMap.get(fname);
if (clazz == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = context.getClassLoader().loadClass(fname);
sClassMap.put(fname, clazz);
}
Fragment f = (Fragment)clazz.newInstance();
if (args != null) {
args.setClassLoader(f.getClass().getClassLoader());
f.mArguments = args;//重点在这里
}
return f;
} catch (ClassNotFoundException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (java.lang.InstantiationException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (IllegalAccessException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
}
}

重点是 try 语句块中的第二个 if 语句块,将 mArguments 赋到了新创建的 Fragment 上,所以如果继续使用构造方法来传参,那么当 Fragment 重启找不到 参数就会产生异常。即使你在 debug 期间关闭这个错误警告,当你打 release 包的时候仍然会导致编译失败。因此就需要使用 setArguments 方法来为 Fragment 传递参数了。

既然我已经抽象出了 BaseFragment ,那么我肯定不希望在每次实例化 Fragment 的时候都要写一遍 setArguments ,最好还是只需要在 BaseFragment 中进行处理就好了。一开始我是这么写的:

1
2
3
4
5
6
7
public BaseFragment getInstance(EventManager manager) {
BaseFragment fragment = new BaseFragment();
Bundle bundle = new Bundle();
bundle.putParcelable("event_manager", manager);
fragment.setArguments(bundle);
return fragment;
}

但是如果这样的返回去的就是一个 BaseFragment 的实例,肯定不能够转型成为其他具体的 Fragment 的,所以只好放弃了。仔细想了下,这样中间需要加工(设置参数),然后生产出同一个种类但是不同口味的产品(具体的 Fragment 的实现),不就是以前了解的工厂模式的试用范围么。立马行动,一个简单的工厂类就出来了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* Created by Alpha on 2017/3/20.
* 借助于工厂模式来构建 fragment ,同时设置共同的参数
*/

public class FragmentFactory {

public static final int TYPE_AGENDA = 1;
public static final int TYPE_CONTEXT = 2;
public static final int TYPE_EVENT = 3;
public static final int TYPE_FINISH = 4;
public static final int TYPE_INBOX = 5;
public static final int TYPE_MEMO = 6;
public static final int TYPE_PROJECT = 7;
public static final int TYPE_TODO = 8;
public static final int TYPE_TRASH = 9;

//private static Map<Integer, Fragment> mFragments = new HashMap<>();
private static SparseArray<Fragment> mFragments = new SparseArray<>();//更新于 3 月 22 日

public static Fragment create(Integer fragmentName) {
Fragment fragment = mFragments.get(fragmentName);
if (fragment == null) {
switch (fragmentName) {
case TYPE_AGENDA:
fragment = new AgendaFragment();
break;
case TYPE_CONTEXT:
fragment = new ContextFragment();
break;
case TYPE_EVENT:
fragment = new EventFragment();
break;
case TYPE_FINISH:
fragment = new FinishFragment();
break;
case TYPE_INBOX:
fragment = new InboxFragment();
break;
case TYPE_MEMO:
fragment = new MemoFragment();
break;
case TYPE_PROJECT:
fragment = new ProjectFragment();
break;
case TYPE_TODO:
fragment = new ToDoFragment();
break;
case TYPE_TRASH:
fragment = new TrashFragment();
break;
}
Bundle bundle = new Bundle();
bundle.putParcelable("event_manager", EventManager.getInstance());
fragment.setArguments(bundle);
if (fragment != null) {
mFragments.put(fragmentName, fragment);
}
}
return fragment;
}
}

看得出来这个工厂模式特别简单,甚至严格来说并不能算是工厂模式,而算是一种编程习惯,因为在这里知识简单解决了我之前的定制化的问题,并没有什么设计思想体现在里面,也没有什么深奥的封装,不过这并没有什么影响啊,在这里我并不需要多么复杂的模式,仅仅依靠上面的代码就已经可以完成我的需求了,那么就不再需要更复杂的模式来增加工作量了,否则我觉得就是过度设计了。

同时 BaseFragment 的内容也需要有所改变了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class BaseFragment extends Fragment implements Observer {
private static final String TAG = "BaseFragment";
protected EventManager eventManager;
protected Handler handler;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Bundle bundle = getArguments();
eventManager = bundle.getParcelable("event_manager");
eventManager.registerObserver(this);
super.onCreate(savedInstanceState);
}

@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof MainActivity) {
MainActivity activity = (MainActivity) context;
this.handler = activity.mHandler;
}
}

@Override
public void onUpdate(Message msg) {
throw new RuntimeException("must override this method in Observer!");
}

@Override
public void onDestroy() {
super.onDestroy();
eventManager.removeObserver(this);
}
}

到这里工厂模式的部分就结束了,不知道你有没有注意到,我在获取 EventManager 的时候用的是 EventManager.getInstance() ,没错,这是个单例模式,而且是双重检查锁定的单例。主要还是上一篇文章中有小伙伴问是否在多线程环境下也适用,正好我后来也确实有了多线程通信的需求,所以为了保证在多线程环境下也能够使用,改进了 EventManager 内部的实现,并且为里面的方法也加了同步保护,不过我现在并不打算写出来,因为我还没有实验过多线程环境下的可靠性,目前还只是能用的程度,所以就当成一个小彩蛋吧。