- 投稿日:2019-12-18T23:53:50+09:00
Android View Bind Library 比較
AndroidにおいてViewをBindする方法はいくつかあります。
今回は、その手法の紹介および比較をしたいと思います。この比較の話はGoogle I/O 2019で紹介されていたものです。
https://www.youtube.com/watch?v=Qxj2eBmXLHgその話を各手法を具体的に紹介しつつ、実際にどういったところに良し悪しがあるのかを説明したいと思います。
各サンプルコードは一部抜粋したものになっていますのでご了承ください。
実際に動くコードはGitHubに上げていますのでそちらをご参照ください。https://github.com/sadashi-ota/AndroidViewBindSample
前提の環境は以下の通りです
- Android Studio 3.6 Beta 5
- Kotlin 1.3.50
- その他ライブラリについては各種Branchに上がってる
app/build.gradle
をご参照くださいfindViewById
入門書にも乗ってる一番基本的な方法で、ActivityやViewにある
findViewById
メソッドを使う方法です。具体的には以下のような実装になります。
layout/activity_main.xml<LinerLayout android:id="@+id/root_layout"> <Button android:id="@+id/submit_button" /> </LinerLayout>MainActivity.ktclass MainActivity : AppCompatActivity() { lateinit var button: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) button = findViewById(R.id.submit_button) button.setOnClickListener(::submit) } private fun submit(view: View) { // do something println("Click $view") } }ただし、もし以下のように
findViewById
で渡すIDを別のViewのものを渡してしまうと、クラッシュしてしまったり、意図しない挙動で動いてしまったり(別のViewが変数に代入されたり)してしまいます。
開発する上では注意が必要です。MainActivity.ktclass MainActivity : AppCompatActivity() { lateinit var button: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // ↓ LinerLayoutをButtonに代入しようとするので、ClassCastExceptionがスローされる button = findViewById(R.id.root_layout) } }この方法は、ビルド速度的には特に懸念はありませんが、ボイラープレートコードが増え、取得できるViewの型もわからないため、型の安全性にも欠けるので、オススメはできません。
Butter Knife
次はButter Knifeです。
かなりお世話になっている or いた方も多いのではないかと思いますが、後述するView Binding
の登場により、とうとう開発は終了する方針になりました。README.mdAttention: Development on this tool is winding down. Please consider switching to view binding in the coming months.今から新規でアプリを作る際にButterKnifeを採用することはないと思いますが、かなりの利用実績はあると思います。
Butter Knifeでは以下のように、アノテーションを使って、バインドするViewのIDを指定します。
Viewだけでなくクリックのメソッドやリソースなどもバインドすることが可能です。
詳細はドキュメントをご参照ください。layout/activity_main.xml<LinerLayout android:id="@+id/root_layout"> <Button android:id="@+id/submit_button" /> </LinerLayout>MainActivity.ktclass MainActivity : AppCompatActivity() { @BindView(R.id.submit_button) lateinit var button: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) ButterKnife.bind(this) println("Button is $button") } @OnClick(R.id.submit_button) fun submit(view: View) { // do something println("Click $view") } }ただし、こちらについても
findViewById
と同じようにIDの指定を間違えるとクラッシュなどに繋がりますので、こちらについても注意が必要です。MainActivity.ktclass MainActivity : AppCompatActivity() { @BindView(R.id.root_layout) // ← IDが違う lateinit var button: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) ButterKnife.bind(this) // ← 呼び出してバインドする際にクラッシュする } }
findViewById
に比べるとかなりボイラープレートコードが少なくなるため、重宝された方も多いかと思います。また、Reflection
も利用していないので、実行時のパフォーマンスの面でも特に問題はありません。
しかし、こちらも型の安全性であったり、Annotation Proessing
を利用しているためビルド時間が長くなったり、といった懸念があります。Kotlin Android Extensions
続いてKotlin Android Extensionsです。
こちらは利用することで、レイアウトファイルで指定したIDでメンバ変数のように扱うことができます。layout/activity_main.xml<LinerLayout android:id="@+id/root_layout"> <Button android:id="@+id/submit_button" /> </LinerLayout>MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) submit_button.setOnClickListener(::submit) println("Button is $submit_button") } private fun submit(view: View) { // do something println("Click $view") } }こちらの場合は、他のIDを指定してしまった時も型が違うのでビルド時にある程度ミスに気づけます。
ただし、別のViewのIDを使ってしまった時はViewが見つからないため、実行時にNullPointerExceptionなどが発生することがあるので注意しましょう。layout/activity_sub.xml<LinerLayout> <Button android:id="@+id/confirm_button" /> </LinerLayout>MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // confirm_buttonは別のレイアウトのIDなのでNullPointerExceptionが発生する confirm_button.setOnClickListener(::submit) } private fun submit(view: View) { // do something println("Click $view") } }
Kotlin Android Extensions
は生成されるJavaコードを見ればわかりますがfindViewById
のキャッシュを実装したシンプルなものになっています。
ボイラープレートコードもより少なく、ビルド速度も懸念はありません。
また、型の安全性についてもをfindViewById
を直接扱うよりも安全に扱えるため、かなり使いやすいと思います。Data Binding
次は
Data Binding
です。詳細は公式ドキュメントを見ていただくのが一番いいとは思いますが、簡単な使い方から紹介したいと思います。
Data Binding
は今まで紹介した手法とは異なり、layout
のXMLファイルでバインドを行います。
まずはレイアウトファイルのルートタグを<layout>
タグで始めます。その後、<data>
要素と通常のView
の要素を宣言します。
<data>
要素にはこのレイアウトにバインドするクラスを記述し、データをどのように反映するかは@{}
構文を使用します。構文の詳細はLayouts and binding expressionsを参考にしてください。レイアウトファイルを作成すると、バインディングクラスが生成されます。
生成されるクラス名はactivity_main.xml
から生成する場合はActivityMainBinding
となります。
Binding
クラスの生成については、生成されたBinding
クラスにあるinflate
メソッドか、DataBindingUtil
を利用しましょう。以下、
DataBindingUtil
を用いたサンプルのコードになります。SampleBindingData.ktdata class SampleBindingData( val text: String )MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.sample = SampleBindingData("Sample") } }layout/activity_main.xml<layout> <data> <variable name="sample" type="jp.sadashi.sample.viewbind.SampleBindingData" /> </data> <LinerLayout> <TextView android:text="@{sample/text}"> </LinerLayout> </layout>また、イベント処理についても同様にバインドすることができます。
MainPresenter.ktclass MainPresenter() { fun submit(view: View) { // do something println("Click $view") } }MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.presenter = MainPresenter() } }layout/activity_main.xml<layout> <data> <variable name="presenter" type="jp.sadashi.sample.viewbind.MainPresenter" /> </data> <LinerLayout> <Button android:onClick="@{presenter::submit}"> </LinerLayout> </layout>
Data Binding
を利用すれば、ボイラープレートコードも少なく、型も安全に使えます。
また、LiveData
やViewModel
といったJetPack
の機能を利用したMVVMアーキテクチャでの実装を考えているのでしたら、こちらを利用するのが良いかと思います。
懸念としてはこちらもAnnotation Proessing
を利用しているため、ビルド速度が遅くなるという点があります。View Binding
最後にViewBindingの紹介です。
こちらはAndroid Studio 3.6 Canary 11 以降から使える機能で、DataBindingと同様にレイアウトファイルごとにBindingクラスが生成されます。
Bindingクラスにはレイアウトファイルで設定したIDヘの直接参照が取り込まれていて、DataBindingのViewへのアクセスに特化したものになっています。
詳しくは公式ドキュメントをご参照ください。使い方のサンプルコードは以下の通りです。
生成されたBindingクラスには必ず
root
というViewがあるのでそれをActivityならsetContentViewすればOKです!layout/activity_main.xml<LinerLayout> <Button android:id="@+id/submit_button" /> </LinerLayout>MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.submitButton.setOnClickListener(::submit) } private fun submit(view: View) { // do something println("Click $view") } }
View Binding
を利用することで、ボイラープレートコードも減り、型の安全性も担保できます。
また、ビルド速度についてもAnnotation Proessing
を利用していないため、十分だと思われます。
Data Binding
と異なり、<layout>
タグも用いる必要がないので、導入もしやすいかと思います。比較
Google I/O 2019のスライドで紹介されていた表がこちらです。
個人的な見解をまとめます。
- Kotlin Android Extensions
- 記述のしやすさが良い
- 厳格な型の安全性をそこまで求めないなら十分
- View Binding
- 型の安全性を求めるならこれ
- ただし、まだAndroid Studio 3.6はベータ版
- Data Binding
- データのバインディングまで利用するなら機能的にもこれ一択
さいごに
最後までお読みいただきありがとうございました!
明日は@azawakhさんによる「Node.js 13.2.0 で--experimental-modules外れたのでESMを試してみた」です。お楽しみに!
- 投稿日:2019-12-18T23:53:14+09:00
Jetpack Composeでサンプルアプリを作ってみる
Jetpack Composeとは
AndroidのUIは基本的にはXMLで作成していきますが、
Jetpack Compose
はGoogle I/O 2019で発表された宣言型のUIツールキットです。今は、まだプレアルファ版ですが2020年を目標にベータ版が公開される予定です。作成するサンプルアプリ
今回は、Qiitaの記事を一覧表示するサンプルアプリを作成していきたいと思います。
※サンプルアプリの案は入社当時にお世話になったKotlinスタートブックにあったものを参考にしました)環境
Android Studio4.0 Canary6
targetSdkVersion 29プロジェクト作成
Start a new Android Studio Project
を押して、Empty Compose Activity
を選択- プロジェク名を入力し、Finish
Jetpack Composeの使い方
基本的な使い方
@Composable
を付けた関数の中に、コンポーネントを記述する- setContentの中で
@Composable
を付けた関数を呼び出すMainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Greeting(name = "Android") } } } @Composable fun Greeting(name: String) { Text (text = "Hello $name!") }プレビュー画面で確認
また、Android Studio4.0 Canary1からIDE上でプレビューを見ることができます。
方法としては、確認したいコンポーネントの@Composable関数
に@Preview
を付けるだけです。MainActivity.kt@Composable fun Greeting(name: String) { Text (text = "Hello $name!") } @Preview @Composable fun PreviewGreeting() { Greeting("Android") }実際にIDE上に表示されるプレビュー画面はこんな感じです。
データクラスの作成
では、さっそくサンプルアプリを作成していきます。
まずは、サンプルアプリで使用するデータを定義します。
定義するデータは記事とユーザで、以下のようにクラスを作成しました。Article.ktpackage com.example.qiita.model data class Article( val id: String, val title: String, val url: String, val user: User )User.ktpackage com.example.qiita.model data class User( val id: String, val name: String, val profileImageUrl: String )記事のコンポーネントを作成
下準備
まず、画像を描画するのに
DrawImage
を使う必要があるのですが、プロジェクト作成当初だと使えないので使えるようにしていきます。build.gradle
に以下のコード追加しました。build.gradledependencies{ //追加するコードだよ implementation 'androidx.ui:ui-foundation:0.1.0-dev03' }記事のコンポーネントを作成
ArticleItem
という@Composable関数
の中に記事のコンポーネントを作成しました。MainActivity.kt@Composable fun ArticleItem(article: Article) { // おそらくまだURLから読み込む方法はない val image = +imageResource(R.drawable.ic_header) val typography = +MaterialTheme.typography() Row(modifier = Spacing(16.dp)) { Container(modifier = Size(60.dp, 60.dp)) { DrawImage(image = image) } Column(modifier = ExpandedWidth wraps Spacing(right = 16.dp, left = 16.dp)) { Text(article.title, style = typography.h6) Text(article.user.name, modifier = Spacing(top = 4.dp), style = typography.subtitle2) } } }それぞれどんな役割?
- Row
- 複数のコンポーネントを横並びに表示するよ
- Container
- DrawImageのみだと、元々の画像のサイズを表示してしまうため、指定したサイズのコンテナを作成するよ
- DrawImage
- 画像を表示するよ
- Column
- 複数のコンポーネントを縦並びに表示するよ
- これがないとViewが重なって表示されるよ
- Text
- 文字列を表示するよ
- Styleに文字のサイズを指定しているよ
- Spacing
- marginを付けるよ
実行結果
ダミーデータでArticleItemを実行した結果が以下の様になります。
MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { ArticleItem( article = Article( id = "123", title = "Kotlin入門", url = "http://www.exsample.com/articles/123", user = User(id = "456", name = "たろう", profileImageUrl = "") ) ) } } } }後はこのコンポーネントを複数並べれば完成しそうです。
記事一覧のコンポーネントを作成
先程作成した記事のコンポーネントを使って、記事を一覧表示するコンポーネントを作成していきます。
MainActivity.kt@Composable fun ArticleList(articles: List<Article>) { VerticalScroller { Column { articles.forEach { article -> Card( modifier = Spacing(4.dp) wraps Expanded, shape = RoundedCornerShape(8.dp) ) { ArticleItem(article = article) } } } } }それぞれどんな役割?
- VerticleScroller
- 縦スクロールできるようになるよ
- Card
- AndroidでいうCardViewだよ
実行結果
MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val articles = mutableListOf<Article>() repeat(11) { articles.add( Article( id = "123", title = "Kotlin入門", url = "http://www.exsample.com/articles/123", user = User(id = "456", name = "たろう", profileImageUrl = "") ) ) } setContent { MaterialTheme { ArticleList(articles = articles) } } } }ツールバーを作成
最後に
setContent
の中にTopAppBar
を追加してツールバーを作成しました。Columnでコンポーネントを縦並びに配置しています。MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) /**省略**/ setContent { MaterialTheme { //追加するやつ Column { //追加するやつ TopAppBar(title = { Text("QiitaClient") }) ArticleList(articles = articles) } } } } }おわりに
Jetpack Composeを使って記事を一覧表示するアプリを作成していきました。
今回のサンプルアプリの記事のデータはダミーデータを使ってるので、今後はQiita APIから記事のデータを取得して表示させたり、ボタンを押すと他の画面に遷移させたりしていきたいなと思います。使ってみた感想ですが、思っていたより簡単かつシンプルに書けたかなと思っています。ただ、プレビュー画面で作成したコンポーネントを確認する際に
Build Refresh
しなければいけないのでそこが不便だと感じました。今はまだプレアルファ版ですが、ベータ版がすごい楽しみです。参考にしたもの
![]()
Kotlinスタートブック
Jetpack Compose
Jetpack Compose Tutorial
android/compose-samples
- 投稿日:2019-12-18T23:28:34+09:00
時代は匿名SNSを求めているので作った!(というか一瞬動いた・・か)
まえおき
ひとり開発 Advent Calendar 2019の18日目!エントリの空きを突然頂戴して無理やり当日凸してます!
余談ですが作業ミスエントリ流行ってるんですかね。Advent Calender以外でも色々書かれてる気がします。
当方もまさにこの実装をギブアップしたのでドシドシ書いていこうと思います。笑
11月末から作り始めたのに途中から全く目途が立たなくなったのでどこぞのAdvent Calenderにエントリできなかったっていうのを諦めてたのに強引に滑り込ませたという経緯ですね・・前回の続き
バズった割にあんまキてない技術の一つとしてiBeaconがありますが、その理由の一つにBluetooth使ってないのにバッテリー勿体ないからオフってる人が多いからというのがありましたが、この実験から見るに2019年現在ではバッテリーの大容量化によってデフォでオンな人が多いと思いました。
ハイプサイクル水面下で密かにBLEがデファクトになっているような気がした実験でした。BLEありきで他のアプリも作れそうな気がしたので仮説をば。
仮説
スマホで様々なアプリが出てきたおかげで便利になった反面、現在のイノベーションがマッチングビジネスといわれるように点と点を結びつける機能しかない。欲しいサービスは唐突に発生するものであり、ラストワンマイル(㍍?)を柔軟に埋めるのは人手しかない。
- とあるデパートのとある場所に行きたいが建物の案内図がシャレオツデザインでよくわからない
- 109のようなビルなので回りに女性しかいない。僕のようなおじさんが手短に訪ねると事案発生
- 勇気をもって尋ねる。「XX階のXXの隣ですよ」と回答を得て一安心
- が、それを傍目から見ていた人が激写。「XXでおじさんがナンパしてる!」とSNSに勝手にアップ
- SNS内で「勝手に他人をアップしてる案件」と炎上して論点と無関係なのに全力で晒されてしまう。
というようなSNSがあるが故にイデオロギー構想に巻き込まれたりするんですよ。怖いですね。~
いや、それ完全におじさん視点の妄想だし~普段、人間対面でのsocialな意見交換は居酒屋や職場での雑談の中で匿名(に近い低いアイデンティティレベル)かつ、狭い範囲で行われいますがネットを介在した瞬間に同じ活動は許容されなかったりする。
匿名であってもログは残る。暗号化しても某国みたいな圧力で開示されてしまうかもしれないし、Threemaとかみたく相手確認を厳密にすればするほどsocialな繋がりは絶たれてしまう。Sarahah、Crypviserも。勇気をもって尋ねる。「XX階のXXの隣ですよ」と回答を得て一安心
→一部の人しか知らない and 後に何も残らないエンドこれをアプリで実装できないものだろうか?
で実装
BLEを残虐に酷使してます。SIGの人が見たら激怒しそう・・
送信したい文字列を分割してservice uuidに収まる3個の数字ずつにします。先頭にメッセージの順番の数字を付与します。
送信側はオフ→オン(酷いwwww)を繰り返しservice uuidを載せてadvertisingします。受診側はMAC毎にメッセージを順番に並べます。最終メッセージである0000が来たらdecodeして文字列にします。デモ
メッセージを送信しています。実装の通りBluetoothのアイコンがぺかぺかしているのが右上で見れます。
送信されたメッセージが一個ずつ受信されデコードされています。
よくあるメッセンジャーサービスのようで実装に書いた通り、この送受信の間にbleのコネクションを張るような処理はありません。
ただ、スキャンしたいデバイス名のservice uuidを並べていって文字にデコードしているという処理なので、
"匿名"で"狭い範囲"で声を出して問いかけているというような条件になっていることがわかるでしょうか。〆
これをSMS広告メッセージの代わりにすることもできると思います。BLE Advertisingが見れれば受信できるのでiBeaconより手間にならないし。
愚痴に書く通り安定したcallbackが期待できないので地獄のビジーウェイト実装してたりしてますが、動作した証拠として載せます。愚痴
「android ble 辛い」で検索すると色々出てくる訳ですが、この程度なら!と無視したクロスカウンター喰らったわけです。
まずスマホ実装前にドライバ/スタブが要りそうなのでGolangで実装
ここまで二日間弱。あれ?あと2、3日で全部出来るかな??と思いきやぎっちょん!currantlabs/bleがWindows未対応(Goのライブラリなのに・・)で使えないのでひたすらWindows環境で作ってくことになった。
まあロジック書けたからいいやーと割り切ったもののそこからcordovaのプラグインに振り回される泥沼に・・
そもそもstartがあるのにstopが無いとかサンプルが無いとかカオスっぷりが凄まじく何度も諦めそうになった。あとerror時にcallbackこねー。どこがおかしいのか切り分けし辛い。。
- cordova-plugin-ble-central
こいつからAPI呼ばないとアプリ初回起動時の位置情報許可の画面が出てこない。毎度アプリの設定を手動でするはめになります。ble enableでも位置許可無しだと動きません。
- cordova-plugin-ble-peripheral
こいつからしかAdvertisingできない。うえにissue上は128bit対応しているぽいのに16bitしか動かないようです。ネット上では128bit実装ばかりでまず気づけない。
- cordova-plugin-bluetoothle
central、peripheralなんでもAPI揃っているようで動作しない実装がある。bleのオンオフは動くのでそれだけ使いたいので入ってる
本来ならそれぞれの修正願いのissue書くべきなんだけどなんで動かないか分からないので書きようがない。。それともgolangの動作が良すぎるのか。
(bluetoothオフオン無しでservice id速攻で切り替わる&検出する)
Android SutdioのSimulatorがBluetoothエミュレーション無いとのことでデバッグもどえらく辛かった。途中で現行環境捨てて、React NativeとかKotlinに移住した方が良かったんだよな。。
なんで動かないか分からないので、かろうじて動作する、なんで動くかわからないコードで実装するハメになるのはどうなのよ?と喚きたい。(という言い訳)
Androidが悪いのか、Cordovaが悪いのか(新しめの機種に対応できてない??)、端末依存なのかとかlogcatからは切り分けもできない・・引き続きジャイロとか使ってみたいけどハード側を触るのはこれ以上Cordovaでは辛い気がする。最初HTML+Javascriptで書いておいてスマホ用に少しずつ実装していくスタイルはめちゃ軽くて好きなんだけどなあ・・残念。
ていう、誰にも聞きこめない、ひとり開発的な悶々でオトシます。
読んで頂いた方に多謝!
- 投稿日:2019-12-18T21:23:41+09:00
ReactNative AndroidのBrige解説
機能
Native Modules
出来ること
・関数の作成
・イベントの通知
・コールバックNative UI Components
出来ること
・Viewの作成
・Viewのイベントの通知Native Modules
ダイアログを表示するModuleを例にします。
[機能]
1, Androidのダイアログを表示する。(関数の作成)
2, ダイアログの"OK"ボタンがクリックされたらコンソールに"true"が表示される。(イベントの通知)
3, "DATE"ボタンがクリックされたら現在時刻が表示される。(コールバック)DialogModule.javapublic class DialogModule extends ReactContextBaseJavaModule { // コンストラクター public DialogModule(@NonNull ReactApplicationContext reactContext) { super(reactContext); } // このモジュールを呼び出すためのタグのようなもの @Override public String getName() { return "DialogModule"; } // RNで使用出来るクラス定数を定義出来る public Map<String, Object> getConstants() { final Map<String, Object> constants = new HashMap<>(); constants.put("SINGLE_BUTTON", "SINGLE"); constants.put("DOUBLE_BUTTON", "DOUBLE"); return constants; } // ダイアログを表示する関数 @ReactMethod public void showDialog(String message, String type) { if (type.equals("SINGLE")) { new AlertDialog.Builder(getCurrentActivity()) .setTitle("DialogModule") .setMessage(message) .setPositiveButton("CLOSE", (dialog, which) -> { dialog.dismiss(); }) .show(); } else { new AlertDialog.Builder(getCurrentActivity()) .setTitle("DialogModule") .setMessage(message) .setPositiveButton("OK", (dialog, which) -> { sendEvent(); }) .setNegativeButton("CLOSE", (dialog, which) -> { dialog.dismiss(); }) .show(); } } // 現在時刻を表示してコールバックする関数 @ReactMethod private void getCurrentTime(Callback callback) { Calendar calendar = Calendar.getInstance(); callback.invoke(calendar.getTime().toString()); } // ボタンがクリックされたことを通知するイベント private void sendEvent() { WritableMap params = Arguments.createMap(); params.putBoolean("click", true); getReactApplicationContext() .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit("onClick", params); } }解説
getName
RNから呼び出す際の文字列を"DialogModule"のように記述する必要があります。
@Override public String getName() { return "DialogModule"; }
getConstants
RNから使用できるMolueのクラス定数を設定できます。
※実装は必須ではありません。
constants.put(定数名, 値);
public Map<String, Object> getConstants() { final Map<String, Object> constants = new HashMap<>(); constants.put("SINGLE_BUTTON", "SINGLE"); constants.put("DOUBLE_BUTTON", "DOUBLE"); return constants; }
@ReactMethod
RNから使用できるメソッドを設定できます。
コールバックを実行する場合は.invoke()
で実行します。@ReactMethod private void getCurrentTime(Callback callback) { Calendar calendar = Calendar.getInstance(); callback.invoke(calendar.getTime().toString()); }
イベント(リスナー)
RNに登録できるイベントを設定できます。// コールバックの値を設定 WritableMap params = Arguments.createMap(); params.putBoolean("click", true); // イベント名とコールバックの値を設定 getReactApplicationContext() .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit("onClick", params);Native UI Components
動画playerを表示するViewを例にします。
[機能]
1, 動画playerを表示する。(Viewの作成)
2, urlをRN側からpropsで受け取り動画を再生する。
3, 再生終了時にログを表示する。(Viewのイベントの通知)VideoViewManager.javapublic class VideoViewManager extends SimpleViewManager<VideoView> { private Context context; // このモジュールを呼び出すためのタグのようなもの @Override public String getName() { return "VideoView"; } // 使用するViewのインスタンを返すコンストラクターのようなもの @Override protected VideoView createViewInstance(ThemedReactContext reactContext) { this.context = reactContext; return new VideoView(reactContext); } // Propsで受け取った値で処理をする関数 @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1) @ReactProp(name="url") public void setVideoPath(VideoView videoView, String urlPath) { Uri uri = Uri.parse(urlPath); videoView.setMediaController(new MediaController(context)); videoView.setVideoURI(uri); // 再生準備出来次第再生する videoView.setOnPreparedListener(mp -> { videoView.start(); }); // 再生終了時にpropsの『onFinish』にコールバックする。(通知) videoView.setOnCompletionListener(mp -> { ReactContext reactContext = (ReactContext)context; WritableMap event = Arguments.createMap(); event.putString("message", "onDirectEvent"); reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(videoView.getId(),"onDirectEvent",event); WritableMap event2 = Arguments.createMap(); event2.putString("message", "onBubblingEvent"); reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(videoView.getId(),"onBubblingEvent",event2); }); videoView.getDuration(); } @Override public Map<String, Object> getExportedCustomDirectEventTypeConstants() { return MapBuilder.<String, Object>builder() .put("onDirectEvent", MapBuilder.of("registrationName", "onDirectEvent")) .build(); } @Override public Map<String, Object> getExportedCustomBubblingEventTypeConstants() { return MapBuilder.<String, Object>builder() .put("onBubblingEvent", MapBuilder.of("phasedRegistrationNames", MapBuilder.of( "bubbled", "onBubble", "captured", "onCapture"))) .build(); } }解説
getName
RNから呼び出す際の文字列を"VideoView"のように記述する必要があります。
@Override public String getName() { return "VideoView"; }
@ReactProp
ViewのPropsを受け取るセッターメソッド
メソッド名自体はなんでもいいです
@ReactProp(name="props名")
public void setProp(Viewの型 view, 型 Propsの値)
@ReactProp(name="url") public void setVideoPath(VideoView videoView, String urlPath) { // ... }
createViewInstance
Viewのインスタンスを返す関数
※必須です@Override protected VideoView createViewInstance(ThemedReactContext reactContext) { return new VideoView(reactContext); }
イベントの通知
Propsのコールバックを設定できます。イベントの登録
イベントを登録するメソッドは2つあります。
・getExportedCustomDirectEventTypeConstants
・getExportedCustomBubblingEventTypeConstants
getExportedCustomDirectEventTypeConstants
1つのイベントで1つのpropsに通知する。
登録方法
MapBuilder.builder().put("イベント名", MapBuilder.of("registrationName", "プロップス名")).build();
getExportedCustomBubblingEventTypeConstants
1つのイベントで2つのpropsに通知することができる。
登録方法
MapBuilder.builder().put("イベント名", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "プロップス名1","captured", "プロップス名2"))).build();
// イベントを登録する関数1 @Override public Map<String, Object> getExportedCustomDirectEventTypeConstants() { return MapBuilder.<String, Object>builder() .put("onDirectEvent", MapBuilder.of("registrationName", "onDirectEvent")) .build(); } // イベントを登録する関数2 @Override public Map<String, Object> getExportedCustomBubblingEventTypeConstants() { return MapBuilder.<String, Object>builder() .put("onBubblingEvent", MapBuilder.of("phasedRegistrationNames", MapBuilder.of( "bubbled", "onBubble", "captured", "onCapture"))) .build(); }通知したい箇所で
receiveEvent
を呼び出す
receiveEvent("viewのID", "イベント名", コールバックの値)
// ... ReactContext reactContext = (ReactContext)context; // イベントを通知する処理1 "onDirectEvent"イベント WritableMap event = Arguments.createMap(); event.putString("message", "onDirectEvent"); reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(videoView.getId(),"onDirectEvent",event); // イベントを通知する処理2 "onBubblingEvent"イベント WritableMap event2 = Arguments.createMap(); event2.putString("message", "onBubblingEvent"); reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(videoView.getId(),"onBubblingEvent",event2); // ...RN側では以下のようにイベントが通知される
<VideoView onDirectEvent={({ nativeEvent }) => console.log(nativeEvent.message)} // onDirectEvent onCapture={({ nativeEvent }) => console.log(nativeEvent.message)} // onBubblingEvent onBubble={({ nativeEvent }) => console.log(nativeEvent.message)} // onBubblingEvent />ModuleとUI Componentの登録
作成したModuleとUI Componentを以下のように登録します
ExamplePackage.javapublic class ExamplePackage implements ReactPackage { @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Collections.<ViewManager>singletonList( // UI Componentが増えるたびに追加していく new VideoViewManager() ); } @Override public List<NativeModule> createNativeModules(ReactApplicationContext reactContext return Collections.<ViewManager>singletonList( // Moduleが増えるたびに追加していく new DialogModule(reactContext) ); } }作成したpackageをMainApplication.java内の
getPackages
に登録しますMainApplication.java@Override protected List<ReactPackage> getPackages() { List<ReactPackage> packages = new PackageList(this).getPackages(); // packageが増えるたびに追加していく packages.add(new ExamplePackage()); return packages; }RN側
Native Modules
Dialog.jsximport React from 'react'; import { NativeModules } from 'react-native'; // getName関数の返り値("DialogModule")を指定 DialogModule = NativeModules.DialogModule; const Dialog = () => { const [date, setDate] = React.useState(""); React.useEffect(() => { const eventEmitter = new NativeEventEmitter(DialogModule); eventEmitter.addListener('onClick', (event) => { console.log(event.change) // "true" }); }, []) return ( <> <TouchableOpacity onPress={() => DialogModule.showDialog('SINGLE BUTTON', DialogModule.SINGLE_BUTTON)}> <Text>SINGLE BUTTON Dialog</Text> </TouchableOpacity> <TouchableOpacity onPress={() => DialogModule.showDialog('DOUBLE BUTTON', DialogModule.DOUBLE_BUTTON)}> <Text>DOUBLE BUTTON Dialog</Text> </TouchableOpacity> <TouchableOpacity onPress={() => DialogModule.getCurrentTime(time => setDate(time))}> <Text>DATE</Text> </TouchableOpacity> <Text>CURRENT DATE:</Text> <Text>[ {date} ]</Text> </> ) } export default DialogNative UI Components
VideoView.jsximport React from 'react'; import { requireNativeComponent } from 'react-native'; VideoView = requireNativeComponent('VideoView'); const VideoView = () => { return ( <> <VideoView style={{ width: '100%', height: '100%' }} url="https://www.radiantmediaplayer.com/media/bbb-360p.mp4" onDirectEvent={({ nativeEvent }) => console.log(nativeEvent.message)} onCapture={({ nativeEvent }) => console.log(nativeEvent.message)} onBubble={({ nativeEvent }) => console.log(nativeEvent.message)} /> </> ) } export default VideoViewおまけ
UI Componentに実装してある関数をModuleから呼びたい場合
ExamplePackage.javapublic class ExamplePackage implements ReactPackage { private ExampleViewManager instance; @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Collections.<ViewManager>singletonList( instance ); } @Override public List<NativeModule> createNativeModules(ReactApplicationContext reactContext instance = new ExampleViewManager(); return Collections.<ViewManager>singletonList( // モジュールにViewManagerのインスタンスを渡す // あとはモジュール内ので定義した関数からインスタンスのメソッドを呼べば良い new ExampleModule(reactContext, instance) ); } }最後に
Androidのブリッジ部分については英語の記事ばかりかつ動かないサンプルコードが多く苦労しました。
記事書く時間が少なかったので、抜けてるところや間違った箇所があるかもしれないので記事の内容は参考程度にしてください。続きまして、React Native Advent Calendar 20日目の記事は @duka さんの「AB test とかの話」です
- 投稿日:2019-12-18T21:23:41+09:00
ReactNative AndroidのBridge解説
機能
Native Modules
出来ること
・関数の作成
・イベントの通知
・コールバックNative UI Components
出来ること
・Viewの作成
・Viewのイベントの通知Native Modules
ダイアログを表示するModuleを例にします。
[機能]
1, Androidのダイアログを表示する。(関数の作成)
2, ダイアログの"OK"ボタンがクリックされたらコンソールに"true"が表示される。(イベントの通知)
3, "DATE"ボタンがクリックされたら現在時刻が表示される。(コールバック)DialogModule.javapublic class DialogModule extends ReactContextBaseJavaModule { // コンストラクター public DialogModule(@NonNull ReactApplicationContext reactContext) { super(reactContext); } // このモジュールを呼び出すためのタグのようなもの @Override public String getName() { return "DialogModule"; } // RNで使用出来るクラス定数を定義出来る public Map<String, Object> getConstants() { final Map<String, Object> constants = new HashMap<>(); constants.put("SINGLE_BUTTON", "SINGLE"); constants.put("DOUBLE_BUTTON", "DOUBLE"); return constants; } // ダイアログを表示する関数 @ReactMethod public void showDialog(String message, String type) { if (type.equals("SINGLE")) { new AlertDialog.Builder(getCurrentActivity()) .setTitle("DialogModule") .setMessage(message) .setPositiveButton("CLOSE", (dialog, which) -> { dialog.dismiss(); }) .show(); } else { new AlertDialog.Builder(getCurrentActivity()) .setTitle("DialogModule") .setMessage(message) .setPositiveButton("OK", (dialog, which) -> { sendEvent(); }) .setNegativeButton("CLOSE", (dialog, which) -> { dialog.dismiss(); }) .show(); } } // 現在時刻を表示してコールバックする関数 @ReactMethod private void getCurrentTime(Callback callback) { Calendar calendar = Calendar.getInstance(); callback.invoke(calendar.getTime().toString()); } // ボタンがクリックされたことを通知するイベント private void sendEvent() { WritableMap params = Arguments.createMap(); params.putBoolean("click", true); getReactApplicationContext() .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit("onClick", params); } }解説
getName
RNから呼び出す際の文字列を"DialogModule"のように記述する必要があります。
@Override public String getName() { return "DialogModule"; }
getConstants
RNから使用できるMolueのクラス定数を設定できます。
※実装は必須ではありません。
constants.put(定数名, 値);
public Map<String, Object> getConstants() { final Map<String, Object> constants = new HashMap<>(); constants.put("SINGLE_BUTTON", "SINGLE"); constants.put("DOUBLE_BUTTON", "DOUBLE"); return constants; }
@ReactMethod
RNから使用できるメソッドを設定できます。
コールバックを実行する場合は.invoke()
で実行します。@ReactMethod private void getCurrentTime(Callback callback) { Calendar calendar = Calendar.getInstance(); callback.invoke(calendar.getTime().toString()); }
イベント(リスナー)
RNに登録できるイベントを設定できます。// コールバックの値を設定 WritableMap params = Arguments.createMap(); params.putBoolean("click", true); // イベント名とコールバックの値を設定 getReactApplicationContext() .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit("onClick", params);Native UI Components
動画playerを表示するViewを例にします。
[機能]
1, 動画playerを表示する。(Viewの作成)
2, urlをRN側からpropsで受け取り動画を再生する。
3, 再生終了時にログを表示する。(Viewのイベントの通知)VideoViewManager.javapublic class VideoViewManager extends SimpleViewManager<VideoView> { private Context context; // このモジュールを呼び出すためのタグのようなもの @Override public String getName() { return "VideoView"; } // 使用するViewのインスタンを返すコンストラクターのようなもの @Override protected VideoView createViewInstance(ThemedReactContext reactContext) { this.context = reactContext; return new VideoView(reactContext); } // Propsで受け取った値で処理をする関数 @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1) @ReactProp(name="url") public void setVideoPath(VideoView videoView, String urlPath) { Uri uri = Uri.parse(urlPath); videoView.setMediaController(new MediaController(context)); videoView.setVideoURI(uri); // 再生準備出来次第再生する videoView.setOnPreparedListener(mp -> { videoView.start(); }); // 再生終了時にpropsの『onFinish』にコールバックする。(通知) videoView.setOnCompletionListener(mp -> { ReactContext reactContext = (ReactContext)context; WritableMap event = Arguments.createMap(); event.putString("message", "onDirectEvent"); reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(videoView.getId(),"onDirectEvent",event); WritableMap event2 = Arguments.createMap(); event2.putString("message", "onBubblingEvent"); reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(videoView.getId(),"onBubblingEvent",event2); }); videoView.getDuration(); } @Override public Map<String, Object> getExportedCustomDirectEventTypeConstants() { return MapBuilder.<String, Object>builder() .put("onDirectEvent", MapBuilder.of("registrationName", "onDirectEvent")) .build(); } @Override public Map<String, Object> getExportedCustomBubblingEventTypeConstants() { return MapBuilder.<String, Object>builder() .put("onBubblingEvent", MapBuilder.of("phasedRegistrationNames", MapBuilder.of( "bubbled", "onBubble", "captured", "onCapture"))) .build(); } }解説
getName
RNから呼び出す際の文字列を"VideoView"のように記述する必要があります。
@Override public String getName() { return "VideoView"; }
@ReactProp
ViewのPropsを受け取るセッターメソッド
メソッド名自体はなんでもいいです
@ReactProp(name="props名")
public void setProp(Viewの型 view, 型 Propsの値)
@ReactProp(name="url") public void setVideoPath(VideoView videoView, String urlPath) { // ... }
createViewInstance
Viewのインスタンスを返す関数
※必須です@Override protected VideoView createViewInstance(ThemedReactContext reactContext) { return new VideoView(reactContext); }
イベントの通知
Propsのコールバックを設定できます。イベントの登録
イベントを登録するメソッドは2つあります。
・getExportedCustomDirectEventTypeConstants
・getExportedCustomBubblingEventTypeConstants
getExportedCustomDirectEventTypeConstants
1つのイベントで1つのpropsに通知する。
登録方法
MapBuilder.builder().put("イベント名", MapBuilder.of("registrationName", "プロップス名")).build();
getExportedCustomBubblingEventTypeConstants
1つのイベントで2つのpropsに通知することができる。
登録方法
MapBuilder.builder().put("イベント名", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "プロップス名1","captured", "プロップス名2"))).build();
// イベントを登録する関数1 @Override public Map<String, Object> getExportedCustomDirectEventTypeConstants() { return MapBuilder.<String, Object>builder() .put("onDirectEvent", MapBuilder.of("registrationName", "onDirectEvent")) .build(); } // イベントを登録する関数2 @Override public Map<String, Object> getExportedCustomBubblingEventTypeConstants() { return MapBuilder.<String, Object>builder() .put("onBubblingEvent", MapBuilder.of("phasedRegistrationNames", MapBuilder.of( "bubbled", "onBubble", "captured", "onCapture"))) .build(); }通知したい箇所で
receiveEvent
を呼び出す
receiveEvent("viewのID", "イベント名", コールバックの値)
// ... ReactContext reactContext = (ReactContext)context; // イベントを通知する処理1 "onDirectEvent"イベント WritableMap event = Arguments.createMap(); event.putString("message", "onDirectEvent"); reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(videoView.getId(),"onDirectEvent",event); // イベントを通知する処理2 "onBubblingEvent"イベント WritableMap event2 = Arguments.createMap(); event2.putString("message", "onBubblingEvent"); reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(videoView.getId(),"onBubblingEvent",event2); // ...RN側では以下のようにイベントが通知される
<VideoView onDirectEvent={({ nativeEvent }) => console.log(nativeEvent.message)} // onDirectEvent onCapture={({ nativeEvent }) => console.log(nativeEvent.message)} // onBubblingEvent onBubble={({ nativeEvent }) => console.log(nativeEvent.message)} // onBubblingEvent />ModuleとUI Componentの登録
作成したModuleとUI Componentを以下のように登録します
ExamplePackage.javapublic class ExamplePackage implements ReactPackage { @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Collections.<ViewManager>singletonList( // UI Componentが増えるたびに追加していく new VideoViewManager() ); } @Override public List<NativeModule> createNativeModules(ReactApplicationContext reactContext return Collections.<ViewManager>singletonList( // Moduleが増えるたびに追加していく new DialogModule(reactContext) ); } }作成したpackageをMainApplication.java内の
getPackages
に登録しますMainApplication.java@Override protected List<ReactPackage> getPackages() { List<ReactPackage> packages = new PackageList(this).getPackages(); // packageが増えるたびに追加していく packages.add(new ExamplePackage()); return packages; }RN側
Native Modules
Dialog.jsximport React from 'react'; import { NativeModules } from 'react-native'; // getName関数の返り値("DialogModule")を指定 DialogModule = NativeModules.DialogModule; const Dialog = () => { const [date, setDate] = React.useState(""); React.useEffect(() => { const eventEmitter = new NativeEventEmitter(DialogModule); eventEmitter.addListener('onClick', (event) => { console.log(event.change) // "true" }); }, []) return ( <> <TouchableOpacity onPress={() => DialogModule.showDialog('SINGLE BUTTON', DialogModule.SINGLE_BUTTON)}> <Text>SINGLE BUTTON Dialog</Text> </TouchableOpacity> <TouchableOpacity onPress={() => DialogModule.showDialog('DOUBLE BUTTON', DialogModule.DOUBLE_BUTTON)}> <Text>DOUBLE BUTTON Dialog</Text> </TouchableOpacity> <TouchableOpacity onPress={() => DialogModule.getCurrentTime(time => setDate(time))}> <Text>DATE</Text> </TouchableOpacity> <Text>CURRENT DATE:</Text> <Text>[ {date} ]</Text> </> ) } export default DialogNative UI Components
VideoView.jsximport React from 'react'; import { requireNativeComponent } from 'react-native'; VideoView = requireNativeComponent('VideoView'); const VideoView = () => { return ( <> <VideoView style={{ width: '100%', height: '100%' }} url="https://www.radiantmediaplayer.com/media/bbb-360p.mp4" onDirectEvent={({ nativeEvent }) => console.log(nativeEvent.message)} onCapture={({ nativeEvent }) => console.log(nativeEvent.message)} onBubble={({ nativeEvent }) => console.log(nativeEvent.message)} /> </> ) } export default VideoViewおまけ
UI Componentに実装してある関数をModuleから呼びたい場合
ExamplePackage.javapublic class ExamplePackage implements ReactPackage { private ExampleViewManager instance; @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Collections.<ViewManager>singletonList( instance ); } @Override public List<NativeModule> createNativeModules(ReactApplicationContext reactContext instance = new ExampleViewManager(); return Collections.<ViewManager>singletonList( // モジュールにViewManagerのインスタンスを渡す // あとはモジュール内ので定義した関数からインスタンスのメソッドを呼べば良い new ExampleModule(reactContext, instance) ); } }最後に
Androidのブリッジ部分については英語の記事ばかりかつ動かないサンプルコードが多く苦労しました。
記事書く時間が少なかったので、抜けてるところや間違った箇所があるかもしれないので記事の内容は参考程度にしてください。続きまして、React Native Advent Calendar 20日目の記事は @duka さんの「AB test とかの話」です
- 投稿日:2019-12-18T20:17:29+09:00
Android 4.4のサポートはもうやめるべきなのだ
23
— minSdkVersion (@minSdkVersion) December 9, 2019minSdkVersionをつぶやくだけの謎なTwitterアカウントが23(Android 6)とツイートしていたのがまだ記憶に新しい。
世の中のminSdkが5.0以上がもう大多数になってきたかなと思っているが、4.4ユーザが数%まだいるからという理由でまだサポートを強いられている現場だってまだ存在している。
まぁ、5.0以上でしか使えないAPIやWebView周りの挙動があったのだとしても切り分けして
ソースコードを無駄に条件分岐でややこしくしたり、最悪4.4以下ユーザはごめんねすればいい話なのかと呑気に思っていた。
しかしとあるニュースを見てちょっと考え直した。RetrofitライブラリがAndroid 5+の対応になった
https://github.com/square/retrofit/blob/master/CHANGELOG.md#version-270-2019-12-09
この変更についての詳しい情報は以下
https://cashapp.github.io/2019-02-05/okhttp-3-13-requires-android-5要するにAndroid4.4はTLS1.2を標準サポートしていない。
そして下記の箇所に一番驚いた
On October 15 our colleagues at Google, Mozilla, Microsoft, and Apple announced that their browsers will require TLSv1.2 or better starting in early 2020.
つまり主要ブラウザはTLS1.0,TLS1.1のサポートを終了する。
即ち、世の中のスタンダードはTLS1.2に移行し、TLS1.1はもうセキュアじゃないという判断だ。If you really need TLSv1.2 on Android 4.x, that’s possible! Ankush Gupta has written a thorough guide that explains how to get Google Play Services to do it. Even if you follow this process you should still use OkHttp 3.12.x with Android 4.x devices.
Android 4.xでもTLS1.2を行う方法はあるとのこと。
しかしすでに大多数のアプリがAndroid 4.4のサポートをすでに終了している。
とある1アプリだけAndroid 4.4のサポートを続けられたとしても、それ以外のサポートの終了したアプリでは
TLS1.1の通信を行う。これでは意味がない。なのでAndroid 4.4のサポートを終了し、よりセキュアな通信ができるAndroidへ移行を促すべきなのだ。
参考
https://github.com/AndroidDagashi/AndroidDagashi/issues/1168
- 投稿日:2019-12-18T17:28:19+09:00
【KotlinConf和訳】Kotlin 1.4以降の展望
以下の和訳/要約です。
What to Expect in Kotlin 1.4 and Beyond - Posted on December 6, 2019 by Svetlana Isakova
Specialist定例会で輪読する目的と自分の理解目的で書き起こしました。Kotlin 1.4以降に期待されること
KotlinConfでのKeynoteで、Andrey氏がKotlinの進化のために現在注力している分野と、来年中にリリースされるKotlin 1.4の計画について、我々の戦略的な見解を強調しました。
以下のキーノート全体を見てください。
https://www.youtube.com/watch?v=0xKTM0A8gdI我々のビジョンは、Kotlinがあなたのすべての頑張りへの信頼できる伴侶となり、あなたの仕事のためのデフォルトの言語選択になることです。これを実現するために、すべてのプラットフォームでKotlinを利用可能にします。業界でよく知られている企業の複数の事例研究が、我々がこの方向に向けて順調に前進していることを見せています。
2020年春に登場予定のKotlin 1.4は、Kotlinのエコシステムのために新たな一歩を踏み出すでしょう。
品質重視
何よりも、Kotlin 1.4は品質とパフォーマンスに重点を置きます。Kotlinは、すでに多くのアイデアやアプローチを開拓したモダンな言語です。我々はそれを最新の状態に保ち、常に進化させていきます。しかし現時点では、Kotlinは大きな機能を追加するよりも、全体的なエクスペリエンスを改善することが重要な段階に達していると考えています。このため、Kotlin 1.4ではいくつかの小さな言語変更のみが提供される予定であり、その詳細については後述します。
我々は、KotlinをサポートするIDEのパフォーマンスを向上させることで、すでにいくつかの素晴らしい成果を達成しています。コード補完の速度は、以前のバージョンに比べて大幅に向上しています。
Gradleチームと協力して、Gradleスクリプトの高速化を実現しました。Kotlin 1.3.60では、Android StudioのGradle ImportはKotlin 1.3.10と比べて約2.5倍高速で、メモリ消費量も約75%少なくなっています。
さらに、
build.gradle.kts
のロードのCPU使用率はほぼ0です!また、DeveloperモードでKotlin/Nativeをコンパイルする速度は、コードキャッシュを使えば最大2倍になります。ビルド速度がユーザにとって最大の懸念であることは我々も理解しており、それに対処するためにtoolchainを継続的に改善しています。しかし、インクリメンタルな改善では、製造コードベースの自然な成長に追いつけません。コンパイルを高速化する一方、ユーザがより多くのコードを書くので、全体的なビルド時間は十分に改善されません。本当に高速にするには、コンパイラを再実装する必要があると明らかになりました。
新しいコンパイラ
新しいコンパイラ実装の目標は、非常に高速で、Kotlinがサポートするすべてのプラットフォームを統合し、コンパイラ拡張のためのAPIを提供することです。これは複数年にわたる取り組みのはずですが、少し前から始めているため、新しい実装の一部は1.4年に導入される予定で、移行は非常に緩やかに行われます。既に行われてもいます…例えば、型推論の新しいアルゴリズムを試したことがあるなら、それが新しいコンパイラの一部です。他の部分のアプローチも同じです…つまり、古いバージョンと新しいバージョンの両方が、しばらくの間実験モードで使用可能になるということです。新しいものが安定したら、それがデフォルトになります。
新しいフロントエンドによる高速化
新しいコンパイラに期待される高速化の大部分は、新しいフロントエンド実装によって実現されます。
背景を少し説明すると、コンパイルは、ソースファイルを取得し、それらを段階的に実行可能コードに変換するパイプラインと考えています。このパイプラインの最初の大きなステップは、俗にコンパイラのフロントエンドと呼ばれます。コードの解析、名前の解決、型チェックなどを実行します。エラーの強調表示、定義への移動、およびプロジェクト内でのシンボル使用の検索の際、コンパイラのこの部分がIDE内で機能します。これは今や
kotlinc
が最も多くの時間を費やすステップなので、更に速くしたいのです。今の実装はまだ完了しておらず、1.4でも完成しません。しかし、時間のかかる作業のほとんどはすでに完了しており、おおよその高速化を測定できます。ベンチマーク(YouTrackとKotlinコンパイラ自体のコンパイル)によると、新しいフロントエンドは既存のものよりも約4.5倍高速です。
バックエンドと拡張性の統合
フロントエンドでコードの分析が完了すると、バックエンドで実行可能ファイルが生成されます。Kotlin/JVM、Kotlin/JS、Kotlin/Nativeという3つのバックエンドがあります。最初の2つは歴史的に独立して書かれており、あまりコードを共有していませんでした。Kotlin/Nativeを開始したとき、Kotlinコードの内部表現(IR)を中心に構築された、新しいインフラストラクチャに基づいていました。そのIRは、仮想マシンのバイトコードに似た機能を提供します。現在、他の2つのバックエンドを同じIRに移行しています。その結果、多くのバックエンドロジックを共有し、統合されたパイプラインを持つことになるでしょう。これにより、ほとんどの機能、最適化、バグ修正をすべてのターゲットに対して1回だけ実行することができます。
我々は徐々に新しいバックエンドに移行します。1.4においてデフォルトで有効になることはなさそうですが、ユーザが明示的に使うかどうかを選択することができます。
共通のバックエンド・インフラストラクチャーは、マルチプラットフォーム・コンパイラー拡張のドアを開きます。パイプラインにプラグインして、すべてのターゲットで自動的に動作するようなカスタム処理や変換を追加することができます。1.4では、そのような拡張機能(APIは後で安定化されるはず)のための公開APIを提供していませんが、すでにコンパイラプラグインを開発しているJetPack Composeを含むパートナーと、緊密に協力しています。
KLibを知る:Kotlinライブラリフォーマット
Kotlinでマルチプラットフォームライブラリを構築し、クライアントがそれに依存できるようにリリースするには、どのプラットフォームでも同じように動作する配布フォーマットが必要です。これがKLibを導入する理由です。KLibは、Kotlinマルチプラットフォーム用のライブラリフォーマットです。KLibファイルには、シリアル化されたIRが含まれています。コードが依存関係として追加する場合があります。コンパイラのバックエンドがこれを取得し、特定のプラットフォーム用の実行可能コードを生成します。JVMのバイトコードのようにKLibを分析し、変換することができます。シリアル化されたIRに対して行われる変換は、KLibが使用されるプラットフォームに影響を与えます。
実際、Kotlin/NativeはKLibsフォーマットを使用してKotlinネイティブライブラリを配布してきましたが、現在は他のバックエンドやマルチプラットフォームライブラリをサポートするようにフォーマットを拡張しています。このフォーマットは1.4では実験的なものになる予定で、将来のバージョンでは安定したABIを提供すべく取り組んでいきます。
その他のマルチプラットフォーム・ニュース
Android StudioでiOSコードを実行する
iOSデバイスやシミュレータ上でKotlinコードを実行、テスト、デバッグできるAndroid Studio用のプラグインを開発中です。プラグインはIntelliJの独自コードを使っているので、非公開のコードになるでしょう。Objective-CやSwiftの言語サポートはなく、AppStoreへのデプロイなど一部の操作には、Xcodeの実行が必要になるかもしれません。が、Kotlinコードを使って行えることは、新しいプラグインがインストールされたAndroid Studioから実行できます。このプラグインのプレビューは、2020年に公開される予定です。
Kotlin/Nativeランタイムの改善
Linux、Windows、macOS、iOSとは別に、Kotlin/NativeはwatchOSとtvOSでも動作するようになったので、事実上どんなデバイスでもKotlinを動かすことができます。また、iOSのKotlinプログラムをさらに高速に動作させるために、Kotlin/Nativeのランタイムパフォーマンスにも取り組んでいます。
コアライブラリ
Kotlinコアライブラリは、すべてのプラットフォームで動作します。これには、すべての基本型とコレクション、
kotlinx.coroutines
、kotlinx.serialization
、kotlinx.io
を扱うkotlin-stdlib
が含まれます。日付のサポートはマルチプラットフォームの世界で本当に必要とされており、我々はそれに取り組んでいます。stdlibには実験的な期間が既に追加されており、DateTimeのサポートも進行中です。Kotlinライブラリに追加されたもうひとつの重要な機能は、Reactive Streamsのコルーチンベース実装であるFlowです。Flowはデータストリームの処理に優れており、それにはKotlinのパワーを利用しています。人間工学とは別に、Flowはさらなるスピードをもたらします。いくつかのベンチマークでは、既存の人気のあるReactive Streams実装のほぼ2倍速いです。
ライブラリの著者にとって
Kotlinエコシステムにとって、新しいライブラリーの作成は不可欠であるため、我々はライブラリー作成者の体験を改善し続けています。新しいライブラリ・オーサリング・モードは、API安定のために最適な方法のコーディングに役立ちます。また、すべてのプラットフォームでドキュメント生成をサポートするDokka 1.0もリリースする予定です。
マルチプラットフォームWeb
プラットフォーム間でコードを共有することは、モバイルにとっては素晴らしいことですが、Webクライアントにとっても素晴らしいことです。サーバーとモバイルアプリの間で、あらゆるものを共有できるからです。我々はKotlin/JSツールにますます多くの投資をしており、Kotlinコードの変更からブラウザーでの結果表示まで、非常に高速な開発ラウンドトリップを行うことができます。
また、JSの相互運用性も改善され、Kotlinプロジェクトやその他のプロジェクトにNPM依存関係を追加できるようになりました。
.d.ts
タイプの定義は、Kotlinツールチェーンによって自動的に選択されます。新しいIRベースのバックエンドでは、バイナリサイズも大幅に改善されます。コンパイルされたJSファイルは、現在のサイズの半分になります。
新しい言語機能
Kotlin 1.4は、いくつかの新しい言語機能を提供します。
KotlinクラスのSAM変換
コミュニティからは、KotlinクラスのSAM変換のサポートを依頼されています(KT-7770)。SAM変換は、1つの抽象メソッドのみを持つインターフェースまたはクラスが、パラメータとして用いられるときに、ラムダを引数として渡す場合に適用されます。次にコンパイラは自動的にラムダを、抽象メンバ関数を実装するクラスのインスタンスに変換します。
SAM変換は現在、Javaインターフェースと抽象クラスに対してのみ機能します。この設計の背景にある最初のアイデアは、このようなユースケースに対して関数型を明示的に使用することでした。しかし、関数型とタイプエイリアスはすべてのユースケースを網羅しているわけではなく、多くの場合、Javaでインターフェースを保持しなければならず、そのためのSAM変換を得ることしかできませんでした。
Javaとは異なり、Kotlinは1つの抽象メソッドですべてのインターフェースをSAM変換することはできません。SAM変換に適用可能なインターフェースを作成する意図は、明確であるべきであると考えます。したがって、SAMインターフェースを定義するには、
fun
キーワードでインターフェースをマークして、汎用関数インターフェースとして使用できることを強調する必要があります。fun interface Action { fun run() } fun runAction(a: Action) = a.run() fun main() { runAction { println("Hello, KotlinConf!") } }
fun interface
の代わりにラムダを渡すことは、新しい型推論アルゴリズムでのみサポートされることに注意してください。名前付き引数と位置指定引数の混在
Kotlinは、すべての定位置引数の後に名前付き引数を置く場合を除き、明示的な名前を持つ引数(named)と名前のない通常の引数(Positional)を混在させることを禁止しています。しかし、すべての引数が正しい位置にあり、途中で1つの引数の名前を指定する必要がある場合、これが非常に面倒に感じます。Kotlin 1.4はこの問題を解決し、以下のようなコードを書けるようになります。
fun f(a: Int, b: Int, c: Int) {} fun main() { f(1, b = 2, 3) }最適化された委任プロパティ
lazy
プロパティーや、その他の委任プロパティーをコンパイルする基本的な方法を改善します。通常、委任されたプロパティーは、対応する
KProperty
というリフレクションオブジェクトにアクセスできます。たとえば、Delegates.observable
を使用する場合、変更されたプロパティに関する情報を表示できます。import kotlin.properties.Delegates class MyClass { var myProp: String by Delegates.observable("<no name>") { kProperty, oldValue, newValue -> println("${kProperty.name}: $oldValue -> $newValue") } } fun main() { val user = MyClass() user.myProp = "first" user.myProp = "second" }これを可能にするために、Kotlinコンパイラは追加の構文メンバプロパティを生成します。このプロパティは、クラス内で使用される委任プロパティを表すすべての
KProperty
オブジェクトを格納する配列です。>>> javap MyClass public final class MyClass { static final kotlin.reflect.KProperty[] $$delegatedProperties; ... }ただし、
KProperty
を使用しない委任プロパティもあります。それらについては、$$delegatedProperties
内でのオブジェクトの生成は最適ではありません。Kotlin 1.4リリースは、このようなケースを最適化します。委任されたプロパティ演算子がinline
で、KProperty
パラメーターが使用されていない場合、対応するリフレクションオブジェクトは生成されません。最も顕著な例は、
lazy
プロパティです。lazy
プロパティーに対するgetValue
の実装はinline
であり、KProperty
パラメーターは使われません。inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = valueKotlin 1.4以降では、
lazy
プロパティーを定義しても、対応するKProperty
インスタンスは生成されません。クラスで使用する委任プロパティがlazy
プロパティ(最適化に適合する他の特性)のみの場合、クラスの$$delegatedProperties
配列全体が生成されません。class MyOtherClass { val lazyProp by lazy { 42 } } >>> javap MyOtherClass public final class MyOtherClass { // no longer generated: static final kotlin.reflect.KProperty[] $$delegatedProperties; ... }末尾のコンマ
このちょっとした構文変更が、信じられないほど便利なことがわかりました。パラメータリストの最後のパラメータの後にコンマを追加できます。それから、行を入れ替えたり、新しいパラメータを追加したりできます。足りないカンマを追加または削除する必要はありません。
その他の注目すべき変更点
Kotlin 1.3.40で導入された便利な関数の
typeof
型が安定し、すべてのプラットフォームでサポートされるようになります。1.3.60リリースのブログ投稿ですでに説明されているように、
when
内でのbreak
とcontinue
を有効にします。ありがとうございます!
Kotlin EAPと実験的な機能を試し、フィードバックをくれた皆様に本当に感謝しています。我々は皆様と一緒にKotlin言語を開発し、皆さんの貴重なインプットに基づいて多くの設計の決定を行っています。この迅速で効果的なフィードバックループをコミュニティと共有することは、Kotlinがベストな状態になるためには非常に重要です。
Kotlinを使って、素晴らしいものをたくさん作ってくれたコミュニティのメンバー全員に感謝しています。これからもKotlinと一緒に続けましょう!
ちなみに、IntelliJ IDEAとAndroid Studio内のKotlinプラグインは、その機能の利用に関する匿名の統計を収集します。何がうまくいっているのか、何が問題を引き起こしているのか、何を改善すべきなのかを理解するのに役立つので、これらの統計に同意するようにお願いします。
- 投稿日:2019-12-18T16:19:05+09:00
@get:XXX と @XXX の違いを教えてエロい人
UIテストを絶賛書いているところで、つまづいたことをメモ。
注意:なぜか動いたが、結果わからん、という記事です背景
UIテストにおいてPermissionを自動で許可してくれる
GrantPermissionRule
を使う際、
Javaのサイトにお世話になっていた@Rule public GrantPermissionRule permissionRule = GrantPermissionRule.grant( Manifest.permission.CAMERA);エラーがでる
いざKotlin変換しつつコピペしたらエラーがでた
@Rule var grantPermissionRule = GrantPermissionRule.grant(Manifest.permission.CAMERA);どうやらテスト前の初期化でつまづいている様子、、、
でもなんで??????java.lang.RuntimeException: Delegate runner 'androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner' for AndroidJUnit4 could not be loaded. at androidx.test.ext.junit.runners.AndroidJUnit4.throwInitializationError(AndroidJUnit4.java:92) at androidx.test.ext.junit.runners.AndroidJUnit4.loadRunner(AndroidJUnit4.java:82) at androidx.test.ext.junit.runners.AndroidJUnit4.loadRunner(AndroidJUnit4.java:51) at androidx.test.ext.junit.runners.AndroidJUnit4.<init>(AndroidJUnit4.java:46) at java.lang.reflect.Constructor.newInstance0(Native Method) at java.lang.reflect.Constructor.newInstance(Constructor.java:343) at org.junit.internal.builders.AnnotatedBuilder.buildRunner(AnnotatedBuilder.java:104) at org.junit.internal.builders.AnnotatedBuilder.runnerForClass(AnnotatedBuilder.java:86) at androidx.test.internal.runner.junit4.AndroidAnnotatedBuilder.runnerForClass(AndroidAnnotatedBuilder.java:63) at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:59) at org.junit.internal.builders.AllDefaultPossibilitiesBuilder.runnerForClass(AllDefaultPossibilitiesBuilder.java:26) at androidx.test.internal.runner.AndroidRunnerBuilder.runnerForClass(AndroidRunnerBuilder.java:153) at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:59) at androidx.test.internal.runner.TestLoader.doCreateRunner(TestLoader.java:73) at androidx.test.internal.runner.TestLoader.getRunnersFor(TestLoader.java:105) at androidx.test.internal.runner.TestRequestBuilder.build(TestRequestBuilder.java:793) at androidx.test.runner.AndroidJUnitRunner.buildRequest(AndroidJUnitRunner.java:575) at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:393) at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2145) Caused by: java.lang.reflect.InvocationTargetException at java.lang.reflect.Constructor.newInstance0(Native Method) at java.lang.reflect.Constructor.newInstance(Constructor.java:343) at androidx.test.ext.junit.runners.AndroidJUnit4.loadRunner(AndroidJUnit4.java:72) ... 17 more Caused by: org.junit.runners.model.InitializationError at org.junit.runners.ParentRunner.validate(ParentRunner.java:418) at org.junit.runners.ParentRunner.<init>(ParentRunner.java:84) at org.junit.runners.BlockJUnit4ClassRunner.<init>(BlockJUnit4ClassRunner.java:65) at androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner.<init>(AndroidJUnit4ClassRunner.java:43) at androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner.<init>(AndroidJUnit4ClassRunner.java:48) ... 20 more直った、でもなんでかわからん
@get:Rule
にアノテーションを変えたら通った。
でもなぜかわからない!!!!!!@get:Rule var grantPermissionRule = GrantPermissionRule.grant(Manifest.permission.CAMERA);JavaのGetterを修飾するらしい
こちらの記事を参考、Kotlin文法 - アノテーション、リフレクション、型安全なビルダー、動的型
Javaでは曖昧に定義して適当にコンパイラが理解してくれたのを、Kotlinだとキッチリ宣言しないと別の意味になってエラーになってるのかな...???
- 投稿日:2019-12-18T12:08:36+09:00
Rx使いに送るKotlin Coroutine入門
Everything is a stream
みなさん良いStreamライフを送っていますか?
私は最高のStreamライフを送っています!
特に最近Kotlin Coroutineを使い始め、新たな風が流れてきています。(Streamだけに)
寒いギャグのStreamはonCompleteするとして・・・昨今のAndroid開発において、RxJava(RxKotlin)とKotlin Coroutineのどっちを使うか問題があると思います。
RxJavaはもちろん素晴らしいフレームワークですが、Kotlin Coroutineは公式が出しているフレームワークなので、こちらを使う方が安心感があるようにも感じます。Kotlin Coroutineを使ってみて、こちらの方が良いと感じる部分も多数あったので
- RxJava使いがKotlin Coroutineを使い始めるための基本知識
- RxJavaに比べてKotlin Coroutineが良いと私が思った部分の二つを話していきたいと思います。
対象読者
- Kotlinが読める
- RxJava(RxKotlin)を使ったことがある
- kotlin Coroutineを使いたいと思っている
- (Kotlin/MPPを信じてiOSをKotlinで書こうと思っているRxSwift使い)
まずはKotlin Coroutineについてざっくりと
と言っても、QiitaにKotlin Coroutineについての記事はたくさんあるので、改めて私が書くことはありません。
なので、私が参考にした記事をひたすら貼っていきます!
つまり、巨人の肩に乗るということです。まずはKotlin Coroutineの概要をざっくりと
実は、CoroutineはRxと違いStreamだけを扱うものではないのです。
async/awaitも一時中断関数(suspend fun
)もKotlin Coroutineの一部です。
以下の記事はこの辺りがわかりやすくまとめられています。
【Kotlin】Coroutineを理解する (AtsushiUemuraさま)Coroutineで動くアプリを作ってみる
Coroutineについてなんとなく理解したら、簡単なアプリを作ってみたくなりますよね?
以下の記事ではHTTP通信をCoroutineを使って行う例が示されています。
Android + Kotlin 1.3のcoroutines(Async, Await)でHTTP通信を非同期処理(jonghyoさま)Coroutineを効果的に運用するために
Coroutineは非同期で動くため、下手に使うとメモリリークします。
効率的に運用するためにはCoroutineContext
について知る必要があります。Coroutineは動かすContextがあり,Coroutineは親子関係を作ります。
そして、親が破棄されたら子は自動的に破棄されます。
なので、AndroidでしたらActivity
でCoroutineContext
を作成し、onDestroy()
で破棄するように設定しておくと、そこから動かした子Coroutineは自動的に破棄されて安全に扱えます!この辺りの親子関係について以下の記事がとても分かりやすいです!
図で理解する Kotlin Coroutine(kawmraさま)だいたい分かってきた時に押さえておきたい
チュートリアルや紹介では頻繁に出てくる
GlobalScope.launch()
ですが、実はアンチパターンです。
Globalなので、ライフサイクルとして最長なためです。この辺りのアンチパターンがまとまっているので、ある程度理解してきたら一読しておく価値ありです!
Kotlin Coroutinesパターン&アンチパターン (ikemura23さま)RxJavaの「あれ」ってKotlin Coroutineではどうするの?[基本編]
ここからが本題です。
Kotlin Coroutineをだいたい理解したところで、Rxとの対応が知りたくなります。
「CoroutineはRxの代替では無い。」と書きましたが、Flow
の登場により、Rxの代替としても使えます。
そして、Flow
型にはmap
やfilter
、scan
やretry
などRxで見慣れたオペレーターが揃っています。
なので、Observable
とFlow
はほとんど互換といって差し支えありません。
しかも、RxのSingle
やCompletable
はCoroutineでは、もっと簡単に書けます。Rxの「あれ」をKotlin Coroutineでやりたい!という気持ちに答えていきます。
RxとCoroutineの違いを大雑把に考察
CoroutineはRxに比べて型が厳しいです。
例えば、RxのHot Stream
,Cold Stream
を型で分類できません。一つ一つ覚える必要があります。
逆にRxはObservable
のオペレーターが全てなので、Subject
だろうがSingle
だろうが同じように扱えます。一方で、Coroutineは
Hot Stream
はChannel
クラス、Cold Stream
はFlow
クラスといったように、型が厳密です。
Channel
とFlow
に生えているオペレーターはまるで別物です。
さらにsuspend fun
まで加わってきて、初見では複雑に感じると思います。
型が多く、厳密ゆえに書くのが難しかったりします。一方で、型が厳密なので、「
Cold Stream
だと思っていたら、Hot Stream
でリークしていた。」なんてことは起こりにくいです。その辺りも交えながら比較していきます!
Single
Rxで値を1度だけ返す
Single
です。DBへのinsertやHTTPアクセスなど、頻繁に使われている印象があります。Rx
fun getRequest(): Single<String> { return Single.create<String> { emitter -> // 何らかの処理する emitter.onSuccess(body.message) } }Coroutine
注意:
suspend fun
はCold Stream
では無いため、厳密にはSingle
の互換ではありません。
しかし、似た扱いができるため、このように紹介しています。suspend fun getRequest(): String{ // 何らかの処理する return body.message }Single[callbackを使う場合]
Singleの内部でcallbackを使う場合はCoroutine側は少し変える必要があります。
Rx
fun getRequest(): Single<String> { return Single.create<String> { emitter -> val callback = object: Callback { override fun onNextValue(value: String) { emitter.onSuccess(value) } override fun onApiError(cause: Throwable) { emitter.onFailure(Exception("API Error", cause)) } } } }Coroutine
suspendCoroutineは関数を抜けたところで一時停止し、
resume
またはresumeWithException
が呼ばれるまで待機します。
(resumeWith
もありますが、説明を省略します)suspend fun getMessages(): String { return suspendCoroutine<String> { coroutine -> val callback = object: Callback { override fun onNextValue(value: String) { coroutine.resume(value) } override fun onApiError(cause: Throwable) { coroutine.resumeWithException(Exception("API Error", cause)) } } } }Completable
Rxで値を返さず完了したことだけ返す
Completable
です。Rx
fun updateRequest(): Completable { return Completable.create { emitter -> // 何らかの処理 emitter.onComplete() } }Coroutine
suspend fun updateRequest() { // 何らかの処理 return }Observable
Observable.just()
一度だけアイテムを流して終えるStreamです。実質
Single
です。Rx
fun getMessage(): Observable<String> { return Observable.just("サンプル") }Coroutine
fun getMessage(): Flow<String> { return flowOf("サンプル") }Observable.create()[固定回数onNextを呼ぶ場合]
任意の回数onNextを呼べる汎用Streamです。
Coroutineの場合は、固定回数onNextを呼ぶ場合と、callbackの場合で異なり、こちらは固定回数呼ぶ場合です。Rx
fun getMessages(): Observable<String> { return Observable.create { observer -> for(/*何回か回す*/) { observer.onNext("サンプル") } observer.onComplete() } }Coroutine
Coroutineの場合は
flow
スコープを抜けると自動的にcompleted
したことになります。fun getMessages(): Flow<String> { return flow { repeat(/*繰り返す回数*/) { emit("サンプル") } } }Observable.create()[callbackからonNextを呼ぶ場合]
任意の回数onNextを呼べる汎用Streamです。
Coroutineの場合は、固定回数onNextを呼ぶ場合と、callbackの場合で異なり、こちらはcallbackバージョンです。Rx
fun getMessages(): Observable<String> { return Observable.create { observer -> val callback = object: Callback { override fun onNextValue(value: String) { observer.onNext(value) } override fun onApiError(cause: Throwable) { observer.onError(Exception("API Error", cause)) } override fun onComplete() = observer.onComplete() } }.doFinally {/*streamが止まった時の後処理*/} }Coroutine
fun getMessages(): Flow<String> { return callbackFlow { val callback = object: Callback { override fun onNextValue(value: String) { offer(value) } override fun onApiError(cause: Throwable) { cancel(Exception("API Error", cause)) } override fun onComplete() = channel.close() } } // awaitCloseのところで処理が止まるため、awaitClose{}の下にコードを書いてはいけない awaitClose {/*streamが止まった時の後処理*/} }BehaviorSubject()
常に値を1個持っている
Host Stream
Rx
class SampleClass { private val behaviorSubject: BehaviorSubject<Int> = BehaviorSubject.create(0) }Coroutine
class SampleClass { private val channel: Channel<Int> = Channel(Channel.CONFLATED) }ReplaySubject()
Rx
class SampleClass { private val replaySubject: ReplaySubject<Int> = ReplaySubject.create() }Coroutine
class SampleClass { private val channel: Channel<Int> = Channel(Channel.UNLIMTED) }Subscribe()
Coroutineの場合は、型によって呼ぶメソッドが変わるのが特徴。
Rx
Single.just(1).subscribeBy( onNext = { v -> // vがonNextした値 } )Coroutine[suspend funを呼ぶ場合]
suspend fun sample(): Int { return 1 } //// ここまで準備 //// launch { sample() // 普通に呼ぶ }Coroutine[flowを呼ぶ場合]
fun getFlow(): Flow<Int> = flowOf(1) //// ここまで準備 //// launch { getFlow().collect{ v -> // vがemitされてきた値 } }Coroutine[channelを呼ぶ場合]
fun getChannel(): ReceiveChannel<Int> { // 予めchannelが作ってある前提 return this.channel } //// ここまで準備 //// launch { getChannel() .consumeAsFlow() // ここで一度Cold Streamに変換する。その後はFlowと同様 .collect { v -> // vがsendされてきた値 } }RxJavaの「あれ」ってKotlin Coroutineではどうするの?[応用編]
複数のStreamをシーケンシャルに処理する
複数のStreamを順番に処理したい場合
Rx
fun getMessages(): Observable<String> { return Observable.create<String> { observer -> val callback = object: Callback { override fun onNextValue(value: String) { observer.onNext(value) } override fun onApiError(cause: Throwable) { observer.onError(Exception("API Error", cause)) } override fun onComplete() = observer.onComplete() } }.doFinally {/*streamが止まった時の後処理*/} } fun getRequest(url: String): Single<String> { return Single.create { emitter -> // http通信をする emitter.onSuccess(message.body) } } fun insertDB(body: String): Completable { return Completable.create { emitter -> // DBに入れる emitter.onComplete() } } //// ここまで準備 //// getMessages() .flatMapSingle { getRequest(it) } .flatMapCompletable { insertDB(it) } .subscribeBy(onComplete = {})Coroutine
onEach
で後ろからStreamが流れてきたら処理をします。
今回は値を返さないCompletable
だったので、onEach
ですが、flatMapMerge
もあります。fun getMessages(): Flow<String> { return callbackFlow { val callback = object : Callback { override fun onNextValue(value: String) { offer(value) } override fun onApiError(cause: Throwable) { cancel(Exception("API Error", cause)) } override fun onComplete() = channel.close() } } // awaitCloseのところで処理が止まるため、awaitClose{}の下にコードを書いてはいけない awaitClose {/*streamが止まった時の後処理*/} } suspend fun getRequest(url: String): String { // http通信をする return message.body } suspend fun insertDB(body: String) { // DBに入れる return } //// ここまで準備 //// launch { getMessages().onEach { url -> val body = getRequest(rul) insertDB(body) }.collect {} }並列に処理して待ち合わせる
2か所にHTTP Requestを送信して、両方が成功したら次に進む処理。
シーケンシャルではなく、同時に送信するのがポイントRx
fun getHogeRequest(): Single<String> { return Single.create { emitter -> // HTTPリクエストする emitter.onSuccess(message.body) } } fun getFugaRequest(): Single<String> { return Single.create { emitter -> // HTTPリクエストする emitter.onSuccess(message.body) } } //// ここまで準備 //// Single.zip(getHogeRequest(), getFugaRequest()).subscribeBy(onNext = { v: Pair<String, String> -> })Coroutine
suspend fun
はasync{}
で囲むことで、並列に処理ができるようになります。
全てが終了するまで待つときはList<Deffered<T>>
にawaitAll()
があるので、それを呼びます。suspend fun getHogeRequest(): String { // HTTPリクエストする return message.body } suspend fun getFugaRequest(): String { // HTTPリクエストする return message.body } //// ここまで準備 //// launch { val resultList: List<String> = listOf(async{ getHogeRequest() }, async{ getFugaRequest() }).awaitAll() }Dispose
Rx
val disposable = hogeObservable().subscribeBy(onNext = {}) disposable.dispose()Coroutine
val job = launch { hogeFlow.collect {} } job.cancel()まとめ
Rx使いがCoroutineを使いこなすためのポイントをまとめます。
- CoroutineScopeやCoroutineContextを何となく理解する
Single
やCompletable
を使いたくなったらsuspend fun
を使うObservable
を使いたくなったらFlow
を使うBehaviorSubject
などのSubject系を使いたくなったらChannel
を使うここでは細かく書きませんでしたが、
Observable
とFlow
のオペレーターは似ていますが、異なる部分も多々あります。
最終的には公式ドキュメントが一番ですので、概要を理解したら公式ドキュメントを読んでみてください。
Rxのリファレンス、Coroutineのリファレンス
- 投稿日:2019-12-18T11:47:03+09:00
【Androidアプリ開発Tips】Widgetをクリックしてもブロードキャストされない
初めてAndroidのWidgetアプリを作る事になり、ネット上のサンプルアプリを動かそうとしたら、ウィジェットをクリックした時にブロードキャストが受け取れない(発生しない)という状況になり、ハマりました。
同じ悩みを抱える人のために取り急ぎ解決方法を記載しておきます。結果的には、SDKバージョンの仕様変更によるものでした。
わたしのAndroid Studioは、2019年12月時点で最新バージョンを導入しています。<前提>
・Android Studio 3.5.3
・compileSdkVersion 29
・buildToolsVersion "29.0.2"
・minSdkVersion 15
・targetSdkVersion 291.サンプルコード
以下の二つを実装してみましたが、うまく行きません。
・クリックするとToast表示するアプリ
https://qiita.com/s-yamda/items/cba2c6c134303f29d4ab
・クリックするとおみくじが引けるアプリ
http://fourtec.net/pc-blogs/2794マニフェストファイルは以下の通り。
サンプル通り、receiverのintent-filterタグに、actionを挿入しています。AndroidManifest.xml<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.testwidget"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <receiver android:name=".NewAppWidget"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="CLICK_WIDGET"/> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/new_app_widget_info" /> </receiver> <activity android:name=".NewAppWidgetConfigureActivity"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> </intent-filter> </activity> </application> </manifest>AppWidgetProviderウィジェット実装ファイル。
ウィジェットの文字をクリックするとブロードキャストが発生し、onReceiveに飛んでToast出力するというだけのもの。NewAppWidget.javapackage com.example.testwidget; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; import android.util.Log; import android.widget.RemoteViews; import android.widget.Toast; /** * Implementation of App Widget functionality. * App Widget Configuration implemented in {@link NewAppWidgetConfigureActivity NewAppWidgetConfigureActivity} */ public class NewAppWidget extends AppWidgetProvider { static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { CharSequence widgetText = NewAppWidgetConfigureActivity.loadTitlePref(context, appWidgetId); // Construct the RemoteViews object RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget); //クリックを識別する文字列 Intent intent = new Intent("CLICK_WIDGET"); PendingIntent pIntent = PendingIntent.getBroadcast(context, appWidgetId, intent , 0); //ウィジェットテキストの変更 views.setTextViewText(R.id.appwidget_text,"CLICK!"); //ウィジェットを押したときにインテントが発行される views.setOnClickPendingIntent(R.id.appwidget_text,pIntent); // Instruct the widget manager to update the widget appWidgetManager.updateAppWidget(appWidgetId, views); } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { // There may be multiple widgets active, so update all of them for (int appWidgetId : appWidgetIds) { updateAppWidget(context, appWidgetManager, appWidgetId); } } @Override public void onDeleted(Context context, int[] appWidgetIds) { // When the user deletes the widget, delete the preference associated with it. for (int appWidgetId : appWidgetIds) { NewAppWidgetConfigureActivity.deleteTitlePref(context, appWidgetId); } } @Override public void onEnabled(Context context) { // Enter relevant functionality for when the first widget is created } @Override public void onDisabled(Context context) { // Enter relevant functionality for when the last widget is disabled } @Override public void onReceive(Context context, Intent intent){ super.onReceive(context,intent); //Manifestに登録されたActionと発行したインテントが同じ場合に実行される if (intent.getAction().equals("CLICK_WIDGET")) { Toast.makeText(context, "クリックされました!", Toast.LENGTH_SHORT).show(); } } }がしかし、いくらウィジェットを押してもToastは出力されません。
logcat仕掛けてみると、onReceiveのイベントすら発生していない模様です。2.解決方法
以下のQAサイトに解決方法が記載されていました。
どうやらAndroid Oreo(API26)以降で仕様が変わり、intentにactionをセットする方法を変更しなければならないようです。
https://teratail.com/questions/225130「// 変更」とコメントしてある2行を変更しました
NewAppWidget.javapackage com.example.testwidget; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; import android.util.Log; import android.widget.RemoteViews; import android.widget.Toast; /** * Implementation of App Widget functionality. * App Widget Configuration implemented in {@link NewAppWidgetConfigureActivity NewAppWidgetConfigureActivity} */ public class NewAppWidget extends AppWidgetProvider { static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { CharSequence widgetText = NewAppWidgetConfigureActivity.loadTitlePref(context, appWidgetId); // Construct the RemoteViews object RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget); //クリックを識別する文字列 Intent intent = new Intent(context, NewAppWidget.class); // 変更 intent.setAction("CLICK_WIDGET"); // 変更 PendingIntent pIntent = PendingIntent.getBroadcast(context, appWidgetId, intent , 0); //ウィジェットテキストの変更 views.setTextViewText(R.id.appwidget_text,"CLICK!"); //ウィジェットを押したときにインテントが発行される views.setOnClickPendingIntent(R.id.appwidget_text,pIntent); // Instruct the widget manager to update the widget appWidgetManager.updateAppWidget(appWidgetId, views); } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { // There may be multiple widgets active, so update all of them for (int appWidgetId : appWidgetIds) { updateAppWidget(context, appWidgetManager, appWidgetId); } } @Override public void onDeleted(Context context, int[] appWidgetIds) { // When the user deletes the widget, delete the preference associated with it. for (int appWidgetId : appWidgetIds) { NewAppWidgetConfigureActivity.deleteTitlePref(context, appWidgetId); } } @Override public void onEnabled(Context context) { // Enter relevant functionality for when the first widget is created } @Override public void onDisabled(Context context) { // Enter relevant functionality for when the last widget is disabled } @Override public void onReceive(Context context, Intent intent){ super.onReceive(context,intent); //Manifestに登録されたActionと発行したインテントが同じ場合に実行される if (intent.getAction().equals("CLICK_WIDGET")) { Toast.makeText(context, "クリックされました!", Toast.LENGTH_SHORT).show(); } } }これで無事にToast出力されるようになりました。
コードを修正したくない場合は、targetSdkVersionをバージョン25以下に下げてもうまく行きます。
- 投稿日:2019-12-18T11:40:00+09:00
アプリ開発初心者が Flutter に入門して 1 か月でアプリをリリースした話
はじめに
最近 Flutter に入門しました。無事に一つアプリをリリースすることができたのですが、ネイティブアプリの開発が全くの素人で苦労した点があったので、同じく入門する初心者の方に記録として残していこうと思います。
また、事情があり家の PC でなかなか開発が進められないため社内の PC で業務終了後に開発を進める形になりました。社内環境では Proxy にネットワーク通信が阻まれてしまうため、よくある入門の記事通りに行かないことがとにかく多くあり・・・(というかむしろここが一番苦労したと思います。)
同じく Proxy 環境での開発を余儀なくされている方の助けになれば幸いでございます。なお、Flutter やその周辺等知識が甘い部分が多いと自負しているため誤りやアドバイス等あればコメントしていただけると幸いです。ただちに確認致します。
リリースしたアプリの紹介
BundleApps
こちらのアプリです ⇒ BundleApps
BundleApps とは
インストールしてあるアプリを一覧に表示して使いたいアプリだけを選んで表示することができるアプリです。Web アプリだとStationやStackなど高機能なものがありますが、ネイティブアプリを管理するツールは現在ないのかなと思いました。(調査が足らないだけでもしかしたらあるかもしれない)比較するのもおこがましいほど簡素な作りですがまずは一つアプリをリリースしてみようと思った次第です。
本当は Firebase とバリバリ連携するアプリを作りたかったのですが後述する Proxy の弊害のせいでうまくいかなかったため、ローカルでこじんまりとデータを管理するつくりになっています・・・宜しければインストールして使ってみてください。
開発の際に参考にした書籍・サイトなど
書籍
下記二冊を購入しました。
⇒ 全くの初心者がガンガン読み進めていくのは難しいかなあという印象を受けました。アプリ開発の知見があって Flutter だとどのように書いていくかを知りたい人向けみたいな。まだ読み終えてません。
⇒ 情報が古いからか、Proxy のせいかもわかりませんが Firebase 連携の部分から動かなくて躓きました・・・説明はわりと丁寧かなと思うのですが Flutter は新しめで更新も早いので環境によっては動かないという人も多いと思います。
サイト
色々なところで言われていますが、入門より少し込み入ったことをやろうとすると英語のリソースが多めだと感じました。これ以外は直接パッケージの GitHub リポジトリを見に行ったり、やりたいことでキーワード検索をしたりして開発を進めていきました。
★ Flutter 公式
⇒ 公式が素晴らしいです。英語ですが確かに動きますし、チュートリアルが充実しています。環境構築からサンプルアプリの作成まで解説されているのでこれをみておけば入門としては間違いないと思います。⇒ Flutter で自動生成される初期サンプルを非常に丁寧に説明されています。初心者への配慮があり、理解の助けになりました。これを読んでから圧倒的に開発のスピードが上がった気がします。
⇒ まだ見始めた段階ですが、字幕付きの動画で電車などで学習を進める際に活用しています。
開発期間内訳
【1~2 週目】 Proxy 設定との戦い
問題発生
冒頭で申し上げた通り Proxy に阻まれ苦労しました・・・
Android Studio に抵抗があり、Visual Studio Code で開発を進めようと思い環境構築を行ったのですがシミュレータとの接続がうまくいかない。初期サンプルアプリが立ち上がらない。
Android Studio やコマンドラインからの flutter run だと無事に起動するのでこれは Visual Studio Code の問題だと判断したのですが検索しても有用な情報がなかなか見つからず悩みに悩んでいました。
光明差す?
Proxy 関連の設定だとあたりを付けてよくよく考えてみるとエミュレータと Visual Studio Code 間の通信はローカル通信ではないか?つまり逆に Proxy 設定ありで通信しようとしているからダメなのではないか?と考えて Visual Studio Code の no proxy 設定を検索しだします。
しかし、探せど探せどそれっぽい情報は見つからず・・・
付けるのは Setting.json の編集でいけるのにそこに no proxy の設定が見つかりませんでした。解決?
結論から言うと Visual Studio Code から proxy の設定を外すことで解決しました。
Visual Studio Code では setting.json に Proxy 設定を記入するとそちらを優先してしまうようなのですが、消した場合はシステム環境変数のほうを参照するような仕組みになっているようです。元々環境変数に
変数 値 http_proxy http://proxy-hogehoge.co.jp https_proxy http://proxy-hogehoge.co.jp no_proxy localhost,127.0.0.1 といった記載があったため、そちらを参照してくれるようになり無事にサンプルアプリが動作するようになりました!
さらば Firebase
サンプルアプリは立ち上がったものの書籍通りに進めても Build に成功はするものの Firestore から一切データが取得できないという事態に陥りました。
いくつもサンプルを写経して試したのにダメだったのでこれもやはり Proxy の弊害なのかなと。
アプリ側に Proxy を超えて通信する設定をする必要があるのかと考え調べたのですが良い情報が見つからず、結果一旦 Firebase との連携は諦めてできる範囲でアプリ開発をすることとしました。
※良い情報をお持ちの方がいらっしゃいましたら教えていただけるとありがたいです・・・
アプリ制作開始【3 週目】
躓いた点 ①
チュートリアルに毛が生えた程度のアプリなので、基本的には書籍や解説サイトを参考にしあげることができました。
ただそれでもいくつか躓いた点がありまして・・・まず Dart の非同期処理である async / await に関して理解が甘く、想定通りの動作にならず苦労しました。
具体的には
. . . //アプリ起動時に一度だけ実行される @override void initState() { super.initState(); . . . } . . .というウィジェット作成のタイミングで処理を行うことができる部分で、アプリ一覧を取得 ⇒ 表示という処理を行おうと思ったのですが
. . . // ローカルからアプリケーションのリストを取得する処理 _getLocalData() { SharedPreferences pref = await SharedPreferences.getInstance(); List<String> apk = pref.getStringList("apk") ?? new List<String>(); } // アプリケーション一覧を取得する処理 _createAppList() async { . . //ローカルの情報を元にアプリ情報を取得、リストに追加 apk.forEach((pn) async { ApplicationWithIcon app = await DeviceApps.getApp(pn, true); setState(() { _iconApps.add(app); }); }); } //アプリ起動時に一度だけ実行される @override void initState() { super.initState(); _getLocalData(); _createAppList(); . . } . . .と書いたところ一向にアプリケーションが格納されているはずのリストが空で半日程度悩んでいました。
結論として、
. . . // アプリケーション一覧を取得する処理 createAppList() async { // ここを中にいれる! // ローカルからアプリケーションのリストを取得する処理 SharedPreferences pref = await SharedPreferences.getInstance(); List<String> apk = pref.getStringList("apk") ?? new List<String>(); //ローカルの情報を元にアプリ情報を取得、リストに追加 apk.forEach((pn) async { ApplicationWithIcon app = await DeviceApps.getApp(pn, true); setState(() { _iconApps.add(app); }); }); } //アプリ起動時に一度だけ実行される @override void initState() { super.initState(); _createAppList(); . . } . . .このようにしたら無事にアプリのリストが取得できるようなりました。
認識に誤りがあったら訂正していただきたいのですが、async の内側で処理の完了を待つひとくくりということでしょうか? そもそも非同期処理の理解を深める必要があるなあと痛感した事例です・・・躓いた点 ②
package についてです。
device_appsという package を利用させていただいているのですが、README.md には
Image.memory(app.icon);でアプリのアイコンが取得できると書いてあるのにそのように書くと icon などという情報はないとエラーが出てしまい悩みました。
これも半日程度悩み、アイコン出せないんじゃ・・・と諦めかけたのですが GitHub ソースを読み issue を参照した結果何とか解決することができました。
issue には
' Cannot call "icon" method #20 '
とまさに問題の issue があり、その回答が
'you need to cast to ApplicationWithIcon to use.'
となっていて、なるほど!とソースを読みだした次第です。
. . static Future<Application> getApp(String packageName, [bool includeAppIcon = false]) async { if (packageName.isEmpty) { throw Exception('The package name can not be empty'); } return _channel.invokeMethod('getApp', { 'package_name': packageName, 'include_app_icon': includeAppIcon }).then((app) { if (app != null && app is Map) { return Application(app); } else { return null; } }).catchError((err) { print(err); return null; }); } . . . class ApplicationWithIcon extends Application { final String _icon; ApplicationWithIcon._fromMap(Map map) : assert(map['app_icon'] != null), _icon = map['app_icon'], super._fromMap(map); get icon => base64.decode(_icon); }となっていて、includeAppIcon のフラグを立てたうえでキャストしなきゃいけないのかな?と気づくことができました。
開発経験も浅く、Package のソースを見るということをしてこなかったのですが良い解決策を得ることができると学べたので今後は積極的にソースを参照する癖を付けたいと思います。
結論と今後の展望
- Flutter(Dart)の仕様理解が甘い部分が多いので一旦書籍や動画で理解を深めることから始めたいと思います。
- コードを読むと得られるモノが多かったので Package を利用したい場合はその内側まで踏み込んで利用したいと思います。
今後は付き合いのある店舗のポイントカードアプリを作成してみようかと考えているのですが、流石に高機能になるのかなと・・・
Firebase との連携も必要でしょうし、おすすめのリソースがあればコメントいただけるとありがたいです。特に切実に Proxy 関連( ^ ω ^)・・・
最後に
ここまで読んでいただきありがとうございました!
よろしければアプリを使ってみてもらえたら嬉しいです。
BundleApps