【Dart/Flutter】Flutter Apprenticeのテクニック抜粋 - 備考2
無料で素晴らしい教育資料Flutter Apprenticeの中で、
個人的に覚えておきたいテクニックをソースコード部分と共に抜粋したものとなります。
順を追っての説明はありませんので、詳細はFlutter Apprentice にてご確認ください。
何か思い出す際等に、お役立ちできれば幸いです。
関連記事
【Dart/Flutter】Flutter Apprenticeのテクニック抜粋
【Dart/Flutter】Flutter Apprenticeのテクニック抜粋 - 備考1
【Dart/Flutter】Flutter Apprenticeのテクニック抜粋 - 備考2
【Dart/Flutter】Flutter Apprenticeのテクニック集
はじめに
-
【参考】Flutter Apprentice:公式の学習サイト(無料)
-
Flutter実行環境
sdk: ">=2.12.0 <3.0.0"
以下、範囲大きめなテクニック
httpパッケージ利用の簡単なクラス
- httpパッケージのgetを利用
import 'package:http/http.dart';
const String apiKey = '<Your Key>';
const String apiId = '<your ID>';
const String apiUrl = 'https://api.edamam.com/search';
class RecipeService {
Future getData(String url) async {
print('Calling uri: $url');
final response = await get(Uri.parse(url));
if (response.statusCode == 200) {
return response.body;
} else {
print(response.statusCode);
}
}
Future<dynamic> getRecipes(String query, int from, int to) async {
final recipeData = await getData(
'$apiUrl?app_id=$apiId&app_key=$apiKey&q=$query&from=$from&to=$to');
return recipeData;
}
}
ChopperによるGet実装
- httpパッケージを工夫して利用する、Chopper
- 準備
-
pubspec.yaml
へ、コード作成用dev_dependencies:
の追加も必要-
dependencies:
chopper: ^4.0.1
-
dev_dependencies:
build_runner: ^2.1.1
chopper_generator: ^4.0.1
-
-
- Chopper便利ポイント
- インターフェースを定義しただけでクライアントを呼び出す部分の実装を自動生成
-
# flutter pub run build_runner build --delete-conflicting-outputs
(上書き)等で、recipe_service.chopper.dart
にコードを生成
- 主な関連ファイル
-
lib/network/model_response.dart
-
https://github.com/raywenderlich/flta-materials/blob/editions/2.0/12-using-the-chopper-library/projects/final/lib/network/model_response.dart
- API結果のResponse情報として、SuccessとErrorクラスを作成
- 別クラスで、
Future<Response<Result<APIRecipeQuery>>> queryRecipes()
と利用
-
https://github.com/raywenderlich/flta-materials/blob/editions/2.0/12-using-the-chopper-library/projects/final/lib/network/model_response.dart
-
lib/network/recipe_service.dart
-
https://github.com/raywenderlich/flta-materials/blob/editions/2.0/12-using-the-chopper-library/projects/final/lib/network/recipe_service.dart
- インスタンスを作成する
create()
と、今回実装したいGet関数を定義
- インスタンスを作成する
-
https://github.com/raywenderlich/flta-materials/blob/editions/2.0/12-using-the-chopper-library/projects/final/lib/network/recipe_service.dart
-
lib/network/recipe_service.chopper.dart
-
lib/network/model_converter.dart
-
- 準備
lib/network/model_response.dart
/// 成功した応答またはエラーのいずれかを保持する汎用応答クラス
/// 必須ではありませんが、サーバーが返す応答の処理を容易にします
abstract class Result<T> {}
/// たとえば、これはJSONデータを保持
class Success<T> extends Result<T> {
final T value;
Success(this.value);
}
/// 間違った資格情報を使用したり、承認なしでデータをフェッチしようとしたりするなど、
/// HTTP呼び出し中に発生するエラーがモデル化
class Error<T> extends Result<T> {
final Exception exception;
Error(this.exception);
}
lib/network/recipe_service.dart
import 'package:chopper/chopper.dart';
import 'model_converter.dart';
import 'model_response.dart';
import 'recipe_model.dart';
/// 以下のコマンドで作成
/// flutter pub run build_runner build --delete-conflicting-outputs
part 'recipe_service.chopper.dart';
/// 自分でアカウント作成し、取得
// https://www.raywenderlich.com/books/flutter-apprentice/v2.0/chapters/11-networking-in-flutter
const String apiKey = '<Your Key Here>';
const String apiId = '<Your Id here>';
/// httpパッケージ利用時と異なり末尾「/search」が削除された
const String apiUrl = 'https://api.edamam.com';
/// 【アノテーション】
/// 「chopper_generator: ^4.0.1」によるpartファイル作成
/// このファイル名の末尾に「.chopper.dart」と付くファイルを作成
/// ChopperServiceクラスを継承したabstractクラスを定義する
()
abstract class RecipeService extends ChopperService {
/// ①ネットワーク呼び出しを行うための汎用インターフェースを定義
/// ※APIキーをリクエストに追加したり、
/// レスポンスをデータオブジェクトに変換したりするなどのタスクを実行する、
/// 実際のコードはありません。これはコンバーターとインターセプターの仕事
/// 【アノテーション】
/// GETリクエストであることをジェネレータに通知するアノテーション
/// 他には、@Post、@Put、@Deleteが存在
(path: 'search') // 末尾の'/search'はここで指定
/// httpパッケージ利用での、
/// Future getData(String url)と、
/// Future<dynamic> getRecipes(String query, int from, int to)が、
/// 合わさった感じ
Future<Response<Result<APIRecipeQuery>>> queryRecipes(
/// 【アノテーション】
/// @Queryアノテーションを使用して、query文字列fromとto整数を受け入れ
/// →中身は、ジェネレータがpartファイルに作成
('q') String query,
('from') int from,
('to') int to,
);
/// ③別途ModelConverter(リクエストとレスポンスの変換)と、インターセプター
/// を準備し、それを利用
static RecipeService create() {
/// クライアントを作成し、自動作成する「_$RecipeService(client)」に渡す
final client = ChopperClient(
baseUrl: apiUrl,
/// 2つのインターセプターを指定
/// 作成した_addQuery()と、HttpLoggingInterceptor
/// ※HttpLoggingInterceptorはログを出す用
interceptors: [_addQuery, HttpLoggingInterceptor()],
/// 作成したコンバータを指定
converter: ModelConverter(),
/// JsonConverterエラーをデコード
errorConverter: const JsonConverter(),
/// ジェネレータスクリプトの実行時に作成されるサービスを定義
services: [
_$RecipeService(),
],
);
/// 生成されたサービスのインスタンスを返します
return _$RecipeService(client);
}
}
/// ②IDとキーを自動的に含めるインターセプター
/// 基本的なクエリに追加して、何かクエリを追加が必要な場合に必要
Request _addQuery(Request req) {
/// 既存のRequestパラメーターからのキーと値のペアを取得
final params = Map<String, dynamic>.from(req.parameters);
/// 必要なものを追加
params['app_id'] = apiId;
params['app_key'] = apiKey;
/// 新規作成として、返す
return req.copyWith(parameters: params);
}
lib/network/recipe_service.chopper.dart
※自動作成ファイル
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'recipe_service.dart';
// **************************************************************************
// ChopperGenerator
// **************************************************************************
// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations
class _$RecipeService extends RecipeService {
_$RecipeService([ChopperClient? client]) {
if (client == null) return;
this.client = client;
}
final definitionType = RecipeService;
Future<Response<Result<APIRecipeQuery>>> queryRecipes(
String query, int from, int to) {
final $url = 'search';
final $params = <String, dynamic>{'q': query, 'from': from, 'to': to};
final $request = Request('GET', $url, client.baseUrl, parameters: $params);
return client.send<Result<APIRecipeQuery>, APIRecipeQuery>($request);
}
}
lib/network/model_converter.dart
import 'dart:convert';
import 'package:chopper/chopper.dart';
import 'model_response.dart';
import 'recipe_model.dart';
/// リクエストとレスポンスの変換
/// コンバーターをChopperクライアントに接続するには、インターセプターが必要
/// インターセプターは、要求を送信したり応答を受信したりするたびに実行される関数
/// →少し多機能なencode,decode
///
/// Converterをimplementsして実装(必要な関数をオーバーライド)
class ModelConverter implements Converter {
/// オーバーライド①
/// リクエストを受け取り、新しいリクエストを返す
Request convertRequest(Request request) {
/// jsonの処理であることを教える
final req = applyHeader(
request,
contentTypeKey,
jsonHeaders,
override: false,
);
return encodeJson(req);
}
/// JSONのエンコード
Request encodeJson(Request request) {
final contentType = request.headers[contentTypeKey];
if (contentType != null && contentType.contains(jsonHeaders)) {
/// 基本的には、リクエストのコピーを作成し、返す
return request.copyWith(body: json.encode(request.body));
}
return request;
}
/// JSONのエンコード
/// サーバーの応答は通常文字列であるため、
/// JSON文字列を解析してAPIRecipeQueryモデルクラスに変換する必要がある
Response<BodyType> decodeJson<BodyType, InnerType>(Response response) {
final contentType = response.headers[contentTypeKey];
var body = response.body;
/// JSONを扱っていることを確認し、文字列にデコード
if (contentType != null && contentType.contains(jsonHeaders)) {
body = utf8.decode(response.bodyBytes);
}
try {
/// 文字列をMapに変換
final mapData = json.decode(body);
if (mapData['status'] != null) {
return response.copyWith<BodyType>(
body: Error(Exception(mapData['status'])) as BodyType);
}
/// Mapを扱いたいモデルクラスに変換
final recipeQuery = APIRecipeQuery.fromJson(mapData);
/// 【成功動作】
/// 扱いたいモデルクラスを持ったSuccess<T>クラスのインスタンスとして返す
return response.copyWith<BodyType>(
body: Success(recipeQuery) as BodyType,
);
} catch (e) {
chopperLogger.warning(e);
return response.copyWith<BodyType>(
body: Error(e as Exception) as BodyType,
);
}
}
/// オーバーライド②
/// 指定された応答を必要な応答に変更(上のdecodeJsonを呼ぶのみ)
Response<BodyType> convertResponse<BodyType, InnerType>(Response response) {
return decodeJson<BodyType, InnerType>(response);
}
}
abstractを利用した継承(extends):アプリ起動時のみ情報を保存するChangeNotifierクラス
- 何か情報を扱うためのクラスのインターフェイス
- 主な関連ファイル
-
lib/data/repository.dart
-
lib/data/memory_repository.dart
-
- 主な関連ファイル
lib/data/repository.dart
import 'models/models.dart';
/// レシピと材料を追加および削除するメソッドを提供するリポジトリインターフェイス
/// これをextendsさせれば、必要な機能(関数)が分かる
abstract class Repository {
/// 基本4つ:見つける
List<Recipe> findAllRecipes();
Recipe findRecipeById(int id);
List<Ingredient> findAllIngredients();
List<Ingredient> findRecipeIngredients(int recipeId);
/// 応用8つ:追加、削除、初期化、終了
int insertRecipe(Recipe recipe);
List<int> insertIngredients(List<Ingredient> ingredients);
void deleteRecipe(Recipe recipe);
void deleteIngredient(Ingredient ingredient);
void deleteIngredients(List<Ingredient> ingredients);
void deleteRecipeIngredients(int recipeId);
Future init();
void close();
}
lib/data/memory_repository.dart
import 'dart:core';
import 'package:flutter/foundation.dart';
import 'models/models.dart';
import 'repository.dart';
/// 材料をメモリに保存する場所
/// アプリを再起動するたびに失われるため、これは一時的な保存
///
/// 用意したabstractクラスのRepositoryを継承
/// (必要な関数をオーバーライドしないとエラー)
class MemoryRepository extends Repository with ChangeNotifier {
/// レシピと、食材情報を管理
final List<Recipe> _currentRecipes = <Recipe>[];
final List<Ingredient> _currentIngredients = <Ingredient>[];
List<Recipe> findAllRecipes() {
return _currentRecipes;
}
Recipe findRecipeById(int id) {
return _currentRecipes.firstWhere((recipe) => recipe.id == id);
}
List<Ingredient> findAllIngredients() {
return _currentIngredients;
}
List<Ingredient> findRecipeIngredients(int recipeId) {
final recipe =
_currentRecipes.firstWhere((recipe) => recipe.id == recipeId);
/// where指定されたレシピIDを持つすべての材料を検索し、リスト化
final recipeIngredients = _currentIngredients
.where((ingredient) => ingredient.recipeId == recipe.id)
.toList();
return recipeIngredients;
}
/// このクラスで管理している変数に追加や削除をする場合は、
/// notifyListeners()を実行
int insertRecipe(Recipe recipe) {
_currentRecipes.add(recipe);
if (recipe.ingredients != null) {
insertIngredients(recipe.ingredients!);
}
notifyListeners();
/// 新しいレシピのIDを返します。現在は、必要ないので、常に0を返します
return 0;
}
List<int> insertIngredients(List<Ingredient> ingredients) {
if (ingredients.length != 0) {
_currentIngredients.addAll(ingredients);
notifyListeners();
}
/// 追加されたIDのリストを返します。今のところ空のリスト。
return <int>[];
}
void deleteRecipe(Recipe recipe) {
_currentRecipes.remove(recipe);
if (recipe.id != null) {
deleteRecipeIngredients(recipe.id!);
}
notifyListeners();
}
void deleteIngredient(Ingredient ingredient) {
_currentIngredients.remove(ingredient);
}
void deleteIngredients(List<Ingredient> ingredients) {
/// 渡されたリストにあるすべての材料を削除
_currentIngredients
.removeWhere((ingredient) => ingredients.contains(ingredient));
notifyListeners();
}
void deleteRecipeIngredients(int recipeId) {
_currentIngredients
.removeWhere((ingredient) => ingredient.recipeId == recipeId);
notifyListeners();
}
/// 一時的なものなので、初期化やクローズ時に
/// 特に何もしない
Future init() {
return Future.value(null);
}
void close() {}
}
Mock活用:2種類のjsonファイルからランダムに読み出す
- Mock:2種類のjsonファイルからランダムに読み出す
- 主な関連ファイル
-
lib/mock_service/mock_service.dart
-
https://github.com/raywenderlich/flta-materials/blob/editions/2.0/13-state-management/projects/final/lib/mock_service/mock_service.dart
- 何か管理して、状態を変更させるクラスではなので、ChangeNotifierProviderではなく、通常のクラス(Provider利用)
-
https://github.com/raywenderlich/flta-materials/blob/editions/2.0/13-state-management/projects/final/lib/mock_service/mock_service.dart
-
main.dart
-
https://github.com/raywenderlich/flta-materials/blob/f7d9e37d7c4212517e8f4271244e89ec2d3610f2/13-state-management/projects/final/lib/main.dart#L35
-
MockService()..create()
カスケード演算子により、MockServiceインスタンスを作成後に関数実行 - lazyfalseに設定すると、必要になるまで待つのではなく、すぐにリポジトリが作成
- リポジトリが起動するためにバックグラウンド作業を行う必要がある場合に役立つ
-
-
https://github.com/raywenderlich/flta-materials/blob/f7d9e37d7c4212517e8f4271244e89ec2d3610f2/13-state-management/projects/final/lib/main.dart#L35
-
- 主な関連ファイル
lib/mock_service/mock_service.dart
import 'dart:convert';
import 'dart:math';
import 'package:http/http.dart' as http;
import 'package:chopper/chopper.dart';
import 'package:flutter/services.dart' show rootBundle;
import '../network/model_response.dart';
import '../network/recipe_model.dart';
/// コードの変更を試みるたびに、
/// 実際のWebサーバーにリクエストを送信するのは良い考えではありません
/// 実際のAPIを模倣する特定の応答を返すモックサービスを構築
class MockService {
/// 2種類のJSONファイルからロードされた結果を保存
late APIRecipeQuery _currentRecipes1;
late APIRecipeQuery _currentRecipes2;
/// 2種類を用意し、ランダムで1つを返す
Random nextRecipe = Random();
/// プロバイダの設定時に、「create: (_) => MockService()..create(),」
/// と呼ぶ
void create() {
loadRecipes();
}
void loadRecipes() async {
var jsonString = await rootBundle.loadString('assets/recipes1.json');
_currentRecipes1 = APIRecipeQuery.fromJson(jsonDecode(jsonString));
jsonString = await rootBundle.loadString('assets/recipes2.json');
_currentRecipes2 = APIRecipeQuery.fromJson(jsonDecode(jsonString));
}
/// Chopperをインポートして、Responseのインスタンスを作成します
/// 以下のMockを作成したい
/// @Get(path: 'search')
/// Future<Response<Result<APIRecipeQuery>>> queryRecipes(
/// @Query('q') String query, @Query('from') int from, @Query('to') int to);
Future<Response<Result<APIRecipeQuery>>> queryRecipes(
String query, int from, int to) {
/// queryRecipesが呼び出される際にランダムに2種類の内から選択
switch (nextRecipe.nextInt(2)) {
case 0:
/// Response<Result<APIRecipeQuery>>として、
return Future.value(
Response(
/// Header:ステータス200の正常なレスポンスとしてのダミー
http.Response('Dummy', 200, request: null),
/// Body:Result<APIRecipeQuery>
Success<APIRecipeQuery>(_currentRecipes1),
),
);
case 1:
return Future.value(Response(http.Response('Dummy', 200, request: null),
Success<APIRecipeQuery>(_currentRecipes2)));
default:
return Future.value(Response(http.Response('Dummy', 200, request: null),
Success<APIRecipeQuery>(_currentRecipes1)));
}
}
}
main.dart
抜粋
return MultiProvider(
providers: [
Provider(
create: (_) => MockService()..create(),
lazy: false,
),
],
abstractを利用した継承(extends):Streamを活用するインターフェイスと管理するクラス
- Streamを管理するためのMemoryRepository
- 主な関連ファイル
-
lib/data/repository.dart
-
lib/data/memory_repository.dart
-
lib/main.dart
-
- 主な関連ファイル
lib/data/repository.dart
import 'models/models.dart';
/// Stream
/// データに加えてエラー送信可能
/// 必要に応じて、ストリームを停止することもできます
///
/// 2つの異なる場所でストリームを使用
/// →MyRecipesListと、ShoppingListで利用
abstract class Repository {
/// 元々Futureのinitと、close以外にFutureか、Streamを追加
/// データベースまたはネットワークからのデータを処理するために
/// 非同期で機能するメソッドを使用できます
/// ※実際にFutureでなくとも、動作しているように見える
Future<List<Recipe>> findAllRecipes();
/// Streamの関数を2つ追加
/// →レシピと食材用
Stream<List<Recipe>> watchAllRecipes();
Stream<List<Ingredient>> watchAllIngredients();
/// インターフェイスとして置いているので、
/// 実際のUI上で利用していない関数もあり
Future<Recipe> findRecipeById(int id);
Future<List<Ingredient>> findAllIngredients();
Future<List<Ingredient>> findRecipeIngredients(int recipeId);
Future<int> insertRecipe(Recipe recipe);
Future<List<int>> insertIngredients(List<Ingredient> ingredients);
Future<void> deleteRecipe(Recipe recipe);
Future<void> deleteIngredient(Ingredient ingredient);
Future<void> deleteIngredients(List<Ingredient> ingredients);
Future<void> deleteRecipeIngredients(int recipeId);
Future init();
void close();
}
lib/data/memory_repository.dart
import 'dart:async';
import 'dart:core';
import 'models/models.dart';
import 'repository.dart';
/// 「with ChangeNotifier」を削除
/// ChangeNotifierProviderからProvider利用へ
/// (Stream利用としたため、追加・削除時にnotifyListeners()不要)
class MemoryRepository extends Repository {
final List<Recipe> _currentRecipes = <Recipe>[];
final List<Ingredient> _currentIngredients = <Ingredient>[];
/// Stream定義
/// ストリームが最初に要求されたときにキャプチャされます。
/// これにより、呼び出しごとに新しいストリームが作成されなくなります。
Stream<List<Recipe>>? _recipeStream;
Stream<List<Ingredient>>? _ingredientStream;
/// 【StreamControllerとStreamSink】
/// シンクはデータの宛先:
/// ストリームにデータを追加する場合は、シンクに追加
/// StreamControllerはシンクを所有
/// シンク上のデータをリッスンし、そのデータをストリームリスナーに送信
/// MemoryRepositoryでは、レシピと食材のStreamと、StreamControllerを定義し、
/// それらを用いて、シンクに追加コードを記載
/// (listenは別の受けて側で記載 = listenの便利な記載方法がFlutterのStreamBuilder)
/// StreamController定義
final StreamController _recipeStreamController =
StreamController<List<Recipe>>();
final StreamController _ingredientStreamController =
StreamController<List<Ingredient>>();
/// 新しいStreamを返す関数を二つ追加
Stream<List<Recipe>> watchAllRecipes() {
/// すでにStreamがあれば、既存を返却
if (_recipeStream == null) {
/// StreamControllerを利用して、シンク上のデータをリッスン
_recipeStream = _recipeStreamController.stream as Stream<List<Recipe>>;
}
return _recipeStream!;
}
Stream<List<Ingredient>> watchAllIngredients() {
/// すでにStreamがあれば、既存を返却
if (_ingredientStream == null) {
/// StreamControllerを利用して、シンク上のデータをリッスン
_ingredientStream =
_ingredientStreamController.stream as Stream<List<Ingredient>>;
}
return _ingredientStream!;
}
/// 既存関数で、RepositoryクラスのインターフェイスにFutureを付けたので追加
Future<List<Recipe>> findAllRecipes() {
return Future.value(_currentRecipes);
}
Future<Recipe> findRecipeById(int id) {
return Future.value(
_currentRecipes.firstWhere((recipe) => recipe.id == id));
}
Future<List<Ingredient>> findAllIngredients() {
return Future.value(_currentIngredients);
}
Future<List<Ingredient>> findRecipeIngredients(int recipeId) {
final recipe =
_currentRecipes.firstWhere((recipe) => recipe.id == recipeId);
final recipeIngredients = _currentIngredients
.where((ingredient) => ingredient.recipeId == recipe.id)
.toList();
return Future.value(recipeIngredients);
}
Future<int> insertRecipe(Recipe recipe) {
_currentRecipes.add(recipe);
/// データの宛先であるシンクに追加
_recipeStreamController.sink.add(_currentRecipes);
if (recipe.ingredients != null) {
insertIngredients(recipe.ingredients!);
}
return Future.value(0);
}
Future<List<int>> insertIngredients(List<Ingredient> ingredients) {
if (ingredients.length != 0) {
_currentIngredients.addAll(ingredients);
/// データの宛先であるシンクに追加
_ingredientStreamController.sink.add(_currentIngredients);
}
return Future.value(<int>[]);
}
Future<void> deleteRecipe(Recipe recipe) {
_currentRecipes.remove(recipe);
/// データの宛先であるシンクに追加(削除の結果)
/// リスト自体をaddする
/// →ストリームが単一の値ではなくリストを期待しているためです。
/// →このようにすることで、前のリストを更新されたリストに置き換えます
_recipeStreamController.sink.add(_currentRecipes);
if (recipe.id != null) {
deleteRecipeIngredients(recipe.id!);
}
return Future.value();
}
Future<void> deleteIngredient(Ingredient ingredient) {
_currentIngredients.remove(ingredient);
/// データの宛先であるシンクに追加(削除の結果)
_ingredientStreamController.sink.add(_currentIngredients);
return Future.value();
}
Future<void> deleteIngredients(List<Ingredient> ingredients) {
_currentIngredients
.removeWhere((ingredient) => ingredients.contains(ingredient));
/// データの宛先であるシンクに追加(削除の結果)
_ingredientStreamController.sink.add(_currentIngredients);
return Future.value();
}
Future<void> deleteRecipeIngredients(int recipeId) {
_currentIngredients
.removeWhere((ingredient) => ingredient.recipeId == recipeId);
/// データの宛先であるシンクに追加(削除の結果)
_ingredientStreamController.sink.add(_currentIngredients);
return Future.value();
}
Future init() {
/// 「return Future.value(null);」からnullを削除
return Future.value();
}
void close() {
/// Stream終了後に閉じる
_recipeStreamController.close();
_ingredientStreamController.close();
}
}
lib/main.dart
抜粋
return MultiProvider(
providers: [
/// ChangeNotifierProviderからProviderへ
/// (Stream利用としたため、追加・削除時にnotifyListeners()不要)
Provider<Repository>(
lazy: false,
create: (_) => MemoryRepository(),
),
],
Interface(implements)を利用した本番APIとMockと使い分け
- インターフェイスで本番APIとMockの使い分け(料理レシピをGetするAPI)
- ポイント
- ServiceInterfaceにより、本番検索とMock検索の切り替えがProvider設定の1行のみ
create: (_) => RecipeService.create(),
MockService()..create(),
- 同名の関数をオーバーライドしているので、FutureBuilder等の利用時の書き方は同様
Provider.of<ServiceInterface>(context).queryRecipes()
- ServiceInterfaceにより、本番検索とMock検索の切り替えがProvider設定の1行のみ
- 主な関連ファイル
-
lib/network/service_interface.dart
-
lib/network/recipe_service.dart
-
lib/mock_service/mock_service.dart
-
lib/main.dart
-
- ポイント
lib/network/service_interface.dart
import 'package:chopper/chopper.dart';
import 'model_response.dart';
import 'recipe_model.dart';
/// インターフェイスを作成することで、
/// 実際のレシピ検索:MemoryRepository
/// Mockのレシピ検索:RecipeService
/// の共存・切り替えが簡単になる
abstract class ServiceInterface {
Future<Response<Result<APIRecipeQuery>>> queryRecipes(
String query, int from, int to);
}
lib/mock_service/mock_service.dart
import 'dart:convert';
import 'dart:math';
import 'package:chopper/chopper.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:http/http.dart' as http;
import '../network/model_response.dart';
import '../network/recipe_model.dart';
import '../network/service_interface.dart';
/// 「implements ServiceInterface」で検索関数をオーバーライド
class MockService implements ServiceInterface {
late APIRecipeQuery _currentRecipes1;
late APIRecipeQuery _currentRecipes2;
Random nextRecipe = Random();
void create() {
loadRecipes();
}
void loadRecipes() async {
var jsonString = await rootBundle.loadString('assets/recipes1.json');
_currentRecipes1 = APIRecipeQuery.fromJson(jsonDecode(jsonString));
jsonString = await rootBundle.loadString('assets/recipes2.json');
_currentRecipes2 = APIRecipeQuery.fromJson(jsonDecode(jsonString));
}
/// オーバーライド
Future<Response<Result<APIRecipeQuery>>> queryRecipes(
String query, int from, int to) {
switch (nextRecipe.nextInt(2)) {
case 0:
return Future.value(Response(http.Response('Dummy', 200, request: null),
Success<APIRecipeQuery>(_currentRecipes1)));
case 1:
return Future.value(Response(http.Response('Dummy', 200, request: null),
Success<APIRecipeQuery>(_currentRecipes2)));
default:
return Future.value(Response(http.Response('Dummy', 200, request: null),
Success<APIRecipeQuery>(_currentRecipes1)));
}
}
}
lib/network/recipe_service.dart
import 'package:chopper/chopper.dart';
import 'model_converter.dart';
import 'model_response.dart';
import 'recipe_model.dart';
import 'service_interface.dart';
part 'recipe_service.chopper.dart';
/// 自分でアカウント作成し、取得
// https://www.raywenderlich.com/books/flutter-apprentice/v2.0/chapters/11-networking-in-flutter
const String apiKey = '<Your Key Here>';
const String apiId = '<Your Id here>';
const String apiUrl = 'https://api.edamam.com';
/// 「implements ServiceInterface」で検索関数をオーバーライド
()
abstract class RecipeService extends ChopperService
implements ServiceInterface {
/// オーバーライド
(path: 'search')
Future<Response<Result<APIRecipeQuery>>> queryRecipes(
('q') String query, ('from') int from, ('to') int to);
static RecipeService create() {
final client = ChopperClient(
baseUrl: apiUrl,
interceptors: [_addQuery, HttpLoggingInterceptor()],
converter: ModelConverter(),
errorConverter: const JsonConverter(),
services: [
_$RecipeService(),
],
);
return _$RecipeService(client);
}
}
Request _addQuery(Request req) {
final params = Map<String, dynamic>.from(req.parameters);
params['app_id'] = apiId;
params['app_key'] = apiKey;
return req.copyWith(parameters: params);
}
lib/main.dart
抜粋
return MultiProvider(
providers: [
/// インターフェイスを作成することで、
/// 実際の検索と、Mock検索の切り替えが簡単に!
/// 本番
Provider<ServiceInterface>(
create: (_) => RecipeService.create(),
lazy: false,
),
/// Mock(※本番を利用の際は、コメントアウト)
// Provider<ServiceInterface>(
// create: (_) => MockService()..create(),
// lazy: false,
// ),
],
abstractを利用した継承(extends):DatabaseにsqfliteかMoorか選択
-
Saving Data With SQLite
-
SQLiteの注意
- Flutter Webでは保存できない
-
SQLiteデータベースを処理
- sqfliteプラグインと、簡単なMoorパッケージが存在
- sqfliteはプラグインであり、プラットフォーム固有のコードが必要なため通常のパッケージではない
- Dart パッケージ: Dart で書かれたパッケージ
- いくつかのものは Flutter 固有の機能を持ち、したがって Flutter フレームワークに依存し Flutter のためにのみ使用
- プラグインパッケージ: Dart で書かれた特別なパッケージ
- Android (using Java or Kotlin) や iOS (using ObjC or Swift) などプラットフォーム固有の実装を含む
- Dart パッケージ: Dart で書かれたパッケージ
-
ポイント
- DatabaseにsqfliteかMoorか選択
- mainの中で以下どちらかを選択
final repository = SqliteRepository();
final repository = MoorRepository();
- mainの中で以下どちらかを選択
- DatabaseにsqfliteかMoorか選択
-
準備(Moor)
-
pubspec.yaml
へ、コード作成用dev_dependencies:
の追加も必要※sqlbrite等とは別に-
dependencies:
moor_flutter: ^4.0.0
-
dev_dependencies:
build_runner: ^2.1.1
moor_generator: ^4.4.1
-
-
-
主な関連ファイル
-
lib/data/repository.dart
-
lib/data/sqlite/sqlite_repository.dart
【データベース:sqflite関連】 -
lib/data/sqlite/database_helper.dart
【データベース:sqflite関連】-
https://github.com/raywenderlich/flta-materials/blob/editions/2.0/15-saving-data-with-sqlite/projects/final/lib/data/sqlite/database_helper.dart
-
Future _onCreate(Database db, int version) async {}
でテーブルを作成する記載がかなり手動的なデメリット
-
-
https://github.com/raywenderlich/flta-materials/blob/editions/2.0/15-saving-data-with-sqlite/projects/final/lib/data/sqlite/database_helper.dart
-
lib/data/moor/moor_repository.dart
【データベース:Moor関連】 -
lib/data/moor/moor_db.dart
【データベース:Moor関連】 -
lib/data/moor/moor_db.g.dart
【データベース:Moor関連】※自動作成 -
lib/main.dart
-
-
lib/data/repository.dart
import 'models/models.dart';
/// 前章までは、MemoryRepositoryがのみがextendsし、
/// 一時的な保存のみに利用していた。
/// 今回は、sqflite利用ではSqliteRepositoryが、
/// Moor利用ではMoorRepositoryがextendsし、
/// 永続保存を使い分ける
///
/// レシピと、食材情報を管理するRepositoryクラスを作成し、
/// そのRepositoryがDatabaseへの追加や削除も実施する
abstract class Repository {
Future<List<Recipe>> findAllRecipes();
Stream<List<Recipe>> watchAllRecipes();
Stream<List<Ingredient>> watchAllIngredients();
Future<Recipe> findRecipeById(int id);
Future<List<Ingredient>> findAllIngredients();
Future<List<Ingredient>> findRecipeIngredients(int recipeId);
Future<int> insertRecipe(Recipe recipe);
Future<List<int>> insertIngredients(List<Ingredient> ingredients);
Future<void> deleteRecipe(Recipe recipe);
Future<void> deleteIngredient(Ingredient ingredient);
Future<void> deleteIngredients(List<Ingredient> ingredients);
Future<void> deleteRecipeIngredients(int recipeId);
Future init();
void close();
}
lib/data/sqlite/sqlite_repository.dart
【データベース:sqflite関連】
import 'dart:async';
import '../models/models.dart';
import '../repository.dart';
import 'database_helper.dart';
/// 【sqfliteを使用する場合】
/// データベースのすべてのテーブルを手動で作成し、
/// SQLステートメントを手動でまとめる必要があります。
class SqliteRepository extends Repository {
/// 別途作成のDatabaseHelperのインスタンスを作成することにより、
/// まず、データベースを作成
/// このファイルでdbHelperの関数を呼び出す書き方にすることで、
/// データベース利用をしていない一時保存の際と大差がない見た目となる
final dbHelper = DatabaseHelper.instance;
Future<List<Recipe>> findAllRecipes() {
return dbHelper.findAllRecipes();
}
Stream<List<Recipe>> watchAllRecipes() {
return dbHelper.watchAllRecipes();
}
Stream<List<Ingredient>> watchAllIngredients() {
return dbHelper.watchAllIngredients();
}
Future<Recipe> findRecipeById(int id) {
return dbHelper.findRecipeById(id);
}
Future<List<Ingredient>> findAllIngredients() {
return dbHelper.findAllIngredients();
}
Future<List<Ingredient>> findRecipeIngredients(int id) {
return dbHelper.findRecipeIngredients(id);
}
/// 挿入メソッドはもう少し多くのことを行います
/// recipeId各材料にを設定するには、
/// 最初にレシピをデータベースに挿入し、
/// 挿入呼び出しから返されたレシピIDを取得する必要があります
/// =ここで初めてrecipeのidを設定しているので、
/// ブックマークの削除機能が機能する
Future<int> insertRecipe(Recipe recipe) {
return Future(
() async {
final id = await dbHelper.insertRecipe(recipe);
recipe.id = id;
if (recipe.ingredients != null) {
recipe.ingredients!.forEach((ingredient) {
ingredient.recipeId = id;
});
/// レシピの食材を追加
insertIngredients(recipe.ingredients!);
}
return id;
},
);
}
Future<List<int>> insertIngredients(List<Ingredient> ingredients) {
return Future(
() async {
if (ingredients.length != 0) {
final ingredientIds = <int>[];
/// forEachのawaitの使い方
await Future.forEach(ingredients, (Ingredient ingredient) async {
final futureId = await dbHelper.insertIngredient(ingredient);
/// 食材にもidを一つずつ付ける
ingredient.id = futureId;
ingredientIds.add(futureId);
});
return Future.value(ingredientIds);
} else {
return Future.value(<int>[]);
}
},
);
}
Future<void> deleteRecipe(Recipe recipe) {
dbHelper.deleteRecipe(recipe);
if (recipe.id != null) {
/// レシピの食材を削除
deleteRecipeIngredients(recipe.id!);
}
return Future.value();
}
Future<void> deleteIngredient(Ingredient ingredient) {
dbHelper.deleteIngredient(ingredient);
return Future.value();
}
Future<void> deleteIngredients(List<Ingredient> ingredients) {
dbHelper.deleteIngredients(ingredients);
return Future.value();
}
Future<void> deleteRecipeIngredients(int recipeId) {
dbHelper.deleteRecipeIngredients(recipeId);
return Future.value();
}
Future init() async {
await dbHelper.database;
return Future.value();
}
void close() {
dbHelper.close();
}
}
lib/data/sqlite/database_helper.dart
【データベース:sqflite関連】
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqlbrite/sqlbrite.dart';
import 'package:synchronized/synchronized.dart';
import '../models/models.dart';
/// このクラスは、すべてのSQLiteデータベース操作を処理
class DatabaseHelper {
static const _databaseName = 'MyRecipes.db';
static const _databaseVersion = 1;
static const recipeTable = 'Recipe';
static const ingredientTable = 'Ingredient';
static const recipeId = 'recipeId';
static const ingredientId = 'ingredientId';
/// sqlbriteライブラリは、sqfliteのリアクティブストリームラッパー
/// データベースに変更があったときにイベントを受信できるように、Streamを設定
/// BriteDatabaseクラスのインスタンスに、queryやinsert関数が存在
static late BriteDatabase _streamDatabase;
/// シングルトンとは シングルトンクラスは、
/// あるクラスのインスタンスが2つ以上生成されないことを保証する
/// デザイン・パターンの1つ
/// ※データベース管理のインスタンスは一つのみが正しい
// make this a singleton class
DatabaseHelper._privateConstructor();
/// 「 instance.streamDatabase」と関数内でシングルトン利用
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
/// 同時アクセスを防止(synchronizedパッケージ)
static var lock = Lock();
/// データベースインスタンス
// only have a single app-wide reference to the database
static Database? _database;
/// db,versionを引数に持つ
/// REALはdouble値
/// idをPRIMARY KEYとする
// SQL code to create the database table
Future _onCreate(Database db, int version) async {
/// レシピテーブル
await db.execute('''
CREATE TABLE $recipeTable (
$recipeId INTEGER PRIMARY KEY,
label TEXT,
image TEXT,
url TEXT,
calories REAL,
totalWeight REAL,
totalTime REAL
)
''');
/// 食材テーブル
// 3
await db.execute('''
CREATE TABLE $ingredientTable (
$ingredientId INTEGER PRIMARY KEY,
$recipeId INTEGER,
name TEXT,
weight REAL
)
''');
}
/// データベースを利用する前に、初期化する用
// this opens the database (and creates it if it doesn't exist)
Future<Database> _initDatabase() async {
/// データベースを保存するアプリドキュメントのディレクトリ名を取得
/// (path_providerパッケージ)
final documentsDirectory = await getApplicationDocumentsDirectory();
/// データベース名をディレクトリパスに追加して、データベースへのパスを作成
final path = join(documentsDirectory.path, _databaseName);
/// デバッグをオンにします。
/// アプリをストアにデプロイする準備ができたら、
/// これをオフにすることを忘れないでください。
Sqflite.setDebugModeOn(true);
/// sqfliteの関数で、データベースファイルを作成:パスと、バージョン番号、作成関数
return openDatabase(path, version: _databaseVersion, onCreate: _onCreate);
}
/// _databaseはプライベートなので、データベースを初期化するgetterを作成
Future<Database> get database async {
if (_database != null) return _database!;
// Use this object to prevent concurrent access to data
await lock.synchronized(
() async {
// lazily instantiate the db the first time it is accessed
if (_database == null) {
_database = await _initDatabase();
/// 初期化後に、BriteDatabaseデータベースをラップして、インスタンスを作成
/// (Streamとして変更を随時受け付ける用)
_streamDatabase = BriteDatabase(_database!);
}
},
);
return _database!;
}
/// _streamDatabaseのgetter
Future<BriteDatabase> get streamDatabase async {
/// データベースを取得(存在しなければ作成)
await database;
return _streamDatabase;
}
/// SQLデータをJSONに、またはその逆に変換するためのヘルパーメソッドをいくつか追加
/// RecipeとIngredientクラスにも個別のjson変換を追加
///
/// レシピテーブルから受けとったデータをRecipeクラスのリストに変換
List<Recipe> parseRecipes(List<Map<String, dynamic>> recipeList) {
final recipes = <Recipe>[];
recipeList.forEach(
(recipeMap) {
final recipe = Recipe.fromJson(recipeMap);
recipes.add(recipe);
},
);
return recipes;
}
/// 食材テーブルから受けとったデータをIngredientクラスのリストに変換
List<Ingredient> parseIngredients(List<Map<String, dynamic>> ingredientList) {
final ingredients = <Ingredient>[];
ingredientList.forEach(
(ingredientMap) {
final ingredient = Ingredient.fromJson(ingredientMap);
ingredients.add(ingredient);
},
);
return ingredients;
}
/// 以下からはこれまで通りRepositoryクラスに必要な関数を実装
/// (データベース利用版)
///
Future<List<Recipe>> findAllRecipes() async {
final db = await instance.streamDatabase;
/// すべてのレシピをMap情報として取得
final recipeList = await db.query(recipeTable);
final recipes = parseRecipes(recipeList);
return recipes;
}
/// yield*を利用するため、async*
Stream<List<Recipe>> watchAllRecipes() async* {
final db = await instance.streamDatabase;
/// 「yield*」を付けることで、Stream<List<Recipe>>の返す
/// ※無いとエラー:The type 'Stream<Stream<List<Recipe>>>' implied by the 'yield' expression must be assignable to 'Stream<List<Recipe>>'.
/// yieldは式を計算し、結果の値を渡す(複数回実行できるreturnのようなもの)
/// yield*を用いて、Streamへの値渡しを一時停止することが可能
/// (Streamの中でStream別関数を呼ぶ )
///
/// createQueryでレシピテーブルを取得することで、便利なmapToList関数利用
/// 行ごとに、その行のレシピMapをRecipeのリストに変換
yield* db.createQuery(recipeTable).mapToList((row) => Recipe.fromJson(row));
}
/// ☝と同様
Stream<List<Ingredient>> watchAllIngredients() async* {
final db = await instance.streamDatabase;
yield* db
.createQuery(ingredientTable)
.mapToList((row) => Ingredient.fromJson(row));
}
Future<Recipe> findRecipeById(int id) async {
final db = await instance.streamDatabase;
/// 特定idのレシピをMap情報として取得
final recipeList = await db.query(recipeTable, where: 'id = $id');
/// 複数ある場合は最初のものを選択
final recipes = parseRecipes(recipeList);
return recipes.first;
}
Future<List<Ingredient>> findAllIngredients() async {
final db = await instance.streamDatabase;
/// 全ての食材をMap情報として取得
final ingredientList = await db.query(ingredientTable);
final ingredients = parseIngredients(ingredientList);
return ingredients;
}
Future<List<Ingredient>> findRecipeIngredients(int recipeId) async {
final db = await instance.streamDatabase;
/// 特定recipeIdの食材をMap情報として取得
final ingredientList =
await db.query(ingredientTable, where: 'recipeId = $recipeId');
final ingredients = parseIngredients(ingredientList);
return ingredients;
}
/// レシピと食材共通のinsert
/// テーブル名とJSONマップを使用して挿入
Future<int> insert(String table, Map<String, dynamic> row) async {
final db = await instance.streamDatabase;
/// 最終行に追加し、その行数が返される(追加位置が分かる:1~)
/// ※ブックマークした中間のレシピを削除した後に、再度追加するとバグりそう
return db.insert(table, row);
}
Future<int> insertRecipe(Recipe recipe) {
return insert(recipeTable, recipe.toJson());
}
Future<int> insertIngredient(Ingredient ingredient) {
return insert(ingredientTable, ingredient.toJson());
}
/// レシピと食材共通の_delete
/// テーブル名とcolumnId(recipeId or ingredientId), idが必要
Future<int> _delete(String table, String columnId, int id) async {
final db = await instance.streamDatabase;
/// レシピの場合は、recipeIdのrecipe.idのものを削除
/// whereArgsには、削除するデータのwhere条件を「?」を使って
/// パラメータで指定した場合のパラメータ値をString配列で指定します。
/// WHERE条件に「?」パラメータが無い場合は、nullを指定します。
/// 以下でも動作したが、可読性優先
/// return db.delete(table, where: '$columnId = $id');
return db.delete(table, where: '$columnId = ?', whereArgs: [id]);
}
Future<int> deleteRecipe(Recipe recipe) async {
if (recipe.id != null) {
return _delete(recipeTable, recipeId, recipe.id!);
} else {
return Future.value(-1);
}
}
Future<int> deleteIngredient(Ingredient ingredient) async {
if (ingredient.id != null) {
return _delete(ingredientTable, ingredientId, ingredient.id!);
} else {
return Future.value(-1);
}
}
Future<void> deleteIngredients(List<Ingredient> ingredients) {
ingredients.forEach((ingredient) {
if (ingredient.id != null) {
_delete(ingredientTable, ingredientId, ingredient.id!);
}
});
return Future.value();
}
/// レシピに紐づいた食材は複数あり
Future<int> deleteRecipeIngredients(int id) async {
final db = await instance.streamDatabase;
return db.delete(ingredientTable, where: '$recipeId = ?', whereArgs: [id]);
}
void close() {
_streamDatabase.close();
}
}
lib/data/moor/moor_repository.dart
【データベース:Moor関連】
import 'dart:async';
import '../models/models.dart';
import '../repository.dart';
import 'moor_db.dart';
/// 'moor_db.dart'と、'moor_db.g.dart'を作成した後に、
/// 表面のリポジトリクラスである本クラスを作成する
///
class MoorRepository extends Repository {
late RecipeDatabase recipeDatabase;
/// Daoはプライベート
late RecipeDao _recipeDao;
late IngredientDao _ingredientDao;
Stream<List<Ingredient>>? ingredientStream;
Stream<List<Recipe>>? recipeStream;
Future<List<Recipe>> findAllRecipes() {
/// Daoが持っているデータベース操作関数を実行
/// findAllRecipes()はFuture<List<MoorRecipeData>>を返すので、
/// thenを利用し、List<MoorRecipeData>→List<Recipe>
/// の変換を中で実施
return _recipeDao.findAllRecipes().then<List<Recipe>>(
(List<MoorRecipeData> moorRecipes) {
final recipes = <Recipe>[];
moorRecipes.forEach(
(moorRecipe) async {
/// Moorレシピをモデルレシピに変換
final recipe = moorRecipeToRecipe(moorRecipe);
if (recipe.id != null) {
/// レシピが持つ食材をここで、Recipeのインスタンスに格納
recipe.ingredients = await findRecipeIngredients(recipe.id!);
}
recipes.add(recipe);
},
);
return recipes;
},
);
}
Stream<List<Recipe>> watchAllRecipes() {
if (recipeStream == null) {
recipeStream = _recipeDao.watchAllRecipes();
}
return recipeStream!;
}
Stream<List<Ingredient>> watchAllIngredients() {
if (ingredientStream == null) {
final stream = _ingredientDao.watchAllIngredients();
ingredientStream = stream.map(
(moorIngredients) {
final ingredients = <Ingredient>[];
moorIngredients.forEach(
(moorIngredient) {
/// 各データベースクラスをモデルクラスに変換が必要
ingredients.add(moorIngredientToIngredient(moorIngredient));
},
);
return ingredients;
},
);
/// mapを利用するなら以下のみ
// ingredientStream = _ingredientDao.watchAllIngredientsTest();
}
return ingredientStream!;
}
Future<Recipe> findRecipeById(int id) {
/// 各データベースクラスをモデルクラスに変換が必要
return _recipeDao
.findRecipeById(id)
.then((listOfRecipes) => moorRecipeToRecipe(listOfRecipes.first));
}
Future<List<Ingredient>> findAllIngredients() {
return _ingredientDao.findAllIngredients().then<List<Ingredient>>(
(List<MoorIngredientData> moorIngredients) {
final ingredients = <Ingredient>[];
moorIngredients.forEach(
(ingredient) {
/// 各データベースクラスをモデルクラスに変換が必要
ingredients.add(moorIngredientToIngredient(ingredient));
},
);
return ingredients;
},
);
}
Future<List<Ingredient>> findRecipeIngredients(int recipeId) {
return _ingredientDao.findRecipeIngredients(recipeId).then(
(listOfIngredients) {
final ingredients = <Ingredient>[];
listOfIngredients.forEach(
(ingredient) {
/// 各データベースクラスをモデルクラスに変換が必要
ingredients.add(moorIngredientToIngredient(ingredient));
},
);
return ingredients;
},
);
}
/// レシピを挿入するには、最初にレシピ自体を挿入し、次にそのすべての材料を挿入
Future<int> insertRecipe(Recipe recipe) {
return Future(
() async {
/// モデルクラスを各データベースクラスに変換が必要
final id =
await _recipeDao.insertRecipe(recipeToInsertableMoorRecipe(recipe));
if (recipe.ingredients != null) {
recipe.ingredients!.forEach(
(ingredient) {
ingredient.recipeId = id;
},
);
insertIngredients(recipe.ingredients!);
}
return id;
},
);
}
/// レシピ追加時に、レシピの持つ複数食材追加関数
Future<List<int>> insertIngredients(List<Ingredient> ingredients) {
return Future(
() {
if (ingredients.length == 0) {
return <int>[];
}
final resultIds = <int>[];
ingredients.forEach(
(ingredient) {
/// モデルクラスを各データベースクラスに変換が必要
final moorIngredient =
ingredientToInsertableMoorIngredient(ingredient);
_ingredientDao
.insertIngredient(moorIngredient)
.then((int id) => resultIds.add(id));
},
);
return resultIds;
},
);
}
Future<void> deleteRecipe(Recipe recipe) {
if (recipe.id != null) {
_recipeDao.deleteRecipe(recipe.id!);
}
return Future.value();
}
Future<void> deleteIngredient(Ingredient ingredient) {
if (ingredient.id != null) {
return _ingredientDao.deleteIngredient(ingredient.id!);
} else {
return Future.value();
}
}
Future<void> deleteIngredients(List<Ingredient> ingredients) {
ingredients.forEach(
(ingredient) {
if (ingredient.id != null) {
_ingredientDao.deleteIngredient(ingredient.id!);
}
},
);
return Future.value();
}
Future<void> deleteRecipeIngredients(int recipeId) async {
final ingredients = await findRecipeIngredients(recipeId);
return deleteIngredients(ingredients);
}
Future init() async {
recipeDatabase = RecipeDatabase();
/// データベース取得後に、DAOのインスタンスを取得
_recipeDao = recipeDatabase.recipeDao;
_ingredientDao = recipeDatabase.ingredientDao;
}
void close() {
recipeDatabase.close();
}
}
lib/data/moor/moor_db.dart
【データベース:Moor関連】
import 'package:moor_flutter/moor_flutter.dart';
import '../models/models.dart';
part 'moor_db.g.dart';
/// 以下のコマンド実行で、「moor_db.g.dart」作成
/// # flutter pub run build_runner build --delete-conflicting-outputs
/// --delete-conflicting-outputs以前に生成されたファイルを削除してから、
/// それらを再構築します。
///
/// 【DAO】
/// DAOは、データベースからのデータへのアクセスを担当するクラス
/// ビジネスロジックコード(たとえば、レシピの成分をフェッチするコード)を永続層
/// (この場合はSQLite)の詳細から分離するために使用
/// DAOは、クラス、インターフェイス、または抽象クラスにすることができます
/// この章では、クラスを使用してDAOを実装
/// 【sqfliteとの違い】
/// テーブル作成も手動的よりは、分かりやすい
/// レシピテーブル
/// await db.execute('''
/// CREATE TABLE $recipeTable (
/// $recipeId INTEGER PRIMARY KEY,
/// label TEXT,
/// image TEXT,
/// url TEXT,
/// calories REAL,
/// totalWeight REAL,
/// totalTime REAL
/// )
/// ''');
///
/// Table:レシピ
class MoorRecipe extends Table {
/// autoIncrement()自動的にIDを作成
IntColumn get id => integer().autoIncrement()();
TextColumn get label => text()();
TextColumn get image => text()();
TextColumn get url => text()();
/// Real→double
RealColumn get calories => real()();
RealColumn get totalWeight => real()();
RealColumn get totalTime => real()();
}
/// Table:食材
class MoorIngredient extends Table {
/// autoIncrement()自動的にIDを作成
IntColumn get id => integer().autoIncrement()();
IntColumn get recipeId => integer()();
TextColumn get name => text()();
RealColumn get weight => real()();
}
/// @UseMoor
/// 使用するテーブルとデータアクセスオブジェクト(DAO)を指定
(tables: [MoorRecipe, MoorIngredient], daos: [RecipeDao, IngredientDao])
/// _$RecipeDatabaseは、自動作成する「part 'moor_db.g.dart';」
/// に含まれる
class RecipeDatabase extends _$RecipeDatabase {
RecipeDatabase()
: super(FlutterQueryExecutor.inDatabaseFolder(
path: 'recipes.sqlite', logStatements: true));
int get schemaVersion => 1;
}
/// @UseDao
/// MoorRecipeテーブルのDAOクラスであることを指定
(tables: [MoorRecipe])
class RecipeDao extends DatabaseAccessor<RecipeDatabase> with _$RecipeDaoMixin {
final RecipeDatabase db;
RecipeDao(this.db) : super(db);
/// 簡単なselectクエリを使用して、すべてのレシピを検索
/// 「MoorRecipeData」というものとする
Future<List<MoorRecipeData>> findAllRecipes() => select(moorRecipe).get();
/// select・watch等を合わせて、すべてのレシピのStreamを返す
/// selectでテーブル指定、
/// watchでStream作成、
/// 行ごとのデータ(List<MoorRecipeData>)を取得、
/// 個々のMoorRecipeDataをRecipeに変換し、
/// 重複したレシピ出なければ、
/// 空の材料リストを作成して、レシピリストに追加
///
/// mapを利用し、上手く<List<Recipe>としているので、
/// 使うときに楽
Stream<List<Recipe>> watchAllRecipes() {
return select(moorRecipe).watch().map(
(rows) {
final recipes = <Recipe>[];
rows.forEach(
(row) {
final recipe = moorRecipeToRecipe(row);
if (!recipes.contains(recipe)) {
recipe.ingredients = <Ingredient>[];
recipes.add(recipe);
}
},
);
return recipes;
},
);
}
/// whereIDでレシピをフェッチするために使用するより複雑なクエリを定義
Future<List<MoorRecipeData>> findRecipeById(int id) =>
(select(moorRecipe)..where((tbl) => tbl.id.equals(id))).get();
/// into()およびinsert()を使用して、新しいレシピを追加
/// テーブルを指定してクラスに渡すだけ
/// Insertableは、Moorが必要とするインターフェイスです。
/// partファイルを生成すると、MoorRecipeDataのインターフェイスを
/// 実装する新しいクラスが生まれる
/// - class MoorRecipeData extends DataClass implements Insertable<MoorRecipeData> {}
Future<int> insertRecipe(Insertable<MoorRecipeData> recipe) =>
into(moorRecipe).insert(recipe);
/// delete()およびwhere()を使用して、特定のレシピを削除
/// goで削除した行のみ取得
Future deleteRecipe(int id) => Future.value(
(delete(moorRecipe)..where((tbl) => tbl.id.equals(id))).go());
}
/// @UseDao
/// MoorIngredientテーブルのDAOクラスであることを指定
(tables: [MoorIngredient])
class IngredientDao extends DatabaseAccessor<RecipeDatabase>
with _$IngredientDaoMixin {
final RecipeDatabase db;
IngredientDao(this.db) : super(db);
/// 簡単なselectクエリを使用して、すべての食材を検索
/// 「MoorIngredientData」というものとする
Future<List<MoorIngredientData>> findAllIngredients() =>
select(moorIngredient).get();
/// Streamを作成するための、watch関数
///
/// mapを利用していないので、呼び出し元で、
/// List<MoorIngredientData>→List<Ingredient>等に変換して利用
Stream<List<MoorIngredientData>> watchAllIngredients() =>
select(moorIngredient).watch();
/// mapを利用するなら以下
Stream<List<Ingredient>> watchAllIngredientsTest() =>
select(moorIngredient).watch().map(
(rows) {
final ingredients = <Ingredient>[];
rows.forEach(
(row) {
final ingredient = moorIngredientToIngredient(row);
if (!ingredients.contains(ingredient)) {
ingredients.add(ingredient);
}
},
);
return ingredients;
},
);
/// レシピIDに一致するすべての材料を選択
Future<List<MoorIngredientData>> findRecipeIngredients(int id) =>
(select(moorIngredient)..where((tbl) => tbl.recipeId.equals(id))).get();
/// into()とinsert()を使用して、新しい材料を追加
Future<int> insertIngredient(Insertable<MoorIngredientData> ingredient) =>
into(moorIngredient).insert(ingredient);
/// 使用delete()に加えてwhere()、特定の成分を削除
Future deleteIngredient(int id) => Future.value(
(delete(moorIngredient)..where((tbl) => tbl.id.equals(id))).go());
}
/// ☝ここまでで、以下コマンドで作成して、データベースクラス関連コードを確認
/// - flutter pub run build_runner build --delete-conflicting-outputs
///
/// 上記のテーブルを定義したので、
/// データベースクラスを通常のモデルクラスに変換したり、
/// 元に戻したりするメソッドを作成する
// Conversion Methods
Recipe moorRecipeToRecipe(MoorRecipeData recipe) {
return Recipe(
id: recipe.id,
label: recipe.label,
image: recipe.image,
url: recipe.url,
calories: recipe.calories,
totalWeight: recipe.totalWeight,
totalTime: recipe.totalTime);
}
/// Moorデータベースに挿入できるクラスに変換
Insertable<MoorRecipeData> recipeToInsertableMoorRecipe(Recipe recipe) {
/// Moor[クラス名]Companion.insert()関数で作成
return MoorRecipeCompanion.insert(
label: recipe.label ?? '',
image: recipe.image ?? '',
url: recipe.url ?? '',
calories: recipe.calories ?? 0,
totalWeight: recipe.totalWeight ?? 0,
totalTime: recipe.totalTime ?? 0);
}
Ingredient moorIngredientToIngredient(MoorIngredientData ingredient) {
return Ingredient(
id: ingredient.id,
recipeId: ingredient.recipeId,
name: ingredient.name,
weight: ingredient.weight);
}
/// Moorデータベースに挿入できるクラスに変換
MoorIngredientCompanion ingredientToInsertableMoorIngredient(
Ingredient ingredient) {
/// Moor[クラス名]Companion.insert()関数で作成
return MoorIngredientCompanion.insert(
recipeId: ingredient.recipeId ?? 0,
name: ingredient.name ?? '',
weight: ingredient.weight ?? 0);
}
lib/data/moor/moor_db.g.dart
【データベース:Moor関連】※自動作成
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'moor_db.dart';
// **************************************************************************
// MoorGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps, unnecessary_this
class MoorRecipeData extends DataClass implements Insertable<MoorRecipeData> {
// ・・・省略
}
// **************************************************************************
// DaoGenerator
// **************************************************************************
mixin _$RecipeDaoMixin on DatabaseAccessor<RecipeDatabase> {
$MoorRecipeTable get moorRecipe => attachedDatabase.moorRecipe;
}
mixin _$IngredientDaoMixin on DatabaseAccessor<RecipeDatabase> {
$MoorIngredientTable get moorIngredient => attachedDatabase.moorIngredient;
}
lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'data/repository.dart';
import 'data/sqlite/sqlite_repository.dart';
import 'mock_service/mock_service.dart';
import 'network/service_interface.dart';
import 'ui/main_screen.dart';
Future<void> main() async {
_setupLogging();
WidgetsFlutterBinding.ensureInitialized();
/// DatabaseにsqfliteかMoorか選択
/// (final repositoryと定義することで以下の1行のみの変更で対応可能)
final repository = SqliteRepository();
// final repository = MoorRepository();
/// 初期化し、データベースを準備(バックグラウンドで開いておく)
/// awaitで呼びたいのでMyAppではなく、ここで呼ぶ
await repository.init();
runApp(MyApp(repository: repository));
}
void _setupLogging() {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen(
(rec) {
print('${rec.level.name}: ${rec.time}: ${rec.message}');
},
);
}
class MyApp extends StatelessWidget {
/// Repositoryを持つと定義し、
/// SqliteRepository or MoorRepositoryを受け取る
final Repository repository;
const MyApp({Key? key, required this.repository}) : super(key: key);
// This widget is the root of your application.
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<Repository>(
lazy: false,
create: (_) => repository,
dispose: (_, Repository repository) => repository.close(),
),
Provider<ServiceInterface>(
/// 本番API or Mock
create: (_) => MockService()..create(),
// create: (_) => RecipeService.create(),
lazy: false,
),
],
child: MaterialApp(
title: 'Recipes',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.light,
primaryColor: Colors.white,
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const MainScreen(),
),
);
}
}
Discussion