Androidで進捗表示ができるまで

## 対象読者
- AndroidStudioの環境設定が終わり、Hello world! を出力できる人
- `MainActivity.java`とは何かを知っている人
- Javaを触ったことのある人
    - AndroidStudioでラムダ式が使えること

# 進捗表示（作りたいやつ）
- 重たい処理をしている時に進捗をリアルタイムで確認したい

![loopOnOtherThread.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/f0830fac-9443-d002-58fa-acf0f521dc5b.gif)

## サンプルコード
- 簡単な例として、何らかの適当な処理を100回繰り返し、1回処理が終わるごとに進捗を1%増やすコードを作る
- ※この例での処理 `doSomeThing()` は0.01秒間スリープするだけ

```java:MainActivity.java
    // ボタンクリック時(関数: busyLoop()を呼び出す)
    execButton.setOnClickListener(view -> busyLoop());

    // ダメ関数
    private void busyLoop(){
        for(int i = 0; i < 100; i++){
            doSomeThing();  // <--- 処理
            progressText.setText(String.valueOf(i+1));  // 進捗表示を更新
        }
    }
```

## 進捗表示（うまく行かないやつ）
![busyLoop.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/7630864c-0db1-cf95-43b0-bd5cb76782fc.gif)

- コンパイルしてみると。。。
    - あれ、0→100%になっちゃった！

## 原因解説
- 原因
    - mainループで処理と画面表示を同時に制御しようとするから
- mainループとは、イベントの検出、画面の更新などを行う無限ループのようなもの
    - 画面の更新は、mainループの最後に行われる
    - つまり、他の処理が終わらない限り画面は固まったままである
    - <img width="282" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/19e10142-e15d-645e-aa68-37595024c19e.png">


### forループ1回ごとに画面が更新されないの？
- サンプルコードでの、mainループの流れは下図の通り
    - 関数busyLoop()内のforループで、進捗%の値は逐次書き換えられている
    - しかし、__値の更新が画面に反映されるのは、関数busyLoop()が終了した後__ となる
    - 従って、`0%でしばらく固まったのち、100%が表示される` という残念な画面表示になってしまった。
    - <img width="463" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/7feb690d-85df-d034-3543-e8e2897d7f46.png">


## 解決策
- 非同期処理を使う
    - mainループとは別のループを生成し、処理と画面更新を分離する
    - 今回は、実装が簡単なThread処理を紹介する
    - <img width="507" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/9def2900-9dd6-ca75-57c8-fb6077a15cde.png">

## Thread処理実装の流れ
- 1. 行いたい処理の部分を `new Thread(()→ { }).start()` 句で囲む
- 2. 画面更新を行うために、`Handler` を使用する

#### 1. new Thread 句で囲む
- 今回は、自作関数 loopOnOtherThread()を作成し、その中にThread処理を定義した

```java:MainActivity.java
    // ボタンクリック時(関数: loopOnOtherThread()を呼び出す)
    execButton.setOnClickListener(view -> loopOnOtherThread());

    // Threadを利用する関数
    void loopOnOtherThread() {
        new Thread(()-> {
            for(int i = 0; i < 100; i++) {
                doSomeThing();  // ← 処理
                updateUIText(i); // ← 画面更新( 後ほど解説 )
            }
        }).start();
    }
```

- 先ほどのダメ関数と構成はほぼ同じであるが、`new Thread(()→ { }).start()` で処理を囲んでいる。
- 囲まれた部分は別スレッドとして動作するため、画面更新は停止しない。


#### 2. Handler の作成
- Handlerとは、別スレッドからmainスレッドの処理を呼ぶための仕組みである。
- 別スレッドでは画面を更新する権限がないため、Handlerを介して、mainスレッドに画面更新を依頼する
    - <img width="604" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/9788a5c2-21c3-f499-59a4-609bafe43124.png">

- 実装部分は以下の通りである
 
```java:MainActivity.java
// mainActivityクラスのメンバ変数として、Handlerを宣言
final Handler handler = new Handler();

// UI更新用の関数を作成
void updateUIText(int num){
    handler.post(() -> {
        progressText.setText(String.valueOf(num + 1)); // 画面更新の処理を書く
    });
}
```
- 上記コードのポイントは次の2点である
    - HandlerをmainActivityクラスのメンバ変数として宣言する
    - 別スレッドからUIを更新するための関数`updateUIText()`を定義し、行いたい画面処理を ` handler.post(() -> {});` 句で囲む
    - ※ 関数 updateUIText() は前述した関数 loopOnOtherThread() 内で呼ばれる


## サンプルコード
- 上述したコードの実装例を以下にまとめた
    - AndroidStudioで`java8`以上の設定であること(lambda式を使用するため)

```java:mainActivity.java

package com.example.testapplication;  //← あなたのプロジェクト名が入る

import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.widget.Button;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    // 定数
    final int LOOP_COUNT = 100;
    final int SLEEP_MSEC = 10;
    // メンバ変数
    TextView progressText;
    Button execButton;
    final Handler handler = new Handler();  // UI制御用のハンドラ

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

        assignLayoutToVariable();
        execButton.setOnClickListener(view -> loopOnOtherThread());  // ボタンにクリックイベント追加
    }


    private void assignLayoutToVariable(){
    // UI上のコンポーネントを変数に割り当てる
    // R.id.○○ には、リソースファイルで定義したボタンのid名を入れる
        setContentView(R.layout.activity_main);
        progressText = findViewById(R.id.progressText);
        execButton = findViewById(R.id.execButton);
    }

    private void doSomeThing(){
    // あなたの行いたい処理を書く
    //（※ 例としてスリープ関数を実装)
        try{
            Thread.sleep(SLEEP_MSEC);
        }catch(InterruptedException e){
            Log.d("MySleep", "Interrupted");
        }
    }

    void loopOnOtherThread() {
    // 別スレッドを立てる関数
        new Thread(()-> {
            for(int i = 0; i < LOOP_COUNT; i++) {
                doSomeThing();
                updateUIText(i);
            }
        }).start();
    }

    void updateUIText(int num){
    // UI更新用の関数
        handler.post(() -> {
            progressText.setText(String.valueOf(num + 1));
        });
    }
}
```

## 参考文献
- [Androidのバックグラウンドを使いこなす Thread, Looper, Handler](https://academy.realm.io/jp/posts/android-thread-looper-handler/)
- [HamdlerとLooper](http://mixi-inc.github.io/AndroidTraining/fundamentals/2.12.handler-and-looper.html?id=#handler)
- [lambda式とRunnable](https://qiita.com/sano1202/items/64593e8e981e8d6439d3)