39
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

DroidKaigi 2023で驚いたところメモ

Last updated at Posted at 2023-09-18

Androidの技術が好きなので、技術よりというかのやつばっかり聞きました。太字で個人的に驚いたところを書いています。何か間違えや聞き間違いなどがあれば教えてください。

Modifier.Node を使いましょう

これを見なければということで来ました。

Composable関数は返り値があるとスキップされない(これは知っていた)

  • Modifier.composed{}
    composed{}はModifireの返り値があるので、スキップされない。
    状態を保持するために結構Compositionツリーが大きくなる。

  • Modifier.Nodeはチェーンが2つ。
    Elementは毎回Compositionごとに比較用に使われる。
    Nodeはレイアウトに必要なプロパティを提供する。

Elementにはcreate()メソッドがあってNodeが作られて、それがComposeで管理される。

Modifier.NodeでCompositionLocalを読むときはcurrentValueOf()を使う

  • DroidKaigiアプリのPRについてメモ

tabIndicatorOffsetModifierNode()を呼び出すようになった
-> .then(TabIndicatorOffsetElement(currentTabPosition = currentTabPosition))

TabIndicatorOffsetElementをnewする
TabIndicatorOffsetElementではcreate()でNode作成する
つまりModifierNodeElementはFactory的なものなのかな?

class TabIndicatorOffsetElement(private val currentTabPosition: TabPosition) :
    ModifierNodeElement<TabIndicatorOffsetNode>() 
    override fun create(): TabIndicatorOffsetNode = TabIndicatorOffsetNode(currentTabPosition)

    override fun update(node: TabIndicatorOffsetNode) {
        node.targetTabWidth = currentTabPosition.width
        node.targetIndicatorOffset = currentTabPosition.left
    }
....

で以下で実際に処理を行うっぽい。override fun MeasureScope.measureっていうのを継承して、layout() composableみたいなやつができるっぽい

class TabIndicatorOffsetNode(
    currentTabPosition: TabPosition,
) : LayoutModifierNode, Modifier.Node() {
...

つまりまとめると
Composeの変更管理の仕組みでうまく管理できなかったので、Compose内の変更管理用のModifierNodeElementModifier.Nodeで分けることでうまくできるようにしたということっぽい。

作って学ぶadbプロトコル

(ちょっと途中から)

adb以下の3つで構成される
adbクライアント -> adbサーバー -> adbデーモン
adbサーバー
adbクライアント サーバーにコマンドを送信する
adbデーモン デバイス上で動くバックグラウンドプロセス

adbのバイナリにサーバー、クライアント両方が含まれている。

adbサーバーがクライアントに公開するのがスマートソケット

adbのメッセージ形式は24バイトの固定長とペイロード。
https://android.googlesource.com/platform/system/core/+/dd7bc3319deb2b77c5d07a51b7d6cd7e11b5beb0/adb/protocol.txt#31

ADB_TRACEで通信内容を確認できる

export ADB_TRACE=all

サーバーからデーモン
CNXN(CONNECT)
でつなぐ。

(非公式) Guide to Architecture Guide Vol. II

  • Hiltが完璧でない話。
  • モジュラーアーキテクチャ。一つの原則について

(Topアプリでたくさんの時間を使っているので、大切にしているという話 :eyes: )
すべてのパターンをカバーするのではなくよくあるパターンをカバー。

Slack Circuitの話: ComposeでMVPを実装するライブラリ
Turo's アノテーションでkspを使う。: コードがきれいで、色々FragmentとComposeをいい感じにしてくれる。

本題: 依存性注入
DaggerはJSR 330を満たすもの。基本的なバインディング (Inject Qualifier)、Scope
DAG (Directed Acyclic Graphs)。Binding, Component

なぜHiltか?Powerful、10億インストール超えのアプリでたくさん使われている。ボイラープレート減らせる。
テスト。 Googleは80から90までのカバレッジが義務。Hiltは元々これを解決するためのものだった。

Hiltのダメなところ。

  • 事前に定義されているComponentを越えようとすると結構ややこしい。マルチアカウントなどでこまる。
    例えばSingleton -> SingletonAccountComponent -> xxxxみたいなのを作らないといけない。できなくはないが。。
    → anvil。個人的にはHiltはこうあるべき。Daggerの拡張。Kotlin compiler pluginで行っている。

anvilではwhetstoneでHiltのような構造を作れる。

  • Kotlinのサポート
  • Composeフレンドリーでない。

Composeのメソッドの引数が増える問題。
カプセル化がうまくできない問題。一旦EntryPointを途中で使うことができる。
ただ、CompositionLocal使わないの?
StateHolderにDIするのがいいのではないか。
ViewModelScopeのようなものを作る。 ComposeScopedみたいなのを定義する。
アイデアとしては、StateHolderの中にComposable関数を書いてしまって、そのStateHolderのフィールドで注入したものを書く。

本題: マルチモジュール
ビルドタイムが一番大きい。
オーナーシップ。コードの健康を保つ人を持てる。
dynamic-featureなども可能。
コードを共有するのも、モジュールの構造だけで何をしているか分かりやすい。

原則: モジュールAがモジュールBの内部実装に依存している場合は、独立していない。その場合はモジュール分けないほうがいい。
Google Playの詳細画面一つで見える範囲で、モジュールが6個。
組織が大きくなればモジュール分けても良い。
モジュール化はカプセル化に役立たなければいけない。パッケージやチームベースで分けるのも良くない。
モジュールの大きさは分ける基準ではない。

いつ分けるべきか? なるはや。モジュールを分けるコストは大きくない。早いほどよい。
モジュールを分けると、見えてはいけないものが見えているなど、アーキテクチャ上の問題が発見しやすい。

アンチパターン: 循環参照。 解決策: Mediator、interface。interfaceがオススメ
アンチパターン: 間違った依存関係。 解決策: 依存性逆転の原則を使う。 DataStoreのinterfaceだけを持つモジュールにするなど。

Intentを作るのはActivityでもいいが、IntentMangaerの実装がActivityにあるなどが良い。

Maintaining E2E Test Automation as We Transition from View to Compose

メルカリの話。

  • メルカリでのテストどうやっているか

2週間リリースを1週間に短くするためにE2Eテストでリリースをジャッジする。
E2Eはどう?
システム全体が見れる
内部がうまく動くかどうか分からない。時間がかかる。

どんな感じ

TestAccount(
  userName = ""
).login()

...
Page
.xxx
.assertDisplayed()

夜にE2Eテスト。

TestRail。すべてのテストを見る。Single souce of truthなツール。
Flankを使って、Firebaseでparallelテストできるようにしている。

ViewからJetpack Compose、機能実装もある。 (両方での)

PageObjectModelというパターンを使っている。

  • どうやってComposeに置き換えたか

Page Object(assertCounterLabel()などがある)のinterfaceと、それのViewとComposeでのテストの実装を作って、ViewとCompose両方の実装を作って、プロダクションコードとテスト両方を切り替えできるようにして移行するという感じ。

SubcomposeLayoutのときにnodeが重複しやすいので、testTagをつけることで回避しやすい。

Compose の Text Field を理解する

これがサンプルっぽいけど、めちゃくちゃ知見だらけでした。
https://github.com/mxalbert1996/DroidKaigi-2023-TextField

文字数制限を行うのにサロゲートペアとかの考慮が必要。

decoration boxを使うと、表示隠せて、認証コード入力フィールドみたいなことができる。またdecoration box部分をタップしてもText Fieldにフォーカスできる。

日本語とローマ字で、同じ高さを使うには?
Line Heightを設定する。フォントサイズの1.5倍にしておくと良い。日本語よりデフォルトだと低い場合がある。例えばfontSize = 16.sp, lineHeight = 24.sp
ただ、これだけだとトリミングされてしまう。lineHeightStyle = LineHeightStyle(Trim.None)みたいな感じにする。これを使うにはplatformTextStyleでincludeFontPadding = falseをする必要がある。 alignment=Proportinalにする。

collectIsFocusedAsState()を使ってPin
https://github.com/mxalbert1996/DroidKaigi-2023-TextField/blob/main/app/src/main/java/com/mxalbert/compose/textfield/ui/demo/PinTextField.kt

TextField2について。
TextFieldはバグレポートがたくさんあって、基礎的な問題があって変更の必要があるため。
1.6 alpha版で試せる。
TextFieldStateを使うようになった。
textはreadonlyで、editという関数を使う必要があり、Bufferというクラスを使って、行う。変更が記録されて、 changesに変更が貯められて変更を戻せる。 revert all changesという関数もある。
引数のInputTransformationを使うと入力文字数制限などもできる。
scrollStateも引数として追加されて、スクロールの位置なども取得できるようになった。

共有

ShareCompatだと簡単にかけることが多い。
category.DEFAULTをつけないと暗黙intentを受け取れない。
ShareCompatのIntentReaderというのを使うと受け取る側もちょっと型安全に受け取れる。

画像ファイルを送る側では、androidxで定義されているcontent providerを使う簡単に共有できる。
画像なんでも受け取るときはワイルドカードでimage/*でなんでも受け取るみたいなことはできる。ちゃんと表示できる必要がある場合はそれぞれ宣言する。
ShareCompatを使えば画像の権限について考えなくて良い。

直接共有のAPI。共有の中でDMで送るみたいなやつ。ショートカットを作ると共有の対象として指定する。
ショートカットを作るには画像とIntentなどを作って、ShortcutManagerCompat.pushDynamicShortcut()で登録する。またxmlも用意するshare-targetなど。またActivityでこのxmlを指定する。
実際に来るときはextra shotcut idというのが渡ってくるので、これを使う。

頻度が高いものを前に出すには?
メッセージを出すときと受け取ったときにショートカットを作るとよい。なぜショートカットを作っているのかをシステムに伝えてあげる。addCapabilityBindingというのでRECEIVE_MESSAGEをつけてあげることで、システム側からわかるので、ランキングにも反映される。reportShortCutUsed()というのがあるって知らせることができるが、pushDyammicShortcut()だけで良い。
25までは互換性がある。
sharetargetのライブラリを入れると23まで互換性ができる。

共有シートでできないことはないはず。

  • 任意のアクションを追加できる。
  • 並び替えもできる
  • 共有先の除外もできる
  • どこが選ばれたか取得できる

API Level 34から、共有シートにカスタムアクション追加できる
Web検索できるIntentを追加したりできる。

初期インテントという機能があり、EXTRA_INITIAL_INTENTSで優先できる。intentの引数を変えるみたいなこともできる。
共有先の除外や共有先のアプリも知れる。

Androidアプリの良いユニットテストを考える

良いユニットテストとは?
アプリの変更が容易な状態を保つため。

なぜユニット
E2Eだとカバーしきれない。安定性、フィードバックの長さなど。
区切ってテストすることで上記の問題を解決。

テストの変更の容易な状態にするには

  • テストが開発者から信頼される
  • グリーンに保つコストが低い
  • テストを保守するコストが低い

Robolectricは実行1個目のテストはセットアップが走って、1秒以上かかって長いが、2個目以降のテスト実行は数十msで動く
テストダブルでの失敗例
ResultのFailureでItemNotFoundExceptionを返すようにして、テスト実行していたときに、空を返すようにしたときに、テストは成功する。

テストダブルは維持するコストがかかる。そのため実オブジェクトを使っちゃったほうが良い時もある。

テスト間で、RobolectricはSQLiteとかは初期化してくれるけど、static変数とかはそのままなので、もし使っているならクリーンアップ必要

(すごくいい感じの言語化がされていました。)

Building a screenshot testing pipeline that scales

なぜ作るか?
品質を守る。
screenshotをPRにはる。
PRにはるスクショを作るのにも時間かかるし、なんかかけてて壊れてても気づかない。
デザインガイドラインのため。

メルカリではロジックではなく、UIのコードが正しく描画されることをテストしている。
スクショテストはユーザーがpost condistionをチェックする必要がある。

mainと比べていない。base commit(PR切った元のブランチ)と比べている。 mainと比べるとたくさんdiffが出る。
→ (個人的に 最新main と 最新mainとマージしたコミットを比較する方法もあるけど、最新mainがビルドされていないときに差が出ちゃうことがあるんですよね。なので良さそうかも)
Paparazziでスクショをテストを作ってreg suiteでdiffを撮る。

Paparazziでは複数のCompositionが必要なものは描画できない。Compositionでサイズ変えたりとか。

できていないこと
Design Component以外。 画面など

(オフィスアワーで拙い英語でRoborazziを宣伝しに突撃しにいった)

Material 3 やめました。

B43で進めていたが、 deisgn systemを使うのをやめた話。

Material3の値を使うのをやめて、MyTheme.colorSchema.primaryみたいな感じで定義できるようにしてそれを使うようにするということっぽい。

ドキュメントがあるらしい。

M3のおさらい

  • デザイントークン。 変数として定義。整合性を保つ。
    • Reference 色と紐づく
    • system Darkなどコンテキストに紐づく
      • color, typo, shapeがある
        • color
          • 5つのkey color
            • key colorからpalette(0が真っ黒100が白)ができる
              • palleteの40がcolor roleのprimaryなどが決まっている。
        • shape
          • 大きさによって7段階に分かれている
    • component コンポーネントの属性に対応する。

つらいとこころ

  • Color
    • ブランドカラーを作りたい。
      • primaryは別の色が違う。
      • 指定できるが。。?他のところ29個指定しないといけないので、大変。
        • 色定義を追加するのはどう?
          • できるが、dynamic colorで色変わらないので、他のdyamicな部分との調和ができない。
          • M2にはdark themeを追加できたが、M3ではできない。(isSystemInDarkTheme()はアプリ内の値が使えない。自分で用意する方法もある。)

かわりにどうした?
どうした?

  • 独自のデザインシステムを作る

    • tokenの仕組みを作る
      • デザイントークンは作るが、コンポーネントトークンは使わなかった。
        • 参照が深いので、ここまでの抽象化が必要かどうかみたいな形になった。
        • Androidだけのコンポーネントトークンどうするみたいなものがあった
      • MyTheme.colorSchema.primaryなどを使う
    • System tokenの種類を作る
      • デフォルトはcolor, typography, shape。
        • もし助けが得られれば、トークンを作る
      • no: 段階的に作っていく。
        • Color backgroundとtext colorは必要。onBackgroundとか。
        • primaryなど
        • フラットにしておいて、増えてきたら階層化が良さそう
        • bodyLarge bodyMedium
        • Dimension
          • spacing
          • radius
  • appモジュールでの依存はmaterial3ではなく、foundationにする

    • rippleとかをdependenciesに追加する必要がある。
  • マイグレーションはどうする?両方のテーマをapplyする。

  • コンポーネント定義

    • Checkboxとかを使うことはできない
  • 独自でM3コンポーネントをラップする

自分の記憶に残った部分だといかのような方針でした。
Material3をやめるといってもMaterial3のコンポーネントは使いまわす。Material3のテーマを定義するのをやめて、自分でSystem tokenを定義する(MyTheme.colorSchema.primaryなど)。Material3の依存をdesign-system以外では隠蔽するようにして、自分でラップしたコンポーネント作る。Snackbarとかラップするのが少し大変なコンポーネントもあるができなくはない。

39
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
39
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?