🎯 最終演習

Dartで1日の献立からカロリーを集計してみよう(総合演習)

ここまでの Dart レッスンで学んだことを全部つないで、動くものを1つ完成させます。新しい構文は出てきません。クラス・名前付き引数・リスト・マップ・null安全・拡張メソッド・switch——すでに練習したものだけで組み立てます。

作るのは1日の献立のカロリー集計です。食事の時間帯ごとにまとめ、目安と比べ、たんぱく質を合計して、一覧にします。

この演習の主題は2つです。——名前付き引数null安全。どちらもDartの中心にある仕組みで、Flutterのコードを読むときに必ず出会います

そして「調べていない栄養値」をわざと混ぜてありますnullを0で埋めてしまうと、「0gだった」と「調べていない」が区別できなくなります——その違いをどう守るかを、実際に書いて確かめます。

このページではコードを実行していません。かわりにお手本と同じコードを書けたかを、その場で判定します(判定はあなたのブラウザの中だけで行われ、どこにも送信されません)。

表示している実行結果は本物です。このページを作るときに、実際のDartで走らせて得た出力をそのまま載せています。

完成イメージ

最後まで進むと、このコードが完成します。下の実行結果は、このページを作るときに開発機で実際にコードを走らせて得たものです(このページ上では実行していません)。

===== 今日の献立 =====
朝食  407 kcal(目安500)
  ごはん         234 kcal  3.5g
  味噌汁          40 kcal  2.2g
  焼き鮭         133 kcal  22.0g
昼食  628 kcal(目安700)
  幕の内弁当     620 kcal     ——
  コーヒー         8 kcal  0.2g
夕食  679 kcal(目安700)
  豚の生姜焼き   380 kcal  24.5g
  サラダ          65 kcal  1.8g
  ごはん         234 kcal  3.5g
間食  210 kcal(目安なし)
  どら焼き       210 kcal     ——
------------------------------------------
品数        : 9品
合計        : 1924 kcal(目安 2000 kcal)
たんぱく質  : 57.7g(未調査2品を除く)
目安まであと76 kcal です。

STEP 1料理をクラスにして、名前付き引数で作る

🎯 このステップのゴール: requiredとthis.を使い、呼ぶ側で意味が読める書き方にする

まずデータの形を決めます。料理1品が持つのは「名前・時間帯・カロリー・たんぱく質」の4つです。

コンストラクタの書き方に注目してください。

Dish({required this.name, required this.time, required this.kcal, this.protein});

波括弧で囲むと名前付き引数になります。呼ぶときはDish(name: 'ごはん', time: ..., kcal: 234)——どの値が何なのか、呼ぶ側のコードだけで分かります

普通の引数だとこうなります。Dish('ごはん', MealTime.breakfast, 234, 3.5)——最後の3.5が何なのか、クラスの定義を見に行かないと分かりません。しかも順番を入れ替えても、型が同じなら気づけません

Flutterのコードが名前付き引数だらけなのは、この読みやすさのためです。

required「省略できない」という指定です。付けないと、呼ぶ側が書き忘れてもコンパイルが通ってしまいます。必ず要るものには必ず付けてください。

this.nameと書くだけで「受け取った値をそのままフィールドに入れる」という意味になります。Dish(String name) { this.name = name; }のような代入を書く必要がありません。

final「作ったあと変わらない」——献立の記録なので、あとから書き換わっては困ります。

そしてdouble? protein?「調べていない料理がある」ことを型に書いています。requiredを付けていないので省略できます

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
品数: 9品
1品目: ごはん 234kcal
幕の内弁当のたんぱく質: null

STEP 2nullを「無い」まま扱う

🎯 このステップのゴール: ??で計算し、nullのままの件数も数える

たんぱく質を合計します。ところがdouble?なので、そのままでは足せません

sum + dish.proteinと書くと、The argument type 'double?' can't be assigned to the parameter type 'num'と怒られます。

これがDartのnull安全です。?の付いた型は、中身があるか確かめるまで使えません。他の多くの言語なら実行時まで気づけないところを、書いた瞬間に止めます。

解決は??です。dish.protein ?? 0.0——nullなら0.0を使う」という意味になります。

ただし、ここで何が起きたかを意識してください。?? 0.0と書いた瞬間、「調べていない」が「0gだった」に化けます。合計だけ見れば同じですが、「未調査が何品あるか」は永遠に分からなくなります

だからこの演習では、合計とは別に「未調査の件数」も数えていますmenu.where((dish) => dish.protein == null).length——失われた情報を、別の形で残しておくのです。

「合計3g」とだけ出すのと、「合計3g(未調査2品を除く)」と出すのとでは、読む人に伝わることがまったく違います。

!という記号もあります。dish.protein!と書けば「nullではないと私が保証する」——外れると実行時に落ちますDartがせっかく用意した仕組みを自分から捨てることになるので、まず使わないでください。

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
たんぱく質: 57.7g
未調査: 2品
この合計に未調査の2品は入っていません

STEP 3switchで時間帯の名前を決める

🎯 このステップのゴール: enumを網羅し、defaultを書かずに済ませる

時間帯に日本語の名前を付けます。switch (time)で振り分けます。

ここでdefault:を書いていないのに注目してください。普通なら「書き漏らしたら危ない」と思うところです。

ですがこの場合は、書かないほうが安全です。——MealTime4つしか値を持たないenumで、その4つ全部にcaseを書いてあります。Dartは「全部書いてある」と分かっているので、文句を言いません

効いてくるのは、あとで時間帯を1つ足したときです。MealTimelateNightを追加すると——このswitchで警告が出ます。「新しい値が扱われていない」と教えてくれるのです。

もしdefault: return 'その他';と書いていたら、この警告は出ません。新しい時間帯が黙って「その他」になり、誰も気づきません

これが「defaultを書かないほうが安全」な理由です。enumを扱うswitchでは、あえて全部書き出して、defaultを省く——覚えておく価値のある書き方です。

逆に整数や文字列を扱うswitchではdefaultが必須です。取りうる値が無限にあるので、書き出しきれません。enumのときだけの話だと区別してください。

caseですぐreturnしているので、breakを書き忘れて次に流れ込む事故も起きません

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
朝食: ごはん
朝食: 味噌汁
朝食: 焼き鮭
昼食: 幕の内弁当
昼食: コーヒー
間食: どら焼き
夕食: 豚の生姜焼き
夕食: サラダ
夕食: ごはん

STEP 4Mapで目安を引く——キーが無い場合ごと

🎯 このステップのゴール: マップの取り出しがnullを返すことを、型で受け止める

時間帯ごとの目安カロリーをマップで持ちます。Map<MealTime, int>——時間帯をキーに、カロリーを値に

ただし間食には目安を決めていません。マップに入れていないので、timeGoal[MealTime.snack]nullを返します。

ここがDartのマップの大事な性質です。——キーが無いとエラーになるのではなく、nullが返ります。そして取り出した値の型は必ずint?になります。

だからMap<MealTime, int>から取り出しても、intではなくint?です。「キーがあるとは限らない」ことが、型に現れているのです。

final goal = timeGoal[time];と受けたあと、goal == nullで分けています。

goal ?? 0としないのが要点です。目安0kcalということはありえません。「目安を決めていない」を0にしてしまうと、必ず超過と判定されます——意味のない警告が毎回出ることになります。

「無い」と「0」は違う——ステップ2と同じ話が、別の形でまた出てきました。これはDartを書くうえで何度も出会う判断です。

表示する順番はtimeOrderという別のリストで持っています。マップの順序に頼らないためです。Dartのマップは入れた順を保ちますが、「朝・昼・夕・間食」という意味のある順に並べたいなら、自分で持つほうが確かです。

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
MealTime.breakfast: 407 kcal / 目安500 kcal → 範囲内
MealTime.lunch: 628 kcal / 目安700 kcal → 範囲内
MealTime.dinner: 679 kcal / 目安700 kcal → 範囲内
MealTime.snack: 210 kcal(目安なし)

STEP 5拡張メソッドでリストに機能を足す

🎯 このステップのゴール: 既存の型に、自分の都合のよいメソッドを後から付ける

合計を出すたびにforを書くのは面倒です。menu.totalKcalと書けたら楽です。

ところがListはDartが用意した型で、中身を書き換えることはできません

そこで拡張メソッドです。extension MenuSummary on List<Dish> { ... }——Dishのリストに、この機能を足す」という宣言です。

これでmenu.totalKcalと、元からあったかのように書けます

on List<Dish>と型を絞っているのが要点です。on Listにすると、数値のリストにも文字列のリストにもtotalKcalが生えてしまいます——意味のない場所に候補が出て、かえって邪魔になります。

中身はfoldです。fold(0, (sum, dish) => sum + dish.kcal)——「0から始めて、1つずつ足していく」という意味です。

最初の0が大事です。これが無いと、空のリストのときに返す値が決まりませんfoldは初期値を必ず渡させることで、空でも落ちないようにしています。

knownProteinではfold(0.0, ...)小数の0から始めています0と書くと整数のリストとみなされて型が合いません。

int get totalKcal => ...get「括弧を書かずに使えるようにする」指定です。menu.totalKcal()ではなくmenu.totalKcal——計算というより「その値」だと感じられるものに向きます

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
合計: 1924 kcal
たんぱく質: 57.7g
朝食: 3品 407 kcal
空のリスト: 0 kcal

STEP 6ひとつの関数にまとめて仕上げる

🎯 このステップのゴール: reportにまとめ、日本語の幅を揃えて表にする

最後の仕上げです。ここまでの部品をreportという1つの関数にまとめます

桁揃えにpadRightを自分で書いています。DartにもString.padRightはありますが、日本語では揃いません

理由は文字の幅です。「ごはん」は3文字ですが、等幅フォントでは英数字6文字ぶんの幅で表示されます。padRight(14)文字数で数えるので、日本語の行だけ長くなります。

s.runesで1文字ずつ見て、0x80より小さければ1、そうでなければ2として数え直しています。

runesを使っているのに理由があります。DartのStringを添字で見るとUTF-16の単位が返り、絵文字などが2つに割れます。runesなら文字として正しく1つずつ取り出せます。

数値の桁揃えはdish.kcal.toString().padLeft(4)です。数字は半角なので、こちらは標準のpadLeftで揃います

toStringAsFixed(1)小数第1位までに固定します。そのまま出すと57.699999999999996のような値が見えることがあります。

締めはdiffの正負で分けます。「あと何kcal」と「何kcal超過」を出し分ける——数字だけ出して終わりにせず、それで、どうなのかに答えるのが道具としての親切さです。

完成です。dailyGoal1800に変えてみてください。締めの一言が「超えています」に変わります。

献立に一品足してみるのもよい確認になります。protein:を省いて追加すれば、未調査の件数が増えます——ステップ2で「別に数えて残しておいた」ものが、ちゃんと働くのが見られます。

📖 お手本コード
✍️ あなたのコード
コードを書いたら「答え合わせ」を押してください
実行結果(開発機で実行したもの)
===== 今日の献立 =====
朝食  407 kcal(目安500)
  ごはん         234 kcal  3.5g
  味噌汁          40 kcal  2.2g
  焼き鮭         133 kcal  22.0g
昼食  628 kcal(目安700)
  幕の内弁当     620 kcal     ——
  コーヒー         8 kcal  0.2g
夕食  679 kcal(目安700)
  豚の生姜焼き   380 kcal  24.5g
  サラダ          65 kcal  1.8g
  ごはん         234 kcal  3.5g
間食  210 kcal(目安なし)
  どら焼き       210 kcal     ——
------------------------------------------
品数        : 9品
合計        : 1924 kcal(目安 2000 kcal)
たんぱく質  : 57.7g(未調査2品を除く)
目安まであと76 kcal です。

よくある質問

このページではコードを実行しないのですか?

していません。Dartはブラウザの中では走らせられないためです。このページではお手本と同じコードを書けたかどうかを判定する形にしています(判定はあなたのブラウザの中だけで行われ、コードはどこにも送信されません)。

ただし表示している実行結果は本物です。このページを作るときに実際のDartで走らせた出力を、そのまま載せています。

?? と ! は、どちらを使えばいいですか

!は使わないでください、というのが基本の答えです。

!は「nullではないと私が保証する」という意味で、外れるとその場で落ちます。Dartがせっかく用意した仕組みを、自分から捨てていることになります。

??で代わりの値を決めるか、if (x != null)で早めに分けるか、たいていはこのどちらかで書けます

どうしても!を書きたくなったら、その値がなぜ?付きの型なのかを見直すほうが早いことが多いです。

null を 0 に置き換えてはいけないのですか

合計を出すだけなら構いません。問題はそのあと区別できなくなることです。

この演習でいえば、たんぱく質を?? 0.0で足した瞬間に、「0gだった料理」と「調べていない料理」が同じになります。合計だけ見れば同じでも、「この数字はどこまで信用できるか」が答えられなくなります

だからこの演習では、合計とは別に未調査の件数も数えています?? 0と書くときは、失われる情報を別の形で残せないかを一度考えてみてください。

enum の switch で default を書かなくていいのですか

enumのときは、書かないほうが安全です。

すべての値にcaseを書いてあれば、Dartは「全部扱われている」と分かるので文句を言いません。そしてあとでenumに値を足したとき、警告で教えてくれます

defaultを書いてしまうと、新しい値が黙ってそこへ落ちて、誰も気づきません

ただし整数や文字列を扱うswitchではdefaultが必要です。取りうる値が無限にあるので、書き出しきれないからです。

🚩 次はこのカテゴリへ

Dartのnull安全と、Kotlinのnull安全はほとんど同じ考え方です。?で「無いかもしれない」を型に書き、??で代わりを決める——Kotlinでは?:という記号になるだけです。片方を知っていると、もう片方はすぐ読めます。Kotlinカテゴリの最終演習では、記録し忘れのある歩数データを集計します。

Kotlinのレッスンへ進む
広告スペース(バナー)
広告スペース(記事内)