インストール不要 Android Instant Apps 対応

 ついに！ 神より授かりし [Android Instant Apps](https://developer.android.com/topic/instant-apps/index.html) を実装できたので実装方法を紹介します。

 [2016年のIOで発表](https://www.youtube.com/watch?v=cosqlfqrpFA)されて、その後試験的に一部の開発者だけがベータ実装できましたが、2017年夏、ついに[一般開発者にも公開](https://developers-jp.googleblog.com/2017/06/android-instant-apps-is-open-to-all.html)され、実装できるようになった待望の機能です！

 インスタントアプリ対応をしようとしてる皆様の参考になれば幸いです。

# Android Instant Apps とは

![android-instant-apps.png](https://qiita-image-store.s3.amazonaws.com/0/70150/7a9d8bf5-7792-68d1-8e06-0e2c9e37bb99.png)

　まず、Instant Appsとは？という人のために簡単に説明しますね。

　インスタントアプリは、簡単に言うと、「**インストールせず、すぐに使い始められるAndroidアプリ**」です。Webを開く感覚です。

　アプリを使い始める際、普通は

```:普通のアプリ利用開始手順
GooglePlay
　↓
ストア掲載情報
　↓
インストールボタン
　↓
ダンロード＆インストール
　↓
アプリによってはユーザー登録画面
　↓
利用開始
```

という流れですが、それを

```:インスタントアプリ利用開始手順
URLをタップ
↓
利用開始
```
って感じに短縮できる機能です
短縮しすぎぃー！笑

ストアすら省略してインストールも飛ばし、登録もスキップしてアプリを使い始められます。
ほぼWebを見るかのように実行されますが、でもこれはネイティブアプリです。デモ動画が流れるとかではなく実際に触ることができる正真正銘のネイティブアプリです。

　そして、インスタントアプリの最終目的はアプリ本体をインストールしてもらうことです。
インスタントアプリでアプリをお試し頂いて、使いたいアプリだな！って思ってもらったらインスタントアプリ内から本体アプリのインストールに誘導します。

　ダンロード画面や登録画面で躊躇してインストールせず離脱していったユーザー層にインストールしてもらうチャンスを得られます！

どんな効果がある？ 気になる方は 
[Developer stories](https://developer.android.com/stories/instant-apps.html) 

 IO2017での紹介はこちら 
[Introduction to Android Instant Apps (Google I/O '17) in Youtube](https://www.youtube.com/watch?v=oispNrpGnIY)

# 実装概要
インスタントアプリと本体のアプリは同じソースコードを使用します。
インスタントアプリだからといって、別のプロジェクトを作るものではないです。既存のアプリがある場合はそのソースコードをインスタントアプリに対応できるように修正していきます。

実装のポイントは大きく分けて３つです。

1. アプリの機能をモジュール分割する
2. アプリのサイズを4MB以下にする
3. AppLinksに対応する

１と２は関連していて、２を実現するために１を頑張らないといけない感じです。 アプリの規模によって違いますが、4MB以下にすることが個人的に一番の難関です。ガチ難関です(;´Д｀)

　インスタントアプリはアプリサイズを極限にまで小さくしてダンロードが無いように見せています。そのために必要な機能だけを切り出してその機能セットだけをダンロードしてアプリを実行します。

　実装としては、特定のURLぞれぞれに応する画面（機能）を作っていく形になります。その機能単位でコンパイルします。

[Android Developers Blog](https://android-developers.googleblog.com/2017/08/android-instant-apps-best-practices-for.html) さんからイメージお借りしますm(_ _)m

例えば、http://goo.gle/1 というURLに対してAcitivty1が開くように実装します。このひとかたまりをフィーチャーという名前で呼称していて、このフィーチャーごとにコンパイルされます。

![url-based-feature.png](https://qiita-image-store.s3.amazonaws.com/0/70150/6383bf46-0cee-3e90-14ac-2eeff8e1d44c.png)

このフィーチャーひとつひとつがAndroid Studioで見るときの「モジュール」で、この構成にするためにモジュール分割が必要になります。

このフィーチャーひとつひとつのコンパイルの成果物(apk)を4MBにしていきます。

URLに対応するフィーチャー（画面）の起動にAppLinksの実装もします。

# 開発環境
Android Studio 3.0 以上が必要です。
フィーチャーを作る際にAndroid Studio 3.0 で追加された`com.android.feature`とインスタントアプリコンパイル用のモジュールに`com.android.instantapp`を使います

```groovy:feature.gradle-com.android.feature
apply plugin: 'com.android.feature'
```
```groovy:instant.gradle-com.android.instantapp
apply plugin: 'com.android.instantapp'
```

そして私の実装時点（2017年末）では、まだAndroid Studio 3.0 Beta で、3.0正規版はリリースされてませんでした。Betaだけあってまだ奇怪な不具合をおきていてコンパイルできる環境がシビアです。バージョンが更新される度に動かなくなったりしてます。私のプロジェクト構成で動かすことできた環境は以下です。動かなくて困ってる方はご参考までにゞ

- Android Studio 3.1 Canary 5
- Android Studio 3.0 Beta7

現時点(2018年１月)最新のAndroid Studio 3.0.1 製品版、もしくはAndroid Studio 3.1 Canary 8では動きません悲しみ(;´Д｀)
Android Studioでデバッグ実行時になぜか、Default Activity not foundが発生して実行できません。Default Activity not foundが発生しないバージョンでもリリースコンパイルした際にPreLolipopでMultiDexに失敗していて実行時にNoClassDefFoundErrorでクラッシュするので注意です。

[バージョン別ダンロードリンク](https://developer.android.com/studio/archive.html)

余談ですがみなさんもこうなってますか 笑
![many-as-version.png](https://qiita-image-store.s3.amazonaws.com/0/70150/f1373e61-1bac-389b-55ed-b6dba991be2c.png)

# アプリの機能をモジュール分割する
 まずはプロジェクトの構成を変更します。

- アプリモジュール（app）

今までは、おそらく”app”という名前の`apply plugin: 'com.android.application'`を適用したモジュール１つで構成していたと思いますが（アプリによってはライブラリモジュールもありますね）、これをマルチモジュール構成にします。

- アプリモジュール（app）
- インスタントアプリモジュール(instant)
- ベースフィーチャーモジュール(base)
- フィーチャー1モジュール(feature1)
- フィーチャー2モジュール(feature2)

フィーチャーが２つの場合は、アプリモジュール、インスタントアプリモジュール、ベースフィーチャーモジュール、フィーチャー1モジュール、フィーチャー2モジュールといった形になります。

フィーチャーが２つある場合の依存関係のイメージです。

![aia-features-multi.png](https://qiita-image-store.s3.amazonaws.com/0/70150/49656fa1-74d8-4577-d382-34c74daedff0.png)

フィーチャーモジュールは必ず、ベースフィーチャーという共通機能な部分を含む必要があります。

アプリモジュールとインスタントアプリモジュールがフィーチャーをとりまとめます。

もしフィーチャーが単一の場合は全てベースフィーチャーに含めて、ベースフィーチャーのみでもいけます！

![aia-features-single.png](https://qiita-image-store.s3.amazonaws.com/0/70150/1673b1f3-bf3d-3289-860b-ffa93146fec7.png)

## Base Feature Module

ベースフィーチャーです。
baseというモジュール名にするのが一般的だと思います。

各フィーチャーで共通するプログラムやリソースを配置します。

コードのほとんどは、このモジュールに書くことになります。既存のアプリがある場合はそのコードを一旦baseモジュールに移動してから始めるとやりやすいと思います。

このモジュールはプロジェクト全体で１つのみです。フィーチャーはベースフィーチャーを必ず含む必要があります。

- `apply plugin: 'com.android.feature'`
- `baseFeature true`でベースフィーチャーであることを宣言
- dependencies.applicationに本体アプリのモジュールを指定
- dependencies.featureでこのベースフィーチャーを含ませるフィーチャーモジュールを指定

```groovy:base.gradle
apply plugin: 'com.android.feature'

android {
    compileSdkVersion rootProject.compileSdk
    buildToolsVersion rootProject.buildTools

    defaultConfig {
        minSdkVersion rootProject.minSdk
        targetSdkVersion rootProject.compileSdk
        versionCode rootProject.versionCode
        versionName rootProject.versionName
    }

    buildTypes {
    }

    baseFeature true
}

dependencies {
    application project(":installed")
    feature project(':feature1')
    feature project(':feature2')
    api "com.android.support:appcompat-v7:$supportLib"
}
```

## Feature Module

フィーチャーモジュールです。先程のbase.gradleで指定してたfeature1やfeature2のモジュールです。

ベースフィーチャーに共通のプログラムやリソースを含めて、各フィーチャーには各フィーチャーでしか使用しないプログラムやリソースを含めます。

プロジェクト内で複数のフィーチャーモジュールを配置できます。フィーチャーはベースフィーチャーを必ず含む必要があります。

- `apply plugin: 'com.android.feature'`
- dependencies.implementationでベースフィーチャーを含ませます

```groovy:feature1.gradle
apply plugin: 'com.android.feature'

android {
    compileSdkVersion rootProject.compileSdk
    buildToolsVersion rootProject.buildTools

    defaultConfig {
        minSdkVersion rootProject.minSdk
        targetSdkVersion rootProject.compileSdk
        versionCode rootProject.versionCode
        versionName rootProject.versionName
    }

    buildTypes {
    }
}

dependencies {
    implementation project(':base')
}
```

## Instant App Module 

インスタントアプリモジュールです。インスタントアプリをコンパイルするときに使います
このモジュールはほぼ皮だけです。gradleのファイルのみで、javaなどのソースコードもありません。どのフィーチャーをコンパイルするのかを依存関係に入れて取りまとめるモジュールです。

名前はinstantが一般的です。

- `apply plugin: 'com.android.instantapp'`
- dependencies.implementationでインスタントアプリで使用するフィーチャーを指定

```groovy:instant.gradle
apply plugin: 'com.android.instantapp'

android {
    compileSdkVersion rootProject.compileSdk
    buildToolsVersion rootProject.buildTools

    defaultConfig {
        minSdkVersion rootProject.minSdkInstant
        targetSdkVersion rootProject.compileSdk
        versionCode rootProject.versionCode
        versionName rootProject.versionName
    }
    buildTypes {
    }
}

dependencies {
    implementation project(':feature1')
    implementation project(':feature2')
    implementation project(':base')
}
```

## App Module 

インストール用の本体のアプリモジュールです。今までのアプリはこのモジュールでしたね。フィーチャーを依存関係に指定して本体アプリで使用するモジュールをとりまとめます。

名前はappでも良いですが分かりやすくinstalledとするのが一般的ですかね。

このモジュールにjavaソースコードを置くこともできますが、マルチモジュール構成にしたのでここには置かず、baseフィーチャーなどに置くべきです。manifestファイルは置きます。

feature3のように、インスタントアプリでは使用しないが本体アプリでは使用するフィーチャーも可能です。

- `apply plugin: 'com.android.application'`
- dependencies.implementationに本体アプリで使用するフィーチャーを指定

```groovy:installed.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion rootProject.compileSdk
    buildToolsVersion rootProject.buildTools

    defaultConfig {
        applicationId "com.example"
        minSdkVersion rootProject.minSdk
        targetSdkVersion rootProject.compileSdk
        versionCode rootProject.versionCode
        versionName rootProject.versionName
    }

    buildTypes {
    }
}

dependencies {
    implementation project(":feature1")
    implementation project(":feature2")
    implementation project(":feature3")
}
```


# アプリのサイズを4MB以下にする

フィーチャーとベースフィーチャー合わせて4MB以下にします。フィーチャーが複数の場合はそれぞれが4MB以下で大丈夫です。

OKな例

- ベースフィーチャー (3.0 MB)
- フィーチャー1 (0.9 MB)
- フィーチャー2 (0.8 MB)

フィーチャー１が3.9MB、フィーチャー２が3.8MBでクリアです

NGな例

- ベースフィーチャー (3.0 MB)
- フィーチャー1 (0.9 MB)
- フィーチャー2 (1.1 MB)

フィーチャー１が3.9MBでクリアしてますが、フィーチャー２が4.1MBなのでリリースできません。

## アプリサイズの確認

まず何が容量を圧迫してるか確認すると良きです。

コンパイルしたapkもじくはzipファイルをAndroid Studioにドラッグ＆ドロップすると、Apk Analyzerを使用して内容を見ることができます。

![apk-analyzer-w256.png](https://qiita-image-store.s3.amazonaws.com/0/70150/2975fda5-5a3f-f6f1-412e-2f5c95af234a.png)


## サイズ削減のポイント

サイズ削減としてやれることで大きなものは以下です

1. モジュール分割、ライブラリモジュール化
2. ProGuardでminify
3. ライブラリの見直し
4. 大きな画像などをアプリ内ではなくサーバーに置く
5. 画像のサイズ縮小、圧縮率調整、WebP化
6. 言語を絞る
7. 使っていないリソースの削除

## モジュール分割、ライブラリ化でのサイズ削減

　まず一番大きなところです！

　アプリにもよりますが、私の場合、10MB〜20MBくらい減らせました。
　インストール用のアプリにしか含まない機能でものすごく大きなものがあったので、モジュール分割してインスタントアプリに含ませないようにできたからです。

 モジュール分割、共通機能のライブラリ化でフィーチャーごとのサイズを減らせます。
 classファイルはもちろん、drawableやlayoutなどフィーチャー固有のものは、しっかりと各フィーチャーに配置します。

### ライブラリ化
URLで起動する画面はないけど、切り出せるものはライブラリモジュールとして切り出します。

例えば、ユーティリティクラスやGoogle Analytics(以下GA)などをラップしたロギング用のクラスなどです。 ライブラリモジュールとして切り出して必要なものだけフィーチャーで含ませます。

本体アプリではGAを使用するが、インスタントアプリでは使用しないといったことが可能になります。

GAを切り出す例として、baseフィーチャーにGaManagerというabstractなクラスを用意して、ライブラリモジュール側に実装クラスGaManagerImplを用意します。GaManagerからリフレクションで実装クラスを呼び出します。

baseフィーチャー側で、dependenciesでライブラリモジュールを含ませれば動きますし、含ませなければ実装クラスを取得できないので何もしません。

base モジュール

```groovy
dependencies {
    api project(':ga-library')
}
```

```java
GaManager gam = GaManager.getInstanceNullable();
if (gam != null) {
    gam.trackEvent(c, a, l);
}
```

```java
abstract class GaManager {

    protected static final String TAG = "GaManager";
    private static final String MANAGER_IMPL_CLASS_NAME = "com.example.manager.ga.GaManagerImpl";
    private static GaManager sManager = null;

    @Nullable
    public static synchronized GaManager getInstanceNullable(@NonNull Context context) {
        if (sManager == null) {
            try {
                sManager = instantiateManagerImpl();
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }
        return sManager;
    }

    private static GaManager instantiateManagerImpl() throws Exception {
        Class clazz = Class.forName(MANAGER_IMPL_CLASS_NAME);
        return (GaManager) clazz.getConstructor()
                .newInstance();
    }

    @RequiresPermission(allOf = {"android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE"})
    public abstract void trackEvent(String category, String action, String label);

}
```

libraryモジュール

```groovy
apply plugin: 'com.android.library'
```

```java
@GaModuleImplClass
public class GaManagerImpl extends GaManager {

    @RequiresPermission(allOf = {"android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE"})
    @Override
    public void trackEvent(String category, String action, String label) {

        // Get tracker.
        Tracker t = getTracker(TrackerName.APP_TRACKER);

        HitBuilders.EventBuilder builder = new HitBuilders.EventBuilder()
                .setCategory(category)
                .setAction(action)
                .setLabel(label);

        // Build and send an Event.
        if (isTrackEnabled()) {
            t.send(builder.build());
        }
    }
}
```

### Fragment の切り出し
フィーチャーがURLベースなのでActivity単位でしか切り出せないのかと思ってしまいそうですが、ライブラリとして切り出せば同じ要領ですね。

baseモジュール

```groovy
dependencies {
    api project(':login')
}
```

```java
if (!isInstantApp) {
    BaseRegisterUserFragment f = BaseRegisterUserFragment.newInstance()
    getSupportFragmentManager()
        .beginTransaction()
        .replace(R.id.container, f, FRAGMENT_TAG)
        .commit()
}
```

```java
public abstract class BaseRegisterUserFragment extends BaseFragment {

    private static final String FRAGMENT_IMPL_CLASS_NAME = "com.example.ui.RegisterUserFragment";

    @NonNull
    private static Class<? extends BaseRegisterUserFragment> getImplementedFragmentClass() throws ClassNotFoundException {
        return (Class<? extends BaseRegisterUserFragment>) Class.forName(FRAGMENT_IMPL_CLASS_NAME);
    }

    @NonNull
    public static BaseRegisterUserFragment newInstanceImplemented(Mode mode) {
        BaseRegisterUserFragment f;
        try {
            f = instantiateImpl();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
        Bundle args = new Bundle();
        args.putSerializable("mode", mode);
        f.setArguments(args);
        return f;
    }

    private static BaseRegisterUserFragment instantiateImpl() throws Exception {
        Class clazz = getImplementedFragmentClass();
        return (BaseRegisterUserFragment) clazz.getConstructor()
                .newInstance();
    }
}
```

loginモジュール

```java
public class RegisterUserFragment extends BaseRegisterUserFragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 処理
    }
}
```


## ProGuardでminify

- minifyEnabled true でminify
- proguardFilesでProGuardの設定ファイル読み込み
- featureやlibraryだとproguardFilesが効かないのでconsumerProguardFilesを使う
- さらに最適化したい場合はproguard-android-optimize.txtを使う
- shrinkResources true で使っていないリソースを削除できるがフィーチャーでは使えないのでfalse

```groovy:base.gradle-minify

android {
    baseFeature true
    defaultConfig {
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        //proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        consumerProguardFiles 'proguard-rules.pro'
    }
    buildTypes {
        release {
            //shrinkResources true
            minifyEnabled true
            zipAlignEnabled true
            debuggable false
            signingConfig signingConfigs.release
        }
        debug {
            //shrinkResources false
            minifyEnabled true
            // メソッドは削除するがリネームなどはしない
            //useProguard false
            zipAlignEnabled true
            debuggable true
            signingConfig signingConfigs.debug
        }
    }
}
```

### shrinkResources true について
使っていないリソースを削除できるようだが、フィーチャーはライブラリという立ち位置なので使えない模様。リソース削除は自力でやる必要あり。（Error:Resource shrinker cannot be used for libraries.）

https://stackoverflow.com/questions/46219987/shrinkresources-true-cant-be-used-on-instant-apps-feature


## ライブラリの見直し

![review-native-library.png](https://qiita-image-store.s3.amazonaws.com/0/70150/184df7e8-8b92-8175-b508-490cbd34b539.png)

ApkAnalyzerを使用して、classes.dexの中身を見ます。
大きさ的に入らないと思ったライブラリは取り外すことを検討です。

ProGurdで小さくなるのでそれも考慮します。
Jacksonなんかは、もともと1.4MBあってキツイなー！って思ってましたが、適用すると800MBになってギリギリ入れられました。

ライブラリの依存関係にも注目です。
ASL (Andoid Support Library)の例では、com.android.support:appcompat-v7ではなく、欲しい機能はcom.android.support:designの中にあるかもしれません。

PlayServiceも欲しい機能のみ使えるようになっています。
com.google.android.gms:play-servicesではなく、com.google.android.gms:play-services-baseだったり。

## 大きな画像などをアプリ内ではなくサーバーに置く

画像に限らないのですが、drawableやraw、assetsの中に大きなファイルを入れてる場合は、サーバーから後でダウンロードできないか検討します。

アプリ内の説明ダイアログに大きなバナー画像を使っている場合は、サーバーに置いてURL指定で読み込むようにします。

assetsからフォントやライブラリを展開して使用してる場合は、これもサーバーに置いて実行時にダウンロードさせるのもありです。

大きなファイルを丸々取り外せるので効果はめちゃくちゃ大です!!

## 画像のサイズ縮小、圧縮率調整、WebP化

- drawable内のJPEGなどの圧縮率調整（クオリティとのせめぎ合い）
- そもそも幅や高さも減らすことも検討（クオリティとのせめぎ合い）
- JPEGやPNGをWebP化してみる

### 圧縮率調整

JPEGの場合は圧縮率調整できるので見た目許容できるところまで削る必要があるかもしれないです。妥協点見つけて7割くらいにできました。

PNGの場合でも、線だけのシステムアイコン系なら16bitカラーなのを8bitや1bitカラーにするのも可能ですね。システムアイコンでも0.1kb〜2.0kbくらいあるので。

例えば、https://tinypng.com/ とかは色数を減らしてくれたりします。
色数を減らしてるので、劣化はしてるので注意ですが、人間には判断できない範囲で削っていかないとです。

### WebP化 について

Jpegやpngに比べて７割くらいのサイズにできます。

注意としては

- Android 4.0 以上で利用可能
- Android 4.3 未満では透過色が使えない
- 可逆式と非可逆の２つの形式がある
- 場合によっては元のファイルより大きくなる

Android 4.3 未満をサポートしてる場合は、透過色のある画像はWebPにできないですね。

場合によってはjpegやpngに負けることがあるので、ひとつひとつチェックするのが良き。

私の場合は、大きな画像のみ、非可逆圧縮WebPで圧縮して半分くらいのサイズにできたので満足です(人´∀｀)

変換方法の説明は[こちら](https://developer.android.com/studio/write/convert-webp.html)

![webp-convertqualitylow_w256.png](https://qiita-image-store.s3.amazonaws.com/0/70150/a5b5ee2d-e4a8-c223-2539-4fd8d9562dfe.png)

## 言語を絞る

なんだかんだ0.5MBくらい削れたかもです。結構大きいです。

自アプリがローカライズしてなくても、TwitterだったりFacebookだったり、ASLだったりが色んな言語のリソースを持ってくれてます。でもサイズ削減が足りない場合は仕方な削ります。日本向けなら日本語と英語のみ、みたいな感じで。

```groovy
android {
    defaultConfig {
        resConfigs "ja", "en"
    }
}
```

## 使っていないリソースの削除
- shrinkResourcesが使えないので自力で使っていないリソースを削除
- drawableの36dp、48dpとかwhite、blackとか使ってなかったり共通化できるものは削除
- Remove Unused Resources ツールを使うと少し楽に探せる

### Remove Unused Resources ツール
Mac Android Studio では、[Refactor] -> [Remove Unused Resources] にあります。Previewでまず確認するとよい。別フィーチャーで使ってるものを上手く認識してくれない時があるので、ここでなんとなく確認してからプロジェクトのビューワーから１個１個注意しながら削除した方が良き。


# App Links に対応する

サイズ削減の話は終わりまして、次は App Linksのお話です。
[App Links](https://developer.android.com/training/app-links/index.html) は、Android 6.0 から導入された機能です。

URLからアプリの画面を開く機能のことをディープリンクといいますが、ディープリンクのURLをインテントのアプリ選択画面を出すこと無く自アプリを優先的に開くようにするための機能がAppLinksです。

また、前提としてディープリンクの実装がされていない場合は、まずディープリンクの実装が必要です。

例としてhttps://www.example.com/ を開くアプリを想定します。

https://www.example.com/ に反応する画面をマニフェストに定義します。
AppLinksで自アプリ優先的に開くようにするには、URLのサーバーを認証する必要があります。
その設定が`android:autoVerify="true"`です。

```xml
<activity
    android:name="com.example.ui.MainActivity">

    <intent-filter
        android:autoVerify="true">

        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data android:scheme="https" />
        <data android:scheme="http" />
        <data android:host="www.example.com" />

        <data android:pathPattern="/" />

  </intent-filter>

</activity>

```

autoVerifyを指定したインテントフィルタではOSが自動的にURLのサーバーの/.well-known/assetlinks.jsonにアクセスしてアプリがこのホストの正規のアプリかを検証します。
この例では https://www.example.com/.well-known/assetlinks.json になります。
この場所に認証用のJSONファイルを置きます。

```json
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "example.com",
    "sha256_cert_fingerprints":
    ["99:99:8B:83:0A:7B:54:FF:99:D1:A0:F5:00:BF:A2:99:74:91:95:FC:36:9F:CE:EB:BF:CC:D0:CA:23:36:99:99"]
  }
}]
```

また認証用のJSONファイルと対になる内容をマニフェストにmeta-dataで指定します。

```xml
<manifest>
    <application>
        <!-- App Links -->
        <meta-data
            android:name="asset_statements"
            android:resource="@string/asset_statements" />
    </application>
</manifest>
```

```xml
<string name="asset_statements" translatable="false">[{\n  \"relation\": [\"delegate_permission/common.handle_all_urls\"],\n  \"target\": {\n    \"namespace\": \"web\",\n    \"site\": \"https://www.exanple.com\",\n  }\n}]</string>
```

これらのファイルの生成は Digital Asset Links File Generater がやってくれます。
Mac Android Studio だと [Tools] -> [App Links Assistant] -> [Open Digital Asset Links File Generater] にあります。

![スクリーンショット 2018-02-10 08.51.08.png](https://qiita-image-store.s3.amazonaws.com/0/70150/bcc71ed6-4a4e-2958-36cf-c73369cb6aa4.png)

公式で詳しく説明を用意してくれてるので詳しくは[こちら](https://developer.android.com/studio/write/app-link-indexing.html)をどうぞ。

# 登録画面やスプラッシュのスキップ

インストール用の多くのアプリはユーザー登録が必要ですね。

インストールしたあと、登録もしくはログイン画面を通りますが、インスタントアプリではURLから起動してきたときに、そのURLのコンテンツをまず見せたいです。

また、起動時にスプラッシュ画面を表示しながらデータの準備をすることがあると思いますが、できればこの処理も外してすぐにコンテンツを表示できるようにしたいです。

一時的にログインをせず利用できるようにし、必要なデータの準備はコンテンツの表示と同時並行で行えるような実装をしたいところです！

各アプリ固有の実装の話なのでここでは割愛。

# 本体アプリのインストール誘導

サービス的には一番重要な部分ですね。
インスタントアプリから本体アプリへと誘導します。

[公式](https://developer.android.com/topic/instant-apps/ux-best-practices.html#explicit_installation_prompts)では、ナビゲーションドロワーや画面下などにインストールボタンを置くパターンが紹介されてました。

![aia-ux-10-w256.png](https://qiita-image-store.s3.amazonaws.com/0/70150/e78246fa-85ce-3895-6202-d59c14761918.png)


インストールの実装方法は、InstantApps.showInstallPromptという便利メソッドが提供されていてこれを利用するとダイアログ形式のGoogle Playインストール画面が表示できます。

```groovy
dependencies {
    api 'com.google.android.instantapps:instantapps:1.1.0'
}
```

```java
// インストール後に立ち上げたい画面
Intent postInstallIntent = new Intent(activity, StartupActivity.class);

// インストールリファラー 
// (ex. utm_source=instant_app&utm_medium=install_prompt&utm_campaign=MainActivity)
String referrer = makeInstallReferrerFromActivityName(activity);

boolean isSuccessfullyDisplayed = InstantApps.showInstallPrompt(
        activity, postInstallIntent, requestCode, referrer);
```

インストール後の立ち上げ画面も指定できます。

またインストールリファラーも設定できるのでGAか何かでインスタントアプリからどれだけインストールしてくれたか計測できます。

本体アプリとインスタントアプリの場合分けには、InstantApps.isInstantApp という便利メソッドもあります。

```groovy
dependencies {
    api 'com.google.android.instantapps:instantapps:1.1.0'
}
```

```java
if (InstantApps.isInstantApp(context)) {
    // Instant App
}
```

# リリース

リリース時の注意事項があります。

- ターゲット設定を本体アプリと合わせる
- バージョンコードを本体アプリ以下にする
- 開発段階で10MB、製品版リリースで4MB以下
- default-urlを指定する

## ターゲット設定を本体アプリと合わせる
```
インストール済みアプリにアップグレードできません
問題

この Instant App APK の一部のユーザーはインストール済みアプリの APK の対象外となります。

解決策

Instant App APK のターゲット設定がアプリの APK のターゲット設定と一致することを確認してください。
```

本体アプリとインスタントアプリの提供先は同じでなければならないようです。よく考えればそうですね。でも変えてたつもりは無かったのですが、Apkの中身を見ると違うところがあったので調整しました。

- ネイティブ プラットフォーム
- ローカライズ
- 機能
- OpenGL ES バージョン
- Supports Rtl

どれが正解だったのか分からないので全て並べておきます。

### ネイティブ プラットフォーム

ネイティブ プラットフォーム（arm64-v8a, armeabi, armeabi-v7a, mips, mips64, x86, x86_64）を揃えます。

### ローカライズ

日本語と英語に絞ってたので、本体アプリも同じにします。

```groovy
android {
    defaultConfig {
        resConfigs "ja", "en"
    }
}
```

### 機能

```xml
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.location" android:required="false" />
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
<uses-feature android:name="android.hardware.location.network" android:required="false" />
<uses-feature android:name="android.hardware.telephony" android:required="false" />
<uses-feature android:name="android.hardware.nfc" android:required="false"/>
<uses-feature android:name="android.hardware.gps" android:required="false"/>
<uses-feature android:name="android.hardware.sensor" android:required="false"/>
<uses-feature android:name="android.hardware.microphone" android:required="false"/>
```

パーミッションの宣言やライブラリの影響で、自動的に宣言されている必要機能があるので、明示的に指定してあげます。


### OpenGL ES バージョン

Open GL のバージョンを2に合わせました。Google Mapsが使用してたようです。

```xml
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
```

### Supports Rtl

```xml
<manifest>
    <application
        tools:replace="android:supportsRtl"
        android:supportsRtl="false">
    </application>
</manifest>
```

## バージョンコードをあわせる

```
バージョン コードが無効です
問題

Instant App APK 内のベース APK に含まれているバージョン コードが、現在このトラックで有効な Instant App APK のバージョン コードより大きくありません。

解決策

Instant App APK を、105 より高いバージョン コードで再構築してください。
```

エラーメッセージの通りですが、リリース済み本体アプリのバージョンコードと同じか、低くなければなりません。

## Instant App APK 上限サイズ

```
Instant App APK に、上限サイズの 4194304 バイトを超える APK が含まれています。
```

開発版、プレリリース版、製品版の３つのステップがあり、開発版で10MB以下、製品版で4MB以下にする必要があります。

4MBというのが厳密にどのサイズなのかというと、コンパイルした際の成果物のzipファイルを解凍した中身のapkの実ファイルサイズです。複数のフィーチャーがある場合はBase Apk + Feature Apkのサイズです。

1MB = 1024KBで、MacのFinderは1MB = 1000KB表記なので注意です。

## default-url を指定

Google Playからインスタントアプリを開くときの「今すぐ試す」ボタンを押したときにどの画面を起動するかに使われます。このURL対応したフィーチャーの画面が起動されます。

```xml
<manifest>
    <application>
        <activity>
            <meta-data
                android:name="default-url"
                android:value="https://example.com/" />
    </application>
</manifest>
```

ここまで来たらついに公開されます！

私のものではないですが、こんな感じで「今すぐ試す」ボタンがGoogle Playに表示されます。

![gp-try-now.jpg](https://qiita-image-store.s3.amazonaws.com/0/70150/a2075d7e-796d-1691-b9c1-8260f84bd3bb.jpeg)



もちろん対応したURLを直接タップしてもインスタントアプリを利用できます。

# 終わり

ということで長くなりましたが、神機能ですねー！ (*˘︶˘*).｡.:*♡

インストールするまで良さを分からなかったり、Webなどから開かれたときにインストールから始まってしまって、ほしいコンテンツにたどり着けなかった！とかあるじゃないですか

そういうとこがこれで大幅に改善できますね。
まずはダウンロードしてみてね！の敷居がグッと低くなってます！

全体の構成知りたい場合は公式のサンプルコードみるとよいかも
https://github.com/googlesamples/android-instant-apps

試験段階時の企業で数ヶ月かかったという話がされていたので、プロジェクト構成によってはものすごく時間がかかり、難解でもあるので、インスタントアプリ対応をしようとしてる皆様の参考になれば幸いです！


## 実装したアプリ
実際にインスタントアプリを実装したアプリは、別の投稿にしますね。