📱

WebViewとアプリ内/外ブラウザ起動

2022/04/24に公開
2
CHANGELOG

本記事では、以下 gif の挙動の違いを解説しています。

iOS Android

はじめに

Flutter アプリ(ネイテイブアプリ)で Web サイトを表示したい場合に、ご存知の通り WebView やブラウザ起動がありますが、各 OS でのそれぞれの挙動の違いや特性について改めて整理しました。Flutter 公式プラグインである以下2つを本記事では取り扱っています。
https://pub.dev/packages/webview_flutter
https://pub.dev/packages/url_launcher

※WebView 表示のプラグインとして flutter_inappwebview | Flutter Package が有名で私も使っていた時期がありますが、HTTPS通信時のホスト名検証がない点 や、 結構な数のイシューが放置されたままになっている などの理由から、現在は webview_flutter を利用しています。

Webサイトを表示する3パターンの方法

ネイテイブアプリで Web サイトを表示する方法として主に下記の3つが上げられます(呼び名はイメージしやすいように便宜上定めています)。

  1. WebView埋め込み型
    • Flutter の PlatformView を使ってネイテイブの WebView クラスを埋め込みで表示
    • 必ずしもフルスクリーンである必要はなく自由な幅・高さで表示ができる
    • UserAgent の指定や URL のホワイトリスと対応などカスタマイズ性が高い
  2. アプリ内ブラウザ
    • Safari アプリをアプリから離脱せずに表示し、Safari アプリと同等のブラウジング操作ができる
    • Flutter の View に対してモーダル遷移で表示される(見た目的には画面遷移のような形)
    • 基本的には表示のみで WebView 埋め込み型ほどカスタマイズ性能はない
    • Android の場合は WebView 埋込み型がフルスクリーン形式で表示されている
  3. 外部アプリケーション起動
    • 端末のデフォルトブラウザで起動する(アプリからいったん離脱する)
    • 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 compositionorVirtual 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 以前は forceSafariVCforceWebView でプラットフォームごとに個別でプロパティを設定する必要がありましたが、これらが 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 uses webOnlyWindowName for web URLs, and behaves like
    /// [LaunchMode.externalApplication] for any other content.

ひとまず iOS と Android に注目すると、

  • Web URL の場合: アプリ内ブラウザ(inAppWebView
  • それ以外の場合(ディープリンク等): 外部ブラウザまたは外部アプリ(externalApplication

といった挙動によしなに切り替えてくれるので特別要件がなければ platformDefault(未指定)で良さそうですね。

一方、アプリ内ブラウザではなく優先的に外部ブラウザまたは外部アプリを起動させたい場合は、externalApplication を明示すると期待通りの動きになります。externalNonBrowserApplicationexternalApplicationと挙動的には似ていますが、以下の違いがあります。

わかりやすさのために、用語をiOSでを統一すると以下となります。

  • externalApplication
    1. Universal Linksで特定のアプリに飛ぶ場合はそこへ遷移
    2. 飛ばないなら、Safariアプリ(外部ブラウザ)に飛んで表示
  • externalNonBrowserApplication
    1. Universal Linksで特定のアプリに飛ぶ場合はそこへ遷移
    2. 飛ばないなら、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 サイトをハックできなさそうで、セキュリティ面でのケアは不要そうです。
https://github.com/flutter/plugins/blob/10c8f9e3fba3ff98dd9344a09b1b6bb98efe3959/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m#L26

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 の方がカスタマイズ性能は高く、細かいハンドリングができるので、その辺りは要件に併せて臨機応変に選定できると良いと思います。

https://github.com/flutter/plugins/blob/10c8f9e3fba3ff98dd9344a09b1b6bb98efe3959/packages/url_launcher/url_launcher_android/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java#L50

参考

Discussion

monomono

(ちなみに externalNonBrowserApplication をした状態で通常の Web URL を指定すると、iOS では何も起きず、Android では外部ブラウザが起動する挙動を確認しましたが、普通に externalApplication 指定で良い気がします)。

以下のように対応したい時などに使えます:

  1. externalNonBrowserApplication指定で、Universal Linksで特定のアプリに飛ぶならそこに遷移
  2. 飛ばないなら、inAppWebViewで呼び直してアプリ内のブラウザで表示

具体的なユースケースとしては、なるべくそのアプリ内にとどまってほしい(Safariアプリには飛ばしたくない)けどUniversal Linksなどが適宜発動した方が利便性高いことも多いのでそのケアも両立させたい、という時などだと思います。

externalApplicationだけだと、以下の挙動で変わりますね:

  1. Universal Linksで特定のアプリに飛ぶならそこに遷移
  2. 飛ばないなら、Safariアプリに飛んで表示