
【データ設計入門】Discord風アプリに必要なclassを考える
この節で学ぶこと
前回の 5-6 では、Discord風のチャンネル一覧UIを作りました。
ChannelSidebar
├─ ChannelCategory
└─ ChannelTile
そこでは、チャンネル1つ分の情報を DiscordChannel classとして整理しました。
今回の 5-7 では、Discord風アプリ全体に必要なデータを整理します。
これまでの教材では、少しずつ次のような情報を扱ってきました。
サーバー情報
チャンネル情報
ユーザー情報
メッセージ情報
これらをバラバラの文字列として持つのではなく、classとして整理していきます。
この節で一番大切なのは、次の一文です。
画面に表示する情報は、先にclassとして整理しておくと、UIを作るときに迷いにくくなる。
なぜデータ設計が必要なのか
アプリの画面には、たくさんの情報が表示されます。
Discord風アプリであれば、次のような情報があります。
サーバー名
サーバーアイコン画像
チャンネル名
チャンネルの種類
未読状態
ユーザー名
ユーザーアイコン
オンライン状態
メッセージ本文
投稿時刻
これらをすべて直接Widgetの中に書くと、コードがすぐに読みにくくなります。
たとえば、次のように書くこともできます。
Text('flutter_dev')
Text('@flutter_dev')
Text('Flutter UIを制作中')
Text('Discord風UIをFlutterで再現しています。')
これでも表示はできます。
しかし、ユーザーが増えたり、プロフィール編集機能を作ったり、メッセージ投稿機能を作ったりすると、どの情報がどこにあるのか分からなくなります。
そこで、データをclassとして整理します。
画面に表示する情報
↓
classにまとめる
↓
Widgetに渡す
↓
画面に表示する
今回整理するデータ
Discord風アプリでは、主に次の4つのclassを考えます。
| class名 | 表すもの | 使う場所 |
|---|---|---|
DiscordServer | サーバー1つ分 | サーバー一覧 |
DiscordChannel | チャンネル1つ分 | チャンネル一覧 |
ChatMessage | メッセージ1件分 | チャット画面 |
UserProfile | ユーザー1人分 | メンバー一覧、プロフィールカード |
この4つを整理できると、Discord風アプリのデータ構造が見えてきます。
Discord風アプリ
├─ サーバー
├─ チャンネル
├─ メッセージ
└─ ユーザー
classとは何か
まず、classの意味を復習します。
classは、データの設計図です。
初心者向けには、次のように理解してください。
class = 同じ形のデータを作るための設計図
たとえば、ユーザー情報を扱いたい場合を考えます。
ユーザー1人には、次のような情報があります。
名前
ハンドル
ステータス
自己紹介
アイコン色
オンライン状態
これを毎回バラバラに書くのではなく、UserProfile というclassにまとめます。
class UserProfile {
const UserProfile({
required this.name,
required this.handle,
required this.status,
required this.about,
required this.avatarColor,
required this.onlineStatus,
});
final String name;
final String handle;
final String status;
final String about;
final Color avatarColor;
final OnlineStatus onlineStatus;
}
これで、ユーザー1人分の形が決まります。
propertyとは何か
classの中にある値を、propertyと呼びます。
final String name;
final String handle;
final String status;
これらがpropertyです。
初心者向けには、次のように理解してください。
property = classが持つ情報
UserProfile のpropertyは、次のようになります。
UserProfile
├─ name
├─ handle
├─ status
├─ about
├─ avatarColor
└─ onlineStatus
つまり、UserProfile は「ユーザー1人分の情報をまとめる箱」です。
constructorとは何か
classから実際のデータを作るときに使う入口がconstructorです。
const UserProfile({
required this.name,
required this.handle,
required this.status,
required this.about,
required this.avatarColor,
required this.onlineStatus,
});
初心者向けには、次のように理解してください。
constructor = classからデータを作るときに値を受け取る入口
実際にユーザー情報を作るときは、次のように書きます。
const UserProfile(
name: 'flutter_dev',
handle: '@flutter_dev',
status: 'Flutter UIを制作中',
about: 'Discord風UIをFlutterで再現しています。',
avatarColor: DiscordColors.blurple,
onlineStatus: OnlineStatus.online,
)
これで、UserProfile という設計図から、1人分のユーザーデータが作られます。
まずはDiscordServerを整理する
最初に、左端のサーバー一覧で使う DiscordServer を整理します。
サーバー1つ分に必要な情報は、次の通りです。
| property | 型 | 内容 |
|---|---|---|
name | String | サーバー名 |
label | String | 画像がない場合に表示する文字 |
imageUrl | String | サーバーアイコン画像URL |
color | Color | 画像がない場合の背景色 |
コードにすると、次のようになります。
class DiscordServer {
const DiscordServer({
required this.name,
required this.label,
required this.imageUrl,
required this.color,
});
final String name;
final String label;
final String imageUrl;
final Color color;
}
このclassは、サーバーアイコンを表示するときに使います。
DiscordServer
↓
ServerIcon
↓
サーバーアイコンとして表示
DiscordServerのデータを作る
実際のサーバーデータは、次のように作れます。
const servers = [
DiscordServer(
name: 'Flutter Lab',
label: 'FL',
imageUrl:
'https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=200&h=200&fit=crop',
color: DiscordColors.blurple,
),
DiscordServer(
name: 'Design Hub',
label: 'UI',
imageUrl:
'https://images.unsplash.com/photo-1561070791-2526d30994b5?w=200&h=200&fit=crop',
color: Color(0xFFEB459E),
),
DiscordServer(
name: 'AI Studio',
label: 'AI',
imageUrl:
'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=200&h=200&fit=crop',
color: DiscordColors.green,
),
];
ここで大切なのは、UI側に直接サーバー名や画像URLを書かないことです。
データ
↓
servers
表示
↓
ServerRail / ServerIcon
このように分けると、後からサーバーを追加しやすくなります。
次にDiscordChannelを整理する
次に、チャンネル一覧で使う DiscordChannel を整理します。
チャンネル1つ分に必要な情報は、次の通りです。
| property | 型 | 内容 |
|---|---|---|
name | String | チャンネル名 |
type | ChannelType | テキストかボイスか |
unread | bool | 未読状態かどうか |
コードにすると、次のようになります。
enum ChannelType {
text,
voice,
}
class DiscordChannel {
const DiscordChannel({
required this.name,
required this.type,
this.unread = false,
});
final String name;
final ChannelType type;
final bool unread;
}
boolとは何か
ここで、bool が出てきます。
final bool unread;
bool は、true または false のどちらかを持つ型です。
初心者向けには、次のように理解してください。
bool = はい / いいえ を表す型
今回の unread は、未読かどうかを表します。
unread: true
↓
未読
unread: false
↓
未読ではない
未読なら、チャンネルの右側に小さな丸を表示できます。
if (channel.unread)
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: DiscordColors.textPrimary,
shape: BoxShape.circle,
),
),
このように、データによって見た目を変えられます。
DiscordChannelのデータを作る
チャンネルデータは、次のように作れます。
const channels = [
DiscordChannel(name: 'general', type: ChannelType.text),
DiscordChannel(name: 'flutter-ui', type: ChannelType.text),
DiscordChannel(name: 'design-review', type: ChannelType.text),
DiscordChannel(
name: 'announcements',
type: ChannelType.text,
unread: true,
),
DiscordChannel(name: 'voice-lounge', type: ChannelType.voice),
DiscordChannel(name: 'pair-programming', type: ChannelType.voice),
];
このデータをもとに、ChannelSidebar でチャンネル一覧を表示します。
channels
↓
ChannelSidebar
↓
ChannelTile
テキストチャンネルとボイスチャンネルを分けたい場合は、type を見て判断できます。
final textChannels = channels
.where((channel) => channel.type == ChannelType.text)
.toList();
次にUserProfileを整理する
次に、ユーザー情報を表す UserProfile を作ります。
ユーザー1人分に必要な情報は、次の通りです。
| property | 型 | 内容 |
|---|---|---|
name | String | 表示名 |
handle | String | @flutter_dev のようなID |
status | String | 現在のステータスメッセージ |
about | String | 自己紹介 |
avatarColor | Color | アイコンの背景色 |
avatarUrl | String | アイコン画像URL |
onlineStatus | OnlineStatus | オンライン状態 |
コードにすると、次のようになります。
enum OnlineStatus {
online,
idle,
doNotDisturb,
offline,
}
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;
}
OnlineStatusとは何か
OnlineStatus は、ユーザーのオンライン状態を表します。
enum OnlineStatus {
online,
idle,
doNotDisturb,
offline,
}
それぞれの意味は、次の通りです。
| 値 | 意味 | 表示色の例 |
|---|---|---|
online | オンライン | 緑 |
idle | 離席中 | 黄色 |
doNotDisturb | 取り込み中 | 赤 |
offline | オフライン | グレー |
このように、状態を enum にしておくと、後から表示を切り替えやすくなります。
Color statusColor(OnlineStatus status) {
switch (status) {
case OnlineStatus.online:
return DiscordColors.green;
case OnlineStatus.idle:
return DiscordColors.yellow;
case OnlineStatus.doNotDisturb:
return DiscordColors.red;
case OnlineStatus.offline:
return DiscordColors.textMuted;
}
}
switchとは何か
ここで switch という書き方が出てきました。
switch (status) {
case OnlineStatus.online:
return DiscordColors.green;
case OnlineStatus.idle:
return DiscordColors.yellow;
}
switch は、値によって処理を分けるための書き方です。
初心者向けには、次のように理解してください。
switch = 値の種類ごとに処理を分ける書き方
今回の場合は、オンライン状態によって色を変えています。
online
↓
緑
idle
↓
黄色
doNotDisturb
↓
赤
offline
↓
グレー
UserProfileのデータを作る
実際のユーザー情報は、次のように作れます。
const currentUser = UserProfile(
name: 'flutter_dev',
handle: '@flutter_dev',
status: 'Flutter UIを制作中',
about: 'Discord風UIをFlutterで再現しています。プロフィールは一時保存のみです。',
avatarColor: DiscordColors.blurple,
avatarUrl: '',
onlineStatus: OnlineStatus.online,
);
画像URLを空にしておけば、アイコンには文字や背景色を表示できます。
avatarUrl: ''
↓
画像なし
avatarColor: DiscordColors.blurple
↓
青紫のアイコン背景
画像URLを入れれば、Image.network で表示できます。
avatarUrl:
'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=120&h=120&fit=crop',
次にChatMessageを整理する
チャット画面では、メッセージ1件分の情報が必要です。
メッセージ1件には、次のような情報があります。
| property | 型 | 内容 |
|---|---|---|
userName | String | 投稿者名 |
handle | String | 投稿者ID |
avatarColor | Color | 投稿者アイコン色 |
avatarUrl | String | 投稿者アイコン画像URL |
body | String | メッセージ本文 |
timestamp | DateTime | 投稿時刻 |
status | OnlineStatus | 投稿者の状態 |
isSystem | bool | システムメッセージかどうか |
コードにすると、次のようになります。
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;
}
DateTimeとは何か
DateTime は、日付や時刻を表す型です。
final DateTime timestamp;
初心者向けには、次のように理解してください。
DateTime = 日付や時刻を表す型
現在時刻を使いたい場合は、次のように書きます。
DateTime.now()
たとえば、今投稿したメッセージなら、次のようにできます。
ChatMessage(
userName: 'flutter_dev',
handle: '@flutter_dev',
avatarColor: DiscordColors.blurple,
avatarUrl: '',
body: 'こんにちは!',
timestamp: DateTime.now(),
status: OnlineStatus.online,
)
過去のメッセージにしたい場合は、次のようにできます。
timestamp: DateTime.now().subtract(const Duration(minutes: 12)),
これは、「今から12分前」という意味です。
ChatMessageのデータを作る
メッセージ一覧は、List<ChatMessage> として持ちます。
final messages = [
ChatMessage(
userName: 'mika_design',
handle: '@mika',
avatarColor: const Color(0xFFEB459E),
avatarUrl:
'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=120&h=120&fit=crop',
body: 'Discord風UIは、画面を部品に分けると作りやすいです。',
timestamp: DateTime.now().subtract(const Duration(minutes: 24)),
status: OnlineStatus.online,
),
ChatMessage(
userName: 'code_senpai',
handle: '@senpai',
avatarColor: DiscordColors.green,
avatarUrl:
'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=120&h=120&fit=crop',
body: 'データをclassで整理すると、Widgetに渡しやすくなります。',
timestamp: DateTime.now().subtract(const Duration(minutes: 12)),
status: OnlineStatus.idle,
),
];
このデータをもとに、チャット画面にメッセージを表示します。
messages
↓
ListView.builder
↓
ChatMessageTile
データclassをまとめて確認する
ここまでで、4つのclassが出てきました。
DiscordServer
DiscordChannel
UserProfile
ChatMessage
それぞれの役割を整理すると、次の通りです。
| class名 | 何を表すか | 主なWidget |
|---|---|---|
DiscordServer | サーバー1つ分 | ServerIcon |
DiscordChannel | チャンネル1つ分 | ChannelTile |
UserProfile | ユーザー1人分 | UserAvatar, ProfileCard, MemberTile |
ChatMessage | メッセージ1件分 | ChatMessageTile |
このように、データclassとWidgetは対応しています。
データclass
↓
画面に表示するWidget
まずはデータだけをprintで確認する
ここでは、いきなりUIに表示せず、まずDartのデータとして扱えるか確認します。
次のコードを main.dart に貼り付けてください。
import 'package:flutter/material.dart';
void main() {
final server = DiscordServer(
name: 'Flutter Lab',
label: 'FL',
imageUrl: '',
color: DiscordColors.blurple,
);
final channel = DiscordChannel(
name: 'general',
type: ChannelType.text,
);
final user = UserProfile(
name: 'flutter_dev',
handle: '@flutter_dev',
status: 'Flutter UIを制作中',
about: 'Discord風UIをFlutterで再現しています。',
avatarColor: DiscordColors.blurple,
avatarUrl: '',
onlineStatus: OnlineStatus.online,
);
final message = ChatMessage(
userName: user.name,
handle: user.handle,
avatarColor: user.avatarColor,
avatarUrl: user.avatarUrl,
body: 'データ設計を学んでいます。',
timestamp: DateTime.now(),
status: user.onlineStatus,
);
print(server.name);
print(channel.name);
print(user.name);
print(message.body);
}
class DiscordColors {
static const Color blurple = Color(0xFF5865F2);
static const Color green = Color(0xFF23A559);
static const Color yellow = Color(0xFFF0B232);
static const Color red = Color(0xFFF23F42);
static const Color textMuted = Color(0xFF949BA4);
}
class DiscordServer {
const DiscordServer({
required this.name,
required this.label,
required this.imageUrl,
required this.color,
});
final String name;
final String label;
final String imageUrl;
final Color color;
}
enum ChannelType {
text,
voice,
}
class DiscordChannel {
const DiscordChannel({
required this.name,
required this.type,
this.unread = false,
});
final String name;
final ChannelType type;
final bool unread;
}
enum OnlineStatus {
online,
idle,
doNotDisturb,
offline,
}
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 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;
}
実行して確認すること
実行すると、コンソールに次のような内容が表示されます。
Flutter Lab
general
flutter_dev
データ設計を学んでいます。
ここでは、まだアプリ画面は表示していません。
しかし、データclassを使って、サーバー、チャンネル、ユーザー、メッセージを作れるようになりました。
データを作れる
↓
次にWidgetへ渡せる
↓
画面に表示できる
この順番が大切です。
UIに表示する最小アプリを作る
次に、作ったデータをUIに表示してみます。
今回は、サーバー名、チャンネル名、ユーザー名、メッセージ本文を1画面に表示します。
import 'package:flutter/material.dart';
void main() {
runApp(const DataDesignPracticeApp());
}
class DataDesignPracticeApp extends StatelessWidget {
const DataDesignPracticeApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Data Design Practice',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: DiscordColors.background,
),
home: const DataDesignPracticePage(),
);
}
}
class DiscordColors {
static const Color background = Color(0xFF313338);
static const Color panel = Color(0xFF232428);
static const Color textPrimary = Color(0xFFF2F3F5);
static const Color textSecondary = Color(0xFFB5BAC1);
static const Color textMuted = Color(0xFF949BA4);
static const Color blurple = Color(0xFF5865F2);
static const Color green = Color(0xFF23A559);
static const Color yellow = Color(0xFFF0B232);
static const Color red = Color(0xFFF23F42);
}
enum ChannelType {
text,
voice,
}
enum OnlineStatus {
online,
idle,
doNotDisturb,
offline,
}
class DiscordServer {
const DiscordServer({
required this.name,
required this.label,
required this.imageUrl,
required this.color,
});
final String name;
final String label;
final String imageUrl;
final Color color;
}
class DiscordChannel {
const DiscordChannel({
required this.name,
required this.type,
this.unread = false,
});
final String name;
final ChannelType type;
final bool unread;
}
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 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;
}
final server = DiscordServer(
name: 'Flutter Lab',
label: 'FL',
imageUrl: '',
color: DiscordColors.blurple,
);
final channel = DiscordChannel(
name: 'general',
type: ChannelType.text,
);
final currentUser = UserProfile(
name: 'flutter_dev',
handle: '@flutter_dev',
status: 'Flutter UIを制作中',
about: 'Discord風UIをFlutterで再現しています。',
avatarColor: DiscordColors.blurple,
avatarUrl: '',
onlineStatus: OnlineStatus.online,
);
final message = ChatMessage(
userName: 'mika_design',
handle: '@mika',
avatarColor: Color(0xFFEB459E),
avatarUrl: '',
body: 'データをclassで整理すると、画面づくりが分かりやすくなります。',
timestamp: DateTime.now(),
status: OnlineStatus.online,
);
class DataDesignPracticePage extends StatelessWidget {
const DataDesignPracticePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: DiscordColors.background,
body: SafeArea(
child: Center(
child: Container(
width: 420,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: DiscordColors.panel,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
server.name,
style: const TextStyle(
color: DiscordColors.textPrimary,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'#${channel.name}',
style: const TextStyle(
color: DiscordColors.textMuted,
fontSize: 16,
),
),
const SizedBox(height: 24),
Row(
children: [
CircleAvatar(
backgroundColor: currentUser.avatarColor,
child: Text(currentUser.label),
),
const SizedBox(width: 12),
Expanded(
child: Text(
currentUser.name,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: DiscordColors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 24),
Text(
message.userName,
style: const TextStyle(
color: DiscordColors.textPrimary,
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 6),
Text(
message.body,
style: const TextStyle(
color: DiscordColors.textSecondary,
fontSize: 15,
height: 1.5,
),
),
],
),
),
),
),
);
}
}
extension UserProfileLabel on UserProfile {
String get label {
if (name.trim().isEmpty) {
return '?';
}
if (name.length >= 2) {
return name.substring(0, 2).toUpperCase();
}
return name[0].toUpperCase();
}
}
extensionとは何か
このコードでは、最後に extension が出てきました。
extension UserProfileLabel on UserProfile {
String get label {
...
}
}
extension は、すでにあるclassに便利な機能を追加する書き方です。
初心者向けには、次のように理解してください。
extension = 既存のclassに後から便利機能を追加する書き方
今回は、UserProfile からアイコン用の短いラベルを作っています。
currentUser.label
たとえば、flutter_dev なら、先頭2文字を使って FL のように表示できます。
ただし、初心者のうちは extension を無理に使わなくても大丈夫です。
完成アプリでは、専用の関数でラベルを作る形でも問題ありません。
データとWidgetの関係を整理する
今回の最小アプリでは、データをWidgetに渡して表示しました。
server.name
↓
Textで表示
channel.name
↓
Textで表示
currentUser.avatarColor
↓
CircleAvatarで表示
message.body
↓
Textで表示
このように、classのpropertyをWidgetの中で使うことで、画面が作られます。
Text(server.name)
Text('#${channel.name}')
Text(message.body)
Flutterアプリでは、この流れが非常に多いです。
データを作る
↓
Widgetに渡す
↓
propertyを取り出す
↓
画面に表示する
完成アプリでのデータの使われ方
この章の完成アプリでは、今回作ったclassが次のように使われます。
DiscordServer
↓
ServerRail / ServerIcon
DiscordChannel
↓
ChannelSidebar / ChannelTile / ChatTopBar
ChatMessage
↓
ChatArea / ChatMessageTile
UserProfile
↓
MemberPanel / MemberTile / ProfileCard / UserAvatar
つまり、画面に出てくるほとんどの情報は、どこかのclassから取り出されます。
Widgetに直接文字を書き続けるのではなく、データを渡して表示する形になります。
なぜUIより先にデータを考えるのか
アプリを作るときは、見た目から作ることもできます。
しかし、少し複雑なアプリでは、先にデータを考えると作りやすくなります。
たとえば、チャットアプリを作る場合、次のように考えます。
画面に何を表示するか?
↓
メッセージ
ユーザー
チャンネル
それぞれ何の情報を持つか?
↓
メッセージ本文
投稿者名
投稿時刻
オンライン状態
それをclassにすると?
↓
ChatMessage
UserProfile
DiscordChannel
この順番で考えると、Widgetの設計もしやすくなります。
データが決まる
↓
Widgetの引数が決まる
↓
画面が作りやすくなる
手を動かす練習1:サーバーデータを追加する
servers を作っている場合は、次のサーバーを追加してみましょう。
DiscordServer(
name: 'Study Room',
label: 'ST',
imageUrl: '',
color: DiscordColors.yellow,
),
サーバー一覧UIに接続している場合は、アイコンが1つ増えます。
まだUIに接続していない場合でも、データとして追加できることを確認してください。
手を動かす練習2:チャンネルを追加する
channels に、次のチャンネルを追加してみましょう。
DiscordChannel(
name: 'questions',
type: ChannelType.text,
unread: true,
),
このデータを ChannelSidebar に渡すと、チャンネル一覧に表示できます。
unread: true にしているため、未読マークも表示できます。
手を動かす練習3:ユーザーの状態を変える
currentUser の onlineStatus を変えてみましょう。
onlineStatus: OnlineStatus.idle,
もしステータス色を表示するUIとつながっていれば、緑から黄色に変わります。
データを変えるだけでUIが変わるのが、class設計の大きなメリットです。
手を動かす練習4:メッセージを追加する
messages のListを作っている場合は、メッセージを1件追加してみましょう。
ChatMessage(
userName: 'flutter_dev',
handle: '@flutter_dev',
avatarColor: DiscordColors.blurple,
avatarUrl: '',
body: 'classで整理すると、チャット画面にも表示しやすいです。',
timestamp: DateTime.now(),
status: OnlineStatus.online,
),
あとで ListView.builder と組み合わせると、メッセージ一覧として表示できます。
よくあるつまずき1:classが多くて混乱する
今回、複数のclassが出てきました。
DiscordServer
DiscordChannel
UserProfile
ChatMessage
最初は多く感じるかもしれません。
しかし、それぞれの役割ははっきりしています。
サーバーならDiscordServer
チャンネルならDiscordChannel
ユーザーならUserProfile
メッセージならChatMessage
画面に表示するものごとにclassを分けているだけです。
よくあるつまずき2:Stringで全部持てばよいのではと思う
たしかに、最初はすべて文字列で持つこともできます。
final userName = 'flutter_dev';
final message = 'こんにちは';
しかし、アプリが大きくなると、文字列だけでは足りません。
たとえば、ユーザーには名前だけでなく、アイコン、ステータス、自己紹介もあります。
ユーザー
├─ 名前
├─ ハンドル
├─ アイコン
├─ ステータス
└─ 自己紹介
このように複数の情報をまとめたいときに、classが便利です。
よくあるつまずき3:enumとStringの違いが分からない
チャンネルの種類を文字列で持つと、打ち間違いが起きやすいです。
type: 'voice'
type: 'voise'
voise と書いても、文字列としては成立してしまいます。
一方、enumなら、用意された選択肢から選びます。
type: ChannelType.voice
間違った名前を書くと、コードを書く段階で気づきやすくなります。
enum
↓
決まった種類を安全に扱える
よくあるつまずき4:DateTimeが難しい
DateTime は、最初は少し難しく感じるかもしれません。
まずは、次の2つだけ覚えれば大丈夫です。
DateTime.now()
これは、現在時刻です。
DateTime.now().subtract(const Duration(minutes: 12))
これは、現在時刻から12分前です。
チャットアプリでは、投稿時刻を表示するために使います。
よくあるつまずき5:データとUIが混ざる
初心者のうちは、Widgetの中に直接データを書きがちです。
Text('flutter_dev')
Text('Discord風UIを作っています')
もちろん最初の練習ではそれでも構いません。
しかし、完成アプリに近づけるには、データとUIを分けます。
データ
↓
UserProfile
UI
↓
ProfileCard
この分離ができると、あとから編集機能や一覧表示を作りやすくなります。
この節の確認問題
確認問題1
DiscordServer は何を表すclassですか。
答え
サーバー1つ分の情報を表すclassです。
確認問題2
DiscordChannel は何を表すclassですか。
答え
チャンネル1つ分の情報を表すclassです。
確認問題3
UserProfile は何を表すclassですか。
答え
ユーザー1人分のプロフィール情報を表すclassです。
確認問題4
ChatMessage は何を表すclassですか。
答え
チャットメッセージ1件分の情報を表すclassです。
確認問題5
enum は何のために使いますか。
答え
決まった種類や状態を安全に扱うために使います。
確認問題6
bool はどのような値を持つ型ですか。
答え
true または false のどちらかを持つ型です。
確認問題7
データをclassにまとめるメリットは何ですか。
答え
画面に表示する情報を整理でき、Widgetに渡しやすくなり、後から機能追加もしやすくなります。
この節のまとめ
この節では、Discord風アプリに必要なデータをclassとして整理しました。
今回扱った主なclassは、次の4つです。
DiscordServer
DiscordChannel
UserProfile
ChatMessage
それぞれ、画面の部品と対応しています。
DiscordServer
↓
サーバー一覧
DiscordChannel
↓
チャンネル一覧
UserProfile
↓
メンバー一覧・プロフィール
ChatMessage
↓
チャット画面
この節で一番大切なのは、次の考え方です。
画面に表示する情報をclassとして整理すると、Widgetはそのデータを受け取って表示するだけでよくなる。
次の節では、このデータ設計をもとに、中央のチャット画面UIを作っていきます。