TECH PLAY

KINTOテクノロジーズ

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

936

この記事は 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日目の記事です🎅🎄 学びの道の駅の中西です。今年は学びの道の駅プロジェクトが立ち上がり組織化されました。そんな中、社内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](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日目の記事です🎅🎄 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 の18日目の記事です🎅🎄 学びの道の駅の中西です。今年は学びの道の駅プロジェクトが立ち上がり組織化されました。そんな中、社内Podcastの運営も行っており、今年のアドベントカレンダーではその内容もお届けしたいと思います。 「学びの道の駅」とは? 「学びの道の駅」は、社内で頻繁に開催される勉強会をもっと便利に、そして効果的に活用するために立ち上げられたプロジェクトです。社内の有志が中心となり勉強会の開催を支援し社内の知識共有を促進することを目的としています。 Figma勉強会 KTC学びの道の駅ポッドキャストでは、社内の勉強会を開催している方にインタビューを行っています。その名も「突撃!となりの勉強会」。本日のポッドキャストのゲストは、Figma勉強会のご担当であるT.M.さんとM.K.さんです。早速ですが、お二人の業務について教えていただけますか?まずはT.M.さんからお願いします。 インタビュー T.M.さん: モビリティプロダクト開発部のDX企画推進グループDXプランニングチームに所属しています。普段は社内の販売店DXに関するプロダクト開発をデザイナーとして担当しています。最近はモバイルアプリのデザインに注力しており、秋頃にリリースを予定しています。チームができてまだ1年ほどなので、これから本格的に事業化を目指していく段階です。 明田さん: ありがとうございます。ではM.K.さん、お願いします。 M.K.さん: マーケティング企画部のデザイナーとして働いています。主にビジュアルデザインとUI/UXデザインを担当しており、最近は新車サブスクサービスのサイトデザインを手掛けています。 明田さん: ありがとうございます。さて、先週金曜日に開催されたFigma勉強会についてですが、開催のきっかけは何だったのでしょうか? T.M.さん: 中西さんがふらっとやってきて、「Figmaの勉強会してよ」と声をかけてくださったのがきっかけです。ただ、その前からKTC内でFigmaの使い方に改善の余地があると感じていたので、良いタイミングでした。そこでM.K.さんと共に、どういった形が良いか詰めていき、先週の金曜日に初回を開催しました。 明田さん: 中西さん、その最初の声かけのきっかけは何だったのでしょうか? 中西: T.M.さんが入社されてから、デザインの改善意欲が強いと感じました。Figmaの勉強会もできたらいいねという話が出ていたので、そろそろその時が来たかなと感じて声をかけました。 明田さん: ありがとうございます。実際に勉強会を開催してみて、参加者の反応はいかがでしたか? T.M.さん: 反応はまずまずでした。多くの参加者がFigmaについてさらに知識を深めたいと感じているようでした。特にQ&Aセッションでは多くの質問が上がり、皆さんが関心を持っていることが分かりました。 M.K.さん: 私も同様に感じました。皆さんがFigmaの使い方やUI/UXの改善に興味を持っていることが分かり、良い機会だったと思います。 明田さん: 具体的にはどのような質問がありましたか? T.M.さん: テクニカルな質問から、KTC内でのUI/UXの取り組みについての質問まで様々でした。特にエンジニアからの質問が多く、彼らもUXに対して強い関心を持っていることが分かりました。 明田さん: これからの勉強会の展望について教えてください。 T.M.さん: 短期的には、生産性向上や効率化を目指し、必要なテーマやコンテンツを提供していきたいです。長期的には、デザイナーのプレゼンスを上げ、ビジネスの成功に貢献するための手段としてFigmaを活用していきたいです。 M.K.さん: 私も同じ意見です。エンジニアやデザイナーが共通言語を持ち、コミュニケーションを取れる場を増やしていきたいと考えています。 明田さん: ありがとうございます。最後に、このポッドキャストを聞いている皆さんに一言お願いします。 M.K.さん: デザインはビジュアルだけではありません。誰でも意見を持つことが大切です。気軽にコミュニケーションを取りましょう。 T.M.さん: 良いプロダクトを作るためには、全員がデザイナーであるという意識を持つことが重要です。気軽に相談してください。 明田さん: ありがとうございました。本日のポッドキャストはここまでです。T.M.さん、M.K.さん、ありがとうございました。Figma勉強会の取り組みや、デザインに対する熱意が伝わりました。これからもKTCのデザインが進化していくことを楽しみにしています。 今回はFigma勉強会の詳細と、その運営の背景、今後の展望についてお届けしました。次回の勉強会も楽しみにしてください!
アバター
初めに 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だけではなく他の自動化テストソースコード作成にも参考になるように期待しています。
アバター
この記事は 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
アバター
Hello, this is Hiroya (@___TRAsh) 🎅 This year, our Mobile App Development Group has focused extensively on sharing our work with the broader community. We have engaged in various initiatives, such as sponsoring and presenting at events like iOSDC and DroidKaigi, as well as contributing to this tech blog. This has provided us with more opportunities to connect with all of you throughout the year. To wrap up 2024, we’re excited to launch a series of posts as part of the KINTO Technologies Advent Calendar, focusing on Android/Flutter/iOS 🎉 https://qiita.com/advent-calendar/2024/kinto-technologies Our Android/Flutter/iOS engineers are working hard to publish the entire series, so please check them out 🎅 Today, I would like to introduce the Mobile App Development Group, the group behind our Android, Flutter, and iOS app development. What does the Mobile App Development Group do? We develop mobile apps for KINTO Technologies across iOS, Android, and Flutter platforms. The main products we develop are as follows. https://kinto-jp.com/entry_app/ https://kinto-jp.com/unlimited/app/ https://top.myroute.fun/ https://ppap.kinto-jp.com/prismjapan In addition to the above, we also take on development projects in response to PoC requests. Moreover, by leveraging the benefits of being a cross-functional team, we frequently host in-house study sessions to actively share new technologies and knowledge among team members. A survey of our team members This time, we conducted a survey with our engineers. We were able to gather insights that are not usually visible in our daily work, and I would like to share them here. 1. What is your development environment? (Up to 2 responses allowed) ![Development Environment Pie Chart](/assets/blog/authors/HiroyaHinomori/mobile_advent_calendar_2024_12_01_01.png =450x) One reason for the high percentage of Android in our development environments is the large number of Android engineers on our team. This is quite rare in Japan, I believe. Also, the Flutter team has been formed this year! We hope to gradually share more about Flutter-related development in the future. 2. How many years have you been developing? ![Development Experience Pie Chart] (/assets/blog/authors/HiroyaHinomori/mobile_advent_calendar_2024_12_01_02.png =450x) Since our company primarily hires mid-career professionals, many of our engineers have extensive experience. I was surprised to see so many with over 10 years of experience. It’s something that I hadn’t realized before. I look forward to learning from these experienced engineers as we continue to grow together. 3. Where are you from? ![Hometowns Pie Chart] (/assets/blog/authors/HiroyaHinomori/mobile_advent_calendar_2024_12_01_03.png =450x) I had a feeling this might be the case, but it turns out that less than half of our team members are Japanese. This is quite a unique workplace! While most communication happens in Japanese, we also hear English, Chinese, and Korean, creating a truly global environment. 4. What do you like about the Mobile App Development Group? We generated a word cloud from the comments we received to showcase the strengths of our team. It’s clear that atmosphere and technology stand out prominently. This seems to reflect the results of our efforts to prioritize communication across product teams and actively share knowledge—something that can be overlooked in daily work. We will continue to do our best next year as well 💪 :::details Summary Technology and learning environment Openness to new technologies: A flexible environment that encourages the adoption and use of the latest technologies. Support for skill development: A learning-friendly setting with active study sessions and knowledge sharing. High skill level: Members with strong technical skills and passion for growth. Focus on output: A dedication to delivering results and achieving goals. Communication and atmosphere Friendly atmosphere: A supportive environment where members can easily ask questions and share comments. Collaborative teamwork: Smooth collaboration both within and across project teams. Diversity and humor: A multicultural and diverse team that embraces individuality and enjoys cultural differences. Flat communication: Minimal barriers between members, regardless of age or background. Work environment Flexible and free work styles: A work culture that respects individual approaches and autonomy. Good team spirit: A cohesive team where everyone works well together. Supportive Environment: A welcoming and safe space where members feel comfortable working. It can be said that these qualities create an ideal team environment where learning, growth, diversity, and collaboration are valued and truly enjoyed. ::: 5. What technology are you most interested in right now? iOS Android We also created a word cloud for this topic using the comments we received. For both Android and iOS, KMP seems to be attracting attention. We can also see mentions of Flutter and Compose Multiplatform, which suggests that many team members are interested in cross-platform technologies. From my perspective, this year has truly been a breakthrough for cross-platform development. Additionally, it appears that many are keeping an eye on the technical evolution of programming languages specific to each platform. Moreover, the attention given to AI-related technologies reflects current industry trends, showing the high level of technical interest among our team members. :::details Summary iOS Top 3 Technologies Swift / SwiftUI Core technologies for Apple platforms, with particular interest in UI development. KMP (Kotlin Multiplatform) Utilized for multiplatform development, including integration on the iOS side. AI (MLC LLM、Apple Intelligence) Growing interest in machine learning and Apple’s AI technologies. Android Top 3 Technologies Jetpack Compose The core UI development technology, with active exploration of efficient coding practices and mastery. KMP (Kotlin Multiplatform) Notable for its use in Android app development and its integration with Compose Multiplatform. Flutter A popular choice for cross-platform development. ::: Summary I feel that the Mobile App Development Group is creating an environment where members can enjoy diversity and collaboration while learning. This is achieved through aspects such as a focus on technology and learning, effective communication and atmosphere, and ease of working. As mobile technology trends evolve rapidly, I aim to stay updated and contribute to the group’s growth. Lastly, I hope this article has conveyed the appeal of the Mobile App Development Group to you 🎅 Stay tuned for our Advent Calendar kicking off tomorrow 🎄
アバター
自己紹介 こんにちは。KINTOテクノロジーズの小山( @_koyasoo )です。今年からアジャイル推進に注力し始め、普段はスクラムマスター専任でチーム作りに励む毎日を送っています。今回はその中の活動を紹介したいと思います。 レトロスペクティブといえば・・? みなさん、ふりかえり(レトロスペクティブ)していますか? レトロスペクティブといえばKPT。ふりかえり手法のもはや代名詞と言われるようになってきましたね。KPTを使っている現場はものすごく多いと思いますが、果たしてそれは機能しているでしょうか? 私は今年の6月にスクラムフェス大阪に参加し、色々なセッションを受ける中で1つ強烈に印象に残ったセッションがありました。 OODA!!!!! (わかる人にはわかる) はい、いくおさん( @dora_e_m )ですね。いくおさんのふりかえりに関するセッションがとても印象に残りました。それまでふりかえりにはKPTしか使っていなかった私に 「ふりかえりが実りある場であり続けるために マンネリ化を避けたい」 という言葉がとても刺さりました。そこで「ふりかえりならいつもやっているし、すぐに行動に移せるのでは?」と思いました。 @ card 特にセッションの22ページ目に書いてあるように、KPTをYWT(KPTと似ている)に変更しただけで、めちゃくちゃ意見が出たというところが目から鱗でした。発想を変えるだけで意見って出るのか・・。 ![いくおさん資料22ページより引用](/assets/blog/authors/tkoyama/retro/ikuo-22.png =600x) いくおさん資料22ページより引用 そこで、本記事では私が実際に実践した5つのふりかえり手法をご紹介します。 ふりかえり手法の使い分け方まとめ いくおさんのセッションにもあるように、状況にあったふりかえり手法を切り替えることができると、より良いチームの成長につながると考えています。以下に各手法の特徴をまとめてみたのでぜひ参考にしてください。 こんな時に使える! 気をつけること KPT いつでも使えるオールマイティ こればっかりにならないこと 熱気球 チームの未来を考えたいとき 現状ある課題より過去のふりかえり要素が小さくなる LeanCoffee いろんな話題をしたいとき, ふりかえりじゃなくても使える 時間に追われてディスカッションするので少々疲れる Celebration Grid 事実に基づいてディスカッションしたいとき 実施したことが少なく、感じたことが多いタイミングだと意見が出ずらい FunDoneLearn ポジティブにふりかえりたいとき ネガティブが無視されがち 象、死んだ魚、嘔吐 チームに不満が溜まっていそうなとき チームが崩壊しないようにファシリテーションする 以降では、それぞれの振り返り手法について具体的にご紹介します。 ふりかえり紹介! この記事を読んでくれた方(特に今までKPTしかやっていない方)にぜひ、 別手法を試す第一歩を踏んでほしい! と考え、実施の仕方をできるだけ具体的に紹介したいと思います。やりやすいもので構わないので、ぜひやってみてください。意外とやってみたらKPTとそんなに変わらないです! なお、紹介する例では基本的にオンラインホワイトボードツールの Miro を使用しています。 @ card 1. 熱気球 中央に描かれる「熱気球」を自身のプロダクトと置き換え、それに対しどんな「荷物」があったか、「上昇気流」は何か、今後妨げとなりうる「雲」は何だろうか、という考え方でふりかえりを進める手法となります。 熱気球のイメージ画像と、3種類の見分けられる付箋を準備しておけば実施可能です。我々のチームではこのような熱気球が作成できました。 熱気球の図 実施方法は以下のとおりです。 まず「上昇気流」について書き(5分)、ディスカッションを実施(8分)。 次に「荷物」について書き(5分)、ディスカッションを実施(8分)。 同様に「雲」について書き(5分)、ディスカッションを実施(8分)。 最後に「より熱気球を高く飛ばすために重要なことは?」という観点で議論(10分)。 この手法では現在を見つめ直し、未来を想像することにフォーカスが当たる議論が生まれやすいです。KPTと比べるとProblemがProblem(現在)、Problem(予想)に分解されるため、より課題点をディスカッションしやすい手法とも言えます。 2. LeanCoffee 話題の洗い出しから始まり、さらにそれらに対して短いタイムボックスに区切って様々な会話を実施する手法です。 話題洗い出し用の付箋を編集できるエリア、ディスカッションに選出された付箋を順番に処理するエリアを準備しておけば実施可能です。こちらはMiroにテンプレートがありましたのでそちらを使ってみました。 LeanCoffeeの図 実施方法です。 はじめに参加者にお題を挙げてもらいます(8分)。このときテーマを指定してあげると参加者が意見を出しやすいです。 投票機能などを使い参加者の興味のあるお題を特定します。 票数の多いものから順番に以下のサイクルでディスカッションします。 最初は5分間、お題の説明するところを含めてディスカッションを開始します。 5分経った時点で一旦議論を止めます。そしてこのお題でまだ続けるかを参加者に問います。投票機能を使っても良いです。 続ける場合は追加で3分、終わる場合は次のお題に移ります。 3分経った時点でも同様に議論を止め、再度続けるか問います。 続ける場合は追加で1分、終わる場合は次のお題に移ります。 最後の1分が終わったらそのお題は終了です。追加で話したい場合は別途時間を設けるようにして、その時間でのディスカッションは終了させましょう。 この手法はとにかく色々な話題が次々に移り変わりますので、たくさんのことについてディスカッションができます。また、チームメンバーのその時興味のある方向性が傾向としてみれます。さらに、メンバーのタイムボックスへの意識付けにも使えるかもしれません。 ファシリテーション時には特に議論を止めるのが難しいですが、タイマーで音を鳴らしたり、会話の切れ目などで何とか止めるようにしましょう。この手法はタイムボックスが守れないと崩壊してしまいますので注意です。 3. Celebration Grid 実施した事柄に対して「成功」「失敗」の軸と「間違ったやり方」「実験的なやり方」「既に知っているやり方」の軸で、六象限に分けてディスカッションを進める手法です。名前の通り、成功・失敗に関わらず起きたことをお祝いするポジティブなマインドで進めます。 こちらのサイトの図を使用して進めることが多いみたいです。 @ card ![CelebrationGridのテンプレート](/assets/blog/authors/tkoyama/retro/celebration-grid-completed.jpg =600x) CelebrationGridのテンプレート 図の通り、それぞれのエリアは事象が発生する確率に合わせて大小サイズが異なっており、以下のような意味合いがあります。これらに沿ってディスカッションを進めました。 間違ったやり方 実験的なやり方 既に知っているやり方 成功 ラッキーだったね! いい体験をしたね! 正しいことをしたね! 失敗 なるべくしてなった 大丈夫、学びはあったさ 運がなかった Celebration Gridをやってみた図 実施方法です。 期間を指定して「やったこと」を挙げてもらいます(5分)。それぞれどこに属すかを考えながら挙げてもらうように誘導します。 それぞれについて深掘ります。 最後に多くの気づきがあったことを全員で祝って終了します。 ぱっと見、難しそうな手法かと思いきや結構シンプルです。「発生した事象・事実」に基づいて会話が進むので、思想に捉われず事実と分離しながらディスカッションできます。逆に「事実」をまず挙げなければいけないので、参加者によっては意見が出ずらい場合があります。メンバーを集める際にできるだけ実働している人を選定するとうまくいきそうです。 4. FunDoneLearn 文字通り、Fun(楽しかったこと)、Done(やったこと)、Learn(学んだこと)を挙げていく手法になります。それぞれを包含する要素は円が重なるところに書いてもらいます。 それぞれの円を重ねたベン図のようなテンプレートを用意しておけば実施可能です。重なるエリアを大きくしておくと付箋が貼りやすいです。 FunDoneLearnの図 記載するほどではないですが、実施方法です。 期間を指定して、付箋を貼ってもらう(5分)。 それぞれに対してディスカッションする Funの要素があるように、ポジティブにフォーカスする手法です。全体的にハッピーな気持ちで振り返りが実施できると思います。逆にProblemのようなネガティブ要素が少ないので、それらが多そうな場合はあまり向かないかもしれないです。 5. 象、死んだ魚、嘔吐 3つの角度から課題を洗い出す手法です。象(みんな知っている課題)、死んだ魚(放置するとまずい課題)、嘔吐(心の中にある課題)といった具合で、メンバーから普段言いづらいことをぶっちゃけてもらいます。 象と魚と嘔吐の図があれば実施可能ですが、メンバーの個人間の対立を防ぐために大きめにルールを書いておくことを推奨します。 象、死んだ魚、嘔吐の図 実施方法です。 まずルールの説明をします。この手法でチーム内を対立させたいわけではなく、今ある課題に対して打ち手を考えたいだけであることを説明します。この手法では大事なポイントだと思っています。 それぞれに沿って付箋を挙げてもらいます(8分)。私のチームではネガティブに寄りすぎないように、反転してポジティブな意見になればピンクの付箋で追記してもらうよう案内しました。 特にこの手法では意見出しの最中に他のメンバーの内容が見えてしまうと、場合によってはモヤモヤしてしまうかもしれないため、編集中の付箋は公開しないようにします。MiroであればPrivate Modeを使うと良いです。 Private Modeを解除し、それぞれに対してディスカッションします。 ネガティブに向き合う手法のため、他の手法に比べると少々気を遣うことが多いです。ですが、そこまでネガティブな雰囲気にはならず、「そんなこと気にしてたのか〜」「同じこと思っていた!」などの意見が出ると予想されます。チームでの課題解決の方向性を合わせるのに非常に有効だと思います。 どのふりかえりにも言えること KPTを含め6つのふりかえりを実施した私の所感ですが、共通項は意外と多いです。 最終的なゴールはいつも「ネクストアクションをチームで合意する」 大きめにルールを書いておくと参加者が迷わない 意見は会の中で挙げてもらえばOK!ベースは5分、考えさせる内容なら8〜10分 ディスカッションではまず付箋を説明してもらい、次に「自分で感想を言うか」「感想ありそうな人に雑に振る」!(雑な方がフラットに意見が出やすいと思ってます笑) 自分も意見を出す場合は、事前に付箋を用意しておいた方がファシリテーションに集中できる 特に1番目の「ネクストアクションを合意」が意識できていれば、どの手法も回すことができます。それさえ決まればふりかえりを実施した価値になります。他のことは全て忘れても良いくらいです。 参加者からの感想 新しい手法を新しいメンバーで実施した後はいつも不安になります。ぶっちゃけKPTで良かったんじゃないか・・、色々指示しすぎて果たして振り返れていたか・・。 もしこんな思いをしていたら(自分のように)、恐れずにメンバーに感想を聞いてみましょう!きっとポジティブな意見しか返ってこないですよ。 いつもと違うふりかえりで新鮮だった。楽しかった。(熱気球) タイムボックスを意識して話ができたので、いつもは同じ話題で終わってしまうが色々な話ができて良かった。課題点が明確になった。(LeanCoffee) 失敗が多くなってしまったが、それがいい失敗なのか悪い失敗なのか分けて考えることができた。(Celebration Grid) チームメンバーが何に楽しさを感じて仕事をしているのかわかって良かった。シンプルに楽しかったことをシェアできたのも良かった。(FunDoneLearn) 思っていた課題がチームの共通認識にできて良かった。ぶっ込んでくれて助かった。(象、死んだ魚、嘔吐) まとめ どれかの手法に固定するのではなく、シーンに合ったふりかえり手法を選んで実施できるようになると、より良いチームになっていくと考えます。私もまだ6手法しか試せていないので、これからどんどん増やしていきたいと思います! 冒頭でも記載した通り、まだKPTしかやっていない方がいればぜひどれかの手法を試してみてください!まずは本記事記載の流れで。慣れてきたらアレンジしてチームに合わせてみるなんてどうでしょうか。 この記事がレトロスペクティブで悩めるスクラムマスターの後押しになったら嬉しいです。
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の17日目の記事です🎅🎄 学びの道の駅の中西です。今年は学びの道の駅プロジェクトが立ち上がり組織化されました。そんな中、社内Podcastの運営も行っており、今年のアドベントカレンダーではその内容もお届けしたいと思います。 「学びの道の駅」とは? 「学びの道の駅」は、社内で頻繁に開催される勉強会をもっと便利に、そして効果的に活用するために立ち上げられたプロジェクトです。社内の有志が中心となり勉強会の開催を支援し社内の知識共有を促進することを目的としています。 セキュリティ&プライバシー勉強会 KTC学びの道の駅ポッドキャストでは、社内の勉強会を開催している方にインタビューを行っています。その名も「突撃!となりの勉強会」。本日のポッドキャストのゲストは「セキュリティ&プライバシー勉強会」を開催している桑原さん、森野さん、笠井さんにインタビューします。みなさん、どうぞよろしくお願いいたします。 インタビュー 森野さん: セキュリティ・プライバシーグループの森野です。よろしくお願いします。 笠井さん: データ&プライバシーガバナンスチームの笠井と申します。よろしくお願いします。 桑原さん: セキュリティCoEグループの桑原です。よろしくお願いします。 HOKAさん: では、まず皆さんの普段のお仕事について教えていただけますか? 森野さん: 私たちのチームは三つに分かれています。データ&プライバシーガバナンスチーム、インフォメーションセキュリティチーム、そしてサイバーセキュリティディフェンスチームです。データ&プライバシーガバナンスチームは、データとプライバシーに関するルールの整備やガバナンスの効かせ方を担当しています。インフォメーションセキュリティチームは、情報セキュリティのスタンダードに基づいてアセスメント活動を行います。そして、サイバーセキュリティディフェンスチームは、脆弱性の管理や脅威情報の収集・調査を担当しています。 笠井さん: 私のチーム、データ&プライバシーガバナンスチームは、データに関するセキュリティ管理や個人情報の管理に関するルールを制定し、リスク評価を行い、それを経営に報告しています。 桑原さん: セキュリティCoEグループは、AWS、Azure、Google Cloudなどのクラウド環境のセキュリティを担当しています。安全にクラウドを利用するためのガードレールの設定や、危険な兆候の監視・サポートを行っています。 HOKAさん: 次に、セキュリティ&プライバシー勉強会について教えてください。どのような勉強会ですか? 桑原さん: この勉強会は、社員の皆さんにセキュリティに関連する知識を広めるために開催しています。昨年度から具体的なガイドラインを作成し、それを周知させるために勉強会を始めました。 森野さん: この勉強会を通じて、セキュリティバイデザインやプライバシーバイデザインが当たり前の要件として検討され、実装されることを目指しています。 笠井さん: 私たちの目的は、社員の皆さんがエンジニアリングに集中できるようにし、プライバシーやセキュリティのリスクについては我々がサポートすることです。 HOKAさん: 勉強会の反響はいかがですか? 桑原さん: 今月までに計4回実施しました。反響は良く、勉強になったという声や改善要望もいただいています。 森野さん: 参加者からは、説明がわかりやすくなったという声もあり、工夫を重ねています。 HOKAさん: この勉強会を通じて、KTCにどのような変化を期待していますか? 森野さん: セキュリティやプライバシーの機能が標準として実装される状態を目指しています。 桑原さん: 私たちの仕事が不要になるくらい、セキュリティが当たり前になることが理想です。 笠井さん: エンジニアの皆さんが安心して開発に集中できる環境を作りたいです。 HOKAさん: 最後に、皆さんがこの分野に進んだきっかけを教えてください。 森野さん: 私はソフトウェア開発からセキュリティに興味を持ち、学び続けています。 笠井さん: もともとセキュリティやプライバシーには関心がありませんでしたが、ガバナンスの面白さに引かれました。 桑原さん: 私はインフラエンジニアとしてキャリアを始めましたが、自分の管理するメールサーバーが侵害された経験からセキュリティに取り組むようになりました。 HOKAさん: 本日はありがとうございました。セキュリティ&プライバシー勉強会がKTCの皆さんにとって有益なものになることを期待しています。 皆さん、どうもありがとうございました。 今回はセキュリティ&プライバシー勉強会の詳細と、その運営の背景、今後の展望についてお届けしました。次回の勉強会も楽しみにしてください!
アバター
Flutterアプリとネイティブ機能の連携 〜Android専用のカメラ解析ライブラリを組み込むために検討したこと〜 こんにちは。Toyota Woven City Payment 開発グループの大杉です。 私たちのチームでは、 Woven by Toyota の Toyota Woven City で使用される決済システムの開発をしており、バックエンドからWebフロントエンド、そして、モバイルアプリケーションまで決済関連の機能を幅広く担当しています。 これまで、私たちはFlutterを使ってProof of Concept (PoC) 用のモバイルアプリの開発を行なって来ました。今回は、そのPoC用アプリに新たにAndroid/iOSネイティブでしか提供されていないカメラ解析ライブラリを組み込み、新機能を開発した際に直面した課題に対して試行錯誤したことをまとめました。 はじめに Flutterアプリにネイティブ機能を組み込むことは、単純な開発工数だけでなくメンテナンスのコストも増大するので、開発のハードルが高くなります。 私たちのプロジェクトでは、開発期間とリソースを鑑みてFlutterアプリにネイティブ機能を組み込まず、PoC用アプリとカメラ解析用のネイティブアプリを別々に開発し、それらを連携させることでPoCを実施しました。PoC完了後、Flutterアプリとカメラ解析アプリの統合を検討した際、Flutterのネイティブ連携機能に関して設計指針や実装方法などの情報が断片的であり、特にAndroidの複雑なUI構成に対して体系的な指針が少ないと感じました。 この記事では、Androidにフォーカスを当て、FlutterアプリにネイティブUIを組み込むための設計指針と具体的な実現方法を紹介します。 同じような境遇に直面しているエンジニアの皆さんの参考になれば幸いです。 :::message 執筆時のサンプルコード作成には Flutter v3.24.3 / Dart v3.5.3 を使用しています。 ::: アプリの概要 実際のPoCで開発したアプリを今回の記事に合わせて簡略化すると、以下のような仕様のアプリとなります。 仕様 startボタンを押下すると、カメラプレビューが表示される カメラプレビューの画像に対してカメラ解析機能が実行され、解析結果が通知される この記事では、このアプリをベースに話を進めていきたいと思います。 FlutterとAndroidネイティブのデータ連携 まず、Flutterからカメラを操作したり、Androidネイティブから解析結果を通知するなど、FlutterとAndroidネイティブ間のデータのやりとりでは、 MethodChannel と EventChannel を使用して実装しました。 特に、カメラの起動・停止などの命令については MethodChannel 、解析結果のイベントの通知については EventChannel を使用しています。 シーケンス図に表すと以下のようになります。 sequenceDiagram actor u as User participant f as Flutter participant mc as MethodChannel participant ec as EventChannel participant an as Android Native u ->> f: press start button activate f f ->> mc: start camera mc ->> an: set up camera an -->> mc: mc -->> f: result deactivate f loop an ->> ec: analyzed result ec ->> f: send analyzed data f ->> f: show data end u ->> f: press stop button activate f f ->> mc: stop camera mc ->> an: reset camera an -->> mc: mc -->> f: result deactivate f 次に、AndroidネイティブのカメラプレビューのUIをFlutter側に表示させる方法について話をしたいと思います。 FlutterアプリでAndroidのネイティブUIを表示する方法 FlutterアプリでAndroidネイティブのUIを表示する方法は、大きく分けて3つあります。 AndroidネイティブのSurfaceに描画された画像をFlutterのWidget tree内で表示する Texture widget AndroidネイティブUIをFlutterのWidget tree内に組み込み、表示・制御できる PlatformView 新たにActivityを起動する Intent それぞれの特徴と実装方法について説明します。 Texture widget Texture widgetは、AndroidネイティブのSurfaceに描画された画像をFlutter側のWidget tree内に表示します。つまり、ネイティブのUIの画像をFlutterからGPUに直接描画するものです。 この機能は、カメラプレビューや動画の再生などレイテンシーがあまり問題にならないユースケースには適していますが、リアルタイム性が求められるUIアニメーションなどではネイティブ側で調整する必要があり、FlutterとAndroidネイティブについて習熟している必要があると言えます。 また、 Texture widget自体にはタッチイベントなどのユーザーインタラクションを検知する機能もないため、Flutter側で GestureDetector などを使って実装する必要があります。 ただし、要件がマッチしていれば以下の実現方法のように比較的簡単に実装できます。 実装方法 まずは、 TextureRegistry というものを取得します。 Flutterアプリの場合は、TextureRegistryの実装である FlutterEngine.FlutterRenderer を取得できます。 Flutterプラグインの場合は、FlutterPluginBindingから取得できます。 // Flutterアプリの場合 val textureRegistry = this.flutterEngine.renderer // Flutterプラグインの場合 val textureRegistry = this.flutterPluginBinding.textureRegistry 次に、 textureRegistry から SurfaceTexture である textureEntry を生成し、 CameraXのプレビューインスタンスに対して Surface を提供する SurfaceProvider を設定します。これで準備は完了です。この Surface が、前述した描画バッファとなります。 val textureEntry = textureRegistry.createSurfaceTexture() val surfaceProvider = Preview.SurfaceProvider { request -> val texture = textureEntry?.surfaceTexture() texture?.setDefaultBufferSize( request.resolution.width, request.resolution.height ) val surface = Surface(texture) request.provideSurface(surface, cameraExecutor) { } } val preview = Preview.Builder().build().apply { setSurfaceProvider(surfaceProvider) } // 記事冒頭のカメラを解析する要件を満たすには、 // cameraProviderを用意し、Previewと解析処理をここでカメラに設定することで実現できます。 try { camera = cameraProvider?.bindToLifecycle( this, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis, // ここにカメラ映像の解析処理を設定 ) } catch(e: Exception) { Log.e(TAG, "Exception!!!", e) } その後、 Surface と関連づけられた TextureEntry のIDを MethodChannel の戻り値としてFlutter側に返してあげるだけです。 fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when(call.method) { "startCamera" -> { result.success(textureEntry.id()) } "stopCamera" -> { stopCamera() } else -> result.notImplemented() } } ネイティブの SurfaceTexture をFlutter側で描画するには、 MethodChannel から取得したtextureIdを Texture widgetに設定するだけで、カメラプレビューがFlutterアプリ上に表示されます。 static const platform = MethodChannel('com.example.camera_preview_texture/method'); int? _textureId; Future<void> onPressed() async { try { final result = await platform.invokeMethod<int>('startCamera'); if (result != null) { setState(() { _textureId = result; }); } } on PlatformException catch (e) { print(e.message); } } Widget build(BuildContext context) { if (_textureId == null) { return const SizedBox(); } return SizedBox.fromSize( size: MediaQuery.of(context).size, child: Texture( textureId: _textureId!, ), ); } この Texture widgetを使用した実装については、 mobile_scanner の実装が非常に参考になります。 PlatformView PlatformView は、AndroidネイティブUIをFlutterのWidget tree内に組み込み、表示・制御できるようにしたものです。 PlatformView には、 Virtual Display ( VD ), Hybrid Composition ( HC ), TextureLayerHybridComposition ( TLHC )という描画モードがあります[^1]。 PlatformView のAPIを利用すると、基本的には TLHC が選択されますが、AndroidネイティブのUIツリーに SurfaceView が含まれる場合は VD または HC にフォールバックします[^2]。 なお、 Texture widgetでは対応できなかったFlutterとAndroidネイティブのフレームレートの同期が改善され、ユーザーのインタラクションを制御でき、カメラプレビューや動画以外のUIも表示できます。 実装方法 この PlatformView を使ったサンプルコードでは、カメラプレビュー画面をJetpack Composeで実装しています。 FlutterアプリでJetpack Composeを使用するには、以下の依存関係や設定を app/build.gradle に追加する必要があります。 android { ~ ~ buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.4.8" } } dependencies { implementation("androidx.activity:activity-compose:1.9.3") implementation(platform("androidx.compose:compose-bom:2024.04.01")) implementation("androidx.compose.material3:material3") } それでは、具体的な実装の説明に移ります。 PlatformView を実装するには次の3つのステップが必要です。 PlatformView を継承したNativeViewを実装する PlatformViewFactory を継承したNativeViewFactoryを実装する FlutterEngine に PlatformViewFactory を登録する まずは、1. NativeViewの実装です。大まかな実装は 公式 を参照してください。 公式との差分として、ここではJetpack Composeを使用しており、Jetpack Composeである CameraPreview を ComposeView を使用してAndroidネイティブのViewのツリーに埋め込んでいます。 class NativeView(context: Context, id: Int, creationParams: Map<String?, Any?>?, methodChannel: MethodChannel, eventChannel: EventChannel) : PlatformView { private var nativeView: ComposeView? = null override fun getView(): View { return nativeView!! } override fun dispose() {} init { nativeView = ComposeView(context).apply { setContent { CameraPreview(methodChannel, eventChannel) } } } } Jetpack Composeの実装では、 View であるCameraXの PreviewView を AndroidView を使用してComposeにしています。 余談ですが、 AndroidView は Fragment に対しても使用できます。 @Composable fun CameraPreview(methodChannel: MethodChannel, eventChannel: EventChannel) { val context = LocalContext.current val preview = Preview.Builder().build() val previewView = remember { PreviewView(context) } suspend fun startCamera(context: Context) { val cameraProvider = context.getCameraProvider() cameraProvider.unbindAll() // 記事冒頭のカメラを解析する要件を満たすには、 // cameraProviderを用意し、Previewと解析処理をここでカメラに設定することで実現できます。 cameraProvider.bindToLifecycle( LocalLifecycleOwner.current, CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build(), preview, analysis, // ここにカメラ映像の解析処理を設定 ) preview.surfaceProvider = previewView.surfaceProvider } suspend fun stopCamera(context: Context) { val cameraProvider = context.getCameraProvider() cameraProvider.unbindAll() } LaunchedEffect(Unit) { fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when(call.method) { "startCamera" -> { runBlocking { CoroutineScope(Dispatchers.Default).launch { withContext(Dispatchers.Main) { startCamera(context) } } } result.success("ok") } "stopCamera" -> { runBlocking { CoroutineScope(Dispatchers.Default).launch { withContext(Dispatchers.Main) { stopCamera(context) } } } } else -> result.notImplemented() } } methodChannel.setMethodCallHandler(::onMethodCall) } AndroidView(factory = { previewView }, modifier = Modifier.fillMaxSize()) } 次に、2. NativeViewFactoryの実装と3. FlutterEngineへの登録は以下の通りです。 class MainActivity: FlutterFragmentActivity() { ~ ~ override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) val methodChannel = MethodChannel( flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL ) val eventChannel = EventChannel( flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL ) flutterEngine .platformViewsController .registry .registerViewFactory(VIEW_TYPE, NativeViewFactory(methodChannel, eventChannel)) } } class NativeViewFactory( private val methodChannel: MethodChannel, private val eventChannel: EventChannel ) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { override fun create(context: Context, viewId: Int, args: Any?): PlatformView { val creationParams = args as Map<String?, Any?>? return NativeView( context, viewId, creationParams, methodChannel, eventChannel ) } } 最後に、Flutter側の実装です。 PlatformViewsService.initSurfaceAndroidView() は、 TLHC / HC のいずれかを使用するためのAPIです。 PlatformViewsService.initAndroidView() を使用すれば TLHC / VD のいずれかを使用でき、 PlatformViewsService.initExpensiveAndroidView() を使用すると強制的に HC となります。 class CameraPreviewView extends StatelessWidget { final String viewType = 'camera_preview_compose'; final Map<String, dynamic> creationParams = <String, dynamic>{}; CameraPreviewView({super.key}); @override Widget build(BuildContext context) { return PlatformViewLink( viewType: viewType, surfaceFactory: (context, controller) { return AndroidViewSurface( controller: controller as AndroidViewController, hitTestBehavior: PlatformViewHitTestBehavior.opaque, gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{}, ); }, onCreatePlatformView: (params) { return PlatformViewsService.initSurfaceAndroidView( id: params.id, viewType: viewType, layoutDirection: TextDirection.ltr, creationParams: creationParams, creationParamsCodec: const StandardMessageCodec(), onFocus: () { params.onFocusChanged(true); }, ) ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) ..create(); }, ); } } このように PlatformView を使用することで、FlutterアプリにAndroidネイティブのUIを組み込むことができます。 Intent Intent は、FlutterではなくAndroidの機能で、Flutterが動作するMainActivityとは別のActivityを起動することできます。 これを使用すると、アプリ内の別の画面に遷移させたり、外部のアプリを起動させることができ、Activity間でデータのやりとりもできます。 前述した2つの方法 ( Texture widgetと PlatformView )にはパフォーマンスの課題があることが報告されています[^3]。 これらの課題を解決するためには、FlutterとAndroidネイティブへの深い造詣が求められるため、場合によってはAndroidアプリを別で作成してしまった方が開発コストを抑えられるかもしれません。 ただし、その場合は別の観点での課題があります。 チームにFlutterエンジニアしかいない場合は、Android開発のキャッチアップが必要となる 外部アプリとして開発した場合は、アプリ間のインターフェースに何らかのセキュリティ対策やライフサイクルを考慮した実装をする必要がある たとえば、以下のような対応が求められます。 Activity間でやりとりするデータに対してバリデーションをする 特定のアプリ以外からは呼び出されないようにする 呼び出されたアプリは、呼び出し元アプリがkillされてしまっている場合でも正しく動作することを保証する それでは、Flutterで Intent を使用する方法を見ていきたいと思います。まずは、Flutterアプリから別のActivityを呼び出す方法についてです。 呼び出し元のActivity(Flutterアプリが動作するMainActivity) override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { if (call.method!!.contentEquals("startCamera")) { val dummyData = call.argument<String>("dummy_data") ?: return result.error( "ERROR", "data is invalid", null ) // 画面遷移の場合 val intent = Intent(this, SubActivity::class.java) // 外部アプリの場合 val packageName = "com.example.camera_preview_intent" val intent = activity.packageManager.getLaunchIntentForPackage(packageName) ?: return result.error( "ERROR", "unexpected error", null ) intent.setClassName(packageName, ".SubActivity") // 送信データの格納 intent.putExtra("EXTRA_DUMMY_DATA", dummyData) intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) activity.startActivityForResult(intent, REQUEST_CODE) } } override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) { eventSink = sink } override fun onCancel(arguments: Any?) { eventSink = null } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) { val result = data.getStringExtra("RESULT_DATA") ?: "", eventSink?.success(result) return true } return false } 続いて、Flutterアプリから呼び出されたActivityの実装です。以下のように、特定の処理が完了したら Intent を使用してデータを返却できます。 呼び出し先のActivity val intent = Intent() intent.putExtra("RESULT_DATA", resultData) activity.setResult(Activity.RESULT_OK, intent) finish() このように Intent を使用することでFlutterとAndroidネイティブ側の複雑なUI制御を考慮しないで済み、さらには、FlutterとAndroidネイティブのActivity間でデータのやりとりができます。 ただし、その場合はセキュリティやデータの整合性については考慮する必要があります。 まとめ この記事では、Flutterアプリにネイティブ機能を組み込む方法について、Androidにフォーカスして説明しました。 FlutterとAndroidネイティブ間のデータ連携は、 MethodChannel と EventChannel を用いて実現しました FlutterへAndroidネイティブのUIを組み込む方法は以下 Texture widget カメラプレビューや動画表示に適しており、比較的簡単に実装できる ユーザーインタラクション制御の実装が必要で、パフォーマンスの課題もある PlatformView ネイティブUIをFlutterのWidget treeに組み込んでくれ、ユーザーインタラクション制御も実現できる View, Fragment, Jetpack Composeが組み込み可能 パフォーマンスの課題もある Intent 画面を遷移や別アプリを起動してAndroidのUIを直接表示でき、データのやりとりもできる セキュリティやデータのやり取りに注意が必要 以上、FlutterアプリにAndroidネイティブ機能を組み込む際はそれぞれの方法にメリットとデメリットがあるため、プロジェクトの要件に応じて適切な選択をすることが求められます。 参照 [^1]: Hosting native Android views in your Flutter app with Platform Views [^2]: Android Platform Views [^3]: Performance 備考 サムネイルのドロイド君は、Googleが作成および提供している作品から複製または変更したものであり、クリエイティブ・コモンズ表示 3.0 ライセンスに記載された条件に従って使用しています。
アバター
This article is day 1 of the KINTO Technologies Advent Calendar 2024 . 🎅🎄 Hello! Rina ( @chimrindayo ) here. I am an engineer at KINTO Technologies, where I am involved in the development and operation of Mobility Market and part of the Developer Relations (DevRel) Group as well. Today, I would like to tell you about a study session called “Mobility Night” that we have formed together with t-kurimura-san at Luup, Inc. 🙌 What is Mobility Night? Source: Documents created by t-kurimura-san Mobility Night is a collaborative study session aimed at advancing the mobility industry. It provides companies and organizations in this field with a platform to share their software technologies and expertise 🚀 We believe that many technical challenges in mobility-related software are shared across the industry, including areas such as GPS, IoT, quality assurance, and product design. This study session was created with the hope that sharing unique insights from the mobility industry on such topics will drive the advancement of software technologies and contribute to overall product improvement across the entire industry. The name "Mobility Night" was chosen to reflect our vision of creating a casual and inclusive space where everyone can come together to share and exchange information freely. Holding a Closed Event For the first event, we held a closed study session called “ Mobility Night #0 .” We invited Luup, Inc., Charichari, Inc., GO Inc., newmo, Inc. (all of which gave presentations), along with many other mobility companies, to join us. Participants shared mobility-related insights, with a focus on introducing their businesses and products to one another. We initially decided to hold a closed event to gauge interest in a technical study session focused on the mobility industry. This approach allowed us to assess whether such an event would resonate with participants and to identify common themes that could make future open study sessions meaningful and valuable for the community. Results from Holding a Closed Event Thankfully, the closed event was a big hit and a huge success. The event was so lively that some attendees continued the excitement by heading straight to an after-party 🍻 It felt inspiring to see people from different companies in the mobility industry engage in passionate discussions about current trends and what the near future holds 🔥 Here are some of the impressions shared by attendees in the study session questionnaire: Gathering insights about the mobility industry was highly beneficial. Industry-specific study sessions are great, aren’t they? With everyone focused on mobility, every discussion was incredibly engaging and fascinating to listen to! The information shared was closely related to my field, making me think, "I want to connect with these people again in the future." It had a warm and welcoming vibe, almost like being at home. Mobility Night Going Forward To ensure the continuity of these study sessions, we plan to hold them every other month (during even-numbered months) to start with. Currently, as the event is still in its early stages, only companies invited by the organizing members are giving talks. However, in the future, we aim to foster an environment where anyone feels comfortable stepping up to share, regardless of whether they were specifically invited by the organizers! If you’re interested in giving a talk, feel free to submit your entry via Connpass 🙌 (If we receive a large number of entries, speakers may be selected through a lottery.) We’re also sharing the latest updates about Mobility Night on Discord. If you’d like to participate in Mobility Night, are interested in giving a talk but want to discuss it with us beforehand, want to join as an organizing member, or are curious about it in any other way, please feel free to join us on Discord! Personally, I hope it becomes a space not only for sharing information about Mobility Night but also for initiatives like finding collaborators to co-host mobility-related study sessions. https://discord.gg/nn7QW5pn8B Notification about the Next Session We are going to hold Mobility Night #1! The schedule is as follows: https://mobility-night.connpass.com/event/334400/ Date and time December 5, 2024 (Thu), starting at 6:30 p.m. Theme GPS and location information Venue KINTO Technologies Muromachi Office We welcome everyone interested in the mobility industry to join us, whether or not they are currently part of mobility-related companies or organizations! Spots are limited, so if you’re interested, be sure to apply soon! We genuinely look forward to seeing you there.
アバター
This article is part of day 12 of the KINTO Technologies Advent Calendar 2024 Introduction Hello everyone! My name is Matsuno, and I am a member of the Platform Group MSP team at KINTO Technologies (my entry from when I first joined can be found here ). In my previous job, I worked for an SES operating company and was in charge of maintenance and operation of systems built on-premise and AWS as an infrastructure engineer. Just as I thought that I wanted to be a little more involved with the system and the people involved with it, I joined KINTO Technologies by chance, and now I am. The reason why I decided to write this article What do you think of when you suddenly hear the MSP? Do you know what it stands for? Embarrassingly, I didn't know the acronym MSP until the recruiter showed me the job posting.... So, in this article, I would like to share what I learned and struggled with since joining KINTO Technologies as a member of the MSP team, as well as what the MSP team is doing! What is MSP? First, let me give you a general description of MSP. MSP stands for "Managed Service Provider" and is introduced in the Gartner Glossary as follows: (Translated by DeepL) Managed Service Providers (MSPs) provide services such as network, application, infrastructure, and security through ongoing, regular support and active management at a customer location, an MSP data center (hosted), or a third-party data center. MSPs may also offer their own services in combination with the services of other providers (for example, Security MSPs provide systems management on top of third-party cloud IaaS). Pure Play MSPs specialize in a single vendor or technology and typically provide their core services. Many MSPs include services from other types of providers. The term MSP used to be applied to infrastructure- and device-centric types of services, but now it includes ongoing routine management, maintenance, and support. Gartner Glossary: Quoted from Managed Service Provider (MSP) "Provide through ongoing, regular support and active management." This is the core idea of MSP. If you look up the term MSP, it seems that there are some differences between businesses, but most of them refer to services that specialize in the maintenance, operation, and monitoring of running systems. MSP team efforts at KINTO Technologies The origins of the MSP team From here on, let’s take a look at what the MSP team is doing within KINTO Technologies! First, the mission of the MSP team at KINTO Technologies. “We will contribute to indirect development speed and quality improvement through application operation support.” In order to understand why this mission came about, I looked back at the time when the MSP team was established. When the concept of the MSP team was first launched, KINTO Technologies faced the following challenges. Since the developer and operator of the system under development are the same, the development speed cannot be increased due to the busy operation work. We are not able to provide support at the same time as the system uptime in response to problems. In order to solve these issues, the MSP team at KINTO Technologies is composed of the following two teams: MSP Service Desk (Outsourced) MSP In-house Team The MSP Service Desk is outsourced and will be a so-called 24h365d capable unit. On the other hand, the MSP in-house team is a relatively new team that was launched in May 2023 and handles the work taken over from each development team within KINTO Technologies during the day on weekdays. Specific responsibilities of the MSP team At KINTO Technologies, various in-house products are being developed every day, and the MSP team mainly handles the following tasks. Account management Account registration and deletion Password resets Account inventory Turnover response Compile and disseminate security reports Support for reruns when data integration batch fails System monitoring alert primary response Response to various inquiries Some of you may be able to imagine from the list above, but the current MSP team mainly deals with systems management and operation that can be handled regularly, those that need to be handled by multiple teams but the response method was not unified, or those that could not be handled. What are we actually doing? Since it might be hard to picture, I’m going to introduce some details of the work that the MSP team is working on. Currently, the MSP team responds to turnover based on HR information published internally every month. The task involves gathering information on employees who are leaving their jobs due to retirement or childcare leave. This includes managing all processes from confirming the existence of accounts on target systems (both in-house and SaaS) to deleting those accounts. As of November 2024, a total of seven systems developed and managed by two groups are responding to turnover. The benefits of having the MSP team handle this task all at once include the following for example. Business standardization Eliminate account management differences between groups and systems. Reduce operating costs and focus on development Reduce operational costs for the team responsible for system development and management. Prevention of work becoming dependent on an individual employee The MSP in-house team creates procedures to ensure that all team members are available. In terms of operational cost reduction, let’s make a specific calculation for the turnover response in the following example. For seven systems, let’s say that the person in charge handles the removal of turnover accounts on a monthly basis. And assume that each task takes about two hours. In that case, the monthly and annual operating costs required overall will be: 2 (monthly workloads) x 7 (number of target systems) = 14 (hours/month) 14 (hours/month) x 12 = 168 (hours/year) This is just an estimate, but it shows that the MSP team handles approximately 150 workloads a year, taking on responsibilities that would otherwise fall to the development team. When I joined KINTO Technologies, the MSP team was already handling employee turnover. Even mid-month, accounts were deleted the next working day after an employee left, which gave me the impression that the team was responding quite efficiently. While there are these advantages, there are also disadvantages of course. You might think of the following: High communication costs Depending on the work, there is an increase in interactions with the team that took over, so it can be difficult to feel the benefits of letting go of the work. Handover risk Due to operational errors by the MSP team, recovery measures are required. Based on these advantages and disadvantages, **how to minimize the disadvantages and increase the advantages at the time of taking over work is the time to show of your skills. ** So far, I have introduced the formation of the MSP team at KINTO Technologies and its specific operations. I wrote some cool things, but I'm still inexperienced, so I'm studying every day. The future of the MSP team What was requested I would like to close this article by talking about what I have been asked to do since joining KINTO Technologies, and about the future of the MSP team! First of all, when I joined KINTO Technologies, I was asked to do the following two things. To grow as an MSP in-house team leader and lead the in-house team To contribute to the expansion of the MSP in-house production team by utilizing my practical experience of system maintenance and operation. These were very challenging for me. This is because the work style up until now was that certain sets of tasks in each time span, such as daily, weekly, and monthly, were required to be output with the same quality without making mistakes. Most of the time, both as an organization and as a system, things were stable. On the other hand, KINTO Technologies is an organization that aims to expand both as an organization and as a business, and of course, the development of new systems is progressing accordingly. As an MSP team whose mission is to improve the speed and quality of development of the development team, we would like to expand our response operations. What I was conscious of as a team leader As I mentioned earlier, I thought I understood what I was asked to do, but it was completely different from doing the work in front of me as an engineer and being a team leader. Up until now, I only had to focus on my own output. However, to grow as a team leader, I need to pay attention to the output of all my team members. Up until now, I only had to focus on my own output. However, to grow as a team leader, I need to pay attention to the output of all my team members. I have gotten used to it with the support of those around me, but I feel that I am learning every day. Expanding the capabilities of the MSP in-house team Lastly, I will contribute to the business expansion of the MSP in-house team by utilizing my practical experience in system maintenance and operation. In the past, I was at a development site that was shrinking as an organization, but the work was becoming more dependent on individual employees. I remember feeling that it was quite difficult to get rid of that dependency, partly because of the aging of the people in charge and the ongoing overload situation. I thought I could do what I could, but there was a limit to what I could do because the workloads were also limited. Based on that experience, I feel that what the MSP team is working on now is useful for KINTO Technologies. I understand what the team is trying to do and the need for it very well, but I don't have the know-how. In order to expand the efforts of the MSP team, I believe that I need to be able to do and practice the following. Think not only from a system perspective but also from a business perspective in order to create an appropriate business design and flow. When taking over work, follow the steps to ensure that the output of the MSP team is consistent. Get to know the MSP team’s efforts within KINTO Technologies. Inherently, I don’t dislike creating documents such as procedure manuals, and I don’t have any opposition to doing routine work, so I am confident that this is appropriate to a certain extent, but I feel that the part of creating work is really difficult.... Up until now, I have been working in a system development-oriented way, so I naturally focus on system specifications and the AWS services we use internally. However, to build a successful business, I need to shift my focus beyond just system specifications and AWS services. “What is our business perspective?” Every day, I face documents while asking myself this question. From the perspective of aligning the output, I understand that the basic procedure is to prepare the workflow, raise a ticket, gather the necessary information, and respond to it. However, we are struggling to organize our business flow. It is difficult to judge which part should be considered properly and which part should not be considered much. I think that if you can practice the above after establishing your own style, you can minimize the risk of business handover and maximize the advantages, but this is quite difficult.... I feel that this is something that cannot be acquired overnight, so I am working on my day-to-day work with the help of my superiors and managers. However, if I can create good work through my work, it will lead to the expansion of the MSP team, so I will do my best. Conclusion In this article, I began by discussing MSP in general and then introduced the efforts of the MSP team from my perspective as an author who joined KINTO Technologies in April 2024. I highlighted its usefulness and shared my future aspirations. Unlike other tech blogs, I don't work on anything technically advanced in my daily work. Therefore, instead of discussing technology or introducing technical problems, I focused on introducing the MSP team at KINTO Technologies from my own subjective perspective. I would be happy if you have become even a little more interested in the MSP team’s efforts.
アバター
Prerequisites and Scope In this post, we will explore how to handle Android Compose navigation from an object-oriented perspective. We will cover methods to encapsulate underlying technologies and develop features that enhance development convenience and preventing lock-in to specific technologies. Through this, we can not only write high-quality navigation code for Android Compose apps but also acquire the chain-of-thought of object-oriented development. This document assumes an MVVM architecture using Dagger / Hilt. This document only delivers data through the navigation route → ViewModel → UI path, NOT navigation route → UI. This document requires prior knowledge of the Android Navigation back stack. This document does not consider UX issues such as rapid consecutive clicks on navigation buttons. This document does not cover deep links. This document does not cover handling back gestures supported at the Android OS level. This document does not cover strict argument validation or performance optimization. The terms and names used in this document are arbitrary and may differ from established technical or academic terminology. This article is the 16th day of the KINTO Technologies Advent Calendar 2024 . 🎅🎄 Navigation Types Detail When navigating to a screen that provides detailed information, the transition typically involves moving from a general list to a specific item screen, such as from a news feed to a news detail or from a menu to a menu item. Each navigation action increases the back stack by one. You can return to the previous screen by removing( pop ) the top of the back stack. You can call the NavController.navigate function with the route string, like navController.navigate("sub3_1") . @Composable fun NavigationGraph() { val navController = rememberNavController() NavHost(navController, "splash") { composable("main3") { val viewModel: Main3ViewModel = hiltViewModel() Main3Screen( id = viewModel.id, navMain1 = { /* ... */ }, navMain2 = { /* ... */ }, navMain3 = { /* ... */ }, navSub31 = { navController.navigate("sub3_1") }, navSub32 = { navController.navigate("sub3_2") }, navSub33 = { navController.navigate("sub3_3") } ) } } } Switching In this type of navigation, the user perceives the content change within the same screen rather than navigating to a different screen. This is typically used with tabs or a bottom navigation bar. In the case of a bottom navigation bar, the back stack height does not change. The bottom of the back stack must always be one of Main#1 , Main#2 , or Main#3 . To remove itself from the back stack, a popUpTo call is required, and saving and restoring the UI state may be necessary as needed. @Composable fun NavigationGraph() { val navController = rememberNavController() NavHost(navController, "splash") { composable("main3") { val viewModel: Main3ViewModel = hiltViewModel() Main3Screen( id = viewModel.id, navMain1 = { navController.navigate("main1") { popUpTo("main1") { inclusive = true saveState = true } launchSingleTop = true restoreState = true } }, navMain2 = { navController.navigate("main2") { popUpTo("main2") { inclusive = true saveState = true } launchSingleTop = true restoreState = true } }, navMain3 = { navController.navigate("main3") { popUpTo("main3") { inclusive = true saveState = true } launchSingleTop = true restoreState = true } }, navSub31 = { /* ... */ }, navSub32 = { /* ... */ }, navSub33 = { /* ... */ } ) } } } One-Way This type of navigation involves moving to a screen from which you cannot return to the previous screen. It removes( pop ) itself from the back stack and adds( push ) the destination screen. Examples include cases where you cannot return to the form screen after submitting a form or navigating away from a splash screen. If you only need to prevent returning to itself, you can simply handle it with popBackStack , but if necessary, you may need to use popUpTo to remove multiple screens from the back stack. @Composable fun NavigationGraph() { val navController = rememberNavController() NavHost(navController, "splash") { composable("splash") { val viewModel: SplashViewModel = hiltViewModel() SplashScreen( timeout = viewModel.timeout, navMain1 = { navController.popBackStack() navController.navigate("main1") } ) } composable("transactional3") { val viewModel: Transactional3ViewModel = hiltViewModel() Transactional3Screen( onClickSave = { /* ... */ }, onClickSubmit = { viewModel.onClickSubmit { navController.navigate("transactional1") { popUpTo("sub1") { inclusive = true } } } } ) } } } Transactional(Split) This type of navigation involves splitting a very complex or long single screen into multiple steps. It is used to improve UX by reducing user stress when there is a lot of information to convey or actions to request from the user. The user can exit the flow midway, but must start from the beginning when re-entering. If the user completes the task or navigates away from the flow, the entire flow is removed from the back stack. This approach can be combined with one-way navigation to prevent users from abandoning complex UI, include forms. Within the flow, you can freely navigate back and forth, but when exiting the flow, you need to popUpTo to remove all screens of the flow from the back stack. @Composable fun NavigationGraph() { val navController = rememberNavController() NavHost(navController, "splash") { composable( route = "transactional1", arguments = listOf( navArgument("draft") { type = NavType.IntType } ) ) { val viewModel: Transactional1ViewModel = hiltViewModel() Transactional1Screen( id = viewModel.id, onClickBack = { viewModel.onClickBack { navController.popBackStack() } }, onClickSave = { viewModel.onClickSave { navController.navigate("sub2") { popUpTo("transactional1") { inclusive = true } } } }, onClickNext = { navController.navigate("transactional2") } ) } } } Navigation Management We will explain how to manage navigation in an object-oriented manner using the Sub#1 screen and related transitions, highlighted in red. We will introduce object-oriented elements step by step. The Sub#1 screen has the following conditions: The Sub#1 screen requires either the draft parameter or the combination of param1 , param2 , param3 , and param4 parameters to open. To navigate from the Sub#1 screen to the Sub#2 screen, you must wait until the long-running save function completes. To navigate from the Sub#1 screen to the Transactional#1 screen, you must wait until the long-running start function completes. The ViewModel should be independent of the navigation graph or UI. It should use the arguments received from the navigation route directly or use them to fetch data and set properties required by the UI. @HiltViewModel class Sub1ViewModel @Inject constructor( handle: SavedStateHandle, private val draftModel: DraftModel ) : ViewModel() { val id: UUID = UUID.randomUUID() val draft: Draft? = handle.get<Int?>("draft")?.let { runBlocking { if (null != draft && 0 < draft) { draftModel.get(draft)!!.also { param1 = it.param1 param2 = it.param2 param3 = it.param3 param4 = it.param4 } } } } var param1: String? = handle["param1"] private set var param2: String? = handle["param2"] private set var param3: String? = handle["param3"] private set var param4: String? = handle["param4"] private set fun onClickSave(callback: () -> Unit) { viewModelScope.launch { // Long save task. delay(2_000) callback() } } fun onClickStart(callback: () -> Unit) { viewModelScope.launch { // Long start task. delay(2_000) callback() } } } Using Only the Basic Features of the Guide Document Based on the navigation written using the Design your navigation graph / Minimal example guide, the NavigationGraph has the following roles: Registering screens. Declaring and registering the list of parameters for screen routes. Creating route strings for screen navigation. Encapsulating NavController.navigate . Executing the screen's UI and passing the encapsulated NavController.navigate logic. /** * Guide document style navigation graph. */ @Composable fun NavigationGraph() { val navController = rememberNavController() NavHost(navController, "splash") { composable( route = "sub1?${ listOf( "draft={draft}", "param1={param1}", "param2={param2}", "param3={param3}", "param4={param4}" ).joinToString("&") }", arguments = listOf( navArgument("draft") { type = NavType.IntType defaultValue = 0 }, navArgument("param1") { type = NavType.StringType nullable = true }, navArgument("param2") { type = NavType.StringType nullable = true }, navArgument("param3") { type = NavType.StringType nullable = true }, navArgument("param4") { type = NavType.StringType nullable = true } ) ) { Sub1Screen( navBack = navController::popBackStack, navSub2 = { navController.navigate("sub2") { popUpTo("sub1") { inclusive = true } } }, navTransactional1 = { draft -> if (null == draft) { navController.navigate("transactional1") } else { navController.navigate("transactional1?draft=${draft.id}") } } ) } } } /** * Bridge between navigation(encapsule navigation), `ViewModel`(state hoisting) and UI. */ @Composable fun Sub1Screen( viewModel: Sub1ViewModel = hiltViewModel(), navBack: () -> Unit = {}, navSub2: () -> Unit = {}, navTransactional1: (Draft?) -> Unit = {} ) { Sub1Content( id = viewModel.id, param1 = viewModel.param1!!, param2 = viewModel.param2!!, param3 = viewModel.param3!!, param4 = viewModel.param4!!, onClickBack = navBack, onClickSave = { viewModel.onClickSave(callback = navSub2) }, onClickStart = { viewModel.onClickStart(callback = { navTransactional1(viewModel.draft) }) } ) } /** * Display state(arguments) only. */ @Composable private fun Sub1Content( id: UUID, param1: String, param2: String, param3: String, param4: String, onClickBack: () -> Unit = {}, onClickSave: () -> Unit = {}, onClickStart: () -> Unit = {} ) { IconButton(onClick = onClickBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, "back") } // ... Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { OutlinedButton( onClick = onClickSave, modifier = Modifier.padding(16.dp) ) { Text("SAVE", style = MaterialTheme.typography.labelLarge) } Button( onClick = onClickStart, modifier = Modifier.padding(16.dp) ) { Text("START", style = MaterialTheme.typography.labelLarge) } } } Ongoing Challenges Long Navigation Graph Code : NavigationGraph code becomes lengthy as the number of screens, arguments for each screen, connected screens and back stack manipulations increase. Mismatch Between Code Order and Execution Order : The order in which the code is written becomes the order in which it is read, which not match the execution order. This discrepancy forces developers to constantly determine whether the code they are reading is relevant to the current context, increasing cognitive overhead and making maintenance difficult. Too Many Parameters in UI Functions : The number of parameters in UI functions ( Sub1Screen ) increases with the number of connected screens. For example, in a settings screen with many detailed settings, the number of parameters grows accordingly. Introducing Navigator The purpose of introducing a navigator is to reduce the number of parameters in UI functions. Navigation-related parameters in UI functions are grouped into a navigator object. Roles of the Navigator: Grouping Screens Navigable from Sub#1 into an Object : The navigator object encapsulates the navigation logic for screens that can navigate from the Sub#1 screen. Creating Navigation route : The navigator object is responsible for generating the route strings required for navigation. Manipulating the Back Stack : The navigator object handles back stack operations such as popping and pushing screens to ensure proper navigation flow. @Immutable class Sub1Navigator( private val navController: NavController ) { fun back() { navController.popBackStack() } fun sub2() { navController.navigate("sub2") { popUpTo("sub1") { inclusive = true } } } fun transactional1(draft: Drift? = null) { if (null == draft) { navController.navigate("transactional1") } else { navController.navigate("transactional1?draft=${draft.id}") } } } The NavigationGraph has become simpler by introducing the Navigator , which separates the navigation route creation and back stack manipulation code. composable( route = "sub1?${ listOf( "draft={draft}", "param1={param1}", "param2={param2}", "param3={param3}", "param4={param4}" ).joinToString("&") }", arguments = listOf( navArgument("draft") { type = NavType.IntType defaultValue = 0 }, navArgument("param1") { type = NavType.StringType nullable = true }, navArgument("param2") { type = NavType.StringType nullable = true }, navArgument("param3") { type = NavType.StringType nullable = true }, navArgument("param4") { type = NavType.StringType nullable = true } ) ) { Sub1Screen(navigator = remember(navController) { Sub1Navigator(navController) }) } The Sub1Screen has fewer parameters by grouping navigation-related parameters into a single object, making the call code clearer by encapsulating it as object members. @Composable fun Sub1Screen( navigator: Sub1Navigator, viewModel: Sub1ViewModel = hiltViewModel() ) { Sub1Content( id = viewModel.id, param1 = viewModel.param1!!, param2 = viewModel.param2!!, param3 = viewModel.param3!!, param4 = viewModel.param4!!, onClickBack = navigator::back, onClickSave = { viewModel.onClickSave(callback = navigator::sub2) }, onClickStart = { viewModel.onClickStart(callback = { navigator.transactional1(viewModel.draft) }) } ) } Ongoing Challenges Although the UI functions have been simplified, the navigation graph remains complex and developers still experience continuous context switching due to the following roles: Registering screens. Declaring and registering the list of parameters for screen routes. Navigator + companion object To simplify the NavigationGraph function and centralize navigation information, you can use a companion object within the navigator class. This approach allows you to define routes and arguments in one place. Here's how you can do it : Define the routes and arguments in the companion object of the navigator class. Use these definitions in the NavigationGraph function. @Immutable class Sub1Navigator( private val navController: NavController ) { @Suppress("MemberVisibilityCanBePrivate") companion object { const val ARG_DRAFT = "draft" const val ARG_PARAM1 = "param1" const val ARG_PARAM2 = "param2" const val ARG_PARAM3 = "param3" const val ARG_PARAM4 = "param4" const val ROUTE = "sub1?" + "$ARG_DRAFT={draft}&" + "$ARG_PARAM1={param1}&" + "$ARG_PARAM2={param2}&" + "$ARG_PARAM3={param3}&" + "$ARG_PARAM4={param4}" val ARGUMENTS = listOf( navArgument(ARG_DRAFT) { type = NavType.LongType defaultValue = 0 }, navArgument(ARG_PARAM1) { type = NavType.StringType nullable = true }, navArgument(ARG_PARAM2) { type = NavType.StringType nullable = true }, navArgument(ARG_PARAM3) { type = NavType.StringType nullable = true }, navArgument(ARG_PARAM4) { type = NavType.StringType nullable = true } ) } // ... } By using the companion object in the navigator class, the navigation graph can be simplified to focus on screen registration and UI function calls. The UI function remains unchanged. composable( route = Sub1Navigator.ROUTE, arguments = Sub1Navigator.ARGUMENTS ) { Sub1Screen(navigator = remember(navController) { Sub1Navigator(navController) }) } Ongoing Challenges Considering only the Sub#1 screen, this is sufficient. However, expanding the scope to include the navigators for Main#1 and Sub#2 , which need to navigate to the Sub#1 screen, is as follows: @Immutable class Main1Navigator( private val navController: NavController ) { // ... fun sub1(item: Main1Item) { navController.navigate("sub1?param1=${item.param1}&param2=${item.param2}&param3=${item.param3}&param4=${item.param4}") } } @Immutable class Sub2Navigator( private val navController: NavController ) { // ... fun sub1(draft: Draft) { navController.navigate("sub1?draft=${draft.id}") { popUpTo(Main2Navigator.ROUTE) { inclusive = true } } } } Sub1Navigator manages valid route formats, but the actual route composition is handled by Main1Navigator and Sub2Navigator , resulting in an inconsistent state where the responsibility for the Sub#1 route is distributed to users rather than being centralized in Sub#1 . Shared Destination Handling It is reasonable to manage valid route formats and the logic for composing valid route values together. By moving the route creation logic to each companion object , it can be standardized. Encapsulate the route itself and composition. Implement object-based navigation. @Immutable class Sub1Navigator( private val navController: NavController ) { companion object { // ... const val ARG_DRAFT = "draft" const val ARG_PARAM1 = "param1" const val ARG_PARAM2 = "param2" const val ARG_PARAM3 = "param3" const val ARG_PARAM4 = "param4" fun route(item: Main1Item) = "sub1?$ARG_PARAM1=${item.param1}&$ARG_PARAM2=${item.param2}&$ARG_PARAM3=${item.param3}&$ARG_PARAM4=${item.param4}" fun route(draft: Draft) = "sub1?draft=${draft.id}" } // ... } @Immutable class Main1Navigator( private val navController: NavController ) { // ... fun sub1(item: Main1Item) { navController.navigate(Sub1Navigator.route(item)) } } @Immutable class Sub2Navigator( private val navController: NavController ) { // ... fun sub1(draft: Draft) { navController.navigate(Sub1Navigator.route(draft)) { popUpTo(Main2Navigator.ROUTE) { inclusive = true } } } } Ongoing Challenges When registering a screen, using the same class for route , arguments , and navigator instance creation is crucial. If a mistake occurs, it can lead to logical errors such as : Inconsistent Route Definitions : If the route and arguments are not defined consistently, the navigation may fail or behave unexpectedly. Incorrect Navigator Instance : Using a different navigator instance can lead to navigation logic errors, causing the app to navigate to incorrect screens or fail to navigate. composable( route = Main1Navigator.ROUTE, arguments = Transactional1Navigator.ARGUMENTS ) { Sub1Screen(navigator = remember(navController) { Sub1Navigator(navController) }) } Abstracting Navigator, companion object and Standardizing Destination Handling You can abstract the navigator and the navigator's companion object and define the following properties. interface Navigator { val destination: Destination } interface Destination { val routePattern: String val arguments: List<NamedNavArgument> fun route(varargs arguments: Any?): String } If the navigator and companion object each implement Navigator and Destination , the navigation graph configuration is standardized as follows. @Immutable class Sub1Navigator( private val navController: NavController ): Navigator { companion object: Destination { const val ARG_DRAFT = "draft" const val ARG_PARAM1 = "param1" const val ARG_PARAM2 = "param2" const val ARG_PARAM3 = "param3" const val ARG_PARAM4 = "param4" override val routePattern = "sub1?$ARG_DRAFT={draft}&$ARG_PARAM1={param1}&$ARG_PARAM2={param2}&$ARG_PARAM3={param3}&$ARG_PARAM4={param4}" override val arguments = listOf( navArgument(ARG_DRAFT) { type = NavType.LongType defaultValue = 0 }, navArgument(ARG_PARAM1) { type = NavType.StringType nullable = true }, navArgument(ARG_PARAM2) { type = NavType.StringType nullable = true }, navArgument(ARG_PARAM3) { type = NavType.StringType nullable = true }, navArgument(ARG_PARAM4) { type = NavType.StringType nullable = true } ) override fun route(varargs arguments: Any?): String = when { 1 == arguments.size && arguments[0] is Main1Item -> route(arguments[0] as Main1Item) 1 == arguments.size && arguments[0] is Draft -> route(arguments[0] as Draft) else -> throw IllegalArgumentException("Invalid arguments : arguments=$arguments") } fun route(item: Main1Item) = "sub1?$ARG_PARAM1=${item.param1}&$ARG_PARAM2=${item.param2}&$ARG_PARAM3=${item.param3}&$ARG_PARAM4=${item.param4}" fun route(draft: Draft) = "sub1?draft=${draft.id}" } override val destination = Companion } The responsibilities of NavigationGraph are summarized as follows : Creating navigator instances. Connecting the abstracted navigation object with the UI function. Main1Navigator(navController).let { navigator -> composable(navigator.destination.routePattern, navigator.destination.arguments) { Main1Screen(navigator) } } Sub1Navigator(navController).let { navigator -> composable(navigator.destination.routePattern, navigator.destination.arguments) { Sub1Screen(navigator) } } Sub2Navigator(navController).let { navigator -> composable(navigator.destination.routePattern, navigator.destination.arguments) { Sub2Screen(navigator) } } Improving Development Productivity Global Navigation Open a web browser, making a phone call, open app settings, restarting the app and reloading the UI are sometimes necessary regardless of the screen. It is more efficient to share a single implementation of code for these universal functions rather than implementing them individually on each screen where needed. @Immutable class Sub1Navigator( private val navController: NavController ): Navigator { fun web(uri: Uri) { /* Indivisual impl */ } fun call(phoneNumber: String) { /* Indivisual impl */ } } @Immutable class Sub31Navigator( private val navController: NavController ): Navigator { fun web(uri: Uri) { /* Indivisual impl */ } fun settings() { /* Indivisual impl */ } } Solution You can declare common functions in the Navigator interface, implement the universal functions, and then delegate them to the individual screen navigators to achieve commonality. /** * Define common navigation functions. */ interface Navigator { val destination: Destination fun web(uri: Uri) fun call(phoneNumber: String) fun settings() fun reopen() fun restart() } /** * Implement common navigation functions. */ open class BaseNavigator( private val activity: Activity, val navController: NavController ): Navigator { override fun web(uri: Uri) { activity.startActivity(Intent(ACTION_VIEW, uri)) } // ... override fun reopen(){ activity.finish() activity.startActivity(Intent(activity, activity::class.java)) } override fun restart() { activity.startActivity(Intent(activity, activity::class.java)) exitProcess(0) } } /** * Delegate common navigation functions to individual screen navigators. */ @Immutable class Sub1Navigator( private val baseNavigator: BaseNavigator ): Navigator by baseNavigagor { fun sub2() { baseNavigator.navController.navigate(Sub2Navigator.route()) { popUpTo(routePattern) { inclusive = true } } } } @AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { NavigationGraph(this@MainActivity) } } } /** * Replace the `NavController` with the base navigator instance. * * @param activity The activity that owns the navigation graph. */ @Composable fun NavigationGraph(activity: Activity) { val baseNavigator = BaseNavigator(activity, rememberNavController()) NavHost(baseNavigator.navController, SplahNavigator.routePattern) { Sub1Navigator(baseNavigator).let { navigator -> // ... } } } Navigation Graph Configuration Utility There is still a risk of logical errors as follows. Sub1Navigator(baseNavigator).let { navigator -> composable(Main1Navigator.routePattern, Transactional1Navigator.arguments) { Sub1Screen(navigator) } } Solution By adding a utility function to handle navigator instances and screen registration, fun <N : Navigator> NavGraphBuilder.composable( navigator: N, content: @Composable AnimatedContentScope.(NavBackStackEntry, N) -> Unit ) { composable( route = navigator.destination.routePattern, arguments = navigator.destination.arguments ) { backStackEntry -> content(backStackEntry, navigator) } } the navigation graph configuration becomes simpler, eliminating the possibility of logical errors. composable(Main1Navigator(baseNavigator)) { _, navigator -> Main1Screen(navigator) } composable(Sub1Navigator(baseNavigator)) { backStackEntry, navigator -> Sub1Screen(navigator) } composable(Sub2Navigator(baseNavigator)) { _, navigator -> Sub2Screen(navigator) } @Preview Support For screens that display static resource in bundle, you can write navigation code by passing only the navigator instead of passing a separate event handler as an argument. Here is an example : @Composable fun StaticResourceListScreen(navigator: StaticResourceListNavigator) { Column { Button(onClick = navigator::static1) { Text("Static#1") } Button(onClick = navigator::static2) { Text("Static#2") } Button(onClick = navigator::static2) { Text("Static#2") } } } @Preview code repeats BaseNavigator(PreviewActivity(), rememberNavController()) for each navigator instance. As the types and number of previews increase, it becomes inconvenient to write previews. And this overhead force developer to skip the preview. @Composable @Preview(showSystemUi = true) fun PreviewStaticResourceListScreen() { MaterialTheme { StaticResourceListScreen(StaticResourceListNavigator(BaseNavigator(PreviewActivity(), rememberNavController()))) } } @Composable @Preview(showSystemUi = true) fun PreviewStatic1Screen() { MaterialTheme { Static1Screen(Static1Navigator(BaseNavigator(PreviewActivity(), rememberNavController()))) } } @Composable @Preview(showSystemUi = true) fun PreviewStatic2Screen() { MaterialTheme { Static2Screen(Static2Navigator(BaseNavigator(PreviewActivity(), rememberNavController()))) } } @Composable @Preview(showSystemUi = true) fun PreviewStatic3Screen() { MaterialTheme { Static3Screen(Static3Navigator(BaseNavigator(PreviewActivity(), rememberNavController()))) } } Solution Create a utility function to instantiate BaseNavigator and implement it to handle real apps and previews separately. @Composable fun baseNavigator( activity: Activity = if (LocalInspectionMode.current) { PreviewActivity() } else { LocalContext.current as Activity } ): BaseNavigator { val navHostController = rememberNavController() val base = remember(activity) { BaseNavigator(activity, navHostController) } return base } @Composable fun NavigationGraph(activity: Activity) { val baseNavigator = baseNavigator(activity) NavHost(navController, SplahNavigator.routePattern) { // ... } } @Composable @Preview(showSystemUi = true) fun PreviewStaticResourceListScreen() { MaterialTheme { StaticResourceListScreen(StaticResourceListNavigator(baseNavigator())) } } @Composable @Preview(showSystemUi = true) fun PreviewStatic1Screen() { MaterialTheme { Static1Screen(Static1Navigator(baseNavigator())) } } @Composable @Preview(showSystemUi = true) fun PreviewStatic2Screen() { MaterialTheme { Static2Screen(Static2Navigator(baseNavigator())) } } @Composable @Preview(showSystemUi = true) fun PreviewStatic3Screen() { MaterialTheme { Static3Screen(Static3Navigator(baseNavigator())) } } Custom Start Screen The NavigationGraph configuration function used so far can change the start screen but can only use a single navigation graph. This means that it is not possible to develop a demo application that utilizes only part of the existing functionality. @AndroidEntryPoint class DemoActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { NavigationGraph(this@DemoActivity) // FIXED start screen. } } } @AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { NavigationGraph(this@MainActivity) } } } @Composable fun NavigationGraph(activity: Activity) { val baseNavigator = baseNavigator(activity) NavHost(navController, SplahNavigator.routePattern) { Sub1Navigator(baseNavigator).let { navigator -> composable(navigator.destination.routePattern, navigator.destination.arguments) { Sub1Screen(navigator) } } } } Solution Implement destination in BaseNavigator , then pass the hoisted BaseNavigator instance from the activity to the navigation graph. open class BaseNavigator( private val activity: Activity, val navController: NavController, override val destination: Desination ): Navigator { // ... } /** * @param startDestination Default value available only in preview. */ @Composable fun baseNavigator( activity: Activity = if (LocalInspectionMode.current) { PreviewActivity() } else { LocalContext.current as Activity }, startDestination: Destination = if(LocalInspectionMode.current || activity is PreviewActivity) { object: Destination { override val routePattern = "preview" // ... } } else { throw IllegalArgumentException("When running the app in real mode, you must provide a startDestination.") } ): BaseNavigator { val navHostController = rememberNavController() val base = remember(activity) { BaseNavigator(activity, navHostController, startDestination) } return base } @AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { NavigationGraph(baseNavigator(destination = SplashNavigator.Companion)) } } } @Composable fun NavigationGraph(baseNavigator: BaseNavigator = baseNavigator()) { // Implement additional and common navigation features here. NavHost( // Encapsulate the `navigation-compose` dependency in activity(`MainActivity`) and UI(`UiRoot`). baseNavigator.navController, baseNavigator.destination.routePattern ) { // ... } } DEMO App Development Support The navigation graph configuration function( NavigationGraph ) can change the start screen but can only use a single navigation graph & start screen. This means that it is not possible to develop a demo application that use only part of the existing screen and functionality. @Composable fun NavigationGraph(baseNavigator: BaseNavigator = rememberBaseNavigator()) { NavHost( navController = baeNavigator.navController, startDestination = baseNavigator.destination.routePattern ) { // ... } } Solution Separate the navigation graph configuration code from the navigation-compose call. This allows for the separation of navigation graph building, the connection between navigators and UI functions, and common navigation features, while additionally encapsulating dependencies. @Composable fun NavigationGraph(baseNavigator: BaseNavigator = rememberBaseNavigator(), builder: NavGraphBuilder.() -> Unit) { NavHost( navController = baeNavigator.navController, startDestination = baseNavigator.destination.routePattern, builder = builder ) } Production App @AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { UiRoot(baseNavigator(destination = SplashNavigator.Companion)) } } } @Composable fun UiRoot(baseNavigator: BaseNavigator) { NavigationGraph(baseNavigator) { // ... composable(Sub1Navigator(baseNavigator)) { _, navigator -> Sub1Screen(navigator) } // ... } } DEMO App @AndroidEntryPoint class DemoActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { DemoUiRoot(baseNavigator(destination = Transactional1Navigator.Companion)) } } } @Composable fun DemoUiRoot(baseNavigator: BaseNavigator) { NavigationGraph(baseNavigator) { composable(Transactional1Navigator(baseNavigator)) { _, navigator -> Transactional1Screen(navigator) } } } Terminology Unification In the guide document , the term screen is used to refer to navigation targets. However, many UI design systems—such as Atomic Design , Carbon Design System , Ant Design , Shopify Polaris — use page to refer to navigation targets, and screen is used to refer to the physical device display. Additionally, when registering a screen in the navigation graph, the function name composable is used to indicate that the @Composable function is called. Changing the developer-centric terminology to consider related fields outside of development can prevent confusion or the need to confirm meanings, ensuring smooth and quick communication. Therefore, it is advisable to unify the terminology. fun <N : Navigator> NavGraphBuilder.composable( navigator: N, content: @Composable AnimatedContentScope.(NavBackStackEntry, N) -> Unit ) { composable( route = navigator.destination.routePattern, arguments = navigator.destination.arguments ) { entry -> content(entry, navigator) } } @Composable fun UiRoot(baseNavigator: BaseNavigator) { NavigationGraph(baseNavigator) { composable(Sub1Navigator(baseNavigator)) { _, navigator -> Sub1Screen(navigator) } } } @Composable fun Sub1Screen(navigator: Sub1Navigator, viewModel: Sub1ViewModel = hiltViewModel()) { // ... } Solution Assuming the adoption of Atomic Design as the design system, unify the terminology by using page instead of screen for navigation targets. fun <N : Navigator> NavGraphBuilder.page( navigator: N, content: @Composable AnimatedContentScope.(NavBackStackEntry, N) -> Unit ) { composable( route = navigator.destination.routePattern, arguments = navigator.destination.arguments ) { entry -> content(entry, navigator) } } @Composable fun UiRoot(baseNavigator: BaseNavigator) { NavigationGraph(baseNavigator) { page(Sub1Navigator(baseNavigator)) { _, navigator -> Sub1Page(navigator) } } } @Composable fun Sub1Page(navigator: Sub1Navigator, viewModel: Sub1ViewModel = hiltViewModel()) { // ... } Conclusion At first, using only the basic features of the guide document, the structure was simple. Introducing object-oriented navigation design results in multiple generic types and functions that are not affected by specific navigation spec, each responsible for specialized functionality. Navigator : Groups navigation related to the UI, defines properties on how it is registered in the navigation graph. Destination : Defines properties for registering pages in the navigation graph. BaseNavigator : Defines the integration between Application , Activity , and the navigation graph, implements common/global navigation. NavGraphBuilder.page : Connects the navigation graph and each page( Navigator ). @Composable fun NavigationGraph : Encapsulates navigation-compose , implements common navigation features, separates common features and the concrete navigation graph. @Composable fun baseNavigator : Provides development convenience. The structure of the generic object is as follows. Using generic objects to construct the navigation graph creates complex structure. Instead, if there are already generic features, the application-level features require less development and fewer considerations when using them, allowing for faster development. Therefore, as more screens are added, the average development cost per screen decreases. Depending on the field, product, development environment and individuals, this design approach may not be necessary. Even if needed, it might have a lower priority and the solutions could differ. The object-oriented design applied in this document is merely one example. However, the recognized issues and their design directions could be applicable to other software as well. The Meaning of Object-Oriented Design Object-oriented design does not necessarily result in short code or simple structure. The code appears shorter or simpler only when looking at already divided code by role. Even when following the guide document, the function that build the entire navigation graph and each screen function are written separately. In an extreme case, all screens can be implemented within the NavigationGraph or without NavigationGraph function; MainActivity can implement all screens while calling setContent . Object-oriented design starts with analyzing what the code does and what responsibilities it requires to achieve that functionality. It then determines what should be grouped together and what should be separated into types or files. Each code connected through import or object references. Finally, it leads to which developer takes the responsibility of maintain the code and overhead of study to handle that code. In the IT industry, developers are very important means of production. Therefore, cost and productivity are determined by how developers spend their time. Thus, a developer's cognitive capacity becomes expensive resource. The developer who has higher technical skills, the deeper understanding of the product and the more knowledge of the work history, the more valuable the developer's focus becomes. The cognitive overhead of determining whether the code being viewed is necessary for the current task consumes the most focus (cognitive capacity) in the least valuable way. Lower focus leads to lower productivity. Applying object-oriented design inevitably results in complex structure and requires deep technical and historical knowledge to properly understand the code. Object-Oriented Design is a development methodology that involves a large amount of development and learning in advance to create an environment where surface-level functionality can be developed quickly and simply. Risks and Costs of Object-Oriented Design Object-oriented design is a development methodology that achieves high productivity based on a large amount of generic functionality and complex structure. However, generic functionality and structure has their limit. Some changes that exceed these limit can occur at any time. It is nearly impossible for planners, managers and designers, who are not developers regularly acquiring and utilizing tacit knowledge (or domain knowledge), to understand why some changes looks so simple but so difficult and time-consuming. Even among developers, those who lack an understanding of tacit knowledge will find it equally incomprehensible. It is not a solution that requires them to acquire tacit knowledge or to request developers to repeatedly explain until they understand. Instead, this problem requires a systematical and cultural solution by the organization or company, rather than an individual one. Especially when object-oriented design is introduced for high productivity in a company or organization, managing conflicts arising from differences in tacit knowledge between developers or between developers and other positions becomes important. And this is where risks (or costs) arise. Of course, this is a problem when object-oriented design is done well. Before that, the issues are who will take responsibility for object-oriented design and how the responsible person will be decided at the organization or company level. If this decision is wrong, the most valuable resource of the organization or company, teamwork will be sacrificed. References Design your navigation graph / Minimal example android/compose-samples > Jetnews > HomeScreens Atomic Design Carbon Design System Ant Design Shopify Polaris
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の16日目の記事です🎅🎄 1. はじめに こんにちは。 モバイルアプリ開発グループ 沖田です。 師走ですね!年の瀬ですね! 思い返せばいろんな出来事がありました。 強く印象に残っていることの1つは、 Osaka Tech Lab で、Developers Summit 2024 KANSAI に参加したことです。 というわけで、今回は、 Developers Summit 2024 KANSAI 参加し、登壇したこと Osaka Tech Labの近況 について、お届けします♪ 2. 登壇前の私の気持ち 弊社は、スポンサーとして、ブース出展と登壇をする機会をいただきました。 ※ 詳しくはこちらのテックブログをご覧ください^^ Developers Summit 2024 KANSAI 振り返り 一緒に未来を描く仲間を募りたい! そのために、KINTOテクノロジーズ株式会社を知ってもらいたい! Osaka Tech Labを知ってもらいたい! そんな想いから、スポンサーに応募しました。 Osaka Tech Lab にとって、初めての試みです。 そして、わたくし、なんと、 スポンサー枠の登壇という大役を仰せつかりました。 できるかな、、登壇とかしたことないんだけど、私にできるのかな、、 と不安がいっぱい。 でもでも、せっかく頂いた貴重な機会。 やれるだけやってみよう!と思い切ってチャレンジすることにしました。 3. 登壇の当日 - 緊張と感動の瞬間 そのような中、迎えた当日。 実は、ストーリーは当日の午前中まで直し、 ギリギリの時間まで、練習を重ねる事態となっておりました w そして、緊張のあまり、早めに会場入りし、うろうろして過ごす。 そうこうしているうちに、 刻々と時計の針は進み、出番がやってきました。 登壇のテーマは " チャレンジ " です。 いまさらジタバタしても仕方がない。 少しでも多くの人に届けるために、落ち着いてゆっくり丁寧に話そう。 そう思って、ゆっくりと話し始めました。 スピーチが終盤に差し掛かったころ、 ふと、顔を上げると、 黒のTシャツを着た人たちが視界に入りました。 なんと、弊社のメンバーが、 KINTOテクノロジーズのTシャツを着て、登壇を見守ってくれていたんです。 なんかもう感動しちゃって。 他社さんがオレンジやイエロー、ブルーなど、鮮やかなカラーのTシャツで参加されている中、 弊社は目立ちづらい黒。 それがまた、Osaka Tech Lab らしいなと、 クスっと笑えてリラックスすることができました。 登壇準備がなかなか捗らない様子を、そっと見守ってくれていたOsaka Tech Lab所属のメンバー 登壇未経験の私の立ち上がりをサポートし、プロフィールや登壇内容を一緒に考えてくれた他拠点所属のメンバー 登壇の数日前に「登壇準備がうまくできません」と駆け込まれ、突然フォローに追われた、モバイルアプリ開発グループのマネージャー陣やチームリーダー そうです。 語り手が私だったというだけで、いろんな人の想いが詰まって完成した登壇だったのです。 そのおかげもあり、 当日の登壇には、174名の方がご参加くださいました。 イベントの運営スタッフのみなさま、サポートしてくれた弊社のメンバー、ならびにセッションに参加してくださったみなさまに、 この場を借りて、御礼申し上げます。 4. 登壇後の私の気持ち 登壇を終えて振り返ったことをここに綴ろうと思います。 1ヶ月以上前から準備を始めたけど、直前までバタバタだった  → 良い経験にはなりましたが、少し悩みすぎてしまったため、もっと効率よくやる方法はあったと思います。 初めての登壇ということもあり、勝手がわからなかった  → 最初に、登壇の目的やゴール、伝えたいことを明確にしてから、登壇資料作成をすれば良かったと思います。 練習では何度やっても20分にしかならなかったけど、本番はちょうど30分で話すことができた リモコンの上下を逆に持っていて、ボタン押しても次のページに行かなくて、初っ端から焦った  資料は、pptで作成して、pdf出力したものを使用した  → こだわって選んだフォントが、pdfには反映されませんでした。悲しかったですがこれも良い教訓になりました。 上記のように、準備はとても大変でしたが、 結果として、後悔のない登壇をすることができました。 同じくスポンサーとして参加されていた他の企業様や、 Ask the Speaker でお声掛け下さった方々との新たな出会いもありました。 そして、なんと、 CodeZine編集部様がセッションレポートをリリースしてくださるという ご褒美までいただきました。 トヨタグループで挑戦するPM兼モバイルアプリエンジニア、Osaka Tech Labで働く魅力とは? 本当に、感無量であります。 5. 登壇のまとめ - 学びと成長 簡単にですが、今回の登壇のついてのまとめです。 ■良かったこと・成長できたこと 登壇がきっかけで、日頃の取り組みを整理・言語化することができた 結果として、所属プロジェクトの次のステップが明確になり、私自身もレベルアップすることができた   → "インプットとアウトプット"、"言語化すること"の意義を体感することができました。 ■改善点 人生で初めての登壇が30分枠というのは少々荷が重かった   → 5分から10分枠の外部登壇を先に経験しておくのがおすすめです♪ 想いを伝えることができた一方で、一歩踏み込んだ具体的な事例は乏しかったように思う   → ご参加いただいた方に何か1つでも持って帰って頂けるような登壇を目指していきたいです。 登壇資料・ストーリーをつくる前に、登壇のポイントを明確にすること   → 先に軸をつくっておくと、ブレないストーリーを描きやすく、効率的に準備を進めることができます。 (サンプル)登壇のポイント ■ 登壇の目的 ■ ゴール (今回の登壇で、なにが出来ればOKか) ■ 伝えたいメッセージ 6. Osaka Tech Labの近況 さてさて。 その後、あっという間に時は経ち、12月になりました。 Developers Summit 2024 KANSAI 後、 Osaka Tech Lab に新たな動きが生まれています。 それは、、 外部イベントの開催に チャレンジ です! 第1弾はこちらでした。 "関西フロントエンド忘年会2024 HACK.BAR × KINTOテクノロジーズ" 今後、モバイルアプリ開発グループのイベントも企画してみたいなと ココロの中で思っています。 他社さんとコラボ開催するのも、たのしそうです♪ チャレンジしたいことリストが、あふれ返って困ってしまいますね w 一歩ずつ(いや、半歩ずつかもしれないけどw)、 歩みを進めていきたく思います。 7. おわりに いかがでしたでしょうか。 いまはまだ見ぬ未来に向かって、 Osaka Tech Lab の 軌跡を一緒に描いていきませんか!? ご応募、お待ちしています! KINTOテクノロジーズ株式会社採用TOP wantedly
アバター
この記事は KINTOテクノロジーズアドベントカレンダー2024 の16日目の記事です🎅🎄 学びの道の駅の中西です。今年は学びの道の駅プロジェクトが立ち上がり組織化されました。そんな中、社内Podcastの運営も行っており、今年のアドベントカレンダーではその内容もお届けしたいと思います。 「学びの道の駅」とは? 「学びの道の駅」は、社内で頻繁に開催される勉強会をもっと便利に、そして効果的に活用するために立ち上げられたプロジェクトです。社内の有志が中心となり勉強会の開催を支援し社内の知識共有を促進することを目的としています。 10X innovation Culture program KTC学びの道の駅ポッドキャストでは、社内の勉強会を開催している方にインタビューを行っています。その名も「突撃!となりの勉強会」。本日のポッドキャストのゲストはGoogleが提供している 10X innovation Culture programご担当の粟田さんとHOKAさんにお越しいただきました。いつもはHOKAさんがインタビューをされているのですが、今日は私、明田がインタビューをします。では早速ですが、お二人にインタビューを進めていきます。 インタビュー 粟田さん: よろしくお願いします。普段の業務は、データベースを軸にオペレーションを誰でもできるようにするプラットフォームエンジニアリングを担当しています。それとは別に、企業カルチャーに興味があるので、いろいろな活動をしています。 HOKAさん: 普段は人事グループの組織人事チームで仕事をしています。研修の企画や実行、入社後面談や全社員面談を通じて、研修のニーズや課題を把握し、企画を行っています。 明田さん: 今回の勉強会の開催のきっかけを教えてください。 粟田さん: Google Cloudのエンタープライズユーザー会「 Jagu'e'r 」の一環で、企業カルチャーとイノベーションを考える分科会に参加していました。その活動の中で10X innovation Culture programをやってみようと考え、有志で15人ほど集めて実施しました。その中にHOKAさんもいて、これがきっかけで広がっていきました。 HOKAさん: その通りです。Googleのオフィスで初めて開催したとき、参加者の反応は非常に良かったです。普段接点がない方々とも一緒にワークショップを行うことで、新たなコミュニケーションの場が生まれました。 明田さん: 次に、開催の詳細についてお伺いします。最初の開催からどのように広がっていったのでしょうか? 粟田さん: 最初はGoogleオフィスで開催し、その後社内での開催に移行しました。社内での開催でも多くの参加者が集まり、非常に前向きな反応をいただきました。 HOKAさん: KTCの社員が前向きに取り組む姿勢を見て、Googleの方々からも好評をいただきました。今後もこのプログラムを社内外に広げていければと考えています。 明田さん: 将来の展望について教えてください。 粟田さん: 将来的には、認定ファシリテーター資格を取得し、他の企業にも10Xを広めていけるようにしたいと考えています。 HOKAさん: まずは社内の他のグループにも展開し、KTC全体でイノベーションカルチャーを醸成していきたいと思っています。 明田さん: KTCをどんな組織にしていきたいですか? 粟田さん: 枠にとらわれない柔軟な発想とコミュニケーション、コラボレーションが活発な組織にしていきたいです。 HOKAさん: 失敗を恐れずに挑戦できるカルチャーを作りたいですね。そのために10Xのメソッドを活用していきたいです。 明田さん: 最後に、これを聞いている皆さんにメッセージをお願いします。 粟田さん: カルチャーは押し付けられるものではなく、自分の行動から生まれるものです。興味がある方はぜひ一緒にやりましょう。 HOKAさん: 興味がある方は見学からでもいいので、ぜひご連絡ください。 10X innovation Culture programを通じて、KTCがより働きやすい組織になることを期待しています。興味を持った方は粟田さんやHOKAさんにご連絡ください。 今回は10X innovation Culture programの詳細と、その運営の背景、今後の展望についてお届けしました。次回の勉強会も楽しみにしてください!
アバター
はじめに こんにちは! KINTOテクノロジーズの新車サブスク開発グループに所属している劉(ユ)です。 完璧ではないかもしれませんが、少しずつ問題をより良い方向に改善していくことを目指して日々努力しています。 この記事ではSpring BootにおいてRedisのPub/Subを導入してシステム日付を変更する内容について共有したいと思います。 導入に至った背景 システムのQAやテストを実施する際、システムの日付を変更しないと確認できないケースが多くあります。 特に、サブスクリプションサービスにおいては特定の日付に依存するビジネスロジックをテストする必要があります。 例えば、期間開始日や期間終了日、月額料金、中途解約の精算金、メンテナンス点検・車検などに基づいた処理の検証が求められます。 これまではシステム日付が環境設定ファイルで定義されていたため、日付を変更するたびにコンテナの再デプロイが必要でした。 その結果、テストやQAを行うたびに再デプロイに5分以上の時間がかかるという問題が発生していました。 このような状況における課題を解決した内容を紹介したいと思います。 これを入れたことでどういうメリットがあったか? RedisのPub/Subの概念を導入することで、テスト環境でのシステム日付変更がより効率的になり、迅速な対応が可能となりました。 これにより、テストやQAなどの工数を削減できるようになり、作業効率が向上しました。 具体的にコンテナの再デプロイは不要になり、変更したい設定項目(トピック)に対するメッセージ(変更したい設定値)を発信するだけで、各コンテナはリアルタイムで変更内容を受信し、設定値の変更ができるようになりました。 また、複数のコンテナが構成されている場合でも、すべてのSubscriberがメッセージを受信しているため、複数のコンテナでも再起動せずに設定値を変更できます。 さらに、システム日付変更のログも出力できるため、変更履歴を追跡することが可能となりました。 他にもSpring BootのProfileの設定で、指定したテスト環境のみでこの機能を有効にし、本番環境や他の環境へ誤って適用することを防げます。 ※Profileについては こちら Redis Pub/Subとは Redis Pub/Subは、メッセージキューのメッセージングパターンの1つです。 メッセージキューとは、サーバーレスやマイクロサービスアーキテクチャにおける非同期通信の手法の1つで、分散システムにおいてリアルタイムのイベント通知を実現します。 この仕組みは、異なるソフトウェアモジュール間で拡張かつ安定した通信をサポートするため、データベースやキャッシュとしての利用に加え、メッセージブローカーとしても広く使用されています。 主な構成 Topic(主題):購読する対象となる主題やテーマです。 Publisher(発行者):特定のTopicに関するメッセージを発信します。 Subscriber(受信者):購読したTopicに対して、発行者のメッセージを受信します。 Keyspace Notifications 何らかの方法でRedisデータセットに影響を与えるイベントを受信し、Redisキーおよび値の変更内容をリアルタイムでモニタリングします。 ではどういう実装か? システム日付変更の仕組み トピックに対するメッセージを送信するPublisherとしてAPIを実装しました。 購読しているトピック(キー)に対するイベントが発生すると複数のコンテナ(Subscriber)がメッセージを受信し、リアルタイムで設定値を変更します。 システム構成 JavaとSpring Bootを使用して構築されています。アプリケーションはコンテナ化され、クラウド環境で稼働しています。 build.gradleにlibraryの追加 implementation 'org.springframework.data:spring-data-redis' RedisConfigクラスの実装 @AllArgsConstructor @Configuration public class RedisTemplateConfig { private final RedissonClient redissonClient; @Bean public RedisTemplate<String, String> redisTemplate() { RedisTemplate<String, String> template = new RedisTemplate<>(); template.setConnectionFactory(new RedissonConnectionFactory(redissonClient)); template.setDefaultSerializer(new StringRedisSerializer()); return template; } @Bean public RedisMessageListenerContainer redisContainer() { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(new RedissonConnectionFactory(redissonClient)); return container; } } Publisherの実装 トピックに対するメッセージを送信するAPIを実装します。 @RestController public class SystemTimeController { private final SomeService service; @PostMapping("/update") public void updateSystemTime(@RequestParam String specifiedDateTime) { service.publish(specifiedDateTime); } } @Service @RequiredArgsConstructor public class SomeService { private final RedisTemplate<String, String> redisTemplate; // Topicのキーを定義 private static final String FOO_TOPIC = "foo-key"; public void publish(String specifiedDateTime) { // Topicに対するメッセージを送信する redisTemplate.opsForValue().set(FOO_TOPIC, specifiedDateTime); } } Subscriberの実装 購読しているトピック(キー)に対するイベントが発生すると、メッセージを受信します。 @Slf4j @Component @Profile("develop1, develop2") // 指定されたテスト環境のプロファイルのみ有効 public class FooKeyspaceEventMessageListener extends KeyspaceEventMessageListener { private final RedisMessageListenerContainer listenerContainer; private final RedisTemplate<String, String> redisTemplate; private static final String FOO_TOPIC = "foo-key"; @Override public void init() { doRegister(listenerContainer); } public FooKeyspaceEventMessageListener( RedisMessageListenerContainer listenerContainer, RedisTemplate<String, String> redisTemplate) { super(listenerContainer); this.listenerContainer = listenerContainer; this.redisTemplate = redisTemplate; } @Override protected void doHandleMessage(Message message) { // Redisからシステム日付を取得する String systemTime = updateSystemTimeConfig(redisTemplate.opsForValue().get(FOO_TOPIC)); // システム日付を反映するメソッドを作成してコールする updateSystemTimeConfig(systemTime); log.info("Receive a message about FOO_TOPIC: {}", message); } } さいごに 今回の記事を最後までお読みいただき、ありがとうございます。 まだまだ至らない点が多いですが、毎回少しずつ問題を改善しながら成長していこうと努力しています。 完璧な構造や実装ではありませんが、少しずつより良い方向へ進んでいくことが重要だと思っています。 今後もこうした小さな進歩が集まり、より良い結果を生み出せるよう、引き続き学び続けていきます。 共に成長する旅路を歩んでいければと思います。ありがとうございました。
アバター