Slide 1

Slide 1 text

coroutinesで 非同期ページネーション 2017/06/29 第6回 Kotlin勉強会@Sansan Keita Kagurazaka

Slide 2

Slide 2 text

こんな実装、経験ありませんか?

Slide 3

Slide 3 text

1. APIやDBからLIMIT分だけデータを読み込み 2. 読み込んだデータをUIに一覧表示 3. 下までスクロールしていくと次のページを読み込み

Slide 4

Slide 4 text

いわゆる無限スクロール 1. APIやDBからLIMIT分だけデータを読み込み 2. 読み込んだデータをUIに一覧表示 3. 下までスクロールしていくと次のページを読み込み

Slide 5

Slide 5 text

無限スクロールの面倒Point ● 現在のページ番号などの管理を呼び出し側が行わなければ ならない ● 下までスクロールしたら次を呼ぶというボイラーテンプレートな ScrollListenerが必要 次のアイテムを要求したらいい感じに 読み込んで返してくれないものか

Slide 6

Slide 6 text

それ、coroutinesでできるよ

Slide 7

Slide 7 text

自己紹介 ● アカウント ○ Twitter: @kkagurazaka ○ Github: k-kagurazaka ● Sansan Android developer ● Kotlin Love! RxProperty for Android https://github.com/k-kagurazaka/rx-property-android AsyncPermissions https://github.com/k-kagurazaka/async-permissions

Slide 8

Slide 8 text

本題の前にcoroutinesの復習

Slide 9

Slide 9 text

coroutines (コルーチン) とは ● 中断・再開可能な関数のようなもの ○ Threadクラスのように作成して、実行する ● 中断している間はスレッドをブロックしない ○ non-blocking ● 特定のスレッドに紐付かない ○ 中断前と再開後で別スレッドで動かせる ● 中断時・完了時に値を返せる

Slide 10

Slide 10 text

コルーチンの作成 ビルダー launch async 戻り値 Job Deferred 使い方 val job = launch(CommonPool) { // 処理 } job.join() val deferred = async(CommonPool) { // T型を返す処理 } val result = deferred.await() 説明 値を返さないコルーチンを作成するビル ダー。 Jobをjoinすると完了まで中断、 cancelで キャンセルできる。 T型の値を返すコルーチンを作成するビル ダー。 Deferredをawaitすると完了まで中断、 cancelでキャンセルできる。

Slide 11

Slide 11 text

中断点 = suspending function / lambda ● suspending function ○ suspend fun hoge() { } ● suspending lambda ○ val lambda: suspend () -> Unit = { } ● suspending function / lambda は suspending function / lambda からしか呼べない

Slide 12

Slide 12 text

スレッドの切り替え suspend fun run(context: CoroutineContext, block: suspend () -> T): T ● 使い方 suspend fun loadItems() { val items = run(CommonPool) { getItems() } run(UI) { itemList.addAll(items) } } suspend fun taskAonUI() = run(UI) { // 処理 }

Slide 13

Slide 13 text

コルーチンの中断と再開例 // コルーチン作成&開始 launch(CommonPool) { val hoge = ... taskAonUI() // UIスレッドでtaskAを実行中はこのコルーチンは中断 val huga = … // taskAが完了するとここからコルーチンを再開 } 中断している間は、コルーチンはスレッドをブロックしない ● taskAの実施中に別のコルーチンがスレッドを使える ● taskA完了後もまだ使われてたら別のスレッドで再開する

Slide 14

Slide 14 text

複数の値を扱いたいときは?

Slide 15

Slide 15 text

Channel

Slide 16

Slide 16 text

Channelとは ● ブロックの代わりに中断するBlockingQueue ● suspend fun send(value: T) ○ Channelのcapacityに空きがあれば送信 ○ なければ空くまで中断 ● suspend fun receive(): T ○ Channelに値があれば受信 ○ なければ値が来るまで中断 capacityが0の場合はsendとreceiveが揃ったら送受信

Slide 17

Slide 17 text

コルーチン間でのデータ受け渡し val channel = Channel() // デフォルトではcapacity = 0のチャンネル launch(CommonPool) { repeat(5) { println(channel.receive()) } // 5回受信 } launch(CommonPool) { repeat(10) { channel.send(it) } // 0から4まで送信、5の送信時に中断 }

Slide 18

Slide 18 text

ようやく本題

Slide 19

Slide 19 text

Channelを使って ページネーションを実現する

Slide 20

Slide 20 text

suspend fun getBooks(page: Int, limit: Int): List // APIやらDBやらから取得

Slide 21

Slide 21 text

suspend fun getBooks(page: Int, limit: Int): List fun loadBooks(): ReceiveChannel { // 受信 (receive) 専用 Channel val channel = Channel() launch(CommonPool) { var page = 0 while (true) { val books = getBooks(page++, LIMIT) if (books.isEmpty()) break books.forEach { channel.send(it) } if (books.size < LIMIT) break } channel.close() } return channel }

Slide 22

Slide 22 text

suspend fun getBooks(page: Int, limit: Int): List fun loadBooks(): ReceiveChannel { val channel = Channel() // capacity = 0 の Channel 作成 launch(CommonPool) { var page = 0 while (true) { val books = getBooks(page++, LIMIT) if (books.isEmpty()) break books.forEach { channel.send(it) } if (books.size < LIMIT) break } channel.close() } return channel }

Slide 23

Slide 23 text

suspend fun getBooks(page: Int, limit: Int): List fun loadBooks(): ReceiveChannel { val channel = Channel() launch(CommonPool) { // スレッドプールで動くコルーチン起動 var page = 0 while (true) { val books = getBooks(page++, LIMIT) if (books.isEmpty()) break books.forEach { channel.send(it) } if (books.size < LIMIT) break } channel.close() } return channel }

Slide 24

Slide 24 text

suspend fun getBooks(page: Int, limit: Int): List fun loadBooks(): ReceiveChannel { val channel = Channel() launch(CommonPool) { var page = 0 while (true) { val books = getBooks(page++, LIMIT) if (books.isEmpty()) break books.forEach { channel.send(it) } if (books.size < LIMIT) break } channel.close() } return channel // 作ったチャンネルを返す }

Slide 25

Slide 25 text

suspend fun getBooks(page: Int, limit: Int): List fun loadBooks(): ReceiveChannel { val channel = Channel() launch(CommonPool) { var page = 0 while (true) { // 無限ループで val books = getBooks(page++, LIMIT) if (books.isEmpty()) break books.forEach { channel.send(it) } if (books.size < LIMIT) break } channel.close() } return channel }

Slide 26

Slide 26 text

suspend fun getBooks(page: Int, limit: Int): List fun loadBooks(): ReceiveChannel { val channel = Channel() launch(CommonPool) { var page = 0 while (true) { val books = getBooks(page++, LIMIT) // ページ数をincrementしながら取得 if (books.isEmpty()) break books.forEach { channel.send(it) } if (books.size < LIMIT) break } channel.close() } return channel }

Slide 27

Slide 27 text

suspend fun getBooks(page: Int, limit: Int): List fun loadBooks(): ReceiveChannel { val channel = Channel() launch(CommonPool) { var page = 0 while (true) { val books = getBooks(page++, LIMIT) if (books.isEmpty()) break books.forEach { channel.send(it) } // 取得したものを Channel に送信 if (books.size < LIMIT) break } channel.close() } return channel }

Slide 28

Slide 28 text

suspend fun getBooks(page: Int, limit: Int): List fun loadBooks(): ReceiveChannel { val channel = Channel() launch(CommonPool) { var page = 0 while (true) { val books = getBooks(page++, LIMIT) if (books.isEmpty()) break // 空か books.forEach { channel.send(it) } if (books.size < LIMIT) break // LIMIT より少なかったら無限ループから抜ける } channel.close() } return channel }

Slide 29

Slide 29 text

suspend fun getBooks(page: Int, limit: Int): List fun loadBooks(): ReceiveChannel { val channel = Channel() launch(CommonPool) { var page = 0 while (true) { val books = getBooks(page++, LIMIT) if (books.isEmpty()) break books.forEach { channel.send(it) } // 受信されないかぎりここで中断 if (books.size < LIMIT) break } channel.close() } return channel }

Slide 30

Slide 30 text

suspend fun getBooks(page: Int, limit: Int): List fun loadBooks(): ReceiveChannel = produce(CommonPool) { var page = 0 while (true) { val books = getBooks(page++, LIMIT) if (books.isEmpty()) break books.forEach { send(it) } if (books.size < LIMIT) break } } 簡単に書けるメソッドもあるよ!

Slide 31

Slide 31 text

suspend fun getBooks(page: Int, limit: Int): List fun loadBooks(): ReceiveChannel = produce(CommonPool) { var page = 0 while (true) { val books = getBooks(page++, LIMIT) if (books.isEmpty()) break books.forEach { send(it) } if (books.size < LIMIT) break } } // 20個読み込む (LIMIT < 20でも問題なし) repeat(20) { val book = channel.receiveOrNull() ?: return@repeat }

Slide 32

Slide 32 text

ページネーションを意識せずに 必要な数だけ要求できる!

Slide 33

Slide 33 text

まとめ ● coroutineは中断可能なThreadのようなもの ● 中断中はスレッドをブロックしない ● coroutine間で値をやりとりするときはChannelを使う ● ページネーションを抽象化できたり、応用は様々 ○ RxJavaのdebounceとかも書けるよ!

Slide 34

Slide 34 text

Thanks!