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. 新しいライフスタイルを作る」大きなプロジェクトが既に走り出しており、一緒に作り上げていく仲間を募集しています。少しでも興味がある方は是非一度お話しましょう。