20191218のAndroidに関する記事は11件です。

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.kt
class 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.kt
class 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.md
Attention: 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.kt
class 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.kt
class 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.kt
class 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.kt
class 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.kt
data class SampleBindingData(
    val text: String
)
MainActivity.kt
class 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.kt
class MainPresenter() {
    fun submit(view: View) {
        // do something
        println("Click $view")
    }
}
MainActivity.kt
class 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を利用すれば、ボイラープレートコードも少なく、型も安全に使えます。
また、LiveDataViewModelといった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.kt
class 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のスライドで紹介されていた表がこちらです。
how_to_access_view.png

個人的な見解をまとめます。

  • Kotlin Android Extensions
    • 記述のしやすさが良い
    • 厳格な型の安全性をそこまで求めないなら十分
  • View Binding
    • 型の安全性を求めるならこれ
    • ただし、まだAndroid Studio 3.6はベータ版
  • Data Binding
    • データのバインディングまで利用するなら機能的にもこれ一択

さいごに

最後までお読みいただきありがとうございました!
明日は@azawakhさんによる「Node.js 13.2.0 で--experimental-modules外れたのでESMを試してみた」です。お楽しみに!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Jetpack Composeでサンプルアプリを作ってみる

Jetpack Composeとは

AndroidのUIは基本的にはXMLで作成していきますが、Jetpack ComposeはGoogle I/O 2019で発表された宣言型のUIツールキットです。今は、まだプレアルファ版ですが2020年を目標にベータ版が公開される予定です。

作成するサンプルアプリ

今回は、Qiitaの記事を一覧表示するサンプルアプリを作成していきたいと思います。
※サンプルアプリの案は入社当時にお世話になったKotlinスタートブックにあったものを参考にしました)

作成するサンプルアプリの画面は以下のようになっています。

環境

Android Studio4.0 Canary6
targetSdkVersion 29

プロジェクト作成

  1. Start a new Android Studio Projectを押して、Empty Compose Activityを選択
  2. プロジェク名を入力し、Finish

スクリーンショット 2019-12-18 17.09.11.png

Jetpack Composeの使い方

基本的な使い方

  1. @Composableを付けた関数の中に、コンポーネントを記述する
  2. setContentの中で@Composableを付けた関数を呼び出す
MainActivity.kt
class 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上に表示されるプレビュー画面はこんな感じです。

スクリーンショット 2019-12-18 18.09.06.png

データクラスの作成

では、さっそくサンプルアプリを作成していきます。
まずは、サンプルアプリで使用するデータを定義します。
定義するデータは記事とユーザで、以下のようにクラスを作成しました。

Article.kt
package com.example.qiita.model

data class Article(
    val id: String,
    val title: String,
    val url: String,
    val user: User
)
User.kt
package com.example.qiita.model

data class User(
    val id: String,
    val name: String,
    val profileImageUrl: String
)

記事のコンポーネントを作成

下準備

まず、画像を描画するのにDrawImageを使う必要があるのですが、プロジェクト作成当初だと使えないので使えるようにしていきます。build.gradleに以下のコード追加しました。

build.gradle
dependencies{
    //追加するコードだよ
    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.kt
class 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

実行結果

MainActivity.kt
class 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.kt
class 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しなければいけないのでそこが不便だと感じました。今はまだプレアルファ版ですが、ベータ版がすごい楽しみです。

参考にしたもの:bow_tone1:

Kotlinスタートブック
Jetpack Compose
Jetpack Compose Tutorial
android/compose-samples

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

時代は匿名SNSを求めているので作った!(というか一瞬動いた・・か)

まえおき

ひとり開発 Advent Calendar 2019の18日目!エントリの空きを突然頂戴して無理やり当日凸してます!

余談ですが作業ミスエントリ流行ってるんですかね。Advent Calender以外でも色々書かれてる気がします。
当方もまさにこの実装をギブアップしたのでドシドシ書いていこうと思います。笑
11月末から作り始めたのに途中から全く目途が立たなくなったのでどこぞのAdvent Calenderにエントリできなかったっていうのを諦めてたのに強引に滑り込ませたという経緯ですね・・

前回の続き

イノベーション仮説のMVPを作ったらクソゲ―になった!

バズった割にあんまキてない技術の一つとして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して文字列にします。

デモ

1.gif

メッセージを送信しています。実装の通りBluetoothのアイコンがぺかぺかしているのが右上で見れます。

2.gif

送信されたメッセージが一個ずつ受信されデコードされています。
よくあるメッセンジャーサービスのようで実装に書いた通り、この送受信の間に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で書いておいてスマホ用に少しずつ実装していくスタイルはめちゃ軽くて好きなんだけどなあ・・残念。

ていう、誰にも聞きこめない、ひとり開発的な悶々でオトシます。
読んで頂いた方に多謝!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactNative AndroidのBrige解説

機能

  • Native Modules
    出来ること
    ・関数の作成
    ・イベントの通知
    ・コールバック

  • Native UI Components
    出来ること
    ・Viewの作成
    ・Viewのイベントの通知

Native Modules

ダイアログを表示するModuleを例にします。

[機能]
1, Androidのダイアログを表示する。(関数の作成)
2, ダイアログの"OK"ボタンがクリックされたらコンソールに"true"が表示される。(イベントの通知)
3, "DATE"ボタンがクリックされたら現在時刻が表示される。(コールバック)

DialogModule.java
public 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.java
public 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.java
public 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.jsx
import 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 Dialog

Native UI Components

VideoView.jsx
import 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.java
public 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 とかの話」です :tada:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactNative AndroidのBridge解説

機能

  • Native Modules
    出来ること
    ・関数の作成
    ・イベントの通知
    ・コールバック

  • Native UI Components
    出来ること
    ・Viewの作成
    ・Viewのイベントの通知

Native Modules

ダイアログを表示するModuleを例にします。

[機能]
1, Androidのダイアログを表示する。(関数の作成)
2, ダイアログの"OK"ボタンがクリックされたらコンソールに"true"が表示される。(イベントの通知)
3, "DATE"ボタンがクリックされたら現在時刻が表示される。(コールバック)

DialogModule.java
public 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.java
public 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.java
public 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.jsx
import 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 Dialog

Native UI Components

VideoView.jsx
import 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.java
public 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 とかの話」です :tada:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android 4.4のサポートはもうやめるべきなのだ

minSdkVersionをつぶやくだけの謎な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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.coroutineskotlinx.serializationkotlinx.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 = value

Kotlin 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内でのbreakcontinueを有効にします。

ありがとうございます!

Kotlin EAPと実験的な機能を試し、フィードバックをくれた皆様に本当に感謝しています。我々は皆様と一緒にKotlin言語を開発し、皆さんの貴重なインプットに基づいて多くの設計の決定を行っています。この迅速で効果的なフィードバックループをコミュニティと共有することは、Kotlinがベストな状態になるためには非常に重要です。

Kotlinを使って、素晴らしいものをたくさん作ってくれたコミュニティのメンバー全員に感謝しています。これからもKotlinと一緒に続けましょう!

ちなみに、IntelliJ IDEAとAndroid Studio内のKotlinプラグインは、その機能の利用に関する匿名の統計を収集します。何がうまくいっているのか、何が問題を引き起こしているのか、何を改善すべきなのかを理解するのに役立つので、これらの統計に同意するようにお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

@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だとキッチリ宣言しないと別の意味になってエラーになってるのかな...???

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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でしたらActivityCoroutineContextを作成し、onDestroy()で破棄するように設定しておくと、そこから動かした子Coroutineは自動的に破棄されて安全に扱えます!

この辺りの親子関係について以下の記事がとても分かりやすいです!
図で理解する Kotlin Coroutine(kawmraさま)

だいたい分かってきた時に押さえておきたい

チュートリアルや紹介では頻繁に出てくるGlobalScope.launch()ですが、実はアンチパターンです。
Globalなので、ライフサイクルとして最長なためです。

この辺りのアンチパターンがまとまっているので、ある程度理解してきたら一読しておく価値ありです!
Kotlin Coroutinesパターン&アンチパターン (ikemura23さま)

RxJavaの「あれ」ってKotlin Coroutineではどうするの?[基本編]

ここからが本題です。
Kotlin Coroutineをだいたい理解したところで、Rxとの対応が知りたくなります。
「CoroutineはRxの代替では無い。」と書きましたが、Flowの登場により、Rxの代替としても使えます。
そして、Flow型にはmapfilterscanretryなどRxで見慣れたオペレーターが揃っています。
なので、ObservableFlowはほとんど互換といって差し支えありません。
しかも、RxのSingleCompletableはCoroutineでは、もっと簡単に書けます。

Rxの「あれ」をKotlin Coroutineでやりたい!という気持ちに答えていきます。

RxとCoroutineの違いを大雑把に考察

CoroutineはRxに比べて型が厳しいです。
例えば、RxのHot Stream, Cold Streamを型で分類できません。一つ一つ覚える必要があります。
逆にRxはObservableのオペレーターが全てなので、SubjectだろうがSingleだろうが同じように扱えます。

一方で、CoroutineはHot StreamChannelクラス、Cold StreamFlowクラスといったように、型が厳密です。
ChannelFlowに生えているオペレーターはまるで別物です。
さらに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 funCold 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 funasync{}で囲むことで、並列に処理ができるようになります。
全てが終了するまで待つときは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を何となく理解する
  • SingleCompletableを使いたくなったらsuspend funを使う
  • Observableを使いたくなったらFlowを使う
  • BehaviorSubjectなどのSubject系を使いたくなったらChannelを使う

ここでは細かく書きませんでしたが、ObservableFlowのオペレーターは似ていますが、異なる部分も多々あります。
最終的には公式ドキュメントが一番ですので、概要を理解したら公式ドキュメントを読んでみてください。
RxのリファレンスCoroutineのリファレンス

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Androidアプリ開発Tips】Widgetをクリックしてもブロードキャストされない

初めてAndroidのWidgetアプリを作る事になり、ネット上のサンプルアプリを動かそうとしたら、ウィジェットをクリックした時にブロードキャストが受け取れない(発生しない)という状況になり、ハマりました。
同じ悩みを抱える人のために取り急ぎ解決方法を記載しておきます。

結果的には、SDKバージョンの仕様変更によるものでした。
わたしのAndroid Studioは、2019年12月時点で最新バージョンを導入しています。

<前提>
・Android Studio 3.5.3
・compileSdkVersion 29
・buildToolsVersion "29.0.2"
・minSdkVersion 15
・targetSdkVersion 29

1.サンプルコード

以下の二つを実装してみましたが、うまく行きません。
・クリックすると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.java
package 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.java
package 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以下に下げてもうまく行きます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アプリ開発初心者が Flutter に入門して 1 か月でアプリをリリースした話

はじめに

最近 Flutter に入門しました。無事に一つアプリをリリースすることができたのですが、ネイティブアプリの開発が全くの素人で苦労した点があったので、同じく入門する初心者の方に記録として残していこうと思います。

また、事情があり家の PC でなかなか開発が進められないため社内の PC で業務終了後に開発を進める形になりました。社内環境では Proxy にネットワーク通信が阻まれてしまうため、よくある入門の記事通りに行かないことがとにかく多くあり・・・(というかむしろここが一番苦労したと思います。)
同じく Proxy 環境での開発を余儀なくされている方の助けになれば幸いでございます。

なお、Flutter やその周辺等知識が甘い部分が多いと自負しているため誤りやアドバイス等あればコメントしていただけると幸いです。ただちに確認致します。

リリースしたアプリの紹介

BundleApps

こちらのアプリです ⇒ BundleApps

BundleApps とは

インストールしてあるアプリを一覧に表示して使いたいアプリだけを選んで表示することができるアプリです。Web アプリだとStationStackなど高機能なものがありますが、ネイティブアプリを管理するツールは現在ないのかなと思いました。(調査が足らないだけでもしかしたらあるかもしれない)比較するのもおこがましいほど簡素な作りですがまずは一つアプリをリリースしてみようと思った次第です。

本当は Firebase とバリバリ連携するアプリを作りたかったのですが後述する Proxy の弊害のせいでうまくいかなかったため、ローカルでこじんまりとデータを管理するつくりになっています・・・宜しければインストールして使ってみてください。

開発の際に参考にした書籍・サイトなど

書籍

下記二冊を購入しました。

⇒ 全くの初心者がガンガン読み進めていくのは難しいかなあという印象を受けました。アプリ開発の知見があって Flutter だとどのように書いていくかを知りたい人向けみたいな。まだ読み終えてません。

⇒ 情報が古いからか、Proxy のせいかもわかりませんが Firebase 連携の部分から動かなくて躓きました・・・説明はわりと丁寧かなと思うのですが Flutter は新しめで更新も早いので環境によっては動かないという人も多いと思います。

サイト

色々なところで言われていますが、入門より少し込み入ったことをやろうとすると英語のリソースが多めだと感じました。これ以外は直接パッケージの GitHub リポジトリを見に行ったり、やりたいことでキーワード検索をしたりして開発を進めていきました。

  • Flutter 公式
    ⇒ 公式が素晴らしいです。英語ですが確かに動きますし、チュートリアルが充実しています。環境構築からサンプルアプリの作成まで解説されているのでこれをみておけば入門としては間違いないと思います。

  • Flutter はじめの一歩

    ⇒ Flutter で自動生成される初期サンプルを非常に丁寧に説明されています。初心者への配慮があり、理解の助けになりました。これを読んでから圧倒的に開発のスピードが上がった気がします。

  • Udacity

    ⇒ まだ見始めた段階ですが、字幕付きの動画で電車などで学習を進める際に活用しています。

開発期間内訳

【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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む