「最適なiOS のGraphQLクライアントを求めて」という発表をしました

こんにちは。カンカクのiOSエンジニアの@redryeryeです。

3月24日に開催された、potatotips #77 iOS/Android開発Tips共有会「最適なiOS のGraphQLクライアントを求めて」という内容でライトニングトークを行いました。

内容

今年の始めに、カンカクではカフェ事業としては初となる、ラウンジ業態の店舗が下北沢でオープンしました。これに伴い、プレオーダーや事前決済ができるモバイルアプリ『COFFEE App』にラウンジの予約機能が追加されました。

COFFEE Appの従来の機能ではアプリとサーバ間の通信にREST APIを使用していますが、今回のラウンジの機能ではGraphQLを採用しています。
GraphQLクライアントには比較的メジャーなApollo iOSを採用しました。

この発表ではApollo iOSを使った所感や、導入検討をしているもう一つのGraphQLクライアントSwiftGraphQLについてApolloと比較しながら紹介しました。

詳しくは以下のスライドをご覧ください。

おわりに

今回はラウンジの機能をiOSで実装する中で直面した課題を紹介しました。Androidでも新規UIの全てをJetpack Composeで記述していたりと面白い挑戦をしているので、気になる方はこちらの記事もぜひご覧ください。

developers.kankak.com

採用案内

カンカクではこのような新たな技術を模索しながら楽しく開発していく仲間をさがしています。 採用案内などは、詳しくは以下のエントリーをご覧ください。
応募までは振り切れないという方でも雑談ベースでのお話大歓迎です!

Jetpack Composeをフル活用してデカめの新規機能を作った

こんにちは、カンカクでAndroidエンジニアをやっている @haru067です。好きな麺は平打ち麺です。

プレスリリースにもありますが、カンカクではカフェ事業としては初となる、ラウンジ業態の店舗を2022年1月20日に開店しました🎉 これに伴い、プレオーダーや事前決済ができるモバイルアプリ『COFFEE App』にも、ラウンジの機能が追加されたのですが、Androidでは新規UIの全てをJetpack Compose(以降Compose)で記述しました。

そこで今回は、Compose導入の舞台裏について、話したいと思います。

Jetpack Composeとは

宣言的にUIを記述する、Androidの新しいUIツールキットです。*1
2021年7月には、安定版である1.0.0がリリースされました。 developer.android.com

技術選定

まず、Compose導入の経緯についてです。多くのアプリがJavaからKotlinから移行していったように、これまでのAndroidのView(以降View)からComposeへの移行は、遅かれ早かれ発生するものだと我々は考えていました。従って、焦点になるのは、Composeを「どのタイミングで」「どのように」導入するかでした。ラウンジ開発でのCompose導入を踏み切るにあたり考慮したのは、大きく分けて、次の3つでした。

  • 開発人数
  • 開発期間
  • 画面要素

開発人数

複数人のAndroidエンジニアを抱える組織では、足並みや認識を揃えるところから出発したり、学習コストについても考える必要があります。今回はAndroidエンジニア1人(わたしです)での開発であったため、学習コストを深く考慮せず、プロトタイピングとある程度平行する形で、機能開発を進めることができました。また、ComposeのCodelab等で、ある程度開発のイメージを掴んでいたのも、導入の後押しになりました。

開発期間

カンカクでの開発は店舗の開店日など、物理的な制約によって開発の期限が決まることも多く、ラウンジ開発も例外ではありませんでした。期限が決まっている開発で技術的な挑戦をすることは、不確定要素を増やすという点でネガティブでしたが、導入の仕方を工夫することでリスクを下げれば、許容できるものと判断しました。これについては後ほど話します。

画面要素

Composeを使用する上で、最も避けたいのはViewの世界とComposeの世界を相互に行き来するような状態でした。既存の機能に手を加えるケースではこのような問題に直面しがちですが、大半が新規の画面追加であるラウンジ開発はそういった懸念が無く、Composeを導入するには絶好の機会でした。また、ラウンジは複雑なUI要素が存在しないものの、画面数がシンプルに多い機能開発でした。そういった点でもラウンジとComposeは相性が良かったです。Composeのメリットである記述の短さは画面数(≒UI要素の量)の多い開発において絶大な効果を発揮しますし、懸念していた「Composeに今回必要な機能やUIコンポーネントが無いかもしれない」といった点もまあ、これぐらいのUIならいけるだろう、とある程度の自信を持って判断することができました。

f:id:kankak_inc:20220124151348p:plain
Figma上でのラウンジの画面

移行戦略

「今回」やると決めたあとは、それを「どのように」導入するか、の話です。ポイントは、Compose導入という技術的な挑戦をしつつ、開発期限を厳守する、という点です。ここで考えられる最悪のシナリオは、Composeの採用を途中で断念し、全てをViewに書き戻す作業が発生することでした。それをなるべく避けるために我々が採用したのは、Composeを使用しつつも、画面は従来どおりFragmentで区切って構成していく、という方針でした。(Compose as a Viewとでも呼びましょうか)

こうではなく
Fragment ┬ ComposeScreenA
         ├ ComposeScreenB
         └ ComposeScreenC

こうする
FragmentA ─ ComposeScreenA
FragmentB ─ ComposeScreenB
FragmentC ─ ComposeScreenC

個々のFragmentがComposeViewを持つことで、Fragmentの境界がComposeの境界となり、Composeで実現できない処理が出てきたとしても、それは該当するFragmentの範囲内での修正で済むはず、という考えです。

そしてFragmentで区切るもう一つのメリットは、既存のView資産を再利用しやすい、という点にあります。Fragmentが存在することで、画面遷移は従来と変わらずに実現できますし、ローディング(ProgressBar)やエラー表示(Snackbar)といった、元々アプリ全体で共通化していた処理も、既存のコードを流用して、手っ取り早く実装することができました。

Composeをどう書くか

次に考えたのは、「何かあったとき、すぐにComposeからViewに戻せるような状態とは何か?」という問いでした。ComposeやViewが依存しうる要素、代表的にはFragmentやViewModelとの相互作用をどう設計するか?という話です。これに関しては次の2つのルールを定めました。

ある画面Hogeが存在し、それに対応するHogeFragmentがあったとする。このとき

  1. HogeFragmentに唯一のstatefulなcomposableである`Screen`を書き、それ以外にcomposableを定義しない
  2. それ以外のcomposableはstatelessで、FragmentやViewModelに一切依存してはいけない
     (Fragment/ViewModel agnosticである)

コードで表すと次のような感じです。

// HogeFramgent.kt

class HogeFragment : Fragment() {
  override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View {
    val composeView = ComposeView(requireContext()).apply {
      setContent { Screen(viewModel) }
    }
    
    ...

    return composeView
  }

  ...

  @Composable
  private fun Screen(viewModel: HogeViewModel) {
    val uiState by viewModel.uiState.collectAsState()

    HogeScreen(
      uiState = uiState,
      onBackPressed = { popBackStack() },
      onClickA = viewModel::onClickA,
      onClickB = viewModel::onClickB,
      ...
    )
  }
}
// HogeScreen.kt

@Composable
fun HogeScreen(
  uiModel: HogeUiState,
  onBackPressed: () -> Unit,
  onClickA: () -> Unit,
  onClickB: () -> Unit,
) {
  ...
  SomeContents(...)
  ...
}

@Composable
private fun SomeContents(
  foo: String,
  bar: Int,
  event: () -> Unit,
) {
  ...
}

composable中でstateが必要になった場合、state hoistingにより、ルートである Screen が子のstateを持つようにし、子のcomposableに必要な値は全て引数として渡される、という方針です。こうすることで、FragmentやViewModelに依存する処理を1箇所にまとめることができますし、それ以外のcomposableをstatelessにすることができます。statelessであるということは、後述のプレビューやUIテストを記述する上で非常に役に立ちました。

Composeを使ってみて

ここからはComposeを使用してみての感想タイムです。まずは良かった点について。

開発効率

今回の開発で最も多かった画面パターンの一つが「スクロールが必要で、コンテンツはネットワークからの取得により動的に定まるが、ページング等は必要としない画面」でした。つまりRecyclerViewでは扱うには大げさで、ScrollViewで扱うには不十分な画面です。ComposeではLazyColumnやScroll modifierによって簡潔にスクロールするUIを記述できるので、大きく助けられました。「動的に定まる」の部分に関しても、コードで記述する、というComposeのスタイルが上手くマッチして、高速に開発をすることができました。

動作確認

previewが重いけど便利です。特にナビゲーション階層の深いところに存在する画面や、特定の状態でしか出現しないUIなど、再現に手間がかかるものほど、IDE上でのレイアウト確認に助けられました。次のスクリーンショットは、Spinnerの状態別のレイアウト確認の例です。

f:id:kankak_inc:20220124153029p:plain
IDE上でのSpinnerの状態別UIプレビュー

ラウンジ開発で特に助かった瞬間は利用履歴の画面でした。この画面は予約中、利用中、利用後、予約キャンセル、といった状態によってUIが微妙に変わり、これを細かい変更の度に全ての画面で確認するのは大きな手間でしたが、previewによりまとめて確認することができました。

f:id:kankak_inc:20220124153758p:plain
状態別の利用履歴画面一覧

あとはUIテストが非常にやりやすくなりました。これはComposeで助かったというよりも、宣言的でstatelessに作った結果、testabilityが高くなった、という感じでした。

f:id:kankak_inc:20220124163725p:plain

リッチな表現

アニメーションが扱いやすくなりました。Viewのアニメーションでは、xmlで表現できない部分をコードで記述したりして、混沌の原因になることも多かったのですが、Composeでは、そもそもxmlを使用しませんし、APIも簡潔で扱いやすくなっていると感じました。 また、Accompanistには、ローディング中に仮のUIを表示するPlaceholderという機能があります。Modifierの仕組みを使って実現しているのが面白く、View時代には実装しづらかった部分も楽に実装することができました。

f:id:kankak_inc:20220124154131p:plain
左:読み込み後、右:読み込み中

困った点

逆に、困った点は次の3つです。

  • Kotlinバージョンのロックイン
  • アプリサイズの増大
  • 成熟度合い

1つ目は開発途中にKotlin 1.6がリリースされて、アップデートを試みました。しかしComposeがKotlinのバージョンを指定するため、Composeが対応するまでKotlinのバージョンが更新できない、という事態に陥りました。2つ目は、ViewとComposeが共存する上で避けられない問題です。ComposeだけでなくAccompanistやCoil composeなど、周辺ライブラリも色々と導入した結果、Compose導入前後で数MBのサイズ増加が確認できました。最後は時間とともに解決していく問題だとは思うのですが、一部のAPIがExperimentalであったり、日本語入力の扱いなどで怪しい挙動が何点か見つかりました。

課題

最後に、課題として残っている部分を挙げておきます。

コード規約

composableな関数は名前がPascalCaseであったり、通常の関数とは異なる扱いをしますが、named argumentをつけるか否かは書く人によってブレそうだな、と感じました。今回は保守的に「composable functionの呼び出しではnamed argumentを必須とする」としましたが、単一の引数しか持たない場合や、modifierは例外的にnamed argument無しを許容してもいいかもしれません。

// 悩ましい

Text("こうするか")
Text(text = "それともこうするか")

共通コンポーネントの設計

Composeでコードを書いていると、アプリ全体で共通して使いたいUIが必ず出てくると思うのですが、それらのAPIをどう書くべきか、記述の柔軟性と簡潔性のトレードオフをどうすべきか、ということに関しては上手く答えが出ませんでした。例えばComposeのButtonはSlot APIにより柔軟性の高い記述を実現しています。

@Composable
fun Button(
  ...
  content: @Composable RowScope.() -> Unit
)

// テキストボタンとして使う場合
Button(...) {
  Text("Button")
}

ライブラリの設計としては良い方針に見えますが、アプリ内では決まったパターンのボタンしか使わないので、ラウンジ開発では、AppButtonというアプリ内の共通ボタンを定義しました。

@Composable
fun AppButton(
  text: String,
  ...
) {
  Button(...) {
    Text(
      text = text,
    )
  }
}

// テキストボタンとして使う場合
AppButton("Button")

記述が簡潔になり、一見すると良さそうに見えますが、例外的なパターンというのはやはり出てくるものです。 大体は上記の方針で上手く行ったものの、「ある画面では例外的に太字にしたい」、「別の画面では文字数が多いので少し文字を小さくしたい」といったパターンが出てきてしまい、徐々に破綻してくる、ということがButtonの例に限らず何度かありました。このあたりはAtomic Designやデザインシステムの話とも絡めて、上手く構造化していく必要があるのかもしれません。

おしまい

というわけで、Composeを使用したラウンジ開発の振り返りでした。実際に動いている様子を見たい方はぜひ、COFFEE Appをダウンロードしてみて下さい! また、カンカクではラウンジ以外にも「Building the next Ordinary. 新しいライフスタイルを作る」大きなプロジェクトが既に走り出しており、一緒に作り上げていく仲間を募集しています。(Androidエンジニアが足りていません・・・!)雑談ベースで話がしたいけど応募はちょっと・・・という方はTwitter等でご気軽に連絡いただければと思います!

おまけ

Composeでの画像読み込みにはCoil Composeを使うことが多いと思うのですが、 COFFEE Appでは元々Coilを使用していたため、Compose採用時にも特に違和感なく導入することができました。 大元のコードベースを書いた、最近はPdMもやられている しほちゃんの先見性にこの場を借りて感謝します🙏

*1:厳密にはマルチプラットフォームも存在します https://www.jetbrains.com/ja-jp/lp/compose-mpp/

エンジニアからPdMになった件。

こんにちは、カンカクでカフェ事業のPdMをやっている しほちゃん @shihochan_jp です。
カンカクへエンジニアとして入社してから2年間はAndroidアプリ開発をメインに担当しました。去年の夏よりだんだんPdM業務をはじめて秋より本格的にPdMとして仕事をしています。

このブログではしほのカンカクでのこれまでこれからについてまとめて、リアル店舗開店などカンカクならではPdM業務について話したいと思います。

エンジニアとしてのこれまで

Androidエンジニアとしての仕事

2019年10月にカンカクにジョインしてから、自社経営カフェのオーダーアプリ COFFEE App の Androidアプリの開発を担当しました。今となってはトレンドと逆行するかもしれませんが、Flutter製のアプリをスクラッチでフルネイティブ化しました。
入社した日にリポジトリ作成からはじめて色々と攻めた技術やライブラリを利用しながらも、予定通り12月にリニューアルできました。

リリース後も、飲み放題の月額メンバーシッププランの開発やメニュー画面リニューアルなど大きな改修もこなしてきました。

PdMとしてのこれから

エンジニアからPdMになるにあたっての不安

自分がPdMになるにあたって社長やマネージャーと何度か1on1を実施し、率直な不安や期待値調整を丁寧に実施しました。最初から迷いなく進んだかと言うとそんなことはなくかなり不安がありました。

  1. コードを書けない不安
    分析のQuery以外のコードをほとんど書かなくなって3ヶ月が立ちましたが、振り返っても自分はコードを書くことが単純に好きだったんだなぁと感じています。仕事上のストレスのほとんどは対人関係によるものだと思っているので、自らのコードと向き合う環境を捨て、ストレスの渦の中へと突撃することには大きな不安がありました。

  2. 技術キャッチアップが遅れる不安
    現在リリースを控えているラウンジ機能は、Jetpack Composeを利用して開発されています( こちらのブログもぜひご覧ください )。後任のAndroidエンジニアのコードレビューも担当しているのですが、プロダクトのコードを書いていないので満足のいくキャッチアップができていないです。時間を見つけて手元で動かしてみてなんとかレビューしている形です(いつもレビューが遅れてすみません)。

  3. 今後のキャリアの不安
    今まで8年間、エンジニアとして仕事をしてきたキャリアを一度ゼロへリセットしてしまうのではないかという不安がありました。またエンジニアとして仕事がしたくなった際に錆びついた技術力が理由となり選択肢が狭まってしまうこともあるかもしれません。

なぜPdMにチャレンジすることにしたのか

ここまでたくさんの不安を吐露したわけですが、最終的にはPdMを引き受けることにしました。

  1. 技術の分かるPdMとしてのキャリア
    店舗側を担当する非エンジニアとの施策策定や意思決定の場においてCOFFEE Appをはじめ、システムの概要がわかっている人材が必要とされていました。今までの仕事の中でも課題感を感じることが多かったことなので、自分がPdMになることでエンジニア→PdMというキャリアの答え合わせをしたいと思いました。

  2. 一番店にいるエンジニア
    他の会社にはないカンカクならではの魅力として実店舗が存在することが挙げられます。自分たちが開発したプロダクトが店舗で実際に利用されているところを見ることが自分にとっての仕事のモチベーションにもなっています。自分は都内在住で自転車移動なこともあり、頻繁にお店に出向き積極的にコミュニケーションを取っています。現場でのコミュニケーションを通してスムーズにPdM業務へ移行できるイメージを持つことができました。

  3. 事業グロースのためにできることはなんでもするマン
    カンカクへは立ち上げフェーズから参画することができ、かなり古株社員になってきました。カンカクへ入社を決めた際にも自分は技術を極めることよりも新しい事業を創りたいという気持ちが強くあったことを覚えています。今一度、会社を見たときに自分ができること、自分が活きることとしてPdMの道があるのであればチャレンジしたいと思いました。

色々書きましたが、社長とマネージャーからの「チャレンジしてみてやっぱりエンジニアが良かったらまたエンジニアに戻ろう」との言葉と、今までの仕事や自分の強みを活かせる仕事であると思ったのが大きなところです。

カンカクのカフェ事業PdMのお仕事

カンカクではリアル店舗を経営しているので、通常のWeb事業のPdMでは経験しないような業務があります。多くの機器や工事もあるので意思決定後の戻しが致命的な問題になることもあります。ここではカンカクならではの仕事について簡単にお話したいと思います。

新店舗開店業務

カンカクでは去年1年で4つの新店舗を開店させました。プロダクトチームとして新店舗の開店を補助するタスクは一覧化して管理しており、開店を重ねる度にかなり洗練されてきました。開店が近づくと各タスクは毎日のStandup Meetingで進捗を確認する時間を設けています。

f:id:shihoochan:20220118220930p:plain

下北沢開店タスク

タスク内容としては

  • コーポレートサイトへ新店舗情報を追加
  • Slackへの売上通知Botの追加
  • オペレーションiPadのセットアップ
  • ネットワーク構築とWiFiセットアップ

など多岐に渡ります。担当は各メンバーに手を上げてもらうケースもあれば、属人化しないように複数人で対応するものもあります。中には特別な技術を必要としないタスクもありますが、情報セキュリティなどの観点からエンジニアで補助を行っています。PdMとしては、こちらの一覧の管理、ディレクション、セキュリティ意識向上のための周知・徹底などを行っています。

店舗との連携業務

実店舗では本当に様々なことが起こります。自社製の店舗コンソールやアプリなどWebの技術を多く利用して店舗運営をしているので、店舗スタッフには通常の飲食業よりは高いITリテラリーが求められます。店舗側とは定例を設定し定期的に情報共有を行う以外でも日々の業務を効率化するためにたくさんの仕組みを導入しています。

  • Slackを活用したオペレーション
    カンカクではアルバイトスタッフさんにもSlackに参加してもらっています。心理的安全性を確保しながら業務を効率化するためにルール整備や改善などを日々実施しています。

    • help-meチャンネル
      スタッフが日々の業務の中で分からないこと、困っていることがあればこちらのチャンネルに書いてもらいます。(自分がすぐ反応しています(๑•̀ㅂ•́)و✧)

      f:id:shihoochan:20220118214504p:plain

      help-meチャンネルの活用例

    • newsチャンネル
      調理オペレーションの変更やアプリの新機能リリースなどの情報が投稿されるチャンネルです。各店舗の店長と分担しながら情報共有とオペレーションの徹底を行っています。

  • 開発チーム発信のプロダクト・店舗業務改善
    前述の通り、自分は普段から積極的に店舗へ来店するようにしています。PdMとして店舗でのオペレーション課題のキャッチアップ、リリースした機能が意図通りに利用されているかの確認などを行っています。カンカクではエンジニアによる店舗での課題キャッチアップがきっかけとなった施策や機能がたくさんあります。こちらについてはまた別の機会にブログにできればと思います。

おわりに

今回は振り返りも含めてかなりエモめの投稿になってしまいました。プレスリリースで発表させていただきましたが、カフェ事業としては1月20日に初となるラウンジ業態の店舗を開店します。カフェ事業以外でも弊社のビジョンである「Building the next Ordinary. 新しいライフスタイルを作る」大きなプロジェクトが控えています。カンカクでは、一緒に新しいライフスタイルを作り上げていく仲間を募集しています。少しでも興味がある方は是非一度お話しましょう。

Jetpack Compose: 文字は何色か

こんにちは、カンカクでAndroidエンジニアをやっている @haru067です。
最近はJetpack Compose(以下、Compose)をゴリゴリに書いています。

ということで、今回はComposeの話をしたいと思います。
(執筆時のComposeのstableバージョンは1.0.5であり、これを想定して説明します。)

TextFieldとエラーラベル

Composeには文字入力を扱うComposableとして、TextFieldがあります。

@Composable
fun Example() {
    TextField(
        value = "value",
        onValueChange = {},
        label = {
            Text("Error label")
        },
        isError = true,
    )
}

f:id:kankak_inc:20211221122617p:plain

labelを設定すると、入力文字の上にラベルが表示されます。isError = trueを設定したときにはエラー表示となり、ラベルは赤色で表示されます。

ここでクイズ

次の2つのComposable、Quiz1Quiz2を実行したとき、エラーラベルの色はそれぞれ何色になるでしょう?

@Composable
fun Quiz1() {
    MaterialTheme(typography = Typography(body1 = TextStyle(Color.Green))) {
        TextField(
            value = "value",
            onValueChange = {},
            label = {
                Text("Error label1", color = Color.Blue, style = TextStyle(Color.Yellow))
            },
            isError = true,
        )
    }
}

@Composable
fun Quiz2() {
    MaterialTheme(typography = Typography(body1 = TextStyle(Color.Green))) {
        TextField(
            value = "value",
            onValueChange = {},
            label = {
                Text("Error label2")
            },
            isError = true,
        )
    }
}
1. Color.Green
2. Color.Blue
3. Color.Yellow
4. エラーの赤色
5. それ以外

正解

次のリンクを押して、答えを確認してみてください。

いかがでしたでしょうか? 両方とも正解できた方はおめでとうございます! このあとの文章を読む必要はなさそうなので、こちらをクリックしてください。

正解できなかった人も安心してください。このあとしっかり解説します。

解説: Quiz1

Quiz1はそこまで難しくありません。Textの実装を確認すると、Textは次のようなロジックで文字色を決定していることがわかります。

  1. 引数color: Colorが指定してある場合、colorを使用する
  2. 引数style: TextStylestyle.colorが指定してある場合、style.colorを使用する
  3. それ以外の場合はLocalContentColorを参照する(後述)

Quiz1Text("Error label1", color = Color.Blue, ...)となっており、colorが最優先された結果、エラーラベルはColor.Blueになります。

ということで、これは単純な優先順位の問題でした。厄介なのはQuiz2です。Quiz2では、Textに何も指定していません。単純に考えるとエラーの赤色が適用されそうですが、実際にはそうなりませんでした。謎ですね。

これを理解するには、CompositionLocalについて知る必要があります。

CompositionLocal

CompositionLocalは、Composableに対して暗黙的にデータ渡すための仕組みです。
・・・と言っても理解しづらいと思うので、まずは例を見てみましょう。

@Composable
fun CompositionLocalExample() {
    MaterialTheme {
        Column {
            // MaterialThemeがデフォルト値としてContentAlpha.highを設定
            Text("High alpha") 
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text("Medium alpha")
                CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
                    DescendantExample()
                }
            }
        }
    }
}

@Composable
fun DescendantExample() {
    // Composableを跨ぐ場合も適用される
    Text("Disabled alpha")
}

f:id:kankak_inc:20211221123138p:plain:w300

Textに何も指定していないにも関わらず、alpha値が変化しています。
Textは内部でLocalContentAlpha.currentという値を参照しており、CompositionLocalProviderを通じてLocalContentAlphaに値を設定することでalpha値が暗黙的に変化する、という仕組みになっています。

なんて危険な仕組みだ!と思った人は嗅覚が鋭いです。知らないうちに値が変わるということは便利であると同時に、問題が発生したときのデバッグを困難にします。実際、公式ドキュメントでも使うべき/使うべきでない用途については丁寧に解説されています。 developer.android.com

では逆に、これを使うべきタイミングはいつでしょうか?典型的な例はThemingです。先程の例ではMaterialThemeで囲った箇所のalpha値が変化していますが、MaterialThemeの実装を覗くと、CompositionLocalProviderを使用していることがわかります。

脱線: provides

LocalContentAlpha provides ContentAlpha.mediumという表記を見て「ん?」と思った方は多いのではないでしょうか。 provides中置記法で、ComposeというよりはKotlinの機能です。LocalContentAlpha provides ContentAlpha.mediumLocalContentAlpha.provides(Content.Alpha.medium)と実質的に同じです。

解説: Quiz2

さて、CompositionLocalについて学んだところで、Quiz2に戻りましょう。

@Composable
fun Quiz2() {
    MaterialTheme(typography = Typography(body1 = TextStyle(Color.Green))) {
        TextField(
            value = "value",
            onValueChange = {},
            label = {
                Text("Error label2")
            },
            isError = true,
        )
    }
}

Quiz2ではMaterialThemetypographyを指定していました。 実装を確認すると、MaterialThemeではtypography.body1に対して CompositionLocalProviderLocalTextStyleを設定していることがわかります。

ここで Textの文字色の決定ロジックを再確認しましょう。

  1. 引数color: Colorが指定してある場合、colorを使用する
  2. 引数style: TextStylestyle.colorが指定してある場合、style.colorを使用する
  3. それ以外の場合はLocalContentColorを参照する

ポイントは2です。styleのデフォルト値は LocalTextStyle.currentなのでQuiz2におけるTextstyleの値はMaterialTheme(...)でprovideされたスタイルになります。 この結果、style.colorMaterialTheme.typography.body1.colorということになり、Color.Greenが優先された、となるわけです。

エラー時の振る舞い

残る最後の疑問は「エラー時の赤色はどうやって実現しているのか?」です。

余白が少ないので細かい流れは追いませんが、TextFieldの実装を追っていくと、最終的にはここにたどり着きます。要するに、TextFieldlabelLocalContentColorをprovideしており、label = {...}内に記述されたText

3.それ以外の場合はLocalContentColorを参照する

の規則に則って赤色になるわけです。おしまい。

おわりに

カンカクでは、Jetpack ComposeやCompositionLocalに興味のあるAndroidエンジニアを募集しています。 気になる方は以下のリンクよりご気軽にご応募ください!

App Clipsの10MB制限を超えないためにEmergeを使ってみた話

この記事はiOS Advent Calendar 2021 18日目の投稿です。

こんにちは。カンカクのiOSエンジニアの@redryeryeです。この記事はApp Clipsのサイズ制限で悩んでいた時に導入した、アプリサイズ削減・モニタリングツールのEmergeについてご紹介します。

はじめに

カンカクが都内で展開している『KITASANDO COFFEE』『TAILORED CAFE』『WAKE』では、アプリをすぐにインストールすることが難しいお客さまでも、事前に注文してスムーズに商品を受け取っていただけるようにApp Clipsでのオーダーに対応しています

f:id:redryerye:20211218140531p:plain
TAILORED CAFE SHIBUYA
App Clipsとは、一言で言ってしまえばアプリをコンパクトにしたものです。アプリ本体の一部の機能のみをApp Clipとして提供するため、本体アプリとApp Clipsのサイズは異なり、本体アプリよりも小さくなります。通常のApp Soreからインストールする体験よりもすばやいインストール時間が求められるため、10MB以下というサイズの制限が存在します。

COFFEE Appの本体アプリはかなり軽量で、現行のバージョンだと16.5MBあり、ほとんど気にせず機能追加を行っていてもApp Clipは10MBに収まっていました。しかし、ある日Failしたビルドログを見に行くとそこには10.8MBに太ったApp Clipの姿がありました。

そこでサイズ削減について調べた結果、辿り着いたのがEmergeでした。

Emergeとは

EmergeとはスタートアップアクセラレータのY Combinatorの卒業生が作ったアプリのサイズ削減ツールで、元々はiOSのみでしたが最近Androidの対応も開始しました。

具体的な機能としては、既存のアセットやファイルの占有率が分かるインサイトを分かりやすく表示したり、CIに紐付けてアプリを定期的にアップロードすることでサイズの偏移をモニタリングして、SlackやGitHubに通知してくれたりします。

この記事の執筆時点では、直接サインアップすることは不可能で、emergetools.comからWaitlistに登録する必要があります。

使うまでの流れ

Waitlistに登録して数日後、Emergeのチームからミーティングのスケジュールに関するメールが届きました。(彼らのチームのベースはUSにあるため、日付を決めるときはタイムゾーンの違いに注意)

ミーティングではEmergeの説明に加えて、その場でアカウント登録して実際にアプリをアップロードして、サイズ削減が可能な箇所を丁寧に説明してくれました。 デモの後でも、困ったことはデモの時に作られたSlackのチャンネルを使って質問すれば答えてくれます。

使ってみての感想

不要なファイルを分かりやすく表してくれるので便利なのと、サイズを常にモニタリングすることで知らぬ間にアプリが肥大化するという事態が防げるので安心感があり助かっています。

不要なアセットの可視化が便利

一般的に、既存の機能を削らずにiOSアプリのサイズを削減するときに主に以下の二つの方法があります。

  • アセットを最小限にする
  • プロジェクトの設定を最適化する(ビルドの設定やライブラリのリンク方法など)

EmergeのInsightsを使えば、前者の「アセットを最小限にする」ことは容易に可能でした。

f:id:redryerye:20211216085548p:plain
Insightsでは最適化されていないアセットが可視化される (Emerge exampleより)

後者の「プロジェクトの設定を最適化してサイズを削減する」というのは、ミニマムにすればいいというシンプルな話ではないため、iOSの様々な知識が必要です。そこで、デモの時にEmergeからアセットの効率化以外の最適化方法についてアドバイスをくれたのですが、これが改めて発見した気づきもあり役立ちました。彼らのブログにもそのサイズ削減の方法についていくつか書かれています。

結果、COFFEE AppのApp Clipは10.8MBから10MBになりました。

f:id:redryerye:20211216084327p:plain
削減されたときの様子
10MBの制限ギリギリに着地させたのは、10MBを数百KB超えていてもApp Store ConnectでSubmit可能であることに加えて、この先にApp Clipの大幅な機能変更を予定していないためです。

サイズを削減した後のリバウンド対策にも

一度サイズを削減したアプリがリバウンドしないためにEmergeのモニタリング機能を重宝しています。

fastlane pluginを使用してEmergeにビルドをアップロードすることが出来るので、CIに紐づけて定期的にサイズを測ることで動向を監視しています。

f:id:redryerye:20211216091510p:plain
Slackに連携するとアップロードするごとにレポートが投稿される
f:id:redryerye:20211216125045p:plain
アプリサイズの動向も見られる

また、GitHubに連携するとPRごとに差分のインサイトをコメントしてくれるので、細かく監視したい場合は便利ですね。

おわりに

Emergeはサイズ削減とモニタリングを簡単に実現させてくれました。まだ新しいツールなので特に国内だと導入例が少ないですが、アプリサイズを管理したい場合はぜひ使ってみてください。

また、カンカクでは仲間を募集しています。新しい挑戦をしてみたい方、ぜひ一緒に開発しましょう。

Resources

Emerge Doing Basic Optimization to Reduce Your App’s Size - Apple Developer

カンカクのこれまでの2年半を簡単に振り返る

はじめまして、株式会社カンカク@k_kinukawa です。 今日からこのブログを通じて、カンカク開発メンバーが日々の業務で得た経験や技術を発信していきます。

今回は、2019年に創業したカンカクのこれまでの2年半を、開発という視点で簡単に振り返りたいと思います。

株式会社カンカクとは

「Building the next Ordinary. 新しいライフスタイルを作る」というミッションの下、現在はCAFE事業、GOOD EAT CLUB事業、EC事業の3つの事業に取り組んでいます。

COFFEE App

カンカクの運営するカフェで利用できるモバイルオーダーアプリです。 システム全体としてはアプリだけでなく、

  • 店舗で注文や販売商品を管理する「店舗コンソール」
  • システム管理画面「管理コンソール」
  • 店頭でお客様が注文状況を確認することができる「オーダーディスプレイ」

も含まれており、全て自社で開発しています。

f:id:k_kinukawa:20211208131745p:plainf:id:k_kinukawa:20211213090729j:plain
COFFEE Appとオーダーディスプレイ

2019年夏、KITASANDO COFFEE 開店直後に代表の松本とフリーランスの開発メンバーと共にミニマムな要件でリリースしました。アプリはFlutter、ユーザー認証はFirebase Authentication、DBはCloud Firestore、店舗コンソール、管理コンソールはPHPで開発をしていました。

リニューアル(2019年12月) ~ 現在

KITASANDO COFFEE 開店後、各領域に長けているエンジニアメンバーの入社が決まっていきました。初期プロトタイプの構成を維持する案もありましたが継続的な開発を見据え、2019年12月にフルコミットメンバーの得意な言語、フレームワーク、構成にリニューアルしました。

  • Ruby on Rails
  • React
  • Swift(iOS)
  • Kotlin(Android)
  • Heroku

アプリのUIもこのタイミングで一新し、体験が格段に向上しました。

リニューアル当初、インフラはHerokuを利用していましたがパフォーマンスとセキュリティの観点から2021年5月にHeroku→AWSに移行。RailsアプリケーションのコンテナをCircleCIでビルド、ECRへプッシュ、ECSへデプロイをしています。構成管理はTerraformを利用しています。

アプリ側では、iOS14のApp Clip対応、店頭のメニューをiPadで電子化など、リアル店舗ならではの開発も行いました。また、最近ではRailsに密結合だったフロントエンドを疎結合化したり、アプリとのインターフェースをRESTful APIからGraphQLへの移行を進めたりもしています。

年明けには初のラウンジ業態店舗をオープンする予定で、こちらの開発も進行中です。

GOOD EAT CLUB

GOOD EAT COMPANYが運営する食のマーケット&ファンクラブです。

goodeatclub.com

代表松本がCPOとして経営に参画しており、カンカクとしても2020年秋からECサイトの開発を担当しています。

2021年1月にリリースしたパブリックβ版ではプラットフォームにShopifyを利用しました。Multi Vendor MarketplaceというShopify app を利用することで、ショッピングモールとして運用することが可能になります。その他にもShopifyには便利なアプリが揃っており、Shopify + Shopify app を活用し短期間でECサービスの立ち上げを実現しました。

Shopifyは非常に便利なサービスですが、ショッピングモール形式のECサイトとしてより使いやすく、大規模にスケールさせていくためには多くの制約がありました。特にサイトデザイン、管理画面、配送会社等の外部システムとのAPI連携などはユーザビリティ、オペレーションの効率化の面で早急な改善が必要であったため、2021年7月にフルスクラッチの正式版へリニューアルを実施しました。

正式版では

  • Ruby on Rails
  • Vue.js
  • AWS

という構成で開発を行いました。spreeというマーケットプレイスの基本機能を提供してくれるGemを利用することで、約半年でリニューアルをすることができました。同時に配送会社とのAPI連携を行い、パブリックβ版では人力で配送会社のシステムにデータ入力をしていた作業が正式版では自動化をすることができました。デザインも大幅に変更し、使い勝手や商品を選ぶ楽しさなどが大きく改善されました。

EC

COFFEE App、GOOD EAT CLUB以外にも、カンカクでは複数のECブランドを立ち上げています。ECプラットフォームはShopify、ecforceを利用しており、サイトごとにデザインをカスタマイズしたり、好みの味を診断できる「コーヒー診断」のような機能のカスタマイズを行っています。

おわりに

初回なので簡単に振り返ってみました。今後、カンカクの開発に関する取り組みをこのブログを通じてより深く発信をしていきます。 また、来年に向けてラウンジの他にも「Building the next Ordinary. 新しいライフスタイルを作る」大きなプロジェクトが既に走り出しており、一緒に作り上げていく仲間を募集しています。少しでも興味がある方は是非一度お話しましょう。