20210725のAndroidに関する記事は2件です。

Hiltを自習したときのメモ

プロジェクトでの Hiltの採用にあたり、 Daggerで全く理解できてなかった DIについて自習したときのメモ。 間違いがあったら指摘してください。 Dependency Injectionを使う動機について class Car { val engine = Engine() } みたいに書くと、 正常系はいざしらず、Engineの挙動に色々障害を起こさせてテストするのにいちいちコードを修正しなきゃいけなくって、それを書き戻すことを忘れたら悲惨なことになるよね、という class Car { // val engine = Engine() val engine = EngineWithTrouble() // 特定の条件で失敗するEngineのテスト。テストが終わったらもとに戻すこと } テスト目的以外でも、Engineが抽象クラスで、普通のエンジン、水素エンジンを使い分けたいってこともあるかもしれない。これも Carのコードを修正することになる。 class Car { // val engine = Engine() val engine = HydrogenEngine() } まあ、いちいち Carのコードをいじってるのはなんか落ち着きがない。 それでいいじゃんって割り切ってしまうのもありだろうけど、Carのコードには手を加えず他のところから「この Engineを使え」って指示を受ける方が見通しが良くならない? という考えもわかる。「使うエンジンの種類を知りたければ指示書を確認してくれ」というのは筋が通っている。 自分のような古い人間には「元のコードにベタで書いてるほうが間違いないでしょう...そんなにコード量も増えないよ?」という引っかかりがとても強い。 けど、「テストコードを間違えて混入してしまう」という問題にはベタ書きでは対処できないのは理解できる。 Dependency Injection 上記の問題をどう解決するかというと、方法は2つ。 Carのコンストラクタで Engineを渡す 直接代入ではない別の方法で engineフィールドに Engineをセットする 前者を「コンストラクタインジェクション」、後者を「フィールドインジェクション」という。 これらを総称して「依存性の注入 (Dependency Injection)」という名前が付けられている。 コンストラクタインジェクションはこう class Car(val engine: Engine) { } フィールドインジェクションはこう class Car(val engine: Engine) { val engine = <Engineのインスタンスをどこかで作り、ここに挿入する> } いずれにしても、「Engineは Carの外で作る」という前提がある。 となると、次に問題となるのは 誰が Engineを作るのか? どうやってそれを Carに渡すのか? ということ。 Hiltという Dependency Injectionライブラリ Androidには Hiltという Dependency Injectionのライブラリが(かなり前に)追加された。 Hiltは Daggerという Dependency Injectionのライブラリを土台にして作られている。 Daggerは結構前から使われていて安定性も良く人気があるが、理解するハードルが高く 「勉強する手間の割に得られる利益が少ない」というものだった。少なくとも自分には。 Hiltはそんな Daggerの難しいところを、使う上での決まり事をいくつか増やすことで比較的簡単に使えるようにしたもの、と理解している。 Hiltを使うと、先程の「どうやってそれを Carに渡すのか?」という問題は以下のように解決できる。 「ここにオブジェクトを入れて欲しい」という箇所に @Inject という修飾子を付けるだけ。 class Car @Inject constructor (val engine: Engine) { } class Car { @Inject val engine : Engine } これで問題の半分は解決した。あとは「誰が Engineを作るのか?」ということだが、これは Engineの生成方法次第で、方法がいくつかに分かれている。 ソースコードが全部自分の管理下にある場合 コードは管理下にあるけど、classでなく interfaceのサブクラスを提供したい場合 他の人が作ったライブラリのオブジェクトを提供したい場合 ソースコードが全部自分の管理下にある場合 コンストラクタに Injectアノテーションを付ける。 class Engine @Inject constructor() { ... } interfaceのサブクラスを提供したい場合 Engineが interfaceの場合、コンストラクタが存在しないので上記の方法が使えない。 interface Engine { ... } // ← constructorを定義できない class GasolineEngine : Engine { ... } class HydrogenEngine : Engine { ... } こういうときは、オブジェクトの作り方を指示する モジュール を作る。 モジュールはオブジェクトをどう作るかの指示書みたいなもの。 interface Engine { ... } class @Inject GasolineEngine : Engine { ... } class @Inject HydrogenEngine : Engine { ... } @InstallIn(SingletonComponent::class) @Module abstract interface EngineModule { @Binds abstract fun bindEngine(engine : HydrogenEngine) : Engine } 「@Module を付けた abstract classを宣言する」「要求される型、それに対して返す実装の型の組み合わせを @Binds で定義する」が鍵。 @Binds はModuleの中にいくつあってもいい。 @InstallIn は「このモジュールをインストールするコンポーネント」だが、これは後述する。SingletonComponentについても後で。 Engineの生成に引数が必要なときは? → 後述 Engineの生成にcontextが必要なときは? → 後述 他の人が作ったライブラリのオブジェクトを提供したい場合 Engine自体が外で作られたライブラリだと、GasolineEngine、HydrogenEngine のコンストラクタ宣言に Injectを追加できない。こういうときは、モジュールのオブジェクト を作ることで対応する。 @InstallIn(SingletonComponent::class) @Module object EngineModule { @Provides fun provideEngine() = HydrogenEngine() } @Provides はModuleの中にいくつあってもいい。 Engineの生成に引数が必要なんだけど? Engineの生成に他のオブジェクト(以下の場合 Plug)が必要という場合は普通にある。 class Engine (val plug : Plug) { ... } その場合、必要なクラスも @Injectの対象にする。 コンストラクタを定義できるなら、こう class Engine @Inject constructor(val plug : Plug) { ... } class Plug @Inject constructor () { ... } Plugが interfaceなら、こう interface Plug { ... } class @Inject PlugImpl : Plug { ... } @InstallIn(SingletonComponent::class) @Module abstract interface EngineModule { @Binds abstract fun bindEngine(engine : GasolineEngine) : Engine @Binds abstract fun bindPlug(plug : PlugImpl) : Plug } 上記の例は EngineModuleに書いたが、PlugModuleを新たに定義してもいい。 ただ分割しすぎても見通しが悪くなるので、モジュールの設計はチームで一貫性をもたせるようにしたほうがいい。 Engineの生成にcontextが必要なんだけど? @ApplicationContext アノテーションを使う。 @ActivityContextというアノテーションもある。必要な方を。 interface Engine { ... } class @Inject GasolineEngine(@ApplicationContext appContext: Context) : Engine { ... } class @Inject HydrogenEngine(@ApplicationContext appContext: Context) : Engine { ... } とか、 @Module object EngineModule { @Provides fun provideEngine(@ApplicationContext appContext: Context) { return Engine(appContext) } } とか。 コンポーネント オブジェクト生成方法の指示書であるモジュールは、必ず コンポーネント に所属する、というルールがある。コンポーネントは何種類かあり、どのコンポーネントに所属させるかを指定するのが @InstallIn アノテーション。 よく使いそうなのは以下の通り SingletonComponent ... Applicationのサブクラス。アプリケーションの生成時から最後まで存在する。 ActivityComponent ... アクティビティのライフサイクルと同期するコンポーネント ActivityRetainedComponent ... ViewModelのライフサイクルと同期するコンポーネント FragmentComponent ... Fragmentのライフサイクルと同期するコンポーネント 完全なリスト → https://developer.android.com/training/dependency-injection/hilt-android#generated-components スコープ Hiltは基本的に要求があれば指示書を元にオブジェクトを生成するが、アプリケーションで唯一のオブジェクト(要するにシングルトン)が欲しいんだけど、という場合がある。そういうときはスコープを調整する。 以下の例では、 @Singleton アノテーションを使うことで、 アプリケーション中で Engineオブジェクトは一個だけになり、必要とされる場所で同じオブジェクトが使われる。 @InstallIn(SingletonComponent::class) @Module object EngineModule { @Singleton @Provides fun provideEngine() = Engine() } ActivityScoped, FragmentScopedなどもあるらしい (使ったことないから知らない)。多分おなじアクティビティ(フラグメント)で要求されたら、同じオブジェクトを返してくれるのだと思われる。 → https://developer.android.com/training/dependency-injection/hilt-android#component-scopes GasolineEngineも HydrogenEngineも提供したいんだけど 識別子を定義して、モジュールと使用箇所でそれを指定する。 → https://developer.android.com/codelabs/android-hilt#8 仕上げ : エントリポイント Hiltのルールとして、HiltAndroidApp アノテーションを付けた Applicationのサブクラスを持つ必要がある。 manifestへの登録も忘れずに。 @HiltAndroidApp class MyApplication : Application() { ... } 最後に、依存性を注入したいクラスにエントリポイント @AndroidEntryPointを設定する。 @AndroidEntryPoint class MainActivity : AppCompatActivity() { ... } その他 テストの方法はどうするの? とか、Hilt でサポートされていないクラスに依存関係を注入する必要もある、といった場合もあるが割愛する。そのへんは参考文献を見てほしい。 参考文献 https://developer.android.com/training/dependency-injection/hilt-android https://developer.android.com/codelabs/android-hilt https://a4rcvv.net/hilt-android/ https://aakira.app/blog/2020/05/dagger-hilt/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JetpackCompose のAlertDialog で時間を溶かした

JetpackCompose でAlertDialog を使うときに失敗した話です まとめると AlertDialog を使うときはText のみで単純な構成にするのが無難 TextField を置きたいときはDialog を使おう そもそもダイアログが必要か考えよう どんな失敗? テキスト入力をするダイアログをAlertDialogを使って作ると、レイアウトが崩れてしまうことがありました @Composable fun MyDialog4(onDismiss: () -> Unit) { val (text, setText) = remember { mutableStateOf("") } AlertDialog( onDismissRequest = onDismiss, text = { TextField( value = text, onValueChange = setText, modifier = Modifier.padding(32.dp), ) }, confirmButton = { ... }, ) } 環境 Android Studio: Arctic Fox 2020.3.1 RC 1 Kotlin: 1.5.10 androidx.compose.*: 1.0.0-rc02 なぜ起きた? このレイアウト崩れが起きた原因を探るため、AlertDialog の中身を見てみましょう すると、ColumnScope.AlertDialogBaselineLayout にたどり着きます ここでは、受け取ったタイトルとテキストを配置しています @Composable internal fun ColumnScope.AlertDialogBaselineLayout( title: @Composable (() -> Unit)?, text: @Composable (() -> Unit)? ) { Layout( { title?.let { title -> Box(...) { title() } } text?.let { text -> Box(...) { text() } } }, Modifier.weight(1f, false) ) { measurables, constraints -> val titlePlaceable = measurables.firstOrNull { it.layoutId == "title" }?.measure( constraints.copy(minHeight = 0) ) val textPlaceable = measurables.firstOrNull { it.layoutId == "text" }?.measure( constraints.copy(minHeight = 0) ) // このあたりでタイトルとテキストの位置を計算 ... layout(layoutWidth, layoutHeight) { titlePlaceable?.place(0, titlePositionY) textPlaceable?.place(0, textPositionY) } } } より詳しく見ていくと、タイトル・テキストそれぞれについて、ベースラインを見ながら縦方向の位置決めをしています(横方向は単純なため本稿では割愛) 細かい内容は実際のコードを読んでもらえればと思いますが、ここでの重要な点はベースラインの位置が特定の位置に来るように配置していることです。(特定の位置は、sp 依存の固定値) // Place the title so that its first baseline is titleOffset from the top val titlePositionY = titleOffset - firstTitleBaseline これを知っていないと、いくつかの問題が起きることがあります top 方向のpadding が効かない ベースラインの位置を特定の位置に合わせようとするため、top にpadding を指定しても効きません また、大きな値を入れてしまうとテキストそのものが潰れてしまいます pad = 0 16 28 @Composable fun MyDialog5(onDismiss: () -> Unit) { val pad = // 表中の値 AlertDialog( onDismissRequest = onDismiss, title = null, text = { Text(text = "MyDialog5", modifier = Modifier.padding(top = pad.dp)) }, confirmButton = { CancelButton(onDismiss) }, ) } TextとTextFieldでズレが起きる TextとTextFieldでは上端からベースラインまでの距離が異なります そのため、単純に配置するだけでもズレが生じます Text TextField @Composable fun MyDialog2(onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, title = null, text = { Text(text = "MyDialog2") }, confirmButton = { CancelButton(onDismiss) }, ) } @Composable fun MyDialog3(onDismiss: () -> Unit) { val (text, setText) = remember { mutableStateOf("") } AlertDialog( onDismissRequest = onDismiss, text = { TextField(value = text, onValueChange = setText) }, confirmButton = { CancelButton(onDismiss) }, ) } 解決案 解決案としては以下のような例が考えられます Dialog を使う 自分でDialog から実装してしまいます 先ほど実際にAlertDialog の中身を見た人はわかると思いますが、これはDialog を使って実装されているため、これを真似ることで同じようなダイアログを自由に実現できます タイトルなどを配置する場合、AlertDialog で設定されていたalpha やtextStyle についても自前で設定する必要があることに注意します @Composable fun MyDialog6(onDismiss: () -> Unit) { val (text, setText) = remember { mutableStateOf("") } Dialog(onDismissRequest = onDismiss) { Surface { Column { TextField( value = text, onValueChange = setText, modifier = Modifier.padding(32.dp), ) Box( modifier = Modifier .fillMaxWidth() .padding(all = 8.dp), ) { CancelButton( onClick = onDismiss, modifier = Modifier .align(Alignment.BottomEnd), ) } } } } } ダイアログを使わない 解決策とは言えないかもしれませんが、そもそもそのダイアログは必要でしょうか? 複雑なUI が必要であれば、ダイアログという考えから脱却し、通常の画面に配置するというのも手の一つです また、ダイアログはユーザの操作をブロックするUI であり、その利用には慎重になる必要があります 参考リンク 公式ドキュメント マテリアルデザインガイドライン 今回作成したサンプルプロジェクト、ダイアログについてはここ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む