20210110のAndroidに関する記事は8件です。

getPackageManagerでインストールアプリ一覧が表示されない(SDK30・Android11)

これまでgetPackageManager()#getInstalledPackages()を用いれば簡単に端末内のインストールアプリ情報を取得することができました。
しかし、Android 11(SDK30)からは単に下記のようなコードを記載するだけではシステムアプリ情報しか取得できなくなりました。

例)デバイス内のインストールアプリをLog出力する

MainActivity.java
PackageManager pm = getApplicationContext().getPackageManager();
        List<PackageInfo> appInfoList = pm.getInstalledPackages(0);
        for(int i =0;i<appInfoList.size();i++){
            Log.d("MainActivity",appInfoList.get(i).applicationInfo.loadLabel(pm).toString());
        }

何がダメだったか

Android 11(SDK30)からはセキュリティ的に厳しくなっており、外部パッケージへのアクセスについてはManifestに明示する必要があります。

解決策

AndroidManifest.xmlにパーミッションの記述を書いてしまう。

AndroidManifest.xml
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>

…全部許可してしまうので非推奨なやり方だと思われます。
本来は<queries>で囲って個別に明示するのが正しい方法なのかも。

参考

Android 11 でのパッケージへのアクセス - Android Developers

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

getPackageManagerでインストールアプリ一覧が表示されない(Android11・APIレベル30)

これまでgetPackageManager()#getInstalledPackages()を用いれば簡単に端末内のインストールアプリ情報を取得することができました。
しかし、Android 11(APIレベル30)からは単に下記のようなコードを記載するだけではシステムアプリ情報しか取得できなくなりました。

例)デバイス内のインストールアプリをLog出力する

MainActivity.java
PackageManager pm = getApplicationContext().getPackageManager();
        List<PackageInfo> appInfoList = pm.getInstalledPackages(0);
        for(int i =0;i<appInfoList.size();i++){
            Log.d("MainActivity",appInfoList.get(i).applicationInfo.loadLabel(pm).toString());
        }

何がダメだったか

Android 11(APIレベル30)からはセキュリティ的に厳しくなっており、外部パッケージへのアクセスについてはManifestに明示する必要があります。

解決策

AndroidManifest.xmlにパーミッションの記述を書いてしまう。

AndroidManifest.xml
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>

…全部許可してしまうので非推奨なやり方だと思われます。
本来は<queries>で囲って個別に明示するのが正しい方法なのかも。

参考

Android 11 でのパッケージへのアクセス - Android Developers

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

ConstraintLayoutの高級属性を使いましょう

現在プロジェクトを新たに生成する時、ConstraintLayoutが自動で作られました。
今回は開発の実用例によって説明します。

GuideLine

  1. 補助線の方向を設置する ログイン画面でよく使われたレイアウトです。 android:orientation="vertical"
    layout_constraintGuide_begin 左側と上の距離
    layout_constraintGuide_end 右側と下の距離
    layout_constraintGuide_percent パーセント%で設定

image.png
      
 

<androidx.constraintlayout.widget.ConstraintLayout 
xmlns:android="http://schemas.android.com/apk/res/android"       
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"//縦のラインを設定
        app:layout_constraintGuide_begin="120dp" //縦のラインを設定する場合は、左から120dp
          />

    <TextView
        android:id="@+id/tv_username"
        android:layout_width="wrap_content"
        android:layout_height="60dp"
        android:layout_marginTop="72dp"
        android:gravity="center_vertical"
        android:text="用户名"
        app:layout_constraintEnd_toStartOf="@+id/guideline"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_password"
        android:layout_width="wrap_content"
        android:layout_height="60dp"
        android:gravity="center_vertical"
        android:text="密码"
        app:layout_constraintEnd_toStartOf="@+id/guideline"
        app:layout_constraintTop_toBottomOf="@+id/tv_username" />

    <EditText
        android:id="@+id/et_username"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="@+id/tv_username"
        app:layout_constraintStart_toStartOf="@+id/guideline" />

    <EditText
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="@+id/tv_password"
        app:layout_constraintStart_toEndOf="@+id/tv_password" />
</androidx.constraintlayout.widget.ConstraintLayout>

Group

1.constraint_referenced_idsの属性を通じて、レイアウトのネストを減らせます。

image.png

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.hencoder.Helpers">

    <Button
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:id="@+id/button"
            android:text="group"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>



    <androidx.constraintlayout.widget.Group
            android:id="@+id/group"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            //コントロールのグループに対してsetVisibilityを同一に設定できます
            app:constraint_referenced_ids="view,view1,view7,view8" />


    <ImageView
            android:id="@+id/view1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="48dp"
            android:src="@mipmap/ic_launcher"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    <ImageView
            android:id="@+id/view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_launcher"
            app:layout_constraintStart_toEndOf="@+id/view1"
            app:layout_constraintTop_toTopOf="@+id/view1" />

    <ImageView
            android:id="@+id/view7"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="80dp"
            android:layout_marginTop="72dp"
            android:src="@mipmap/ic_launcher"
            app:layout_constraintStart_toEndOf="@+id/view1"
            app:layout_constraintTop_toTopOf="@+id/view1" />

    <ImageView
            android:id="@+id/view8"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="72dp"
            android:src="@mipmap/ic_launcher"
            app:layout_constraintStart_toEndOf="@+id/view1"
            app:layout_constraintTop_toTopOf="@+id/view1" />

</androidx.constraintlayout.widget.ConstraintLayout>

ConstraintSet

setContentIdを使用して、指定したコントロールをプレースホルダーの位置に配置します。

image.png

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/constraintLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.hencoder.PlaceHolder">

    <androidx.constraintlayout.widget.Placeholder
        android:id="@+id/placeholder"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:layout_marginTop="16dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/favorite"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:onClick="onClick"
            android:src="@drawable/ic_favorite_black_24dp"
            android:tint="#E64A19"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@id/mail"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/mail"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:onClick="onClick"
            android:src="@drawable/ic_mail_black_24dp"
            android:tint="#512DA8"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@id/save"
            app:layout_constraintStart_toEndOf="@id/favorite"
            app:layout_constraintTop_toTopOf="parent" />

    <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/save"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:onClick="onClick"
            android:src="@drawable/ic_save_black_24dp"
            android:tint="#D32F2F"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@id/play"
            app:layout_constraintStart_toEndOf="@id/mail"
            app:layout_constraintTop_toTopOf="parent" />

    <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/play"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:onClick="onClick"
            android:src="@drawable/ic_play_circle_filled_black_24dp"
            android:tint="#FFA000"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@+id/save"
            app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

画像を押すと位置を交換できます

class PlaceHolder : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_place_holder)
    }

    fun onClick(view: View) {
        findViewById<Placeholder>(R.id.placeholder).setContentId(view.id)

    }
}

Flow

wrapMode
chain aligned none(デフォルト)
image.png

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.hencoder.FlowActivity">

    <androidx.constraintlayout.helper.widget.Flow
        android:id="@+id/flow"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:background="@color/colorAccent"
        android:orientation="horizontal"
        app:flow_wrapMode="chain" //モード
        app:flow_verticalGap="16dp"
        app:flow_horizontalGap="16dp"
        app:constraint_referenced_ids="view1,view2,view3,view4,view5,view6"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <View
        android:id="@+id/view1"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="@color/colorPrimaryDark" />

    <View
        android:id="@+id/view2"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="@color/colorPrimaryDark" />

    <View
        android:id="@+id/view3"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_margin="8dp"
        android:background="@color/colorPrimaryDark" />

    <View
        android:id="@+id/view4"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="@color/colorPrimaryDark" />

    <View
        android:id="@+id/view5"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="@color/colorPrimaryDark" />

    <View
        android:id="@+id/view6"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="@color/colorPrimaryDark" />

</androidx.constraintlayout.widget.ConstraintLayout>

この開発する時よく使う四つ属性を紹介しました。実はどうやってこの属性を活用するのが大事だと思ってます。

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

AndroidスマホをNFCタグにかざして家電を操作する方法

iPhone(iOS13以降)ではショートカットを利用してNFCタグにスマホをかざして家電を操作する方法がありますが、Androidスマホでも同じことができないか色々調べて試してみようと思いました。
そこで今回はNature Remoを利用してNFCタグにAndroidスマホをかざして家電を操作する方法についてまとめました

準備するもの

・PC
・NFC対応Androidスマホ
・Nature Remo(旧版でもminiでもOK)
https://www.amazon.co.jp/Nature-%E3%82%B9%E3%83%9E%E3%83%BC%E3%83%88%E3%83%AA%E3%83%A2%E3%82%B3%E3%83%B3-Remo-%E3%83%8D%E3%82%A4%E3%83%81%E3%83%A3%E3%83%BC%E3%83%AA%E3%83%A2-Remo-1W3/dp/B08BLSLWH4
・NFCタグ(対応しているものをお使いください、以下は例です)
https://www.amazon.co.jp/gp/product/B079JTYTWV/
・Nature Remoアプリ
https://play.google.com/store/apps/details?id=global.nature.remo&hl=ja&gl=US
・NFC Tools Pro Editionアプリ(HTTP POSTするために有料版を使用)
https://play.google.com/store/apps/details?id=com.wakdev.nfctools.pro&hl=ja&gl=US

方法

Nature RemoのCloud APIを使用します。それには、「アクセストークン」と「Signal ID」が必要となります。

アクセストークンの発行

まずは以下のリンク先から、Nature Remoのアプリで登録したアカウントを使いログインをして、「home.nature.global」からのアクセスを許可します。

https://home.nature.global/

アクセスを許可すると以下の画面へ移行するので「Go」をクリックします。

Home-2018-07-19-21-47-41.jpg

すると、以下のようなNature Remoのアクセストークンを管理できるページへ移動するので、ページ内の左下あたりにある「Generate access token」をクリックします。

スクリーンショット 2021-01-10 19.23.51.png

「Generate access token」をクリックすると、以下の画面のように新しいアクセストークンが表示され、有効なアクセストークンのリストに追加されます。

また、表示されているアクセストークンの下にある「Copy」をクリックするとアクセストークンがクリップボードにコピーされます。

スクリーンショット 2021-01-10 19.27.56.png

上記のアクセストークンが表示されているページは、更新するとアクセストーク表示が消えてしまい、再表示することはできません。

そのため、アクセストークンがわからなくなった場合には、もう一度「Generate access token」をクリックして新しいアクセストークンを発行する必要があります。

Signal IDについて

Nature Remoアプリでリモコンのボタンを一つづつ登録すると、ボタン毎にSignal IDが振られます。そのSignal IDは、Cloud APIでリモコン送信を行う際に利用します。

Signal IDなどのリモコン情報を取得する方法

Cloud APIを使いSignal IDを含むリモコン情報を取得するには、まず、Nature Remoのアプリを使い赤外線リモコンを登録します。

今回の例では、アプリで「新しい家電を追加する」ボタンから「電気」を追加して名前を「電気」とします。

そして、追加した家電内に照明をコントロールするリモコンのON、OFFの赤外線ボタンを、それぞれ「オン」と「オフ」という名前で登録します。

Signal IDを含むリモコン情報を取得するには、コマンドライン(Macのターミナル・Windows のコマンドプロンプトなど)で以下のcurlコマンドを実行します。

XXXXXXXXの箇所には自身で発行したアクセストークンの値を指定します。

curl -X GET "https://api.nature.global/1/appliances" -H "accept: application/json" -H "Authorization: Bearer XXXXXXXX"

上記のコマンドを実行すると、次のようなJSONデータが取得できます。

[
  {
    "id": "XXXXXXXX",
    "device": {
        // 中略(Nature Remoのデバイスのデータ)
    },
    "model": null,
    "nickname": "電気",
    "image": "ico_light",
    "type": "IR",
    "settings": null,
    "aircon": null,
    "signals": [
      {
        "id": "XXXXXXXX",
        "name": "オン",
        "image": "ico_on"
      },
      {
        "id": "XXXXXXXX",
        "name": "オフ",
        "image": "ico_off"
      }
    ]
  }
]

※jsonが整形されていない場合は以下のようなツールを使用するかHomebrewでjqを導入して整形してみてください
http://tools.m-bsys.com/development_tooles/json-beautifier.php
https://qiita.com/tomitz/items/f48a6bfae1123cadc69a

Nature Remoアプリでリモコンのボタンを一つづつ登録した場合、JSONデータ内のキー"type"の値が"IR"となります。

そして、 キー"signals"の値にリモコンボタンの情報がオブジェクトの配列で取得できます。

今回の例では、照明リモコンのボタン「オン」「オフ」を登録したため、その2つのリモコンボタンが、キー"signals"の値にオブジェクトの配列として取得されています。

キー"id"の値が割り振られたSignal IDとなります。また、キー"name"と"image"の値 は、アプリで赤外線ボタンを登録した際の「ボタン名」と「アイコン名」となります。

なお、JSONデータの先頭にあるキー"id"の値はAppliance IDとなり、アプリで登録した家電毎に割り振られるIDとなります。

Nature RemoのCloud APIを利用し赤外線信号を送信するタスクを作成

以下、NFC Tools Pro EditionでCloud APIを使用し、Nature Remoから赤外線信号を送信するタスクの作成手順となります。

「タスク」タブを開いて「タスクを追加」をタップします
Screenshot_20210110-194750.png

「ネットワーク」をタップします
Screenshot_20210110-195022.png

「HTTP POST」をタップします
※有料版でなければ使用できないようなので注意
Screenshot_20210110-195122.png

POSTパラメータの横の「+」をタップします

「リクエスト」に「https://api.nature.global/1/signals/XXXXXXXX/send 」と入力します
XXXXXXXXの箇所は、操作したい赤外線送信ボタンのSignal IDを指定します

「名前」に「access_token」、「値」に発行した自身のアクセストークンを入力します

入力したらOKをタップします
Screenshot_20210110-195215.png

「書く」をタップしてNFCタグにスマホをかざして情報を書き込みます
Screenshot_20210110-195323.png

アプリを閉じてもう一度NFCタグにスマホをかざしてみて電気がオンorオフできたら成功です
PXL_20210110_083715696.jpeg

(´ - `).。oO(安っぽい。。w)

最後に

Nature Remoを利用してNFCタグにAndroidスマホをかざして家電を操作する方法についてまとめました。
あくまでこれは一つの方法であって、やり方は色々あると思うので参考になれば幸いです。

参考

https://blog-and-destroy.com/12244
https://support.nature.global/hc/ja/articles/900001736023--iOS13%E4%BB%A5%E9%99%8D-%E3%82%B7%E3%83%A7%E3%83%BC%E3%83%88%E3%82%AB%E3%83%83%E3%83%88%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6-Siri%E3%81%8B%E3%82%89Nature-Remo%E3%82%92%E6%93%8D%E4%BD%9C%E3%81%99%E3%82%8B

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

【Android】公式に記載されている Glide のセットアップ方法は間違っている

あらすじ

Glideの README の「Download」を見ると、Gradle のセットアップ方法が以下のように記載されています。

app/build.gradle
dependencies {
  implementation 'com.github.bumptech.glide:glide:4.11.0'
  annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
}

./gradlew buildコマンドでビルドをしていた時に、Glide で以下のような警告が出ていることに気付きました。

app: 'annotationProcessor' dependencies won't be recognized as kapt annotation processors. Please change the configuration name to 'kapt' for these artifacts: 'com.github.bumptech.glide:compiler:4.11.0'.

解決方法

警告文の通り、app/build.gradleannotationProcessorとなっている箇所を、kaptに変更してください。

app/build.gradle
dependencies {
    def glide_version = "4.11.0"
    implementation "com.github.bumptech.glide:glide:$glide_version"
    kapt "com.github.bumptech.glide:compiler:$glide_version"
}

kapt について

kotlin-annotation-processing toolsの略で、Kotlin でもアノテーションを使えるようにするためのツールのようです。
下記の記事が参考になりました。

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

rooted+MagiskなAndroidのアップデート

rooted+MagiskなAndroid端末にアップデートをあてるには、いくつか作業があります。
今回は下記前提条件のもとでAndroid端末にアプデをあてたいと思います。

■前提条件

  • bootloader unlock済かつmagiskを導入済
  • Clockworkmod recovery等のカスタムRecoveryは導入していない
  • Androidセキュリティアップデートを2020年12月5日から2021年1月5日へアップデート
  • 端末はPixel 3 XL
  • Androidは11(APIは30)
  • Magiskは21.2

■作業

1 必要なファイルの用意

すべてFactory Images for Nexus and Pixel Devicesで揃います。

(1)Full OTA Image(zip)

Factory Images for Nexus and Pixel Devicesの左ペインから「Full OTA Images」へアクセスし、必要なOTAファイルをダウンロードします。
今回はPixel 3 XLに2021年1月15日のアップデートをあてるので、「"crosshatch" for Pixel 3 XL」の11.0.0 (RQ1A.210105.003, Jan 2021)をダウンロードします。
スクリーンショット 2021-01-10 16.31.09.png

ダウンロードしたzipファイルはadbのパスが通っているところに置いておきます。

(2)(アップデート前のバージョンの)boot.img

アプデをあてる前に、弄っていないboot.imgを先に当てておく必要があります。
(Magiskを適用する際に、 patchedなboot.imgをあてた状態になっているため)
後述しますが、前回アップデート適用時に保存しておいたものを使ってください。
「保存し忘れた!」という人は下(3)を参考に、現在バージョンのboot.imgを用意してください。
(今回の例ならば、Factory Imageの11.0.0 (RQ1A.201205.003, Dec 2020, All carriers except Verizon)を使用します。)

(3)(アップデート後の)boot.img

Factory Images for Nexus and Pixel Devicesの左ペインから「Factory Images」へアクセスし、OTAと同じバージョンのFactory Image(今回は11.0.0 (RQ1A.210105.003, Jan 2021))をダウンロードします。(今回は2021年1月15日版)
スクリーンショット 2021-01-10 13.50.24.png

zipファイルをダウンロートしたら、任意の場所に解凍します。
解凍したファイルの中にあるimage-crosshatch-rq1a.210105.003.zipを更に解凍します。
(下のスクショだとimage-crossha....003.zipとなっているzip)
スクリーンショット 2021-01-10 16.41.14.png

更に解凍したファイルの中にあるboot.imgが、今回必要な現在バージョンのboot.imgになります。スクリーンショット 2021-01-10 16.36.55.png
このboot.imgは、次回アップデート時(今回の例では2021年2月5日前後のアップデート)に必要になるので、どこかに保存しておくと良いでしょう。(保存し忘れてもダウンロードできます。)

2 (現在バージョンの)boot.imgをあてる

ターミナル(コマンドプロンプト)から、現在バージョンのboot.imgをあてます。(便宜上、1205boot.imgというファイル名にしています。)

adb reboot bootloader
fastboot flash boot 1205boot.img
fastboot reboot

cmd1.png

3 OTAをあてる

ターミナル(コマンドプロンプト)から、recoveryに入ります。

adb reboot recovery

画面中央に腹が開いたドロイド君と「No command」と表示が出たら、電源ボタンを押しながら音量上ボタン(Volume+)を一度押すと、Android Recoveryに入ることができます。
「Apply update from ADB」を選択(音量ボタンで上下・電源ボタンで決定)すると、adbからの入力を受け付けるようになるので、ターミナル(コマンドプロンプト)からOTAをあてます。(便宜上、0105ota.zipというファイル名にしています。)

あてた後、Recoveryの「Reboot system now」を選択して再起動します。

adb sideload 0105ota.zip

cmd2.png

4 Magiskでpatchedなboot.imgを作成する。

ここまででOTAはあてられましたが、root権限が消失しているので、復活させます。
Screenshot_20210110-172814.png

アップデート後のboot.imgを、端末内に配置します。(ファイル名は0105boot.imgとしています。)
Download内に置くと便利です。

Magiskからインストール→パッチするファイルの選択→Let's Goでpatchedなboot.imgがDownloadフォルダ内に生成されます。(magisk_patched_A4gr0.img)

mgsk1.png
mgsk2.png
mgsk3.png
Screenshot_20210110-193052.png

このmagisk_patched_A4gr0.imgをAndroid端末からPCのadbパスが通っているフォルダに置いておきます。

5 patchedなboot.imgをあてる

magisk_patched_A4gr0.imgを、ターミナル(コマンドプロンプト)からあてます。
(ファイル名を便宜上0105boot_patched.imgとしています)

adb reboot bootloader
fastboot flash boot 0105boot_patched.img
fastboot reboot

cmd3.png

これで終了です。

参考:How to sideload the Android 9 Pie OTA on Pixel, Pixel XL, Pixel 2, Pixel 2 XL - 9To5 Google

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

TensorFlowとCameraXでリアルタイム物体検知Androidアプリ

今回やること

CameraXTensorflow liteを使ってリアルタイムに物体検知するアプリをcameraXの画像解析ユースケースを使ってサクッと作っていきます。
(注: CameraXの実装は1.0.0-rc01のものです。)
GitHubリポジトリを今記事最下部に載せてますので適宜参照してください。
ちょっと長めなのでとりあえず試したい方はリポジトリを見てください。

こんな感じのもの作っていきます↓

バウンディングボックスとスコアを表示するものです

モデルの用意

物体検知に使用する訓練済みモデルを探してとってきます。
今回はTensorFlow Hub のssd_mobileNet_v1を使用します。tfliteモデルをダウンロードします。
ssd_mobileNet_v1はこんな感じのモデルです。

input
shape 300 x 300
color channel 3
output sahpe
location [1, 10, 4] バウンディングボックス
category [1, 10] カテゴリラベルのインデックス (91クラスのcoco_datasetで学習したモデルです)
score [1, 10] 検出結果のスコア
number of detection [1] 検出した物体の数(今回のモデルは10で一定)

TensorFlow Hubにはほかにも色々訓練済みモデルがあるので好きなものを選んでください。
ただ、input sizeが大きいものはパラメータ数が多くAndroidだと推論に時間がかかるので注意が必要です。
また、場合によってはtfliteモデルを自分でエクスポートする必要がある場合もあります。

今回はそのままモデルを使いますが、Tensorflow APIとか使って転移学習させるのも面白そうですね。

Android Studio で実装

gradle

Tensorflow lite APIとCameraX、カメラ権限用にpermission dispatcherの依存関係を追加します。

build.gradle
    // permissionDispatcher
    implementation "org.permissionsdispatcher:permissionsdispatcher:4.7.0"
    kapt "org.permissionsdispatcher:permissionsdispatcher-processor:4.7.0"

    // cameraX
    def camerax_version = "1.0.0-rc01"
    implementation "androidx.camera:camera-core:${camerax_version}"
    implementation "androidx.camera:camera-camera2:$camerax_version"
    implementation "androidx.camera:camera-lifecycle:$camerax_version"
    implementation "androidx.camera:camera-view:1.0.0-alpha20"

    // tensorflow lite
    implementation 'org.tensorflow:tensorflow-lite:2.2.0'
    implementation 'org.tensorflow:tensorflow-lite-support:0.0.0-nightly'

assetsフォルダの用意

先ほどダウンロードした.tfliteモデルをAndroid Studioのassetsフォルダに入れます。(assetsはプロジェクト右クリック「New -> Folder -> Assets Folder」で作れます)
検出結果のインデックスをラベルにマッピングするために正解ラベルも用意しておきます。
自分のリポですがこちらからcoco_datasetのラベルをDLして同様にassetsフォルダにtxtファイルを入れてください。

これでAndroid Studioのassetsフォルダには
ssd_mobile_net_v1.tflitecoco_dataset_labels.txtの2つが入っている状態になったと思います。

CameraXの実装

(注: CameraXの実装は1.0.0-rc01のものです。)
基本的にはこちらの公式チュートリアルのままやっていくだけです。

マニフェストにカメラ権限を追加

AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" />

レイアウトファイルの定義
カメラビューとsurfaceViewを定義します。
バウンディングボックスなどリアルタイムに描写するのでViewではなくsurfaceViewを使用してビューに検出結果を反映させます。

activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout 
//省略// >

    <androidx.camera.view.PreviewView
        android:id="@+id/cameraView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        //省略// />

    <SurfaceView
        android:id="@+id/resultView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        //省略// />
</androidx.constraintlayout.widget.ConstraintLayout>

MainActivityにcameraXの実装。後からpermissionDispatcherを追加します。
この辺はチュートリアルと一緒なので最新のチュートリアルを参考にしたほうがいいかもしれません。

MainActivity.kt
private lateinit var cameraExecutor: ExecutorService

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    cameraExecutor = Executors.newSingleThreadExecutor()
    setupCamera()
}

fun setupCamera() {
    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

    cameraProviderFuture.addListener({
        val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

        // プレビューユースケース
        val preview = Preview.Builder()
            .build()
            .also { it.setSurfaceProvider(cameraView.surfaceProvider) }

        // 背面カメラを使用
        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

        // 画像解析(今回は物体検知)のユースケース
        val imageAnalyzer = ImageAnalysis.Builder()
            .setTargetRotation(cameraView.display.rotation)
            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) // 最新のcameraのプレビュー画像だけをを流す
            .build()
        // TODO ここに物体検知 画像解析ユースケースのImageAnalyzerを実装

        try {
            cameraProvider.unbindAll()

            // 各ユースケースをcameraXにバインドする
            cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalyzer)

        } catch (exc: Exception) {
            Log.e("ERROR: Camera", "Use case binding failed", exc)
        }
    }, ContextCompat.getMainExecutor(this))
}

override fun onDestroy() {
    super.onDestroy()
    cameraExecutor.shutdown()
}

とりあえずここまで来たら設定から手動でカメラ権限を許可すればカメラプレビューが見れるはずです。ただ、surfaceViewはデフォルトでは黒なので画面が黒くなっている場合はいったんsurfaceViewをコメントアウトして確認してください。

permission dispatcherの実装

カメラ権限リクエスト用にpermission disptcherを実装します。(手動で権限許可するから別にいいというかたは飛ばしてください)

MainActivity.kt
@RuntimePermissions
class MainActivity : AppCompatActivity() {
    // 略
    @NeedsPermission(Manifest.permission.CAMERA)
    fun setupCamera() {...}
}

各アノテーションを対象クラスとメソッドに追加していったんビルドします。
パーミッションリクエスト用の関数が自動生成されます。

先ほどのsetupCameraメソッドを以下のように変更し、権限リクエスト結果からコールされるようにします。
なお、今回は拒否された時などの処理に関しては実装しません。

MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    cameraExecutor = Executors.newSingleThreadExecutor()
    //setupCamera() 削除
    // permissionDispatcherでsetUpCamera()メソッドをコール
    setupCameraWithPermissionCheck()
}

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    onRequestPermissionsResult(requestCode, grantResults)
}

これでカメラのプレビュー関連については実装完了です。
続いて、画像解析ユースケースやモデル読み込み、結果の表示などを実装します。

モデル読み込み関数の実装

tfliteモデルの読み込みや正解ラベルをassetsから読み込む関数をMainActivityに実装します。
特に難しいこともしていないのでコピペでokです。

MainActivity.kt
companion object {
    private const val MODEL_FILE_NAME = "ssd_mobilenet_v1.tflite"
    private const val LABEL_FILE_NAME = "coco_dataset_labels.txt"
}

// tfliteモデルを扱うためのラッパーを含んだinterpreter
private val interpreter: Interpreter by lazy {
    Interpreter(loadModel())
}

// モデルの正解ラベルリスト
private val labels: List<String> by lazy {
    loadLabels()
}

// tfliteモデルをassetsから読み込む
private fun loadModel(fileName: String = MainActivity.MODEL_FILE_NAME): ByteBuffer {
    lateinit var modelBuffer: ByteBuffer
    var file: AssetFileDescriptor? = null
    try {
        file = assets.openFd(fileName)
        val inputStream = FileInputStream(file.fileDescriptor)
        val fileChannel = inputStream.channel
        modelBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, file.startOffset, file.declaredLength)
    } catch (e: Exception) {
        Toast.makeText(this, "モデルファイル読み込みエラー", Toast.LENGTH_SHORT).show()
        finish()
    } finally {
        file?.close()
    }
    return modelBuffer
}

// モデルの正解ラベルデータをassetsから取得
private fun loadLabels(fileName: String = MainActivity.LABEL_FILE_NAME): List<String> {
    var labels = listOf<String>()
    var inputStream: InputStream? = null
    try {
        inputStream = assets.open(fileName)
        val reader = BufferedReader(InputStreamReader(inputStream))
        labels = reader.readLines()
    } catch (e: Exception) {
        Toast.makeText(this, "txtファイル読み込みエラー", Toast.LENGTH_SHORT).show()
        finish()
    } finally {
        inputStream?.close()
    }
    return labels
}

画像解析ユースケースの実装

メインの物体検知の推論パイプラインを実装していきます。
CameraXの画像解析ユースケースを利用することでより手軽に実装できるようになりました。(数行で実装できるというわけではないですが。。。)
チュートリアルでは画素値の平均をとったりしています。

cameraXで用意されているImageAnalysis.Analyzerを実装しカメラのプレビューを受け取り、解析結果を返すようなObjectDetectorクラスを作ります。
typealiasでコールバックとして解析結果を受け取れるように定義します。

ObjectDetector.kt
typealias ObjectDetectorCallback = (image: List<DetectionObject>) -> Unit
/**
 * CameraXの物体検知の画像解析ユースケース
 * @param yuvToRgbConverter カメラ画像のImageバッファYUV_420_888からRGB形式に変換する
 * @param interpreter tfliteモデルを操作するライブラリ
 * @param labels 正解ラベルのリスト
 * @param resultViewSize 結果を表示するsurfaceViewのサイズ
 * @param listener コールバックで解析結果のリストを受け取る
 */
class ObjectDetector(
    private val yuvToRgbConverter: YuvToRgbConverter,
    private val interpreter: Interpreter,
    private val labels: List<String>,
    private val resultViewSize: Size,
    private val listener: ObjectDetectorCallback
) : ImageAnalysis.Analyzer {
    override fun analyze(image: ImageProxy) {
         //TODO 推論コードの実装
    }
}

/**
 * 検出結果を入れるクラス
 */
data class DetectionObject(
    val score: Float,
    val label: String,
    val boundingBox: RectF
)

MainActivityの「TODO ここに物体検知 画像解析ユースケースのImageAnalyzerを実装」の部分を以下のように書き換えます。

MainActivity.kt
// 画像解析(今回は物体検知)のユースケース
val imageAnalyzer = ImageAnalysis.Builder()
    .setTargetRotation(cameraView.display.rotation)
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) // 最新のcameraのプレビュー画像だけをを流す
    .build()
    .also {
        it.setAnalyzer(
            cameraExecutor,
            ObjectDetector(
                yuvToRgbConverter,
                interpreter,
                labels,
                Size(resultView.width, resultView.height)
            ) { detectedObjectList ->
               // TODO 検出結果の表示
            }
        )
    }

各コンストラクタ変数についてはコメントを参照してください。
ここでYuvToRgbConverterがエラーになっていると思いますが今から説明しますので大丈夫です。

ImageAnalysis.Analyzerインターフェースのanalyzeメソッドを実装していくのですが、ここでanalyzeメソッドの引数にImageProxyという型でカメラのプレビュー画像が流れてきます。
このImageProxyをbitmapやtensorに変換しないと推論とかができないのですが、これがちょっと面倒なんです。。。

ImageProxyの中にはandroid.Media.Imageが入っており画像ピクセルデータを一つもしくは複数のPlaneとしてグルーピングして保存しています。アンドロイドのカメラではYUV_420_888という形式でImageが生成されるのでこれをRGB bitmapに変換するコンバーターを作る必要があります。

確か、pytorch mobileにはコンバーターが用意されていた気がしますが、tensorflowにはありませんでした。リポジトリあさってたらcameraXのサンプルにソースがあったので今回はそれを使用します。(自分で実装するのもありですが)

ということで
この公式サンプルのコンバータをコピーしてYuvToRgbConverterクラスを作って、MainActivityにそのインスタンスを以下のように追加してください。

MainActivity.kt
// カメラのYUV画像をRGBに変換するコンバータ
private val yuvToRgbConverter: YuvToRgbConverter by lazy {
    YuvToRgbConverter(this)
}

モデル関連の変数定義

モデルのinput画像サイズや結果を受け取るための変数を先ほどのObjectDetectorクラスに定義します。使用するモデルのshapeに合わせる必要があります。

ObjectDetector.kt
companion object {
    // モデルのinputとoutputサイズ
    private const val IMG_SIZE_X = 300
    private const val IMG_SIZE_Y = 300
    private const val MAX_DETECTION_NUM = 10

    // 今回使うtfliteモデルは量子化済みなのでnormalize関連は127.5fではなく以下の通り
    private const val NORMALIZE_MEAN = 0f
    private const val NORMALIZE_STD = 1f

    // 検出結果のスコアしきい値
    private const val SCORE_THRESHOLD = 0.5f
}

private var imageRotationDegrees: Int = 0
private val tfImageProcessor by lazy {
    ImageProcessor.Builder()
        .add(ResizeOp(IMG_SIZE_X, IMG_SIZE_Y, ResizeOp.ResizeMethod.BILINEAR)) // モデルのinputに合うように画像のリサイズ
        .add(Rot90Op(-imageRotationDegrees / 90)) // 流れてくるImageProxyは90度回転しているのでその補正
        .add(NormalizeOp(NORMALIZE_MEAN, NORMALIZE_STD)) // normalization関連
        .build()
}

private val tfImageBuffer = TensorImage(DataType.UINT8)

// 検出結果のバウンディングボックス [1:10:4]
// バウンディングボックスは [top, left, bottom, right] の形
private val outputBoundingBoxes: Array<Array<FloatArray>> = arrayOf(
    Array(MAX_DETECTION_NUM) {
        FloatArray(4)
    }
)

// 検出結果のクラスラベルインデックス [1:10]
private val outputLabels: Array<FloatArray> = arrayOf(
    FloatArray(MAX_DETECTION_NUM)
)

// 検出結果の各スコア [1:10]
private val outputScores: Array<FloatArray> = arrayOf(
    FloatArray(MAX_DETECTION_NUM)
)

// 検出した物体の数(今回はtflite変換時に設定されているので 10 (一定))
private val outputDetectionNum: FloatArray = FloatArray(1)

// 検出結果を受け取るためにmapにまとめる
private val outputMap = mapOf(
    0 to outputBoundingBoxes,
    1 to outputLabels,
    2 to outputScores,
    3 to outputDetectionNum
)

なんだか変数ばっかりで見づらいですが全部必要です。
画像の前処理はtensorflow lite ライブラリのImageProcessorを使用して行います。
各変数の説明はコメントを参照してください。基本的にここで示したモデルinfoをkotlinで定義しています。

推論コードの実装

続いてinterpreterを使ってモデルで推論します。

ObjectDetector.kt
// 画像をYUV -> RGB bitmap -> tensorflowImage -> tensorflowBufferに変換して推論し結果をリストとして出力
private fun detect(targetImage: Image): List<DetectionObject> {
    val targetBitmap = Bitmap.createBitmap(targetImage.width, targetImage.height, Bitmap.Config.ARGB_8888)
    yuvToRgbConverter.yuvToRgb(targetImage, targetBitmap) // rgbに変換
    tfImageBuffer.load(targetBitmap)
    val tensorImage = tfImageProcessor.process(tfImageBuffer)

    //tfliteモデルで推論の実行
    interpreter.runForMultipleInputsOutputs(arrayOf(tensorImage.buffer), outputMap)

    // 推論結果を整形してリストにして返す
    val detectedObjectList = arrayListOf<DetectionObject>()
    loop@ for (i in 0 until outputDetectionNum[0].toInt()) {
        val score = outputScores[0][i]
        val label = labels[outputLabels[0][i].toInt()]
        val boundingBox = RectF(
            outputBoundingBoxes[0][i][1] * resultViewSize.width,
            outputBoundingBoxes[0][i][0] * resultViewSize.height,
            outputBoundingBoxes[0][i][3] * resultViewSize.width,
            outputBoundingBoxes[0][i][2] * resultViewSize.height
        )

        // しきい値よりも大きいもののみ追加
        if (score >= ObjectDetector.SCORE_THRESHOLD) {
            detectedObjectList.add(
                DetectionObject(
                    score = score,
                    label = label,
                    boundingBox = boundingBox
                )
            )
        } else {
            // 検出結果はスコアの高い順にソートされたものが入っているので、しきい値を下回ったらループ終了
            break@loop
        }
    }
    return detectedObjectList.take(4)
}

まずcameraXの画像をYUV -> RGB bitmap -> tensorflowImage -> tensorflowBufferと変換していき
interpreterを使って推論します。引数に入れたoutputMapに推論結果が格納されるので定義した各output変数から結果を整形してリストとして返すようなdetect関数を作成します。

続いてanalyze関数からこのdetect関数をコールするようにしてObjectDetectorクラスは完成です。

ObjectDetector.kt
// cameraXから流れてくるプレビューのimageを物体検知モデルに入れて推論する
@SuppressLint("UnsafeExperimentalUsageError")
override fun analyze(image: ImageProxy) {
    if (image.image == null) return
    imageRotationDegrees = image.imageInfo.rotationDegrees
    val detectedObjectList = detect(image.image!!)
    listener(detectedObjectList) //コールバックで検出結果を受け取る
    image.close()
}

image.close()は必ず呼ぶ必要があるので注意してください。android.Media.Imageはシステムリソースを食うので開放する必要があります。

ここまで実装出来たらが推論パイプラインの実装は完了です。
最後に検出結果の表示を実装します。

検出結果の表示を実装

viewの描画がリアルタイムに行われるのでViewではなくsurfaceViewを使ってバウンディングボックスなどの表示を実装します。
初期化処理をOverlaySurfaceViewクラスを作って適当に書いていきます。
コールバックやsurfaceViewとは?みたいなのはほかの方の記事でたくさん書かれているので割愛します。

OverlaySurfaceView.kt
class OverlaySurfaceView(surfaceView: SurfaceView) :
    SurfaceView(surfaceView.context), SurfaceHolder.Callback {

    init {
        surfaceView.holder.addCallback(this)
        surfaceView.setZOrderOnTop(true)
    }

    private var surfaceHolder = surfaceView.holder
    private val paint = Paint()
    private val pathColorList = listOf(Color.RED, Color.GREEN, Color.CYAN, Color.BLUE)

    override fun surfaceCreated(holder: SurfaceHolder) {
        // surfaceViewを透過させる
        surfaceHolder.setFormat(PixelFormat.TRANSPARENT)
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
    }
}

これにバウンディングボックスを表示するdraw関数を作っていきます。

OverlaySurfaceView.kt
fun draw(detectedObjectList: List<DetectionObject>) {
    // surfaceHolder経由でキャンバス取得(画面がactiveでない時にもdrawされてしまいexception発生の可能性があるのでnullableにして以下扱ってます)
    val canvas: Canvas? = surfaceHolder.lockCanvas()
    // 前に描画していたものをクリア
    canvas?.drawColor(0, PorterDuff.Mode.CLEAR)

    detectedObjectList.mapIndexed { i, detectionObject ->
        // バウンディングボックスの表示
        paint.apply {
            color = pathColorList[i]
            style = Paint.Style.STROKE
            strokeWidth = 7f
            isAntiAlias = false
        }
        canvas?.drawRect(detectionObject.boundingBox, paint)

        // ラベルとスコアの表示
        paint.apply {
            style = Paint.Style.FILL
            isAntiAlias = true
            textSize = 77f
        }
        canvas?.drawText(
            detectionObject.label + " " + "%,.2f".format(detectionObject.score * 100) + "%",
            detectionObject.boundingBox.left,
            detectionObject.boundingBox.top - 5f,
            paint
        )
    }

    surfaceHolder.unlockCanvasAndPost(canvas ?: return)
}

surfaceHolder経由で取得するcanvasですが、viewがリークする可能性があるのでnullableで扱ってます。
canvasを使ってバウンディングボックス(Rect)と文字を表示しているだけです。

あとは、SurfaceViewのコールバックなどをセットするだけです。

MainActity.kt
private lateinit var overlaySurfaceView: OverlaySurfaceView

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    overlaySurfaceView = OverlaySurfaceView(resultView)
    // 略
}

MainActivityの画像解析ユースケースのコールバック「TODO 検出結果の表示」の部分を以下のように変更します。

MainActivity.kt
// 画像解析(今回は物体検知)のユースケース
val imageAnalyzer = ImageAnalysis.Builder()
    .setTargetRotation(cameraView.display.rotation)
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) // 最新のcameraのプレビュー画像だけをを流す
    .build()
    .also {
        it.setAnalyzer(
            cameraExecutor,
            ObjectDetector(
                yuvToRgbConverter,
                interpreter,
                labels,
                Size(resultView.width, resultView.height)
            ) { detectedObjectList ->
                // 解析結果の表示
                overlaySurfaceView.draw(detectedObjectList)
            }
        )
    }

これで完成です!
いい感じに実装出来ましたか?

おわり

cameraXもrcになってもうそろそろかっってみんな思ってるんじゃないでしょうか。ユースケースが色々用意されていてそれに則って実装するとやりやすくて拡張性があるのが魅力ですね。個人的にはもうプロダクトにバンバン投入していってもいいんじゃないかって思ってたり。。

今回のGitHubはこちらからどうぞ

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

【Android/Kotlin】最小構成でLiveDataを使う

環境

  • Mac
  • AndroidStudio4.1
  • Kotlin

LiveDataとは

データの変更をリアルタイムで検知して、それをトリガーとしてなんらかの処理を行う
LiveDataに検知したいデータを格納し、
Observerに変更された時の処理を記述する

結論

ソースコード全文 - Github

コード解説

今回のコードの動き

事前準備

  1. LiveDataを初期化
  2. Observerを定義

動かす

  1. 背景をタップする
  2. LiveDataの中身を変更する
  3. Observerが変更を検知し、text_viewsampleLiveDataの値を入れる

コード

MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // LiveDataを初期化
        val sampleLiveData: MutableLiveData<LocalDateTime> by lazy {
            MutableLiveData<LocalDateTime>()
        }

        // Observerを定義
        // sampleLiveData の値が変化したら text_view.text を変更
        sampleLiveData.observe(this, Observer { value ->
            // LiveDataが変化したらここの内容が実行される
            // sampleLiveData.valueの値が変数valueに入っている
            value?.let { 
                text_view.text = it.toString()
            }
        })

        // 背景タップでLiveDataに現在時刻を入れる
        constraint.setOnClickListener {
            sampleLiveData.value = LocalDateTime.now()
        }

    }
}

参考にした記事

LiveData の概要 - Android Developer

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