DialogFragment踩坑记

2021-09-29 15:09:08 浏览数 (1)

忙完了美国大选,可以继续更新公众号了

DialogFragment推出来已经很久了,网上相关的文档一大堆,但是稍微不注意,还是会踩坑,本篇基于自身经历总结

  • 要选android X下的DialogFragment
  • 普通的Fragment还是DialogFragment
  • 同时设置不要Title跟背景
  • 隐藏Dialog
  • 不保留活动
  • 显示的逻辑
  • 推荐使用DialogFragment

要选android X下的DialogFragment

DialogFragment有两个不同的包名

  1. androidx.fragment.app.DialogFragment
  2. android.app.DialogFragment

为了兼容各个Android版本的,记得要选择第一种DialogFragment,当然,系统源码也很贴心的把它Framework的DialogFragment设置为不推荐使用,而且还注明让你使用support包下面的DialogFragment,当然项目也基本都从support转成了Android x,感觉这个注释可以更新下了

普通的Fragment还是DialogFragment

先看下系统onCreate的方法

mShowsDialog = mContainerId == 0; 只有当mContainerId等于0的时候,才会生成dialog,不然的话,跟普通的Fragment没有任何区别,所以要用DialogFragment#show()方法来展示

DialogFragment如果只是当做普通的Fragment,建议使用普通的Fragment就可以了,这样代码逻辑比较清晰

同时设置不要Title跟背景

想同时不要Title跟背景,这个时候,可以去网上搜索下,可以发现一大堆的答案,都是需要繁琐的配置各种theme,最少也需要十几行代码,其实看下源码,就可以轻松知道,只要下面一行代码就可以了

在onCreate9()的时候设置下 setStyle(STYLE_NO_FRAME, 0)

先看下DialogFragment#setupDialog代码

代码语言:javascript复制
public void setupDialog(@NonNull Dialog dialog, int style) {                 
    switch (style) {                                                         
        case STYLE_NO_INPUT:                                                 
            Window window = dialog.getWindow();                              
            if (window != null) {                                            
                window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);    
            }                                                                
            // fall through...                                               
        case STYLE_NO_FRAME:                                                 
        case STYLE_NO_TITLE:                                                 
            dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);            
    }                                                                        
}                                                                            

可以看到,设置NO_FRAME会带Window.FEATURE_NO_TITLE的feature,所以设置NO_Frame可以同时实现没有title的效果 另外,看下NO_FRAME是如何实现没有背景的

代码语言:javascript复制
public void setStyle(@DialogStyle int style, @StyleRes int theme) {
    mStyle = style;                                                
    if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) {    
        mTheme = android.R.style.Theme_Panel;                      
    }                                                              
    if (theme != 0) {                                              
        mTheme = theme;                                            
    }                                                              
}                                                                  

可以发现,最终实际生效的,其实是mTheme这个主题参数,我们继续看下系统R.style.Theme_Panel的源码

代码语言:javascript复制
<style name="Theme.Panel">
    <item name="windowBackground">@color/transparent</item>
    <item name="colorBackgroundCacheHint">@null</item>
    <item name="windowFrame">@null</item>
    <item name="windowContentOverlay">@null</item>
    <item name="windowAnimationStyle">@null</item>
    <item name="windowIsFloating">true</item>
    <item name="backgroundDimEnabled">false</item>
    <item name="windowIsTranslucent">true</item>
    <item name="windowNoTitle">true</item>
</style>

所以设置了NO_FRAME,相当于帮我们设置了上面的Theme属性,其实我们也可以自己手动设置上面的theme,效果也是一样的,不过就会无谓的增加许多代码了

隐藏Dialog

很容易想到,调用DialogFragment#dismiss方法来隐藏dialog,不过这个方法在线上运行,很容易会报下面的错误

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState

可以看下实际报错的地方

代码语言:javascript复制
private void checkStateLoss() {                                          
    if (isStateSaved()) {                                                
        throw new IllegalStateException(                                 
                "Can not perform this action after onSaveInstanceState");
    }                                                                    
}                                                                        

在实际项目中,很可能展示dialog后,用户按了home键去操作其他的,这个时候调用dismiss,就会触发这个报错

可以改成DialogFragment#dismissAllowingStateLoss方法,就不会走到checkStateLoss,就可以轻易规避这个问题了

特别强调的是,这个报错自测阶段很不容易暴露出来,但是一上线上环境就容易发生,需要注意规避

不保留活动

这是一个不得不考虑的场景,实际情况下,发生了不保留活动,业务这边的逻辑一般是重置了,所以也是不需要再展示dialog,不过发生不保留,系统会自动重新展示dialog,这个时候,需要手动关闭dialog

代码语言:javascript复制
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)                    
    if (savedInstanceState != null) {                                
        //处理不保留活动下的场景,这个时候,返回会重新展示,改成主动关闭,不展示
        dismissAllowingStateLoss()                                               
    }                                                                
}                                                                    

显示的逻辑

大多数场景,显示dialog,一般都是用户在操作的时候,这个时候调用DialogFragment#show()方法展示是没有问题的 先看下源码

代码语言:javascript复制
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
    mDismissed = false;                                                   
    mShownByMe = true;                                                    
    FragmentTransaction ft = manager.beginTransaction();                  
    ft.add(this, tag);                                                    
    ft.commit();                                                          
}                                                                         

其实内部是通过commit方法,而不是commitAllowingStateLoss方法,所以当业务方有可能在页面不可见的时候调用展示dialog,也还是会触发崩溃

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState

作为规避,有两个方案 一 在页面不可见的时候,调用展示dialog无效

代码语言:javascript复制
fun showDialog(activity:FragmentActivity) {
    if (activity.isFinishing || activity.isDestroyed) {   
        return                                                     
    }                                                                                                                                               
    if (activity.supportFragmentManager.isStateSaved) {           
        //已经走到save状态,不展示dialog                             
        return                                                     
    }                             
}                                                                                                                                                   

二 先不展示,在下次页面返回可见的时候再展示 在监听到onResume的时候,触发Dialog的显示,代码就不贴了

推荐使用DialogFragment

建议展示Dialog的地方,统一使用DialogFragment,更好的处理生命周期的各种场景,而且在Fragment回收后,也可以自动帮我们关闭Dialog,避免逻辑异常

代码语言:javascript复制
public void onDestroyView() {              
    super.onDestroyView();                 
    if (mDialog != null) {                 
        mViewDestroyed = true;             
        //fragment销毁后,会自动关闭dialog
        mDialog.setOnDismissListener(null);
        mDialog.dismiss();                 
        if (!mDismissed) {                 
            onDismiss(mDialog);            
        }                                  
        mDialog = null;                    
    }                                      
}                                          

0 人点赞