every Tech Blog

株式会社エブリーのTech Blogです。

【ハンズオン】 MCP サーバー作成からリモートにホスティングしてみる

はじめに

こんにちは、@きょーです!普段はデリッシュキッチン開発部のバックエンド中心で業務をしています。

このブログでは簡単な MCP サーバーを作成し、ローカルでの動作確認。そしてリモート化させるところまでをハンズオン形式で紹介しようと思います。すでに MCP サーバーを多数作成されていたり、豊富な知見をお持ちの方には物足りない内容になっているかもしれません。

MCP サーバーとは

MCP サーバーは、AI アプリケーションと外部システムの間の橋渡しをする役割を担います。具体的には以下のような機能を提供します。

  • リソース
    • ファイルやデータベースなどの外部リソースへのアクセス
  • ツール
    • 外部 API の呼び出しや特定の操作の実行

自分がよく使っている MCP サーバーを例としてあげると GitHub があり、主に issue の読み込み、作成や PR の作成などをしてもらっています。今となっては手放せない MCP サーバーです。

他にも自分や他の人が使っている MCP サーバーとして以下のようなものもあります。

github.com (↑ 最近見つけた面白いリポジトリがあるので共有させてください。いろんな MCP サーバーが紹介されています。)

MCP の詳細な説明は公式に書かれているためここでは説明を省略とさせてください。

ハンズオン

このハンズオンではmcp/go-sdkの実装をもとに自分の名前を入力したら「Hi, {自分の名前}」と返す MCP サーバーを作成します。最初はローカル環境のみで動作できるようにサーバーを構築し、その後リモート環境(cloud run)に載せられるようにサーバーを修正していきます。完成したコードはこちらのリポジトリに残してあるので、適宜見に行っていただけますと幸いです。

必要な環境は以下の通りです。

  • golang
  • docker
  • google cloud にログインできるアカウント
  • node.js: ^22.7.5 (動作確認で使うツール用)

それでは実際にハンズオン形式でやっていこうと思います。

step 1

まずはローカルで動く MCP サーバーを作成します(通信形式は STDIO)

# まずは作業場所を作成します
mkdir mcp-sample
cd mcp-sample

# 次にgo周りの環境を整えます
go mod init
touch main.go

# dockerファイルも用意しておきます
touch Dockerfile

ベースとなるコードをmain.goに書いていきます。公式のコードをそのまま持ってきます。

package main

import (
    "context"
    "log"

    "github.com/modelcontextprotocol/go-sdk/mcp"
)

type Input struct {
    Name string `json:"name" jsonschema:"the name of the person to greet"` // ユーザーに入力してもらうパラメータ
}

type Output struct {
    Greeting string `json:"greeting" jsonschema:"the greeting to tell to the user"` // アウトプットとなるデータの説明
}

// MCPサーバーに登録するツールの中身
func SayHi(ctx context.Context, req *mcp.CallToolRequest, input Input) (*mcp.CallToolResult, Output, error) {
    return nil, Output{Greeting: "Hi " + input.Name}, nil
}

func main() {
    // MCPサーバーを作成
    server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v1.0.0"}, nil)

  // MCPサーバーにツールを登録
    mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)

  // サーバーを起動し、クライアントが接続を切るまで待機(通信方式はSTDIO)
    if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
        log.Fatal(err)
    }
}
FROM golang:1.24-alpine AS builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o mcp-greeter .

FROM gcr.io/distroless/base-debian12

WORKDIR /app
COPY --from=builder /app/mcp-greeter /app/mcp-greeter

ENTRYPOINT ["/app/mcp-greeter"]

ここでgo mod tidyを実行するとディレクトリ構成は以下のようになっているかと思います。

.
├── Dockerfile
├── go.mod
├── go.sum
└── main.go

以下のコマンドを実行してイメージを用意しておきます。

# イメージをビルド
docker build -t mcp-greeter .

mcp/inspectorというツールを使って動作確認をしてみます。

npx @modelcontextprotocol/inspector

上記のコマンドを実行し、inspector の画面で以下の情報を入力し画面下部にあるconnectをタップすると MCP サーバーのツールの動作確認などができるようになります。

  • Transport Type
    • STDIO
  • Command
    • docker
  • Arguments
    • run -i --rm mcp-greeter

これでローカル環境で MCP サーバーを作成、動作確認までは終えました。

step 2

次は MCP サーバーの通信方式を変えます。

STDIO は「クライアントが MCP サーバーをサブプロセスとして起動し、標準入出力で直接通信する」ことを前提にしています。これは同一マシン上でのプロセス間通信には適していますが、リモート環境では以下の問題があります。

  • クライアントがリモートサーバー上でサブプロセスを起動できない
  • 標準入出力による直接通信がネットワーク越しでは成立しない

そこで、MCP 仕様で定義されている Streamable HTTP の通信方式を使用することで、ネットワーク越しの通信を可能にさせます。これによって docker や golang などを必要としていた個人の環境に依存することなく、MCP クライアントさえあれば簡単に MCP サーバーを利用できるようになります。

STDIO から Streamable HTTP の通信方式に変えるにあたり MCP サーバーと MCP クライアントの通信は大きく変わります。

左: STDIO の通信方式  右: Streamable HTTP の通信方式

詳細は公式に書いてあるため省きますが、通信のやり取りから Streamable HTTP ではセッションの管理で大変そうなのがわかるかと思います。

これをコードに落とし込むためにmain.goを修正していきます。(修正した際の PR:https://github.com/keyl0ve/mcp-migration-sample/pull/1

func main() {
    // MCPサーバーを作成
    server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v1.0.0"}, nil)

    // MCPサーバーにツールを登録
    mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)

-  // サーバーを起動し、クライアントが接続を切るまで待機(通信方式はSTDIO)
-  if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
-      log.Fatal(err)
-  }
+   // HTTPハンドラーを介してMCPリクエストを処理
+   handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
+       return server
+   }, nil)
+
+   // 8080でサーバーを起動
+   if err := http.ListenAndServe(":8080", handler); err != nil {
+       log.Fatal(err)
+   }
}

実際に動くか確認してみましょう。

# イメージのリビルド
docker build -t mcp-greeter .

# 8080でリクエストを受け付ける
docker run --rm -p 8080:8080 mcp-greeter

上記でサーバーを建て直したら inspector の設定を変えます。

無事に動いているのが確認できるかと思います。

ログを出すミドルウェアを挟む(対応 PR)と MCP サーバー側でどんなリクエストが来ているか確認することもできます。

% docker run --rm -p 8080:8080 mcp-greeter
2025/11/12 07:18:56 MCP HTTP server listening on :8080
2025/11/12 07:19:02 POST / from 192.168.65.1:49815 -> 200 (2.46225ms)
time=2025-11-12T07:19:02.807Z level=INFO msg="MCP method started" method=initialize session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ has_params=true
time=2025-11-12T07:19:02.807Z level=INFO msg="MCP method completed" method=initialize session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ duration_ms=0 has_result=true
time=2025-11-12T07:19:02.821Z level=INFO msg="MCP method started" method=notifications/initialized session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ has_params=true
time=2025-11-12T07:19:02.821Z level=INFO msg="MCP method completed" method=notifications/initialized session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ duration_ms=0 has_result=false
2025/11/12 07:19:02 POST / from 192.168.65.1:49815 -> 202 (64.833µs)
2025/11/12 07:19:02 POST / from 192.168.65.1:62785 -> 200 (436.167µs)
time=2025-11-12T07:19:02.825Z level=INFO msg="MCP method started" method=logging/setLevel session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ has_params=true
time=2025-11-12T07:19:02.825Z level=INFO msg="MCP method completed" method=logging/setLevel session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ duration_ms=0 has_result=true
time=2025-11-12T07:19:07.384Z level=INFO msg="MCP method started" method=tools/list session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ has_params=true
time=2025-11-12T07:19:07.385Z level=INFO msg="MCP method completed" method=tools/list session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ duration_ms=0 has_result=true
2025/11/12 07:19:07 POST / from 192.168.65.1:62785 -> 200 (1.72925ms)
time=2025-11-12T07:19:09.673Z level=INFO msg="MCP method started" method=tools/call session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ has_params=true
time=2025-11-12T07:19:09.673Z level=INFO msg="Calling tool" name=greet args="{\"name\":\"aaa\"}"
time=2025-11-12T07:19:09.673Z level=INFO msg="MCP method completed" method=tools/call session_id=3XXMTTX77WN7EGTJXI3GRGMGFQ duration_ms=0 has_result=true
time=2025-11-12T07:19:09.673Z level=INFO msg="tool result" isError=false structuredContent="{\"greeting\":\"Hi aaa\"}"
2025/11/12 07:19:09 POST / from 192.168.65.1:62785 -> 200 (1.143916ms)

step 3

サーバーをホスティングしていきます。

色々な方法でホスティングすることはできますが、今回はドキュメントも豊富だった cloud run を使っていこうと思います。試してはいませんが、aws の ecs などでもホスティングはできるかと思います。

# ログイン
gcloud auth login

# プロジェクトの選択(自分が所属しているプロジェクトを選択してください)
gcloud config set project {sample-project}

# イメージの作成
docker build --platform linux/amd64 -t gcr.io/{sample-project}/mcp-greeter:latest .

# イメージのアップロード
docker push gcr.io/{sample-project}/mcp-greeter:latest

# cloud runにデプロイ(値は適当です)
gcloud run deploy mcp-greeter --image gcr.io/{sample-project}/mcp-greeter:latest --region asia-northeast1 --project {sample-project} --platform managed --no-allow-unauthenticated --memory 512Mi --max-instances 3

cloud run にデプロイするときに--no-allow-unauthenticatedフラグをつけることで認証を強制させることができます。MCP サーバーに認証機能をつけたいけど、まだ実装できていない時につけると良さそうです。

先ほどのデプロイ時にはフラグをつけているのでそのままだと動作確認できないはずです。デプロイ時に吐き出された URL を inspector に貼り接続を確認してみてください。

gcloud auth print-identity-tokenコマンドを実行して token を取得しましょう。取得した token を inspector の Authorization に埋め込むことでリモート MCP サーバーに接続することができます。

(mcp.json に貼る場合は以下の通り)

  {
    "mcp-greeter": {
      "url": "https://sample-url",
      "headers": {
        "Authorization": "Bearer xxx"
      }
    }
  }

これでリモート MCP サーバーの作成は終了です。適宜ソースコードの内容を変えたり、ホスティングの方式を変えたりしてユースケースにあったリモート MCP サーバーを作成していけるかと思います。

以上です!お疲れ様でした!

最後に

実際にリモート MCP 化してみて、mcp/go-sdk は通信方式が変わってもアプリケーションコードへの影響を最小限に抑え、簡単にツールを拡張できるよう設計されていることがわかりました。また簡単に MCP サーバーをリモート化できるということを学べたので業務で活用できそうなケースがあればどんどん MCP サーバーを作っていきたいなと思います。

ただ、シンプルな MCP サーバーなら簡単にデプロイできそうだなと思いつつ、まだ以下のような課題はあると思っています。

  • 認証まわり
    • 今だと gcloud に権限がある人しかアクセスできない
    • oauth などで認証を突破させたい
  • 機密情報まわり
    • API Key を使ってアクセスしたいリソースがある場合、人によって変わる複数の API Key をどうやって管理すれば良いか

こういったケースのことはまだ考えられていないので引き続き色々と試してユースケースに合わせた MCP サーバーを構成できるようにしていきたいです!

もしいい感じに課題を解決できそうな方がいましたら気軽に ↓ の repo に PR や issue で教えていただけますと幸いです! github.com

Claude Code × Maestro MCPでFlutterのE2Eテストを実施してみた

はじめに

こんにちは、リテールハブ開発部の杉森です。

近年、Playwright MCPを使ってブラウザ操作やテストを自然言語経由で実施している事例が多数見られるようになりました。その流れを見ていて、「これをFlutterアプリでも実現できないか?」と考えるようになりました。

調査を進める中で、Maestro MCPという選択肢があることを知り、実際にFlutterアプリのE2Eテストを試してみました。本記事では、その取り組みについて紹介します。

Maestroとは

Maestro(マエストロ)は、モバイルアプリケーション向けのE2Eテスト自動化フレームワークです。iOS、Android、React Native、Flutterなど、幅広いプラットフォームに対応しています。

主な特徴:

  • シンプルなYAML記法でテストシナリオを記述
  • クロスプラットフォーム対応(iOS/Android両方で同じテストを実行可能)
  • 直感的なコマンドでタップ、スワイプ、入力などの操作を記述

Maestroは、複雑なセットアップを必要とせず、短期間でE2Eテストを構築できることが大きな魅力です。

参考リンク
Maestro公式サイト
Maestro GitHub

Maestro MCPとは

Maestro MCPは、Model Context Protocol(MCP)に対応したMaestroのサーバー実装です。MCPは、AIモデルとツールを接続するための標準プロトコルで、Claude Codeなどのツールから自然言語でMaestroの機能を呼び出せるようになります。

従来は、Maestro YAMLファイルを手動で記述してテストを実行する必要がありましたが、Maestro MCPを使うことで、自然言語でテストシナリオを記述するだけで、AIエージェントが適切にテストを実行してくれます。

(備考)Maestro MCPで利用可能なTools

以下は、2025年11月10日時点でMaestro MCPで利用可能なToolsです。

カテゴリ ツール 説明
デバイス管理 list_devices 接続可能なシミュレータ/エミュレータの一覧を取得する
start_device 指定したデバイスを起動する
アプリ操作 launch_app 指定したバンドルID/パッケージ名のアプリを起動する
stop_app 実行中のアプリを停止する
UI操作 tap_on テキスト、ID、座標などで指定した画面要素をタップする
input_text フォーカスされたテキストフィールドに文字列を入力する
back デバイスの戻るボタンを押す(Androidのみ有効)
情報取得 take_screenshot 現在の画面をPNG画像として取得する
inspect_view_hierarchy 画面のUI階層を取得する
テスト実行 run_flow Maestro YAMLフローをインラインで記述して実行する
run_flow_files 既存のMaestro YAMLファイルを読み込んで実行する
check_flow_syntax Maestro YAMLフローの構文が正しいかを検証する
ドキュメント cheat_sheet Maestroの基本コマンドと構文例を含むチートシートを取得する
query_docs Maestro公式ドキュメントに質問を投げて関連情報を取得する

インストール方法

Maestro MCPを使用するには、以下の準備が必要です。

1. Maestroのインストール

Maestroをシステムにインストールします。

# Java 17以降が必要です(インストールされていない場合は別途インストールしてください)
java -version

# Homebrewを使用する場合(macOS)
brew tap mobile-dev-inc/tap
brew install mobile-dev-inc/tap/maestro

# Curlを使用する場合(Linux/macOS)
curl -Ls "https://get.maestro.mobile.dev" | bash

参考リンク
Maestro公式ドキュメント - Installing Maestro

2. Maestro MCPの設定

Claude Codeの設定ファイルにMaestro MCPサーバーの設定を追加します。

{
  "mcpServers": {
    "maestro": {
      "command": "maestro",
      "args": ["mcp"]
    }
  }
}

設定後、Claude Codeを再起動することで、Maestro MCPが利用可能になります。

参考リンク
Maestro公式ドキュメント - Installing Maestro MCP

検証

テスト対象アプリの概要

今回は、手動でテストを実施していたFlutterアプリに対して、Maestro MCPを使った自動テストをiOSシミュレータを利用して試してみました。

検証したテストの一例

既存のテスト内容

・アプリを起動(初期化状態)
・オンボーディングの実施
・ホーム画面の表示
・ボトムナビゲーションから「設定ボタン」を押下
・「お気に入り店舗ボタン」を押下
(省略)
・各画面で設定されている店舗情報が反映されているかを確認する

指示内容

MCPを利用してテストを実施するために下記プロンプトを作成しました。

[テスト内容]
・アプリを起動(初期化状態)
・オンボーディングの実施
・ホーム画面の表示
・ボトムナビゲーションから「設定ボタン」を押下
・「お気に入り店舗ボタン」を押下
(省略)
・各画面で設定されている店舗情報が反映されているかを確認する

[環境情報]
appId: xxxxxxxxxxxx

[前提条件]
アプリを初期状態(キャッシュクリア)から起動

[確認したいこと]
・各画面で設定されている店舗情報が反映されているかを確認する

[スクリーンショット]
各画面のスクリーンショットを/xxxxx/yyyy-mm-dd_hhmmss/*に保存してください。

[注意事項]
必ず、maestro-mcp-testing-guide.md を参照してからテストを行ってください。

maestro-mcp-testing-guide.mdは、テスト実施時のノウハウやエラー対処法を蓄積した学習ガイドファイルです。詳細は後述の「3. 実行時のエラーと学習プロセスの整備」で説明します。

結果

  • 手動で実施していたテスト内容を自然言語ベースの指示だけで実施・確認できた
  • 証跡として各画面のスクリーンショットを取得できた

困った点と対応内容

想定通りテストを実施することができたのですが、Maestro MCPを使う上で、何点か詰まる箇所がありました。以下、遭遇した問題と対応策を紹介します。

1. 一部のMCP Toolが利用できない

問題

Maestroに関する情報を取得する際に利用される、cheat_sheetquery_docsを実行した際に下記エラーが発生してしまいました。

Error: MAESTRO_CLOUD_API_KEY environment variable is required

対応策

Maestro Studio Desktopをインストールすると、APIキーの指定なしで利用できるようです。

参考リンク
Maestro公式ドキュメント - Maestro Studio Desktop

2. tap_onで押下できないウィジェットがある

問題

特定のボタン押下ができず、run_flowを利用した座標ベースのボタン押下処理を実施するケースが発生していました。

対応策

Maestro MCPに限らず、一般的なE2Eテストでも同様ですが、ウィジェットを識別しやすい状態にしておかないと、ボタンを押下する際に、座標情報でボタン押下等を実施する必要が出てきます。
FlutterのSemanticsウィジェットを活用し、各UI要素に適切なラベルや識別子を付与することが重要です。

例)

Semantics(
  label: 'ログインボタン',
  button: true,
  child: ElevatedButton(
    onPressed: _handleLogin,
    child: Text('ログイン'),
  ),
)

参考リンク
Maestro公式ドキュメント - Flutter Support

3. 利用可能な操作が限定的

問題

Maestro MCPで直接実行できる操作には制限があり、以下の操作で困ることがありました:

  • スクリーンショットの保存先指定take_screenshotコマンドでは、指定したフォルダに保存することができない
  • 画面録画:Maestro MCPには画面録画機能がない
  • スワイプ操作:Toolsだけだと、tap_onやinput_textなどの最低限の操作しか実施できない。

対応策

これらの制約に対して、以下のアプローチを取りました:

スクリーンショット保存の場合

xcrun simctlを使用して、スクリーンショットを指定したパスに保存できるようにしました。

例)

# iOSシミュレータのスクリーンショットを指定パスに保存
xcrun simctl io <device_id> screenshot /xxxxx/yyyy-mm-dd_hhmmss/screenshot.png
画面録画の場合

同様にxcrun simctlを使用して、画面を録画することができました。
ですが、Claude Code Sonnet 4.5でテストを実行した場合、AIエージェントの処理時間が含まれるため、録画時間がどうしても長くなってしまいます。そのため、個人的にはあまり実用的ではないと感じております。

例)

# 録画を開始(バックグラウンド実行)
xcrun simctl io <device_id> recordVideo xxxxx/yyyy-mm-dd_hhmmss/xxxxx.mov &

# 操作を実行...

# 録画を正常終了(バックグラウンド処理の停止)
スワイプ操作の場合

run_flowコマンドを使用して、Maestro YAMLを直接記述するように指示をして対応しました。

例)

- swipe:
    direction: UP
    duration: 500

4. 実行時のエラーと学習プロセスの整備

問題

実行時には、スムーズにテストが実施されないことが何点かありました:

  • run_flow内のコマンドのsyntaxエラー
  • 画面要素の識別ミス
  • 適切に動作しないmaestroコードの生成

これらの問題はAIエージェントが自律的に解決をしてくれるのですが、再度別プロセスでテストを実施した際に同じミスを繰り返すケースが見受けられました。

対応策

テスト実施時に詰まった内容と解決方法をmaestro-mcp-testing-guide.mdに適宜蓄積していくアプローチを取りました。テスト実行時に作成したmdファイルを参照してもらうことで、同じエラーを繰り返さないようにしています。

以下のような情報を蓄積しています:

  • よくあるミスと対処法
  • 各画面での要素の識別方法
  • テストパターンのサンプル
  • 参考するべきMaestro関連のリンク集

今後の展望

今回はテストケースをこちらから提供する形でしたが、今後は以下のような取り組みにも挑戦したいと考えています:

  • テスト設計からの自動化:機能仕様から自動的にテストケースを生成
  • 探索的テストの実施:AIに自由にアプリを操作させ、潜在的なバグを発見
  • 回帰テストの整備:既存機能の動作を継続的に確認する自動テストスイートの構築

これらの取り組みは、今回のようにテストケースを事前に提供する形よりも、より効果的なテスト自動化を実現できる可能性があると考えています。

まとめ

Maestro MCPを使ったFlutterアプリのE2Eテスト自動化について紹介しました。

モバイルアプリの自動テストに興味がある方は、ぜひMaestro MCPを試してみてください!

Go言語のガベージコレクションについて学んでみた

はじめに

エブリーでデリッシュキッチンの開発をしている本丸です。
1ヶ月前にGo Conference 2025があり色々と面白い発表があったのですが、その中にGo言語のガベージコレクションについての発表がありました。
ガベージコレクションについてやGo言語におけるガベージコレクションの動作について、学習したことがなかったため自分の知識を整理するという意味を込めてまとめられればと思います。
本記事の多くはGoのガベージコレクションの公式ドキュメントを参考にしているので合わせて確認いただければと思います。

tip.golang.org

ガベージコレクションの基礎

GCとは何か

ガベージコレクション(GC)とは、プログラムが動的に割り当てたメモリの中で、プログラムが将来アクセスしないと判断された領域(ガベージ)を自動的に検出し、解放して再利用するメモリ管理の仕組みです。

GCの最大の利点は、プログラマが手動でのメモリ確保・解放を行う必要がなくなることです。これにより、メモリリーク二重解放などの深刻なメモリ関連バグを根本的に防止し、システムの安定性が向上します。

Goの値が格納される場所

GCの中身の話に移る前にGoの値が格納される場所について確認したいと思います。 Goの値は主にスタックヒープの2つの場所に格納されるのですが、動的にメモリが割り当てられコンパイラから解放のタイミングを決定できないヒープ領域がGCの対象となります。

  • スタック: GC管理対象外、コンパイラが解放タイミングを決定
  • ヒープ: GC管理対象、動的メモリ割り当ての場所

GCの種類

GCにはいくつか種類があるのですが、Go言語ではマーク・スイープ方式を採用しています。

方式 動作の概要 メリット デメリット Go採用
参照カウント 各オブジェクトに参照数を記録し、0で即時解放 レイテンシが低い 循環参照を検出できない
マーク・スイープ ルートからポインタを辿り、到達可能オブジェクトをマーク 循環参照を処理可能 断片化が発生
コピーGC 生存オブジェクトのみを別領域にコピーし、元の領域を解放 断片化なし ヒープの半分しか利用不可

Go言語のGCの動作原理

GCサイクル(3つのフェーズ)

Go言語のGCは主に以下の3つのフェーズに分かれており、3つのフェーズを繰り返すことで動作しています。

  1. スイープフェーズ: 以前のサイクルで生存オブジェクトとしてマークされていないメモリを、新しい割り当てのために利用可能にする
  2. オフ: GCが非アクティブな期間
  3. マークフェーズ: 生存オブジェクトを識別しマークする

マークフェーズ

3つのフェーズの中でもマークフェーズに関しては動作の仕組みで2つポイントがあるので押さえて置けたらと思います。

三色マーキングアルゴリズム

Go言語のGCでは三色マーキングアルゴリズムを採用し、すべてのオブジェクトを3つの色に分類し、その色によってスイープフェーズでメモリを解放するかを決定します。

状態 意味 処理
未到達 まだスキャンされていない 回収対象
グレー 到達済み・未スキャン 到達したが子要素未確認 スキャン待ち
到達済み・スキャン完了 すべての子要素も確認済み 生存確定
ライトバリア

GoのGCでは、アプリケーションの実行中にGCが動作するため、本来マークされなければならないオブジェクトがマークされない可能性があります。これを解決するためにライトバリアと呼ばれる仕組みを利用して、参照先を即座にグレーに変更することで、オブジェクトの見落としを防いでいます。

GreenTea Garbage Collector

最後にGreenTea Garbage Collector(GreenTea GC)について少しだけ触れておこうかと思います。 従来のGCのマークアルゴリズムだとオブジェクトの位置を考慮せず局所性が低いことがパフォーマンス上の問題になっていました。そこで1.25からGreenTea Garbage Collectorが実験的に導入されました。 ここでは、GreenTea Garbage Collectorの詳細には触れないため、気になった方は参考文献のIssueやスライドをご確認いただければと思います。

GreenTea GCでなにが変わったか

従来のGCでは局所性に問題があったため、GreenTea GCでは局所性の課題の解消を目標にしています。そのためにマークフェーズにおけるマークの単位をオブジェクトからスパン(オブジェクトを格納するメモリブロックの単位)で行うように変更されています。厳密にはスパン単位とオブジェクト単位のマークを組み合わせたり、スパンのスパースによって処理を分岐したりとスパン単位でのマークを追加したのに合わせて、効率よく処理が行われるように処理を分岐させているようです。

まとめ

今回はGo言語でのGCを中心にガベージコレクションについて学んだことをまとめてみました。ガベージコレクションがどんなことをするものなのか概要については知っていましたが、具体的な動き方については知らなかったのでこの機会に勉強できてよかったかと思います。 また、学習の中でGo言語のGCのランタイムを追ってみたのですが、実際のコードを追ってみることで内容を理解する助けにもなったので、機会を見つけて別のランタイムのコードを追ってみようかと思いました。

参考文献

Laravel開発環境にDev Containersを入れてみた

はじめに

こんにちは、エブリーでサーバーサイドをメインに担当している清水です。
私のチームではPHP, Laravelを使用して小売店向けのSaaS側Webサービスの開発を行っています。

過去の記事でご紹介した通り、 私たちはモノレポの構成を採用しており、リポジトリの中身は大きく3つに分けることができます。

  • モバイルアプリ向け(mobile-api)
  • 管理画面向けAPI (dashboard-api)
  • 両APIで共通の部分(共通パッケージやGitHub Actionsの設定ファイルなど)

過去に本ブログで紹介されたDev Containersを私たちのチームでも導入検討を行いましたので、本記事でご紹介いたします。

開発するときに感じていた課題

開発環境は Docker を使って整備していたものの、実際の現場では次のような課題を感じていました。

  1. 新メンバーの環境構築に時間がかかる
    ローカルのIDEで使用するPHPのインストール、gitの設定など必要な手順が複数あり、ドキュメントを読んでも初回セットアップで詰まるケースがありました。
    ※アプリ自体は Docker コンテナ上で動作していたものの、ローカルの IDE(VS Code)で補完や静的解析を有効にするため、ローカルにも PHP をインストールする必要がありました。

  2. ローカルを汚したくない
    PHP等のプロジェクト固有の開発に必要なものをローカルに直接インストールしたくない、という意見もありました。
    PCを変更するときにいろいろインストールし直さなければならなくなる手間が増えることといった問題も起こりえます。

  3. VS Code の拡張機能や設定など、開発環境がメンバーによって違う
    特に「どの拡張機能を入れるべきか」が明文化されておらず、新規メンバーが最初に迷いやすい状態でした。
    「そんな便利な拡張機能あったの!?」みたいなことが起きることもあります。

Dev Containers導入で期待できること

  • 「誰が開いても同じ環境になる」再現性
    各メンバーが VS Code で “Reopen in Container” するだけで PHP やその他ツールのバージョン、拡張機能が完全に一致します。
  • 環境構築を自動化できる postCreateCommand / postAttachCommand
    .env 作成、composer install、DB マイグレーション、GitHub 認証など、手作業になりがちな初期セットアップを自動化できます。
    私たちはこれらの作業をmakeコマンドを利用することで部分的に自動化できていましたが、さらに楽できそうです。
  • チーム全体で同じ VS Code 拡張をプリセット
    Intelephense, Xdebug, Namespace Resolver など、Laravel 開発に必要な拡張を共通化し、環境差異を解消しました。

postCreateCommand / postAttachCommand とは

  • postCreateCommand: コンテナが 作成直後またはリビルド直後 に実行されるコマンドを定義する機能
  • postAttachCommand: 既存のコンテナに VS Code/Dev Containers が接続(アタッチ)されたときに実行されるコマンドを定義する機能

参考: https://containers.dev/implementors/json_reference/#lifecycle-scripts

devcontainer.jsonに以下のように設定すると起動時の操作を自動化できる

  // 初回セットアップ
  "postCreateCommand": "bash mobile-api/.devcontainer/scripts/postCreateCommand.sh",

  // 再接続ごとの軽い同期
  "postAttachCommand": "bash mobile-api/.devcontainer/scripts/postAttachCommand.sh",

導入にあたって悩んだこと・着地点

1. モノレポ全体を開発するためのコンテナを新しく作るか、既存のコンテナにDev Containersを入れるか

最初に悩んだのは、「Dev Containers専用のコンテナを新規に構築するか、それとも既存のコンテナを流用するか」でした。

リポジトリはもともと Docker Compose ベースで構築されており、すでにアプリケーションやDBなどの実行環境が整っていました。
そのため、「開発用に新しくコンテナを作り直すのは時間がかかるうえに、既に動いている環境を追加で作成するのも冗長ではないか」という懸念がありました。

まずはお試しとして、既存の mobile-api コンテナに .devcontainer を追加する形 でDev Containers を導入してみることにしました。

2. モノレポ構成ゆえに、コンテナ内から外側を編集できずに困った

既存の mobile-api.devcontainer を追加して起動してみたところ、思わぬ落とし穴がありました。
mobile-api はモノレポの一部のディレクトリであり、その外側には共通パッケージ (packages/common) や GitHub Actions の設定が配置されています。
しかし、Dev Container のマウント対象が mobile-api 直下だけだったため、コンテナ内からリポジトリ外側のファイルを編集できない状況でした。

たとえば、API 側で共通パッケージの修正をしたいときに、コンテナを立ち上げたままでは packages/common にアクセスできず、いったんホスト側で開き直す必要がありました。
モノレポで開発しているからこそ、API側と一緒に共通パッケージを編集したいケースも多いのでかなり悩みました。

最終的には、Dev Container のワークスペースマウントをリポジトリ全体(/workspaces)に変更することで解決しました。
これにより、mobile-apidashboard-apipackages のすべてをコンテナ内から一貫して操作できるようになりました。

devcontainer.jsonに以下のような内容を追加します

  "workspaceFolder": "/workspaces",

  "mounts": [
    "source=${localWorkspaceFolder}/..,target=/workspaces,type=bind,consistency=cached",
  ],

3. VS CodeのターミナルからGitHubに接続できない

コンテナ内はローカルとは異なる環境なのでGit関連の設定が必要です。
Git操作だけローカルでやればいいのではないか?とも思いましたが、VS Code上の操作ができないことはかなり不便に感じたので対応が必要でした。

devcontainer.json内に git / GitHub CLI のインストール設定を追加

"features": {
  "ghcr.io/devcontainers/features/git:1": {},
  "ghcr.io/devcontainers/features/github-cli:1": {}
}

コンテナ内でgh auth loginを叩いてGitHub認証を行う形を採用

  • postAttachCommandで以下のコマンドを自動的に実行する形にしました
gh auth login --hostname github.com --git-protocol https --web && gh auth setup-git
  • 開き直すたびに認証を求められないように、GitHub CLIの認証情報は名前付きボリュームに保存する形を採用しました
  "mounts": [
    "source=${localWorkspaceFolder}/..,target=/workspaces,type=bind,consistency=cached",
    "source=gh_config,target=/root/.config/gh,type=volume" //GitHub用のボリューム
  ],
  • どうしても原因がわからないのですが、自動実行の場合のみ以下のようなエラー表示になるので、手動でブラウザでURLを開いて認証する形で妥協しました
Running the postAttachCommand from devcontainer.json...

[devcontainer-postattach] start
[devcontainer-postattach] vendor present -> skip install
[devcontainer-postattach] gh not logged in -> launching web login

! First copy your one-time code: XXXX-XXXX
Press Enter to open https://github.com/login/device in your browser... 
/usr/bin/xdg-open: 1032: www-browser: not found
/usr/bin/xdg-open: 1032: links2: not found
/usr/bin/xdg-open: 1032: elinks: not found
/usr/bin/xdg-open: 1032: links: not found
/usr/bin/xdg-open: 1032: lynx: not found
/usr/bin/xdg-open: 1032: w3m: not found
xdg-open: no method available for opening 'https://github.com/login/device'
! Failed opening a web browser at https://github.com/login/device
  exit status 3
  Please try entering the URL in your browser manually

最終的なdevcontainer.jsonの内容

{
  "name": "mobile-api devcontainer",

  // ルートの compose.yml を使用
  "dockerComposeFile": "../compose.yml",
  "service": "mobile-api",

  // モノレポ全体を作業対象に
  "workspaceFolder": "/workspaces",

  // 一緒に起動するサービス
  "runServices": ["mobile-api", "database"],

  // VS Code 設定
  "settings": {
    "terminal.integrated.defaultProfile.linux": "bash",
    "git.openRepositoryInParentFolders": "always"
  },

  // 推奨拡張機能
  "extensions": [
    "bmewburn.vscode-intelephense-client",
    "xdebug.php-pack",
    "mehedidracula.php-namespace-resolver",
    "ms-azuretools.vscode-docker"
  ],

  // コンテナ機能(git と GitHub CLI をインストール)
  "features": {
    "ghcr.io/devcontainers/features/git:1": {},
    "ghcr.io/devcontainers/features/github-cli:1": {}
  },

  // モノレポ全体を /workspaces にマウント
  // gh CLI の認証情報は volume で永続化
  "mounts": [
    "source=${localWorkspaceFolder}/..,target=/workspaces,type=bind,consistency=cached",
    "source=gh_config,target=/root/.config/gh,type=volume"
  ],

  // 初回セットアップ
  "postCreateCommand": "bash mobile-api/.devcontainer/scripts/postCreateCommand.sh",

  // 再接続ごとの軽い同期
  "postAttachCommand": "bash mobile-api/.devcontainer/scripts/postAttachCommand.sh",

  // コンテナユーザー
  "remoteUser": "root"
}

※部分的にブログ用の内容に書き換えてあります

おわりに

Dev Containers を導入したことで、VS Code を開いて GitHub の認証を行うだけで、すぐに開発できる環境が整うようになりました。
これまで初期セットアップに時間を取られていた部分が一気に自動化され、特に新メンバーのオンボーディングが大幅に楽になったのではないかと感じています。

まだ本格的な運用段階には入っておらず、今後チーム全体で利用を進めていく中で、思わぬ問題が出てくる可能性もあります。
運用していくうちに「やはり Dev Containers 専用のコンテナを新規に構築した方がよかったかもしれない」と感じる場面が出てくるかもしれない気がしています。
新たに気付くことがあればまた本ブログで紹介したいと思います。

最後までお読みいただきましてありがとうございました。

Swift SDK for Androidを使ってiOSアプリのロジックをAndroidアプリで再利用する

はじめに

デリッシュキッチンのiOSアプリを開発している成田です。 2025年10月24日にSwift SDK for Androidのプレビュー版がリリースされました(Announcing the Swift SDK for Android)。

Swift SDK for Androidは、Swiftで書いたコードをAndroid向けにビルド・実行できるようにするためのSDKです。 今回は、そんなSwift SDK for Androidを使ったサンプルアプリの1つを実際に動かしてみながら、その応用としてiOSアプリ内の汎用的な文字列バリデーションロジックを抽出し、Androidアプリで使ってみようと思います。

サンプルプロジェクトを起動してみる

サンプルアプリを動かす前に、Swift SDK for Androidのセットアップが必要です。公式ガイドを参考に、Host Toolchain、Swift SDK、Android NDKのセットアップを行いました。詳細な手順は他の記事でも紹介されているため、ここでは省略します。

今回はサンプルアプリとして、swift-android-examplesリポジトリのhello-swift-raw-jniを動かしてみました。

Hello from Swift ❤️という文字列が画面の中央に表示される非常にシンプルなアプリになっています。

調べてみると、確かにhelloswift.swiftというSwiftファイルがあり、Swiftで書かれたコードが呼ばれているのが分かります。

import Android

@_cdecl("Java_org_example_helloswift_MainActivity_stringFromSwift")
public func MainActivity_stringFromSwift(env: UnsafeMutablePointer<JNIEnv?>, clazz: jclass) -> jstring {
    let hello = ["Hello", "from", "Swift", "❤️"].joined(separator: " ")
    return hello.withCString { ptr in
        env.pointee!.pointee.NewStringUTF(env, ptr)!
    }
}

このコードでは、以下の処理を行っています。

  1. @cdecl属性: JNIの命名規則に合わせた関数名を指定します。Java{パッケージ名}{クラス名}{メソッド名}という形式で、Kotlin側のMainActivity.stringFromSwift()メソッドに対応します。
  2. JNI環境パラメータ: envはJNIのAPIを呼び出すために必要な環境へのポインタ、clazzは呼び出し元のJavaクラスです。
  3. 文字列の生成と変換: Swiftの文字列を配列から生成し、withCStringでC文字列に変換した後、NewStringUTFを使ってJavaのjstring型に変換して返します。 このように、生のJNIを使う場合は、Swift側でJNIのAPIを直接使ってJava/Kotlin側の型と相互変換する必要があります。

JNIについて

JNI(Java Native Interface)は、Java/KotlinからC/C++を呼び出すための標準的なインターフェースです。 上記の例では、KotlinからC/C++をJNI経由で呼び出し、さらにそのC/C++からSwift関数を呼び出しています。

バリデーションロジックをAndroidで使えるようにする手順

概要

今回は、以下のような汎用的なバリデーションロジックを移行することを考えます。例えば、iOS側では以下のようにメールアドレスとパスワードの、正規表現を使ったバリデーションロジックを実装していたとします:

iOS側での使用例(Swift):

import Foundation

extension String {
    func isValidEmail() -> Bool {
        let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
        return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: self)
    }

    func isValidPassword() -> Bool {
        let passRegax = "^[a-zA-Z0-9^$*.\\[\\]{}()?\\-\"!@#%&\\/\\,><':;|_~`=+]{8,256}$"
        return NSPredicate(format: "SELF MATCHES %@", passRegax).evaluate(with: self)
    }
}

この汎用的なバリデーションロジックをAndroidでも使えるようにするため、Swift SDK for Androidを使用して移行していきます。

実装手順

実装は以下の7つのステップで進めます:

  1. Swiftライブラリの作成: Swift PackageとしてStringValidatorを実装します
  2. swift-java.configの設定: Javaラッパー生成のための設定ファイルを作成します
  3. build.gradleの設定: SwiftビルドとJavaラッパー生成の設定を追加します
  4. swiftkit-coreの公開: 必要なJavaパッケージをローカルMavenリポジトリに公開します
  5. Androidアプリの作成: validation-appを作成し、validation-libへの依存関係を追加します
  6. KotlinからSwift関数を呼び出す: 生成されたJavaラッパーを使ってKotlinから呼び出します
  7. UI実装: バリデーションをテストするためのメールアドレスとパスワードのフォームを作成します

それでは、各ステップの詳細を見ていきましょう。

1. Swiftライブラリの作成

サンプルプロジェクトには既にhello-swift-javaディレクトリ配下にhashing-lib/hashing-appという例が含まれています。これらをテンプレートとして、同じディレクトリ配下にvalidation-libディレクトリを作成し、Swift PackageとしてStringValidatorを実装してみます。

Package.swiftの設定

// swift-tools-version: 6.1
import CompilerPluginSupport
import PackageDescription

let package = Package(
  name: "StringValidator",
  platforms: [.macOS(.v15)],
  products: [
    .library(
      name: "StringValidator",
      type: .dynamic,
      targets: ["StringValidator"])
  ],
  dependencies: [
    .package(url: "https://github.com/swiftlang/swift-java", branch: "main"),
  ],
  targets: [
    .target(
      name: "StringValidator",
      dependencies: [
        .product(name: "SwiftJava", package: "swift-java"),
        .product(name: "CSwiftJavaJNI", package: "swift-java"),
      ],
      plugins: [
        .plugin(name: "JExtractSwiftPlugin", package: "swift-java")
      ]
    ),
  ]
)

StringValidator.swiftの実装

元のiOSコードではextension Stringとして実装されていましたが、swift-javaプラグインが自動的にJavaラッパーを生成するため、トップレベルのpublic funcとして実装します:

import Foundation
import SwiftJava

public func isValidEmail(_ email: String) -> Bool {
    let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
    return matches(pattern: emailRegex, string: email)
}

public func isValidPassword(_ password: String) -> Bool {
    guard password.count >= 8 && password.count <= 256 else {
        return false
    }
    let passRegex = "^[a-zA-Z0-9^$*.\\[\\]{}()?\\-\"!@#%&\\/\\,><':;|_~`=+]+$"
    return matches(pattern: passRegex, string: password)
}

private func matches(pattern: String, string: String) -> Bool {
    guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return false }
    let range = NSRange(location: 0, length: string.utf16.count)
    return regex.firstMatch(in: string, options: [], range: range) != nil
}

iOSコードとの違い

  • iOS側: extension StringisValidEmail()isValidPassword()として実装("test@example.com".isValidEmail()のように呼び出し)
  • Android側: トップレベルのpublic funcとして実装(isValidEmail("test@example.com")のように呼び出し)

swift-javaプラグインはトップレベルのpublic funcに対してJavaラッパーを生成するため、この形式で実装しています。機能的には同じバリデーションロジックを提供します。

2. swift-java.configの設定

swift-javaプラグインの設定ファイルを作成します。このファイルはSources/StringValidator/ディレクトリに配置します:

{
  "javaPackage": "com.example.stringvalidator",
  "mode": "jni"
}
  • javaPackage: 生成されるJavaラッパーのパッケージ名を指定
  • mode: jniモードを指定(JNI経由でSwift関数を呼び出す)

3. build.gradleの設定

hashing-libbuild.gradleを参考に、SwiftビルドとJavaラッパー生成の設定を追加します。

主な設定内容

  1. Swift SDKのパス設定: getSwiftlyPath()getSwiftSDKPath()関数でSwift SDKとSwiftlyのパスを自動検出します。
  2. 全ABI向けのビルドタスク: arm64-v8a、armeabi-v7a、x86_64の3つのABI向けにSwiftコードをビルドします。各ABIごとにbuildSwift${abi}タスクが作成されます。
  3. Swiftランタイムライブラリのコピー: Swiftランタイムライブラリ(swiftCoreFoundationなど)を自動的にコピーします。
  4. 生成されたJavaファイルのソースディレクトリへの追加: swift-javaプラグインが生成したJavaラッパーファイルを、Androidライブラリのソースセットに追加します。

build.gradleの主要な設定は以下の通りです:

plugins {
    alias(libs.plugins.android.library)
}

android {
    namespace "com.example.validationlib"
    compileSdkVersion 34
    
    defaultConfig {
        minSdkVersion 28
    }
}

dependencies {
    implementation('org.swift.swiftkit:swiftkit-core:1.0-SNAPSHOT')
}

// Swift SDKのパスを取得する関数
def getSwiftlyPath() {
    // 環境変数またはgradle.propertiesから取得
    // または一般的なパスを検索
}

def getSwiftSDKPath() {
    // Swift SDKのパスを取得
}

// ABI定義
def abis = [
    "arm64-v8a"     : [triple: "aarch64-unknown-linux-android28", ...],
    "armeabi-v7a"   : [triple: "armv7-unknown-linux-android28", ...],
    "x86_64"        : [triple: "x86_64-unknown-linux-android28", ...]
]

// 全ABI向けにSwiftビルドタスクを作成
def buildSwiftAll = tasks.register("buildSwiftAll") {
    inputs.file(new File(projectDir, "Package.swift"))
    inputs.dir(new File(projectDir, "Sources/StringValidator"))
    // ...
}

abis.each { abi, info ->
    def task = tasks.register("buildSwift${abi.capitalize()}", Exec) {
        workingDir = layout.projectDirectory
        executable(getSwiftlyPath())
        args("run", "swift", "build", "+${swiftVersion}", "--swift-sdk", info.triple)
    }
    buildSwiftAll.configure { dependsOn(task) }
}

// 生成されたJavaファイルとJNIライブラリをソースセットに追加
android {
    sourceSets {
        main {
            java { srcDir(buildSwiftAll) }
            jniLibs { srcDir(generatedJniLibsDir) }
        }
    }
}

preBuild.dependsOn(copyJniLibs)

4. swiftkit-coreの公開

swift-javaプロジェクトは、SwiftからJava/Kotlinへのラッパー生成に必要なJavaパッケージ(swiftkit-coreなど)をまだ公式のMavenリポジトリに公開していません。そのため、Androidプロジェクトで利用するには、ローカルMavenリポジトリに公開して参照可能にする必要があります。

SwiftやiOS開発での例に置き換えると、CocoaPodsやSwift Package ManagerでまだGitHubや公式リポジトリに公開されていないライブラリをローカルパスから利用するのと同じイメージです。Mavenリポジトリは、Java/Kotlinのライブラリを配布する仕組みで、SPMやCocoaPodsリポジトリのようなものです。

以下のコマンドをターミナルで実行します:

$ cd hello-swift-java/validation-lib
$ swift package resolve
$ ./.build/checkouts/swift-java/gradlew --project-dir .build/checkouts/swift-java :SwiftKitCore:publishToMavenLocal

5. Androidアプリの作成

validation-libと同じディレクトリ配下(hello-swift-javaディレクトリ配下)にvalidation-appディレクトリを作成し、validation-libへの依存関係を追加します。

Androidアプリのビルドと依存関係の管理には、Gradleというビルドツールを使用します。iOS開発でSwift Package Manager (SPM)のPackage.swiftやCocoaPodsのPodfileで依存関係を定義するのと同様に、Androidではbuild.gradle.ktsファイルで依存関係を定義します。また、Xcodeプロジェクトの.xcodeprojで設定を管理するのと同様に、Gradleではbuild.gradle.ktsでアプリの設定と依存関係を一括で管理します。

build.gradle.ktsの設定

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
}

android {
    namespace = "com.example.validationapp"
    compileSdk = 36
    
    defaultConfig {
        applicationId = "com.example.validationapp"
        minSdk = 28
        targetSdk = 36
    }
    
    buildFeatures {
        compose = true
    }
}

dependencies {
    implementation(project(":hello-swift-java-validation-lib"))
    // Jetpack Compose関連の依存関係
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.activity.compose)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.material3)
    // ...
}

6. KotlinからSwift関数を呼び出す

生成されたJavaラッパーを使って、KotlinからSwift関数を呼び出します。swift-javaプラグインが自動的に生成したStringValidatorクラスを使用します:

import com.example.stringvalidator.StringValidator

val isValid = if (email.isNotEmpty()) {
    StringValidator.isValidEmail(email)
} else null

このコードでは、swift-javaプラグインが自動生成したStringValidatorクラスの静的メソッドisValidEmail()を呼び出しています。 生成されたJavaラッパーは、Swiftのトップレベル関数をJava/Kotlinの静的メソッドとして提供するため、通常のクラスメソッドを呼び出すのと同じ感覚で、Swiftで書いた関数を利用できます。

7. UI実装

Jetpack Composeでバリデーションをテストするための入力フォームを実装します。 この辺はCursorによしなに作ってもらいました。偉大です。

MainActivity.ktの実装例(主要部分のみ)

import com.example.stringvalidator.StringValidator
import androidx.compose.runtime.*
import androidx.compose.material3.*

@Composable
fun ValidationScreen() {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }

    Column {
        // Emailバリデーション
        val isValidEmail = if (email.isNotEmpty()) {
            StringValidator.isValidEmail(email)
        } else null
        
        TextField(
            value = email,
            onValueChange = { email = it },
            isError = isValidEmail == false
        )
        
        // Passwordバリデーション
        val isValidPassword = if (password.isNotEmpty()) {
            StringValidator.isValidPassword(password)
        } else null
        
        TextField(
            value = password,
            onValueChange = { password = it },
            isError = isValidPassword == false
        )
    }
}

このコードでは、StringValidator.isValidEmail()StringValidator.isValidPassword()を直接呼び出すことで、Swiftで書いたバリデーションロジックを使用しています。remember { mutableStateOf(...) }で状態を管理し、TextFieldonValueChangeで値が変更されるたびにバリデーション関数が自動的に再実行されます。その結果がisErrorプロパティに反映されるため、ユーザーが入力している間、リアルタイムでバリデーション結果が表示されます。

動作確認・デモ

エミュレータでの動作確認

Android Studioでvalidation-appを実行し、エミュレータで動作確認をします。

アプリを起動すると、メールアドレスとパスワードの入力フォームが表示されます。各フィールドに入力すると、リアルタイムでバリデーションが実行され、以下のように動作します。

  • Emailフィールド: 有効なメールアドレス形式で入力すると、エラー表示が消えます。無効な形式(例:test@testなど)では、エラー状態が表示されます
  • Passwordフィールド: 8文字以上256文字以下の要件を満たす有効な文字列を入力すると、エラー表示が消えます。要件を満たさない場合は、エラー状態が表示されます

まとめ

今回は、Swift SDK for Androidを使ってiOSアプリで使っていた汎用的な文字列バリデーションロジックをAndroidアプリで再利用する手順を紹介しました。ポイントは以下の通りです。

  • Swift SDK for Androidを使うことで、既存のiOSのSwiftコードをAndroidでも活かせる
  • swift-javaプラグインを使えば、Swiftのトップレベル関数をJava/Kotlinから簡単に呼び出せる

まだプレビュー版で制約もありますが、今後はより多くのiOSアプリでのコードをAndroidで再利用できるようになると思われます。

おまけ

今日はハロウィンらしいので、おまけとしてSwiftUIだけで作った可愛いアニメーションを載せておきます(笑)。