【Android】记录一下使用全局context创建AlertDialog时遇到的一些坑!

项目场景:

本文用于记录一下使用AlertDialog的时候遇到的一些坑!首先介绍一下业务需求场景:

本业务需求为在一个非Activity中,如果出现某种指定情况时,创建一个Dialog进行一个提示,由于是非Activity中为了获取context,所以只能使用一个全局context。在下文中,我们将这个全局的context称为applicationContext。


问题描述

第一个问题,直接使用applicationContext作为dialog的context参数:
如果这样使用了applicationContext,恭喜你了,会导致如下异常,然后App就崩掉了:

BadTokenException: Unable to add window -- token null is not valid;

为了能够使用applicationContext来启动一个dialog,所以在解决了第一个问题后,采用了使用applicationContext创建一个Acitvity,然后间接的在Activity中去创建dialog的方式去完成需求,然而在这个过程中引发了第二个问题。
第二个问题,使用applicationContext启动Activity抛出异常:
在使用applicationContext去启动activity的时候会抛出异常。
解决完第二个问题后,需要在Activity中去创建一个dialog,为了保证dialog的效果,需要去设置activity处于一个半透明的状态,而就是设置了这个状态从而引发了第三个问题。
第三个问题,在使用了设置半透明主题的activity去启动一个AlertDialog时,报出了异常

java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.

原因分析:

针对第一个问题:
造成抛出这个异常的原因在上面那句话里面说的也很清楚了,如果使用applicationContext是不会让我们的dialog有一个token的。而dialog在创建时,必须拥有这个token,为的就是防止出现乱弹窗的情况(比如说:之前规定的是我们的dialog必须在activity显示的时候弹出,如果没有这个token,就表示diaolog可以随便弹了。假如我们把activity已经关了,但是这时候莫名其妙的出现桌面弹窗,这肯定不是我们所希望出现的情况)。
那么这个token到底是什么呢?他其实就是一个Binder对象(这个不清楚的话,可以去了解一下,他是Android中一个重要的跨进程通信机制),主要给WMS使用的,token会和window绑定,而WMS正是通过这个binder对象来管理窗口的。

针对第二个问题:
第二个问题,是想要使用applicationContext去启动一个Activity导致的,造成这个原因的罪魁祸首就是ContextWrapper中的startActivity方法,具体方法如下:

public void startActivity(Intent intent, Bundle options) {
    
    
    warnIfCallingFromSystemProcess();
    if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
    
    
        throw new AndroidRuntimeException(
                "Calling startActivity() from outside of an Activity "
                + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                + " Is this really what you want?");
    }
    mMainThread.getInstrumentation().execStartActivity(
            getOuterContext(), mMainThread.getApplicationThread(), null,
            (Activity) null, intent, -1, options);
}

最关键的地方就是那个if语句里面的条件**(intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0**这个条件的意思就是在Intent里面会有一个flag,如果不是FLAG_ACTIVITY_NEW_TASK就会抛出我们遇到的那个异常。
我们再来思考一下为什么会这样设计,众所周知,在Activity中启动Activity是会有任务栈的,便于我们重复使用Activity,而我们现在启动Activity并不依托于Activity,而是去使用Context,为了同样能够保证Activity的复用,需要我们手动的去设置这个flag。那为什么在Activity中启动不需要呢,很简单Activity类中重写了这个方法,没有了我们上面所说的对于flag的判断。

针对第三个问题:
首先来找找问题出在哪里了,假如我们不设置Activity的主题(处于非半透明状态)时,去启动一个AlertDialog,代码如下:

val dialogBuilder = AlertDialog.Builder(this)
dialogBuilder.setTitle("测试")
dialogBuilder.setMessage("我是一个用于测试的dialog")
dialogBuilder.create().show()

然后,创建的dialog是长这样的,一切都很正常,但是这样很明显是不满足我们的需求的,我们看不到上一个Activity了:
在这里插入图片描述
为了满足需求,所以将Activity的主题修改为半透明,在AndroidManifest文件中在对应的Activity中添加属性:

android:theme="@android:style/Theme.Translucent"	//将Activity设置为半透明状态

这时候,问题来了,我们的App就会抛出我们上面所提到的异常:IllegalStateException,其他地方我们都没有修改过,新增了一个属性就造成了这个异常,所以我们的问题肯定和这个属性有关系!!!再仔细看一下异常信息You need to use a Theme.AppCompat theme (or descendant) with this activity,为什么会这样呢,难道说是AlertDialog和普通的Theme不兼容吗?难道AlertDialog使用的主题是AppCompat吗?
追踪到AlertDialog中去!果然和我想的一样,可以看到AlertDialog继承自AppCompatDialog,从AppCompatDialog这个名字就可以看出来,他们肯定是AppCompat主题下的控件!,所以我们的问题应该就是在这,如果要使用AlertDialog,应该只能使用AppCompat主题。

public class AlertDialog extends AppCompatDialog implements DialogInterface {
    
    
	...
	...
}

但是到这里,大家有没有一个疑惑就是,平时的时候我们在没有设置过主题的时候,AlertDialog是正常使用的,这是为什么呢?经过一系列的追踪,可以发现Android的默认主题的parent最后会追踪到Theme.AppCompat去,所以说AlertDialog可以使用,大家有兴趣可以自己追踪一下,这里就不赘述了。


解决方案:

针对第一个问题:

由于使用application去创建dialog无法获取token,所以可以采用间接的一些思路去完成去求。Activity中是有这个token的,所以说可以用全局的context去创建一个Activity,然后在Activity中创建dialog。

针对第二个问题:
解决方案就是手动设置这个flag,即intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK即可。

针对第三个问题:
为了保证修改过的theme和AppCompat有关系,我们可以自定义一个主题,并将它的parent设置为AppCompat,自定义主题如下:

<style name="TransparentTheme" parent="Theme.AppCompat">
	<item name="android:windowBackground">@android:color/transparent</item>
	<item name="android:windowIsTranslucent">true</item>
</style>

然后在AndroidManifest中修改相应Activity的主题:

<activity
    android:name=".SecondActivity"
    android:exported="false"
    android:theme="@style/TransparentTheme"
/>

问题就得到解决了,这样可以在保证背景透明的情况下,使用applicationContext来间接的实现一个dialog!

总结:

在最后,我写了一个demo,来演示一下整体的一个效果,这个demo主要用到了三个类,ApplicationWrapper是为了获取全局的context,MainActivity里面有一个按钮用于展示最终结果,SecondActivity用来作为一个间接的容器来使用全局context创建dialog。
在这里插入图片描述
ApplicationWrapper代码如下:

class ApplicationWrapper: Application() {
    
    
    companion object{
    
    
        private lateinit var mContext:Context
        fun getContext(): Context = mContext
    }

    override fun onCreate() {
    
    
        super.onCreate()
        mContext = applicationContext
    }
}

MainActivity长这样:

class MainActivity : AppCompatActivity() {
    
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<Button>(R.id.mBtn1).setOnClickListener {
    
    
            val intent = Intent(ApplicationWrapper.getContext(), SecondActivity::class.java)
            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
            ApplicationWrapper.getContext().startActivity(intent)
        }
    }
}

作为dialog容器的SecondActivity长这样:

class SecondActivity : Activity() {
    
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        requestWindowFeature(Window.FEATURE_NO_TITLE)
        setContentView(R.layout.activity_second)
        val dialogBuilder = AlertDialog.Builder(this)
        dialogBuilder.setTitle("测试")
        dialogBuilder.setMessage("我是一个用于测试的dialog")
        dialogBuilder.setOnDismissListener {
    
    
        	//关闭dialog的同时关闭Activity
            this.finish()
        }
        dialogBuilder.create().show()
    }
}

具体演示效果如下:
请添加图片描述

猜你喜欢

转载自blog.csdn.net/qq_42788340/article/details/125151339