はじめに
Flutterを網羅的に学習するにあたってRoadmapを使って学習を進めることにしました。
この記事では、Flutter初学者やこれからFlutterを学習し始める方に向けて、Dartについてまとめています。
RoadmapはFlutterだけでなく、他の言語やスキルのロードマップも提供されており、何から学習して良いか分からないと悩んでいる方にとって有用なサイトになっています。
ぜひRoadmapを利用して学習してみてください。
Roadmapとは
簡潔に言えば、Roadmap.shは学習者にとってのガイドブックであり、学習の方向性を提供する学習ロードマップサイトです。
初心者から上級者まで、ステップバイステップでスキルを習得するための情報が提供されています。
学習の進め方が分かりやすく示されているだけでなく、個々の項目に参考資料やリソースへのリンクも提供されているので、学習者は目標を設定し、自分自身のペースで学習を進めることができます。
Advanced Dart
FlutterロードマップAdvanced Dartでは以下の15のサイトが紹介されています。興味のある方はぜひお読みください。
- Core libraries:https://dart.dev/guides/libraries
- Libraries: https://api.flutter.dev/
- List Class: https://api.flutter.dev/flutter/dart-core/List-class.html
- Dart Programming – List: https://www.geeksforgeeks.org/dart-programming-list/
- Generic collections in Flutter: https://dart.dev/language/generics#generic-collections-and-the-types-they-contain
- Iterable collections: https://dart.dev/codelabs/iterables
- Lambda functions in Dart: https://medium.com/jay-tillu/lambda-functions-in-dart-7db8b759f07a
- Anonymous Function in Dart | Lambda Function: https://www.youtube.com/watch?v=XTKKQdTAR0U
- Brief Overview of Functional Programming: https://buildflutter.com/functional-programming-with-flutter/
- Functional Programming in Dart & Flutter: https://yogi-6.medium.com/list/functional-programming-in-dart-flutter-2f3ac9d7fa39
- Functional programming - Flutter: https://docs.flutter.dev/resources/faq
- How isolates work: https://dart.dev/language/concurrency#how-isolates-work
- Dart - Isolates and event loops: https://medium.com/dartlang/dart-asynchronous-programming-isolates-and-event-loops-bffc3e296a6a
- Asynchronous programming: async, await: https://dart.dev/codelabs/async-await
- Async widgets: https://docs.flutter.dev/ui/widgets/async
Core Library
Core Libraryは、Dart に標準で組み込まれている基本的なライブラリです。これらのライブラリは、基本的なデータ構造、データ型、例外処理、数学関数、非同期機能など様々な機能を含んでいます。
Core Libraryには、たくさんのライブラリが含まれていますが、ここでは以下の3つのライブラリについて説明します。
-
dart:core: 基本的なデータ型、例外処理、コレクションなどの基本機能を使用できるようになります。
-
dart:async: 非同期プログラミングのための
Future
やStream
などが含まれています。 -
dart:math: 数学関数やランダム数の生成など数学的な機能を提供します。
dart:core
import 'dart:core';
void main() {
// 基本的なデータ型
int x = 5;
double y = 3.14;
String message = 'Hello, Dart!';
// リスト
List<int> numbers = [1, 2, 3, 4, 5];
// 例外処理
try {
int result = x ~/ 0; // 例外が発生する
print(result);
} catch (error) {
print('Error: $error');
}
}
dart:async
import 'dart:async';
Future<void> fetchData() async {
await Future.delayed(Duration(seconds: 2));
print('Data fetched!');
}
void main() {
print('Start of main');
fetchData().then((_) {
print('End of main');
});
}
dart:math
import 'dart:math';
void main() {
// 最小値と最大値の取得
int minNumber = min(5, 10);
int maxNumber = max(5, 10);
print('Min: $minNumber, Max: $maxNumber');
// 乱数の生成
Random random = Random();
int randomValue = random.nextInt(100);
print('Random Value: $randomValue');
}
List
List は Dart のコアライブラリで提供されるデータ構造の一つで、順序があり、要素の重複が許されるリストを表します。List はウィジェットのリストやデータのリストなど、さまざまな用途で利用されます。
void main() {
// Listの作成
List<int> numbers = [1, 2, 3, 4, 5];
// 要素へのアクセスと表示
print('Numbers: $numbers'); // Numbers: [1, 2, 3, 4, 5]
print('First element: ${numbers[0]}'); // First element: 1
print('Length: ${numbers.length}'); // Length: 5
// リストの変更
numbers.add(6); // 要素の追加
numbers.addAll([7, 8, 9]); // 複数の要素を追加
numbers.remove(3); // 指定した要素の削除
print('Modified Numbers: $numbers'); // Modified Numbers: [1, 2, 4, 5, 6, 7, 8, 9]
// リストの反復処理
for (int number in numbers) {
print('Iterating: $number'); // Iterating: 1 Iterating: 2 Iterating: 4 Iterating: 5 Iterating: 6 Iterating: 7 Iterating: 8 Iterating: 9
}
// 条件に基づいたフィルタリング
List<int> evenNumbers = numbers.where((number) => number.isEven).toList();
print('Even Numbers: $evenNumbers'); // Even Numbers: [2, 4, 6, 8]
}
-
整数型のリスト
numbers
を作成し、初期値として1から5までの整数を設定しています。 -
リストに要素を追加したり、要素をまとめて追加したり、指定した要素を削除しています。
-
for-in
文を使用して、リスト内の各要素を反復処理しています。 -
where
メソッドを使用して、リスト内の偶数だけを抽出しています。
GridView
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyGridView(),
);
}
}
class MyGridView extends StatelessWidget {
final List<int> numbers = List.generate(100, (index) => index + 1);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GridView Example'),
),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // グリッドの列数
crossAxisSpacing: 8.0, // 列間のスペース
mainAxisSpacing: 8.0, // 行間のスペース
),
itemCount: numbers.length,
itemBuilder: (context, index) {
return GridTile(
child: Container(
color: Colors.tealAccent,
child: Center(
child: Text(
'${numbers[index]}',
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
),
),
),
);
},
),
);
}
}
-
numbers
フィールドは、1から100までの整数を生成してリストに格納しています。 -
GridView.builder
ウィジェットでSliverGridDelegateWithFixedCrossAxisCount
を使用してグリッドを構築しています。このデリゲートは、固定された列数を持つグリッドを作成します。 -
itemCount
はnumbers
リストの要素数です。 -
itemBuilder
は各グリッドアイテムを構築するコールバック関数で、GridTile
ウィジェット内で表示するコンテナを設定しています。
Collection
Flutterにおけるコレクションは、データを格納して操作するためのデータ構造のことです。コレクションクラスには、たくさんのクラスが含まれていますが、ここでは Set
と Map
のクラス、StackとQueueについて説明します。
Set
Setは、順序がなく、重複を許さない要素の集まりを表すデータ構造です。一意な要素を管理するために使用されます。DartのSetはSetクラスを使用して表現されます。以下はSetクラスの特徴です。
-
順序がない: 要素の順序を保持しないため、要素が挿入された順序を記憶していません。
-
重複を許さない: 同じ要素は一度しか含まれません。そのため、重複する要素を追加しても無視されます。
サンプルコード
void main() {
// Setの作成
Set<String> fruits = {'apple', 'banana', 'orange'};
print('Original Set: $fruits'); // Original Set: {apple, banana, orange}
// 要素の追加
fruits.add('grape');
fruits.add('banana'); // 重複しているため無視される
print('Modified Set: $fruits'); // Modified Set: {apple, banana, orange, grape}
// 要素の削除
fruits.remove('apple');
print('Set after removing "apple": $fruits'); // Set after removing "apple": {banana, orange, grape}
// Setの反復処理
for (String fruit in fruits) {
print('Fruit: $fruit'); // Fruit: banana Fruit: orange Fruit: grape
}
}
-
Setの作成:
Set<String> fruits = {'apple', 'banana', 'orange'};
で、文字列型のSetを作成しています。 -
要素の追加:
fruits.add('grape');
で新しい要素 "grape" を追加しています。また、fruits.add('banana');
では既に存在する "banana" は重複と見なされ、無視されます。 -
要素の削除:
fruits.remove('apple');
で要素 "apple" を削除しています。 -
Setの反復処理:
for (String fruit in fruits)
でSetの中の要素を反復処理しています。Setは順序がないため、要素の順序は保証されません。
Map
Mapは、キーと値のペアを関連付けるデータ構造です。キーは一意であり、それぞれに対応する値を取得することができます。DartのMapはMapクラスを使用して表現されます。以下はMapクラスの特徴です。
-
キー(key): マップ内の要素を一意に識別するための識別子です。キーは重複せず、一つのキーには対応する値があります。
-
値(value): キーに関連付けられたデータやオブジェクトのことです。キーと値の組み合わせが1つのマップを構成します。
-
要素の追加と取得: マップには新しいキーと値のペアを追加することができます。また、キーを指定して、対応する値を取得できます。
-
イテレーション(反復処理): マップ内のすべてのキーと値に対して反復処理ができます。これにより、すべてのマップ内のデータにアクセスして処理を行うことができます。
-
要素の削除: 特定のキーに対応する要素を削除することができます。
サンプルコード
void main() {
// Mapの作成
Map<String, int> studentAges = {
'Alice': 25,
'Bob': 22,
'Charlie': 28,
};
// 要素へのアクセスと表示
print('Student Ages: $studentAges'); // Student Ages: {Alice: 25, Bob: 22, Charlie: 28}
print('Alice\'s Age: ${studentAges['Alice']}'); // Alice's Age: 25
// 要素の変更
studentAges['Bob'] = 23; // Bobの年齢を更新
studentAges['David'] = 24; // 新しい学生を追加
print('Modified Student Ages: $studentAges'); // Modified Student Ages: {Alice: 25, Bob: 23, Charlie: 28, David: 24}
// 要素の削除
studentAges.remove('Charlie');
print('Student Ages after removing Charlie: $studentAges'); // Student Ages after removing Charlie: {Alice: 25, Bob: 23, David: 24}
// Mapの反復処理
studentAges.forEach((name, age) {
print('$name is $age years old'); // Alice is 25 years old Bob is 23 years old David is 24 years old
});
}
-
Mapの作成:
studentAges
という名前のMapを作成し、それに3人の学生の名前と年齢を初期値として設定しています。 -
要素の変更: Bobの年齢を23に変更し、新しい学生DavidをMapに追加しています。
-
要素の削除: Charlieという学生をMapから削除しています。
-
Mapの反復処理:
forEach
メソッドを使用して、反復処理をして、各学生の名前と年齢を表示しています。
List / Set / Map の違い
List,Set,Mapには主に以下の3つの違いがあります。
-
宣言の仕方:
List
を使う場合は[]
、Set・Map
を使う場合は{}
を使ってインスタンス化します。
// リスト
var list = [];
var list2 = <int>[];
list = new List<int>();
// Set
Set set = {};
var set2 = <int>{};
set = new Set<int>();
// Map
var map = {};
var map2 = Map<String, int>();
-
重複要素を持てるか: 重複した要素を持てるかどうかにも違いがあります。
List
は重複要素を持つことができ、Set・Map
は重複要素を持つことができません。
var list = <int>[];
list.add(1);
list.add(2);
list.add(2);
print(list.length); // 3
var set = <int>{};
set.add(1);
set.add(2);
set.add(2);
print(set.length); // 2
var map = Map<String, int>();
map['hoge'] = 1;
map['fuga'] = 2;
map['fuga'] = 2;
print(map.length); // 2
-
順序付けできるか: 順序付けとは何番目の要素は何であるというのが分かる機能になります。
List
は順序付きコレクションです。Set・Map
はLinkedHashMap
クラスによって実装されているため、デフォルトでは順序付きで設定されていますが、変更することもできるので、順序が付いていない場合もあり注意が必要です。
var list = <int>[];
list.add(1);
list.add(2);
print(list[0]); // 1
var set = <int>{};
set.add(1);
set.add(2);
print(set.elementAt(0)); // 1
var map = Map<String, int>();
map['hoge'] = 1;
map['fuga'] = 2;
print(map.values.elementAt(0)); // 1
Stack / Queue
Stack (スタック):
スタックは、データの挿入と削除が末尾からのみ行われるデータ構造のことです。これは、後入れ先出し(LIFO Last-In-First-Out)とも呼ばれます。スタックはよく 「本を積み重ねる」 ことに例えられます。新しい本を一冊ずつ積み上げ、最後に置かれた本が最初に取り去られます。
基本操作
- Push: 要素をスタックの末尾に追加する操作。
- Pop: スタックの末尾から要素を削除する操作。
- Peek (or Top): スタックの要素を取得する操作。
Queue (キュー):
キューは、データの挿入が末尾から、削除が先頭から行われるデータ構造です。これは、先入れ先出し(FIFO First-In-First-Out)とも呼ばれます。キューはよく、 「レストランでの行列」 に例えられます。最初に入店した客が最初にサービスを受け、新しい客は列の最後に追加されます。
基本操作
- Enqueue (or Push): 要素をキューの末尾に追加する操作。
- Dequeue (or Shift): キューの先頭から要素を削除する操作。
- Front (or Peek): キューの要素を取得する操作。
より詳しく知りたい方はこちらにわかりやすい記事があったので、興味があれば、是非見てください。
Lambda
Lambda(ラムダ)は、関数を簡潔に表現するための手法の一つで、匿名関数(無名関数)とも呼ばれます。Dartでは、 =>
を使用してラムダ式を定義します。
ラムダ式を使用することによって、関数を変数として扱い、引数として渡すなどの柔軟な利用が可能になり、コードの量の削減と可読性の向上を同時に達成することができるようになります。
// 引数がない場合
() => expression;
// 引数がある場合
(parameter) => expression;
// 複数の引数がある場合
(parameter1, parameter2, ...) => expression;
void main() {
// 引数なしのLambda
var greet = () => print('Hello, Dart!');
greet(); // Hello, Dart!
// 引数ありのLambda
var number = (int a) => print(a);
number(4); // 4
// 複数の引数があるLambda
var add = (int x, int y) => x + y;
print('Sum: ${add(4, 6)}'); // Sum: 10
}
引数なしのLambda:
-
greet
という変数に引数がないラムダ式が代入されています。このラムダ式では 'Hello, Dart!' を出力する処理を行っています。 -
greet()
を呼び出すことで、このラムダ式が実行され、コンソールに 'Hello, Dart!' が表示されます。
引数ありのLambda:
-
number
という変数に引数があるラムダ式が代入されています。このラムダ式は引数a
を受け取り、その値を出力する処理を行っています。 -
number(4)
を呼び出すことで、このラムダ式が実行され、コンソールに 4 が表示されます。
複数の引数があるLambda:
-
add
という変数に複数の引数を受け取り、それらの和を返却するラムダ式が代入されています。 -
add(4, 6)
を呼び出すことで、このラムダ式が実行され、和である 10 がコンソールに表示されます。
Callback
コールバック(Callback)とは、プログラム内で特定のイベントが発生したときに、あらかじめ登録された関数(コールバック関数)が呼び出される仕組みです。
コールバック関数についてこちらに分かりやすい記事があったので、興味のある方は是非見てください。
void main() {
// コールバック関数
void myCallbackFunction() {
print('Callback executed!');
}
performOperation(myCallbackFunction);
}
// コールバックを受け取る関数
void performOperation(void Function() callback) {
print('Performing operation...');
callback();
}
-
performOperation
関数が引数としてコールバック関数を受け取り、その関数を実行することでコールバックが発生します。 -
main
関数では、myCallbackFunction
をコールバックとして渡しています。プログラムの実行結果として、「Performing operation...」と「Callback executed!」が順番に表示されます。
関数型プログラミング
関数型プログラミングとは、関数の組み合わせでコーディングするプログラミング方法です。関数型プログラミングの中では個々の関数が独立しており、実行中の処理の影響を受けないため、コード全体がシンプルで人が見て分かりやすく、保守性や再利用性に優れています。
関数型プログラミングで記述されたコードは、同じ入力に対し常に同じ結果を返し、実行中に変数の値の変更が起きません。
高階関数
Higher-Order Functions(高階関数)とは、他の関数を引数として受け取る、あるいは関数を戻り値として返す関数のことをいいます。高階関数を使用することによって、関数をデータとして扱うことができます。
サンプルコード
void myFunction(int a, int b, Function operation) {
int result = operation(a, b);
print("Result: $result");
}
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
void main() {
// myFunctionにadd関数を渡す
myFunction(3, 4, add); // Result: 7
// myFunctionにmultiply関数を渡す
myFunction(3, 4, multiply); // Result: 12
}
-
myFunction
は2つの整数と関数を引数として受け取り、その関数を実行して結果を表示しています。 -
add
関数とmultiply
関数が具体的な操作を定義し、それらをmyFunction
に渡すことで異なる結果を得ることができます。
Function calculator(String operation) {
if (operation == "add") {
return (int a, int b) => a + b;
} else if (operation == "multiply") {
return (int a, int b) => a * b;
} else {
return (int a, int b) => a - b; // デフォルトは減算
}
}
void main() {
var addFunction = calculator("add");
print(addFunction(3, 4)); // 7
var multiplyFunction = calculator("multiply");
print(multiplyFunction(3, 4)); // 12
var subtractFunction = calculator("subtract");
print(subtractFunction(3, 4)); // -1
}
不変データ
Immutable Data(不変データ)は、一度作成されたら変更できないデータ構造のことをいいます。不変データで、新しいデータが必要な場合、変更ではなく新しいデータを生成します。新しいデータを生成する場合には、全体をコピーするのではなく、必要な部分だけをコピーして新しいデータを生成するので、効率的にコピーが行えます。これらの特性により、プログラムが安定して、予期せぬバグを最小限に抑えることができるのです。
freezed / build_runner パッケージ
freezed
パッケージは、Dartで簡単に不変データを作成するためのパッケージになります。このパッケージを使用すると、イミュータブル(変更不可)なデータクラスを簡単に作成することができます。
freezed
で不変なデータクラスを生成するには build_runner
というパッケージと組み合わせて使用します。このパッケージを使用することで、繰り返し作業を極力抑えることができます。
freezed
、 build_runner
のパッケージを導入後、下記サンプルコードを実装してみてください。また、実装が完了したタイミングで、 build_runner
のコマンドを入力してみましょう。
flutter pub run build_runner build
freezed
パッケージによって作成したクラスの修正があった時、変更の度に上記のコマンドを入力するのは手間になります。そこで、 build_runner
にはファイルの更新を検知して自動で再生成してくれるコマンドが用意されています。
flutter pub run build_runner watch
また、 freezed
クラスを編集してコード生成をすると、生成済みのファイルと競合してエラーが発生することがあります。 delete-conflicting-outputs
オプションを付けることで、競合ファイルを削除して再生成してくれるので、ぜひ活用してみてください。
flutter pub run build_runner watch --delete-conflicting-outputs
サンプルコード
import 'package:freezed_annotation/freezed_annotation.dart';
// 不変データクラスのファイル名 {ファイル名}.freezed.dart
import 'package:freezed_annotation/freezed_annotation.dart';
// 不変データクラスのファイル名 {ファイル名}.freezed.dart
part 'main.freezed.dart';
@freezed
abstract class Person with _$Person {
const factory Person({
required String name,
required int age,
String? address,
@Default('Unknown') String country,
}) = _Person;
}
void main() {
// 不変なPersonオブジェクトを作成
final person = Person(name: 'Alice', age: 25, address: '123');
// person.age = 30; // エラー: 不変データは変更できません
// データの表示 Name: Alice, Age: 25, Address: 123, Country: Unknown
print('Name: ${person.name}, Age: ${person.age}, Address: ${person.address}, Country: ${person.country}');
}
-
@freezed
アノテーションは、不変データクラスを生成するためのアノテーションです。これをクラスに付与することで、Freezed
が不変データクラスのコードを生成します。 -
@Default
アノテーションは、デフォルト値を指定するためのアノテーションです。このアノテーションを使用することで、指定されたプロパティに値が渡されなかった場合、そのプロパティのデフォルト値が使用されます。 -
part 'main.freezed.dart';
はFreezed
によって作成されるファイルを指定しています。 -
const factory Person({ ... }) = _Person;
はFreezed
が作成したファクトリーメソッドです。このメソッドは、Person
クラスのインスタンスを作成するために使用され、それぞれ必須のname
,age
プロパティに加えて、address
プロパティとcountry
プロパティが指定されています。 -
final person = Person(name: 'Alice', age: 25, address: '123');
では、不変データクラスPerson
のインスタンスを作成しており、ここではcountry
に値が設定されていないので、デフォルト値が使用されます。不変データクラスは、一度インスタンスが作成されると変更できないので、person.age = 30;
のような変更はエラーが発生します。
クロージャ
クロージャ(Closure)とは、スコープ内の変数や関数を参照できる関数のことです。クロージャを使用することで、スコープ内に閉じられた変数を参照できるようになり、無駄なグローバル変数の定義を防ぐことができます。
また、外部の変数を内部で利用できるため、柔軟で動的な関数を作成することができるようになります。特に関数を返す関数を作成する場合や、関数の振る舞いを動的に変更する必要がある場合に使用されます。
サンプルコード
void main() {
// クロージャを使わない場合
print(addWithoutClosure(5)); // 10
// クロージャを使う場合
var addWithClosure = addClosure(5);
print(addWithClosure(3)); // 13
}
// クロージャを使わない関数
int addWithoutClosure(int a) {
int b = 5;
return a + b;
}
// クロージャを使う関数
Function addClosure(int a) {
int b = 5;
// クロージャを返す
return (int c) {
return a + b + c;
};
}
-
addWithoutClosure
関数はクロージャを使用していない通常の関数です。この関数は引数としてa
を受け取り、受け取った値にローカル変数b
を足しています。クロージャを使用していないため、外部の変数に依存していません。 -
addClosure
関数はクロージャを使用した関数です。この関数は、引数a
と関数内で定義された変数b
を使用してクロージャ(return (int c) { return a + b + c; };
)を返しています。このクロージャは引数としてc
を受け取り、外部のaddCloser
関数内で定義された変数a
とb
にアクセスすることができます。 -
addWithClosure
ではaddClosure(5)
を呼び出して得られたクロージャを代入しています。この時点で、a
は 5 となり、b
はaddClosure
関数内で定義されていた 5 のままです。 -
addWithClosure(3)
を呼び出すと、外部のa
(ここでは 5)、内部のb
(ここでは 5)、引数c
(ここでは 3)を足して結果の 13 が返却されます。
クロージャを使うことで、関数が生成された時点の状態を覚えておくことができます。そのため、 addWithClosure
を異なる箇所で呼び出しても、生成時のコンテキスト(この場合は a
と b
の値)を覚えていてくれるのです。
純粋関数
純粋関数(Pure Function)とは、次の2つの特性を持つ関数です。
-
同じ入力に対して常に同じ結果を返す: 関数が同じ引数で複数回呼び出された場合、同じ結果が得られることが保証されているため、プログラムが予測可能になり、テストが容易になります。
-
副作用がない: 関数が外部の状態を変更したり、外部のデータにアクセスしたりすることがないため、関数によってプログラムの状態が変化することがありません。そのため返り値が保証されている関数になり、プログラム自体の複雑度を下げることができます。
サンプルコード
// 純粋関数の例
int add(int a, int b) {
return a + b;
}
void main() {
// 同じ引数での呼び出し
print(add(3, 5)); // 8
// 同じ引数での呼び出し
print(add(3, 5)); // 8
}
上記の add
関数は同じ引数で呼び出されると、常に同じ結果を返します。また、この関数は外部の状態を変更する副作用がないため、純粋関数と見なされます。
int num = 2;
int add(int a) {
return a + num;
}
一見純粋関数に見えますが、関数外部の変数を参照していることがわかります。 num
は、現在 2
の状態ですが、これが 3
や 4
に変更されたら、関数 add
が返す値は都度変わってしまいます。外部の影響を受けて返す値が変わってしまう関数は純粋関数とは言えません。
Isolate
Isolate(アイソレート)とは、Dart言語におけるマルチスレッドプログラミングのための機能です。Isolateを使用することでそれぞれが独立して動作し、直接的な相互作用がないため、安全に並列処理を行うことができます。
非同期処理と並列処理
-
非同期処理 : 一つのスレッド内で複数のタスクを進行させ、待機時間に他のタスクを実行する。
-
並列処理 : 複数のスレッドやプロセスを同時に実行して、複数のタスクを同時に進行する。
非同期処理は特にI/O待機がある場合に有効で、並列処理はプログラムが大幅にCPUのリソースを使用しており、他のリソース(ディスクやネットワークなど)の待ち時間が少ない場合に有効です。以下は非同期処理と並列処理を行う際の具体例です。
-
非同期処理
- データの入出力処理
- データベースアクセス
- ネットワーク通信
-
並列処理
- サイズの大きいJSONのパース作業
- 数値計算
- 画像処理
- 暗号化
サンプルコード
import 'dart:isolate';
void main() {
print('Main thread start');
// Isolateを起動して非同期処理を実行
Future<void> runIsolate() async {
ReceivePort receivePort = ReceivePort();
// Isolateを起動
Isolate isolate = await Isolate.spawn(isolateFunction, receivePort.sendPort);
// Isolateからのメッセージを待機
receivePort.listen((message) {
print('Received message from Isolate: $message');
// Isolateを停止
receivePort.close();
isolate.kill();
});
print('Main thread continues');
}
runIsolate();
print('Main thread end');
}
void isolateFunction(SendPort sendPort) {
print('Isolate start');
Future.delayed(Duration(seconds: 2), () {
// メッセージをメインスレッドに送信
sendPort.send('Isolate completed');
print('Isolate end');
});
}
-
runIsolate
関数では、ReceivePort
を作成し、Isolateからメッセージを受信するために使用します。次に、Isolate.spawn
を使用して新しいIsolateを生成し、isolateFunction
関数をそのIsolate内で実行します。IsolateはreceivePort.sendPort
を通じてメインスレッドにメッセージを送信できるようになります。 -
receivePort.listen
でメッセージを待機し、Isolateからのメッセージを受信すると "Received message from Isolate: Isolate completed" と表示します。その後、receivePort.close()
とisolate.kill()
を使用してIsolateを停止します。 -
isolateFunction
関数はIsolate内で実行され、非同期処理を行います。Future.delayed
を使用して2秒後にメッセージ "Isolate completed" をメインスレッドに送信し、"Isolate end" を表示します。
flutter: Main thread start
flutter: Main thread end
flutter: Main thread continues
flutter: Isolate start
flutter: Isolate end
flutter: Received message from Isolate: Isolate completed
処理の流れ
- "Main thread start" が表示されます。
-
runIsolate
関数が呼び出され、Isolateが生成され、非同期処理が開始されます。 - "Main thread end" が表示され、メインスレッドは
runIsolate
の非同期処理を待機せずに続行します。 - Isolate内での処理が2秒後に完了し、"Isolate completed" がメインスレッドに送信されます。
- "Main thread continues" が表示され、メインスレッドが
runIsolate
関数内の receivePort.listen からのメッセージを待機します。 - "Isolate start" と "Isolate end" が表示されます。
- Isolateからのメッセージが受信され、"Received message from Isolate: Isolate completed" が表示されます。
- Isolateが終了し、プログラムが終了します
Async / Await
async
と await
は、Dartで非同期プログラミングを行うための機能です。非同期処理は、プログラムが他の処理を待たずに進行できるようにし、非同期関数はそのような処理を記述するために使用されます。
-
async: 非同期関数を定義する際に使用します。
async
が指定された関数は非同期であることを示します。 -
await:
async
関数内でのみ使用可能で、非同期処理が完了するまでプログラムの実行を一時停止します。通常は非同期関数内で非同期処理を行う際に使用されます。 -
async
、await
を利用すると同期処理のように記述できるが、非同期処理には変わりありません。 -
await
は、await
キーワードを付与した処理のみ待ち、await
を使用する場合は、async
キーワードを必ず明記する必要があります。
サンプルコード
void main() async {
print('Start of main');
// 同期的な関数の呼び出し
syncFunction();
// 非同期関数の呼び出し
await asyncFunction();
// Futureを返す関数の処理結果を取得
String result = await futureReturningFunction();
print('Result from Future: $result');
print('End of main');
}
void syncFunction() {
print('Sync Function - Task 1');
print('Sync Function - Task 2');
}
Future<void> asyncFunction() async {
print('Async Function - Start of Task 1');
await Future.delayed(Duration(seconds: 2)); // 2秒待機
print('Async Function - End of Task 1');
print('Async Function - Task 2');
}
Future<String> futureReturningFunction() async {
await Future.delayed(Duration(seconds: 1));
return 'Task completed';
}
main 関数:
-
main
関数はasync
キーワードが関数に追加されており、非同期関数として宣言されています。 -
syncFunction
は同期関数で、即座にタスク1とタスク2を実行します。 -
await asyncFunction()
は非同期関数の呼び出しです。await
キーワードにより、非同期関数の完了を待ってから次の処理に進みます。 -
String result = await futureReturningFunction()
では、非同期関数futureReturningFunction
の完了を待ち、その結果を取得しています。
各関数の詳細:
-
syncFunction
: 同期的な処理を実行し、即座に結果を表示します。 -
asyncFunction
: タスク1では2秒待機し、その後タスク2を実行します。この関数はawait
を使用して非同期処理を行います。 -
futureReturningFunction
: 1秒待機してから "Task completed" を返す非同期関数です。
コード全体の流れ
-
main
関数が実行され、同期的な処理としてsyncFunction
が呼び出されます。 -
await asyncFunction()
により、非同期処理が待機され、asyncFunction
のタスク1とタスク2が順次実行されます。 -
await futureReturningFunction()
により、futureReturningFunction
の非同期処理が待機され、その結果がresult
に格納され、出力されます。 -
"End of main"が出力されます。
参考資料