フル Kotlin で Mastodon クライアントを作ってみて

# 自己紹介

* tomoya0x00
    * GitHub/Twitter/Qiita/mstdn.jp
* メインは組み込み系
* 一年ちょっと前に車載業界から小規模のベンチャーに転職
* 最近は業務でAndroid/iOSのアプリ開発も
    * 実務経験は少ないのでお手柔らかに…

- - -

# Mastodon クライアント開発中

* MVVM、Data binding、Realm、mastodon4j
* **フル Kotlin**
* [ベータ配布中](https://goo.gl/ZIz0ph)

![troutoss.png](https://qiita-image-store.s3.amazonaws.com/0/24788/3dda3a3d-dfaf-8568-2296-37eae3038848.png)

- - -

# なぜKotlin？

* 型推論
* null安全
* スコープ関数
* 拡張関数
* Collections(Java8相当のStream)

などなど…本当の理由は

- - -

# Swift書いてたらJava書くのが辛くなってきた！

- - -

# Kotlinを使ってみて

* 便利だったこと

* ちょっとはまったこと

- - -

# 便利だったこと

* Realmとの親和性
* ActivityのExtraをlazyで遅延取得
* 拡張関数でUtilityクラス乱立防止
* coroutineで簡単非同期処理

- - - 

# Realmとの親和性 - Entityの定義が楽

```kotlin
// Mastodonのアカウント情報
open class MastodonAccount(
        @PrimaryKey open var uuid: String = UUID.randomUUID().toString(),

        open var instanceName: String = "",
        open var userName: String = "",
        open var accessToken: String = ""
) : RealmObject()
```
* コンストラクタの引数にval/varでプロパティ化
* デフォルト引数対応しているので、デフォルト値の定義も楽
* ただし、Kotlinのクラス・プロパティはデフォルト継承不可なので修飾子open付与が必要
    * 参考：[realm-javaのkotlinExample](https://github.com/realm/realm-java/tree/master/examples/kotlinExample)
    * 追記：all-open compiler pluginで楽できます。[たろうさんのブログ](http://taro.hatenablog.jp/entry/2016/12/28/105949)をご参照。

- - - 

# Realmとの親和性 - プロパティ名の参照

```kotlin
// Mastodonのアカウント情報取得
fun loadAccountOf(instanceName: String, userName: String): MastodonAccount? =
        Realm.getDefaultInstance().use { realm ->
            realm.where(MastodonAccount::class.java)
                    .equalTo(MastodonAccount::instanceName.name, instanceName)
                    .equalTo(MastodonAccount::userName.name, userName)
                    .findFirst()?.let { realm.copyFromRealm(it) }
        }
```

* クラス名::プロパティ名.name でプロパティ名をStringとして取得可能
    * JavaだとEntity内にstatic finalで手動定義するか、自動生成するか？

- - - 

# ActivityのExtraをlazyで遅延取得

```kotlin
// Status投稿用のActivity
class PostStatusActivity : AppCompatActivity() {

    lateinit private var binding: ActivityPostStatusBinding
    lateinit private var viewModel: PostStatusViewModel

    private val accountType: AccountType by lazy {
        intent.extras.getString(EXTRA_ACCOUNT_TYPE)?.let { AccountType.valueOf(it) } ?: AccountType.UNKNOWN
    }

    private val accountUuid: String by lazy { intent.extras.getString(EXTRA_ACCOUNT_UUID) }
    private val replyToId: Long? by lazy {
        if (intent.extras.containsKey(EXTRA_REPLY_TO_ID)) intent.extras.getLong(EXTRA_REPLY_TO_ID) else null
    }
    private val replyToUsers: Array<String>? by lazy { intent.extras.getStringArray(EXTRA_REPLY_TO_USERS) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_post_status)
        viewModel = PostStatusViewModel(accountType, accountUuid, replyToId, replyToUsers)
        binding.viewModel = viewModel
    }
}
```

* 関数lazyで遅延初期化できる
* Extraの抽出処理をlazyでくるんであげると普通のプロパティっぽく使える

- - - 

# 拡張関数でUtilityクラス乱立防止

```kotlin
// Status添付メディアの実際のタイプ
// remote-only attachmentの場合、"unknown"となるっぽいのでURLから推測する
fun Attachment.actualType(): String {
    return when (this.type) {
        "image", "video", "gifv" -> this.type
        else -> {
            val mimeType = URLConnection.guessContentTypeFromName(this.actualUrl()) ?: ""
            when {
                mimeType.startsWith("image") -> "image"
                mimeType.startsWith("video") -> "video"
                else -> "unknown"
            }
        }
    }
}
```

* いちいちAttachmentUtilsなどUtilityクラスを増やさなくて良い
* Javaにおけるstatic関数として実現している
    * 参考：[解剖Kotlin ~バイトコードを読み解く~ by yagi](https://speakerdeck.com/sys1yagi/jie-pou-kotlin-baitokodowodu-mijie-ku)
* 公式の[Motivation](https://speakerdeck.com/sys1yagi/jie-pou-kotlin-baitokodowodu-mijie-ku)でもユーティリティクラスについて触れているので、意図された使い方だと思う

- - - 

# coroutineで簡単非同期処理
```kotlin
// タイムラインの更新処理
fun refresh() = async(CommonPool) {
    // getTimeline = (Range) -> Single<Pageable<Status>>
    // 最新の Status 20個を非同期で取得
    pageable = getTimeline(Range(limit = 20)).await()
    pageable?.let {       
        statuses.clear()
        statuses.addAll(it.part)
        launch(UI) { notifyDataSetChanged() }        
    }
}
```

* 同期処理と同じように非同期処理が書けて可読性が良い
* 特にSingleでJSのPromise的に処理している箇所はcoroutineで事足りるのでは？
* **開発中のMastodonクライアントでは一行もRxJavaのコードを書いてません**
* Goのgoroutineのようにchannelもあるので、より複雑な非同期処理も書きやすそう
* 参考：[第5回Kotlin勉強会 async/awaitで快適非同期ライフ @kkagurazaka](https://t.co/Glx9r5MaJm)

- - -

# ちょっとはまったこと

* 無名クラスのインスタンス化
* BaseObservableを継承してData Binding
* coroutineの例外処理
* coroutineのUIスレッド実行
* coroutineのライフサイクル管理

- - -

# 無名クラスのインスタンス化

RecyclerView.Adapterのクリック時メソッドをoverrideしたインスタンスを得たい

```kotlin
// MastodonのAttachment用アダプター
open class MastodonAttachmentAdapter(private val attachments: List<Attachment>) : 
        RecyclerView.Adapter<MastodonAttachmentAdapter.ViewHolder>() {

    open protected fun onClickImage(urls: Array<String>, index: Int) {}
    open protected fun onClickVideo(url: String) {}
    open protected fun onClickUnknown(url: String) {}
}
```

```kotlin
val attachmentAdapter: MastodonAttachmentAdapter =
        object : MastodonAttachmentAdapter(showableStatus.mediaAttachments) {
            override fun onClickImage(urls: Array<String>, index: Int) {
                messenger.send(ShowImagesMessage(urls, index))
            }

            override fun onClickVideo(url: String) {
                messenger.send(OpenUrlMessage(url))
            }

            override fun onClickUnknown(url: String) {
                messenger.send(OpenUrlMessage(url))
            }
        }
```

* 参考：[Y.A.M の 雑記帳 - RecyclerView のクリックをどこで処理するか](http://y-anz-m.blogspot.jp/2016/11/recyclerview.html)

- - -


# BaseObservableを継承してData Binding

Kotlinはプロパティを定義するとsetter/getterを自動生成する…bindingのアノテーションはどう書くの？

```kotlin
// Mastodonのステータス用ViewModel
class MastodonStatusViewModel(private val status: Status) :
        BaseObservable() {

    @get:Bindable
    val isBoost: Boolean
        get() = status.reblog != null
}
```

- - -

# coroutineの例外処理

普通にtry catchでいけます

```kotlin
class MastodonTimelineFragment : Fragment() {

    lateinit private var binding: FragmentMastodonHomeBinding
    private var adapter: MastodonTimelineAdapter? = null

    private fun onRefresh() = launch(UI) {
        binding.timeline.setRefreshing(true)
        try {
            adapter?.refresh()?.await()
        } catch (e: Exception) {
            Timber.e("refresh failed: %s", e)
            Toast.makeText(getContext(), R.string.comm_error, Toast.LENGTH_SHORT).show()
        } finally {
            binding.timeline.setRefreshing(false)
        }
    }

}
```

* launch(){}をtry catchでくくっても例外キャッチできないので注意

- - -

# coroutineのUIスレッドでの実行

RxJavaのように実行するスレッド指定・切替はどうするの？

```kotlin
// タイムラインの更新処理
fun refresh() = async(CommonPool) { // common thread pool で実行
    // getTimeline = (Range) -> Single<Pageable<Status>>
    // 最新の Status 20個を非同期で取得
    pageable = getTimeline(Range(limit = 20)).await()
    pageable?.let {       
        statuses.clear()
        statuses.addAll(it.part)
        launch(UI) { notifyDataSetChanged() } // UIスレッドで実行       
    }
}
```

* 参考：[Guide to UI programming with coroutines](https://github.com/Kotlin/kotlinx.coroutines/blob/master/ui/coroutines-guide-ui.md)

- - -

# coroutineのライフサイクル管理

ActivityのonDestroy時にcoroutineをキャンセルするには？

```kotlin
class MainActivity : AppCompatActivity() {
    val job: Job = Job() // the instance of a Job for this activity

    override fun onCreate() {
        launch(job + CommonPool) {
            // very long and cancellable operation
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        job.cancel() // cancel the job when activity is destroyed
    }
 
    // the rest of code
}
```

* 参考：[Lifecycle and coroutine parent-child hierarchy](https://github.com/Kotlin/kotlinx.coroutines/blob/master/ui/coroutines-guide-ui.md#lifecycle-and-coroutine-parent-child-hierarchy)
* ただし、複数のcoroutineでjobを継承してどれか一つでもキャンセル（例外をtry catchし忘れた場合も）すると他のcoroutineが実行されない
    * キャンセル済みのjobで新たなcoroutineは実行できないっぽい
* RxJava2のCompositeDisposableみたいに、launchの戻り（Job）をaddしていって一気にclearできるクラスを作った方が良いのかも？

- - -

# Kotlinを使ってみて - まとめ

* 便利だったこと
    * Realmとの親和性
    * ActivityのExtraをlazyで遅延取得
    * 拡張関数でUtilityクラス乱立防止
    * coroutineで簡単非同期処理

* ちょっとはまったこと
    * 無名クラスのインスタンス化
    * BaseObservableを継承してData Binding
    * coroutineの例外処理
    * coroutineのUIスレッド実行
    * coroutineのライフサイクル管理