自己紹介
- tomoya0x00
- GitHub/Twitter/Qiita/mstdn.jp
- メインは組み込み系
- 一年ちょっと前に車載業界から小規模のベンチャーに転職
- 最近は業務でAndroid/iOSのアプリ開発も
- 実務経験は少ないのでお手柔らかに…
Mastodon クライアント開発中
- MVVM、Data binding、Realm、mastodon4j
- フル Kotlin
- ベータ配布中
なぜKotlin?
- 型推論
- null安全
- スコープ関数
- 拡張関数
- Collections(Java8相当のStream)
などなど…本当の理由は
Swift書いてたらJava書くのが辛くなってきた!
Kotlinを使ってみて
-
便利だったこと
-
ちょっとはまったこと
便利だったこと
- Realmとの親和性
- ActivityのExtraをlazyで遅延取得
- 拡張関数でUtilityクラス乱立防止
- coroutineで簡単非同期処理
Realmとの親和性 - Entityの定義が楽
// 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
- 追記:all-open compiler pluginで楽できます。たろうさんのブログをご参照。
Realmとの親和性 - プロパティ名の参照
// 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で遅延取得
// 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クラス乱立防止
// 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関数として実現している
- 公式のMotivationでもユーティリティクラスについて触れているので、意図された使い方だと思う
coroutineで簡単非同期処理
// タイムラインの更新処理
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
ちょっとはまったこと
- 無名クラスのインスタンス化
- BaseObservableを継承してData Binding
- coroutineの例外処理
- coroutineのUIスレッド実行
- coroutineのライフサイクル管理
無名クラスのインスタンス化
RecyclerView.Adapterのクリック時メソッドをoverrideしたインスタンスを得たい
// 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) {}
}
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))
}
}
BaseObservableを継承してData Binding
Kotlinはプロパティを定義するとsetter/getterを自動生成する…bindingのアノテーションはどう書くの?
// Mastodonのステータス用ViewModel
class MastodonStatusViewModel(private val status: Status) :
BaseObservable() {
@get:Bindable
val isBoost: Boolean
get() = status.reblog != null
}
coroutineの例外処理
普通にtry catchでいけます
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のように実行するスレッド指定・切替はどうするの?
// タイムラインの更新処理
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スレッドで実行
}
}
coroutineのライフサイクル管理
ActivityのonDestroy時にcoroutineをキャンセルするには?
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
- ただし、複数の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のライフサイクル管理