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

カプセル化で安全に使えるVideo部品を作る

この節で学ぶこと

前回の 3-5 では、methodgetter を使って、動画データに「表示用のふるまい」を持たせる方法を学びました。

たとえば、完成アプリの Video classには、次のようなgetterがありました。

String get viewLabel {
  if (views >= 100000000) {
    return '${(views / 100000000).toStringAsFixed(1)}億回視聴';
  }

  if (views >= 10000) {
    return '${(views / 10000).toStringAsFixed(1)}万回視聴';
  }

  return '$views回視聴';
}

String get metaLabel {
  if (isLive) {
    return 'ライブ配信中';
  }

  return '$viewLabel・$publishedAt';
}

このコードによって、128000 という再生回数を、画面では 12.8万回視聴 のように表示できるようになりました。

今回は、さらに一歩進んで、データを安全に扱うための考え方を学びます。

その中心になるのが、カプセル化 です。

この節のゴール

この節のゴールは、YouTube風アプリで使う Video データを、できるだけ安全に扱う考え方を理解することです。

最終的には、次のようなコードを見たときに、「なぜ final を使うのか」「なぜ getter を使うのか」が分かる状態を目指します。

class Video {
  const Video({
    required this.title,
    required this.channelName,
    required this.views,
    required this.publishedAt,
    required this.duration,
    required this.category,
    required this.thumbnailColor,
    required this.channelColor,
    required this.isLive,
  });

  final String title;
  final String channelName;
  final int views;
  final String publishedAt;
  final String duration;
  final String category;
  final Color thumbnailColor;
  final Color channelColor;
  final bool isLive;

  String get viewLabel {
    if (views >= 100000000) {
      return '${(views / 100000000).toStringAsFixed(1)}億回視聴';
    }

    if (views >= 10000) {
      return '${(views / 10000).toStringAsFixed(1)}万回視聴';
    }

    return '$views回視聴';
  }

  String get metaLabel {
    if (isLive) {
      return 'ライブ配信中';
    }

    return '$viewLabel・$publishedAt';
  }
}

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

カプセル化とは、データをむやみに外から変更させず、安全に使える形にまとめる考え方である。

まずカプセル化とは何か

カプセル化 という言葉は、初心者には少し難しく聞こえるかもしれません。

しかし、考え方はシンプルです。

大切なデータを、むやみに外からいじられないようにする。
使う側は、決められた方法で安全に使う。

たとえば、YouTube風アプリの動画データを考えてみます。

title: 'FlutterでYouTube風UIを作る'
views: 128000
publishedAt: '2日前'
duration: '12:34'
isLive: false

このようなデータは、動画カードを表示するための大切な材料です。

もし、アプリの途中で勝手に書き換わってしまったら困ります。

タイトルが勝手に変わる
再生回数がマイナスになる
動画時間が空になる
ライブではないのにライブ表示になる

このようなミスを防ぐために、データの扱い方を安全に設計します。

それがカプセル化の考え方です。

カプセル化を身近な例で考える

カプセル化は、日常生活にも似た考え方があります。

たとえば、自動販売機を考えてみます。

利用者は、自動販売機の中の機械を直接触りません。

利用者ができることは、次のようなことです。

お金を入れる
ボタンを押す
商品を受け取る

中のモーターや配線を直接いじる必要はありません。

むしろ、誰でも中身を自由に触れてしまうと危険です。

プログラムでも同じです。

データの中身を何でも外から自由に変更できると、ミスが起きやすくなります。

だから、必要なものだけを外から使えるようにして、大切な部分は安全に守ります。

Video classで考えるカプセル化

YouTube風アプリの Video classは、動画1本分の情報をまとめる部品です。

class Video {
  const Video({
    required this.title,
    required this.channelName,
    required this.views,
  });

  final String title;
  final String channelName;
  final int views;
}

このclassでは、titlechannelNameviewsfinal がついています。

final String title;
final String channelName;
final int views;

final は、一度値を入れたら、あとから変更しないという意味です。

これも、安全にデータを扱うための大切な考え方です。

finalがない場合を見てみる

まず、final を使わない例を見てみます。

void main() {
  final video = Video(
    title: 'FlutterでYouTube風UIを作る',
    views: 128000,
  );

  print(video.title);
  print(video.views);

  video.title = 'まったく別のタイトル';
  video.views = -100;

  print(video.title);
  print(video.views);
}

class Video {
  Video({
    required this.title,
    required this.views,
  });

  String title;
  int views;
}

このコードでは、titleviews をあとから変更できます。

video.title = 'まったく別のタイトル';
video.views = -100;

しかし、これは動画データとして少し危険です。

再生回数が -100 になるのは不自然です。

動画タイトルが途中で勝手に変わるのも、意図しないバグにつながるかもしれません。

finalを使って変更できないようにする

次に、final を使った例を見ます。

void main() {
  final video = Video(
    title: 'FlutterでYouTube風UIを作る',
    views: 128000,
  );

  print(video.title);
  print(video.views);

  // video.title = 'まったく別のタイトル';
  // video.views = -100;
}

class Video {
  const Video({
    required this.title,
    required this.views,
  });

  final String title;
  final int views;
}

final をつけると、作ったあとに値を変更できません。

次のようなコードはエラーになります。

video.title = 'まったく別のタイトル';
video.views = -100;

これにより、動画データを安全に扱いやすくなります。

初心者向けには、まず次のように覚えてください。

finalは、あとから勝手に変えられないようにする安全装置。

なぜ動画データはfinalにするとよいのか

YouTube風アプリでは、videos に動画データをまとめています。

const videos = [
  Video(
    title: 'FlutterでYouTube風UIを作る|ListViewとCardの実践入門',
    channelName: 'Code Studio',
    views: 128000,
    publishedAt: '2日前',
    duration: '12:34',
    category: 'Flutter',
    thumbnailColor: Color(0xFF121212),
    channelColor: Color(0xFFE53935),
    isLive: false,
  ),
];

この動画データは、画面に表示するための元データです。

基本的には、アプリの中で勝手に変わる必要はありません。

むしろ、変わってしまうと画面表示が不安定になります。

データ勝手に変わると困る理由
title動画タイトルが表示と合わなくなる
channelNameチャンネル情報がずれる
views再生回数の表示がおかしくなる
publishedAt投稿日表示が変になる
durationサムネイル右下の時間表示が崩れる
isLive通常動画とライブ動画の表示が混ざる

だから、Video のpropertyには final を使っています。

final String title;
final String channelName;
final int views;
final String publishedAt;
final String duration;
final String category;
final Color thumbnailColor;
final Color channelColor;
final bool isLive;

finalとconstの違いをざっくり理解する

この章では、finalconst が何度も出てきます。

初心者が混乱しやすいところなので、ざっくり整理します。

書き方初心者向けの理解
final一度入れたら変えないfinal String title;
const最初から固定された値const Video(...)

たとえば、final はpropertyでよく使います。

final String title;

これは、Video を作ったあとに title を変更しないという意味です。

一方、const は、固定データを作るときに使います。

const video = Video(
  title: 'FlutterでYouTube風UIを作る',
  views: 128000,
);

最初は、次の理解で十分です。

final:
あとから変えない

const:
最初から固定

細かい違いで止まらなくて大丈夫です。

getterもカプセル化に役立つ

前回学んだ getter も、カプセル化に役立ちます。

たとえば、再生回数は views という数字で持っています。

final int views;

でも、画面にはそのまま 128000 と表示するのではなく、12.8万回視聴 と表示したいです。

そこで、viewLabel というgetterを作ります。

String get viewLabel {
  if (views >= 10000) {
    return '${(views / 10000).toStringAsFixed(1)}万回視聴';
  }

  return '$views回視聴';
}

UI側では、次のように使えます。

Text(video.viewLabel)

UI側は、128000 をどうやって 12.8万回視聴 にするのかを知る必要がありません。

Video classが、表示用の形に整えてくれます。

これもカプセル化の一部です。

変換ルールをVideo classの中に閉じ込める。
UI側は、video.viewLabelを使うだけにする。

UI側に計算を書きすぎるとどうなるか

もし、viewLabel を作らずに、UI側に直接計算を書いたらどうなるでしょうか。

たとえば、画面側で次のように書くことになります。

Text(
  video.views >= 10000
      ? '${(video.views / 10000).toStringAsFixed(1)}万回視聴'
      : '${video.views}回視聴',
)

このコードは動きます。

しかし、少し読みにくいです。

UI側のコードに、再生回数の変換ルールが混ざっています。

画面側では、本来はできるだけシンプルに書きたいです。

Text(video.viewLabel)

こちらのほうが、何を表示しているのか分かりやすいです。

書き方特徴
UI側に計算を書く画面コードが読みにくくなる
getterにまとめるUI側がすっきりする

カプセル化を使うと、処理の置き場所を整理できます。

metaLabelもカプセル化の例

完成アプリでは、metaLabel もgetterとして用意しています。

String get metaLabel {
  if (isLive) {
    return 'ライブ配信中';
  }

  return '$viewLabel・$publishedAt';
}

これにより、UI側では次のように書けます。

Text(video.metaLabel)

通常動画なら、

12.8万回視聴・2日前

ライブ動画なら、

ライブ配信中

と表示されます。

UI側は、細かい条件分岐を知らなくてよくなります。

isLiveがtrueならライブ配信中
isLiveがfalseなら再生回数と投稿日

このルールを Video classの中にまとめています。

これも、カプセル化です。

カプセル化で役割を分ける

ここまでの内容を、役割で整理します。

場所役割
Video class動画データと表示用ルールを持つviewLabelmetaLabel
VideoInfo Widget画面に表示するText(video.metaLabel)
Thumbnail Widgetサムネイルを表示するvideo.thumbnailColorvideo.duration
VideoCard Widget動画カード全体を組み立てるVideoInfo(video: video)

大切なのは、全部を1か所に書かないことです。

動画データに関係する処理は、できるだけ Video classに近い場所に置きます。

画面の見た目に関係する処理は、Widget側に置きます。

データのこと:
Video class

見た目のこと:
Widget

この分け方ができると、コードが読みやすくなります。

ここまでのまとめ

ここまでの前半では、カプセル化の基本を学びました。

カプセル化は、難しい言葉ですが、考え方は次の通りです。

大切なデータを安全にまとめる。
外から勝手に変更されにくくする。
使う側は、決められた方法で使う。

YouTube風アプリでは、Video classがその役割を持ちます。

Video class
├─ 元データを持つ
│  ├─ title
│  ├─ views
│  ├─ publishedAt
│  └─ isLive
│
└─ 表示用の値を作る
   ├─ viewLabel
   └─ metaLabel

前半で大切なポイントは、次の通りです。

- finalは、あとから勝手に変えないための安全装置
- getterは、表示用の値を作るために便利
- UI側に計算を書きすぎると読みにくくなる
- 表示用のルールをVideo classにまとめると安全で読みやすい
- データのことはVideo class、見た目のことはWidgetに分けて考える

private風の設計とは何か

前半では、final を使うことで、動画データをあとから勝手に変更されにくくできることを学びました。

final String title;
final int views;

ここからは、もう少しだけカプセル化らしい考え方を見ていきます。

Dartでは、変数名やmethod名の先頭に _ をつけると、そのファイルの外から直接触りにくくできます。

final int _views;

このような書き方を、この教材では「private風の設計」と呼びます。

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

_ がついたデータは、外から直接いじるものではなく、classの中で大切に扱うデータ。

なぜ外から直接触らせないのか

たとえば、再生回数 views を外から自由に変更できるとします。

video.views = -100;

再生回数が -100 になるのは不自然です。

このような不正な値が入ると、画面表示もおかしくなります。

-100回視聴

本来、再生回数は0以上であるべきです。

そこで、外から直接変更させず、classの中で安全に扱う考え方が大切になります。

まずは通常のVideo classを見る

これまで使ってきた Video classは、次のような形でした。

class Video {
  const Video({
    required this.title,
    required this.views,
  });

  final String title;
  final int views;

  String get viewLabel {
    if (views >= 10000) {
      return '${(views / 10000).toStringAsFixed(1)}万回視聴';
    }

    return '$views回視聴';
  }
}

この形でも、final がついているので、作ったあとに views を変更することはできません。

// video.views = -100;

ただし、さらに「元データを外にそのまま見せず、表示用の値だけ使わせたい」と考える場合があります。

そのときに、_views のようなprivate風のpropertyを使います。

_viewsを使った例

次のコードを見てください。

void main() {
  final video = Video(
    title: 'FlutterでYouTube風UIを作る',
    views: 128000,
  );

  print(video.title);
  print(video.viewLabel);

  // print(video._views); // classの外から直接使うものではない
}

class Video {
  const Video({
    required this.title,
    required int views,
  }) : _views = views;

  final String title;
  final int _views;

  String get viewLabel {
    if (_views >= 10000) {
      return '${(_views / 10000).toStringAsFixed(1)}万回視聴';
    }

    return '$_views回視聴';
  }
}

このコードでは、外から渡す名前は views ですが、classの中では _views として持っています。

required int views,
}) : _views = views;

これは少し難しく見えますが、意味は次の通りです。

Videoを作るときにviewsを受け取る。
受け取ったviewsを、classの中の_viewsに入れる。

: _views = views の読み方

次の部分を見てください。

const Video({
  required this.title,
  required int views,
}) : _views = views;

この : _views = views は、constructorで受け取った views を、_views に入れる書き方です。

部分意味
required int viewsVideoを作るときにviewsを受け取る
final int _views;classの中で_viewsとして持つ
: _views = views受け取ったviewsを_viewsへ入れる

初心者のうちは、最初からこの書き方を完璧に覚えなくても大丈夫です。

まずは、次のイメージを持ってください。

外から受け取る名前:
views

classの中で大切に持つ名前:
_views

private風にすると何がうれしいのか

_views のようにすると、classの外からは元データを直接使うよりも、用意されたgetterを使う設計にできます。

print(video.viewLabel);

つまり、使う側には「再生回数の表示方法」だけを見せます。

中でどのように計算しているかは、Video classに任せます。

使う側:
video.viewLabel を使うだけ

Video classの中:
viewsが10000以上なら万回視聴にする
10000未満なら回視聴にする

これは、自動販売機の例と同じです。

使う人はボタンを押すだけ。

中の仕組みを直接触る必要はありません。

ただし今回の完成アプリではpublicなfinalで十分

ここで大切な注意があります。

今回のYouTube風モックでは、次の形でも十分です。

final int views;

なぜなら、final がついているので、あとから変更できないからです。

完成アプリでは、学習の分かりやすさを優先して、次のようにしています。

final String title;
final String channelName;
final int views;
final String publishedAt;
final String duration;
final String category;
final Color thumbnailColor;
final Color channelColor;
final bool isLive;

つまり、この教材では次の順番で理解するとよいです。

まずは final で安全にする
↓
getterで表示用の値を作る
↓
必要に応じて _ を使ったprivate風の設計も知る

初心者の段階では、まず finalgetter をしっかり理解することが大切です。

データの安全性を考える

安全なデータ設計では、「どんな値が入るべきか」を考えます。

YouTube風アプリの Video では、次のようなルールが考えられます。

property入ってほしい値避けたい値
title空ではない文字空文字
channelName空ではない文字空文字
views0以上の整数マイナス
duration12:34 のような表示空文字
isLivetrue または falseそれ以外は入らない
thumbnailColorFlutterのColor不正な色指定

Dartの型を使うと、ある程度は安全になります。

たとえば、viewsint にしておけば、文字は入りません。

final int views;

isLivebool にしておけば、truefalse しか入りません。

final bool isLive;

型を決めることも、カプセル化や安全な設計につながります。

型が守ってくれること

次のようなclassがあるとします。

class Video {
  const Video({
    required this.title,
    required this.views,
    required this.isLive,
  });

  final String title;
  final int views;
  final bool isLive;
}

この場合、次のように正しい型で値を渡します。

final video = Video(
  title: 'FlutterでYouTube風UIを作る',
  views: 128000,
  isLive: false,
);

一方で、次のような書き方は型が合いません。

final video = Video(
  title: 'FlutterでYouTube風UIを作る',
  views: 'たくさん',
  isLive: 'いいえ',
);

viewsint なのに文字を渡しています。

isLivebool なのに文字を渡しています。

このようなミスは、Dartが気づいてくれます。

型を書くことで、入れてはいけない種類のデータを防ぎやすくなる。

getterで表示ルールを1か所にまとめる

カプセル化で特に大切なのは、「同じルールをあちこちに書かない」ことです。

たとえば、再生回数の表示ルールを考えます。

1億以上なら、億回視聴
1万以上なら、万回視聴
それ以外なら、回視聴

このルールを画面のあちこちに直接書いてしまうと、あとから修正するときに大変です。

VideoInfoにも書く
SearchResultにも書く
HistoryPageにも書く
FavoritePageにも書く

もし表示ルールを変えたくなったら、全部直さなければいけません。

しかし、Video classの viewLabel にまとめておけば、修正する場所は1か所です。

String get viewLabel {
  if (views >= 100000000) {
    return '${(views / 100000000).toStringAsFixed(1)}億回視聴';
  }

  if (views >= 10000) {
    return '${(views / 10000).toStringAsFixed(1)}万回視聴';
  }

  return '$views回視聴';
}

UI側は、どの画面でもこれを使うだけです。

Text(video.viewLabel)

これが、カプセル化の大きなメリットです。

表示ルールを変える例

たとえば、今は小数1桁で表示しています。

12.8万回視聴

もし、あとから小数なしにしたい場合は、viewLabel だけを直せばよいです。

変更前です。

return '${(views / 10000).toStringAsFixed(1)}万回視聴';

変更後です。

return '${(views / 10000).floor()}万回視聴';

UI側の Text(video.viewLabel) は変えなくて大丈夫です。

表示ルールを変える場所:
Video classのviewLabelだけ

画面側:
そのまま

このように、変更に強くなることも、カプセル化のメリットです。

完成アプリのVideo classを安全な部品として見る

完成アプリの Video classは、次のように見ることができます。

Video classは、安全に使える動画データ部品

中には、元データがあります。

final String title;
final String channelName;
final int views;
final String publishedAt;
final String duration;
final String category;
final Color thumbnailColor;
final Color channelColor;
final bool isLive;

さらに、表示用のgetterがあります。

String get viewLabel { ... }
String get metaLabel { ... }

これにより、UI側は次のようにシンプルに使えます。

Text(video.title)
Text(video.channelName)
Text(video.metaLabel)

サムネイル側でも、次のように使えます。

color: video.thumbnailColor
Text(video.duration)

つまり、Video classは、データと表示に必要な小さなルールをまとめた部品です。

UIとデータの責任を分ける

カプセル化は、責任を分ける考え方とも関係します。

担当何をするか
Video class動画データを安全に持つtitleviews
Video class表示用の文字を作るviewLabelmetaLabel
VideoInfo Widgetテキストを画面に並べるText(video.title)
Thumbnail Widgetサムネイルを描画するvideo.thumbnailColor
VideoCard Widget1枚の動画カードを組み立てるThumbnailVideoInfo

このように役割を分けると、コードが読みやすくなります。

Video class:
動画データそのものに関すること

Widget:
画面にどう並べるかに関すること

プログラミング初心者のうちは、全部を一つの場所に書きたくなることがあります。

しかし、アプリが大きくなるほど、役割を分けることが大切になります。

DartPadで試すコード1:finalで安全にする

まずは、final を使って、あとから変更できないデータを作ります。

void main() {
  final video = Video(
    title: 'FlutterでYouTube風UIを作る',
    views: 128000,
  );

  print(video.title);
  print(video.views);

  // 次の2行は、finalなので変更できません。
  // video.title = '別タイトル';
  // video.views = -100;
}

class Video {
  const Video({
    required this.title,
    required this.views,
  });

  final String title;
  final int views;
}

このコードで確認することは、次の2つです。

titleとviewsは、作るときに決まる。
finalなので、あとから変更できない。

DartPadで試すコード2:getterで表示用に整える

次に、views から viewLabel を作ります。

void main() {
  final video = Video(
    title: 'FlutterでYouTube風UIを作る',
    views: 128000,
  );

  print(video.title);
  print(video.viewLabel);
}

class Video {
  const Video({
    required this.title,
    required this.views,
  });

  final String title;
  final int views;

  String get viewLabel {
    if (views >= 10000) {
      return '${(views / 10000).toStringAsFixed(1)}万回視聴';
    }

    return '$views回視聴';
  }
}

出力結果です。

FlutterでYouTube風UIを作る
12.8万回視聴

views は数字として安全に持ち、viewLabel で画面用の文字に整えています。

DartPadで試すコード3:metaLabelもまとめる

さらに、publishedAtisLive を追加して、metaLabel を作ります。

void main() {
  final normalVideo = Video(
    title: 'FlutterでYouTube風UIを作る',
    views: 128000,
    publishedAt: '2日前',
    isLive: false,
  );

  final liveVideo = Video(
    title: 'ライブ:Flutter質問会',
    views: 5600,
    publishedAt: '現在',
    isLive: true,
  );

  print('${normalVideo.title} / ${normalVideo.metaLabel}');
  print('${liveVideo.title} / ${liveVideo.metaLabel}');
}

class Video {
  const Video({
    required this.title,
    required this.views,
    required this.publishedAt,
    required this.isLive,
  });

  final String title;
  final int views;
  final String publishedAt;
  final bool isLive;

  String get viewLabel {
    if (views >= 10000) {
      return '${(views / 10000).toStringAsFixed(1)}万回視聴';
    }

    return '$views回視聴';
  }

  String get metaLabel {
    if (isLive) {
      return 'ライブ配信中';
    }

    return '$viewLabel・$publishedAt';
  }
}

出力結果です。

FlutterでYouTube風UIを作る / 12.8万回視聴・2日前
ライブ:Flutter質問会 / ライブ配信中

metaLabel に表示ルールをまとめたことで、使う側はシンプルに書けています。

手を動かす練習1:finalの役割を確認する

次のclassで、あとから変更できないpropertyを答えてください。

class Video {
  const Video({
    required this.title,
    required this.views,
  });

  final String title;
  final int views;
}

解答例

titleviews です。

どちらにも final がついているため、Video を作ったあとに値を変更できません。

手を動かす練習2:変更できるclassを安全にする

次のclassは、あとから値を変更できてしまいます。

class Video {
  Video({
    required this.title,
    required this.views,
  });

  String title;
  int views;
}

final を使って、安全な形にしてください。

解答例

class Video {
  const Video({
    required this.title,
    required this.views,
  });

  final String title;
  final int views;
}

String title;final String title; にしました。

int views;final int views; にしました。

さらに、constructorに const をつけられる形にしました。

手を動かす練習3:getterに表示ルールをまとめる

次の条件で viewLabel を作ってください。

views が 10000以上なら「万回視聴」
それ以外なら「回視聴」

解答例

class Video {
  const Video({
    required this.title,
    required this.views,
  });

  final String title;
  final int views;

  String get viewLabel {
    if (views >= 10000) {
      return '${(views / 10000).toStringAsFixed(1)}万回視聴';
    }

    return '$views回視聴';
  }
}

手を動かす練習4:UI側をシンプルにする

次のようにUI側に直接計算を書くと、少し読みにくくなります。

Text(
  video.views >= 10000
      ? '${(video.views / 10000).toStringAsFixed(1)}万回視聴'
      : '${video.views}回視聴',
)

Video classに viewLabel を用意した場合、UI側はどのように書けますか。

解答例

Text(video.viewLabel)

表示用の変換ルールを Video classにまとめることで、UI側が読みやすくなります。

手を動かす練習5:metaLabelの役割を説明する

次のgetterは、何をするためのものですか。

String get metaLabel {
  if (isLive) {
    return 'ライブ配信中';
  }

  return '$viewLabel・$publishedAt';
}

解答例

通常動画なら、再生回数と投稿日をまとめた文字を返します。

ライブ動画なら、ライブ配信中 を返します。

UI側で毎回条件分岐を書かなくても、video.metaLabel と書くだけで適切な表示ができます。

よくあるつまずき1:カプセル化は隠すことだけだと思ってしまう

カプセル化は、単に「隠すこと」ではありません。

大切なのは、安全に使える形にまとめることです。

データを安全に持つ。
表示ルールを1か所にまとめる。
使う側が迷わない形にする。

Video classで言えば、次のような設計です。

final int views;

String get viewLabel {
  // 表示用のルール
}

元データは views

表示用の文字は viewLabel

役割を分けて、安全に使える形にしています。

よくあるつまずき2:全部privateにすればよいと思ってしまう

_views のようにprivate風にすることはできます。

しかし、何でもかんでもprivateにすればよいわけではありません。

学習段階では、まず次の形で十分です。

final int views;
String get viewLabel { ... }

重要なのは、views をあとから勝手に変えないことと、表示用のルールをgetterにまとめることです。

本格的なアプリで、外から直接見せたくない値が出てきたら、_ を使った設計も検討します。

よくあるつまずき3:getterの中で値を書き換えようとする

getterは、基本的に「値を返す」ために使います。

String get viewLabel {
  return '$views回視聴';
}

getterの中で、propertyを書き換えるような処理は初心者のうちは避けたほうが分かりやすいです。

getter:
値を取り出す・表示用に整える

変更処理:
別のmethodとして考える

今回の viewLabelmetaLabel は、表示用の値を作って返すだけです。

よくあるつまずき4:UIとデータの役割が混ざる

UI側に計算や条件分岐をたくさん書くと、あとから読みにくくなります。

Text(
  video.isLive
      ? 'ライブ配信中'
      : '${video.viewLabel}・${video.publishedAt}',
)

これよりも、Video classに metaLabel を用意して、

Text(video.metaLabel)

と書くほうが、UI側の見通しがよくなります。

データの変換ルール:
Video class

画面に並べること:
Widget

この分け方を意識してください。

よくあるつまずき5:finalにすると何も変えられないから不便だと思う

final にすると、たしかにあとから値を変更できません。

しかし、表示用の固定データには、それがメリットになります。

YouTube風モックでは、動画データは最初から用意されたサンプルです。

そのため、あとから勝手に変える必要はありません。

固定の動画データ:
finalが向いている

ユーザー操作で変わる状態:
別の仕組みで管理する

たとえば、動画の「いいね数」や「選択中カテゴリ」は、ユーザー操作で変わる可能性があります。

そのような状態は、次の章以降で扱うUIや状態管理の考え方につながります。

この節で覚える対応表

考え方Dartでの表現YouTube風アプリでの例
変更させないfinalfinal int views;
固定データconstconst Video(...)
外から直接触らせにくくする_viewsprivate風のproperty
表示用の値を作るgetterviewLabel
条件に応じて表示を変えるifisLive で分岐
UIをシンプルにするText(video.metaLabel)表示ルールをVideoにまとめる
役割を分けるclass / WidgetデータはVideo、見た目はWidget

確認問題1

カプセル化とは、どのような考え方ですか。

答え

データをむやみに外から変更させず、安全に使える形にまとめる考え方です。

使う側は、classが用意したpropertyやgetterを使って、必要な情報を取り出します。

確認問題2

final int views; は、なぜ安全な設計につながりますか。

答え

views の値を作ったあとに変更できないためです。

再生回数が途中でマイナスになったり、意図せず変わったりすることを防ぎやすくなります。

確認問題3

viewLabel をgetterとして用意するメリットは何ですか。

答え

再生回数を表示用の文字に変換するルールを、Video classの中にまとめられることです。

UI側では、Text(video.viewLabel) と書くだけで済みます。

確認問題4

metaLabel は何をするgetterですか。

答え

通常動画なら、再生回数と投稿日をまとめた文字を返します。

ライブ動画なら、ライブ配信中 を返します。

確認問題5

UI側に計算を書きすぎると、どのような問題がありますか。

答え

画面を作るコードが読みにくくなります。

また、同じ表示ルールを複数の場所に書くと、あとから修正するときに大変になります。

この節のまとめ

この節では、カプセル化を使って、Video データを安全に扱う考え方を学びました。

カプセル化は、難しい言葉ですが、初心者向けには次のように理解するとよいです。

データを安全にまとめる。
外から勝手に変更されにくくする。
表示用のルールをclassの中にまとめる。
使う側は、用意されたものを使うだけにする。

YouTube風アプリでは、Video classが動画データの安全な部品になります。

class Video {
  const Video({
    required this.title,
    required this.views,
    required this.publishedAt,
    required this.isLive,
  });

  final String title;
  final int views;
  final String publishedAt;
  final bool isLive;

  String get viewLabel {
    if (views >= 10000) {
      return '${(views / 10000).toStringAsFixed(1)}万回視聴';
    }

    return '$views回視聴';
  }

  String get metaLabel {
    if (isLive) {
      return 'ライブ配信中';
    }

    return '$viewLabel・$publishedAt';
  }
}

この節で一番大切なこと

Video classは、動画データを安全に持ち、画面で使いやすい形に整える部品である。

次の 3-7 では、ListMapclass を組み合わせて、動画一覧を操作する方法を学びます。

人気動画だけを取り出す、カテゴリ別に分ける、ライブ動画だけを表示する、といった処理につなげていきます。

教材トップへ戻る