🤖

DroidKaigiに現地参加してきた(1日目)

に公開

DroidKaigiに初現地参加しました
この記事では私が1日目のConference Dayに参加したセッションの内容をまとめました
メモを取って整理した箇所、知らなかったけれど理解できた箇所が中心なので、私がすでに知っていたり、頑張って理解しようとしたけれど難しかった箇所は飛ばしてます
誤解してそうな箇所がもし見つけたら、優しくコメントしてもらえると幸いです・・・
スライドは見つけたら掲載しています
動画はテザリングで書いているので、帰宅してから掲載します

KotlinでのAI活用による開発

AI支援コーディングがどう役立つのか

AIを使ってコーディングの支援を受けられるが、主に以下のように分けられます
上に来れば来るほどミニマムなAI支援、下に来れば来るほどゴリゴリなAI支援になります

使い方 知らなかったところ
コード補完 ユーザーのコーディングの途中でAIから提案される ・提案された補完を単語単位で補完させることも可能
。JetBrains AIはローカルで動くよ
・定義は違っても類似したコードに同じような修正を提案できるよ(例:地図のXY座標の計算)
プロンプトでクラスやメソッドをコーディング 〇〇をするクラスを作って 差分を見てコミットメッセージもAIにおまかせできる
AIチャットで会話しながらコーディング(ペアプロに近い) ここにキャッシュを効かせるコードを書く ローカルでも可能
AIエージェントとやり取りしてテストまでコーディング ある仕様を実装してテストコードまで書く JunieというAI
実装する内容は決まっている(雰囲気だけでも可)がコードがわからない(バイブコーディング) 楽しい!を表現するUIを作る

Junieは知らなかったので、色々使ってみます

KotlinでAI搭載アプリを作る

KoogというAIが紹介されてたので、使ってみます
スライドに出てきたサンプルコードはシンプルに使えてたので、コーディングは難しくなさそうな印象でした

Koog使って難しいなーと思ったらYouTubeに動画あるので学習ハードル低め

共有と分離 ─ Compose Multiplatform “本番導入” の設計指針

スライド

略称を多用するので、最初に書いておきます
KMP(Kotlin Multiplatform):Kotlinで書いたロジックをマルチプラットフォームで共有
CMP(Compose Multiplatform):UIをマルチプラットフォームで共有

KMP/CMPの超ざっくりな動向

KMPは去年の11月に安定版がリリースされて、いろんなパッケージがKMP対応しています
どんどん右肩上がりなので、安定していると言って良さそう

CMPは同時期だとβ版だったが、今年の5月に安定版が出たので、個人的にはこれからという印象です

コードの共通化の限界

KMP/CMP共通して言えることは、大前提として要件を明確にします

KMPでの判断基準は

  • Kotlinの公式共通ライブラリで実現できるか
    • Kotlinのスタンダードライブラリ、コアライブラリ、ビジネスロジックは実現可能
    • 各プラットフォーム依存のAPI、デバイス依存、ネットワークやファイルなどの入出力は実現不可
  • KMP対応のライブラリで実現できるか
    • Klibs.ioでKMP対応のライブラリが検索できる
      • GitHubとMaven Centralをクロール
      • 対応プラットフォームや技術カテゴリで検索可能
    • Kotlin Multiplatform samplesも参考になるかも

どちらかで共通化できないなら、分離しよう!

CMPも似たような判断基準です

  • Composeの基盤モジュール、Material(M2、M3)モジュール、AndroidXライブラリで実現できるか
    • Composeの基盤モジュール = rutime ui foundation animation
    • AndroidXライブラリ = org.jetbrains.androidx.*
    • MapsライブラリやWebViewなど、Jetpack Composeに対応しているが、CMPに非対応のものがあったりする
    • AndroidXライブラリに含まれているが、Androidだけ提供されているAPIもあったりする
    • OS毎のUIらしさを優先したい場合、基本的には実現不可
  • CMP対応ライブラリで実現できるか
    • CMPもKlibs.ioで検索できる
    • WebViewとかはここで実現できる

これらを駆使すると、91.5%は共通化できたそうです
8.5%は分離したので、その手法を大きく2つ紹介されました

expect/actualで分離する

expect宣言子で定義して、actual宣言子で実装する、Kotlinでよく使われるやつ
iosMainで実装する時、Kotlinで書くことになるが、何もせずともiOSのAPIをimportできる(仕組みは割愛)
ただし、SwiftだけのAPIとか、サードパーティ製のネイティブSDKはここではimportできないが、自前で準備すれば可能だったりします

ネイティブSDKを利用する

ここでは、現在Firebase SDKがKMP非対応なので、これを自前で準備することを考えます
方法としては、Koinを使って、Android、iOSそれぞれのアプリモジュールから共通モジュールに依存性を注入して、ネイティブSDKを参照すればOKです
やることは

  1. commonMainでインターフェースを用意
  2. androidMainで実装
  3. iosMainで実装(使用言語はSwift)
  4. 各アプリのエントリーポイントでKoinを使ってDI

iOSのネイティブ実装で、cinteropが公式で紹介されているが、準備が大変だったり、iOSの開発環境がレガシーになってしまうので見送った

これでもう迷わない!Jetpack Composeの書き方実践ガイド

スライド

Composeの基本

Composeには3つのフェーズがあります

  1. コンポジション(表示するUIの作成)
  2. レイアウト(UIをどこに表示するか)
  3. ドローイング(実際に描画)

これらのフェーズは入力が同じだと以降のフェーズをスキップするようになっており、再描画を避けてパフォーマンスを向上させています

Composeのデザインパターン

命名規則

正直なところ、ノリと雰囲気でよしなに命名していました、すみませんでした

Unitを返すComposable関数

パスカルケースかつ名詞

Unit以外を返すComposable関数

キャメルケースかつ名詞が一般的です
しかし、ヘルパーメソッドは動詞で始めることがあります(処理の内容次第)
UIコンポーネントと見分けがつかなくなるので、パスカルケースかつ名詞で名付けるのはタブーです

rememberを返す関数

キャメルケースかつ、プレフィックスにrememberをつけます
privateなComposable関数

CompositionLocal

パスカルケースかつ、プレフィックスにLocalをつけます

状態とイベントの分離

内部で状態を持たず、外部で制御します
こうすることで、色々恩恵があります

  • テストしやすくなる
  • 状態やイベントが1つの情報源として集約される(SSOT)ので、バグが発生しにくい

コンポーネントの分割と単一責任

コンポーネントを分割する時、特にパブリックなコンポーネントは分割する目的と必要性をよく考える

  • 目的: コンポーネントの目的を一つにして単一責任を意識する
  • 必要性: そのコンポーネントの存在価値と認知負荷を考慮する
Good Bad
インターフェースがシンプルかつ汎用的 存在を伝えるコストと気づかれずに重複定義される
コンポーネントが存在する目的が一つ 柔軟性がなく、類似のコンポーネントが定義
存在することで価値が複数ある 既存のコンポーネントで簡単に作れてしまう

パフォーマンス

前述の通り、Composeには3つのフェーズがあり、入力が同じだとComposableの処理をスキップするので、これを使ってパフォーマンスを上げていきます

必要最低限の引数

例えば、複数のプロパティを持ったクラスを引数に渡すと、描画に無関係なプロパティが更新された場合、入力が変化したのでリコンポジションしてしまう
なので、Composableで引数を用意するときは、必要最低限の引数にしてリコンポジションを最小限に留めます

状態の読み取りを遅延させる

スクロール位置を取得する場合、スクロールのたびにリコンポジションされるので、状態の読み取りをドローイングまで遅延させることで、リコンポジションの回数を最小限にとどめます

チーム開発と継続的な運用

大きく分けると3つあります

  • シンプルで柔軟なルール
    • 厳格すぎると、柔軟性がなかったり、開発速度が遅くなったり、ストレスになったり・・・
    • 完璧すぎないかつ、個人にある程度ゆとりをもたせよう
  • AIに頼る
    • ルールをコンテキスト化して、AIにレビューしてもらう
    • 多くなっても、AIは見てくれる
  • Linterに頼る
    • 確実かつ早く書き方を整備してくれる
    • AIにLinterを実装させるのもあり
    • IDEで直感的に気付ける
    • runtime-lint、slack compose-lintとかCompose周りのLintでおすすめ

プロパティベーステストによるUIテスト: LLMによるプロパティ定義生成でエッジケースを捉える

↓ってよくあるよね
開発でシナリオテストOK、他のテストもOKでした
なので本番リリースしたら、想定していない入力の組み合わせ、操作シーケンスでバグが発生してしまった・・・
入力の組み合わせや操作シーケンス全部をテストするのは無理なので事例テストで担保しておく

これで品質担保していくが、「自分たちの想定自体が正しい」か検証できません
これを検証するために、プロパティベーステスト(PBT)を使います

プロパティベーステスト(PBT)とは?

システムのあるべき挙動を満たすルールをコードとして書き起こして、ランダム生成された入力がこのルールを満たすか確認するテストを指します

このテストの例

お店のレジのお金を出し入れする関数cashを例にしてみます
この関数cashは、価格と支払額を入力として持ち、Map形式でお釣りに必要な紙幣と硬貨の枚数を出力します
実装的には↓のようなイメージです

fun cash(price: Double, paid: Double): Map<Double, Int> {
    // お釣りで必要な硬貨、紙幣と枚数を計算

    return mapOf(hoge)
}

日本円でEBT(Example-based Test、いくつかの代表的なパターンを検証するテスト)でテストする場合、↓のようになります

// JUnit
// 190円の価格から200円で支払った場合、10円玉硬貨1枚のお釣りとなる
assertEquals(mapOf(10.0 to 1), cash(190.0, 200.0))
// 390円の価格から500円で支払った場合、100円玉硬貨1枚、10円玉硬貨1枚のお釣りとなる
assertEquals(mapOf(100.0 to 1, 10.0 to 1), cash(390.0, 500.0))

これをPBTでテストしてみます
まずはプロパティを設定します

  • お釣りの合計額は、常に支払額から価格を引いた額と等しい(お釣り = 支払額 - 価格)
  • お釣りの紙幣、硬貨は高額から低額で降順に構成されている(お釣りの枚数を最小にする)
  • 硬貨と紙幣は常に正数

ランダムなデータに対して、↑の性質を常に成り立つかテストする
これがPBT

EBTとPBTの関係性

EBTはコードが正しいことを確認するためのテストです
PBTはコードに未知の不具合を探索するためのテストです
なので、両者は相互補完的であるべきで、EBTをPBTに置換したりPBTだけで担保するべきではないです(逆も然り)

AndroidでのPBT

Kotestとかjqwikとかあるので、まずはこれらを導入してみよう

PBTをUIテストに応用する

UIはステートやイベントがあり、これらを適切に処理されていなければ、特定の画面操作でバグるかもしれないと言える
つまり、UIは履歴依存なので、PBTテストで操作を探索してバグを出せそうです
しかしながら、ステートレスPBTはKotestにユーティリティがあるので実現できるが、ステートフルPBTはユーティリティがないので、自前実装する必要がありコストが高いです
また、UI層にビジネスロジックを排除していれば、シーケンス破綻が起きにくく、不具合検知につながる恩恵は少ないかもしれないです
それを解決するのがLLMでのPBT支援をしてもらうことです

LLMでPBT支援

(ステートレスだろうがステートフルだろうが、)PBTは以下を考える課題があります

  • 想定をどのようにコーディングするか
  • どんなプロパティを設定するのか

これらの思考プロセスをLLMに支援してもらいます
具体的には以下を支援してもらうことで、これらの思考プロセスをLLMにお願いしました

  • 自然言語からユーザーの操作をコマンド化
  • 考慮すべきプロパティを幅広く生成
  • どこで失敗するか、何故失敗するかを特定して、コードから根本原因を特定する

WebViewとはさようなら:KMP + Composeによるサーバー駆動UI

WebViewとアプリを比較した時、以下のような感じになる

クライアント リリース速度 UI 動作 デバッグ セキュリティ
WebView デプロイしたら反映される アプリっぽくない見た目になりがち 重くなりがち 大変になりがち 長期的に見ると危ない
ネイティブアプリ 各ストアの審査を待つ必要がある OSらしさが出る 基本的に軽い WebViewに比べると楽 長期的に見ても安全

リリース速度はWebViewだが、それ以外はネイティブアプリの方が良いよね
これらの両方をいいとこどりしたのがサーバー駆動UIです

サーバー駆動UI(以降SDUI)とは

サーバーがアプリにどのように表示するか指示する開発のこと
サーバーでJSON形式でUIを提供し、アプリではその解析を元にネイティブ表示します

記事を取得するAPIを実行したら、タイトルや記事の本文など表示に必要なデータだけでなく、これらに加えてUIに関する情報も含まれているJSONが取得されるようなイメージです

SDUIができるライブラリはあるの?

すでにいくつかあるそうですが、UIをカスタマイズする機会が多いので、融通が効くように自前で用意するのがベストです
その時に、QAチームやデザイナー、プロダクタ他のためにPlaygroundを用意します

もし既存のアプリをSDUIにしたいなら

  • いきなり全ての画面をSDUIにするのではなく、徐々に適用しよう
    • 何かを移行するときとほぼ同じような感覚な気がする
  • UIを提供するサーバーはUIだけに専念して、それ以外(認証やデータの取得など)は他のサーバーを実行するようにしよう
  • JSONが古かろうが、アプリバージョンが古かろうが、新旧どの組み合わせでも動くようにしよう

SDUIで動いてるアプリの例

現地参加しての感想(1日目)

初めて現地参加したが、ライブと音楽フェスのように現地が一番良いですね
参加したいセッションに合わせて部屋移動するときとかは、まさに音楽フェスっぽかったです

個人的な一番の収穫は、AI周りです
ChatGPTやGitHub Copilotをよく使うのですが、JunieとかちょっとズレるがKoogとか知らないAIがいたり、そういうAIの使い方があるのかぁとなりました
今回知ったAIはちょっと使ってみようと思います
なんだか、自分の気に入るAIを探すのは、テキストエディタとかカードゲーマーにしか伝わらないが外スリーブを探すのに似ている気がしてきた

KMP/CMPが関わりそうなセッションにも積極的に参加したのですが、参加すればするほど、個人的にはFlutter(というよりDart)が自分には合わないのだなと実感しました
かつてFlutterでアプリ開発して今はAndroidアプリ開発をしていますが、開発体験的にAndroidアプリ開発の方が圧倒的に満足です
なんでちょっとPadding追加したいなーっていうときにでもいちいちネストさせられるんだ

最後に

2日目も記事を書いて公開する予定です

Discussion