CodexのSubagentsでMulti-Agentを実現する

はじめに

元々CodexにはGA版のリリース時点からSubagentsという形で1つのタスクを並列で実行する機能があったんですが、3月に入ってから色々と更新が入り、Subagentの複数起動・識別・待ち合わせができるようになりました。

が、実際にMulti-Agentとしてオーケストレーションしようとすると意外とハマりどころも多く、みんなClaude Codeを使ってるからなのか、シンプルにあんまり需要がないからなのかその辺のノウハウもあんまり転がってないので、メモがてらに書いておきます。

とはいえCodexの更新ペース自体が滅茶苦茶早いので、明日には意味のない記事になっているかもしれません。…というか、呼び出し側で制御しなきゃいけないことが多すぎるので、意味のない記事になってくれることを願っています。

Subagentの使いどころ

そもそもSubagentをどこで使うと良いか、というのはここに要約がまとまっています。

Even with large context windows, models have limits. If you flood the main conversation (where you’re defining requirements, constraints, and decisions) with noisy intermediate output such as exploration notes, test logs, stack traces, and command output, the session can become less reliable over time.
This is often described as:
- Context pollution: useful information gets buried under noisy intermediate output.
- Context rot: performance degrades as the conversation fills up with less relevant details.


Subagent workflows help by moving noisy work off the main thread:
- Keep the main agent focused on requirements, decisions, and final outputs.
- Run specialized subagents in parallel for exploration, tests, or log analysis.
- Return summaries from subagents instead of raw intermediate output.

大雑把にまとめると、以下のようなことを言っています。

  • Mainagentのコンテクストの肥大化を防ぐために使った方がいい。
    • ソース・ドキュメントの調査やテスト結果・ログ解析など、作業中の調べものをコンテクストから除外するために使うべき。
  • Mainagentは要件と意思決定、そして最終成果物に注力する。
  • Subagentは調査やテスト、ログの解析を行い、生の中間成果物ではなくサマリをMainagentに渡すようにする。

ここはちょっと大事なポイントなんですが、修正まで含めたすべてのタスクをSubagentにやらせる(つまり、Mainagentがオーケストレーションにだけ注力する)のはあんまり効率が良くないです。

タスク実行中に生じる不要な生ログや調査メモによるコンテクストの肥大化を防ぐことを主目的に一部の作業をSubagentに委譲する、というスタンスの方が望ましいです。

カスタムエージェントの定義

CodexにはビルトインのSubagent roleがあります。以下の3つです。

  • default
  • worker
  • explorer

これとは別に、.codex/agents/${role}.toml*1を作ることで、自分で使用モデル・推論の労力・役割を設定したカスタムエージェントを定義することも可能です。

具体的に何が設定できるかは同じページ内に記載されてますし、「こういうエージェントを作りたい」とCodexに頼めばいいだけなので、詳細は割愛します。

Subagentを使う上での注意点

特に以下の点がハマりやすい・誤解しやいすいところです。

  • Subagentは「Subagentを使用する」ことをプロンプトで明示しない限り生成(spawn)されない。
  • すべてのSubagentはAGENTS.mdを共有し、これを上書きすることはできない。
    • 「特定のrole向けのAGENTS.md」を定義することはできない。
    • AGENTS.mdに細かいフローが記載されてしまっていると、roleとして期待している役割を超えて勝手に作業し始めることがある。
  • 基本的にMainagentはSubagentの作業完了を待たない。
    • 明確に作業完了を待ち受けるよう指示しないと並行で作業を行う。
    • Subagentに任せた作業をMainagentがそのまま実行することもあるし、酷い場合はSubagentの作業・報告が終わる前にMainagentが作業を打ち切ることもある。トークン返してくれ。
  • Subagentのプロセス起動時、Mainagentの思考に引きずられて何も作業をしないことがある。
    • Mainagentがプロセス起動と並行でSubagentに任せるはずだった作業を始め、Subagentは何もせずechoで変な文字列を垂れ流したり、意味もなくtrueコマンドを実行するだけの存在になることがある。普通にバグでは?

注意点に対する解決法

Subagentに関する指示のSkill化

上記の注意点に関しては、Skillを量産することで一通り対応できます。

具体的には、以下内容をSkill化しておくと大分制御しやすくなります。

  • MainagentがSubagentを呼び出すとき / 作業を待ち受けるときのルール
  • Subagentが守るべき共通ルール
  • Mainagent / Subagentに期待する役割・振る舞いまで記載した作業フロー
  • 特定のSubagentに実際に任せたい作業詳細・ルール

Skill内で使いたいSubagentを指定することでもSubagentをspawnさせることが可能です。自分が良く出す指示の中で、Subagentを使いたいものがあったらSkill化しておきましょう。また、Mainagentがどのタイミングで何のSubagentの作業を待ち受けるか、もSkill内に定義しておくと楽になります。

また、カスタムエージェントのtoml内に[[skills.config]]を定義することで、事前にどのSkillを使うべきか指示出しができます。共通ルールと作業詳細のSkillを渡しておくことで暴走を食い止めやすくなりますし、toml内のdeveloper_instructionsに記載する内容も「求める役割」だけに注力できるので、汎用性を持たせることが可能になります。

Mainagent / Subagentが守るべき共通ルールの詳細化

具体的にMainagent / Subagentにどういうルールを設けるか…については、実際に作ったSkillを見てもらった方がわかりやすいかもしれません。subagent-orchestrationの方にはSubagentのrole名まで入っていますが、まぁ何となくわかるでしょう。

Mainagentのオーケストレーションルール(subagent-orchestration

---
name: subagent-orchestration
description: Main Agent 向け sub agent 共通ルール。ownership、fork_context、done/blocked、wait/recovery、stale result の invariant を短く保つために使う。
---

# Subagent Orchestration

## When To Use

- Main Agent が sub agent を spawn するとき
- ownership と wait/recovery の invariant だけを短く思い出したいとき
- scenario skill へ流れる前に、共通の事故防止策だけ確認したいとき

この skill は Main Agent 向けである。  
sub agent 共通の作法は `subagent-execution` を前提とする。

## Ownership

- Scope の owner を決める権限は Main Agent のみにある。
- product code 実装、test 修正、docs 更新、最終統合は Main Agent が持つ。
- test 実行そのものは `test_runner` へ委譲してよいが、何を実行するかと結果をどう扱うかの責任は Main Agent が持つ。
- sub agent は調査結果または review 結果を返すだけで、implementation continuation をしない。
- 同じ file / dir / symbol を複数 sub agent に同時委譲しない。
- `explorer` は事実整理専用であり、次担当や owner を決めない。

## Context Handoff

- `fork_context` は原則 `false` とする。
- sub agent へ渡す authority は、委譲文に再記述した Scope、Goal、Do not、Deliver、Done when だけに絞る。
- 親 agent の途中思考、別案比較は、委譲文へ明記していない限り authority ではない。
- review agent へは diff、対象 file、test 結果、必要なら screenshot refs だけを渡す。

## Return Status

- sub agent の正式な応答は `done` または `blocked` のどちらかとする。
- `done` は delegated scope の結果要約と根拠を返す。
- `blocked` は不足情報、停止理由、Main Agent に求める対応だけを返す。
- `started` のような途中報告は補助情報であり、統合判断の材料にしない。

## Wait And Recovery

- Main Agent は agent ID と delegated scope を対で管理する。
- 次の一手が sub agent の結果に依存するなら、推測で先回りせず待つ。
- `wait_agent` 未完了だけを理由に、同じ scope の二重委譲や再催促をしない。
- 追加連絡は `blocked`、明示的失敗、または close 後の再委譲が必要な場合に限る。
- stale result を避けるため、入力 docs や delegated scope が Main Agent 側で更新されたら、古い `triage`、`plan_sync`、`explorer` 結果は破棄または再実行する。

Subagentの共通ルール(subagent-execution

---
name: subagent-execution
description: sub agent 共通実行規約。委譲文だけを authority として扱い、Assigned Scope を広げず、`done` または `blocked` だけを正式応答として返すために使う。
---

# Subagent Execution

## When To Use

- sub agent が Main Agent から委譲を受けて動くとき
- `.codex/agents/*.toml` を更新し、sub agent 共通の振る舞いを揃えたいとき
- fork された文脈と委譲文のどちらを優先するか迷いやすいとき

この skill は sub agent 向けである。  
Main Agent 側の振り分け、wait、stale result の扱いは `subagent-orchestration` で扱う。

## Common Rules

- authority として扱うのは、委譲文に再記述された Scope、Goal、Do not、Deliver、Done when だけである。
- `fork_context: true` で親の履歴が見えていても、委譲文を優先する。
- 与えられた Assigned Scope の内部だけで前進し、自分で scope を広げない。
- 自分が task 全体の owner かどうか、次に誰へ渡すか、追加の sub agent が必要かは決めない。
- 委譲を受けたら ownership 確認のために待機せず、継続できるなら Assigned Scope の作業を進める。
- 続行に必要な情報、権限、前提、追加 scope が不足している場合だけ `blocked` を返す。
- 正式な応答は `done` または `blocked` のどちらかに限る。
- `started` のような途中進捗は補助情報であり、正式な状態として扱わない。
- review task や調査 task では、自分で実装、テスト実行、docs 更新へ進まない。
- 報告形式を別途指定されていない場合でも、正式な状態は `done` または `blocked` で明示する。

## Default Return Format

### When done

- `Status: done`
- `Scope handled: <担当した file / dir / symbol>`
- `Result: <担当範囲で何を確認したか>`
- `Verification: <担当範囲で実施した確認。なければ none>`
- `Out-of-scope findings: <範囲外で見つけた課題。なければ none>`

### When blocked

- `Status: blocked`
- `Scope handled: <担当した file / dir / symbol>`
- `Blocking reason: <なぜ続行できないか>`
- `What is missing: <不足している情報 / 権限 / 前提 / 追加 scope>`
- `Requested action from Main Agent: <何を補ってほしいか>`
- `Out-of-scope findings: <範囲外で見つけた課題。なければ none>`

## What Stays In Agent TOML

- その agent 固有の責務境界
- その agent が優先して見る観点
- その agent 固有の deliver や done の期待

特に重要なポイントは以下の点です。…重要なポイントが多すぎでは?

  • MainagentにSubagentのIDを管理させる。
  • MainagentのみがSubagentの担当範囲(scope)を決められるようにする。
  • Subagentが勝手にSubagentを作れないようにする。

⇒自分がMainagentなのかSubagentなのかを自分では判断できないので、Skillである程度縛っておいた方が安全です。

  • MainagentがSubagentをspawnする時はfork_contextfalseにさせる。

⇒これはCodex内部に持つオーケストレーションの仕組みに対する指示ですが、これをtrueにするとMainagentとSubagentが滅茶苦茶に混線するのでfalse固定にさせた方がいいです。

  • Mainagentの待ち受け・催促条件を固定化させる。
  • Subagentからの返答形式を固定化させる。

⇒Subagent側で時間のかかるタスクを持っているとき、Mainagentが「Subagentの返答が返ってこない!何か詰まっているのかも!自分でやらなきゃ!」と勝手にプロセスを殺して自分でやり始めます。どうもMainagent(というか、Agent間のやり取り?)では「思考中」に発しているメッセージを拾うことができず、ただ無応答として判定してしまうようです。バグでは?

おわりに

こんな感じでガチガチにルールで縛ったことにより、大分まともに動くようになりました。

タスクの完了が早くなった…とは言い難いですが、前述した通り、そもそもコンテクストの肥大化を防ぐための仕組みであり、実際compactionが起こる回数は大分減ったように感じます。

あと、レビューに関しては下手にコンテクストを持たせない方がしっかりやってくれるようです。「Claude Codeで修正したコードをCodexでレビューさせる」人が多いのはこういうのを期待していたんだなぁと思いました。*2

*1:~/.codex内はもちろん、プロジェクトの.codex内に作ってもよい。Skillとかと同じ扱い。

*2:ついでに「Codexはめっちゃネチネチ言う」という人の気持ちもわかったので、語尾にハートマークを付けさせるよう指示して緩和している。

オレオレAI Agent Coding(不満駆動開発)

はじめに

2か月ほど前にChatGPTのPlusプランに加入した。理由は色々あるけど割愛する。

で、Codexを使えるようになったので、折角だから何か作ってみようと思い、「Spotifyでライブラリに登録している音源と、ローカルで持っている音源を一括で管理できるアプリ」を作っている。*1

作ってるアプリ(デスクトップ版)

アーキテクチャとしてはTauri + Vite + Reactを採用したが、RustもReactも全然知らない。*2それでもプロンプトさえちゃんとしていればほとんどバグも出ずに動くので、感動するよりも先に「じゃあもう俺コード書かなくていいじゃん…」となった。

閑話休題。

基本的な機能は2日で実装が終わり、もう2日でAndroid版のビルドもできるようにした。世の中に公開するかどうかはあんまり考えてない。*3

後はちょこちょこと気に入らない部分を直してもらったり、パッと思いついたアホみたいな機能を雑に実装してもらうだけとなっている。

今回は「人間は気にいらねぇと思った部分とジャストアイデアを書き殴るだけ」で開発を進める方法をメモしておく。

初めにとっていた開発手法

最初はこんな感じのドキュメントを作っていた。

plan
│- PLAN.md
│- TODO.md
└─layout
    │- layout.md
    │- main-collapsed.excalidraw
    │- main-expanded.excalidraw
    └  main-search.excalidraw
  • PLAN.md:アプリ全体の設計方針
  • TODO.md:CodexにPLAN.mdを読み込ませて作ってもらった実装計画。
  • layout.md:各画面の説明。
    • モックとしてExcalidrawで作った*.excalidrawファイル(実体はJSON)を用意しておき、細かいレイアウトはそっちを参照させるように指示している。

最初期のモック。こんなにクソ雑なモックでも、自然言語で説明するより圧倒的にマシ。

こんな雑な仕組みでもそれなりに回るのだけれど、以下のような問題が出てきた。

  • エージェントに指示するたびにいちいちPLAN.md / TODO.mdの全量を読みに行くのでトークン効率が滅茶苦茶悪い。
  • やりたいこと(要件)は決まってるけど、仕様・設計に落とし込むのが面倒臭い。「そんなもんお前がやれ」と思うようになってきた。
  • TODO.mdの内容から何をするのか何となくわかるが、「それって具体的にどうやって実装するつもりなの?」となるものが度々出てくる。
    • そういう悪臭がするものは、人間でもAIでも「そうはならんやろ」となる実装が出てくるんだなぁ…と学んだ。
      • 実際は自分がPLAN.mdに書いた仕様が曖昧過ぎるのが原因。
      • 「でも仕様詰めるのって面倒臭いよね…。」という悪循環。

今やっている開発手法

で、大掛かりな構成変更を行って自分は「ここがクソ」「この機能が欲しい」とMarkdownに書くだけである程度回るような仕組みに再構築した。

docs
│- README.md  … docsディレクトリ内のファイル構成
├─decisions  … 設計に関する意思決定のログ。ADRは「Architecture Decision Record」の略
│  │- ADR-0001-XXX.md  … ADRの実体
│  │- ADR-0002-XXX.md
│  │- …
│  │- README.md  … ADRの運用ルール
│  └─templates
│        └─ ADR-template.md  … ADRのテンプレート
├─layout
│  │- index.md  … screensディレクトリ配下のレイアウトファイルの説明。
│  └─screens
│      │- album-detail.excalidraw  … Excalidrawで作ったモック
│      │- album-detail.md … 各画面の仕様・ルール・契約と、関連するADR
│      └─ …
├─planning
│  │- PLAN.md  … アプリの長期方針と実装基盤・データ・UIの原則
│  └─TODO.md  … 次にAIエージェントが行う作業内容
└─requirements
    │- accepted.md    … DISCONTENT.mdの内容から「実装・設計へ反映することを決めた要件」としてまとめたもの
    └─DISCONTENT.md  … アプリを触っていて感じた不満・要望。人間はこれだけ更新すればOK。

色々ファイルがあってややこしく見えるが、人間が触るのはrequirements/DISCONTENT.mdだけ。 他ファイルの更新は全部エージェントに任せる。

細かいフロー

  1. 人間がDISCONTENT.mdに不平不満をぶちまける。
  2. エージェントがDISCONTENT.mdから「まぁこれは実装してやるか…」と思ったものをaccepted.mdに落とし込む。
  3. エージェントがaccepted.mdの内容からdicisions / layout / planningの内容を更新し、具体的な実装方針をplanning/TODO.mdに書き込む。
  4. 人間は更新されたドキュメントの内容をレビューし、イメージと合っていたらTODO.mdの作業を実施するよう指示する。
    • イメージと合わない部分があったらaccepted.mdやADRの内容をベースにエージェントと詳細について話し合う。
flowchart TD
    A[DISCONTENT.md]
    B[accepted.md]
    C[decisions/]
    D[layout/]
    E[planning/]
    F[planning/TODO.md]
    G[実装]

    A --> B
    B --> C
    C --> D
    D --> E
    E --> F
    F --> G

    F -.レビュー差分.-> B

Skillとの併用

当然このフローを毎回指示するわけにもいかないので、docs-triagedocs-plan-syncという2つのSkillを使っている。

docs-triageDISCONTENT.mdに記載された内容からaccepted.md / PLAN.mdの更新と、ADRの更新 / 生成を担当する。

docs-triage

---
name: docs-triage
description: Use this skill when the task is to process docs/requirements/DISCONTENT.md or other raw feedback notes into stable documentation. Trigger when asked to organize complaints, extract adopted requirements, propose or add ADRs, or turn exploratory notes into accepted.md/rejected.md updates. Do not use for code implementation. Do not use for routine TODO or layout updates unless they are direct consequences of requirement triage.
---

# Purpose
This skill governs the **requirements triage** workflow for the repository's documentation set.

Its job is to convert raw dissatisfaction notes, complaints, observations, and improvement ideas into stable, reviewable artifacts with minimal context bloat.

## Repository documentation model
Treat the documentation set as four layers:

1. **Raw input**
   - `docs/requirements/DISCONTENT.md`
   - This is the inbox and source log.
   - Preserve the user's voice when possible.
   - Do not over-edit for style.

2. **Accepted requirements**
   - `docs/requirements/accepted.md`
   - Contains only requirements that are judged worth carrying forward.
   - Keep concise and structured.

3. **Planning**
   - `docs/planning/PLAN.md`
   - Long-term product direction and durable design principles.
   - Do not put narrow one-off decisions here.

4. **Architecture decisions**
   - `docs/decisions/ADR-xxxx-*.md`
   - One decision per file.
   - Use when a requirement implies or depends on a concrete design or governance choice.

## Core rules
- `DISCONTENT.md` is a **raw log**, not a polished spec.
- `accepted.md` is the **stable shortlist** of requirements adopted so far.
- `PLAN.md` contains only **durable direction** and broad product / architecture intent.
- An ADR is required when a requirement implies a concrete answer to a design question.
- Prefer creating a new ADR over expanding `PLAN.md` with detailed implementation rationale.
- Preserve traceability: every accepted requirement should be traceable to one or more raw complaints or observations.

## When triaging raw notes
For each meaningful item in `DISCONTENT.md`, classify it into one of these outcomes:

- **accepted**: belongs in `accepted.md`
- **rejected**: belongs in `rejected.md` if that file exists and the rejection matters
- **defer**: leave in `DISCONTENT.md` only, optionally note as future consideration elsewhere
- **already-covered**: do not duplicate; instead tighten existing accepted requirement or ADR if needed

## What belongs in accepted.md
A requirement belongs in `accepted.md` only if at least one of these is true:
- It describes a recurring UX or product problem.
- It constrains future implementation.
- It is useful for future review by humans or agents.
- It materially changes task prioritization or screen behavior.

Do not copy brainstorming noise, implementation chatter, or low-signal one-off frustrations into `accepted.md`.

## accepted.md writing format
Use a compact, stable structure. Each requirement should be self-contained and easy to reference.

Recommended template:

```md
## R-XXXX: Short title
status: accepted
priority: high | medium | low
scope: screen / feature / system
source: DISCONTENT.md
summary: one-sentence problem statement
acceptance:
  - concrete observable outcome
  - concrete observable outcome
related:
  - ADR-XXXX
  - layout/screens/example.md
```

Guidance:
- Make the title problem-oriented, not solution-oriented, unless the solution is already fixed.
- Keep acceptance criteria observable.
- Keep each requirement narrow.

## When to create or update an ADR
Create or update an ADR when any accepted requirement needs a durable answer to questions like:
- Which source of truth should govern a screen or data flow?
- Which architecture, storage, or sync pattern should be used?
- Which UX rule is fixed across future changes?
- Which technical tradeoff has been consciously accepted?

Do **not** create an ADR for every UX wish. Create one only when there is an actual decision worth preserving.

## ADR writing rules
- One decision per file.
- Use 4-digit numbering.
- File name format: `ADR-0001-short-kebab-case-title.md`
- Status values: `proposed`, `accepted`, `superseded`, `deprecated`
- Keep the file short and decision-centric.
- Prefer consequences and review triggers over long narrative.

Use the template from `assets/ADR-template.md`.

## Updating PLAN.md
Only touch `PLAN.md` if raw notes reveal a change in durable product direction.

Examples that may justify a PLAN update:
- the product's primary user journey changes
- a platform strategy changes
- a major architectural pillar changes
- a long-term prioritization principle changes

Examples that do **not** justify a PLAN update:
- one screen needs a new control
- a single field should move
- a spacing or toolbar issue on one platform
- a detailed implementation sequence changes

## Output discipline
When performing triage:
1. Read the raw notes.
2. Identify unique issues.
3. Merge duplicates.
4. Update `accepted.md` only with adopted requirements.
5. Create or update ADRs only where justified.
6. Avoid inflating `PLAN.md`.
7. Do not rewrite unrelated files.

## Definition of done
The triage is complete when:
- `DISCONTENT.md` remains the raw source log
- `accepted.md` contains only adopted, deduplicated requirements
- any new durable design decision has its own ADR
- `PLAN.md` was changed only if long-term direction changed
- no routine TODO churn was introduced unnecessarily

## Files to inspect first
1. `docs/requirements/DISCONTENT.md`
2. `docs/requirements/accepted.md`
3. relevant ADRs in `docs/decisions/`
4. `docs/planning/PLAN.md` only if a long-term direction question appears

## Files to avoid touching by default
- `docs/planning/TODO.md`
- unrelated `docs/layout/screens/*.md`
- source code

## Template and examples
- ADR template: `assets/ADR-template.md`
- accepted requirement template: `assets/accepted-template.md`
- triage example: `assets/triage-checklist.md`

docs-plan-syncdocs-triageで整理した要件・仕様をTODO.mdに落とし込みつつ、改めめて他のドキュメントを更新する必要がないかチェックする。

docs-plan-sync

---
name: docs-plan-sync
description: Use this skill when the task is to synchronize stable documentation into execution-ready planning artifacts. Trigger when asked to update PLAN.md, TODO.md, or layout screen docs from accepted requirements and existing ADRs. Use it for implementation planning and layout synchronization. Do not use it for raw complaint triage; use docs-triage for that.
---

# Purpose
This skill governs the **planning sync** workflow.

Its job is to turn already-accepted requirements and existing ADRs into a small, execution-ready set of planning artifacts with minimal token overhead.

## Documentation model
Treat the documentation stack as layered inputs and outputs:

### Inputs
- `docs/requirements/accepted.md`
- relevant ADRs in `docs/decisions/`
- relevant screen docs in `docs/layout/screens/`
- existing `docs/planning/PLAN.md`
- existing `docs/planning/TODO.md`

### Outputs
- updated `docs/planning/PLAN.md` only if durable direction changed
- updated `docs/planning/TODO.md` as a short near-term queue
- updated `docs/layout/index.md` only if navigation / indexing changed
- updated `docs/layout/screens/*.md` only for affected screens

## Core rules
- `accepted.md` is the source of adopted requirements.
- ADRs are the source of durable design decisions.
- `PLAN.md` should stay stable and strategic.
- `TODO.md` should stay short and operational.
- `layout/screens/*.md` should explain screen intent, fixed UI rules, and notable constraints.
- Do not expand planning files into prose logs or status history.

## PLAN.md rules
`PLAN.md` should contain only:
- product direction
- broad architecture principles
- durable UX principles
- platform strategy
- major sequencing / roadmap logic

Do **not** place the following in `PLAN.md`:
- step-by-step implementation procedures
- narrow UI tweaks
- issue logs
- repeated rationale already preserved in an ADR
- completed tasks

When in doubt, prefer leaving `PLAN.md` unchanged.

## TODO.md rules
`TODO.md` is a **near-term execution queue**, not a full project ledger.

Keep it small:
- ideally 3 to 7 active items
- each item should be implementation-sized
- each item should point to the relevant requirement and ADR
- remove or archive completed work rather than accumulating history here

Recommended task format:

```md
- [ ] Short implementation task
  - refs: R-XXXX, ADR-XXXX
  - targets: path/or/screen
  - done when:
    - observable outcome
    - observable outcome
```

Use the template from `assets/todo-template.md`.

## Layout synchronization rules
Use `docs/layout/index.md` as a lightweight index only.

Use `docs/layout/screens/*.md` for per-screen detail.
Each screen file should contain:
- purpose
- primary user actions
- fixed layout / interaction rules
- constraints from ADRs or accepted requirements
- references to companion `.excalidraw` files if present

Do not place broad product strategy inside screen docs.
Do not duplicate full accepted requirements if a concise reference is enough.

## When to update layout docs
Update a screen doc if one of these is true:
- the accepted requirement changes visible behavior on that screen
- an ADR changes a fixed UI rule or source of truth
- the screen purpose or major actions changed

Do not update layout docs for purely backend changes unless user-visible behavior changes.

## Synchronization procedure
1. Read `accepted.md`.
2. Read only the ADRs relevant to the changed area.
3. Identify whether the change affects strategy, near-term work, or screen behavior.
4. Update `PLAN.md` only if strategy changed.
5. Update `TODO.md` with only the next implementation-sized tasks.
6. Update `layout/screens/*.md` only for impacted screens.
7. Keep `layout/index.md` terse and navigational.

## Definition of done
The sync is complete when:
- `PLAN.md` remains strategic and compact
- `TODO.md` contains only the near-term queue
- each active task traces back to accepted requirements and/or ADRs
- only impacted layout docs were changed
- no raw feedback or decision rationale was duplicated unnecessarily

## Files to inspect first
1. `docs/requirements/accepted.md`
2. relevant `docs/decisions/ADR-*.md`
3. `docs/planning/PLAN.md`
4. `docs/planning/TODO.md`
5. affected `docs/layout/screens/*.md`

## Files to avoid touching by default
- `docs/requirements/DISCONTENT.md`
- unrelated ADRs
- unrelated screen docs
- source code, unless the user explicitly asked for implementation too

## Output style
- Prefer edits over rewrites.
- Preserve stable headings when possible.
- Keep queue items concrete and reviewable.
- Make references explicit: `R-XXXX`, `ADR-XXXX`, and screen paths.

## Template and examples
- TODO template: `assets/todo-template.md`
- screen template: `assets/layout-screen-template.md`
- plan sync checklist: `assets/plan-sync-checklist.md`

Skillのトリガー

「SkillはAGENTS.mdと違って強制力はない」というのは知っての通りだが、少なくともCodexにおいてはプロンプトに${Skill名}と入力しておくと確実にそのSkillを使ってくれる。*4

なので

$docs-triage DISCONTENTを更新しました

とか

$docs-plan-sync TODO.mdの内容を実装して

といったプロンプトを投げれば良く、トリガーは意識しないで良い。

まとめ

もちろんこれで常に完璧なコードが出てくるというわけではなく、ちゃんとアウトプットされたものの品質を担保するための仕組みは別で用意しないといけない*5が、少なくとも仕様についてあれこれ考えたり、どう実装するかなどを考えるのはほとんどAI任せにできるようになった。

とはいえ、この手法は

  • 個人で開発している
  • 自分が欲しい機能が実装されればそれでいい

という「要件・仕様を決められる権限」が自分一人に集約しているからできることだと思う。

企業で何かプロダクトを作ったり、チームで開発するなら別の方法を取った方が多分良い。

*1:自分は音楽サブスクとしてSpotifyを使っているのだけれど、世の中には配信されていない音源が大量にある。廃盤だったり、契約関係の都合だったり、そもそも最初から配信されていなかったり。Spotifyにもモバイル版ならローカルファイルを扱う仕組みがあるが、操作性は本当にゴミなので困っていた。

*2:もちろんどういう特性を持っているかぐらいは知識として知っているが、コードを書いたことはない。

*3:そもそも利用者がSpotifyのプレミアム会員じゃないとまともにAPIが使えないので、そんなに需要がないんじゃないかなぁ…と思っている。

*4:Claude Codeは使ったことがないので知らない。ありそうだけどね。

*5:よく言われている「絶対テストしろ」だけでなく、「RustやReactのベストプラクティスを考えた上でセルフレビューしろ」といったAGENTS.md / Skillを入れてたりする。

【JavaScript】【Vue.js】Vuex でも型補完がほしい

前書き

最近お仕事で Vue.js + Vuex をモリモリやっているんですが、せっかく VSCode を使っているのに公式ドキュメントに書いてある方法をそのまま使うと型補完が効かないのでしんどい気持ちになってきました。

commit / dispatchで指定する名前とかペイロードとか、stateから参照するメンバ名とその型とか、さくさく補完して欲しくないですか?IntelliSense に出てこないものは存在しないものと同義じゃないですか?

そういった辛いお気持ちをどうにかするため、色々やってみたメモです。

前提・環境

仕組みの概要

  • コンポーネント内では$vm.storeを参照するのではなく、importでストアのインスタンスを取得する。
  • マッピング用のヘルパー関数(mapStateとか)は使わない。*1
  • ステート用のクラスを作る。
  • ゲッターはステート用のクラスに関数として定義する。
  • commit / dispatchに指定する名前とペイロードを持つクラスを作る。
  • modules.namespacedによる名前空間の管理を行わない。
    • 全部グローバル空間に突っ込むことになるが、commit / dispatchに指定する名前が一意になっていれば問題ない。やり方は後述。
    • ちなみにmodules.namespacedtrueになっていなくても、ステートはstore[${モジュール名}]でアクセスできる。
  • 型情報が失われてしまう箇所はJSDocを書いて情報を補完する。*2

サンプルコード

サンプルとなるコードがないと説明しづらいので適当に例を載せておきます。

また、Githubに今回の動作検証用プロジェクトをあげてあります。今回の論旨に関係ない部分はめちゃめちゃ適当に作ったので、そのまま流用すると痛い目に合うと思います。

ストアの定義

要点は以下の通りです。

  • 検索処理用のモジュール
  • ステートとして検索結果を持つ
  • アクションに検索処理が定義されている
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    search: {
      state: {
        result: []
      },
      mutations: {
        setResult(state, payload) {
          state.result = payload;
        }
      },
      actions: {
        async search({ commit }, payload) {
          const body = JSON.stringify(payload);
          const resp = await fetch("/search", {
            method: "POST",
            headers: {
              Accept: "application/json",
              "Content-Type": "application/json"
            },
            body
          });
          commit("setResult", resp.json());
        }
      }
    }
  }
});

export default store;

コンポーネントの定義

先程のストアがマッピングされている検索用フォーム(src/components/SearchForm.vue)と検索結果(src/components/SearchResult.vue)のコンポーネントです。

<template>
  <div>
    <label> ID: <input type="text" @input="id = $event.target.value" /> </label>

    <button @click.prevent="onSearch">Search</button>
  </div>
</template>

<script>
  export default {
    name: "SearchForm",
    data() {
      return {
        id: ""
      };
    },
    methods: {
      async onSearch() {
        await this.$store.dispatch("search", { id: this.id });
      }
    }
  };
</script>
<template>
  <table v-if="result.length > 0">
    <thead>
      <th>ID</th>
      <th>Name</th>
    </thead>
    <tbody>
      <tr v-for="item in result" :key="item.id">
        <td>{{item.id}}</td>
        <td>{{item.name}}</td>
      </tr>
    </tbody>
  </table>
</template>

<script>
  export default {
    name: "SearchResult",
    computed: {
      result() {
        return this.$store.state.search.result;
      }
    }
  };
</script>

モジュールを別ファイルに分割する 9dafad2

とりあえずモジュールを分割しましょう。こんな感じのディレクトリ構成にします。

src
└─store
    │  index.js
    └─modules
        └─search
                actions.js
                index.js
                mutations.js
                namespace.js
                state.js

store/modules/search/index.jsでモジュールと名前空間をエクスポートするようにします。

// store/modules/search/index.js
import state from "./state";
import mutations from "./mutations";
import actions from "./actions";

const module = {
  state,
  mutations,
  actions
};

export default module;
export { default as nsSearch } from "./namespace";
// store/modules/search/namespace.js
export default "search";

state.js / mutations.js / actions.jsは先程のストア定義からそのまま切り出すだけです。

// store/modules/search/state.js
const state = {
  result: []
};

export default state;
// store/modules/search/mutations.js
const mutations = {
  setResult(state, payload) {
    state.result = payload;
  }
};

export default mutations;
// store/modules/search/actions.js
const actions = {
  async search({ commit }, payload) {
    const body = JSON.stringify(payload);
    const resp = await fetch("/search", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json"
      },
      body
    });
    commit("setResult", resp.json());
  }
};

export default actions;

store/index.jsでモジュールをインポートし、Vuex.Storeインスタンスを作るようにします。

// store/index.js
import Vue from "vue";
import Vuex from "vuex";
import searchModule, { nsSearch } from "./modules/search/index";

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    [nsSearch]: searchModule
  }
});

export default store;

これでモジュールが増えてもそれなりに管理しやすくなりました。

ステートのクラス化 8287dd

まずはステートをクラスにしてしまいましょう。何にも考えずにやると、こんな感じでしょうか。

/**
 * 検索用ステート
 */
class SearchState {
  /**
   * 検索用ステート
   */
  constructor() {
    /** 検索結果 */
    this.result = [];
  }
}

export { SearchState };

export default new SearchState();

クラスの定義(export { SearchState })とシングルトンインスタンスexport default new SearchState())は別々にエクスポートしておきます。理由は後で定義だけ参照したくなるからです。

ステートに定義されたオブジェクトのクラス化

このままだとSearchState#resultの型はany[]となってしまいます。これでは片手落ちなので、ちゃんと検索結果用のクラスも作ってあげます。

// store/modules/search/models.js
/**
 * 検索結果
 */
class SearchResult {
  /**
   * 検索結果
   */
  constructor() {
    /** ID */
    this.id = "";
    /** 名前 */
    this.name = "";
  }
}

export { SearchResult };

state.js側では@typeを指定して型情報を補完してあげます。

import * as Models from "./models";

/**
 * 検索用ステート
 */
class SearchState {
  /**
   * 検索用ステート
   */
  constructor() {
    /**
     * 検索結果
     * @type {Models.SearchResult[]}
     */
    this.result = [];
  }
}

export { SearchState };

export default new SearchState();

これでSearchModule#result[n]からidnameが補完されるようになりました。

ステートのエクスポート 461a29

とはいえ、今のままではコンポーネント側で参照するときに補完が効きません。意味ないですね。

そこでちゃんと型情報を付与してあげつつ、ストアとは別にステートをエクスポートするようにします。

// store/modules/search/index.js
import state from "./state";
import mutations from "./mutations";
import actions from "./actions";

const module = {
  state,
  mutations,
  actions
};

export default module;
// 型定義だけエクスポート
export { SearchState } from "./state";
export { default as nsSearch } from "./namespace";
// store/index.js
import Vue from "vue";
import Vuex from "vuex";
// 型定義 (SearchState) をインポートに追加
import searchModule, { nsSearch, SearchState } from "./modules/search/index";

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    [nsSearch]: searchModule
  }
});

export default store;
// エクスポートする時に @type を指定してあげるとインポート先で型情報が補完される
/** @type {SearchState} */
export const searchState = store.state[nsSearch];

後はコンポーネントsearchStateをインポートしてやれば OK です。

<template>
  <table v-if="result.length > 0">
    <thead>
      <th>ID</th>
      <th>Name</th>
    </thead>
    <tbody>
      <tr v-for="item in result" :key="item.id">
        <td>{{item.id}}</td>
        <td>{{item.name}}</td>
      </tr>
    </tbody>
  </table>
</template>

<script>
  // searchState をインポート
  import { searchState } from "../store/index.js";

  export default {
    name: "SearchResult",
    computed: {
      result() {
        // $vm.store ではなくインポートしたステートを直接参照
        // return this.$store.state.search.result;
        return searchState.result;
      }
    }
  };
</script>

副次効果として、コンポーネント内のmethodsなどからthis.resultを参照するとちゃんとSearchResult[]として扱われます。最高ですね。

MutationType / ActionTypeの作成 37fa2d

次はcommit / dispatchに指定する名前とペイロードがあやふやすぎる問題をどうにかしていきましょう。

commit / dispatchともに、第一引数に名前を渡す代わりにtypeを持つオブジェクトを渡すことが許されているので、これを活用します。

// 以下はどちらも mutations.setResult に対し { result: [] } を渡す
commit("setResult", { result: [] });
commit({ type: "setResult", result: [] });

// 以下はどちらも actions.search に対し { param: {} } を渡す
dispatch("search", { param: {} });
dispatch({ type: "search", param: {} });

具体的には、typeを参照するとcommit / dispatchで呼び出すべき名前が返ってくるクラスを作成します。

MutationTypeの定義

まずはミューテーションから。

// store/modules/search/mutationTypes.js
import namespace from "./namespace";

/**
 * 検索結果設定
 */
export class SetResult {
  get type() {
    return SetResult.type;
  }

  static get type() {
    return `${namespace}/SetResult`;
  }
}

そしてこのクラスのコンストラクタにペイロードを指定するようにすれば完璧です。

// store/modules/search/mutationTypes.js
import namespace from "./namespace";
import { SearchResult } from "./models";

/**
 * 検索結果設定
 */
export class SetResult {
  /**
   * 検索結果設定
   * @param {SearchResult[]} result 検索結果
   */
  constructor(result) {
    /**
     * 検索結果
     * @type {SearchResult[]}
     */
    this.result = result;
  }

  get type() {
    return SetResult.type;
  }

  static get type() {
    return `${namespace}/SetResult`;
  }
}

これでnew SetResult(result)という形でインスタンスを作れば{type: "search/SetResult", result: result}というオブジェクトを得られるようになりました。

また、@paramを記述することでペイロードとして渡すべき型の情報も明示されるようになったため、より安全にcommitを呼び出すことが可能になります。

さらにtypenamespaceを含めて返すようにすることで、実質namespacedtrueにしたのと同じ効果を得ることができます。

さらにさらに、今まで作ったクラスを組み合わせることでmutations内の関数でも型補完を効かせることができるようになります。

// store/modules/search/mutations.js
import { SearchState } from "./state";
import * as types from "./mutationTypes";

const mutations = {
  /**
   * 検索結果設定
   * @param {SearchState} state
   * @param {types.SetResult} payload
   */
  [types.SetResult.type](state, { result }) {
    state.result = result;
  }
};

export default mutations;

ActionTypeの定義

アクションも同じようなものを作れば OK です。

// store/modules/search/actionTypes.js
import namespace from "./namespace";

/**
 * 検索処理
 */
export class Search {
  /**
   * 検索処理
   * @param {string} id ID
   */
  constructor(id) {
    /**
     * ID
     * @type {string}
     */
    this.id;
  }

  get type() {
    return Search.type;
  }

  static get type() {
    return `${namespace}/Search`;
  }
}
// store/modules/search/actions.js
import { ActionContext } from "vuex";
import * as types from "./actionTypes";
import * as mutationTypes from "./mutationTypes";

const actions = {
  /**
   * 検索処理
   * @param {ActionContext} context
   * @param {types.Search} payload
   */
  async [types.Search.type]({ commit }, { id }) {
    const body = JSON.stringify({
      id
    });
    const resp = await fetch("/search", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json"
      },
      body
    });
    // action 内で commit を呼ぶ時も当然 MutationType を使える
    commit(new mutationTypes.SetResult(resp.json()));
  }
};

export default actions;

MutationType / ActionTypeのエクスポート

後はこれらをエクスポートしてあげればコンポーネントからも自由に使うことができます。

今はモジュールも一つ、MutationTypeActionTypeも一つずつしか定義されていませんが、これらがどんどん増えていくとインポートが大変なので、store/modules/search/index.jsでは* as SearchMutation / SearchActionとして一度インポートしておき、そのままエクスポートしておくと良いです。

// store/modules/search/index.js
import state from "./state";
import mutations from "./mutations";
// MutationType
import * as SearchMutation from "./mutationTypes";
import actions from "./actions";
// ActionType
import * as SearchAction from "./actionTypes";

const module = {
  state,
  mutations,
  actions
};

export default module;
export { SearchState } from "./state";
export { default as nsSearch } from "./namespace";
// MutationType / ActionType をエクスポート
export { SearchMutation, SearchAction };
// store/index.js
import Vue from "vue";
import Vuex from "vuex";
import searchModule, { nsSearch, SearchState } from "./modules/search/index";

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    [nsSearch]: searchModule
  }
});

export default store;
/** @type {SearchState} */
export const searchState = store.state[nsSearch];
// MutationType / ActionType をエクスポート
export { SearchMutation, SearchAction } from "./modules/search/index";

コンポーネントからは以下のように呼び出します。

<template>
  <div>
    <label> ID: <input type="text" @input="id = $event.target.value" /> </label>

    <button @click.prevent="onSearch">Search</button>
  </div>
</template>

<script>
  // SearchAction をインポート
  import store, { SearchAction } from "../store/index.js";
  export default {
    name: "SearchForm",
    data() {
      return {
        id: ""
      };
    },
    methods: {
      async onSearch() {
        // dispatch の引数を SearchAction.Search のインスタンスに変更
        // await this.$store.dispatch("search", { id: this.id });
        await store.dispatch(new SearchAction.Search(this.id));
      }
    }
  };
</script>

ゲッターの扱いについて 279266

後 Vuex の機能として残っているのはゲッターぐらいなんですが、実はこれ、stateのクラスに関数として定義してしまっても特に問題ありません。

というか、store.getters経由だとどう頑張っても型補完は無理です。*3

大人しくstate内に定義しましょう。

// store/modules/search/state.js
import * as Models from "./models";

/**
 * 検索用ステート
 */
class SearchState {
  /**
   * 検索用ステート
   */
  constructor() {
    /**
     * 検索結果
     * @type {Models.SearchResult[]}
     */
    this.result = [];
  }

  // これをゲッターの代わりにする
  get sortedByIdResult() {
    return this.result.sort((a, b) => {
      const idA = a.id.toUpperCase();
      const idB = b.id.toUpperCase();
      if (idA < idB) {
        return -1;
      }
      if (idA > idB) {
        return 1;
      }

      return 0;
    });
  }
}

export { SearchState };

export default new SearchState();

まとめ

と言うわけで、ここまでやると Vuex で型補完をバリバリ活用することができます。

そこそこ面倒な感じがするかもしれませんが、これぐらいやっておかないと大規模なアプリを作るときにかなりしんどいです。かなりしんどいです。今。ナウ。

*1:この辺を使おうとすると型補完は不可能。

*2:TypeScript を使うなら不要。

*3:store.getters["search/foo"] で返ってくる型をどこでどう定義しろと。

【PowerShell】PowerShellの$LASTEXITCODEとcmdの%ERRORLEVEL%が一致しないケース

前書き

約二年ぶりの更新です。元々メモ書きのようなブログですが、ここ二年ほどメモするほどの内容がありませんでした。

Spring Boot とか、Flutter とか、vue.js とか、Azure のアレコレとか、AWS のアレコレとか、Firebase のアレコレとか、色々やってたんですけどね。

上記のうち、Flutter の話はいつかしますが、今日は PowerShell の話です。

$LASTEXITCODEとは

PowerShell には自動変数と呼ばれる、一種の予約語のような変数があります。

$LASTEXITCODEはその中の一つであり、

最後に実行された Windows ベースのプログラムの終了コードが格納されます。

と書いてある通り、他プロセスの実行結果が格納されます。

.batでバリバリとバッチ処理を作ったことがある人なら「あ、%ERRORLEVEL%みたいなもんね。」と思うかもしれません。合ってます。合っていますが、完全に%ERRORLEVEL%と互換性があるわけではなく、それが原因でめちゃめちゃにハマりました。

$LASTEXITCODE%ERRORLEVEL%が一致しなくなるバッチ

こんな.batファイルを作ります。サブルーチンの方でexit /b 1を返し、メイン処理は特に何も返さない、それだけの処理です。

@echo off
call :hoge
exit /b
:hoge
exit /b 1

作った.batファイルをコマンドプロンプトで呼び出し、%ERRORLEVEL%を調べます。普通に1が返ってきます。

> test.bat

> echo %ERRORLEVEL%
1

PowerShell で同じように呼び出し、$LASTEXITCODEを調べます。

> .\test.bat

> $LASTEXITCODE
0

$LASTEXITCODE%ERRORLEVEL%を見ているわけではないので、メイン処理の返り値を取得してしまいます。よって、0が表示されます。

ワークアラウンド

自分で作った.batならメイン処理でexit /b %ERRORLEVEL%とすればいいんですが、諸々の事情*1.batの方を直せない場合、PowerShell 側で何とかする必要があります。

考え方は簡単です。明示的にcmd.exe /Cでバッチを叩いた後、exit %ERRORLEVEL%すればいいだけです。

&でコマンドを連続実行してやればワンライナーで楽勝です。遅延環境変数を展開できるよう/V:ONの指定を忘れずに。

> cmd /V:ON /C "test.bat & exit !ERRORLEVEL!"

> $LASTEXITCODE
1

まとめ

うーん。めんどくさい。

*1:他チームが作ったバッチ処理なので勝手に直せない、とか…。

【Git】他人のプロジェクトをsubmoduleで追加し、Pull Requestを自分でマージする方法

前書き

色々なライブラリを調べていると、「おっ、この PR いいじゃーん」と思ってもメンテナが全然やる気を出さないせいで一向にmasterに取り込まれないことが多々あり、mastersubmoduleで引っ張ってきた後、PR をマージするってことをたまにやるんですが、たまにしかやらないからか方法を毎回忘れるのでメモしておきます。

サブモジュールの追加

git submodule addすれば OK

git submodule add ${cloneしたいリポジトリのURL} ${新規に作るディレクトリ名}

PR のマージ

マージ用のブランチ作成

サブモジュールのディレクトリで適当なブランチを作ってそっちに切り替えておきましょう。*1

git checkout -b work

PR のマージ

以下のコマンドでpullしてきましょう。

git pull https://github.com/${PRを送ってきた人のID}/${プロジェクト名} ${PRを送ってきた人のブランチ名}

まとめ

特に何も言うことはないです。

*1:うっかり pull しちゃってもこれで安心ですね。

【JavaScript】明日から使えるRiot.js

前書き

相変わらず Web エンジニアみたいなお仕事をしているんですが、最近訳あって IE 8 からの呪縛から解き放たれました。

で、前々から使ってみたかったんだけど機会がなかったRiot.jsを使うことができるようになったので、ここ一ヶ月ほどもりもり書いていました。

ある程度の知見を得ることができたので、適当にメモしていきます。

Riot.js の目的

割とReactと比較されることが多い…と言うか、自分でReact の目指すところは悪くないんだけどあの文法はいくらなんでもクソすぎと言っている*1んですが、最終目標はDOM 操作における jQuery みたいな立ち位置を Web Components で目指すところっぽいです。Polymerの方が実質的なライバルでしょう。*2

また、ちょっとしたデータバインディングもできる*3ためか、Vue.jsからイチャモンをつけられているんですが、そもそもお互いが目指すゴールが違うので比較すること自体がなんだか妙な話です。*4

なお、あくまでライブラリであって、フレームワークではないです。「これ一本で Web アプリをバリバリつくるんじゃ~い!」と思うのであれば、AngularJSでも使ったほうがいいと思います。

つい最近Shadow DOM v1がとうとう Chrome に実装されたことでちょっとしたニュースになりましたが、あれもまだまだ気軽に使えるとは言いがたい*5ので、こう言うライブラリでもっと楽できればいいよね、みたいな気楽なスタンスです。

基本的な使い方

できる限り簡単に Web Components を実装したい!が最終目的なので、公式のガイドを 30 分~ 1 時間ぐらいかけて読めばほとんど全機能を使うことができます。

が、流石に実例がないとわかりづらいので、Bootstrap のサンプルを黙々と Riot.js に書き換えていく作業をやってみましょう。

Bootstrap も「CSS によるコンポーネント化を目指す」みたいなところがあるので、Web Components の概念と非常に相性がよくサンプルとして最適ですし、わかりやすく実用性があります。*6

また、読むだけだとやっぱりわかりづらいので、適当なリポジトリを作っておきました。Express で動くので、Node.js が入ってればすぐ遊べます。

元のソース

面倒なので<body>以下のところを一部引っこ抜いてきました。

<body>
  <nav class="navbar navbar-default navbar-fixed-top">
    <div class="container">
      <div class="navbar-header">
        <button
          type="button"
          class="navbar-toggle collapsed"
          data-toggle="collapse"
          data-target="#navbar"
          aria-expanded="false"
          aria-controls="navbar"
        >
          <span class="icon-bar"></span> <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">Project name</a>
      </div>
      <div id="navbar" class="navbar-collapse collapse">
        <ul class="nav navbar-nav">
          <li class="active"><a href="#">Home</a></li>
          <li><a href="#about">About</a></li>
          <li><a href="#contact">Contact</a></li>
        </ul>
      </div>
    </div>
  </nav>

  <div class="container">
    <div class="jumbotron">
      <h1>Navbar example</h1>
      <p>
        This example is a quick exercise to illustrate how the default, static
        and fixed to top navbar work. It includes the responsive CSS and HTML,
        so it also adapts to your viewport and device.
      </p>
      <p>
        To see the difference between static and fixed top navbars, just scroll.
      </p>
      <p>
        <a
          class="btn btn-lg btn-primary"
          href="../../components/#navbar"
          role="button"
          >View navbar docs &raquo;</a
        >
      </p>
    </div>
  </div>

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <script type="text/javascript" src="./javascripts/bootstrap.min.js"></script>
</body>

どっから手を付けて行こうかな、って感じですが、簡単そうな<div class="jumbotron">からいきますか。

カスタムタグと<yield/> [diff]

まずは適当な.tagファイルを作ります。jumbotron なんだから、jumbotron.tagとかでいいんじゃないですかね。(適当)

作り方はとても簡単です。

  1. ルート要素として新しく作りたいタグ(今回は<jumbotron>)を指定する。
  2. ルート要素の配下に実際に表示される内容を書く
<jumbotron>
  <div class="jumbotron">
    <yield/>
  </div>
</jumbotron>

これで終わりです。<yield/>を指定することで、呼び出し元のカスタムタグの中に記述された HTML をそのまま読み込んでくれます。

次は実際にこのカスタムタグを先ほどの html で読み込んでみましょう。これもとても簡単です。

  1. riot+compiler.min.jsを読み込む
  2. script type="riot/tag"を指定し、.tagを読み込む
  3. riot.mountでマウントする
<div class="container">
  <jumbotron>
    <h1>Navbar example</h1>
    <p>
      This example is a quick exercise to illustrate how the default, static and
      fixed to top navbar work. It includes the responsive CSS and HTML, so it
      also adapts to your viewport and device.
    </p>
    <p>
      To see the difference between static and fixed top navbars, just scroll.
    </p>
    <p>
      <a
        class="btn btn-lg btn-primary"
        href="../../components/#navbar"
        role="button"
        >View navbar docs &raquo;</a
      >
    </p>
  </jumbotron>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script type="text/javascript" src="./javascripts/bootstrap.min.js"></script>
<script
  type="text/javascript"
  src="./javascripts/riot+compiler.min.js"
></script>
<script type="riot/tag" src="./components/jumbotron.tag"></script>
<script>
  riot.mount("jumbotron");
</script>

当然これだけだと何の旨味もありません。次は<nav>に書かれたグローバルナビゲーションもコンポーネント化してみましょう。

まずは何も考えず html をコピペして.tagを作ってしまいます。名前はglobal-nav.tagとかでいいでしょう。

次はカスタムタグのマウントです。マウントする要素が増えてきたらriot.mount('*')で一括指定してしまいましょう。

<body>
  <global-nav></global-nav>

  <div class="container">
    <jumbotron>
      <h1>Navbar example</h1>
      <p>
        This example is a quick exercise to illustrate how the default, static
        and fixed to top navbar work. It includes the responsive CSS and HTML,
        so it also adapts to your viewport and device.
      </p>
      <p>
        To see the difference between static and fixed top navbars, just scroll.
      </p>
      <p>
        <a
          class="btn btn-lg btn-primary"
          href="../../components/#navbar"
          role="button"
          >View navbar docs &raquo;</a
        >
      </p>
    </jumbotron>
  </div>

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <script type="text/javascript" src="./javascripts/bootstrap.min.js"></script>
  <script
    type="text/javascript"
    src="./javascripts/riot+compiler.min.js"
  ></script>
  <script type="riot/tag" src="./components/jumbotron.tag"></script>
  <script type="riot/tag" src="./components/global-nav.tag"></script>
  <script>
    riot.mount("*");
  </script>
</body>

随分すっきりしましたね。すっきりしすぎてむしろちょっと不安です。

ですが、これで<global-nav>を指定するだけでどんなページでもグローバルナビゲーションを呼び出すことが可能になりました。

ついでに<nav>の中に書かれていた色んな要素もコンポーネント化してしまいましょう。一つの.tagファイルに複数のカスタムタグを定義できるので、黙々と分解してしまいます。

<global-nav>
  <nav class="navbar navbar-default navbar-fixed-top">
    <div class="container">
      <nav-header></nav-header>
      <navbar></navbar>
    </div>
  </nav>
</global-nav>

<nav-header>
  <div class="navbar-header">
    <button
      type="button"
      class="navbar-toggle collapsed"
      data-toggle="collapse"
      data-target="#navbar"
      aria-expanded="false"
      aria-controls="navbar"
    >
      <span class="sr-only">Toggle navigation</span>
      <span class="icon-bar"></span> <span class="icon-bar"></span>
      <span class="icon-bar"></span>
    </button>
    <a class="navbar-brand" href="#">Project name</a>
  </div>
</nav-header>

<navbar>
  <div id="navbar" class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
      <li class="active"><a href="#">Home</a></li>
      <li><a href="#about">About</a></li>
      <li><a href="#contact">Contact</a></li>
    </ul>
  </div>
</navbar>

カスタムタグの中にロジックを持たせる [diff]

困ったことに、このまま使っても<navbar>.activeが一切変わらず使い物になりません。

普通ならページごとに.activeを適用する場所だけを変えればいいんですが、せっかくコンポーネント化したのにそれはダサすぎです。自動でやってもらえるようにしましょう。

<navbar>
  <div id="navbar" class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
      <li each="{ nav in navs }" class="{ active: isActive(nav.link) }">
        <a href="{ nav.link }">{ nav.title }</a>
      </li>
    </ul>
  </div>

  <script>
    this.navs = [
      {
        link: "index.html",
        title: "Home"
      },
      {
        link: "about.html",
        title: "About"
      },
      {
        link: "contact.html",
        title: "Contact"
      }
    ];

    this.isActive = function(path) {
      return path == location.pathname.replace(/^\//, "");
    };
  </script>
</navbar>

Riot.js ではカスタムタグ自身が持つ JavaScript のオブジェクトを自由にバインディング可能です。*7

また、ifeachを使うことで要素の表示/非表示やループを簡単に定義することができます。

カスタムタグ内にのみ適用されるスタイルを定義する [diff]

ある日、不意に「グローバルナビゲーションの.activeの背景色だけ変えたいな…」と思う日が来るかもしれません。*8ちゃんとコンポーネント化してあれば簡単にできます。カスタムタグ内で<style scoped>を定義しましょう。*9

<global-nav>
  <nav class="navbar navbar-default navbar-fixed-top">
    <div class="container">
      <nav-header></nav-header>
      <div id="navbar" class="navbar-collapse collapse">
        <navbar></navbar>
        <ul class="nav navbar-nav navbar-right">
          <li class="active"><a href="./">scope test</a></li>
        </ul>
      </div>
    </div>
  </nav>
</global-nav>

<navbar>
  <ul class="nav navbar-nav">
    <li each="{ nav in navs }" class="{ active: isActive(nav.link) }">
      <a href="{ nav.link }">{ nav.title }</a>
    </li>
  </ul>

  <style scoped>
    /* bootstrapの指定が複雑すぎるので!importantで強制的に上書き */
    .active > a {
      background-color: #bbdefb !important;
    }
  </style>

  <script>
    this.navs = [
      {
        link: "index.html",
        title: "Home"
      },
      {
        link: "about.html",
        title: "About"
      },
      {
        link: "contact.html",
        title: "Contact"
      }
    ];

    this.isActive = function(path) {
      return path == location.pathname.replace(/^\//, "");
    };
  </script>
</navbar>

ついでに本当にカスタムタグ内だけで完結しているかすぐわかるようscope testなるものも<global-nav>に追加しておきました。scopedを削除するとそっちも変な青色になるのがわかると思います。

また、「ある特定のルールだけは scoped にして、他は全体に適用できるようにしたい」と思ってしまった人のために、:scope擬似クラスなんてものも用意されています。

正直デフォルトで全部scopedにしておいてほしいような気もします*10が、まぁ妙な事故を起こさないよう適切に指定してあげてください。

ちょっと応用的な使い方

なんとなく使い方はわかってきたと思うので、もう少し応用的な使い方も説明していきましょう。

次はこっちのサンプルを Riot.js で書きなおしていきます。

呼び出し元からのパラメータを設定 [diff]

まずはブログの内容部分をコンポーネント化してみますか。

元々はこんな感じでした。

<div class="blog-post">
  <h2 class="blog-post-title">Sample blog post</h2>
  <p class="blog-post-meta">January 1, 2014 by <a href="#">Mark</a></p>

  <!-- 本文が書かれているだけなので省略 -->
</div>

カスタムタグにぶち込みます。ついでに CSS も持ってきてしまいましょう。ここ以外で使わなそうだし。

<blog-post>
  <div class="blog-post">
    <yield/>
  </div>

  <style scoped>
    .blog-post {
      margin-bottom: 60px;
    }
    .blog-post-title {
      margin-bottom: 5px;
      font-size: 40px;
    }
    .blog-post-meta {
      margin-bottom: 20px;
      color: #999;
    }

  </style>
</blog-post>

</blog-post>

せっかくコンポーネント化したんだから、タイトルや投稿日、投稿者なんかはできれば外部からパラメータとして欲しいですね。当然できます。

カスタムタグはこんな感じに定義します。

<blog-post>
  <div class="blog-post">
    <h2 class="blog-post-title">{ opts.title }</h2>
    <p class="blog-post-meta">{ opts.meta } by <a href="{ opts.link }">{ opts.author }</a></p>
    <yield/>
  </div>

  <style scoped>
    .blog-post {
      margin-bottom: 60px;
    }
    .blog-post-title {
      margin-bottom: 5px;
      font-size: 40px;
    }
    .blog-post-meta {
      margin-bottom: 20px;
      color: #999;
    }

  </style>
</blog-post>

呼び出し元はこんな感じです。

<blog-post
  title="Sample blog post"
  meta="January 1, 2014"
  author="Mark"
  link="#"
>
  <!-- 本文なので省略 -->
</blog-post>

見てもらえればわかる通り、HTML の属性として指定したものはカスタムタグ内でoptsから取得することができます。どこでoptsを使ってどこで<yield/>を使うかは設計者の腕の見せどころです。

また、optsに渡す情報はタグのマウント時に動的に指定することもできます。*11

<script>
  riot.mount("blog-post", {
    title: "Sample blog post",
    meta: "January 1, 2014",
    author: "Mark",
    link: "#"
  });
</script>

サーバからの情報と連携 [diff]

「ブログの内容は全部 JSON に書かれていて、それを読み込んで表示する」みたいなパターンをやってみましょう。

まずはデータの準備です。どこかの何かの API 叩くと、こんな感じの JSON 配列が来るとします。

[
  {
    "title": "Sample blog post",
    "meta": "January 1, 2014",
    "author": "Mark",
    "link": "#",
    "body": "<p>This blog post shows a few different types of content that's supported and styled with Bootstrap. Basic typography, images, and code are all supported.</p>\n<hr>\n<p>Cum sociis natoque penatibus et magnis <a href=\"#\">dis parturient montes</a>, nascetur ridiculus mus. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Sed posuere consectetur est at lobortis. Cras mattis consectetur purus sit amet fermentum.</p>\n<blockquote>\n<p>Curabitur blandit tempus porttitor. <strong>Nullam quis risus eget urna mollis</strong> ornare vel eu leo. Nullam id dolor id nibh ultricies vehicula ut id elit.</p>\n</blockquote>\n<p>Etiam porta <em>sem malesuada magna</em> mollis euismod. Cras mattis consectetur purus sit amet fermentum. Aenean lacinia bibendum nulla sed consectetur.</p>\n<h2>Heading</h2>\n<p>Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>\n<h3>Sub-heading</h3>\n<p>Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.</p>\n<pre><code>Example code block</code></pre>\n<p>Aenean lacinia bibendum nulla sed consectetur. Etiam porta sem malesuada magna mollis euismod. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa.</p>\n<h3>Sub-heading</h3>\n<p>Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aenean lacinia bibendum nulla sed consectetur. Etiam porta sem malesuada magna mollis euismod. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.</p>\n<ul>\n<li>Praesent commodo cursus magna, vel scelerisque nisl consectetur et.</li>\n<li>Donec id elit non mi porta gravida at eget metus.</li>\n<li>Nulla vitae elit libero, a pharetra augue.</li>\n</ul>\n<p>Donec ullamcorper nulla non metus auctor fringilla. Nulla vitae elit libero, a pharetra augue.</p>\n<ol>\n<li>Vestibulum id ligula porta felis euismod semper.</li>\n<li>Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.</li>\n<li>Maecenas sed diam eget risus varius blandit sit amet non magna.</li>\n</ol>\n<p>Cras mattis consectetur purus sit amet fermentum. Sed posuere consectetur est at lobortis.</p>"
  }
  // 以下、同じようなものなので省略
]

この配列をeachでぐるぐる回し、先ほどの<blog-post>に表示させたいですね。こんな感じに。

<blog-main>
  <div class="col-sm-8 blog-main">
    <blog-post
      each="{ post in posts }"
      title="{ post.title }"
      meta="{ post.meta }"
      author="{ post.author }"
      link="{ post.link }"
      body="{ post.body }"
    >
    </blog-post>

    <nav>
      <ul class="pager">
        <li><a href="#">Previous</a></li>
        <li><a href="#">Next</a></li>
      </ul>
    </nav>
  </div>

  <style scoped>
    .blog-main {
      font-size: 18px;
      line-height: 1.5;
    }
  </style>

  <script>
    var self = this;
    this.posts = [];
    // API作るの面倒なのでJSON読みこむだけ
    $.get("/javascripts/blog.json").done(function(data) {
      self.posts = data;
      self.update();
    });
  </script>
</blog-main>

<blog-post>
  <div class="blog-post" name="body">
    <h2 class="blog-post-title">{ opts.title }</h2>
    <p class="blog-post-meta">
      { opts.meta } by <a href="{ opts.link }">{ opts.author }</a>
    </p>
  </div>

  <style scoped>
    .blog-post {
      margin-bottom: 60px;
    }
    .blog-post-title {
      margin-bottom: 5px;
      font-size: 40px;
    }
    .blog-post-meta {
      margin-bottom: 20px;
      color: #999;
    }
  </style>

  <script>
    // opts.bodyをそのまま表示するとエスケープされてしまうので、何らかの要素のinnerHTMLに追加しないといけない。
    // カスタムタグ内のnameが付いているDOMはthisからアクセスできるので、適当にdivにbodyなんてnameを指定している。
    $(this.body).append($(opts.body));
  </script>
</blog-post>

ここでのキモは JSONAjax で取得した後のself.update()です。確かに Riot.js はバインディング機構を持ってはいますが、厳密に Observable なわけではありません。カスタムタグ外のコンテクスト*12で何らかの値を更新した場合はちゃんと通知してあげる必要があります。*13

まとめ

本当はもっと落とし穴的な要素も紹介したかったんですが、あまりにも長くなってしまった*14ので一旦切ります。

続きを書けるのはいつになるんでしょうね。1 ヶ月後とかですかね。

参考

*1:私が Riot.js を気に入ったのはこれをはっきり言い切ったところが一番大きいです。

*2:今 Polymer ってどうなってるんでしょうね?React 以降全然話を聞かなくなってしまいましたが。

*3:と言うか、「コンポーネント」を謳っている以上、何らかの形で動的に表示するデータを差し替えられなきゃ使えたもんじゃない。

*4:実際、Riot.js 側の公式ガイドでは Vue.js の話は全くしていない。

*5:かなり頑張ってるとは思いますが…。

*6:私が最近やった仕事もひたすら「Bootstrap のせいでネストが深くなるところを Riot.js で書き直す」でした。

*7:script タグは省略可能ですが、個人的には唐突感があってあまり好きじゃないのでちゃんと書いてます。

*8:来ないと思う。

*9:なお、style scoped = Scoped CSS 自体は Riot.js 独自の機能ではなく、ちゃんとした HTML の仕様です。(実装が進んでいるとは言っていない) この記事で詳しく解説されているので読んでおきましょう。

*10:全体に適用するなら普通に.css ファイル書けばいいし…。

*11:ちなみに、マウント時に動的指定しつつ属性値で渡している場合は属性値で渡した値が優先される。

*12:めちゃめちゃ簡単に言うと、this がカスタムタグ以外を指している状態

*13:公式にObserverがあるように見えますが、これはどちらかと言うとカスタムタグ同士の Pub/Sub を実現するためのものです。

*14:サンプルがデカすぎるのが悪い。

【Android】【Retrofit】Retrofit 2.0.1使い方メモとハマりどころメモ

2018/11/29 追記

  • UTF-8 以外の文字コードで POST / PUT を行う方法を追記しました。
  • RxJava 2.x 用の CallAdapter を追記しました。

前書き

最近Retrofitを使うことがあったんですが、イントロダクションをちょろっと読んだぐらいだと「え、そーなの?」と思うような事象に何度も見舞われたので、メモしておきます。

API の設定

HTTP メソッドやパス、クエリやパラメータなどを適当に作ったインターフェースとアノテーションで表現します。

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

ベースとなる URL はRetrofit.Builder#baseUrlで渡すので、相対パスを書いておけば OK です。

アノテーションだけでどうにでもなってしまうので、色んなサービスの API を叩きたい時に共通の型がなくて却って使いにくいこともたまーにありますが、その辺は自分で共通の型を作ればいいだけの話です。

HTTP Methods

HTTP のメソッドに対応するアノテーションは以下のものが用意されています。必要十分って感じですね。

PATCHってDELETE以上に見たことないけど、意識の高い API 設計者は使うんでしょうか。

独自のメソッドを用意している意識が高すぎる API に対してはHTTP#methodに渡しましょう。

あと、微妙にハマりポイントなんですが、HEADを指定したメソッドの返り値の型は必ずCall<Void>にする必要があります。*1

普通にヘッダだけ欲しいんだけどどうしたらええの?と言う質問が Issue にあるんですが、ガン無視されています。私はもう面倒なので GET メソッド投げるようにしてしまいました。(怒)

ルーティング

@GET("users/{user}/repos"){user}部分をメソッドの引数で渡すことができます。

対応する引数にPathアノテーションをつけてあげれば OK です。

マルチバイト文字とか記号とかをうにゃうにゃしたい時はPath#encodedtrueを渡してあげましょう。

クエリ

Queryアノテーションを指定してあげればよしなにやってくれます。

@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @Query("sort") String sort);

optional なパラメータが多い時はQueryMapアノテーションを使うと便利です。

@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @QueryMap Map<String, String> options);

@Query@QueryMapencodedtrueを指定することで URL エンコードできるので、特に理由がないならやっておいた方が色々安全だと思います。

また、うっかりミスでこういうことをやってしまうことがあるんですが、これは怒られます。

// クエリ部分にPathで値を渡すのは無理
// 渡せるようにしても良い気がするけどね。
@GET("group/{id}/users?sort={sort}")
Call<List<User>> groupList(@Path("id") int groupId, @Path("sort") String sort);

クエリ応用編

@Queryを指定する引数を配列 / 可変長引数にすることで、同じクエリを複数指定することができます。

@GET("/list")
Call<ResponseBody> list(@Query("category") String... category);

例えば、service.list("hoge", "fuga")と呼び出したら、/list?category=hoge&category=&fugaになります。*2

また、特定のクエリを固定値で指定しておく場合はこのように書けば OK です。うまいこと後ろにクエリを足してくれます。

@GET("group/{id}/users?sort=desc")
Call<List<User>> groupList(@Path("id") int groupId, @Query("filter") String filter);

クエリ更に応用編(外法)

GETだけでなくどのメソッドでもいいんですが、Urlアノテーションを引数として渡すことで API のパス自体を動的に変更できます。

public interface GitHubService {
  @GET
  Call<List<Repo>> listRepos(@Url String url);
}

極端な話、この@Urlにパスもクエリもぶち込んでしまえば動いてくれます。型安全とは何だったのか。

POST のパラメータ

多分、PUTでも同じだと思います。

パラメータとして渡すにはBodyField(もしくはFieldMapアノテーションを使います。

application/x-www-form-urlencoded形式での POST

application/x-www-form-urlencoded (key=value) 形式に自動で変換して欲しい場合は@FormUrlEncoded@Filedを使います。

@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);

デフォルト値を設定する方法は現在開発中なので、@FieldMapと適当なデフォルト値を持つMap<String, String>を作るメソッドを併用するぐらいしか対処法がありません。

@FormUrlEncoded
@POST("user/edit")
// Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);
Call<User> updateUser(@FieldMap Map<String, String> names);
public static Map<String, String> createFullName(String first) {
    final Map<String, String> param = new HashMap<>();
    param.put("first_name", first);
    param.put("last_name", "hogehoge");
    return param;
}

それ以外の形式での POST

@Bodyを使うとそのまんま Body 部に値をぶちこみます。application/jsonなどを渡したい場合はこちらを使いましょう。

ちなみにConverter(後述)を指定することで独自の型でもいい感じに変換してくれます。*3

@POST("users/new")
Call<User> createUser(@Body User user);

UTF-8 以外でエンコードしたapplication/x-www-form-urlencoded

@FormUrlEncodedはパラメータを強制的に UTF-8エンコードしてしまいます。この文字コードを変えることはできません。*4

どうしても他の文字コードで POST する必要がある場合も@Bodyを使う必要があります。

Converterを自作してもいいですが、スカラー値を扱う Converterが公式で用意されているので、自分でapplication/x-www-form-urlencoded形式のStringを作ってしまうのが早いです。*5

// API定義
@POST
//@FormUrlEncoded
//Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);
Call<User> updateUser(@Body String body);
// 呼び出し方
String firstName = "山田";
String lastName = "太郎";

Function<String, String> encode = (s) -> {
  try {
    // java.net.URLEncoderでUTF-8以外の文字コードに変換
    return URLEncoder.encode(s, "Shift-JIS");
  } catch (UnsupportedEncodingException e) {
    throw new RuntimeException(e);
  }
};

// 自分でapplication/x-www-form-urlencoded形式のStringを作る
String body = String.format("%s=%s&%s=%s",
    encode.apply("first_name"),
    encode.apply(firstName),
    encode.apply("last_name"),
    encode.apply(lastName));

// StringをRequestBodyとして投げる
Call<User> user = service.updateUser(body);

Multipart なデータを POST

ちゃんとMultipartと言うアノテーションがあります。

渡す値はPart(もしくはPartMapアノテーションで指定します。

@Multipart
@PUT("user/photo")
Call<User> updateUser(@Part("photo") RequestBody photo, @Part("description") RequestBody description);

渡せる型はデフォルトだとokhttp3.MultipartBody.Partokhttp3.RequestBodyで固定されているようです。もちろん対応するConverterがあれば独自の型でも OK です。*6

なお、@Multipart@FormUrlEncodedは併用できません。Content-Typeが矛盾するからです。*7

ヘッダの指定

Headersアノテーションを指定しておくことで、事前にヘッダを作っておくことができます。

@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
Call<List<Widget>> widgetList();
@Headers({
    "Accept: application/vnd.github.v3.full+json",
    "User-Agent: Retrofit-Sample-App"
})
@GET("users/{username}")
Call<User> getUser(@Path("username") String username);

また、Headerアノテーションを引数に指定することで、動的にヘッダ部を変更することができます。

@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)

ただ、API 共通で指定したいヘッダはokhttp3.Interceptorを作ったほうがいいです。(後述)

実際の通信部分の設定

Retrofit.Builder#clientで指定するokhttp3.OkHttpClientが実際に HTTP 通信を行うクライアントになります。

どんな設定ができるかはokhttp3.OkHttpClient.Builderのメソッドを見れば大体わかると思います。また、Wiki の Recipesもかなり参考になると思います。

Proxy や Timeout、Cookie / キャッシュの管理もこれでやることになるので、Retrofit を使う場合は必ず一読しておいたほうがいいです。

ちなみにOkHttp3 ではOkHttpClientのコネクションプールを各インスタンス毎に持っているのでシングルトンで持っておくことが推奨されています。Interceptorタイムアウト値、リトライ回数なんかを API 別に設定したい場合はokhttp3.OkHttpClient#newBuilderを使いましょう

okhttp3.Interceptorの設定

下手なコードを読むよりWikiを読んだほうがわかりやすいです。

okhttp3.Interceptorを実装するにあたって覚えておくことは以下の 3 つだけです。

okhttp3.Responseも色々さわりたい*8場合はproceedで取得した内容をごにょごにょしましょう。

単純に共通のヘッダを指定したいだけならこんな感じになります。複数のヘッダを指定したい場合はokhttp3.Headersを使ったりしましょう。

public class CommonInterceptor implements Interceptor {
    @Override public Response intercept(Interceptor.Chain chain) throws IOException {
        return chain.proceed(
            chain.request().newBuilder()
                .header("User-Agent", "OkHttp Example")
                .build()
        );
    }
}

Cookie の設定

okhttp3.CookieJarを適当に実装してあげましょう。

一ミリも頭を使わない実装をするとこんな感じになります。ただ、あまりにも Cookie の仕様をガン無視しているのでおすすめできません。

public class NoHeadCookieJar implements CookieJar {
    private final List<Cookie> saveCookies = new ArrayList<>();

    @Override
    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
        for(Cookie c : cookies) {
           if(!saveCookies.contains(c)) saveCookies.add(c);
        }
    }

    @Override
    public List<Cookie> loadForRequest(HttpUrl url) {
        return saveCookies;
    }
}

きちんと Cookie を管理するのであればJavaNetCookieJarを使用するのが安全です。

JavaNetCookieJarCookieCookieHandler(=CookieManager)で管理するので、他の HTTP クライアントや、Android であればWebViewCookie を共有しやすくもなります。

ちなみにokhttp本体とは別モジュール*9なので、依存関係に追記が必要なことを忘れないでおきましょう。

dependencies {
    compile "com.squareup.okhttp3:okhttp-urlconnection:3.2.0"
}

今回は詳しく説明しませんが、同じくokhttp-urlconnection内にあるJavaNetAuthenticatorも中々便利そうなので、Retrofit or okhttp でプロキシ認証が必要になったら参考にしたり流用したりしましょう。

レスポンスの Call Adapter

Retrofit 2.x は、デフォルトのままだとレスポンスを必ずCall<T>でラップするようになっています。*10

Call<T>には同期的に実行するexecuteと非同期的に実行するenqueueの 2 つのメソッドが用意されています。

enqueueで渡すCallback<T>は確かに必要最低限の機能が用意されていますが、Promise パターンが使えないので、複数の API を組み合わせて実行しようとすると JavaScript もびっくりなコールバック地獄になります。

それは面倒だよね、ってことで、もっと便利なものにラップできるよう、Call Adapterが用意されています。

ライブラリ ラップする型 モジュール
RxJava 1.x Observable / Single com.squareup.retrofit2:adapter-rxjava
RxJava 2.x Observable / Flowable / Single / Maybe / Completable com.squareup.retrofit2:adapter-rxjava2
Guava ListenableFuture com.squareup.retrofit2:adapter-guava
Java 8 CompleteableFuture com.squareup.retrofit2:adapter-java8

実際にどの Call Adapter を使うかはRetrofit.Builder#addCallAdapterFactoryで指定します。

RxJava 1.x の Adapter を指定するならこんな感じです。

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.example.com")
    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
    .build();

なお、RxJavaCallAdapterFactoryにはRxJavaCallAdapterFactory#createWithScheduler(Scheduler)と言うメソッドも用意されています。ここで指定しておけば生成されるObservableに対して自動でsubscribeOnしてくれるので、上手く活用しましょう。*11

リクエスト・レスポンスの自動型変換

HTTP 通信によって取得した内容(Entity)は事前にConverterを指定しておくことで自動で型変換を行うことができます。*12

公式に用意されているものは以下の通りです。

ライブラリ モジュール
Gson com.squareup.retrofit2:converter-gson
Jackson com.squareup.retrofit2:converter-jackson
Moshi com.squareup.retrofit2:converter-moshi
Protobuf com.squareup.retrofit2:converter-protobuf
Wire com.squareup.retrofit2:converter-wire
Simple XML com.squareup.retrofit2:converter-simplexml
Scalars (primitives, boxed, and String) com.squareup.retrofit2:converter-scalars

使用する Converter はRetrofit.Builder#addConverterFactoryで指定することができます。Call Adapter と一緒ですね。

Gson の Converter を指定する場合はこんな感じになります。

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com")
    .addConverterFactory(GsonConverterFactory.create())
    .build();

HTTP 通信結果の取得

ステータスコードやヘッダといった情報をコールバックで取得したい場合は、返り値の型をResponse<T>でラップしましょう。(ex. Call<Response<String>>

ちなみにResponse<T>は Converter に一切依存しないので、どんな Converter を指定していても使用できます*13

また、戻り値の型をokhttp3.ResponseBodyにすることで、生の Entity や Body を取得することもできます。(ex. Call<ResponseBody>

こちらもConverter に一切依存しないので、「欲しい Converter が用意されてないけど自力で実装するのダルい」「型変換のところでエラーが出るから値を確認しつつデバッグしたい」などの場合に使いましょう。*14

まとめ

またなんか無駄に濃密な記事になってしまいました。

*1:それ以外の型を指定すると「HEAD method must use Void as response type」と怒られる

*2:@QueryMap で同じようなことができるかどうかは未検証。Key が同じになっちゃうから難しいような…?

*3:Retrofit 1.x では勝手に JSON 形式にしていたようだが、今はちゃんと Converter を指定してやる必要がある。

*4:現在の application/x-www-form-urlencoded の仕様的に仕方ないんですが…。

*5:Converter を自作しても結局同じようなことをする必要がある。

*6:Introductionで紹介されている Converter にはそれ用のものがないので、余程のことがないなら MultipartBody.Part か RequestBody でいい気がする。

*7:と、そんな回答がされている。まぁ、サーバ側の実装が悪いとしか言いようがないっちゃないよね。

*8:本来返されるレスポンスの内容をここで書き換えたりするのは行儀が悪い(と言うか、Retrofit においてそれは Converter のお仕事である)ので、各情報の取得だけにとどめておいた方が無難。

*9:なんでやねん。

*10:1.x では違ったらしいけど割愛。

*11:特に Android では「呼び出し元で subscribeOn し忘れた」のようなしょうもない事故を防止するためになるべくこっちを使うべき。

*12:レスポンスだけでなく、リクエストで使う引数の型も Converter で変換できる。

*13:もちろん一つも Converter を指定していなくても使用可能。

*14:Converter.Factoryの実装はどうしても「どんな型が指定されても大丈夫!」って感じにせざるを得ないので、しょうもない Converter を大量生産するよりはコールバック内でそれぞれ型変換した方がまだマシな気がする。(個人の感想です。)