結論:外部アプリかどうかではなく、戻る体験で判断する
Jetpack ComposeのLocalUriHandlerで外部リンクを開くとき、「外部アプリだから」という理由だけでFLAG_ACTIVITY_NEW_TASKを追加する必要はありません。
リンクを開くこと自体は簡単ですが、そのあとどこに戻るのかも気になるところです。元の作業の途中で少しリンク先を見たいのか、それとも別の作業を始めたいのか。この違いも含めて考えた方が良いのではないでしょうか。
今回の検証では、Recentsでブラウザとテストアプリが別々に表示されていても、Backでテストアプリへ戻れました。一方、Xアプリをインストールすると、NEW_TASKを付けたケースだけXが開き、BackでXホームへ移動しました。
もちろん、手元の環境での結果なので、どの端末でも同じになるとは限りません。今回は、どのアプリが開くかだけでなく、そのあとBackでどこに戻るかまで確認した方が良さそう、という話をまとめてみます。
背景:LocalUriHandlerはNEW_TASKを付けていない
そもそも、今回調べ始めたのはこんな疑問からでした。
ComposeのLocalUriHandlerはFLAG_ACTIVITY_NEW_TASKを付与していないが、外部アプリを開く場合は付けるべきなのか?
最初は「他のアプリのActivityなら、別Taskにしておいた方が良いのでは?」と思っていました。
ところが、実際に試してみると、Taskが別に見えてもBackで元のアプリへ戻れます。どうもTaskを分ければ良い、というだけの話ではなさそうでした。
せっかく調べてみたので、公式ドキュメントと実装、実際に試した結果を順に残しておこうと思います。
Android公式仕様:Taskはアプリの境界と一致しない
通常の起動と、Taskを変える要素
Taskは、ある目的に向かって操作するActivityの集まりです。通常は呼び出し元のTaskにActivityが追加されますが、起動先のlaunchModeやtaskAffinity、Intentのflagによっても変わります。アプリの境界とTaskの境界は、必ずしも一致しません。Tasks and the back stack
NEW_TASKは「外部アプリ用」のflagではない
NEW_TASKは、新しいTaskでの起動や既存Taskの前面化に関わるflagです。外部アプリだから必要、というものではありません。ランチャーのように独立した操作を始める用途が説明されています。Intent.FLAG_ACTIVITY_NEW_TASK
なお、Activity以外のContextからの起動ではNEW_TASKが必要です。今回のActivity上からの起動とは分けて考えます。Context.startActivity)
LocalUriHandlerのAndroid実装
では、LocalUriHandlerは実際に何をしているのでしょうか。AndroidXのAndroidUriHandlerを確認すると、起動処理は次の一行でした。
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(uri)))
NEW_TASKは付けていません。確認した実装はAndroidUriHandlerの公式ソース(HEAD)です。検証に使用したComposeのバージョンとの照合は、公開前TODOに残しています。
検証方法:3通りの起動を比較する
というわけで、Composeで簡単なテストアプリを作ってみました。同じURLを、次の3通りで開いて比較します。
| パターン | 起動方法 | NEW_TASK |
|---|---|---|
| 1 | LocalUriHandler.openUri() |
標準実装では付与なし |
| 2 | Activity ContextからACTION_VIEW |
付与なし |
| 3 | Activity ContextからACTION_VIEW |
付与あり |
呼び出し元のManifest
<activity android:name=".MainActivity" android:exported="true" android:label="@string/app_name" android:theme="@style/Theme.LocalUriHandlerTest" android:windowSoftInputMode="adjustResize"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
Task関連の設定は、デフォルトのままです。
実際に使用した検証コードの要点
実際の起動処理を抜粋します。ActivityのsetContent内で、各処理を別々のボタンから実行しています。
val context = LocalContext.current val uriHandler = LocalUriHandler.current var url by remember { mutableStateOf("https://x.com/kotlin/status/2070181489703088130") } // 1. LocalUriHandler(ボタンのonClick内) uriHandler.openUri(url.trim()) // 2. ACTION_VIEW(別のボタンのonClick内) context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url.trim()))) // 3. ACTION_VIEW + NEW_TASK(さらに別のボタンのonClick内) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url.trim())).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } context.startActivity(intent)
結果表示で確認できる範囲
画面のwas invokedは、呼び出し時に捕捉対象の例外が出なかったことを示すだけです。起動先、Recents、Back後の画面は目視で確認しました。
ここでいうBackはシステムの戻る操作です。アプリ内のUp操作とは区別しています。Principles of navigation
F-42A/Android 10での検証結果
Xインストール前:別々のRecents表示でも戻れた
まずはXを入れていない状態で試してみました。結果は、3パターンとも外部ブラウザが起動しました。
| 起動方法 | 表示されたアプリ | Recentsの見え方 | Backの結果 |
|---|---|---|---|
| LocalUriHandler | 外部ブラウザ | テストアプリと別表示 | テストアプリへ戻った |
| ACTION_VIEW | 外部ブラウザ | テストアプリと別表示 | テストアプリへ戻った |
| ACTION_VIEW + NEW_TASK | 外部ブラウザ | テストアプリと別表示 | テストアプリへ戻った |
少なくとも、履歴が別に表示されるからといって、Backで戻れなくなるわけではなさそうです。
Xインストール後:全てXアプリが開いた
次に、同じ端末にXアプリを入れて試してみました。すると、今度は開くアプリ自体が変わりました。
| 起動方法 | 最終的に表示されたアプリ | 記録されたBackの結果 |
|---|---|---|
| LocalUriHandler | Xのリンク先画面 | テストアプリに戻った |
| ACTION_VIEW | Xのリンク先画面 | テストアプリに戻った |
| ACTION_VIEW + NEW_TASK | Xのリンク先画面 | Xホームへ移動し、テストアプリには戻らなかった |
NEW_TASK付きの場合はBackではテストアプリに戻れず、Xホームへ移動しました。
Android Emulatorでの追加検証時の注意点
エミュレーター環境ではPlay Storeにログインできず、Chromeなどを最新状態へ更新できていません。これはGoogle Play 開発者サービスが更新できないためです。
Play Storeにログインするためにはパスワード検証+別端末からの承認が必要となります。また、エミュレーター独自の検証と思われますが、24時間後にGmailに届く認証メールから認証する必要があります。
そのため、エミュレーターでは実機と同じ状態での検証は難しいため、できる限り実機を用意することが重要となります。
TaskとBackward Navigationは分けて確認する
Android公式のTask解説にも、singleTaskで別Taskに起動した場合でも、Backで前のActivityへ戻るケースが記載されています。Taskの境界があること自体は、リンク元へ戻れないことを意味しません。Tasks and the back stack
NEW_TASKは、呼び出し元の履歴を消す指定でもありません。元のTaskは通常バックグラウンドに残ります。「Xホームへ戻った」ことだけでは、元アプリの履歴が失われたとは判断できません。Tasks and the back stack
X側で戻り先を用意した可能性
追加で調べたところ、NavigationのDeep Link解説に、今回の挙動に近い仕組みがありました。
Navigationコンポーネントでは、NEW_TASK付きの暗黙的Deep Linkに対し、リンク先だけでなくNavigation Graphの開始画面などを含むBack Stackを構築します。実際にはホームを経由していなくても、Backでそこへ戻れるわけです。
これはNavigationコンポーネントの仕組みで、すべてのアプリにOSが自動で行う処理ではありません。
今回のXも独自のBack処理でホームへ移動したと考えています。ただし、XがNavigationコンポーネントを使っているかも含め、内部実装は未確認です。
検証時は、次の3点を分けると整理しやすくなります。
| 確認するもの | 確認したい内容 |
|---|---|
| 起動先の選択・中継 | どのアプリ・Activityがリンクを受け取り、何を表示したか |
| Taskの構造 | どのTaskにActivityが入り、既存Taskが再利用されたか |
| Backの体験 | 戻る操作でどの画面へ移動し、元の作業を再開できたか |
UXとしてどう判断すべきか
現在の目的の続きを見に行くのか
例えば、アプリ内の説明を読んでいる途中で関連するX投稿を確認したい場合、期待する流れは「元アプリ → X投稿を確認 → Backで元アプリへ戻る」かもしれません。自分としても、投稿を見たらBackで元のアプリへ戻るつもりでした。
ところが、NEW_TASK付きで観測した流れは「元アプリ → Xのリンク先画面 → BackでXホーム」でした。投稿を見たかっただけなのに、元のアプリを探し直すことになります。ちょっとしたことですが、使ってみると気になるところです。
もちろん、Xホームへ戻るのがいつも困るという話ではありません。何をしようとしてリンクを開いたのか、その流れに戻り先が合っているかが大事なのだと思います。
呼び出し側の判断は、リンク先での体験にも関わる
今回の結果からは、呼び出し側のIntent設計が、リンク先でのNavigation体験にも関わる可能性がうかがえます。
使っている側からすると、リンク元のIntentが原因なのか、Xなのか、Androidなのかは分からないですよね。
そう考えると、「元のアプリに戻れない」という不満は、リンク元にもリンク先にも向くかもしれません。このあたりは公式仕様の話ではなく、今回試してみて感じたことです。
起動先も自分たちのアプリなら、基本方針は起動先で設計する
起動先も自分たちで開発しているアプリなら、そのアプリをどのようなTaskとして扱ってほしいかは、まず起動先で設計するのが自然ではないでしょうか。ActivityのTask設計に加えて、Deep Linkを受け取った時にどんな戻り先を用意するかまで、起動先で考えるのが良さそうです。
ただし、launchModeだけで見え方や戻り先のすべてが決まるわけではなく、呼び出し側のflagが優先される場合もあります。既存Activityの再利用やBack Stackへの影響も含めて設計する必要があります。Define launch modes
なお、Navigation利用時はlaunchMode="standard"が推奨されています。起動先で設計することと、特殊なlaunchModeへ変更することは同じではありません。Handle deep links
Xのようにこちらで変更できないアプリなら、通常の起動から試し、必要な理由があればflagを変更するくらいが良さそうです。
実装時の判断基準
| リンクを開く目的 | 今回の考察に基づく判断 |
|---|---|
| 関連情報を少し確認して、元の作業を続ける | Activityからの通常の起動を出発点とし、Backでの復帰を検証する |
| 自分たちの別アプリで、独立した作業を開始する | 起動先のTaskとDeep Linkの戻り先を設計し、flagは個別の要件で検討する |
| 管理できない別アプリを開く | 通常の起動を出発点にする。NEW_TASKは相手を独立したアプリとして見せたいという理由だけで追加せず、必要性と実際の戻り方を確認する |
| 「外部アプリだから」という理由しかない | それだけを根拠にNEW_TASKを追加しない |
まとめ
LocalUriHandlerのAndroid標準実装は、NEW_TASKを付けずにACTION_VIEWを発行しています。NEW_TASKは、外部アプリを開くために一律で必要なflagではありません。Activity以外のContextから起動する場合の要件は別に考えます。- 起動先も自分たちのアプリなら、TaskとDeep Linkの戻り先を起動先で設計し、flagは個別の起動要件として考えます。







