- 投稿日:2020-12-27T23:29:12+09:00
【Android】RecyclerView で無限スクロールを実装する
やりたいこと
RecyclerView で作ったリストに無限スクロールを実装してみます。
API でデータセットを取得してリスト表示するような機能を想定しています。
画面生成時にリストを 10 件しておき、リストの下端までスクロールしたタイミングで 10 件ずつ追加で表示するサンプルアプリを作っていきます。
最終的に作成したアプリは Github で公開していますので参考にしていただければと思います。
https://github.com/yudai0308/infinite_scroll_sample
実装手順
1. レイアウトファイル作成
RecyclerView のレイアウトと、リストに表示させる1行分のレイアウトを作成します。
activity_main.xml<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.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=".MainActivity"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <ProgressBar android:id="@+id/progress_bar" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" android:visibility="invisible"/> </androidx.constraintlayout.widget.ConstraintLayout>list_item.xml<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/list_item" android:layout_width="match_parent" android:layout_height="80dp" android:gravity="center_vertical" android:padding="16dp" android:textSize="20sp"> </TextView>2. ビューホルダーとアダプターの作成
リストに表示させるデータを保持するビューホルダーと、ビューホルダーを管理してリストを画面表示するロジックを担うアダプターを作成します。
MyAdapter.ktclass MyAdapter(private val listData: MutableList<String>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { val inflater = LayoutInflater.from(parent.context) val textView = inflater.inflate(R.layout.list_item, parent, false) as TextView return MyViewHolder(textView) } override fun onBindViewHolder(holder: MyViewHolder, position: Int) { holder.textView.text = listData[position] } override fun getItemCount(): Int { return listData.size } /** * リストデータを追加して画面に反映させるメソッド。 */ fun add(listData: List<String>) { this.listData += listData notifyDataSetChanged() } class MyViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView) }MyAdapter クラス内に
add()
メソッドを作成しています。リスト下端までスクロールしたタイミングでデータを追加で取得するので、取得したデータをこのメソッドに渡してリストを更新するようにします。
3. 無限スクロールの実装
MainActivity に無限スクロールの実装を行います。
MainActivity.ktclass MainActivity : AppCompatActivity() { /** * API から取得するデータセットを想定したプロパティ。 */ private val dataSet = mutableListOf<String>() /** * API に問い合わせ中は true になる。 */ private var nowLoading = false private lateinit var myAdapter: MyAdapter private val handler = Handler() private val progressBar by lazy { findViewById<ProgressBar>(R.id.progress_bar) } init { // 仮想データセットを生成。 for (i in 0..99) { dataSet.add("Number $i") } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val listData = runBlocking { fetch(0) } myAdapter = MyAdapter(listData as MutableList<String>) findViewById<RecyclerView>(R.id.recycler_view).also { it.layoutManager = LinearLayoutManager(this) it.adapter = myAdapter it.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) it.addOnScrollListener(InfiniteScrollListener()) } } /** * API でリストデータを取得することを想定したメソッド。 */ private suspend fun fetch(index: Int): List<String> { // API 問い合わせの待ち時間を仮想的に作る。 handler.post { progressBar.visibility = View.VISIBLE } delay(3000) handler.post { progressBar.visibility = View.INVISIBLE } return when (index) { in 0..90 -> dataSet.slice(index..index + 9) in 91..99 -> dataSet.slice(index..99) else -> listOf() } } /** * API でリストデータを取得して画面に反映することを想定したメソッド。 */ private suspend fun fetchAndUpdate(index: Int) { val fetchedData = withContext(Dispatchers.Default) { fetch(index) } // 取得したデータを画面に反映。 if (fetchedData.isNotEmpty()) { handler.post { myAdapter.add(fetchedData) } } // 問い合わせが完了したら false に戻す。 nowLoading = false } /** * リストの下端までスクロールしたタイミングで発火するリスナー。 */ inner class InfiniteScrollListener : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) // アダプターが保持しているアイテムの合計 val itemCount = myAdapter.itemCount // 画面に表示されているアイテム数 val childCount = recyclerView.childCount val manager = recyclerView.layoutManager as LinearLayoutManager // 画面に表示されている一番上のアイテムの位置 val firstPosition = manager.findFirstVisibleItemPosition() // 何度もリクエストしないようにロード中は何もしない。 if (nowLoading) { return } // 以下の条件に当てはまれば一番下までスクロールされたと判断できる。 if (itemCount == childCount + firstPosition) { // API 問い合わせ中は true となる。 nowLoading = true GlobalScope.launch { fetchAndUpdate(myAdapter.itemCount) } } } } }
init {...}
について今回は実際に API へ問い合わせは行いませんので、その代わりに文字列型のリストデータを作成しています。
onCreate()
について
fetch()
メソッドで API からリストデータを取得し、そのデータを引数として MyAdapter オブジェクトを作成しています。また、RecyclerView にレイアウトマネージャー、アダプター、デコレーション(区切り線)、スクロールリスナー(無限スクロール)を設定しています。
fetch()
についてAPI に問い合わせてリストデータを 10 件取得することを想定したメソッドです。インデックスを引数として取るので、仮に引数が 20 だった場合、20 ~ 29 のデータを返却します。
取得までにかかる時間を
delay()
で仮想的に実装しています。待ち時間は ProgressBar を表示させて、さらにそれっぽくしてます。
fetchAndUpdate()
についてAPI からのデータ取得と、取得したデータを画面に反映させる処理を一緒に行うメソッドです。
InfiniteScrollListener
から呼び出して使います。
InfiniteScrollListener
について無限スクロールの実装部分です。
変数 itemCount、childCount、firstPosition の役割はコメントに書かれているとおりです。条件式にあるとおり
itemCount == childCount + firstPosition
のとき、下端までスクロールされたと判断できます。下端までスクロールされてデータを取得する際、変数 nowLoading を ture にします。データを取得している途中でもユーザーが上下にスクロールすると
onScrolled()
が発火してしまい、同じデータを何度もリクエストしてしまいます。これを防止するためにリクエスト中は変数 nowLoading を true にして制御します。その後データ取得が完了したタイミングで false に戻します。以上で冒頭の GIF と同じ無限スクロールが実装できたと思います。
参考
- 投稿日:2020-12-27T11:13:48+09:00
Kotlin の Coroutine で suspend 関数を理解する
先日、ドリーム・アーツでは、売場ノートというアプリを Android に対応させた。
全面的に Coroutine を使って非同期処理を書いたので、そのとき理解のために実験した内容について備忘がてら書いておく。
前提
Kotlin 1.4.21
Android Studio 4.1.1dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' }Coroutine の動作をおさらいする
launch とか async/await がどういう順番で呼び出されるかといったことはここでは話題とはせず、Coroutine の利用シーンに絞って、Android アプリ内でどのようにコードが動くのかといったことを記述する。
Coroutine の典型的な利用シーンとして、何らかのユーザーの操作をトリガーとして HTTP をリクエストしてそのレスポンスでビューを更新するというものがある。
AsyncTask による旧来の非同期処理パターン
Android において従来この手のパターンは、伝統的に
AsyncTask
を利用してきた。System.out.println("1. Begin: thread=${Thread.currentThread().name}") object : AsyncTask<Void, Int, String?>() { override fun doInBackground(vararg params: Void?): String? { System.out.println("2-1. AsyncTask started: thread=${Thread.currentThread().name}") Thread.sleep(1000) return "OK" } override fun onPostExecute(result: String?) { System.out.println("2-2. AsyncTask ended: thread=${Thread.currentThread().name}") } }.execute(null) System.out.println("3. End: thread=${Thread.currentThread().name}")これを実行してみた結果、以下のように Logcat に出力される。
I/System.out: 1. Begin: thread=main I/System.out: 3. End: thread=main 2-1. AsyncTask started: thread=AsyncTask #1 2-2. AsyncTask ended: thread=main
AsyncTask#doInBackground
が実行されるのは、ThreadPoolExecutor
で管理されるバックグラウンドスレッドであって、メインスレッドで動くUIの操作に影響を及ぼさない。
onPostExecute(result)
をオーバーライドすると、このメソッドはメインスレッドで呼び出されるのでここでUIを操作することができるのである。この方法の問題点は、
AsyncTask
のライフサイクルが、アクティビティやフラグメントのライフサイクルと切り離されているため、タスク完了時にはすでにビューは存在しないといった場合もあり、そういった状態を適切に扱うのは細心の注意が必要である。Coroutie による非同期処理パターン
画面の回転などによる設定の変更に追随するために、アクティビティやフラグメントといったUIコントローラーでデータを保持するのではなく、
ViewModel
でデータを保持することが一般的だ。androidx.lifecycle:lifecycle-viewmodel-ktx
によって、ViewModel にはインスタンスごとに専用のCoroutineScope
であるviewModelScope
が提供されている。この
CoroutineScope
を利用すれば、画面のライフサイクルにあわせて自動的にSystem.out.println("1. Begin: thread=${Thread.currentThread().name}") viewModel.viewModelScope.launch(Dispatchers.Main) { // 上の行で Dispatchers.Main を指定しているが、指定しなくてもメインスレッドになる withContext(Dispatchers.IO) { System.out.println("2-1. AsyncTask started: thread=${Thread.currentThread().name}") Thread.sleep(1000) } System.out.println("2-2. AsyncTask ended: thread=${Thread.currentThread().name}") } System.out.println("3. End: thread=${Thread.currentThread().name}")これを実行してみた結果は以下の通りである。
I/System.out: 1. Begin: thread=main I/System.out: 3. End: thread=main I/System.out: 2-1. AsyncTask started: thread=DefaultDispatcher-worker-1 I/System.out: 2-2. AsyncTask ended: thread=main
ViewModel
の仕組みと、Coroutine
によって、非同期処理用にクラスやインスタンスを作成せずとも自然な流れで処理を記述することが可能になる。suspend 関数とは何なのか
上の Coroutine のコードを suspend 関数を用いて別メソッドに切り出してみる。
private fun testCoroutine() { System.out.println("1. Begin: thread=${Thread.currentThread().name}") viewModel.viewModelScope.launch { asyncTask() // suspend System.out.println("2-2. AsyncTask ended: thread=${Thread.currentThread().name}") } System.out.println("3. End: thread=${Thread.currentThread().name}") } private suspend fun asyncTask() { System.out.println("2-1. AsyncTask started: thread=${Thread.currentThread().name}") Thread.sleep(1000) }このコードを実行すると以下のようにログに出力され、メインスレッドはブロッキングしてしまいUIが固まる。
I/System.out: 1. Begin: thread=main I/System.out: 2-1. AsyncTask started: thread=main I/System.out: 2-2. AsyncTask ended: thread=main I/System.out: 3. End: thread=mainsuspend を付けたからといって、自動的にバックグラウンドスレッドになるわけではない。
バックグラウンドスレッドでの実行にしたい場合はやはり明示的にコンテキストを指定する必要がある。以下のように書くことができる。private suspend fun asyncTask() = withContext(Dispatchers.IO) { System.out.println("2-1. AsyncTask started: thread=${Thread.currentThread().name}") Thread.sleep(1000) }では、suspend 関数とは何なのか?
suspend 関数とは非同期処理のための仕組みであって、「別の suspend 関数を呼び出すために使用する」のである。HTTP でサーバー側のAPIを呼び出すようなケースを考えてみる。
典型的なコードは以下のようなものになるだろう。private suspend fun callAPI(): SomeData? { // HTTPリクエストのためのパラメーターを生成する val req = buildRequest() // HTTPリクエストを発行する val res = sendRequest(req) // suspend 関数 // HTTPレスポンスで取得したデータ(JSONなど)を解析する return parseResponse(res) }これらの処理のうち、HTTPでの通信や、ストレージへの保存、ストレージからの読み込みはIOを伴う処理で、CPUの計算に比べて比較にならないくらい遅い。
だから非同期で処理したい。
従来はスレッドプール内のワーカースレッドにこのコードブロック全体を渡してそこで処理して、
Future
などで結果を受け取るようなやり方か、Rxな仕組みで非同期をどうしても意識せざるを得ない書き方を強制されてきた。実はCoroutine を用いると、suspend 関数の内部では、別の suspend 関数を呼び出す箇所まででタスクを分割して、そのタスクを順次処理していくということが行われる。
private suspend fun callAPI(): SomeData? { // -------- コードブロック[1] ここから // HTTPリクエストのためのパラメーターを生成する val req = buildRequest() // HTTPリクエストを発行する val res = sendRequest(req) // suspend 関数 // -------- コードブロック[1] ここまで // -------- コードブロック[2] ここから // HTTPレスポンスで取得したデータ(JSONなど)を解析する return parseResponse(res) // -------- コードブロック[2] ここまで }一連の流れのように見えるが、細かいタスクに分割されて Coroutine のコンテキストスコープのスレッドで順次実行される。
この様子は実際に上のようなコードを書いてみてデバッガーでブレークポイントを置いてみるとわかり易い。
suspend 関数の呼び出しを超えて次の行へステップ実行できないはずだ。
そして呼び出した先の suspend 関数は、さらに別の suspend 関数を呼び出すときに同様にブロックに分割されて、そこで処理を保留する。(suspend する)
呼び出していった先、これ以上別の suspend 関数を呼び出さないところまでたどり着いて、そのブロックが終わったらコールスタック上で保留されているブロックを順番に実行していく。
そしてそれらが終わったらさらに上の suspend 関数で保留されたブロックも実行する。
つまり、Rx なら then とかで書いていたブロックをそのままひと続きの流れで書くことが可能になる。
Coroutine を使った標準パターン
以上のことを理解すれば、以下のコードでメインスレッドをブロックすることなく、時間のかかる呼び出しの結果をメインスレッドでそのまま扱うことができることがわかる。
private fun testCoroutine() { viewModel.viewModelScope.launch { // ここはメインスレッドで呼び出される // asyncTask() は suspend 関数なので、呼び出してもメインスレッドをブロックしない val result = asyncTask() // asyncTask() が終了したら、メインスレッドでこのブロックが実行される viewModel.textValue.value = result } } private suspend fun asyncTask(): String = withContext(Dispatchers.IO) { // ここは DefaultDispatcher-worker-X というバックグラウンドスレッドで呼び出される Thread.sleep(1000) return@withContext "RESULT" }まとめ
Coroutine を使うことで、やっていることに集中できるコードになり、AsyncTask とか Rx とか何をやってたのだろうという気分になれたのはよかった。
- 投稿日:2020-12-27T10:30:58+09:00
KotlinでRoomデータベース
KotlinでRoomデータベースを使用してみたので、覚書。
関連
続編:
RoomデータベースとListView
https://qiita.com/clipbord/items/0d8b7f973e3f5b277827RoomデータベースをListViewで表示するように作成したもの。
全ソース
https://github.com/CoffCookie/memo
gradle
Roomとkaptが必要だったため、2つを指定。
最新版は下記Room:
https://developer.android.com/training/data-storage/room?hl=jakapt:
https://kotlinlang.org/docs/reference/kapt.html私の場合、kaptはversion指定すると上手く行かなかったため、version指定なしで作成している。
app/build.gradleplugins { ... id 'org.jetbrains.kotlin.kapt' } dependencies { ... def room_version = "2.2.5" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" // optional - Kotlin Extensions and Coroutines support for Room implementation "androidx.room:room-ktx:$room_version" // optional - Test helpers testImplementation "androidx.room:room-testing:$room_version" implementation "com.google.android.material:material:1.3.0-beta01" }データベースのテーブル(フィールド名)を定義
Memo.kt... @Entity class Memo ( @PrimaryKey(autoGenerate = true) val id: Int, var text: String? )データベースにアクセスする際に使用するメソッドを格納
MemoDao.kt... @Dao interface MemoDao { @Query("Select * From memo") fun getAll() : List<Memo> @Insert fun insert(memo: Memo) @Update fun update(memo: Memo) @Delete fun delete(memo: Memo) }データベースの作成とインスタンスの取得
MemoDatabase.kt... //データベースの作成 @Database(entities = [Memo::class], version = 2) abstract class MemoDatabase:RoomDatabase() { abstract fun memoDao(): MemoDao companion object{ private var INSTANCE: MemoDatabase? = null private val lock = Any() fun getInstance(context: Context): MemoDatabase{ synchronized(lock){ if (INSTANCE == null) { //作成したデータベースのインスタンス取得 INSTANCE = Room.databaseBuilder( context.applicationContext, MemoDatabase::class.java, "Memodata.db" ) .allowMainThreadQueries() .build() } return INSTANCE!! } } } }View作成
activity_main.xml... <TextView android:id="@+id/memoView" android:layout_width="320dp" android:layout_height="wrap_content" android:text="@string/memo_view" android:textAppearance="@style/TextAppearance.AppCompat.Large" tools:layout_editor_absoluteX="43dp" tools:layout_editor_absoluteY="64dp" /> <EditText android:id="@+id/memoText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:ems="10" android:inputType="textPersonName" android:text="@string/memo_text" app:layout_constraintStart_toStartOf="@+id/memoView" app:layout_constraintTop_toBottomOf="@+id/memoView" /> <Button android:id="@+id/saveButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="@string/save_button" app:layout_constraintStart_toStartOf="@+id/memoText" app:layout_constraintTop_toBottomOf="@+id/memoText" /> ...処理部分の作成
データベースの処理部分に絞って記載したかったため、
表示部分は簡易的に作成している。MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //Viewの定義 val saveButton: Button = findViewById(R.id.saveButton) val memoText: TextView = findViewById(R.id.memoText) val memoView: TextView = findViewById(R.id.memoView) //データベースの定義 val database = MemoDatabase.getInstance(this) val memoDao = database.memoDao() //保存ボタン押下時の処理 saveButton.setOnClickListener{ //エンティティにデータ設定 val newMemo = Memo(0,memoText.text.toString()) //データベースに保存 memoDao.insert(newMemo) //データをQueryで取得して、データベースの最後の要素をmemoViewに出力 val memoList = memoDao.getAll() memoView.text = memoList[memoList.size-1].text } } }
- 投稿日:2020-12-27T10:05:09+09:00
[Jetpack Compose] 1.0.0-alpha09から変わったこと。
おすすめ
- Android Studio Arctic Fox Canary 2
- https://developer.android.com/studio/preview
前はBetaバージョンを使ってましたが、
最近のバージョンにはCanary Buildに変わって使いましょう。
ComposeのPreviewはAndroud studio Beta Buildバージョンでは使わなくなりました。Lib Version
https://mvnrepository.com/artifact/androidx.compose.ui/ui?repo=google
https://androidx.tech/artifacts/compose.ui/ui-geometry/ChangeLog
- 意外と公式サイトのアップデートは遅いですか、alpah07までは載せています。
- https://developer.android.com/jetpack/androidx/releases/compose-ui#version_100_2
Blog- Medium
今の時点の意見
- Jetpack Composeを勉強していますけど、やはりUIはFlutterの方が早いし、強いと感じでいます。
- 投稿日:2020-12-27T01:51:02+09:00
【Dart】Mapのvalueを使ってソートする
Mapのvalueでソートしたい事があったので調査しました。
SplayTreeMap class
SplayTreeMapのfromにソートしたいmapとcompareを渡せばソートできるみたいです。下記のようなオブジェクトを作成しました。属性idでソートする予定。
Objectclass Employee{ int id; String name; Employee(this.id,this.name); }最初の並び順は下記の様な感じです。
unSortMap<String,Employee> map = Map<String,Employee>(); map['hoge'] = new Employee(1, "sato"); map['huga'] = new Employee(3, "tanaka"); map['piyo'] = new Employee(2, "yamashita"); map.forEach((key, value) { print("key:"+key+" | id:"+value.id.toString()+" | name:"+value.name); });出力
key:hoge | id:1 | name:sato key:huga | id:3 | name:tanaka key:piyo | id:2 | name:yamashitaソート後のMapを作成し再度出力すると…
sortedfinal sortedMap = SplayTreeMap.from( map, (a, b) => map[a].id.compareTo(map[b].id)); sortedMap.forEach((key, value) { print("key:"+key+" | id:"+value.id.toString()+" | name:"+value.name); });出力
flutter: key:hoge | id:1 | name:sato flutter: key:piyo | id:2 | name:yamashita flutter: key:huga | id:3 | name:tanakaオホ
できましたね。