androidandroid-activitystacksingletasksingletop

How to manage Activity stack behavior in Android based on dynamic IDs?


I have one ActivityA that contains a specific ID in the intent data, one ActivityB that is open from this ActivityA, and ActivityB can also open the same ActivityA with the same ID or a different one. Seeing that opening activities this way can lead to an infinity activities in the stack, I need to manage the stack in the following way:

  1. User opens ActivityA with ID 1, then opens ActivityB from there. So far the stack will be like: ActivityA(id: 1) → ActivityB
  2. Then, from ActivityB user opens the same ActivityA with ID 1 and the stack will be like: ActivityA(id: 1). Note the ActivityB was just popped from the stack.

I know I can achieve this behaviour simply using android:launchMode="singleTask". But my problem is when the user opens the ActivityA with a different ID. So here is the desired second behaviour:

  1. User opens ActivityA with ID 1, then opens ActivityB from there. The stack will be now like: ActivityA(id: 1) → ActivityB
  2. Now from ActivityB user opens the ActivityA with the ID 2 and the stack should be like: ActivityA(id: 1) → ActivityB → ActivityA(id: 2). Note that instead of popping out ActivityB, it keeps in the stack and add another instance of ActivityA but now with a different ID.

Giving these two scenarios, I want two different behaviors depending on the ID from the ActivityA. And one limitation that I have is that I got no control of ActivityB, so I can't do any changes in the ActivityB.

PS: ActivityB opens ActivityA via deeplink.

Attempts

I could achieve similar behaviour using

android:documentLaunchMode="intoExisting"
android:launchMode="singleTask"

but it creates a new task every time the ActivityA is open and it would be a bad user experince.

Using taskAffinity will also create a new task and I can't afford that.

Update

My current attempt is:

AndroidManifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application>
        <activity
            android:name=".ActivityA"
            android:exported="false"
            android:theme="@style/Theme.Light.NoActionBar"
            android:windowSoftInputMode="stateAlwaysHidden"
            tools:ignore="AppLinkUrlError">
            <intent-filter>
                <data android:scheme="myscheme" />
                <data android:host="myhost" />

                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

    </application>
</manifest>

ActivityA code

private val idsInStack = mutableSetOf<String>()


class ActivityA : AppCompatActivity(R.layout.activity_a) {

    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val id = intent.data?.getQueryParameter("QUERY_ID")
        if (idsInStack.contains(id)) {
            val intent = Intent(this, ActivityA::class.java)
            intent.data = this.intent.data
            intent.addFlags(
                Intent.FLAG_ACTIVITY_CLEAR_TOP or
                        Intent.FLAG_ACTIVITY_SINGLE_TOP or
                        Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP
            )
            startActivity(intent)
            finish()
        }

        id?.let { idsInStack.add(it) }

        diInjection()
    }
}

PS: ActivityB launches ActivityA via deeplink and add no flags to the intent.


Solution

  • You can probably accomplish want you want like this:

    When ActivityA is launched, in its onCreate() it can store the ID into a static variable, that is available to all instances of ActivityA (in Kotlin you would use a property in a companion object)

    When ActivityB launches ActivityA, it will always create a new instance of ActivityA.

    When ActivityA is launched, the first thing it does in onCreate() (after calling super.onCreate() is to check if the id it was launched with is the same as the id stored in the static variable. If it is, it should remove all activities from the stack that are on top of the original instance of ActivityA (including itself), like this:

    Intent intent = new Intent(this, ActivityA.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
                    Intent.FLAG_ACTIVITY_SINGLE_TOP |
                    Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
    startActivity(intent);
    finish();
    

    Of course, because the static variable will be there forever, you'll need to think about cleaning it up so that it doesn't break further executions of your app.

    Try this out and see if it works for you.

    NOTE: I don't know what should happen if ActivityA is first launched with id=5, then it launches ActivityB, which then launches ActivityA with id=3, which then launches ActivityB, which then launches ActivityA. Is this a real scenario? What should happen in this case if ActivityB launches ActivityA with id = 3? What should happen in this case if ActivityB launches ActivityA with id = 5?