KotlinでSwipeToRefresh

# 検証環境
この記事の内容は、以下の環境で検証しました。

* Java:open jdk 1.8.0_152
* Android Studio 3.0
* CompileSdkVersion:26
* MinSdkVersion:21
* TargetSdkVersion:26
* BuildToolsVersion:26.0.2
* gradle:3.0.0

# SwipeToRefreshとは
リストを使った多くのアプリでは、リストの先頭が表示された状態で下方向にスワイプするとインジケータが表示され、更新処理が始まります。
このような機能（スワイプ時にインジケータを表示し、更新処理などを行える機能）をAndroidでは、『SwipeToRefresh』と呼びます。（参照：https://developer.android.com/training/swipe/index.html）
AndroidでSwipeToRefreshを実現するには、SwipeRefreshLayoutとそのレイアウトのコールバックメソッドを利用します。
操作イメージは下図のとおりです。
![02_SwipeToRefreshの例.png](https://qiita-image-store.s3.amazonaws.com/0/99960/3cd48aa6-1558-d662-64e4-2e837e86f4b2.png)

# SwipeRefreshLayout
SwipeRefreshLayoutは、標準ライブラリーに同梱されていません。利用するには、「Support Library v4」を導入します。
SwipeRefreshLayoutは、SwipeRefreshLayoutタグ内のListViewなどをスワイプした際、以下の機能を提供します。

* インジケータの表示
* SwipeRefreshLayoutのコールバックメソッドを呼び出す

また、インジケータを非表示にするには、非表示にする処理をプログラムで実装する必要があります。

# 完成イメージ
この記事で作成するサンプルアプリの完成イメージは、下図のとおりです。
![03_アプリの完成イメージ.png](https://qiita-image-store.s3.amazonaws.com/0/99960/af193f5b-bb03-c327-6190-853e63bc498e.png)


## サンプルアプリの詳細
### 更新処理前のイメージ
「5行のサンプルデータが表示された初期画面」が表示される。
### 更新処理のイメージ
リストを下方向にスワイプするとインジケータが表示され、5行分のデータが約1秒間隔で1行ずつ追加される。
### 更新完了後のイメージ
5行分追加すると、表示されていたインジケータが非表示になる。

# ソースコードと解説
サンプルアプリは、以下のファイルで構成されています。
（プロジェクト生成後から変更がないマニフェストファイルなどは、省略しています。）
## ファイル一覧
### レイアウト
* row.xml
* activity_main.xml

### Activity
* MainActivity.kt

### Gradle
* build.gradle(app)

## Gradle
「Support Library v4」を導入するため、build.gradle(app)のdependenciesに下記の1行を追記します。

```groovy
implementation 'com.android.support:support-v4:26.1.0'
```
ライブラリーを追加する際、次の事に注意してください。
他のSupport Libraryを導入している場合、必ずライブラリー同士のバージョンを一致させる必要があります。
この記事では、「Appcompat v7」も使用している為、dependenciesは以下のようになります。

```groovy
dependencies {
　　　　　・・・省略・・・
    implementation 'com.android.support:appcompat-v7: 26.1.0 '
    implementation 'com.android.support:support-v4:26.1.0'
}
```

## レイアウト
最終的なレイアウトのイメージは、下図のとおりです。

![01_レイアウト.png](https://qiita-image-store.s3.amazonaws.com/0/99960/988c70fc-4ad1-189a-3dec-9a4ceed8e557.png)


### row.xml
リストに表示する1行分のデザインのレイアウトファイルの内容は、以下のとおりです。

```xml:row.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
```
サンプルアプリは、リストの1行に対して1行の文字列のみを表示するため、TextViewだけを実装しています。



### activity_main.xml
画面全体のレイアウトファイルの内容は、以下のとおりです。

```xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="jp.co.casareal.swipetorefreshsample.MainActivity">

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/mySwipeRefreshLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@android:id/list"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginBottom="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </android.support.v4.widget.SwipeRefreshLayout>
</android.support.constraint.ConstraintLayout>
```
SwipeRefreshLayoutを利用する際、以下の3つに注意点してください。

* ListViewもしくはGridViewのみが包括できる
* 包括するビューは必ず1つのみ
* 包括したビューのidは「@android:id/list」にする

この記事のサンプルアプリは、ListViewを利用してます。
また、SwipeRefreshLayoutのオブジェクトにリストが下方向にスワイプされたときのコールバックメソッドを設定するため、SwipeRefreshLayoutに任意のidを付与します。

```xml
    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/mySwipeRefreshLayout"
　　　　　　・・・省略・・・>
        <ListView
            android:id="@android:id/list"
            ・・・省略・・・ />
    </android.support.v4.widget.SwipeRefreshLayout>
```


## Activity
サンプルアプリのアクティビティのソースコードは、以下のとおりです。

```kotlin:MainActivity
package jp.co.casareal.swipetorefreshsample

import android.os.AsyncTask
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.widget.ArrayAdapter

import kotlinx.android.synthetic.main.activity_main.*
import java.text.SimpleDateFormat
import java.util.*

class MainActivity : AppCompatActivity() {

    // 内部クラス内でも参照できるように
    // Adapterをプロパティとして宣言
    var adapter: ArrayAdapter<String>? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ListViewに設定するAdapterの生成
        adapter = ArrayAdapter(this, R.layout.row, createData())

        // ListViewにAdapterを設定
        list.adapter = adapter

        // リストを下にスワイプされたら更新処理を実行
        mySwipeRefreshLayout.setOnRefreshListener {
            MyTask().execute()
        }
    }

    // 内部クラスとしてMyTaskを定義
    // innerキーワードを付与している理由は、
    // 内部クラス内でMainActivityクラスの参照を使用するため
    inner class MyTask : AsyncTask<Void, String, Void>() {

        // 5つのデータを生成後、１秒スリープしてから
        // ListViewに反映させる
        override fun doInBackground(vararg p0: Void?): Void? {

            createData().forEach {

                Thread.sleep(1000)

                publishProgress(it)
            }
            return null
        }

        // データ追加の更新処理
        override fun onProgressUpdate(vararg values: String?) {
            values.forEach {
                adapter?.add(it)
            }
            adapter?.notifyDataSetChanged()
        }

        // データの追加処理が修了後、インジケータを非表示にする
        override fun onPostExecute(result: Void?) {
            mySwipeRefreshLayout.isRefreshing = false
        }
    }
}

inline fun createData(): List<String> {

    var mList = mutableListOf<String>()
    val dateFormat = SimpleDateFormat("MM月dd日のhh時mm分ss秒SSSミリ秒に作成したデータ")

    for (i in 0 until 5) {
        Date(System.currentTimeMillis()).let {
            mList.add(dateFormat.format(it))
        }
        Thread.sleep(20)
    }
    return mList
}
```
### OnRefreshListenerインターフェイス
リストを下方向にスワイプすると、SwipeRefreshLayoutにリスナーとして登録されている
OnRefreshListenerインターフェイスのonRefreshメソッドが呼び出されます。
メソッドのシグニチャは、以下のとおりです。

```java
void onRefresh();
```
OnRefreshListenerインターフェイスを実装したオブジェクトを、SwipeRefreshLayoutのオブジェクトのリスナーに設定します。
サンプルアプリは、ラムダ式で実装しています。
※ここでのmySwipeRefreshLayoutは、SwipeRefreshLayoutのオブジェクトです。

```java
mySwipeRefreshLayout.setOnRefreshListener {・・・更新処理などを記述・・・}
```
### インジケータを非表示にする
インジケータを非表示にするには、SwipeRefreshLayoutクラスのsetRefreshingメソッドの引数にfalseを渡します。[^1]
Kotlinでは、isRefreshingプロパティにfalseを渡すことで実現します。isRefreshingプロパティにfalseを渡すことで、setRefreshingメソッドが内部的に呼び出されます。
実装例は、以下のとおりです。

```kotlin
mySwipeRefreshLayout.isRefreshing = false
```




[^1]: setRefreshingメソッドの引数にtrueを渡すと、インジケータが表示されます。

# まとめ
SwipeRefreshLayoutを利用することにより、比較的少ないコード量でリッチな機能が実装できます。