📱

Reactエンジニア向けFlutter入門

2026/09/25に公開

Reactエンジニア向けFlutter入門

はじめに

業務では主にFlutter開発を行っていますが、趣味でReactを触っているのでその知見をもとに
Reactエンジニア向けFlutter入門を執筆しました。

本章では、ReactとFlutterの共通点や違いを比較しながら解説します。
普段Reactを使っている方がFlutterに興味を持っていただけたらと思います。
また、ReactやFlutterの経験がなくても理解できるように構成しています。

そもそもFlutterとは

FlutterとはUIフレームワークで、Dart言語によって記述します。
DartはもともとJavaScriptの代替を目指して設計された静的型付け言語ですが、現在ではFlutterを中心にクロスプラットフォーム開発で広く使われています。
そのためJavaScriptと構文が似ており、JavaScriptの不便な部分が改良されています。

デメリットでもメリットでもありますが、DartとFlutterはどちらもGoogleによって開発されています。

ReactはDOMやブラウザの描画エンジンを前提としていますが、FlutterはSkia/Impellerという描画エンジンで直接レンダリングします。そのためプラットフォームに依存しない一貫したUIを提供できるのが大きな特徴です。
Web向けビルドではデフォルトでCanvasKit(SkiaベースのWebAssemblyレンダラーでキャンバスに描画される)が使われますが、軽量なHTMLレンダラーも選択可能です。

また、FlutterもReactと同じくHot Reloadを備えており、状態を保持したままコードの変更を即座に反映できます。これにより開発効率が大幅に向上します。

ローカルステート管理

まずはサンプルアプリとしておなじみの「カウンターアプリ」を例にします。
Reactでは宣言的なUIと単方向データフローを使って、シンプルかつ直感的にステート管理を記述できます。

import { useState } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount((c) => c + 1)}>count: {count}</button>;
};

Reactでは関数コンポーネントとuseStateを組み合わせて状態を扱いますが、FlutterではStatefulWidgetクラスとsetStateを使って同じことを実現します。
書き方は異なりますが「状態を変更するとUIが更新される」という考え方は同じです。

import 'package:flutter/material.dart';

class Counter extends StatefulWidget {
  const Counter({super.key});
  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int count = 0;
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () => setState(() => count++),
      child: Text('count: $count'),
    );
  }
}

Reactに比べると記述がやや冗長に感じられるかもしれません。次に紹介する、flutter_hooksというステート管理ライブラリを利用すればReactに近くなります。
これはReact Hooksに着想を得たライブラリで、useStateやuseEffectなどReactに馴染みのある書き方でFlutterのステート管理を記述できます。

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

class Counter extends HookWidget {
  const Counter({super.key});

  @override
  Widget build(BuildContext context) {
    final count = useState(0);
    return TextButton(
      onPressed: () => count.value++,
      child: Text('count: ${count.value}'),
    );
  }
}

このように、flutter_hooksを使うとReactに近い感覚でステート管理が可能になります。
ReactのuseStateは配列を返しますが、FlutterのuseStateはValueNotifierオブジェクトを返します。
ValueNotifierのvalueプロパティに代入すると自動的に再レンダリングが発生します。
Reactでは「関数の実行」で状態の更新を通知しますが、Flutterでは「オブジェクトのプロパティ更新」で状態の更新を通知します。

グローバルステート管理

ローカルステートは小規模なアプリには便利ですが、画面をまたいでデータを共有するには限界があります。
そこで登場するのがグローバルステート管理で、FlutterではRiverpodがデファクトスタンダードです。
RiverpodはRecoilやJotaiのように「小さな状態を組み合わせる」設計思想を持っています。
違いとして、RiverpodはDartの型安全性やコード生成を活用でき、依存関係の追跡や非同期処理をより明示的に扱えるのが特徴です。

まずはReact(Jotai + Valibot)でGitHub APIを呼び出すサンプルを見てみましょう。
Jotai は軽量なステート管理、Valibot はデータの型とバリデーションを定義するライブラリです。
atomFamilyで「ユーザー名ごと」の状態を作り、loadableで非同期状態を安全に扱います。

import { atom, useAtomValue } from 'jotai';
import { atomFamily, loadable } from 'jotai/utils';
import * as v from 'valibot';

// オブジェクトを定義する
const UserSchema = v.object({
  avatar_url: v.string(),
});

// グローバルステート管理を定義する
const githubUserAtomFamily = atomFamily((username: string) =>
  atom(async () => {
    const res = await fetch(`https://api.github.com/users/${username}`);
    if (!res.ok) throw new Error('Failed to fetch user');
    const data = await res.json();
    return v.parse(UserSchema, data);
  })
);

const githubUserLoadable = (username: string) =>
  loadable(githubUserAtomFamily(username));

// コンポーネントを定義する
export const GithubUser = () => {
  const userLd = useAtomValue(githubUserLoadable('octocat'));
  if (userLd.state === 'loading') return <p>loading...</p>;
  if (userLd.state === 'hasError') return <p>error: {(userLd.error as Error).message}</p>;
  return <img src={userLd.data.avatar_url} alt="avatar" />;
};

このコードでは、まずValibotでAPIレスポンスの型を定義しています。
次に、atomを組み合わせてユーザーごとに独立した状態を作り、さらにloadableで非同期処理の進行状態を管理しています。
コンポーネント側ではuserLd.stateに応じてローディング・エラー・成功を分岐し、結果を描画します。

これをFlutterで記述してみます。

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'main.freezed.dart';
part 'main.g.dart';

// オブジェクトを定義する
@freezed
abstract class User with _$User {
  @JsonSerializable(fieldRename: FieldRename.snake)
  const factory User({required String avatarUrl}) = _User;
  factory User.fromJson(Map<String, Object?> json) => _$UserFromJson(json);
}

// グローバルステート管理を定義する
@riverpod
Future<User> getGithubUser(Ref ref, String username) async {
  final url = Uri.parse('https://api.github.com/users/$username');
  final response = await http.get(url);
  return User.fromJson(jsonDecode(response.body));
}

// ウィジェット(コンポーネント)を定義する
class GithubUser extends ConsumerWidget {
  const GithubUser({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final user = ref.watch(getGithubUserProvider('octocat'));
    return switch (user) {
      AsyncData(:final value) => Image.network(value.avatarUrl),
      AsyncError(:final error) => Text('error: $error'),
      AsyncLoading() => const Text('loading...'),
    };
  }
}

このコードではFreezedで型安全なデータクラスを定義しています。
Freezedはデータクラスを自動生成するライブラリで、型安全なステート管理やシリアライズを簡単に実現できます。
次に、Riverpodを使用しユーザーごとに独立した状態(Provider)を作っています。
UI側ではref.watchを使ってProviderを監視し、Dart 3から導入されたswitch式で状態を網羅的に分岐しています。
これにより「型で保証された非同期処理のハンドリング」が可能になり、Reactのloadableと同じような記述をより堅牢に実現できます。

Dart言語はTypeScriptのような柔軟な型システム、マクロ、メタクラスを持ちませんが、その代わりにコード生成を活用する文化があります。
この例では_$User、_User、_$UserFromJson、getGithubUserProviderが自動生成されています。
FreezedやRiverpodのようなライブラリはこの文化を前提に発展しています。アノテーションを付けるだけで膨大なボイラープレートコードを自動生成でき、強力で安全なプログラミングが可能になります。

レイアウト構築の思想

ReactではDOM要素にCSS(Flexboxなど)を適用してレイアウトを調整します。
これに対してFlutterでは、HTMLやCSSを直接使うのではなく、RowやColumnといった専用のウィジェットを組み合わせて「レイアウトそのものをコードとして表現する」スタイルを取ります。

React(Tailwind CSS)で簡単なレイアウトの例を見てみましょう。

<div className="flex gap-2">
  <span>Hello</span>
  <button>Click</button>
</div>

このコードでは、divにflexとgap-2のクラスを付け、Flexboxレイアウトでテキストとボタンを横並びに配置しています。
CSSがレイアウトの役割を担い、HTML要素はあくまで入れ物として使われています。

Row(
  spacing: 8,
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    const Text('Hello'),
    ElevatedButton(onPressed: () {}, child: const Text('Click')),
  ],
);

Flutterでは同じことをRowウィジェットで表現します。
spacing: 8がgap-2に相当し、childrenにテキストとボタンを並べています。
スタイルをCSSに委ねるのではなく、レイアウトも含めてすべてコード(Widgetツリー)で記述します。

TailwindのようなユーティリティCSSに比べるとFlutterの記述はやや冗長に見えます。
ただし、スタイルもレイアウトもWidgetとして一元管理できるため、ツリー構造を見ればUIの構成をそのまま理解できるという利点があります。

まとめ

ここまで見てきたように、FlutterはReactと多くの共通点を持ちながら、型とコード生成を活かした開発体験など独自の強みもあります。
Reactの経験があれば、コンポーネントベースのUI構築やステート管理といった基礎概念をそのまま活かせます。
本書はあくまで入り口です。
次のステップとしては、公式ドキュメントや Riverpod の公式サンプルアプリ のコードリーディングがおすすめです。
Riverpod には「コード生成を使う書き方」と「使わない書き方」がありますが、推奨されるのはコード生成を使う方法です。
公式リポジトリの example/ フォルダには、コード生成を使ったサンプルがいくつか用意されているので、ぜひ参考にしてみてください。
Flutterは最初の学習コストこそありますが、その分、良い開発体験が得られます。

本書がFlutterを学ぶきっかけになればうれしいです。

GitHubで編集を提案