SwipeRefreshLayoutとRecycleViewをListViewに導入してみた


ブログエントリで試しているアプリにSwipeRefreshLayoutとRecycleViewをListViewに導入してみたのでまとめていく。

SwipeRefreshLayout

SwipeRefreshLayoutはViewGroupの1つで引っ張って更新する機能を導入することができる。

まずはレイアウトからまとめていく。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    .support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipeRefreshLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

        // 更新する要素

        
    .support.v4.widget.SwipeRefreshLayout>
.support.constraint.ConstraintLayout>

SwipeRefreshLayoutの子要素は必ず1つになるようにレイアウトを組む必要がある。


次にコードをまとめていく。

1
swipeRefreshLayout.setOnRefreshListener { request() }

setOnRefreshListenerで更新処理をリスナーに登録する。

更新処理が完了したところでisRefreshingをfalseにすることでインジケータを停止することができる。

1
swipeRefreshLayout.isRefreshing = false

RecycleView

次にRecycleViewをまとめていく。RecycleViewは大量なリスト表示が必要や頻繁にデータが変わるような場面で限られたViewを効率的に維持してくれる。

RecyclerView.Adapterを実装する

1行のデータをViewとして生成するAdapterを実装する。FragmentからはRecyclerAdapterのreplaceAllを呼び出しRecyclerViewを更新する。

BaseRecyclerAdapterはRecyclerView.Adapterを継承してonCreateViewHolder, onBindViewHolder, getItemCountを実装している。onCreateViewHolder, onBindViewHolderはそれぞれViewHolderが生成に合わせてviewTypeやpositionに応じたオブジェクトをリストから取得して返している。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class RecyclerAdapter : BaseRecyclerAdapter<RecyclerView.ViewHolder>() {

    fun <B : Binder<RecyclerView.ViewHolder>> replaceAll(objects: List<B>) {
        clear()
        objects.withIndex().forEach {
            insert(it.index, it.value)
        }
        notifyDataSetChanged()
    }
}

open class BaseRecyclerAdapter<VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {

    private val mObjects: MutableList<Binder<VH>> = mutableListOf()

    override fun onCreateViewHolder(parent: ViewGroup,
                                    viewType: Int): VH = getItemByViewType(viewType).onCreateViewHolder(parent)

    override fun onBindViewHolder(holder: VH, position: Int) = getItem(position).onBindViewHolder(holder, position)

    override fun getItemCount(): Int = mObjects.size


    fun getItem(position: Int): Binder<VH> =
            mObjects.withIndex().filter { it.index == position }
                    .takeIf { it.isNotEmpty() }?.let { it[0].value }
                    ?: throw IllegalArgumentException("invalid position=${position}")

    private fun getItemByViewType(viewType: Int): Binder<VH> =
            mObjects.filter { it.getViewType() == viewType }
                    .takeIf { it.isNotEmpty() }?.let { it[0] }
                    ?: throw IllegalArgumentException("invalid viewType=${viewType}")

    fun insert(index: Int, obj: Binder<VH>) {
        mObjects.add(index, obj)
    }

    fun clear() {
        mObjects.clear()
    }
}

interface Binder<VH> {
    fun onCreateViewHolder(parent: ViewGroup): VH
    fun onBindViewHolder(viewHolder: VH, position: Int)
    fun getViewType(): Int
}

RecyclerView.ViewHolderを保持するBinderクラスを実装する

RecyclerView.ViewHolderは1行分のView参照を保持するクラス。このRecyclerView.ViewHolderを自作のBinderクラスで包みAdapterに応じたメソッドを提供する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface Binder<VH> {
    fun onCreateViewHolder(parent: ViewGroup): VH
    fun onBindViewHolder(viewHolder: VH, position: Int)
    fun getViewType(): Int
}

interface ViewType {
    fun viewType(): Int
}

abstract class RecycleBinder(private val context: Context, private val viewType: ViewType) : Binder<RecyclerView.ViewHolder> {

    @LayoutRes
    abstract fun layoutResId(): Int

    abstract fun onCreateViewHolder(view: View): RecyclerView.ViewHolder

    override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder {
        return onCreateViewHolder(LayoutInflater.from(context).inflate(layoutResId(), parent, false))
    }

    override fun getViewType(): Int {
        return viewType.viewType()
    }
}

上述したRecycleBinderを継承したForecastViewBinderクラスでViewHolderを引数に取りViewにデータをセットしていく。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class ForecastViewBinder(private val context: Context,
                         private val forecast: Forecast) : RecycleBinder(context, ForecastViewType.FORECAST) {

    override fun layoutResId() = R.layout.forcast_binder

    override fun onCreateViewHolder(view: View): RecyclerView.ViewHolder {
        return ViewHolder(view)
    }

    override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
        viewHolder as ViewHolder

        viewHolder.date.text = DateUtils.formatDateTime(context, forecast.dt * 1000L, FORMAT_NO_YEAR)
        viewHolder.main.text = forecast.weather.get(0).main
        viewHolder.max.text = forecast.temp.max.toString()
        viewHolder.min.text = forecast.temp.min.toString()

        forecast.weather.get(0).main.let {
            viewHolder.main.text = it
            when (it.toLowerCase()) {
                "clear" -> R.drawable.ic_sun
                "clouds" -> R.drawable.ic_cloud
                "fog" -> R.drawable.ic_haze
                "light_clouds" -> R.drawable.ic_cloudy
                "light_rain" -> R.drawable.ic_rain
                "rain" -> R.drawable.ic_rain
                "snow" -> R.drawable.ic_snowing
                "storm" -> R.drawable.ic_storm
                else -> R.drawable.ic_sun // fix me
            }.let {
                viewHolder.image.setBackgroundResource(it)
            }
        }
    }
}

class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {

    val date: TextView = view.findViewById(R.id.forecast_date)
    val main: TextView = view.findViewById(R.id.forecast_weather)
    val max: TextView = view.findViewById(R.id.forecast_temp_max)
    val min: TextView = view.findViewById(R.id.forecast_temp_min)
    val image: AppCompatImageView = view.findViewById(R.id.forecast_ic)
}

FragmentからadapterのreplaceAllを呼び出す

Fragmentでadapterを生成してリスト取得したデータをreplaceAllに渡しViewを更新する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val context = context ?: return

        forecastsStore.forecasts()
                .observeOn(AndroidSchedulers.mainThread())
                .`as`(autoDisposable(this))
                .subscribe { forecasts ->

                    swipeRefreshLayout.isRefreshing = false

                    cityView.text = "%s/%s".format(forecasts.city.name, forecasts.city.country)
                    adapter.replaceAll(forecasts.list.map {
                        ForecastViewBinder(context, it)
                    })
                }

        savedInstanceState ?: request()

        swipeRefreshLayout.setOnRefreshListener { request() }
    }

コード

SwipeRefreshLayoutとRecycleViewを追加したプルリクエストがあるので参考にしてほしい。

Feature/improve list view by soushin · Pull Request #5 · soushin/sunshine-app · GitHub