DroidKaigi 2021 で発表 https://www.youtube.com/watch?v=IOnpHyOg5sc
長期的にメンテナンス可能な Android アプリの WebView 画面実装について発表しました。
2020年代のWebView 実装こやまカニ大好き
View Slide
自己紹介こやまカニ大好きクックパッド モバイル基盤部 所属モバイルアプリ開発の生産性を向上するようなタスクに従事最近は WebView 大好き
AgendaWebView とはWebView の基礎知識クックパッドアプリとWebView2020年代の WebView最後に0102030405
AgendaWebView とはWebView の基礎知識クックパッドアプリと WebView2020年代の WebView最後に0102030405
WebView とはウェブページをレンダリングするためのView通常のViewと比べて以下の利点がある(と思う)● ウェブサービスの場合、低コストでコンテンツを表示可能● 表示するコンテンツの変更をリリースを介さずに行うことができる● 動画や音楽再生などのリッチコンテンツを扱う機能が実装済み
WebView 以外の方法ウェブページの表示自体は Chrome などの外部ブラウザに移譲できるAndroid では AndroidX Browser のCustomTabs を利用したアプリと親和性の高いブラウザ表示も可能
WebView でないとダメなケース外部ブラウザや CustomTabs では実現できないケースが存在する● 画面内のコンテンツの一部として WebView を利用している● ウェブコンテンツからネイティブ画面への遷移が多い● ネイティブコードの呼び出しを実行したい
ケース #1コンテンツの一部としてWebView を利用しているボトムタブや ViewPager の一部としてウェブコンテンツを表示したい場合、WebView を利用するしかありません。タブの中に表示している
ケース #2ウェブコンテンツからネイティブ画面への遷移外部ブラウザ と 自アプリのネイティブ画面を行き来するのはActivity同士の遷移になるため制限が多く、ユーザー体験を損なう押下するとログイン画面に遷移する
ケース #3ネイティブコードの呼び出しカスタムJavaScript や独自スキームのURLフック等を通じてネイティブ実装された処理を呼び出したい場合がある● ユーザー情報の更新● GooglePlay決済処理の実行決済関連の処理を呼び出す
例外 : 認証系の処理アプリのセッションを引き継いでWebコンテンツを表示したい場合以下のような方法で実装する場合は CustomTabs でも実装可能● 認証情報をURLに含める場合● 認証情報をリクエストヘッダに含める場合○ Chrome側の実装により制限あり○ https://developer.chrome.com/docs/android/custom-tabs/headers/
WebView の歴史(1/3)● 有史以前○ WebKit ベースのシステム組み込み WebView コンポーネント○ OSを更新しなければアップデートされない○ 同じOSバージョンでもメーカーごとに挙動が違ったりする● Android 4.4○ Chromium(Blink) ベースの WebView コンポーネント○ 挙動はかなりまともになった○ OSを更新しなければアップデートされない
WebView の歴史(2/3)● Android 5.0○ WebView コンポーネントが独立してアップデートできるようになった○ 外部ブラウザと分離されたため、履歴やブックマークの同期機能が無くなった○ 基本的に Android 5.0 以降であれば WebView を利用する上で大きな問題はない○ WebView を多用するアプリなら最低でも minSdkVersion 21 にしておいたほうが良い● Android 7.0○ Chrome アプリを利用してレンダリングされるようになった○ Chrome Beta や Dev 版をレンダリングに利用できるようになった
WebView の歴史(3/3)● Android 10○ 再び WebView コンポーネントアプリを利用するようになった○ WebView コンポーネントアプリにも Beta, Dev がそれぞれ存在する
WebView 実装を行う上での注意点● minSdkVersion● API レベルごとの機能差異● WebView のライフサイクル● WebView 起因の障害
minSdkVersion● できれば Android 6.0 (API 23) 以上○ WebView というよりも RuntimePermission の都合○ WebView 内で Permission を扱う処理がある場合、ある程度実装を揃えられる● 最低でも Android 5.0 (API 21) 以上○ すべての端末で WebView の更新が期待できる○ 問題があった場合は Chromium のバグトラッカーを見れば良いので分かり易い○ https://bugs.chromium.org/p/chromium/issues/list
WebView のライフサイクル管理● Fragment/Activity のライフサイクルに沿っていくつかのメソッドを呼び出す必要がある○ onPause(), onResume() など● バックグラウンドで動画再生が続いてしまうとリジェクトされる可能性がある
WebView 誕生編クックパッドの Android アプリは 2012年リリースリリース当初は完全な WebView アプリとして実装されていた(らしい)翌年ネイティブアプリとして再実装され、当初の WebView はCustomWebView として残った
WebView 成長編ネイティブアプリ後も引き続きコンテンツのいくつかは WebView 表示ネイティブ画面が増えたことで画面遷移やカスタムJavaScript実装が急増2018年時点でCustomWebView 841行WebViewFragment 1335 行WebView 関連コードはおよそ 3000行
WebView 増殖編(1)2018年 GooglePlay 定期購入機能の追加WebViewで表示するランディングページからGooglePlay決済を呼び出す必要があった「既存のWebViewは触りたくない」「別Activityで表示する決済機能だけのシンプルなWebViewを作ろう」GooglePlaySubscriptionWebViewActivity 爆誕
2019 年 ViewPager 内に表示するWebViewにGooglePlay 決済機能を追加「既存のWebViewは触りたくない」「決済機能だけのシンプルなWebView を作ろう」「決済ページ以外は通常WebViewに遷移させよう」GooglePlaySubscriptionWebViewContainerFragment 爆誕WebView 増殖編(2)
その後も成長と増殖を繰り返したWebView2021年8月時点で Activity 2つ、Fragment が 4つ存在WebView 関連コードの総量はおよそ5000行(ほぼ全てJava)WebView 2021・夏
見えてきた課題● カオスなカスタム JavaScript● 各 WebView 画面がサポートする機能がわからない● 全部 Activity/Fragment にベタ書きされていて辛い
カオスなカスタム JavaScript● 正しい実装なのかわからない 45 個のメソッド● それぞれの利用頻度や利用ページも一切分からない● 認証情報を一時的に WebViewFragment に持たせる危険な仕組み● JavaScript からほぼ直接 SharedPreference に読み書きさせる処理○ getStorage(key), setStorage(key, value), getStorageKeys() などなど● アプリを終了させる finishApplication() メソッド○ 実際には Activity.finish() を呼ぶ○ 別Activityでの WebView 表示では正しく動いていない
各WebView画面がサポートする機能がわからないインターフェイスの整備が中途半端なため、実装の有無を返せるメソッドと返せないメソッドがあるboolean で実装の有無を返せるboolean で実装の有無を返せない
全部 Activity/Fragment にベタ書きされていて辛い● 各インターフェイスが Activity/Fragment に直接実装されている○ 最初の WebViewFragment がその状態で、そこから全部コピペされている○ WebViewFragment の更新にコピペ先が追従しないので差分が生まれていく● ライフサイクル管理、ダイアログなどの共通処理が全てのWebView画面にほぼそのままコピペされている○ 300行くらいは全ての WebView で必要としている共通処理○ 一部は Kotlin 化されていたりして微妙〜に違う
2010 年代 WebView の限界最初の実装から8年維持できたのは本当にすごいでもさすがにもうメンテナンスできないWebView の機能を整理したい
2020年代の WebView10年戦える最高の WebView を作りたい● カスタムJavaScriptが理解可能● 他の画面と同じアーキテクチャで実装されている● 共通実装は一カ所にまとまっていてコピペしなくても利用できる● 利用箇所ごとにカスタマイズ可能
カオスなカスタム JavaScript の改善● JavaScriptメソッドにステージの概念を追加○ 利用可能 : 名前の通り利用可能○ 非推奨 : ネイティブ側の実装に何らかの問題があり廃止したい○ 廃止 : JSメソッドは存在するがネイティブ側の実装が削除済み○ ネイティブ処理呼び出しのための callback interface も分割● すべてのメソッドの呼び出しにログ追加● 一定期間呼び出しがないメソッドは降格● 未整理45の状態から利用可能 10、非推奨 2 まで削減できた
クックパッドアプリはVIPERアーキテクチャ● View : 画面処理○ ダイアログ表示、ライフサイクルの管理● Interactor : ビジネスロジック○ セッション引き継ぎ用トークンの取得処理など● Presenter : View とのやりとりや他のコンポーネントの処理呼び出し○ URLチェックやカスタムJavaScriptは Presenter だけ知っていれば良い状態が理想● Routing : 画面遷移○ ネイティブ画面への遷移はここに集約他の画面と同じアーキテクチャで実装されている
WebView の VIPER アーキテクチャFragmentViewPresenterInteractorRoutingWebView
WebView の VIPER アーキテクチャFragmentViewPresenterInteractorRoutingWebViewWebView は 内部にシステムから用意されているコールバックを実装するそれぞれのイベントと対応したPresenter のメソッドを呼び出す
WebView の VIPER アーキテクチャFragmentViewPresenterInteractorRoutingWebViewPresenter は呼び出されたメソッドに応じて他のコンポーネントの実装を呼び出す
WebView の VIPER アーキテクチャFragmentViewPresenterInteractorRoutingWebViewPresenter は呼び出されたメソッドに応じて Routing や Interactor の実装を呼び出す
WebView の VIPER アーキテクチャFragmentViewPresenterInteractorRoutingWebViewView はダイアログやページ読み込み中の Progress の管理を行っているWebView の操作が必要な場合は必ずView を経由する
WebView の VIPER アーキテクチャFragmentViewPresenterInteractorRoutingWebViewInteractor はセッション引き継ぎが必要なURLにアクセスした際の認証処理などを実装している
WebView の VIPER アーキテクチャFragmentViewPresenterInteractorRoutingWebViewRouting は主にネイティブ画面への遷移を定義している「このWebViewではこのネイティブ画面への遷移はサポートしない」という定義ができるインターフェイスになっている
WebView の VIPER アーキテクチャFragmentViewPresenterInteractorRoutingWebView個人的に VIPER が他のモバイルアプリアーキテクチャと比べて優れているところは Routing interfaceに画面遷移をすべて定義しているところだと思います。これは WebView のような画面遷移が複雑化しやすい画面では特に有効で、パラメータが異なる画面遷移や今回のような実装クラスによって挙動が大きく変わるような仕様もきちんと定義できるのが便利だと思っています。クックパッドアプリでは Routing クラスで画面遷移を処理するための便利な仕組みをいくつか用意していますが、時間が無いのでまた別の機会に話します。
共通実装がまとまっている共通実装が多いのでまとめたい● WebViewのライフサイクル管理● ファイル選択、ダイアログ表示などの基本UI● 特定ドメインのURLにアクセスした際のセッション引き継ぎ処理● どのWebViewでも利用する基本的な画面遷移○ 外部ブラウザや外部メーラーアプリへの遷移など
利用箇所ごとにカスタマイズ可能WebView 画面の増殖は避けられないInterface でカスタマイズ可能な箇所を定義してクックパッドアプリで画面によってカスタマイズしたい箇所● ログイン、ログアウトなど認証系の処理○ 認証用途で実装している WebView 以外では不要● カスタムJavaScript○ 一部の画面でのみ追加したい JavaScript メソッド● ネイティブ画面への遷移○ 別Activityで WebView を表示する場合はサポートしない、等
ExternalWebView Contractカスタマイズ可能な Contract の分離RoutingInteractorPresenterViewBaseWebView ContractRoutingInteractorPresenterView継承
ExternalWebView Contractカスタマイズ可能な Contract の分離RoutingInteractorPresenterViewBaseWebView ContractRoutingInteractorPresenterView継承WebView の表示箇所によるカスタマイズを許す処理のみ External Contract に定義する
ExternalWebView Contractカスタマイズ可能な Contract の分離RoutingInteractorPresenterViewBaseWebView ContractRoutingInteractorPresenterView継承すべての WebView で共通する処理はBase Contract に定義する
ExternalWebView Contractカスタマイズ可能な Contract の分離RoutingInteractorPresenterViewBaseWebView ContractRoutingInteractorPresenterView継承BaseContract のそれぞれのコンポーネントが 対応する ExternalContract を継承することで、 WebView や他のコンポーネントからは BaseContract だけ参照すれば良い状態になる
カスタマイズ可能な Contract の分離
カスタマイズ可能な Contract の分離ここで ExternalContract.View を継承して View を定義している
GooglePlaySubscriptionWebView VIPER Sceneカスタマイズ可能な WebView の実装BaseWebView VIPER SceneCustomWebViewBaseRoutingBaseInteractorBasePresenterBaseViewWebView VIPER SceneRoutingInteractorPresenterView継承
GooglePlaySubscriptionWebView VIPER Sceneカスタマイズ可能な WebView の実装BaseWebView VIPER SceneCustomWebViewBaseRoutingBaseInteractorBasePresenterBaseViewWebView VIPER SceneRoutingInteractorPresenterView継承共通実装BaseWebView VIPER シーンのクラスは全て abstract classExternalContract 以外 のメソッドをすべて final で実装している
GooglePlaySubscriptionWebView VIPER Sceneカスタマイズ可能な WebView の実装BaseWebView VIPER SceneCustomWebViewBaseRoutingBaseInteractorBasePresenterBaseViewWebView VIPER SceneRoutingInteractorPresenterView継承各画面固有の WebView 実装(ExternalContract)各画面固有の WebView 以外の実装各 WebView 実装ではBaseWebView のコンポーネントをそれぞれ継承し、足りないメソッドを実装する
実際のカスタマイズ例
実際のカスタマイズ例ここで BaseWebViewRouting を継承しているPsLandingPageWebViewContract.Routing は ActionBar など WebView 以外の部分からの画面遷移をサポートするための定義が書かれた interface
実際のカスタマイズ例このWebViewではこの画面遷移は未実装なので、 Unimplemented を返している各画面遷移は BaseWebViewPresenter 内のメソッドで UnImplemented かどうか判定され、ネイティブ画面に遷移するかウェブページとして読み込むかが変わる
GooglePlaySubscriptionWebView VIPER Sceneカスタマイズ可能な WebView の実装BaseWebView VIPER SceneCustomWebViewBaseRoutingBaseInteractorBasePresenterBaseViewWebView VIPER SceneRoutingInteractorPresenterView継承最高の WebView できた
GooglePlaySubscriptionWebView VIPER Sceneクックパッドアプリの現状BaseWebView VIPER SceneCustomWebViewBaseRoutingBaseInteractorBasePresenterBaseViewWebView VIPER SceneRoutingInteractorPresenterView継承
GooglePlaySubscriptionWebView VIPER Sceneクックパッドアプリの現状BaseWebView VIPER SceneCustomWebViewBaseRoutingBaseInteractorBasePresenterBaseViewWebView VIPER SceneRoutingInteractorPresenterView継承WebViewがでかい。でかすぎるURL判定ロジックなど Presenter が持つべき実装がまだ WebView に残っているせいその分 Presenter と View が薄くなっていて明確に分離できていない
GooglePlaySubscriptionWebView VIPER Sceneクックパッドアプリの現状BaseWebView VIPER SceneCustomWebViewBaseRoutingBaseInteractorBasePresenterBaseViewWebView VIPER SceneRoutingInteractorPresenterView継承特定の WebView 画面の実装だけコード量が異 常に多い新しいWebViewアーキテクチャでは VIPER シーンを分割すべきところを無理矢理ひとつのWebViewFragment に押し込めていた名残
GooglePlaySubscriptionWebView VIPER Sceneクックパッドアプリの現状BaseWebView VIPER SceneCustomWebViewBaseRoutingBaseInteractorBasePresenterBaseViewWebView VIPER SceneRoutingInteractorPresenterView継承ExternalWebView Contract を透過的に扱うために BaseWebView Contract が ExternalWebViewContract を継承し、さらに abstract class として実装する仕組みなっているが、 この構造だとExternalWebView Contract の分離が十分ではない理想としては WebView 実装者がExternalWebView Contract だけ意識して実装できるようにしたいが、うまくアーキテクチャに落とし込めていない
GooglePlaySubscriptionWebView VIPER Sceneクックパッドアプリの現状BaseWebView VIPER SceneCustomWebViewBaseRoutingBaseInteractorBasePresenterBaseViewWebView VIPER SceneRoutingInteractorPresenterView継承最高の WebViewまだできてない!!!
まとめ引き続き 10年戦える最高の WebView を目指して改善していきますクックパッドでは WebView 大好きな仲間を募集中WebView が好きじゃない仲間でも扱える WebView を用意しているのでWebView が好きじゃない仲間も募集中です!!!
WebView これだけはやっておけリスト● WebViewでサポートする挙動を決めておく○ ダイアログ、ファイル操作、音声入力などなど● とにかくネイティブ画面への遷移を可視化できるようにする● カスタムJavaScriptはできるだけ使わない方針にする● カスタムJavaScriptを使う場合はメンテナンスできる仕組みを入れる○ 全メソッドの発火ログ記録○ 非推奨メソッド可視化の仕組みおまけ
2020年代 の WebViewご静聴ありがとうございました。