SwipeRefreshLayoutとRecycleViewをListViewに導入してみた
ブログエントリで試しているアプリにSwipeRefreshLayoutとRecycleViewをListViewに導入してみたのでまとめていく。
SwipeRefreshLayout
SwipeRefreshLayoutはViewGroupの1つで引っ張って更新する機能を導入することができる。
まずはレイアウトからまとめていく。
<android.support.v4.widget.SwipeRefreshLayout android:id="@+id/swipeRefreshLayout" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> // 更新する要素 </LinearLayout> </android.support.v4.widget.SwipeRefreshLayout> </android.support.constraint.ConstraintLayout>
SwipeRefreshLayoutの子要素は必ず1つになるようにレイアウトを組む必要がある。
次にコードをまとめていく。
swipeRefreshLayout.setOnRefreshListener { request() }
setOnRefreshListener
で更新処理をリスナーに登録する。
更新処理が完了したところでisRefreshing
をfalseにすることでインジケータを停止することができる。
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に応じたオブジェクトをリストから取得して返している。
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に応じたメソッドを提供する。
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にデータをセットしていく。
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を更新する。
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