Firebase Crashlytics のログを活用する

Android アプリでクラッシュログを Crashlytics に送っているケースが多いと思いますが、そのクラッシュログからどういう操作をしたらクラッシュを再現できるのか分からず、諦めてしまっていたりしませんか。
Firebase Crashlytics のログを活用すれば解決できるかもしれません。是非試してみてください。

## Firebase Crashlytics の導入
導入してない方はこちらの公式のドキュメントを呼んで Firebase Crashlytics を入れましょう。
https://firebase.google.com/docs/crashlytics/?hl=ja

Firebase ではない Fabric Crashlytics を利用している方も同じ機能が使えるのでそのままで大丈夫です。

## ログはどんなときに役に立つの？
ログを残しておくとユーザーがどんな操作をしたか残しておくことができるので、バグを再現しやすくなります。特殊な操作をしないと再現できないバグなどが発生しているときなどは特に便利です。
言葉で言うと分かりづらいので、例を用いて説明します。

## 実際にログを取ってみる

例えば以下のコードがあったとします。ボタンが２つ配置されていて、どっちのボタンが押されたとしても ButtonTextTask という AsyncTask が実行されて、ボタンのテキストが "okay"になるように作ってみました。

```java
public class MainActivity extends AppCompatActivity {

    Button button1;
    Button button2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button1 = findViewById(R.id.button1);
        button2 = findViewById(R.id.button2);

        button1.setOnClickListener((view) -> new ButtonTextTask().execute("okay"));
        button2.setOnClickListener((view) -> new ButtonTextTask().execute());
    }

    public class ButtonTextTask extends AsyncTask<String, String, String> {

        @Override
        protected String doInBackground(String... strings) {
            return strings[0]; // button2 をタップすると、ここでクラッシュする
        }

        @Override
        protected void onPostExecute(String o) {
            button1.setText("okay");
            button2.setText("okay");
        }
    }
}

```

ただコード中にも記載されているようにこのコードにはバグがあって、button2 のボタンを押すと以下のように ArrayIndexOutOfBoundsException になってしまう。

```text
    java.lang.ArrayIndexOutOfBoundsException: length=0; index=0
        at crashlyticslogtest.net.crashlyticslogtest.MainActivity$ButtonTextTask.doInBackground(MainActivity.java:28)
        at crashlyticslogtest.net.crashlyticslogtest.MainActivity$ButtonTextTask.doInBackground(MainActivity.java:24)
        at android.os.AsyncTask$2.call(AsyncTask.java:333)
        ...
```

button2 には "okay" という文字列を渡していないため doInBackground の引数の strings が null になっているので、string[0] は参照できず、例外が発生している。
このスタックトレースを見ても setOnClickListener をしている箇所がスタックトレースに出てこないので、どっちのボタンが押された結果発生したのかよくわからないです。

以下のようにログを取るように改良すると

```java
public class MainActivity extends AppCompatActivity {

    Button button1;
    Button button2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
        button1 = findViewById(R.id.button1);
        button2 = findViewById(R.id.button2);

        button1.setOnClickListener((view) -> {
            Crashlytics.log("button1"); // これを追加した
            new ButtonTextTask().execute("okay");
        });
        button2.setOnClickListener((view) -> {
            Crashlytics.log("button2"); // これを追加した
            new ButtonTextTask().execute();
        });
    }

    public class ButtonTextTask extends AsyncTask<String, String, String> {

        @Override
        protected String doInBackground(String... strings) {
            return strings[0];
        }

        @Override
        protected void onPostExecute(String o) {
            button1.setText("okay");
            button2.setText("okay");
        }
    }
}

```

以下のように Firebase Crashlytics の管理画面で button1 、button2 の順番でボタンが押されたことがログに残っているので、最後に button2 を押したときに何か起きたこと簡単に特定することができます。
![961e2909b86a5d764958ca76ebe94741.png](https://qiita-image-store.s3.amazonaws.com/0/7869/edc62f2f-8800-f164-0657-1812e287d3ab.png)


## もう一つの例
わざとらしい例だったので、もうちょっと実用的な例を。
どういう画面遷移を経てエラーは発生したのかよくわからないことがあります。そこで Activity が開始されたときのログを残しておくと、概ねどういう動きをしたのか分かるようになるので便利です。 

```java
public class MainActivity extends AppCompatActivity {

    String textToSend = "myText!!!";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Crashlytics.log("MainActivity onCreate");

        // ボタンを押したら SecondActivity を開く
        findViewById(R.id.button1).setOnClickListener((view) -> {
            final Intent intent = new Intent(this, SecondActivity.class);
            intent.putExtra("value", textToSend); // ２回目開いたときは textToSend が null になっている
            startActivity(intent);
        });
    }

    @Override
    protected void onRestart() {
        super.onRestart();
        textToSend = null; // これが問題を引き起こす
    }

    @Override
    public void onBackPressed() {
        super.onBackPressed();
        Crashlytics.log("onBackPressed");
    }
}


public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        String text = getIntent().getStringExtra("value");

        // 2回目開いたときは MainActivity の onRestart によって text が null になっているので、ここでクラッシュする
        text.concat("test"); 
    }

    @Override
    public void onBackPressed() {
        super.onBackPressed();
        Crashlytics.log("onBackPressed");
    }
}

```

このコードは以下のようにソースコード上に記載した箇所でクラッシュします。ただ SecondActivity 開いただけクラッシュせず、２回目に開いたときにクラッシュするコードです。

```
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.lang.String.concat(java.lang.String)' on a null object reference
        at crashlyticslogtest.net.crashlyticslogtest.SecondActivity.onCreate(SecondActivity.java:18)
        at android.app.Activity.performCreate(Activity.java:6975)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1213)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2770)
...
```

これを Firebase Crashlytics で見ると以下のようになります。
![022835d281e8929fe199d191a83c0480.png](https://qiita-image-store.s3.amazonaws.com/0/7869/5006a2fc-4fd8-2f00-077c-5f013779163c.png)

「Log」ところを見ると MainAcitivty -> SecondActivity -> 戻るボタン -> SecondActivity と操作したあとに問題が発生しているのがわかると思います。

## 全部の onCreate に挟むとか面倒じゃない？
[ActivityLifecycleCallbacks](https://developer.android.com/reference/android/app/Application.ActivityLifecycleCallbacks) や [LifecycleObserver](https://developer.android.com/reference/android/arch/lifecycle/LifecycleObserver) を使用するとお手軽です。