[Android] Record some pitfalls encountered when using global context to create AlertDialog!

Project scenario:

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

This business requirement is to create a Dialog to give a prompt if a specified situation occurs in a non-Activity. Since it is a non-Activity to obtain context, only one global context can be used. In the following, we will refer to this global context as applicationContext.


Problem Description

第一个问题,直接使用applicationContext作为dialog的context参数:
If you use applicationContext in this way, congratulations, it will cause the following exception, and then the App will crash:

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

In order to use applicationContext to start a dialog, after solving the first problem, use the applicationContext to create an Acitvity, and then indirectly create a dialog in the Activity to complete the requirement. However, in this process, the first problem occurs Two questions.
第二个问题,使用applicationContext启动Activity抛出异常:
An exception will be thrown when using the applicationContext to start the activity.
After solving the second problem, you need to create a dialog in the Activity. In order to ensure the effect of the dialog, you need to set the activity in a translucent state, and this state is set to cause the third problem.
第三个问题,在使用了设置半透明主题的activity去启动一个AlertDialog时,报出了异常

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

Cause Analysis:

针对第一个问题:
The reason for throwing this exception is also very clear in the above sentence. If we use applicationContext, our dialog will not have a token. When the dialog is created, it must have this token , in order to prevent random pop-ups (for example: it was stipulated that our dialog must pop up when the activity is displayed, if there is no such token, it means that the dialog can be popped up casually Yes . If we have closed the activity, but the desktop pop-up window appears inexplicably at this time, this is definitely not what we want).
So what exactly is this token? It is actually a Binder object (if this is not clear, you can find out, it is an important cross-process communication mechanism in Android), mainly for WMS, token will be bound to window, and WMS is through this binder Object to manage windows.

针对第二个问题:
The second problem is that you want to use applicationContext to start an Activity. The culprit is the startActivity method in ContextWrapper. The specific method is as follows:

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);
}

The most critical part is the condition in the if statement **(intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0** This condition means that there will be a flag in the Intent, if it is not FLAG_ACTIVITY_NEW_TASK, it will throw the to the exception.
Let's think about why this design is made. As we all know, when starting an Activity in an Activity, there will be a task stack, which is convenient for us to reuse the Activity. Now we start the Activity without relying on the Activity, but using the Context, in order to ensure the same The reuse of Activity requires us to manually set this flag. Then why is it not necessary to start in the Activity? It is very simple to rewrite this method in the Activity class, without the judgment of the flag we mentioned above.

针对第三个问题:
First of all, let's find out where the problem is. If we don't set the theme of the Activity (in a non-transparent state), start an AlertDialog, the code is as follows:

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

Then, the created dialog looks like this, and everything is normal, but this obviously does not meet our needs, and we cannot see the previous Activity: in order to meet the needs, the theme of the Activity is changed to
insert image description here
translucent, Add attributes to the corresponding Activity in the AndroidManifest file:

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

At this time, the problem comes, our App will throw the exception we mentioned above: IllegalStateException , we have not modified other places, and this exception is caused by adding a new attribute, so our problem must be related to this Attributes matter! ! ! Take a closer look at the exception information You need to use a Theme.AppCompat theme (or descendant) with this activity , why is this happening? Could it be that AlertDialog is not compatible with ordinary Theme? Is the theme used by AlertDialog AppCompat?
Track to AlertDialog! Sure enough, it is the same as I thought. You can see that AlertDialog inherits from AppCompatDialog. From the name of AppCompatDialog, you can see that they must be controls under the theme of AppCompat! , so our problem should be here, if you want to use AlertDialog, you should only use the AppCompat theme.

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

But at this point, do you have any doubts? Usually, when we have not set the theme, AlertDialog is used normally. Why is this? After a series of tracking, it can be found that the parent of Android's default theme will eventually be tracked to Theme.AppCompat, so AlertDialog can be used. If you are interested, you can track it yourself, so I won't go into details here.


solution:

针对第一个问题:

Since the token cannot be obtained by using the application to create a dialog, some indirect ideas can be used to complete the request. There is this token in the Activity, so you can use the global context to create an Activity, and then create a dialog in the Activity.

针对第二个问题:
The solution is to manually set this flag, that is, intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK.

针对第三个问题:
In order to ensure that the modified theme has a relationship with AppCompat, we can customize a theme and set its parent to AppCompat. The custom theme is as follows:

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

Then modify the theme of the corresponding Activity in the AndroidManifest:

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

The problem is solved, so you can use applicationContext to indirectly implement a dialog while ensuring the background is transparent!

Summarize:

In the end, I wrote a demo to demonstrate the overall effect. This demo mainly uses three classes. ApplicationWrapper is to obtain the global context. There is a button in MainActivity to display the final result, and SecondActivity is used as a An indirect container to create dialogs using the global context.
insert image description here
ApplicationWrapper code is as follows:

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

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

MainActivity looks like this:

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)
        }
    }
}

SecondActivity as a dialog container looks like this:

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()
    }
}

The specific demonstration effect is as follows:
Please add a picture description

Guess you like

Origin blog.csdn.net/qq_42788340/article/details/125151339