Upgrade to Pro — share decks privately, control downloads, hide ads and more …

coroutinesで非同期ページネーション

 coroutinesで非同期ページネーション

2017/06/29に開催された第6回Kotlin勉強会 @ Sansanの発表スライドです。

Keita Kagurazaka

June 29, 2017
Tweet

More Decks by Keita Kagurazaka

Other Decks in Programming

Transcript

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  6. それ、coroutinesでできるよ

    View full-size slide

  7. 自己紹介
    ● アカウント
    ○ 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

    View full-size slide

  8. 本題の前にcoroutinesの復習

    View full-size slide

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

    View full-size slide

  10. コルーチンの作成
    ビルダー 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でキャンセルできる。

    View full-size slide

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

    View full-size slide

  12. スレッドの切り替え
    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) {
    // 処理
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  17. ようやく本題

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  20. 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
    }

    View full-size slide

  21. 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
    }

    View full-size slide

  22. 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
    }

    View full-size slide

  23. 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 // 作ったチャンネルを返す
    }

    View full-size slide

  24. 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
    }

    View full-size slide

  25. 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
    }

    View full-size slide

  26. 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
    }

    View full-size slide

  27. 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
    }

    View full-size slide

  28. 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
    }

    View full-size slide

  29. 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
    }
    }
    簡単に書けるメソッドもあるよ!

    View full-size slide

  30. 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
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide