【转载请注明出处】
作者:DrkCore (http://blog.csdn.net/DrkCore)
CSDN链接:(http://blog.csdn.net/drkcore/article/details/51262189)
DialogFragment是自3.0开始加入的对话框基类,后来在support-v4包推出后DialogFragment也能在2.x版本上使用。因为其具有Fragment的特性并且能够在页面销毁后自动重建而广受开发者们的青睐,然而在将传统的AlertDialog切换到DialogFragment的路上想必大家一定遇到了和笔者一样的问题:
页面销毁时该如何保存回调?
AlertDialog在页面被销毁后不会重建所以是不存在回调的问题的。DialogFragment会重建,但是之前传入的回调对象在重建之后就丢失了。我们可以在onSaveInstanceState(Bundle)方法中保存数据,但是回调通常会和监听者的上下文产生联系而这个上下文在重建后就被销毁了,因而没办法将回调保存到Bundle之中(或者这么做了会泄露上下文?)。
前文说到DialogFragment具有Fragment的特性而Fragment可以通过getActivity等方法获得自己所处的环境上下文,所以不少同行想必都在自己的DialogFragment基类中写过如下的方法:
protected final <T> T getListenerFromParent(Class<T> listener) {
if (!listener.isInterface()) {
throw new IllegalArgumentException("getListenerFromParent()方法只允许获取接口");
}
Object obj;
if (listener.isInstance(obj = getTargetFragment())/*targetFragment为最优先级*/
|| listener.isInstance(obj = getParentFragment()/*父fragment为次*/)
|| listener.isInstance(obj = getContext())/*Activity最次*/) {
return (T) obj;
}
return null;
}
如果DialogFragment所依附的Activity或者Fragment实现了指定的回调接口就将之类型转换并返回监听者实例。如果使用的对话框只有一两个并且相互之间的监听接口的类型不同的话,这是最好的解决方案。但是一旦需要设置监听的对话框有多种的话我们的Activity和Fragment就会变得很臃肿,并且同一个回调方法可能要给多个对话框用,造成可读性的下降。这也是为什么我们常常在方法内使用局部匿名内部类来实现监听者接口的原因。
虽然DialogFragment有着以上的问题但是代码还是要写的,好在经过一番摸索后笔者找到了思路。
我们知道JAVA中有一个十分重要的机制叫反射,通过反射我们可以越过权限获得类和对象的所有属性,其中当然就包括类的成员域和对象中成员域所引用的实例。在DialogFragment中我们虽然没有办法将回调实例保存到Bundle中但我们可以将这个回调实例的位置信息保存起来,如果这个回调实例是定义在Activity或者父Fragment中的成员域的话,重建之后我们就可以通过记录下的位置信息从Activity或者父Fragment中用反射将之抽取出来。
确定思路之后就是代码实现了。首先我们先写一个类用来保存回调对象的位置,如下:
//继承自Serializable方便将数据扔进Bundle之中,当然,用Parcelable也可以
private static class ListenerLocation implements Serializable {
//回调接口的class
public Class listenerClz;
//出处的标志
public int from;
//回调实例所在的成员域名
public String fieldName;
}
之后我们用以下的方法来查找回调实例所处的位置:
/** * 按照{@link #getTargetFragment()}、{@link #getParentFragment()}和{@link #getContext()}的顺序依次 * 查找listener是否是其某一成员变量。如果是则返回该成员变量的位置信息,否则为null。 * * @param listenerType * @param listener * @return */
@Nullable
private ListenerLocation findListenerLocation(@NonNull Class listenerType, @NonNull Object listener) {
//按照顺序逐一查找
Object[] from = {getTargetFragment(), getParentFragment(), getContext()};
for (int i = 0, len = from.length; i < len; i++) {
String fieldName = from[i] != null ? checkListenerBelongs(from[i], listener) : null;
if (fieldName == null) {
continue;
}
//执行到这里说明已经找到了,直接return就行了
return new ListenerLocation(listenerType, i, fieldName);
}
return null;
}
/** * 检查listener是否被toCheck对象的某一个成员所引用。 * 如果是则返回该成员域的名字,否则为null。 * * @param toCheck * @param listener * @return */
@Nullable
private static String checkListenerBelongs(@NonNull Object toCheck, @NonNull Object listener) {
//反射获取被检查的类的所有成员域
Class toCheckClz = toCheck.getClass();
Field[] fields = toCheckClz.getDeclaredFields();
for (Field field : fields) {
try {
field.setAccessible(true);//打开限制
//如果要保存的接口是toCheck的非静态的且被final修饰的成员则返回域名称,表明查找成功
if (!Modifier.isStatic(field.getModifiers())
&& Modifier.isFinal(field.getModifiers())
&& field.get(toCheck) == listener) {
return field.getName();
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return null;
}
为了稳定性考虑这里只查找非静态且被final修饰的成员域。DialogFragment基类的完整代码如下:
public abstract class CoreDlgFrag extends DialogFragment {
/* 继承 */
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
//将回调位置信息保存到Bundle中
outState.putSerializable(KEY_LISTENER_HOLDER_BUNDLE, listenerLocations);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
//Dlg被show了,是时候将临时存储起来的Listener倒出来保存其位置信息了
dumpTmpListener();
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//恢复回调位置信息
ArrayList<ListenerLocation> holders = savedInstanceState != null ? (ArrayList<ListenerLocation>) savedInstanceState.getSerializable(KEY_LISTENER_HOLDER_BUNDLE) : null;
if (holders != null && !holders.isEmpty()) {
if (listenerLocations != null) {
listenerLocations.addAll(holders);
} else {
listenerLocations = holders;
}
}
}
/*回调辅助*/
private static final String KEY_LISTENER_HOLDER_BUNDLE = "KEY_LISTENER_HOLDER_BUNDLE";
/** * 通常Dlg是现setListener之后再被show的,在此之前是无法获得parentFrag或者context等。 * 这里用一个Map先将之保存下来,当{@link #onAttach(Context)}时再重新{@link #saveListener(Class, Object)}。 */
private ArrayMap<Class, Object> tmpListeners;
/** * 所哟的回调位置信息 */
private ArrayList<ListenerLocation> listenerLocations;
/** * 存储回调对象的位置信息以便在Activity重建之后找到对应的实例。 * 使用该方法你必须将回调实现定义在非静态且被final修饰的成员变量内,比如: * <p/> * <code> * class TmpFrag extends Fragment{ <br/><br/> * //theListener就是会被保存下的位置。<br/> * 任意权限修饰符 final XXXListener theListener = new XXXListener(){}; * <br/><br/> * } * </code> * <br/> * * @param listenerType * @param listener */
protected final void saveListener(Class listenerType, Object listener) {
if (listener == null || listener instanceof Activity || listener instanceof Fragment) {
//为null直接退出不解释
//如果是activity或者fragment的话不保存回调,用getListenerFromParent就行了
return;
}
if (!isAdded()) {
/**还没添加到界面时无法获得context和parentFrag所以这里先添加到临时的栈中 * 当{@link #onAttach(Context)}时再重新save*/
if (tmpListeners == null) {
tmpListeners = new ArrayMap<>();
}
tmpListeners.put(listenerType, listener);
return;
}
//搜索回调对象的位置
ListenerLocation listenerLocation = findListenerLocation(listenerType, listener);
if (listenerLocation == null) {
return;//搜不到,直接退出
}
//执行到这里,说明已经搜索到了所在的类和Field名
if (listenerLocations == null) {
listenerLocations = new ArrayList<>();
}
listenerLocations.add(listenerLocation);
}
/** * 将临时保存的Listener重新save一下。 */
private void dumpTmpListener() {
if (tmpListeners == null) {
return;
}
for (Map.Entry<Class, Object> entry : tmpListeners.entrySet()) {
saveListener(entry.getKey(), entry.getValue());
}
//清空数据
tmpListeners.clear();
tmpListeners = null;
}
//省略代码
private ListenerLocation findListenerLocation();
private static String checkListenerBelongs();
//从保存的位置中查找回调
protected final <T> T getListenerFromSavedLocation(Class<T> listener) {
if (listenerLocations == null) {
return null;
}
//查找ListenerHolders中是否存在接口对应的位置信息
ListenerLocation location = null;
ListenerLocation tmp;
for (int i = 0, size = listenerLocations.size(); i < size; i++) {
tmp = listenerLocations.get(i);
if (tmp.listenerClz == listener) {
location = tmp;
break;
}
}
if (location == null) {
return null;
}
//执行到这说明location已经是要的那个接口的类,让我们开始反射一下
Object from = new Object[]{getTargetFragment(), getParentFragment(), getContext()}[location.from];
try {
Field field = from != null ? from.getClass().getDeclaredField(location.fieldName) : null;
if (field != null) {
field.setAccessible(true);
return (T) field.get(from);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
//省略代码
protected final <T> T getListenerFromParent() ;
/** * 按照{@link #getListenerFromParent(Class)}、{@link #getListenerFromSavedLocation(Class)}的顺序获取指定的回调实例。 * * @param listener * @param <T> * @return */
protected final <T> T getListenerEx(Class<T> listener) {
T t = getListenerFromParent(listener);
if (t == null) {//父容器不是接口则检查是否有保存下接口位置
t = getListenerFromSavedLocation(listener);
}
return t;
}
}
我们新建一个对话框:
public class AlertDialogFragment extends CoreDlgFrag {
public interface OnAlertDialogFragmentListener {
void onPositiveClick(AlertDialogFragment dlgFrag);
}
private OnAlertDialogFragmentListener listener;
public AlertDialogFragment setOnAlertDialogFragmentListener(OnAlertDialogFragmentListener listener) {
this.listener = listener;
saveListener(OnAlertDialogFragmentListener.class, listener);
return this;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle("这是标题");
builder.setMessage("对话框正在显示。现在请你进入【开发者模式】打开【不保留活动】选项以促使Activity重建。" +
"如果重建之后你点击确定按钮监听者仍能响应回调的话,说明成功了。");
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
listener = listener != null ? listener : getListenerEx(OnAlertDialogFragmentListener.class);
if (listener != null) {
listener.onPositiveClick(AlertDialogFragment.this);
}
}
});
return builder.create();
}
}
这是使用对话框的Activity:
public class MainActivity extends AppCompatActivity {
private final AlertDialogFragment.OnAlertDialogFragmentListener listener = new AlertDialogFragment.OnAlertDialogFragmentListener() {
@Override
public void onPositiveClick(AlertDialogFragment dlgFrag) {
Toast.makeText(MainActivity.this, "Activity响应了回调", Toast.LENGTH_SHORT).show();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View btn = findViewById(R.id.button_main_dialog);
if (btn != null) {
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new AlertDialogFragment().setOnAlertDialogFragmentListener(listener)
.show(getSupportFragmentManager(), AlertDialogFragment.class.getCanonicalName());
}
});
}
}
@Override
protected void onDestroy() {
super.onDestroy();
Toast.makeText(this, "Activity.onDestroy()", Toast.LENGTH_SHORT).show();
}
}
Demo本身很简单这里便不再赘述。
经过上面一番折腾之后我们已经能够保存回调实例的位置了。然而在应用过程中笔者遇到了不少问题:
1. 将回调实例定义为成员变量时项目的复杂度瞬间爆炸,不好维护
2. DialogFragment未被触发时成员变量始终被实例化,浪费内存
原本笔者打算后面慢慢优化来解决这个问题的,直到后来笔者在简书上看到一篇讲DialogFragment封装的博文。在回调的处理上对面的博主大体也是留一个标识符在重建后作为凭据恢复回调的,实现方式上有不少可取之处。
看了他的代码我忽然意识到为了让DialogFragment在重建之后恢复回调会增加大量的代码,但是无论用什么方式来恢复回调都比不上局部的匿名内部类来的简洁,这让我开始思考问题本身存在的意义:
我们真的有必要为重建的对话框找回回调吗?
或者说,有些对话框真的需要重建吗?
“确定要删除好友”、“选择文件排序方式”,大部分时候对话框是用来询问用户意图的,而意图本身需要语境来支持。DialogFragment被重建说明用户曾经离开并且使用别的应用很长一段时间,这样系统才会回收Activity导致重建的发生。此时对话框所处的语境早已被破坏,用户甚至可能忘了自己当初想要删好友是哪个了。所以有的时候直接进入初始的界面重建语境反而是最好的。
有些对话框本身承载着比较重要的业务,比如知乎的用户登陆对话框,而一两个重要的对话框让Activity或Fragment实现回调接口完全是可以接受的,甚至直接将逻辑封装在DialogFragment自身也可以。
对于重建的问题小的对话框可以直接dismiss()掉,大的对话框靠一个getListenerFromParent()方法就行了,引入回调的保存机制反而会让代码瞬间复杂起来,综上笔者得出的结论是——
我们并不需要封装这样的回调保存机制。
而配合lambda表达式写匿名类简直不要太简洁,堪称优雅代码的典范啊!
以上,即是笔者对于DialogFragment和回调的思考。如果你有自己的想法的话请在文末留言。
如果你觉得笔者的代码有可取之处或者想要亲自改进的话,源码如下:(http://download.csdn.net/detail/drkcore/9506994)