<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>taiseidev</title><description>日々の仕事や趣味、考えごとを静かに残していく場所。</description><link>https://taiseidev.com/</link><item><title>中目黒探訪</title><link>https://taiseidev.com//posts/20260329-nakameguro-snap/</link><guid isPermaLink="true">https://taiseidev.com//posts/20260329-nakameguro-snap/</guid><pubDate>Sun, 29 Mar 2026 00:00:00 GMT</pubDate><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>江ノ島探訪</title><link>https://taiseidev.com//posts/20260328-enoshima-snap/</link><guid isPermaLink="true">https://taiseidev.com//posts/20260328-enoshima-snap/</guid><pubDate>Sat, 28 Mar 2026 00:00:00 GMT</pubDate><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>港区探訪</title><link>https://taiseidev.com//posts/20260327-minato-snap/</link><guid isPermaLink="true">https://taiseidev.com//posts/20260327-minato-snap/</guid><pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>上野探訪</title><link>https://taiseidev.com//posts/20260322-ueno-snap/</link><guid isPermaLink="true">https://taiseidev.com//posts/20260322-ueno-snap/</guid><pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>表参道探訪</title><link>https://taiseidev.com//posts/20260321-omotesando-snap/</link><guid isPermaLink="true">https://taiseidev.com//posts/20260321-omotesando-snap/</guid><pubDate>Sat, 21 Mar 2026 00:00:00 GMT</pubDate><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>鎌倉探訪</title><link>https://taiseidev.com//posts/20260317-kamakura-snap/</link><guid isPermaLink="true">https://taiseidev.com//posts/20260317-kamakura-snap/</guid><description>春の気配を探しに鎌倉へ。坂道や路地で、桜が少しずつほころびはじめていた一日の記録。</description><pubDate>Tue, 17 Mar 2026 00:00:00 GMT</pubDate><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>Jetpack ComposeのModifier適用順序を理解する</title><link>https://taiseidev.com//posts/modifier-order/</link><guid isPermaLink="true">https://taiseidev.com//posts/modifier-order/</guid><description>Jetpack ComposeのModifierは適用順序によってUIの見た目や挙動が大きく変わります。本記事では、Modifierの順序がUIにどのような影響を与えるかを図や具体例を交えて分かりやすく解説します。</description><pubDate>Sat, 19 Apr 2025 00:00:00 GMT</pubDate><content:encoded>## はじめに

Jetpack ComposeはAndroidのUIを簡潔に構築できるフレームワークであり、その中でも***Modifier***は、レイアウトの調整や装飾、インタラクションの追加など幅広い用途で活用される重要な機能です。

しかし、***Modifier***の適用順序を誤ると、意図しないレイアウト崩れや想定外の動作を引き起こす可能性があります。本記事では、***Modifier***の適用順序がUIにどのような影響を与えるのかを図や具体例を交えながら、なるべく初学者の方にも理解しやすい形で解説していきます。

なお、***Modifier***とは何かや基本的な使い方については公式ドキュメントが詳しく解説しているので、以下のリンクを参考にしてください。
&lt;br&gt;
&lt;br&gt;
[Compose 修飾子](https://developer.android.com/develop/ui/compose/modifiers?hl=ja)

## Modifierは適用順序が重要

冒頭でも述べたように、***Modifier***の適用順序を誤ると意図しないレイアウト崩れや想定外の動作を引き起こす可能性があります。
***Modifier***は上から順に適用されるため、どの順番で設定するかによって見た目や動作が大きく変わります。

ここでは、Textコンポーネントに背景色（***background***）とパディング（***padding***）を適用するシンプルな例を見ていきます。

### backgroundを先に適用した場合

```kotlin
    Text(
        text = &quot;Hello World!&quot;, modifier = modifier
            .background(Color.Red)
            .padding(16.dp)
    )
```

&lt;img src=&apos;/images/modifier-order/5.png&apos; width=&quot;250&quot;&gt;

上記の場合、まずは***background***が評価され、Textのテキストサイズに応じて色がつきます。その後***padding***が評価され、Textに対して***padding***が追加されることによって全体のサイズが拡張されます（背景色に影響することによって大きく見える）。

次に***background***と***padding***の順序を逆にしてみます。

### paddingを先に適用した場合

```kotlin
    Text(
        text = &quot;Hello World!&quot;, modifier = modifier
            .padding(16.dp)
            .background(Color.Red)
    )
```

&lt;img src=&apos;/images/modifier-order/6.png&apos; width=&quot;250&quot;&gt;

すると、先ほどとは異なり、先に***padding***が評価され、背景色のサイズ自体は変わりません（***padding***が背景色に影響しないので大きさが変わらない）。
このように***Modifier***は、前後を入れ替えるだけで簡単にUIが変わってしまうため、複雑なレイアウトを組む際には適切な知識を持っておく必要があります。

## なぜ順番が影響を与えるのか？

その理由は、Jetpack ComposeのUIは「UIツリー」という構造で管理されており、***Modifier***はこのツリー内でラップ構造として適用されるためです。

では、このUIツリーがどのように構築され、***Modifier***の適用順がどのように影響を与えるのかを具体的に見ていきましょう。

## UIツリーとは？

Jetpack ComposeのUIは、以下の3つのフェーズで構築されます。

### 1. コンポーズフェーズ

- Composable関数が評価され、UIツリーが生成される
- ここで、Text()やBox()などの組み込みコンポーザブルがUIツリー内に追加される

### 2. レイアウトフェーズ

- 親コンポーネントが子コンポーネントに「どのくらいのサイズで描画するか」を決める
- fillMaxSize(), size(100.dp), padding(16.dp) などの レイアウト関連の***Modifier***は、このフェーズで影響を受ける

### 3. 描画フェーズ

- UIツリーの情報を元に、実際に画面に描画する
- background(Color.Blue), clip(RoundedCornerShape(16.dp)), border(2.dp, Color.Black)などの描画関連の***Modifier***は、このフェーズで適用される

[UI ツリー内の修飾子](https://developer.android.com/develop/ui/compose/layouts/constraints-modifiers?hl=ja#modifiers-ui)に説明があるように、「***Modifier***はUIツリー内でノードをラップ」します。簡単に説明すると、***Modifier***を適用しているComposableを包み込むように適用されるということです。

&lt;https://developer.android.com/develop/ui/compose/phases?hl=ja&gt;

## Modifierを組む上でどのように意識したら良いか？

***レイアウト***→***配置***→***装飾***→***その他（クリックイベントやアニメーション）***
上記の順で***Modifier***を適用していきましょう。

## 順序を間違えたときによくある影響

実際に***Modifier***の順序を誤ると、次のような問題が起こりがちです。

| 組み合わせ | 影響 |
|---|---|
| `background()` + `padding()` | 背景色の適用範囲が意図しないものになる |
| `size()` + `clip()` | `size()` を後に適用すると `clip()` の影響を受けない |
| `fillMaxSize()` + `padding()` | `fillMaxSize()` を後に適用すると `padding()` が無視される |
| `clickable()` + `background()` | `clickable()` を先に適用すると、タップ範囲が狭くなる |

こうしたトラブルを避けるために、***Modifier***を5つのカテゴリに分けて適用順を整理しておくと便利です。

## Modifierの適用順序

| 順番 | カテゴリ         | 例                                      |
|------|--------------|--------------------------------------|
| 1️⃣  | **レイアウト系** | `size()`, `fillMaxSize()`, `weight()`  |
| 2️⃣  | **配置系**     | `padding()`, `align()`, `offset()`   |
| 3️⃣  | **描画系**     | `background()`, `border()`, `clip()` |
| 4️⃣  | **動作系**     | `clickable()`, `pointerInput()`      |
| 5️⃣  | **アニメーション系** | `alpha()`, `graphicsLayer()`         |

## カテゴリ別の実装サンプル

### 1️⃣ レイアウト系

レイアウト系のModifierは、コンポーネントの基本的なサイズや形状を決定します。これらは最初に適用するのが基本です。

```kotlin
Box(
    modifier = Modifier
        // レイアウト系: サイズを指定
        .size(width = 200.dp, height = 100.dp)
        // 以降のModifierはこのサイズに対して適用される
        .background(Color.LightGray)
        .padding(16.dp)
) {
    Text(&quot;レイアウト系を先に適用&quot;)
}
```

&lt;img src=&quot;/images/modifier-order/2.png&quot; width=&quot;250&quot;&gt;

レイアウト系のModifierを後に適用すると、他のModifierの効果が無視されることがあります：

```kotlin
Box(
    modifier = Modifier
        // 先にpaddingを適用
        .padding(16.dp)
        // 後からサイズを指定すると、paddingの効果が見えなくなる
        .size(width = 200.dp, height = 100.dp)
        .background(Color.LightGray)
) {
    Text(&quot;レイアウト系を後に適用（問題あり）&quot;)
}
```

&lt;img src=&quot;/images/modifier-order/1.png&quot; width=&quot;250&quot;&gt;

### 2️⃣ 配置系

配置系のModifierは、コンポーネントの位置や余白を調整します。レイアウト系の後に適用するのが適切です。

```kotlin
Column(
    modifier = Modifier
        .fillMaxWidth()
        // 配置系: paddingを適用
        .padding(16.dp)
        .background(Color.LightGray)
) {
    Text(&quot;配置系のModifierの例&quot;)
    Text(&quot;paddingを先に適用すると背景色の範囲が狭くなる&quot;)
}
```

&lt;img src=&quot;/images/modifier-order/3.png&quot; width=&quot;250&quot;&gt;

`offset`を使った例：

```kotlin
Box(
    modifier = Modifier
        .size(150.dp)
        .background(Color.LightGray)
) {
    Box(
        modifier = Modifier
            .size(50.dp)
            // 配置系: offsetで位置をずらす
            .offset(x = 20.dp, y = 20.dp)
            .background(Color.Red)
    )
}
```

&lt;img src=&quot;/images/modifier-order/4.png&quot; width=&quot;250&quot;&gt;

### 3️⃣ 描画系

描画系のModifierは、コンポーネントの見た目（背景色、境界線、形状など）を装飾します。

```kotlin
Box(
    modifier = Modifier
        .size(150.dp)
        .padding(16.dp)
        // 描画系: 背景色、角丸、境界線を適用
        .background(Color.LightGray, RoundedCornerShape(8.dp))
        .border(2.dp, Color.Black, RoundedCornerShape(8.dp))
) {
    Text(&quot;描画系のModifierの例&quot;, modifier = Modifier.align(Alignment.Center))
}
```

描画系のModifierの順序も重要です：

```kotlin
Row(
    modifier = Modifier.padding(16.dp)
) {
    // 正しい順序: clip -&gt; background
    Box(
        modifier = Modifier
            .size(100.dp)
            .clip(CircleShape)
            .background(Color.Blue)
    )

    Spacer(modifier = Modifier.width(16.dp))

    // 誤った順序: background -&gt; clip
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(Color.Red)
            .clip(CircleShape) // 背景色の後にclipを適用しても効果がない
    )
}
```

### 4️⃣ 動作系

動作系のModifierは、タップやスワイプなどのユーザー操作に対する反応を定義します。

```kotlin
var clickCount by remember { mutableStateOf(0) }

Box(
    modifier = Modifier
        .size(150.dp)
        .padding(16.dp)
        .background(Color.LightGray)
        // 動作系: クリックイベントを追加
        .clickable { clickCount++ }
) {
    Text(
        text = &quot;クリック回数: $clickCount&quot;,
        modifier = Modifier.align(Alignment.Center)
    )
}
```

動作系のModifierを先に適用すると、後から適用する視覚的なModifierに影響されないため、タップ可能な領域が意図したものと異なる場合があります：

```kotlin
Box(
    modifier = Modifier
        // 動作系を先に適用
        .clickable { /* クリックイベント */ }
        .size(150.dp)
        .padding(16.dp)
        .background(Color.LightGray)
) {
    Text(&quot;動作系を先に適用（問題あり）&quot;)
    // この場合、paddingやbackgroundで拡張された領域がクリック可能にならない
}
```

### 5️⃣ アニメーション系

アニメーション系のModifierは、透明度や変形などの視覚効果を動的に変化させます。

```kotlin
var isAnimating by remember { mutableStateOf(false) }
val alpha by animateFloatAsState(
    targetValue = if (isAnimating) 0.3f else 1.0f,
    label = &quot;alpha&quot;
)

Box(
    modifier = Modifier
        .size(150.dp)
        .padding(16.dp)
        .background(Color.LightGray)
        .clickable { isAnimating = !isAnimating }
        // アニメーション系: 透明度を変更
        .alpha(alpha)
) {
    Text(&quot;アニメーション系の例&quot;, modifier = Modifier.align(Alignment.Center))
}
```

`graphicsLayer`を使った例：

```kotlin
var isRotated by remember { mutableStateOf(false) }
val rotation by animateFloatAsState(
    targetValue = if (isRotated) 180f else 0f,
    label = &quot;rotation&quot;
)

Box(
    modifier = Modifier
        .size(150.dp)
        .padding(16.dp)
        .background(Color.LightGray)
        .clickable { isRotated = !isRotated }
        // アニメーション系: 回転効果を適用
        .graphicsLayer {
            rotationZ = rotation
        }
) {
    Text(&quot;回転アニメーション&quot;, modifier = Modifier.align(Alignment.Center))
}
```

## 複合的な例

実際のアプリ開発では、複数のカテゴリのModifierを組み合わせて使用することが一般的です。以下は、すべてのカテゴリを適切な順序で組み合わせた例です：

```kotlin
@Composable
fun ComplexModifierExample() {
    var isPressed by remember { mutableStateOf(false) }
    val scale by animateFloatAsState(
        targetValue = if (isPressed) 0.95f else 1f,
        label = &quot;scale&quot;
    )

    Box(
        contentAlignment = Alignment.Center,
        modifier = Modifier
            // 1️⃣ レイアウト系
            .fillMaxWidth()
            .height(120.dp)

            // 2️⃣ 配置系
            .padding(horizontal = 16.dp, vertical = 8.dp)

            // 3️⃣ 描画系
            .clip(RoundedCornerShape(12.dp))
            .background(MaterialTheme.colorScheme.primary)
            .border(
                width = 2.dp,
                color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.2f),
                shape = RoundedCornerShape(12.dp)
            )

            // 4️⃣ 動作系
            .pointerInput(Unit) {
                detectTapGestures(
                    onPress = {
                        isPressed = true
                        tryAwaitRelease()
                        isPressed = false
                    }
                )
            }

            // 5️⃣ アニメーション系
            .graphicsLayer {
                scaleX = scale
                scaleY = scale
            }
    ) {
        Text(
            text = &quot;すべてのカテゴリを組み合わせた例&quot;,
            color = MaterialTheme.colorScheme.onPrimary,
            style = MaterialTheme.typography.titleMedium
        )
    }
}
```

この例では、各カテゴリのModifierを推奨される順序で適用しています：

1. `fillMaxWidth()`と`height()`でレイアウトのサイズを設定
2. `padding()`で余白を追加
3. `clip()`、`background()`、`border()`で視覚的な装飾を適用
4. `pointerInput()`でタッチイベントを処理
5. `graphicsLayer()`でアニメーション効果を追加

## よくある間違いと解決策

### 間違い1: paddingとbackgroundの順序を逆にする

```kotlin
// bad
Box(
    modifier = Modifier
        .background(Color.Red)
        .padding(16.dp)
)

// good
Box(
    modifier = Modifier
        .padding(16.dp)
        .background(Color.Red)
)
```

解決策: 背景色の範囲を制御したい場合は、`padding()`を先に適用します。背景色を含めた全体にパディングを適用したい場合は、`background()`を先に適用します。

### 間違い2: clickableを先に適用する

```kotlin
// bad
Box(
    modifier = Modifier
        .size(100.dp)
        .clickable { /* クリックイベント */ }
        .padding(16.dp)
        .background(Color.Blue)
)

// good
Box(
    modifier = Modifier
        .size(100.dp)
        .padding(16.dp)
        .background(Color.Blue)
        .clickable { /* クリックイベント */ }
)
```

解決策: `clickable()`は視覚的なModifierの後に適用して、視覚的に見えている領域全体がタップ可能になるようにします。先に適用すると、背景色のない余白部分までタップに反応してしまいます。

### 間違い3: clipとbackgroundの順序を逆にする

```kotlin
// 間違い
Box(
    modifier = Modifier
        .background(Color.Green)
        .clip(CircleShape)
)

// 正しい
Box(
    modifier = Modifier
        .clip(CircleShape)
        .background(Color.Green)
)
```

解決策: `clip()`は`background()`の前に適用して、背景色がクリップされた形状に合わせて描画されるようにします。

## まとめ

Jetpack ComposeのModifierは、適用順序によってUIの見た目や挙動が大きく変わります。本記事では、Modifierを以下の5つのカテゴリに分類し、適切な適用順序を解説しました：

1. **レイアウト系**: サイズや形状を決定する基本的なModifier（`size()`, `fillMaxSize()`など）
2. **配置系**: 位置や余白を調整するModifier（`padding()`, `align()`など）
3. **描画系**: 見た目を装飾するModifier（`background()`, `border()`など）
4. **動作系**: ユーザー操作に対する反応を定義するModifier（`clickable()`, `pointerInput()`など）
5. **アニメーション系**: 視覚効果を動的に変化させるModifier（`alpha()`, `graphicsLayer()`など）

これらのカテゴリを意識し、適切な順序でModifierを適用することで、意図したとおりのUIを実現できます。特に初学者の方は、Modifierの適用順序に注意して、UIの挙動を理解しながら開発を進めることをおすすめします。

また、複雑なUIを構築する際には、各ModifierがどのようにUIツリーに影響を与えるかを理解することが重要です。Jetpack Composeの公式ドキュメントやサンプルコードを参考にしながら、実際に様々な順序でModifierを適用して動作を確認してみることで、理解を深めることができるでしょう。

Modifierの適用順序を正しく理解することで、Jetpack Composeを使ったUI開発がより直感的かつ効率的になります。ぜひ本記事の内容を参考に、美しく機能的なUIを構築してください。

## 参考文献

- [制約と修飾子の順序](https://developer.android.com/develop/ui/compose/layouts/constraints-modifiers?hl=ja)&lt;br&gt;
- [JetpackComposeのModifierの順序について](https://zenn.dev/mitohato/articles/b9840ea54d66cd)&lt;br&gt;
- [Compose修飾子の順序](https://developer.android.com/jetpack/compose/modifiers?hl=ja#order)&lt;br&gt;
- [Jetpack Compose Modifier API](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier)&lt;br&gt;</content:encoded><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>KotestのLifecycle hooksについてまとめてみた</title><link>https://taiseidev.com//posts/kotest-before-after/</link><guid isPermaLink="true">https://taiseidev.com//posts/kotest-before-after/</guid><description>Kotestで使えるbefore/after系のライフサイクル関数をまとめて解説。使い分けに迷う方に向けて、実際のコード例とともに詳しく紹介します。</description><pubDate>Sat, 29 Mar 2025 00:00:00 GMT</pubDate><content:encoded>## はじめに

Kotestに限らず、テストコードを書く際にはbefore/afterといった前後処理関数を使うことがあります。ただ、Kotestではそれらの関数が非常に多く用意されており、結局どれを使えば良いのか迷うことがあるかと思います。

そこで今回は、Kotestで使えるライフサイクル系のbefore/after関数について、実際のコード例を交えてまとめてみました。
どのように活用できるかを実際のコードを交えながら解説していくので、是非、Kotestを使ったテストコードを書く際の参考にしていただけたら嬉しいです！

### version
Kotest: ver 5.9.1

## 対象者
- Kotlinでテストを書く際にbefore/afterの使い方を学びたい方
- Kotestを使いこなしたい初心者〜中級者向け
- テストの前後処理を効率的に行いたい方

## Kotestのbefore/afterの種類

Kotestには多くのライフサイクル関数が用意されており、以下のようなものがあります（公式：[Lifecycle hooks](https://kotest.io/docs/framework/lifecycle-hooks.html)）：

| 関数名            |
|-------------------|
| beforeContainer    |
| afterContainer     |
| beforeEach         |
| afterEach          |
| beforeAny          |
| afterAny           |
| beforeTest         |
| afterTest          |
| beforeSpec         |
| afterSpec          |
| finalizeSpec       |
| beforeInvocation   |
| afterInvocation    |

&gt; 公式ドキュメントでは***prepareSpec***というメソッドが記載されていますが、[Sanitize prepareSpec and finalizeSpec behavior ](https://github.com/kotest/kotest/pull/1617/files#diff-cead89fdee3b0f2c28eb11479c5e4e8a7124877fa30ee93b591c58e75f5e8652)でDeprecatedになり、現在は使用できないです。

### 前提

今回はKotestの中でも***DescribeSpec***を使って解説してきますが、他のKotestの書き方を使っても基本的には動作に変わりはありません。
***DescribeSpec***を使ったことがない方のために簡単な使い方を解説します。慣れている方は読み飛ばしてください。

***DescribeSpec***は、テストケースを階層構造で整理しやすい***BDDスタイル***のSpecです。
テスト対象の機能や振る舞いをdescribe {} ブロックで表現し、その中で条件ごとの前提をcontext{}で区切り、個別の動作確認をit{}で記述するのが特徴です。
簡単に説明すると、***describe***にテスト対象のクラスや関数名を記述、***context***でテスト条件を指定、***it***で結果を記述し、個別のテストを行なっていきます。

```kotlin
class SampleTest : DescribeSpec({
    describe(&quot;ユーザー登録機能&quot;) {
        context(&quot;入力が正しい場合&quot;) {
            it(&quot;登録に成功すること&quot;) {
                // アサーション
            }
        }

        context(&quot;メールアドレスが無効な場合&quot;) {
            it(&quot;登録に失敗すること&quot;) {
                // アサーション
            }
        }
    }
})
```

このように、describe→context→itの構造にすることで、テストの意図を自然な言葉で表現でき、可読性の高いテストを書くことができます。

## 1. Spec単位で1回だけ実行される関数

|関数名|説明|
|---|---|
|beforeSpec|Specインスタンスが生成されたあと、最初に1回だけ実行される処理。DBの初期化や一時ファイル作成などに使用|
|afterSpec|Spec内の全てテストが終わった後に1回だけ実行される。リソース解放やログ出力に使用|
|finalizeSpec|afterSpecの後に呼ばれる|

```kotlin
class KotestLifecycleDemo : DescribeSpec({
    beforeSpec {
        println(&quot;[beforeSpec] Specの最初に1回だけ実行&quot;)
    }

    afterSpec {
        println(&quot;[afterSpec] Specの最後に1回だけ実行&quot;)
    }

    finalizeSpec {
        println(&quot;[finalizeSpec] Spec終了後のクリーンアップ処理&quot;)
    }

    describe(&quot;ライフサイクルテスト&quot;) {

        it(&quot;テストケース1&quot;) {
            println(&quot;→ テストケース1 実行中&quot;)
        }

        it(&quot;テストケース2&quot;) {
            println(&quot;→ テストケース2 実行中&quot;)
        }
    }
})
```

```
[beforeSpec] Specの最初に1回だけ実行
→ テストケース1 実行中
→ テストケース2 実行中
[afterSpec] Specの最後に1回だけ実行
[finalizeSpec] Spec終了後のクリーンアップ処理
```

SpecとはKotestにおけるテストクラスのインスタンスのことを指します。
つまり、今回のケースだと***KotestLifecycleDemo***インスタンスが作成された際に***beforeSpec***が実行され、すべてのテストケースが終了した際に***afterSpec***が実行されます。
***finalizeSpec***はあまり使う機会はないかもしれませんが、***afterSpec***が実行された後に実行されます。

上記の用途としては、***beforeSpec***でDBのインスタンスを立ち上げて、***afterSpec***でDBの接続を切るといった使い方ができると思います。

## 2. 各テスト関数の前後で実行される関数

|関数名|説明|
|---|---|
|beforeEach|各テスト関数の直前に毎回実行。共通の前準備に使用する|
|afterEach|各テスト関数の直後に毎回実行。テストの後処理やクリーンアップに使用する。テストが失敗した場合も必ず実行される。|

```kotlin
class KotestLifecycleDemo : DescribeSpec({
    beforeEach {
        println(&quot;[beforeEach] 各テスト単位の前に実行&quot;)
    }

    afterEach {
        println(&quot;[afterEach] 各テスト単位の後に実行&quot;)
    }

    describe(&quot;ライフサイクルテスト&quot;) {

        it(&quot;テストケース1&quot;) {
            println(&quot;→ テストケース1 実行中&quot;)
        }

        it(&quot;テストケース2&quot;) {
            println(&quot;→ テストケース2 実行中&quot;)
        }
    }
})
```

```
[beforeEach] 各テスト単位の前に実行
→ テストケース1 実行中
[afterEach] 各テスト単位の後に実行
[beforeEach] 各テスト単位の前に実行
→ テストケース2 実行中
[afterEach] 各テスト単位の後に実行
```

***beforeEach***/***afterEach***はシンプルで、各テスト（***it***）が実行される前後に実行されます。

## 3. describe/contextブロックの前後で実行される関数

|関数名|説明|
|---|---|
|beforeContainer|describeやcontextなどのブロックの直前に実行。|
|afterContainer|ブロックのすべてのテストが終わった後に実行。テストが失敗した場合も必ず実行される。|

```kotlin
class KotestLifecycleDemo : DescribeSpec({
    beforeContainer {
        println(&quot;[beforeContainer] Containerの前に実行&quot;)
    }

    afterContainer {
        println(&quot;[afterContainer] Containerの後に実行&quot;)
    }

    describe(&quot;ライフサイクルテスト - describe ブロック&quot;) {

        it(&quot;テストケース1&quot;) {
            println(&quot;→ テストケース1 実行中&quot;)
        }

        it(&quot;テストケース2&quot;) {
            println(&quot;→ テストケース2 実行中&quot;)
        }

        context(&quot;ライフサイクルテスト - context ブロック&quot;) {

            it(&quot;テストケース3&quot;) {
                println(&quot;→ テストケース3 実行中&quot;)
            }

            it(&quot;テストケース4&quot;) {
                println(&quot;→ テストケース4 実行中&quot;)
            }
        }
    }

    describe(&quot;テストケース5&quot;) {
        it(&quot;テストケース5&quot;) {
            println(&quot;→ テストケース5 実行中&quot;)
        }
    }

    describe(&quot;テストケース6&quot;) {}
})
```

```
[beforeContainer] Containerの前に実行
→ テストケース1 実行中
→ テストケース2 実行中
[beforeContainer] Containerの前に実行
→ テストケース3 実行中
→ テストケース4 実行中
[afterContainer] Containerの後に実行
[afterContainer] Containerの後に実行
[beforeContainer] Containerの前に実行
→ テストケース5 実行中
[afterContainer] Containerの後に実行
```

***beforeContainer***/***afterContainer***は、各***describe***や***context***ブロックの中に実行対象となるテスト（***it***）がある場合に限り、そのブロックの実行前後に1回ずつ実行されます。&lt;br&gt;
注意点として、ブロック内に有効なテストが1つもない場合（すべてスキップなど）には呼ばれません。

## 4. すべてのTestTypeに対して実行される関数

|関数名|説明|
|---|---|
|beforeAny|各TestCase（describe,context,itなど）の直前に呼ばれる。ラムダでTestCaseを受け取る。|
|afterAny|各TestCaseの実行後に呼ばれる。スキップされたテストには呼ばれない。ラムダでTuple2&lt;TestCase, TestResult&gt;を受けとる。テストが失敗した場合も必ず実行される。|

```kotlin
class KotestLifecycleDemo : DescribeSpec({

    beforeAny { testCase: TestCase -&gt;
        println(&quot;[beforeAny] 実行前: ${testCase.name.testName} - type: ${testCase.type.name}&quot;)
    }

    afterAny { (testCase, result) -&gt;
        println(&quot;[afterAny] 実行後: ${testCase.name.testName} - type: ${testCase.type.name} - result: ${result.status}&quot;)
    }

    describe(&quot;ライフサイクルテスト - describe ブロック&quot;) {

        it(&quot;テストケース1&quot;) {
            println(&quot;→ テストケース1 実行中&quot;)
        }

        it(&quot;テストケース2&quot;) {
            println(&quot;→ テストケース2 実行中&quot;)
        }

        context(&quot;ライフサイクルテスト - context ブロック&quot;) {

            it(&quot;テストケース3&quot;) {
                println(&quot;→ テストケース3 実行中&quot;)
            }

            it(&quot;テストケース4&quot;) {
                println(&quot;→ テストケース4 実行中&quot;)
            }
        }
    }
})
```

```
[beforeAny] 実行前: ライフサイクルテスト - describe ブロック - type: Container
[beforeAny] 実行前: テストケース1 - type: Test
→ テストケース1 実行中
[afterAny] 実行後: テストケース1 - type: Test - result: Success
[beforeAny] 実行前: テストケース2 - type: Test
→ テストケース2 実行中
[afterAny] 実行後: テストケース2 - type: Test - result: Success
[beforeAny] 実行前: ライフサイクルテスト - context ブロック - type: Container
[beforeAny] 実行前: テストケース3 - type: Test
→ テストケース3 実行中
[afterAny] 実行後: テストケース3 - type: Test - result: Success
[beforeAny] 実行前: テストケース4 - type: Test
→ テストケース4 実行中
[afterAny] 実行後: テストケース4 - type: Test - result: Success
[afterAny] 実行後: ライフサイクルテスト - context ブロック - type: Container - result: Success
[afterAny] 実行後: ライフサイクルテスト - describe ブロック - type: Container - result: Success
```

***beforeAny***/***afterAny***は***beforeEach***/***afterEach***と違って全てのTestTypeの前後で実行されます。
そもそもTestTypeとは何かについて、TestTypeはKotest内部で定義されているenumで、このTestCaseがどういう種類かを区別するために使われます。

|type|説明|
|---|---|
|TestType.Test|通常のテストケースに使われる（it {}やshould {}など）。実行されるテスト本体。|
|TestType.Container|グループ化されたテスト構造（describe {}やcontext {}など）に使われる。ラムダ内にitなどのテストを書く構造。|
|TestType.Dynamic|forAll {}やcheckAll {}などの動的に生成されるテストケースに使われる。|

2章で各テスト毎に実行される***beforeEach***/***afterEach***について説明しましたが、これらはTestType.Testの時のみ実行され、***beforeAny***/***afterAny***は全てのTestTypeの前後において実行されるということになります。

&gt; ***beforeTest***/***afterTest***に関しては***beforeAny***/***afterAny***と基本的には同じ挙動なので今回は省略します。

## 5. テストの繰り返し（invocation）ごとに実行される関数

|関数名|説明|
|---|---|
|beforeInvocation|テスト関数が複数回実行される場合に、各実行（invocation）の直前に呼ばれる。第一引数ではTestCaseクラスを受け取り、第二引数では現在の実行回数を受け取る。|
|afterInvocation|各実行（invocation）の直後に呼ばれる。こちらも***beforeInvocation***と同様の引数を受け取る|

&gt; 注意点: 第二引数のインデックスはListのインデックスと同様に 0から始まります

```kotlin
class KotestLifecycleDemo : DescribeSpec({

    beforeInvocation { testCase: TestCase, iteration: Int -&gt;
        println(&quot;  → [beforeInvocation] ${testCase.name.testName} : invocation #$iteration&quot;)
    }

    afterInvocation { testCase: TestCase, iteration: Int -&gt;
        println(&quot;  → [afterInvocation] ${testCase.name.testName} : invocation #$iteration&quot;)
    }

    describe(&quot;ライフサイクルテスト - describe ブロック&quot;) {

        it(&quot;テストケース1（3回繰り返し）&quot;).config(invocations = 3) {
            println(&quot;→ テストケース1 実行中&quot;)
        }

        it(&quot;テストケース2（1回のみ）&quot;) {
            println(&quot;→ テストケース2 実行中&quot;)
        }

        context(&quot;ライフサイクルテスト - context ブロック&quot;) {

            it(&quot;テストケース3（2回繰り返し）&quot;).config(invocations = 2) {
                println(&quot;→ テストケース3 実行中&quot;)
            }

            it(&quot;テストケース4（1回のみ）&quot;) {
                println(&quot;→ テストケース4 実行中&quot;)
            }
        }
    }
})
```

```
  → [beforeInvocation] ライフサイクルテスト - describe ブロック : invocation #0
  → [beforeInvocation] テストケース1（3回繰り返し） : invocation #0
→ テストケース1 実行中
  → [afterInvocation] テストケース1（3回繰り返し） : invocation #0
  → [beforeInvocation] テストケース1（3回繰り返し） : invocation #1
→ テストケース1 実行中
  → [afterInvocation] テストケース1（3回繰り返し） : invocation #1
  → [beforeInvocation] テストケース1（3回繰り返し） : invocation #2
→ テストケース1 実行中
  → [afterInvocation] テストケース1（3回繰り返し） : invocation #2
  → [beforeInvocation] テストケース2（1回のみ） : invocation #0
→ テストケース2 実行中
  → [afterInvocation] テストケース2（1回のみ） : invocation #0
  → [beforeInvocation] ライフサイクルテスト - context ブロック : invocation #0
  → [beforeInvocation] テストケース3（2回繰り返し） : invocation #0
→ テストケース3 実行中
  → [afterInvocation] テストケース3（2回繰り返し） : invocation #0
  → [beforeInvocation] テストケース3（2回繰り返し） : invocation #1
→ テストケース3 実行中
  → [afterInvocation] テストケース3（2回繰り返し） : invocation #1
  → [beforeInvocation] テストケース4（1回のみ） : invocation #0
→ テストケース4 実行中
  → [afterInvocation] テストケース4（1回のみ） : invocation #0
  → [afterInvocation] ライフサイクルテスト - context ブロック : invocation #0
  → [afterInvocation] ライフサイクルテスト - describe ブロック : invocation #0
```

Kotestの***it***は***TestWithConfigBuilder***を返します。このビルダーが持つ***config***関数を用いることで、各テストケースに対して設定を追加できます。
その中でも***invocations***は、同じテストを何回繰り返すかを指定するためのオプションです。

```kotlin
it(&quot;Sample Test&quot;).config(invocations = 3) {
    // 3回実行される
}
```

このように設定した場合、***beforeInvocation***/***afterInvocation***は3回それぞれの前後で呼び出され、各回のインデックス（0, 1, 2）を受け取ることができます。

## 補足：スキップされたテストケースには呼ばれない

各before/after関数にもテストケースが呼ばれない例外があります。
それは、スキップされたテスト（無視・無効・非アクティブな状態のテスト）にはコールバックが呼ばれないという点です。

公式ドキュメントにも、次のように明記されています：

&gt; If a test case was skipped (ignored / disabled / inactive), then this callback will not be invoked for that particular test case.

### スキップされる例

以下のようなケースでは特定のライフサイクル関数は呼び出されません。

```kotlin
xit(&quot;無効化されたテスト&quot;) {
}

it(&quot;無効化されたテスト&quot;).config(enabled = false) {

}

```

## まとめ
Kotestでは、テストの実行タイミングに応じてさまざまなライフサイクル関数が用意されています。
どのタイミングでどの関数が呼ばれるのかを理解しておくことで、テストコードをより効率的かつ安全に構築することができます。
是非、before/afterを上手に活用して、テストの品質を向上させましょう！！</content:encoded><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>Kotlin Multiplatform Mobile（KMM）を触ってみた</title><link>https://taiseidev.com//posts/kotlin-multiplatform-mobile-intro/</link><guid isPermaLink="true">https://taiseidev.com//posts/kotlin-multiplatform-mobile-intro/</guid><description>iOSとAndroidでビジネスロジックを共通化できるKotlin Multiplatform Mobileの概要と、最初の一歩をまとめます。</description><pubDate>Sun, 03 Dec 2023 00:00:00 GMT</pubDate><content:encoded>## はじめに

「iOSとAndroid、どちらも同じロジックを書くのは面倒だ...」と感じたことがある方も多いはず。
Flutterのようなクロスプラットフォームは有力な選択肢ですが、UIはネイティブのまま、ロジックだけ共通化したい場合にマッチするのが[Kotlin Multiplatform Mobile (KMM)](https://kotlinlang.org/lp/multiplatform/)です。

本記事では、KMMの全体像と最初の一歩をまとめます。

## KMMとは

- Kotlinで書いたコードをiOS/Android両方で使えるようにする仕組み
- Android側はKotlinそのまま、iOS側はObjective-Cフレームワーク経由でSwiftから呼び出し
- UIは各プラットフォームでネイティブ実装

**UIまで共通化したい場合は[Compose Multiplatform](https://www.jetbrains.com/lp/compose-multiplatform/)の方が適しています。**
KMMは「ロジック層の共通化」に集中できる、というのが特徴です。

## 何が共通化できるのか

例えばこんなコードが共通モジュールに置けます。

- API通信（Ktor Clientが代表例）
- データベース（SQLDelightなど）
- ドメインロジック（Use Case等）
- Entity/DTOの定義

プラットフォーム固有のAPIは`expect/actual`という仕組みで吸収します。

## プロジェクト構成

典型的なKMMプロジェクトはこのような構成になります。

```
shared/
  commonMain/   # 共通コード
  androidMain/  # Android固有実装
  iosMain/      # iOS固有実装
androidApp/     # Android UI
iosApp/         # iOS UI（Xcodeプロジェクト）
```

`shared`モジュールが両プラットフォームで共有されるコア部分です。

## expect / actual の例

例えば時刻を取得する処理は、プラットフォームごとに実装を分けられます。

```kotlin
// commonMain
expect fun currentTimestamp(): Long

// androidMain
actual fun currentTimestamp(): Long = System.currentTimeMillis()

// iosMain
import platform.Foundation.NSDate
import platform.Foundation.timeIntervalSince1970

actual fun currentTimestamp(): Long =
    (NSDate().timeIntervalSince1970 * 1000).toLong()
```

共通コードからは`currentTimestamp()`を呼ぶだけ。
プラットフォーム実装の切り替えはコンパイラが面倒をみてくれます。

## Ktorで共通のAPIクライアントを書く

```kotlin
class UserApi(private val client: HttpClient) {
    suspend fun getUser(id: Long): UserDto =
        client.get(&quot;https://api.example.com/users/$id&quot;).body()
}
```

このコードが、AndroidでもiOSでも**そのまま動きます**。
これはシンプルな例ですが、実務でも大部分のロジックをこのように共通化できるのは非常にインパクトがあります。

## iOSでの利用

共通コードはXcodeからはObjective-Cのフレームワークとして見えるので、Swiftから呼ぶことができます。

```swift
let api = UserApi(client: ...)

Task {
    let user = try await api.getUser(id: 1)
    print(user.name)
}
```

Kotlinの`suspend`関数はSwiftから`async/await`で呼べるので、そこまで違和感はありません。

## 導入してみて感じたメリット/デメリット

### メリット
- ビジネスロジックの2重実装が不要になる
- テストも共通に書ける
- iOSのUIはSwiftUI/UIKitのまま、ユーザー体験を犠牲にしなくていい

### デメリット
- セットアップがやや重い（Xcode/Gradle/CocoaPodsの連携）
- Kotlin Nativeの挙動（メモリモデル等）を別途理解する必要がある
- iOS側のビルド時間が長くなりがち

## どんなプロジェクトに向くか

個人的には以下のような場面に強いと感じます。

- すでにAndroid/iOSのネイティブアプリがある
- 両アプリでビジネスロジックが重複してきている
- UIはネイティブのままキープしたい

逆に「一人で両OS対応をゼロから作る」場合はFlutterやCompose Multiplatformの方が圧倒的に楽かもしれません。

## おわりに

KMMは「UIはネイティブのまま、ロジックだけ共通化する」という現実的な選択肢を提供してくれます。
Jetpack系のKotlinライブラリの多くがマルチプラットフォーム対応を進めているので、今後さらに選択肢としての存在感が増していきそうです。

## 参考文献

- [Kotlin Multiplatform公式](https://kotlinlang.org/lp/multiplatform/)
- [KMMのドキュメント](https://kotlinlang.org/docs/multiplatform-mobile-getting-started.html)</content:encoded><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>AndroidのDI入門：Hiltの基本的な使い方</title><link>https://taiseidev.com//posts/android-hilt-basics/</link><guid isPermaLink="true">https://taiseidev.com//posts/android-hilt-basics/</guid><description>Android公式推奨のDIライブラリHiltの基本概念と、ViewModel/Repository構成での使い方をまとめます。</description><pubDate>Sat, 18 Nov 2023 00:00:00 GMT</pubDate><content:encoded>## はじめに

Android開発でDI（Dependency Injection）を導入したいとき、公式が推奨しているのが[Hilt](https://developer.android.com/training/dependency-injection/hilt-android)です。
HiltはDaggerをAndroid向けにシンプル化したライブラリで、セットアップがかなり楽になっています。

本記事では、HiltをAndroidアプリに導入するための基本的な流れをまとめます。

## 依存関係

```kotlin
// プロジェクトレベル build.gradle.kts
plugins {
    id(&quot;com.google.dagger.hilt.android&quot;) version &quot;2.48&quot; apply false
}

// アプリレベル build.gradle.kts
plugins {
    id(&quot;kotlin-kapt&quot;)
    id(&quot;com.google.dagger.hilt.android&quot;)
}

dependencies {
    implementation(&quot;com.google.dagger:hilt-android:2.48&quot;)
    kapt(&quot;com.google.dagger:hilt-android-compiler:2.48&quot;)
}
```

`kapt`ではなく`ksp`を使いたい場合は、Hiltがkspに対応次第乗り換えることができます。

## アプリケーションクラスの設定

Hiltを使うには、Applicationクラスに`@HiltAndroidApp`を付けます。

```kotlin
@HiltAndroidApp
class MyApp : Application()
```

`AndroidManifest.xml`にも登録を忘れずに。

```xml
&lt;application android:name=&quot;.MyApp&quot; ... /&gt;
```

## ActivityとFragmentで使う

```kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() { /* ... */ }
```

`@AndroidEntryPoint`を付けると、そのクラスでフィールドインジェクションやViewModelの注入ができるようになります。

## ViewModelへのインジェクション

```kotlin
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository,
) : ViewModel() { /* ... */ }
```

Activity/Fragmentからはおなじみの`by viewModels()`で取得できます。

```kotlin
class MainActivity : ComponentActivity() {
    private val viewModel: UserViewModel by viewModels()
}
```

## Moduleで依存を提供する

インターフェースの実装や外部ライブラリのインスタンスなど、`@Inject constructor`で解決できないものは`@Module`で提供します。

```kotlin
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit =
        Retrofit.Builder()
            .baseUrl(&quot;https://api.example.com&quot;)
            .addConverterFactory(MoshiConverterFactory.create())
            .build()

    @Provides
    @Singleton
    fun provideUserApi(retrofit: Retrofit): UserApi =
        retrofit.create(UserApi::class.java)
}
```

- `@InstallIn(SingletonComponent::class)`：アプリ全体でシングルトンとして共有
- `@Provides`：インスタンス生成方法を示す
- `@Singleton`：スコープ内で単一インスタンス

## Interfaceの実装を束ねる

Repositoryなど、インターフェースを実装するクラスがある場合は`@Binds`を使います。

```kotlin
interface UserRepository {
    suspend fun fetch(id: Long): User
}

class UserRepositoryImpl @Inject constructor(
    private val api: UserApi,
) : UserRepository {
    override suspend fun fetch(id: Long): User = api.getUser(id)
}

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    @Binds
    @Singleton
    abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
}
```

`@Binds`は`@Provides`より最適化が効くので、単にインターフェースをバインドするだけなら`@Binds`を選びます。

## Scopeの使い分け

Hiltで代表的なスコープは以下。

| Scope | ライフサイクル |
|---|---|
| `@Singleton` | Application全体 |
| `@ActivityRetainedScoped` | 画面回転を跨いで保持 |
| `@ActivityScoped` | Activityごと |
| `@ViewModelScoped` | ViewModelごと |
| `@FragmentScoped` | Fragmentごと |

Repositoryは基本`@Singleton`、Use Caseは`@ViewModelScoped`、のように使い分けるとシンプルです。

## Composeへの注入

JetpackComposeでは`hiltViewModel()`が用意されています。

```kotlin
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    // UI
}
```

## よくあるハマりどころ

- **`@HiltAndroidApp`の付け忘れ**：AppクラスでのDIが動かない
- **AndroidManifestへの登録忘れ**：同上
- **`@AndroidEntryPoint`を付け忘れる**：ViewModelのinjectができない
- **Moduleの`@InstallIn`が間違い**：スコープ不一致エラー

Hiltはエラーメッセージがかなり親切なので、コンパイルエラーをよく読むと解決できるケースがほとんどです。

## おわりに

Hiltはセットアップさえ乗り越えれば、DaggerほどのボイラープレートがなくDIを導入できます。
Androidでテスタブルな設計にしたい場合、個人的には真っ先に入れるライブラリのひとつです。

## 参考文献

- [Dependency Injection with Hilt](https://developer.android.com/training/dependency-injection/hilt-android)
- [Dagger Hilt公式](https://dagger.dev/hilt/)</content:encoded><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>Kotlin CoroutinesとFlowの違いを整理する</title><link>https://taiseidev.com//posts/kotlin-coroutines-flow/</link><guid isPermaLink="true">https://taiseidev.com//posts/kotlin-coroutines-flow/</guid><description>Kotlinの非同期処理でよく出てくるCoroutinesとFlowの違い・使い分けを、Androidでの利用シーンを交えて整理します。</description><pubDate>Wed, 11 Oct 2023 00:00:00 GMT</pubDate><content:encoded>## はじめに

Kotlinの非同期処理と言えば、CoroutinesとFlowが定番です。
ただ、この2つはどちらも`suspend`や`CoroutineScope`が絡むので、初めて触ると「結局どう使い分ければいいんだ？」と迷ってしまいがち。

本記事では、両者の立ち位置と使い分けをコンパクトに整理します。

## 一言でいうと

- **Coroutines**：非同期処理を&quot;同期っぽく&quot;書くための仕組み（単発の値）
- **Flow**：非同期なストリームを扱うための仕組み（複数の値）

RxJavaを知っている方なら、FlowはほぼRxのObservableの立ち位置と捉えると分かりやすいです。

## Coroutines（単発の値）

ネットワーク通信のように、「1回呼んで1回結果が返ってくる」処理はCoroutinesが向いています。

```kotlin
suspend fun fetchUser(id: Long): User {
    return api.getUser(id)
}

viewModelScope.launch {
    val user = fetchUser(1)
    _state.value = UiState.Success(user)
}
```

- `suspend`：一時停止可能な関数
- `launch`：新たにCoroutineを起動する
- `viewModelScope`：ViewModelのライフサイクルに紐付いたScope

## Flow（連続する値）

DB更新通知や位置情報の変化のように「連続して値が流れてくる」処理はFlowが向いています。

```kotlin
val usersFlow: Flow&lt;List&lt;User&gt;&gt; = userDao.observeAll()

viewModelScope.launch {
    usersFlow.collect { users -&gt;
        _state.value = UiState.Success(users)
    }
}
```

`collect`でFlowから値を受け取り続けます。
Roomやデータストアの多くがFlowを返すようになっているので、Android開発ではFlowに触れる機会が非常に多いです。

## StateFlow と SharedFlow

Android UI層でよく使われるのがこの2つ。

### StateFlow

UIの状態を持つのに向いた、**現在値を必ず保持する**Flow。

```kotlin
private val _counter = MutableStateFlow(0)
val counter: StateFlow&lt;Int&gt; = _counter.asStateFlow()

fun increment() {
    _counter.value += 1
}
```

Composeの`collectAsStateWithLifecycle()`と組み合わせると、状態駆動のUIが簡単に書けます。

### SharedFlow

エラー通知や一度きりのイベントなど、**現在値を持たない**ストリームに向いています。

```kotlin
private val _events = MutableSharedFlow&lt;ToastEvent&gt;()
val events: SharedFlow&lt;ToastEvent&gt; = _events

suspend fun showError() {
    _events.emit(ToastEvent(&quot;エラーが発生しました&quot;))
}
```

## 使い分けの目安

| シーン | 使うべきもの |
|---|---|
| 1回だけ値を取得したい | suspend関数 |
| UIの現在状態を表現 | StateFlow |
| 一度きりのイベント | SharedFlow |
| Roomや位置情報の変化を監視 | Flow |

## Coroutine Scopeに注意

Coroutineは必ず「いつ終わるか」を決めたScopeの中で起動します。
Androidでは以下が基本。

| Scope | 推奨用途 |
|---|---|
| `viewModelScope` | ViewModel内の処理 |
| `lifecycleScope` | ActivityやFragment内 |
| `GlobalScope` | 原則使わない |

`GlobalScope`はアプリのライフサイクルと切り離されてしまうので、意図せずメモリリークやクラッシュの原因になりがち。
ライブラリ側で必要になった時以外は避けるのが無難です。

## Flow演算子の例

Flowには豊富な演算子があります。

```kotlin
usersFlow
    .map { users -&gt; users.filter { it.isActive } }
    .distinctUntilChanged()
    .debounce(300)
    .flowOn(Dispatchers.Default) // ここまでをDefault Dispatcherで動かす
    .collect { /* UI更新 */ }
```

- `map`：要素を変換
- `distinctUntilChanged`：同じ値が連続した場合はスキップ
- `debounce`：一定時間経過してから流す
- `flowOn`：上流の実行Dispatcherを切り替える

RxJavaを使っていた方なら、ほぼ馴染みのある概念で書けます。

## おわりに

CoroutinesとFlowは、

- **1回の非同期 → Coroutines**
- **連続した非同期 → Flow**

というイメージで使い分ければ、ほとんどのケースは迷わないと思います。
Android開発ではFlow/StateFlowを触る機会が本当に多いので、基本的な演算子だけでも抑えておくとコーディングが速くなります。

## 参考文献

- [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html)
- [Kotlin Flow](https://kotlinlang.org/docs/flow.html)
- [Android公式: Kotlin Flows](https://developer.android.com/kotlin/flow)</content:encoded><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>AndroidのRoomで始めるローカル永続化</title><link>https://taiseidev.com//posts/android-room-basics/</link><guid isPermaLink="true">https://taiseidev.com//posts/android-room-basics/</guid><description>AndroidのRoomライブラリを使ってSQLiteをラップし、型安全にローカル永続化を行う方法を紹介します。</description><pubDate>Sat, 23 Sep 2023 00:00:00 GMT</pubDate><content:encoded>## はじめに

Androidでローカルにデータを永続化したいとき、直接SQLiteを叩くとボイラープレートが多すぎて辛いですよね。
そこで活躍するのがJetpackの一部として提供されている[Room](https://developer.android.com/training/data-storage/room)です。

Roomは「SQLiteの薄いラッパー」ではなく、アノテーションベースで型安全なDBアクセスを提供してくれるライブラリ。
コンパイル時にクエリの整合性まで検査してくれるので、かなりミスが減ります。

本記事では、Roomの基本的な使い方をまとめます。

## 依存関係

```kotlin
dependencies {
    val roomVersion = &quot;2.5.2&quot;
    implementation(&quot;androidx.room:room-runtime:$roomVersion&quot;)
    implementation(&quot;androidx.room:room-ktx:$roomVersion&quot;)
    ksp(&quot;androidx.room:room-compiler:$roomVersion&quot;)
}
```

kaptよりも高速な`ksp`が主流になってきているので、新規プロジェクトは`ksp`でOKです。

## Roomの3つの構成要素

Roomは以下の3つの役割でDBを構成します。

| 要素 | 役割 |
|---|---|
| Entity | テーブルとなるデータクラス |
| DAO | SQLを発行するインターフェース |
| Database | Entity/DAOをまとめるDB本体 |

## Entityの定義

```kotlin
@Entity(tableName = &quot;users&quot;)
data class UserEntity(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val name: String,
    val email: String,
)
```

- `@Entity`でテーブルを表す
- `@PrimaryKey`で主キーを指定
- `autoGenerate = true`にしておくとIDを自動採番

## DAOの定義

DAO (Data Access Object) はCRUD操作を定義するインターフェースです。

```kotlin
@Dao
interface UserDao {

    @Query(&quot;SELECT * FROM users ORDER BY id DESC&quot;)
    fun observeAll(): Flow&lt;List&lt;UserEntity&gt;&gt;

    @Query(&quot;SELECT * FROM users WHERE id = :id&quot;)
    suspend fun findById(id: Long): UserEntity?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsert(user: UserEntity)

    @Delete
    suspend fun delete(user: UserEntity)
}
```

Flowを戻り値にしておくと、DBの変更をリアルタイムに監視できます。
Composeと組み合わせた時の体験が良いです。

## Databaseクラス

```kotlin
@Database(
    entities = [UserEntity::class],
    version = 1,
    exportSchema = true,
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}
```

## シングルトンとして提供する

```kotlin
object DatabaseProvider {
    @Volatile private var instance: AppDatabase? = null

    fun get(context: Context): AppDatabase {
        return instance ?: synchronized(this) {
            instance ?: Room.databaseBuilder(
                context.applicationContext,
                AppDatabase::class.java,
                &quot;app.db&quot;,
            ).build().also { instance = it }
        }
    }
}
```

実務ではHiltでDIしてしまう方が多いですが、まずはシングルトンで動かしてみるとイメージがつきやすいです。

## 実際に使ってみる

```kotlin
class UserRepository(private val dao: UserDao) {
    fun observeUsers(): Flow&lt;List&lt;UserEntity&gt;&gt; = dao.observeAll()

    suspend fun addUser(name: String, email: String) {
        dao.upsert(UserEntity(name = name, email = email))
    }
}
```

ViewModelからはこうです。

```kotlin
class UserViewModel(
    private val repository: UserRepository,
) : ViewModel() {

    val users: StateFlow&lt;List&lt;UserEntity&gt;&gt; =
        repository.observeUsers().stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList(),
        )

    fun add(name: String, email: String) {
        viewModelScope.launch {
            repository.addUser(name, email)
        }
    }
}
```

## マイグレーション

テーブルに列を追加したい、といった変更にはマイグレーションが必要です。

```kotlin
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL(&quot;ALTER TABLE users ADD COLUMN age INTEGER NOT NULL DEFAULT 0&quot;)
    }
}

Room.databaseBuilder(context, AppDatabase::class.java, &quot;app.db&quot;)
    .addMigrations(MIGRATION_1_2)
    .build()
```

破壊的で良いなら`fallbackToDestructiveMigration()`で全データを消してマイグレーション...という手もありますが、本番では避けましょう。

## Type Converter

`Date`や`Enum`をそのままEntityに持たせたい場合は、Type Converterを使います。

```kotlin
class Converters {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? = date?.time
}

@Database(
    entities = [UserEntity::class],
    version = 2,
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { /* ... */ }
```

## おわりに

Roomは一度セットアップしてしまえば、型安全・Flow対応・マイグレーションと至れり尽くせりのDBライブラリです。
個人開発でもちょっとしたキャッシュ層として使えるので、SQLiteを直接叩くくらいならぜひRoomを試してみてください！

## 参考文献

- [Room persistence library](https://developer.android.com/training/data-storage/room)
- [Room with Kotlin Flow](https://developer.android.com/kotlin/flow)</content:encoded><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>Jetpack Composeで始めるAndroid UI開発</title><link>https://taiseidev.com//posts/jetpack-compose-intro/</link><guid isPermaLink="true">https://taiseidev.com//posts/jetpack-compose-intro/</guid><description>宣言的UIツールキットJetpack Composeの基本概念と、シンプルな画面を作るところまでを紹介します。</description><pubDate>Tue, 15 Aug 2023 00:00:00 GMT</pubDate><content:encoded>## はじめに

AndroidのUI開発といえば、長らくXMLレイアウト + Activity/Fragmentという構成が一般的でした。
そこに登場したのが[Jetpack Compose](https://developer.android.com/jetpack/compose)。
宣言的UIに慣れ親しんだ身からすると、「やっとAndroidにも来たか...！」という気持ちになるツールキットです。

本記事では、Jetpack Composeをこれから触る人向けに、基本概念とシンプルな画面を作る手順をまとめます。

## Composeとは

ざっくり言うと、***宣言的にUIを構築するためのKotlin DSL***です。
React/Flutterと近い発想で、「状態から画面を導出する」という考え方をします。

- XMLレイアウトが不要になる
- `@Composable`関数を組み合わせてUIを作る
- 状態が変わると、再描画が自動で起きる

## プロジェクトのセットアップ

Android Studio（Flamingo以降推奨）で「Empty Compose Activity」を選ぶのがいちばん手軽です。
手動で設定する場合は`build.gradle.kts`に以下を追加。

```kotlin
android {
    buildFeatures {
        compose = true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = &quot;1.5.3&quot;
    }
}

dependencies {
    implementation(&quot;androidx.compose.ui:ui:1.5.4&quot;)
    implementation(&quot;androidx.compose.material3:material3:1.1.2&quot;)
    implementation(&quot;androidx.activity:activity-compose:1.8.0&quot;)
}
```

## 最初のComposable

```kotlin
@Composable
fun Greeting(name: String) {
    Text(text = &quot;Hello, $name!&quot;)
}
```

`@Composable`アノテーションがついた関数は、Composeの描画対象になります。
Activityからはこう呼び出します。

```kotlin
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Greeting(&quot;World&quot;)
            }
        }
    }
}
```

## レイアウト基本：Column / Row / Box

FlutterでいうColumn/Row/Stackにあたるのがこれら。

```kotlin
@Composable
fun Profile() {
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
    ) {
        Text(&quot;Taisei&quot;, style = MaterialTheme.typography.titleLarge)
        Text(&quot;Mobile Engineer&quot;)
        Row {
            Button(onClick = {}) { Text(&quot;Follow&quot;) }
            Spacer(Modifier.width(8.dp))
            OutlinedButton(onClick = {}) { Text(&quot;Message&quot;) }
        }
    }
}
```

`Modifier`を使うと、パディング、背景色、クリック可能化などを**関数チェーン**で適用できます。

## 状態管理：`remember`と`mutableStateOf`

状態を持たせたいときは`remember`と`mutableStateOf`の組み合わせが基本です。

```kotlin
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Column {
        Text(&quot;Count: $count&quot;)
        Button(onClick = { count++ }) {
            Text(&quot;Increment&quot;)
        }
    }
}
```

- `mutableStateOf`：値の変更を検知できる`State`を作る
- `remember`：再コンポーズされても値を保持する
- `by`：delegated propertyで`count`を直接扱えるようにする

画面回転などの構成変更でも残したい場合は`rememberSaveable`を使います。

## プレビュー機能

Composeの強みは、AndroidStudioで**即座にプレビューできる**こと。

```kotlin
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    MaterialTheme {
        Greeting(&quot;Preview&quot;)
    }
}
```

実機やエミュレータを起動せずに、UI調整が進められるので体験が一段階よくなります。

## Material 3への対応

Android 12以降のDynamic Colorにも、Material 3コンポーネントを使えば比較的簡単に対応できます。

```kotlin
@Composable
fun MyAppTheme(content: @Composable () -&gt; Unit) {
    val context = LocalContext.current
    val colors = if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.S) {
        dynamicLightColorScheme(context)
    } else {
        lightColorScheme()
    }
    MaterialTheme(colorScheme = colors, content = content)
}
```

## おわりに

XMLの山からKotlin DSLへの移行は最初は戸惑いますが、慣れると圧倒的に書くのが楽しくなります。
既存プロジェクトへ段階的に取り入れる場合は`ComposeView`をXMLに埋め込めるので、新規画面から少しずつ移植するのがおすすめです。

## 参考文献

- [Jetpack Compose公式](https://developer.android.com/jetpack/compose)
- [Compose Pathway](https://developer.android.com/courses/pathways/compose)</content:encoded><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>FlutterにFirebase Authenticationを導入する</title><link>https://taiseidev.com//posts/flutter-firebase-auth/</link><guid isPermaLink="true">https://taiseidev.com//posts/flutter-firebase-auth/</guid><description>FlutterアプリでFirebase Authenticationを使ってメール・Google・匿名ログインを実装する手順をまとめます。</description><pubDate>Sun, 02 Jul 2023 00:00:00 GMT</pubDate><content:encoded>## はじめに

個人開発でもSaaSでも、ユーザー認証は欠かせない機能のひとつ。
自前で実装するとセキュリティやインフラ面が重くなりがちなので、Flutterでは[Firebase Authentication](https://firebase.google.com/docs/auth)に頼るのが手軽で安全です。

本記事では、FlutterでFirebase Authを使うときの基本的な手順をまとめていきます。

## セットアップ

### 1. Firebaseプロジェクトの作成

[Firebaseコンソール](https://console.firebase.google.com/)で新規プロジェクトを作成します。
その後、Authentication &gt; Sign-in methodでメール/パスワードなど使いたい方法を有効化しておきます。

### 2. FlutterFire CLIで初期化

従来は`GoogleService-Info.plist`や`google-services.json`を手動で配置する必要がありましたが、今は[flutterfire_cli](https://firebase.flutter.dev/docs/cli)を使うのが圧倒的に楽です。

```bash
dart pub global activate flutterfire_cli
flutterfire configure
```

`lib/firebase_options.dart`が自動生成されます。

### 3. パッケージの追加

```yaml
dependencies:
  firebase_core: ^2.13.0
  firebase_auth: ^4.6.0
  google_sign_in: ^6.1.0 # Googleログインする場合
```

### 4. 初期化

`main.dart`でFirebaseを初期化します。

```dart
import &apos;firebase_options.dart&apos;;

Future&lt;void&gt; main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}
```

## メール/パスワード認証

### サインアップ

```dart
Future&lt;void&gt; signUp(String email, String password) async {
  try {
    await FirebaseAuth.instance.createUserWithEmailAndPassword(
      email: email,
      password: password,
    );
  } on FirebaseAuthException catch (e) {
    if (e.code == &apos;weak-password&apos;) {
      print(&apos;パスワードが脆弱です&apos;);
    } else if (e.code == &apos;email-already-in-use&apos;) {
      print(&apos;そのメールアドレスは既に使われています&apos;);
    }
  }
}
```

### サインイン

```dart
Future&lt;void&gt; signIn(String email, String password) async {
  await FirebaseAuth.instance.signInWithEmailAndPassword(
    email: email,
    password: password,
  );
}
```

### サインアウト

```dart
await FirebaseAuth.instance.signOut();
```

## Googleログイン

```dart
Future&lt;UserCredential&gt; signInWithGoogle() async {
  final googleUser = await GoogleSignIn().signIn();
  final googleAuth = await googleUser?.authentication;

  final credential = GoogleAuthProvider.credential(
    accessToken: googleAuth?.accessToken,
    idToken: googleAuth?.idToken,
  );

  return FirebaseAuth.instance.signInWithCredential(credential);
}
```

iOSの場合は`Info.plist`にReversed Client IDの設定が必要です。詳しくは公式ドキュメントを参照してください。

## 匿名ログイン

試用ユーザー用に匿名ログインを提供しておくと便利です。

```dart
await FirebaseAuth.instance.signInAnonymously();
```

後からメールアドレスを登録してもらえば、同じアカウントを引き継げます。

```dart
final credential = EmailAuthProvider.credential(
  email: email,
  password: password,
);
await FirebaseAuth.instance.currentUser?.linkWithCredential(credential);
```

## 認証状態の監視

リアルタイムに認証状態を監視するには`authStateChanges`が便利です。

```dart
StreamBuilder&lt;User?&gt;(
  stream: FirebaseAuth.instance.authStateChanges(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const CircularProgressIndicator();
    }
    if (snapshot.hasData) {
      return const HomePage();
    }
    return const LoginPage();
  },
);
```

ログイン/ログアウトに応じて画面が自動で切り替わります。

## よくあるつまずきポイント

- **iOSで真っ白になる**：`Info.plist`のURL Types設定漏れ
- **Androidでクラッシュ**：SHA-1フィンガープリントの登録忘れ
- **Googleログイン後にUserがnull**：`signInWithCredential`の戻り値を`await`していない

エラーが出たら一度`flutterfire configure`を再実行するだけで解決するケースも多いです。

## おわりに

Firebase AuthはFlutterと非常に相性が良く、個人開発レベルなら数十分で認証フローが組めます。
プロダクションで使うときは、**メール認証必須**や**reCAPTCHAの有効化**など、セキュリティ設定の見直しも忘れずに。

## 参考文献

- [FlutterFire公式ドキュメント](https://firebase.flutter.dev/docs/overview)
- [Firebase Auth](https://firebase.google.com/docs/auth/flutter/start)</content:encoded><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>Riverpodを学んで初学者の壁をぶち破る</title><link>https://taiseidev.com//posts/riverpod-beginner-guide/</link><guid isPermaLink="true">https://taiseidev.com//posts/riverpod-beginner-guide/</guid><description>Riverpodの基本的な使い方を学び、Flutterでの状態管理の理解を深める。サンプルアプリを作成し、Riverpod 2.0の新機能も紹介。</description><pubDate>Wed, 07 Jun 2023 00:00:00 GMT</pubDate><content:encoded>## はじめに

2022年12月1日、ついにRiverpodがFlutter公式の動画で紹介されました🚀

[X - post by @FlutterDev](https://twitter.com/FlutterDev/status/1598074394181599262?s=20&amp;t=xzavpqI2zE0U4vT-i8w7vA)

Flutter公式の「***List of state management approaches***」でもRiverpodが紹介されています！
これまではRiverpodの前身のProviderパッケージがFlutter公式推奨として紹介されていましたが、ついにRiverpodも公式推奨となりました✨

[riverpod](https://docs.flutter.dev/development/data-and-backend/state-mgmt/options#riverpod)

日本では既に様々なプロジェクトでRiverpodが採用されていると思いますが、
Flutter公式が推奨することによりさらにRiverpodの人気が世界中で高まっていく事が予想されます🚀

今回は公式推奨になった記念にRiverpodを学んで初学者を脱しようという内容です。
私自身もまだ十分に理解できていないなと感じるので、誤っている部分がありましたらコメントをいただけますと幸いです🙏

## 記事の目的と対象者

この記事を読むことによってRiverpodの基本的な使い方を習得する事を目的にしたいと思います。サンプルアプリを一緒に作りながらRiverpodを学んでいく形です。そのため、この記事では下記の方を対象にしています。

・これからRiverpodを学ぶ方
・Riverpodの使い方に不安を感じている方（私です）

（基礎的な使い方を押さえている方には少々物足りないかもしれません）

また、記事の後半では2022年9月にリリースされたRiverpod2.0についても解説していきたいと思います。
Riverpod2.0をキャッチアップはまだだよ〜って方にも読んでいただけると嬉しいです！

今回作成したサンプルアプリは下記から確認する事が出来ます。

[riverpod-sample](https://github.com/taiseidev/riverpod-sample)

では、解説していきます🚀

## Riverpodの概要

Riverpodとは、状態管理パッケージとして主流だったProviderパッケージを進化させる形で開発された、リアクティブなキャッシュとデータバインディングの状態管理パッケージです。

&gt; **本記事の注意点：**
&gt; 「Provider」という文言がややこしいので、Riverpodの前身を「***Providerパッケージ***」とし、RiverpodとProviderパッケージで使われるProviderを「***Provider***とします。

では、Riverpodで何が進化したかを学ぶためにも、Providerパッケージの主な欠点を確認していきましょう！

ProviderパッケージはInheritedWidgetを改良する形で開発されたパッケージでWidgetツリーに依存します。

&lt;img src=&apos;https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2466417/31dce8f1-7ab4-ae32-5812-20d14eda2560.png&apos; width=&apos;300&apos;&gt;

(画像は[Flutter Riverpod 2.0: The Ultimate Guide](https://codewithandrea.com/articles/flutter-state-management-riverpod/)からお借りしました)

画像のように、親のWidgetツリーを見て登録されているProviderにアクセスする事が出来ます。裏を返すと親のWidgetツリーには使用したいProviderが登録されている必要があるため、もしProviderが登録がされていない場合は***ProviderNotFoundException***エラーが発生してしまいます。

&gt; 実際にサンプルアプリでも***ProviderNotFoundException***を発生させるサンプルを作成してみました。
確認したい方は***provider_package***ディレクトリ***main()***からアプリを起動させてみてください！

一方でRiverpodは、ProviderをWidgetツリーから切り離してグローバルに定義する事が出来るため、定義したProviderに確実にアクセスする事が出来ます🚀

&lt;img src=&apos;https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2466417/c90674be-a885-569b-d20c-4b1e46d1530f.png&apos; width=&apos;300&apos;&gt;

(画像は[flutter-study.dev](https://www.flutter-study.dev/firebase-app/riverpod)からお借りしました)

そのほかにも、Providerパッケージでは同じ型のものが複数同時に使用できない（Widgetツリー直近で指定された型が取得される）のに対して、Riverpodでは同じ型のProviderを複数参照できるなどProviderパッケージの欠点を補ってくれます。

そのほかにもRiverpodのメリットは沢山ありますが、全て書いてると長くなりそうなので下記の記事をご覧ください🙇‍♂️

[Flutterの状態管理手法の選定](https://medium.com/flutter-jp/state-1daa7fd66b94)

## 実際に使ってみよう

では実際にRiverpodを学んでいきましょう🔥

### 1. Riverpodをインストール

Riverpodは複数のパッケージがあり、それぞれ用途が異なります。

|アプリの形態  |パッケージ名  |説明  |
|---|---|---|
|Flutterのみ  |[flutter_riverpod](https://pub.dev/packages/flutter_riverpod)  |FlutterアプリでRiverpodを使用する場合の基本パッケージ  |
|Flutter + [flutter_hooks](https://github.com/rrousselGit/flutter_hooks)  |[hooks_riverpod](https://pub.dev/packages/hooks_riverpod)  |flutter_hooksとRiverpodを併用する場合のパッケージ  |
|Dartのみ（Flutterを使用しない）  |[riverpod](https://github.com/rrousselGit/riverpod/tree/master/packages/riverpod)  |Flutter関連のクラスを全て除いたRiverpodパッケージ  |

今回はFlutterで基本的なRiverpodの使い方を解説するだけなのでhooks_riverpodやriverpodは解説しません。flutter_riverpodのみを使用します。

pubspec.yamlに下記を追加してインストールします。

```yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.1.1 // 追加
```

次に***ProviderScope***でアプリ全体をラップします

```dart
void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}
```

ProviderScopeは作成したすべてのProviderの状態を保存してくれるWidgetです。

以上でRiverpodを使う準備が整いました🚀

### 2. サンプルアプリを作ろう

今回作るアプリはQiitaのAPIを使ってタグで投稿を検索アプリを作成します。

[GET /api/v2/tags/:tag_id/items](https://qiita.com/api/v2/docs#get-apiv2tagstag_iditems)

アーキテクチャ（ディレクトリ構成）は下記を参考にさせていただいています。

[flutter-architecture-blueprints](https://github.com/wasabeef/flutter-architecture-blueprints)

**今回作るアプリ**

&lt;img src=&apos;https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2466417/d90fc4d3-78ec-8b2f-0a24-7552a8d582ac.gif&apos; width=&apos;300&apos;&gt;

では作っていきます。

### ①データクラスを作成

本旨ではないのでパパッと解説していきます。
API通信を行い、Jsonで返却されるデータをアプリで使える形に変換してあげる必要があります。下記のパッケージを用いてデータクラスを作成します。

```yaml
dependencies:
  flutter:
    sdk: flutter
  freezed: ^2.3.0
  freezed_annotation: ^2.2.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.3.2
  json_serializable: ^6.5.4
```

作成したデータクラスは下記の通りです。

&lt;details&gt;
&lt;summary&gt;qiita_post.dart&lt;/summary&gt;

```dart
import &apos;package:freezed_annotation/freezed_annotation.dart&apos;;
import &apos;package:riverpod_sample/riverpod/data/models/tag.dart&apos;;
import &apos;package:riverpod_sample/riverpod/data/models/user.dart&apos;;

part &apos;qiita_post.freezed.dart&apos;;
part &apos;qiita_post.g.dart&apos;;

@freezed
abstract class QiitaPost with _$QiitaPost {
  factory QiitaPost({
    String? title,
    @JsonKey(name: &apos;likes_count&apos;) int? likesCount,
    @JsonKey(name: &apos;stocks_count&apos;) int? stocksCount,
    User? user,
    String? url,
    List&lt;Tag&gt;? tags,
  }) = _QiitaPost;

  factory QiitaPost.fromJson(Map&lt;String, dynamic&gt; json) =&gt;
      _$QiitaPostFromJson(json);
}
```

&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;tag.dart&lt;/summary&gt;

```dart
import &apos;package:freezed_annotation/freezed_annotation.dart&apos;;

part &apos;tag.freezed.dart&apos;;
part &apos;tag.g.dart&apos;;

@freezed
abstract class Tag with _$Tag {
  factory Tag({
    String? name,
  }) = _Tag;

  factory Tag.fromJson(Map&lt;String, dynamic&gt; json) =&gt; _$TagFromJson(json);
}
```

&lt;/details&gt;

&lt;details&gt;
&lt;summary&gt;user.dart&lt;/summary&gt;

```dart
import &apos;package:freezed_annotation/freezed_annotation.dart&apos;;

part &apos;user.freezed.dart&apos;;
part &apos;user.g.dart&apos;;

@freezed
abstract class User with _$User {
  factory User({
    @JsonKey(name: &apos;profile_image_url&apos;) String? profileImageUrl,
  }) = _User;

  factory User.fromJson(Map&lt;String, dynamic&gt; json) =&gt; _$UserFromJson(json);
}

```

&lt;/details&gt;

freezedを使ったデータクラスの作成については下記が参考になります。

[Flutter freezed のチートシート、もとい、知っている人向けのメモ](https://zenn.dev/sakusin/articles/b19e9a2c3829e0)

### ②APIクライアントの実装

今回API通信はretrofitを使います。下記パッケージをインストールしてください。

```yaml
dependencies:
  flutter:
    sdk: flutter
  retrofit: ^3.3.1
  dio: ^4.0.6

dev_dependencies:
  flutter_test:
    sdk: flutter
  retrofit_generator: ^4.2.0
```

パッケージをインストールしたらAPI通信を行う抽象クラスを作成します。

```dart
import &apos;package:flutter_riverpod/flutter_riverpod.dart&apos;;
import &apos;package:retrofit/retrofit.dart&apos;;
import &apos;package:dio/dio.dart&apos;;
import &apos;package:riverpod_sample/riverpod/data/models/qiita_post.dart&apos;;
import &apos;package:riverpod_sample/riverpod/data/remote/app_dio.dart&apos;;

part &apos;posts_data_source.g.dart&apos;;

final postsDataSourceProvider = Provider&lt;PostsDataSource&gt;((ref) {
  return PostsDataSource(
    ref.watch(dioProvider),
  );
});

@RestApi(baseUrl: &quot;https://qiita.com/api/v2&quot;)
abstract class PostsDataSource implements IPostsDataSource {
  factory PostsDataSource(Dio dio, {String baseUrl}) = _PostsDataSource;

  @override
  @GET(&quot;/tags/{tag}/items&quot;)
  Future&lt;List&lt;QiitaPost&gt;&gt; getQiitaPosts(
    @Path(&quot;tag&quot;) String tag,
    @Query(&quot;per_page&quot;) int perPage,
  );
}

```

retrofitはメソッド（@GET）、エンドポイント、パスやクエリを定義するだけでAPIクライアントの実体を生成してくれる便利なパッケージです。***IPostsDataSource***を継承している部分は後ほど説明します。
抽象クラスの作成が終わったらターミナルで下記コマンドを実行

```terminal
flutter pub run build_runner watch --delete-conflicting-outputs
```

***posts_data_source.g.dart***ファイルが自動で生成されます。

ここでやっとRiverpodのProviderが出てきたので解説します。

```dart
final postsDataSourceProvider = Provider&lt;PostsDataSource&gt;((ref) {
  return PostsDataSource(
    ref.read(dioProvider),
  );
});
```

ここでは***Provider***を使ってPostsDataSourceのインスタンスを公開しています。***Provider***は変更できない値を公開できるProvider群の一つで、今回のようにAPIクライアントやRepositoryクラスを公開する時などに役立ちます。

また、PostsDataSourceの引数にDioのインスタンスを返却するdioProviderを渡しています。こういったDioのインスタンスのように複数インスタンスを作る必要がないものをProviderで公開することによって使い回しやすくなります。こういった点もRiverpodのメリットかなと思います。

dioProviderではHTTP通信を行った際にログを出力するコードを追加しています。

```dart
import &apos;package:dio/dio.dart&apos;;
import &apos;package:flutter_riverpod/flutter_riverpod.dart&apos;;

final dioProvider = Provider&lt;Dio&gt;((_) {
  final dio = Dio();
  dio.interceptors.add(LogInterceptor()); // ←を追加することによってコンソールにログが出力されます。
  return dio;
});
```

Providerについてのもっと詳しく知りたい方は公式ドキュメントを参照ください。

[Provider](https://riverpod.dev/ja/docs/providers/provider)

### ③Repositoryを作成

次にDataSourceにアクセスするためのRepositoryを作成します。

```dart
import &apos;package:flutter_riverpod/flutter_riverpod.dart&apos;;
import &apos;package:riverpod_sample/riverpod/data/i_posts_data_source.dart&apos;;
import &apos;package:riverpod_sample/riverpod/data/models/qiita_post.dart&apos;;
import &apos;package:riverpod_sample/riverpod/data/models/result.dart&apos;;
import &apos;package:riverpod_sample/riverpod/data/remote/posts_data_source.dart&apos;;

final postsRepositoryProvider =
    Provider((ref) =&gt; PostsRepository(ref.read(dataSourceProvider)));

final dataSourceProvider =
    Provider&lt;IPostsDataSource&gt;((ref) =&gt; throw UnimplementedError());

class PostsRepository {
  PostsRepository(this._dataSource);

  final IPostsDataSource _dataSource;

  static const defaultPostCount = 50;

  Future&lt;Result&lt;List&lt;QiitaPost&gt;&gt;&gt; getQiitaPosts(
    String tag,
    int defaultPostCount,
  ) {
    return _dataSource
        .getQiitaPosts(tag, defaultPostCount)
        .then((articles) =&gt; Result&lt;List&lt;QiitaPost&gt;&gt;.success(articles))
        .catchError((error) =&gt; Result&lt;List&lt;QiitaPost&gt;&gt;.failure(error));
  }
}

```

ここではRiverpodのDI機能を活用してDataSourceの差し替えを行なっています。
あらかじめダミーデータを取得するためのStubPostsDataSourceを作成。

```dart
import &apos;dart:convert&apos;;

import &apos;package:flutter/services.dart&apos;;
import &apos;package:flutter_riverpod/flutter_riverpod.dart&apos;;
import &apos;package:riverpod_sample/riverpod/data/i_posts_data_source.dart&apos;;
import &apos;package:riverpod_sample/riverpod/data/models/qiita_post.dart&apos;;

final stubPostsDataSourceProvider = Provider&lt;StubPostsDataSource&gt;((ref) {
  return StubPostsDataSource();
});

class StubPostsDataSource implements IPostsDataSource {
  // dammy_data.jsonにダミーデータが入っているのでそれを非同期で取得
  @override
  Future&lt;List&lt;QiitaPost&gt;&gt; getQiitaPosts(String tag, int perPage) async {
    final content =
        json.decode(await rootBundle.loadString(&apos;assets/stub/dammy_data.json&apos;))
            as Iterable;
    return content.map((e) =&gt; QiitaPost.fromJson(e)).toList();
  }
}

```

API通信を行う***PostsDataSource***とローカルのダミーデータを取得する***StubPostsDataSource***は、抽象クラスである***IPostsDataSource***を継承しているのでどちらもコンストラクタで渡す事が可能です。

```dart
import &apos;package:riverpod_sample/riverpod/data/models/qiita_post.dart&apos;;

abstract class IPostsDataSource {
  Future&lt;List&lt;QiitaPost&gt;&gt; getQiitaPosts(String tag, int perPage);
}
```

&gt; Dartでは「暗黙的インターフェース」を活用することによって、明示的にインターフェースを定義しなくても別クラスが別クラスをインターフェイスとして実装することが可能です。
今回の場合ですと、***i_posts_data_source.dart***を削除して、***StubPostsDataSource***が実装しているIPostsDataSourceをAPIクライアントの ***PostsDataSource***に換えてあげれば完了です🙆‍♂️
インターフェースを定義する必要がなくなるので、クラスを差し替えるだけであれば「暗黙的インターフェース」をうまく活用した方が良さそうですね。

[Implicit interfaces](https://dart.dev/language/classes#implicit-interfaces)

Repositoryでは***PostsRepository***の引数にIPostsDataSourceを返す***dataSourceProvider***を渡す形で実装しています。
しかし、***dataSourceProvider***はデフォルトで未実装のエラー（UnimplementedError）を投げるようにしているためどこかでoverrideしてあげる必要があります。
どこでoverrideしてあげるかというと、***main.dart***のProviderScope内で行います。

```dart
void main() {
  runApp(
    ProviderScope(
      overrides: [
// ここを差し替えることによってAPI通信を行うか、ダミーデータを取得するか変更する事ができる。
        dataSourceProvider
            .overrideWith(((ref) =&gt; ref.watch(stubPostsDataSourceProvider))),
      ],
      child: QiitaApp(),
    ),
  );
}

```

これでAPI通信を行うか、ダミーデータを取得するかを***main.dart***で簡単に変更する事が出来るようになりました！

&gt; ***overrideWithProvider***というメソッドもありますが現在は非推奨となっています。
代わりに今回サンプルで使用したのと同じ***overrideWith***を使用してください。

### ④ViewModelを作成

```dart
import &apos;package:flutter_riverpod/flutter_riverpod.dart&apos;;
import &apos;package:riverpod_sample/riverpod/data/models/qiita_post.dart&apos;;
import &apos;package:riverpod_sample/riverpod/data/repository/posts_repository.dart&apos;;

// エラーメッセージを管理。isNotEmptyになったらViewのref.listenのコールバックが発火してダイアログ表示
final errorMessageProvider = StateProvider&lt;String&gt;((_) =&gt; &apos;&apos;);
// 現在のタグを管理
final tagProvider = StateProvider&lt;String&gt;((_) =&gt; &apos;Flutter&apos;);

// autoDisposeをつけることによってこのProviderが参照されなくなったらProviderを破棄してくれます。
final postsViewModelProvider = FutureProvider.autoDispose&lt;List&lt;QiitaPost&gt;&gt;(
  (ref) async {
    final posts = await ref
        .watch(postsRepositoryProvider)
        .getQiitaPosts(ref.watch(tagProvider), 50);
// Resultクラスを作って成功時と失敗時の処理を変えています。
// Resultクラスの説明は時間がないので割愛..
    return posts.when(
      success: (value) =&gt; value,
      failure: (error) {
        ref
            .read(errorMessageProvider.notifier)
            .update((state) =&gt; state = error.response!.statusCode.toString());
        return [];
      },
    );
  },
);

```

ViewModelは***FutureProvider***を使って実装しています。***FutureProvider***は非同期操作が可能なProviderで、戻り値が***AsyncValue***という特殊な型になっています。このAsyncValueを使ってView側ではデータ取得時、エラー時、ローディング時に表示させるWidgetを自動的に切り替えています。
（最初使った時結構感動しました）

```dart
Widget build(BuildContext context, WidgetRef ref) {
  final posts = ref.watch(postsViewModelProvider); // AsyncValue型
// 省略
  return posts.when(
    loading: () =&gt; const CircularProgressIndicator(),
    error: (err, stack) =&gt; Text(&apos;Error: $err&apos;),
    data: (posts) {
// データ取得時に表示するWidgetを返却
    },
  );
}
```

AsyncValueについては下記の記事が参考になります。

[Riverpod v2のAsyncValueを理解する](https://zenn.dev/tsuruo/articles/52f62fc78df6d5)

### ⑤Viewを作成

Viewは一部抜粋して解説していきます。

```dart
// StatelessWidgetをConsumerWidgetに変更
class PostsPage extends ConsumerWidget {
  const PostsPage({super.key});

  static const primaryColor = Color(0xff59bb0c);
  static const defaultTag = &apos;TypeScript&apos;;

  @override
  Widget build(BuildContext context, WidgetRef ref) { // WidgetRefを追加
    final posts = ref.watch(postsViewModelProvider);
    final controller = ref.watch(textEditingControllerProvider);
// 省略
```

ViewでProviderにアクセスする場合は下記の変更が必要です。

**1. StatelessWidgetをConsumerWidgetに書き換える**
**2. buildメソッドの引数にWidgetRefを追加**

これでViewでProviderにアクセスする事ができます。

```dart
    ref.listen&lt;String&gt;(
      errorMessageProvider,
      ((previous, next) {
        if (next == &apos;403&apos;) {
          errorDialog(&apos;検索できないよ😡&apos;);
        }
        if (next == &apos;404&apos;) {
          errorDialog(&apos;投稿が見つかりません😢&apos;);
        }
      }),
    );
// 省略
```

buildメソッド内に***ref.listen***というものを使っていますが、こちらもRiverpodの機能の一つです。
***ref.listen***はプロバイダの値を監視し、値が変化するたびに第二引数に指定したコールバックが発火します。今回はerrorMessageProviderを監視して、エラーメッセージが入ったらダイアログが表示される形で実装しています。

&lt;img src=&apos;https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2466417/ee32cf0b-0e86-87d1-b11f-13711da199f5.gif&apos; width=&apos;350&apos;&gt;

駆け足になってしまいましたが、一旦QiitaのAPIを使って投稿を取得するアプリの完成です🚀🚀

&lt;img src=&apos;https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2466417/bd26d509-f4df-fdad-84fc-e6593b79e19d.gif&apos; width=&apos;350&apos;&gt;

## Riverpod2.0

ここからは8/31,9/1に開催されたFlutterVikingsで発表されたRiverpod2.0について勉強していきましょう！

&lt;iframe width=&quot;560&quot; height=&quot;315&quot; src=&quot;https://www.youtube.com/embed/C2Zp731g8Es?si=zhdQ7TYo5_X7E0H7&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&gt;&lt;/iframe&gt;

Riverpod2.0のポイントは下記の二つです。

**1. riverpod_generatorの登場**
**2. NotifierとAsyncNotifier**

### 1. riverpod_generator

本記事では全てをカバーしていませんが、Riverpodは6種類のProviderが用意されています。

|プロバイダの種類|生成されるステートの型|具体例|
|-----------------------------|--------------------------|----------------------------------|
|Provider|任意|サービスクラス / 算出プロパティ（リストのフィルタなど|
|StateProvider|任意|フィルタの条件 / シンプルなステートオブジェクト|
|FutureProvider|任意のFuture| API の呼び出し結果|
|StreamProvider|任意のStream| API の呼び出し結果の Stream|
|StateNotifierProvider|StateNotifierのサブクラス|イミュータブル（インタフェースを介さない限り）で複雑なステートオブジェクト|
|ChangeNotifierProvider|ChangeNotifierのサブクラス|ミュータブルで複雑なステートオブジェクト|

どのProviderを使うべきか悩みますよね？
そんな悩みをriverpod_generatorを使えば解決してくれるかもしれません！

[riverpod-generator](https://pub.dev/packages/riverpod_generator)

**パッケージを追加**
riverpod_generatorを使用するために下記のパッケージを追加

```yaml
dependencies:
  riverpod_annotation: ^1.0.6

dev_dependencies:
  riverpod_generator: ^1.0.6
```

Dioのインスタンスを返すProviderをriverpod_generatorを使って書き換えてみます。

```dart
import &apos;package:dio/dio.dart&apos;;
import &apos;package:flutter_riverpod/flutter_riverpod.dart&apos;;

final dioProvider = Provider&lt;Dio&gt;((_) {
  final dio = Dio();
  dio.interceptors.add(LogInterceptor());
  return dio;
});

```

↓新しい構文

```dart
import &apos;package:dio/dio.dart&apos;;
import &apos;package:riverpod_annotation/riverpod_annotation.dart&apos;;

part &apos;app_dio.g.dart&apos;; // 自動生成ファイルを定義

@riverpod // riverpod_annotationをimportして@riverpodを追加
Dio dio(DioRef ref) {
  final dio = Dio();
  dio.interceptors.add(LogInterceptor());
  return dio;
}
```

書き換えたらbuild_runnerを実行

```terminal
flutter packages pub run build_runner build --delete-conflicting-outputs
```

app_dio.g.dartファイルが生成されました！

![スクリーンショット 2022-12-08 4.39.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2466417/98b60ddb-7e96-8dab-7360-83bd1440ef2a.png)

```dart
// GENERATED CODE - DO NOT MODIFY BY HAND

part of &apos;app_dio.dart&apos;;

// 省略

/// See also [dio].
final dioProvider = AutoDisposeProvider&lt;Dio&gt;(
  dio,
  name: r&apos;dioProvider&apos;,
  debugGetCreateSourceHash:
      const bool.fromEnvironment(&apos;dart.vm.product&apos;) ? null : $dioHash,
);
typedef DioRef = AutoDisposeProviderRef&lt;Dio&gt;;

```

自動生成ファイルでは下記の定義がされています。
・Providerの生成
・引数に渡されるDioRefの型定義

また、riverpod_generatorを使用するとデフォルトでautoDispose修飾子がついたProviderが生成されるようになっています。

&gt; 「Providerへの参照がなくなっても状態を保持したいのにriverpod_generatorを使うとデフォルトでautoDisposeされてしまう.... 」とお困りの方もいるかもしれません。そんな方は***keepAlive***を使う事で解決します。

```dart
// keepAlive: trueにすることでアプリがkillされない限り状態が保持される
@Riverpod(keepAlive: true)
Future&lt;Post&gt; fetchPost(FetchPostRef ref, int postId) {
  print(&apos;init: fetchPost($postId)&apos;);
  ref.onDispose(() =&gt; print(&apos;dispose: fetchPost($postId)&apos;));
  return ref.watch(postsRepositoryProvider).fetchPost(postId);
}
```

詳しくは下記をご覧ください。

[How does keepAlive work?](https://codewithandrea.com/articles/flutter-riverpod-data-caching-providers-lifecycle/#how-does-keepalive-work?)

今回作成したサンプルアプリでは使用していないですが、riverpod_generatorを使うことによってfamily修飾子の欠点を補ってくれます。
例えばfamily修飾子を使用して次のようなFutureProviderを作ったとします。

```dart
// postIDから該当のpostデータを取得するProvider
final postProvider = FutureProvider.autoDispose
    .family&lt;Post, int&gt;((ref, postId) {
  return ref
      .watch(postRepositoryProvider)
      .post(postId: postId);
});
```

famliy修飾子を追記することによってProviderにパラメーターを渡す事ができますが、複数のパラメーターを渡す事ができません。
（正しくはtupleパッケージを使用するなど工夫しないと複数のパラメーターを渡す事ができない）
これをriverpod_generatorを使って書き換えると次のようになります。

```dart
@riverpod
Future&lt;Post&gt; post(
  PostRef ref, {
  required int postId,
  required String postType
// 名前付きで複数のパラメーターを渡す事ができる
}) {
  return ref
      .watch(postRepositoryProvider)
      .post(postId: postId, type: postType);
}
```

このようにriverpod_generatorを使うことによって複数のパラメーターを渡す事が出来るようになりました！

View側では名前付きで値を渡す事ができます。

```dart
final asyncValue = ref.watch(postProvider(postId: 0, type: &apos;&apos;));
```

riverpod_generatorのおかげでますますRiverpodが使いやすくなりましたね！

&gt; **注意点:**
&gt; riverpod_generatorは現在2種類のProviderしかサポートされていません。
**・Provider**
**・FutureProvider**

今回はRiverpodを使ったサンプルアプリの実装とRiverpod2.0について書いてみました！

## 参考文献

[Flutter Riverpod 2.0: The Ultimate Guide](https://codewithandrea.com/articles/flutter-state-management-riverpod/)

[flutter-riverpod-generator](https://codewithandrea.com/articles/flutter-riverpod-generator/)

[flutter-riverpod-async-notifier](https://codewithandrea.com/articles/flutter-riverpod-async-notifier/)

[unit-test-async-notifier-riverpod](https://codewithandrea.com/articles/unit-test-async-notifier-riverpod/)

[flutter-riverpod-data-caching-providers-lifecycle](https://codewithandrea.com/articles/flutter-riverpod-data-caching-providers-lifecycle/)

[Flutterの状態管理手法の選定](https://medium.com/flutter-jp/state-1daa7fd66b94)

## 余談

ちなみに今回作成したサンプルアプリのデータクラスは最近流行りの[ChatGPT](https://chat.openai.com/chat)に作ってもらいました。（一部修正）。技術の進歩って凄いですね。</content:encoded><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>FlutterのAnimationControllerを使いこなす</title><link>https://taiseidev.com//posts/flutter-animation-controller/</link><guid isPermaLink="true">https://taiseidev.com//posts/flutter-animation-controller/</guid><description>Flutterのアニメーションの基礎となるAnimationControllerの使い方を、実例を交えながら解説します。</description><pubDate>Sat, 20 May 2023 00:00:00 GMT</pubDate><content:encoded>## はじめに

Flutterでリッチなアニメーションを作る場合、避けて通れないのが***AnimationController***です。
Tweenアニメーションのパッケージに頼ってしまえばそれなりに楽ではあるのですが、AnimationControllerの仕組みを理解しておくと、独自アニメーションの自由度がぐっと上がります。

本記事では、AnimationControllerの基本と、よく使うパターンを紹介していきます。

## AnimationControllerとは

- アニメーションの「再生・停止・リセット」などを制御するクラス
- 値は `0.0 〜 1.0` で変化し、`duration`の時間をかけて進行する
- `TickerProvider`（通常は`SingleTickerProviderStateMixin`）が必要

まずはシンプルな例から。

## 基本的な使い方

フェードイン/アウトするボックスを作ってみます。

```dart
class FadeBox extends StatefulWidget {
  @override
  State&lt;FadeBox&gt; createState() =&gt; _FadeBoxState();
}

class _FadeBoxState extends State&lt;FadeBox&gt;
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 600),
    )..repeat(reverse: true);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: _controller,
      child: Container(
        width: 100,
        height: 100,
        color: Colors.deepPurple,
      ),
    );
  }
}
```

ポイントは3つ。

1. `vsync: this`でTickerを渡す（画面がオフの時にアニメーションを止めるため）
2. `dispose`でControllerを破棄する（メモリリーク防止）
3. `FadeTransition`などのBuilt-inトランジションに渡すだけで動く

## Tweenで値の範囲を変える

AnimationControllerの値は0.0〜1.0ですが、`Tween`を使えば任意の範囲に変換できます。

```dart
final _scale = Tween&lt;double&gt;(begin: 0.5, end: 1.5).animate(_controller);

ScaleTransition(
  scale: _scale,
  child: const FlutterLogo(size: 80),
);
```

色を変化させるなら`ColorTween`。

```dart
final _color = ColorTween(begin: Colors.red, end: Colors.blue)
    .animate(_controller);

AnimatedBuilder(
  animation: _color,
  builder: (_, __) =&gt; Container(color: _color.value, width: 80, height: 80),
);
```

## Curvesで動きに個性を出す

直線的な動きだけでは味気ないので、`CurvedAnimation`を使って緩急をつけます。

```dart
final curve = CurvedAnimation(
  parent: _controller,
  curve: Curves.easeInOutCubic,
);

final _opacity = Tween&lt;double&gt;(begin: 0, end: 1).animate(curve);
```

よく使うCurvesは以下あたり。

| Curve | 特徴 |
|---|---|
| `Curves.easeIn` | 始まりゆっくり |
| `Curves.easeOut` | 終わりゆっくり |
| `Curves.easeInOut` | 両端ゆっくり |
| `Curves.bounceOut` | 跳ねる |
| `Curves.elasticOut` | ぴょんと跳ねる |

## アニメーションの制御メソッド

```dart
_controller.forward();      // 0→1 へ進める
_controller.reverse();      // 1→0 へ戻す
_controller.stop();         // 一時停止
_controller.reset();        // 0 に戻す
_controller.repeat();       // 繰り返し
_controller.repeat(reverse: true); // 往復
_controller.animateTo(0.5); // 任意の値へ
```

## 複数のアニメーションを合成する

複雑なアニメーションを作るときは、1つのAnimationControllerから複数のTweenを派生させると管理が楽です。

```dart
// 前半でフェードイン、後半でスケールアップ
final _fade = Tween&lt;double&gt;(begin: 0, end: 1).animate(
  CurvedAnimation(
    parent: _controller,
    curve: const Interval(0.0, 0.5),
  ),
);

final _scale = Tween&lt;double&gt;(begin: 0.8, end: 1.0).animate(
  CurvedAnimation(
    parent: _controller,
    curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
  ),
);
```

`Interval`を使うと「0〜0.5の区間でフェード」「0.5〜1.0の区間でスケール」のようにタイミングを指定できます。

## AnimatedBuilder vs ImplicitlyAnimatedWidget

Flutterには暗黙的アニメーションWidget（`AnimatedContainer`、`AnimatedOpacity`など）もあります。
ざっくりの使い分けはこのイメージ。

- **単発・プロパティ1つ**：`AnimatedContainer`など暗黙的Widgetで十分
- **繰り返し・複雑な動き**：AnimationControllerを使う

## おわりに

AnimationControllerは最初こそ取っつきにくいですが、一度理解してしまえば自由自在にUIを動かせるようになります。
特に、`Interval`で複数のアニメーションを組み合わせる方法を覚えるとかなり表現の幅が広がるので、ぜひ試してみてください。

## 参考文献

- [Flutter公式 Animations](https://docs.flutter.dev/ui/animations)
- [AnimationController class](https://api.flutter.dev/flutter/animation/AnimationController-class.html)</content:encoded><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>Flutterでダークモードをスマートに実装する</title><link>https://taiseidev.com//posts/flutter-dark-mode/</link><guid isPermaLink="true">https://taiseidev.com//posts/flutter-dark-mode/</guid><description>FlutterのThemeDataを使ってダークモードを実装する方法と、ユーザーが手動でテーマを切り替えられるようにする仕組みを紹介します。</description><pubDate>Sat, 08 Apr 2023 00:00:00 GMT</pubDate><content:encoded>## はじめに

スマートフォンユーザーにとって、ダークモードはもはや標準機能のひとつ。
Flutterはこれを非常にスマートに扱える仕組みを持っています。

本記事では、以下の2点をコンパクトにまとめます。

1. OSの設定にあわせてダーク/ライトを自動で切り替える
2. アプリ内でユーザーが手動で切り替えられるようにする

## OS設定に追従させる

もっともシンプルな実装は、`MaterialApp`に`theme`と`darkTheme`を渡すことです。

```dart
MaterialApp(
  theme: ThemeData.light(useMaterial3: true),
  darkTheme: ThemeData.dark(useMaterial3: true),
  themeMode: ThemeMode.system, // OSの設定に従う
  home: const HomePage(),
);
```

`themeMode`には次の3つが指定できます。

| 値 | 挙動 |
|---|---|
| `ThemeMode.system` | OS設定に追従（既定） |
| `ThemeMode.light` | 常にライト |
| `ThemeMode.dark` | 常にダーク |

## カスタムテーマの定義

単に`ThemeData.light()`を使うだけでなく、色やフォントもカスタマイズしておくとアプリの個性が出ます。

```dart
final lightTheme = ThemeData(
  useMaterial3: true,
  colorScheme: ColorScheme.fromSeed(
    seedColor: Colors.indigo,
    brightness: Brightness.light,
  ),
  textTheme: const TextTheme(
    bodyMedium: TextStyle(fontFamily: &apos;NotoSansJP&apos;),
  ),
);

final darkTheme = ThemeData(
  useMaterial3: true,
  colorScheme: ColorScheme.fromSeed(
    seedColor: Colors.indigo,
    brightness: Brightness.dark,
  ),
);
```

`ColorScheme.fromSeed`は1色指定するだけで、Material 3基準のカラーパレットを自動生成してくれる優れものです。

## ユーザーが手動で切り替えられるようにする

OS設定に追従するだけでなく、アプリ内で切り替えたいケースも多いと思います。
ここではシンプルに`ValueNotifier`で実装してみます（Riverpodやbloc、好きな状態管理ツールに置き換えてOK）。

```dart
final themeModeNotifier = ValueNotifier&lt;ThemeMode&gt;(ThemeMode.system);

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder&lt;ThemeMode&gt;(
      valueListenable: themeModeNotifier,
      builder: (_, mode, __) {
        return MaterialApp(
          theme: lightTheme,
          darkTheme: darkTheme,
          themeMode: mode,
          home: const HomePage(),
        );
      },
    );
  }
}
```

切り替え用のトグルをどこかに置きます。

```dart
Switch(
  value: themeModeNotifier.value == ThemeMode.dark,
  onChanged: (isDark) {
    themeModeNotifier.value =
        isDark ? ThemeMode.dark : ThemeMode.light;
  },
);
```

## 永続化する

ユーザーが選んだテーマを記憶させたい場合は`shared_preferences`などで永続化します。

```dart
Future&lt;void&gt; saveThemeMode(ThemeMode mode) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString(&apos;themeMode&apos;, mode.name);
}

Future&lt;ThemeMode&gt; loadThemeMode() async {
  final prefs = await SharedPreferences.getInstance();
  final value = prefs.getString(&apos;themeMode&apos;) ?? &apos;system&apos;;
  return ThemeMode.values.firstWhere((m) =&gt; m.name == value);
}
```

アプリ起動時に`loadThemeMode`を呼び出して、`themeModeNotifier`の初期値にするだけで完成です。

## 現在のテーマを判定する

Widgetの中で現在がダークモードかどうかを判定するときは`Theme.of(context).brightness`を使います。

```dart
final isDark = Theme.of(context).brightness == Brightness.dark;
```

画像やアイコンの差し替えに使えます。

## おわりに

Flutterのテーマシステムは本当によくできていて、`themeMode`の一行だけでダークモード対応が始められます。
個人的には、最初から`ColorScheme.fromSeed`を使って色を一元管理しておくと、後からの変更にも強くなるのでおすすめです。

## 参考文献

- [Material 3 in Flutter](https://docs.flutter.dev/ui/design/material)
- [ColorScheme.fromSeed](https://api.flutter.dev/flutter/material/ColorScheme/ColorScheme.fromSeed.html)</content:encoded><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>Flutterのfreezedでイミュータブルなデータクラスを扱う</title><link>https://taiseidev.com//posts/flutter-freezed-guide/</link><guid isPermaLink="true">https://taiseidev.com//posts/flutter-freezed-guide/</guid><description>Dart/Flutterでイミュータブルなデータクラスを書くためのパッケージ「freezed」の基本的な使い方をまとめます。</description><pubDate>Sun, 12 Mar 2023 00:00:00 GMT</pubDate><content:encoded>## はじめに

Dartには標準でイミュータブルなデータクラスを簡潔に書く仕組みがありません。
自分で`copyWith`や`==`、`hashCode`、`toString`を書こうとすると、ボイラープレートが膨大になりがちです。

そこで登場するのが[freezed](https://pub.dev/packages/freezed)です。
コード生成によって、上記をすべて自動で用意してくれます。

本記事では、freezedの基本的な使い方とよくあるハマりどころを紹介します。

## インストール

```yaml
dependencies:
  freezed_annotation: ^2.4.1
  json_annotation: ^4.8.1

dev_dependencies:
  build_runner: ^2.4.6
  freezed: ^2.4.5
  json_serializable: ^6.7.1
```

パッケージを追加したら、`build_runner`でコード生成を実行します。

```bash
dart run build_runner watch --delete-conflicting-outputs
```

`watch`にしておくと、ファイルの変更を検知して自動生成してくれるので便利です。

## 基本的な使い方

シンプルなユーザークラスを例に見てみます。

```dart
import &apos;package:freezed_annotation/freezed_annotation.dart&apos;;

part &apos;user.freezed.dart&apos;;
part &apos;user.g.dart&apos;;

@freezed
class User with _$User {
  const factory User({
    required String id,
    required String name,
    int? age,
  }) = _User;

  factory User.fromJson(Map&lt;String, dynamic&gt; json) =&gt; _$UserFromJson(json);
}
```

これだけで、以下の機能が自動的に生成されます。

- `copyWith`
- `==` と `hashCode`
- `toString`
- `fromJson` / `toJson`

### copyWithの使用例

```dart
final user = User(id: &apos;1&apos;, name: &apos;Alice&apos;);
final updated = user.copyWith(name: &apos;Alicia&apos;);
```

ミュータブルにしないとフィールドを更新できないという悩みが解消されます。

## Unionによる状態表現

freezedの強力な機能のひとつがUnion型です。
たとえばAPIの結果を表現したいとき、こんなふうに書けます。

```dart
@freezed
class Result&lt;T&gt; with _$Result&lt;T&gt; {
  const factory Result.success(T data) = Success&lt;T&gt;;
  const factory Result.failure(String message) = Failure&lt;T&gt;;
}
```

使う側ではパターンマッチングで分岐できます。

```dart
result.when(
  success: (data) =&gt; print(&apos;OK: $data&apos;),
  failure: (msg)  =&gt; print(&apos;NG: $msg&apos;),
);
```

`when`は全パターンを漏らさず扱うように強制されるので、バグが起きづらくなります。
ローディング状態を追加するならこう。

```dart
@freezed
class AsyncState&lt;T&gt; with _$AsyncState&lt;T&gt; {
  const factory AsyncState.loading() = _Loading&lt;T&gt;;
  const factory AsyncState.data(T value) = _Data&lt;T&gt;;
  const factory AsyncState.error(Object error) = _Error&lt;T&gt;;
}
```

## よくあるハマりどころ

### 1. `part`宣言を忘れる

```dart
part &apos;user.freezed.dart&apos;;
part &apos;user.g.dart&apos;; // fromJson/toJsonを使うときだけ必要
```

`freezed`は`.freezed.dart`を生成し、`json_serializable`は`.g.dart`を生成します。
どちらも`part`宣言が必要です。

### 2. `const factory`にし忘れる

`const factory`にしておくことで、コンパイル時定数として扱えます。
基本はすべて`const factory`でOKです。

### 3. カスタムメソッドを書きたい

メソッドを追加したい場合はプライベートコンストラクタを使います。

```dart
@freezed
class User with _$User {
  const User._();

  const factory User({
    required String id,
    required String name,
  }) = _User;

  String greeting() =&gt; &apos;Hello, $name!&apos;;
}
```

`const User._();`がないとカスタムメソッドを定義できないので注意です。

## おわりに

freezedを使えば、Dartでも安全で簡潔にイミュータブルなクラスを扱えます。
個人的にはRiverpodと組み合わせて状態管理する時にも非常に相性が良く、Flutterプロジェクトには欠かせないパッケージになっています。

## 参考文献

- [freezed](https://pub.dev/packages/freezed)
- [Flutter freezed のチートシート](https://zenn.dev/sakusin/articles/b19e9a2c3829e0)</content:encoded><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item><item><title>go_routerで始めるFlutterの宣言的ルーティング</title><link>https://taiseidev.com//posts/go-router-basics/</link><guid isPermaLink="true">https://taiseidev.com//posts/go-router-basics/</guid><description>go_routerの基本的な使い方と、Navigator 1.0 / 2.0の違いを踏まえた宣言的ルーティングの実装方法について解説します。</description><pubDate>Sun, 05 Feb 2023 00:00:00 GMT</pubDate><content:encoded>## はじめに

Flutterのルーティングは長らく***Navigator 1.0***の命令的なAPI（`Navigator.push`など）が主流でしたが、ディープリンクやWeb対応が増えるにつれ、ルート状態をアプリ全体で整合的に扱いたい場面が増えてきました。

そこで登場したのが***Navigator 2.0***（宣言的ルーティング）ですが、そのままではボイラープレートが多く、使いこなすのが難しいという課題がありました。
この課題を解決してくれるのが[go_router](https://pub.dev/packages/go_router)パッケージです。

本記事では、go_routerの基本的な使い方を紹介していきます。

## go_routerとは

go_routerはFlutter公式（flutter.dev）が提供する、宣言的ルーティングを簡潔に書くためのパッケージです。

- URLベースのルート定義
- ディープリンク対応
- ネストされたナビゲーション
- リダイレクト（認証ガード等）

これらが直感的なAPIで書けるようになっています。

## インストール

```yaml
dependencies:
  flutter:
    sdk: flutter
  go_router: ^7.0.0
```

## 最小構成

まずはシンプルなルート定義です。

```dart
import &apos;package:flutter/material.dart&apos;;
import &apos;package:go_router/go_router.dart&apos;;

final _router = GoRouter(
  routes: [
    GoRoute(
      path: &apos;/&apos;,
      builder: (context, state) =&gt; const HomePage(),
    ),
    GoRoute(
      path: &apos;/detail&apos;,
      builder: (context, state) =&gt; const DetailPage(),
    ),
  ],
);

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: _router,
    );
  }
}
```

`MaterialApp.router`にGoRouterを渡すだけで準備完了です。

## 画面遷移

画面の遷移は`context.go`や`context.push`を使います。

```dart
// スタックを置き換えて遷移
context.go(&apos;/detail&apos;);

// スタックに積んで遷移
context.push(&apos;/detail&apos;);

// 戻る
context.pop();
```

`go`は履歴を置き換え、`push`はスタックに積むのでブラウザの「戻る」ボタンの挙動とも揃えやすくなっています。

## パスパラメータ

詳細画面に`id`を渡したいときはパスパラメータを使います。

```dart
GoRoute(
  path: &apos;/posts/:id&apos;,
  builder: (context, state) {
    final id = state.pathParameters[&apos;id&apos;]!;
    return PostDetailPage(id: id);
  },
),
```

遷移時はこうです。

```dart
context.go(&apos;/posts/42&apos;);
```

## クエリパラメータ

検索条件などはクエリパラメータが便利です。

```dart
GoRoute(
  path: &apos;/search&apos;,
  builder: (context, state) {
    final q = state.uri.queryParameters[&apos;q&apos;] ?? &apos;&apos;;
    return SearchPage(query: q);
  },
),
```

```dart
context.go(&apos;/search?q=flutter&apos;);
```

## リダイレクト（認証ガード）

未ログイン時にログイン画面へ飛ばす処理もリダイレクトで一括管理できます。

```dart
final _router = GoRouter(
  redirect: (context, state) {
    final loggedIn = AuthService.instance.isLoggedIn;
    final loggingIn = state.matchedLocation == &apos;/login&apos;;

    if (!loggedIn &amp;&amp; !loggingIn) return &apos;/login&apos;;
    if (loggedIn &amp;&amp; loggingIn) return &apos;/&apos;;
    return null;
  },
  routes: [ /* ... */ ],
);
```

画面ごとに認証チェックを書かなくて済むのがありがたいところです。

## ShellRouteでボトムナビを実装

タブ切り替え時もルートの状態を保ちたい、というケースは`ShellRoute`で実現できます。

```dart
ShellRoute(
  builder: (context, state, child) {
    return ScaffoldWithNavBar(child: child);
  },
  routes: [
    GoRoute(path: &apos;/home&apos;, builder: (_, __) =&gt; const HomePage()),
    GoRoute(path: &apos;/settings&apos;, builder: (_, __) =&gt; const SettingsPage()),
  ],
),
```

各タブのルート履歴を個別に保持したい場合は`StatefulShellRoute`を検討すると良いです。

## エラーハンドリング

ルートが見つからない場合のフォールバックも設定できます。

```dart
GoRouter(
  errorBuilder: (context, state) =&gt; NotFoundPage(error: state.error),
  routes: [ /* ... */ ],
);
```

## おわりに

go_routerは宣言的ルーティングを格段に書きやすくしてくれるパッケージです。
個人的にはディープリンクやWeb対応のしやすさが大きく、小規模〜中規模のアプリでも十分メリットを感じます。

まだ使ったことがない方は、ぜひ一度試してみてください！

## 参考文献

- [go_router](https://pub.dev/packages/go_router)
- [Flutterのルーティングを宣言的に扱う](https://docs.flutter.dev/ui/navigation)</content:encoded><author>Taisei Onishi &lt;onishi.taisei1997@gmail.com&gt;</author></item></channel></rss>