1
1

(前編・文法やビルトインの機能中心)数ヵ月くらいDartとFlutterを触ったので個人の所感と学んだことを全体的ににまとめてみる

Posted at

お仕事で数か月程度DartとFlutterでのスマホアプリ開発をしていたので触ってみて感じた所感や学んだことなど、追加で調べたことなどを雑多に全体的にまとめておきます。
(個人の復習や知識の漏れの補完・その他チームメンバーへの共有も兼ねて)

※DartやFlutterの基礎であったり、ビルドインやサードパーティのライブラリ含め利用しているものも全体的に触れていきます。

※色々まとめていたら長くなったので前編後編で分けます。本記事では所感やDart周りなどを中心に触れ、後編はFlutterの細かい点も触れていきます(考えなしに書いていたらとても長くなったのでほぼ自分の勉強用の記事となってきています・・・w)。

前置き

専任のアプリクライアントエンジニアやフロントエンジニアなどをやってきているわけではないので専門の方々からすると色々知識が荒い点はご了承ください(仕事でサーバーやクラウドなどを触っている時間なども多めです)。

もし専門の方から見て間違いなど気づかれましたらこっそりコメントなどで優し目にマサカリ投げていただけますと幸いです。

そもそもFlutterって?

  • Googleによって開発されたOSSのクロスプラットフォーム向けのエンジン(フレームワーク)です。
    • Windows、iOS、Android、web・・・と様々なプラットフォーム向けのアプリなどを開発することができます。
  • 言語は同じくGoogleによって開発されたDartという静的型付け言語を使います。
  • 本記事執筆時点でGitHubのスター数が16万を超えており、世界のGitHubのパブリックリポジトリの中で19位くらいに位置しており、大分人気を博している印象です。

image.png

  • ネイティブほどでは無いですが必要十分には感じるくらいの良いパフォーマンスを出してくれます。
  • Google製なだけあってマテリアルデザイン関係のウィジェットなどが非常に充実しており、マテリアルデザインなどを採用する場合用意されているものを活用して効率的にアプリを作成することができます。

雑な所感

DartとFlutterの全体的な個人の所感について以降の節で触れていきます。

Dartで書いてみての所感

Dart言語自体はすんなりと書けるようになった(学習コストは低めに感じる)印象です。少なくともRustとかよりも遥かに楽に手に馴染んだ印象です。

雰囲気的にはTypeScriptとかUnityとかでのC#とかSwiftとか型アノテーション付きのPythonとか、もしくは古の時代に滅んだActionsScript3とかの静的型付け言語を触っていた経験のある方ならスムーズに手に馴染みそうな気がしています。

Dart3系になってからはnull安全とかもしっかりしていますし、Rustとかほどきっちりしている・・・という感じでもないですがサクサク書けて且つ必要十分に感じる程度には安全に書ける・・・形でバランスは良さそうに感じます。

世の中的にはDartの好き嫌いは結構分かれていそうな印象ですが個人的には割と気にいってきています。

Flutterを使ってみての所感

Dartの方はすぐに手に馴染んだのですが割とFlutter側は序盤は苦戦しました。

非常に長いことスマホアプリとかは開発しておらず、長期間サーバーサイドとかのお仕事がメインだったので浦島状態だったためReactなどもキャッチアップしていなかった勢なのでその辺も影響が大きかったのかもしれません(逆に言うとReactとかに慣れている方はすんなりと使えるようになるのかもしれません)。

状態管理や様々なウィジェットとかも色々迷ったりしましたし、マテリアルデザインの知見とかもかなり浅かったといった面もFlutterキャッチアップの大変さに絡んでいたかもしれません。

Dart + Flutter本を1冊消化した時点ではDartは抵抗感は大分無くなっていましたが、Flutter側はそれくらいでは全然分からないことだらけで、その後しばらく実際に無理やり手を動かし続けていたらやっとこ馴染んできた感じがあります。

まだまだ日々新しく知る発見とかも多いですし全然使いこなせている感じはしませんが、ただし人気なだけあって凄い便利と感動することもありますしサードパーティのライブラリなども含めるとウィジェットやアニメーションなどの充実っぷりも凄いなと思うので慣れてきたら結構効率的に作業できそうとは思っています。

一方でifやforとかでネストしていくわけではないので認知的複雑度がやばくなる・・・という感じでもないのですが、一方でchild引数とかでどんどんインデントが深くなる書き方が多くなる・・・というのは最初ちょっと違和感を感じました(慣れてきたら気にならなくなってきました)。

この辺はまあHTMLとかでもぼちぼちインデントする形で書く感じですしまあ慣れの問題か・・・と思っています。

また、用意されているウィジェットの調整が効くパラメーター等は非常に充実していますが、それらのパラメーターで対応できる範囲を超えたカスタマイズとかが要件的に必要になってくると自前で色々書かないといけなくなるのでFlutterの効率的な良さはぐっと減るとは思います。要件的に特殊なUIが多い・・・とかの場合には本当にFlutterが最適なのかなどを考える必要があります。

私は気にならないレベルなのですがアプリサイズなどに関してはネイティブと比べるとある程度増えたり、パフォーマンスに関してもネイティブと比べると若干劣るのは確かです。また、ネイティブで導入された最新機能なども必ずしもすぐにFlutter側で実装されるという感じではなく、ネイティブと比べると利用可能になるまで若干待つ必要などもあります。

その辺よりもクロスプラットフォーム対応による工数削減やホットリロードとかの便利機能の面で私は魅力を感じていますが、一方で要件的にその辺のアプリサイズ削減などが優先といった場合にもFlutterの採用が正解なのか等は考える必要があります。

Flutterのホットリロードは割と感動した

Flutterを触っていて個人的に一番感動したと言える点がホットリロードです。コードを保存した瞬間にデバッグビルドで起動しているアプリが即時で更新されます。

ちょっとした設定値の変更やレイアウト調整などがエディタ上でCtrl + Sでコードを保存したら即時で反映されます。

特定の画面とかで作業している場合コード変更 -> ビルド -> その画面まで手作業で遷移・・・という対応がぐっと減りますし、その画面を開いたまま何度もコード調整と即時反映を繰り返すことができます(インクリメンタルビルドとかでビルドがさくっと終わるような場合でも画面遷移などが手作業で何度も発生すると地味に時間が取られるのでホットリロードでさくっと調整分などが確認できるのはとても快適に感じます)。

ただ、コードの修正全てを反映できるわけではなくもっと大元から再実行しないといけなくなるケースがあります(メソッドとかに引数を加えた影響で一度そのメソッドを通さないといけないもののホットリロードだけだとその辺を通らないケースなど)。

そういった場合にはホットリスタートと呼ばれるものでデバッグビルドのアプリは起動したまま通常の起動と同じプロセスを通すことができ、こちらもホットリロードのようにごく短時間で処理がされます(ただし画面は起動直後の画面に移ってしまいます)。

ホットリロードとホットリスタートを使うことで通常のビルドをする回数がかなり少なくなったように感じます(VS Codeを再起動した際などには通常のビルドを再度通すのが必要です)。

なお、ホットリロードとホットリスタートはスマホ端末にデバッグビルドでアプリを入れた時にもデスクトップ上でプレビューしている時と同じように使用することができます。こちらもアプリを入れ直してアプリを起動しなおす・・・といったフローを踏まずにコードを保存した瞬間などに即時でスマホ上に反映されます。スマホ上で特有の挙動をするもの等の効率的な確認や調整にとても便利です。

GitHub Actions上でのFlutterのCI/CDの整備がとてもシンプルで良かった

ゲームなどに絡んだ会社に所属しているのでアプリクライアントに関してはクラウド上のCI/CDの整備とかが結構面倒で(ゲームエンジンとかの感覚だと)インストール時間なども考えるとJenkins上で組むケースが多い・・・という印象だったのですが、Flutterをクラウド版のGitHub Actions上で扱う際には設定がとてもシンプルで動かせるようになるまでもかなり短時間で終わる形でこの点もとても良いなと思いました(クライアント側のCI/CDの整備がもっと大変なのかと思っていました)。

それこそFlutterを設定して依存関係のライブラリを入れてとりあえずFlutterをUbuntuのrunner上で動かせるようにする・・・とかであれば数行程度YAMLを書く程度で完了しますし、FlutterやXcodeなども含めた環境設定からiOSのビルドとDeployGateなどへのアプリのデプロイなどの諸々を含めても50行くらいのYAMLの記述で完了するというのは中々感動しました(GitHub ActionsとFlutter関係無く、iOSのInHouseの証明書設定とかXcode関係とかは久々過ぎて記憶が曖昧で結構苦戦しましたが・・・w)。

自前でCI/CD用の環境のOSや使うもの(Jenkinsなど)のアップデートなども対応しなくてOKになりますし、環境構築用の設定とかも全部YAML上で残る形になりIaC的な対応もできる点(将来見直した際や複数人で作業したりする際などに環境構築に使ったコマンドなどが全部YAMLに残るのは保守面で良いなと思います)、その他サーバーサイドと同様にGitHub Actionsで統一できる点なども良いと思います。

国内でのFlutter人材の採用のしやすさは何とも言えない

世界的には盛り上がっていそうですしGitHubのスター数やらサードパーティのライブラリの充実具合などやら大分しっかりしている・・・という印象でいます。

一方で国内だと段々とFlutter人材は増えてきたり日本語の記事とかも増えては来ていそうですが、技術者の総数としてはどうなんだろう・・・?というのは何とも言えない感覚でいます(Flutter人材の採用のしやすさ含め。FlutterというかiOS・Androidアプリエンジニアも足りていないとかもありそうですが・・・)。

参考 :

(※なんとなくDartを書きたくなく感じている方も一定数いそうな気配も感じています・・・w)

弊社も採用などかけていますがそんなにバンバンと良い人の応募が来る・・・という雰囲気はあまり感じられていません(採用ブランディング的な面はさておき)。

そのため人材の確保的な面ではなんとも言えないという所感を個人的には持っています。

個々の要素になると割と情報が少ないことがある

英語圏まで調べれば結構色々な記事が見つかったりするのと、ChatGPTやCopilot Chatとかに聞けば割と解決することも多い・・・ところではありますが、一方で機能やウィジェットの数は膨大だったりするためマイナーなところやアップデートで変わったばかりのタイミングで引っかかったりすると調べものをしても解決策が中々見つからない・・・といったケースは何度か遭遇しています。

また、日本語圏で情報が見つからないことも結構あります。

この辺は用意されているウィジェットの豊富さなどの面でしょうがないところもあると思いますがある程度は覚悟しておく必要はあるかもしれません(他の技術でも似たような面はあるとは思いますしFlutterだけの問題という感じでも無いですが・・・)。

※日本語圏の資料の充実とかは今後の年経過で改善するかもしれません。

ウィジェットテストなどの機能はとても良くできていると思った

Flutterだと主に単体テスト・ウィジェットテスト・インテグレーションテストと3種類あります。右に行くほど大がかりになって書いたり保守したりするのが大変になったり処理時間も伸びるイメージです。

単体テストは普通に書くとして、インテグレーションテストは結構書くのも保守するのもきつい、でも単体テストだけだとテストでカバーできる範囲的に不安・・・といった時にウィジェットテストは個人的には良い塩梅に思います。結構数を書いてもまあテスト時間も許容できる範囲に思います。

ウィジェットテストでも結構インテグレーションテストに近いこともできるので境界線は少し曖昧な所もありますが・・・。

挙動的にはヘッドレスでSeleniumやPlaywrightとかを動かすのに近いイメージで、特定のウィジェットを対象としてテストすることもできますしある程度複数のウィジェットが配置されているものに対してテストを行うこともできます(特定のページとかにも実施できます)。

Seleniumとかにもfind_element_by_idとかの関数を始めとして様々な要素検索の機能がありますが、Flutterのウィジェットテストにも特定のウィジェットの検索用の仕組みが色々用意されています。タップ操作やテキスト入力などもできますし、個々のウィジェットの座標を取ったりアニメーションが完了するまで待機する制御だったりとテストをしやすくするための便利機能がたくさん用意されています。

ただしウィジェットテストはエミュレーター上とかで実施するインテグレーションテストと比べると実際のビルドされたアプリとのズレが気になることもあります。そのため手間がかかっても厳密にテストしたい機能とか要件に関しては必要に応じてインテグレーションテストも混ぜるとかも良い選択肢のケースもあると思います。

フォーマッタやLintのチェックなどに関して

フォーマッタやLintチェックなどに関しては公式が提供してくれているもので満足できています。スタイルガイドとなるEffective Dartにも結構沿う形に整形してくれますし、書き方が好ましくない時に警告とかでしっかり教えてくれるのは助かっています。

公式で完成度の高いものをしっかりと提供してくれているのは迷わなくて良いなと思います。

本記事で使うもの

以降の節では実際にFlutter環境を整えたり各機能について触れて行こうと思います。その際には以下のものを使っていきます。

※自宅のPCがWinなのでWin前提で書きますがMacなどの方はインストール周りは別の記事などをご参照ください。

  • OS: Windows 11 (デスクトップ環境)
  • Flutter: v3.19.1(2024-02-22リリース)
  • Dart: v3.3.0
  • VS Code
    • Flutter公式の拡張機能含む

環境設定

FlutterのダウンロードとVS Codeでの設定

※VS Codeに関しては既にインストール済みの前提で進めます(VS Code自体のインストールなどは本記事では割愛します)。

※必要要件とかは以下のドキュメントのSystem requirementsとかをご参照ください。

まずはVS CodeでFlutter公式の拡張機能をインストールします。VS Codeの拡張機能のUIでFlutterと検索して青バッジの付いている公式のものをインストールします。

image.png

image.png

続いてFlutter SDKを以下のページの「flutter_windows_x.xx.xx-stable.zip」ボタンを押してダウンロードします。zipファイルは1GBくらいあるのでそれなりにダウンロードに時間がかかります。

image.png

zipの展開先は任意ですがそれなりにディスク容量が必要になるので余裕のあるディスクドライブとかを選択しておきます。今回は雑にD:\workspace\というフォルダに展開しました。flutterというフォルダ名で展開されるので私の環境の場合D:\workspace\flutter\というパスにFlutter SDKが展開されています。

※特殊文字やスペースが含まれるパスは避けるように、とドキュメントに書かれているのでその辺りは避けておきます。

今度はVS Code上でCtrl + Shift + Pを押してコマンドパレットを開き、flutterと検索します。

image.png

検索結果に表示される「flutter: New Project」を選択します。恐らくVS CodeからSDKが見つからないと以下のようなエラーが右下に表示されると思います。SDKのダウンロード自体は完了しているのでLocate SDKボタンをクリックします。

image.png

Flutter SDKが含まれているフォルダを選択するためのダイアログが開かれるためzipを展開したフォルダ(私の場合はD:\workspace\flutter\)を選択して「Set Flutter SDK folder」をクリックします。

image.png

Flutterのパス設定を行う

Flutterコマンドを使えるようにするためFlutterのバイナリが配置されているパスを環境変数のPATHへと追加していきます。

対象のパスは展開したFlutter SDK直下のbinフォルダのパスが該当します。私の場合はD:\workspace\flutter\bin\となります。これをPATHへ追加します。

Windowsの検索窓で「環境変数」などと検索すると「システムの環境変数の編集」という検索結果が表示されるのでクリックします。

システムのプロパティというダイアログが表示されるので右下の方にある「環境変数(N)」というボタンを選択します。

image.png

「<ユーザー名>のユーザー環境変数」という方で変数がPATHとなっている行を選択して編集ボタンを押します。

image.png

「新規」ボタンをクリックするとパスの入力ができるのでFlutter SDK内のbinフォルダのパス(私の場合D:\workspace\flutter\bin\)を入力してOKをクリックします。

image.png

これで新しく開いたPowerShellなどでFlutterのコマンドが実行できるようになります。試しにPowerShellやコマンドプロンプトなどを新しく開いて$ flutter --helpとコマンドを打つと色々メッセージが表示されます。

image.png

環境設定が正しくできているかを確認してくれるflutter doctorコマンドを活用していく

Flutterには環境設定が正しくできているかを確認してくれる$ flutter doctorというコマンドが存在します。

実行してみると各設定が出来ているか出来ていないかを表示してくれます。対応が足りていないものなどはどのような対応が必要なのかの説明なども表示してくれます。

以下のコマンド結果のスクショではまだAndroid関係の整備をしていないのでAndroid Studio周りなどで引っかかっています。

image.png

※同僚の方にご共有いただいたのですが、もしflutter doctorのコマンドを最初に実行してみても結果が返ってこない・・・といった場合には再起動したらちゃんと帰ってくるようになったそうです。

Android Studioなどの対応

flutter doctorコマンドでAndroid Studioなどの箇所が引っかかったのでその辺を対応していきます。

まずはAndroid Studioをインストールしていきます。以下のページからインストーラーのダウンロードボタンをクリックします。ダウンロードファイルは約1GBくらいあります。

image.png

ダウンロードが終わったらインストーラーを起動してインストールしていきます。特に理由が無ければ設定そのままでインストールを進めて問題ないと思います。

インストールされたAndroid Studioを起動します。最初は設定をインポートするかが問われますが、Android Studioのインストールが今回使う環境では初なので「Do not import settings」を選択してインポートせずに進めます。

image.png

その後はログ送信を行うかどうかなどのダイアログが出ますがその辺は各々の判断で良いと思います(今回は送信しない設定で進めました)。Nextボタンを押したりしているとインストールタイプの選択画面となりますがStandardを今回は選択します。

image.png

あとは利用規約などに同意していればインストールインストールが進むのでしばらく待ちます。

image.png

インストールが終わると以下のような画面になります。

image.png

左のメニューの「Plugins」を選択し、検索窓でFlutterと入力してFlutterのプラグインを検索しInstallボタンでインストールを行います。

image.png

インストール時に警告が出ますが特に問題ないのでそのまま進めます。

image.png

Dart関係もインストールが必要と言われるのでそちらもInstallボタンをクリックしてインストールします。

image.png

インストール後「Restart IDE」というボタンが表示されるので一応押してAndroid Studioを再起動しておきます(この辺はVS Codeを基本的に使っていくので要らないかもしれません)。

Android SDK Command-line Toolsのインストール

続いてAndroid SDK Command-line Tools周りの対応をしていきます。Android StudioのProjectsのメニューの画面の下の方にある「More Actions」メニューをクリックして「SDK Manager」をクリックします。

image.png

設定画面が開かれます。左のメニューでAndroid SDKの部分が選択されている状態でSDK Toolsのタブを選択します。Android SDK Command-line Toolesという項目でチェックが外れているのでチェックを入れて右下のApplyをクリックします。

image.png

この時点で再度$ flutter doctorコマンドを実行してみましょう。

image.png

赤文字のエラーは無くなりました。ただ1か所以下のようにAndroidのライセンス関係を承諾する必要がある旨の警告が出ています。

[!] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses

Androidのライセンスの同意を行う

flutter doctorの警告で表示されていく通り、$ flutter doctor --android-licensesというコマンドを実行することでライセンスの同意を行うことができます。

コマンドを実行するとライセンス内容が表示されるので確認して問題がなければyを入力してEnterを押していけば同意作業が進んでいきます。

image.png

最後まで同意が終わったら再度flutter doctorコマンドを実行してみます。

image.png

全てのチェックを通過し、「• No issues found!」と表示されたことを確認できました。これで必要な基本的なインストール周りなどは対応が完了となります。

その他環境によってはインストールが必要になるかもしれないものなど

私の場合昔に入れたと思われるVisual Studio関係が最初からチェックを通過していましたが人によってはこの辺もインストールが必要になってくると思われます。その場合はflutter doctorコマンド上に表示されるメッセージもしくはネット上の記事などを参照してご対応ください。

プロジェクトの作成

新規のFlutterプロジェクトを作成するには$ flutter createコマンドやVS Code上での操作などいくつかの方法があります。

普段はコマンドで作成していたのですが本記事ではお試しでVS Codeを使う形でプロジェクトを作成してみます。

VS Code上でCtrl + Shift + Pを押してコマンドパレットを開き、flutterと入力してflutter関係のコマンドを検索します。Flutter: New Projectというメニューが表示されるのでそちらを選択します。

image.png

いくつか候補が出てきます。どのみち生成される初期コードとかを編集していけば問題ないのでApplication、Empty Application、Skeleton Applicationどれでも問題なさそうですが今回は一番上のApplicationを選択します。

image.png

プロジェクトを生成するフォルダを選択するためのダイアログが表示されるので任意のフォルダを選択します。今回はD:\workspaceフォルダとしました。

image.png

コマンドパレットの位置にプロジェクト名の入力用のフォームが表示されます。今回は雑にtest_flutter_projectと設定します。

image.png

名前を入力してEnterを押すとVS Codeが再起動しつつFlutterの作成したプロジェクトがワークスペースに設定された状態で起動します。
※必要に応じて切り替わる前のVS Codeのワークスペース設定の保存などはご対応ください。

image.png

生成されたmain.dartファイルが開かれた状態となりますが、このファイルのmain関数部分がこのプロジェクトのエントリーポイントとなります。

image.png

デバッグビルドでアプリの起動を行う

VS Code上からのFlutterのデバッグビルドなどによるアプリ起動はサイドメニューのRun and Debugメニューから行います。

image.png

対象のメニューを選択すると以下のスクショのようなUIが表示されます。青いRun and Debugボタンをクリックするとデスクトップアプリのデバッグビルドとしてアプリのビルドがスタートします。

image.png

※Flutterのアプリ起動自体はマシンスペックにもよりますが結構時間がかかります。ただし一度起動してしまえば後は再度最初からビルドしなくともホットリロードやホットリスタートでコード変更分を反映できるのでぐっとビルド回数は少なくて済むため起動した後はビルド時間は気にならなくなります。

ビルドが終わると以下のスクショのようにサンプルプロジェクトのアプリが起動します。内容としては右下のボタンを押すと真ん中のカウントが1ずつ加算されていくだけのシンプルな内容になっています。

image.png

また、起動時にプロファイラー関係について開くかどうかなどの確認がVS Codeの右下に表示されたと思います。ChromeのDevToolsのようにVS Code上でFlutter用に様々なツールが用意されています。この辺りについては後々の節で詳しく触れます。

外部ライブラリパッケージについて

プロジェクトをVS Codeやコマンドなどで作成した場合、プロジェクトフォルダの直下にpubspec.yamlというファイルが作成されていると思います。このファイルはプロジェクトの設定を管理するためのファイルで、アプリ名やFlutterなどのバージョン、その他外部のライブラリパッケージの設定や画像などの埋め込むアセットの定義を行うことができます。

image.png

以降の節では外部ライブラリパッケージについて色々触れていきます。

pub.devについて

外部ライブラリパッケージに関してはDartではpub.devというサイトで情報が公開されており、様々なライブラリの情報を確認することができます。Pythonで言うところのPyPIのようなサイトになります。

image.png

サンプルとしてflutter_svgというライブラリで説明をしていきます。

image.png

まずページ上部について見てみます。上部にはパッケージ名と現在公開されている最新バージョンの文字列が表示されます(スクショの2.0.10+1の部分が最新バージョンの文字列になります。)。

image.png

また、「Published 8 days ago」といった部分は最後にパッケージのアップデートが公開された日が何日前かといった表示になります。この表示がかなり前のパッケージに関しては保守がされなくなりアップデートが止まっている可能性が高いので使用を検討する際には注意が必要になってきます。

「Dart 3 compatible」というチップの表示もありますが、これはDart3系に対応している場合に表示されます。利用の多いライブラリパッケージに関してはほぼほぼ付いている気もしますが付いていない場合には最近は(少なくとも新規のプロジェクトでは)Dart3系を利用されているプロジェクトが大半だと思いますのでそのライブラリパッケージは互換性的に利用できない可能性が高くなります。また、Dart3系になってから結構期間が経っているのでまだDart3系のサポートがされていない場合、そのライブラリパッケージはメンテナンスされているかの面で使用は警戒した方が無難かもしれません。

以下のスクショのように、Versionsタブを表示すると各バージョンとリリース日が表示されます。

image.png

各バージョンのリリース日を確認することで、どのくらいの頻度でバージョンアップがされているのかを確認できます。各バージョン間でかなり期間が空いているかどうかもそのライブラリの利用を決めるかどうかの指標の一つとして使えるかもしれません。

右の方にあるLIKESやPOPULARITYは対象のライブラリパッケージがどのくらい多くのユーザーに使われている人気のパッケージなのかの判断基準の一つとして使うことができます。

image.png

人気のパッケージであればLIKESがたくさん付いて、POPULATIONも100%付近になっています。人気パッケージでもアップデートが日々されて保守されているとは必ずしも限りませんが、指標の一部として個人的にはLIKESが数百、POPULATIONが90%強だと割と使用を考えてもいいのでは・・・という感覚でいます(他の指標なども確認はしたりしますが)。

pubspec.yamlへの外部ライブラリパッケージの記述方法

pubspec.yamlの内容を更新して外部ライブラリパッケージのプロジェクトへの追加を試してみます。

外部ライブラリパッケージを指定する箇所はpubspec.yamlの中でdependenciesとdev_dependenciesの2つがあります。

本記事の手順でプロジェクトを作成した場合dependenciesのセクションとdev_dependenciesのセクションの初期設定は以下のようになっています。

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.6
dev_dependencies:
  flutter_test:
    sdk: flutter

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^3.0.0

dependenciesとdev_dependenciesで何が違うのか?というところですが、dev_dependencies側で設定したものは開発環境(ローカルなど)でのみ反映されます。一方でアプリストアなどで配布するためのリリースビルドなどをした際にはdev_dependenciesで指定したものはビルド結果に含まれなくなります。

そのためdev_dependenciesの方にはリリースビルドなどで含まなくても問題ないもの(使わないもの)を指定していきます。例えばテスト用やLint用などのライブラリパッケージが該当します(リリースビルドでは単体テストやLintなどを基本的に動かさないため)。これによってリリースビルドで使わないパッケージのコードの分が含まれなくなりビルド後のアプリサイズで無駄が減るようになっています。

リリースビルドなどにも含めたいものに関してはdependenciesの方で指定します(通常のウィジェットライブラリなど)。

記法に関しては<ライブラリパッケージ名>: <バージョン>といった形でコロンで繋げて記述します。例えばflutter_svgパッケージでバージョンが2.0.10の場合にはflutter_svg: 2.0.10といった記述になります。

バージョンに^の記号が先頭に付いていた場合には最低バージョンの指定となり、インストールがされていない場合その最低バージョン以降で可能な最新バージョンがインストールされます。たとえばflutter_lints: ^3.0.0となっていれば3.0.0以降のバージョンのflutter_lintsパッケージがインストールされる・・・という挙動をします。

試しにflutter_svgパッケージで^2.0.0というバージョンをpubspec.yamlのdependenciesセクションに追加してみましょう(本記事執筆時点でflutter_svgの最新バージョンは2.0.10+1)。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.6

  flutter_svg: ^2.0.0

前節までで触れたFlutterのVS Codeの拡張子がインストールされていればVS Code上でCtrl + Sを押してpubspec.yamlを保存するとライブラリパッケージの同期が開始されます。VS Codeの右下に同期中を示す表示が以下のスクショのように表示されます。表示されているflutter pub getという記述はライブラリパッケージの同期を行うためのコマンドとなります。

image.png

また、VS CodeのOUTPUTタブには同期処理のコマンドの出力が表示されます(このUIが表示されていない場合にはCtrl + @などで表示することができます)。

image.png

先頭に+記号が付いているものが追加されたライブラリパッケージとなります。flutter_svgも含まれており、且つバージョンが最新の2.0.10+1がインストールされたことが確認できました。

追加した外部ライブラリパッケージの削除方法

追加したライブラリパッケージが不要になった時などに削除するには単純にpubspec.yamlから記述を削除してVS Code上でCtrl + Sで保存するだけです。

試しにflutter_svgパッケージの記述を削除して保存してみます。

image.png

image.png

少し待つと「These packages are no longer being depended on:」というメッセージと共に不要になったライブラリパッケージが一覧表示されます。

また、対象のプロジェクトのリポジトリフォルダでTERMINALもしくはPowerShellなどから$ flutter pub depsというコマンドを打つことでも現在インストールされているパッケージを一覧表示することができ、一覧の中にflutter_svgが含まれなくなっていることが確認てきます。

image.png

VS Codeのプロファイラーなどについて

前節までで少し擦れましたが、VS CodeのプロファイラーはChromeのDevToolsのように色々用意されています(実務だとこの辺りまだしっかり使いこなせていません・・・)。

初回のビルド時に開くかどうかの確認ダイアログが右下に表示されたり、他にもコマンドパレットからも開くことができます。

デスクトップ上でデバッグビルドを起動した状態でコマンドパレットをCtrl + Shift + Pで開きます(起動していない場合は起動時にプロファイラー側で接続されるようになります)。

コマンドパレットでflutter devtoolsなどと検索すると対象のものがヒットするので選択します。

image.png

DevToolsの中でも色々ありますがまずはWidget Inspector Pageを選択します。

image.png

するとChromeのDevToolsの要素タブのようなUIがVS Code上で表示されます。Flutterではウィジェットをchild引数で結構入れ子にしたりして記述していきますが、その親子関係だったりウィジェットのサイズなどを確認することができます。

image.png

親子関係を辿っていって特定のウィジェットを選択してWidget Details Treeのタブを選択したりすると対象ウィジェットの各属性の詳細とかも確認することができます。

image.png

ChromeのDevToolsだと要素をクリックして選択する機能がありますが、FlutterのこのDevToolsでも似たような機能があります。

DevToolsの左上の方にあるSelect Widget Modeというボタンを押して有効化すると、それをウィジェットをクリックしてデバッグビルドのアプリ上でウィジェットが選択できるようになり、且つ選択しているウィジェットのサイズなどの情報が表示されるようになります。

image.png

image.png

※一度クリックした後に別のウィジェットをクリックしたい場合には左下の虫メガネのアイコンが乗っているボタンをクリックすると別のウィジェットを選択できるようになるようです。

ウィジェットの領域をガイドライン表示する

DevToolsの右上の方に|↔|といった表示のアイコンがありますが、これを選択すると各ウィジェットごとにサイズ領域のガイドラインを表示してくれます。

image.png

この機能で境界線とかの無い透明なコンテナとかでどのくらいの領域が設定されているのか等の可視化を行うことができます。

image.png

その他のDevToolsについて

まだ使いこなせていないというのもあり本記事ではDevToolsに関しては深くは触れないため軽く紹介する程度に留めておきますが、Widget InspectorのDevTools以外にも様々なDevToolsがVS Code上で用意されています。

image.png

例えばNetwork Pageでは外部のAPIへのリクエストや外部アセットの読み込みなどのURLや結果のステータスコード、レスポンスのサイズの確認などを行うことができます。

Dartの文法

以降の節ではDartの文法について色々復習しながら触れていきます。

Dartの実行環境について

Dartの文法を学んでいくにあたってVS Code上でFlutterのプロジェクトを使って色々動かしていく・・・というのもいいのですが、本記事ではDartの文法について触れていく際には気軽に試せるようにDartPadというブラウザ上でインストール対応など無しにDartを書いて動かすことの出来るサービスを使っていきます(※Flutterを動かす時にはVS Codeの方を使っていきます)。

image.png

DartPadの代わりにVS Code上で動かして・・・とかでも(若干の調整とかは調べていただく必要は出てくるかもしれませんが)大丈夫かと思いますので利用されるものはお任せします。

DartPadではコードを書いてRunボタンを押すと、少し待っているとコンソール出力の結果が右側に表示されます(スクショのhello 1といった各表示がコンソール出力になります)。

image.png

DartPadはお手軽な実行環境としてとても便利ですが、一方で入力補完とかドキュメンテーションコメントの表示とかはVS Codeとかの方が便利だったり、あとは本記事執筆時点ではランタイムエラーが一括でScript error.と表示されてエラー詳細が表示されない・・・といった面もあるので一長一短な感じではあります(ランタイムエラーの調査とかはDartPadだと中々辛いものがあります)。

Dartのエントリーポイントの書き方

DartPad上の初期コードが以下のようになっているため、まずはエントリーポイントから説明していきます。

void main() {
  for (int i = 0; i < 10; i++) {
    print('hello ${i + 1}');
  }
}

Dartではエントリーポイント(プログラムの開始地点のとなる最初の関数)はmainという関数で扱います。Flutterアプリのエントリーポイントでもファイルことのテストコードでもエントリーポイントはそれぞれmainとなります(後々の節でも触れますがテスト側でもエントリーポイントが必要になります)。

また、Dartでは関数やメソッド定義時には<返却値の型> 関数名(<引数内容>) {<関数の処理>}といったように書きます。前述のコードで指定されているvoidは返却値を返さない関数の返却値の型となります。

エントリーポイントは引数を必要としないためmain()の括弧の中の引数内容は空になっています。

また、関数の処理のスコープを表すにはjsなどと同じように{}の括弧を使います。

コンソール出力

Dartでコンソールになんらかの値を出力したい時にはprint関数を使います。引数に出力したい値や変数などを指定します。

たとえば以下のように書くとコンソールに100が表示されます。

void main() {
  print(100);
}

image.png

※後々の節で詳しく触れますが、関数を呼び出すには<関数名>(引数の指定);といったように書きます。また、関数だけではないのですが処理の最後にはセミコロンが必要になります。

前述のコードでは100という引数を指定しているのでprint(100);という記述になっています。

また、DartではインデントはHTMLとかで良くあるように半角スペース2個が基本となります(公式のフォーマッタが半角スペース2個になっています)。

なお、VS Code上だったり公式のLintでチェックしたりするとprint関数が残っていると警告で引っかかります。

image.png

あくまでprint関数は一時的な確認用などのために使用して、不要になったら削除するのが基本的な運用方針となります。

永続的にコンソール出力を開発中に表示したい場合は別途ロギングライブラリを使います。その辺りを使うとリリースビルドの際にはロギングの処理が無効化されて無駄が無くなったり見やすくなったりといったメリットがあります。

ロギング関係のライブラリパッケージに関して後々の節で触れていきます。

変数宣言

Dartで変数を宣言する場合には型を明示する方法とvarを使って型推論で定義する方法の2つがあります。

型を明示する場合には<型名> <変数名>;といった形で書きます(例 : int myIntValue;)。変数宣言と同時に初期値を与える場合にはイコールで繋いで値を指定します(例 : int myIntValue = 200;)。

void main() {
  int myIntValue = 200;
  print(myIntValue);
}

image.png

varを使って型推論を使う場合にはvar <変数名> = <初期値>;といった形で設定します。型推論で判定する性質上基本的には初期値を与える形で定義します(例 : var myIntValue = 300;)。

void main() {
  var myIntValue = 300;
  print(myIntValue);
}

image.png

型推論を使う形で初期値を与えない場合は警告が出ます。

image.png

初期値を与えない形で変数宣言したい場合にはvarではなく型を明示する形で変数を宣言します。

※この辺りのルールなどはコーディングスタイルガイドの資料などが書かれているEffective Dartなどで触れられていたり、公式のLintチェックなどでもチェックすることができます(Flutterの公式のVS Codeの拡張機能を入れていればVS Code上でもリアルタイムに表示されます)。

以前まとめた記事 :

定数宣言

Dartには定数がconstで定義するものとfinalを使って定義するものの2つがあります。

書き方としては<constもしくはfinalキーワード> <型の指定> <変数名> = <設定する値>;という形で記述します(例 : const String userName = 'myName';)。

値の変更が効かない点を除けば扱いは変数と似た感じになります。

void main() {
  const String userName = 'myName';
  print(userName);
}

image.png

型推論を使って定義したい場合にはconstfinalなどのキーワードは残しつつ型の指定を省略すれば型推論を使いつつ定数を定義することができます。

void main() {
  const userName = 'myName';
  print(userName);
}

image.png

定数なので当たり前ですが再代入とかは効きません。コンパイルエラーとなります。

void main() {
  const userName = 'myName';
  userName = 'yourName';
  print(userName);
}

image.png

constfinalで何が違うのか?という点ですが、constの方はコンパイル時に既に既知の値である必要があります。つまりconstの初期値は10'Hello'といった直接的な値もしくは他のconstの定数になっている必要があります。他の変数を設定したり、もしくは関数の返却値などをconstの値に設定することはできません。

例えば以下のように別の変数がconstの値として設定されているとコンパイルエラーになります。

void main() {
  var userName = 'myName';
  const otherName = userName;
  print(otherName);
}

image.png

関数の返却値が分岐などなく固定値を返すといった場合でも関数を経由する場合はその値はconstとして定義することはできません。

void main() {
  const userName = getUserName();
  print(userName);
}

String getUserName() {
  return 'myName';
}

image.png

一方でconstで設定する値が別のconstの値であればコンパイルが通ります。

void main() {
  const userName = 'myName';
  const otherName = userName;
  print(otherName);
}

image.png

finalの方はconstとは異なり、設定される値は1回まで計算されてその後は変更されない定数値として扱われます。つまり定数の初期値には変数を設定したり関数の返却値を設定してもコンパイルが通ります。

void main() {
  final userName = getUserName();
  print(userName);
}

String getUserName() {
  return 'myName';
}

image.png

定義した後はconstと同じように再代入しようとずるとコンパイルエラーとなります。

void main() {
  final userName = getUserName();
  userName = 'otherName';
  print(userName);
}

String getUserName() {
  return 'myName';
}

image.png

慣れてくるまではconstfinalどちらを使うのか一瞬迷ってしまう・・・といったケースも出てきそうですが、その辺りはVS Code上でFlutterの拡張機能を入れていればconstとして定義すべきケースなのにfinalで定義している場合には警告が表示されます。その辺りをエディタで設定しておいたり、後はCI/CDとかで自動チェックするようにしておけば引っかかったら直すくらいの感覚で大丈夫だと思います。

void main() {
  final userName = 'myName';
  print(userName);
}

image.png

注意点として、(少なくとも本記事執筆時点では)リスト(配列)などを扱う場合、finalで定義していても要素の追加などの更新処理が通ってしまいます(割と初見の時この挙動は違和感が強かったですがJavaとかに慣れている方は恐らく違和感が少ない・・・仕様ですかね・・・?)。

void main() {
  final List<int> myList = [100, 200];
  myList.add(300);
  print(myList);
}

image.png

constを使う場合にはこういったケースはコンパイルが通らなくなります(constの方が厳密な定数といった感じになります)。そのためチェック時の警告を抜きにしても基本的にはconstを使えるケースではconstを使う方が好ましいといった形になります。

lateを使った定数の遅延設定

finalを使った定数定義では別途lateを使って値の設定を定数定義よりも遅らせることができます。

例えば以下のように定数定義と値の設定の行をずらしたりすることができます。

void main() {
  late final int myIntFinalValue;
  myIntFinalValue = 20;
  print(myIntFinalValue);
}

image.png

何に使うのか?という感じですが、Flutterで扱う際にクラスの属性などをlate finalで定義しておいて、初期化用のメソッドなど(例 : initStateメソッドなど)で初期値を与えたりなど、定義と値の設定タイミングをずらしたい一方でなるべく定数として扱いたい・・・といった場合に便利です(初期化を確実に1回だけにしたいといった場合などに)。

なお、late finalで定数を定義した場合、定数定義と値の設定タイミングがずれる都合定数定義時点で型を明示しないと警告が出るようになります(lateを付けない場合には警告が出ません)。

lateを使っている一方で型を明示していないため警告が出る例
void main() {
  late final myIntFinalValue;
  myIntFinalValue = 20;
  print(myIntFinalValue);
}

image.png

また、通常のfinal定義時と同じように複数回値を設定しようとずるとエラーになります。

void main() {
  late final int myIntFinalValue;
  myIntFinalValue = 20;
  myIntFinalValue = 30;
  print(myIntFinalValue);
}

image.png

型について

以降の節では型の基本について触れていきます。

型の基本的な指定方法

変数や定数、引数などへの型の指定はそれぞれの名前の左にスペースを空けて設定します(例 : int myIntValue;)。

変数であれば型の指定だけ行っておいて値の設定は後で行う・・・といった書き方も行うことができます(if文で分岐するケースなど)。

void main() {
  int myIntValue;
  bool isTarget = true;
  if (isTarget) {
    myIntValue = 100;
  } else {
    myIntValue = 200;
  }
  print(myIntValue);
}

image.png

ジェネリックの型の指定方法

ジェネリックな型、例えば文字列の値のみを格納するリスト(配列)といった型を定義するには型名<ジェネリックの型>といった形で書きます(例 : List<String>)。

ジェネリックの型の指定を使うことで「整数のみを格納するリストだったのにうっかり文字列を入れてしまった」とか「辞書のキーは文字列だけの想定だったのにうっかり整数のキーが紛れ込んでしまった」みたいなことを弾くことができ堅牢にコーディングを行うことができます。

また、特定の型だけでなく様々な型に対応できるといった使い方もできるので実装の重複を減らしたり利用時の柔軟性を上げたりといったメリットもあります。

リスト(配列)で値の型を指定するにはList<値の型>と書きます。

void main() {
  List<int> myIds = [10, 20, 30];
  print(myIds);
}

image.png

試しにList<int>と指定したリストの変数に文字列を含めてみるとコンパイルエラーになることを確認できます。

void main() {
  List<int> myIds = [10, 'Hello', 30];
  print(myIds);
}

image.png

マップ(辞書)でキーと値の型を指定するにはMap<キーの型, 値の型>と書きます。

void main() {
  Map<String, bool> myMap = {
    'key1': true,
    'key2': false,
  };
  print(myMap);
}

image.png

試しにキーに不正な型を指定してみてエラーになることを確認してみます。

void main() {
  Map<String, bool> myMap = {
    10: true,
    'key2': false,
  };
  print(myMap);
}

image.png

同様に値に対して不正な型を指定してみてエラーになることを確認してみます。

void main() {
  Map<String, bool> myMap = {
    'key1': true,
    'key2': 20,
  };
  print(myMap);
}

image.png

整数型

整数の型はintとなります。500-1000といった値を設定することができます。

void main() {
  int myIntValue1 = 500;
  print(myIntValue1);

  int myIntValue2 = -1000;
  print(myIntValue2);
}

image.png

加算処理は+の記号や+=の記号で行うことができます。

void main() {
  int myIntValue = 500;
  myIntValue = myIntValue + 300;
  print(myIntValue);

  myIntValue += 200;
  print(myIntValue);
}

image.png

1だけ加算したい場合には++の記号を使うことで対応することができます。

++記号で1だけ加算する例
void main() {
  int myIntValue = 500;
  myIntValue++;
  print(myIntValue);
}

image.png

減算処理は-の記号や-=の記号で行うことができます。

void main() {
  int myIntValue = 1000;
  myIntValue = myIntValue - 300;
  print(myIntValue);

  myIntValue -= 200;
  print(myIntValue);
}

image.png

1だけ減算したい場合には--の記号を使うことで対応することができます。

--記号で1だけ減算する例
void main() {
  int myIntValue = 500;
  myIntValue--;
  print(myIntValue);
}

image.png

除算処理は/の記号や/=の記号で行うことができます。ただし結果の値は浮動小数点数(double型)となるため、結果は他の変数に格納するなどの調整が必要になります(double型については後の節で触れます)。

void main() {
  int myIntValue = 10;
  double myDoubleValue = myIntValue / 4;
  print(myDoubleValue);

  myDoubleValue /= 5;
  print(myDoubleValue);
}

image.png

切り捨て除算は~/の記号や~/=の記号で行うことができます。Pythonとかに慣れ親しんだ身からすると//の方に慣れていますが~/となります。

void main() {
  int myIntValue1 = 10;
  myIntValue1 = myIntValue1 ~/ 4;
  print(myIntValue1);

  int myIntValue2 = 10;
  myIntValue2 ~/= 4;
  print(myIntValue2);
}

image.png

剰余計算は%の記号や%=の記号で行うことができます。

void main() {
  int myIntValue = 10;
  int remainder = myIntValue % 4;
  print(remainder);

  myIntValue %= 3;
  print(myIntValue);
}

image.png

なお、Dartのint型の値の上限は64bitもしくは環境に応じてそれ以下の値となります。たとえばWebターゲットで書きだしてjsに変換された場合には64bitよりも小さな値までしか扱うことができません。

注意:JavaScriptに変換されたDartコードの場合は、整数は倍精度浮動小数点値で表現されうる値に制限される。従って使える整数は -2^53 と 2^53 の範囲及びより大きな値の一部の整数(これには一部の2^63より大きな数が含まれる)である。従ってintクラスの演算子とメソッドの振る舞いはDart VMとJavaScriptにコンパイルされたDartコードとでは差が生じることがある。

それよりも大きな整数を扱いたい時のためにBigIntという型がDartにはあるようです(何となくMySQLとかの感覚でBigIntと聞くと64bitな印象を受けますがDartではもっと大きな値まで扱えます)。

使い方としては初期化などするにはBigInt.fromメソッドを使います。

void main() {
  BigInt myBigIntValue = BigInt.from(1000);
  print(myBigIntValue);
}

image.png

大きな値を使いたい場合、そのままintの値を設定する感覚で設定しようとするとintの上限値を超えているということでエラーになります。

void main() {
  BigInt myBigIntValue = BigInt.from(123456789012345678901234567890);
  print(myBigIntValue);
}

image.png

大きな数値を使いたい場合には指数表記などを使います。

void main() {
  BigInt myBigIntValue = BigInt.from(1e+30);
  print(myBigIntValue);
}

image.png

もしくは文字列に対してparseメソッドを使う形でも対応ができます。

void main() {
  BigInt myBigIntValue = BigInt.parse('123456789012345678901234567890');
  print(myBigIntValue);
}

image.png

とはいえ、Flutterがアプリ開発用となっているのと各ライブラリパッケージなどでも通常のint型を使うのが大半でしょうから基本的にはint型で扱う形となります(扱いが煩雑だったり制限が多い点やパフォーマンス面などを加味しても使う機会はあまりないかなという印象です)。

int型のisEven属性

※他の型でもそうですが、全てではありませんが使うことがありそうな属性やメソッドなどに関しても各節で触れていこうと思います。

int型のisEven属性は整数値が偶数かどうかの真偽値の属性となります。

void main() {
  int myIntValue = 10;
  print(myIntValue.isEven);
}

image.png

int型のisOdd属性

int型のisOdd属性は整数値が奇数かどうかの真偽値の属性となります。

void main() {
  int myIntValue = 10;
  print(myIntValue.isOdd);
}

image.png

int型のisNegative属性

int型のisNegative属性は整数が負の値かどうかの真偽値の属性となります。負の値で荒れはtrueになります。

void main() {
  int myIntValue = -10;
  print(myIntValue.isNegative);
}

image.png

int型のabsメソッド

int型のabsメソッドでは整数の絶対値を取得します。引数は必要としません。

void main() {
  int myIntValue = -10;
  print(myIntValue.abs());
}

image.png

※このメソッドは後々の節で触れる浮動小数点数のdouble型などにも存在します。

int型のtoDoubleメソッド

int型のtoDoubleメソッドでは整数の値を浮動小数点数のdouble型に変換した値を返します。

void main() {
  int myIntValue = 10;
  double myDoubleValue = myIntValue.toDouble();
  myDoubleValue += 0.5;
  print(myDoubleValue);
}

image.png

int型のtoStringメソッド

int型のtoStringメソッドでは整数の値を文字列のString型に変換した値を返します。

void main() {
  int myIntValue = 1000;
  String myStringValue = myIntValue.toString();
  myStringValue += '円';
  print(myStringValue);
}

image.png

int型のparseメソッド

int型自体が持っているstaticメソッドとしてparseメソッドがあります(後の節で触れるdouble型など他の型でも存在しています)。

このメソッドでは引数に指定された文字列を整数に変換します。

void main() {
  String intFormatString = '12345';
  int myIntValue = int.parse(intFormatString);
  print(myIntValue);
}

image.png

整数に変換できない値を指定するとエラーになります。0になったりとかはしません。

void main() {
  String myStringValue = 'abcdef';
  int myIntValue = int.parse(myStringValue);
  print(myIntValue);
}

image.png

浮動小数点数の形式の文字列だった場合などでもエラーとなります。

void main() {
  String doubleFormatString = '1.234';
  int myIntValue = int.parse(doubleFormatString);
  print(myIntValue);
}

image.png

浮動小数点数の型

浮動小数点数の型はdoubleとなります(64bitの値となります)。基本的な使い方はintと大体一緒です。

void main() {
  double myDoubleValue = 20.5;
  myDoubleValue += 10.2;
  print(myDoubleValue);
}

image.png

double型のceilメソッドとceilToDoubleメソッド

double型のceilメソッドは小数点以下を切り上げます。例えば元の値が1.1であれば結果は2となり、元の値が1.0であれば結果は1となります。

また、返却値はdouble型ではなくint型になります。

void main() {
  double myDoubleValue = 1.1;
  int myIntValue = myDoubleValue.ceil();
  print(myIntValue);
}

image.png

似たようなメソッドとしてceilToDoubleというメソッドも存在します。これも挙動はceilとほぼ同じなのですが結果の型がintではなくdoubleとなります(2とか1といった値のdoubleになります)。元の変数をそのまま使いたい場合などに便利です。

void main() {
  double myDoubleValue = 1.1;
  myDoubleValue = myDoubleValue.ceilToDouble();
  print(myDoubleValue);
}

image.png

double型のfloorメソッドとfloorToDoubleメソッド

ceilと同じような形でdouble型にはfloorfloorToDoubleというメソッドがあります。

こちらは小数点以下を切り捨てます。例えば1.1や1.9という値であれば結果の値は1となります。floorfloorToDoubleの違いはceilと同じように結果の値の型がintになるかdoubleになるかの違いだけです。

void main() {
  double myDoubleValue = 1.1;
  int myIntValue = myDoubleValue.floor();
  print(myIntValue);

  myDoubleValue = 1.9;
  myDoubleValue = myDoubleValue.floorToDouble();
  print(myDoubleValue);
}

image.png

double型のroundメソッドとroundToDoubleメソッド

double型のroundメソッドは四捨五入の挙動をします(偶数丸めかと思ったら四捨五入の挙動をするようです)。roundroundToDoubleの違いは他のメソッドと同様に結果がint型になるかdouble型になるかだけです。

void main() {
  double myDoubleValue = 1.4;
  print(myDoubleValue.round());

  myDoubleValue = 1.5;
  print(myDoubleValue.round());

  myDoubleValue = 1.6;
  print(myDoubleValue.round());

  myDoubleValue = 2.5;
  print(myDoubleValue.round());
}

image.png

double型のtoStringAsFixedメソッド

double型のtoStringAsFixedメソッドでは特定の小数点の桁数で四捨五入をすることができます。引数に対象の小数点数の桁数を指定します(引数に2を指定すれば3桁目が四捨五入されて2桁目までが残るようになります)。また、結果の値は文字列になります。

roundメソッドは特定の桁数での四捨五入とかではなく整数になる形での小数点以下の四捨五入となるため、特定の桁数で四捨五入したい場合などに便利です。

ただし、結果の値をdouble型などで扱いたい場合には結果が文字列になっているので一旦parseメソッドで変換する必要があります。

void main() {
  double myDoubleValue = 1.245;
  myDoubleValue = double.parse(myDoubleValue.toStringAsFixed(2));
  print(myDoubleValue);
}

image.png

文字列の型

文字列の型はStringとなります。文字列の値に関してはシングルクォーテーション('')もしくはダブルクォーテーション("")で囲んで扱います。どちらでも動きますが理由がなければフォーマッタとかでどちらかに統一しておくといいかもしれません。

void main() {
  String myStringValue = "Hello";
  print(myStringValue);
}

image.png

文字列の連結は++=の記号で行えます。

void main() {
  String myStringValue = "Hello";
  myStringValue += ' World!';
  print(myStringValue);
}

image.png

また、良く使う文字列の機能として文字列中で直前に$記号を設定することで変数を文字列の中で展開することができます(Pythonのf-stringsのような機能)。文字列中に変数の値を含めたい場合に文字列を連結して書くよりも記述がシンプルになります。

void main() {
  String myStringValue = "太郎";
  print('私の名前は$myStringValueです。');
}

image.png

${対象の変数}といったように{}の括弧で囲むと変数の属性やメソッドにアクセスしたり呼び出したりしても動きます。

void main() {
  final myString = 'abcdef';
  print('文字列を大文字に変換すると${myString.toUpperCase()}になります。');
}

image.png

この機能は文字列以外の変数も含めることができます。例えば以下のように整数の変数を文字列中に含めたりすることができます。

void main() {
  int age = 17;
  print('年齢は$age歳です。');
}

image.png

String型のisEmpty属性とisNotEmpty属性

String型のisEmpty属性は文字列が空文字列かどうかの真偽値となります。もし文字列が空であればtrueとなります。逆にisNotEmpty属性は空でなければtrueとなります。

void main() {
  String myStringValue = '';
  print(myStringValue.isEmpty);
  print(myStringValue.isNotEmpty);
}

image.png

String型のlength属性

String型のlength属性は文字列の文字数の整数となります。

void main() {
  String myStringValue = 'Hello';
  print(myStringValue.length);
}

image.png

日本語なども想定通りに文字数をカウントしてくれます。

void main() {
  String myStringValue = 'あいうえお安以宇衣於';
  print(myStringValue.length);
}

image.png

ただし特殊な漢字や絵文字などの特殊文字は正確に取れないようです(この辺は他の言語でも想定通りの値が取れたり取れなかったりなのでDartだけの問題ではありませんが要注意な感じではあります)。

void main() {
  String myStringValue = '👀🎉🙇‍♂️';
  print(myStringValue.length);
}

image.png

String型のcontainsメソッド

String型のcontainsメソッドでは引数に指定された文字列を含んでいるかどうかの真偽値を取得することができます。含んでいればtrueとなります。

void main() {
  String myStringValue = 'Hello World!';
  print(myStringValue.contains('lo'));
}

image.png

String型のstartsWithメソッドとendsWithメソッド

String型のstartsWithメソッドでは文字列が引数で指定した特定の文字列でスタートしているかの真偽値を取得することができます。スタートしていればtrueとなります。

void main() {
  String myStringValue = 'Hello World!';
  print(myStringValue.startsWith('He'));
}

image.png

逆にendsWithメソッドでは文字列の最後が引数で指定した文字列で終了しているかの真偽値を取得することができます。終了していればtrueとなります。

void main() {
  String myStringValue = 'Hello World!';
  print(myStringValue.endsWith('ld!'));
}

image.png

String型のindexOfメソッドとlastIndexOfメソッド

String型のindexOfメソッドでは文字列中で引数に指定した文字列がどの位置に最初に出現するかどうかのインデックスの整数を取得することができます。複数該当する文字列がある場合は最初の位置となります。また、最初の文字がインデックス0となります。

void main() {
  String myStringValue = 'Hello World!';
  print(myStringValue.indexOf('l'));
}

image.png

lastIndexOfメソッドの方は文字列の右端から該当する文字列を検索します。ただし結果のインデックスは左端からカウントされます(右側からカウントされたりはしません)。

void main() {
  String myStringValue = 'Hello World!';
  print(myStringValue.lastIndexOf('l'));
}

image.png

String型のpadLeftメソッドとpadRightメソッド

String型のpadLeftメソッドは文字列の左端を特定の文字で引数で指定した文字数になるまで埋めます。第一引数に最終的な文字数、第二引数に埋める文字を指定します。第二引数は省略可で、省略した場合は半角スペースで埋められます。

void main() {
  String myStringValue = 'Hello';
  print(myStringValue.padLeft(20));
}

※スクショでは分かりづらいですが合計で20文字になるまで左端に半角スペースが追加になっています。

image.png

第二引数に文字を指定すると半角スペースの代わりにその文字で文字列の左端が埋められます。

void main() {
  String myStringValue = 'Hello';
  print(myStringValue.padLeft(20, 'A'));
}

image.png

padRightメソッドはpadLeftメソッドとほぼ使い方は変わりませんが、文字を埋める位置が左端ではなく右端となります。

void main() {
  String myStringValue = 'Hello';
  print(myStringValue.padRight(20, 'A'));
}

image.png

String型のreplaceAllメソッド

String型のreplaceAllメソッドは任意の文字列を別の文字列で全て置換した文字列を返却します。第一引数に置換したい文字列、第二引数に置換後の文字列を指定します。

以下の例では小文字のlを大文字のLで置換しています。

void main() {
  String myStringValue = 'Hello World!';
  print(myStringValue.replaceAll('l', 'L'));
}

image.png

String型のsplitメソッド

String型のsplitメソッドでは文字列を引数で指定された文字で分割されたリスト(配列)を返却します。例えばコンマ区切りで分割したりスペース区切りで分割したりといった具合です。

void main() {
  String myStringValue = 'AA BBB C DD EEE';
  List<String> splittedList = myStringValue.split(' ');
  print(splittedList);
}

image.png

String型のsubstringメソッド

String型のsubstringメソッドは文字列の特定のインデックス範囲の文字列を返却します(文字列の一部を抽出することができます)。

第一引数には抽出範囲の開始インデックス(最初の文字のインデックスは0となります)、第二引数には抽出範囲の終了インデックス(このインデックス自体は含みません)を指定して使います(つまり第一引数のインデックス~第二引数のインデックス - 1の範囲の各文字が対象となります)。

第二引数は省略可で、その場合は文字列の最後までが抽出対象となります。

以下の例では第一引数に2を指定指定しているのでインデックス2(3文字目)~最後の文字列までが抽出対象となります。

void main() {
  String myStringValue = 'HelloWorld!';
  print(myStringValue.substring(2));
}

image.png

以下の例では第一引数に2、第二引数に5を指定しているので2~4のインデックス範囲が抽出対象となります。

void main() {
  String myStringValue = 'HelloWorld!';
  print(myStringValue.substring(2, 5));
}

image.png

String型のtoLowerCaseメソッドとtoUpperCaseメソッド

String型のtoLowerCaseメソッドは文字列を小文字に変換した値を返却します。

void main() {
  String myStringValue = 'HELLO WORLD!';
  print(myStringValue.toLowerCase());
}

image.png

逆にtoUpperCaseメソッドでは大文字に変換した文字列のあたぽを返却します。

void main() {
  String myStringValue = 'hello world!';
  print(myStringValue.toUpperCase());
}

image.png

String型のtrimメソッド、trimLeftメソッド、trimRightメソッド

String型のtrimメソッドは文字列の左右の両端の空白文字(スペースや改行など)を取り除いた文字列を返却します。

void main() {
  String myStringValue = '  \n  Hello world!   ';
  print(myStringValue.trim());
}

image.png

trimLeftメソッドはtrimメソッドと似た挙動をしますが文字列の左端だけ空白文字が取り除かれます。

void main() {
  String myStringValue = '  \n  Hello world!   \n\n     ';
  print(myStringValue.trimLeft());
}

※以下のスクショは分かりづらいですが、出力の文字列を選択してみるとスペースなどが残っていることが分かります。

image.png

逆にtrimRightメソッドは文字列の右端からのみ空白文字が取り除かれます。

void main() {
  String myStringValue = '  \n\n  Hello world!   \n\n     ';
  print(myStringValue.trimRight());
}

image.png

真偽値の型

真偽値の型はboolとなります。値はtrueもしくはfalseのどちらかとなります。

void main() {
  bool isCompleted = true;
  print(isCompleted);
}

image.png

真偽値の値の直前に!記号を配置すると逆に値になります(trueであればfalseに、falseであればtrueになります)。

void main() {
  bool isCompleted = true;
  print(!isCompleted);
}

image.png

リスト(配列)の型

リスト(配列)はList型を使います。また、値を設定する際には[]の括弧を使いコンマ区切りで複数の値を指定します。

※ループ用のfor文などに関しては後々の節で触れます。

void main() {
  List myList = [10, 20, 'Hello'];
  for (final value in myList) {
    print(value);
  }
}

image.png

前節まででも触れましたがリストの要素の型を固定したい場合には<対象の型>といった指定をListの後に記述します。

整数のリストとして明示するためにという型の指定をしている例
void main() {
  List<int> myList = [10, 20, 30];
  print(myList);
}

image.png

※以降の節でList型の各属性やメソッドについて触れていきますがDartはかなりその辺多いな・・・!と思いました。充実していると思ったり中々初見の時「これ何のメソッドなんだろう・・・?」といったものも割とあって調べていて面白かったです。

List型のlength属性

List型のlength属性はリスト内の要素の件数となります。3件要素が格納されていれば3となります。

void main() {
  List<int> myListValue = [10, 20, 30];
  print(myListValue.length);
}

image.png

List型のfirst属性とlast属性

List型のfirst属性はリストの先頭の要素となります。

void main() {
  List<int> myListValue = [10, 20, 30];
  print(myListValue.first);
}

image.png

last属性では逆にリストの最後の要素となります。

void main() {
  List<int> myListValue = [10, 20, 30];
  print(myListValue.last);
}

image.png

対象が空のリストの場合にはfirst属性・last属性共にエラーとなります。

void main() {
  List<int> myListValue = [];
  print(myListValue.first);
}

image.png

List型のfirstOrNull属性とlastOrNull属性

List型のfirstOrNull属性とlastOrNull属性はfirstlast属性のようにリストの先頭の要素もしくは最後の要素となります。違いとしてはfirst属性などと異なりリストが空の場合でもランタイムエラーにならず、代わりにnullとなります。

先頭の要素を参照している例
void main() {
  List<int> myListValue = [10, 20, 30];
  print(myListValue.firstOrNull);
}

image.png

最後の要素を参照している例
void main() {
  List<int> myListValue = [10, 20, 30];
  print(myListValue.lastOrNull);
}

image.png

空のリストではエラーではなくnullとなっている例
void main() {
  List<int> myListValue = [];
  print(myListValue.firstOrNull);
}

image.png

List型のsingle属性とsingleOrNull属性

List型のsingle属性はリストの要素の件数が1件になっていることを確認し、もし1件になっていればその要素となり、もし1件ではない場合はエラーとなります。

要素が1件のみのリストの場合の例
void main() {
  List<int> myListValue = [100];
  print(myListValue.single);
}

image.png

要素の件数が1件ではないためエラーになる例
void main() {
  List<int> myListValue = [100, 200, 300];
  print(myListValue.single);
}

image.png

List型のisEmpty属性とisNotEmpty属性

List型のisEmpty属性はリストが空かどうかの真偽値となります。空であればtrue、そうでなければfalseとなります。

空の場合の例
void main() {
  List<int> myListValue = [];
  print(myListValue.isEmpty);
}

image.png

空ではない場合の例
void main() {
  List<int> myListValue = [100, 200];
  print(myListValue.isEmpty);
}

image.png

逆にisNotEmpty属性では空では無ければtrueとなります。!記号での否定とかを使うよりもこちらを使った方がぱっと見で分かりやすいかもしれません。

void main() {
  List<int> myListValue = [100, 200];
  print(myListValue.isNotEmpty);
}

image.png

List型のindexed属性

List型のindexed属性はリストのインデックスとリストの要素の組み合わせのIterable型の値となります。

これを使うことでループ中にインデックスと要素の両方に同時にアクセスすることができます。

Iterable型やループについては後々の節で触れます。

ループと一緒に使うと以下のような記述になります。

void main() {
  List<String> myListValue = ['A', 'B', 'C'];
  for (final (index, value) in myListValue.indexed) {
    print('index: $index, value: $value');
  }
}

image.png

Iterable型についての補足

List型の範疇から少し逸脱しますがList関係の操作で良くIndex型が出てくるので軽く触れておきます。

Iterable型はList型のように複数の要素を順番に格納する型となります。ただし利用の際にはfor文によるループや後々触れるmapwhereなどのメソッドと一緒に使う形になります。リストのように添え字用の括弧([])を使って要素にアクセスすることは出来ずエラーとなります。

void main() {
  List<String> myListValue = ['A', 'B', 'C'];
  print(myListValue.indexed[0]);
}

image.png

Iterable型からList型に戻したい場合

List型の操作でIterable型の値となった場合に再びList型に変換したい場合のためにtoListというメソッドが用意されているためそちらを使うとListの型で再び扱うことができます。

void main() {
  List<String> myListValue = ['A', 'B', 'C'];
  List indexedListValue = myListValue.indexed.toList();
  print(indexedListValue);
}

image.png

List型のreversed属性

List型のreversed属性はリストの各要素を逆順にしたIterable型の値となります。

逆順でC,B,Aという順番で出力される例
void main() {
  List<String> myListValue = ['A', 'B', 'C'];
  for (final value in myListValue.reversed) {
    print(value);
  }
}

image.png

List型のnonNulls属性

List型のnonNulls属性はリストの要素内でnullではない要素のみに絞ったIterable型の値となります。

nullに関しては後々の節で触れます。以下のコード例で出てくる?記号もnull関係となります。

null以外の要素が出力される例
void main() {
  List<String?> myListValue = ['A', null, 'B', null, 'C'];
  for (final value in myListValue.nonNulls) {
    print(value);
  }
}

image.png

List型のaddメソッドとaddAllメソッド

List型のaddメソッドはリストに新たな要素を追加します。引数に追加したい要素を指定します。追加される位置はリストの末尾となります。

末尾にDという文字列をaddメソッドで追加する例
void main() {
  List<String?> myListValue = ['A', 'B', 'C'];
  myListValue.add('D');
  for (final value in myListValue.nonNulls) {
    print(value);
  }
}

image.png

また、addAllメソッドでは引数に別のリスト(もしくは他のIterable型の値)を追加することで複数の要素を一気に追加することができます。

末尾にD,E,Fの3要素を一括でリストに追加する例
void main() {
  List<String?> myListValue = ['A', 'B', 'C'];
  myListValue.addAll(['D', 'E', 'F']);
  for (final value in myListValue.nonNulls) {
    print(value);
  }
}

image.png

List型のinsertメソッドとinsertAllメソッド

List型のinsertメソッドはaddメソッドのようにリストに要素を追加します。ただしaddメソッドと異なり要素を追加する位置を引数で指定することができます。

第一引数に要素を追加したい位置のインデックス(先頭に追加したい場合には0、その次は1...といった値になります)、第二引数に追加したい要素を指定します。

インデックス2の位置に200という要素を追加している例
void main() {
  List<int> myListValue = [100, 100, 100, 100];
  myListValue.insert(2, 200);
  print(myListValue);
}

また、insertAllメソッドでは指定するインデックスの位置に複数の要素をまとめて追加することができます。第二引数は別のリスト(もしくは他のIterable型の値)を指定します。

インデックス2の位置に200,300,400という複数の要素を一気に追加している例
void main() {
  List<int> myListValue = [100, 100, 100, 100];
  myListValue.insertAll(2, [200, 300, 400]);
  print(myListValue);
}

image.png

List型のremoveメソッド

List型のremoveメソッドは引数で指定された値をリスト内から検索し、見つかった要素を1件リストから取り除きます。該当の要素が全て取り除かれるわけでは無く、一致した先頭の要素が取り除かれます。

また、返却値は要素が取り除かれたかどうかの真偽値となります。もし該当する要素がなければfalseが返却されます。

先頭の200の要素が1件取り除かれ、返却値もtrueとなる例
void main() {
  List<int> myListValue = [100, 200, 200, 100];
  bool isRemoved = myListValue.remove(200);
  print(myListValue);
  print(isRemoved);
}

image.png

取り除く対象が存在しないためリストの要素は変動せず返却値がfalseとなる例
void main() {
  List<int> myListValue = [100, 200, 200, 100];
  bool isRemoved = myListValue.remove(300);
  print(myListValue);
  print(isRemoved);
}

image.png

List型のremoveAtメソッド

List型のremoveAt関数は第一引数に指定したインデックス位置の要素をリストから取り除きます。返却値は取り除いた値となります。

インデックス1の位置の要素200を取り除く例
void main() {
  List<int> myListValue = [100, 200, 300];
  int removedValue = myListValue.removeAt(1);
  print(myListValue);
  print(removedValue);
}

image.png

指定したインデックスの位置に要素が無い場合にはランタイムエラーとなります。

存在しないインデックス5の位置を指定してエラーになる例
void main() {
  List<int> myListValue = [100, 200, 300];
  int removedValue = myListValue.removeAt(5);
  print(myListValue);
  print(removedValue);
}

image.png

List型のremoveLastメソッド

List型のremoveLastメソッドはリストの最後の要素を取り除きます。引数は必要とせず、返却値は取り除かれた値となります。

最後の要素300を取り除いている例
void main() {
  List<int> myListValue = [100, 200, 300];
  int removedValue = myListValue.removeLast();
  print(myListValue);
  print(removedValue);
}

image.png

もしリストが空の場合にはランタイムエラーになります。

対象が空のリストなのでエラーになる例
void main() {
  List<int> myListValue = [];
  int removedValue = myListValue.removeLast();
  print(myListValue);
  print(removedValue);
}

List型のremoveRangeメソッド

List型のremoveRangeメソッドはリストから指定した範囲の要素を取り除きます。第一引数は取り除く範囲の開始インデックス、第二引数は取り除く範囲の終了インデックス(ただしこのインデックスは含まず、第二引数 - 1の範囲が取り除く対象となります)となります。例えば第一引数に2、第二引数に4を指定した場合にはインデックスが2~3の範囲の2要素が取り除かれます。

インデックス2~3の範囲(300と400)の要素を取り除く例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  myListValue.removeRange(2, 4);
  print(myListValue);
}

image.png

指定したインデックス範囲がリストの範囲外になっているとランタイムエラーとなります。

リストの最後のインデックスが4の一方で除外範囲を5のインデックスにまで指定していてエラーになる例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  myListValue.removeRange(2, 6);
  print(myListValue);
}

image.png

List型のremoveWhereメソッド

List型のremoveWhereメソッドでは条件を満たした要素をリストから取り除きます。引数には判定用の関数を指定します。判定用の関数には引数に要素、返却値に真偽値を設定する必要があります。

条件を満たす要素が複数ある場合には一通り取り除かれます。

値が300以上の要素を一通り取り除く例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  myListValue.removeWhere((element) => element >= 300);
  print(myListValue);
}

image.png

List型のretainWhereメソッド

List型のretainWhereメソッドはremoveWhereメソッドの判定用の関数が逆の動作をします。つまり判定用の関数で条件を満たした要素をリストに残す(他は取り除く)挙動になります。

引数はremoveWhereメソッドと同様に要素を引数に受け取って真偽値を返す判定用の関数となります。

値が300以上の要素のみを残す例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  myListValue.retainWhere((element) => element >= 300);
  print(myListValue);
}

image.png

List型のclearメソッド

List型のclearメソッドではリストを空にします(要素をまったく含んでいない状態にします)。

clearメソッドでリストの要素が空になる例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  myListValue.clear();
  print(myListValue);
}

image.png

List型のelementAtメソッドとelementAtOrNullメソッド

List型のelementAtメソッドは引数で指定されたインデックス位置の要素を返却します。他のメソッドなどと同様にインデックスは0からスタートします。

インデックスが2の位置の要素を取得する例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  print(myListValue.elementAt(2));
}

image.png

リストの範囲外のインデックスを引数に指定した場合にはランタイムエラーとなります。

リストの最後の要素のインデックスが4の一方で5のインデックスを指定したためエラーになる例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  print(myListValue.elementAt(5));
}

image.png

elementAtOrNullメソッドはelementAtメソッドと似た挙動をしますが、もし引数に指定したインデックス範囲がリストの範囲を超えている場合でもエラーにはならずに代わりにnullを返却します。

インデックスがリストの範囲内なのでelementAtと同じ挙動をする例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  print(myListValue.elementAtOrNull(4));
}
インデックスがリストの範囲外なのでnullが返却される例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  print(myListValue.elementAtOrNull(5));
}

image.png

List型のwhereメソッド

List型のwhereメソッドは引数の関数で条件を満たしたIterable型の値を返却します。各要素を参照して判定を行い、条件を満たせば返却値に含まれるようになるといった挙動になります。

引数の関数の第一引数には対象の要素が渡され、返却値には真偽値を設定する必要があります(trueを返却した要素が結果のIterable内に含まれる形になります)。

retainWhereと似たような挙動となりますが、retainWhereの方はリスト自体の内容が変更されるのに対してこちらはIterable型の値を返却する形になっています(元のリストは変化しません)。

値が300以上の要素のみのIterableの値を作成する例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  Iterable<int> myIterableValue =
      myListValue.where((element) => element >= 300);
  print(myIterableValue);
}

List型のsingleWhereメソッド

List型のsingleWhereメソッドはwhereメソッドのように引数に判定用の関数を受け付けますが、条件を満たす要素が1件であることも確認します。もし条件を満たす要素が1件のみであればその要素を返却し、条件を満たす要素が1件ではない場合にはエラーとなります。

値が300の要素は1件のみなのでエラーにならずに対象の要素を返却出来ている例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  int myValue = myListValue.singleWhere((element) => element == 300);
  print(myValue);
}

image.png

条件を満たす要素が複数あるためエラーになる例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  int myValue = myListValue.singleWhere((element) => element >= 300);
  print(myValue);
}

image.png

List型のfirstWhereメソッドとlastWhereメソッド

List型のfirstWhereは引数で判定用の関数を受け付け、その関数で条件を満たす要素の中で先頭の要素を返却します。引数の関数には他のwhereなどのメソッドと同様に引数に対象の要素を受け付け返却値には判定結果の真偽値が必要になります。条件を満たす場合には返却値にtrueが設定されるようにします。

リストの中で250以上という条件を満たす先頭の要素を取得する例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  int firstValue = myListValue.firstWhere((element) => element >= 250);
  print(firstValue);
}

image.png

第二引数を指定しない条件且つ該当する要素が1件も存在しない場合にはエラーになります(第二引数に関しては後で説明します)。

条件を満たす要素が1件も存在しないためエラーになく例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  int firstValue = myListValue.firstWhere((element) => element >= 550);
  print(firstValue);
}

image.png

第二引数はorElseという名前の引数となり、こちらも関数を引数に取りますが、要素は引数に渡されたりはせず単純に特定の返却値を設定するのみの関数となります。もしfirstWhereメソッドで該当する要素が無かった場合にはこの関数の返却値がデフォルト値のように設定されます(エラーにもならなくなります)。

第二引数にorElse引数を指定しているため該当する要素が無くてもエラーにならない例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  int firstValue = myListValue.firstWhere(
    (element) => element >= 550,
    orElse: () => -1,
  );
  print(firstValue);
}

image.png

lastWhereメソッドはfirstWhereメソッドと使い方は同じですが、条件のチェックがリストの最後の要素から逆順に実行されていきます。

300以上という条件を満たす要素をリストの末尾からチェック・取得する例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  int lastValue = myListValue.lastWhere((element) => element >= 300);
  print(lastValue);
}

image.png

第二引数のorElseメソッドもfirstWhereメソッドと同じです。

void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  int lastValue = myListValue.lastWhere(
    (element) => element <= 50,
    orElse: () => -1,
  );
  print(lastValue);
}

image.png

List型のgetRangeメソッド

List型のgetRangeメソッドは引数で指定したインデックス範囲のIterable型の値を取得します。第一引数は開始インデックス、第二引数は最終インデックスとなります(ただし第二引数の指定のインデックス自体は含みません。最終インデックス - 1のインデックス範囲となります)。

インデックス1~3の要素を格納したIterableの値を取得する例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  print(myListValue.getRange(1, 4));
}

image.png

List型のindexOfメソッドとlastIndexOfメソッド

List型のindexOfメソッドは引数で指定された値の要素をリスト内で左から検索し、最初に該当するインデックスを返却します。

200の要素はインデックス1の位置にあるので1が返却される例
void main() {
  List<int> myListValue = [100, 200, 300, 400, 500];
  print(myListValue.indexOf(200));
}

image.png

対象の要素が見つからない場合には-1が返却されます。エラーなどにはなりません。

存在しない500の要素を検索しているので-1が返却される例
void main() {
  List<int> myListValue = [100, 200, 300, 100, 200, 300];
  print(myListValue.indexOf(500));
}

image.png

lastIndexOfメソッドはindexOfと似たような挙動をしますが検索がリストの右端から実行されます(ただし返却されるインデックスは右端からの値ではなく左端からの位置となります)。

右端から検索しているので真ん中付近にある100の要素のインデックス3が返却される例
void main() {
  List<int> myListValue = [100, 200, 300, 100, 200, 300];
  print(myListValue.lastIndexOf(100));
}

要素が見つからない場合に-1が返却される点も同様です。

void main() {
  List<int> myListValue = [100, 200, 300, 100, 200, 300];
  print(myListValue.lastIndexOf(500));
}

image.png

List型のindexWhereメソッドとlastIndexWhereメソッド

List型のindexWhereは条件を満たす要素のインデックスを返却します。第一引数には条件を満たすかどうかの判定用の関数を指定します。他のメソッドと同様この関数には第一引数に要素が渡され、返却値で真偽値を返す必要があります。返却値がtrueになる最初の要素のインデックスが対象となります。

250以上という条件を満たす要素は300なのでインデックス2が返却される例
void main() {
  List<int> myListValue = [100, 200, 300, 100, 200, 300];
  print(myListValue.indexWhere((element) => element >= 250));
}

image.png

該当するものが存在しない場合にはindexOfメソッドなどと同じように-1が返却されます。

500以上の要素が存在しないため-1が返却される例
void main() {
  List<int> myListValue = [100, 200, 300, 100, 200, 300];
  print(myListValue.indexWhere((element) => element >= 500));
}

image.png

indexWhereメソッドの第二引数は条件のチェックを開始するインデックス位置となります。省略可の引数となり、省略した場合にはリストの先頭の要素から順番にチェックされていきます。

第二引数で開始インデックスを3としているため最後の300の要素のインデックスが返却されている例
void main() {
  List<int> myListValue = [100, 200, 300, 100, 200, 300];
  print(myListValue.indexWhere((element) => element >= 250, 3));
}

image.png

lastIndexWhereメソッドはindexWhereと同じような使い方となりますが、要素の判定はリストの右端から順番にチェックされる形となります。右側からチェックされはするものの、lastIndexOfメソッドなどと同じように返却値のインデックスは左から数えたインデックスとなります。

右側からチェックされるため右から2番目の300の要素のインデックスが返却されている例
void main() {
  List<int> myListValue = [100, 200, 300, 100, 200, 300, 200];
  print(myListValue.lastIndexWhere((element) => element >= 250));
}

image.png

第二引数でチェックを開始するインデックスを指定できる点もindexWhereメソッドと同様です。ただし開始位置のインデックスは右側からカウントした値となります。判定と第二引数の開始インデックスは右側から、返却値のインデックスは左からのインデックスとなります。

第二引数の開始インデックスに2を指定しているため右から2番目の300の要素が対象になっていない例
void main() {
  List<int> myListValue = [100, 200, 300, 100, 200, 300, 200];
  print(myListValue.lastIndexWhere((element) => element >= 250, 2));
}

image.png

List型のfoldメソッド

List型のfoldメソッドは指定する初期値とリストの各要素を参照して単一の値を返却します。第一引数に初期値、第二引数に直前の要素までの計算結果(previousValue)と対象の要素(element)の引数を持ち計算結果を返却値に設定する形の関数を指定します。

初期値10とリストの各要素で10+1+2+3で16という結果が返却される例
void main() {
  List<int> myListValue = [1, 2, 3];
  print(
    myListValue.fold<int>(
      10,
      (previousValue, element) => previousValue + element,
    ),
  );
}

image.png

なお、ジェネリックでfold<int>といったように計算値に対する型を明示していますが、これをやらないと値がnullになったりで想定した計算にならないようです。無難にジェネリックの型の指定をしておいた方が無難?そうな印象です。

image.png

List型のreduceメソッド

List型のreduceメソッドはfoldメソッドと似た感じでリストの各要素に対して順番に計算を行い最終的な単一の値を返却します。

foldメソッドとの違いは初期値を別の値として指定するかどうかです。reduceメソッドでは初期値の引数はありません。また、こちらのメソッドはジェネリックの型指定をしなくとも問題無いようです。

リストの各要素を順番に加算していく計算で1+2+3で6が返却される例
void main() {
  List<int> myListValue = [1, 2, 3];
  print(
    myListValue.reduce(
      (previousValue, element) => previousValue + element,
    ),
  );
}

image.png

List型のmapメソッド

List型のmapメソッドはリストの各要素に対して特定の処理を反映した結果のIterable型の値を返却します。例えば全ての要素に特定の値を足したり型を変換したりといった制御に使えます。

第一引数には各要素への処理用の関数を指定します。そちらの関数の引数にはリストの要素が渡され、返却値には処理後の要素を設定する必要があります。

全ての要素に10加算したIterableを取得する例
void main() {
  List<int> myListValue = [1, 2, 3];
  print(myListValue.map((element) => element + 10));
}

image.png

List型のfollowedByメソッド

List型のfollowedByメソッドは引数に指定した別のリストなど(Iterable型なども可)を末尾に連結したIterableの値を返却します。例えば1, 2, 3という3つの値も持つリストでこのメソッドを使い、引数に4, 5, 6という要素を持つリストを指定した場合には1, 2, 3, 4, 5, 6という連結された各要素を持ったIterableの値が返却されます。

void main() {
  List<int> myListValue = [1, 2, 3];
  print(myListValue.followedBy([4, 5, 6]));
}

image.png

List型のreplaceRangeメソッド

List型のreplaceRangeメソッドは引数で指定されたインデックス範囲を、別途指定したリスト(もしくは他のIterable型の値)で差し替えます。Iterableの値を返す形ではなく元のリストに対して変更が入ります。

第一引数は置換の開始インデックス、第二引数は置換の最終インデックス(ただしこのインデックスは含まず、第二引数を1減算したインデックスまでが範囲となります)、第三引数に差し替えで使うリストなどの値を指定します。

インデックス1~インデックス3の範囲を20,30,40の値で置換する例
void main() {
  List<int> myListValue = [1, 2, 3, 4, 5, 6];
  myListValue.replaceRange(1, 4, [20, 30, 40]);
  print(myListValue);
}

なお、挙動としては第一引数と第二引数のインデックス範囲の要素を削除 → 第一引数のインデックス位置に第三引数のリストなどの各要素を入れるという流れの挙動になるため置換対象のインデックス範囲の件数と第三引数のリストなどの要素の件数がずれていてもエラーなどにはなりません。第三引数の要素の件数次第で結果のリストの件数が増減します。

置換範囲の要素数よりも多い要素数のリストを第三引数に指定した例
void main() {
  List<int> myListValue = [1, 2, 3, 4, 5, 6];
  myListValue.replaceRange(1, 4, [20, 30, 40, 50, 60]);
  print(myListValue);
}

image.png

List型のsetAllメソッド

List型のsetAllメソッドは引数で指定した位置以降の要素を別途引数で指定したリスト(もしくは他のIterableの値)で置換します。replaceRangeメソッドと近い挙動をしますが、setAllメソッドの方は終了インデックスの引数は無く、固定で引数で指定されたリストなどの要素数分が置換されます。また、こちらもIterableの値を返却するのではなく元のリストに対して変更が入ります。

第一引数には開始位置のインデックス、第二引数には置換後として反映するリスト(もしくは他のIterableの値)を指定して使います。

void main() {
  List<int> myListValue = [1, 2, 3, 4, 5, 6];
  myListValue.setAll(1, [20, 30, 40]);
  print(myListValue);
}

image.png

また、第一引数に指定した開始インデックス位置を加味した第二引数に指定したリストなどの要素の件数が元のリストの長さを超える場合にはエラーになります(元のリストの要素の件数は変動しないようになっています)。

元のリストの長さを超える引数指定になっているためエラーになる例
void main() {
  List<int> myListValue = [1, 2, 3, 4, 5, 6];
  myListValue.setAll(1, [20, 30, 40, 50, 60, 70]);
  print(myListValue);
}

image.png

List型のsetRangeメソッド

List型のsetRangeメソッドはreplaceRangeメソッドやsetAllメソッドなどと同様にリスト内の複数の要素を置換します。

replaceRangeメソッドやsetAllなどと異なり以下のような挙動をします。

  • setAllメソッドとは異なり置換する範囲をそれぞれ引数で指定します。
  • replaceRangeメソッドのように範囲を超えて置換されたりしません。
    • ※リストの件数は置換処理後も変わらず、もし置換範囲がリストのインデックス範囲を超えている場合にはエラーになります。
  • 第四引数にskipCountという引数があり、置換で指定したリスト(もしくは別のIterableの値)の中で先頭の任意の個数の要素をスキップすることができます。

第一引数は置換範囲の開始インデックス、第二引数は置換範囲の終了インデックス(ただしこのインデックス自体は含まず-1した位置までが対象となります)、第三引数には置換で設定するリスト(もしくは他のIterableの値)、第四引数は要素のスキップ数の指定(省略可)となります。

1~2のインデックス範囲の要素を第三引数で指定したリストで置換する例
void main() {
  List<int> myListValue = [1, 2, 3, 4, 5, 6];
  myListValue.setRange(1, 3, [20, 30, 40, 50, 60, 70]);
  print(myListValue);
}

image.png

第四引数に2を指定しているため、置換に使われるリストの要素位置がずれる例
void main() {
  List<int> myListValue = [1, 2, 3, 4, 5, 6];
  myListValue.setRange(1, 3, [20, 30, 40, 50, 60, 70], 2);
  print(myListValue);
}

image.png

置換範囲指定の第二引数がリストのインデックス範囲を超えるためエラーになる例
void main() {
  List<int> myListValue = [1, 2, 3, 4, 5, 6];
  myListValue.setRange(1, 10, [20, 30, 40, 50, 60, 70], 2);
  print(myListValue);
}

image.png

List型のsublistメソッド

List型のsublistメソッドは引数で指定されたインデックス範囲のリストを返却します。第一引数には開始インデックス、第二引数には終了インデックス(ただし他と同様にこのインデックスを-1した位置が最後のインデックスとなります)を指定します。返却値はIterable型ではなくListになります。

インデックス範囲2~3のリストを取得する例
void main() {
  List<int> myListValue = [0, 1, 2, 3, 4, 5, 6];
  print(myListValue.sublist(2, 4));
}

List型のshuffleメソッド

List型のshuffleメソッドはリストの要素をシャッフルします。そのため実行の度にリストの要素の順番が変動します。処理はリストを直接更新します。

void main() {
  List<int> myListValue = [0, 1, 2, 3, 4, 5, 6];
  myListValue.shuffle();
  print(myListValue);
}

image.png

List型のsortメソッド

List型のsortメソッドはリストの要素をソートします。引数は省略可で、省略した場合には数値の昇順でソートを行います。

数値の要素を昇順でソートする例
void main() {
  List<int> myListValue = [20, -5, 15, 3, 2, 10, 35, -16];
  myListValue.sort();
  print(myListValue);
}

image.png

数値以外、例えば文字列ではどのような判定になるのだろう・・・と思ったのですが、どうやらUnicodeのコードポイントの順序の昇順でソートがされるようです。この辺はPythonとかと似たような挙動でしょうか。

文字列の要素をコードポイントの順番の昇順でソートする例
void main() {
  List<String> myListValue = ['CCCC', 'BBB', 'DD', 'AAAAA', 'EE'];
  myListValue.sort();
  print(myListValue);
}

image.png

第一引数は省略可能ですが、指定する場合にはソート順判定用の関数を指定します。この関数にはabの2つの引数を取り、もしabよりも前に配置したい時には-1abが同値条件ならbabよりも後に配置させたい場合には1を返却する必要のある関数となります。

引数を指定して文字数の昇順でソートする例
void main() {
  List<String> myListValue = ['CCCC', 'BBB', 'DD', 'AAAAA', 'EE'];
  myListValue.sort((a, b) {
    if (a.length <= b.length) {
      return -1;
    } else if (a.length == b.length) {
      return 0;
    } else {
      return 1;
    }
  });
  print(myListValue);
}

なお、int型などの基本的な型ではこの辺の制御がしやすいように別の値と比較して小さければ-1、同値であれば0、大きければ1を返却するcompareToというメソッドがあります。こちらを使うことでsortメソッドなどでの記述をシンプルにすることができます。

compareToメソッドを使ってシンプルなsortの引数を設定する例
void main() {
  List<String> myListValue = ['CCCC', 'BBB', 'DD', 'AAAAA', 'EE'];
  myListValue.sort((a, b) => a.length.compareTo(b.length));
  print(myListValue);
}

image.png

昇順でソートしたリストを降順にしたい場合にはreversed属性を使うか、もしくは引数の関数の条件で設定します。

reversedを使って降順でソートされたリストを取得する例
void main() {
  List<int> myListValue = [20, -5, 15, 3, 2, 10, 35, -16];
  myListValue.sort();
  myListValue = myListValue.reversed.toList();
  print(myListValue);
}

image.png

List型のskipメソッドとskipWhileメソッド

List型のskipメソッドは引数で指定した要素数分先頭の要素を取り除いたIterableの値を返却します。

先頭の2つの要素をスキップして残りの要素のIterableの値を取得する例
void main() {
  List<int> myListValue = [1, 2, 3, 4, 5];
  print(myListValue.skip(2));
}

image.png

skipWhileメソッドはskipメソッドと同様に先頭の方の要素を省いたIterableの値を返却します。ただしこちらの引数はスキップする数ではなく判定用の関数を指定する必要があります。引数で渡された関数でリストの要素を先頭から順番にチェックしていって、条件を満たしている限りは要素がスキップされます。関数の第一引数には対象の要素が渡され、返却値には条件を満たすかどうかの真偽値が必要になります。

要素が3以下である限り先頭の方の要素をスキップする例
void main() {
  List<int> myListValue = [1, 2, 3, 4, 5, 6];
  print(myListValue.skipWhile((element) => element <= 3));
}

image.png

List型のtakeメソッドとtakeWhileメソッド

List型のtakeメソッドは引数で指定した個数の件数分の要素を格納したIterableの値を返却します。例えば引数に3を指定すれば3件の要素を格納したIterableが返却されます。対象となる要素は先頭から順番に処理されていきます。

先頭の3件の要素のIterableを取得する例
void main() {
  List<int> myListValue = [1, 2, 3, 4, 5, 6];
  print(myListValue.take(3));
}

image.png

もし引数で指定した件数よりもリストの要素数が少ない場合には結果の要素数は引数の値よりも少なくなります(エラーになったりはしません)。

引数の値よりもリストの要素数が少ないため結果も3要素のみのIterableとなる例
void main() {
  List<int> myListValue = [1, 2, 3];
  print(myListValue.take(5));
}

image.png

takeWhileメソッドはtakeメソッドのように先頭からの要素を参照したIterableの値を返却します。ただしこちらは引数で一定の件数を対象とするのではなく、引数で指定したチェック用の関数の条件を満たす限りの要素を格納したIterableの値を返却します。条件を満たさなくなったらその直前の要素までが返却値の対象となります。引数の関数には引数に対象の要素、返却値に真偽値が必要になります(条件を満たす場合にはtrueを返却する形にします)。

先頭から要素が3以下という条件を満たす範囲のIterableを取得する例
void main() {
  List<int> myListValue = [1, 2, 3, 4, 5];
  print(myListValue.takeWhile((element) => element <= 3));
}

image.png

List型のanyメソッド

List型のanyメソッドは引数に関数を受け付けて、リストの各要素ごとにその関数が条件を満たすかどうかを判定し、もし1つでも条件を満たす要素が存在すればtrueを返却するメソッドです。

対象の関数は第一引数に対象の要素が指定され、返却値には真偽値を設定する必要があります。

※Dartにおける関数の書き方や無名関数の書き方については後々の節で詳しく触れますが、(引数) => 返却値という記述で無名関数がDartでは定義できる形になっています。

関数内で文字列の文字数が2文字かどうかをチェックし、1件2文字の要素があるためtrueが返却されている例
void main() {
  List<String> myListValue = ['AAA', 'BB', 'CCCC'];
  print(myListValue.any((element) => element.length == 2));
}

image.png

どの要素も条件を満たさないのでfalseが返却されている例
void main() {
  List<String> myListValue = ['A', 'B', 'CCC'];
  print(myListValue.any((element) => element.length == 2));
}

image.png

List型のeveryメソッド

List型のeveryメソッドはanyメソッドと似た書き方と挙動になりますが、anyメソッドとは異なり全ての要素が関数内で条件を満たした場合にtrueを返却するメソッドとなります。

リストの全ての要素が250以下を満たすためtrueが返却される例
void main() {
  List<int> myListValue = [100, 200, 150, 220];
  print(myListValue.every((element) => element <= 250));
}

image.png

要素が1つだけ条件を満たさないのでfalseが返却される例
void main() {
  List<int> myListValue = [100, 180, 150, 220];
  print(myListValue.every((element) => element <= 200));
}

image.png

List型のjoinメソッド

List型のjoinメソッドはリストの要素を連結した文字列を返却します。引数は区切り文字となり、省略した場合は区切り文字無しで連結されます。

区切り文字無しで連結する例
void main() {
  List<int> myListValue = [1, 2, 3, 4, 5];
  print(myListValue.join());
}

image.png

コンマを区切り文字として指定した例
void main() {
  List<int> myListValue = [1, 2, 3, 4, 5];
  print(myListValue.join(','));
}

image.png

List型のasMapメソッド

ListasMapメソッドはリストをMap(辞書)に変換した値を返却します。キーにはリストのインデックス、値にリストの要素が設定されます。
Map型については後々の節で触れます。

void main() {
  List<int> myListValue = [1, 2, 3, 4, 5];
  print(myListValue.asMap());
}

image.png

List型のtoSetメソッド

List型のtoSetメソッドはリスト型をSet型(集合の型)に変換した値を返却します。リストに似た値となりますが、重複した値はSetからは取り除かれます(一意な要素の結果となります)。

Set型については後々の節で詳しく触れます。

重複している要素が取り除かれる例
void main() {
  List<int> myListValue = [1, 2, 1, 2, 3, 4, 3];
  print(myListValue.toSet());
}

image.png

集合の型

集合はSet型を使います。また、値を設定する際には{}の括弧を使いコンマ区切りで複数の値を指定します。Mapと同じ括弧となりますがこちらはキーと値のような設定ではないためコロンは使わずにコンマのみで値を区切って使います。
Map型については後々の節で触れます。

void main() {
  Set mySetValue = {1, 2, 3};
  print(mySetValue);
}

image.png

集合に含める値の型を固定したい場合のジェネリックの型の指定はListなどと同じように<対象の型>といったように<>の括弧をSetの後に記述します。

整数の集合と明示ずるためにと型を設定している例
void main() {
  Set<int> mySetValue = {1, 2, 3};
  print(mySetValue);
}

また、Set型では重複した値が存在する場合には1件のみ値が残されます。そのためリストなどをSetに変換した場合には重複は取り除かれて一意な値のみ残ります。Setに固定の重複値を設定していたりすると警告が表示されたりもします。

void main() {
  Set<int> mySetValue = {1, 2, 1, 2, 1, 2};
  print(mySetValue);
}

image.png

ListのようにSetの値もfor文でループを行うことができます。

void main() {
  Set<int> mySetValue = {1, 2, 3};
  for (final value in mySetValue) {
    print(value);
  }
}

image.png

Setで使える属性やメソッドに関して

Set型ではList型と同じ属性やメソッドの多くを利用することができます。

例えば属性で言えばfirstfirstOrNulllengthsinglesingleOrNullisEmptyなどのListと同様の様々な各属性が、メソッドで言えばaddaddAllanyclearjoinなどの多くのメソッドが用意されています。

それらの属性やメソッドに関してはList型の節で詳しく触れたためSetのそれらの説明は割愛します。

マップ(辞書)の型

マップ(辞書)はMap型を使います。キーと値を持つ値となります。値を定義する際には{}の括弧を使い、キーと値をコロンで区切り、そして要素をコンマで区切ります。

void main() {
  Map myMapValue = {"myKey1": 10, "myKey2": "Hello"};
  print(myMapValue);
}

image.png

前節まででも触れましたがマップでキーと値の型を固定したい場合には<キーの型, 値の型>といった形でコンマ区切りでそれぞれを指定します。

キーをString、値をintの型に設定する例
void main() {
  Map<String, int> myMapValue = {
    'key1': 100,
    'key2': 200,
  };
  print(myMapValue);
}

image.png

MapEntry型

Map型について詳しく触れて行く前にMapEntryという型について触れた方がスムーズなので先に触れておきます。

MapEntry型は単一のキーと値の組み合わせを扱う型となります。Map型が複数のキーと値を持てる形になる一方でこちらは単一のキーと値になります。

使い方はクラスのコンストラクタで初期化し、第一引数にキー、第二引数に値を設定します。

void main() {
  MapEntry myMapEntry = MapEntry('key', 100);
  print(myMapEntry);
}

image.png

MapEntry型でキーを参照したい場合にはkey属性を使います。

void main() {
  MapEntry myMapEntry = MapEntry('myKey', 100);
  print(myMapEntry.key);
}

image.png

値の方はvalue属性でアクセスすることができます。

void main() {
  MapEntry myMapEntry = MapEntry('myKey', 100);
  print(myMapEntry.value);
}

image.png

Map型のisEmpty属性とisNotEmpty属性

Map型のisEmpty属性は辞書が空かどうか(1つのキーが存在しないかどうか)の真偽値となります。

空の辞書なのでtrueとなっている例
void main() {
  Map<String, int> myMapValue = {};
  print(myMapValue.isEmpty);
}

image.png

空ではないのでfalseとなっている例
void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
  print(myMapValue.isEmpty);
}

image.png

isNotEmpty属性はisEmptyと逆の値となります。つまり辞書が空でなければtrueとなります。

空ではないのでtrueとなっている例
void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
  print(myMapValue.isNotEmpty);
}

image.png

Map型のkeys属性とvalues属性

Map型のkeys属性は辞書の各キーを格納したIterableの値となります。

void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20, 'key3': 30};
  print(myMapValue.keys);
}

image.png

似たような形でvalues属性は辞書の値のIterableの値となります。

void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20, 'key3': 30};
  print(myMapValue.values);
}

image.png

Map型のlength属性

Map型のlength属性は辞書の要素数(キーの数)となります。

3件の要素(キー)を持つため3となる例
void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20, 'key3': 30};
  print(myMapValue.length);
}

image.png

Map型のentries属性

Map型のentries属性は各キーと値のMapEntry型の値を格納したIterableとなります。

void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20, 'key3': 30};
  print(myMapValue.entries);
}

image.png

Map型のaddAllメソッド

Map型のaddAllメソッドは引数に指定された別のMapの各キーと値を対象の辞書に追加します(2つの辞書を統合したような結果になります)。

void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
  myMapValue.addAll({'key3': 30, 'key4': 40});
  print(myMapValue);
}

image.png

キーが被っている場合には引数で指定した方の値で上書きされます。

key2が30で上書きされる例
void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
  myMapValue.addAll({'key2': 30, 'key3': 40});
  print(myMapValue);
}

image.png

Map型のaddEntriesメソッド

Map型のaddEntriesメソッドは引数に指定されたMapEntryのリスト(もしくは別のIterable)のキーと値を対象の辞書に追加します。

void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
  myMapValue.addEntries([
    MapEntry('key3', 30),
    MapEntry('key4', 40),
  ]);
  print(myMapValue);
}

image.png

こちらもaddAllメソッドと同様にキーが被っている場合には引数で指定された方の値で上書きされます。

key2が30で上書きされる例
void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
  myMapValue.addEntries([
    MapEntry('key2', 30),
    MapEntry('key3', 40),
  ]);
  print(myMapValue);
}

image.png

Map型のclearメソッド

Map型のclearメソッドは辞書の内容を空にします(1つもキーが無い状態にします)。

void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
  myMapValue.clear();
  print(myMapValue);
}

image.png

Map型のcontainsKeyメソッド

Map型のcontainsKeyメソッドは引数に指定したキー名を対象の辞書が持つかどうかの真偽値を返却します。キーがあればtrue、無ければfalseとなります。

キーがあるのでtrueとなる例
void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
  print(myMapValue.containsKey('key2'));
}

image.png

キーが無いのでfalseとなる例
void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
  print(myMapValue.containsKey('key3'));
}

image.png

Map型のforEachメソッド

Map型のforEachメソッドは引数に関数を受け取り、その関数では各キーと値を引数に持ち各要素数分実行されます。実質的にfor文でループを回すような挙動をします。関数の第一引数がキー、第二引数が値となります。

void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
  myMapValue.forEach((key, value) {
    print('キー: $key, 値: $value');
  });
}

image.png

Map型のmapメソッド

Map型のmapメソッドは引数に関数を受け付け、各キーと値に対してなんらかの処理を行った新たなMapの値を返却します。

関数の第一引数には辞書のキー、第二引数には辞書の値が設定されます。また、返却値にはMapEntryの値が必要になります。

各キーの末尾に0の文字を加えて、各値に100加算した新しいMapを取得する例
void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
  Map<String, int> newMapValue =
      myMapValue.map((key, value) => MapEntry(key + '0', value + 100));
  print(newMapValue);
}

image.png

Map型のputIfAbsentメソッド

Map型のputIfAbsentメソッドは第一引数で指定したキーがもし存在しなければ第二引数で指定した関数によって生成される値をそのキーに設定します。既に該当のキーが存在すれば処理をスキップします。

また、返却値にはもし対象のキーが元から存在すればそのキーの値が設定され、もし対象のキーが無ければ新たに設定された値が返却されます。

key3というキーは存在しないため30という値が設定される例
void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
  int putValue = myMapValue.putIfAbsent('key3', () => 30);
  print(myMapValue);
  print(putValue);
}

image.png

key2というキーが元から存在するため値の設定がスキップされる例
void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
  int putValue = myMapValue.putIfAbsent('key2', () => 30);
  print(myMapValue);
  print(putValue);
}

image.png

Map型のremoveメソッド

Map型のremoveメソッドは引数で指定したキーを辞書から取り除きます。返却値は取り除かれた値となります。もし削除対象のキーが存在しない場合には返却値はnullとなります(エラーにはなりません)。

辞書からkey2というキーを削除する例
void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
  int? removedValue = myMapValue.remove('key2');
  print(myMapValue);
  print(removedValue);
}

image.png

key3というキーは辞書に存在しないのでnullが返却されている例
void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
  int? removedValue = myMapValue.remove('key3');
  print(myMapValue);
  print(removedValue);
}

image.png

※余談ですがキーが存在する場合でも元から辞書の値でnullを格納していた場合にはnullが返却されたからといって必ずしもキーが存在しないという判定にはなりません。

Map型のremoveWhereメソッド

Map型のremoveWhereメソッドでは引数に判定用の関数を受け付け、その関数内で条件を満たすものを辞書から取り除きます。関数の第一引数には辞書キー、第二引数には辞書の値が設定されるため、キーか値(もしくは両方)を参照して判定を行うことができます。返却値には真偽値が必要となり、trueを返却した要素が辞書から取り除かれます。

条件を満たす要素が複数存在する場合にはそれらがまとめて取り除かれます。

辞書の値が20以上の要素を辞書から取り除く例
void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20, 'key3': 30};
  myMapValue.removeWhere((key, value) => value >= 20);
  print(myMapValue);
}

image.png

Map型のupdateメソッド

Map型のupdateメソッドは特定のキーの値を更新することができます。

普通にキーを指定して値を設定するのと何が違うのか?という所ですが、「現在設定されている値を参照して結果の値を設定する」「キーが存在しない場合には別の値を設定する」といった制御が可能です。

第一引数には対象のキーの文字列、第二引数には対象のキーが存在する場合にそのキーの値を参照して結果の値を返却する関数、第三引数には対象のキーが存在しない場合の値の設定用の関数(省略可)を指定します。第二引数の関数の引数には対象のキーの値が渡されます。また、第二引数と第三引数の関数両方とも設定したい値を返却する必要があります。

対象のキーが存在するためkey2に現在の値に対して100加算した値を設定する例
void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20, 'key3': 30};
  myMapValue.update('key2', (value) => value + 100, ifAbsent: () => -1);
  print(myMapValue);
}

image.png

Map型のupdateAllメソッド

Map型のupdateAllメソッドはmapメソッドのように関数の引数を使用して辞書の全ての要素に対して処理を反映します。mapメソッドのように引数で指定する関数の引数にはキーと値、返却値には更新後の値が必要になります。

mapメソッドと何が違うのか?というところですが、mapメソッドは新しい辞書を返却する一方でupdateAllメソッドの方は元の辞書自体を更新します。

元の辞書の全ての値に対して100加算する例
void main() {
  Map<String, int> myMapValue = {'key1': 10, 'key2': 20, 'key3': 30};
  myMapValue.updateAll((key, value) => value + 100);
  print(myMapValue);
}

image.png

dynamic型

dynamic型は特定の型に限られない任意の型を表します。型が明示できない場合は複数の型になりうる場合などに使用します。

柔軟性は得られるものの安全面やIDEなどでの補完やチェック等でマイナス面が多いため不要であれば使わずに型を明示した方が好ましいです。

任意の型のキーと値を辞書に設定できるようにdynamic型を使用している例
void main() {
  Map<dynamic, dynamic> myMapValue = {
    'key1': 10,
    350.5: true,
    false: 0.005,
  };
  print(myMapValue);
}

image.png

型のキャストについて

Dartでの型のキャスト(型変換)は主にtoやasで始まる名前のメソッドを使います(例 : toStringasMapなど)。

20.5という浮動小数点数をtoIntメソッドで20の整数に型変換する例
void main() {
  double myDoubleValue = 20.5;
  int myIntValue = myDoubleValue.toInt();
  print(myIntValue);
}

image.png

as 型名といったようにasのキーワードを使う方法もあります。ただし多くのケースで変換が効かない印象なのと、ランタイムエラーにもなりやすいという面を加味して基本的には整数や文字列などの基本的な型変換用の各メソッドを使う形がメインになりそうな印象です(一方で型が不明になる際に特定の型でasを使う・・・といったケースはたまにあるかなという印象です)。

20.5という値はasを使って整数に変換できないためエラーになる例
void main() {
  int myIntValue = 20.5 as int;
  print(myIntValue);
}

image.png

また、他の言語で良くあるような対象の型のコンストラクタとしてキャストする・・・といったこともDartではできません。コンパイルエラーとなります。

int型によるコンストラクタでのキャストをしようとしてエラーになっている例
void main() {
  int myIntValue = int(20.5);
  print(myIntValue);
}

image.png

void型

void型は関数やメソッドの返却値の型の指定などで使用します。void型を指定した場合返却値を返さないということを示します。関数に関してはまだ詳しくは触れていませんが、今までの節でもmain関数などで記述してきた型となります。

void main() {
  printMessage();
}

void printMessage() {
  print('Hello, World!');
}

image.png

nullについて

Dartでの何も値を持っていないものを表すにはnullを使います。有効な値を返却できない場合や初期化前などで様々なケースで使われます。

型で定義する際には型名の直後に?の記号を付けます。例えばnullの値を取りうる文字列であればString?といったように書きます。

nullで初期化してその後に文字列を設定している例
void main() {
  String? myStringValue = null;
  print(myStringValue);

  myStringValue = 'Hello!';
  print(myStringValue);
}

image.png

また、nullの型指定をした値に関しては初期値を与えなくても参照することができます(初期化をしなくても使えます)。その場合その値はnullとなります。jsのundefinedのような概念はありません。

初期化せずに変数をprintしている例
void main() {
  String? myStringValue;
  print(myStringValue);
}

image.png

nullの型指定をしていない場合、初期値を与えていないと参照時にエラーになります。

void main() {
  String myStringValue;
  print(myStringValue);
}

image.png

Dartのnull安全について

Dartは3系のバージョン以降では基本的にnull安全になっています。うっかりミスなどでnullになって欲しくないケースでnullを参照してしまっていてランタイムエラーになる・・・といったことをしっかりと減らすことができます(コンパイルエラーとなるためうっかりnullに絡んでランタイムエラーになる条件が残ったままリリースしてしまうといったことを減らすことができます)。

たとえば以下のように文字列の引数を必要にする関数でnullを受け付ける文字列の変数を指定した場合にはコンパイルエラーとなります。

void main() {
  String? myStringValue;
  printMessage(message: myStringValue);
}

void printMessage({required String message}) {
  print(message);
}

image.png

もうDart2系を使うケースもほとんど無くなって来ていると思うため、null安全の面で悩まされるケースも少ないのでは・・・という印象です。

nullを取りうる値に対する型ガード

nullを取りうる変数などに対してif文などでnullかどうかを判定することで型ガードのように変数の型の絞り込みを行うことができます。

※if文などの条件分岐は後々の節で詳しく触れます。

if文でnullではないという条件で絞り込んでいるためエラーにならずに引数に指定できている例
void main() {
  String? myStringValue = getMessage();
  if (myStringValue != null) {
    printMessage(message: myStringValue);
  }
}

String? getMessage() {
  return 'Hello!';
}

void printMessage({required String message}) {
  print(message);
}

image.png

!記号による参照

if文などで確実にnull安全を担保できているとコンパイルエラーでミスを弾けて好ましいのですが、if文で判定するだけでは不便なことがあります。例えばif文などは挟んでいないものの、nullであれば確実にエラーで弾くようにしているためnullにはならないことが分かっている・・・といったケースです。

そういった場合にはnullを取りうる変数の後に!記号を付けることでnullを取らない変数と同じように扱うことができコンパイルエラーを回避することができます。

myStringValue!と!記号を使っているのでnullに絡んでコンパイルエラーにならない例
void main() {
  String? myStringValue = getMessage();
  printMessage(message: myStringValue!);
}

String? getMessage() {
  return 'Hello!';
}

void printMessage({required String message}) {
  print(message);
}

image.png

ただし、うっかりコード変更などで実はnullを取りうる形になっていた・・・みたいなケースが発生するとnullに起因してエラーが発生したりしてくるたと、安全のためにももしif文とかで対応が効くケースなら!の記号よりもそちらの方が推奨されます。

?記号による安全な属性やメソッドなどへのアクセス

?記号を使うことでnullになりうる値で属性やメソッドにアクセスする場合、nullの場合にはそれらの属性などにはアクセスできないためランタイムエラーになってしまいます。

そういった場合にはその変数などの直後に?の記号を付けることでランタイムエラーを回避することができます。例えばmyStringValueというnullを取りうる変数でmyStringValue.lengthといった属性にアクセスすると変数がnullだった場合にランタイムエラーになってしまいます。一方でmyStringValue?.lengthといったように?記号を付与しつつアクセスした場合には変数がnullであればランタイムエラーにはならずにそのままnullnullでなければlength属性の値が参照できる・・・といった挙動になります。

?記号を付与しているのでコンパイルエラーにならない例
void main() {
  String? myStringValue = getMessage();
  print(myStringValue?.length);
}

String? getMessage() {
  return 'Hello!';
}

image.png

対象の変数がnullの場合に属性にアクセスしてもエラーにならない例
void main() {
  String? myStringValue = getMessage();
  print(myStringValue?.length);
}

String? getMessage() {
  return null;
}

image.png

??記号によるnull合体演算子

対象の変数などの値の後にスペースと??の記号を記述すると、もしその値がnullだった場合にその後に指定した値を代わりに参照する・・・といった制御ができます。

例えばmyStringValue ?? '値がnullです。'といった記述を使った場合、myStringValueがnullでなければmyStringValueの値がそのまま参照され、もしnullであれば'値がnullです。'という文字列が参照されます。三項演算子のような挙動をします。

値がnullの場合に??の後の値が参照される例
void main() {
  String? myStringValue = getMessage();
  print(myStringValue ?? '値がnullです。');
}

String? getMessage() {
  return null;
}

image.png

値がnullではないので変数の値がそのまま使われる例
void main() {
  String? myStringValue = getMessage();
  print(myStringValue ?? '値がnullです。');
}

String? getMessage() {
  return 'Hello!';
}

image.png

??=記号によるnullの代替の値の設定

??=記号を使うと対象の変数がもしnullであれば代替の値を設定するという挙動をします。たとえばmyStringValue ??= '値がnullです';という記述であれば、もしmyStringValueという変数がnullであれば右辺の'値がnullです'という値が設定されます。値がnullでなければ元の値がそのまま維持されます。

値がnullなので代替の文字列が設定される例
void main() {
  String? myStringValue = getMessage();
  myStringValue ??= '値がnullです';
  print(myStringValue);
}

String? getMessage() {
  return null;
}

image.png

値がnullではないので元の値がそのまま使用される例
void main() {
  String? myStringValue = getMessage();
  myStringValue ??= '値がnullです';
  print(myStringValue);
}

String? getMessage() {
  return 'Hello';
}

image.png

メソッドチェーン風の記述

Dart自体ではクラスのメソッドでthisを返却することでメソッドチェーンのようなことは他の言語と同じように実装することができます。
※クラスについての詳細は後々の節で触れます。

class Counter {
  int _currentCount = 0;

  Counter increment() {
    _currentCount++;
    return this;
  }

  Counter decrement() {
    _currentCount--;
    return this;
  }

  Counter printCurrentCount() {
    print('現在のカウント: $_currentCount');
    return this;
  }
}

void main() {
  Counter counter = Counter();
  counter.increment().increment().increment().decrement().printCurrentCount();
}

image.png

一方で、メソッドなどでインスタンスを返却しない場合でもDartでは..の記号を使うことでメソッドチェーンのようなことができます。インスタンスを返却する形に作られていないクラスとかでも使えたり、返却用の無駄な行が不要になるため個人的にはこちらを使うことが多めです。

インスタンスを返却しない各メソッドで..記号を使ってメソッドチェーンのような書き方をしている例
class Counter {
  int _currentCount = 0;

  void increment() {
    _currentCount++;
  }

  void decrement() {
    _currentCount--;
  }

  void printCurrentCount() {
    print('現在のカウント: $_currentCount');
  }
}

void main() {
  Counter counter = Counter();
  counter..increment()..increment()..increment()..decrement()..printCurrentCount();
}

image.png

なお、フォーマッタをかげると..記号で改行されるようです。

image.png

関数について

以降の節では関数について詳しく触れていきます。

関数定義の基本

関数は返却値の型 関数名(引数内容) { 関数の処理内容 }といったように定義します。引数定義の箇所は変数定義などと同じように型名 引数名といった順番で書きます。また、複数の引数を受け付ける場合にはコンマ区切りで定義します。

例えば返却値の型がvoid、関数名がprintMessage、引数にはString型でmessageという引数を受け付ける場合にはvoid printMessage(String message) { ... }といった書き方になります。

void main() {
  printMessage('Hello!');
}

void printMessage(String message) {
  print(message);
}

image.png

関数の呼び出し

関数を呼び出したい場合には対象の関数名(指定する引数内容)といった形で()の括弧を使います。また、返却値(後の節で触れます)を別の変数などに設定したければ=の記号を使って左側に対象の変数などを指定します。

10と20という2つの引数値を指定して、返却値をaddedValueという変数に設定している例
void main() {
  int addedValue = addTwoValues(10, 20);
  print(addedValue);
}

int addTwoValues(int a, int b) {
  return a + b;
}

image.png

位置引数の定義と指定

関数を呼び出す際に単純に順番通りに引数の値を指定する際には引数の定義も前節の通りシンプルにコンマ区切りで定義すれば扱うことができます(位置引数と言います)。関数を呼び出す時もコンマ区切りで順番に値を設定すれば対応ができます。

void main() {
  printMessage('Hello', ' World!');
}

void printMessage(String message1, String message2) {
  print('Message1: $message1\nMessage2: $message2');
}

image.png

一方で引数名と共に引数の値を指定する方法もあり、こちらはDartだと名前付き引数などと呼ばれるようです(他の言語だとキーワード引数などと呼ばれたりもします)。名前付き引数については次の節で触れていきます。

名前付き引数の定義と指定

名前付き引数を使うと関数を呼び出す際に引数の値と共に引数名をセットで指定することができます。

名前付き引数を使うと何が嬉しいのか?というところですが、例えば関数呼び出し箇所で引数名も一緒に記述されるので何の引数なのかが分かりやすいといった面や、引数の順番を変えたりした際に影響を少なくする・・・といった形で可読性を高めたり堅牢なコードを書きたい時に役立つことがあります。

この辺りはDartではなくPythonで以前記事にもしているので必要でしたらご参照ください。

名前付き引数を使った関数を定義したい場合には引数の箇所で{引数内容}といったように{}の括弧で囲む必要があります。また、省略可能な引数以外では引数の前にrequiredと付ける必要があります(省略可能な引数に関しては後々の節で触れます)。

名前付き引数を使う形で関数を呼び出す場合には引数名: 指定する値といった形でコロンで名前と値を区切って指定します。

名前付き引数のmessage1とmessage2を使って関数を呼び出している例
void main() {
  printMessage(message1: 'Hello', message2: ' World!');
}

void printMessage({required String message1, required String message2}) {
  print('Message1: $message1\nMessage2: $message2');
}

image.png

名前付き引数のデフォルト値

名前付き引数を省略可の状態(requiredを付けない形)にするには対象の引数でnullも設定可能にするか、もしくは=の記号を使ってデフォルト値を引数に設定しておく必要があります。

名前付き引数に?記号を付けてnullを受け付ける形にしたのでrequiredを省ける例
void main() {
  printMessage();
}

void printMessage({String? message}) {
  print('Message: $message');
}

image.png

デフォルト値を設定する場合には引数の型 引数名 = デフォルト値といった形で引数部分で記述します。

'Hello!'という文字列のデフォルト値を引数に設定しているのでrequiredを省ける例
void main() {
  printMessage();
}

void printMessage({String message = 'Hello!'}) {
  print('Message: $message');
}

image.png

返却値の設定

関数に返却値を設定したい場合には関数の処理内でreturn 返却値に設定する値といった形でreturnキーワードを使います。

また、return部分で指定した値の型に合わせて関数名の左に記述する型を設定します。

2つの値を加算した整数を返却する例
void main() {
  int addedValue = addTwoValues(100, 200);
  print(addedValue);
}

int addTwoValues(int a, int b) {
  return a + b;
}

image.png

関数の型設定

関数を別の関数の引数に設定することはDart/Flutterでは結構あります(イベントのハンドラやコールバックなどで)。そういった場合には引数で型の定義が必要になることがありますが、関数の型の定義を含めた引数は位置引数の場合には返却値の型 Function(引数内容) 引数名といった形で書きます。例えば返却値の型はvoid、整数のpriceという引数と文字列のmessageという2つの位置引数を持ち、引数名がotherFunctionという名前の関数の引数を定義したい場合にはvoid Function(int price, String message) otherFunctionという引数の書き方になります。

※関数呼び出し箇所で無名関数を使っていますが、そちらは後々の節で詳しく触れます。

myFunction関数の引数でotherFunctionという関数の引数を受け付けている場合
void main() {
  myFunction((int price, String message) => print('$price, $message'));
}

void myFunction(void Function(int price, String message) otherFunction) {
  otherFunction(120, 'Hello');
}

位置引数ではなく名前付き引数を設定したい場合には通常の関数定義時と同じように引数に{}の括弧で囲ったり必要に応じてrequiredの設定やデフォルト値の設定などを行います(例 : {required int price, required String message})。

名前付き引数を持った関数を引数に設定する例
void main() {
  myFunction(
    ({required int price, required String message}) =>
        print('$price, $message'),
  );
}

void myFunction(
  void Function({required int price, required String message}) otherFunction,
) {
  otherFunction(price: 120, message: 'Hello');
}

image.png

無名関数について

Dartで無名関数(関数名が設定されていない関数)を定義するには{}の括弧を使う方法と=>の記号を使う場合の2パターンが存在します。

通常の関数と{}を使う無名関数の場合と=>を使う無名関数の場合の句使い分けですが、人によって意見は様々だと思いますが目安程度に個人的には以下のような感じで考えています。

  • =>の記号を使った無名関数: 1つのステートメントもしくは単一行のシンプルな処理の関数
  • {}の括弧を使った無名関数: 1行~数行程度のシンプルな処理の関数
  • 通常の関数: 上記に該当しない処理が多めな関数など

=>を使った無名関数の定義方法

=>記号を使って無名関数を定義する場合には(引数内容) => 単一のステートメントなどといった形で書きます。=>よりも右側の値は返却値としても扱われます。返却値の設定のためにreturnなどのキーワードは不要です。

例えば整数のabという2つの引数を受け付け、abを加算した値を返却する無名関数を定義したい場合には(int a, int b) => a + bといった記述になります。

定義した無名関数は変数として扱ったり、もしくは引数に指定したりして扱うことができます。また、関数の呼び出しも通常の関数のように行うことができます。

void main() {
  final addTwoValuesFunc = (int a, int b) => a + b;
  int addedValue = addTwoValuesFunc(10, 20);
  print(addedValue);
}

image.png

{}の括弧を使った無名関数の定義方法

{}の括弧を使って無名関数を定義する場合には(引数内容) { 関数内容 }といった形で書きます。こちらでは返却値を設定するにはreturnキーワードが必要になります。

例えば整数のabという2つの引数を受け付け、abを加算した値を返却する無名関数を定義したい場合には以下のような書き方になります。

void main() {
  final addTwoValuesFunc = (int a, int b) {
    return a + b;
  };
  int addedValue = addTwoValuesFunc(10, 20);
  print(addedValue);
}

image.png

無名関数での名前付き引数の利用

無名関数でも名前付き引数を使うことができます。書き方も通常の関数の時と同様で、引数定義の箇所で{}の括弧や必要に応じてrequiredの記述などを使います。また、呼び出す際も同様で引数名: 指定する値といった形で引数を指定して使います。

void main() {
  final addTwoValuesFunc = ({required int a, required int b}) {
    return a + b;
  };
  int addedValue = addTwoValuesFunc(a: 10, b: 20);
  print(addedValue);
}

image.png

無名関数の引数に型の指定を行うかどうか

通常の関数もそうですが、無名関数では引数の型の記述を省略することができます。

無名関数のa引数とb引数で型を明示していない例
void main() {
  final addTwoValuesFunc = (a, b) {
    return a + b;
  };
  int addedValue = addTwoValuesFunc(10, 20);
  print(addedValue);
}

image.png

ただしこの場合引数に任意の型の値を受け付けてしまうため、コンパイルエラーにならずに予期せぬランタイムエラーが発生することが起こり得ます。

各引数が整数と文字列だと関数の内容を処理できなくてランタイムエラーになる例
void main() {
  final addTwoValuesFunc = (a, b) {
    return a + b;
  };
  int addedValue = addTwoValuesFunc(10, 'Hello');
  print(addedValue);
}

image.png

そのため前述のような無名関数を定義する場合には引数の型を明示しておいた方が無難だと思います。

一方で無名関数はイベントのハンドラやコールバックとして引数に指定することも多くあります。これらの用途の場合には呼び出す関数側で引数の関数の型が明示されていれば型の不一致などによるランタイムエラーは防げてビルド時点で事前にミスなどを検知することができます。

otherFunctionという引数側で関数の引数の型を明示している例
void main() {
  myFunction(otherFunction: (message) => print(message));
}

void myFunction({required void Function(String message) otherFunction}) {
  otherFunction('Hello!');
}

この場合には無名関数側は型を省略しても安全面で差は少ないですし記述がシンプルになるので積極的に使っても良いと思われる書き方と言えます(Effective Dartなどの資料でもこの辺りに触れられていたような気がします)。

関数でのジェネリックな型の利用

関数でジェネリックの型を使う際には返却値の型 関数名<ジェネリックの型名>(引数内容) { ... }といった形で書きます。例えばTというジェネリックの型名をmyFunctionという関数で使う場合にはvoid myFunction<T>() { ... }といったように書きます。

定義したジェネリックの型は返却値や引数の型に設定することができます。

myFunction関数の返却値と引数にもジェネリックの型を設定している例
void main() {
  final returnedValue = myFunction('Hello!');
  print(returnedValue);
}

T myFunction<T>(T value) {
  return value;
}

また、関数を呼び出す箇所でジェネリックの型を明示することもできます。その場合は呼び出す関数名<ジェネリックで設定したい型>(引数内容)といったように書きます。

関数呼び出し箇所でという形で文字列の型を指定している例
void main() {
  final returnedValue = myFunction<String>('Hello!');
  print(returnedValue);
}

T myFunction<T>(T value) {
  return value;
}

以下のように呼び出し側でジェネリックの型に指定した型以外が返却値や引数に使われていたらコンパイルエラーになることが確認できます。

ジェネリックの型にintを指定した一方で返却値と引数が文字列になっていてエラーになる例
void main() {
  String returnedValue = myFunction<int>('Hello!');
  print(returnedValue);
}

T myFunction<T>(T value) {
  return value;
}

image.png

クラスについて

以降の各節でDartのクラスについて詳しく触れていきます。

クラス定義の基本

Dartでクラスはclass クラス名 { クラス内容 }といった形で書きます。

なにも内容が定義されていない最低限のクラス定義をすると以下のような感じになります。

void main() {
  final instance = MyClass();
  print(instance);
}

class MyClass {}

クラスの型設定と初期化

クラスを初期化(インスタンス化)するにはクラス名(引数内容)といった形で関数と同じように()の括弧を使います。

例えばコンストラクタでidという名前付き引数を必要とするクラスであればMyClass(id: 10)といった書き方になります(コンストラクタについては後で触れます)。

void main() {
  final instance = MyClass(id: 10);
  print(instance.id);
}

class MyClass {
  final int id;
  MyClass({required this.id});
}

image.png

また、定義したクラスは型として他の型と同様にクラス名 変数などといった形で指定することができます(例 : MyClass instance = MyClass())。

instance変数にクラスの型を指定している例
void main() {
  MyClass instance = MyClass(id: 10);
  print(instance.id);
}

class MyClass {
  final int id;
  MyClass({required this.id});
}

image.png

属性の定義方法と参照

属性を定義するにはクラス内のスコープ(クラス名の後の{}の括弧内)で変数を宣言するのと似たような形で定義できます。例えばidという整数且つ初期値が0の属性をクラスで定義したい場合にはクラス内のスコープでint id = 0;といった記述を追加しておきます。また、定義した属性に関してはインスタンス名.属性名といった形でドットで繋いで参照したり更新などを行うことができます。

void main() {
  MyClass instance = MyClass();
  print(instance.id);

  instance.id = 20;
  print(instance.id);
}

class MyClass {
  int id = 0;
  MyClass();
}

image.png

コンストラクタの引数でthis.引数名とすることでコンストラクタに渡された引数の値を直接属性に設定することもできます(例 : this.id)。この辺りは後々の節で詳しく触れます。

void main() {
  MyClass instance = MyClass(20);
  print(instance.id);
}

class MyClass {
  int id;
  MyClass(this.id);
}

image.png

また、属性定義時に初期値を与えている場合やコンストラクタで属性に値を設定している場合には属性にfinalの設定を付与することができます(後の節で触れるstaticを使うケースを除いてクラスの属性にconstの属性は使用できません)。

id属性をfinalで定義している例
void main() {
  MyClass instance = MyClass(20);
  print(instance.id);
}

class MyClass {
  final int id;
  MyClass(this.id);
}

image.png

メソッドの書き方と呼び出し方

クラスのスコープ内で関数を定義する形でメソッド(クラスのインスタンスが持つ関数)を定義することができます。

そのメソッドを呼び出したい場合にはインスタンス名.メソッド名(引数内容) { メソッド内容 }といった形でドットで繋ぐ形で関数呼び出しの時と同じように書きます(例 : instance.incrementId();)。

id属性を1加算するincrementIdメソッドをクラスに定義した例
void main() {
  MyClass instance = MyClass(20);
  instance.incrementId();
  print(instance.id);
}

class MyClass {
  int id;
  MyClass(this.id);

  void incrementId() {
    id++;
  }
}

また、クラスの属性や他のメソッドに関してはメソッド内ではそのまま参照します。他の言語であるようなthisselfなどを経由しません(前記のコードでもthis.idといった記述ではなく直接id属性をメソッド内で参照しています)。

ただし名前が被る際にはこのインスタンスの属性やメソッドということを明示するためにthisを付けて属性やメソッドにアクセスすることもできます(例 : this.id++;)。

その他、名前付き引数など関数側で説明してきた機能に関してはクラスのメソッドでも同様に使用することができます。

staticの属性とメソッドの定義

クラスの属性やメソッドはstaticキーワードを付与して定義することもできます。例えば属性であればstatic int id = 0;、メソッドであればstatic void incrementId() { ... }みたいな形で定義します。

staticのキーワードが付与されたものはその属性へのアクセスやメソッドの呼び出しはインスタンス化した後のインスタンス経由ではなくクラス自体を参照して利用する形が主になります。

例えばMyClassというクラスに定義されているidというstaticの属性を参照する場合にはMyClass.idといったように書きます。

staticのメソッドと属性を利用する例
void main() {
  MyClass.incrementId();
  MyClass.incrementId();
  print(MyClass.id);
}

class MyClass {
  static int id = 0;

  static void incrementId() {
    MyClass.id++;
  }
}

image.png

また、通常はクラスの定数の属性にはfinalしか設定できませんがstaticキーワードを付与した場合にはconstも利用可能になります。

属性の定数にconstを使う例
void main() {
  print(Language.dart);
}

class Language {
  static const String dart = 'Dart';
  static const String python = 'Python';
  static const String rust = 'rust';
}

image.png

コンストラクタの引数で直接属性に値を設定する書き方

前節までで軽く触れてきましたがコンストラクタの引数を直接属性に設定したい場合には引数定義でthis.引数名といった形でドット区切りでthisを付けて定義します(例 : this.id)。記述がシンプルになっておすすめです。また、Flutterではウィジェットの引数とかでそもそもこの書き方を使わないと静的解析で警告が出たりすることもあります。

void main() {
  final instance = MyClass(id: 10, name: 'John');
  print(instance.id);
  print(instance.name);
}

class MyClass {
  final int id;
  final String name;

  MyClass({required this.id, required this.name});
}

image.png

クラスをconstキーワードと共に初期化する

属性が全てfinalで定義されている場合・・・といった制約がありますが、条件を満たした場合コンストラクタのクラス名の前にconstキーワードを設定することができます(例 : const MyClass();)。

また、コンストラクタの定義でconstキーワードを付与した場合にはそのクラスのインスタンスもconstキーワードを使って定義することができます。

constで定義することでそのクラスのインスタンス自体を定数化でき堅牢になるのに加えて(そこまで差は大きくないと思いますが)constで定義した方がパフォーマンスが良いそうです。

コンストラクタの定義とインスタンス化でconstキーワードを使用している例
void main() {
  const instance = MyClass(id: 10, name: 'John');
  print(instance.id);
  print(instance.name);
}

class MyClass {
  final int id;
  final String name;

  const MyClass({required this.id, required this.name});
}

image.png

コンストラクタの引数で直接親クラスに値を渡す書き方

クラスの親子関係を作るための継承については後々の節で触れますが、親クラスのコンストラクタの引数に直接引数を渡したい場合にはsuper.親の引数名といった形で書きます(例 : super.id)。

thisを使った書き方と同様に、こちらの書き方で対応が効く場合には記述がシンプルになります。

thisを使って子のクラスのコンストラクタで親のコンストラクタに直接引数を渡している例
void main() {
  const instance = ChildClass(id: 10, name: 'John');
  print(instance.id);
  print(instance.name);
}

class ParentClass {
  final int id;
  final String name;

  const ParentClass({required this.id, required this.name});
}

class ChildClass extends ParentClass {
  const ChildClass({required super.id, required super.name});
}

image.png

コンストラクタで直接親クラスの引数に引数を一部渡しつつ、別途固定値などを親の引数に渡す書き方

親のクラスのコンストラクタにsuperで直接引数を渡しつつ、一部の値や別の固定値などを子のクラス側で指定する・・・みたいな書き方をすることもできます。

その場合にはコンストラクタの引数の定義の後に : super(固定値などを指定する残りの親クラスへの引数設定);といった具合にコロンやメソッドとしてのsuperを記述して書きます(例 : ChildClass({required super.id, required super.name}) : super(language: '日本語'))。

id引数とname引数は直接親のクラスの引数に渡し、language引数は別途固定値を指定している例
void main() {
  const instance = ChildClass(id: 10, name: 'John');
  print(instance.id);
  print(instance.name);
}

class ParentClass {
  final int id;
  final String name;
  final String language;

  const ParentClass({
    required this.id,
    required this.name,
    required this.language,
  });
}

class ChildClass extends ParentClass {
  const ChildClass({required super.id, required super.name})
      : super(language: '日本語');
}

image.png

クラスでのジェネリックな型の利用

クラスでジェネリックの型を使いたい場合にはクラス名の後に<ジェネリックの型名>といった形で設定します(例 : class MyClass<T> { ... })。

ジェネリックの型を設定した後は属性や引数などに対象のジェネリックの型を設定することができます(属性での例 : final T value;)。

また、そのクラスを使ったインスタンスの変数をの型を定義する際やインスタンス化のタイミングでクラス名の直後に<ジェネリックに指定する型>といった形で指定する形で設定する型を明示することができます(例 : MyClass<int>(value: 20);)。

void main() {
  const instance = MyClass<int>(value: 20);
  print(instance.value);
}

class MyClass<T> {
  final T value;

  const MyClass({required this.value});
}

複数のジェネリックの型を使いたい場合にはコンマ区切りで型を設定します。

SとTという2つのジェネリックの型を使う例
void main() {
  const instance = MyClass<int, String>(value1: 20, value2: 'Hello!');
  print(instance.value1);
  print(instance.value2);
}

class MyClass<S, T> {
  final S value1;
  final T value2;

  const MyClass({required this.value1, required this.value2});
}

image.png

getterメソッドの定義

Dartのクラスでgetterメソッド(属性のように値の取得で利用できるメソッド)は対象のメソッド名の前にgetキーワードを記述することで対応することができます。また、引数は使わないので引数の括弧なども使いません(例 : int get value { ... })。返却値の型が実質的に属性の型のように動作します。

getterを参照する場合には括弧などは使わずに普通の属性のように参照します。

valueのgetterメソッドに対して属性のようにアクセスしている例
void main() {
  final instance = MyClass();
  print(instance.value);
}

class MyClass {
  int _value = 50;

  int get value {
    return _value;
  }

  MyClass();
}

image.png

setterメソッド定義

setterメソッド(属性のように値の更新で利用できるメソッド)は対象のメソッド名の前にsetキーワードを記述することで対応することができます。引数は1つの値のみ受け付けます。

値を設定するには属性に値を設定するように=で値を指定します(例 : instance.value = 100;)。

また、基本的に返却値はvoid相当となるため返却値の型の記述は省略できます。

void main() {
  final instance = MyClass();
  instance.value = 100;
  print(instance.value);
}

class MyClass {
  int _value = 50;

  int get value {
    return _value;
  }

  set value(int value) {
    _value = value;
  }

  MyClass();
}

image.png

なお、他の言語と同様にsetterを設けずにgetterのみを設けて値の取得のみを許可して外部からの更新をできなくする・・・といった制御も行うことができます。

継承について

親となるクラスを継承して子クラスを作るにはclass クラス名 extends 親クラス名 { ... }といった形でextendsキーワードを使って書きます。

継承に関しては使い方が微妙だと苦しくなるケースもあるとは思うため気軽に複雑だったり巨大な継承などは控えめになると良いとかはあるかもしれませんが、継承することで親クラスの属性定義やメソッド定義を使いまわすことができるようになります(Flutterでは継承を多用する形となります)。

Animalという親クラスを継承したCatというクラスを使用している例
void main() {
  final cat = Cat(name: 'タマ', age: 8);
  print(cat.species);
  print(cat.name);
  print(cat.age);
}

class Animal {
  final String species;
  final String name;
  final int age;

  const Animal({
    required this.species,
    required this.name,
    required this.age,
  });
}

class Cat extends Animal {
  const Cat({
    required super.name,
    required super.age,
  }) : super(species: '猫');
}

image.png

継承したメソッドの上書き

継承した親クラスのメソッドなどを上書きしたい場合にはメソッドの上に@overrideと付けて、子のクラス側で同名・同じ型で再度メソッドを定義します。

walkメソッドを子のCatクラス側で上書きしている例
void main() {
  final cat = Cat(name: 'タマ');
  cat.walk();
  print(cat.xPosition);
}

class Animal {
  final String species;
  final String name;
  int xPosition = 0;

  Animal({
    required this.species,
    required this.name,
  });

  void walk() {
    xPosition += 1;
  }
}

class Cat extends Animal {
  Cat({
    required super.name,
  }) : super(species: '猫');

  @override
  void walk() {
    xPosition += 3;
  }
}

image.png

privateでの定義とpublicでの定義

本記事では別ファイルに記述したDartコードの取り扱いまでは触れません(次記事でFlutterとかと併せて触れます)が、Dartではprivate(他のファイル内からは参照できないようにする設定)をしたい場合には変数名・定数名・関数(メソッド)名・クラス名などの先頭に_のアンダースコアを付けます(例 : _id)。

クラスの属性やメソッドなどに関しては同じファイル内であればアンダースコアを付けていても参照できます(そのクラス内でしか参照できないといったことはありません)。

変数やクラス・メソッドなどでプライベートの定義を利用した例
late _MyClass _instance;

void main() {
  _instance = _MyClass();
  _instance._printIdAndName();
}

class _MyClass {
  final int _id = 10;
  final String _name = 'John';

  _MyClass();

  void _printIdAndName() {
    print(_id);
    print(_name);
  }
}

image.png

privateで定義したものは対象のファイル内で参照されていなければ警告が出るようになります(Lintとかでチェックされるようにしてあれば使用していないものを切り落としたりなどがしやすくなります)。

参照していないprivateのものが存在する場合に警告が出る例
late _MyClass _instance;

void main() {
  _instance = _MyClass();
}

class _MyClass {
  final int _id = 10;
  final String _name = 'John';

  _MyClass();

  void _printIdAndName() {
    print(_id);
    print(_name);
  }
}

image.png

public(他のファイルから参照できる定義設定)で定義したい場合には前節までのようにアンダースコアを先頭に付けずに各名前を設定します。

ミックスインについて

通常の継承とは別に、Dartではミックスインをクラスに設定することができます。ミックスインは通常の継承のように特定のクラスに属性やメソッドなどの機能を付与することができます(Rustのトレイトのような機能になります)。

通常の継承と比べて以下のような違いがあります。

  • 通常の継承のようにミックスイン自体が他のミックスインやクラスなどを継承することはできません(何階層も継承を重ねるといったことはできません)。
    • ただし特定のクラスの継承やミックスイン設定が無いと対象のミックスインを利用できない制約を付与することはできます。
  • 複数のミックスインを特定のクラスに同時に設定することができます。
    • 通常の継承は1度に1つの継承しか行うことはできません。

また、人によって意見は様々だとは思いますがミックスインは継承よりも複雑で変更が効きづらい形にはなりにくく、パーツのように付け外しなどを行うことで機能の追加や削除などの調整が効きやすいので個人的には通常の継承よりも好みではあります(Flutterを扱う上では継承は多用する形にはなりますが・・・)。

ミックスインの設定の仕方

ミックスインを定義するにはクラスと似たような感じでmixin ミックスイン名 { ... }といった形でmixinキーワードを使用して定義します。

mixin WalkMixin {
  int xPosition = 0;

  void walk() {
    xPosition += 1;
  }
}

定義したミックスインを特定のクラスで使うためにはクラス名の後などにwith ミックスイン名といった形でwithキーワードを使います(通常の継承をしている場合にはextendsでの継承の記述の後などにwithの設定を追加していきます)。

WalkMixinをAnimalクラスに設定して、Animalクラスでwalkメソッドを利用可能にしている例
void main() {
  final animal = Animal(name: 'タマ');
  animal.walk();
  print(animal.xPosition);
}

mixin WalkMixin {
  int xPosition = 0;

  void walk() {
    xPosition += 1;
  }
}

class Animal with WalkMixin {
  final String name;

  Animal({required this.name});
}

image.png

複数のミックスインを設定する

複数のミックスインを特定のクラスに設定したい場合にはwithの後の指定でコンマ区切りで複数のミックスインを設定することができます。

新たにRunMixinをAnimalクラスに設定した例
void main() {
  final animal = Animal(name: 'タマ');
  animal.walk();
  print(animal.xPosition);

  animal.run();
  print(animal.xPosition);
}

mixin WalkMixin {
  int xPosition = 0;

  void walk() {
    xPosition += 1;
  }
}

mixin RunMixin {
  int xPosition = 0;

  void run() {
    xPosition += 3;
  }
}

class Animal with WalkMixin, RunMixin {
  final String name;

  Animal({required this.name});
}

image.png

他の特定のクラスやミックスインを継承していないと使えないミックスインにする

ミックスインを特定のクラスや他のミックスインが設定されていないと使えないようにする・・・といった設定も行うことができます。

これによって、ミックスイン自体は他のクラスやミックスインを継承したりはしていないものの、それらの属性やメソッドをミックスイン内で参照できるようになります(コンパイルが通るようになります)。

そのような設定をしたい場合にはミックスイン名の後にon 制限で設定したいクラス名やミックスイン名といった形でonのキーワードを使います。

例えば以下の例ではRunMixinの方はonキーワードでWalkMixinを設定しているクラスなどにしか設定できない制約を追加してあります。また、RunMixinの方にはxPosition属性を定義していませんがonで設定した制約のおかげでコンパイルエラーにならずに対象の属性を参照できています。

void main() {
  final animal = Animal(name: 'タマ');
  animal.walk();
  print(animal.xPosition);

  animal.run();
  print(animal.xPosition);
}

mixin WalkMixin {
  int xPosition = 0;

  void walk() {
    xPosition += 1;
  }
}

mixin RunMixin on WalkMixin {
  void run() {
    xPosition += 3;
  }
}

class Animal with WalkMixin, RunMixin {
  final String name;

  Animal({required this.name});
}

image.png

インターフェイスについて

Dartでは通常の継承やミックスインの他にもインターフェイス設定も行うことができます。任意のインターフェイスを設定したクラスではインターフェイス側で定義されていたメソッドは必ずoverrideで上書きする必要があります。

インターフェイスを使うにはまずインターフェイス用にクラスを定義し、そのインターフェイスを設定したいクラスでclass クラス名 implements インターフェイス名といった感じでimplementsキーワードを使って設定します。

また、インターフェイスを設定したクラスではインターフェイスのメソッドを一通り@overrideを付けて内容を上書きする必要があります。

void main() {
  final animal = Animal(name: 'タマ');
  animal.walk();
  print(animal.xPosition);
}

class WalkInterface {
  void walk() {}
}

class Animal implements WalkInterface {
  final String name;
  int xPosition = 0;

  Animal({required this.name});

  @override
  void walk() {
    xPosition += 1;
  }
}

image.png

クラス・ミックスイン・インターフェイスを継承しているかの判定と型のキャスト

インスタンスが特定のクラス・ミックスイン・インターフェイスを継承しているかどうかを判定するにはisキーワードを使います。例えばinstance is Animalみたいな書き方をします。結果は真偽値になるためif文などの条件分岐で使えます。

また、isを使った条件分岐をした場合には型ガードが有効になる場合があり、条件を満たしたスコープ内ではそのクラスなどの属性やメソッドなどにアクセスすることができます。任意の型となるdynamic型を使う場合などに型を絞り込んで安全に処理を行うことができます(参照した属性やメソッドが存在せずにエラーになるといったことを避けられます)。

isで対象がCatクラスのインスタンスかどうかを判定し、安全にwalkメソッドの呼び出しなどを行っている例
void main() {
  final animal = createAnimalInstance();
  if (animal is Cat) {
    animal.walk();
    print(animal.xPosition);
  }
}

dynamic createAnimalInstance() {
  return Cat();
}

class WalkInterface {
  void walk() {}
}

class Cat implements WalkInterface {
  int xPosition = 0;

  @override
  void walk() {
    xPosition += 1;
  }
}

dynamic型でisで判定していない、且つ定義されていないメソッドや属性などを参照している場合にはランタイムエラーになります。

runメソッドはanimalインスタンスで定義されていないのでエラーになる例
void main() {
  final animal = createAnimalInstance();
  animal.run();
  print(animal.xPosition);
}

dynamic createAnimalInstance() {
  return Cat();
}

class WalkInterface {
  void walk() {}
}

class Cat implements WalkInterface {
  int xPosition = 0;

  @override
  void walk() {
    xPosition += 1;
  }
}

image.png

もしisを使ったif文での条件分岐の型ガードがされていればそのクラスなどで定義されていないメソッドなどを参照していればコンパイルエラーとなるためデプロイ前などに気づけて安全です。

isを使った条件分岐と型ガードを使っているので定義されていないrunメソッドで事前にコンパイルエラーになる例
void main() {
  final animal = createAnimalInstance();
  if (animal is Cat) {
    animal.run();
    print(animal.xPosition);
  }
}

dynamic createAnimalInstance() {
  return Cat();
}

class WalkInterface {
  void walk() {}
}

class Cat implements WalkInterface {
  int xPosition = 0;

  @override
  void walk() {
    xPosition += 1;
  }
}

image.png

このisの判定は通常のクラスの継承だけでなくインターフェイスやミックスインなどでも使用できます。

isの判定でインターフェイスを利用している例
void main() {
  final animal = createAnimalInstance();
  if (animal is WalkInterface) {
    animal.walk();
    print('walkメソッドが実行されました。');
  }
}

dynamic createAnimalInstance() {
  return Cat();
}

class WalkInterface {
  void walk() {}
}

class Cat implements WalkInterface {
  int xPosition = 0;

  @override
  void walk() {
    xPosition += 1;
  }
}

image.png

enumについて

Dartでenumを定義するにはenum enum名 { コンマ区切りでのenumの各値 }といった形で定義します。

enum EquipmentType {
  weapon,
  armor,
  accesory,
}

Pythonなどの言語のようにenumの値に任意の値を設定することはできません(weapon = 10,といったような右辺の設定はできません)。そういった個別の値も必要になる場合はクラスでstatic const 定数名 = ...といったようにstaticキーワードを使って定数を定義して扱います。

定義したenumは引数などの型でも使用することができ、特定のenumの定義のみ受け付ける・・・といった制御が可能です(どんな値を引数に指定すれば良いのかが分かりやすくなります)。

enum EquipmentType {
  weapon,
  armor,
  accesory,
}

void main() {
  final jpLabel = getEquipmentTypeJpLabel(
    equipmentType: EquipmentType.armor
  );
  print(jpLabel);
}

String getEquipmentTypeJpLabel({
  required EquipmentType equipmentType
}) {
  if (equipmentType == EquipmentType.weapon) {
    return '武器';
  }
  if (equipmentType == EquipmentType.armor) {
    return '防具';
  }
  if (equipmentType == EquipmentType.accesory) {
    return '装飾';
  }

  throw new Error();
}

image.png

enumをswitchでの条件分岐で使った場合の挙動

switch文に関しては後々の節で詳しく触れますが、Dartでenumに対してswitch文で分岐を書く場合、defaultケースを書いていない場合であれば全enumに対して分岐を書かない場合コンパイルエラーになってくれます。

これを利用することでenumの各値に対する分岐を確実に記述するように制約を設けることができます。途中でenumの値を追加したりした時も分岐条件の追加が漏れていればコンパイルエラーで事前に気づくことができます。

switch文で全てのenumの値の分岐をカバーしている例
enum EquipmentType {
  weapon,
  armor,
  accesory,
}

void main() {
  final jpLabel = getEquipmentTypeJpLabel(
    equipmentType: EquipmentType.armor
  );
  print(jpLabel);
}

String getEquipmentTypeJpLabel({
  required EquipmentType equipmentType
}) {
  switch (equipmentType) {
    case EquipmentType.weapon:
      return '武器';
    case EquipmentType.armor:
      return '防具';
    case EquipmentType.accesory:
      return '装飾';
  }
}

switch文で一通りのenumの分岐条件が書かれていない場合にはdefaultケースが無ければコンパイルエラーになります。

分岐条件が足りないのでコンパイルエラーになる例
enum EquipmentType {
  weapon,
  armor,
  accesory,
}

void main() {
  final jpLabel = getEquipmentTypeJpLabel(
    equipmentType: EquipmentType.armor
  );
  print(jpLabel);
}

String getEquipmentTypeJpLabel({
  required EquipmentType equipmentType
}) {
  switch (equipmentType) {
    case EquipmentType.weapon:
      return '武器';
    case EquipmentType.armor:
      return '防具';
  }
}

image.png

比較演算子について

条件分岐について触れる前に、必要となる比較演算子について以降の節で触れていきます。

比較演算子の結果は真偽値(trueもしくはfalse)となります。条件分岐でも真偽値が必要になるため、比較演算子は条件分岐と共によく使われます。

等値の比較演算子の結果で真偽値が返却されている例
void main() {
  int age = getAge();
  bool result = age == 17;
  print(result);
}

int getAge() {
  return 17;
}

image.png

値が一致しているかどうかの演算子

値が一致しているかどうかの比較演算子には==の記号を使い、その左右に比較したい2つの値を記述します(例 : age == 17)。一致していれば値がtrue、一致していなければfalseとなります。

値が一致していないかどうかの演算子

値が一致していないかどうかの比較演算子は!=の記号を使い、その左右に比較したい2つの値を記述します(例 : age != 17)。一致していなければtrue、一致していればfalseとなります。

void main() {
  int age = getAge();
  bool result = age != 17;
  print(result);
}

int getAge() {
  return 17;
}

image.png

未満の演算子

値が未満かどうかの比較演算子には<の記号を使い、その左右に比較したい2つの値を記述します(例 : age < 17)。

以下の演算子

値が以下かどうかの比較演算子には<=の記号を使い、その左右に比較したい2つの値を記述します(例 : age <= 17)。

超過の演算子

値が大きいかどうか(超過)の比較演算子には>の記号を使い、その左右に比較したい2つの値を記述します(例 : age > 17)。

以上の演算子

値が以上かどうかの比較演算子には>=の記号を使い、その左右に比較したい2つの値を記述します(例 : age >= 17)。

AND条件

複数の条件を満たすかどうか(AND条件)を比較演算子での比較で扱う場合には、それぞれの比較の記述の間を&&で繋げます。例えば年齢と性別の2つの変数それぞれに対して比較演算子を使ってAND条件で比較したい場合にはage <= 20 && gender == Gender.maleといった形で&&で繋げます。

年齢が20歳以下、且つ性別が男性かどうかを判定する例
enum Gender {
  male,
  female,
}

void main() {
  final age = 17;
  final gender = Gender.male;

  if (age <= 20 && gender == Gender.male) {
    print('年齢は20歳以下で且つ性別は男性です。');
  }
}

image.png

なお、3つ以上の条件が必要な場合には追加でさらに&&で条件を繋げていくことができます。

OR条件

複数の条件でいずれかの1つを満たすかどうか(OR条件条件)を比較演算子での比較で扱う場合には、それぞれの比較の記述の間を||で繋げます。例えば年齢が15歳もしくは17歳のどちらかを満たすかどうかという条件であればage == 15 || age == 17といったように書きます。

年齢が15歳もしくは17歳かどうかを判定する例
void main() {
  final age = 17;

  if (age == 15 || age == 17) {
    print('年齢は15歳もしくは17歳です。');
  }
}

image.png

条件分岐について

前節まででも少し使ってきましたが、以降の節ではDartでの条件分岐について触れていきます。

if文

まずは一番基本的なif文からです。

if文では条件を満たした場合に特定の処理を実行する場合に利用します。if (真偽値) { 条件を満たした場合に実行する処理 }といった具合に書きます。

真偽値の部分はそのまま真偽値を指定する場合や、比較演算子を使った条件式を記述することもあります。

age変数の値が17の場合のみprint関数を実行する例
void main() {
  final age = 17;
  if (age == 17) {
    print('年齢は17歳です。');
  }
}

image.png

else if文

if文で条件を書いた後に、その条件には当てはまらない場合に別の条件を満たすかどうかを判定したい場合にはelse if文を使います。if (真偽値) { ... }の括弧の直後に記述していく必要があります。

else if (真偽値) { 条件を満たした場合に実行する処理 }といった形で書きます。

age変数の値が17ではない場合に追加で15かどうかの判定を行う例
void main() {
  final age = 15;
  if (age == 17) {
    print('年齢は17歳です。');
  } else if (age == 15) {
    print('年齢は15歳です。');
  }
}

image.png

なお、else if文は複数設定することができます。

age変数の値が13かどうかという条件を書き足した例
void main() {
  final age = 13;
  if (age == 17) {
    print('年齢は17歳です。');
  } else if (age == 15) {
    print('年齢は15歳です。');
  } else if (age == 13) {
    print('年齢は13歳です。');
  }
}

image.png

else文

if文で条件を書いた後もしくはelse if文の条件を書いた後に、それらの全ての条件にも該当しない場合の条件を一括して扱う場合にはelse文を使います。

書き方はelse { 条件を満たした場合に実行する処理 }といった形になります。直前の各条件を一通り満たさない場合を全て対象とするため、判定用の条件の真偽値は必要ありません。

else文で各条件を満たさなかった場合を扱っている例
void main() {
  final age = 13;
  if (age == 17) {
    print('年齢は17歳です。');
  } else if (age == 15) {
    print('年齢は15歳です。');
  } else {
    print('年齢は17歳でも15歳でもありません。');
  }
}

image.png

switch文

比較の対象値が特定の値かどうかの判定条件(==による等値の比較演算子を使うような条件)がたくさんあるときにはelse if文をひたすら記述していくよりもswitch文を使うと記述がシンプルになります。

書き方としてはswitch (対象値の変数など) { 各条件のケース }といった形で書きます。各条件のケースではcase 比較で使う値:といった形で書いてセミコロンの後の行で比較条件を満たした時の処理を書いていきます。対象値と比較で使う値が一致している場合に条件を満たしたと判定されます。

void main() {
  final age = 13;
  switch (age) {
    case 11:
      print('年齢は12歳です。');
    case 13:
      print('年齢は13歳です。');
    case 15:
      print('年齢は15歳です。');
  }
}

image.png

switch文のデフォルトケース設定

switch文では各ケース全てに該当しない場合に実行するためのデフォルトケースを設定することができます。if文のelseと同じような挙動になります。

書き方としてはcaseによる各ケース設定の最後にdefault:という記述をすることでデフォルトケースを設定することができます。

void main() {
  final age = 13;

  switch (age) {
    case 15:
      print('年齢は15歳です。');
    case 17:
      print('年齢は17歳です。');
    default:
      print('年齢は15歳でも17歳でもありません。');
  }
}

image.png

switch文で複数の値をケースの条件に設定する

switch文の特定のケースで複数の値の条件を満たしている場合の処理を設定したい場合には||記号によるOR条件を使うことで対応することができます。

void main() {
  final age = 17;

  switch (age) {
    case 15 || 17:
      print('年齢は15歳もしくは17歳です。');
  }
}

image.png

switch文でのbreakについて

他の言語ではswitch文の各ケースの中で複数ケースの条件を満たす場合に複数のケースの処理が実行される場合がありますが、Dart(正確にはDart3系以降)では特定のケースの条件を満たした場合にはそれ以降のケースはチェックされず、必ず最大で1つのケースの処理のみ実行されます(braekの記述は必要ありません)。

他の言語だと場合によってはbreakの記述(switch文の判定終了用の記述)が無いと想定外な感じで複数のケースの処理が実行されてしまったり、もしくはそもそもコンパイルが通らないといったケースがあると思います。うっかりミスを防ぎつつ、且つ記述もシンプルになるので個人的にはDart3系のこの挙動は好みではあります。

例えば以下の例では1番目のケースと2番目のケースを条件自体は満たす感じになっていますが、最初のケースの処理のみ実行されて「年齢は17歳です。」というメッセージのみ表示されていることを確認できます。

void main() {
  final age = 17;

  switch (age) {
    case 17:
      print('年齢は17歳です。');
    case 15 || 17:
      print('年齢は15歳もしくは17歳です。');
  }
}

image.png

switch文でのenumの網羅性のチェック

enumの節でも触れましたが、switch文でenumを使った場合には網羅性がチェックされます。

例えば以下のように特定の関数などでenumをswitch文の判定対象にしている場合、enumが全てカバーされていればswitch文の後にreturnキーワードで返却値が設定されていなくともコンパイルエラーにはなりません(必ずswitch文のどこかのケースを通るといった判定にすることができます)。

enum EquipmentType {
  weapon,
  armor,
  accesory,
}

void main() {
  final jpLabel = getEquipmentTypeJpLabel(
    equipmentType: EquipmentType.armor
  );
  print(jpLabel);
}

String getEquipmentTypeJpLabel({
  required EquipmentType equipmentType
}) {
  switch (equipmentType) {
    case EquipmentType.weapon:
      return '武器';
    case EquipmentType.armor:
      return '防具';
    case EquipmentType.accesory:
      return '装飾';
  }
}

image.png

ループについて

以降の節ではリスト(もしくは他のIterable)や辞書に対するループ処理(繰り返し処理)について触れていきます。

リストなどに対してインデックスを設定するループを行う

リスト(もしくは他のIterable)のインデックスを参照する形でループを回すには色々と書き方はありますが書き方の一つとしてfor (var i = 0; i < リストなど.length; i++) { ループでの処理 }といった形で書きます(iは別の名前でも動きます)。

var i = 0;という部分はインデックスを扱うための変数i0で初期化するといった処理になります。0で初期化しているのでループ中のインデックスを参照してみると0からスタートしています。

i < リストなど.length;という部分はループを継続する条件です。lengthプロパティでリストなどの要素の件数を参照しているのと<記号を使って未満の比較演算子での比較をしているため、インデックス(i)がリストの要素の件数未満であればループを行うという条件になります。

i++の部分はループが1回分終わったタイミングで行う処理です。++でインクリメント(値を1増やす)挙動となるためループが1回終わるたびにインデックスが1加算されるという挙動になります。

以下の例ではリストに3つの要素が格納されているため3回分ループが実行され、print関数でのインデックスの出力が0, 1, 2の3つになっていることを確認できます。

void main() {
  final names = ['John', 'Mike', 'Olivia'];

  for (var i = 0; i < names.length; i++) {
    print(i);
  }
}

image.png

インデックスを使用してループ中にリストの各要素を参照したい場合には対象のリスト[i]といった形でリストのインデックスを設定すれば対象の要素にアクセスすることができます。

void main() {
  final names = ['John', 'Mike', 'Olivia'];

  for (var i = 0; i < names.length; i++) {
    print(names[i]);
  }
}

リストなどに対して要素を設定するループを行う

インデックスを参照する必要がなく、直接ループでリストなどの各要素を参照したい場合にはfor (final 要素の変数名 in リストなど) { ループ中の処理 }といった形でシンプルに書くことができます(例 : for (final name in names) { ... })。

void main() {
  final names = ['John', 'Mike', 'Olivia'];

  for (final name in names) {
    print(name);
  }
}

image.png

Map(辞書)のキーを設定するループを行う

Map(辞書)の各キーをループで扱いたい場合にはkeys属性を使う形でfor (final キーの変数名 in 対象のMapの変数.keys) { ループ中の処理 }といった形で書けます(例 : for (final name in agesMap.keys) { ... })。

void main() {
  final agesMap = {'John': 17, 'Mike': 15, 'Olivia': 13};

  for (final name in agesMap.keys) {
    print(name);
  }
}

image.png

Map(辞書)の値を設定するループを行う

Map(辞書)の値をループで扱いたい場合にはvalues属性を使う形でfor (final 値の変数名 in 対象のMapの変数.values) { ループ中の処理 }といった形で書きます(例 : for (final age in agesMap.values) { ... })。

void main() {
  final agesMap = {'John': 17, 'Mike': 15, 'Olivia': 13};

  for (final age in agesMap.values) {
    print(age);
  }
}

Map(辞書)のキーと値を同時に設定するループを行う

Map(辞書)のキーと値を両方ループの中で扱うにはいくつか方法があります。この節では主な方法についていくつか触れていきます。

1つ目はfor文ではなくMapforEachメソッドを使う方法です。メソッドの第一引数に渡される関数の引数にはキーと値の2つの引数が渡されるのとキーの数だけその関数が呼び出されるためfor文でループを回した時と同じような形でキーと値を参照して処理を行うことができます。

void main() {
  final agesMap = {'John': 17, 'Mike': 15, 'Olivia': 13};

  agesMap.forEach((key, value) {
    print('key: $key, value: $value');
  });
}

image.png

2つ目はMapentries属性をfor文で使う方法です。この場合ループ中の変数はMapEntry型となり、そちらのkeyvalue属性でキーと値を参照することができます。

void main() {
  final agesMap = {'John': 17, 'Mike': 15, 'Olivia': 13};

  for (final mapEntry in agesMap.entries) {
    print('key: ${mapEntry.key}, value: ${mapEntry.value}');
  }
}

image.png

その他、キーに対してループを行ってループの中でそのキーを使用して対象の値を参照するという形もシンプルで良いと思います。

コメントについて

Dartではいくつかコメントの書き方があるので以降の節ではそれぞれを触れていきます。

通常のインラインコメント

通常のインラインコメントは//の記号を使います。対象行でこの記号の後の記述はプログラムとして実行されません。主に何らかの処理の説明の補足などに使います。また、基本的にスラッシュの後に半角スペースを1つ入れて使います。

void main() {
  // いろは歌
  print('いろはにほへとちりぬるを');
}

ドキュメンテーションコメント用のインラインコメント

///といった形でスラッシュを3つ重ねるとドキュメンテーションコメントとなります(jsDocやPythonのdocstringのような扱いとなります)。

ドキュメンテーションコメントは主にVS Codeなどのエディタで使ったりドキュメントを出力したりする際に参照されます。

また、ドキュメンテーションコメントはファイル全体(ライブラリファイル対象)、変数、定数、クラス、関数、メソッドなど様々な箇所に設定することができます。

例えば関数に設定すると以下のような雰囲気になります。

/// メッセージを出力する
///
/// [message]には出力するメッセージを指定する。
void printMessage({required String message}) {
  print(message);
}

VS Code上で対象の関数にマウスオーバーしてみると以下のようにドキュメンテーションコメントの内容が表示されることが確認できます。

image.png

ブロックコメント

Dartで/* */で囲むとブロックコメント(複数行のコメントアウト)となります。一括で処理を無効化できます。主にDartでは一時的に特定部分の処理を止めたりに使います。一方でドキュメントや補足として残すためのコメントは前節までの各インラインコメントを使用します。

void main() {
  /*print('いろはにほへと');
  print('ちりぬるを');*/
}

image.png

例外について

Dartで例外(ランタイムエラー)を投げたい場合にはthrow エラークラス名(引数内容);といった形でthrowキーワードを使います(例 : throw Exception('エラーメッセージ'))。

※DartPadではランタイムエラーの詳細なエラーメッセージが表示されなかったりしますが、通常のビルド時などにはエラークラスに指定したメッセージなどが確認できるようになっています(VS Code上からビルドした場合など)。

void main() {
  throw Exception('エラーが発生しました。');
}

image.png

※実際にthrowによるエラー設定を行う場合はより具体的なビルトインのエラークラスを使うか、もしくは継承などして独自に設けたクラスを使うなどが推奨されます。

参考サイト・参考文献まとめ

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1