WebViewとアプリ内/外ブラウザ起動
CHANGELOG
- 2024.06.06
-
コメント 頂いた
externalNonBrowserApplication
の挙動について アプリ内ブラウザ、外部アプリケーション起動 に追記しました。
-
コメント 頂いた
本記事では、以下 gif の挙動の違いを解説しています。
iOS | Android |
---|---|
はじめに
Flutter アプリ(ネイテイブアプリ)で Web サイトを表示したい場合に、ご存知の通り WebView やブラウザ起動がありますが、各 OS でのそれぞれの挙動の違いや特性について改めて整理しました。Flutter 公式プラグインである以下2つを本記事では取り扱っています。
※WebView 表示のプラグインとして flutter_inappwebview | Flutter Package が有名で私も使っていた時期がありますが、HTTPS通信時のホスト名検証がない点 や、 結構な数のイシューが放置されたままになっている などの理由から、現在は webview_flutter
を利用しています。
Webサイトを表示する3パターンの方法
ネイテイブアプリで Web サイトを表示する方法として主に下記の3つが上げられます(呼び名はイメージしやすいように便宜上定めています)。
-
WebView埋め込み型
- Flutter の PlatformView を使ってネイテイブの WebView クラスを埋め込みで表示
- 必ずしもフルスクリーンである必要はなく自由な幅・高さで表示ができる
- UserAgent の指定や URL のホワイトリスと対応などカスタマイズ性が高い
-
アプリ内ブラウザ
- Safari アプリをアプリから離脱せずに表示し、Safari アプリと同等のブラウジング操作ができる
- Flutter の View に対してモーダル遷移で表示される(見た目的には画面遷移のような形)
- 基本的には表示のみで WebView 埋め込み型ほどカスタマイズ性能はない
- Android の場合は WebView 埋込み型がフルスクリーン形式で表示されている
-
外部アプリケーション起動
- 端末のデフォルトブラウザで起動する(アプリからいったん離脱する)
- Universal Links / App Links に対応している場合はアプリケーションが起動する
ネイテイブレベルで分解すると以下の表に分類できます。
画面 | WebView埋め込み型 | アプリ内ブラウザ | 外部アプリケーション起動 |
---|---|---|---|
iOS | WKWebView | SFSafariViewController | 端末のデフォルトブラウザ(大半はSaferiアプリ)もしくはアプリ |
Android | WebView | 同左 | 端末のデフォルトブラウザもしくはアプリ |
以降、実際に Flutter プラグインで実装する様子をサラッと見ていきます。
WebView埋め込み型
webview_flutter を使うことで「WebView 埋め込み型」が実現できます。実装自体は非常に簡単です。
final controller = Completer<WebViewController>();
WebView(
initialUrl: url,
onWebViewCreated: controller.complete,
navigationDelegate: (request) {
// requestに応じてアクセスをブロックできるなどカスタマイズ性が高い
},
),
On iOS the WebView widget is backed by a WKWebView; On Android the WebView widget is backed by a WebView.
内部的には、iOS では WKWebView
, Android では WebView
を利用して、Flutter の PlatformView(Android では AndroidView
、iOS では UiKitView
)を使ってネイテイブビューを描画しています。
また、Android での PlatformView では Hybrid composition
orVirtual display
が利用され、これらは 毎フレームごとにメモリコピーが行われることでFlutterのUI全体のパフォーマンスに影響を与える可能性があるとドキュメントに記載 されておりましたが、冒頭でお見せした gif からわかるように WebView で表示する程度ではその違和感は感じませんでした。
アプリ内ブラウザ、外部アプリケーション起動
url_launcher を使うことで「アプリ内ブラウザ」「外部アプリケーション起動」が実現できます。こちらも実装は非常に簡単です。
launchUrl(Uri.https('flutter.dev', ''));
// ref. https://github.com/flutter/plugins/blob/ca63d964d9fdfff9be36533ea4248d0d0ca28fd0/packages/url_launcher/url_launcher/lib/src/types.dart#L12
enum LaunchMode {
/// Leaves the decision of how to launch the URL to the platform
/// implementation.
platformDefault,
/// Loads the URL in an in-app web view (e.g., Safari View Controller).
inAppWebView,
/// Passes the URL to the OS to be handled by another application.
externalApplication,
/// Passes the URL to the OS to be handled by another non-browser application.
externalNonBrowserApplication,
}
バージョン 6.1.0
以前は forceSafariVC
や forceWebView
でプラットフォームごとに個別でプロパティを設定する必要がありましたが、これらが LaunchMode
という形で統一されました。LaunchMode.platformDefault
はプラットフォームごとに下記の挙動となっています。
- [LaunchMode.platformDefault] is supported on all platforms:
/// - On iOS and Android, this treats web URLs as
/// [LaunchMode.inAppWebView], and all other URLs as
/// [LaunchMode.externalApplication].
/// - On Windows, macOS, and Linux this behaves like
/// [LaunchMode.externalApplication].
/// - On web, this useswebOnlyWindowName
for web URLs, and behaves like
/// [LaunchMode.externalApplication] for any other content.
ひとまず iOS と Android に注目すると、
- Web URL の場合: アプリ内ブラウザ(
inAppWebView
) - それ以外の場合(ディープリンク等): 外部ブラウザまたは外部アプリ(
externalApplication
)
といった挙動によしなに切り替えてくれるので特別要件がなければ platformDefault
(未指定)で良さそうですね。
一方、アプリ内ブラウザではなく優先的に外部ブラウザまたは外部アプリを起動させたい場合は、externalApplication
を明示すると期待通りの動きになります。externalNonBrowserApplication
もexternalApplication
と挙動的には似ていますが、以下の違いがあります。
わかりやすさのために、用語をiOSでを統一すると以下となります。
-
externalApplication
- Universal Linksで特定のアプリに飛ぶ場合はそこへ遷移
- 飛ばないなら、Safariアプリ(外部ブラウザ)に飛んで表示
-
externalNonBrowserApplication
- Universal Linksで特定のアプリに飛ぶ場合はそこへ遷移
- 飛ばないなら、SFSafariViewController(アプリ内ブラウザ)で表示
- Safariアプリには遷移しない
ユースケースとしては、基本的にはアプリ内にとどまってほしい(Safariアプリなど外部のブラウザには飛ばしたくない)けど、Universal Linksなどが発動した方がユーザー体験が良いことが多いのでそのケアも両立させたい場合などが考えられます。
6.1.0以前の場合
※執筆途中でタイムリーに更新が入ったために、途中まで記載していた内容です。
iOS では forceSafariVC
、Android では forceWebView
とそれぞれ異なるプロパティを指定することで、アプリ内で表示するか外部ブラウザ起動にするか選択できます。デフォルト値が逆転している点が注意です。
画面 | プロパティ | デフォルト値 |
---|---|---|
iOS | forceSafariVC | true(アプリ内ブラウザ起動) |
Android | forceWebView | false(外部ブラウザ起動) |
※iOS 用に universalLinksOnly
というプロパティがありますが、url_launcuer
側で「ユニバーサルリンクの場合はアプリを起動し、それ以外はブラウザで開く」挙動を実現できているため、利用機会はほぼないと認識しています(ちなみに、universalLinksOnly: true
とした場合にユニバーサルリンクではないページにアクセスしようとすると PlatformException となります)。
launch(
url,
forceSafariVC: false, // default: false
forceWebView: true, // default: true
);
アプリ内ブラウザのネイテイブの挙動を見る
iOS では内部で Safari View Controller が使われています。iOS 9,10では Safari アプリと Cookie 等のデータが共有されていたそうですが、以降はアプリごとに独立しており、特段 Web サイトをハックできなさそうで、セキュリティ面でのケアは不要そうです。
The view controller includes Safari features such as Reader, AutoFill, Fraudulent Website Detection, and content blocking. In iOS 9 and 10, it shares cookies and other website data with Safari. The user's activity and interaction with SFSafariViewController are not visible to your app, which cannot access AutoFill data, browsing history, or website data. You do not need to secure data between your app and Safari.
https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller
Androidのアプリ内ブラウザはWebViewのフルスクリーン表示
一方で、Android の場合はアプリ内ブラウザ(=WebView のフルスクリーン表示)の形となっており、表示するだけであれば良いですが、何かしらのセキュリティ面でのケアが必要な場合は対応できません。WebView 表示をするのであれば前述の webview_flutter
の方がカスタマイズ性能は高く、細かいハンドリングができるので、その辺りは要件に併せて臨機応変に選定できると良いと思います。
Discussion
以下のように対応したい時などに使えます:
externalNonBrowserApplication
指定で、Universal Linksで特定のアプリに飛ぶならそこに遷移inAppWebView
で呼び直してアプリ内のブラウザで表示具体的なユースケースとしては、なるべくそのアプリ内にとどまってほしい(Safariアプリには飛ばしたくない)けどUniversal Linksなどが適宜発動した方が利便性高いことも多いのでそのケアも両立させたい、という時などだと思います。
externalApplicationだけだと、以下の挙動で変わりますね:
コメントありがとうございます。
ご指摘いただいた
externalNonBrowserApplication
の挙動について アプリ内ブラウザ、外部アプリケーション起動 に追記しました。