TECH PLAY

KINTOテクノロジーズ

KINTOテクノロジーズ の技術ブログ

969

こんにちは、人事グループ組織人事チームのHOKAです。 このBlogは 10X innovation culture program 内製化における苦悩と挑戦【前編】 の続きです。 8月:Google Cloud Next Tokyo'24に登壇して気づいたこと Google Cloud Next Tokyo'24とは、グーグル・クラウド・ジャパン合同会社が2024年8月1日(木)~2日(金)に開催した旗艦イベントで、ビジネスリーダー、イノベーター、エンジニアのためのクラウドカンファレンスです。生成AIやセキュリティをはじめとするビジネスに欠かせないテーマをもとに、基調講演やライブセッション、ハンズオンなどさまざまなプログラムが用意されていました。  詳細はこちら→ https://cloudonair.withgoogle.com/events/next-tokyo-24 KTCからは開発支援部 部長のきっしーとDBREグループ マネージャーのあわっちが「10X Innovation Culture Program 体験ワークショップ」に登壇しました。ワークショップ参加者の皆様に10X Innovation Culture Programを導入している事例をお話する機会をいただいたのです。 本番までには何度も打ち合わせをし、Google オフィスでリハーサルをし、ランチもご一緒させていただき、Googleの方々と接する時間が増えました。 そして迎えた本番。 パシフィコ横浜の広い会場における非日常感が「今日は学ぶぞ」という気持ちを醸成させてくれます。 初めに、Googleの方から10X についてご案内しました。なぜGoogleが10X innovation culture programを社内外で推進しているか、その背景や理由、結果を「生きた言葉」で話してくださり、会場にいたKTCのメンバーは「10XはやはりKTCにも必要だ!!!」と士気が上がりました。 続いて、きっしーとあわっちが登壇しました。 きっしーとあわっちからは下記3つについてお話しました。 カルチャー変革に取り組んだ背景、なぜ 10X Innovation Culture Program を実施することになったのか 10X Innovation Culture Program を実際にやってみて、実施する上でのポイント 今後の展開について 特に、10X Innovation Culture Program を実施しながら感じている 「Google だからできる」という思い込みを捨て、深く考えずにまずはやってみることが大事 ボトムアップから社内自立化の実現に向けた社内ファシリテーターの育成が重要 カルチャーは一足飛びではなく、何度も気づき、体験して浸透していくもの ということをKey Messageとして会場で伝えました。 質問タイムではたくさんの方がKTCに質問してくださり、「10Xは素晴らしい。しかし、導入するのは難しくないのか?」という疑問を投げかけてくださいました。まだ10X innovation culture programによる大きな効果は見えないものの、私たちの取り組みが少しでも参考になっていれば幸いです。 Google Cloud Next Tokyo’24に参加して、私は7月に自分たちで手作りした10X innovation culture programと圧倒的な違いを感じました。それは オフィスとは別の会場で参加者の聞く体制の準備が整う Googleの社員によって語られる「10X」は、とても心に響く ディスカッションメインの場と思っていたが、もっとインプットメインでも良いのかもしれない。 その上で、「10Xの考えを日々の業務に取り入れたい」と変わるのかもしれない。 ということでした。上記を仮説として、秋に開催する10X innovation culture programで検証していくことが定まりました。 9月:10Xファシリテーター研修を受けてみた まず、私たちは10Xの理解およびディスカッションをスムーズにさせるファシリテーターの育成から着手することにしました。私たちが立てた仮説 Googleの社員によって語られる「10X」は、すごく浸透する ディスカッションメインの場と思っていたが、もっとインプットメイン を紐解くと、10Xを運営する側が「10Xとは何か」のインプットが少ないから、相手に伝わらないと思ったからです。 思い切って、Googleの方に相談したところ、ファシリテーター研修を実施していただける運びとなりました。 ファシリテーター研修を実施するにあたり、メンバー選出で悩みましたが、そもそも10X innovation culture programはリーダーシッププログラムなので、マネージャーに受講いただくことにしました。 ファシリテーター研修当日 今回の研修のメイン担当であるGoogleのKotaさんから、研修の冒頭で 「めちゃくちゃスパルタです。1回言ったことを覚えて、話せるようにする研修です。 Googleで大切にされている言葉 Steal with pride(プライドを持って盗もう) Feedback is a gift!(フィードバックは贈り物) を体感する構成となっております。 Googleの社員よりも10Xについて濃密な時間を過ごします。」 というメッセージをいただきました。 一体、何が始まるのか?という空気感で、研修は幕を開けました。 【研修前半】インプット Kotaさのメッセージにあった通り、初めにGoogleの方から10Xの6つの要素をプレゼンテーションし、KTC社員が理解する「インプットの時間」がありました。 例えば、ひとつ目の要素「DEI」について。 Googleでは Unconscious Bias @ Work という無意識の偏見に目を向ける研修を社員の50%が受講済。 週に一度の意識調査: Googlegeist を行い、ダイバーシティの受け入れや公平性に関する成果を測定している。 といった具体的な施策と、それによる効果についてお話いただきました。 同様に、10Xの6つの要素ひとつひとつに、Googleにおける具体的事例と、プレゼンターご自身が社員として感じていることやエピソードを交えて話してくださいました。 【研修後半】アウトプット Googleの方のプレゼンテーションを聞いた後、KTCのメンバーは6人1グループに分かれました。 そして、1人1要素を暗記し、自分のエピソードを交えてプレゼンテーションをしました。 プレゼンテーションを聞く人は聞いているだけではなく、1人ずつプレゼンテーションの良かったところや改善点をフィードバックしていきました。Googleの方からもフィードバックをいただきました。 このプロセスは、冒頭にKotaさんが仰ったとおり「スパルタ」かもしれませんが、Googleのやり方を「Steal with pride」し、「Feedback is a gift」する時間でした。  また、フィードバックの内容も 「もっと全社でビジョン共有した方が良いね」 「目標設定、どうなってんでしたっけ?」 「もっと事例を言えるようになりたい。経営者との接点を増やそう!」 など、どんどんアイデアが生まれて来ました。 10Xをインプットしたことで、「KTCの今」と照らし合わせることができるようになった瞬間に立ち会うことができました。 実際に、ファシリテーター研修を受けたKTCのメンバーから「今まで10Xを素晴らしいものとは思いつつも、どこか他人事だったが、研修を通して短時間で暗記し、自分のエピソードと絡め、他者にプレゼンテーションすることで一気に自分のものにすることが出来ました」というコメントが多数寄せられました。 また、その場でGoogleの方からフィードバックをもらえたのも貴重な機会だったと思います。 10月:3回目の10X innovation culture programに向けて準備中 ファシリテーター研修を終え、3回目の10X innovation culture program準備中(10月)にこのBlogを書きました。 3回目の10X innovation culture programは2日程に分かれます。新しいやり方です。 また、参加者のうち初めて参加するメンバーは47%を占めています。 前回(7月)より前のめりで参加してもらえるか? 9月のファシリテーター研修の成果は出るか? また改めてTechBlogに投稿いたします。 10X innovation culture program 内製化における苦悩と挑戦【後編】に続く ※2025年1月公開予定
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の22日目の記事です🎅🎄 はじめまして! KINTOテクノロジーズでUnlimited(Android)アプリを開発しているkikumidoと申します。 弊アプリでは、 GraphQL クライアントとして Apollo Kotlin を使用しています。 Apollo Kotlin v4では、パフォーマンスの向上や新機能の追加など、多くの改善が行われています。 これらの利点を活かすため、私たちはv3からv4へのアップグレードを決定しました。 本記事では、今年の7月にリリースされたApollo Kotlin v4への移行作業の流れと、その過程で遭遇した課題について詳しく説明します。 当初はスムーズな移行を期待していましたが、予想外の例外に直面し、解決策を見出すのに苦労する場面もありました。 この記事が、これからバージョンアップを検討している方々にとって、有益な情報源となれば幸いです。 v3からv4への移行 公式サイト に従い対応していきます。 尚、 Android Studioにプラグインをインストールすることで、半自動でバージョンアップすることも出来ます。 必要な対応を確認しながら移行作業を実施したかったので、私はプラグインを使わずに愚直に手動で対応しました。 一方、プラグインでどこまで自動でできるのかにも興味があったので、手動の場合との作業方法の比較を後半に記載します。 1.対応必須箇所 v3からv4への移行で、弊アプリでAndroid Studio上でエラーを無くす為に必須だった対応は5つあります。 以下に対応内容をひとつずつ記載していきます。 :::message alert アプリの実装状況により対応内容が異なる可能性は大いにあります あくまでも弊アプリの対応内容であることはご留意ください ::: 1.1.ライブラリのバージョンアップ 何はともあれ、まずは ライブラリ のバージョンアップをします。 apollographql = "3.8.5" apollo-runtime = { module = "com.apollographql.apollo3:apollo-runtime", version.ref = "apollographql" } apollographql-apollo = { id = "com.apollographql.apollo3", version.ref = "apollographql" } ↓ // 記事執筆時はv4.1.0がリリースされていますが、移行当時の最新である4.0.1で記載しています apollographql = "4.0.1" apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollographql" } apollographql-apollo = { id = "com.apollographql.apollo", version.ref = "apollographql" } ふむふむ。 com.apollographql.apollo3 が com.apollographql.apollo4 になったのではなく、「3」が消えましたね。 1.2.gradleファイルの修正 v3の時は不要でしたが、v4ではserviceで囲う必要があります。 apollo { packageName.set("com.example.xxx.xxx.xxx") ・・・ } ↓ apollo { service("service") { packageName.set("com.example.xxx.xxx.xxx") ・・・ } } 1.3.importの変更 先述の「3」が消えた時に予想できましたが、importのパッケージ名を変更する必要があります。 「3」を消すだけです。 import com.apollographql.apollo3.* ↓ import com.apollographql.apollo.* 1.4.例外処理を修正 execute() でフェッチエラーがスローされないように変更になりましたので対応します。 プロジェクトごとに対応方針が異なると思いますが、私たちは一旦既存処理への影響を最小限に抑える対応を実施することにしました。 v3の時にフェッチエラーが発生し ApolloException をスローしていたのと同じ条件の時に、 DefaultApolloException をスローするように修正する方法です。 共通処理を修正することにより、共通処理を呼び出している側では実装を修正することなく、既存と同じ挙動にできました。 apolloClient.query(query).execute() apolloClient.mutation(mutation).execute() ↓ execute(apolloClient.query(query)) execute(apolloClient.mutation(mutation)) private suspend inline fun <D : Operation.Data> execute(apolloCall: ApolloCall<D>): ApolloResponse<D> { val response = apolloCall.execute() if (response.data == null) { response.exception?.let { // response.dataがnullかつresponse.exceptionがnullでない場合はフェッチエラー throw DefaultApolloException(it.message, it.cause) } } return response } 既存実装やv4対応の方針により一気に移行することが難しい場合は、v3の挙動を変えずに移行する為のヘルパーとして executeV3() が準備されていますので、そちらに一旦置き換えることも可能です。 apolloClient.query(query).executeV3() apolloClient.mutation(mutation).executeV3() 機能毎など、徐々にv4への移行を進めたい場合はこちらを使うとよさそうです。 :::message alert executeV3() は非推奨メソッドなのでいずれは対応が必要になるはずですのでご注意ください ::: 1.5.ApolloExceptionをDefaultApolloExceptionに修正 ApolloException が sealed class になったため、インスタンスを生成していた箇所を DefaultApolloException に置き換えます。 Android Studio上でエラーを無くすために必要な対応は以上となります。 2.ビルド エラーも無くなったので・・・・ それでは、待ちに待ったビルドを実行してみましょう! ダララララララララララララララララララララララララララララララララ(ドラムロール)・・・ジャン! はい!出ました! ビルドエラー・・・! ・・・まあ、そんなにすんなり行くとは思っていませんでしたが・・・地味にショック・・・ さて、気を取り直してエラーログを確認していきます。 2.1.エラーログを確認 見慣れないエラーログが大量に吐かれていました。 要するに「KSP[^1]でAssistedInjectProcessingStep[^2]の時に必要なClassがないよ」ということらしい。 [^1]:弊アプリでは、 KSP を使用しています [^2]:弊アプリでは、依存関係インジェクションライブラリとして Hilt を使用しています If type 'error.NonExistentClass' is a generated type, check above for compilation errors that may have prevented the type from being generated. Otherwise, ensure that type 'error.NonExistentClass' is on your classpath. e: [ksp] AssistedInjectProcessingStep was unable to process 'XXXXXViewModel(java.lang.String,long,com.xx.xx.XXXXXRepository)' because 'error.NonExistentClass' could not be resolved. もちろんソース上は存在するクラスですし、本対応前は問題なくビルドが成功していました。 v4移行で必要な対応が漏れていないか?など色々調べましたがなかなかわからず・・・ 色々調査した結果、下記3種類の方法でビルドを成功させることができました。 A. Hilt を KSP から kapt に戻す B. build.gradle.ktsに追記する:パターン1 androidComponents { onVariants(selector().all()) { variant -> afterEvaluate { val variantName = variant.name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } val generateTask = project.tasks.findByName("generateServiceApolloSources") val kspTask = project.tasks.findByName("ksp${variantName}Kotlin") as? org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompileTool<*> kspTask?.run { generateTask?.let { setSource(it.outputs) } } } } } C. build.gradle.ktsに追記する:パターン2 apollo { service("service") { packageName.set("com.example.xxx.xxx.xxx") // 追記start outputDirConnection { connectToAndroidSourceSet("main") } // 追記end ・・・ } } ちなみに Hilt の AssistedInject を使用している(定義方法によっては問題ない場合もあるようです) Hilt に KSP を使用している 先述の「1.2.gradleファイルの修正」で service で囲う 上記の条件が揃っていればv3でも再現しますので厳密には「v4対応」とは言えませんが、同時に対応したので記載しておきます。 弊アプリでは最初「B」で対応していましたが、ApolloExtensionの service の不具合[^3]で「C」で対応できることがわかった為、「C」を採用しました。 [^3]: Issues → 記事執筆時最新のv4.1.0でも再現しました 以上でビルドが成功し、アプリが起動・既存と同じ挙動をするようv4へ移行することが出来ました! 3.非推奨箇所の対応 続いて、非推奨でワーニングとなった以下の2つに対応します。 3.1.ApolloResponse.Builderを修正 @Deprecated("Use 2 params constructor instead", ReplaceWith("Builder(operation = operation, requestUuid = requestUuid).data(data = data)")) とのことなので、その通りに修正します。 ApolloResponse.Builder(operation(), UUID_CONST, data).build() ↓ ApolloResponse.Builder<D>(operation(), UUID_CONST).data(data).build() data が外に出ましたね。 Builderパターンに則った形式に変更されたようです。 3.2.Errorインスタンス生成処理をBuilderに修正 Errorインスタンス生成でコンストラクタを使用していた箇所を、Builderを使用するように修正します。 Error("occurred Error", null, null, mapOf("errorCode" to responseCode), null) ↓ Error.Builder(message = "occurred Error") .putExtension("errorCode", responseCode) .build() 以上でワーニングを消すことも出来ました。 4.プラグインでの移行 Android StudioでApolloのプラグインを使用することによりある程度自動で移行してくれます。 実行方法はとても簡単で、 Android StudioでApolloのプラグインをインストールし Tools > Apollo > Migrate to Apollo Kotlin 4... をタップするだけです。簡単ですね。 ![Migrate to Apollo Kotlin 4](/assets/blog/authors/kikumido/plugin.png =500x) では実行結果を確認します。 先述の 1.1.ライブラリのバージョンアップ 1.2.gradleファイルの修正 1.3.importの変更 は自動で移行してくれました。 1.4.例外処理を修正 は executeV3() に置き換えることしか行われないので、v4として適切な対応は手動で行う必要があります。 1.5.ApolloExceptionをDefaultApolloExceptionに修正 は移行してくれませんでしたので、対応は手動で行う必要があります。 文字通り、機械的にできる箇所は自動で移行してくれるので、 プラグインを使用して移行した上で、必要な箇所のみ手動で対応する方法もありだと思います。 ちなみに このプラグインは未使用フィールドをグレー表示してくれたり、移行時以外も活躍してくれるので、インストールしておくと良いと思います。 5.まとめ Apollo Kotlinをv3からv4へ移行した時の話は以上になります。 実際に対応した時から時間が経過してしまったので「すでに対応済み」という方も多くいらっしゃると思いますが、何かしらお役に立つものがあると嬉しいです。 最後までお読みいただきありがとうございました。 6. 関連リンク https://www.apollographql.com/docs/kotlin/migration/4.0 https://www.apollographql.com/docs/kotlin/testing/android-studio-plugin https://developer.android.com/build/migrate-to-ksp?hl=ja
アバター
こんにちは、人事グループ組織人事チームのHOKAです。 本日は、 2024年3月に10X innovation culture programを受けた その後のお話をいたします。 はじめに KINTOテクノロジーズ(以下、KTC)では、イノベーションを生み出す組織環境づくりに挑戦しています。現在はまだその「過程」であり、試行錯誤をしていますが、これから 10X innovation culture program を導入しようと考えている方や、何かイノベーションを起こす組織を創りたいと考えている方に読んでもらえたら幸いです。 7月:自分たちで10X innovation culture programやってみた 3月にGoogleオフィスで10X innovation culture programを受ける前から、決意していたことがあります。 それは、10X innovation culture programを自社で実施していくということ。 「3か月ごとに実施しよう。アセスメント結果を見よう。」 「2回目からは自分たちで10X を実施しよう。」と誓った2024年3月の私たち。 夏の開催に向け、再びあわっちとHOKAは動き始めました。 まず最初に行ったのが、ファシリテーターを決めること。 今回はGoogleの方はいないので、自分たちで担当しなくてはなりません。 Slackで声をかけたところ、4人のメンバーがファシリテーターに立候補してくれました。 10X innovation culture programは、大きく分けて下記3つのコンテンツを実施します。そして、参加者が自分だったらどうするか?KTCだったらどれが良いかを考え、実行していきます。 【1】10Xの動画を視聴し、10Xとは何かを理解する「事前MTG」 【2】アセスメント結果から「現在地」を知る 【3】10Xの要素をテーマにディスカッションする  これらを実施するだけです。 それなのに、すごく難しかった!!のです。 難しかったポイント 【時間配分】当日のプログラムは2時間必要なのか、3時間必要なのかさえ分からない 【コンテンツ】初めて参加する人と2回目に参加する人、2パターンいるがどうやって進めたら良いのか 【品質】「10Xとは」を語るシーンが重要なのだが、いざKTCメンバーが話すと、ただ読み上げているだけになってしまう   等々、基本の「き」から手探り状態でした。あわっちとファシリテーターの4人と一緒に、3月の研修をふまえて「たぶん、こうだよね」と話し合いながら何とか実施したというのが本音です。 コンテンツごとに振り返り 【1】動画を視聴し10Xとは何かを理解する「事前MTG」 前回と同様にzoomで実施。ファシリテーターが「10Xとは」を説明し、その後の10Xの動画を再生。最後にアセスメント回答をしました。 なぜか3月ほど盛り上がっていない。 もしかしたら、「なぜ10XをKTCが学ぶのか」が足りないのかも? と運営側では感じていました。 【2】アセスメントの結果から現在地を知る そして、2024年7月2日、10X innovation culture programを室町オフィスで開催しました。 Osaka Tech Lab.(大阪オフィス)や、名古屋からも参加し、総勢39名。初めて参加した人も、2回目に参加する人もいました。 事前にみんなが回答したアセスメントの結果を見て、前回よりも上がった/下がったを報告したが、なんだか反応が薄い。 前回(3月)から今回(7月)まで何かに取り組んだ訳ではないから、数値に思い入れが持てないのかも? アセスメント結果を共有されているだけでは、まだ良い・悪いは判断できないのかも? と運営側では仮説を立てました。 【3】10Xの要素をテーマにディスカッションする 続いて、「この3か月間の振り返り」をディスカッションしました。 全く覚えていない人がいたり、初めて参加した人は「動画を見ただけだから、何をしたら良いか分からない」という人も。前回、Googleオフィスで行った時には、スムーズにディスカッションが進んでいただけに、運営側としては想定外の事態でした。(今、思えば当たり前なんですけど) その後、10Xの要素の1つ「自主性」についてディスカッションを実施。前回と同様にグループに分かれて課題から書き出し、解決策をディスカッションしていただきました。 3か月間、特に10Xを意識した行動してない人がいる なぜか前回よりもディスカッションしづらい雰囲気がある という発見がありました。 実施後アンケート 前回 に比べ、3や2の数が増え、1が付いた項目もありました。 推進のモチベーションも下がっています。 また、アンケートコメントにおいても、参加者の方から以下のアドバイスをいただきました。 全体の感想 運営お疲れ様でした!楽しい会になりました。 今のところ特になし ぜひ他の組織でも行ってほしいです。他の人の考えが知れるだけでもとてもいい活動 前回の内容に関するフィードバック 前回の内容を簡単に説明して欲しかった。前回参加できていないかたが置いてきぼりの時間があった。 前回の課題の記憶取り戻しに時間を要したので、事前に振り返りの時間を作れたらと思った。 前回のほうがディスカッションが盛り上がった気がした、場を積極的に回す人がいなかったし(自発性の欠如)事前準備をきちんとしておらず、入り込むまでに時間がかかった。 時間・スケジュールに関するフィードバック 時間的には短かったよね。もっとグループワークしてディスカッションする時間があったほうがよかったかも。 タイムスケジュールがタイトすぎる(大体その通りいかないので余裕持たないと。分単位は危険すぎる) 確保時間の問題かな?(目的はこれだったと思うので、流れ的にネクストアクションとまではいっていなかった気がする) グループディスカッションに関するフィードバック グループディスカッションのメンバー選定について、チームを横断したメンバーではなく、実際の業務で組んでいるチームメンバーでやっても良いかなと 前回、今回のように他チームのメンバーと協議することは刺激的であり、新鮮です。ですが業務に即した結果が出にくいということも感じています。そこで同じチームメンバー同士で改めて10Xについて会話することも良いかなと思いました。 ワークショップの進行・運営に関するフィードバック アイスブレイク(自己紹介)などの時間が少なすぎ。ワークショップを始めるにあたって、知らない人同士の環境を緩めるには非常に重要。自己紹介も終わらなかった。 ワークショップの質問が抽象的で回答の出し合いに困っていた おそらく、このワークショップのゴール設定がなされていなかった可能性がある(運営側が) 参加する方も今回これを持ち帰ってねという握りをはじめにしておいた方がいいとおもう 内容の理解・フォローアップに関するフィードバック 初参加でしたが内容としては既習者向けの感があり、全体としてぼんやりとした内容という印象を受けました。 内容を絞ってコンパクトにするか、尺を拡大してしっかりした説明を加えたほうが良いのではと感じました。 その他 配布された紙(回答コメント)がなんのためか分からない デスク毎にリーダーを決めてもらうことをはじめにした方がよさそう 自発性とチーム内の話し合い また前回を振り返った時にアクションが少なかったので、定期的にチームでも話し合いの時間をもうけたい 内発的動機に興味あります。自立性と組み合わせれるとより一層のスピード感を持てると思います。 さらに、10X innovation culture program実施後にファシリテーターメンバーで集まって、振り返りを実施。アイデアがたくさん集まりました。 さて、どこから手をつけるべきか? そんな中、Googleの方から Google Cloud Next Tokyo'24 で登壇する機会をいただきました。 <<中編へ続く>>
アバター
This is the Day-2 post of the KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Hello! I am Hasegawa , an Android engineer at KTC! This article highlights common mistakes developers often make when working with the Application class in Android development and offers solutions to prevent them. What is the Application Class? The Application class in Android can be described using the official documentation as follows: "Base class for maintaining global application state. It is instantiated before any other class for your application/package is created." This means that it is responsible for maintaining a global state and is instantiated before any other class in the application. While implementation may vary depending on the project, the Application class is generally used for tasks such as initializing libraries used throughout the app or configuring Dependency Injection (DI). class MyApplication: Application() { override fun onCreate() { super.onCreate() // Library initialization // DI setup } } What would happen if you made an API call to fetch data from the server during the application startup? class MyApplication: Application() { override fun onCreate() { super.onCreate() // Library initialization // DI setup // API call } } At least in my experience, I’ve come across code like this more than once. Although it may not cause immediate issues, it could lead to problems down the line. In this article, I’ll discuss scenarios where this code might lead to issues. The Connection Between the Four Android App Components and the Application Class To understand why making API calls in the Application class can be problematic, it’s essential to have a good grasp of Android’s 4 app components . The diagram below shows these four components and the typical features associated with each. Activities are primarily responsible for managing the app's user interface and navigation between screens, making them the most commonly used component. Apps that provide notification functionality often rely on Services. Apps with widget functionality typically use Broadcast Receivers. Content Providers may be less commonly used, but they are useful for sharing your app’s data with other apps. It's important to note that the Application class is instantiated whenever any of these components are active. Notably, components other than Activities can run even if the user hasn't explicitly opened the app. For instance, in apps with widgets, the Application class may be instantiated when the device restarts, triggering the widget to be recreated. At this point, if the Application class includes API calls, they might be triggered even when the user hasn’t opened the app, potentially occurring at times unintended by the developer. In apps with notification functionality, the Application class might be instantiated when a push notification is received If push notifications are sent to multiple users simultaneously, API calls could be triggered at the same time, potentially causing a situation similar to a DDoS attack. This issue might only surface later, such as when the user base expands. Hence, it’s crucial to recognize this risk and include it in your knowledge base. What If You Want to Make an API Call at Startup? There are many possible solutions, so there is no single “correct” answer. However, here’s one example: Data should be fetched only when and where it is necessary, and in the precise amount needed. Hence, it’s best to retrieve data within the context of each of the four components. If the fetched data needs to be reused across components, you can persist it locally, store it in the Application class, or use dependency injection (DI) to store it in a class with a Singleton lifecycle scope. Conclusion Thank you for taking the time to read this article. Today we explored the Application class lifecycle, highlighting key implementation considerations to keep in mind. Specifically, I examined the use of API calls in the Application class as an example. Another frequent issue developers encounter is sending events —such as app startup events—, directly from the Application class. As mentioned earlier, the instantiation of the Application class does not always align with the moment a user explicitly launches the app. If the event is triggered by the user launching the app, it should be managed within the Activity. Even in apps with multiple activities, it's crucial to identify the correct entry point for the app. I hope this article proves helpful to your development journey. *The Android robot is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License.
アバター
はじめに KINTOテクノロジーズ株式会社(略して、KTC)でプロダクトマネージャーをやっている、super_spa3(中の人は、ナカノヒロフミ)です。 今回は、KTC初の海外カンファレンスとして、WWDC24に参加してきましたので、参加からしばらく経ってしまいましたが、その間のアップデートも含めてテックブログにまとめていこうと思います。 あれ?iOSとどういう関わりが?と思いますよね。 実は、ナカノ、社会人キャリアをスタートしたときにiOS開発にアサインされ(それまでは趣味でAndroidをかじってました)、2社目のKTCにはiOSエンジニアで入社、とあるきっかけがあって、現在はプロダクトマネージャーをしています。 仕事だと、アプリのリリースでApp Store Connectをよく使ったり、個人でもApple Developer Programに登録して開発をしています。 そんなナカノが関わっているアプリは、KINTO かんたん申し込みアプリという新車のサブスクKINTOをアプリでサクサク見積りから審査までができるアプリです。 https://kinto-jp.com/entry_app/ WWDC24参加のきっかけ WWDCへの参加は毎回抽選という形で3月あたりにAppleからApple Developer Programに登録しているメンバーにお知らせメールが送られてきます。 そのお知らせ先のリンクから応募を押して、抽選で当たったメンバーが現地のイベントに参加できるというわけです。 ただ、ナカノも前職のときに何度か応募をしてみましたが、なかなか当たらず、今回もそうだろうなと思い、あまり期待せず応募を押しました。 抽選結果当日、メールの受信箱を開くと、"Great News, Hirofumi!"という内容のメールがあり、内容を読んでみると抽選に当たった趣旨のメールでした。 ![](/assets/blog/authors/hirofumi.nakano/WWDC24/WWDC24_Accepted.png =750x) 間違いなく何かいい知らせが伝わる当選メール 所属するモバイルアプリ開発グループ内で当選したのはナカノだけだったらしく、小寺社長に打診したところ、無事、承認され参加できることが決まりました! 前夜祭 at Apple Infinite Loop Campus メインイベント前日にAppleの旧社屋であるApple Infinite Loop One Campusでグッズの受け取りとネームバッジをもらいました。 ![](/assets/blog/authors/hirofumi.nakano/WWDC24/Apple_Infinite_Loop_Entrance.jpg =750x) Infinite Loop Oneのエントランス、すごくミニマリスティックでおしゃれ ![](/assets/blog/authors/hirofumi.nakano/WWDC24/WWDC24_Badge.jpg =750x) 期間中のネームバッジ、TOYOTA FINANCIAL SERVICE CORPORATIONでアプリを出しています 毎年SWAGの内容が違うようで、今年はレジャーシートが入っていました。コロナが明けて自由に移動ができるようになったのでピクニック用によさそうです。 青空コーディングとか、どうでしょう。 ![](/assets/blog/authors/hirofumi.nakano/WWDC24/WWDC24_Swag.jpg =750x) WWDC24のSwagとApple Parkでもらえた追加のピン ![](/assets/blog/authors/hirofumi.nakano/WWDC24/WWDC24_Pins.jpg =750x) Apple Vision Proのバッジがかっこいい! 会場に入って、みんなWWDC24のモニュメントの前で記念撮影、中庭でのイベントだったため、ちょっとTBSのアナザースカイみたいなポーズをしてみました。 ![](/assets/blog/authors/hirofumi.nakano/WWDC24/WWDC24_Board.jpg =750x) 立体的なWWDC24のボード、この後ここで写真を撮りました ![](/assets/blog/authors/hirofumi.nakano/WWDC24/Apple_Infinite_Loop_Garden.jpg =750x) ここが私のアナザースカイ、Apple Infinite Loop Campusです(笑) 食べ物と飲み物を持って、会場に来ているAppleエコシステム内で開発している人たちと交流しました。 今回一人で行ったため、話し相手を探すために、空いている席があったら「相席いいですか?」と聞いて、お互いどういうものを開発しているのか話しながら交流を深めました。 メインイベント at Apple Park メインイベントの朝は早いです。朝7時にチェックインだったのですが、早めに乗り込みに行くという人たちを事前に聞いていたので、自分も予定より結構早く7時前にApple Park横のApple Park Visitor Center(Apple Storeにカフェ、ジオラマ、テラスが併設されている)で整列をしました。 日本人は朝早いのが得意なのか、先頭には結構な人数の日本人のグループが今か今かと待ちわびていました。8時入場近くになって、WWDCコールが発生し、みんなテンション高く「ダーブダーブ、ディーシー!」と叫んでました。(もちろん、しっかりテンション高く叫びました) 8時になってゲートオープン、Keynoteの会場まで行き、前から6列目の席を確保することが出来ました。 ![](/assets/blog/authors/hirofumi.nakano/WWDC24/WWDC24_Keynote_Stage.jpg =750x) めっちゃ近い! ここからは、10時までの間は自由時間のため、朝食とApple Parkの散策、日本で深夜遅くにKeynoteを見るために準備している東京のメンバーとZoomをしました。そのときのツイートがこちら https://twitter.com/ioco95/status/1800239213851574355 ステージ右側の席だったため、裏手にTim Cook、Craig Federighiがスタンバイするところを見られました。今年のCraigはめっちゃアクロバティックに動いていましたね。 今回のKeynoteではApple Intelligenceが大きな目玉となりましたが、個人的には、Math Notesによる書いた数式を計算してくれる機能や、Apple Vision ProのvisionOS 2からMac仮想ディスプレイがウルトラワイドモニターのように広くなるところがワクワクしました。(まだ、Apple Vision Proは持っていませんが) お昼もCaffe Macsの両サイドにいろんな地域のご飯が選べて、何回も並んでいろんなランチを食べた人もいました。 午後は、Platform State of the UnionをApple Parkの中で見ました。 In-person labsでの出来事と散策 そして、いよいよ、In-person labsの時間です。この時間はエリア分けされたところにAppleのエンジニアやスタッフが質問や課題を見てくれる時間です。自分は、まず事前にDesign Labsの予約を入れていたので、3階のエリアに上がりました。 ![会場内にエリア分けされたIn-person labs](/assets/blog/authors/hirofumi.nakano/WWDC24/WWDC24_Labs_Session.jpg =750x) Design Labsでは、自分が担当しているKINTO かんたん申し込みアプリについて、Apple社員の率直なフィードバックとデザイナーさんが課題に思う部分を聞くことができました。 なかなかApple社員の方からフィードバックをもらうということができないので、この機会はとても貴重でした。 1つ、Design Labsでの教訓として、もし、アプリが日本限定公開になっている場合は、TestFlightを使うなどして日本国外からでもアクセスができるようにしましょう。自分は事前予約フォーム内にAppStoreのURLを貼って応募したのですが、 Appleの中の人だから大丈夫だろうと思っていたら、日本限定になっていたのでアプリを事前に確認できなかった、といわれてしまったため、限りある時間の中でアプリを説明して課題と質問について回答してもらいました。(みなさんもお気をつけください) Design Labsが終わったら、社内のエンジニアからもらった質問の答えをもらいにいくつかのエリアを渡り歩き、合間にApple Park内(中庭とビル内)を散策しました。 ![Apple Parkの中庭を背にイエイ](/assets/blog/authors/hirofumi.nakano/WWDC24/Apple_Park_with_Rainbow.jpg =750x) 写真を撮りたそうにしている人がたくさんいるので、「写真撮りましょうか?」と声かけて、そのままお友達になりましょう OMT Conf参加 実は、WWDCの現地イベントは、日、月、火の3日間しかないんです。セッションは月曜日から金曜日にかけてですが、現地ではそうではありません。 そこで、Apple ParkがあるCupertino近くのホテルで開催された、One More Thing Conference(略して、OMTConf)にも火曜日から金曜日参加してきました。 ![](/assets/blog/authors/hirofumi.nakano/WWDC24/OMTConf_Entrance.jpg =750x) よく聞くスポンサーさんがずらり ![](/assets/blog/authors/hirofumi.nakano/WWDC24/OMTConf_Badge.jpg =750x) ここでもバッジゲット。Pro Ticketと書いてあるが、実は無料のチケット OMTConfは、スピーカーセッション(Main Room)と各エキスパートにテーマ別に相談ができるセッション(Big TentとSmall Tent)に分かれた構成になっていました。 気になるテーマのスピーカーを聞いたり、相談したいテーマがある場合にテントの方に言って喋ったりしていました。 その他、木曜日の午後には、Paul Hudsonさんのワークショップがあり、元々はSwiftDataに関するワークショップを企画していたそうなのですが、WWDC24ではそこまでアップデートがなかったから急遽テーマを変えて、What's New in SwiftUIというテーマで3時間ほどワークショップを実施しました。 いつも、Hacking with Swiftをよく見るのですが、初めてのPaulさんのワークショップに参加して思ったのが、めっちゃ教えるのがうまいのと、質問に対する回答もめっちゃ速かったです。 Swift Social 2024とCore Coffeeに参加 WWDCやカンファレンスに参加する以外にコミュニティイベントにも2つほど参加しました。 1つは、Swift Social 2024。このイベントはSwiftコミュニティが主催したイベントで、サンノゼ市内のバーを貸し切って、誕生10周年のSwiftをお祝いしました。 ![](/assets/blog/authors/hirofumi.nakano/WWDC24/Swift_Social_Board.JPG =750x) 祝10周年! ![](/assets/blog/authors/hirofumi.nakano/WWDC24/Swift_Social_Sign.JPG =750x) おしゃれなSWIFTロゴ看板 イベント内では、Swiftにまつわるクイズ大会(Kahoot)で誰が一番早く答えられるか勝負し、上位の人たちはSwiftのグッズをもらえました。 ![](/assets/blog/authors/hirofumi.nakano/WWDC24/Swift_Social_Play_Kahoot.jpg =750x) 結構いいところまでいったんですが、残念! 2つ目は、Core Coffeeです。Core CoffeeはWWDC期間中、毎日別の場所で開催がされており、自分が参加した回はApple Parkの隣にあるApple Park Visitor Center屋上にあるテラスでの開催で、Apple Parkに近いからか、何人かAppleの社員の方も参加されていました。 WWDC24で発表されたトピックでどれが気になる?という話題が盛り上がったりしました。そして、やはりみなホーム画面のTint色が果たしてどう影響するのか、好き嫌いが結構激しかったです。他、Apple社員の方のワークスタイルやどのプロダクトに関わっているのかも聞くことができ、結構身近に感じることが出来ました。 海外カンファレンスに参加するための基準整備中 WWDCに参加したあと、アメリカ西海岸で他にもいくつかのカンファレンスが開催されたため、KTCからも数名海外カンファレンスに参加してきたメンバーが増えました。 また、社内でも海外カンファレンスに参加するための基準の整備に取り組んでいます。 海外のカンファレンスに参加することで刺激を受け、持ち帰ってきて個人・チームのモチベーションにつながるきっかけになるといいなと思っています。 まとめ 今回初めて念願のWWDCに参加して、開発欲への刺激と仲間作りが個人的に印象に残りました。 実際、WWDCに参加する人はみな、Appleエコシステムの何かしらのアプリを開発していて、どんなアプリを開発しているのか、どういうポジションの人なのか、普段開発しながらどういうことを課題に感じているのかなどを知ることが出来ました。(もちろん、開発者だけではなく、プロダクトマネージャーなどの参加者もいました) また、メインの仕事がアプリ開発ではなく、趣味としてアプリ開発をしている人も何人か会うことができ、とても刺激をもらいました。 他、日本から参加した開発者の人たちと知り合ったり、期間中に話しかけた人ともその後日本に帰って顔馴染みになったりしました。 Appleエコシステムのプロダクト開発に関わる人であればぜひ参加を検討してみてください、世界中から集まる開発者の人たちと交流するのが楽しいと思います。
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の21日目の記事です🎅🎄 はじめに こんにちは。KINTO FACTORY開発グループのうえはら( @penpen_77777 )です。 2024年の7月に入社し、KINTO FACTORYのバックエンドの開発を担当しています。 今回は12/8に開催されたISUCON14にKINTO Technologies社員で参加してきたので、その内容や結果を共有したいと思います。 ISUCON14とは ISUCONとは、LINEヤフー株式会社が運営窓口となって開催している、お題となるWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトルです。 優勝すると賞金100万円を獲得できます! 今年は12/8(日)の10:00〜18:00で開催されました。 私(うえはら)は昨年のISUCON13からパフォーマンスチューニングの知識習得はもちろん、エンジニアとしての腕試し・スキル向上を目的に参加しています。 :::message ISUCONは、LINEヤフー株式会社の商標または登録商標です。 https://isucon.net/ ::: チーム「ktc-isucon-bu」 社内Slackでうえはらがメンバーを募集し、チーム「ktc-isucon-bu」を結成しました。 メンバは以下の通りです。 うえはら( @penpen_77777 ) ISUCON13にも参加しており、今年で2回目 古谷 ISUCON初参加 西田 ISUCON初参加 ISUCONでは初期実装で使われている言語がいくつかありますが、今回はGo言語を選択しました。 また、初参加のメンバーが多いチームのためランキング上位を狙うというよりもスコアが10000点を超えることを目標にしました。 参加に向けての事前準備 ISUCONでは競技本番での改善タスクに集中できるように、典型的な作業は自動化もしくは容易に実行できるようにするのが重要です。 今回は、以下の準備を行いました。 環境構築・デプロイコマンドの整備 ドキュメント生成ツールの整備 計測ツールの整備 個人練習/チーム練習 環境構築・デプロイコマンドの整備 go-taskはターミナルでタスクを実行するのに特化したツール、いわゆるタスクランナーです。 https://taskfile.dev/ 従来だとmakeコマンドをタスクランナーとして使われることが多いと思いますが、個人的にはgo-taskの方がより便利に感じたので今回はgo-taskを使用しました。 (makeと異なりインストールが必要なのが難点ですが、そこを除けば業務でも十分活用できるかなと思います) 例えば以下のようなtaskfile.yamlを作成し、 setup タスクと deploy タスクを定義します。 version: '3' tasks: setup: cmds: # 環境構築用コマンドを記述 - echo "setup" deploy: cmds: # デプロイ用コマンドを記述 - echo "deploy" 作成後、以下のように task タスク名 でコマンドを叩くことでタスクを実行できます。 # 環境構築タスクを実行 $ task setup setup # デプロイタスクを実行 $ task deploy deploy また、タスク実行時にコマンドライン引数を渡すこともでき、受け取った引数をタスク内に埋め込むこともできます。 version: '3' tasks: setup: cmds: - echo "setup {{ .CLI_ARGS }}" deploy: cmds: - echo "deploy {{ .CLI_ARGS }}" # サーバisu1に対してsetupタスクを実行 $ task setup -- isu1 setup isu1 # サーバisu2に対してdeployタスクを実行 $ task deploy -- isu2 deploy isu2 上のコードは簡単な例ではありますが、実際使用する際にはコマンドライン引数を用いて反映先のサーバを自由に切り替えられるように作っておくことで チームでの分担作業が捗ります。 その他、go-taskを使うメリットは以下の通りです。 サブディレクトリで作業していても、親ディレクトリに存在するtaskfile.yamlを検出してタスクを実行できる ディレクトリの位置を気にせず、タスクを実行できる タスクからタスクを呼び出すことができるため、定義したタスクの再利用性を高められる deploy タスクを実行する前に setup タスクを実行するなど、タスクを組み合わせて実行できる ISUCONで使用するツールを全てtaskfile.yamlに記述しておくことで、ツールの使用方法(必要なオプションなど)を知らなくても task コマンドを叩くだけで実行できる 事前に taskfile.yaml のテンプレートを作成しておき、本番では一部の変数を書き換えるだけであらゆる問題がきても柔軟に対応できるようにしておきました。 ドキュメント生成ツールの整備 以下の2つのツールを使用しました。 https://github.com/k1LoW/tbls DBの中身を読み取り、ER図とスキーマの説明が入ったドキュメント(markdown)を生成 データベースに繋ぎに行かなくてもスキーマ定義を確認できるため、DBの構造を理解するのに便利 CI/CDパイプラインでドキュメントを自動生成することで、DB構造の変更を追いやすくなる 詳しくは以下の記事を参照ください https://devblog.thebase.in/entry/auto_generated_er_graph_by_tbls_and_github_actions https://github.com/mazrean/isucrud アプリケーションコードを読み取り、DBテーブルとの関係を可視化・ドキュメント(markdown)を生成 関数が多くなってくると図がわかりにくくなるのが欠点だったのですが、webブラウザ上でインタラクティブに見たい箇所を絞り込めるようになったのでとても使いやすくなった 計測ツールの整備 以下のツールを使用しました。 https://github.com/kaz/pprotein アクセスログ、スロークエリログ、プロファイルデータを収集し、可視化するツール ベンチマークを叩くとwebhookによって自動的にデータ集計を始めてくれるようになるので便利 データ収集時点でのコミットハッシュも記録してくれるので、どのコミットがスコアアップに寄与したかがわかりやすい^[pproteinでうまくGitコミットハッシュが取得できない現象に遭遇し、fork・コードを修正してPRを出しました。 https://github.com/kaz/pprotein/pull/37] 個人練習/チーム練習 練習に関して、初参加のメンバーがいきなりISUCONの過去問を解こうとしても難易度の高さから挫折してしまう可能性があります。 まずはパフォーマンスチューニングの雰囲気を感じ取ってもらうため、達人が教えるWebパフォーマンスチューニングという本を読んでもらいました。 https://gihyo.jp/book/2022/978-4-297-12846-3 書籍を読み進めつつ、以下を練習問題として週末チームで集まり解いていきました。 https://github.com/catatsuy/private-isu (書籍でもprivate-isuを使ってどのようにチューニングしていけば良いか紹介されています) 大体10~20万点くらいまでスコアを取れるようになってきたところで、昨年のISUCON13の問題に変えて解いていきました。 ここまできたら本番を意識した練習にスライドしても特に問題なく進められるようになったかなと思います。 今回のお題 https://www.youtube.com/watch?v=UFlcAUvWvrY ライドチェアサービス「ISURIDE」の改善 チェアオーナー(椅子の所有者)が椅子を提供 ユーザはアプリから椅子が配車され、その椅子を使って目的地まで移動できる スコアは移動距離やライド完了率などから計算される ユーザ満足度を高めるように改善を進める必要がある https://github.com/isucon/isucon14 結果 結果は以下の通りです。目標の1万点を超えることができました! 135位 / 831チーム中 12514点 できたこと デプロイスクリプト等の準備(うえはら 10:00~10:30) 当日マニュアル(古谷 10:00) 動作しているプロセスを確認しておおよその構成を理解する(古谷 10:00) アプリケーションマニュアルを読む(西田 10:00) MySQLにログインし、テーブルサイズを調べる インデックスを追加(西田 11:21) -- chair_locations CREATE INDEX idx_chair_id ON chair_locations(chair_id); CREATE INDEX idx_chair_id_created_at ON chair_locations(chair_id, created_at); -- chairs CREATE INDEX idx_owner_id ON chairs(owner_id); -- ride_statuses CREATE INDEX idx_ride_statuses_ride_id_chair_sent_at_created_at ON ride_statuses (ride_id, chair_sent_at, created_at); pproteinの仕込み (うえはら 11:48) nginxの設定を修正するのに手こずり、1時間以上かかってしまう インデックス追加(西田 11:54) CREATE INDEX idx_ride_statuses_created_at_ride_id_chair_sent_at ON ride_statuses (created_at, ride_id, chair_sent_at); -- rides CREATE INDEX idx_chair_id_updated_at ON rides (chair_id, updated_at DESC); 動的パラメータを有効化・jsonパッケージを高速化(うえはら 12:38) // import文を修正 "encoding/json" → "github.com/goccy/go-json" // DB接続時の設定でInterpolateParamsをtrueに dbConfig.InterpolateParams = true インデックス追加(うえはら 13:05) -- chairs CREATE INDEX idx_chairs_access_token ON chairs(access_token); -- ride_statuses CREATE INDEX idx_ride_statuses_ride_id_app_sent_at_created_at ON ride_statuses (ride_id, app_sent_at, created_at); -- rides CREATE INDEX idx_rides ON rides (user_id, created_at); -- coupons CREATE INDEX idx_coupons_used_by ON coupons(used_by); ユーザ・ステータスキャッシュ(古谷 14:30) usersとride_statusesテーブルをインメモリキャッシュ トランザクション範囲の修正(西田 14:48, 15:09) notificationのポーリング間隔調整(古谷 16:27) appGetNotificationとchairGetNotificationで返しているRetryAfterMsを30 msから300 msに変更 椅子とユーザのマッチング間隔を短くした(西田 17:18, 17:28) ISUCON_MATCHING_INTERVALを0.5 sから0.1 sに短くした スコアが2倍近く上がった(一番効いた施策) binログ停止(古谷 17:24) MySQLの設定ファイル(/etc/mysql/mysql.conf.d/mysqld.cnf)でbinログの出力を停止 disable-log-bin=1 innodb_flush_log_at_trx_commit=0 ログ出力を無効化(うえはら 17:43) nginx、mysqlのログ出力を停止 アプリケーション側のログ出力停止 ownerGetChairsの改善(西田 17:43) distanceをメモ化するようにした コネクション数を調整(うえはら 17:49) db.SetMaxIdleConns(50) db.SetMaxOpenConns(50) 時間が足りない or スコアがあがらずできなかった施策 マッチング処理の修正(うえはら 13:00~16:00) 実装したが椅子が配車されないエラーの解消を乗り越えられず、断念… chairsのアクセストークンをキャッシュ(古谷 15:36) 叩かれるクエリ数は大幅に削減できた(30000→100)が、うまくスコアが上がらなかった 別の箇所にボトルネックが移ったのかもしれない nginxパラメータチューニング(古谷 17:39) ベンチマークのエラーでうまく動かず… サーバ分割(うえはら 16:00 ~ 17:00) nginx+goとMySQLの2台構成に変更しようとしたら、ベンチマークでデータ不整合のエラーがおきうまく動かすことができなかった 2台ともchairとrideのマッチングを行うサービスが動いていたため、データ不整合が起きたことに競技終了後気づいた…(止めておけば…) 良かった点 1万点を超えるという目標を達成できた isucon2回目1名+初参加2名のチームで135位、12514点というのは結果としては上々なのではないかと感じる 全員で改善タスクを進められた 何も手をつけられないということがなく、全員が何かしらの改善を進められた サーバ上への環境構築、デプロイスクリプトの仕込みがほぼ問題なく進められた デプロイ面で特に問題が生じることはなかった ただpproteinの仕込みでnginxの設定がうまくいかず1時間ほど手こずってしまったので、手順書に注意書きを書いておきたい ツールを作っておくと快適に改善を進められるので今後もツール面を充実させていきたい 反省点 あまりアプリケーションの中身を理解できずに改善を進めてしまった アプリケーションの仕様理解が重要な今回の問題だと、機械的にスロークエリログやアプリケーションログを元に高速化してもスコアアップに繋がりにくく、手を出せる箇所が少なかった アプリケーション理解が深まるような何らかの仕組みづくりをしたい 普段の業務でも仕様理解は重要なので、今後も意識していきたい マッチング処理をバグらせてしまっていつまでも実装できなかった テーブル構造を変えずにクエリだけで実装しようとして辛くなってしまったので、テーブルに都合の良いカラム追加してアプリケーションコードを単純にして実装しやすくするのが大事だなと思った SSE(Server-Sent Events)について全く知らなかったので勉強しておきたい ユーザの椅子の状態をリアルタイムに更新するのに使える技術として紹介されていた これからの業務で生かそうと思う点 技術的な知識習得だけでなく目の前のアプリケーション理解は何よりも大事 タスクランナー(go-task)やドキュメント自動生成ツール(tblsなど)を使って業務効率化を図っていきたい SSE(Server-Sent Events)についてプロダクトに組み込めるようなところがあれば使ってみたい まとめ ISUCON14に参加し、チーム「ktc-isucon-bu」として12514点、全チーム831チームのうち135位という結果を残すことができました。 反省点はありますが、ISUCON初参加のメンバーがいる中でスコア10000点を超えるという目標を達成できた点は良かったです。 この反省を糧に、次回のISUCON15ではさらなる高みを目指していきたいと思います。 初ISUCONにもかかわらず参加いただいたチームメンバーの古谷さん、西田さん、また今回参加までには至らずとも興味・関心をお寄せいただいた社員の皆さんありがとうございました! 最後にこの素晴らしいイベントを開催して下さった運営の皆さんに感謝いたします。
アバター
This article is the entry for day [21] in the KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Hey there! I’m Viacheslav, an iOS engineer, and today’s article is part of the Advent Calendar 2024 event at KINTO Technologies. This year, I had the opportunity to work on a new feature in our Unlimited app called "これなにガイド" (Kore Nani Gaido, meaning "What's This Guide"). Kore Nani Gaido is an augmented reality (AR) manual that allows users to "scan" their car's dashboard and displays virtual markers over a car's buttons, switches, and other elements. By selecting a marker corresponding to a specific button, users can access a detailed manual page for that control. Today, I’d like to share a short and simple solution to one of the challenges I encountered while working on this feature: accurately capturing the coordinates of a physical object recognized by Vision framework on the screen and converting them into 3D coordinates within an AR scene. What initially seemed like a trivial task turned out to be more complex than expected. After exploring several approaches and performing a lot of manual calculations, I finally arrived at a solution that is both straightforward and surprisingly concise. I wish I’d known about it when I started, as there is relatively little information available about ARKit and CoreML integration. So, let’s add to that knowledge base! A Couple of Preconditions Before we get to the actual code, let's clearly define the environment we’ll be working with: ARSCNView This is a view that displays the video feed from the device's camera, capturing the real-world environment and allowing 3D objects to "blend in" for an AR experience. ARSCNView is part of Apple's ARKit , built on top of SceneKit , which handles rendering 3D objects in an AR scene. Core ML Object Detection Model Before we can determine the coordinates of an object, we first need to recognize it within the video feed frames provided by the device's camera. Vision framework utilizes Core ML Object detection models for that purpose. For this article, I’ll assume you already have a model ready to use. If not, there are many pre-trained models available for download, such as YOLOv3-Tiny , which you can find here . And that’s all you need for a bare-bones solution! We’ll capture video frames from ARSCNView , use the Core ML model to detect the object’s position within the ARSCNView viewport, and apply a technique called "hit-testing" to determine the object’s coordinates in 3D AR space. Capturing the Coordinates of a Recognized Object in ARSCNView When performing recognition requests with Vision, a typical setup you might have is described below. You initialize a Core ML model and a VNCoreMLRequest to handle recognition using that model. let vnModel = try! VNCoreMLModel(for: myCoreMLModel) let vnRequest = VNCoreMLRequest(model: vnModel) { [weak self] request, _ in guard let observations = request.results else { return } // Observations handling } request.imageCropAndScaleOption = .centerCrop You then keep a reference to vnRequest in a suitable place, ready to be performed with the next set of arguments. The argument types depend on where you’re capturing video feed frames from. In our scenario, we are passing frames from an ARSCNView , which should be captured in the session(_:didUpdate:) method of ARSessionDelegate . This delegate method is called whenever a new frame is available for ARSCNView to display. func session(_ session: ARSession, didUpdate frame: ARFrame) { guard let vnRequest else { return } // 1 let options: [VNImageOption: Any] = [.cameraIntrinsics: frame.camera.intrinsics] // 2 let requestHandler = VNImageRequestHandler( cvPixelBuffer: frame.capturedImage, // 3 orientation: .downMirrored, // 4 options: options ) try? requestHandler.perform([vnRequest]) // 5 } Breaking Down the Code: Reference to VNCoreMLRequest : Once we receive a new frame, we are ready to perform the request we have initialized earlier. Camera Intrinsics : frame.camera.intrinsics provides camera calibration data to help Vision interpret the geometric properties of the scene. Image Input : VNImageRequestHandler accepts raw image data as a CVPixelBuffer , obtained from the AR frame. Image Orientation : The .downMirrored orientation accounts for the inverted coordinate system of the camera feed compared to Vision's default orientation. Performing the Request : The prepared request is executed using the request handler. Once you start passing frames to Vision, object detection results are returned as arrays of VNRecognizedObjectObservation objects in the VNCoreMLRequest completion handler. While you might filter these results by confidence level or perform other processing, today we’ll focus on extracting the coordinates of a specific recognized object. Extracting Bounding Box Coordinates At first, this might seem straightforward since VNRecognizedObjectObservation has a boundingBox property — a CGRect enclosing the recognized object. However, there are a few complications: The boundingBox is in a normalized coordinate system relative to the input image of the object recognition model (meaning that the coordinates' values are between 0 and 1) which also has inverted Y-axis. The sizes and aspect ratios of the camera feed, Core ML model input, and the ARSCNView viewport differ from each other. This means that a series of coordinate conversions and rescaling steps are required to transform the boundingBox into the ARSCNView viewport's coordinate system. Doing those conversions manually might be cumbersome and mistake-prone. Fortunately, there’s an embarrassingly simple way to handle this using CGAffineTransform . Here’s how: let sceneView: ARSCNView func getDetectionCenter(from observation: VNRecognizedObjectObservation) -> CGRect? { guard let currentFrame = sceneView.session.currentFrame else { return nil } let viewportSize = sceneView.frame.size // 1 let fromCameraImageToViewTransform = currentFrame.displayTransform(for: .portrait, viewportSize: viewportSize) let viewNormalizedBoundingBox = observation.boundingBox.applying(fromCameraImageToViewTransform) // 2 let scaleTransform = CGAffineTransform(scaleX: viewportSize.width, y: viewportSize.height) let viewBoundingBox = viewNormalizedBoundingBox.applying(scaleTransform) return viewBoundingBox } Explanation: Transforming to View Coordinates : Using displayTransform(for:viewportSize:) the detected bounding box is converted from the normalized coordinate system of the input image to a normalized coordinate system of ARSCNView . Scaling to Pixel Dimensions : The normalized bounding box is scaled to match the size of the ARSCNView viewport, resulting in a bounding box in screen pixel dimensions. And that’s it! You now have the bounding box in the coordinate system of the ARSCNView viewport. Getting the Third Coordinate I promised that we will get the coordinates of the recognized object within the 3D coordinate space of the AR scene. To do that, we are going to utilize a technique called "hit-testing." It allows us to measure the distance to the closest physical object at an arbitrary point in the viewport. You can imagine this technique as casting a straight ray from your device to the first intersection with a physical object at the selected point of the viewport and then measuring the length of that ray. This functionality is a part of ARKit and is really easy to use. Here is how we can find the 3D coordinates of the perceivable center of the object we detected earlier. func performHitTestInCenter(of boundingBox: CGRect) -> SCNVector3? { let center = CGPoint(x: boundingBox.midX, y: boundingBox.midY) // 1 return performHitTest(at: center) } func performHitTest(at location: CGPoint) -> SCNVector3? { guard let query = sceneView.raycastQuery( // 2 from: location, allowing: .estimatedPlane, // 3 alignment: .any // 4 ) else { return nil } guard let result = sceneView.session.raycast(query).first else { return nil } // 5 let translation = result.worldTransform.columns.3 // 6 return .init(x: translation.x, y: translation.y, z: translation.z) } Explanation: Here, we calculate the center of the bounding box from the previous step, as we need a single point to perform the hit-testing. Creates a raycast query starting from the given 2D point. Allows hit-testing to consider non-planar surfaces or planes about which ARKit can only estimate. Enables hit-testing for both horizontal and vertical surfaces (default is horizontal only). Executes the raycast query using the AR session. Returns nil if there’s no intersection. Each ARRaycastResult contains a worldTransform , which is a 4x4 matrix representing the 3D transformation of the detected point in world space. The columns.3 contains the translation vector, which specifies the 3D position of the intersection. This translation is returned as an SCNVector3 , which ARKit/SceneKit uses to represent 3D positions. Done! You now have the 3D coordinates of an object detected by Vision. Feel free to use them for your purposes. :) Conclusion In the Unlimited app, we use these 3D coordinates to display AR manual markers in a car. Of course, there are many additional techniques we employ to make the user experience smoother and the marker positions more stable, but this is one of the core techniques. That said, this same method can be used for any other purpose you can think of. I hope you find it helpful. Finally, here’s a tiny sneak peek into our testing process and how our AR manual markers are displayed after object detection. That's it for today—thanks for reading! Wishing you the Merriest Christmas and the Happiest New Year!
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の21日目の記事です🎅🎄 はじめに こんにちは。KINTO FACTORY開発グループのうえはら( @penpen_77777 )です。 2024年の7月に入社し、KINTO FACTORYのバックエンドの開発を担当しています。 今回は12/8に開催されたISUCON14にKINTO Technologies社員で参加してきたので、その内容や結果を共有したいと思います。 ISUCON14とは ISUCONとは、LINEヤフー株式会社が運営窓口となって開催している、お題となるWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトルです。 優勝すると賞金100万円を獲得できます! 今年は12/8(日)の10:00〜18:00で開催されました。 私(うえはら)は昨年のISUCON13からパフォーマンスチューニングの知識習得はもちろん、エンジニアとしての腕試し・スキル向上を目的に参加しています。 :::message ISUCONは、LINEヤフー株式会社の商標または登録商標です。 https://isucon.net/ ::: チーム「ktc-isucon-bu」 社内Slackでうえはらがメンバーを募集し、チーム「ktc-isucon-bu」を結成しました。 メンバは以下の通りです。 うえはら( @penpen_77777 ) ISUCON13にも参加しており、今年で2回目 古谷 ISUCON初参加 西田 ISUCON初参加 ISUCONでは初期実装で使われている言語がいくつかありますが、今回はGo言語を選択しました。 また、初参加のメンバーが多いチームのためランキング上位を狙うというよりもスコアが10000点を超えることを目標にしました。 参加に向けての事前準備 ISUCONでは競技本番での改善タスクに集中できるように、典型的な作業は自動化もしくは容易に実行できるようにするのが重要です。 今回は、以下の準備を行いました。 環境構築・デプロイコマンドの整備 ドキュメント生成ツールの整備 計測ツールの整備 個人練習/チーム練習 環境構築・デプロイコマンドの整備 go-taskはターミナルでタスクを実行するのに特化したツール、いわゆるタスクランナーです。 https://taskfile.dev/ 従来だとmakeコマンドをタスクランナーとして使われることが多いと思いますが、個人的にはgo-taskの方がより便利に感じたので今回はgo-taskを使用しました。 (makeと異なりインストールが必要なのが難点ですが、そこを除けば業務でも十分活用できるかなと思います) 例えば以下のようなtaskfile.yamlを作成し、 setup タスクと deploy タスクを定義します。 version: '3' tasks: setup: cmds: # 環境構築用コマンドを記述 - echo "setup" deploy: cmds: # デプロイ用コマンドを記述 - echo "deploy" 作成後、以下のように task タスク名 でコマンドを叩くことでタスクを実行できます。 # 環境構築タスクを実行 $ task setup setup # デプロイタスクを実行 $ task deploy deploy また、タスク実行時にコマンドライン引数を渡すこともでき、受け取った引数をタスク内に埋め込むこともできます。 version: '3' tasks: setup: cmds: - echo "setup {{ .CLI_ARGS }}" deploy: cmds: - echo "deploy {{ .CLI_ARGS }}" # サーバisu1に対してsetupタスクを実行 $ task setup -- isu1 setup isu1 # サーバisu2に対してdeployタスクを実行 $ task deploy -- isu2 deploy isu2 上のコードは簡単な例ではありますが、実際使用する際にはコマンドライン引数を用いて反映先のサーバを自由に切り替えられるように作っておくことで チームでの分担作業が捗ります。 その他、go-taskを使うメリットは以下の通りです。 サブディレクトリで作業していても、親ディレクトリに存在するtaskfile.yamlを検出してタスクを実行できる ディレクトリの位置を気にせず、タスクを実行できる タスクからタスクを呼び出すことができるため、定義したタスクの再利用性を高められる deploy タスクを実行する前に setup タスクを実行するなど、タスクを組み合わせて実行できる ISUCONで使用するツールを全てtaskfile.yamlに記述しておくことで、ツールの使用方法(必要なオプションなど)を知らなくても task コマンドを叩くだけで実行できる 事前に taskfile.yaml のテンプレートを作成しておき、本番では一部の変数を書き換えるだけであらゆる問題がきても柔軟に対応できるようにしておきました。 ドキュメント生成ツールの整備 以下の2つのツールを使用しました。 https://github.com/k1LoW/tbls DBの中身を読み取り、ER図とスキーマの説明が入ったドキュメント(markdown)を生成 データベースに繋ぎに行かなくてもスキーマ定義を確認できるため、DBの構造を理解するのに便利 CI/CDパイプラインでドキュメントを自動生成することで、DB構造の変更を追いやすくなる 詳しくは以下の記事を参照ください https://devblog.thebase.in/entry/auto_generated_er_graph_by_tbls_and_github_actions https://github.com/mazrean/isucrud アプリケーションコードを読み取り、DBテーブルとの関係を可視化・ドキュメント(markdown)を生成 関数が多くなってくると図がわかりにくくなるのが欠点だったのですが、webブラウザ上でインタラクティブに見たい箇所を絞り込めるようになったのでとても使いやすくなった 計測ツールの整備 以下のツールを使用しました。 https://github.com/kaz/pprotein アクセスログ、スロークエリログ、プロファイルデータを収集し、可視化するツール ベンチマークを叩くとwebhookによって自動的にデータ集計を始めてくれるようになるので便利 データ収集時点でのコミットハッシュも記録してくれるので、どのコミットがスコアアップに寄与したかがわかりやすい^[pproteinでうまくGitコミットハッシュが取得できない現象に遭遇し、fork・コードを修正してPRを出しました。 https://github.com/kaz/pprotein/pull/37] 個人練習/チーム練習 練習に関して、初参加のメンバーがいきなりISUCONの過去問を解こうとしても難易度の高さから挫折してしまう可能性があります。 まずはパフォーマンスチューニングの雰囲気を感じ取ってもらうため、達人が教えるWebパフォーマンスチューニングという本を読んでもらいました。 https://gihyo.jp/book/2022/978-4-297-12846-3 書籍を読み進めつつ、以下を練習問題として週末チームで集まり解いていきました。 https://github.com/catatsuy/private-isu (書籍でもprivate-isuを使ってどのようにチューニングしていけば良いか紹介されています) 大体10~20万点くらいまでスコアを取れるようになってきたところで、昨年のISUCON13の問題に変えて解いていきました。 ここまできたら本番を意識した練習にスライドしても特に問題なく進められるようになったかなと思います。 今回のお題 https://www.youtube.com/watch?v=UFlcAUvWvrY ライドチェアサービス「ISURIDE」の改善 チェアオーナー(椅子の所有者)が椅子を提供 ユーザはアプリから椅子が配車され、その椅子を使って目的地まで移動できる スコアは移動距離やライド完了率などから計算される ユーザ満足度を高めるように改善を進める必要がある https://github.com/isucon/isucon14 結果 結果は以下の通りです。目標の1万点を超えることができました! 135位 / 831チーム中 12514点 できたこと デプロイスクリプト等の準備(うえはら 10:00~10:30) 当日マニュアル(古谷 10:00) 動作しているプロセスを確認しておおよその構成を理解する(古谷 10:00) アプリケーションマニュアルを読む(西田 10:00) MySQLにログインし、テーブルサイズを調べる インデックスを追加(西田 11:21) -- chair_locations CREATE INDEX idx_chair_id ON chair_locations(chair_id); CREATE INDEX idx_chair_id_created_at ON chair_locations(chair_id, created_at); -- chairs CREATE INDEX idx_owner_id ON chairs(owner_id); -- ride_statuses CREATE INDEX idx_ride_statuses_ride_id_chair_sent_at_created_at ON ride_statuses (ride_id, chair_sent_at, created_at); pproteinの仕込み (うえはら 11:48) nginxの設定を修正するのに手こずり、1時間以上かかってしまう インデックス追加(西田 11:54) CREATE INDEX idx_ride_statuses_created_at_ride_id_chair_sent_at ON ride_statuses (created_at, ride_id, chair_sent_at); -- rides CREATE INDEX idx_chair_id_updated_at ON rides (chair_id, updated_at DESC); 動的パラメータを有効化・jsonパッケージを高速化(うえはら 12:38) // import文を修正 "encoding/json" → "github.com/goccy/go-json" // DB接続時の設定でInterpolateParamsをtrueに dbConfig.InterpolateParams = true インデックス追加(うえはら 13:05) -- chairs CREATE INDEX idx_chairs_access_token ON chairs(access_token); -- ride_statuses CREATE INDEX idx_ride_statuses_ride_id_app_sent_at_created_at ON ride_statuses (ride_id, app_sent_at, created_at); -- rides CREATE INDEX idx_rides ON rides (user_id, created_at); -- coupons CREATE INDEX idx_coupons_used_by ON coupons(used_by); ユーザ・ステータスキャッシュ(古谷 14:30) usersとride_statusesテーブルをインメモリキャッシュ トランザクション範囲の修正(西田 14:48, 15:09) notificationのポーリング間隔調整(古谷 16:27) appGetNotificationとchairGetNotificationで返しているRetryAfterMsを30 msから300 msに変更 椅子とユーザのマッチング間隔を短くした(西田 17:18, 17:28) ISUCON_MATCHING_INTERVALを0.5 sから0.1 sに短くした スコアが2倍近く上がった(一番効いた施策) binログ停止(古谷 17:24) MySQLの設定ファイル(/etc/mysql/mysql.conf.d/mysqld.cnf)でbinログの出力を停止 disable-log-bin=1 innodb_flush_log_at_trx_commit=0 ログ出力を無効化(うえはら 17:43) nginx、mysqlのログ出力を停止 アプリケーション側のログ出力停止 ownerGetChairsの改善(西田 17:43) distanceをメモ化するようにした コネクション数を調整(うえはら 17:49) db.SetMaxIdleConns(50) db.SetMaxOpenConns(50) 時間が足りない or スコアがあがらずできなかった施策 マッチング処理の修正(うえはら 13:00~16:00) 実装したが椅子が配車されないエラーの解消を乗り越えられず、断念… chairsのアクセストークンをキャッシュ(古谷 15:36) 叩かれるクエリ数は大幅に削減できた(30000→100)が、うまくスコアが上がらなかった 別の箇所にボトルネックが移ったのかもしれない nginxパラメータチューニング(古谷 17:39) ベンチマークのエラーでうまく動かず… サーバ分割(うえはら 16:00 ~ 17:00) nginx+goとMySQLの2台構成に変更しようとしたら、ベンチマークでデータ不整合のエラーがおきうまく動かすことができなかった 2台ともchairとrideのマッチングを行うサービスが動いていたため、データ不整合が起きたことに競技終了後気づいた…(止めておけば…) 良かった点 1万点を超えるという目標を達成できた isucon2回目1名+初参加2名のチームで135位、12514点というのは結果としては上々なのではないかと感じる 全員で改善タスクを進められた 何も手をつけられないということがなく、全員が何かしらの改善を進められた サーバ上への環境構築、デプロイスクリプトの仕込みがほぼ問題なく進められた デプロイ面で特に問題が生じることはなかった ただpproteinの仕込みでnginxの設定がうまくいかず1時間ほど手こずってしまったので、手順書に注意書きを書いておきたい ツールを作っておくと快適に改善を進められるので今後もツール面を充実させていきたい 反省点 あまりアプリケーションの中身を理解できずに改善を進めてしまった アプリケーションの仕様理解が重要な今回の問題だと、機械的にスロークエリログやアプリケーションログを元に高速化してもスコアアップに繋がりにくく、手を出せる箇所が少なかった アプリケーション理解が深まるような何らかの仕組みづくりをしたい 普段の業務でも仕様理解は重要なので、今後も意識していきたい マッチング処理をバグらせてしまっていつまでも実装できなかった テーブル構造を変えずにクエリだけで実装しようとして辛くなってしまったので、テーブルに都合の良いカラム追加してアプリケーションコードを単純にして実装しやすくするのが大事だなと思った SSE(Server-Sent Events)について全く知らなかったので勉強しておきたい ユーザの椅子の状態をリアルタイムに更新するのに使える技術として紹介されていた これからの業務で生かそうと思う点 技術的な知識習得だけでなく目の前のアプリケーション理解は何よりも大事 タスクランナー(go-task)やドキュメント自動生成ツール(tblsなど)を使って業務効率化を図っていきたい SSE(Server-Sent Events)についてプロダクトに組み込めるようなところがあれば使ってみたい まとめ ISUCON14に参加し、チーム「ktc-isucon-bu」として12514点、全チーム831チームのうち135位という結果を残すことができました。 反省点はありますが、ISUCON初参加のメンバーがいる中でスコア10000点を超えるという目標を達成できた点は良かったです。 この反省を糧に、次回のISUCON15ではさらなる高みを目指していきたいと思います。 初ISUCONにもかかわらず参加いただいたチームメンバーの古谷さん、西田さん、また今回参加までには至らずとも興味・関心をお寄せいただいた社員の皆さんありがとうございました! 最後にこの素晴らしいイベントを開催して下さった運営の皆さんに感謝いたします。
アバター
This article is the entry for day 21 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Hello everyone! My name is DL, and I’m part of the Business Development Group team in the Group Core System Division at KINTO Technologies (KTC) . Currently, we’re collaborating with KINTO divisions in Latin America to implement an in-house system for KINTO One, a car leasing business with additional services like insurance. As a Business Analyst (BA), I focus on analyzing KINTO's operations and crafting IT solutions to streamline workflows and enhance efficiency. Before business analysis became part of the global team, system designs often fell short to meet KINTO’s global business expectations for enterprise-level solutions that addressed their daily operational intricacies. This is where a BA becomes indispensable in capturing the daily operational processes and addressing pain points, making Business Analysis a strategic pillar for adding value to global projects. Today, I want to invite you into the often-misunderstood world of the BA. You might think, “Isn’t that just a numbers person?” or “Aren’t they the ones writing down what others say?” But being a BA is far more—it’s about problem-solving, strategy, and communication. At its core, a BA bridges the technical and business worlds. Whether improving efficiency, streamlining operations, or adopting technology, the BA steps in to listen, analyze, and design practical, impactful solutions. By engaging stakeholders from executives to end-users, the BA becomes the crucial translator, turning business needs into effective technical solutions. Story 1 - The Power of Listening: A Business Analyst (BA)’s Role One critical skill for a BA is listening—not just to words, but to the underlying needs. In one project, I tackled a financial company’s inefficient reporting process. Business units worldwide submitted quarterly reports via email, creating a bottleneck with over 100 daily emails. Files were manually downloaded, consolidated, and checked for errors—a time-consuming, error-prone process. Witnessing this firsthand, I worked with stakeholders to identify pain points. Collaborating with IT, we developed a secure LAN directory structure for direct submissions. Automated batch and VBA scripts streamlined checking, copying, and aggregating data, reducing manual effort by 70% and enabling more valuable analysis. The solution’s success led to its adoption across the department. Story 2 - A Lesson in Overlooking Stakeholders Afterwards, a SaaS product was introduced without the BA and users’ full review. It was selected for its cost savings and dashboards, but it couldn’t support existing automated workflows, forcing teams back to manual processes... Despite complaints, the decision was final, leaving users frustrated and dissatisfied. This experience underscored the importance of involving a BA early to align solutions with real user needs. The Story Continues… Although Story 2 might initially seem like a letdown, it offers an important lesson: whether we’re introducing process changes or developing new products, the key to success lies in listening to and truly understanding the needs of the end users. This brings us to the critical role a BA plays in KTC’s global project. As mentioned earlier, the global team is working with KINTO divisions in Latin America to implement an in-house system for KINTO One business (a car leasing business with additional services, e.g., insurance). Developing a global product/system means addressing diverse specifications across countries and languages. The challenge lies in creating a system that is flexible enough to accommodate each country’s varying needs. So, how do we approach this? Business users in each country are experts in their own processes and the processes are different country by country. Engineers are tasked with the challenge of building a flexible system that can accommodate all the differences. This is where the BA steps in, bridging the gap between business users and engineers. As a BA, we take a systematic approach: Gathering Business Processes: We carefully document the processes unique to each country. As the BA, this step is highly critical. The BA basically needs to become the expert of the business processes and fully understand every detailed step to ensure no steps are missed. To accomplish this step, our BAs in KTC have visited and been on-site at the businesses in Latin America, which is essential for grasping the nuances of each process. Moreover, the key in this step is to understand the businesses’ current pain points. Why? Because the system KTC develops should address these pain points to add tangible value to the business users’ daily workflows. By addressing the pain points, the business users can see a decrease in manual work, increase in productivity and effectiveness. And this can be measured in terms of hours of work reduced, reduction in lead time, and general welfare of the business users. Conducting Gap Analysis: This analysis highlights the differences and commonalities across these processes for each country. As the BA, there are two critical considerations in this step: Order of Operations: If the countries have similar order of operation, then that’s great news! However, if one country has a reverse order of operation, then this becomes much more complex when designing a system that can accommodate both. For example, to perform a repair on a vehicle, in case 1: the repair is approved first as a KINTO service, and then the execution of service is carried out. However, in case 2: the execution of service is carried out first, then the service is verified if it’s included as a KINTO service. Such differences in the order of operation adds complexity to the system design. External Systems Integration: Another critical consideration in this step is to note the external systems/platforms that are currently being used by the different countries. For example, one country may be using SAP as their accounting system, another country may be using another type of accounting system. This adds complexity to the integration needed for the global product. Collaborating on Solutions: Using the gap analysis, the BA works closely with the engineers to design the flexibility needed to meet the requirements of all countries involved. In this step, the BA is now trying to solve a complex puzzle with the help of the engineers. As the BA, this step is highly critical because the BA basically represents the business users of both countries and needs to design a system that can accommodate both. As an analogy, maybe one country is like a sedan, and the other country is like a van, so the BA works with the engineers to come up with an SUV?... lol... something along those lines. This step involves looking at the details of every step of the process and making sure that every step can accommodate both countries. Then after that, making sure that all the steps can be stitched together to create a cohesive and flexible system. It’s a lot of fun to solve such complex problems! The result is a collaborative effort that ensures the global product not only aligns with business needs but also has the right balance of system flexibility. This demonstrates just how essential the role of a BA is in the success of a global project. The Impact of a Business Analyst (BA) What does it mean to be a BA? It means being a problem solver, a communicator, and a strategist—having the curiosity to ask questions, the patience to listen, and the persistence to find answers. Ultimately, it’s about making a real difference in how organizations run, how people work, and how decisions are made. BAs may not always be in the spotlight, but our work shapes outcomes, saves resources, and makes lives easier. We are the bridge that turns ideas into reality, translating needs into solutions. If there’s one takeaway, it’s this: Never underestimate the power of listening. Whether you’re a BA, a stakeholder, or someone striving to make a difference, real progress comes from listening, translating, and connecting. Thank you!
アバター
Introduction This is a report from the organizers of the second "KINTO Technologies MeetUp!" Our Event page: [Second] KINTO Technologies MeetUp! - connpass Previous related articles: Preparations for our first "KINTO Technologies MeetUp!" | KINTO Tech Blog | KINTO Tech Blog (kinto-technologies.com) KINTO Technologies MeetUp! (Organizers’ edition) | KINTO Tech Blog | KINTO Tech Blog (kinto-technologies.com) The first event was held exclusively on-site, but this time we prepared to allow participation online as well. Building on the knowledge gained from our first event, we embraced the challenge of hosting in a new hybrid format. Here’s a look at the organizer’s experience this time around! Pre-Event Tasks Snack arrangement To ensure no one felt “a bit unsatisfied” with the food, we focused on enhancing the snack selection. During the latter half of our last event, we found that the pizza we had prepared wasn’t enough, and some participants commented that they felt “a bit unsatisfied.” Initially, we planned for pizza again and increased the budget to address this, but we eventually learned about Maisen’s mini burgers (thanks to a recommendation) and decided to offer those instead. Here are some of the great things about the mini burger: It is individually packaged and easy to grab. Therefore, there is no need to prepare paper plates. It is easy to give away if there are leftovers, reducing waste. It is designed to be served at room temperature, allowing more flexibility with delivery timing. Although we had a strong desire to serve hot pizza, we decided on the mini burgers after considering the benefits. Additionally we prepared individually wrapped snacks to make it easy for participants to enjoy at their convenience. As a result, many participants enjoyed the mini burgers, and we ended up with just a few leftovers, making it a successful choice overall! Novelty selection We aimed to provide novelties that would make participants think, "I want to take this home and actually use it.” With this goal in mind, we carefully selected items, believing that a good event combined with appealing novelties can bring back fond memories of the day just by looking at them. In choosing the novelties, we considered not only practicality but also whether the items would have an irresistible appeal. For distribution, we thought carefully about how to present the novelties in a way that encourage attendees to take them home. Although not all novelties were taken by participants, we hope that those who did take them will remember the event whenever they see the items. Event timetable design I was entrusted with designing the event timetable, which is an important factor that determines the progression and flow of the event. It was a significant responsibility, and I felt the pressure as I worked through it. My main focus during the planning was, “ Will the participants find the event engaging and enjoyable without feeling bored? ” In the timetable for the previous MeetUp, we missed incorporating break times and a group photo session with participants, which were later highlighted as areas for improvement. For this event, we addressed these points by allocating time for breaks and photos. Additionally, we included a Lightning Talk session to bring energy and keep the pace lively, something that was missing last time. Thankfully, the event proceeded smoothly, mostly according to the timetable, and the atmosphere was lively throughout—a welcome outcome. However, we made some last-minute adjustments to the timetable, and I realized in hindsight that more careful pre-event planning would have helped. Event documents preparation To prepare documents, not only did we organized presentations for speakers but also prepared various guides and resources for attendees, including event instructions, seating arrangements for discussion sessions, and pre-event handouts. These documents were aimed at ensuring the smooth progression of the Meetup. In the previous event, we realized that verbal directions to restrooms or having a QR code for Wi-Fi access would have been helpful, so this time we created documents to cover these needs. Promotion and Outreach Attracting attendees to corporate events can be challenging, and we also faced difficulties. Here are two approaches we found especially effective: Included the product names mentioned in the presentations within the promotional text. Posted in the IT Slack community. Included the product names mentioned in the presentations within the promotional text. Initially, we planned to announce the event on X twice. The first announcement was scheduled for when it went live on connpass, and the second just before the event. However, we soon realized that the key metrics we wanted to focus on were not growing as expected, which were: Views on the connpass event page The number of event participants To address these challenges, we considered increasing the opportunities for people to learn about the event by adding more posts on X. However, simply posting about the event might not catch people’s attention, and awareness of the event could remain low. So, we decided to drill down our thinking to create more effective post content. We asked ourselves, “what kind of post would catch my eye as a reader?” ↓ Would I be more interested if the post mentioned a product I care about? ↓ Would it feel more relatable if it included some of the challenges we faced? Through this simple drill-down, we concluded that the post should include not only the product names mentioned in the presentations but also what we achieved with them. Here’s an example of the posts we created: By posting such kind of content, we successfully increased both the views on the connpass event page and the number of participants, addressing the challenges we had been facing. We couldn't be happier with the results!! IT Slack community posts On the day of the event, we asked some of the on-site attendees how they had heard about the event. All of them answered, “on the IT Slack.” Its reach is truly impressive! As someone who regularly checks the Slack channel #share-event as well, I highly recommend posting announcements there when hosting events related to IT or corporate IT. This time, our event aimed to connect with other corporate IT teams, and the warmth of the IT Slack community was truly heartwarming. To those who participated, whether you found us through connpass or X, thank you, and we look forward to your continued support! Disruptive participants prevention In the past, we experienced a situation at another KTC-hosted event where a person seemed to attend solely for the food. There was an atmosphere that could have escalated into a disturbance, as the person drank excessively and reacted negatively when approached. Through this experience, I decided to firmly establish a clear protocols and preventative measures. After researching how others handle such situations, we adopted the following approaches: Identify and prevent disruptive individuals before they can cause issues. If a disruptive individual does enter (or tries to get in), ensure minimal impact on legitimate participants, and act swiftly to manage the situation. Thoughts on handling troublemakers at networking events | wakatono (note.com) We prepared with the approach of preventing suspicious individuals from entering while also establishing grounds and methods for their removal if necessary. After consulting with the building management team at KINTO Technologies’ Muromachi Office in COREDO Muromachi 2, which was our venue, they assured us that they could intervene if needed, even restraining individuals if the situation escalated. We shared their contact information with the event staff to ensure everyone was prepared to respond if necessary. Additionally, we updated the connpass event page to include a clear policy prohibiting disruptive behavior and informing participants that they may be asked to leave if such actions occur. Fortunately, no such incidents happened this time. However, having these measures in place gave us peace of mind and allowed us to focus fully on running the event smoothly. Event Day Tasks Venue set up We followed the same basic process as in the previous event, but this time we also needed to account for the space required for the streaming equipment. Its placement was determined by the length of our cables, and we also made sure that participants still had a comfortable viewing area while adressing those constraints. While we prepared the basic layout in advance, adjustments were made on the day to accommodate the number of attendees. Although there was some last-minute scrambling, we successfully created a space where participants could relax and enjoy the event. Unfortunately, we weren't able to update the seating layout for the discussion session in time, which caused some confusion during the event. Equipment setup, connections, and control With limited equipment on hand, we carefully planned how to deploy it effectively. Hosting the event in a hybrid format required extra attention to various details. Thankfully, there were no major issues, and we were able to deliver a seamless experience for our online attendees as well. Overall, it turned out to be a successful event! Camera setup Since the camera feed would be projected onto a small wipe, we adjusted the framing to ensure that the speaker's face was clearly visible. The camera had to be connected to the “switcher” used for stream control, so we positioned it near the operation desk (commonly referred as the "ops desk") while ensuring it faced the speaker directly. The camera was basically fixed in place! Minimal adjustments were needed when speakers took turns to correct any misalignment caused by differences in their heights. Audio setup We conducted pre-event checks for microphones, volume levels in the venue, and audio levels for online participants. Since this was a hybrid event, these checks were absolutely essential! We made sure to prevent any issues, such as audio distortion or volume levels that were either too low or too high for both online and in-person attendees. Additionally, during presentations, we continuously monitored the online audio to respond immediately in case of any audio dropouts or sudden distortion. Streaming For this event, we decided on a relatively simple streaming setup, drawing from previous successful experiences. By keeping the setup straightforward, we minimized the risk of unexpected issues during the broadcast. We projected the same video feed onto a screen so that the presenters could see how their slides and wipe were displayed. To ensure the speaker’s slides and the wipe didn’t overlap, we manually adjusted the position of the wipe, moving it around to different corners as needed. Each speaker was able to present using their own familiar laptop, and we managed the visuals without any disruptions. This made the process smooth and hassle-free! Microphones, document projection, the wipe-everything was checked and ready to go! Reception, guidance, and exit support Preparation Since we had the same team as the previous event, we already had a good amount of knowledge. However, we documented tasks in Confluence, assigned roles to each member, and prepared accordingly. By deciding everyone’s roles for the event day in advance, we avoided unnecessary chaos during the event. On the day of the event As soon as the registration closed, we started preparing the building entry passes. They looked too plain on their own, so we added a custom cover for a personalized touch. This small effort, which we’ve also used for other events, really makes a difference. Reception start Since the event took place during rush hour, many non-participants were coming and going. To ensure that attendees could recognize the reception area, we called out to anyone who looked like they might be attending the event (Though sometimes we mistakenly approached unrelated people, haha). This time, we didn’t assign staff to guide attendees from the reception area to the 16th floor venue. Instead, whenever participants headed to the venue, we notified the operations team via Slack with updates like “X number of people are coming!” This was initially done spontaneously but turned out to be very well-received by the team waiting at the venue. In the end, it was a great idea, and I’m glad we did it! Exit support To be honest, we didn’t have detailed discussions about this part as a team, so whoever was available handled it on the spot. Personally, I really enjoyed chatting with attendees in the elevator as they left. Hearing comments like "That was fun!" "Please hold another event again" or "KINTO Technologies seems like such a fun company!" Left me feeling thrilled. Being able to hear this feedback live felt like a special reward! ♬ Moderation and hosting We managed to get through most of it with energy and quick adaptability! About 80% of the moderation was handled this way. Here, I’ll share the remaining 20% of the things we consciously focused on. Adding comments before each presentation In an in-person-only event, transitions like speaker changes are visible and don’t stand out much. However, in a hybrid event, online attendees can only see what’s shown on camera, which creates awkward silences during transitions. To address this, I added short introductions about the upcoming presentation or summarized the previous one, much like the opening remarks before classic Showa-era song performances. This helped create a smoother transition. Separate remarks for in-person and online audiences only at the beginning and end Time constraints were the main reason, but I avoided making separate comments for the in-person and online audiences during the event. For online attendees, hearing jokes or commentary that only resonates with the in-person audience can make them feel left out and reduce their sense of immersion. I kept this in mind while moderating. Using a microphone sparingly after the online broadcast ended This was the opposite approach compared to the one mentioned above. I wanted to maximize the in-person experience! It’s similar to how bands or idol groups sometimes speak directly to the audience without a mic at the end of a concert. It gives you that feeling of being on a live session. Yes, like that moment. I was initially hesitant about speaking in front of the audience, but I’m relieved that I was able to fulfill my role. To anyone reading this, I encourage you to give it a try as well. Snack preparation and distribution Last time was our first external event, and organizing refreshments was a bit of a challenge. But after hosting several other events over the past few months, we’ve gotten the hang of it! This time, we prepared Maisen’s cutlet sandwiches🐷 Everything looks delicious...Kin-chan Which should I take...Take-chan Compared to pizza, it's easier to eat. We adopted a casual style, encouraging participants to grab a sandwich after checking in and enjoy the meetup! while eating. Since the event was held around dinnertime (pretty much), I think this was a welcome addiction! Even though we ordered plenty, the fillet cutlet sandwiches were the first to disappear ( ..)φ [takes notes] We will do our best to adjust the quantities next time. For drinks, beer was the most popular. We had three types available, but after attending another company’s event that offered about ten varieties, I was impressed! Next time, we’ll try expanding our selection. Kaizen, kaizen! Post-Event Tasks Retrospective When it comes to wrapping up team activities, our go-to process is doing Retrospectives. What is a Retrospective Retrospectives are sessions: to look back on what went well, what didn’t go so well, and what happened during an activity or project, and from there, to identify and develop concrete action plans for the future. At KINTO Technologies, retrospectives are deeply embedded in our everyday practices. Across all departments, they are commonly referred to as “furikaeri” or simply “retro,” and they often come up in casual conversations. The event retrospective After the MeetUp concluded, we held a retrospective specifically for the organizing team. While there are no strict rules or fixed formats for these sessions, we often use the KPT framework (Keep/Problem/Try) to guide our discussions, which is what we used this time as well. In event operations, there are always things that could have gone better or areas for improvement. The KPT framework helps us identify these “seeds of kaizen” and turn them into actionable steps for future events. Retrospective preparation When there are many participants and everyone’s time is limited, proper preparation is key to make the most out of retrospective sessions. This time, we followed these steps: Created a Wiki where everyone can freely contribute (we used Confluence as our Wiki tool) Prepared a page with the KPT framework (for this retrospective, we created additional categories such as "Preparation," "Promotion and Outreach," and "On-site Setup" to make it easier to organize inputs). Announced on Slack, asking participants to write down their observations before the retrospective day. The facilitator reviewed the pages right before the retrospective, processes the contributions, and planned the flow of the discussion (for example, deciding what to prioritize and how much time to allocate for each topic). The retrospective: From icebreaker to ground rules And then, the retrospective took place! If we had more time, we would have started with an icebreaker, asking participants to share their best moments of the event to liven up the atmosphere. However, due to time constraints, the retrospective kicked off with a positive summary from the event leader. After the Icebreaker, we explained the “magic rules” (ground rules) to make it easier for everyone to share their thoughts and ensure the best retrospective experience. The following rules were read aloud to all participants: Be active in the conversation! No matter how trivial it may seem, don't hesitate to share it! Don't monopolize the conversation! Do not interrupt others while they’re speaking! Remember that even those who are quiet have something valuable to contribute Trust that everyone did their best Focus on finding causes, not assigning blame Frame it as “the problem versus us” Appreciate the opportunity to learn from mistakes and celebrate our growth! After reading the ground rules, the main retrospective began. The retrospective: KPT implementation Thanks to the contributions made by participants beforehand, we were able to start the session with a clear sense of direction. We began by reviewing the submitted entries one by one. An essential part of the process is having the person who wrote each entry read it aloud. While the facilitator could do this, it’s important for the group to understand the intent and emotions behind the words. That’s why we prioritize self-reading as much as possible. Process: The author reads their entry aloud. The facilitator reflects on the content, organizes it, and opens the floor for comments, additions, and feedback. Consolidate the input and update the Wiki to keep everything organized. For any proposed actions, achieve consensus and add them as a “Try” item. We followed this flow and kept the discussion lively and engaging for as long as time allows. At the end, we celebrated the completion of the event by finalizing the Wiki, enriched with action items (“Try”). A retrospective built together Reflecting on the retrospective Through this retrospective, we were able to create actionable items for the next similar event, leaving a legacy for our future selves to build upon. At the same time, we believe the retrospective served as a space to improve the “quality of relationships” and the “quality of thinking” among participants. By repeating these collaborative events, we aim to create a “positive cycle of success”. Better actions will lead to better results, ultimately strengthening the organization as a whole. Closing thoughts Thanks to the autonomy and dedication of each team member, we were able to host another enjoyable event with no major issues, just like last time! Now that we have established a track record of holding offline/online hybrid events, we plan to leverage this knowledge for future opportunities. This time, we replicated the approach from the first event to achieve the same results with less effort. However, it made me reflect on balancing "standardization" with "creativity." If a task is merely "something to do" without requiring creativity, it's better to standardize it through manuals (as we did this time). However, with each event held by KINTO Technologies, whether it’s managing the reception or arranging light meals, I’ve seen firsthand how creativity and ingenuity make these processes more attractive and efficient. Rather than attempting to overly systematize everything, perhaps it’s more important to create an environment where individuals are empowered to grow through their own autonomy and ingenuity. We also incorporated online participation to reach as many people as possible. However, I found it incredibly valuable to share challenges with other companies’ IT teams during in-person social gatherings. As we move forward, I would like to continue hosting events while carefully considering whether to stick with the hybrid format or focus solely on on-site events.
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の20日目の記事です🎅🎄 Impact Effort Matrix(インパクト・エフォートマトリックス)を使って社内交流を実践してみた こんにちは、KINTO テクノロジーズの技術広報グループに所属しているMayaと木下です。 はじめに 私たちは過去に神保町で社内交流会を開催し、オフィス内の交流不足を解消し、チーム間のつながりを強化することを目指しました。 実施したときの記事はこちら↓ https://blog.kinto-technologies.com/posts/2023-09-19-JimbochoISM/ このイベントでは、企画段階にImpact/Effort Matrix(インパクト・エフォートマトリックス)を活用して、催しの内容を決定しました。 これにより、イベントを盛り上げるためのアイデアを整理し、タスクの優先順位を明確にすることができました。 その結果、複数回のイベントをスムーズに運営することができました。 この記事では、その時の経験を基に、Impact/Effort Matrixを用いたイベントの企画・実践、振り返りまでのアプローチを紹介します。 本記事が、読者の皆さんのプロジェクトやイベント計画に役立てば幸いです。 ぜひ、最後までお楽しみください。 Impact/Effort Matrixについて Impact/Effort Matrix(インパクト・エフォートマトリックス)は、プロジェクトやタスクの優先順位を効率的に決定するためのシンプルかつ効果的なツールです。 このマトリックスは、タスクやアイデアを 「成果(Impact)」 と 「労力(Effort)」 の2軸で分類することで、どの項目にリソースを集中させるべきかを視覚的に判断できます。 基本構造 Impact/Effort Matrix は4つの象限で構成されます: Quick Wins(すぐに実行すべきタスク) 高い成果をもたらし、必要な労力が少ないタスク 優先的に取り組むべきです Major Projects(戦略的に取り組むべきタスク) 高い成果が期待できるが、労力も多く必要なタスク リソース計画が重要になります Fill-ins(余裕があるときに実施するタスク) 労力が少なく成果も小さいタスク 優先順位は低めです Parking Lot(避けるべきタスク) 成果が低く、労力が多いタスク 基本的に取り組むべきではありません さらに詳しくは Miroのテンプレート説明ページ をご参照ください。 なぜ導入したのか 第1回目の神保町共有会開催後、それなりに良い感想をいただくことができました。 しかし、参加者全員が一体感を持てているわけではなかったため、もっと「ワイワイ感」を出すにはどうすればいいのか?というブレインストーミングをしたかったのです。 運営メンバーで議論し合いながら、目指す目的を明確にし、やるべきタスクとやらないタスクを選定しました。 そして、優先順位をメンバーで共有するための適切な手法を探している中、メンバーが過去に利用経験のあるImpact/Effort Matrixを用いることにしました。 活用して感じたメリット 優先順位付けが明確になる: 4象限に相対的に全ての課題を配置するので、議論を進め、付箋を並べるにつれて、優先するべきタスクが視覚的に浮き上がってくる 容易に共有でき、全員の理解が一致しやすい: 上記の延長線ではありますが、何を優先すべきかを議論するきっかけにもなるため、認識をチームで合わせる時間になります 全員のコンセンサスを得ながら進めるため、コミットメントも高く、全員が自分ごとと捉えて進めやすい チームのリソースを最適化できる: 次のアクションへも全員が納得する落とし込みにもつながるため、出戻りの発生を防げます 苦労したところ・工夫したところ よくある話ではありますが、一番チームで苦労したところは意思決定の部分でした。 2回実施したImpact/Effort Matrixの初回では、意見や共通認識の課題が多く出たものの、その情報をどう整理し、次のイベントの改善に繋げるかのコンセンサスに至るまでたくさん議論を重ねる必要がありました。 整理した内容の粒度がバラバラで、やりたいことのスコープも広かったため、思うように成果を出せませんでした。 2回目では、上記の経験を踏まえて課題の粒度を揃え、目的を明確にし、スコープを絞ることで、より具体的な結果を得ることができました。 メンバーの理解が深まり、プロセスをスムーズに進めることができました。 イベントの方向性をより具体化することができて、二回目も無事に成功し、神保町共有会の三回目に向けたアクションプランを策定し、Jiraボードでタスクを見える化しました。 この時、新しいメンバーが数名加わり、初めてIEMを体験する人たちでした。 未経験者を多く含む状況ながらも、これまでの経験からスムーズに進行することができ、三回目の神保町共有会を盛り上げることができました。 Impact/Effort matrixやってみての感想 Impact/Effort Matrixの手法を理解しても、実際に自分たちの状況に当てはめて実践しようとすると、最初は本当にこれで良いのかと迷うことがあり、チーム全体に不安が広がることもありました。 しかし、違和感を覚える点や改善すべき点について、皆が意見を出し合い、それを尊重し合うことで、私たちは様々なアイデアを整理し、どのタスクを優先すべきかを明確にすることができました。 このImpact/Effort Matrixの手法が上手く機能するまでの過程では、意見の対立もありましたが、それぞれのアイデアを客観的に評価することで、全員が納得できる形で進めることができました。 イベントに向けて準備が進むにつれ、チームとしての一体感が高まり、最終的には社内交流会の参加者から高い評価を得ることができました。 終わりに 今回の記事では、神保町での社内交流会を例に、Impact/Effort Matrixを活用したイベントの企画・実践、振り返りのアプローチをご紹介しました。 初めは手探りで不安もあり、うまくできているのか自信が持てない部分もありましたが、何度か実践を重ねる中で次第に慣れ、運営メンバーが途中で増えたにもかかわらず、スムーズに運営できるようになりました。 この手法が、皆さんのプロジェクトやイベントの成功に役立つことを願っています。 皆さんのプロジェクトやイベントの参考になれば幸いです。 KINTO テクノロジーズでは、一緒に働く仲間を広く募集しています。 ご興味を持たれた方は、ぜひお気軽にご連絡ください。お待ちしております! https://hrmos.co/pages/kinto-technologies/jobs
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の20日目の記事です🎅🎄 はじめに はじめまして、KINTOテクノロジーズ( KTC )でモバイルアプリ(Flutter)の開発を担当しているHand-Tomiです。 最近、KTCでは「Flutterチーム」を立ち上げ、アプリケーション開発を進めています。その中で、導入して特に効果的だと感じた手法をいくつかご紹介したいと思います。 今回は、「GitHub Actions」と「Firebase Hosting」を活用して、コードレビュー時に便利なWebプレビューを実現する方法について解説します。 皆さんの参考になれば幸いです。 🎯 目標 この記事の目的は、プルリクに /preview というコメントを追加することで、デバッグ用のWebページリンクが自動でコメントとして投稿される仕組みを実現することです。 ![preview_comment_and_link](/assets/blog/authors/semyeong/2024-12-20-flutter-web-preview/preview_comment_and_link.png =600x) 上記のリンクをクリックすると、以下のようにFlutterプロジェクトのアプリケーションが表示されます。 ![preview](/assets/blog/authors/semyeong/2024-12-20-flutter-web-preview/preview.png =400x) 🔍 Webプレビューを使う理由 コードレビューする際、動作を確認するにはソースコードをクローンし、設定やビルドを行う必要がありますが、この方法は時間がかかります。一方、 Webプレビュー を設定しておくと、簡単かつ迅速に動作確認を行うことが可能です。 以降では、この仕組みを実現する方法をstep-by-stepでご紹介します。 Firebaseのセットアップ 🌐 Firebaseプロジェクトを作成 まだFirebaseプロジェクトがない場合、Firebaseコンソールから新しいプロジェクトを作成してください。 プロジェクト名は「sample」としました。 他の機能を使用する予定がない場合は無効にしておきます(有効にしておいても問題ありません)。 しばらく待つと プロジェクトの作成が完了しました! ⚙️ Firebase CLIのセットアップ FlutterプロジェクトにFirebase Hostingを設定する予定です。Firebase CLIを使用すれば簡単に設定できるので、Terminalでセットアップしてみましょう。 1. Firebase CLIのインストール Firebase CLIのインストール方法はいくつかありますが、npmがインストールされているMacOSでは、以下のコマンドを使用することで簡単にインストールできます。 npm install -g firebase-tools 他の環境でのインストール方法は、 こちら を参照してください。 2. Firebase CLIへのログイン 以下のコマンドを実行して、CLI上でFirebaseにログインしてください。 firebase login 🔧 Firebase Hostingのセットアップ 準備が整ったので、FlutterプロジェクトにFirebase Hostingを設定してみましょう。 1. webframeworksの有効化 FlutterアプリケーションをFirebase Hostingにデプロイするためには、実験的な機能である webframeworks を有効にする必要があります。 firebase experiments:enable webframeworks 2. Firebase Hostingの初期化 Flutterプロジェクトのルートディレクトリで、以下のコマンドを実行してFirebaseをセットアップしましょう。 firebase init hosting 上記のコマンドを実行すると、以下のような質問が表示されます。 # Firebaseプロジェクトは先ほど作成したsampleプロジェクトを選択します。 ? Please select an option: Use an existing project ? Select a default Firebase project for this directory: sample-1234 (sample) # こちらは「Yes」で問題ありません。 ? Detected an existing Flutter Web codebase in the current directory, should we use this? Yes # リージョン選択の質問です。デフォルトの「us-central1 (Iowa)」を選択しました。 ? In which region would you like to host server-side content, if applicable? us-central1 (Iowa) # 自分で作成する予定なので「No」にしました。 ? Set up automatic builds and deploys with GitHub? No i Writing configuration info to firebase.json... i Writing project information to .firebaserc... ✔ Firebase initialization complete! 質問に回答すると、 firebase.json ファイルが生成されます。 :::message alert FlutterプロジェクトにWebプラットフォームが含まれていない場合、エラーが発生することがあります。その場合は、以下のコマンドを実行してWebプラットフォームを追加してください。 flutter create . --platform web ::: 3. デプロイ 試しにデプロイしてみましょう。 firebase deploy 上記のコマンドを実行すると、以下のようにHosting URLが表示されます。 ... ✔ Deploy complete! Project Console: https://console.firebase.google.com/project/sample-1234/overview Hosting URL: https://sample-1234.web.app このURLを開くと、Flutterプロジェクトが正常に表示されることを確認できます。 GitHub Actionsの作成 次に、プルリクエストに /preview とコメントすると実行されるYAMLファイルを作成しましょう。 🔑 Firebaseサービスアカウントキーの準備 GitHub Actionsを通じてFirebaseにデプロイするためには、Firebaseサービスアカウントのキーが必要です。簡単にキーを取得する方法は、以下のコマンドを使用することです。 firebase init hosting:github 上記のコマンドを入力すると、以下のような質問が表示されます。ソースコードがあるリポジトリを user/repository の形式で指定してください。 # Github repositoryを入力してください。 `user/repository`のように記載してください。 ? For which GitHub repository would you like to set up a GitHub workflow? (format: user/repository) Hand-Tomi/sample すると、Firebaseが自動的にGiGitHubポジトリのSecretsにサービスアカウントキーを設定し、以下のようにSecretsの定数名を教えてくれます(例: FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 )。この定数名は保存しておきましょう。 ✔ Created service account github-action-1234 with Firebase Hosting admin permissions. ✔ Uploaded service account JSON to GitHub as secret FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234. i You can manage your secrets at https://github.com/Hand-Tomi/sample/settings/secrets. 続いて、以下のような質問が出てきますが、必要なものは揃っているので Control+C (Windowsの場合は Ctrl+C )で終了します。 ? Set up the workflow to run a build script before every deploy? ✍️ GitHub ActionsのYAMLファイル作成 いよいよGitHub ActionsのYAMLファイルを作成しましょう。 Flutterプロジェクトのルートにある .github/workflows ディレクトリにYAMLファイルを作成し、以下のコードを入力します。 name: Command Execute Deploy Web on: issue_comment: types: [created] jobs: deploy-web: if: ${{ github.event.issue.pull_request && github.event.comment.body == '/preview' }} name: Deploy Web runs-on: ubuntu-latest concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: true steps: - uses: actions/checkout@v4 with: ref: refs/pull/${{ github.event.issue.number }}/head fetch-depth: 0 - name: Set Up Flutter uses: subosito/flutter-action@v2 with: channel: 'stable' - name: Install Dependencies run: flutter pub get - id: deploy-web name: Deploy to Firebase Hosting uses: FirebaseExtended/action-hosting-deploy@v0 with: firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 }} expires: 7d channelId: "issue_number_${{ github.event.issue.number }}" env: FIREBASE_CLI_EXPERIMENTS: webframeworks - name: Comment on Success if: success() uses: peter-evans/create-or-update-comment@v4 with: token: ${{ secrets.GITHUB_TOKEN }} issue-number: ${{ github.event.issue.number }} body: | ✅ Previewがデプロイされました。 - **リンク** : ${{ steps.deploy-web.outputs.details_url }} firebaseServiceAccount には、事前に作成しておいたSecretsの定数名を指定してください(例: FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 )。 firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 }} その後、該当リポジトリにマージし、プルリクエストに /preview とコメントを残すと、自動的にActionsが実行され、GitHub Actionsがリンクをコメントしてくれます。 💡 参考:GitHub ActionsのYAMLコードの説明 上記のYAMLコードについて解説します。 実行タイミング on: issue_comment: types: [created] issue_comment を使用すると、コメントが作成されたときに自動的にWorkflowが実行されます。このコメントはプルリクエストだけでなく、Issueにコメントされた場合も含まれます。 今回の記事ではプルリクエストのコメントに限定したいので、以下のように jobs の if に github.event.issue.pull_request を入れて、プルリクエストのみ実行するようにします。 jobs: deploy-web: if: ${{ github.event.issue.pull_request && github.event.comment.body == '/preview' }} また、 issue_comment を使う場合、Checkoutする場所を変更する必要があります。 issue_comment は現在のプルリクエストの最新コミット情報を持っていないため、そのままCheckoutするとデフォルトブランチの最新コミットにCheckoutされてしまいます。 そのため、以下のように actions/checkout に ref を指定する必要があります(参考: https://github.com/actions/checkout/issues/331#issuecomment-1438220926 )。 - uses: actions/checkout@v4 with: ref: refs/pull/${{ github.event.issue.number }}/head fetch-depth: 0 コメントメッセージ確認 コメントのメッセージが /preview かどうかを確認します。以下のように github.event.comment.body を確認し、メッセージが /preview の場合のみdeploy-webジョブ内の処理を実行します。 jobs: deploy-web: if: ${{ github.event.issue.pull_request && github.event.comment.body == '/preview' }} 同時実行の防止 /preview を残した後、すぐに再度 /preview をコメントすると、同時実行になる可能性があります。 concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: true この場合、GitHub Actionsは concurrency を通じて同時実行を防止します。 重要なのは、 jobs の下に配置することです。 jobs: deploy-web: if: ${{ github.event.issue.pull_request && github.event.comment.body == '/preview' }} name: Deploy Web runs-on: ubuntu-latest concurrency: group: ${{ github.workflow }}-${{ github.event.issue.number }} cancel-in-progress: true 上記のように jobs の下に配置しないと、 if で github.event.comment.body == '/preview' を確認せずに concurrency が実行され、 /preview コメント後すぐに /preview 以外のコメントを残した場合、Actionが実行されなくなります。 デプロイ 以下のステップはFirebase Hostingにデプロイするものです。 - id: deploy-web name: Deploy to Firebase Hosting uses: FirebaseExtended/action-hosting-deploy@v0 with: firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 }} expires: 7d channelId: "issue_number_${{ github.event.issue.number }}" env: FIREBASE_CLI_EXPERIMENTS: webframeworks firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SAMPLE_1234 }} : Firebaseのサービスアカウントの認証キーです。以前に取得しておいたものを入力してください。 expires: 7d : 有効期限です。このように設定するとプレビューサイトは7日後に無効になります。 channelId: "issue_number_${{ github.event.issue.number }}" : Firebase Previewのチャンネル名です。 live 以外の channelId を指定すると、Firebase Previewにデプロイされ、有効期限を設定できます。 FIREBASE_CLI_EXPERIMENTS: webframeworks : Firebase CLIの実験的な機能である webframeworks を使用します。Flutter Webの場合は必須です。 リンクコメント peter-evans/create-or-update-comment を使用して、リンクのコメントを残しました。これを使うと、簡単にリアクションを残したり、コメントを追加・編集できます。 - name: Comment on Success if: success() uses: peter-evans/create-or-update-comment@v4 with: token: ${{ secrets.GITHUB_TOKEN }} issue-number: ${{ github.event.issue.number }} body: | ✅ Previewがデプロイされました。 - **リンク** : ${{ steps.deploy-web.outputs.details_url }} if: success() : 成功したときのみこのステップが実行されます。 token: ${{ secrets.GITHUB_TOKEN }} : コメントを残すためには GITHUB_TOKEN が必要です。別途の設定は必要ありません。 issue-number: ${{ github.event.issue.number }} : どのIssueにコメントするかの指定です。 issue_comment で実行したWorkflowの場合、 github.event.issue.number でIssue番号を確認できます。 steps.deploy-web.outputs.details_url : 上記のデプロイステップから取得したURLを表示します。 他の情報を載せたい場合は、 こちら を参照してください。 おわりに 今回ご紹介した手法により、コードレビュー時の動作確認がこれまでよりも迅速かつ簡単に行えるようになります。チーム全体の開発効率が向上し、より良いプロダクトの提供につながるでしょう。 ただし、デバッグ用のWebプレビューを導入する際には、セキュリティ面への配慮が必要であり、OS特有の差異をどのように解決するかも検討する必要があります。OSの機能を多用する場合、メリットよりもデメリットが大きいかもしれません。 しかし、OSの機能をあまり使用しないプロジェクトでは、デメリットよりもメリットが多いため、ぜひ皆さんの開発環境にも導入してみてください。 また、flutterチームのメンバーが執筆した他の記事もぜひご覧ください! Flutter開発: CustomPaintとPathでQRコードの枠線をデザインする ここまで読んでいただき、ありがとうございました。
アバター
はじめに こんにちは🎄クラウドインフラグループに所属している島川です。クラウドインフラグループではAWSをはじめとした社内全体のインフラ領域の設計から運用まで担当しています。弊社でも様々なプロダクトで生成AIの活用が進んできておりクラウドインフラグループとしても様々な支援を実施しています。 本記事ではAmazon BedrockのKnowledge BaseをTerraformで構築した際の情報を共有します。またre:Invent 2024で発表された RAG評価 にも触れていきたいと思います。 構成 今回構築する構成はこちらです。 Bedrock Knowledge BaseのVector storeとしてOpenSearch Serverlessを利用し、データソースはS3を指定します。 Terraformで構築 ディレクトリ構造はこちら。それぞれ内容を説明していきます。 なお今回使用しているTerraformバージョンは 1.7.5 です。 $ tree . ├── aoss.tf # OpenSearch Serverless ├── bedrock.tf # Bedrockリソース ├── iam.tf # iam ├── s3.tf # bedrock用のS3 ├── locals.tf # 変数定義 ├── provider.tf # provider定義 └── terraform.tf # Backendなどの設定 ここでは変数定義をしています。 locals { env = { environment = "dev" region_name = "us-west-2" sid = "test" } aoss = { vector_index = "vector_index" vector_field = "vector_field" text_field = "text_field" metadata_field = "metadata_field" vector_dimension = 1024 } } AWS provider、OpenSearch providerのバージョンを指定かつtfstateを保存するバックエンドS3を指定しています。ここで使用しているS3は別途手動で作成したものなので今回のterraformコードには含まれていません。 terraform { required_providers { # https://registry.terraform.io/providers/hashicorp/aws/ aws = { source = "hashicorp/aws" version = "~> 5.0" } opensearch = { source = "opensearch-project/opensearch" version = "2.2.0" } } backend "s3" { bucket = "***-common-bucket" region = "ap-northeast-1" key = "hogehoge-terraform.tfstate" encrypt = true } } AWSとOpenSearchのプロバイダーを定義します。OpenSearchのプロバイダーはindexを追加するために使用します。 provider "aws" { region = local.env.region_name default_tags { tags = { SID = local.env.sid Environment = local.env.environment } } } provider "opensearch" { url = aws_opensearchserverless_collection.collection.collection_endpoint aws_region = local.env.region_name healthcheck = false } OpenSearch Serverlessのリソースとindexを作成します。 Deploy Amazon OpenSearch Serverless with Terraform を参考にしました。 今回はセキュリティポリシーでpublicにしていますが、VPCエンドポイントで制御するのが理想です。 data "aws_caller_identity" "current" {} # Creates a collection resource "aws_opensearchserverless_collection" "collection" { name = "${local.env.sid}-collection" type = "VECTORSEARCH" standby_replicas = "DISABLED" depends_on = [aws_opensearchserverless_security_policy.encryption_policy] } # Creates an encryption security policy resource "aws_opensearchserverless_security_policy" "encryption_policy" { name = "${local.env.sid}-encryption-policy" type = "encryption" description = "encryption policy for ${local.env.sid}-collection" policy = jsonencode({ Rules = [ { Resource = [ "collection/${local.env.sid}-collection" ], ResourceType = "collection" } ], AWSOwnedKey = true }) } # Creates a network security policy resource "aws_opensearchserverless_security_policy" "network_policy" { name = "${local.env.sid}-network-policy" type = "network" description = "public access for dashboard, VPC access for collection endpoint" policy = jsonencode([ ###VPC エンドポイントを利用する際の参考 # { # Description = "VPC access for collection endpoint", # Rules = [ # { # ResourceType = "collection", # Resource = [ # "collection/${local.env.sid}-collection}" # ] # } # ], # AllowFromPublic = false, # SourceVPCEs = [ # aws_opensearchserverless_vpc_endpoint.vpc_endpoint.id # ] # }, { Description = "Public access for dashboards and collection", Rules = [ { ResourceType = "collection", Resource = [ "collection/${local.env.sid}-collection" ] }, { ResourceType = "dashboard" Resource = [ "collection/${local.env.sid}-collection" ] } ], AllowFromPublic = true } ]) } # Creates a data access policy resource "aws_opensearchserverless_access_policy" "data_access_policy" { name = "${local.env.sid}-data-access-policy" type = "data" description = "allow index and collection access" policy = jsonencode([ { Rules = [ { ResourceType = "index", Resource = [ "index/${local.env.sid}-collection/*" ], Permission = [ "aoss:*" ] }, { ResourceType = "collection", Resource = [ "collection/${local.env.sid}-collection" ], Permission = [ "aoss:*" ] } ], Principal = [ data.aws_caller_identity.current.arn, iam_role.bedrock.arn, ] } ]) } resource "opensearch_index" "vector_index" { name = local.aoss.vector_index mappings = jsonencode({ properties = { "${local.aoss.metadata_field}" = { type = "text" index = false } "${local.aoss.text_field}" = { type = "text" index = true } "${local.aoss.vector_field}" = { type = "knn_vector" dimension = "${local.aoss.vector_dimension}" method = { engine = "faiss" name = "hnsw" } } } }) depends_on = [aws_opensearchserverless_collection.collection] } Knowledge Baseとデータソースの作成をします。 data "aws_bedrock_foundation_model" "embedding" { model_id = "amazon.titan-embed-text-v2:0" } resource "aws_bedrockagent_knowledge_base" "this" { name = "test-kb" role_arn = iam_role.bedrock.arn knowledge_base_configuration { type = "VECTOR" vector_knowledge_base_configuration { embedding_model_arn = data.aws_bedrock_foundation_model.embedding.model_arn } } storage_configuration { type = "OPENSEARCH_SERVERLESS" opensearch_serverless_configuration { collection_arn = aws_opensearchserverless_collection.collection.arn vector_index_name = local.aoss.vector_index field_mapping { vector_field = local.aoss.vector_field text_field = local.aoss.text_field metadata_field = local.aoss.metadata_field } } } depends_on = [iam_role.bedrock] } resource "aws_bedrockagent_data_source" "this" { knowledge_base_id = aws_bedrockagent_knowledge_base.this.id name = "test-s3-001" data_source_configuration { type = "S3" s3_configuration { bucket_arn = "arn:aws:s3:::****-dev-test-***" ### バケット名マスク } } depends_on = [aws_bedrockagent_knowledge_base.this] } bedrockが使用するサービスロールを設定します。 resource "aws_iam_role" "bedrock" { name = "bedrock-role" managed_policy_arns = [aws_iam_policy.bedrock.arn] assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Sid = "" Principal = { Service = "bedrock.amazonaws.com" } }, ] }) } resource "aws_iam_policy" "bedrock" { name = "bedrock-policy" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = ["bedrock:InvokeModel"] Effect = "Allow" Resource = "*" }, { Action = [ "s3:GetObject", "s3:ListBucket", ] Effect = "Allow" Resource = "***-dev-test-***" ### 作成したS3のARN }, { Action = [ "aoss:APIAccessAll", ] Effect = "Allow" Resource = "arn:aws:aoss:us-west-2:12345678910:collection/*" }, ] }) } Bedrockで使用するS3バケットを作成します。またCORSの設定もしておきます。下記エラー参考画像です。 resource "aws_s3_bucket" "bedrock" { bucket = "***-dev-test-***" ### マスキング } resource "aws_s3_bucket_cors_configuration" "this" { bucket = aws_s3_bucket.bedrock.id cors_rule { allowed_headers = ["*"] allowed_methods = [ "GET", "PUT", "POST", "DELETE" ] allowed_origins = ["*"] } } 実行 terraform apply で一括で作成します。 作成されたものの確認 BedrockでKnowledge Baseが作成されていてデータソースがあることを確認します。 次にOpenSearchのコレクションが作成されていることとindexが設定されていることを確認します。 Knowledge baseの動作確認 S3にデータソースとして使用する適当なテキストを送ります。 犬が好きなものは肉です 猫が好きなものは魚です aws s3 cp ./test001.txt s3://[S3 Bucket Name]/test001.txt 次にデータソースの同期を実施します。 実際に質問を投げかけてみます。(PromptはClaude 3.5 Sonnetを使用) テキストに記載された回答を答えてくれているのと記載されていない内容については回答はしないようになっています。 簡単ではありますが以上Terraformを用いたKnowledge BaseとOpenSearch Serverlessの内容でした。 RAG評価を試してみる 次にここで作成したKnowledge Baseに対してre:Invent 2024で発表された RAG評価 を試してみます。 @ card 事前準備 評価のためのデータセットファイル(jsonl)を用意します。ここではプロンプトとそれに期待する答えを記載します。 {"conversationTurns":[{"referenceResponses":[{"content":[{"text":"猫の好きな食べ物は魚です"}]}],"prompt":{"content":[{"text":"猫の好きなものは?"}]}}]} {"conversationTurns":[{"referenceResponses":[{"content":[{"text":"犬の好きな食べ物は肉です"}]}],"prompt":{"content":[{"text":"犬の好きなものは?"}]}}]} S3から参照させるのでテキストをS3にコピーします。 aws s3 cp ./dataset001.txt s3://[S3 Bucket Name]/datasets/dataset001.txt jobの作成 次にjobを作成します。 マネジメントコンソールからでもOKですが今回はCLIで実施しました。 @ card aws bedrock create-evaluation-job \ --job-name "rag-evaluation-complete-stereotype-docs-app" \ --job-description "Evaluates Completeness and Stereotyping of RAG for docs application" \ --role-arn "arn:aws::iam:<region>:<account-id>:role/AmazonBedrock-KnowledgeBases" \ --evaluation-context "RAG" \ --evaluationConfig file://knowledge-base-evaluation-config.json \ --inference-config file://knowledge-base-evaluation-inference-config.json \ --output-data-config '{"s3Uri":"s3://docs/kbevalresults/"}' knowledge-base-evaluation-config.json 内でjsonlのファイルと結果の保存先の指定が必要です。 jobの確認 15分~20分ほど待ってjobが完了したので中身を見てみます。 まずはsummaryから確認します。ほぼ答え通りのことを書いているので面白みはないですがCorrectness(正確さ)、Completeness(完全)が1になっていて期待通りの動きになっていることがわかります。 Helpfulness(有用性)のみ0.83という数字ですが評価コメントを確認すると 解答は特に興味深いものでも、予想外のものでもないが、この文脈ではそうである必要はない。 文脈がこれである必要はないということがスコアを下げている原因なのではないかと考えられます。 最後に 弊社内でもBedrock含め生成AIの活用が進んできており実際に利用シーンも増えてきています。今後も様々な機能に触れながらプロジェクトの要件を満たせるよう準備を進めていきたいです。このブログが少しでも参考になればうれしいです。
アバター
Hello! This is Guo from KINTO Technologies’ Generative AI Utilization Project. How does your company manage AWS resources? Several options are available, including Terraform, the AWS CLI, or manual operations through the AWS Console. This time, I harnessed the capabilities of generative AI to develop a system that allows you to manage AWS resources using natural language commands directly in Slack. The backend utilizes Agents for Amazon Bedrock (hereafter referred to as "Bedrock") to handle resource management seamlessly. System overview The overall structure is shown in the diagram below. System overview How it works Users enter natural language commands in Slack, and the backend, powered by Bedrock, processes these commands to create or delete S3 buckets. How it works Steps to create the system The system can be built in three steps: Create an Agent on Bedrock Set up AWS Chatbot Configure Slack Below, I’ll explain each step in detail. By following these steps, you’ll be able to build the same system yourself, so please give it a try! 1. Create an Agent on Bedrock Open the Bedrock Management Console. Click “Agents” in the left menu. Click “Create Agent” Enter a name and click “Create” You will be taken to the Agent Builder screen. Select a model, such as Claude 3 or Sonnet (you can choose any model you prefer) . Click “Save and Exit” in the top-right corner. Click “Prepare” on the right side. A message saying "Successfully prepared" will appear. Adding an Action Group. Click "Add" in the upper right corner of the Action Groups section. Configure the Action Group. ・Enter an Action Group Name. ・Set the Action Group Type to "Define using function details". ・Under “Specify how to define the Lambda function", select "Quickly create a new Lambda function (recommended)" ・For the Action Group Invocation, choose recommended option: "Quickly create a new Lambda function (recommended)" Adding and configuring Action Group Functions ・ Create the following Action Group Functions: “delete-ai-agent-gu-function" and "create-ai-agent-gu-function" ・Configure each function in the “Description (optional) field as follows: "delete S3 bucket posted bucket name" and "create S3 bucket posted bucket name" ・Add a parameter as following. The name should be bucket_name, the description should be S3 bucket name, the type should be String, and the mandatory value should be True. Adding Instructions for the Agent ・ Open the Agent edit screen and enter the following in the “Agent Instructions” field: You are an agent working with an S3 bucket. Use the appropriate functions to create or delete S3 buckets based on user requests. Task 1: If a user submits a request such as "Please create an S3 bucket named test-gu," trigger the Lambda function named create-ai-agent-gu-function Task 2: If the user requests something like “Please delete an S3 bucket named test-gu,” execute the Lambda function named delete-ai-agent-gu-function. Creating Lambda Functions Access the Lambda console. Since the option to quickly create a new Lambda function was selected, dummy lambda function has been automatically created. Add the code for creating and deleting S3 buckets to dummy_lambda.py import json import boto3 AWS_REGION = "ap-northeast-1" s3Client = boto3.client("s3",region_name=AWS_REGION) location = {"LocationConstraint":AWS_REGION} def lambda_handler(event, context): agent = event["agent"] actionGroup = event["actionGroup"] function = event["function"] parameters = event.get("parameters", []) # Execute your business logic here. For more information, # refer to: https://docs.aws.amazon.com/bedrock/latest/userguide/agents-lambda.html bucket_name = next(item for item in parameters if item["name"] == "bucket_name")["value"] if function == 'delete-ai-agent-gu-function': bucket_instance=s3Client.delete_bucket(Bucket=bucket_name) responseBody = { "TEXT": { "body": f"Instance Deleted: {str(bucket_instance)}" } } elif function == 'create-ai-agent-gu-function': bucket_instance=s3Client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=location) responseBody = { "TEXT": { "body": f"Instance Created: {str(bucket_instance)}" } } action_response = { "actionGroup": actionGroup, "function": function, "functionResponse": { "responseBody": responseBody }, } function_response = {"response": action_response, "messageVersion": event["messageVersion"]} print(f"Response: {function_response}") return function_response Extract the function value from the event dictionary and route the process to the corresponding predefined action group function, either create-ai-agent-gu-function or delete-ai-agent-gu-function. Grant the Lambda function permissions to manage S3 buckets by attaching the required policies to its execution role. Click Deploy (Ctrl+Shift+U) on the left side. Return to the Agent page, click “Create Alias” at the top. Enter an “Alias Name,” and click “Create Alias.” The alias will be created. This completes the agent setup. 2. Set up AWS Chatbot Open the Chatbot configuration page in the AWS Console. Click "Set up a new client." Select “Slack” as the chat client, and click "Configure." Authorize AWS Chatbot to access your Slack workspace Return to the Chatbot configuration page and click “Set up new channel.” Enter the Configuration Name and Channel ID. For permissions, follow these steps: Specify the IAM Role Name. Attach the AmazonBedrockFullAccess policy to the Channel Guardrail Policy. (For production environments, ensure to adhere to the principle of least privilege.) Click Save in the bottom-right corner. Click the link for the added configuration (in this case, ktc-gu-test). Click the link for the channel role. In the permission policy, click “Add permissions” and select “Attach Policies” Search for "AmazonBedrockFullAccess," select it from the list, and click "Add permissions." This completes the AWS Chatbot setup. 3. Configure Slack Finally, let’s configure Slack. Send the following message in the Slack channel: @aws connector add {Connector Name} {Bedrock agent's Agent ARN} {Bedrock agent's Alias ID} Once the connection is successful, the following message will appear. This completes the Slack setup. Now, let's test the system. System Verification: Enter an S3 operation command in Slack. Enter an operation command like this: @aws ask {Connector Name} {Prompt} S3 bucket creation and deletion should now work successfully! Summary In this demonstration, we showcased how to utilize AWS's generative AI agent service, Agents for Bedrock, to enable S3 bucket creation and deletion solely through natural language input. This approach demonstrates how various operations can now be executed effortlessly using natural language commands. Thank you, and see you next time!
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の19日目の記事です🎅🎄 KINTO Technologiesのモバイルアプリ開発グループに所属しているiOSエンジニアのGOSEOです。今担当しているアプリはUnlimitedです。 UnlimitedのiOS担当者の66%は外国籍の方です。日々、外国籍のiOSエンジニアの方と英会話を楽しんでます。 Paradoxが好きで、クルセイダーキングスⅢに今ハマってます。 UnlimitedアプリにおけるGoogle MapsからMapKitへのマイグレーション検証 近年、Appleマップの性能向上により、Google MapsからMapKitへの移行が注目されています。この変更により、利用料の削減やアプリの評価向上が期待できます。 本記事では、UnlimitedアプリでGoogle MapsからMapKitへ移行した際の具体的な実装方法、直面した課題、そして検証結果について詳しく紹介します。 Google MapsからMapKitへのマイグレーション検証とその過程 1. マップの描画とグラデーションラインの生成 Unlimitedでは、Google Maps上にグラデーションラインを描画しています。これをMapKitで実現するには、 MKGradientPolylineRenderer を使い、カラーをセットして、開始地点と終了地点を locations で指定することで可能かどうか検証しました。さらに、将来的にはユーザーの超過スピードに応じてラインの色を動的に変化させる実装にも使えるのではと思いました。 func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { if let polyline = overlay as? MKPolyline { let gradientRenderer = MKGradientPolylineRenderer(polyline: polyline) gradientRenderer.setColors( [Asset.Colors.primary.color, Asset.Colors.cdtRouteGradientLight.color], locations: [0.0, 1.0] ) gradientRenderer.lineWidth = 2.0 return gradientRenderer } return MKOverlayRenderer(overlay: overlay) } 2. タップ検知の違い Google Mapsでは、地図上でのタップやマーカーのタップイベントを簡単に取得できますが、MapKitにはそのためのAPIがありません。そのため、地図全体のタップ検知には UITapGestureRecognizer が必要でした。しかし、マーカーに対しては didSelect と didDeselect メソッドでタップ状態を把握できます。 マーカーをタップしたのか地図をタップしたのか制御しないのが厄介ですが.....タップ位置にマーカーが存在しているのか確認すれば大丈夫でした。 悩みポイント: 独自にジェスチャーを設定する必要があり、少し面倒ではありますが、なんとか動作は確認できました。 let tapGestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.handleMapTap(_:))) tapGestureRecognizer.delegate = context.coordinator mapView.addGestureRecognizer(tapGestureRecognizer) 3. マーカーの追加と管理 Unlimitedの地図上には、複数種類のマーカーが重なり合う場面があるため、 zPriority を活用して優先的に表示する仕組みを取り入れました。同じマーカー画像のインスタンスを使い回すことでインスタンスをマーカーごとに生成しなくて済むのでパフォーマンス向上も実現できました。 課題: デフォルトのタップアニメーションが消えない... 試行錯誤した結果、MKAnnotationViewに画像を追加せずに、 MKAnnotationViewにUIViewをaddSubviewで追加し、UIImageViewをaddSubviewでUIViewに追加し、UIImageViewに画像を追加することで 、アニメーションを無効にするという方法にたどり着きました。 この解決策は、まさにチームメイトの天の声でした! 4. レンダリングが遅い 大分から福岡まで走ったテスト走行データを投入して、マップ表示後、拡大縮小を繰り返すと、線のレンダリングが追いついていないバグがありました。座標位置データが23000個あり、地図の表示画面が切り替わるたびにレンダリングが発生していました。そのため、UIの更新においてメモリやCPUのリソース消費が激しくなっていました。。 課題: 座標位置が多いとレンダリングが追いつかない… Ramer-Douglas-Peuckerアルゴリズムを使って、類似している座標位置を削除し、ポリラインの簡略化と分割により、一本のポリラインに合体させる方法で乗り切れました。 // UIColorを補間する関数。fraction値に応じて2つの色の間を補間した色を返す func interpolateColor(fraction: CGFloat) -> UIColor { // 開始色と終了色のRGBAコンポーネントを取得 let fromComponents = Asset.Colors.primary.color.cgColor.components ?? [0, 0, 0, 1] let toComponents = Asset.Colors.cdtRouteGradientLight.color.cgColor.components ?? [0, 0, 0, 1] // fractionを基に色を補間 let red = fromComponents[0] + (toComponents[0] - fromComponents[0]) * fraction let green = fromComponents[1] + (toComponents[1] - fromComponents[1]) * fraction let blue = fromComponents[2] + (toComponents[2] - fromComponents[2]) * fraction return UIColor(red: red, green: green, blue: blue, alpha: 1) } // 座標の配列からポリライン情報を生成する関数 func makePolylines(_ coordinates: [CLLocationCoordinate2D]) -> [PolylineInfo] { // 座標が空の場合は空配列を返す guard !coordinates.isEmpty else { return [] } // チャンクのサイズを計算(最小でも全体を1チャンクとする) let chunkSize = coordinates.count / 20 > 0 ? coordinates.count / 20 : coordinates.count var cumulativeDistance = 0.0 let totalDistance = coordinates.totalDistance() // 全体の距離を計算 var previousEndColor: UIColor = Asset.Colors.primary.color var previousEndCoordinate: CLLocationCoordinate2D? var polylines: [PolylineInfo] = [] // 座標をチャンクに分割し、各チャンクに対して処理を実行 let chunks = stride(from: 0, to: coordinates.count, by: chunkSize) .map { startIndex -> [CLLocationCoordinate2D] in // チャンクの座標を取得し、前のチャンクの最後の座標を追加 var chunk = Array(coordinates[startIndex..<min(startIndex + chunkSize, coordinates.count)]) if let lastCoordinate = previousEndCoordinate { chunk.insert(lastCoordinate, at: 0) } previousEndCoordinate = chunk.last return chunk } for chunk in chunks { let chunkDistance = chunk.totalDistance() // チャンクの距離を計算 let startFraction = cumulativeDistance / totalDistance // 開始点の割合を計算 cumulativeDistance += chunkDistance let endFraction = cumulativeDistance / totalDistance // 終了点の割合を計算 let startColor = previousEndColor let endColor = interpolateColor(fraction: CGFloat(endFraction)) // 終了色を補間で計算 previousEndColor = endColor // ポリラインを簡略化(高精度を維持しつつポイントを削減) let simplified = PolylineSimplifier.simplifyPolyline(chunk, tolerance: 0.00001) let polyline = MKPolyline(coordinates: simplified, count: simplified.count) // ポリライン情報をリストに追加 polylines.append(PolylineInfo( polyline: polyline, startFraction: startFraction, endFraction: endFraction, startColor: startColor, endColor: endColor )) } return polylines } // 座標を簡略化する関数(Ramer-Douglas-Peuckerアルゴリズムを実装) static func simplifyPolyline(_ coordinates: [CLLocationCoordinate2D], tolerance: Double) -> [CLLocationCoordinate2D] { // 座標数が2以下、またはtoleranceが0未満の場合はそのまま返す guard coordinates.count > 2 else { return coordinates } guard tolerance >= 0 else { return coordinates } var result: [CLLocationCoordinate2D] = [] var stack: [(startIndex: Int, endIndex: Int)] = [(0, coordinates.count - 1)] var include: [Bool] = Array(repeating: false, count: coordinates.count) include[0] = true include[coordinates.count - 1] = true // スタックを使用して再帰的に処理 while !stack.isEmpty { let (startIndex, endIndex) = stack.removeLast() let start = coordinates[startIndex] let end = coordinates[endIndex] var maxDistance: Double = 0 var currentIndex: Int? // 現在のラインに対して最も遠い点を探す for index in (startIndex + 1)..<endIndex { let distance = perpendicularDistance(point: coordinates[index], lineStart: start, lineEnd: end) if distance > maxDistance { maxDistance = distance currentIndex = index } } // 最も遠い点がtoleranceを超える場合、その点を含めて再分割 if let currentIndex, maxDistance > tolerance { include[currentIndex] = true stack.append((startIndex, currentIndex)) stack.append((currentIndex, endIndex)) } } // includeがtrueの座標だけを結果に追加 for (index, shouldInclude) in include.enumerated() where shouldInclude { result.append(coordinates[index]) } return result } // 点と線の間の垂直距離を計算する関数 private static func perpendicularDistance(point: CLLocationCoordinate2D, lineStart: CLLocationCoordinate2D, lineEnd: CLLocationCoordinate2D) -> Double { let x0 = point.latitude let y0 = point.longitude let x1 = lineStart.latitude let y1 = lineStart.longitude let x2 = lineEnd.latitude let y2 = lineEnd.longitude // 距離の計算式(2次元平面での点と直線の距離) let numerator = abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1) let denominator = sqrt(pow(y2 - y1, 2) + pow(x2 - x1, 2)) // 直線の長さが0の場合は距離を0とする return denominator != 0 ? numerator / denominator : 0 } 5. 検証結果と結論 MapKitでもUnlimitedアプリでGoogle Mapsを使用して実装されている同様の動作が可能であることが確認でき、Google Mapsからのマイグレーションは実現可能です。これにより、利用料の削減が期待できます。この調査を通じて、Unlimited iOSチーム全員のMapKitの技術理解も深まりました。 おわりに 今後、プロジェクトではMapKitを活用して開発を進めてまいります。 引き続き、さらなる改善を重ね、より良いサービスの提供を目指していきます!
アバター
Introduction This article shares insights and mindsets I have found valuable as a new leader. It’s written from my personal experience and intended for those transitioning into a leadership role or who have recently started navigating this path. In November 2023, I was assigned my first team leadership role at KTC (KINTO Technologies). Until then, I had virtually no prior experience in leadership, not even in my previous jobs. To make up for this lack of experience, I started studying “leadership and management mindsets” through books, web articles, and advice from my supervisors as soon as the assignment was confirmed. It would have been great to introduce everything I learned comprehensively. Still, some concepts felt too advanced, obvious, irrelevant to my current role, or simply unappealing. Rather than forcing these ideas on myself, I left them for future opportunities when they might resonate more. In this article, I’ll share eight ideas I’ve successfully “installed” into my current mindset. Although this is far from exhaustive, I hope some of these tips will provide helpful hints for your leadership journey. Collective Hunting and Sharing the Mammoths Some say that an organization’s greatest benefit is the sense of unity among its members. While truth exists, it is more of a secondary advantage. A group that hunted mammoth likely bonded over the shared experience of dividing the meat. The camaraderie was a byproduct as the meat took priority. Kodai Ando, The Mask of Leadership: A Mindset Shift from Individual Contributor to Manager The first book I picked up after being assigned the role of team leader was The Mask of Leadership . At first, I imagined leadership as something like "motivating team members" or "connecting with them on a deeper level." However, I vividly remember being struck by the pragmatic and somewhat cold approach of “Shikigaku” (a theory of organizational behavior). Among its many ideas, I was particularly impressed by the concept that individuals benefit only after the collective benefit is achieved. The correct order is this: First, the group achieves collective benefits. Then, individuals gain their share of those benefits. This perspective resonated with me immediately and has become one of the guiding principles for my leadership approach. Management Roles are "Goal Achievement" and "Group Maintenance" There are various perspectives on how to approach these roles, but one relevant framework is the "PM Theory," which highlights two core functions: performance and maintenance. First, the performance function is about achieving goals. It emphasizes the importance of setting and meeting objectives. On the other hand, the maintenance function is about group maintenance. This involves sustaining and energizing the team, ensuring its cohesion and vitality. An article titled Why Are Middle Managers Perpetually Overburdened? Common Pitfalls and Four Steps to Rebuild Management , highlights several challenges faced by managers. It is noted that environmental and societal changes have made both goal achievement and group maintenance increasingly difficult. The article points out that, recently, too much emphasis has been placed on group maintenance, which has hindered the development of managers capable of strategic planning and achieving goals. However, putting aside those difficult things, I was able to simply take the frame that “the job of a manager is to achieve goals and maintain the group.” I was able to clarify what I should do as a manager, which was good in terms of my mental health. Communicate the Purpose and Delegate the Method Some managers perceive their staff’s trial-and-error efforts as a “loss” and try to speed things up by teaching the “right answers” from the start or giving step-by-step instructions. This mindset is absolutely wrong. People only grow through experience. An organization that simply provides answers will ultimately slow down. Without the growth of its members, the organization’s overall speed and efficiency decline over time. Kodai Ando, The Mask of Leadership: A Mindset Shift from Individual Contributor to Manager For those who are capable, simply communicate the purpose and leave the rest to them For those who are slightly inexperienced, provide the purpose along with actionable hints, like “If it were me, I would do it this way.” And for those who are still immature, communicate both the purpose and the specific actions needed. Shu Yamaguchi, Project Management Taught by Foreign Consultants Micromanagement is generally a poor approach. I have understood this intuitively for some time, but I also recognized from my very first day as a team leader that I simply do not have the capacity to sustain micromanagement. Instead, I am constantly seeking non-micromanagement approaches. My aim is to clearly define the desired outcome and the requirements for success, then delegate the rest to my team members. At the same time, I support them as needed to ensure they can move forward independently. If I Enjoy the Work, It Doesn’t Feel Like a Burden. If I enjoy the work, it doesn’t feel like a burden. It varies from person to person, and isn’t something that can be measured by volume alone. Understanding what kinds of tasks each member enjoys can be incredibly helpful. It’s good to properly manage tasks and workload, but it often lacks significant impact relative to the effort it requires. I once sought advice from my manager about understanding team members’ workload capacity. When I asked, "Should I track everyone’s tasks in detail to understand their workload better?" The response to the question was insightful: "You can, but it’s not worth the effort. Instead, focus on understanding what each person likes and dislikes, and allocate tasks accordingly.” This advice stuck with me. While I do monitor working hours and assign tasks based on individual responsibilities, I’ve also started paying attention to whether the work aligns with each person’s preferences. Managing tasks in overly fine detail often takes up time on non-essential aspects, such as aligning things for visibility or rationalization. I also felt that I lacked the capacity to manage everything at such a granular level. So, I decided not to pursue that approach. Instead, I now assign work based on each member’s area of responsibility while keeping the perspective of “Am I assigning tasks in line with their preferences?” in mind. Creating Exceptions, Like Allowing a Car to Speed through an Intersection at 60 km/h Exceptions can make a team or organization incredibly fragile. If you let even one car run a red light because it’s in a hurry, the entire road system will descend into chaos. Kodai Ando, The Mask of Leadership: A Mindset Shift from Individual Contributor to Manager How many members on your team can you trust to “follow the rules 100%”? Whether it’s meeting deadlines, processing data at the start of the month, escalating issues, or sharing files, workplace rules exist for a reason. If trust is lacking, what happens? Someone ends up covertly monitoring tasks, reminding others, or even correcting mistakes behind the scenes. This reliance on manual intervention hinders team autonomy and efficiency. Decoupling Work: Experts in Loosely Coupled Systems (Practical Edition 3) | RINARU To improve workflow efficiency, I aim to create system where tasks can be completed independently, without requiring constant confirmation. Having even one uncertain step can lead to errors or delays. If it is certain, there is no need to confirm. Establishing clear rules—and ensuring that everyone trusts these rules will be followed—is crucial. While overly rigid rules can hinder the value we aim to deliver, establishing a solid framework is crucial. I focus on setting clear and basic guidelines to provide a reliable foundation for our work. Constants, Variables, and "Variables Close to Constants" Both Professor Hayashi and Mr. Morioka emphasized the importance of identifying constants and variables and focusing on changing variables. This means dedicating your time and energy to things you can influence. "Moving Variables, Not Constants" I learned about this concept during a company meeting where my manager shared it with the team. Observing other managers around me, I’ve noticed that they don’t simply fight against the flow. Instead, they assess the dynamics at play and create conditions where things naturally align with their goals. What stood out most from the manager’s insights was that some variables may currently appear close to constants. What’s important is the ability to make this distinction. This includes aspects such as company culture, policies, and people’s mindsets. These may appear unchangeable and resistant to effort, but it’s important to remember that they are variables that can be shifted over time. People Tend to Forget If They’re Told Only Once Team members find it harder to work with a manager who is “hard to understand” rather than one who is simply “incompetent.” Even critical information, if communicated just once, can get lost in the daily flood of information.   "The Leader's Communication Skills" That Eliminate Waiting for Instructions The key is to guide team members to repeatedly return to the project’s purpose during their decision-making process. When a team member asks a question, respond with another question that leads them back to the project’s purpose. By doing this, team members will gradually develop the habit of consistently grounding their decisions in the project’s goals whenever they face uncertainty. Shu Yamaguchi, Project Management Taught by Foreign Consultants I tend to be forgetful myself, and there have been many times when someone has said to me, “I already told you,” leaving me frustrated. Conversely, when someone forgets something I’ve said, I believe that communication is not just an individual issue but a shared responsibility between the two parties. If didn’t communicate effectively enough for the other person to remember, I bear part of the responsibility. When it comes to sharing thoughts and ideas I want to convey, it seems best to repeat them frequently, to the point of saying them at every opportunity. Cheerful Leaders and Grumpy Leaders Edmondson defines psychological safety as "a climate in which people feel safe to take interpersonal risks, such as speaking up with ideas, questions, or concerns.” Why "Psychological Safety" Continues to Cause Confusion | Q by Livesense When the flow of information within a team decreases, projects almost inevitably fall into danger. When the sender of information cannot anticipate how the recipient will react, the overall volume of information exchanged drops. Ultimately, teams led by cheerful leaders see an increase in the flow of information, both between members and between the leader and the team. Shu Yamaguchi, Project Management Taught by Foreign Consultants Based on my experience working with many younger co-workers and staff, I can summarize “how to motivate everyone” in one sentence. Leaders must work more seriously and happily than anyone else. There is no better way to inspire and nurture the members. Noriyuki Sakuma, Noriyuki Sakuma's Unfair Work Technique—How I Did What I Wanted without Burning out at Work Simply put, when your boss is in a bad mood, it creates an awkward atmosphere, making it hard for team members to focus (at least for me). That’s why I always try to maintain a cheerful attitude and create an environment where it’s OK to say anything. Of course, I’m not always genuinely in a good mood, and pretending to be cheerful when I’m not can be draining. If you see a leader or manager struggling, take a moment to acknowledge and appreciate their efforts. Conclusion How was it? I hope you found some ideas here that could be useful in your work. At KTC, being a leader doesn’t mean you’re superior or more skilled than others. It’s simply a function or role that can be assigned or removed based on what’s best for the organization and its current situation. On the other hand, if the leader is just a function, then the members are also just functions. Ultimately, success lies in teamwork. It’s not enough for individual members to simply take action, nor for the leader to handle everything alone. What truly matters is the team's ability to face challenges and overcome them together. Let's take it easy and do our best!
アバター
この記事は [KINTOテクノロジーズアドベントカレンダー2024](https://qiita.com/advent-calendar/2024/kinto-technologies) の19日目の記事です🎅🎄 1. Tech Blog デビュー こんにちは! 生成AI活用PJTのShiori( @shor_t8q )です。現在、生成AIの研修・ユースケース開発・技術支援を担当しています!(が、デビュー作はWeb3の内容となりました) このブログは2024/11/28に開催されたKINTOテクノロジーズ超本部会(=会社の全体会)の懇親会のNFT活用企画についての記事です。会社総会の企画やNFTの取り組みについて関心のある方、KINTOテクノロジーズの会社文化を知りたい方に最適な内容となっています。 前提 このブログで紹介するNFT企画は、超本部会の実行委員、懇親会、ノベルティ、クリエイティブ室メンバーの全員のご協力と尽力により実現できた内容です。 また、この記事で取り扱うNFTサービスの提供企業のスポンサー記事ではありません。あくまで懇親会&ノベルティ担当の体験、工夫、学び、教訓を共有することを目的としています。 2. 超本部会について ![01_chohonbukai](/assets/blog/authors/shiori/2024-12-19/01_chohonbukai.png =750x) 2-1. そもそも超本部会とは? 超本部会とは、KINTOテクノロジーズが年に1・2回程度開催している会社の全体会です。このブログで取り上げる超本部会は、2024/11/28に開催され、2024/11/28の開催規模はオフラインで300名程度、オンラインで50名程度です。時間は、15時〜21時まで開催され、コンテンツ盛りだくさんの大盛り上がりの会となり、幕を閉じました。 2-2. 懇親会とノベルティ担当になる 超本部会の1.5ヶ月前に、各部のグループからメンバーが集い、実行委員会が結成され、私は懇親会とノベルティを担当しました。 ![02_novelty](/assets/blog/authors/shiori/2024-12-19/02_novelty.png =750x) 社内のコミュニケーション促進を目的に、デスク用のネームプレートをノベルティとしました。ノベルティはクリエイティブ室のJさんがデザインしました。 2-3. 超本部会のコンセプト コンセプトは、「KTCはキミで、できている」です。このコンセプトは、経営陣 / 部長 / マネージャーの方からの、お気持ち、チームワーク、未来へのビジョンに関する意見をまとめて作成されました。 ![03_concept](/assets/blog/authors/shiori/2024-12-19/03_concept.png =750x) 3. 超本部会の懇親会 3-1. 懇親会とは? 懇親会は超本部会の一番最後の企画で19:00~21:00の超本部会の最後に開催されました。 超本部会のコンセプトを基に懇親会チームは懇親会ゴールを以下2つに定義しました。 参加者同士、他部署のメンバーとのコミュニケーションを促進する。 参加してよかったと思えるような体験を提供する。 3-2. 懇親会でNFTを活用することになったきっかけは? 懇親会ではNFTスタンプラリーを実施しましたが、なぜ懇親会でNFTスタンプラリーを取り入れることにしたのでしょうか?私はもともとノベルティ担当だったですが、発想の起点は「まだやったことがなうことを試す」を前提とした時に「ノベルティがNFTだったら面白いのではないか?」、「今後のNFT・Web3プロジェクトのインスピレーションとなるのではないか?」と考えたところにあります。 そこで、本部会のコンセプトに沿ったNFT活用方法を模索し、Web調査をしたところ「NFTスタンプラリー」という概念があることがわかりました。その後実行委員メンバーから、前回の本部会の懇親会の企画で「デジタルスタンプラリー」という企画を実施したことを共有してもらい、「デジタルスタンプラリー」を「NFTスタンプラリー」にアップデートすることにしました。ここで懇親会メンバーと合流して、NFTスタンプラリー活用の検討に入りました。 ![04_nft_idea](/assets/blog/authors/shiori/2024-12-19/04_nft_idea.png =400x) 4. NFTとは? 4-1. NFTは唯一無二のデジタル資産 NFTとは「Non-Fungible Token」の略で、日本語では「非代替性トークン」と訳されます。これは、他のトークンと交換が不可能な、唯一無二のデジタル資産を指し、独自性と希少性の特徴を持っています。また、NFTはブロックチェーン技術により、取引の透明性が確保されます。例えば、ビットコインは1ビットコインが他の1ビットコインと同じ価値を持つ代替可能なトークンですが、NFTはそれぞれが異なる価値を持つため、代替することができません。 ![05_nft](/assets/blog/authors/shiori/2024-12-19/05_nft.png =400x) このような「非代替性トークン」特性から、NFTは、アート・ゲーム・音楽・映画・スポーツ・エンターテインメント、企業のマーケティング活動において新しいファンエンゲージメント向上施策ツールとして活用されており、その他の業界においてもNFTの応用が進んでいます。NFTはクリエイターの収益化の手段や企業の新たなマーケティング集団を提供し、取引の透明性とトレーサビリティを向上させる一方で、マイニング(採掘)に伴う多くの消費電力による環境への影響や法的リスク、市場のボラティリティによる価値としての不安定さなどの課題があります。 5.NFTスタンプラリー 5-1. 懇親会でのNFTスタンプラリーの実現可能性は? 調査実施 そもそも会社の懇親会でNFTを配布したり、交換したりすることは実現可能なのか、検証することにしました。 ウェブ調査の結果、 Sushi Top Marketing という企業がNFTで多くの活用事例があり、NFTスタンプラリーの実績を持っており、トークングラフマーケティングという新しいNFTの概念を提案しているということがわかりました。 トークングラフマーケティングとは? トークングラフマーケティングは、ブロックチェーン技術を活用し、ユーザーのプライバシーを保護して顧客にアプローチ可能な新しいマーケティング手法です。ユーザーが所有するNFTやその他のトークン情報を基に、その人の趣味嗜好や行動履歴を分析できます。 ![06_token_graph_marketing](/assets/blog/authors/shiori/2024-12-19/06_token_graph_marketing.png =750x) 引用元: トークングラフマーケティングとは? 初期のアイディア そこで、以下のような初期アイディアをベースにご担当者の方と数回のディスカッションを行いました。 内容 説明 ノベルティコンセプト 普段会わない人とのコミュニケーションを活性化、 コミュニケーションの誘発とイノベーションの促進、団結意識、老若男女問わずもらって嬉しいなどです。 このコンセプトからはズレないようなソリューション。 アクセシビリティー イベントで一日のみ利用可能なため、 操作説明なしでだれでもすぐに利用できるような手軽さが重要。 アプリとQRスキャンの両方に対応しているなど。 ノベルティとしての価値 NFTを個人のウォレットに送金し、 資産として保存できるというような新しいノベルティの概念を伝えたい。 革新性 会社はクリエイティブとテクノロジーを重要な価値基準としており、 まだやったことのない技術的チャレンジに積極的。 ブランド ブランドのキャラクターやカラーを反映したUIにカスタマイズできると嬉しい。 利用イメージ 例えば、「部署ごとに違うスタンプを所有しており、Aさんが違う部署のBさんと話すと、 Bの部署のスタンプをもらうことができて6個のスタンプを集めることを目指す」などの企画は、コミュニケーションを促進できて面白いのではないか。 5-2. NFTスタンプラリーの基盤となるブロックチェーン技術 今回のNFTスタンプラリーでは、Astarを採用しました。Astarは、次世代のブロックチェーン技術として注目されており、スマートコントラクトとdApp(分散型アプリケーション)の開発に特化した技術です。 Astar は、 Polkadot エコシステムの一部であり、異なるブロックチェーン間の相互運用性を実現することができます。 ![07_astar](/assets/blog/authors/shiori/2024-12-19/07_astar.png =750x) 5-3. NFTスタンプラリーの構想 アイディアブレスト 4つのNFTスタンプラリーアイディアをご提案いただきました。 内容 説明 NFTカード交換 複数のNFTカードを交換し合ってお互いにNFTを取得。NFT配布後に、別のNFTを届けることも可能。 動的NFTスタンプラリー 任意のNFTペアを集めると、特別なNFTに変化する仕組み。NFTの組み合わせで、ユニークなNFTを配布可能。 LINEでのNFT配布 LINE公式から記念NFTを配布し、NFT配布後に、記念NFT保持者にギフトを送付。 NFT名刺&スタンプラリー NFCまたはQRスキャンでNFTが発行できるカードを配布し、スタンプラリーを実施。ノベルティとしての価値を持ちつつ、参加者の収集意欲を刺激。 アイディアを踏まえ、以下の判断基準を基に懇親会メンバーでディスカッションを重ね、アイディアをブラッシュアップしました。 判断基準 内容 説明 工数 約1ヶ月で準備が完了するか。 ユーザビリティ マニュアルや説明が不要、もしくは最小限で、直感的に利用できるか。 運用可能性 300人が集まる懇親会のような自由な雰囲気の中でも運用が可能か。 簡便性 複雑な操作や手順を必要とせず、簡単に利用できるか。 アクセシビリティ 外部サービス利用前提などの制限なく、誰でもアクセスできるか。 先進性 NFTを活用した興味深い体験を提供できるか。 セキュリティ データの保護やプライバシーを確保できるか。 スケーラビリティ 多数のユーザーの同時アクセスに対応可能か。 互換性 スマホデバイスやOS互換性があるか、既存のWeb3プラットフォームと統合可能か。 予算適合性 予算内に収まるか。 最終的に、初期アイディアを組み合わせ次のような企画になりました。 5-4. NFTスタンプラリーの企画内容 NFTスタンプラリーの概要 NFTは、各部署、経営陣、コンプリート用を含め全部で14種発行しました。 参加者が、ゾーン(経営陣や表彰メンバーなどが滞在するスポット)を巡り、懇親会中に自身の持つNFTとは異なるNFTを収集し、普段話す機会が少ない他部署メンバーや経営陣とコミュニケーションを取ることで、新しいプロジェクトのスタートや関係構築を促進します。 全てのNFTを収集すると、コンプリートNFTを取得でき、後日景品と交換できるゲーミフィケーション要素を取り入れました。 NFTのタッチポイント 名札(超本部会の参加者全員が入口で名札を受け取る)にNFCタグ(後ほど解説)とQRコードを入れ、NFCタップとQRのスキャンの両方からNFTを獲得できるようにします。NFCとQRの両方を準備した背景には、NFCタップが利用できない参加者が発生する事態を考慮するためです。 NFTスタンプラリーの流れ 参加者は各ゾーンへ行く メンバー(他部署や経営陣)と交流 NFTに紐づくチャットに、どのような内容を話したのか投稿する(NFTに紐づくチャットに、部署のメンバーとどのような会話をしたのか共有することで、各部署のNFTのユニークな価値にする) 会話をしたメンバーとNFTを交換し、13個のNFTを収集する コンプリートNFTを獲得する コンプリートNFTに紐づくチャットに、Slackの名前を投稿する 後日運営がSlack経由でコンプリートNFTメンバーに景品をプレゼントする NFTデザイン NFTのデザインは、会社の全体会であることを考慮し、統一感や一体感を出すために、KINTOブランドキャラクターであるくもびぃを採用しました。 ![nft_collections](/assets/blog/authors/shiori/2024-12-19/08_nft_collections.png =750x) クリエイティブ室の桃井さんがNFTコレクションをデザインしました。 NFTの工夫 NFTにオリジナルな価値を付与します。 部署ごとに異なるデザイン NFT名はグローバルメンバーを考慮した日英併記 NFTにユニークなコレクション名や開催日の日付属性を追加 各NFTごとに超本部会のテーマ「KTCはキミで、できている」キーワードを仕込み、全てのNFTを集めると懇親会テーマになる # 6. NFTスタンプラリーを実現する上での課題 6-1. NFTをどのように交換するのか?チャネルは?採用技術は? NFT x NFCタグを採用 今回の懇親会の要素として、「そんなこともできるんだ!」といったようなちょっとした驚きや発見も重要視していました。そこで、サービス提供企業の方からいただいたアイディアと実行委員メンバーのアイディアを融合し、NFCタグにNFTの情報を書き込み、名札に入れたNFCチップにスマホをかざすだけで、NFTを受け取れるようにしました。 *NFC(Near Field Communication)とは、 対応する機器同士を近づけることで無線通信を行うことができる技術・規格 です。NFCは、スマートフォンや交通系ICカード、クレジットカード、家電など、幅広い用途で使用されています。 NFCタグにNFT情報をどのように書き込むのか? NFCタグ: www.amazon.co.jp/dp/B0DFW9WTRW NFC書き込みアプリ: https://apps.apple.com/jp/app/nfc-tools/id1252962749 AmazonでNFCタグを調達し、13種約300人分のNFT URLをNECタグへ書き込みました。またNFCタグが機能しないことにも備えて300人分のNFT QRコードも準備しました。この工程はマンパワーです。(懇親会メンバーthytさん、岡さんが書き込みをしました。) 書き込んだNFCタグをネームホルダーに入れました。 ![09_nfctag](/assets/blog/authors/shiori/2024-12-19/09_nfctag.png =400x) 6-2. NFTスタンプラリーのルールを参加者全員にどのように周知するのか? NFTスタンプラリー開始前の司会の案内の時に「NFTスタンプラリー進め方ガイド」を投影し、司会と協力して、NFTスタンプラリーのルールを周知しました。 ![10_guide](/assets/blog/authors/shiori/2024-12-19/10_guide.png =750x) 6-3. NFT交換はブラウザベースのウェブアプリ上で行うため、獲得したNFTを見失わないようにするには? 司会と連携し、以下の内容を投影資料内で案内しました。 ブラウザで利用するウェブアプリであること シークレットモードでは利用しないこと NFTを最初に交換した後、「NFT一覧ページのショートカットをホーム画面に追加する NFT交換時に表示されるNFTのバックアップコードをスクショしておくこと 6-4. コンプリートNFTの発行方法は? NFTをコンプリートしたら懇親会担当に獲得したNFT一覧画面を見せて、懇親会担当からコンプリートNFTを獲得できるようにしました。 ![11_nft_list](/assets/blog/authors/shiori/2024-12-19/11_nft_list.png =400x) 7. 学びと教訓 7-1. 懇親会中に実行できるタスクはNFT交換のみ 懇親会中は、普段あまり話さないメンバーと沢山会ったため、ごはんを食べるのもやっとというような状況でした。そのため、当初想定していたNFTに紐づくチャットに、メンバーとの会話を共有することは難しく、「各部署のNFTに、部署メンバーの気付きやアイディアを蓄積し、各部署のNFTのユニークな価値にする」ということを実現するのは難しかったです。 また当初の企画では、ゾーン(経営陣や表彰メンバーなどが滞在するスポット)を巡ってNFTを交換することを想定していましたが、懇親会という自由なコミュニケーションを促進する場では、ゾーンを設置する必要ありませんでした。 7-2. 全種類のNFTを確認するオペレーションの工夫を 参加者からの意見として、任意のA部署メンバーのNFTを複数取得できるため、A部署のNFT1、A部署のNFT2、、、といったように同じデザインのNFTが一覧に表示され、「どの種類のNFTを獲得したのか、コンプリートまでにあとどのNFTを獲得する必要があるのか、確認するのが少し大変。」という声があり、この点は事前に考慮して対応策を検討していれば良かったポイントです。 7-3. NFTコンプリート後のタスクはシンプルに 「5-4. NFTスタンプラリーの企画内容」で企画想定した通りには上手く機能しませんでした。 この点は、「13種類のNFTを獲得→NFT一覧の画面をスクショしてSlackチャンネルで共有する」 といったSlackチャンネルでコンプリートを証明するオペレーションにした方が、懇親会担当は誰がコンプリートしたのかすぐに把握できるので、スムーズに機能したと思われます。 またSlackチャンネルなどのオープンなスペースでコンプリート報告をすることで、懇親会参加者が「同僚の〇〇さんがコンプリートした!」ということが人目でわかるので、NFTスタンプラリー参加促進の誘発要素にもなると考えられます。 7-4. NFTタッチポイントのチャネル選定は柔軟に 今回の懇親会では300人分のNFT URLをマンパワーでNFCへ書き込みました。しかし、人的ミスや工数を考慮すると、これ以上の大規模イベントでは別のタッチポイントやチャネルを検討した方が良いと考えられます。 7-5. 国際メンバー向けには英語でのフォローアップを KINTOテクノロジーズは約25%が国際メンバーです。投影資料は日英併記で準備しましたが、懇親会で複数人の国際メンバーと会話した際に、口頭でNFTスタンプラリーの進め方フォローアップを行いました。スタンプラリー開始時に、Slackチャンネルで投影資料を共有したり、英語での補足説明をするとさらにスムーズだったのはないかと思います。 8. NFTスタンプラリーの結果 8-1. 10%がコンプリート!!! 約300人の参加中30人という、 参加者約10%がNFTスタンプラリーをコンプリート しました。 ![12_nft_complete](/assets/blog/authors/shiori/2024-12-19/12_nft_complete.png =400x) 8-2. 全体の結果は・・? 全体としては、 1734個 のNFTを配布し、一人あたりの NFT平均交換数6.6個 、 NFT交換数の中央値は6 になりました。つまり、約2時間で 1人当たり平均約6部署のメンバーと会話したこと になります。 懇親会で他部署メンバーとの交流をゴールの一つとしていたため、NFTスタンプラリーは、ゴール達成の一助になったと考えられます。 項目 数 NFTスタンプラリーコンプリート人数 30人 NFT発行総数 1734個 NFT平均交換数 6.6個 NFT交換数中央値 6個 9. NFT活用の展望 NFTスタンプラリーを活用することで、懇親会のような物理的なイベントにおける人の動きを可視化できることがわかりました。興味深いことに、今回のNFTスタンプラリーでは、どの部署がどのくらいのNFTを発行したかわかるので、他部署とのコミュニケーションに積極的な部署を把握することができました。 このNFTスタンプラリーを通して、KINTOテクノロジーズが手掛ける、KINTO製品、MaaS関連アプリ、会員向けサービスへのオンライン・オフラインでのNFT活用イメージが広がりました。具体的には、既存の年代や性別といった属性ではない、趣味嗜好ベースでのお客様へのアプローチが可能といったことや、お客様のコミュニケーションと行動の可視化という観点での活用が有効だと思いました。 ![13_products.jpeg](/assets/blog/authors/shiori/2024-12-19/13_products.png =750x) 10. 最後に NFTスタンプラリーの試みを通じて、新たな技術とアイデアの融合がもたらす可能性を実感しました。懇親会という場で、異なる部署のメンバーが交流し、楽しみながらテクノロジーに触れる機会を実現できたのは貴重な経験でした。今後も、新たな技術を活用した取り組みを積極的に行い、興味深い発見や洞察を共有できるよう努めていきたいと思います。 皆さんもぜひ、AIやWeb3など新たな技術を取り入れた企画に挑戦してみてください。何か面白い発見があると思います! 最後までご覧いただきありがとうございました!
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の19日目の記事です🎅🎄 学びの道の駅の中西です。今年は学びの道の駅プロジェクトが立ち上がり組織化されました。そんな中、社内Podcastの運営も行っており、今年のアドベントカレンダーではその内容もお届けしたいと思います。 「学びの道の駅」とは? 「学びの道の駅」は、社内で頻繁に開催される勉強会をもっと便利に、そして効果的に活用するために立ち上げられたプロジェクトです。社内の有志が中心となり勉強会の開催を支援し社内の知識共有を促進することを目的としています。 WWDC参加報告 KTC学びの道の駅ポッドキャストでは、社内の勉強会を開催している方にインタビューを行っています。その名も「突撃!となりの勉強会」。本日はいつもと趣旨を変えて先日WWDCに現地参加された仲野さんにインタビューを行います。現地でWWDCに参加して持ち帰っていただいた刺激を聞いている皆さんにもお届けできれば幸いです。 インタビュー 明田さん: 本日はWWDCに現地参加された仲野さんにお越しいただきました。よろしくお願いします。 仲野さん: よろしくお願いします。 明田さん: 最初に、仲野さんが普段どんな仕事をされているのか自己紹介をお願いします。 仲野さん: モバイルアプリ開発グループで「KINTOかんたん申し込みアプリ(以降、申し込みアプリ)」のプロダクトマネージャーをしています。仕事の内容としては、申し込みアプリの開発や、Web側のKINTO ONEの新車の方と同期するケースがあるので、毎週のミーティングに参加してキャッチアップしています。また、仕事以外ではKTCの中で司会業などもよくやっています。 明田さん: ありがとうございます。仲野さんは社内でも顔が広いという印象がありますね。では、WWDCに参加したきっかけについて教えてください。 仲野さん: 申し込みアプリがモバイルアプリのサービスなので、リリース時にはAppStoreの審査が必要です。WWDCはApple Developer Programに紐づくアカウントを持っている人が応募できるので、社内で行きたい人がいたら申し込んでみてということで、私も応募しました。前職でもiOSエンジニアをしていて、同じくアカウントを持っていましたが、その時は残念ながら受からず、今回は嬉しいことに当たったので行くことができました。 明田さん: 参加するための費用を勝ち取るのは大変でしたか? 仲野さん: そうですね、実は受かったとわかった後にどうするかマネージャーに相談しました。最終的には小寺さんに話を持って行くことになり、資料を作って提出し、承認いただけました。運が良かったのと、会社がウェルカムな環境だったのも大きいかもしれません。 明田さん: WWDCに行ってからのエピソードで、他のところで話していないことがあれば教えてください。 仲野さん: WWDCはApple Parkで開催され、その中庭はカリフォルニアの植物で揃えられているみたいでした。果物の木もあり、社員は取らないようにといわれていたり、デベロッパーリレーションの方がトリビアを話してくれたりと、面白かったです。例えば、外周と内周を回るのにどれくらいかかるかなどの話もありました。 明田さん: エンジニアやテックの開発者とコミュニケーションを取った中で得られた学びはありましたか? 仲野さん: はい、エンジニアと触れ合う機会があり、どんなアプリをお互い開発しているのかを共有したりしました。個人でもアプリを作っている人や、メインの仕事はソフトウェアエンジニアではないけど趣味でアプリを作っている人も多かったです。Apple Parkに来ることでモチベーションが上がったという人がたくさんいました。 明田さん: 参加したことを通じて、アプリの開発や業務にプラスになったことはありますか? 仲野さん: 直接アプリのコンサルを受けることができ、いくつかの課題に対してアドバイスをもらいました。その結果、アプリのUIやUXを改善する方向で進めています。次のリリースではそのアドバイスを反映したものを出す予定です。 明田さん: 英語に自信がない方でもWWDCに参加できますか? 仲野さん: 全然問題ないと思います。英語はできた方がいいですが、ノリでいけると思います。Uberで目的地に移動するので、アプリで目的地を指定するため特に困りませんでした。英語ができるに越したことはありませんが、参加する気持ちがあれば大丈夫です。 明田さん: 最後に、これを聞いている皆さんに対して一言お願いします。 仲野さん: 今回の経験を通じて、海外カンファレンスに行って情報収集することの意義を感じました。KTCをもっと良い方向に持っていくために、ぜひ皆さんもチャレンジしてみてください。熱意があればきっと通ると思います。 明田さん: ありがとうございました。仲野さんでした。
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の18日目の記事です🎅🎄 はじめまして。KINTOテクノロジーズのAndroidエンジニア 山田 剛 です。 本記事では、Android View で構築されたアプリを Jetpack Compose を用いたUI記述へ移行させる、もしくは、Android View ベースのUIに徐々に Jetpack Compose ベースのUIを導入する際の第一歩となるような、ちょっとしたテクニックの一部を紹介します。 1. はじめに 宣言的UI のAndroid版として Jetpack Compose の開発開始が数年前にアナウンスされ、2021年7月にバージョン 1.0 がリリースされました。 多くのAndroid開発者が、その以前からJavaに続きAndroid開発の公式サポート言語となっていた Kotlin の柔軟性・拡張性を活用した Jetpack Compose のコンセプトを受け入れ、以降のAndroidアプリ開発において Jetpack Compose の採用が徐々に拡がっているようです。 宣言的UIの導入によって、従来のViewベースのUIと比べて少ないコード量で直感的な記述が可能になり、開発効率/生産性が向上すると考えられています。 最近になって Compose Multiplatform がリリースされ、Android 以外にも Jetpack Compose のスキルを活用できる領域が拡がってきました。 今後は魅力的なライブラリが Jetpack Compose のみに対応される、といったことも増えてくるかもしれません。 その魅力を既存アプリにも取り入れるべく、弊社でも当初 Android View ベースで構築されていたアプリのUIを Jetpack Compose ベースのUIに移行する取り組みを一部の開発プロジェクトで進めています。 しかし従来のViewベースのUIは 手続き的 な要素が多く、Jetpack Compose の 宣言的 なスタイルの導入は簡単というわけにはいきません。 この記事では、Jetpack Compose によって簡潔になったことで削ぎ落とされた、あるいは見えにくくなった要素を補完するためのちょっとしたテクニック、とりわけ Composable の位置やデバッグ情報の確認、 View と Composable の相互運用などについて紹介し、ViewベースからJetpack ComposeベースのUIにできるだけスムーズに移行するための一助となることを目指します。 2. Composable の位置合わせとデバッグ View の表現を今までになかった Composable に替えるという作業をするにあたって、同じ表示内容をどのように表すのか、という課題と同等以上に、正しい位置に配置できるのだろうか、という点が、 Android View に馴染んだ筆者にとっても大きな不安要素でした。まずは、 Composable の表示位置を View と同じに揃えること、そのために必要なデバッグのための情報を取得・確認する方法を紹介します。 2.1. Composable と View の位置を確かめる Android API level 1 から提供されている View は、Java のオブジェクト指向の考え方に基づき、矩形の画面要素としての表現のほか、View 同士の包含関係や相互作用、サブクラスによる機能拡張などもうまく表現しています。 たとえば、単一の View や ViewGroup が内包した多くの View の位置をログに出力することは、以下のようなコードでできます: private fun Resources.findResourceName(@IdRes resId: Int): String = try { getResourceName(resId) } catch (e: Resources.NotFoundException) { "?" } fun View.logCoordinators(logger: (String) -> Unit, res: Resources, outLocation: IntArray, rect: Rect, prefix: String, density: Float) { getLocationInWindow(outLocation) rect.set(outLocation[0], outLocation[1], outLocation[0] + width, outLocation[1] + height) var log = "$prefix${this::class.simpleName}(${res.findResourceName(id)})${rect.toShortString()}(${rect.width().toFloat() / density}dp, ${rect.height().toFloat() / density}dp)" if (this is TextView) { log += "{${if (text.length <= 10) text else text.substring(0, 7) + "..."}}" } logger(log) if (this is ViewGroup) { val nextPrefix = "$prefix " repeat(childCount) { getChildAt(it).logCoordinators(logger, res, outLocation, rect, nextPrefix, density) } } } fun View.logCoordinators(logger: (String) -> Unit = { Log.d("ViewLogUtil", it) }) = logCoordinators(logger, resources, IntArray(2), Rect(), "", resources.displayMetrics.density) ここで View$getLocationInWindow(IntArray) は、View が属する Activity のウィンドウ上での左上の座標を取得する関数です。Android View に慣れている開発者なら、このようなコードで動作を確認するのは簡単でしょう。 一点 Activity$onResume() などから直接これらの関数を呼び出してもまだ View の配置は完了しておらず意味のある情報が取得できないため、多くの場合は View$addOnLayoutChangeListener(OnLayoutChangeListener) などのコールバックから呼び出す必要があることに注意が必要です。 同じようなことを Jetpack Compose ではどうするかよく分からず、 Composable で同じように動作しているか確かめにくく移行作業がおっくう、という開発者も少なくないのではないでしょうか。 Jetpack Compose では、以下のように Modifier の拡張関数 onGloballyPositioned((LayoutCoordinator) -> Unit) を使って Composable の配置を取得できます: @Composable fun BoundsInWindowExample() { Text( modifier = Modifier .padding(16.dp) .background(Color.White) .onGloballyPositioned { Log.d("ComposeLog", "Target boundsInWindow: ${it.boundsInWindow()}") }, text = "Hello, World!", style = TextStyle( color = MaterialTheme.colorScheme.onSecondary, fontSize = 24.sp ) ) } onGloballyPositioned(...) の引数にコールバックを与えることで、Composable の位置が更新されるたびにコールバックで位置の座標を取得できます。 ここで LayoutCoordinator.boundsInWindow() は Composable の矩形の上下左右を Activity 座標系で取得する拡張関数です。 ひとつの Composable に内包されるすべての Composable の位置を一気に取得するのは今のところ難しそうですが、その代わりに単一の Composable の位置は簡単に取得できます。 しかも多くの場合、Activity などの複雑なライフサイクルを気にせずに位置情報を取得できそうです。 onGloballyPositioned(...) に似たコールバックとして onPositioned(...) もあり、こちらは親となる Composable の内部での相対的な位置が決定された後にコールバックが呼ばれるようです。 boundsInWindow() のほかにも boundsInRoot() 、 boundsInParent() などがあり、場合に応じて使い分けることができますが、今回は割愛します。 2.2. 画面表示確認用Composableを作ってみる Composable でも View$getLocationInWindow(IntArray) と互換性のある boundsInWindow() で位置を取得する方法がわかったので、テスト動作させているときの位置の動きをログに出力して動作を確かめながら Composable を作り込んでいけば、徐々に compose に慣れながら View と同じものを作れそうです。 それは正しく、お手軽な方法なのですが、 LogCat は大量の文字が流れていくので、特に情報量が多いときには見づらいことも多々あります。 そこで、画面表示確認用の Composable を別に作ってみることを考えます。 テストのときに限ってアプリの一部にデバッグ用表示領域を作って、常に更新させるようにすれば、流れていく大量の LogCat の中から決定的なログを必死に探す必要もなくなります。 …そんなことは Android View の時代からわかっていたのだがそれを作り込むのがおっくうで…という嘆息が聞こえてきそうですね。 宣言的UI たる Jetpack Compose では、そのようなデバッグ用表示領域を少ない手間で作りやすくなっています: class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK != Configuration.UI_MODE_NIGHT_YES setContentView(R.layout.activity_main) findViewById<WebView>(R.id.webView).let { webView -> webView.loadUrl("https://blog.kinto-technologies.com/") } val targetRect = mutableStateOf(Rect.Zero) // androidx.compose.ui.geometry.Rect findViewById<ComposeView>(R.id.composeTargetContainer).let { containerComposeView -> containerComposeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) containerComposeView.setContent { KtcAdventCalendar2024Theme { ScrollComposable(targetRect) } } } val visibleRect = mutableStateOf(Rect.Zero) val outLocation = IntArray(2) findViewById<View>(R.id.layoutMain).addOnLayoutChangeListener { v, left, top, right, bottom, _, _, _, _ -> v.getLocationInWindow(outLocation) visibleRect.value = Rect(outLocation[0].toFloat(), outLocation[1].toFloat(), outLocation[0].toFloat() + (right - left), outLocation[1].toFloat() + (bottom - top)) } findViewById<ComposeView>(R.id.composeTargetWatcher).let { watcherComposeView -> watcherComposeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) watcherComposeView.setContent { KtcAdventCalendar2024Theme { TargetWatcher(visibleRect.value, targetRect.value) } } } } } サンプルコードで作っているアプリは Jetpack Compose 化作業の途中で、まだ Activity$setContentView(Int) で View を使っており、その View の中で ComposeView を使って Composable を導入しようとしています。 ここでは mutableStateOf(...) で View および Composable の矩形の位置の情報を共有し、画面上での振る舞いを確かめようとしています。 画面構成は下のようになります。HorizontalScrollView の部分を Composable化しようとしており、そのために画面下部をデバッグ情報の表示に利用します: ![画面構成](/assets/blog/authors/tsuyoshi_yamada/advent-calendar_sample_screen-area.png =252x) MainActivity のレイアウトXMLファイルは以下のようになっています: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:layout_marginVertical="48dp"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/layoutMain" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <WebView android:id="@+id/webView" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginHorizontal="16dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/scrollView" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:ignore="NestedWeights" /> <HorizontalScrollView android:id="@+id/scrollView" android:layout_width="0dp" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/webView" app:layout_constraintTop_toTopOf="parent"> <androidx.compose.ui.platform.ComposeView android:id="@+id/composeTargetContainer" android:layout_width="wrap_content" android:layout_height="match_parent" /> </HorizontalScrollView> </androidx.constraintlayout.widget.ConstraintLayout> <androidx.compose.ui.platform.ComposeView android:id="@+id/composeTargetWatcher" android:layout_width="match_parent" android:layout_height="300dp" android:paddingTop="16dp" /> </LinearLayout> MainActivity.kt と activity_main.xml は Jetpack Compose 化作業の途中で、View と Composable が混在しています。 WebView などは、本記事執筆時点ではまだ Composable が提供されていないため、現時点では混在させながらうまく運用する実装も必要です。[^1] activity_main.xml において、 HorizontalScrollView の内部に含まれる ComposeView は既存の View を Composable で置き換えようとするもの、もう一方の ComposeView は上の Composable の位置を監視するために設置したものです。 View にとって代わる Composable は以下のようになっており、 "Target" と表示される部分の配置をチェックするべく、 onGloballyPositioned(...) を実装しています。 @Composable fun ScrollComposable(targetRect: MutableState<Rect>) { val textStyle = TextStyle( textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSecondary, fontSize = 24.sp, fontWeight = FontWeight.W600 ) Row( modifier = Modifier .fillMaxSize(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(Color.Red), contentAlignment = Alignment.Center ) { Text("1", style = textStyle) } Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(Color.Magenta), contentAlignment = Alignment.Center ) { Text("2", style = textStyle) } Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(MaterialTheme.colorScheme.primary) .onGloballyPositioned { targetRect.value = it.boundsInWindow() }, contentAlignment = Alignment.Center ) { Text("Target", style = textStyle) } Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(Color.Cyan), contentAlignment = Alignment.Center ) { Text("4", style = textStyle) } } } これを監視する Composable が以下です。ここではこの Composable 自身も onGloballyPositioned(...) を使って、自身のサイズを取得しています。 @Composable fun TargetWatcher(visibleRect: Rect, targetRect: Rect) { if (visibleRect.width <= 0f || visibleRect.height <= 0f) return val rootAspectRatio = visibleRect.width / visibleRect.height val density = LocalDensity.current // For calculating toDp() val targetColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.25F) var size by remember { mutableStateOf(IntSize.Zero) } Box( modifier = Modifier .fillMaxSize() .onGloballyPositioned { coordinates -> size = coordinates.size } ) { if (size.width <= 0F || size.height <= 0F) return@Box val watchAspectRatio = size.width.toFloat() / size.height val (paddingH: Float, paddingV: Float) = if (rootAspectRatio < watchAspectRatio) { (size.width - size.height * rootAspectRatio) / 2 to 0F } else { 0F to (size.height - size.width / rootAspectRatio) / 2 } with(density) { Box( modifier = Modifier .padding(horizontal = paddingH.toDp(), vertical = paddingV.toDp()) .fillMaxSize() .background(Color.Gray) ) } if (targetRect.width <= 0f || targetRect.height <= 0f) return@Box with(density) { Box( modifier = Modifier .padding( // Caution: exception is thrown if padding is negative start = max( 0F, marginOf( size.width, paddingH, visibleRect.left, visibleRect.right, targetRect.left ) ).toDp(), end = max( 0F, size.width - marginOf( size.width, paddingH, visibleRect.left, visibleRect.right, targetRect.right ) ).toDp(), top = max( 0F, marginOf( size.height, paddingV, visibleRect.top, visibleRect.bottom, targetRect.top ) ).toDp(), bottom = max( 0F, size.height - marginOf( size.height, paddingV, visibleRect.top, visibleRect.bottom, targetRect.bottom ) ).toDp() ) .fillMaxSize() .background(targetColor) ) } } } private fun marginOf( sizePx: Int, halfPx: Float, visibleFrom: Float, visibleTo: Float, px: Float ): Float { val alpha = (px - visibleFrom) / (visibleTo - visibleFrom) return (1 - alpha) * halfPx + alpha * (sizePx - halfPx) } このComposable関数で、Activityの画面と "Target" と表示される Composable の位置関係を図示しています。 padding の設定が少しわずらわしいですが、難しい算術ではありません。 Composable は形式的には関数ですが、 MutableState や remember を用いて状態を継続的に保持することでUIの表現を成り立たせています。同時に、継続的な情報が変更されたとき Composable の再描画 (recomposition) が行われるようになっており、わずらわしいイベント処理の記述を減らすことができ、 宣言的 なコーディングを可能にしています。 画面右上部分の HorizontalScrollView を左右にスクロールすると、画面下の Composable がスクロールに追従して位置を表示してくれます。ScrollView の内部に存在する View や Composable の位置を View$getLocationInWindow(IntArray) や LayoutCoordinator.boundsInWindow() で取得すると画面外の座標も得られますので、画面の内外を正常に行き来できるかどうかを確かめられます(Composable で Modifier.horizontalScroll(...) や  Modifier.verticalScroll(...) などを用いてスクロールさせる場合はこの限りではありません)。 従来からの View でこのようなデバッグ情報を表示するには、レイアウトXMLファイルと Java/Kotlin のコードをそれぞれ書き換える必要があり、リリース後のアプリに反映されない作業としてはちょっとわずらわしいものでした。変数の更新に追随するレイアウトXMLファイルを作成できるデータバインディングも提供されましたが、直感的とはいいがたいものでした。Jetpack Compose は情報をデザインに直接書き込むような感覚で画面を設計でき、時間のかからない作業で実装の成果を確認できます。 なんでもグラフィカルに表示するのがよいわけではありませんが、表現の選択肢が増え、コーディング作業に入っていきやすくなると思います。 [^1]: WebView の機能を Composable として利用するラッパー が accompanist のライブラリで提供されていますが、現在は 非推奨 となっています。View の Composable化はご本家でも簡単ではないようですし、一般のアプリ開発で View と Composable の同居が長く続くのは特におかしなことではありませんので、安心してComposable化に踏み切りましょう。 2.3. 宣言的にデバッグ情報を書く Composable によるデバッグ情報の表現は、さらに少ない手数で情報の追加を行なえます: @Composable fun TargetWatcher(visibleRect: Rect, targetRect: Rect) { // ... // ... Text( text = when { targetRect in visibleRect -> "Target: inside screen" visibleRect.overlaps(targetRect) -> "Target: crossing edge of screen" else -> "Target: outside screen" }, modifier = Modifier.align(Alignment.TopStart), style = TextStyle( textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurface, fontSize = 24.sp, fontWeight = FontWeight.W600 ) ) } } /** * "operator fun receiver.contains" defines in and !in (syntax: other in(!in) receiver) */ private operator fun Rect.contains(other: Rect) = left <= other.left && top <= other.top && right >= other.right && bottom >= other.bottom 上記では Text(...) を使ってデバッグ情報を追加しています。 Text(...) の引数 style は、デフォルト設定で充分なら省略できます。 Log.d(...) や println(...) などとほとんど変わらないタイプ数で情報を表示できます。しかもそれらと違ってスクロールで流れていってしまうこともありません。 「print文デバッグ」のような手軽さでUI構築ができるのも、 宣言的UI の意義の1つでしょう。 アプリをビルドして実行し、右上のスクロールビューを横スクロールすると、以下のように画面下部でアプリの動作する様子が確認できます: 画面外に表示中 画面の境界上にかかっている 画面内に表示中 3. LaunchedEffect で手続き的な処理 これまで 宣言的UI の意義を説明してきましたが、状態が変わったときのイベント処理や、状態が変わったことを示すアニメーションなどを実装するには、 手続き的 な処理も必要です。従来からの View で行なってきたイベント処理など手続き的な記述もできなければ、Jetpack Compose へは移行できません。 Jetpack Compose では LaunchedEffect を使って状態の変化に応じた処理を書くのが一般的です: @Composable fun TargetWatcher(visibleRect: Rect, targetRect: Rect) { // ... // ... var currentState by remember { mutableStateOf(TargetState.INSIDE) } var nextState by remember { mutableStateOf(TargetState.INSIDE) } var stateText by remember { mutableStateOf("") } var isTextVisible by remember { mutableStateOf(true) } nextState = when { targetRect in visibleRect -> TargetState.INSIDE visibleRect.overlaps(targetRect) -> TargetState.CROSSING else -> TargetState.OUTSIDE } LaunchedEffect(key1 = nextState) { if (stateText.isNotEmpty()) { if (currentState == nextState) return@LaunchedEffect stateText = when (nextState) { TargetState.INSIDE -> "Target: entered screen" TargetState.OUTSIDE -> "Target: exited screen" TargetState.CROSSING -> if (currentState == TargetState.INSIDE) "Target: exiting screen" else "Target: entering screen" } currentState = nextState repeat(3) { isTextVisible = true delay(250) isTextVisible = false delay(250) } } stateText = when (nextState) { TargetState.INSIDE -> "Target: inside screen" TargetState.CROSSING -> "Target: crossing edge of screen" TargetState.OUTSIDE -> "Target: outside screen" } isTextVisible = true } if (isTextVisible) { Text( text = stateText, modifier = Modifier.align(Alignment.TopStart), ) } } } enum class TargetState { INSIDE, CROSSING, OUTSIDE } LaunchedEffect の key は複数個の設定も可能です。また、Composable が呼び出される最初の1回だけ処理を行ないたい場合は、 LaunchedEffect(Unit) { ... } として実現できます。 ここで key に指定された nextState が変化するたびに、その変化に応じた処理が行なわれます。上記のコードは、状態が変化した直後の1.5秒間はテキストが点滅しながら直前の状態と比較した現在の状態を表示し、その後は静的な状態を表示します。 LaunchedEffect の key に状態の変数を指定し、それが変化したときの処理をブロック内に記述することでイベント処理が可能です。 LaunchedEffect の ブロック内では delay(...) などの suspend関数 を使った時間のかかる処理の記述が可能です。ブロック内の処理が終わる前に次の状態の変化があった場合、ブロック内の処理はキャンセルされ、次の状態の変化に応じた処理が最初から行なわれます。 LaunchedEffect の ブロック内では 手続き的 な処理を書いていますが、その後の Text(...) は手続き的に与えられた変数の値にしたがって 宣言的 に記述されていることに注目してください。UIの変化に応じた処理は LaunchedEffect を利用するのがよいでしょう。このほか、 SideEffect や、Activity および Fragment のライフサイクルに応じた処理を行なう DisposableEffect など、さまざまな状況への備えが用意されています。 一方、必要以上に複雑にならないよう、このような処理を多く書きすぎないことも重要です。たとえばインターネットからのレスポンスやNFCなど各種センサーの入力を契機としたイベント処理は ViewModel などに記述して、Composable 内の手続き的な記述はUIに関する要素のみにとどめることをお勧めします。 4. ComposeView と AndroidView の相互運用 Activity や Fragment の中で Composable を使う場合、上記のように ComposeView を使って Composable を表示できます。 逆に先述の WebView などを Composable の中で使うために AndroidView または AndroidViewBinding を使って View を Composable に埋め込むこともできます。方法は こちら(Compose でビューを使用する) をご参照ください。 本記事では詳細は省きますが、 AndroidView Composable の存在によって、Composable化の作業を進めたものの一部の View は Composable への置き換えが困難、もしくは非常に時間を要する、といった場合にも、Compose と View を混在させながら開発を進めることができます。 この相互運用は非常に強力で、 ComposeView で呼び出している Composable の中で AndroidView を呼び出し、その中で再度 ComposeView を設置して Composable を呼び出し、さらにその中で AndroidView を…という階層構造も可能です。Composable の中に View を使う余地を残すことで、Composable化作業が思ったように進展しない、あるいは Composable への置き換え作業に開発スプリントの期間を大きく超える時間を要する、といった場合にも、作業に費やした時間を無駄にするリスクが抑えられています。 5. Preview関数 Composable も View と遜色ない水準で位置合わせやデバッグ情報取得のテクニックが使え、手続き的な処理も書くことができ、あるいは Composable化の障害に直面しても強力な相互運用によって一部を View に戻す選択肢がある、と、これまで来た道を振り返ることのできる柔軟さについて主に述べました。ここでは Composable化によって新たに得られる果実、Preview関数の簡単で強力なプレビュー機能について述べます。 5.1. Preview関数を作成する レイアウトXMLファイルによる View の表現も Android Studio のプレビュー機能が利用できましたが、Jetpack Compose ではさらに強力なプレビュー関数を書けます。 @Preview アノテーションで修飾した関数を作成するだけで、Composable がプレビュー表示されます: // ... private class TargetWatcherParameterProvider : PreviewParameterProvider<TargetWatcherParameterProvider.TargetWatcherParameter> { class TargetWatcherParameter( val visibleRect: Rect, val targetRect: Rect ) override val values: Sequence<TargetWatcherParameter> = sequenceOf( TargetWatcherParameter( visibleRect = Rect(0f, 0f, 100f, 300f), targetRect = Rect(90f, 80f, 110f, 120f) ), TargetWatcherParameter( visibleRect = Rect(0f, 0f, 300f, 100f), targetRect = Rect(80f, 90f, 120f, 110f) ) ) } @Preview @Composable private fun PreviewTargetWatcher( @PreviewParameter(TargetWatcherParameterProvider::class) params: TargetWatcherParameterProvider.TargetWatcherParameter ) { KtcAdventCalendar2024Theme { TargetWatcher(params.visibleRect, params.targetRect) } } 上記は PreviewParameterProvider を利用して、ひとつのプレビュー関数で複数のパラメータを与えてプレビュー表示する例を示しています。 Android View のレイアウトXMLファイルで tools:??? 属性を使ってプレビューを設定する方式では、このようなことはできません。 PreviewParameterProvider を使わなくても、 @Preview と @Composable の2つのアノテーションで修飾された関数内で Composable関数を呼び出す記述を書くだけでプレビュー表示が可能です。 筆者といたしましては、新しい Composable を作り始めたときにはすぐにプレビュー関数を作成することをお勧めします。Composable の新規作成時に最初から強力なプレビュー機能を使える長所だけでも、Jetpack Compose への移行の動機として充分であると考えます。 デバッグ情報の表示の確認がプレビュー関数で行なえることも、Composable を活用してデバッグする利点の1つでしょう。 また最近では、 roborazzi などのライブラリを利用してプレビュー関数をUIテストに利用する手法が注目されており、テスト効率化の観点からもプレビュー関数の作成は有用です。 5.2. Preview関数を実行してみる こちら(プレビューを実行) に書かれている通り、Android Studio の左側の**Run '...'**アイコンをクリックすると、Preview関数をAndroid実機またはエミュレータで実行してみることができます。従来からの特定のActivityを実行する機能と同じようなものですが、Preview関数が簡単に書けること、およびインテントなどの要素がなく単純化された状況での実行が可能であることから、より強力になっているといえます。ボタンタップなどのUIアクションのコールバックも実行されるので、Preview関数をあたかも簡易なアプリであるかのようにテストに使えます。 ただし、Preview関数でテストができることを重視するあまりに Composable に多くの機能を持たせすぎることはお勧めしません。ビジネスロジックはできるだけ ViewModel や Circuit における Presenter などの別のクラスないしは関数に分離し、Composable はあくまでも 宣言的UI であり続けるべく、極力UIの表現のみを記述することをお勧めします。 6. おわりに 本記事が、ひとりでも多くの開発者にとって Android View から Jetpack Compose に踏み出すきっかけとなれれば幸いです。考え方の異なるUIシステムへの移行は一筋縄ではいかず、挫折の不安がつきまといます。その第一歩の足取りができるだけ軽やかになること、かつ徒労に終わるリスクができるだけ抑えられることを願っています。 7. 参考文献 Android API reference [Android]View の位置を取得する関数たち Jetpack Compose Modifier 徹底解説 What can Advanced / Lesser Known Modifiers do for your UI? — A Comprehensive Exploration in Jetpack Compose A Journey Through Advanced and 備考 Android ロボットは、Google が作成および提供している作品から複製または変更したものであり、 クリエイティブ・コモンズ 表示 3.0 ライセンスに記載された条件に従って使用しています。[^2] [^2]: https://developer.android.com/distribute/marketing-tools/brand-guidelines#android_robot
アバター
初めに KINTOテクノロジーズのQAグループに所属しているパンヌウェイ(PannNuWai)です。QAグループでアプリチームのテスト自動化担当として KINTOかんたん申し込みアプリ のテスト自動化環境の構築と整備をしていたり、テスト仕様書とテストスクリプトを書いています。 前回は、 Appiumを利用したDarkMode自動化テスト に関する技術ブログを記載しました。 Appiumによる自動化テストに携わったのは3年になり、自分のチームでは自動化テストソースコード前にテスト仕様書作成の順番で行なっております。 だからAppiumのJavaソースコード書く時とても役に立ているテスト仕様書作成方法についてお話ししたいと思います。 テスト仕様書作成前になぜこの仕様書作成方法が必須ですかの質問について回答させて頂きます。 テスト仕様書作成のメリット 自動化テストソースコード書く場合時間削減できる 自動化テスト者以外の方にも分かり安い 画面ごと、ソースコードクラスと操作機能によって分別してるので読みやすい 本記事ではKINTOかんたん申し込みアプリのログインシナリオ仕様書をサンプルテスト仕様書として説明します。 ログインステップ こちらにKINTOかんたん申し込みアプリログインの各ステップについて教えます。 ステップ 1 ステップ 2 ステップ 3 ログインに関するテストデータを収集する ログインテスト仕様書作成の初期ステップとして、テスト環境、テストアカウント、パスワード、テスト実行後に自動出力されているレポート名、テスト実行ファイルなどのテストデータを収集する必要があります。それでは、iOSのログインシナリオ仕様書から始めましょう。 データ名 データ情報 環境 Stg4 テストアカウント ******@gmail.com パスワード ******** レポート名 iOS用 iOS.poc.login android用 android.poc.login 実行ソースファイル iOS用 iOS.poc.login.xml android用 android.poc.login.xml iOS用のログインシナリオ仕様書 確認機能 画面 操作 確認ポイント 実行ファイル(xml) ソースファイル メソッド ログインする メイン画面 マイページロゴを押下する ログインボタンが押下できる iOS.poc.login.xml iOS.MainPage clickMyPagingLogo マイページ画面 『お申し込み内容』を押下する 『ログインはこちら!』を押下する iOS.MyPagingPage clickApplyTab clickLoginHereButton ログイン画面 『メールアドレス(KINTO ID)』に上記のテストアカウントを記入する 『ログインパスワード』に上記のパスワードを記入する 『My KINTOへログイン』を押下する iOS.LoginPage fillMailAddress fillPassword clickToMyKintoLoginButton これからシナリオ仕様書の各画面によってJavaのクラスを作成しましょう。まずは、メイン画面(MainPage.java)のマイページロゴを押下します。 MainPage.java public class MainPage extends Base { public static final String MY_PAGING_LOGO = "//XCUIElementTypeButton[@name=\"マイページ\"]"; /** * メイン画面の『マイページ』ロゴのクリックするテストメソッド * * このメソッドはXPathを使用してメインページ上の"マイページ"ロゴを特定し、 * クリックアクションを実行する * */ @Test(groups = "MainPage") public void clickMyPagingLogo() { driver.findElementByXPath(MY_PAGING_LOGO).click(); } } ステップ 2 としてマイページ画面の『お申し込み内容』を押下した後『ログインはこちら』をクリックします。 MyPagingPage.java public class MyPagingPage extends Base { public static final String APPLY_TAB = "//XCUIElementTypeButton[@name=\"お申し込み内容\"]"; public static final String LOGIN_HERE_BUTTON = "//XCUIElementTypeButton[@name="My KINTOへログイン\"]"; /** * マイページ画面の『お申し込み内容』をクリックするテストメソッド * * このメソッドはXPathを使用してマイページ画面の『お申し込み内容』を特定し、 * クリックアクションを実行する * */ @Test(groups = "MyPagingPage", dependsOnGroups = "MainPage") public void clickApplyTab() { driver.findElementByXPath(APPLY_TAB).click(); } /** * マイページ画面の『ログインはこちら』をクリックするテストメソッド * * このメソッドはXPathを使用してマイページ画面の『ログインはこちら』を特定し、 * クリックアクションを実行する * */ @Test(groups = "MyPagingPage", dependsOnMethods = "clickApplyTab") public void clickLoginHereButton() { driver.findElementByXPath(LOGIN_HERE_BUTTON).click(); } } 次は ステップ 3 として『ログイン画面』にメールアドレスとパスワードを記入しながらログインボタンを押下します。 LoginPage.java public class LoginPage extends Base { public static final String EMAIL_TEXT_FIELD = "//XCUIElementTypeApplication[@name=\"KINTO かんたん申し込み \"]/XCUIElementTypeOther[2]/XCUIElementTypeTextField"; public static final String PASSWORD_TEXT_FIELD = "//XCUIElementTypeApplication[@name=\"KINTO かんたん申し込み \"]/XCUIElementTypeOther[3]/XCUIElementTypeSecureTextField"; public static final String ENTER_KEY = "//XCUIElementTypeButton[@name=\"Return\"]"; public static final String TO_MY_KINTO_LOGIN_BUTTON = "//XCUIElementTypeButton[@name=\"My KINTOへログイン\"]"; /** * ログイン画面の『メールアドレス(KINTO ID)』にテストアカウントを記入するテストメソッド * * このメソッドはXPathを使用してログイン画面の『メールアドレス』で * xmlファイルのテストアカウント(Parameter)を記入する * */ @Parameters("email") @Test(groups= "LoginPage", dependsOnGroups = "MyPagingPage") public void fillMailAddress(String email) { driver.findElementByXPath(EMAIL_TEXT_FIELD).click(); driver.getKeyboard().sendKeys(email); }   /** * ログイン画面の『ログインパスワード』にパスワードを記入するテストメソッド * * このメソッドはXPathを使用してログイン画面の『ログインパスワード』で * xmlファイルのパスワード(Parameter)を記入する * */ @Parameters("password") @Test(groups= "LoginPage", dependsOnGroups = "MyPagingPage") public void fillPassword(String password) { driver.findElementByXPath(PASSWORD_TEXT_FIELD).click(); driver.getKeyboard().sendKeys(password); driver.findElementByXPath(ENTER_KEY).click(); } /** * ログイン画面の『My KINTOへログイン』を押下するテストメソッド * * このメソッドはXPathを使用してログイン画面の『My KINTOへログイン』を特定し、 * クリックアクションを実行する * */ @Test(groups= "LoginPage", dependsOnGroups = "MyPagingPage") public void clickToMyKintoLoginButton() { driver.findElementByXPath(TO_MY_KINTO_LOGIN_BUTTON).click(); } } 下記のxmlファイルは自動化テストのテスト実施ファイルです。テスト仕様書に基づいて各機能を順番に書いています。 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"> <suite name="iOS.poc.login"> <test verbose="2" name="iOS.poc.login"> <classes> <class name="iOS.MainPage"> <methods> <include name="clickMyPagingLogo"/> </methods> </class> <class name="iOS.MyPagingPage"> <methods> <include name="clickApplyTab"/> <include name="clickLoginHereButton"/> </methods> </class> <class name="iOS.LoginPage"> <methods> <parameter name="email" value="******.gmail.com"/> <parameter name="password" value="*********"/> <include name="fillMailAddress"/> <include name="fillPassword"/> <include name="clickToMyKintoLoginButton"/> </methods> </class> </classes> </test> </suite> まとめ 本記事では、AppiumのJavaソースコード向けのテスト仕様書作成方法を説明しましたが、Appiumだけではなく他の自動化テストソースコード作成にも参考になるように期待しています。
アバター