- 投稿日:2020-09-20T22:01:07+09:00
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=jabuild.gradledependencies { 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に記入
MainActivityoverride 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."); } } }
- 投稿日:2020-09-20T20:36:40+09:00
AndroidのKotlinで電卓作成 プログラム編
下記の記事
https://qiita.com/yanasep/items/503b378c439752f70880
を参考にしながら電卓を作成しました。数字ボタン
初期値の0だったら、ボタンの数字を入れ、それ以外だったら、入力した値を追加して行きます。calcApp/app/src/main/java/com/example/calcapp/MainActivity.ktzero.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.ktplus.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.ktprivate 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.ktequel.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.ktval result = calculation().toBigDecimal()小数点第一位が0の時に整数として表示するため0削除と文字型に変換。
BigDecimal使用時にtoString()を呼ぶと指数表記になることがあるため、toPlainString()を使用。calcApp/app/src/main/java/com/example/calcapp/MainActivity.ktcalc_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.ktprivate 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() }
- 投稿日:2020-09-20T17:58:16+09:00
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 の定期実行を確認する - QiitaGoogle 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))ポイントとしては以下の通りです。
- setRequiresChargingに
true
を設定することで、充電中の時だけ実行され、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: 充電してない充電中の時のログを見ると、
ChargingWorker
とNoChargingWorker
が両方実行されてしまっていました。「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 を使用した方法は使えたとしても止めたほうが良さそう
- 投稿日:2020-09-20T10:10:56+09:00
画面上の戻るボタン
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) } }
- 投稿日:2020-09-20T08:43:17+09:00
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.javapublic 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.javapublic 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.javapublic interface SampleRepository { List<SampleEntity> find(); }SampleRepositoryImpl.javapublic 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.javapublic 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を使って検索結果を画面に表示させたいと思います。
では、また!参考
- Android デベロッパーガイド - Hiltを使用した依存関係の注入
- 投稿日:2020-09-20T05:51:19+09:00
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_width
とandroid:layout_height
の値には意味がありません。しかし他のViewと同様android:layout_width
とandroid: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_begin
、app:layout_constraintGuide_end
、app: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_width
とandroid:layout_height
の値は省略不可のため、ここでも無意味な値"0dp"
を指定しています。android:layout_width
とandroid: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/ConstraintLayout7Guidelineは他の方法でも代替できるものの、親Viewの水平方向もしくは垂直方向の特定の位置を簡潔に表すのに便利です。
Barrierを使いたくなるケースはあまり多くないかもしれませんが、動的に長さが変わる複数の子Viewの位置を揃えるレイアウトが突然必要になったとき、きっとあなたを救ってくれると思います。参考文献
ConstraintLayout でレスポンシブ UI を作成する | Android デベロッパー
ConstraintLayout | Android Developers
Guideline | Android Developers
Barrier | Android DevelopersBarrierのバグ?
ConstraintLayoutバージョン2.0.1にて、ConstraintLayoutに
android:layoutDirection="rtl"
を指定、Barrierにapp:barrierDirection="end"
を指定してapp:barrierMargin
に正の長さを指定したとき、左側にマージンをとることが期待されるのに実際には右側にマージンがとられるようです。これ、IssueとしてGoogleに報告した方がいいでしょうかね…?