20200920のAndroidに関する記事は6件です。

Androidアプリに広告を貼る方法 

https://akira-watson.com/android/admob-google-play-service.html

https://developers.google.com/admob/android/quick-start?hl=ja#kotlin

https://developers.google.com/admob/android/banner?hl=ja

ソースコード設定

下記適切なバージョンを使用
https://developers.google.com/admob/android/quick-start?hl=ja

build.gradle
dependencies {
    implementation 'com.google.android.gms:play-services-ads:19.3.0'
}

ネットワーク権限などを設定

AndroidManifest.xml
        <meta-data
            android:name="com.google.android.gms.ads.APPLICATION_ID"
            android:value="ca-app-pub-3940256099942544~3347511713"/>

レイアウト記入

activity_main.xml
    <com.google.android.gms.ads.AdView
        android:id="@+id/adView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_alignParentBottom="true"
        ads:adSize="BANNER"
        ads:adUnitId="ca-app-pub-3940256099942544/6300978111">
    </com.google.android.gms.ads.AdView>

Activityに記入

MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
        // Test App ID
        MobileAds.initialize(activity, "ca-app-pub-3940256099942544~3347511713")

        val adRequest: AdRequest = AdRequest.Builder().build()
        adView.loadAd(adRequest)

        // ad's lifecycle: loading, opening, closing, and so on
        adView.adListener = object:AdListener(){
            override fun onAdLoaded() {
                Log.d("debug","Code to be executed when an ad finishes loading.");
            }

            override fun onAdFailedToLoad(errorCode : Int) {
                Log.d("debug","Code to be executed when an ad request fails.");
            }

            override fun onAdOpened() {
                Log.d("debug","Code to be executed when an ad opens an overlay that covers the screen.");
            }

            override fun onAdLeftApplication() {
                Log.d("debug","Code to be executed when the user has left the app.");
            }

            override fun onAdClosed() {
                Log.d("debug","Code to be executed when when the user is about to return to the app after tapping on an ad.");
            }
        }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidのKotlinで電卓作成 プログラム編

下記の記事
https://qiita.com/yanasep/items/503b378c439752f70880
を参考にしながら電卓を作成しました。

数字ボタン
初期値の0だったら、ボタンの数字を入れ、それ以外だったら、入力した値を追加して行きます。

calcApp/app/src/main/java/com/example/calcapp/MainActivity.kt
        zero.setOnClickListener{
            if (calc_display.text == "0") calc_display.text = "0"
            else calc_display.text = "${calc_display.text}0"
            inputNum += "0"
        }
        one.setOnClickListener{
            if (calc_display.text == "0") calc_display.text = "1"
            else calc_display.text = "${calc_display.text}1"
            inputNum += "1"
        }

...

演算子ボタン

addInput()でinputNum(文字型)をdoubleに変換して配列に格納。
演算子も演算子用の配列に格納。
inputNumは次の数値入力のため初期化します。

calcApp/app/src/main/java/com/example/calcapp/MainActivity.kt
        plus.setOnClickListener{
            calc_display.text = "${calc_display.text}+"
            addInput(inputNum,'+')
            inputNum = ""
        }
        minus.setOnClickListener{
            calc_display.text = "${calc_display.text}-"
            addInput(inputNum,'-')
            inputNum = ""
        }
...
calcApp/app/src/main/java/com/example/calcapp/MainActivity.kt
private fun addInput(inputNum:String, ope:Char){
        try {
            var num = inputNum.toDouble()
            numList.add(num)
            if (ope != '=') opeList.add(ope)
        }catch(e:Exception){
            calc_display.text = "Numeric Error"
        }
    }

=ボタンは、計算処理が入ります。

calcApp/app/src/main/java/com/example/calcapp/MainActivity.kt
equel.setOnClickListener{
            calc_display.text = ""
            addInput(inputNum, '=')
            val result = calculation().toBigDecimal()
            calc_display.text = result.stripTrailingZeros().toPlainString()
            inputNum = ""
            numList.clear()
            opeList.clear()
        }

calculation()の戻り値は下記の理由によりdoubleからStringに変換した物を戻り値にして、その後にBigdecimal型に変換しています。

https://www.ne.jp/asahi/hishidama/home/tech/java/math/bigdecimal.html
BigDecimal自身は10進数の固定小数を扱えるのだが、doubleを使って初期化する場合、doubleは2進数の浮動小数なので、BigDecimalに変換する際に小数点以下の値が変わってしまうことがある。
その為、小数を扱いたい場合は、doubleを使わず、Stringで初期化する方がよい。

calcApp/app/src/main/java/com/example/calcapp/MainActivity.kt
val result = calculation().toBigDecimal()

小数点第一位が0の時に整数として表示するため0削除と文字型に変換。
BigDecimal使用時にtoString()を呼ぶと指数表記になることがあるため、toPlainString()を使用。

calcApp/app/src/main/java/com/example/calcapp/MainActivity.kt
calc_display.text = result.stripTrailingZeros().toPlainString()

calculation()
徐算と乗算を先に行います。
numListを計算した数に置き換え、次の数を削除します。

入力例
1*3/4-5+10

numList[2,3,4,5,10]
opeList[*,/,-,+]

while (i < opeList.size)の処理
1
2*3が行われ、3が消える。
合計の6が代入される。
numList[6,4,5,10]
opeList[/,-,+]
opeList.size=3
i=0
2
6/4が行われ1.5が代入される。
4は消える。
numList[1.5,5,10]
opeList[-,+]
opeList.size=2
i=0
3
-を負の数に変換。
opeListを+に変更。
numList[1.5,-5,10]
opeList[+,+]
opeList.size=2
i=1
4
+だったため特に何もしない。
numList[1.5,-5,10]
opeList[+,+]
opeList.size=2
i=2

for(i in numList)の処理
numList[1.5,-5,10]
の内容を全て足しています。
calcApp/app/src/main/java/com/example/calcapp/MainActivity.kt
private fun calculation():String {
        var i = 0
        var result = 0.0
        while (i < opeList.size){
            if(opeList.get(i) == '/' || opeList.get(i) == '*'){
                if(opeList.get(i) == '*') result = numList.get(i) * numList.get(i+1)
                else result = numList.get(i) / numList.get(i+1)

                numList.set(i,result)           //計算に使った一つ目の数を計算結果に置き換え
                numList.removeAt(i+1)           //二つ目の数をリストから削除
                opeList.removeAt(i)             //使い終わった演算子をリストから削除
                i--                             //リストの次の要素が一つ手前に来たのでiを一つ戻す
            } else if (opeList.get(i) == '-') {
                opeList.set(i, '+')
                numList.set(i + 1, numList.get(i + 1) * -1) //引かれる数をマイナスにしてあとでまとめて足す
            }
            i++
        }

        result = 0.0
        for(i in numList){
            result += i
        }

        return result.toString()
    }

全コード
https://github.com/CoffCookie/calcApp.git

デザイン編
https://qiita.com/clipbord/items/53b832066b1269dcc822

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

Android 8.0以降で充電状態の変化を監視する方法について調査してみた

背景

Android で充電状態の変化を監視する方法として、BroadcastReceiverを使ってリアルタイムに充電状態を監視する方法があります。

デバイスを電源に接続するか未接続にすると、BatteryManager によってアクションがブロードキャストされます。アプリが実行されていないときでもこれらのイベントを受信することが重要です(特に、これらのイベントがバックグラウンド アップデートを開始するためにアプリを起動する頻度に影響する場合)。したがって、BroadcastReceiver をマニフェストで登録し、ACTION_POWER_CONNECTED と ACTION_POWER_DISCONNECTED をインテント フィルタ内で定義することで両方のイベントをリッスンする必要があります。

ですが、Android 8.0(API レベル 26)からブロードキャストの制限事項がさらに強化され、BroadcastReceiver を使ってリアルタイムに充電状態を監視できなくなりました。

アプリがブロードキャストを受信するように登録されている場合、ブロードキャストが送信されるたびにアプリのレシーバーがリソースを消費します。 そのため、システム イベントに基づくブロードキャストの受信を登録しているアプリが多すぎると、ブロードキャストをトリガーするシステム イベントによって、これらのすべてのアプリが続けざまにリソースを消費し、ユーザー エクスペリエンスに悪影響を与え、問題が発生する可能性があります。

Android 8.0 以降を対象にしているアプリは、暗黙的なブロードキャストに対するブロードキャスト レシーバーをマニフェストで登録できなくなりました。

代替手段として、JobSchedulerを使用してくださいとのことでした。

多くの場合、以前に暗黙的なブロードキャストに対して登録していたアプリは、JobScheduler ジョブを使用して同様の機能を得ることができます。

JobScheduler について調べたところ、以下の記事が見つかりました。
Android 8 で JobScheduler の定期実行を確認する - Qiita

Google I/O 2018 において発表された「JetPack」において、Android のバージョンに応じてそれらの処理を切り替える WorkManager が含まれているので、今後はそちらを利用していくべきかと思います。

なので、WorkManager を使用してリアルタイムに充電状態を監視する方法を調べてみました。

WorkManager の概要

WorkManagerの主な機能としては以下が挙げられます。

  • API 14 までの下位互換性
    • API 23 以上が搭載されたデバイスでは JobScheduler を使用
    • API 14 ~ 22 が搭載されたデバイスでは BroadcastReceiver と AlarmManager を組み合わせて使用
  • ネットワークの可用性や充電ステータスなどの処理の制約を追加する
  • 非同期の 1 回限りのタスクや定期的なタスクのスケジュールを設定する
  • スケジュール設定されたタスクの監視と管理
  • タスクを連携させる
  • アプリやデバイスが再起動してもタスクを確実に実行する
  • Doze モードなどの省電力機能に準拠する

使いどころとしては以下が挙げられます。

  • ログやアナリティクスをバックエンド サービスに送信する
  • アプリデータをサーバーと定期的に同期する

導入方法

アプリの build.gradleに以下の異存関係を追加してください。

implementation "androidx.work:work-runtime-ktx:2.4.0"

Constraints を使って検証

WorkManager にはConstraintsという、実行可能なタイミングを指定できる機能があったので、充電中または充電中でない時だけ実行できるか検証しました。
まず、以下のように充電中の時だけ実行するChargingWorkerクラスと充電してない時だけ実行するNoChargingWorkerクラスを作成します。

/**
 * 充電中の時だけ実行するWorker
 */
class ChargingWorker(appContext: Context, workerParams: WorkerParameters) :
    Worker(appContext, workerParams) {
    override fun doWork(): Result {
        Log.d(TAG, "充電中")
        return Result.success()
    }
    companion object {
        private const val TAG = "ChargingWorker"
    }
}
/**
 * 充電してない時だけ実行するWorker
 */
class NoChargingWorker(appContext: Context, workerParams: WorkerParameters) :
    Worker(appContext, workerParams) {
    override fun doWork(): Result {
        Log.d(TAG, "充電してない")
        return Result.success()
    }
    companion object {
        private const val TAG = "NoChargingWorker"
    }
}

次に、以下のように呼び出し処理を作成します。呼び出すタイミングは任意です。

// 制約を作成
val constraintsCharging = Constraints.Builder()
    .setRequiresCharging(true) // 充電中である
    .build()
val constraintsNoCharging = Constraints.Builder()
    .setRequiresCharging(false) // 充電中でない
    .build()

// 15分おきに定期実行するrequestを作成
// 充電中だったら実行
val requestCharging = PeriodicWorkRequestBuilder<ChargingWorker>(15, TimeUnit.MINUTES)
    .setConstraints(constraintsCharging)
    .build()

// 充電中じゃなかったら実行
val requestNoCharging = PeriodicWorkRequestBuilder<NoChargingWorker>(15, TimeUnit.MINUTES)
    .setConstraints(constraintsNoCharging)
    .build()

// 各種リクエストを実行
WorkManager.getInstance(this).enqueue(mutableListOf(requestCharging, requestNoCharging))

ポイントとしては以下の通りです。

  • setRequiresChargingtrueを設定することで、充電中の時だけ実行され、falseを設定することで充電中でない時だけ実行されるのではないか、と仮説を立てた
  • PeriodicWorkRequestを作成し、繰り返し処理として定義した

なお、15 分おきとしているのは以下の制約があるためです。

注: 定義可能な最小繰り返し間隔は 15 分です(JobScheduler API と同じ)。

充電中の時と充電中でない時とでそれぞれ動作させてみたところ、以下のような結果になりました。

充電中の時のログ。

2020-09-17 11:56:41.569 8448-8494/com.example.workertest D/NoChargingWorker: 充電してない
2020-09-17 11:56:41.571 8448-8495/com.example.workertest D/ChargingWorker: 充電中
2020-09-17 12:00:04.810 8448-8526/com.example.workertest D/ChargingWorker: 充電中
2020-09-17 12:10:11.527 8448-8494/com.example.workertest D/NoChargingWorker: 充電してない
2020-09-17 12:11:41.693 8448-8495/com.example.workertest D/NoChargingWorker: 充電してない
2020-09-17 12:11:41.752 8448-8526/com.example.workertest D/ChargingWorker: 充電中

充電中でない時のログ。

2020-09-17 11:25:11.225 7886-7963/com.example.workertest D/NoChargingWorker: 充電してない
2020-09-17 11:40:11.353 7886-8107/com.example.workertest D/NoChargingWorker: 充電してない

充電中の時のログを見ると、ChargingWorkerNoChargingWorkerが両方実行されてしまっていました。「falseを設定することで充電中でない時だけ実行されるのではないか」という仮説は外れてしまいました。
また、充電中でない時のログを見ると、NoChargingWorkerのみが実行されているので、「trueを設定することで、充電中の時だけ実行される」という仮説は合っていました。
とはいえこの方法ではうまくいかなかったので、別の方法を試してみました。

現在の充電状態を特定する方法と WorkManager を組み合わせて検証

BroadcastReceiver を使わなくとも、現在の充電状態を特定する方法があったので、WorkManager と組み合わせて検証しました。
まず、充電状態を確認するPowerConnectionWorkerクラスを作成します。

/**
 * 充電状態を確認するWorker
 */
class PowerConnectionWorker(appContext: Context, workerParams: WorkerParameters) :
    Worker(appContext, workerParams) {
    override fun doWork(): Result {
        val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { filter ->
            applicationContext.registerReceiver(null, filter)
        }
        val status: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
        val isCharging: Boolean = status == BatteryManager.BATTERY_STATUS_CHARGING
                || status == BatteryManager.BATTERY_STATUS_FULL
        Log.d(TAG, "isCharging=$isCharging")
        // How are we charging?
        val chargePlug: Int = batteryStatus?.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) ?: -1
        val usbCharge: Boolean = chargePlug == BatteryManager.BATTERY_PLUGGED_USB
        val acCharge: Boolean = chargePlug == BatteryManager.BATTERY_PLUGGED_AC
        Log.d(TAG, "usbCharge=$usbCharge")
        Log.d(TAG, "acCharge=$acCharge")
        return Result.success()
    }
    companion object {
        private const val TAG = "PowerConnectionWorker"
    }
}

次に、以下のように呼び出し処理を作成します。

// 15分おきに定期実行するrequestを作成
val requestPowerConnection =
    PeriodicWorkRequestBuilder<PowerConnectionWorker>(
        15,
        TimeUnit.MINUTES
    ).build()
// リクエストを実行
WorkManager.getInstance(this).enqueue(requestPowerConnection)

充電していない状態で実行してみたところ、以下のような結果になりました。

2020-09-18 14:30:00.364 7679-7765/com.example.workertest D/PowerConnectionWorker: isCharging=false
2020-09-18 14:30:00.364 7679-7765/com.example.workertest D/PowerConnectionWorker: usbCharge=false
2020-09-18 14:30:00.364 7679-7765/com.example.workertest D/PowerConnectionWorker: acCharge=false
2020-09-18 14:30:00.373 7679-7713/com.example.workertest I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=2136c521-df0c-4ab2-b282-7a9de07a926c, tags={ com.example.workertest.PowerConnectionWorker } ]
2020-09-18 14:30:40.096 7679-7778/com.example.workertest D/PowerConnectionWorker: isCharging=false
2020-09-18 14:30:40.096 7679-7778/com.example.workertest D/PowerConnectionWorker: usbCharge=false
2020-09-18 14:30:40.096 7679-7778/com.example.workertest D/PowerConnectionWorker: acCharge=false
2020-09-18 14:30:40.104 7679-7717/com.example.workertest I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=fb646f90-ba6d-48c1-8f49-cb791050371a, tags={ com.example.workertest.PowerConnectionWorker } ]
2020-09-18 14:41:33.290 7679-7721/com.example.workertest D/PowerConnectionWorker: isCharging=false
2020-09-18 14:41:33.290 7679-7721/com.example.workertest D/PowerConnectionWorker: usbCharge=false
2020-09-18 14:41:33.291 7679-7721/com.example.workertest D/PowerConnectionWorker: acCharge=false
2020-09-18 14:41:33.295 7679-7716/com.example.workertest I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=2aa6df1b-2d95-451a-ab9d-8507b20ed128, tags={ com.example.workertest.PowerConnectionWorker } ]
2020-09-18 14:46:39.895 7679-7765/com.example.workertest D/PowerConnectionWorker: isCharging=false
2020-09-18 14:46:39.895 7679-7765/com.example.workertest D/PowerConnectionWorker: usbCharge=false
2020-09-18 14:46:39.895 7679-7765/com.example.workertest D/PowerConnectionWorker: acCharge=false
2020-09-18 14:46:39.909 7679-7778/com.example.workertest D/PowerConnectionWorker: isCharging=false
2020-09-18 14:46:39.910 7679-7778/com.example.workertest D/PowerConnectionWorker: usbCharge=false
2020-09-18 14:46:39.910 7679-7778/com.example.workertest D/PowerConnectionWorker: acCharge=false
2020-09-18 14:46:39.911 7679-7713/com.example.workertest I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=2136c521-df0c-4ab2-b282-7a9de07a926c, tags={ com.example.workertest.PowerConnectionWorker } ]
2020-09-18 14:46:39.954 7679-7713/com.example.workertest I/WM-WorkerWrapper: Worker result SUCCESS for Work [ id=fb646f90-ba6d-48c1-8f49-cb791050371a, tags={ com.example.workertest.PowerConnectionWorker } ]

以下のように、いずれも false と表示されているので充電状態を検知できているようです。

2020-09-18 14:41:33.290 7679-7721/com.example.workertest D/PowerConnectionWorker: isCharging=false
2020-09-18 14:41:33.290 7679-7721/com.example.workertest D/PowerConnectionWorker: usbCharge=false
2020-09-18 14:41:33.291 7679-7721/com.example.workertest D/PowerConnectionWorker: acCharge=false

ですが、15 分おきに処理されておらず、何回も処理されているのが気になりました。

過去に定期実行したリクエストを全終了させる方法

15 分おきに処理されなかった原因として、過去に実行したリクエストを終了させず、そのままにしていたためでした。WorkManager の概要にも「アプリやデバイスが再起動してもタスクを確実に実行する」とあり、終了させない限りずっと処理され続けると思われます。

過去に実行したリクエストを全て終了させるには、事前に以下の処理を呼び出してください。

// 過去に実行したリクエストを全終了
WorkManager.getInstance(this).cancelAllWork()

同一のリクエストを複数実行させないようにする方法

前述した方法だと、呼び出すタイミングによっては終了させてはいけない
リクエストを終了させてしまう恐れがあります。
同一のリクエストを複数実行させないようにする方法として、ユニークなリクエストとして実行する方法があります。
WorkManager.getInstance(this).enqueue(requestPowerConnection)を以下のように書き換えてください。

// ユニークなリクエストとして実行
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
    "powerConnection", // ワーク名
    ExistingPeriodicWorkPolicy.REPLACE, // 既存のワークがあればキャンセルして置き換える
    requestPowerConnection
)

enqueueUniquePeriodicWorkに変更し、第 2 引数にExistingPeriodicWorkPolicy.REPLACEを指定することで、確実に 15 分おきに処理されます。

まとめ

充電状態の変化を監視する方法について調査した結果をまとめます。

  • Android 8.0(API レベル 26)からブロードキャストの制限事項がさらに強化され、BroadcastReceiver を使ってリアルタイムに充電状態を監視できなくなった
  • 代替手段として、WorkManager を使う方法がある
  • 現在の充電状態を特定する方法と WorkManager と組み合わせることで、15 分おきに充電状態を監視することは可能
  • ただし API の制約上リアルタイムに監視できない
  • Android 7.0 以前では未検証だが、リソース消費の観点からすると BroadcastReceiver を使用した方法は使えたとしても止めたほうが良さそう
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画面上の戻るボタン

http://rikisha-android.hatenablog.com/entry/2014/04/04/202207

    override fun onCreate(savedInstanceState: Bundle?) {
..
        //戻る
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            android.R.id.home ->{
                finish()
                return true
            }
            else -> return super.onOptionsItemSelected(item)
        }
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidのHiltをJavaで実装してみる

はじめに

Hiltのアルファ版が出たので調査しました。
あのわかりづらかったDagger2が非常にわかりやすくなっていました。導入の敷居がさがったと思います。そこで、「DIしたい、Hiltを使ってみたい、基本的なことを知りたい」という方に向け、サンプルアプリを作成しながら基本的なことを説明したいと思います。
なお、Dagger2からHiltへの移行方法などは記載していません。

いきなり実装

概要等の説明は省き、いきなり実装です。DIやHiltについては公式ページを参照してください。
今回作成するサンプルは、画面の中のボタンを押すとデータベースを検索し結果を表示するアプリです。

作成するクラスの構成は以下のようにします。
MainActivity --> SampleUseCase --> SampleRepository --> SampleDao --> DB

このアプリをHiltを使って実装します。
データベースまわりはRoomを使用しますがRoomについては触れません。

環境は以下のようになっています。
- Android Studio 4.0.1
- Android Virtual Device - android10.0 / 1080 x 2160

ライブラリの追加

まずは、ルートのbuild.gradleの設定です。

buildscript {
    dependencies {
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
        // ・・・(省略)・・・
   }
}

次に、app/build.graldeの設定です。

apply plugin: 'dagger.hilt.android.plugin'

android {
    // ・・・(省略)・・・
}

dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    annotationProcessor "com.google.dagger:hilt-android-compiler:2.28-alpha"
    // ・・・(省略)・・・
}

Applicationクラスの作成

次に、Applicationクラスを作成します。
Applicationクラスに@HiltAndroidAppをつけるだけです。今まではDaggerApplicationを継承するか、HasAndroidInjectorをimplしていましたが、HiltではアノテーションをつけるだけでOKです。

Applicationクラスを作成したら、AndroidManifest.xmlに追加します。

@HiltAndroidApp
public class SampleApplication extends Application {
}

AndroidManifest.xml
<application
    android:name=".SampleApplication"    
    android:icon="@mipmap/ic_launcher">
    ・・・(省略)・・・            
</application>

今まではAppComponentを作成していましたが、Hiltでは不要になりました。Dagger2からの移行の場合はバッサリ削除します。

// @Singleton
// @Component(modules={AndroidInjectionModule.class})
// public interface AppComponent extends AndroidInjector<SampleApplication> {
//   ・・・(省略)・・・
// }

Activityに注入する

Activityに@AndroidEntryPoint アノテーションを付けるとinjectできるようになります。
今までは、HasAndroidInjectorのimplや AndroidInjection.inject(this);の実行などを行っていましたが、Hiltではアノテーションをつけるだけです。

今回のサンプルでは、MainActiviytにSampleUseCaseを注入します。

MainActivity.java
@AndroidEntryPoint     // ・・・(1)
public class MainActivity extends AppCompatActivity {

    @Inject            // ・・・(2)
    SampleUseCase useCase; 

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = findViewById(R.id.execute);
        button.setOnClickListener(v -> useCase.execute());   // ・・・(3)
    }
}

(1) AndroidEntryPoint アノテーションをつけます。
(2) Inject アノテーションをつけます。 このuseCase 変数にHiltによってインスタンスが注入されます。
(3)ボタン押下時にuseCaseを実行します。 コード上はSampleUseCaseはnew していないのに実行できます。Hiltによってインスタンスが生成されて注入されているからです。

次に、SampleUseCaseです。
まずは、ログを出力するだけの実装です。

SampleUseCase.java
public class SampleUseCase {
    private static String TAG = SampleUseCase.class.getName();

    @Inject    // ・・・(1)
    public SampleUseCase() {
    }

    public void execute() {
        Log.d(TAG, "実行!!");
    }
}

(1) コンストラクタにInject アノテーションをつけます。これがないとHilt管理のオブジェクトにならず、ActivityにInjectされません。

ここまでで実際に動きます。DIができています。
非常に簡単ですね。Dagger2のときと比べると圧倒的に簡単になっています。

Hiltモジュールの作成

上のサンプルではInjectするクラス(SampleUseCase)では、コンストラクタに@Injectを指定しています。しかし、@Injectを付与できないことがあります。たとえば、インターフェースの場合や、外部ライブラリのクラスなどです。
このような場合は@Module アノテーションを付与したクラス作成し、インスタンスの生成方法をHiltに知らせます。

今回のサンプルでは、SampleUseCaseにインターフェースのSampleRepository を呼び出すところが該当します。
SampleUseCaseクラスに実装を追加します。

SampleUseCase.java
public class SampleUseCase {
    private static String TAG = SampleUseCase.class.getName();

    @Inject
    SampleRepository repository;   // インターフェース

    @Inject
    public SampleUseCase() {
    }

    public void execute() {
        Log.d(TAG, "実行!!");

        // 実行します
        List<SampleEntity> results = repository.find();

        // 結果をログに表示      
        results.forEach(result -> {
            Log.d(TAG, result.getName());
        });
    }

Bindsを使用してインスタンスを注入する

SampleRepositoryの実装です。インターフェースと実体クラスは以下のようになります。

SampleRepository.java
public interface SampleRepository {
    List<SampleEntity> find();
}
SampleRepositoryImpl.java
public class SampleRepositoryImpl implements SampleRepository {
    public static final String TAG = SampleRepositoryImpl.class.getName();

    @Inject                           // ・・・(1)
    public SampleRepositoryImpl() {
    }

    public List<SampleEntity> find() {
        Log.d(TAG, "find!");
        return null;
    }
}

(1) 実体クラスのコンストラクタには@Injectを付与します

これだけではInjectできません。
このインターフェースのインスタンスを生成する方法をHiltに教える必要があります。

Hiltモジュールは、@Moduleアノテーションが付けられたクラスです。Daggerのモジュールとは異なり、Hiltは@InstallInアノテーションを付けて依存関係を指定します。

DataModule.java
@Module                                        // ・・・ (1)
@InstallIn(ApplicationComponent.class)         // ・・・ (2)
abstract public class DataModule {

    @Binds                                     // ・・・ (3)
    public abstract SampleRepository bindSampleRepository(SampleRepositoryImpl impl);

}

(1) @Moduleアノテーションを付与しHiltモジュールクラスであることを宣言します。クラス名はなんでもよいです。
(2) このModuleの依存関係を指定します。この例では、ここで宣言したクラスたちは、アプリ内のどのクラスにでもInjectできます。この指定は以下の表のようにいろいろと指定することができます。
例えば、FragmentComponentを指定した場合は、FragmentにインジェクションできますがActivityにはインジェクションできません。今回はアプリのどのクラスにでも注入できるように、ApplicationComponentを指定します。

コンポーネント インジェクションの対象
ApplicationComponent Application
ActivityRetainedComponent ViewModel
ActivityComponent Activity
FragmentComponent Fragment
ViewComponent View
ViewWithFragmentComponent WithFragmentBindingsアノテーションが付いた View
ServiceComponent Service

(3) Bindsアノテーションを付与し、どの実体を生成するか宣言します。メソッドの戻り値には、インターフェースを指定します。メソッドのパラメータで、生成したい実体を指定します。

Providesを使用してインスタンスを注入する

Binds以外にもインスタンスの生成方法を指定することができます。
外部ライブラリはコンストラクタにインジェクションを付与できません。そのような場合にはProvidesを利用します。

今回のサンプルでは、SampleRepositoryImplがDaoを呼び出すところが該当します。
Room関連をDIするときの実装ですね。(Roomに関する説明はしません。別の記事でする予定です)

上のサンプルコードのDataMudule.javaに追加します。

DataModule.java
@Module
@InstallIn(ApplicationComponent.class)
abstract public class DataModule {

    @Provides                                           //  ・・・ (1) 
    @Singleton                                          //  ・・・ (2)
    public static SampleDatabase provideDb(Application context) {
        return Room.databaseBuilder(context.getApplicationContext(), SampleDatabase.class, "sample.db")
                .addCallback(SampleDatabase.INITIAL_DATA)
                .allowMainThreadQueries()
                .build();
    }

    @Provides                                           //  ・・・ (3)
    @Singleton                                         
    public static SampleDao provideSampleDao(SampleDatabase db) {
        return db.getSampleDao();
    }


    @Binds
    public abstract SampleRepository bindSampleRepository(SampleRepositoryImpl impl);
}

(1) Providesアノテーションを付与し、どの実体を生成するか宣言します。
メソッドの戻り値には、生成したインスタンスです。パラメータは、Hiltが管理しているインスタンスを渡すことができます。
(2)このメソッドにはSingletonアノテーションがついています。これはスコープの設定です。
通常、Hiltはリクエストがあるたびに、毎回、新しいインスタンスを作成します。これをアノテーションを付与することにより制御することができます。今回のサンプルは、Singletonですので、アプリケーションで1つのインスタンスの状態を実現します。(毎回、新しいインスタンスはつくりません)。どのクラスの注入しても同じインスタンスになります。

スコープは以下のようなものが用意されています。

Android クラス 生成されたコンポーネント スコープ
Application ApplicationComponent Singleton
View Model ActivityRetainedComponent ActivityRetainedScope
Activity ActivityComponent ActivityScoped
Fragment FragmentComponent FragmentScoped
View ViewComponent ViewScoped
WithFragmentBindings ViewWithFragmentComponent ViewScoped
Service ServiceComponent ServiceScoped

例えば、今回のサンプルのDataModule.javaのInstantRunをActivityComponentの変更し、SampleDaoをActivityScopedに変更すると、Activityが存続する間は同じインスタンスになります。
SampleActivity、SampleUseCase、SampleRespositoryにDaoをInjectした場合、そのDaoはすべて同じインスタンスです。

サンプルアプリの実装に戻ります。
SampleRepositoryImplにDaoを注入して実装を完成させます。

SampleRepositoryImpl.java
public class SampleRepositoryImpl implements SampleRepository {
    public static final String TAG = SampleRepositoryImpl.class.getName();

    @Inject                    // ・・・(1)
    SampleDao dao; 

    @Inject
    public SampleRepositoryImpl() {
    }

    public List<SampleEntity> find() {
        Log.d(TAG, "find!");
        return dao.find();     // ・・・(2)
    }
}

(1)SampleDaoを注入します。スコープはSingletonなので毎回同じインスタンスが注入されます。
(2)newしていませんが、Hiltによって注入されるのでNullPointerExceptionにはなりません。

その他のコード

上記で説明していないサンプルアプリの、DaoとEntityを載せておきます。

SampleEntity.java
@Entity(tableName = "sample")
public class SampleEntity implements Serializable {

    @PrimaryKey
    @NonNull
    private String code;
    private String name;

    //setter/getter省略
}
SampleDao.java
@Dao
public interface SampleDao {

    @Insert
    long save(SampleEntity dto);

    @Query("select * from sample")
    List<SampleEntity> find();
}

完成!!

ここまでで、画面のボタンを押すと、ログに検索結果が表示されます。
Dagger2で必要だったものがほとんど不要となり、シンプルに実現できるようになりました。非常に簡単ですね。

まとめ

今回のサンプルを通じてHiltのポイントを整理します。
1. Activityに@AndroidEntryPointを付与する
2. Inject対象のクラスのコンストラクタに@Injectを忘れずにつける
3. インターフェースや他のライブラリをInject対象とするときは、@Bindsまたは@Providesを利用する
以上です。
簡単ですね。次回はHiltを利用しViewModelを使って検索結果を画面に表示させたいと思います。
では、また!

参考

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

ConstraintLayout入門その7 - Guideline, Barrier

ConstraintLayoutを使用するための設定については、ConstraintLayout入門その1をご覧ください。

ConstraintLayout2.0安定版ようやく登場

alphaもbetaもつかないConstraintLayout2.0安定版がようやくリリースされました。本記事執筆時点の最新安定版は2.0.1です。

今後、筆者の書くConstraintLayout入門の記事は、バージョン2.0準拠といたします。

補助線

ConstraintLayoutでは、子Viewそのものの配置のほか、「補助線」となる表示対象外のオブジェクトを設置し、これを子Viewに参照させることで子Viewの位置を規定することができます。Guidelineは親View、すなわちConstraintLayoutを基準とした位置を指定します。対するBarrierは、子Viewの配置から位置が決まります。

Guideline

ConstraintLayoutの上下左右端からの距離、もしくは相対位置を指定することで「補助線」を設置します。
なお、Guidelineが意味を持つ情報は位置情報のみであり、Guideline自体には「大きさ」がないため、Guidelineの android:layout_widthandroid:layout_height の値には意味がありません。しかし他のViewと同様 android:layout_widthandroid:layout_height はいずれも省略すると実行時エラーになります(Lintエラーも出ますがビルドは可能)。「無意味な値」であることを明示する意味で、筆者としては両方とも "0dp" を指定することを推奨します。

activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"

    ...

    >

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guidelinePercent"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintGuide_percent="0.25"
        android:orientation="vertical" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guidelineDpEnd"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintGuide_end="120dp"
        android:orientation="vertical" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guidelineDpTop"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintGuide_begin="100dp"
        android:orientation="horizontal" />

    ...

</androidx.constraintlayout.widget.ConstraintLayout>

android:orientation でガイドラインの向きを水平または垂直のいずれかに指定します。 android:orientation"horizontal" を指定した場合は水平のガイドラインとなり、親Viewの上端または下端からの距離もしくは相対位置の指定により配置が決まります。 android:orientation"vertical" を指定した場合は垂直のガイドラインとなり、親Viewの左端または右端からの距離もしくは相対位置の指定により配置が決まります。
位置指定は app:layout_constraintGuide_beginapp:layout_constraintGuide_endapp:layout_constraintGuide_percent のいずれか1つを設定して行います。
水平のガイドラインでは app:layout_constraintGuide_begin で上端からの距離、 app:layout_constraintGuide_end で下端からの距離を設定します。app:layout_constraintGuide_percent を使う場合は0と1の間の小数を指定し、0に近いほど上端近く、1に近いほど下端近く、0.5なら中段に設定されます。
垂直のガイドラインでは app:layout_constraintGuide_begin で左端からの距離、 app:layout_constraintGuide_end で右端からの距離、app:layout_constraintGuide_percent を使う場合は0と1の間の小数を指定し、0に近いほど左端近く、1に近いほど右端近くに設定することになりますが、親Viewに android:layoutDirection="rtl" が設定されている場合は(明示的設定、もしくは親の親から継承されている場合ともに)左右がこれと逆になります。

Guidelineの位置指定は距離または比率の値を1つだけ用いた単純なものであり、子Viewを用いた位置指定でも代替できます。たとえば以下のXMLのように記述して、Spaceの上下左右端を参照することでSpaceをガイドラインとして使うこともできます。 android:layoutDirection="rtl" の場合にも左右の入れ替えを生じさせたくない場合は、以下のようにLeftとRightで位置を指定したSpaceを使うとよいでしょう。

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"

    ...

    >

    <Space
        android:id="@+id/guideSpacePercent"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintWidth_default="percent"
        app:layout_constraintWidth_percent="0.25"
        app:layout_constraintTop_toTopOf="parent" />

    <Space
        android:id="@+id/guideSpaceDpEnd"
        android:layout_width="120dp"
        android:layout_height="0dp"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    ...

</androidx.constraintlayout.widget.ConstraintLayout>

Barrier

以下の2つの画面例をご覧ください。

表示結果

表示結果

上記2つの画面例中、黒い文字で表示された部分のように「項目名-項目の値」という配列のリストを表示させる場合、左側の文字列のうち一番長い文字列の右端を基準に右側の文字列を揃えたい、という場合があります。この例では、言語が英語のとき上から2番目の文字列が最長、日本語のとき一番下の文字列が最長、と状況によって最長となるViewが変わっています。

このような場合、「子Viewのうちもっともせり出しているViewに合わせた補助線」としてBarrierが使えます。

activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"

    ...

    >

    <androidx.constraintlayout.widget.Barrier
        android:id="@+id/barrier"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:barrierDirection="end"
        app:barrierMargin="24dp"
        app:constraint_referenced_ids="itemName1, itemName2, itemName3, itemName4" />

    ...

</androidx.constraintlayout.widget.ConstraintLayout>

上記XMLにて app:constraint_referenced_ids で列挙したIDで表されたView(ここで、View IDには先頭の @id/ を付与する必要はありません)のうち、 app:barrierDirection で指定した方向にもっともせり出したViewの端に設置される「補助線」がBarrierとなります。他のViewがこのBarrierを参照することにより、画面例のような表示が可能になります。

BarrierもGuidelineと同じく大きさを持たず、Barrier自体はマージンの役割を果たすことができません。しかし、ConstraintLayoutバージョン2からは app:barrierMargin に長さを指定することでBarrierの位置を app:barrierDirection で指定した方向に動かすことが可能になったようです。これで「もっともせり出しているView」と、Barrierを基準に配置したViewとの間にマージンを設定できるようになりました。
Guidelineと同じく android:layout_widthandroid:layout_height の値は省略不可のため、ここでも無意味な値 "0dp" を指定しています。 android:layout_widthandroid:layout_height に長さを指定しても app:barrierMargin と同じような効果を持たせることはできません。

Barrierは他の方法での代替が困難な「補助線」です。たとえば、BarrierをFrameLayoutに置き換えて app:constraint_referenced_ids で指定していた子ViewをこのFrameLayoutの中に入れ、FrameLayoutのlayout_widthとlayout_heightに "wrap_content" を指定すれば、FrameLayoutの大きさを「もっともせり出しているView」に合わせることは可能です。しかし、このようにConstraintLayoutの内部にネストを作ってしまうと、ネストの中のViewとネストの外のViewとを相互に参照させることができなくなり不自由です。Barrierは、このような「ネストの壁」を避けて子View同士の参照を妨げずに制約を設ける効果があります。

サンプルコード

今回のサンプルコードは以下のリポジトリにあります。
https://github.com/csayamada/ConstraintLayout7

Guidelineは他の方法でも代替できるものの、親Viewの水平方向もしくは垂直方向の特定の位置を簡潔に表すのに便利です。
Barrierを使いたくなるケースはあまり多くないかもしれませんが、動的に長さが変わる複数の子Viewの位置を揃えるレイアウトが突然必要になったとき、きっとあなたを救ってくれると思います。

参考文献

ConstraintLayout でレスポンシブ UI を作成する | Android デベロッパー
ConstraintLayout | Android Developers
Guideline | Android Developers
Barrier | Android Developers

Barrierのバグ?

ConstraintLayoutバージョン2.0.1にて、ConstraintLayoutに android:layoutDirection="rtl" を指定、Barrierに app:barrierDirection="end" を指定して app:barrierMargin に正の長さを指定したとき、左側にマージンをとることが期待されるのに実際には右側にマージンがとられるようです。これ、IssueとしてGoogleに報告した方がいいでしょうかね…?

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