Flutter
はじめに
[編集]Flutterとは
[編集]Flutterは、Googleが開発したオープンソースのUIソフトウェア開発キット(SDK)です。これを使用することで、単一のコードベースから、Android、iOS、Web、デスクトップ向けのネイティブコンパイルされたアプリケーションを作成することができます。Flutterは、特にクロスプラットフォーム開発において優れた選択肢であり、開発者に高い柔軟性と効率を提供します。
Flutterの特徴
[編集]- クロスプラットフォーム開発: 1つのコードベースで複数のプラットフォーム向けにアプリケーションを開発することができます。これにより、開発時間とリソースの節約が可能となります。
- 高速な開発: ホットリロード機能により、コードの変更をリアルタイムで確認することができ、開発者はアプリケーションの挙動を即座に確認しながら進めることができます。
- カスタマイズ可能なウィジェット: Flutterには豊富なウィジェットが標準で用意されており、これを使って美しく、ネイティブに近いユーザーインターフェースを構築することができます。これにより、独自のデザインやユーザーエクスペリエンスを実現できます。
- 高性能: FlutterはDartというプログラミング言語を使用し、ネイティブコードにコンパイルされるため、優れたパフォーマンスを実現しています。これにより、モバイルアプリケーションのパフォーマンスが大幅に向上し、スムーズな動作が保証されます。
開発環境のセットアップ
[編集]Flutterでアプリ開発を始めるには、以下の手順で開発環境をセットアップする必要があります。環境の準備を整えることで、スムーズに開発を開始することができます。
Flutter SDKのダウンロードとインストール
[編集]- Flutter公式サイトのインストールガイドから、お使いのOSに適したFlutter SDKをダウンロードします。
- ダウンロードしたアーカイブを解凍し、任意のディレクトリに配置します。
環境変数の設定
[編集]Flutterをコマンドラインから利用するためには、Flutter SDKのbin
ディレクトリを環境変数PATH
に追加する必要があります。
- Windows: システム環境変数の編集画面を開き、
PATH
にFlutterのbin
ディレクトリのパスを追加します。 - macOS/Linux:
~/.bash_profile
または~/.zshrc
ファイルに以下を追加し、ターミナルで設定を反映させます:
export PATH="$PATH:[Flutterのインストールパス]/flutter/bin"
依存関係のインストール
[編集]依存関係をインストールするために、ターミナルで以下のコマンドを実行します:
flutter doctor
このコマンドは、Flutter開発に必要なツールや依存関係が正しくインストールされているかをチェックします。もし不足している項目があれば、指示に従ってインストールを行ってください。
IDEのセットアップ
[編集]Flutterの開発に適したIDEとして、以下の2つが推奨されています:
- Android Studio(IntelliJベース)
- Visual Studio Code
いずれのIDEにも、FlutterプラグインとDartプラグインをインストールする必要があります。これにより、コード補完やデバッグ、エミュレータの管理などの機能を利用できるようになります。
エミュレータのセットアップ
[編集]Flutterアプリを開発するには、エミュレータを設定してテストすることが重要です。以下の手順に従ってエミュレータをセットアップします。
- iOSの場合: Xcodeをインストールし、iOSシミュレータをセットアップします。これにより、iPhoneやiPad向けのアプリをシミュレータでテストできます。
- Androidの場合: Android Studioを使用してAVD(Android Virtual Device)を作成します。これにより、さまざまなAndroidデバイスでのテストが可能になります。
これで、Flutter開発環境の準備が整いました。次の章では、Flutterアプリケーションを作成するために必要なDartプログラミング言語について学びます。
Dartの基礎
[編集]Dartは、Flutterアプリケーション開発の基盤となるプログラミング言語です。この章では、Dartの主要な概念と構文について学びます。
変数と型
[編集]Dartは静的型付け言語ですが、型推論もサポートしており、コードを書く際の柔軟性と可読性が向上します。
変数宣言
[編集]Dartでは、変数の型を明示的に指定することも、型推論に任せることもできます。
// 型を明示的に指定 int age = 30; String name = 'John'; // 型推論を使用 var score = 100; // int型と推論される var isActive = true; // bool型と推論される // 定数 final PI = 3.14159; const GRAVITY = 9.8;
基本的なデータ型
[編集]Dartにはいくつかの基本的なデータ型があります。
int
: 整数型double
: 浮動小数点数型String
: 文字列型bool
: 真偽値型List
: 配列型Map
: キーと値のペアを扱う型
List<String> fruits = ['apple', 'banana', 'orange']; Map<String, int> ages = {'Alice': 25, 'Bob': 30};
関数
[編集]Dartの関数は第一級オブジェクトであり、変数に代入したり、他の関数に渡したりすることができます。
関数の定義と呼び出し
[編集]関数は、引数と戻り値を定義することで作成できます。
int add(int a, int b) { return a + b; } // 関数の呼び出し int result = add(5, 3); // result = 8
アロー関数
[編集]単一の式を返す関数は、アロー構文を使って簡潔に書くことができます。
int multiply(int a, int b) => a * b;
オプショナルパラメータ
[編集]Dartでは、位置引数と名前付き引数の両方をオプショナルに設定できます。
// 位置引数(角括弧で囲む) String greet(String name, [String? greeting]) { greeting ??= 'Hello'; return '$greeting, $name!'; } // 名前付き引数(波括弧で囲む) void printPerson({String? name, int? age}) { print('Name: $name, Age: $age'); } // 関数の呼び出し print(greet('Alice')); // Hello, Alice! print(greet('Bob', 'Hi')); // Hi, Bob! printPerson(name: 'Charlie', age: 30);
クラスとオブジェクト指向プログラミング
[編集]Dartは完全なオブジェクト指向言語であり、クラスを使ってデータとメソッドをまとめることができます。
クラスの定義
[編集]クラスは、データメンバ(フィールド)とメソッドをまとめて定義します。
class Person { String name; int age; // コンストラクタ Person(this.name, this.age); // メソッド void introduce() { print('My name is $name and I am $age years old.'); } } // クラスの使用 var person = Person('Alice', 25); person.introduce();
継承
[編集]Dartでは、extends
キーワードを使ってクラスを継承し、親クラスのメソッドやプロパティを引き継ぐことができます。
class Student extends Person { String school; Student(String name, int age, this.school) : super(name, age); @override void introduce() { super.introduce(); print('I study at $school.'); } }
Getter と Setter
[編集]クラスのプロパティにアクセスする際に、GetterやSetterを使ってカスタマイズできます。
class Rectangle { double _width, _height; Rectangle(this._width, this._height); // Getter double get area => _width * _height; // Setter set width(double value) { if (value > 0) _width = value; } }
非同期プログラミング
[編集]Dartはasync
とawait
キーワードを使用して非同期プログラミングをサポートしています。これにより、非同期操作を同期的なコードのように扱うことができます。
Future<String> fetchUserData() async { // ネットワークリクエストをシミュレート await Future.delayed(Duration(seconds: 2)); return 'User data'; } void main() async { print('Fetching user data...'); var userData = await fetchUserData(); print(userData); }
これらの基本的な概念を理解することで、Flutterアプリケーション開発におけるDartの基礎が身につきます。次の章では、これらの知識を活用して、Flutterの基本概念について学んでいきます。
Flutterの基本概念
[編集]この章では、Flutterアプリケーション開発の基本となる重要な概念について学びます。
Widgetの概要
[編集]Flutterでは、ほぼすべてのものがWidgetです。Widgetは、ユーザーインターフェースを構築するための基本的な構成要素です。
Widgetの種類
[編集]- 視覚的Widget: テキスト、ボタン、画像など
- レイアウトWidget: Row、Column、Stackなど
- 構造的Widget: MaterialApp、Scaffoldなど
基本的なWidgetの例
[編集]import 'package:flutter/material.dart'; class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(title: Text('My First Flutter App')), body: Center( child: Text('Hello, Flutter!'), ), ), ); } }
StatelessWidgetとStatefulWidget
[編集]Flutterには、2種類の基本的なWidgetがあります:StatelessWidgetとStatefulWidgetです。
StatelessWidget
[編集]StatelessWidgetは、状態を持たないWidgetです。一度描画されると、その後変更されることはありません。
class MyStatelessWidget extends StatelessWidget { final String text; MyStatelessWidget({required this.text}); @override Widget build(BuildContext context) { return Container( child: Text(text), ); } }
StatefulWidget
[編集]StatefulWidgetは、動的に変更可能な状態を持つWidgetです。
class MyStatefulWidget extends StatefulWidget { @override _MyStatefulWidgetState createState() => _MyStatefulWidgetState(); } class _MyStatefulWidgetState extends State<MyStatefulWidget> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Column( children: [ Text('Count: $_counter'), ElevatedButton( onPressed: _incrementCounter, child: Text('Increment'), ), ], ); } }
ビルドコンテキストとツリー構造
[編集]ビルドコンテキスト
[編集]BuildContext
は、Widgetツリー内のWidgetの位置に関する情報を提供するオブジェクトです。これは、親Widgetの情報にアクセスしたり、テーマデータを取得したりするのに使用されます。
class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { // ビルドコンテキストを使用してテーマデータにアクセス final textTheme = Theme.of(context).textTheme; return Text( 'Hello', style: textTheme.headline4, ); } }
ウィジェットツリー
[編集]Flutterアプリケーションは、ネストされたWidgetのツリー構造として表現されます。各Widgetは、親Widgetのbuild
メソッド内で作成されます。
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(title: Text('Widget Tree Example')), body: Column( children: [ Text('First child'), Container( child: Row( children: [ Icon(Icons.star), Text('Nested child'), ], ), ), ], ), ), ); } }
この例では、MaterialApp
が最上位のWidgetで、その子にScaffold
、さらにその下にColumn
とRow
があり、それぞれが他のWidgetを含んでいます。
キー (Keys)
[編集]Keyは、Widgetの識別と状態の保持に使用されます。特に、動的にWidgetを追加・削除する場合に重要です。
class MyListItem extends StatelessWidget { final String text; MyListItem({required this.text, required Key key}) : super(key: key); @override Widget build(BuildContext context) { return ListTile(title: Text(text)); } } // 使用例 ListView( children: [ MyListItem(text: 'Item 1', key: UniqueKey()), MyListItem(text: 'Item 2', key: UniqueKey()), ], )
これらの基本概念を理解することで、Flutterアプリケーションの構造と動作原理をより深く把握できるようになります。次の章では、これらの知識を活用してUIデザインについて学んでいきます。
UIデザイン
[編集]この章では、Flutterを使用して魅力的で機能的なユーザーインターフェース(UI)を設計・実装する方法について学びます。
基本的なレイアウトWidget
[編集]Flutterには、UIを構築するための様々なレイアウトWidgetが用意されています。以下に主要なものを紹介します。
Container
[編集]Container
は、パディング、マージン、境界線、背景色などを設定できる汎用的なWidgetです。
Container( padding: EdgeInsets.all(8.0), margin: EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(5.0), ), child: Text('Hello, Flutter!'), )
Row と Column
[編集]Row
とColumn
は、子Widgetを水平方向(Row)または垂直方向(Column)に配置します。
Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('First item'), Text('Second item'), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Icon(Icons.star), Icon(Icons.star), Icon(Icons.star), ], ), ], )
Stack
[編集]Stack
を使用すると、Widgetを重ねて表示できます。
Stack( children: [ Image.asset('background.jpg'), Positioned( bottom: 10, right: 10, child: Text('Overlay Text'), ), ], )
ListView
[編集]ListView
は、スクロール可能なリストを作成します。
ListView.builder( itemCount: 100, itemBuilder: (context, index) { return ListTile( title: Text('Item $index'), ); }, )
マテリアルデザインとCupertino
[編集]Flutterは、GoogleのマテリアルデザインとAppleのCupertinoデザインの両方をサポートしています。
マテリアルデザイン
[編集]マテリアルデザインを使用するには、MaterialApp
をアプリのルートWidgetとして使用します。
import 'package:flutter/material.dart'; class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: Scaffold( appBar: AppBar(title: Text('Material App')), body: Center(child: Text('Hello, Material!')), floatingActionButton: FloatingActionButton( onPressed: () {}, child: Icon(Icons.add), ), ), ); } }
Cupertinoデザイン
[編集]iOS風のデザインを使用するには、CupertinoApp
を使用します。
import 'package:flutter/cupertino.dart'; class MyCupertinoApp extends StatelessWidget { @override Widget build(BuildContext context) { return CupertinoApp( theme: CupertinoThemeData(primaryColor: CupertinoColors.activeBlue), home: CupertinoPageScaffold( navigationBar: CupertinoNavigationBar(middle: Text('Cupertino App')), child: Center(child: Text('Hello, Cupertino!')), ), ); } }
カスタムWidgetの作成
[編集]複雑なUIを管理しやすくするために、カスタムWidgetを作成することができます。
class CustomButton extends StatelessWidget { final String text; final VoidCallback onPressed; CustomButton({required this.text, required this.onPressed}); @override Widget build(BuildContext context) { return ElevatedButton( style: ElevatedButton.styleFrom( primary: Colors.blue, onPrimary: Colors.white, padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), ), ), onPressed: onPressed, child: Text(text, style: TextStyle(fontSize: 18)), ); } } // 使用例 CustomButton( text: 'Click me!', onPressed: () { print('Button pressed!'); }, )
レスポンシブデザイン
[編集]異なる画面サイズに対応するレスポンシブなデザインを作成することも重要です。
class ResponsiveLayout extends StatelessWidget { final Widget mobileBody; final Widget desktopBody; ResponsiveLayout({required this.mobileBody, required this.desktopBody}); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth < 600) { return mobileBody; } else { return desktopBody; } }, ); } } // 使用例 ResponsiveLayout( mobileBody: MobileLayout(), desktopBody: DesktopLayout(), )
これらの概念とテクニックを活用することで、魅力的で機能的なUIを設計・実装することができます。次の章では、アプリケーションの状態管理について学んでいきます。
ステート管理
[編集]この章では、Flutterアプリケーションにおける状態(state)の管理方法について学びます。適切なステート管理は、アプリケーションの保守性、スケーラビリティ、パフォーマンスに大きな影響を与えます。
setState
[編集]setState
は、Flutterが提供する最も基本的なステート管理方法です。主に小規模なウィジェット内での状態更新に使用されます。
class CounterWidget extends StatefulWidget { @override _CounterWidgetState createState() => _CounterWidgetState(); } class _CounterWidgetState extends State<CounterWidget> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Column( children: [ Text('Count: $_counter'), ElevatedButton( onPressed: _incrementCounter, child: Text('Increment'), ), ], ); } }
InheritedWidget
[編集]InheritedWidget
は、ウィジェットツリーを通じて効率的にデータを下位のウィジェットに渡すための方法です。これは、Providerなどの高度なステート管理ソリューションの基礎となっています。
class CounterProvider extends InheritedWidget { final int count; final Function increment; CounterProvider({ required this.count, required this.increment, required Widget child, }) : super(child: child); static CounterProvider of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<CounterProvider>()!; } @override bool updateShouldNotify(CounterProvider oldWidget) { return count != oldWidget.count; } } // 使用例 class CounterApp extends StatefulWidget { @override _CounterAppState createState() => _CounterAppState(); } class _CounterAppState extends State<CounterApp> { int _count = 0; void _increment() { setState(() { _count++; }); } @override Widget build(BuildContext context) { return CounterProvider( count: _count, increment: _increment, child: // ... 子ウィジェット ); } } // 子ウィジェットでの使用 class CounterDisplay extends StatelessWidget { @override Widget build(BuildContext context) { final counter = CounterProvider.of(context); return Text('Count: ${counter.count}'); } }
Provider
[編集]Provider
は、InheritedWidget
を基にした、より使いやすいステート管理ソリューションです。依存性注入とステート管理を簡単に行うことができます。
まず、pubspec.yaml
にprovider
パッケージを追加します:
dependencies: provider: ^6.0.0
そして、以下のように使用します:
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class CounterModel extends ChangeNotifier { int _count = 0; int get count => _count; void increment() { _count++; notifyListeners(); } } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (context) => CounterModel(), child: MaterialApp( home: CounterPage(), ), ); } } class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Counter Example')), body: Center( child: Consumer<CounterModel>( builder: (context, counter, child) => Text( 'Count: ${counter.count}', style: Theme.of(context).textTheme.headline4, ), ), ), floatingActionButton: FloatingActionButton( onPressed: () => context.read<CounterModel>().increment(), child: Icon(Icons.add), ), ); } }
Riverpod(オプション)
[編集]Riverpodは、Providerの作者によって開発された、Providerの問題点を解決するための新しいステート管理ソリューションです。Riverpodは型安全性が高く、プロバイダの衝突を防ぐことができます。
まず、pubspec.yaml
にflutter_riverpod
パッケージを追加します:
dependencies: flutter_riverpod: ^2.0.0
そして、以下のように使用します:
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter()); class Counter extends StateNotifier<int> { Counter() : super(0); void increment() => state++; } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return ProviderScope( child: MaterialApp( home: CounterPage(), ), ); } } class CounterPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final count = ref.watch(counterProvider); return Scaffold( appBar: AppBar(title: Text('Counter Example')), body: Center( child: Text( 'Count: $count', style: Theme.of(context).textTheme.headline4, ), ), floatingActionButton: FloatingActionButton( onPressed: () => ref.read(counterProvider.notifier).increment(), child: Icon(Icons.add), ), ); } }
これらのステート管理手法を理解し、適切に使用することで、効率的で保守性の高いFlutterアプリケーションを開発することができます。次の章では、ナビゲーションとルーティングについて学んでいきます。
ナビゲーションとルーティング
[編集]この章では、Flutterアプリケーションにおける画面遷移(ナビゲーション)と路線管理(ルーティング)について学びます。適切なナビゲーションとルーティングは、ユーザーエクスペリエンスを向上させ、アプリケーションの構造を整理するのに役立ちます。
基本的なナビゲーション
[編集]Flutterでは、Navigator
クラスを使用して画面遷移を管理します。
新しい画面への遷移
[編集]class FirstPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('First Page')), body: Center( child: ElevatedButton( child: Text('Go to Second Page'), onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (context) => SecondPage()), ); }, ), ), ); } } class SecondPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Second Page')), body: Center( child: ElevatedButton( child: Text('Go Back'), onPressed: () { Navigator.pop(context); }, ), ), ); } }
画面遷移時のアニメーション
[編集]カスタムのトランジションアニメーションを作成することもできます:
Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => SecondPage(), transitionsBuilder: (context, animation, secondaryAnimation, child) { var begin = Offset(1.0, 0.0); var end = Offset.zero; var curve = Curves.ease; var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); return SlideTransition( position: animation.drive(tween), child: child, ); }, ), );
名前付きルート
[編集]大規模なアプリケーションでは、名前付きルートを使用してナビゲーションを管理するのが一般的です。
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( initialRoute: '/', routes: { '/': (context) => HomePage(), '/second': (context) => SecondPage(), '/third': (context) => ThirdPage(), }, ); } } // 使用例 Navigator.pushNamed(context, '/second');
onGenerateRoute
[編集]動的なルーティングが必要な場合は、onGenerateRoute
を使用します:
MaterialApp( onGenerateRoute: (settings) { if (settings.name == '/') { return MaterialPageRoute(builder: (context) => HomePage()); } var uri = Uri.parse(settings.name!); if (uri.pathSegments.length == 2 && uri.pathSegments.first == 'user') { var id = uri.pathSegments[1]; return MaterialPageRoute(builder: (context) => UserPage(id: id)); } return MaterialPageRoute(builder: (context) => UnknownPage()); }, ) // 使用例 Navigator.pushNamed(context, '/user/123');
引数の受け渡し
[編集]画面遷移時に引数を渡すことができます。
ルート生成時に引数を渡す
[編集]class UserPage extends StatelessWidget { final String userId; UserPage({required this.userId}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('User Page')), body: Center(child: Text('User ID: $userId')), ); } } // 使用例 Navigator.push( context, MaterialPageRoute( builder: (context) => UserPage(userId: '123'), ), );
名前付きルートで引数を渡す
[編集]MaterialApp( onGenerateRoute: (settings) { if (settings.name == '/user') { final args = settings.arguments as Map<String, dynamic>; return MaterialPageRoute( builder: (context) { return UserPage(userId: args['userId']); }, ); } // その他のルート処理 }, ) // 使用例 Navigator.pushNamed( context, '/user', arguments: {'userId': '123'}, );
DeepLinkの処理
[編集]モバイルアプリでは、外部からのDeepLinkを処理することも重要です。
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( onGenerateRoute: (settings) { // DeepLinkの処理 if (settings.name != null) { final uri = Uri.parse(settings.name!); if (uri.pathSegments.length == 2 && uri.pathSegments.first == 'product') { final productId = uri.pathSegments[1]; return MaterialPageRoute( builder: (context) => ProductPage(productId: productId), ); } } // 通常のルーティング処理 // ... }, ); } }
これらのナビゲーションとルーティングの技術を使用することで、複雑なアプリケーション構造を効率的に管理し、スムーズな画面遷移を実現することができます。次の章では、非同期プログラミングについて学んでいきます。
非同期プログラミング
[編集]この章では、Flutterアプリケーションにおける非同期プログラミングについて学びます。非同期プログラミングは、ネットワーク要求、ファイル操作、データベース操作など、時間のかかる処理を効率的に行うために不可欠です。
Futureとasync/await
[編集]Future
は、将来のある時点で完了する処理の結果を表します。async
とawait
キーワードを使用することで、非同期コードを同期的に書くように見せることができます。
Future
[編集]Future<String> fetchUserName() { // サーバーからユーザー名を取得する処理をシミュレート return Future.delayed(Duration(seconds: 2), () => 'John Doe'); } void main() { print('Fetching user name...'); fetchUserName().then((name) { print('User name: $name'); }).catchError((error) { print('Error: $error'); }); print('This will print before the user name'); }
async/await
[編集]Future<void> printUserName() async { try { print('Fetching user name...'); String name = await fetchUserName(); print('User name: $name'); } catch (e) { print('Error: $e'); } } void main() async { await printUserName(); print('This will print after the user name'); }
StreamとStreamBuilder
[編集]Stream
は、時間の経過とともに複数の値を非同期に生成するデータのシーケンスです。StreamBuilder
は、Streamからのデータに基づいてUIを構築するのに便利なWidgetです。
Stream
[編集]Stream<int> countStream(int max) async* { for (int i = 1; i <= max; i++) { await Future.delayed(Duration(seconds: 1)); yield i; } } void main() { Stream<int> stream = countStream(5); stream.listen( (data) => print('Received: $data'), onError: (error) => print('Error: $error'), onDone: () => print('Stream is done'), ); }
StreamBuilder
[編集]class CounterWidget extends StatelessWidget { @override Widget build(BuildContext context) { return StreamBuilder<int>( stream: countStream(10), builder: (BuildContext context, AsyncSnapshot<int> snapshot) { if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } switch (snapshot.connectionState) { case ConnectionState.none: return Text('Not connected to the Stream'); case ConnectionState.waiting: return Text('Waiting for values...'); case ConnectionState.active: return Text('Value: ${snapshot.data}'); case ConnectionState.done: return Text('Stream has finished'); } }, ); } }
非同期処理におけるエラーハンドリング
[編集]非同期処理では、エラーハンドリングが特に重要です。try-catch
ブロックやcatchError
メソッドを使用してエラーを適切に処理します。
try-catchを使用したエラーハンドリング
[編集]Future<void> fetchData() async { try { final result = await someAsyncOperation(); print('Result: $result'); } catch (e) { print('An error occurred: $e'); } finally { print('This will always execute'); } }
Futureチェーンでのエラーハンドリング
[編集]void fetchDataWithChain() { someAsyncOperation() .then((result) => print('Result: $result')) .catchError((error) => print('An error occurred: $error')) .whenComplete(() => print('This will always execute')); }
複数の非同期操作の処理
[編集]複数の非同期操作を同時に処理する場合、Future.wait
を使用します。
Future<void> fetchMultipleData() async { try { final results = await Future.wait([ fetchUserProfile(), fetchUserPosts(), fetchUserFriends(), ]); print('User Profile: ${results[0]}'); print('User Posts: ${results[1]}'); print('User Friends: ${results[2]}'); } catch (e) { print('An error occurred: $e'); } }
タイムアウトの処理
[編集]非同期操作にタイムアウトを設定することも重要です。
Future<String> fetchWithTimeout() { return fetchData().timeout( Duration(seconds: 5), onTimeout: () => throw TimeoutException('The operation timed out'), ); }
非同期プログラミングを適切に理解し使用することで、レスポンシブで効率的なFlutterアプリケーションを開発することができます。ユーザーインターフェイスをブロックすることなく、長時間実行される操作を処理できるようになります。次の章では、データの永続化について学んでいきます。
データの永続化
[編集]この章では、Flutterアプリケーションにおけるデータの永続化、つまりデータをデバイス上に保存し、後で取り出す方法について学びます。適切なデータ永続化手法を選択することで、アプリケーションのオフライン機能を向上させ、ユーザー体験を改善することができます。
SharedPreferences
[編集]SharedPreferencesは、キーと値のペアを使用して小規模なデータを保存するのに適しています。アプリの設定や小さな状態情報の保存に最適です。
まず、pubspec.yaml
にshared_preferences
パッケージを追加します:
dependencies: shared_preferences: ^2.0.0
使用例
[編集]import 'package:shared_preferences/shared_preferences.dart'; class PreferencesService { Future<void> saveUsername(String username) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('username', username); } Future<String?> getUsername() async { final prefs = await SharedPreferences.getInstance(); return prefs.getString('username'); } Future<void> clearUsername() async { final prefs = await SharedPreferences.getInstance(); await prefs.remove('username'); } } // 使用例 void main() async { final prefsService = PreferencesService(); // ユーザー名を保存 await prefsService.saveUsername('JohnDoe'); // ユーザー名を取得 final username = await prefsService.getUsername(); print('Saved username: $username'); // ユーザー名をクリア await prefsService.clearUsername(); }
SQLite(sqflite)
[編集]SQLiteは、より構造化されたデータや大量のデータを保存する必要がある場合に適しています。Flutterでは、sqflite
パッケージを使用してSQLiteデータベースにアクセスできます。
pubspec.yaml
にsqflite
パッケージを追加します:
dependencies: sqflite: ^2.0.0+3 path: ^1.8.0
使用例
[編集]import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; class DatabaseHelper { static final DatabaseHelper instance = DatabaseHelper._init(); static Database? _database; DatabaseHelper._init(); Future<Database> get database async { if (_database != null) return _database!; _database = await _initDB('notes.db'); return _database!; } Future<Database> _initDB(String filePath) async { final dbPath = await getDatabasesPath(); final path = join(dbPath, filePath); return await openDatabase(path, version: 1, onCreate: _createDB); } Future<void> _createDB(Database db, int version) async { await db.execute(''' CREATE TABLE notes( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, createdAt TEXT NOT NULL ) '''); } Future<int> createNote(Note note) async { final db = await instance.database; return db.insert('notes', note.toJson()); } Future<Note?> readNote(int id) async { final db = await instance.database; final maps = await db.query( 'notes', columns: ['id', 'title', 'content', 'createdAt'], where: 'id = ?', whereArgs: [id], ); if (maps.isNotEmpty) { return Note.fromJson(maps.first); } else { return null; } } Future<List<Note>> readAllNotes() async { final db = await instance.database; final result = await db.query('notes'); return result.map((json) => Note.fromJson(json)).toList(); } Future<int> updateNote(Note note) async { final db = await instance.database; return db.update( 'notes', note.toJson(), where: 'id = ?', whereArgs: [note.id], ); } Future<int> deleteNote(int id) async { final db = await instance.database; return db.delete( 'notes', where: 'id = ?', whereArgs: [id], ); } } class Note { final int? id; final String title; final String content; final DateTime createdAt; Note({ this.id, required this.title, required this.content, required this.createdAt, }); Map<String, dynamic> toJson() => { 'id': id, 'title': title, 'content': content, 'createdAt': createdAt.toIso8601String(), }; static Note fromJson(Map<String, dynamic> json) => Note( id: json['id'] as int?, title: json['title'] as String, content: json['content'] as String, createdAt: DateTime.parse(json['createdAt'] as String), ); }
Firebase(オプション)
[編集]Firebaseは、Googleが提供するモバイルおよびウェブアプリケーション開発プラットフォームです。リアルタイムデータベース、認証、クラウドストレージなど、多くの機能を提供します。
まず、Firebase プロジェクトを設定し、必要な依存関係をpubspec.yaml
に追加します:
dependencies: firebase_core: ^2.1.0 firebase_auth: ^4.0.2 cloud_firestore: ^4.0.2
使用例(Firestore)
[編集]import 'package:cloud_firestore/cloud_firestore.dart'; class FirestoreService { final CollectionReference notesCollection = FirebaseFirestore.instance.collection('notes'); Future<void> addNote(String title, String content) { return notesCollection.add({ 'title': title, 'content': content, 'createdAt': FieldValue.serverTimestamp(), }); } Stream<QuerySnapshot> getNotes() { return notesCollection.orderBy('createdAt', descending: true).snapshots(); } Future<void> updateNote(String id, String title, String content) { return notesCollection.doc(id).update({ 'title': title, 'content': content, 'updatedAt': FieldValue.serverTimestamp(), }); } Future<void> deleteNote(String id) { return notesCollection.doc(id).delete(); } } // 使用例 void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); final firestoreService = FirestoreService(); // ノートを追加 await firestoreService.addNote('My First Note', 'This is the content of my first note.'); // ノートのストリームを取得 firestoreService.getNotes().listen((snapshot) { for (var doc in snapshot.docs) { print('${doc.id}: ${doc.data()}'); } }); }
これらのデータ永続化手法を理解し、適切に使用することで、アプリケーションのデータを効率的に管理し、オフライン機能を実装することができます。プロジェクトの要件に応じて、最適な手法を選択してください。次の章では、ネットワーキングについて学んでいきます。