もうWebViewに飽きた？そんなときはGeckoViewを使ってみよう。

## まえおき

WebView使うと、ブラウザっぽいアプリはすぐに作れる！
とおもいきや、実際つくってみると結構ボイラープレートコードが多いんですよね。
（ http://arianrod-berry.hatenadiary.com/entry/2018/01/29/233841 あたりにいい感じにまとめられています）

なんかいい感じのWebView-alternativeはないかなーと探していたところ、[GeckoView](https://wiki.mozilla.org/Mobile/GeckoView)というものを見つけました。Firefox Focusなどで使われているGeckoベースのWebViewてきなものみたいです。

Qiitaで現時点では「使ってみた」レベルの記事もなかったので、とりあえず使ってみた感じを書いておきます。

## セットアップ: build.gradleへの追記

implementationを１行ペロッと書くだけ・・・ではありません。

[オフィシャルのWiki](https://wiki.mozilla.org/Mobile/GeckoView#Configure_Gradle)に載っていますが、

```
android {

    ...

    flavorDimensions "abi"

    productFlavors {
        x86     { dimension "abi" }
        x86_64  { dimension "abi" }
        arm     { dimension "abi" }
        aarch64 { dimension "abi" }
    }
}

repositories {
    maven { url "https://maven.mozilla.org/maven2/" }
}

ext {
    geckoviewChannel = "beta"
    geckoviewVersion = "64.0.20181105164654"
}

dependencies {

    ....

    x86Implementation     "org.mozilla.geckoview:geckoview-${geckoviewChannel}-x86:${geckoviewVersion}"
    x86_64Implementation  "org.mozilla.geckoview:geckoview-${geckoviewChannel}-x86_64:${geckoviewVersion}"
    armImplementation     "org.mozilla.geckoview:geckoview-${geckoviewChannel}-armeabi-v7a:${geckoviewVersion}"
    aarch64Implementation "org.mozilla.geckoview:geckoview-${geckoviewChannel}-arm64-v8a:${geckoviewVersion}"
}
```

こんな感じでいろいろ書き足す必要があります。

おためしでやったときはこんな感じ→https://github.com/YusukeIwaki/GeckoViewPlayground/commit/a09dca7f4e87403f2387cfd2b05217361287c407

## GeckoViewを使う・・前に

GeckoViewを使うときに出てくるクラスをさらっとだけ書いておきます。

### GeckoSession

ブラウザの内部状態をコントロールするもの。

WebViewだと、　`loadUrl()` とか `goBack()` は直接WebViewに生えているAPIを叩いていたけど、GeckoViewではGeckoSessionを通じて行う。

### GeckoRuntime

GeckoSession（内部状態）とGeckoView（ビュー）の橋渡してきなもの？

### ContentDelegate, NavigationDelegate, ProgressDelegate, ...

WebViewだとWebViewClientとかWebChromeClientが各種コールバックを使って（割とごちゃまぜな感じで）実装しないといけなかったやつ。GeckoViewだと役割ごとにDelegateを実装するらしい。

## GeckoViewを使う。

レイアウトファイルの適当なところにGeckoViewを置くのはWebViewと同じなので省略。

セットアップ処理はWebViewだと、

* setJavascriptEnabledでJS有効化したり
* WebSettingsでクッキーを有効にしてみたり
* WebViewClientを実装したクラスをセットして・・・
* WebChromeClientを実装したクラスをセットして・・・

みたいなのがありましたが、GeckoViewだと、概ねこんな感じになります

```
class BrowserActivity : AppCompatActivity() {

    private lateinit var geckoView: GeckoView
    private lateinit var geckoSession: GeckoSession

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        geckoView = findViewById(R.id.geckoview)

         ...

        // セッションの初期化
        geckoSession = GeckoSession()
        geckoSession.open(GeckoRuntime.getDefault(this))
        geckoview.session = geckoSession

        // 各種Delegateのセットアップ
        geckoSession.contentDelegate = MyContentDelegate(...)
        geckoSession.navigationDelegate = MyNavigationDelegate(...)
        geckoSession.progressDelegate = MyProgressDelegate(...)

        // とりあえずホームページの表示
        geckoSession.loadUri("https://www.yahoo.co.jp/")
    }
```

## GeckoView こんなときどうする？集

### APKがインストールできない？

![image.png](https://qiita-image-store.s3.amazonaws.com/0/74571/5e7baa5a-0cb9-5593-e712-c72e23874fa8.png)

ビルドバリアントが正しく設定されてない可能性大です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/74571/f7186cc0-e6a1-4643-73b0-eb3f4b97ddb7.png)

### アプリを終了して２回目の起動時にクラッシュする？

オフィシャルWikiのほうだと

<img src="https://qiita-image-store.s3.amazonaws.com/0/74571/c92e7991-cfa2-2506-8610-048e501fdaad.png" width=320 />

のように `GeckoRuntime.create(this)` するように書いてありますが、これだと２回目以降のアプリ起動時にクラッシュします。

```
11-12 18:01:17.689 E/AndroidRuntime( 4534): Caused by: java.lang.IllegalStateException: Failed to initialize GeckoRuntime
11-12 18:01:17.689 E/AndroidRuntime( 4534):     at org.mozilla.geckoview.GeckoRuntime.create(GeckoRuntime.java:270)
11-12 18:01:17.689 E/AndroidRuntime( 4534):     at org.mozilla.geckoview.GeckoRuntime.create(GeckoRuntime.java:244)
```

雑にやるなら `GeckoRuntime.getDefault(this)` を使う。真面目にやるなら、[サンプルコード](https://searchfox.org/mozilla-central/source/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java#134)にあるようにシングルトンな実装にする必要があるみたいです。

### リンクタップしても反応しないものがある？

GeckoViewはマルチセッションが前提らしく、`target="_blank"` みたいな属性付きのリンクは自然には辿れなくなっているみたいです。
新しいタブで開くようなリンクを踏んだときは、NavigationDelegateの

```
    override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? {

    }
```

が呼ばれます。

雑にやるなら、

```
    override fun onNewSession(session: GeckoSession, uri: String): GeckoResult<GeckoSession>? {
        session.loadUri(uri)
        return null
    }
```

のように、とりあえずloadUriする。真面目にやるなら[サンプルコード](https://searchfox.org/mozilla-central/source/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java#653)にあるように、新しくセッションを作ってGeckoResultでラップして返す必要があるようです。


### 「バックキーで戻る」をどうやって実装する？

WebViewと違って、GeckoViewは `canGoBack()` `canGoForward()` みたいなメソッドが生えていません。

NavigationDelegateの

```
    override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {

    }

    override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) {

    }
```

を使います。

GeckoSessionの内部状態の考慮は、ライフサイクル依存したくないのでViewModelと併用すると良いでしょう。


```
class BrowserViewmodel: ViewModel() {
  val canGoBack = MutableLiveData<Boolean>()
  val canGoForward() = MutableLiveData<Boolean>()
}

class MyNavigationDelegate(private val viewModel: BrowserViewmodel): GeckoSession.NavigationDelegate {

   ...

    override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
        viewModel.canGoBack.postValue(canGoBack)
    }

    override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) {
        viewModel.canGoForward.postValue(canGoForward)
    }

   ...

}

class BrowserActivity: ... {

    private lateinit var geckoView: GeckoView
    private lateinit var viewModel: BrowserViewModel
    private lateinit var geckoSession: GeckoSession

    override fun onCreate(...) {
       ...

       viewModel = ViewModelProviders.of(this).get(BrowserActivity::class.java)

        // セッションの初期化
         ...

        // 各種Delegateのセットアップ
        geckoSession.navigationDelegate = MyNavigationDelegate(viewModel) 

         ...      
    }

    override fun onBackPressed() {
        if (viewModel.canGoBack.value == true) {
            geckoSession.goBack()
        } else {
            super.onBackPressed()
        }
    }
}
```

### プログレス表示はどうやって実装する？

ProgressDelegateの

```
    override fun onPageStart(session: GeckoSession, url: String) {

    }

    override fun onPageStop(session: GeckoSession, success: Boolean) {

    }

    override fun onProgressChange(session: GeckoSession, progress: Int) {

    }
```

を使います。

* onPageStart→プログレス表示を開始
* onProgressChange→値を更新
* onPageStop→プログレスを消す

これもライフサイクル依存にならないように、ViewModelを介してやると楽だと思います。

```
class BrowserViewModel: ViewModel() {
  ...

  val progress = MutableLiveData<Int>()
  val loading = MutableLiveData<Boolean>()
}


class MyProgressDelegate(private val viewModel: BrowserViewModel) : GeckoSession.ProgressDelegate {
    override fun onPageStart(session: GeckoSession, url: String) {
        viewModel.loading.postValue(true)
    }

    override fun onPageStop(session: GeckoSession, success: Boolean) {
        viewModel.loading.postValue(false)
    }

    override fun onProgressChange(session: GeckoSession, progress: Int) {
        viewModel.progress.postValue(progress)
    }

   ...

}
```

あとは、レイアウトでデータバインディングを使って

```
    <data>
        <variable
            name="viewModel"
            type="com.example.hogehoge.BrowserViewModel" />
        <import type="android.view.View" />
    </data>



    <ProgressBar
        android:id="@+id/browser_progress"
        style="@style/Widget.AppCompat.ProgressBar.Horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="100"
        android:progress="@{viewModel.progress}"
        android:visibility="@{viewModel.loading ? View.VISIBLE : View.GONE}" />
```

こんな感じでOK。

### 特定のURLを踏んだときの処理を横取りできる？

NavigationDelegateのonLoadRequestあたりをごにょればできそう。私の手元では未検証・・・

### エラー画面はカスタマイズできる？

404とかの画面。
[サンプルコード](https://searchfox.org/mozilla-central/source/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java#789)を見る限りだと、NavigationDelegateのonLoadErrorでHTML文字列を返せばできそう。私の手元では未検証・・・

### GeckoViewのwebページのキャプチャはどうやって撮るの・・・？

まだ調べてない・・・
 

 

## まとめ

なんとなくalt-WebViewをもとめてGeckoViewを使ってみたら、結構いい感じにコードが書けたので、とりあえず「まずは使ってみた」レベルで共有してみました。

サンプルコードが
https://searchfox.org/mozilla-central/source/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
これ１ファイルでかなり参考になるので、「とりあえずGeckoViewを使ってみたい！」ってからはここから見るとよさそうです。

私が試したサンプルも一応置いておきます↓
https://github.com/YusukeIwaki/GeckoViewPlayground