20211128のAndroidに関する記事は3件です。

Android Studio Gradleのセッティング

Could not install gradle distribution from XXXが表示された場合の対処法をマイ忘備録として記載します。 こちらも参考 https://note.com/yvonne8823/n/n80214762a8d4 file>close projectでいったん編集中のプロジェクトを閉じる。 カスタマイズから、All settings...を選択
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google codelabs Android DataBinding のコードのつながりをふわ~っと確認

最近、Databindingを理解するために、Google codelabs Android DataBindingで学習してみたのですが、思考停止で書かれている通りに実装したところ、Databindingは実装できたものの、どのコードと、どのコードが関連して動作しているのかが、いまいちピンときていなかったので、コード同士のつながりを確認することにしました。 この記事はその確認結果のまとめです。 Google codelabs Android DataBindingで作るもの このCodelabsでは以下のようなものを作ります。(ほぼ完成しているものが準備されている) LIKEボタンをポチポチするとLikesの数字が増えて、Likesが一定以上になると、プログレスバーとアイコンが変化します。 最初から準備されているコードは、DataBindingを使用せずにこの挙動を実現している状態なので、それを修正してDataBindingで実現する、というのがこのCodelabsの主旨です。 Codelabsの内容 全力でサボりますが、こちらの記事で全編内容が紹介されています。分かりにくい部分(Codelabs内のケアレスミスとか)も注釈として書かれてるので参考になりました。 コードのつながりを読み解く 読み解くというほどのものでもないかもですが、読み解きます笑 ※ここで扱うコードは、Codelabsの最初に提供されるものではなく、Codelabsが完了した段階でのものです。 かなりざっくり言うと、このアプリは ・PlainOldActivity.kt ・SimpleViewModel.kt ・BindingAdapter ・plain_activity で構成されています。 これらの中から、DataBindingの構成要素を独断と偏見で抜き出すと、こんな概略図になります。(いろいろ省略してます) これらのつながりを、ざっくり見ていきます。 まず、plain_activity.xml内でレイアウト変数(layout variables)が定義されています。 plain_activity.xml <data> <variable name="viewmodel" type="com.example.android.databinding.basicsample.data.SimpleViewModel"/> </data> nameはviewmodelとなっていて、 typeは"com.example.android.databinding.basicsample.data.SimpleViewModel" を指定しており、これはSimpleViewModel.ktのことを指しています。 なので意味的には、「viewmodelは、SimpleViewModel.ktのことを指す」という具合になりそうです。 続いてタグ内で android:text="@{}"というような表記 (layout expression、レイアウト式) が出てきます。 ここにdataタグ内で記述したレイアウト変数が入ります。 こんな感じ↓ plain_activity.xml <TextView android:id="@+id/plain_name" android:text="@{viewmodel.name}" 以下省略 /> ここでdataタグ内のnameで定義した、viewmodelが@{viewmodel.name}という形で記述されています。 これはデータの紐付け先を示しています。 viewmodelはSimpleViewModel.kt(com.example.android.databinding.basicsample.data.SimpleViewModel)を型として指定していたので、「SimpleViewModel.ktからnameを引っ張って来い」というように読めます。 ただ、plain_activity.xmlからSimpleViewModel.ktへ直接紐付けされているわけでありません。 PlainOldActivity.ktは以下のようになっています。 PlainOldActivity.kt class PlainOldActivity : AppCompatActivity() { // Obtain ViewModel from ViewModelProviders private val viewModel by lazy { ViewModelProviders.of(this).get(SimpleViewModel::class.java) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //➀ val binding : PlainActivityBinding = DataBindingUtil.setContentView(this, R.layout.plain_activity) binding.lifecycleOwner = this //➁ binding.viewmodel = viewModel } } ➀はbinding先のレイアウト(plain_activity.xml)が定義されています。 ➁はbinding.viewmodel = viewModelと定義されています。 binding.viewmodelの”viewmodel”は、plain_activity.xmlで定義したviewodelのことを指しています。 一方、右辺のviewModel(※Mが大文字なので”viewmodel”とは異なる)はSimpleViewModel.ktのViewModelを、 ViewModelProvidersで受け取ったものです。 なので、binding.viewmodel = viewModelは plain_activity.xmlとSimpleViewModel.ktのViewModelの紐付けを表しています。 ここまでの関係図を矢印で書くと、こんな感じになります。↓ plain_activity.xmlとSimpleViewModel.ktが紐づけされた状態なので、 android:text="@{viewmodel.name}"や、app:hideIfZero="@{viewmodel.likes}"**のように、レイアウトからSimpleViewModel.ktの変数(name, likes)を引用することができます。 例えばnameは、_nameが代入されており、_nameはMutableLiveData("Ada")として定義されているので、Nameのところに”Ada”が表示されます。 BindingAdapter さて、残りのBindingAdapter.ktでは、plain_activity.xmlが受け取った変数を引数とするメソッドが定義されています。以下に一例を挙げます。 BindingAdapter.kt @BindingAdapter("app:popularityIcon") fun popularityIcon(view: ImageView, popularity: Popularity) { val color = getAssociatedColor(popularity, view.context) ImageViewCompat.setImageTintList(view, ColorStateList.valueOf(color)) view.setImageDrawable(getDrawablePopularity(popularity, view.context)) } plain_activity.xml <ImageView app:popularityIcon="@{viewmodel.popularity}" 以下省略 /> この場合だと@BindingAdapter(app:popularityIcon)という表記で、popularityIconメソッドが、plain_activity.xmlのImageViewと紐付けされています。 メソッドの引数の型は、(ビューの型,レイアウト側から受けとる変数の型)となっています。 上の例だと、fun popularityIcon (view: ImageView, popularity: Popularity)のようになっていて、渡されたpopularityの中身によってメソッドの処理結果が変わるため、ImageViewの画像表示が切り替わるようになっています。 これで、SimpleViewModel.ktからBindingAdapter.ktまでのつながりが(ふわ~っと笑)確認できました。 BindingAdapterの部分だけ抜き出した場合はこんな感じ↓ ボタンタップからUI更新の流れ このアプリはLIKEボタンがトリガーになっていて、LIKEボタンのタップ回数によって画像や色が変わっていきますが、どういう挙動になっているのか確認するために、流れをまとめました。 (図はなんやかんや省略してます。"イメージをつかむ"程度の認識で眺めてください) LIKEボタンのタップ ・LIKEボタンを押すと、ボタンがSimpleViewModelのonLike()メソッドと紐付いているので、_likesが増える。 ↓ ・_likesはlikes、popularityの二つのLiveDataに代入されるので、2つに分岐する。 likes側 ➀TextViewがlikesを受け取り、String型に変換し、タップ数として表示する。 ➁プログレスバー(ProgressBar)は2つの@BindingAdapterと紐づいているので分岐する。  ➁-1 likesが0ならばプログレスバーを非表示、それ以外の場合は表示する。  ➁-2 likesの数量に応じてプログレスバーの進捗具合を表示する。 popularity側 Transformations.mapで_likesがPopularity型へ変換され、when式でPopularityレベルを分ける。 ↓ ➀、➁共にSimpleViewModel.ktからpopularityを受け取り、@BindingAdapterで紐づいたメソッドへ、引数として渡す。 ➀はImageViewの画像の切り替え、➁はプログレスバーの色変更をする。 感想 まとめるのにだいぶ時間がかかってしまいましたが、概略図がなんとなくイメージできるようになったので、やった意味はあったと思います。実際にたくさん使って、何か作ってみようと思います~^^
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】Modifier.requiredXXXについて、モヤモヤしなくなるくらいに理解する【Jetpack Compose】

はじめに Modifierでサイズを指定するとき、widthとrequiredWidth、heightとrequiredHeight、sizeとrequiredSizeがあるよね。 Composeの公式ドキュメントを読めば言いたいことは大体理解できる。 親レイアウトが指定したサイズを無視して、子レイアウトのサイズを決定したい場合にrequiredXXXを使うよってことよね。 これはわかる。 わかるんだけど、いざコードのコメントを和訳して読んでみると、 requireSize コンテンツのサイズを正確に[size] dpの幅と高さになるように宣言します。 着信測定[制約]はこの値を上書きしません。 コンテンツが着信[制約]を満たさないサイズを選択した場合、親レイアウトは[制約]で強制されたサイズとして報告され、コンテンツの位置は、割り当てられたスペースを中心に自動的にオフセットされます。 [制約]が尊重されることを前提とした親レイアウトによる子。 はいこの通り。 全然さっぱりわけがわからない。 このままでは、予測にrequiredXXXが出てくるたびにモヤモヤしてしまう。 それはあまりにも精神衛生に良くない。 ということで、実際にコードを読んでみて、なんとなくModifier.requiredXXXについて理解し、モヤモヤせず開発できるようになるくらいの粒度でまとめていくよ。 ちなみに、「width requireWidth 違い」って結果だけ欲しい人は、まとめだけ読んでくれれば多分満足できるよ。 widthとrequiredWidthの違い まずは、Modifier.widthとModifier.requiredWidthがどう違うのか見ていく。 ■ Modifier.width fun Modifier.width(width: Dp) = this.then( SizeModifier( minWidth = width, maxWidth = width, enforceIncoming = true, inspectorInfo = debugInspectorInfo { name = "width" value = width } ) ) ■ Modifier.requiredWidth fun Modifier.requiredWidth(width: Dp) = this.then( SizeModifier( minWidth = width, maxWidth = width, enforceIncoming = false, inspectorInfo = debugInspectorInfo { name = "requiredWidth" value = width } ) ) 見比べると違いは一目瞭然。 enforceIncomingに渡している真偽値が、widthの場合はtrue、requiredWidthの場合はfalseになっているね。 なんとなくだけど、このenforceIncomingが何に使われているのかがわかればスッキリしそうな気がする。 SizeModifierの中身を読む enforceIncomingのフラグで動きがどう変わるのか見るために、SizeModifierの中身を読んでいく。 ■ SizeModifier private class SizeModifier( private val minWidth: Dp = Dp.Unspecified, private val minHeight: Dp = Dp.Unspecified, private val maxWidth: Dp = Dp.Unspecified, private val maxHeight: Dp = Dp.Unspecified, private val enforceIncoming: Boolean, inspectorInfo: InspectorInfo.() -> Unit ) : LayoutModifier, InspectorValueInfo(inspectorInfo) { private val Density.targetConstraints: Constraints get() { val maxWidth = if (maxWidth != Dp.Unspecified) { maxWidth.coerceAtLeast(0.dp).roundToPx() } else { Constraints.Infinity } val maxHeight = if (maxHeight != Dp.Unspecified) { maxHeight.coerceAtLeast(0.dp).roundToPx() } else { Constraints.Infinity } val minWidth = if (minWidth != Dp.Unspecified) { minWidth.roundToPx().coerceAtMost(maxWidth).coerceAtLeast(0).let { if (it != Constraints.Infinity) it else 0 } } else { 0 } val minHeight = if (minHeight != Dp.Unspecified) { minHeight.roundToPx().coerceAtMost(maxHeight).coerceAtLeast(0).let { if (it != Constraints.Infinity) it else 0 } } else { 0 } return Constraints( minWidth = minWidth, minHeight = minHeight, maxWidth = maxWidth, maxHeight = maxHeight ) } override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints ): MeasureResult { val wrappedConstraints = targetConstraints.let { targetConstraints -> if (enforceIncoming) { constraints.constrain(targetConstraints) } else { val resolvedMinWidth = if (minWidth != Dp.Unspecified) { targetConstraints.minWidth } else { constraints.minWidth.coerceAtMost(targetConstraints.maxWidth) } val resolvedMaxWidth = if (maxWidth != Dp.Unspecified) { targetConstraints.maxWidth } else { constraints.maxWidth.coerceAtLeast(targetConstraints.minWidth) } val resolvedMinHeight = if (minHeight != Dp.Unspecified) { targetConstraints.minHeight } else { constraints.minHeight.coerceAtMost(targetConstraints.maxHeight) } val resolvedMaxHeight = if (maxHeight != Dp.Unspecified) { targetConstraints.maxHeight } else { constraints.maxHeight.coerceAtLeast(targetConstraints.minHeight) } Constraints( resolvedMinWidth, resolvedMaxWidth, resolvedMinHeight, resolvedMaxHeight ) } } val placeable = measurable.measure(wrappedConstraints) return layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } // ここから先は関係なかったので省略 } 幸いにも、SizeModifierの中でenforceIncomingを参照しているのはequalsを除いて、MeasureScope.measureの一箇所だけだった。 MeasureScope.measureでは多分レイアウトの計算をしているんだろうけど、その中でenforceIncomingが true(widthを使った場合)はconstraints.constrain(targetConstraints)の結果が、 false(requiredWidthを使った場合)はなんかゴチャゴチャやった結果がそれぞれwrappedConstraintsの中に入れられてるのがわかる。 wrappedConstraintsは最後の方でmeasurable.measureに渡されているので、多分wrappedConstraintsが持つ制約を元にしてレイアウトが計算されるって考えて良さそう。 ってことはこの二つのパターンをそれぞれ理解すれば、もう怖いものはなさそうね。 enforceIncoming == true の場合 再度trueの場合を見てみる。 constraints.constrain(targetConstraints) 一件シンプルに見えるけど、それはtargetConstraintsが中の処理を隠してくれているからね。 targetConstraintsの初期化処理をみると private val Density.targetConstraints: Constraints get() { val maxWidth = if (maxWidth != Dp.Unspecified) { maxWidth.coerceAtLeast(0.dp).roundToPx() } else { Constraints.Infinity } val maxHeight = if (maxHeight != Dp.Unspecified) { maxHeight.coerceAtLeast(0.dp).roundToPx() } else { Constraints.Infinity } val minWidth = if (minWidth != Dp.Unspecified) { minWidth.roundToPx().coerceAtMost(maxWidth).coerceAtLeast(0).let { if (it != Constraints.Infinity) it else 0 } } else { 0 } val minHeight = if (minHeight != Dp.Unspecified) { minHeight.roundToPx().coerceAtMost(maxHeight).coerceAtLeast(0).let { if (it != Constraints.Infinity) it else 0 } } else { 0 } return Constraints( minWidth = minWidth, minHeight = minHeight, maxWidth = maxWidth, maxHeight = maxHeight ) } こんな感じで、結構複雑そうに見える。 けど、ちゃんと読めばそんなに難しくはなかった。 最初は最大値の制約について。 maxWidthが指定されていた場合(maxWidth != Dp.Unspecified)はmaxWidthが0dp以上であることを保証してから、px変換する。 指定されていなかった場合は最大値(Constraints.Infinity)になる。 heightに関しても同じ。 次に最低値の制約について。 minWidthが指定されていた場合(minWidth != Dp.Unspecified)はまずpx変換してから、最大値以下であることを保証(coerceAtMost(maxWidth))して、0px以上であることを保証してから、最大値が指定されていなかった場合は指定した値を、最大値が指定されていたら0に設定している。 指定されていなかった場合は0になる。 heightに関しても同じ。 上記の処理で作った高さ、幅の最大値/最小値制約を持っているのが、targetConstraintsということね。 これを constraints.constrainに渡しているので、中身も見てみる。 fun Constraints.constrain(otherConstraints: Constraints) = Constraints( minWidth = otherConstraints.minWidth.coerceIn(minWidth, maxWidth), maxWidth = otherConstraints.maxWidth.coerceIn(minWidth, maxWidth), minHeight = otherConstraints.minHeight.coerceIn(minHeight, maxHeight), maxHeight = otherConstraints.maxHeight.coerceIn(minHeight, maxHeight) ) イマイチこれだとわからないので、コメントを読んでみる。 otherConstraintsを受け取り、現在の制約でそれらを強制した結果を返します。 これは、結果の制約を満たすサイズは現在の制約を満たしますが、2つの制約セットが互いに素である場合はotherConstraintsを満たさない可能性があることに注意してください。 例(幅のみを表示、高さは同じように機能します):( minWidth = 2、maxWidth = 10).constrain(minWidth = 7、maxWidth = 12)->(minWidth = 7、maxWidth = 10)(minWidth = 2、maxWidth = 10).constrain(minWidth = 11、maxWidth = 12)->(minWidth = 10、maxWidth = 10)(minWidth = 2、maxWidth = 10).constrain(minWidth = 5、maxWidth = 7)->(minWidth = 5 、maxWidth = 7) これはなんとなく言ってることわかるね。 つまり、constraint.constrainを使って、親レイアウト無視で作られた制約のtargetConstraintsを、親レイアウトを考慮した制約として生成し直しているわけね。 なるへそ〜。 enforceIncoming == false の場合 次はfalseの場合。 さっきは複雑に見えた処理も、targetConstraintsを読み解いた後なら何をやっているのか見えてくる。 val resolvedMinWidth = if (minWidth != Dp.Unspecified) { targetConstraints.minWidth } else { constraints.minWidth.coerceAtMost(targetConstraints.maxWidth) } val resolvedMaxWidth = if (maxWidth != Dp.Unspecified) { targetConstraints.maxWidth } else { constraints.maxWidth.coerceAtLeast(targetConstraints.minWidth) } val resolvedMinHeight = if (minHeight != Dp.Unspecified) { targetConstraints.minHeight } else { constraints.minHeight.coerceAtMost(targetConstraints.maxHeight) } val resolvedMaxHeight = if (maxHeight != Dp.Unspecified) { targetConstraints.maxHeight } else { constraints.maxHeight.coerceAtLeast(targetConstraints.minHeight) } Constraints( resolvedMinWidth, resolvedMaxWidth, resolvedMinHeight, resolvedMaxHeight ) falseの場合は最小値、最大値ともに、指定されていた場合はtargetConstraintsの値を使う。 指定されていなかった場合は最小値が最大値以下であること、最大値が最小値以上であることを保証する。 今回はtrueの場合と違い、constraints.constrainを使っていないので、親レイアウトは完全無視の制約を使うことになるわけですね〜。 はい、完全に理解した。 対戦ありがとうございました。僕の勝ち。 まとめ Modifier.width/height/sizeは親レイアウトの最小値/最大値制約を考慮してサイズを決定する。 内部でenforceIncomingにtrueが渡されることでconstraints.constrainが実行されて、親レイアウトを考慮した制約が適用される。 Modifier.requiredXXXは親レイアウトの最小値/最大値制約を無視してサイズを決定する。 内部でenforceIncomingにfalseが渡されることで、親レイアウト無視で生成された制約がそのまま適用される。 こんな感じかしら。 モヤモヤしなくなるくらいには理解できた。やったね。 Droid Kaigiのセッションに触発されて中身のコード読んでみたけど、理解するのもまとめるのも大変な分、恩恵は大きいなあと感じた。 また今度、別のモヤモヤポイントも読んでまとめてみる。 おわり。 参考 https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt;l=1?q=Size.kt&sq=
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む