Kotlin1.3 まとめ
10/30日にKotlin1.3がりりーすされました。 1.2や1.3RCからの変更点等を主にJVMで動かすときに関わる範囲で適当にまとめようと思います。
変更・追加点
kotlinx.coroutines
がexperimentalからstableにcapturing when subject
引数なしエントリポイント
main
contracts
Result<T>
の実装inline class
の実験的なサポートunsigned integer
の実験的なサポートCollection
まわりでなんかいろいろ拡張関数追加された。annotation calss
をネストして宣言できるようになった。@JvmStatic
と@JvmField
をcompanion object
内で使えるようになったBool
型がcompanion object
を持つようになった。基本型の
companion object
になんかいろいろ追加された。
適当にピックアップしてそれぞれ例を挙げて解説してみます。
引数なしエントリポイント
引数なしのmain関数がエントリポイントになるようになりました。
従来の main(args: Array<String>)
は当然なくなってないです。
初心者フレンドリらしい。
fun main() { println("はろわ!") } //はろわ!
capturing when subject
when
式に使う変数をwhen
式の中で宣言できるようになったやつ。
when (val hoge = createRandomString()) { "hoge" -> println(hoge) }
こんなかんじ
変数のスコープはwhen式の中に制限される。
Collection まわりの拡張関数
なんかいろいろ追加されてる。
Sequence
の orEmpty()
とか Collection
,Map
,List
のisNullOrEmpty()
とかはAPIの一貫性を向上させるために用意されたらしい。
val strArray = arrayOf("aa", "bbb", "ccccccc") strArray.isNullOrEmpty() //false strArray.ifEmpty { arrayOf("hoge", "hoge") } val strSequence: Sequence<String>? = strArray.asSequence() strSequence.orEmpty() requireNotNull(strSequence).ifEmpty { emptySequence() } val strList: List<String>? = strArray.toList() strList.isNullOrEmpty() // false if (!strList.isNullOrEmpty()) { //contractsのおかげでスマートキャストされるようになった。 strList.ifEmpty { listOf("hoge") } }
Result
成功した処理の結果 T
か 失敗した結果 Throwable
を格納する共用体。
早速inline class
で実装されてる。
1.3 m1
時点では SuccessOrFailure
という名前で実装され rc-190
にて Result
に名前が変わった。
自分は偶然 Result
ってtypealias
を作って使っていたので I get kotonaki でした。
基本的にはrunCatching
関数を使って生成するものだと思う。
val result: Result<String>= runCatching { randomeFailuire() //一定確率で例外投げる関数だと思って "hoge" } result.onSuccess(::println) .onFailure(Throwable::printStackTrace) fun Throwable.zero() = 0 val fold: Int = result.fold(String::length, Throwable::zero)
onSuccess
はT->Unit
を受け取りResult<T>
を返します。
成功した場合は当然それを実行。
onFailure
は Throwable->Unit
を受け取りResult<T>
を返します。
fold
は T->R
とThrowable->R
を受け取り成功した場合は前者、失敗した場合は後者を実行しR
を返す関数です。
多分よく使うのはこの3つだと思う。
他にもmap()
とかgetOrDefault()
とか便利なのいろいろ用意されてます。
ちなみにResult
を返す関数を作るには今の所コンパイラに-Xallow-result-return-type
を渡す必要があります。
inline class
primary constructor
で必ず一つの引数を受け取ってそれがコンパイル時にインライン展開されるかもしれない。
当然普通のクラスとは違っていろいろ制限がある。
primary constructor
はval
でアノテートされたプロパティを一つだけ持つ必要がある。backing field
を持つproperty
(よく見る普通のプロパティ)は定義できない。initializer block
を持てない。===
(参照等価性)での比較は不可
他にも制限あったかもしれない。
正確には必ずコンパイル時にプロパティがインライン展開されるわけではなくて、可能な限りインスタンスの生成等を行わずに済むように最適化しようとする感じです。
その最適化を行うために上記の制限があるみたい。
fun main() { val age = Age(14) val ageValue = age.value println(age) println(ageValue) println(age.birthYear()) } inline class Age(val value: Int) { fun birthYear(): LocalDateTime { //todo 雑 val now = LocalDateTime.now() return LocalDateTime.of( now.year - value, now.month, now.dayOfMonth, now.hour, now.minute ) } }
こんな感じのコードはコンパイルされると
fun main() { val age: Int = Age.constructor_impl(14) val var2: Age = Age.box_impl(age) println(var2) println(age) val var3:LocalDateTime = Age.birthYear_impl(age) println(var3) }
こんな感じになります。
クラスの実装は割愛です。
inline class
を使用するにはコンパイラに引数として-XXLanguage:+InlineClasses
を渡す必要があります。
kotlinx.coroutines
これは例とか出してるとやたら長くなりそうなので 1.0.0
での変更点を自分が実際に使った際に気づいたところを軽くまとめようと思います。
グローバルで
suspend property
なcoroutineContext
が消えてCoroutineScope
のプロパティになった。launch()
等の関数がGlobalScope
オブジェクトの拡張関数になった。launch()
やasync
からparent: Job?
の引数が消えた。async(coroutineContext + CommonPool){}
とかしてたと思うけどCommonPool
消えのでForkJoinPool.commonPool().asCoroutineDispatcher()
とかする。launch(UI){ }
とかしてたと思うけどUI
消えた。Dispatchers.Main
に置き換える
launch
と async
くらいしか使わないから気づいたこと少なすぎた…
んぐぉぉぉぉぉ……
contracts
コンパイラになんかいろいろ教えてあげるやつ。
これのおかげで特定のメソッド呼び出しの結果が true
ならレシーバがnon-null
型にスマートキャストされたりとか、ラムダ式が一度だけ同期的に呼び出されることを保証してその中で外のスコープの変数を初期化できるようになったりとかする。
早速Array<T>?.isNullOrEmpty()
とか requireNotNull()
とか T.run()
とかに使われている。
使い方の例として一度だけ呼ばれるラムダ式のcontracts
を挙げ…ようとおもったけどなんかビルド通らないな…んんん~~また今度。
1.3-m2
が出て即試したときは普通にできたけどBackend Internal error
吐きまくってなんかビルドできないので寝ます。
標準ライブラリのrequire
関数とか使う分には問題なく動きます。
以上
なんか変なこと書いてたりしてたら指摘していただければ嬉しいです。
参考
非同期処理とFragment
なんか非同期処理してそれが終わったらFragmentを生成してFragmentManagerに追加するみたいな処理するときってあれですよね。
onSaveInstanceStateの後にやってしまうと怒られますよね。
それ回避用のユーティリティークラス作ってみたのでメモ書きです。
Android Architecture Componentsのlifecycleを使用します。
import android.arch.lifecycle.Lifecycle import android.arch.lifecycle.LifecycleObserver import android.arch.lifecycle.LifecycleOwner import android.arch.lifecycle.OnLifecycleEvent interface IOnActiveRunner { fun setOwner(owner: LifecycleOwner) fun runOnActive(handle: () -> Unit) } class OnActiveRunner : IOnActiveRunner { private val mObserver = OnActiveLifeCycleObserver() private var mIsOwnerInitialized = false override fun setOwner(owner: LifecycleOwner) { if (!mIsOwnerInitialized) owner.lifecycle.addObserver(mObserver) else throw IllegalStateException() mIsOwnerInitialized = true } override fun runOnActive(handle: () -> Unit) { if (!mIsOwnerInitialized) throw IllegalStateException() mObserver.run(handle) } class OnActiveLifeCycleObserver : LifecycleObserver { private var isSafe = true private val tasks = ArrayList<() -> Unit>() fun run(task: () -> Unit) { if (isSafe) { task() } else { tasks.add(task) } } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun onPause() { isSafe = false } @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onResume() { isSafe = true tasks.forEach { it() } tasks.clear() } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy() { isSafe = false } } }
単純にonPause()が呼ばれたら与えられた関数オブジェクトをリザーブしてonResume()で実行するってだけです。
インターフェースを用意したのは移譲を使ってActivity等で楽に使えるようにするためです。
使うときは…
class HogeActivity : AppCompatActivity(), IOnActiveRunner by OnActiveRunner() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //runOnActiveを呼ぶ前にsetOwnerしておく setOwner(this) //例としてkotlinのcroutine使います launch(UI) { //async/awaitつかったりとか runOnActive { //何かしらの処理 } } } }
みたいな感じで使います。
非同期処理のコールバックでonPause後に呼ばれたくないような処理を行なう時に使う感じです。
最たるものはFragmentTransaction周りの処理だと思います。
他にもonPause後に呼ばれたくないような処理っていくらでもあると思うので使い所は結構多いのでは。
いや、こんなの用意しなくてもそれできるぜって情報があれば教えていただければ幸いです。
commitAllowingStateLossは無しの方向で
Androidでカメラアプリとかから画像取得
なんかやたらと intent.extras.get("data") で画像が取れるってブログが引っかかるので
ちゃんと撮影した画像のサイズのままのBitmapとFileが取れる方法をメモっとこうかと思います。
まずこんな関数を用意します
/** * @param ctx 適当なコンテキスト * @param directory ディレクトリの名前 例 "hoge/fuga" * @param fileName 拡張子抜きのファイルの名前 例 "piyo" * @param mimeType mimeType 例 "image/png" * @return 確保したUriとそのパスのPair */ fun reserveContentUri(ctx: Context, directory: String, fileName: String, mimeType: String): Pair<Uri, String> { val extension = MimeTypeMap.getSingleton() .getExtensionFromMimeType(mimeType)//mimeTypeから拡張子を得る val name = fileName + ".$extension" val directoryNames = directory.split("/") var directoryPath = Environment.getExternalStorageDirectory().absolutePath //ディレクトリを作るのと存在確認 directoryNames.forEach { directoryName -> directoryPath += "/$directoryName" val file = File(directoryPath) if (!file.exists() && !file.mkdir()) throw IOException("failed directory creation") } //contentResolverにinsert val path = "${File(directoryPath).absolutePath}/$name" val values = ContentValues().apply { put(MediaStore.Images.Media.TITLE, name) put(MediaStore.Images.Media.DISPLAY_NAME, name) put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) put(MediaStore.Images.Media.DATA, path) } return ctx.contentResolver .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) to path }
上記のコードだと問題がありました。
これ、カメラアプリの起動キャンセルしたり失敗したるすると余計なものがcontentResolverにinsertされちゃいます。
下記に修正版を
fun createSaveUri(directory: String, fileName: String, mimeType: String): Pair<Uri, String> { val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) val name = fileName + ".$extension" val directoryNames = directory.split("/") var directoryPath = Environment.getExternalStorageDirectory().absolutePath directoryNames.forEach { directoryName -> directoryPath += "/$directoryName" val file = File(directoryPath) if (!file.exists() && !file.mkdir()) throw IOException("directory creation failure") } val path = "${File(directoryPath).absolutePath}/$name" return Uri.fromFile(File(path)) to path }
この関数で保存先を確保しておきます。
そしたらAndroidManifest.xmlには
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
のpermissionを追加します。
そしたらこんな感じで使う
class HogeActivity : AppCompatActivity() { private var mPath: String = "" private var mUri: Uri = Uri.EMPTY companion object { private val REQUEST_CODE_CAPTCHA = 100 private val SAVED_LATEST_CAPTCHA_IMAGE_PATH = "SAVED_LATEST_CAPTCHA_IMAGE_PATH" private val SAVED_LATEST_CAPTCHA_IMAGE_URI = "SAVED_LATEST_CAPTCHA_IMAGE_URI" } override fun onSaveInstanceState(outState: Bundle) { outState.putString(SAVED_LATEST_CAPTCHA_IMAGE_PATH, mPath) outState.putParcelable(SAVED_LATEST_CAPTCHA_IMAGE_URI, mUri) super.onSaveInstanceState(outState) } override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) mPath = savedInstanceState.getString(SAVED_LATEST_CAPTCHA_IMAGE_PATH) mUri = savedInstanceState.getParcelable(SAVED_LATEST_CAPTCHA_IMAGE_URI) } //このメソッドを適当に呼ぶ private fun executeCaptchaApp() { //val (uri, path) = reserveContentUri(ctx, "numetter/captcha", "${System.currentTimeMillis()}", "image/jpeg") val (uri, path) = createSaveUri("numetter/captcha", "${System.currentTimeMillis()}", "image/jpeg") mPath = path mUri = uri val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { putExtra(MediaStore.EXTRA_OUTPUT, uri) } startActivityForResult(intent, REQUEST_CODE_CAPTCHA) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode != Activity.RESULT_OK) return when (requestCode) { REQUEST_CODE_CAPTCHA -> { val bitmap = BitmapFactory.decodeFile(mPath)//撮影した画像のBitmap val imageFile = File(mPath)//撮影した画像のFile val imageView = findViewById<ImageView>(R.id.image) imageView.setImageURI(mUri)//Uriを元に画像をセット } } } }
取得された画像のサイズとか考慮してなかったり、パーミションのチェックと要求をしていなかったり雑ではありますが 大方こんな感じのコードでカメラアプリ等で撮影された画像を取得できます。
AndroidN以降はFileProviderを使用してFileからUriを取得する必要があります。
Android Architecture Components + Kotlin Croutine
Androidでasync awaitする時のライフサイクルとのバインド 初ブログで慣れませんが頑張ります。
ごめんなさいですが、ここではAndroid Architecture Componentsについての詳しい解説はしないです…
Android Arch(以下略)については公式のドキュメントを見ると良いと思います。
Android Architecture Components
例としてここでは画像のダウンロードと表示を行なうようなプログラムを書いていきます。
(画像のダウンロード処理の実装は省きます)
前提として画像はActivity起動時の一度しかダウンロードせず、さらにそれはキャッシュされるという条件で作っていきます。
Android Arch(以下略)のViewModelとLiveDataを使用しているならば、以下のようにすることでそれは実装できます。
//とりあえずこんな感じの拡張関数を用意 //簡単にLiveDataをmapできるようにして(Listのmapとかと大体同じ感じ) fun <T, R> LiveData<T>.map(func: (T) -> R): LiveData<R> = Transformations.map(this, func) fun <T> MutableLiveData<T>.mediate() = map { it }
画像を表示したいActivityで使うViewModelを定義
//ImageRepositoryという画像のDLとキャッシュを行なうためのクラスが外からDIされるということにします class HogeViewModel @Inject constructor(private val mImageRepository: ImageRepository) : ViewModel() { //これの値をViewModel内で弄る private val mImageLiveData = MutableLiveData<Bitmap>() //mImageLiveDataの変更を仲介して伝播する為のLiveDataを外に公開 val imageLiveData = mImageLiveData.mediate() //画像のダウンロードを開始して結果をimageLiveDataまで伝えるメソッド fun loadImage(url: String) { AsyncTask.execute { val bitmap = mImageRepository.downloadOrGet(url)//画像をDLしてキャッシュする処理 mImageLiveData.postValue(bitmap)//非同期スレッドから値を変更するときはpostValue } } }
画像を表示したいActivity
class HogeActivity : AppCompatActivity() { //ViewModelに何かしらをDIしたい場合このコードでは無理ですが、例なのであしからず。 //生成されたActivityのライフサイクルとバインドされたViewModelが取ってこれます。 //つまり画面回転等でActivityが再生成されたとしても同一のViewModelを持ってきます。 val model by lazy { ViewModelProviders.of(this).get(HogeViewModel::class.java) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_hoge) val imageView = findViewById<ImageView>(R.id.image) model.imageLiveData.observe(this, Observer { imageView.setImageBitmap(it) }) //最後の変更がobserveした時に渡ってくるのでloadImageするのは一回でおk if (savedInstanceState == null) model.loadImage("hogehogeImageUrl") } }
雑な実装でエラー処理だとか色々と省いてますが、ざっとこんな感じにすれば非同期的に画像をダウンロードしてそれを表示することができるとおもいます。
(適当に書いたのでできなかったらごめんなさい。)
しかし、ModelViewには「外に公開する用のLiveData」と「公開せずに値の変更を行なうためのLiveData」、
Activityでは「observe」と「画像のダウンロードを開始するためのメソッド呼び出し」と画像一回ダウンロードして表示するためだけに少々大仰になってしまいました。
更に画像をダウンロードするために使用している「ImageRepositoryのdownloadOrGet」メソッドで画像のキャッシュも行っているとなればLiveDataを使用する必要性は薄いです。
(LiveDataは最後の変更をobserve時に渡してきます。これにより結果的に画面回転時の復帰が可能です。)
そこでKotlinのCoroutineを用いてできるだけ簡潔になるようにしてみます。
class HogeViewModel @Inject constructor(private val mImageRepository: IImageRepository) : ViewModel() { //Deferred<Bitmap>が返ります。 //これをActivity側でawait()します。 fun loadImage(url: String) = async { mImageRepository.downloadOrGet(url) } }
class HogeActivity : AppCompatActivity() { val model by lazy { ViewModelProviders.of(this).get(HogeViewModel::class.java) } private var mLoadImageJob: Job? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_hoge) val imageView = findViewById<ImageView>(R.id.image) //Jobを保存 mLoadImageJob = launch(UI) { val image = model.loadImage("hogehogeImageUrl").await() imageView.setImageBitmap(image) } } override fun onDestroy() { mLoadImageJob?.cancel()//Activityが死んだら処理をキャンセル super.onDestroy() } }
簡潔になりましたが、LiveDataを用いていたときと違いって、onDestroy()でJobの中断処理をしないといけなくなったので何やら辛い感じになってしまいました。
この例では画像一枚を落とすだけなのでこれで済んでいますが、複数のJobを扱うようになると、Jobを生成する度にListに加え、Jobが終わればListから外す、等の処理を書かなければならなくなりますし大変面倒ですし、書き忘れ等があればとても悲しいです。
ですが、幸いAndroid Arch(以下略)には、ライフサイクルを扱うためのモジュールがあるので、これを使えばなんとかなりそうです。 試してみましょう。
Jobとライフサイクルをバインドする関数
//与えられたLifecycleOwnerのライフサイクルのonDestroyで自動的にキャンセルされるJobを作る fun bindLaunch(owner: LifecycleOwner, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit) = launch(UI, start, block).apply { val observer = createLifecycleObserver(this) val lifecycle = owner.lifecycle lifecycle.addObserver(observer) //ジョブが終わればLifecycleOwnerからObserverをremove invokeOnCompletion { lifecycle.removeObserver(observer) } } //与えられたjobを元にLifecycleObserverを生成 private fun createLifecycleObserver(job: Job) = object : LifecycleObserver { val mRef = WeakReference<Job>(job) //onDestroyにバインドされるメソッド @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy() { val j = mRef.get() ?: return if (!j.isCompleted) job.cancel()//Jobをキャンセル } }
LifecycleOwnerからニョキッと生やす
//上で定義したbindLaunchのLifecycleOwnerの拡張関数版を定義 //AppCompatActivityはLifecycleOwnerを継承しているのでいい感じに呼び出せます。 fun LifecycleOwner.bindLaunch(start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit) = bindLaunch(this, start, block)
こんな感じで呼び出すだけでLifecycleOwnerとJobをバインドしてくれる関数を定義してやれば……
class HogeActivity : AppCompatActivity() { val model by lazy { ViewModelProviders.of(this).get(HogeViewModel::class.java) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_hoge) val imageView = findViewById<ImageView>(R.id.image) bindLaunch { val image = model.loadImage("hogehogeImageUrl").await() imageView.setImageBitmap(image) } } }
ライフサイクルについてあまり考えずにasync/awaitをActivity上で使えるようになりました。
ただこれだけだとエラー処理がしづらいところがあるので、「非同期処理の結果と例外が発生すればその例外情報を保持するようなクラスを作ってそのクラスにラッピングして返す」というような工夫が必要かもです。
(なにかこれ以外でいい方法があれば教えてほしいです。)
と、こんなかんじでAndroid Arch(以下略)とkotlin Coroutineを組み合わせてAndroidのライフサイクルと仲良くする方法を書きましたが。 この中で「ここをこうすればもっと良い」や「ここがおかしい」、「別にこんなことしなくても、もっといい方法がほかにあるよ」っていうのがあれば教えていただければ幸いです。 ここの日本語がおかしいとかの添削も大歓迎です。(驚くべきことに僕は日本人です)
以下、Jobとライフサイクルのバインドを行なう関数のあれのGistです。
”_BindLaunch.kt”
これがブログですか
なんとなくブログ始めました。
レイアウトとかよくわからんです。頑張ります。
手始めにお題スロットがあったので回してみました。お題は「コンビニ」です。
コンビニ、すごいですよね。
通販の決済できたり光熱費払ったりできるし、なんならパンツとシャツも買える。
近所のコンビニの店員さんもたまにチキンくれるのでコンビニがあれば生きていける気がします。
しかし、僕はコンビニに入ると高確率でなんとかPlayカードとかいうのと引き換えに諭吉が消えてそれをスマホで無に還してしまうのでコンビニが近くにあると生きていけなくる気がします。最近は崩壊3rdというゲームで諭吉を無に還したので…しばらくもやしで生きていきます。
コンビニについて深い造詣があるわけじゃないので全く話が広がらない…
ブログが続くとすれば主にAndroidアプリ開発とかのブログになりそうです。
よろしくおねがいします。