お仕事で数か月程度DartとFlutterでのスマホアプリ開発をしていたので触ってみて感じた所感や学んだことなど、追加で調べたことなどを雑多に全体的にまとめておきます。
(個人の復習や知識の漏れの補完・その他チームメンバーへの共有も兼ねて)
※DartやFlutterの基礎であったり、ビルドインやサードパーティのライブラリ含め利用しているものも全体的に触れていきます。
※色々まとめていたら長くなったので前編後編で分けます。本記事では所感やDart周りなどを中心に触れ、後編はFlutterの細かい点も触れていきます(考えなしに書いていたらとても長くなったのでほぼ自分の勉強用の記事となってきています・・・w)。
前置き
専任のアプリクライアントエンジニアやフロントエンジニアなどをやってきているわけではないので専門の方々からすると色々知識が荒い点はご了承ください(仕事でサーバーやクラウドなどを触っている時間なども多めです)。
もし専門の方から見て間違いなど気づかれましたらこっそりコメントなどで優し目にマサカリ投げていただけますと幸いです。
そもそもFlutterって?
- Googleによって開発されたOSSのクロスプラットフォーム向けのエンジン(フレームワーク)です。
- Windows、iOS、Android、web・・・と様々なプラットフォーム向けのアプリなどを開発することができます。
- 言語は同じくGoogleによって開発されたDartという静的型付け言語を使います。
- 本記事執筆時点でGitHubのスター数が16万を超えており、世界のGitHubのパブリックリポジトリの中で19位くらいに位置しており、大分人気を博している印象です。
- ※参考 : Repositories Ranking
- ネイティブほどでは無いですが必要十分には感じるくらいの良いパフォーマンスを出してくれます。
- 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と検索して青バッジの付いている公式のものをインストールします。
続いてFlutter SDKを以下のページの「flutter_windows_x.xx.xx-stable.zip」ボタンを押してダウンロードします。zipファイルは1GBくらいあるのでそれなりにダウンロードに時間がかかります。
zipの展開先は任意ですがそれなりにディスク容量が必要になるので余裕のあるディスクドライブとかを選択しておきます。今回は雑にD:\workspace\
というフォルダに展開しました。flutterというフォルダ名で展開されるので私の環境の場合D:\workspace\flutter\
というパスにFlutter SDKが展開されています。
※特殊文字やスペースが含まれるパスは避けるように、とドキュメントに書かれているのでその辺りは避けておきます。
今度はVS Code上でCtrl + Shift + Pを押してコマンドパレットを開き、flutterと検索します。
検索結果に表示される「flutter: New Project」を選択します。恐らくVS CodeからSDKが見つからないと以下のようなエラーが右下に表示されると思います。SDKのダウンロード自体は完了しているのでLocate SDKボタンをクリックします。
Flutter SDKが含まれているフォルダを選択するためのダイアログが開かれるためzipを展開したフォルダ(私の場合はD:\workspace\flutter\
)を選択して「Set Flutter SDK folder」をクリックします。
Flutterのパス設定を行う
Flutterコマンドを使えるようにするためFlutterのバイナリが配置されているパスを環境変数のPATHへと追加していきます。
対象のパスは展開したFlutter SDK直下のbinフォルダのパスが該当します。私の場合はD:\workspace\flutter\bin\
となります。これをPATHへ追加します。
Windowsの検索窓で「環境変数」などと検索すると「システムの環境変数の編集」という検索結果が表示されるのでクリックします。
システムのプロパティというダイアログが表示されるので右下の方にある「環境変数(N)」というボタンを選択します。
「<ユーザー名>のユーザー環境変数」という方で変数がPATHとなっている行を選択して編集ボタンを押します。
「新規」ボタンをクリックするとパスの入力ができるのでFlutter SDK内のbinフォルダのパス(私の場合D:\workspace\flutter\bin\
)を入力してOKをクリックします。
これで新しく開いたPowerShellなどでFlutterのコマンドが実行できるようになります。試しにPowerShellやコマンドプロンプトなどを新しく開いて$ flutter --help
とコマンドを打つと色々メッセージが表示されます。
環境設定が正しくできているかを確認してくれるflutter doctorコマンドを活用していく
Flutterには環境設定が正しくできているかを確認してくれる$ flutter doctor
というコマンドが存在します。
実行してみると各設定が出来ているか出来ていないかを表示してくれます。対応が足りていないものなどはどのような対応が必要なのかの説明なども表示してくれます。
以下のコマンド結果のスクショではまだAndroid関係の整備をしていないのでAndroid Studio周りなどで引っかかっています。
※同僚の方にご共有いただいたのですが、もしflutter doctorのコマンドを最初に実行してみても結果が返ってこない・・・といった場合には再起動したらちゃんと帰ってくるようになったそうです。
Android Studioなどの対応
flutter doctorコマンドでAndroid Studioなどの箇所が引っかかったのでその辺を対応していきます。
まずはAndroid Studioをインストールしていきます。以下のページからインストーラーのダウンロードボタンをクリックします。ダウンロードファイルは約1GBくらいあります。
ダウンロードが終わったらインストーラーを起動してインストールしていきます。特に理由が無ければ設定そのままでインストールを進めて問題ないと思います。
インストールされたAndroid Studioを起動します。最初は設定をインポートするかが問われますが、Android Studioのインストールが今回使う環境では初なので「Do not import settings」を選択してインポートせずに進めます。
その後はログ送信を行うかどうかなどのダイアログが出ますがその辺は各々の判断で良いと思います(今回は送信しない設定で進めました)。Nextボタンを押したりしているとインストールタイプの選択画面となりますがStandardを今回は選択します。
あとは利用規約などに同意していればインストールインストールが進むのでしばらく待ちます。
インストールが終わると以下のような画面になります。
左のメニューの「Plugins」を選択し、検索窓でFlutterと入力してFlutterのプラグインを検索しInstallボタンでインストールを行います。
インストール時に警告が出ますが特に問題ないのでそのまま進めます。
Dart関係もインストールが必要と言われるのでそちらもInstallボタンをクリックしてインストールします。
インストール後「Restart IDE」というボタンが表示されるので一応押してAndroid Studioを再起動しておきます(この辺はVS Codeを基本的に使っていくので要らないかもしれません)。
Android SDK Command-line Toolsのインストール
続いてAndroid SDK Command-line Tools周りの対応をしていきます。Android StudioのProjectsのメニューの画面の下の方にある「More Actions」メニューをクリックして「SDK Manager」をクリックします。
設定画面が開かれます。左のメニューでAndroid SDKの部分が選択されている状態でSDK Toolsのタブを選択します。Android SDK Command-line Toolesという項目でチェックが外れているのでチェックを入れて右下のApplyをクリックします。
この時点で再度$ flutter doctor
コマンドを実行してみましょう。
赤文字のエラーは無くなりました。ただ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を押していけば同意作業が進んでいきます。
最後まで同意が終わったら再度flutter doctorコマンドを実行してみます。
全てのチェックを通過し、「• No issues found!」と表示されたことを確認できました。これで必要な基本的なインストール周りなどは対応が完了となります。
その他環境によってはインストールが必要になるかもしれないものなど
私の場合昔に入れたと思われるVisual Studio関係が最初からチェックを通過していましたが人によってはこの辺もインストールが必要になってくると思われます。その場合はflutter doctorコマンド上に表示されるメッセージもしくはネット上の記事などを参照してご対応ください。
プロジェクトの作成
新規のFlutterプロジェクトを作成するには$ flutter create
コマンドやVS Code上での操作などいくつかの方法があります。
普段はコマンドで作成していたのですが本記事ではお試しでVS Codeを使う形でプロジェクトを作成してみます。
VS Code上でCtrl + Shift + Pを押してコマンドパレットを開き、flutterと入力してflutter関係のコマンドを検索します。Flutter: New Project
というメニューが表示されるのでそちらを選択します。
いくつか候補が出てきます。どのみち生成される初期コードとかを編集していけば問題ないのでApplication、Empty Application、Skeleton Applicationどれでも問題なさそうですが今回は一番上のApplicationを選択します。
プロジェクトを生成するフォルダを選択するためのダイアログが表示されるので任意のフォルダを選択します。今回はD:\workspace
フォルダとしました。
コマンドパレットの位置にプロジェクト名の入力用のフォームが表示されます。今回は雑にtest_flutter_project
と設定します。
名前を入力してEnterを押すとVS Codeが再起動しつつFlutterの作成したプロジェクトがワークスペースに設定された状態で起動します。
※必要に応じて切り替わる前のVS Codeのワークスペース設定の保存などはご対応ください。
生成されたmain.dartファイルが開かれた状態となりますが、このファイルのmain関数部分がこのプロジェクトのエントリーポイントとなります。
デバッグビルドでアプリの起動を行う
VS Code上からのFlutterのデバッグビルドなどによるアプリ起動はサイドメニューのRun and Debugメニューから行います。
対象のメニューを選択すると以下のスクショのようなUIが表示されます。青いRun and Debugボタンをクリックするとデスクトップアプリのデバッグビルドとしてアプリのビルドがスタートします。
※Flutterのアプリ起動自体はマシンスペックにもよりますが結構時間がかかります。ただし一度起動してしまえば後は再度最初からビルドしなくともホットリロードやホットリスタートでコード変更分を反映できるのでぐっとビルド回数は少なくて済むため起動した後はビルド時間は気にならなくなります。
ビルドが終わると以下のスクショのようにサンプルプロジェクトのアプリが起動します。内容としては右下のボタンを押すと真ん中のカウントが1ずつ加算されていくだけのシンプルな内容になっています。
また、起動時にプロファイラー関係について開くかどうかなどの確認がVS Codeの右下に表示されたと思います。ChromeのDevToolsのようにVS Code上でFlutter用に様々なツールが用意されています。この辺りについては後々の節で詳しく触れます。
外部ライブラリパッケージについて
プロジェクトをVS Codeやコマンドなどで作成した場合、プロジェクトフォルダの直下にpubspec.yaml
というファイルが作成されていると思います。このファイルはプロジェクトの設定を管理するためのファイルで、アプリ名やFlutterなどのバージョン、その他外部のライブラリパッケージの設定や画像などの埋め込むアセットの定義を行うことができます。
以降の節では外部ライブラリパッケージについて色々触れていきます。
pub.devについて
外部ライブラリパッケージに関してはDartではpub.devというサイトで情報が公開されており、様々なライブラリの情報を確認することができます。Pythonで言うところのPyPIのようなサイトになります。
サンプルとしてflutter_svgというライブラリで説明をしていきます。
まずページ上部について見てみます。上部にはパッケージ名と現在公開されている最新バージョンの文字列が表示されます(スクショの2.0.10+1の部分が最新バージョンの文字列になります。)。
また、「Published 8 days ago」といった部分は最後にパッケージのアップデートが公開された日が何日前かといった表示になります。この表示がかなり前のパッケージに関しては保守がされなくなりアップデートが止まっている可能性が高いので使用を検討する際には注意が必要になってきます。
「Dart 3 compatible」というチップの表示もありますが、これはDart3系に対応している場合に表示されます。利用の多いライブラリパッケージに関してはほぼほぼ付いている気もしますが付いていない場合には最近は(少なくとも新規のプロジェクトでは)Dart3系を利用されているプロジェクトが大半だと思いますのでそのライブラリパッケージは互換性的に利用できない可能性が高くなります。また、Dart3系になってから結構期間が経っているのでまだDart3系のサポートがされていない場合、そのライブラリパッケージはメンテナンスされているかの面で使用は警戒した方が無難かもしれません。
以下のスクショのように、Versionsタブを表示すると各バージョンとリリース日が表示されます。
各バージョンのリリース日を確認することで、どのくらいの頻度でバージョンアップがされているのかを確認できます。各バージョン間でかなり期間が空いているかどうかもそのライブラリの利用を決めるかどうかの指標の一つとして使えるかもしれません。
右の方にあるLIKESやPOPULARITYは対象のライブラリパッケージがどのくらい多くのユーザーに使われている人気のパッケージなのかの判断基準の一つとして使うことができます。
人気のパッケージであれば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
)。
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
という記述はライブラリパッケージの同期を行うためのコマンドとなります。
また、VS CodeのOUTPUTタブには同期処理のコマンドの出力が表示されます(このUIが表示されていない場合にはCtrl + @などで表示することができます)。
先頭に+
記号が付いているものが追加されたライブラリパッケージとなります。flutter_svg
も含まれており、且つバージョンが最新の2.0.10+1
がインストールされたことが確認できました。
追加した外部ライブラリパッケージの削除方法
追加したライブラリパッケージが不要になった時などに削除するには単純にpubspec.yamlから記述を削除してVS Code上でCtrl + Sで保存するだけです。
試しにflutter_svg
パッケージの記述を削除して保存してみます。
少し待つと「These packages are no longer being depended on:」というメッセージと共に不要になったライブラリパッケージが一覧表示されます。
また、対象のプロジェクトのリポジトリフォルダでTERMINALもしくはPowerShellなどから$ flutter pub deps
というコマンドを打つことでも現在インストールされているパッケージを一覧表示することができ、一覧の中にflutter_svg
が含まれなくなっていることが確認てきます。
VS Codeのプロファイラーなどについて
前節までで少し擦れましたが、VS CodeのプロファイラーはChromeのDevToolsのように色々用意されています(実務だとこの辺りまだしっかり使いこなせていません・・・)。
初回のビルド時に開くかどうかの確認ダイアログが右下に表示されたり、他にもコマンドパレットからも開くことができます。
デスクトップ上でデバッグビルドを起動した状態でコマンドパレットをCtrl + Shift + Pで開きます(起動していない場合は起動時にプロファイラー側で接続されるようになります)。
コマンドパレットでflutter devtools
などと検索すると対象のものがヒットするので選択します。
DevToolsの中でも色々ありますがまずはWidget Inspector Pageを選択します。
するとChromeのDevToolsの要素タブのようなUIがVS Code上で表示されます。Flutterではウィジェットをchild引数で結構入れ子にしたりして記述していきますが、その親子関係だったりウィジェットのサイズなどを確認することができます。
親子関係を辿っていって特定のウィジェットを選択してWidget Details Treeのタブを選択したりすると対象ウィジェットの各属性の詳細とかも確認することができます。
ChromeのDevToolsだと要素をクリックして選択する機能がありますが、FlutterのこのDevToolsでも似たような機能があります。
DevToolsの左上の方にあるSelect Widget Modeというボタンを押して有効化すると、それをウィジェットをクリックしてデバッグビルドのアプリ上でウィジェットが選択できるようになり、且つ選択しているウィジェットのサイズなどの情報が表示されるようになります。
※一度クリックした後に別のウィジェットをクリックしたい場合には左下の虫メガネのアイコンが乗っているボタンをクリックすると別のウィジェットを選択できるようになるようです。
ウィジェットの領域をガイドライン表示する
DevToolsの右上の方に|↔|といった表示のアイコンがありますが、これを選択すると各ウィジェットごとにサイズ領域のガイドラインを表示してくれます。
この機能で境界線とかの無い透明なコンテナとかでどのくらいの領域が設定されているのか等の可視化を行うことができます。
その他のDevToolsについて
まだ使いこなせていないというのもあり本記事ではDevToolsに関しては深くは触れないため軽く紹介する程度に留めておきますが、Widget InspectorのDevTools以外にも様々なDevToolsがVS Code上で用意されています。
例えばNetwork Pageでは外部のAPIへのリクエストや外部アセットの読み込みなどのURLや結果のステータスコード、レスポンスのサイズの確認などを行うことができます。
Dartの文法
以降の節ではDartの文法について色々復習しながら触れていきます。
Dartの実行環境について
Dartの文法を学んでいくにあたってVS Code上でFlutterのプロジェクトを使って色々動かしていく・・・というのもいいのですが、本記事ではDartの文法について触れていく際には気軽に試せるようにDartPadというブラウザ上でインストール対応など無しにDartを書いて動かすことの出来るサービスを使っていきます(※Flutterを動かす時にはVS Codeの方を使っていきます)。
DartPadの代わりにVS Code上で動かして・・・とかでも(若干の調整とかは調べていただく必要は出てくるかもしれませんが)大丈夫かと思いますので利用されるものはお任せします。
DartPadではコードを書いてRunボタンを押すと、少し待っているとコンソール出力の結果が右側に表示されます(スクショのhello 1といった各表示がコンソール出力になります)。
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);
}
※後々の節で詳しく触れますが、関数を呼び出すには<関数名>(引数の指定);
といったように書きます。また、関数だけではないのですが処理の最後にはセミコロンが必要になります。
前述のコードでは100という引数を指定しているのでprint(100);
という記述になっています。
また、DartではインデントはHTMLとかで良くあるように半角スペース2個が基本となります(公式のフォーマッタが半角スペース2個になっています)。
なお、VS Code上だったり公式のLintでチェックしたりするとprint関数が残っていると警告で引っかかります。
あくまでprint関数は一時的な確認用などのために使用して、不要になったら削除するのが基本的な運用方針となります。
永続的にコンソール出力を開発中に表示したい場合は別途ロギングライブラリを使います。その辺りを使うとリリースビルドの際にはロギングの処理が無効化されて無駄が無くなったり見やすくなったりといったメリットがあります。
ロギング関係のライブラリパッケージに関して後々の節で触れていきます。
変数宣言
Dartで変数を宣言する場合には型を明示する方法とvar
を使って型推論で定義する方法の2つがあります。
型を明示する場合には<型名> <変数名>;
といった形で書きます(例 : int myIntValue;
)。変数宣言と同時に初期値を与える場合にはイコールで繋いで値を指定します(例 : int myIntValue = 200;
)。
void main() {
int myIntValue = 200;
print(myIntValue);
}
var
を使って型推論を使う場合にはvar <変数名> = <初期値>;
といった形で設定します。型推論で判定する性質上基本的には初期値を与える形で定義します(例 : var myIntValue = 300;
)。
void main() {
var myIntValue = 300;
print(myIntValue);
}
型推論を使う形で初期値を与えない場合は警告が出ます。
初期値を与えない形で変数宣言したい場合には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);
}
型推論を使って定義したい場合にはconst
やfinal
などのキーワードは残しつつ型の指定を省略すれば型推論を使いつつ定数を定義することができます。
void main() {
const userName = 'myName';
print(userName);
}
定数なので当たり前ですが再代入とかは効きません。コンパイルエラーとなります。
void main() {
const userName = 'myName';
userName = 'yourName';
print(userName);
}
const
とfinal
で何が違うのか?という点ですが、const
の方はコンパイル時に既に既知の値である必要があります。つまりconst
の初期値は10
や'Hello'
といった直接的な値もしくは他のconst
の定数になっている必要があります。他の変数を設定したり、もしくは関数の返却値などをconst
の値に設定することはできません。
例えば以下のように別の変数がconst
の値として設定されているとコンパイルエラーになります。
void main() {
var userName = 'myName';
const otherName = userName;
print(otherName);
}
関数の返却値が分岐などなく固定値を返すといった場合でも関数を経由する場合はその値はconst
として定義することはできません。
void main() {
const userName = getUserName();
print(userName);
}
String getUserName() {
return 'myName';
}
一方でconst
で設定する値が別のconst
の値であればコンパイルが通ります。
void main() {
const userName = 'myName';
const otherName = userName;
print(otherName);
}
final
の方はconst
とは異なり、設定される値は1回まで計算されてその後は変更されない定数値として扱われます。つまり定数の初期値には変数を設定したり関数の返却値を設定してもコンパイルが通ります。
void main() {
final userName = getUserName();
print(userName);
}
String getUserName() {
return 'myName';
}
定義した後はconst
と同じように再代入しようとずるとコンパイルエラーとなります。
void main() {
final userName = getUserName();
userName = 'otherName';
print(userName);
}
String getUserName() {
return 'myName';
}
慣れてくるまではconst
とfinal
どちらを使うのか一瞬迷ってしまう・・・といったケースも出てきそうですが、その辺りはVS Code上でFlutterの拡張機能を入れていればconst
として定義すべきケースなのにfinal
で定義している場合には警告が表示されます。その辺りをエディタで設定しておいたり、後はCI/CDとかで自動チェックするようにしておけば引っかかったら直すくらいの感覚で大丈夫だと思います。
void main() {
final userName = 'myName';
print(userName);
}
注意点として、(少なくとも本記事執筆時点では)リスト(配列)などを扱う場合、final
で定義していても要素の追加などの更新処理が通ってしまいます(割と初見の時この挙動は違和感が強かったですがJavaとかに慣れている方は恐らく違和感が少ない・・・仕様ですかね・・・?)。
void main() {
final List<int> myList = [100, 200];
myList.add(300);
print(myList);
}
const
を使う場合にはこういったケースはコンパイルが通らなくなります(const
の方が厳密な定数といった感じになります)。そのためチェック時の警告を抜きにしても基本的にはconst
を使えるケースではconst
を使う方が好ましいといった形になります。
lateを使った定数の遅延設定
final
を使った定数定義では別途late
を使って値の設定を定数定義よりも遅らせることができます。
例えば以下のように定数定義と値の設定の行をずらしたりすることができます。
void main() {
late final int myIntFinalValue;
myIntFinalValue = 20;
print(myIntFinalValue);
}
何に使うのか?という感じですが、Flutterで扱う際にクラスの属性などをlate final
で定義しておいて、初期化用のメソッドなど(例 : initState
メソッドなど)で初期値を与えたりなど、定義と値の設定タイミングをずらしたい一方でなるべく定数として扱いたい・・・といった場合に便利です(初期化を確実に1回だけにしたいといった場合などに)。
なお、late final
で定数を定義した場合、定数定義と値の設定タイミングがずれる都合定数定義時点で型を明示しないと警告が出るようになります(late
を付けない場合には警告が出ません)。
void main() {
late final myIntFinalValue;
myIntFinalValue = 20;
print(myIntFinalValue);
}
また、通常のfinal
定義時と同じように複数回値を設定しようとずるとエラーになります。
void main() {
late final int myIntFinalValue;
myIntFinalValue = 20;
myIntFinalValue = 30;
print(myIntFinalValue);
}
型について
以降の節では型の基本について触れていきます。
型の基本的な指定方法
変数や定数、引数などへの型の指定はそれぞれの名前の左にスペースを空けて設定します(例 : int myIntValue;
)。
変数であれば型の指定だけ行っておいて値の設定は後で行う・・・といった書き方も行うことができます(if文で分岐するケースなど)。
void main() {
int myIntValue;
bool isTarget = true;
if (isTarget) {
myIntValue = 100;
} else {
myIntValue = 200;
}
print(myIntValue);
}
ジェネリックの型の指定方法
ジェネリックな型、例えば文字列の値のみを格納するリスト(配列)といった型を定義するには型名<ジェネリックの型>
といった形で書きます(例 : List<String>
)。
ジェネリックの型の指定を使うことで「整数のみを格納するリストだったのにうっかり文字列を入れてしまった」とか「辞書のキーは文字列だけの想定だったのにうっかり整数のキーが紛れ込んでしまった」みたいなことを弾くことができ堅牢にコーディングを行うことができます。
また、特定の型だけでなく様々な型に対応できるといった使い方もできるので実装の重複を減らしたり利用時の柔軟性を上げたりといったメリットもあります。
リスト(配列)で値の型を指定するにはList<値の型>
と書きます。
void main() {
List<int> myIds = [10, 20, 30];
print(myIds);
}
試しにList<int>
と指定したリストの変数に文字列を含めてみるとコンパイルエラーになることを確認できます。
void main() {
List<int> myIds = [10, 'Hello', 30];
print(myIds);
}
マップ(辞書)でキーと値の型を指定するにはMap<キーの型, 値の型>
と書きます。
void main() {
Map<String, bool> myMap = {
'key1': true,
'key2': false,
};
print(myMap);
}
試しにキーに不正な型を指定してみてエラーになることを確認してみます。
void main() {
Map<String, bool> myMap = {
10: true,
'key2': false,
};
print(myMap);
}
同様に値に対して不正な型を指定してみてエラーになることを確認してみます。
void main() {
Map<String, bool> myMap = {
'key1': true,
'key2': 20,
};
print(myMap);
}
整数型
整数の型はint
となります。500
や-1000
といった値を設定することができます。
void main() {
int myIntValue1 = 500;
print(myIntValue1);
int myIntValue2 = -1000;
print(myIntValue2);
}
加算処理は+
の記号や+=
の記号で行うことができます。
void main() {
int myIntValue = 500;
myIntValue = myIntValue + 300;
print(myIntValue);
myIntValue += 200;
print(myIntValue);
}
1だけ加算したい場合には++
の記号を使うことで対応することができます。
void main() {
int myIntValue = 500;
myIntValue++;
print(myIntValue);
}
減算処理は-
の記号や-=
の記号で行うことができます。
void main() {
int myIntValue = 1000;
myIntValue = myIntValue - 300;
print(myIntValue);
myIntValue -= 200;
print(myIntValue);
}
1だけ減算したい場合には--
の記号を使うことで対応することができます。
void main() {
int myIntValue = 500;
myIntValue--;
print(myIntValue);
}
除算処理は/
の記号や/=
の記号で行うことができます。ただし結果の値は浮動小数点数(double
型)となるため、結果は他の変数に格納するなどの調整が必要になります(double
型については後の節で触れます)。
void main() {
int myIntValue = 10;
double myDoubleValue = myIntValue / 4;
print(myDoubleValue);
myDoubleValue /= 5;
print(myDoubleValue);
}
切り捨て除算は~/
の記号や~/=
の記号で行うことができます。Pythonとかに慣れ親しんだ身からすると//
の方に慣れていますが~/
となります。
void main() {
int myIntValue1 = 10;
myIntValue1 = myIntValue1 ~/ 4;
print(myIntValue1);
int myIntValue2 = 10;
myIntValue2 ~/= 4;
print(myIntValue2);
}
剰余計算は%
の記号や%=
の記号で行うことができます。
void main() {
int myIntValue = 10;
int remainder = myIntValue % 4;
print(remainder);
myIntValue %= 3;
print(myIntValue);
}
なお、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);
}
大きな値を使いたい場合、そのままint
の値を設定する感覚で設定しようとするとint
の上限値を超えているということでエラーになります。
void main() {
BigInt myBigIntValue = BigInt.from(123456789012345678901234567890);
print(myBigIntValue);
}
大きな数値を使いたい場合には指数表記などを使います。
void main() {
BigInt myBigIntValue = BigInt.from(1e+30);
print(myBigIntValue);
}
もしくは文字列に対してparseメソッドを使う形でも対応ができます。
void main() {
BigInt myBigIntValue = BigInt.parse('123456789012345678901234567890');
print(myBigIntValue);
}
とはいえ、Flutterがアプリ開発用となっているのと各ライブラリパッケージなどでも通常のint
型を使うのが大半でしょうから基本的にはint
型で扱う形となります(扱いが煩雑だったり制限が多い点やパフォーマンス面などを加味しても使う機会はあまりないかなという印象です)。
int型のisEven属性
※他の型でもそうですが、全てではありませんが使うことがありそうな属性やメソッドなどに関しても各節で触れていこうと思います。
int
型のisEven
属性は整数値が偶数かどうかの真偽値の属性となります。
void main() {
int myIntValue = 10;
print(myIntValue.isEven);
}
int型のisOdd属性
int
型のisOdd
属性は整数値が奇数かどうかの真偽値の属性となります。
void main() {
int myIntValue = 10;
print(myIntValue.isOdd);
}
int型のisNegative属性
int
型のisNegative属性は整数が負の値かどうかの真偽値の属性となります。負の値で荒れはtrue
になります。
void main() {
int myIntValue = -10;
print(myIntValue.isNegative);
}
int型のabsメソッド
int
型のabs
メソッドでは整数の絶対値を取得します。引数は必要としません。
void main() {
int myIntValue = -10;
print(myIntValue.abs());
}
※このメソッドは後々の節で触れる浮動小数点数のdouble
型などにも存在します。
int型のtoDoubleメソッド
int
型のtoDouble
メソッドでは整数の値を浮動小数点数のdouble
型に変換した値を返します。
void main() {
int myIntValue = 10;
double myDoubleValue = myIntValue.toDouble();
myDoubleValue += 0.5;
print(myDoubleValue);
}
int型のtoStringメソッド
int
型のtoString
メソッドでは整数の値を文字列のString
型に変換した値を返します。
void main() {
int myIntValue = 1000;
String myStringValue = myIntValue.toString();
myStringValue += '円';
print(myStringValue);
}
int型のparseメソッド
int
型自体が持っているstaticメソッドとしてparse
メソッドがあります(後の節で触れるdouble
型など他の型でも存在しています)。
このメソッドでは引数に指定された文字列を整数に変換します。
void main() {
String intFormatString = '12345';
int myIntValue = int.parse(intFormatString);
print(myIntValue);
}
整数に変換できない値を指定するとエラーになります。0になったりとかはしません。
void main() {
String myStringValue = 'abcdef';
int myIntValue = int.parse(myStringValue);
print(myIntValue);
}
浮動小数点数の形式の文字列だった場合などでもエラーとなります。
void main() {
String doubleFormatString = '1.234';
int myIntValue = int.parse(doubleFormatString);
print(myIntValue);
}
浮動小数点数の型
浮動小数点数の型はdouble
となります(64bitの値となります)。基本的な使い方はint
と大体一緒です。
void main() {
double myDoubleValue = 20.5;
myDoubleValue += 10.2;
print(myDoubleValue);
}
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);
}
似たようなメソッドとしてceilToDouble
というメソッドも存在します。これも挙動はceil
とほぼ同じなのですが結果の型がint
ではなくdouble
となります(2とか1といった値のdouble
になります)。元の変数をそのまま使いたい場合などに便利です。
void main() {
double myDoubleValue = 1.1;
myDoubleValue = myDoubleValue.ceilToDouble();
print(myDoubleValue);
}
double型のfloorメソッドとfloorToDoubleメソッド
ceil
と同じような形でdouble
型にはfloor
とfloorToDouble
というメソッドがあります。
こちらは小数点以下を切り捨てます。例えば1.1や1.9という値であれば結果の値は1となります。floor
とfloorToDouble
の違いはceil
と同じように結果の値の型がint
になるかdouble
になるかの違いだけです。
void main() {
double myDoubleValue = 1.1;
int myIntValue = myDoubleValue.floor();
print(myIntValue);
myDoubleValue = 1.9;
myDoubleValue = myDoubleValue.floorToDouble();
print(myDoubleValue);
}
double型のroundメソッドとroundToDoubleメソッド
double
型のround
メソッドは四捨五入の挙動をします(偶数丸めかと思ったら四捨五入の挙動をするようです)。round
とroundToDouble
の違いは他のメソッドと同様に結果が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());
}
double型のtoStringAsFixedメソッド
double
型のtoStringAsFixed
メソッドでは特定の小数点の桁数で四捨五入をすることができます。引数に対象の小数点数の桁数を指定します(引数に2を指定すれば3桁目が四捨五入されて2桁目までが残るようになります)。また、結果の値は文字列になります。
round
メソッドは特定の桁数での四捨五入とかではなく整数になる形での小数点以下の四捨五入となるため、特定の桁数で四捨五入したい場合などに便利です。
ただし、結果の値をdouble
型などで扱いたい場合には結果が文字列になっているので一旦parse
メソッドで変換する必要があります。
void main() {
double myDoubleValue = 1.245;
myDoubleValue = double.parse(myDoubleValue.toStringAsFixed(2));
print(myDoubleValue);
}
文字列の型
文字列の型はString
となります。文字列の値に関してはシングルクォーテーション(''
)もしくはダブルクォーテーション(""
)で囲んで扱います。どちらでも動きますが理由がなければフォーマッタとかでどちらかに統一しておくといいかもしれません。
void main() {
String myStringValue = "Hello";
print(myStringValue);
}
文字列の連結は+
や+=
の記号で行えます。
void main() {
String myStringValue = "Hello";
myStringValue += ' World!';
print(myStringValue);
}
また、良く使う文字列の機能として文字列中で直前に$
記号を設定することで変数を文字列の中で展開することができます(Pythonのf-stringsのような機能)。文字列中に変数の値を含めたい場合に文字列を連結して書くよりも記述がシンプルになります。
void main() {
String myStringValue = "太郎";
print('私の名前は$myStringValueです。');
}
${対象の変数}
といったように{}
の括弧で囲むと変数の属性やメソッドにアクセスしたり呼び出したりしても動きます。
void main() {
final myString = 'abcdef';
print('文字列を大文字に変換すると${myString.toUpperCase()}になります。');
}
この機能は文字列以外の変数も含めることができます。例えば以下のように整数の変数を文字列中に含めたりすることができます。
void main() {
int age = 17;
print('年齢は$age歳です。');
}
String型のisEmpty属性とisNotEmpty属性
String
型のisEmpty
属性は文字列が空文字列かどうかの真偽値となります。もし文字列が空であればtrueとなります。逆にisNotEmpty
属性は空でなければtrueとなります。
void main() {
String myStringValue = '';
print(myStringValue.isEmpty);
print(myStringValue.isNotEmpty);
}
String型のlength属性
String
型のlength
属性は文字列の文字数の整数となります。
void main() {
String myStringValue = 'Hello';
print(myStringValue.length);
}
日本語なども想定通りに文字数をカウントしてくれます。
void main() {
String myStringValue = 'あいうえお安以宇衣於';
print(myStringValue.length);
}
ただし特殊な漢字や絵文字などの特殊文字は正確に取れないようです(この辺は他の言語でも想定通りの値が取れたり取れなかったりなのでDartだけの問題ではありませんが要注意な感じではあります)。
void main() {
String myStringValue = '👀🎉🙇♂️';
print(myStringValue.length);
}
String型のcontainsメソッド
String
型のcontains
メソッドでは引数に指定された文字列を含んでいるかどうかの真偽値を取得することができます。含んでいればtrue
となります。
void main() {
String myStringValue = 'Hello World!';
print(myStringValue.contains('lo'));
}
String型のstartsWithメソッドとendsWithメソッド
String
型のstartsWith
メソッドでは文字列が引数で指定した特定の文字列でスタートしているかの真偽値を取得することができます。スタートしていればtrue
となります。
void main() {
String myStringValue = 'Hello World!';
print(myStringValue.startsWith('He'));
}
逆にendsWith
メソッドでは文字列の最後が引数で指定した文字列で終了しているかの真偽値を取得することができます。終了していればtrue
となります。
void main() {
String myStringValue = 'Hello World!';
print(myStringValue.endsWith('ld!'));
}
String型のindexOfメソッドとlastIndexOfメソッド
String
型のindexOf
メソッドでは文字列中で引数に指定した文字列がどの位置に最初に出現するかどうかのインデックスの整数を取得することができます。複数該当する文字列がある場合は最初の位置となります。また、最初の文字がインデックス0となります。
void main() {
String myStringValue = 'Hello World!';
print(myStringValue.indexOf('l'));
}
lastIndexOf
メソッドの方は文字列の右端から該当する文字列を検索します。ただし結果のインデックスは左端からカウントされます(右側からカウントされたりはしません)。
void main() {
String myStringValue = 'Hello World!';
print(myStringValue.lastIndexOf('l'));
}
String型のpadLeftメソッドとpadRightメソッド
String
型のpadLeft
メソッドは文字列の左端を特定の文字で引数で指定した文字数になるまで埋めます。第一引数に最終的な文字数、第二引数に埋める文字を指定します。第二引数は省略可で、省略した場合は半角スペースで埋められます。
void main() {
String myStringValue = 'Hello';
print(myStringValue.padLeft(20));
}
※スクショでは分かりづらいですが合計で20文字になるまで左端に半角スペースが追加になっています。
第二引数に文字を指定すると半角スペースの代わりにその文字で文字列の左端が埋められます。
void main() {
String myStringValue = 'Hello';
print(myStringValue.padLeft(20, 'A'));
}
padRight
メソッドはpadLeft
メソッドとほぼ使い方は変わりませんが、文字を埋める位置が左端ではなく右端となります。
void main() {
String myStringValue = 'Hello';
print(myStringValue.padRight(20, 'A'));
}
String型のreplaceAllメソッド
String
型のreplaceAll
メソッドは任意の文字列を別の文字列で全て置換した文字列を返却します。第一引数に置換したい文字列、第二引数に置換後の文字列を指定します。
以下の例では小文字のl
を大文字のL
で置換しています。
void main() {
String myStringValue = 'Hello World!';
print(myStringValue.replaceAll('l', 'L'));
}
String型のsplitメソッド
String
型のsplit
メソッドでは文字列を引数で指定された文字で分割されたリスト(配列)を返却します。例えばコンマ区切りで分割したりスペース区切りで分割したりといった具合です。
void main() {
String myStringValue = 'AA BBB C DD EEE';
List<String> splittedList = myStringValue.split(' ');
print(splittedList);
}
String型のsubstringメソッド
String
型のsubstring
メソッドは文字列の特定のインデックス範囲の文字列を返却します(文字列の一部を抽出することができます)。
第一引数には抽出範囲の開始インデックス(最初の文字のインデックスは0となります)、第二引数には抽出範囲の終了インデックス(このインデックス自体は含みません)を指定して使います(つまり第一引数のインデックス~第二引数のインデックス - 1の範囲の各文字が対象となります)。
第二引数は省略可で、その場合は文字列の最後までが抽出対象となります。
以下の例では第一引数に2を指定指定しているのでインデックス2(3文字目)~最後の文字列までが抽出対象となります。
void main() {
String myStringValue = 'HelloWorld!';
print(myStringValue.substring(2));
}
以下の例では第一引数に2、第二引数に5を指定しているので2~4のインデックス範囲が抽出対象となります。
void main() {
String myStringValue = 'HelloWorld!';
print(myStringValue.substring(2, 5));
}
String型のtoLowerCaseメソッドとtoUpperCaseメソッド
String
型のtoLowerCase
メソッドは文字列を小文字に変換した値を返却します。
void main() {
String myStringValue = 'HELLO WORLD!';
print(myStringValue.toLowerCase());
}
逆にtoUpperCase
メソッドでは大文字に変換した文字列のあたぽを返却します。
void main() {
String myStringValue = 'hello world!';
print(myStringValue.toUpperCase());
}
String型のtrimメソッド、trimLeftメソッド、trimRightメソッド
String
型のtrim
メソッドは文字列の左右の両端の空白文字(スペースや改行など)を取り除いた文字列を返却します。
void main() {
String myStringValue = ' \n Hello world! ';
print(myStringValue.trim());
}
trimLeft
メソッドはtrim
メソッドと似た挙動をしますが文字列の左端だけ空白文字が取り除かれます。
void main() {
String myStringValue = ' \n Hello world! \n\n ';
print(myStringValue.trimLeft());
}
※以下のスクショは分かりづらいですが、出力の文字列を選択してみるとスペースなどが残っていることが分かります。
逆にtrimRight
メソッドは文字列の右端からのみ空白文字が取り除かれます。
void main() {
String myStringValue = ' \n\n Hello world! \n\n ';
print(myStringValue.trimRight());
}
真偽値の型
真偽値の型はbool
となります。値はtrue
もしくはfalse
のどちらかとなります。
void main() {
bool isCompleted = true;
print(isCompleted);
}
真偽値の値の直前に!
記号を配置すると逆に値になります(true
であればfalse
に、false
であればtrue
になります)。
void main() {
bool isCompleted = true;
print(!isCompleted);
}
リスト(配列)の型
リスト(配列)はList
型を使います。また、値を設定する際には[]
の括弧を使いコンマ区切りで複数の値を指定します。
※ループ用のfor文などに関しては後々の節で触れます。
void main() {
List myList = [10, 20, 'Hello'];
for (final value in myList) {
print(value);
}
}
前節まででも触れましたがリストの要素の型を固定したい場合には<対象の型>
といった指定をList
の後に記述します。
void main() {
List<int> myList = [10, 20, 30];
print(myList);
}
※以降の節でList
型の各属性やメソッドについて触れていきますがDartはかなりその辺多いな・・・!と思いました。充実していると思ったり中々初見の時「これ何のメソッドなんだろう・・・?」といったものも割とあって調べていて面白かったです。
List型のlength属性
List
型のlength
属性はリスト内の要素の件数となります。3件要素が格納されていれば3となります。
void main() {
List<int> myListValue = [10, 20, 30];
print(myListValue.length);
}
List型のfirst属性とlast属性
List
型のfirst
属性はリストの先頭の要素となります。
void main() {
List<int> myListValue = [10, 20, 30];
print(myListValue.first);
}
last
属性では逆にリストの最後の要素となります。
void main() {
List<int> myListValue = [10, 20, 30];
print(myListValue.last);
}
対象が空のリストの場合にはfirst
属性・last
属性共にエラーとなります。
void main() {
List<int> myListValue = [];
print(myListValue.first);
}
List型のfirstOrNull属性とlastOrNull属性
List
型のfirstOrNull
属性とlastOrNull
属性はfirst
とlast
属性のようにリストの先頭の要素もしくは最後の要素となります。違いとしてはfirst
属性などと異なりリストが空の場合でもランタイムエラーにならず、代わりにnull
となります。
void main() {
List<int> myListValue = [10, 20, 30];
print(myListValue.firstOrNull);
}
void main() {
List<int> myListValue = [10, 20, 30];
print(myListValue.lastOrNull);
}
void main() {
List<int> myListValue = [];
print(myListValue.firstOrNull);
}
List型のsingle属性とsingleOrNull属性
List
型のsingle
属性はリストの要素の件数が1件になっていることを確認し、もし1件になっていればその要素となり、もし1件ではない場合はエラーとなります。
void main() {
List<int> myListValue = [100];
print(myListValue.single);
}
void main() {
List<int> myListValue = [100, 200, 300];
print(myListValue.single);
}
List型のisEmpty属性とisNotEmpty属性
List
型のisEmpty
属性はリストが空かどうかの真偽値となります。空であればtrue
、そうでなければfalse
となります。
void main() {
List<int> myListValue = [];
print(myListValue.isEmpty);
}
void main() {
List<int> myListValue = [100, 200];
print(myListValue.isEmpty);
}
逆にisNotEmpty
属性では空では無ければtrue
となります。!
記号での否定とかを使うよりもこちらを使った方がぱっと見で分かりやすいかもしれません。
void main() {
List<int> myListValue = [100, 200];
print(myListValue.isNotEmpty);
}
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');
}
}
Iterable型についての補足
List
型の範疇から少し逸脱しますがList
関係の操作で良くIndex
型が出てくるので軽く触れておきます。
Iterable
型はList
型のように複数の要素を順番に格納する型となります。ただし利用の際にはfor文によるループや後々触れるmap
やwhere
などのメソッドと一緒に使う形になります。リストのように添え字用の括弧([]
)を使って要素にアクセスすることは出来ずエラーとなります。
void main() {
List<String> myListValue = ['A', 'B', 'C'];
print(myListValue.indexed[0]);
}
Iterable型からList型に戻したい場合
List
型の操作でIterable
型の値となった場合に再びList
型に変換したい場合のためにtoList
というメソッドが用意されているためそちらを使うとList
の型で再び扱うことができます。
void main() {
List<String> myListValue = ['A', 'B', 'C'];
List indexedListValue = myListValue.indexed.toList();
print(indexedListValue);
}
List型のreversed属性
List
型のreversed
属性はリストの各要素を逆順にしたIterable
型の値となります。
void main() {
List<String> myListValue = ['A', 'B', 'C'];
for (final value in myListValue.reversed) {
print(value);
}
}
List型のnonNulls属性
List
型のnonNulls
属性はリストの要素内でnull
ではない要素のみに絞ったIterable
型の値となります。
※null
に関しては後々の節で触れます。以下のコード例で出てくる?
記号もnull
関係となります。
void main() {
List<String?> myListValue = ['A', null, 'B', null, 'C'];
for (final value in myListValue.nonNulls) {
print(value);
}
}
List型のaddメソッドとaddAllメソッド
List
型のadd
メソッドはリストに新たな要素を追加します。引数に追加したい要素を指定します。追加される位置はリストの末尾となります。
void main() {
List<String?> myListValue = ['A', 'B', 'C'];
myListValue.add('D');
for (final value in myListValue.nonNulls) {
print(value);
}
}
また、addAll
メソッドでは引数に別のリスト(もしくは他のIterable
型の値)を追加することで複数の要素を一気に追加することができます。
void main() {
List<String?> myListValue = ['A', 'B', 'C'];
myListValue.addAll(['D', 'E', 'F']);
for (final value in myListValue.nonNulls) {
print(value);
}
}
List型のinsertメソッドとinsertAllメソッド
List
型のinsert
メソッドはadd
メソッドのようにリストに要素を追加します。ただしadd
メソッドと異なり要素を追加する位置を引数で指定することができます。
第一引数に要素を追加したい位置のインデックス(先頭に追加したい場合には0、その次は1...といった値になります)、第二引数に追加したい要素を指定します。
void main() {
List<int> myListValue = [100, 100, 100, 100];
myListValue.insert(2, 200);
print(myListValue);
}
また、insertAll
メソッドでは指定するインデックスの位置に複数の要素をまとめて追加することができます。第二引数は別のリスト(もしくは他のIterable
型の値)を指定します。
void main() {
List<int> myListValue = [100, 100, 100, 100];
myListValue.insertAll(2, [200, 300, 400]);
print(myListValue);
}
List型のremoveメソッド
List
型のremove
メソッドは引数で指定された値をリスト内から検索し、見つかった要素を1件リストから取り除きます。該当の要素が全て取り除かれるわけでは無く、一致した先頭の要素が取り除かれます。
また、返却値は要素が取り除かれたかどうかの真偽値となります。もし該当する要素がなければfalse
が返却されます。
void main() {
List<int> myListValue = [100, 200, 200, 100];
bool isRemoved = myListValue.remove(200);
print(myListValue);
print(isRemoved);
}
void main() {
List<int> myListValue = [100, 200, 200, 100];
bool isRemoved = myListValue.remove(300);
print(myListValue);
print(isRemoved);
}
List型のremoveAtメソッド
List
型のremoveAt
関数は第一引数に指定したインデックス位置の要素をリストから取り除きます。返却値は取り除いた値となります。
void main() {
List<int> myListValue = [100, 200, 300];
int removedValue = myListValue.removeAt(1);
print(myListValue);
print(removedValue);
}
指定したインデックスの位置に要素が無い場合にはランタイムエラーとなります。
void main() {
List<int> myListValue = [100, 200, 300];
int removedValue = myListValue.removeAt(5);
print(myListValue);
print(removedValue);
}
List型のremoveLastメソッド
List
型のremoveLast
メソッドはリストの最後の要素を取り除きます。引数は必要とせず、返却値は取り除かれた値となります。
void main() {
List<int> myListValue = [100, 200, 300];
int removedValue = myListValue.removeLast();
print(myListValue);
print(removedValue);
}
もしリストが空の場合にはランタイムエラーになります。
void main() {
List<int> myListValue = [];
int removedValue = myListValue.removeLast();
print(myListValue);
print(removedValue);
}
List型のremoveRangeメソッド
List
型のremoveRange
メソッドはリストから指定した範囲の要素を取り除きます。第一引数は取り除く範囲の開始インデックス、第二引数は取り除く範囲の終了インデックス(ただしこのインデックスは含まず、第二引数 - 1の範囲が取り除く対象となります)となります。例えば第一引数に2、第二引数に4を指定した場合にはインデックスが2~3の範囲の2要素が取り除かれます。
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
myListValue.removeRange(2, 4);
print(myListValue);
}
指定したインデックス範囲がリストの範囲外になっているとランタイムエラーとなります。
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
myListValue.removeRange(2, 6);
print(myListValue);
}
List型のremoveWhereメソッド
List
型のremoveWhere
メソッドでは条件を満たした要素をリストから取り除きます。引数には判定用の関数を指定します。判定用の関数には引数に要素、返却値に真偽値を設定する必要があります。
条件を満たす要素が複数ある場合には一通り取り除かれます。
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
myListValue.removeWhere((element) => element >= 300);
print(myListValue);
}
List型のretainWhereメソッド
List
型のretainWhere
メソッドはremoveWhere
メソッドの判定用の関数が逆の動作をします。つまり判定用の関数で条件を満たした要素をリストに残す(他は取り除く)挙動になります。
引数はremoveWhere
メソッドと同様に要素を引数に受け取って真偽値を返す判定用の関数となります。
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
myListValue.retainWhere((element) => element >= 300);
print(myListValue);
}
List型のclearメソッド
List
型のclear
メソッドではリストを空にします(要素をまったく含んでいない状態にします)。
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
myListValue.clear();
print(myListValue);
}
List型のelementAtメソッドとelementAtOrNullメソッド
List
型のelementAt
メソッドは引数で指定されたインデックス位置の要素を返却します。他のメソッドなどと同様にインデックスは0からスタートします。
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
print(myListValue.elementAt(2));
}
リストの範囲外のインデックスを引数に指定した場合にはランタイムエラーとなります。
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
print(myListValue.elementAt(5));
}
elementAtOrNull
メソッドはelementAt
メソッドと似た挙動をしますが、もし引数に指定したインデックス範囲がリストの範囲を超えている場合でもエラーにはならずに代わりにnull
を返却します。
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
print(myListValue.elementAtOrNull(4));
}
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
print(myListValue.elementAtOrNull(5));
}
List型のwhereメソッド
List
型のwhere
メソッドは引数の関数で条件を満たしたIterable
型の値を返却します。各要素を参照して判定を行い、条件を満たせば返却値に含まれるようになるといった挙動になります。
引数の関数の第一引数には対象の要素が渡され、返却値には真偽値を設定する必要があります(true
を返却した要素が結果のIterable
内に含まれる形になります)。
retainWhere
と似たような挙動となりますが、retainWhere
の方はリスト自体の内容が変更されるのに対してこちらは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件ではない場合にはエラーとなります。
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
int myValue = myListValue.singleWhere((element) => element == 300);
print(myValue);
}
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
int myValue = myListValue.singleWhere((element) => element >= 300);
print(myValue);
}
List型のfirstWhereメソッドとlastWhereメソッド
List
型のfirstWhere
は引数で判定用の関数を受け付け、その関数で条件を満たす要素の中で先頭の要素を返却します。引数の関数には他のwhere
などのメソッドと同様に引数に対象の要素を受け付け返却値には判定結果の真偽値が必要になります。条件を満たす場合には返却値にtrue
が設定されるようにします。
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
int firstValue = myListValue.firstWhere((element) => element >= 250);
print(firstValue);
}
第二引数を指定しない条件且つ該当する要素が1件も存在しない場合にはエラーになります(第二引数に関しては後で説明します)。
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
int firstValue = myListValue.firstWhere((element) => element >= 550);
print(firstValue);
}
第二引数はorElse
という名前の引数となり、こちらも関数を引数に取りますが、要素は引数に渡されたりはせず単純に特定の返却値を設定するのみの関数となります。もしfirstWhere
メソッドで該当する要素が無かった場合にはこの関数の返却値がデフォルト値のように設定されます(エラーにもならなくなります)。
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
int firstValue = myListValue.firstWhere(
(element) => element >= 550,
orElse: () => -1,
);
print(firstValue);
}
lastWhere
メソッドはfirstWhere
メソッドと使い方は同じですが、条件のチェックがリストの最後の要素から逆順に実行されていきます。
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
int lastValue = myListValue.lastWhere((element) => element >= 300);
print(lastValue);
}
第二引数のorElse
メソッドもfirstWhere
メソッドと同じです。
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
int lastValue = myListValue.lastWhere(
(element) => element <= 50,
orElse: () => -1,
);
print(lastValue);
}
List型のgetRangeメソッド
List
型のgetRange
メソッドは引数で指定したインデックス範囲のIterable
型の値を取得します。第一引数は開始インデックス、第二引数は最終インデックスとなります(ただし第二引数の指定のインデックス自体は含みません。最終インデックス - 1のインデックス範囲となります)。
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
print(myListValue.getRange(1, 4));
}
List型のindexOfメソッドとlastIndexOfメソッド
List
型のindexOf
メソッドは引数で指定された値の要素をリスト内で左から検索し、最初に該当するインデックスを返却します。
void main() {
List<int> myListValue = [100, 200, 300, 400, 500];
print(myListValue.indexOf(200));
}
対象の要素が見つからない場合には-1
が返却されます。エラーなどにはなりません。
void main() {
List<int> myListValue = [100, 200, 300, 100, 200, 300];
print(myListValue.indexOf(500));
}
lastIndexOf
メソッドはindexOf
と似たような挙動をしますが検索がリストの右端から実行されます(ただし返却されるインデックスは右端からの値ではなく左端からの位置となります)。
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));
}
List型のindexWhereメソッドとlastIndexWhereメソッド
List
型のindexWhere
は条件を満たす要素のインデックスを返却します。第一引数には条件を満たすかどうかの判定用の関数を指定します。他のメソッドと同様この関数には第一引数に要素が渡され、返却値で真偽値を返す必要があります。返却値がtrue
になる最初の要素のインデックスが対象となります。
void main() {
List<int> myListValue = [100, 200, 300, 100, 200, 300];
print(myListValue.indexWhere((element) => element >= 250));
}
該当するものが存在しない場合にはindexOf
メソッドなどと同じように-1
が返却されます。
void main() {
List<int> myListValue = [100, 200, 300, 100, 200, 300];
print(myListValue.indexWhere((element) => element >= 500));
}
indexWhere
メソッドの第二引数は条件のチェックを開始するインデックス位置となります。省略可の引数となり、省略した場合にはリストの先頭の要素から順番にチェックされていきます。
void main() {
List<int> myListValue = [100, 200, 300, 100, 200, 300];
print(myListValue.indexWhere((element) => element >= 250, 3));
}
lastIndexWhere
メソッドはindexWhere
と同じような使い方となりますが、要素の判定はリストの右端から順番にチェックされる形となります。右側からチェックされはするものの、lastIndexOf
メソッドなどと同じように返却値のインデックスは左から数えたインデックスとなります。
void main() {
List<int> myListValue = [100, 200, 300, 100, 200, 300, 200];
print(myListValue.lastIndexWhere((element) => element >= 250));
}
第二引数でチェックを開始するインデックスを指定できる点もindexWhere
メソッドと同様です。ただし開始位置のインデックスは右側からカウントした値となります。判定と第二引数の開始インデックスは右側から、返却値のインデックスは左からのインデックスとなります。
void main() {
List<int> myListValue = [100, 200, 300, 100, 200, 300, 200];
print(myListValue.lastIndexWhere((element) => element >= 250, 2));
}
List型のfoldメソッド
List
型のfold
メソッドは指定する初期値とリストの各要素を参照して単一の値を返却します。第一引数に初期値、第二引数に直前の要素までの計算結果(previousValue
)と対象の要素(element
)の引数を持ち計算結果を返却値に設定する形の関数を指定します。
void main() {
List<int> myListValue = [1, 2, 3];
print(
myListValue.fold<int>(
10,
(previousValue, element) => previousValue + element,
),
);
}
なお、ジェネリックでfold<int>
といったように計算値に対する型を明示していますが、これをやらないと値がnull
になったりで想定した計算にならないようです。無難にジェネリックの型の指定をしておいた方が無難?そうな印象です。
List型のreduceメソッド
List
型のreduce
メソッドはfold
メソッドと似た感じでリストの各要素に対して順番に計算を行い最終的な単一の値を返却します。
fold
メソッドとの違いは初期値を別の値として指定するかどうかです。reduce
メソッドでは初期値の引数はありません。また、こちらのメソッドはジェネリックの型指定をしなくとも問題無いようです。
void main() {
List<int> myListValue = [1, 2, 3];
print(
myListValue.reduce(
(previousValue, element) => previousValue + element,
),
);
}
List型のmapメソッド
List
型のmap
メソッドはリストの各要素に対して特定の処理を反映した結果のIterable
型の値を返却します。例えば全ての要素に特定の値を足したり型を変換したりといった制御に使えます。
第一引数には各要素への処理用の関数を指定します。そちらの関数の引数にはリストの要素が渡され、返却値には処理後の要素を設定する必要があります。
void main() {
List<int> myListValue = [1, 2, 3];
print(myListValue.map((element) => element + 10));
}
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]));
}
List型のreplaceRangeメソッド
List
型のreplaceRange
メソッドは引数で指定されたインデックス範囲を、別途指定したリスト(もしくは他のIterable
型の値)で差し替えます。Iterable
の値を返す形ではなく元のリストに対して変更が入ります。
第一引数は置換の開始インデックス、第二引数は置換の最終インデックス(ただしこのインデックスは含まず、第二引数を1減算したインデックスまでが範囲となります)、第三引数に差し替えで使うリストなどの値を指定します。
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);
}
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);
}
また、第一引数に指定した開始インデックス位置を加味した第二引数に指定したリストなどの要素の件数が元のリストの長さを超える場合にはエラーになります(元のリストの要素の件数は変動しないようになっています)。
void main() {
List<int> myListValue = [1, 2, 3, 4, 5, 6];
myListValue.setAll(1, [20, 30, 40, 50, 60, 70]);
print(myListValue);
}
List型のsetRangeメソッド
List
型のsetRange
メソッドはreplaceRange
メソッドやsetAll
メソッドなどと同様にリスト内の複数の要素を置換します。
replaceRange
メソッドやsetAll
などと異なり以下のような挙動をします。
-
setAll
メソッドとは異なり置換する範囲をそれぞれ引数で指定します。 -
replaceRange
メソッドのように範囲を超えて置換されたりしません。- ※リストの件数は置換処理後も変わらず、もし置換範囲がリストのインデックス範囲を超えている場合にはエラーになります。
- 第四引数に
skipCount
という引数があり、置換で指定したリスト(もしくは別のIterable
の値)の中で先頭の任意の個数の要素をスキップすることができます。
第一引数は置換範囲の開始インデックス、第二引数は置換範囲の終了インデックス(ただしこのインデックス自体は含まず-1した位置までが対象となります)、第三引数には置換で設定するリスト(もしくは他のIterable
の値)、第四引数は要素のスキップ数の指定(省略可)となります。
void main() {
List<int> myListValue = [1, 2, 3, 4, 5, 6];
myListValue.setRange(1, 3, [20, 30, 40, 50, 60, 70]);
print(myListValue);
}
void main() {
List<int> myListValue = [1, 2, 3, 4, 5, 6];
myListValue.setRange(1, 3, [20, 30, 40, 50, 60, 70], 2);
print(myListValue);
}
void main() {
List<int> myListValue = [1, 2, 3, 4, 5, 6];
myListValue.setRange(1, 10, [20, 30, 40, 50, 60, 70], 2);
print(myListValue);
}
List型のsublistメソッド
List
型のsublist
メソッドは引数で指定されたインデックス範囲のリストを返却します。第一引数には開始インデックス、第二引数には終了インデックス(ただし他と同様にこのインデックスを-1
した位置が最後のインデックスとなります)を指定します。返却値はIterable
型ではなくList
になります。
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);
}
List型のsortメソッド
List
型のsort
メソッドはリストの要素をソートします。引数は省略可で、省略した場合には数値の昇順でソートを行います。
void main() {
List<int> myListValue = [20, -5, 15, 3, 2, 10, 35, -16];
myListValue.sort();
print(myListValue);
}
数値以外、例えば文字列ではどのような判定になるのだろう・・・と思ったのですが、どうやらUnicodeのコードポイントの順序の昇順でソートがされるようです。この辺はPythonとかと似たような挙動でしょうか。
void main() {
List<String> myListValue = ['CCCC', 'BBB', 'DD', 'AAAAA', 'EE'];
myListValue.sort();
print(myListValue);
}
第一引数は省略可能ですが、指定する場合にはソート順判定用の関数を指定します。この関数にはa
とb
の2つの引数を取り、もしa
をb
よりも前に配置したい時には-1
、a
とb
が同値条件ならb
、a
をb
よりも後に配置させたい場合には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
メソッドなどでの記述をシンプルにすることができます。
void main() {
List<String> myListValue = ['CCCC', 'BBB', 'DD', 'AAAAA', 'EE'];
myListValue.sort((a, b) => a.length.compareTo(b.length));
print(myListValue);
}
昇順でソートしたリストを降順にしたい場合にはreversed
属性を使うか、もしくは引数の関数の条件で設定します。
void main() {
List<int> myListValue = [20, -5, 15, 3, 2, 10, 35, -16];
myListValue.sort();
myListValue = myListValue.reversed.toList();
print(myListValue);
}
List型のskipメソッドとskipWhileメソッド
List
型のskip
メソッドは引数で指定した要素数分先頭の要素を取り除いたIterable
の値を返却します。
void main() {
List<int> myListValue = [1, 2, 3, 4, 5];
print(myListValue.skip(2));
}
skipWhile
メソッドはskip
メソッドと同様に先頭の方の要素を省いたIterable
の値を返却します。ただしこちらの引数はスキップする数ではなく判定用の関数を指定する必要があります。引数で渡された関数でリストの要素を先頭から順番にチェックしていって、条件を満たしている限りは要素がスキップされます。関数の第一引数には対象の要素が渡され、返却値には条件を満たすかどうかの真偽値が必要になります。
void main() {
List<int> myListValue = [1, 2, 3, 4, 5, 6];
print(myListValue.skipWhile((element) => element <= 3));
}
List型のtakeメソッドとtakeWhileメソッド
List
型のtake
メソッドは引数で指定した個数の件数分の要素を格納したIterable
の値を返却します。例えば引数に3
を指定すれば3件の要素を格納したIterable
が返却されます。対象となる要素は先頭から順番に処理されていきます。
void main() {
List<int> myListValue = [1, 2, 3, 4, 5, 6];
print(myListValue.take(3));
}
もし引数で指定した件数よりもリストの要素数が少ない場合には結果の要素数は引数の値よりも少なくなります(エラーになったりはしません)。
void main() {
List<int> myListValue = [1, 2, 3];
print(myListValue.take(5));
}
takeWhile
メソッドはtake
メソッドのように先頭からの要素を参照したIterable
の値を返却します。ただしこちらは引数で一定の件数を対象とするのではなく、引数で指定したチェック用の関数の条件を満たす限りの要素を格納したIterable
の値を返却します。条件を満たさなくなったらその直前の要素までが返却値の対象となります。引数の関数には引数に対象の要素、返却値に真偽値が必要になります(条件を満たす場合にはtrue
を返却する形にします)。
void main() {
List<int> myListValue = [1, 2, 3, 4, 5];
print(myListValue.takeWhile((element) => element <= 3));
}
List型のanyメソッド
List
型のany
メソッドは引数に関数を受け付けて、リストの各要素ごとにその関数が条件を満たすかどうかを判定し、もし1つでも条件を満たす要素が存在すればtrue
を返却するメソッドです。
対象の関数は第一引数に対象の要素が指定され、返却値には真偽値を設定する必要があります。
※Dartにおける関数の書き方や無名関数の書き方については後々の節で詳しく触れますが、(引数) => 返却値
という記述で無名関数がDartでは定義できる形になっています。
void main() {
List<String> myListValue = ['AAA', 'BB', 'CCCC'];
print(myListValue.any((element) => element.length == 2));
}
void main() {
List<String> myListValue = ['A', 'B', 'CCC'];
print(myListValue.any((element) => element.length == 2));
}
List型のeveryメソッド
List
型のevery
メソッドはany
メソッドと似た書き方と挙動になりますが、any
メソッドとは異なり全ての要素が関数内で条件を満たした場合にtrue
を返却するメソッドとなります。
void main() {
List<int> myListValue = [100, 200, 150, 220];
print(myListValue.every((element) => element <= 250));
}
void main() {
List<int> myListValue = [100, 180, 150, 220];
print(myListValue.every((element) => element <= 200));
}
List型のjoinメソッド
List
型のjoin
メソッドはリストの要素を連結した文字列を返却します。引数は区切り文字となり、省略した場合は区切り文字無しで連結されます。
void main() {
List<int> myListValue = [1, 2, 3, 4, 5];
print(myListValue.join());
}
void main() {
List<int> myListValue = [1, 2, 3, 4, 5];
print(myListValue.join(','));
}
List型のasMapメソッド
List
のasMap
メソッドはリストをMap
(辞書)に変換した値を返却します。キーにはリストのインデックス、値にリストの要素が設定されます。
※Map
型については後々の節で触れます。
void main() {
List<int> myListValue = [1, 2, 3, 4, 5];
print(myListValue.asMap());
}
List型のtoSetメソッド
List
型のtoSet
メソッドはリスト型をSet
型(集合の型)に変換した値を返却します。リストに似た値となりますが、重複した値はSet
からは取り除かれます(一意な要素の結果となります)。
※Set
型については後々の節で詳しく触れます。
void main() {
List<int> myListValue = [1, 2, 1, 2, 3, 4, 3];
print(myListValue.toSet());
}
集合の型
集合はSet
型を使います。また、値を設定する際には{}
の括弧を使いコンマ区切りで複数の値を指定します。Map
と同じ括弧となりますがこちらはキーと値のような設定ではないためコロンは使わずにコンマのみで値を区切って使います。
※Map
型については後々の節で触れます。
void main() {
Set mySetValue = {1, 2, 3};
print(mySetValue);
}
集合に含める値の型を固定したい場合のジェネリックの型の指定は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);
}
List
のようにSet
の値もfor文でループを行うことができます。
void main() {
Set<int> mySetValue = {1, 2, 3};
for (final value in mySetValue) {
print(value);
}
}
Setで使える属性やメソッドに関して
Set
型ではList
型と同じ属性やメソッドの多くを利用することができます。
例えば属性で言えばfirst
、firstOrNull
、length
、single
、singleOrNull
、isEmpty
などのList
と同様の様々な各属性が、メソッドで言えばadd
、addAll
、any
、clear
、join
などの多くのメソッドが用意されています。
それらの属性やメソッドに関してはList
型の節で詳しく触れたためSet
のそれらの説明は割愛します。
マップ(辞書)の型
マップ(辞書)はMap
型を使います。キーと値を持つ値となります。値を定義する際には{}
の括弧を使い、キーと値をコロンで区切り、そして要素をコンマで区切ります。
void main() {
Map myMapValue = {"myKey1": 10, "myKey2": "Hello"};
print(myMapValue);
}
前節まででも触れましたがマップでキーと値の型を固定したい場合には<キーの型, 値の型>
といった形でコンマ区切りでそれぞれを指定します。
void main() {
Map<String, int> myMapValue = {
'key1': 100,
'key2': 200,
};
print(myMapValue);
}
MapEntry型
Map
型について詳しく触れて行く前にMapEntry
という型について触れた方がスムーズなので先に触れておきます。
MapEntry
型は単一のキーと値の組み合わせを扱う型となります。Map
型が複数のキーと値を持てる形になる一方でこちらは単一のキーと値になります。
使い方はクラスのコンストラクタで初期化し、第一引数にキー、第二引数に値を設定します。
void main() {
MapEntry myMapEntry = MapEntry('key', 100);
print(myMapEntry);
}
MapEntry
型でキーを参照したい場合にはkey
属性を使います。
void main() {
MapEntry myMapEntry = MapEntry('myKey', 100);
print(myMapEntry.key);
}
値の方はvalue
属性でアクセスすることができます。
void main() {
MapEntry myMapEntry = MapEntry('myKey', 100);
print(myMapEntry.value);
}
Map型のisEmpty属性とisNotEmpty属性
Map
型のisEmpty
属性は辞書が空かどうか(1つのキーが存在しないかどうか)の真偽値となります。
void main() {
Map<String, int> myMapValue = {};
print(myMapValue.isEmpty);
}
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
print(myMapValue.isEmpty);
}
isNotEmpty
属性はisEmpty
と逆の値となります。つまり辞書が空でなければtrue
となります。
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
print(myMapValue.isNotEmpty);
}
Map型のkeys属性とvalues属性
Map
型のkeys
属性は辞書の各キーを格納したIterable
の値となります。
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20, 'key3': 30};
print(myMapValue.keys);
}
似たような形でvalues
属性は辞書の値のIterable
の値となります。
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20, 'key3': 30};
print(myMapValue.values);
}
Map型のlength属性
Map
型のlength
属性は辞書の要素数(キーの数)となります。
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20, 'key3': 30};
print(myMapValue.length);
}
Map型のentries属性
Map
型のentries
属性は各キーと値のMapEntry
型の値を格納したIterable
となります。
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20, 'key3': 30};
print(myMapValue.entries);
}
Map型のaddAllメソッド
Map
型のaddAll
メソッドは引数に指定された別のMap
の各キーと値を対象の辞書に追加します(2つの辞書を統合したような結果になります)。
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
myMapValue.addAll({'key3': 30, 'key4': 40});
print(myMapValue);
}
キーが被っている場合には引数で指定した方の値で上書きされます。
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
myMapValue.addAll({'key2': 30, 'key3': 40});
print(myMapValue);
}
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);
}
こちらもaddAll
メソッドと同様にキーが被っている場合には引数で指定された方の値で上書きされます。
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
myMapValue.addEntries([
MapEntry('key2', 30),
MapEntry('key3', 40),
]);
print(myMapValue);
}
Map型のclearメソッド
Map
型のclear
メソッドは辞書の内容を空にします(1つもキーが無い状態にします)。
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
myMapValue.clear();
print(myMapValue);
}
Map型のcontainsKeyメソッド
Map
型のcontainsKey
メソッドは引数に指定したキー名を対象の辞書が持つかどうかの真偽値を返却します。キーがあればtrue
、無ければfalse
となります。
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
print(myMapValue.containsKey('key2'));
}
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
print(myMapValue.containsKey('key3'));
}
Map型のforEachメソッド
Map
型のforEach
メソッドは引数に関数を受け取り、その関数では各キーと値を引数に持ち各要素数分実行されます。実質的にfor文でループを回すような挙動をします。関数の第一引数がキー、第二引数が値となります。
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
myMapValue.forEach((key, value) {
print('キー: $key, 値: $value');
});
}
Map型のmapメソッド
Map
型のmap
メソッドは引数に関数を受け付け、各キーと値に対してなんらかの処理を行った新たなMap
の値を返却します。
関数の第一引数には辞書のキー、第二引数には辞書の値が設定されます。また、返却値にはMapEntry
の値が必要になります。
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);
}
Map型のputIfAbsentメソッド
Map
型のputIfAbsent
メソッドは第一引数で指定したキーがもし存在しなければ第二引数で指定した関数によって生成される値をそのキーに設定します。既に該当のキーが存在すれば処理をスキップします。
また、返却値にはもし対象のキーが元から存在すればそのキーの値が設定され、もし対象のキーが無ければ新たに設定された値が返却されます。
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
int putValue = myMapValue.putIfAbsent('key3', () => 30);
print(myMapValue);
print(putValue);
}
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
int putValue = myMapValue.putIfAbsent('key2', () => 30);
print(myMapValue);
print(putValue);
}
Map型のremoveメソッド
Map
型のremove
メソッドは引数で指定したキーを辞書から取り除きます。返却値は取り除かれた値となります。もし削除対象のキーが存在しない場合には返却値はnull
となります(エラーにはなりません)。
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
int? removedValue = myMapValue.remove('key2');
print(myMapValue);
print(removedValue);
}
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20};
int? removedValue = myMapValue.remove('key3');
print(myMapValue);
print(removedValue);
}
※余談ですがキーが存在する場合でも元から辞書の値でnull
を格納していた場合にはnull
が返却されたからといって必ずしもキーが存在しないという判定にはなりません。
Map型のremoveWhereメソッド
Map
型のremoveWhere
メソッドでは引数に判定用の関数を受け付け、その関数内で条件を満たすものを辞書から取り除きます。関数の第一引数には辞書キー、第二引数には辞書の値が設定されるため、キーか値(もしくは両方)を参照して判定を行うことができます。返却値には真偽値が必要となり、true
を返却した要素が辞書から取り除かれます。
条件を満たす要素が複数存在する場合にはそれらがまとめて取り除かれます。
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20, 'key3': 30};
myMapValue.removeWhere((key, value) => value >= 20);
print(myMapValue);
}
Map型のupdateメソッド
Map
型のupdate
メソッドは特定のキーの値を更新することができます。
普通にキーを指定して値を設定するのと何が違うのか?という所ですが、「現在設定されている値を参照して結果の値を設定する」「キーが存在しない場合には別の値を設定する」といった制御が可能です。
第一引数には対象のキーの文字列、第二引数には対象のキーが存在する場合にそのキーの値を参照して結果の値を返却する関数、第三引数には対象のキーが存在しない場合の値の設定用の関数(省略可)を指定します。第二引数の関数の引数には対象のキーの値が渡されます。また、第二引数と第三引数の関数両方とも設定したい値を返却する必要があります。
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20, 'key3': 30};
myMapValue.update('key2', (value) => value + 100, ifAbsent: () => -1);
print(myMapValue);
}
Map型のupdateAllメソッド
Map
型のupdateAll
メソッドはmap
メソッドのように関数の引数を使用して辞書の全ての要素に対して処理を反映します。map
メソッドのように引数で指定する関数の引数にはキーと値、返却値には更新後の値が必要になります。
map
メソッドと何が違うのか?というところですが、map
メソッドは新しい辞書を返却する一方でupdateAll
メソッドの方は元の辞書自体を更新します。
void main() {
Map<String, int> myMapValue = {'key1': 10, 'key2': 20, 'key3': 30};
myMapValue.updateAll((key, value) => value + 100);
print(myMapValue);
}
dynamic型
dynamic
型は特定の型に限られない任意の型を表します。型が明示できない場合は複数の型になりうる場合などに使用します。
柔軟性は得られるものの安全面やIDEなどでの補完やチェック等でマイナス面が多いため不要であれば使わずに型を明示した方が好ましいです。
void main() {
Map<dynamic, dynamic> myMapValue = {
'key1': 10,
350.5: true,
false: 0.005,
};
print(myMapValue);
}
型のキャストについて
Dartでの型のキャスト(型変換)は主にtoやasで始まる名前のメソッドを使います(例 : toString
やasMap
など)。
void main() {
double myDoubleValue = 20.5;
int myIntValue = myDoubleValue.toInt();
print(myIntValue);
}
as 型名
といったようにas
のキーワードを使う方法もあります。ただし多くのケースで変換が効かない印象なのと、ランタイムエラーにもなりやすいという面を加味して基本的には整数や文字列などの基本的な型変換用の各メソッドを使う形がメインになりそうな印象です(一方で型が不明になる際に特定の型でas
を使う・・・といったケースはたまにあるかなという印象です)。
void main() {
int myIntValue = 20.5 as int;
print(myIntValue);
}
また、他の言語で良くあるような対象の型のコンストラクタとしてキャストする・・・といったこともDartではできません。コンパイルエラーとなります。
void main() {
int myIntValue = int(20.5);
print(myIntValue);
}
void型
void
型は関数やメソッドの返却値の型の指定などで使用します。void
型を指定した場合返却値を返さないということを示します。関数に関してはまだ詳しくは触れていませんが、今までの節でもmain
関数などで記述してきた型となります。
void main() {
printMessage();
}
void printMessage() {
print('Hello, World!');
}
nullについて
Dartでの何も値を持っていないものを表すにはnull
を使います。有効な値を返却できない場合や初期化前などで様々なケースで使われます。
型で定義する際には型名の直後に?
の記号を付けます。例えばnull
の値を取りうる文字列であればString?
といったように書きます。
void main() {
String? myStringValue = null;
print(myStringValue);
myStringValue = 'Hello!';
print(myStringValue);
}
また、null
の型指定をした値に関しては初期値を与えなくても参照することができます(初期化をしなくても使えます)。その場合その値はnull
となります。jsのundefined
のような概念はありません。
void main() {
String? myStringValue;
print(myStringValue);
}
null
の型指定をしていない場合、初期値を与えていないと参照時にエラーになります。
void main() {
String myStringValue;
print(myStringValue);
}
Dartのnull安全について
Dartは3系のバージョン以降では基本的にnull安全になっています。うっかりミスなどでnull
になって欲しくないケースでnull
を参照してしまっていてランタイムエラーになる・・・といったことをしっかりと減らすことができます(コンパイルエラーとなるためうっかりnull
に絡んでランタイムエラーになる条件が残ったままリリースしてしまうといったことを減らすことができます)。
たとえば以下のように文字列の引数を必要にする関数でnull
を受け付ける文字列の変数を指定した場合にはコンパイルエラーとなります。
void main() {
String? myStringValue;
printMessage(message: myStringValue);
}
void printMessage({required String message}) {
print(message);
}
もうDart2系を使うケースもほとんど無くなって来ていると思うため、null安全の面で悩まされるケースも少ないのでは・・・という印象です。
nullを取りうる値に対する型ガード
null
を取りうる変数などに対してif文などでnull
かどうかを判定することで型ガードのように変数の型の絞り込みを行うことができます。
※if文などの条件分岐は後々の節で詳しく触れます。
void main() {
String? myStringValue = getMessage();
if (myStringValue != null) {
printMessage(message: myStringValue);
}
}
String? getMessage() {
return 'Hello!';
}
void printMessage({required String message}) {
print(message);
}
!記号による参照
if文などで確実にnull安全を担保できているとコンパイルエラーでミスを弾けて好ましいのですが、if文で判定するだけでは不便なことがあります。例えばif文などは挟んでいないものの、null
であれば確実にエラーで弾くようにしているためnull
にはならないことが分かっている・・・といったケースです。
そういった場合にはnull
を取りうる変数の後に!
記号を付けることでnull
を取らない変数と同じように扱うことができコンパイルエラーを回避することができます。
void main() {
String? myStringValue = getMessage();
printMessage(message: myStringValue!);
}
String? getMessage() {
return 'Hello!';
}
void printMessage({required String message}) {
print(message);
}
ただし、うっかりコード変更などで実はnull
を取りうる形になっていた・・・みたいなケースが発生するとnull
に起因してエラーが発生したりしてくるたと、安全のためにももしif文とかで対応が効くケースなら!
の記号よりもそちらの方が推奨されます。
?記号による安全な属性やメソッドなどへのアクセス
?
記号を使うことでnull
になりうる値で属性やメソッドにアクセスする場合、null
の場合にはそれらの属性などにはアクセスできないためランタイムエラーになってしまいます。
そういった場合にはその変数などの直後に?
の記号を付けることでランタイムエラーを回避することができます。例えばmyStringValue
というnull
を取りうる変数でmyStringValue.length
といった属性にアクセスすると変数がnull
だった場合にランタイムエラーになってしまいます。一方でmyStringValue?.length
といったように?
記号を付与しつつアクセスした場合には変数がnull
であればランタイムエラーにはならずにそのままnull
、null
でなければlength
属性の値が参照できる・・・といった挙動になります。
void main() {
String? myStringValue = getMessage();
print(myStringValue?.length);
}
String? getMessage() {
return 'Hello!';
}
void main() {
String? myStringValue = getMessage();
print(myStringValue?.length);
}
String? getMessage() {
return null;
}
??記号によるnull合体演算子
対象の変数などの値の後にスペースと??
の記号を記述すると、もしその値がnull
だった場合にその後に指定した値を代わりに参照する・・・といった制御ができます。
例えばmyStringValue ?? '値がnullです。'
といった記述を使った場合、myStringValue
がnullでなければmyStringValue
の値がそのまま参照され、もしnull
であれば'値がnullです。'
という文字列が参照されます。三項演算子のような挙動をします。
void main() {
String? myStringValue = getMessage();
print(myStringValue ?? '値がnullです。');
}
String? getMessage() {
return null;
}
void main() {
String? myStringValue = getMessage();
print(myStringValue ?? '値がnullです。');
}
String? getMessage() {
return 'Hello!';
}
??=記号によるnullの代替の値の設定
??=
記号を使うと対象の変数がもしnull
であれば代替の値を設定するという挙動をします。たとえばmyStringValue ??= '値がnullです';
という記述であれば、もしmyStringValue
という変数がnull
であれば右辺の'値がnullです'
という値が設定されます。値がnull
でなければ元の値がそのまま維持されます。
void main() {
String? myStringValue = getMessage();
myStringValue ??= '値がnullです';
print(myStringValue);
}
String? getMessage() {
return null;
}
void main() {
String? myStringValue = getMessage();
myStringValue ??= '値がnullです';
print(myStringValue);
}
String? getMessage() {
return 'Hello';
}
メソッドチェーン風の記述
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();
}
一方で、メソッドなどでインスタンスを返却しない場合でも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();
}
なお、フォーマッタをかげると..
記号で改行されるようです。
関数について
以降の節では関数について詳しく触れていきます。
関数定義の基本
関数は返却値の型 関数名(引数内容) { 関数の処理内容 }
といったように定義します。引数定義の箇所は変数定義などと同じように型名 引数名
といった順番で書きます。また、複数の引数を受け付ける場合にはコンマ区切りで定義します。
例えば返却値の型がvoid
、関数名がprintMessage
、引数にはString
型でmessage
という引数を受け付ける場合にはvoid printMessage(String message) { ... }
といった書き方になります。
void main() {
printMessage('Hello!');
}
void printMessage(String message) {
print(message);
}
関数の呼び出し
関数を呼び出したい場合には対象の関数名(指定する引数内容)
といった形で()
の括弧を使います。また、返却値(後の節で触れます)を別の変数などに設定したければ=
の記号を使って左側に対象の変数などを指定します。
void main() {
int addedValue = addTwoValues(10, 20);
print(addedValue);
}
int addTwoValues(int a, int b) {
return a + b;
}
位置引数の定義と指定
関数を呼び出す際に単純に順番通りに引数の値を指定する際には引数の定義も前節の通りシンプルにコンマ区切りで定義すれば扱うことができます(位置引数と言います)。関数を呼び出す時もコンマ区切りで順番に値を設定すれば対応ができます。
void main() {
printMessage('Hello', ' World!');
}
void printMessage(String message1, String message2) {
print('Message1: $message1\nMessage2: $message2');
}
一方で引数名と共に引数の値を指定する方法もあり、こちらはDartだと名前付き引数などと呼ばれるようです(他の言語だとキーワード引数などと呼ばれたりもします)。名前付き引数については次の節で触れていきます。
名前付き引数の定義と指定
名前付き引数を使うと関数を呼び出す際に引数の値と共に引数名をセットで指定することができます。
名前付き引数を使うと何が嬉しいのか?というところですが、例えば関数呼び出し箇所で引数名も一緒に記述されるので何の引数なのかが分かりやすいといった面や、引数の順番を変えたりした際に影響を少なくする・・・といった形で可読性を高めたり堅牢なコードを書きたい時に役立つことがあります。
この辺りはDartではなくPythonで以前記事にもしているので必要でしたらご参照ください。
名前付き引数を使った関数を定義したい場合には引数の箇所で{引数内容}
といったように{}
の括弧で囲む必要があります。また、省略可能な引数以外では引数の前にrequired
と付ける必要があります(省略可能な引数に関しては後々の節で触れます)。
名前付き引数を使う形で関数を呼び出す場合には引数名: 指定する値
といった形でコロンで名前と値を区切って指定します。
void main() {
printMessage(message1: 'Hello', message2: ' World!');
}
void printMessage({required String message1, required String message2}) {
print('Message1: $message1\nMessage2: $message2');
}
名前付き引数のデフォルト値
名前付き引数を省略可の状態(required
を付けない形)にするには対象の引数でnull
も設定可能にするか、もしくは=
の記号を使ってデフォルト値を引数に設定しておく必要があります。
void main() {
printMessage();
}
void printMessage({String? message}) {
print('Message: $message');
}
デフォルト値を設定する場合には引数の型 引数名 = デフォルト値
といった形で引数部分で記述します。
void main() {
printMessage();
}
void printMessage({String message = 'Hello!'}) {
print('Message: $message');
}
返却値の設定
関数に返却値を設定したい場合には関数の処理内でreturn 返却値に設定する値
といった形でreturn
キーワードを使います。
また、return
部分で指定した値の型に合わせて関数名の左に記述する型を設定します。
void main() {
int addedValue = addTwoValues(100, 200);
print(addedValue);
}
int addTwoValues(int a, int b) {
return a + b;
}
関数の型設定
関数を別の関数の引数に設定することはDart/Flutterでは結構あります(イベントのハンドラやコールバックなどで)。そういった場合には引数で型の定義が必要になることがありますが、関数の型の定義を含めた引数は位置引数の場合には返却値の型 Function(引数内容) 引数名
といった形で書きます。例えば返却値の型はvoid
、整数のprice
という引数と文字列のmessage
という2つの位置引数を持ち、引数名がotherFunction
という名前の関数の引数を定義したい場合にはvoid Function(int price, String message) 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');
}
無名関数について
Dartで無名関数(関数名が設定されていない関数)を定義するには{}
の括弧を使う方法と=>
の記号を使う場合の2パターンが存在します。
通常の関数と{}
を使う無名関数の場合と=>
を使う無名関数の場合の句使い分けですが、人によって意見は様々だと思いますが目安程度に個人的には以下のような感じで考えています。
-
=>
の記号を使った無名関数: 1つのステートメントもしくは単一行のシンプルな処理の関数 -
{}
の括弧を使った無名関数: 1行~数行程度のシンプルな処理の関数 - 通常の関数: 上記に該当しない処理が多めな関数など
=>を使った無名関数の定義方法
=>
記号を使って無名関数を定義する場合には(引数内容) => 単一のステートメントなど
といった形で書きます。=>
よりも右側の値は返却値としても扱われます。返却値の設定のためにreturn
などのキーワードは不要です。
例えば整数のa
とb
という2つの引数を受け付け、a
とb
を加算した値を返却する無名関数を定義したい場合には(int a, int b) => a + b
といった記述になります。
定義した無名関数は変数として扱ったり、もしくは引数に指定したりして扱うことができます。また、関数の呼び出しも通常の関数のように行うことができます。
void main() {
final addTwoValuesFunc = (int a, int b) => a + b;
int addedValue = addTwoValuesFunc(10, 20);
print(addedValue);
}
{}の括弧を使った無名関数の定義方法
{}
の括弧を使って無名関数を定義する場合には(引数内容) { 関数内容 }
といった形で書きます。こちらでは返却値を設定するにはreturn
キーワードが必要になります。
例えば整数のa
とb
という2つの引数を受け付け、a
とb
を加算した値を返却する無名関数を定義したい場合には以下のような書き方になります。
void main() {
final addTwoValuesFunc = (int a, int b) {
return a + b;
};
int addedValue = addTwoValuesFunc(10, 20);
print(addedValue);
}
無名関数での名前付き引数の利用
無名関数でも名前付き引数を使うことができます。書き方も通常の関数の時と同様で、引数定義の箇所で{}
の括弧や必要に応じてrequired
の記述などを使います。また、呼び出す際も同様で引数名: 指定する値
といった形で引数を指定して使います。
void main() {
final addTwoValuesFunc = ({required int a, required int b}) {
return a + b;
};
int addedValue = addTwoValuesFunc(a: 10, b: 20);
print(addedValue);
}
無名関数の引数に型の指定を行うかどうか
通常の関数もそうですが、無名関数では引数の型の記述を省略することができます。
void main() {
final addTwoValuesFunc = (a, b) {
return a + b;
};
int addedValue = addTwoValuesFunc(10, 20);
print(addedValue);
}
ただしこの場合引数に任意の型の値を受け付けてしまうため、コンパイルエラーにならずに予期せぬランタイムエラーが発生することが起こり得ます。
void main() {
final addTwoValuesFunc = (a, b) {
return a + b;
};
int addedValue = addTwoValuesFunc(10, 'Hello');
print(addedValue);
}
そのため前述のような無名関数を定義する場合には引数の型を明示しておいた方が無難だと思います。
一方で無名関数はイベントのハンドラやコールバックとして引数に指定することも多くあります。これらの用途の場合には呼び出す関数側で引数の関数の型が明示されていれば型の不一致などによるランタイムエラーは防げてビルド時点で事前にミスなどを検知することができます。
void main() {
myFunction(otherFunction: (message) => print(message));
}
void myFunction({required void Function(String message) otherFunction}) {
otherFunction('Hello!');
}
この場合には無名関数側は型を省略しても安全面で差は少ないですし記述がシンプルになるので積極的に使っても良いと思われる書き方と言えます(Effective Dartなどの資料でもこの辺りに触れられていたような気がします)。
関数でのジェネリックな型の利用
関数でジェネリックの型を使う際には返却値の型 関数名<ジェネリックの型名>(引数内容) { ... }
といった形で書きます。例えばT
というジェネリックの型名をmyFunction
という関数で使う場合にはvoid myFunction<T>() { ... }
といったように書きます。
定義したジェネリックの型は返却値や引数の型に設定することができます。
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;
}
以下のように呼び出し側でジェネリックの型に指定した型以外が返却値や引数に使われていたらコンパイルエラーになることが確認できます。
void main() {
String returnedValue = myFunction<int>('Hello!');
print(returnedValue);
}
T myFunction<T>(T value) {
return value;
}
クラスについて
以降の各節で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});
}
また、定義したクラスは型として他の型と同様にクラス名 変数など
といった形で指定することができます(例 : MyClass instance = MyClass()
)。
void main() {
MyClass instance = MyClass(id: 10);
print(instance.id);
}
class MyClass {
final int id;
MyClass({required this.id});
}
属性の定義方法と参照
属性を定義するにはクラス内のスコープ(クラス名の後の{}
の括弧内)で変数を宣言するのと似たような形で定義できます。例えば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();
}
コンストラクタの引数でthis.引数名
とすることでコンストラクタに渡された引数の値を直接属性に設定することもできます(例 : this.id
)。この辺りは後々の節で詳しく触れます。
void main() {
MyClass instance = MyClass(20);
print(instance.id);
}
class MyClass {
int id;
MyClass(this.id);
}
また、属性定義時に初期値を与えている場合やコンストラクタで属性に値を設定している場合には属性にfinal
の設定を付与することができます(後の節で触れるstatic
を使うケースを除いてクラスの属性にconst
の属性は使用できません)。
void main() {
MyClass instance = MyClass(20);
print(instance.id);
}
class MyClass {
final int id;
MyClass(this.id);
}
メソッドの書き方と呼び出し方
クラスのスコープ内で関数を定義する形でメソッド(クラスのインスタンスが持つ関数)を定義することができます。
そのメソッドを呼び出したい場合にはインスタンス名.メソッド名(引数内容) { メソッド内容 }
といった形でドットで繋ぐ形で関数呼び出しの時と同じように書きます(例 : instance.incrementId();
)。
void main() {
MyClass instance = MyClass(20);
instance.incrementId();
print(instance.id);
}
class MyClass {
int id;
MyClass(this.id);
void incrementId() {
id++;
}
}
また、クラスの属性や他のメソッドに関してはメソッド内ではそのまま参照します。他の言語であるようなthis
やself
などを経由しません(前記のコードでもthis.id
といった記述ではなく直接id
属性をメソッド内で参照しています)。
ただし名前が被る際にはこのインスタンスの属性やメソッドということを明示するためにthis
を付けて属性やメソッドにアクセスすることもできます(例 : this.id++;
)。
その他、名前付き引数など関数側で説明してきた機能に関してはクラスのメソッドでも同様に使用することができます。
staticの属性とメソッドの定義
クラスの属性やメソッドはstatic
キーワードを付与して定義することもできます。例えば属性であればstatic int id = 0;
、メソッドであればstatic void incrementId() { ... }
みたいな形で定義します。
static
のキーワードが付与されたものはその属性へのアクセスやメソッドの呼び出しはインスタンス化した後のインスタンス経由ではなくクラス自体を参照して利用する形が主になります。
例えばMyClass
というクラスに定義されているid
というstatic
の属性を参照する場合にはMyClass.id
といったように書きます。
void main() {
MyClass.incrementId();
MyClass.incrementId();
print(MyClass.id);
}
class MyClass {
static int id = 0;
static void incrementId() {
MyClass.id++;
}
}
また、通常はクラスの定数の属性にはfinal
しか設定できませんがstatic
キーワードを付与した場合にはconst
も利用可能になります。
void main() {
print(Language.dart);
}
class Language {
static const String dart = 'Dart';
static const String python = 'Python';
static const String rust = 'rust';
}
コンストラクタの引数で直接属性に値を設定する書き方
前節までで軽く触れてきましたがコンストラクタの引数を直接属性に設定したい場合には引数定義で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});
}
クラスをconstキーワードと共に初期化する
属性が全てfinal
で定義されている場合・・・といった制約がありますが、条件を満たした場合コンストラクタのクラス名の前にconst
キーワードを設定することができます(例 : const MyClass();
)。
また、コンストラクタの定義で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});
}
コンストラクタの引数で直接親クラスに値を渡す書き方
クラスの親子関係を作るための継承については後々の節で触れますが、親クラスのコンストラクタの引数に直接引数を渡したい場合にはsuper.親の引数名
といった形で書きます(例 : super.id
)。
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});
}
コンストラクタで直接親クラスの引数に引数を一部渡しつつ、別途固定値などを親の引数に渡す書き方
親のクラスのコンストラクタにsuper
で直接引数を渡しつつ、一部の値や別の固定値などを子のクラス側で指定する・・・みたいな書き方をすることもできます。
その場合にはコンストラクタの引数の定義の後に : super(固定値などを指定する残りの親クラスへの引数設定);
といった具合にコロンやメソッドとしてのsuper
を記述して書きます(例 : ChildClass({required super.id, required super.name}) : super(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: '日本語');
}
クラスでのジェネリックな型の利用
クラスでジェネリックの型を使いたい場合にはクラス名の後に<ジェネリックの型名>
といった形で設定します(例 : 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});
}
複数のジェネリックの型を使いたい場合にはコンマ区切りで型を設定します。
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});
}
getterメソッドの定義
Dartのクラスでgetterメソッド(属性のように値の取得で利用できるメソッド)は対象のメソッド名の前にget
キーワードを記述することで対応することができます。また、引数は使わないので引数の括弧なども使いません(例 : int get value { ... }
)。返却値の型が実質的に属性の型のように動作します。
getterを参照する場合には括弧などは使わずに普通の属性のように参照します。
void main() {
final instance = MyClass();
print(instance.value);
}
class MyClass {
int _value = 50;
int get value {
return _value;
}
MyClass();
}
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();
}
なお、他の言語と同様にsetterを設けずにgetterのみを設けて値の取得のみを許可して外部からの更新をできなくする・・・といった制御も行うことができます。
継承について
親となるクラスを継承して子クラスを作るにはclass クラス名 extends 親クラス名 { ... }
といった形でextends
キーワードを使って書きます。
継承に関しては使い方が微妙だと苦しくなるケースもあるとは思うため気軽に複雑だったり巨大な継承などは控えめになると良いとかはあるかもしれませんが、継承することで親クラスの属性定義やメソッド定義を使いまわすことができるようになります(Flutterでは継承を多用する形となります)。
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: '猫');
}
継承したメソッドの上書き
継承した親クラスのメソッドなどを上書きしたい場合にはメソッドの上に@override
と付けて、子のクラス側で同名・同じ型で再度メソッドを定義します。
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;
}
}
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);
}
}
privateで定義したものは対象のファイル内で参照されていなければ警告が出るようになります(Lintとかでチェックされるようにしてあれば使用していないものを切り落としたりなどがしやすくなります)。
late _MyClass _instance;
void main() {
_instance = _MyClass();
}
class _MyClass {
final int _id = 10;
final String _name = 'John';
_MyClass();
void _printIdAndName() {
print(_id);
print(_name);
}
}
public(他のファイルから参照できる定義設定)で定義したい場合には前節までのようにアンダースコアを先頭に付けずに各名前を設定します。
ミックスインについて
通常の継承とは別に、Dartではミックスインをクラスに設定することができます。ミックスインは通常の継承のように特定のクラスに属性やメソッドなどの機能を付与することができます(Rustのトレイトのような機能になります)。
通常の継承と比べて以下のような違いがあります。
- 通常の継承のようにミックスイン自体が他のミックスインやクラスなどを継承することはできません(何階層も継承を重ねるといったことはできません)。
- ただし特定のクラスの継承やミックスイン設定が無いと対象のミックスインを利用できない制約を付与することはできます。
- 複数のミックスインを特定のクラスに同時に設定することができます。
- 通常の継承は1度に1つの継承しか行うことはできません。
また、人によって意見は様々だとは思いますがミックスインは継承よりも複雑で変更が効きづらい形にはなりにくく、パーツのように付け外しなどを行うことで機能の追加や削除などの調整が効きやすいので個人的には通常の継承よりも好みではあります(Flutterを扱う上では継承は多用する形にはなりますが・・・)。
ミックスインの設定の仕方
ミックスインを定義するにはクラスと似たような感じでmixin ミックスイン名 { ... }
といった形でmixin
キーワードを使用して定義します。
mixin WalkMixin {
int xPosition = 0;
void walk() {
xPosition += 1;
}
}
定義したミックスインを特定のクラスで使うためにはクラス名の後などにwith ミックスイン名
といった形でwith
キーワードを使います(通常の継承をしている場合にはextends
での継承の記述の後などにwith
の設定を追加していきます)。
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});
}
複数のミックスインを設定する
複数のミックスインを特定のクラスに設定したい場合にはwith
の後の指定でコンマ区切りで複数のミックスインを設定することができます。
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});
}
他の特定のクラスやミックスインを継承していないと使えないミックスインにする
ミックスインを特定のクラスや他のミックスインが設定されていないと使えないようにする・・・といった設定も行うことができます。
これによって、ミックスイン自体は他のクラスやミックスインを継承したりはしていないものの、それらの属性やメソッドをミックスイン内で参照できるようになります(コンパイルが通るようになります)。
そのような設定をしたい場合にはミックスイン名の後に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});
}
インターフェイスについて
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;
}
}
クラス・ミックスイン・インターフェイスを継承しているかの判定と型のキャスト
インスタンスが特定のクラス・ミックスイン・インターフェイスを継承しているかどうかを判定するにはis
キーワードを使います。例えばinstance is Animal
みたいな書き方をします。結果は真偽値になるためif
文などの条件分岐で使えます。
また、is
を使った条件分岐をした場合には型ガードが有効になる場合があり、条件を満たしたスコープ内ではそのクラスなどの属性やメソッドなどにアクセスすることができます。任意の型となるdynamic
型を使う場合などに型を絞り込んで安全に処理を行うことができます(参照した属性やメソッドが存在せずにエラーになるといったことを避けられます)。
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で判定していない、且つ定義されていないメソッドや属性などを参照している場合にはランタイムエラーになります。
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;
}
}
もしis
を使ったif文での条件分岐の型ガードがされていればそのクラスなどで定義されていないメソッドなどを参照していればコンパイルエラーとなるためデプロイ前などに気づけて安全です。
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;
}
}
この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;
}
}
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();
}
enumをswitchでの条件分岐で使った場合の挙動
switch
文に関しては後々の節で詳しく触れますが、Dartでenumに対してswitch文で分岐を書く場合、defaultケースを書いていない場合であれば全enumに対して分岐を書かない場合コンパイルエラーになってくれます。
これを利用することでenumの各値に対する分岐を確実に記述するように制約を設けることができます。途中で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 '防具';
}
}
比較演算子について
条件分岐について触れる前に、必要となる比較演算子について以降の節で触れていきます。
比較演算子の結果は真偽値(true
もしくはfalse
)となります。条件分岐でも真偽値が必要になるため、比較演算子は条件分岐と共によく使われます。
void main() {
int age = getAge();
bool result = age == 17;
print(result);
}
int getAge() {
return 17;
}
値が一致しているかどうかの演算子
値が一致しているかどうかの比較演算子には==
の記号を使い、その左右に比較したい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;
}
未満の演算子
値が未満かどうかの比較演算子には<
の記号を使い、その左右に比較したい2つの値を記述します(例 : age < 17
)。
以下の演算子
値が以下かどうかの比較演算子には<=
の記号を使い、その左右に比較したい2つの値を記述します(例 : age <= 17
)。
超過の演算子
値が大きいかどうか(超過)の比較演算子には>
の記号を使い、その左右に比較したい2つの値を記述します(例 : age > 17
)。
以上の演算子
値が以上かどうかの比較演算子には>=
の記号を使い、その左右に比較したい2つの値を記述します(例 : age >= 17
)。
AND条件
複数の条件を満たすかどうか(AND条件)を比較演算子での比較で扱う場合には、それぞれの比較の記述の間を&&
で繋げます。例えば年齢と性別の2つの変数それぞれに対して比較演算子を使ってAND条件で比較したい場合にはage <= 20 && gender == Gender.male
といった形で&&
で繋げます。
enum Gender {
male,
female,
}
void main() {
final age = 17;
final gender = Gender.male;
if (age <= 20 && gender == Gender.male) {
print('年齢は20歳以下で且つ性別は男性です。');
}
}
なお、3つ以上の条件が必要な場合には追加でさらに&&
で条件を繋げていくことができます。
OR条件
複数の条件でいずれかの1つを満たすかどうか(OR条件条件)を比較演算子での比較で扱う場合には、それぞれの比較の記述の間を||
で繋げます。例えば年齢が15歳もしくは17歳のどちらかを満たすかどうかという条件であればage == 15 || age == 17
といったように書きます。
void main() {
final age = 17;
if (age == 15 || age == 17) {
print('年齢は15歳もしくは17歳です。');
}
}
条件分岐について
前節まででも少し使ってきましたが、以降の節ではDartでの条件分岐について触れていきます。
if文
まずは一番基本的なif
文からです。
if
文では条件を満たした場合に特定の処理を実行する場合に利用します。if (真偽値) { 条件を満たした場合に実行する処理 }
といった具合に書きます。
真偽値の部分はそのまま真偽値を指定する場合や、比較演算子を使った条件式を記述することもあります。
void main() {
final age = 17;
if (age == 17) {
print('年齢は17歳です。');
}
}
else if文
if
文で条件を書いた後に、その条件には当てはまらない場合に別の条件を満たすかどうかを判定したい場合にはelse if
文を使います。if (真偽値) { ... }
の括弧の直後に記述していく必要があります。
else if (真偽値) { 条件を満たした場合に実行する処理 }
といった形で書きます。
void main() {
final age = 15;
if (age == 17) {
print('年齢は17歳です。');
} else if (age == 15) {
print('年齢は15歳です。');
}
}
なお、else if
文は複数設定することができます。
void main() {
final age = 13;
if (age == 17) {
print('年齢は17歳です。');
} else if (age == 15) {
print('年齢は15歳です。');
} else if (age == 13) {
print('年齢は13歳です。');
}
}
else文
if
文で条件を書いた後もしくはelse if
文の条件を書いた後に、それらの全ての条件にも該当しない場合の条件を一括して扱う場合にはelse
文を使います。
書き方はelse { 条件を満たした場合に実行する処理 }
といった形になります。直前の各条件を一通り満たさない場合を全て対象とするため、判定用の条件の真偽値は必要ありません。
void main() {
final age = 13;
if (age == 17) {
print('年齢は17歳です。');
} else if (age == 15) {
print('年齢は15歳です。');
} else {
print('年齢は17歳でも15歳でもありません。');
}
}
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歳です。');
}
}
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歳でもありません。');
}
}
switch文で複数の値をケースの条件に設定する
switch
文の特定のケースで複数の値の条件を満たしている場合の処理を設定したい場合には||
記号によるOR条件を使うことで対応することができます。
void main() {
final age = 17;
switch (age) {
case 15 || 17:
print('年齢は15歳もしくは17歳です。');
}
}
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歳です。');
}
}
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 '装飾';
}
}
ループについて
以降の節ではリスト(もしくは他のIterable
)や辞書に対するループ処理(繰り返し処理)について触れていきます。
リストなどに対してインデックスを設定するループを行う
リスト(もしくは他のIterable
)のインデックスを参照する形でループを回すには色々と書き方はありますが書き方の一つとしてfor (var i = 0; i < リストなど.length; i++) { ループでの処理 }
といった形で書きます(i
は別の名前でも動きます)。
var i = 0;
という部分はインデックスを扱うための変数i
を0
で初期化するといった処理になります。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);
}
}
インデックスを使用してループ中にリストの各要素を参照したい場合には対象のリスト[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);
}
}
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);
}
}
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
文ではなくMap
のforEach
メソッドを使う方法です。メソッドの第一引数に渡される関数の引数にはキーと値の2つの引数が渡されるのとキーの数だけその関数が呼び出されるためfor
文でループを回した時と同じような形でキーと値を参照して処理を行うことができます。
void main() {
final agesMap = {'John': 17, 'Mike': 15, 'Olivia': 13};
agesMap.forEach((key, value) {
print('key: $key, value: $value');
});
}
2つ目はMap
のentries
属性をfor
文で使う方法です。この場合ループ中の変数はMapEntry
型となり、そちらのkey
とvalue
属性でキーと値を参照することができます。
void main() {
final agesMap = {'John': 17, 'Mike': 15, 'Olivia': 13};
for (final mapEntry in agesMap.entries) {
print('key: ${mapEntry.key}, value: ${mapEntry.value}');
}
}
その他、キーに対してループを行ってループの中でそのキーを使用して対象の値を参照するという形もシンプルで良いと思います。
コメントについて
Dartではいくつかコメントの書き方があるので以降の節ではそれぞれを触れていきます。
通常のインラインコメント
通常のインラインコメントは//
の記号を使います。対象行でこの記号の後の記述はプログラムとして実行されません。主に何らかの処理の説明の補足などに使います。また、基本的にスラッシュの後に半角スペースを1つ入れて使います。
void main() {
// いろは歌
print('いろはにほへとちりぬるを');
}
ドキュメンテーションコメント用のインラインコメント
///
といった形でスラッシュを3つ重ねるとドキュメンテーションコメントとなります(jsDocやPythonのdocstringのような扱いとなります)。
ドキュメンテーションコメントは主にVS Codeなどのエディタで使ったりドキュメントを出力したりする際に参照されます。
また、ドキュメンテーションコメントはファイル全体(ライブラリファイル対象)、変数、定数、クラス、関数、メソッドなど様々な箇所に設定することができます。
例えば関数に設定すると以下のような雰囲気になります。
/// メッセージを出力する
///
/// [message]には出力するメッセージを指定する。
void printMessage({required String message}) {
print(message);
}
VS Code上で対象の関数にマウスオーバーしてみると以下のようにドキュメンテーションコメントの内容が表示されることが確認できます。
ブロックコメント
Dartで/* */
で囲むとブロックコメント(複数行のコメントアウト)となります。一括で処理を無効化できます。主にDartでは一時的に特定部分の処理を止めたりに使います。一方でドキュメントや補足として残すためのコメントは前節までの各インラインコメントを使用します。
void main() {
/*print('いろはにほへと');
print('ちりぬるを');*/
}
例外について
Dartで例外(ランタイムエラー)を投げたい場合にはthrow エラークラス名(引数内容);
といった形でthrow
キーワードを使います(例 : throw Exception('エラーメッセージ')
)。
※DartPadではランタイムエラーの詳細なエラーメッセージが表示されなかったりしますが、通常のビルド時などにはエラークラスに指定したメッセージなどが確認できるようになっています(VS Code上からビルドした場合など)。
void main() {
throw Exception('エラーが発生しました。');
}
※実際にthrow
によるエラー設定を行う場合はより具体的なビルトインのエラークラスを使うか、もしくは継承などして独自に設けたクラスを使うなどが推奨されます。
参考サイト・参考文献まとめ