Flutterアプリケーション開発概論

【完成コードを読み解く】Discord風チャットアプリの全体構造を理解する

この節で学ぶこと

ここまでの節で、Discord風チャットアプリを作るための部品を1つずつ作ってきました。

ServerRail
ChannelSidebar
ChatArea
MemberPanel
ProfileCard
ProfileEditorDialog
MobileDiscordLayout
DesktopDiscordLayout

ここまで来ると、部品単体はなんとなく分かっていても、次のような疑問が出てきやすいです。

結局、全部はどうつながっているのか
どこが入口なのか
状態はどこで持っているのか
どのWidgetがどのWidgetを呼んでいるのか

今回の 5-16 では、完成コードを役割ごとに分解して読み解く ことを行います。

つまり、新しい機能を増やす回ではなく、今まで作ってきたものを整理して、「全体像が頭の中で見える状態」にします。

この節で一番大切なのは、次の一文です。

完成コードは、上から順に読むのではなく、「役割ごとのまとまり」に分けて読むと理解しやすい。

なぜ完成コードの読解が大事なのか

初心者が完成コードを見ると、よく次の状態になります。

コード量が多い
↓
どこから見ればいいか分からない
↓
全部が同じ重さに見える
↓
混乱する

でも実際には、完成コードは全部を同じように読む必要はありません。

アプリは、役割ごとに分けるとかなり整理できます。

今回のDiscord風アプリなら、次のように分けられます。

1. アプリ起動部分
2. 色・定数・enum
3. データ設計
4. 状態を持つ親Widget
5. レスポンシブ切り替え
6. PC/タブレット用レイアウト
7. スマホ用レイアウト
8. 左側パネル
9. 中央チャット画面
10. 右側メンバー欄
11. プロフィール編集機能
12. 補助関数

このように読むと、完成コードはかなり理解しやすくなります。

この節で扱う完成コードの見方

今回の節では、完成コードを次の順番で読みます。

アプリの入口
↓
見た目の共通ルール
↓
データの形
↓
状態を持つ中心
↓
画面幅による切り替え
↓
各画面部品
↓
補助関数

つまり、流れ で読むのではなく、責任の分担 で読む、ということです。

まず全体構造を図で見る

完成コードの全体構造を図で表すと、次のようになります。

main()
↓
DiscordResponsiveApp
↓
DiscordHomePage(StatefulWidget)
├─ currentUser
├─ selectedServerIndex
├─ selectedChannelIndex
├─ channelMessages
├─ updateProfile()
├─ sendMessage()
└─ build()
   ↓
   LayoutBuilder
   ├─ MobileDiscordLayout
   └─ DesktopDiscordLayout

さらに、DesktopDiscordLayout の中はこうなります。

DesktopDiscordLayout
├─ ServerRail
├─ ChannelSidebar
└─ Column
   ├─ ChatTopBar
   └─ Row
      ├─ ChatArea
      └─ MemberPanel(PCのみ)

MobileDiscordLayout の中はこうです。

MobileDiscordLayout
├─ drawer
│  └─ ServerRail + ChannelSidebar
├─ endDrawer
│  └─ MemberPanel
└─ body
   ├─ ChatTopBar
   └─ ChatArea

この全体像を頭に入れてからコードを見ると、かなり読みやすくなります。


1. アプリの入口を読む

まず、アプリの入口です。

void main() {
  runApp(const DiscordResponsiveApp());
}

この部分の意味は、次の通りです。

main()
↓
Flutterアプリのスタート地点

runApp(...)
↓
最初に表示するWidgetを起動する

ここでは、DiscordResponsiveApp がアプリ全体の土台です。

MaterialAppを確認する

次に、DiscordResponsiveApp を見ます。

class DiscordResponsiveApp extends StatelessWidget {
  const DiscordResponsiveApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Discord Responsive UI',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        brightness: Brightness.dark,
        scaffoldBackgroundColor: DiscordColors.background,
        colorScheme: ColorScheme.fromSeed(
          seedColor: DiscordColors.blurple,
          brightness: Brightness.dark,
        ),
      ),
      home: const DiscordHomePage(),
    );
  }
}

ここで大切なのは、次の3つです。

MaterialApp
↓
アプリ全体の土台

theme
↓
共通の見た目の設定

home
↓
最初の画面

つまり、実際の画面の中心は DiscordHomePage です。


2. 見た目の共通ルールを読む

完成コードでは、色や余白を定数でまとめています。

たとえば、色は次のようにまとめます。

class DiscordColors {
  static const Color appRail = Color(0xFF1E1F22);
  static const Color sidebar = Color(0xFF2B2D31);
  static const Color background = Color(0xFF313338);
  static const Color panel = Color(0xFF232428);
  static const Color input = Color(0xFF383A40);
  static const Color hover = Color(0xFF35373C);
  static const Color selected = Color(0xFF404249);

  static const Color textPrimary = Color(0xFFF2F3F5);
  static const Color textSecondary = Color(0xFFB5BAC1);
  static const Color textMuted = Color(0xFF949BA4);

  static const Color green = Color(0xFF23A559);
  static const Color red = Color(0xFFF23F42);
  static const Color yellow = Color(0xFFF0B232);
  static const Color blurple = Color(0xFF5865F2);
}

このまとまりは、UIの共通ルール です。

初心者向けには、次のように理解してください。

DiscordColors
↓
アプリ全体で共通して使う色の一覧

このようにまとめておくと、次のメリットがあります。

見た目に統一感が出る
色を変えたいときに1か所修正で済む
コードを読んだときに意味が分かりやすい

たとえば、次のコードは意味が分かりやすいです。

color: DiscordColors.sidebar

もし直書きでこう書いてあったら、役割が分かりづらくなります。

color: Color(0xFF2B2D31)

なので、完成コードを読むときは、まずこうした「共通ルールの塊」を見つけるのが大切です。


3. データ設計を読む

次に、アプリで扱うデータの形を見ます。

今回のアプリでは、主に次のデータがあります。

DiscordServer
DiscordChannel
ChatMessage
UserProfile
OnlineStatus
ChannelType

UserProfile を読む

たとえば、UserProfile は次のようなclassです。

class UserProfile {
  const UserProfile({
    required this.name,
    required this.handle,
    required this.status,
    required this.about,
    required this.avatarColor,
    required this.avatarUrl,
    required this.onlineStatus,
  });

  final String name;
  final String handle;
  final String status;
  final String about;
  final Color avatarColor;
  final String avatarUrl;
  final OnlineStatus onlineStatus;
}

このclassの役割は、次の通りです。

ユーザー1人分のプロフィール情報をまとめる

つまり、プロフィールカード、メンバー一覧、編集Dialogなどで使う情報を1つにまとめています。

ChatMessage を読む

class ChatMessage {
  const ChatMessage({
    required this.userName,
    required this.handle,
    required this.avatarColor,
    required this.avatarUrl,
    required this.body,
    required this.timestamp,
    required this.status,
    this.isSystem = false,
  });

  final String userName;
  final String handle;
  final Color avatarColor;
  final String avatarUrl;
  final String body;
  final DateTime timestamp;
  final OnlineStatus status;
  final bool isSystem;
}

これは、チャット1件分のデータです。

投稿者名
本文
時刻
状態
システムメッセージかどうか

enum を読む

enum は、種類を限定するためのものです。

enum ChannelType {
  text,
  voice,
}

enum OnlineStatus {
  online,
  idle,
  doNotDisturb,
  offline,
}

これにより、文字列の打ち間違いを減らせます。

'onnline'
みたいなミスを防げる

完成コードを読むときは、まず このアプリはどんなデータを扱っているか を見ると理解が進みます。


4. 状態を持つ中心Widgetを読む

次に、完成コードの最重要部分です。

それが、状態を持つ親Widgetです。

今回のアプリでは、DiscordHomePage が中心になります。

class DiscordHomePage extends StatefulWidget {
  const DiscordHomePage({super.key});

  @override
  State<DiscordHomePage> createState() => _DiscordHomePageState();
}

この StatefulWidget が、アプリ全体の状態を持ちます。

ここで何を持っているか

たとえば、次のような状態があります。

int selectedServerIndex = 0;
int selectedChannelIndex = 0;
bool showMemberPanel = true;

UserProfile currentUser = UserProfile(...);

late final Map<int, List<ChatMessage>> channelMessages;

それぞれの意味は次の通りです。

状態意味
selectedServerIndex今選ばれているサーバー
selectedChannelIndex今選ばれているチャンネル
showMemberPanel右側メンバー欄を出すかどうか
currentUser自分のプロフィール情報
channelMessagesチャンネルごとのメッセージ一覧

ここが、アプリ全体の「今どういう状態か」を持っている場所です。

今どのチャンネルを見ているか
今のプロフィールは何か
今のメッセージは何か

これらは全部、親Widgetが持っています。

なぜ親が持つのか

理由は、複数の子Widgetで共有する必要があるからです。

たとえば、currentUser は次の場所で使われます。

ProfileCard
ProfileEditorDialog
MemberPanel
UserControlBar
送信メッセージのユーザー情報

もしそれぞれが別々に状態を持ってしまうと、内容がズレやすくなります。

なので、親が1つだけ持って、必要な子に渡します。

親が状態を持つ
↓
子は受け取って表示する

これが、完成コードを読むときの大切な視点です。


5. 状態変更の関数を読む

状態を持っているだけではアプリは動きません。

次は、状態を変える関数を読みます。

sendMessage

void sendMessage(String text) {
  final trimmed = text.trim();

  if (trimmed.isEmpty) {
    return;
  }

  setState(() {
    channelMessages.putIfAbsent(selectedChannelIndex, () => []);
    channelMessages[selectedChannelIndex]!.add(
      ChatMessage(
        userName: currentUser.name,
        handle: currentUser.handle,
        avatarColor: currentUser.avatarColor,
        avatarUrl: currentUser.avatarUrl,
        body: trimmed,
        timestamp: DateTime.now(),
        status: currentUser.onlineStatus,
      ),
    );
  });
}

この関数の流れはこうです。

入力文字を受け取る
↓
空なら終わる
↓
現在のチャンネルのメッセージ一覧に追加する
↓
setStateで画面更新

updateProfile

void updateProfile(UserProfile profile) {
  setState(() {
    currentUser = profile;
  });
}

これは単純ですが、とても重要です。

編集後のプロフィールを受け取る
↓
currentUserを差し替える
↓
setStateで画面更新

selectChannel / selectServer

void selectChannel(int index) {
  setState(() {
    selectedChannelIndex = index;
  });
}

void selectServer(int index) {
  setState(() {
    selectedServerIndex = index;
  });
}

このように、状態変更の関数はとてもシンプルなことが多いです。

完成コードを読むときは、まず どんな状態があって、それを変える関数が何か を見ると整理しやすくなります。


6. buildの中でレイアウト切り替えを読む

次に、build の中で画面幅に応じて切り替える部分を見ます。

@override
Widget build(BuildContext context) {
  return LayoutBuilder(
    builder: (context, constraints) {
      final width = constraints.maxWidth;
      final isDesktop = width >= 1100;
      final isTablet = width >= 760 && width < 1100;
      final isMobile = width < 760;

      if (isMobile) {
        return MobileDiscordLayout(...);
      }

      return DesktopDiscordLayout(
        isTablet: isTablet,
        ...
      );
    },
  );
}

ここでやっていることは、次の通りです。

画面幅を取得する
↓
PC / タブレット / スマホを判定する
↓
スマホならMobileDiscordLayout
↓
それ以外はDesktopDiscordLayout

つまり、この build交通整理係 の役割です。

どの画面を出すか決める

これが、レスポンシブUIの中心です。


7. DesktopDiscordLayoutを読む

次に、PC・タブレット用レイアウトを見ます。

class DesktopDiscordLayout extends StatelessWidget {
  const DesktopDiscordLayout({
    super.key,
    required this.isTablet,
    ...
  });

  final bool isTablet;
}

このWidgetの役割は、次の通りです。

横に並べるレイアウトを担当する

構造はこうです。

Row
├─ ServerRail
├─ ChannelSidebar
└─ Expanded
   └─ Column
      ├─ ChatTopBar
      └─ Expanded
         └─ Row
            ├─ ChatArea
            └─ MemberPanel(PCのみ)

ここで一番大事なのは、isTablet の扱いです。

if (showMemberPanel)
  SizedBox(
    width: 260,
    child: MemberPanel(...),
  ),

ただし、実際には showMemberPanel は PCのときだけ true にしています。

PC
↓
右側メンバー欄あり

タブレット
↓
右側メンバー欄なし

つまり、DesktopDiscordLayout は1種類ではなく、PCとタブレットの2つの見せ方を持つ と読むと分かりやすいです。


8. MobileDiscordLayoutを読む

スマホ用は、Drawer中心の構造です。

class MobileDiscordLayout extends StatelessWidget {
  const MobileDiscordLayout({
    super.key,
    ...
  });
}

役割は次の通りです。

スマホではChatAreaを主役にし、
左と右の補助情報をDrawerへ収納する

構造はこうです。

Scaffold
├─ drawer
│  └─ ServerRail + ChannelSidebar
├─ endDrawer
│  └─ MemberPanel
└─ body
   ├─ ChatTopBar
   └─ ChatArea

ここで重要なのは、PC版とまったく同じ部品を再利用していることです。

ServerRail
ChannelSidebar
MemberPanel
ChatArea

新しく全部を作り直していません。

配置だけ変える

これが、レスポンシブ実装の大きな考え方です。


9. 左側パネル群を読む

左側には、ServerRailChannelSidebar があります。

ServerRail

ServerRail は、サーバー一覧です。

丸いサーバーアイコンを縦に並べる
選択中のインジケーターを出す
hover時に見た目を変える

構造としては、ListView.builderServerIcon の組み合わせです。

ServerRail
└─ ListView.builder
   └─ ServerIcon

ChannelSidebar

ChannelSidebar は、チャンネル一覧です。

サーバー名
TEXT CHANNELS
テキストチャンネル一覧
VOICE CHANNELS
ボイスチャンネル一覧
下部の自分用バー

読むときのコツは、次のように分けることです。

上:サーバー名ヘッダー
中:チャンネル一覧
下:UserControlBar

完成コードは長く見えますが、実は3ブロックです。


10. 中央ChatAreaを読む

ChatArea は、中央の本体です。

ここも役割で分けると読みやすいです。

ChatArea
├─ ChannelIntro
├─ ChatMessageTileの一覧
└─ ChatInputBar

何を受け取っているか

final DiscordChannel channel;
final List<ChatMessage> messages;
final ValueChanged<String> onSend;

意味はこうです。

受け取る値意味
channel今表示中のチャンネル
messagesそのチャンネルのメッセージ一覧
onSend送信時に親へ渡す処理

ここで大切なのは、ChatArea自身はメッセージの本体を持たない ことです。

データは親が持つ
ChatAreaは受け取って表示する

ただし、入力欄の TextEditingController やスクロール制御の ScrollController は、ChatAreaの中で持ちます。

一覧データそのもの
↓
親

入力欄やスクロール位置
↓
ChatArea内部

この区別が重要です。


11. 右側MemberPanelを読む

MemberPanel は、メンバー一覧とプロフィールカードを担当します。

構造はこうです。

MemberPanel
├─ ONLINE
├─ MemberTile
├─ IDLE
├─ MemberTile
├─ DO NOT DISTURB
├─ MemberTile
└─ ProfileCard

コードを読むときは、最初にこれを見つけると分かりやすいです。

final online = members.where(...).toList();
final idle = members.where(...).toList();
final dnd = members.where(...).toList();

つまり、やっていることは次の流れです。

members の List を受け取る
↓
状態ごとに分ける
↓
見出しと一緒に表示する
↓
最後にProfileCardを出す

このWidgetの本質は、分類して表示すること です。


12. プロフィール編集Dialogを読む

ProfileEditorDialog は、完成コードの中でも少し長く見える部分です。

でも、役割で分けると読みやすいです。

ProfileEditorDialog
├─ 入力欄を管理するController群
├─ 選択状態(selectedColor, selectedStatus)
├─ save()
└─ build()
   ├─ バナー
   ├─ アイコンプレビュー
   ├─ 入力欄
   ├─ 色選択
   ├─ 状態選択
   └─ Cancel / Save

ここで特に重要なのは、save() です。

void save() {
  final name = ...
  final handle = ...
  ...

  widget.onSave(
    UserProfile(
      ...
    ),
  );

  Navigator.of(context).pop();
}

つまり流れはこうです。

入力欄の値を集める
↓
新しいUserProfileを作る
↓
親に渡す
↓
Dialogを閉じる

ここでも、状態を直接書き換えるのではなく、新しいデータを作って親へ返す という考え方が重要です。


13. 補助関数を読む

完成コードの最後のほうには、補助関数があります。

たとえば、次のような関数です。

Color statusColor(OnlineStatus status) { ... }
String statusLabel(OnlineStatus status) { ... }
String buildInitials(String name) { ... }
String formatTime(DateTime dateTime) { ... }

こうした関数の役割は、次の通りです。

表示ルールをまとめる

たとえば、statusColor はこうです。

OnlineStatus.online
↓
緑

OnlineStatus.idle
↓
黄色

formatTime はこうです。

DateTime
↓
Just now / 12m ago / 5/13/2026

このような処理をWidgetの中に直接書きすぎると、コードが読みにくくなります。

なので、補助関数として分けています。


14. 完成コードを読むときの順番を再整理する

ここまでを整理すると、完成コードは次の順番で読むと理解しやすいです。

1. main()
2. MaterialApp
3. 色・定数・enum
4. データclass
5. 状態を持つ親Widget
6. 状態変更の関数
7. buildでのレイアウト切り替え
8. DesktopDiscordLayout
9. MobileDiscordLayout
10. 左側の部品
11. 中央の部品
12. 右側の部品
13. 編集Dialog
14. 補助関数

この順番で読むと、完成コードがかなり整理されます。

逆に、上から1行ずつ順番に理解しようとすると詰まりやすいです。


15. このアプリの「情報の流れ」を図で理解する

コード構造だけでなく、情報の流れも大切です。

DiscordHomePage(State)
├─ currentUser
├─ selectedServerIndex
├─ selectedChannelIndex
├─ channelMessages
│
├─ DesktopDiscordLayout / MobileDiscordLayout
│  ├─ ServerRail ← selectedServerIndex, onSelected
│  ├─ ChannelSidebar ← selectedChannelIndex, onSelected
│  ├─ ChatArea ← currentChannel, currentMessages, onSend
│  ├─ MemberPanel ← members, currentUser
│  └─ ProfileEditorDialog ← currentUser, onSave

つまり、データの流れは次のようになります。

親が状態を持つ
↓
子に渡す
↓
子で表示する
↓
操作が起きる
↓
親の関数を呼ぶ
↓
親が状態を更新する
↓
画面が再描画される

これが、Flutterアプリ全体の基本の流れです。


16. 手を動かす練習1:今どこを読んでいるか見出しを書く

完成コードを自分で読むとき、次のようにコメントを入れて整理してみましょう。

// 1. アプリ起動
// 2. 色と定数
// 3. データclass
// 4. 親Widgetの状態
// 5. レスポンシブ切り替え
// 6. PCレイアウト
// 7. スマホレイアウト
// 8. 左側パネル
// 9. チャット画面
// 10. 右側パネル
// 11. 編集Dialog
// 12. 補助関数

これだけでも、完成コードの見通しがかなり良くなります。


17. 手を動かす練習2:状態をどこが持っているか書き出す

次に、完成コードを見ながら、次をノートに書いてみてください。

selectedServerIndex はどこが持っているか
selectedChannelIndex はどこが持っているか
currentUser はどこが持っているか
messages はどこが持っているか

答えはすべて、親WidgetのStateです。

この練習で、「表示するだけのWidget」と「状態を持つWidget」の違いがかなり分かりやすくなります。


18. 手を動かす練習3:Widgetツリーを自分で書く

次の3つのツリーを、自分の手で書いてみましょう。

DesktopDiscordLayout のツリー
MobileDiscordLayout のツリー
ChatArea のツリー

たとえば、ChatArea はこうです。

ChatArea
├─ ChannelIntro
├─ ListView.builder
│  └─ ChatMessageTile
└─ ChatInputBar

これが書けるようになると、コードの見通しが一気に良くなります。


19. 手を動かす練習4:1つの機能変更がどこに影響するか考える

たとえば、「プロフィール画像を変えたい」とき、どこが関係するかを考えてみてください。

関係するのは次のような場所です。

UserProfile.avatarUrl
UserAvatar
ProfileCard
ProfileEditorDialog
currentUser
updateProfile()

このように、1つの機能変更でも「どのデータ」「どのWidget」「どの状態更新関数」が関係するかを追う練習をすると、完成コード読解力が上がります。


20. 手を動かす練習5:完成コードを別のアプリに置き換えてみる

次のように頭の中で置き換えてみてください。

DiscordServer → プロジェクト
DiscordChannel → タスクカテゴリ
ChatMessage → 作業ログ
UserProfile → 担当者プロフィール

こうすると、Discord風UIが「ただのDiscordの真似」ではなく、業務アプリの型として見えてきます。


21. よくあるつまずき1:完成コードを全部覚えようとしてしまう

完成コードは、丸暗記するものではありません。

大切なのは、次の3点です。

どこが入口か
どこが状態を持つか
どの部品がどの部品を使っているか

これが分かれば、かなり強いです。


22. よくあるつまずき2:StatefulWidget と StatelessWidget の違いが曖昧

完成コードを読むときは、まず次を見てください。

StatefulWidget
↓
状態を持つ可能性が高い

StatelessWidget
↓
受け取った値を表示する部品であることが多い

もちろん例外はありますが、まずはこの理解で十分です。


23. よくあるつまずき3:親子関係が分からなくなる

親子関係が分からなくなったら、buildreturn の中だけを見てください。

たとえば、DesktopDiscordLayout なら、

Row
↓
その中に何があるか
↓
さらにその子は何か

という順番でツリーをたどります。

コード全体を一度に見ようとすると混乱します。


24. よくあるつまずき4:データclassとWidgetを混同する

初心者は、次を混同しやすいです。

UserProfile
↓
データ

ProfileCard
↓
見た目

同じように、

ChatMessage
↓
データ

ChatMessageTile
↓
見た目

という対応があります。

この「データ」と「UI」を分けて考えることがとても大切です。


25. よくあるつまずき5:レスポンシブ切り替えの場所が分からない

レスポンシブ切り替えは、完成コードのかなり上流でやるのが基本です。

今回なら、DiscordHomePagebuild です。

ここで PC / タブレット / スマホ を分ける
↓
各レイアウトに処理を渡す

個々の小さなWidgetの中で画面幅判定を乱発すると、コードが複雑になりやすいです。


この節の確認問題

確認問題1

完成コードを読むとき、最初に見るべき入口は何ですか。

答え

main()runApp() です。

確認問題2

アプリ全体の状態を持っている中心のWidgetは何ですか。

答え

DiscordHomePageStatefulWidget とその State です。

確認問題3

PC・タブレット・スマホの切り替えをしているのはどこですか。

答え

build の中の LayoutBuilder です。

確認問題4

UserProfile は何ですか。

答え

ユーザー1人分のプロフィール情報を持つデータclassです。

確認問題5

ProfileCard は何ですか。

答え

UserProfile を受け取って見た目として表示するWidgetです。

確認問題6

完成コードを読むときに大切なのは、上から1行ずつ理解することですか。

答え

いいえ。役割ごとのまとまりに分けて読むことが大切です。

確認問題7

完成コードの中で、状態変更の中心となるキーワードは何ですか。

答え

setState です。


この節のまとめ

この節では、Discord風チャットアプリの完成コードを、役割ごとに分けて読み解きました。

今回の大きな整理は、次の通りです。

アプリの入口
↓
共通ルール
↓
データ設計
↓
状態を持つ親Widget
↓
レイアウト切り替え
↓
各部品
↓
補助関数

完成コードを理解するために大切なのは、次の3つです。

どこが状態を持っているか
どのデータを扱っているか
どの部品がどこで使われているか

この節で一番大切なのは、次の考え方です。

完成コードは、役割ごとに分解して読むと、長く見えても整理して理解できる。

ここまで理解できると、Discord風アプリに限らず、別のアプリでも次の流れで作れるようになってきます。

画面を観察する
↓
必要なデータを考える
↓
画面を部品に分ける
↓
状態をどこで持つか決める
↓
部品をつなぐ
↓
画面幅ごとに配置を変える
↓
完成コードを役割ごとに整理する

この章全体を通して身につけてほしいのは、見た目をまねる力 だけではなく、部品・状態・レイアウトの関係を整理して組み立てる力 です。

教材トップへ戻る