あなたのAPIデータのローカルコピーを保つために、Flutter
ヶ月前に私はarticle そこで、リポジトリパターンを使用してAPIからダウンロードしたデータのローカルコピーを保存する方法を説明しました.
この記事では、同じ実装を行いますが、いくつかの改善を適用するつもりです.私の目標を達成するために必要なコード量を減らすために、より多くのパッケージとプラグインを利用します.
基本的に私が達成したいのは、リモートAPIから取得したデータのデータベースコピーを保つことができることです.また、これは、アプリケーションがインターネットに接続せずに使用することができます.
この記事で説明すること フラッタにおけるリポジトリパターンの説明と応用 APIリクエストを使用する方法dio パッケージ SQLiteデータベースとの対話方法sqflite
より速くモデルを書く方法json_serializable
私がこの記事の範囲から出るつもりです. 私が使用するUIは最小限になるでしょう、この記事はユーザーインターフェースを作成することに集中しません 私はどんなデータ同期システムも実装するつもりはありません、理由はそれをするいくつかの方法があるということです、そして、それについて話すために全く新しい記事を作成することはより適切です
この開発パターンは通常、Clean Architecture . その中で2つの基本要素があります. データソース:このコンポーネントは、データに対して生の操作を行います.例は、APIに対する呼び出しを行うための専用のクラス、またはデータベースに対してクエリを実行するデータアクセスオブジェクト(DAO)です. リポジトリ:このコンポーネントは、1つ以上のデータソースを明確にし、データがどこからまたはその方向に流れるかを決定します. この特定の例では、我々はこれを消費するつもりですRecipe API 我々はレシピのリストを取得します.このため、これらのAPIリクエストを作成するためのクラスを作成します.このデータをデータベースに永続化するには、2番目のデータソースとなるDAO(Data Access Object)を作成します.
これらの2つのデータ源はタイプリポジトリのオブジェクトによって連結されるでしょう.
これは典型的なリポジトリパターンの簡略形式である.私は古典的な抽象化のすべてを実装しません、理由は私の経験においてそれをするこの方法が小さな媒体サイズアプリケーションに絶好であるということです.多くのデータを管理し、いくつかの開発者が働くつもりである非常に複雑なアプリケーションを開発しなければならないならば、私はあなたにこの記事の後で古典的なきれいな建築を研究して、より深くそれについて学ぶように助言します.
フラッタープロジェクトを作成し、次の依存関係を追加します
次のステップはネットワークコールを実行できるように必要なクラスを作成することです.まず第一に私たちは創造するつもりです
さて、データベースパーツを実装します.まず最初に、データベースモデルを作成します
次はデータベースで動作するDAOを作成することです.まず第一に、我々は2009年にベースとして役立つ抽象クラスを作成します
それぞれのレイヤーの排他的なモデルを持っているので、それらの間で変換できるメカニズムを作成しなければなりません
今、我々はリポジトリを中心に我々のリポジトリを作成するつもりです
上記のすべての結果を見るために、我々はAを作成するつもりです
変更する
これは、我々の小さな例が終わるところです.この例では2つのデータソースを実装していますが、他のリモートAPI、デバイスキャッシュなどのように実装する必要があります.
このコードにできるいくつかの改善は、より良いエラー処理であり、より洗練されたプレゼンテーション層を追加したり、データベース内のデータが常にAPI内のデータと同期していることを保証するための同期機構を追加することです.
具体例のソースコードを見ることができますhere .
この遠く、ハッピーコーディングを読んでくれてありがとう!
この記事では、同じ実装を行いますが、いくつかの改善を適用するつもりです.私の目標を達成するために必要なコード量を減らすために、より多くのパッケージとプラグインを利用します.
基本的に私が達成したいのは、リモートAPIから取得したデータのデータベースコピーを保つことができることです.また、これは、アプリケーションがインターネットに接続せずに使用することができます.
この記事で説明すること
リポジトリパターン
この開発パターンは通常、Clean Architecture . その中で2つの基本要素があります.
これらの2つのデータ源はタイプリポジトリのオブジェクトによって連結されるでしょう.
これは典型的なリポジトリパターンの簡略形式である.私は古典的な抽象化のすべてを実装しません、理由は私の経験においてそれをするこの方法が小さな媒体サイズアプリケーションに絶好であるということです.多くのデータを管理し、いくつかの開発者が働くつもりである非常に複雑なアプリケーションを開発しなければならないならば、私はあなたにこの記事の後で古典的なきれいな建築を研究して、より深くそれについて学ぶように助言します.
プロジェクト設定
フラッタープロジェクトを作成し、次の依存関係を追加します
pubspec.yaml
:dependencies:
dio: 4.0.6
flutter:
sdk: flutter
json_annotation: 4.4.0
logger: 1.1.0
path: 1.8.0
path_provider: 2.0.9
provider: 6.0.2
sqflite: 2.0.2
dev_dependencies:
build_runner: 2.1.8
flutter_test:
sdk: flutter
json_serializable: 6.1.5
lint: 1.8.2
今、我々はビジネス層を定義するつもりですdomain
, そして、それでrecipe.dart
我々が我々のレシピを持つファイル// lib/domain/recipe.dart
class Recipe {
final int id;
final String name;
final String thumbnailUrl;
final String description;
const Recipe({
required this.id,
required this.name,
required this.thumbnailUrl,
required this.description,
});
}
ネットワーク層の作成
次のステップはネットワークコールを実行できるように必要なクラスを作成することです.まず第一に私たちは創造するつもりです
data/network/entity/
ファイルrecipe_entity.dart
ここでは、ReceipeEntityネットワークモデルを使用します.このモデルはリモートAPIから受け取るデータのコピーです.// lib/data/network/entity/recipe_entity.dart
import 'package:json_annotation/json_annotation.dart';
part 'recipe_entity.g.dart';
@JsonSerializable()
class RecipeListResponse {
final int count;
final List<RecipeEntity> results;
RecipeListResponse({required this.count, required this.results});
factory RecipeListResponse.fromJson(Map<String, dynamic> json) =>
_$RecipeListResponseFromJson(json);
}
@JsonSerializable()
class RecipeEntity {
final int id;
final String name;
@JsonKey(name: 'thumbnail_url')
final String thumbnailUrl;
final String description;
RecipeEntity({
required this.id,
required this.name,
required this.thumbnailUrl,
required this.description,
});
factory RecipeEntity.fromJson(Map<String, dynamic> json) =>
_$RecipeEntityFromJson(json);
}
これまでのチュートリアルに従った場合は、IDEでエラーが表示されますpart 'recipe_entity.g.dart';
. それはfromJson()
メソッドは生成されません.以下のコマンドを実行して自動生成します.flutter pub run build_runner build
さて、コールを実行するクラスを作成できます.私たちは使用するつもりですdio 書くコードの量を最小にしながらこれを実現するには:// lib/data/network/client/api_client.dart
import 'package:dio/dio.dart';
import 'package:flutter_data_layer_repository_pattern_v2/data/network/entity/recipe_entity.dart';
class KoException implements Exception {
final int statusCode;
final String? message;
const KoException({required this.statusCode, this.message});
@override
String toString() {
return 'KoException: statusCode: $statusCode, message: ${message ?? 'No message specified'}';
}
}
class ApiClient {
final String baseUrl;
final String apiKey;
ApiClient({
required this.baseUrl,
required this.apiKey,
});
Future<RecipeListResponse> getRecipes() async {
try {
final response = await Dio().get(
'https://$baseUrl/recipes/list',
queryParameters: {
'from': 0,
'size': 20,
},
options: Options(
headers: {
'X-RapidAPI-Host': baseUrl,
'X-RapidAPI-Key': apiKey,
},
),
);
if (response.data != null) {
final data = response.data;
return RecipeListResponse.fromJson(data as Map<String, dynamic>);
} else {
throw Exception('Could not parse response.');
}
} on DioError catch (e) {
if (e.response != null && e.response!.statusCode != null) {
throw KoException(
statusCode: e.response!.statusCode!,
message: e.response!.data.toString(),
);
} else {
throw Exception(e.message);
}
}
}
}
データベース
さて、データベースパーツを実装します.まず最初に、データベースモデルを作成します
lib/data/database/entity
:// lib/data/database/entity/recipe_db_entity.dart
class RecipeDbEntity {
static const fieldId = 'id';
static const fieldName = 'name';
static const fieldThumbnailUrl = 'thumbnail_url';
static const fieldDescription = 'description';
final int id;
final String name;
final String thumbnailUrl;
final String description;
const RecipeDbEntity({
required this.id,
required this.name,
required this.thumbnailUrl,
required this.description,
});
RecipeDbEntity.fromMap(Map<String, dynamic> map)
: id = map[fieldId] as int,
name = map[fieldName] as String,
thumbnailUrl = map[fieldThumbnailUrl] as String,
description = map[fieldDescription] as String;
Map<String, dynamic> toMap() => {
fieldId: id,
fieldName: name,
fieldThumbnailUrl: thumbnailUrl,
fieldDescription: description,
};
}
この例のモデルに注意してください:あなたが気づいたかもしれないように、私は同じデータを含む3つの異なるモデルを作成しています.一つのクラスでそれをまとめる人がいて、必ずしも悪いことではない.しかし、ほとんどの場合、それぞれの層のそれぞれのモデルが異なったニーズに応えるので、私はそれを分離するのを好みます.いくつかのモデルを作成すると、たとえ最初に同じであっても、新しい要件が発生するにつれて、異なる進化と変換を持つことができるようになります.このようにして、層の結合を低減している.次はデータベースで動作するDAOを作成することです.まず第一に、我々は2009年にベースとして役立つ抽象クラスを作成します
data/database/dao/base_dao.dart
:// lib/data/database/dao/base_dao.dart
import 'package:flutter/widgets.dart';
import 'package:flutter_data_layer_repository_pattern_v2/data/database/entity/recipe_db_entity.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
abstract class BaseDao {
static const databaseName = 'data-layer-sample-v2.db';
static const recipeTableName = 'recipe';
@protected
Future<Database> getDatabase() async {
return openDatabase(
join(await getDatabasesPath(), databaseName),
onCreate: (db, version) async {
final batch = db.batch();
_createRecipeTable(batch);
await batch.commit();
},
version: 1,
);
}
void _createRecipeTable(Batch batch) {
batch.execute(
'''
CREATE TABLE $recipeTableName(
${RecipeDbEntity.fieldId} INTEGER PRIMARY KEY NOT NULL,
${RecipeDbEntity.fieldName} TEXT NOT NULL,
${RecipeDbEntity.fieldThumbnailUrl} TEXT NOT NULL,
${RecipeDbEntity.fieldDescription} TEXT NOT NULL
);
''',
);
}
}
さて、レシピdaoを作ります.// lib/data/database/dao/recipe_dao.dart
import 'package:flutter_data_layer_repository_pattern_v2/data/database/dao/base_dao.dart';
import 'package:flutter_data_layer_repository_pattern_v2/data/database/entity/recipe_db_entity.dart';
class RecipeDao extends BaseDao {
Future<List<RecipeDbEntity>> selectAll() async {
final db = await getDatabase();
final List<Map<String, dynamic>> maps =
await db.query(BaseDao.recipeTableName);
return List.generate(maps.length, (i) => RecipeDbEntity.fromMap(maps[i]));
}
Future<void> insertAll(List<RecipeDbEntity> assets) async {
final db = await getDatabase();
final batch = db.batch();
for (final asset in assets) {
batch.insert(BaseDao.recipeTableName, asset.toMap());
}
await batch.commit();
}
}
データマッピング
それぞれのレイヤーの排他的なモデルを持っているので、それらの間で変換できるメカニズムを作成しなければなりません
data
:// lib/data/mapper.dart
import 'package:flutter_data_layer_repository_pattern_v2/data/database/entity/recipe_db_entity.dart';
import 'package:flutter_data_layer_repository_pattern_v2/data/network/entity/recipe_entity.dart';
import 'package:flutter_data_layer_repository_pattern_v2/domain/recipe.dart';
class MapperException<From, To> implements Exception {
final String message;
const MapperException(this.message);
@override
String toString() {
return 'Error when mapping class $From to $To: $message}';
}
}
class Mapper {
Recipe toRecipe(RecipeEntity entity) {
try {
return Recipe(
id: entity.id,
name: entity.name,
thumbnailUrl: entity.thumbnailUrl,
description: entity.description,
);
} catch (e) {
throw MapperException<RecipeEntity, Recipe>(e.toString());
}
}
List<Recipe> toRecipes(List<RecipeEntity> entities) {
final List<Recipe> recipes = [];
for (final entity in entities) {
recipes.add(toRecipe(entity));
}
return recipes;
}
Recipe toRecipeFromDb(RecipeDbEntity entity) {
try {
return Recipe(
id: entity.id,
name: entity.name,
thumbnailUrl: entity.thumbnailUrl,
description: entity.description,
);
} catch (e) {
throw MapperException<RecipeDbEntity, Recipe>(e.toString());
}
}
List<Recipe> toRecipesFromDb(List<RecipeDbEntity> entities) {
final List<Recipe> recipes = [];
for (final entity in entities) {
recipes.add(toRecipeFromDb(entity));
}
return recipes;
}
RecipeDbEntity toRecipeDbEntity(Recipe recipe) {
try {
return RecipeDbEntity(
id: recipe.id,
name: recipe.name,
thumbnailUrl: recipe.thumbnailUrl,
description: recipe.description,
);
} catch (e) {
throw MapperException<Recipe, RecipeDbEntity>(e.toString());
}
}
List<RecipeDbEntity> toRecipesDbEntity(List<Recipe> entities) {
final List<RecipeDbEntity> list = [];
for (final entity in entities) {
list.add(toRecipeDbEntity(entity));
}
return list;
}
}
リポジトリを通してデータを調合すること
今、我々はリポジトリを中心に我々のリポジトリを作成するつもりです
Recipe
どちらがdata/repository
. この関数は、データベースからデータを提供するか、リモートAPIからデータを出力します.// lib/data/repository/recipe_repository.dart
import 'package:flutter_data_layer_repository_pattern_v2/data/database/dao/recipe_dao.dart';
import 'package:flutter_data_layer_repository_pattern_v2/data/mapper.dart';
import 'package:flutter_data_layer_repository_pattern_v2/data/network/client/api_client.dart';
import 'package:flutter_data_layer_repository_pattern_v2/domain/recipe.dart';
class RecipeRepository {
final ApiClient apiClient;
final Mapper mapper;
final RecipeDao recipeDao;
RecipeRepository({
required this.apiClient,
required this.mapper,
required this.recipeDao,
});
Future<List<Recipe>> getRecipes() async {
// First, try to fetch the recipes from database
final dbEntities = await recipeDao.selectAll();
if (dbEntities.isNotEmpty) {
return mapper.toRecipesFromDb(dbEntities);
}
// If the database is empty, fetch from the API, saved it to database,
// and return it to the caller
final response = await apiClient.getRecipes();
final recipes = mapper.toRecipes(response.results);
await recipeDao.insertAll(mapper.toRecipesDbEntity(recipes));
return recipes;
}
}
プレゼンテーション層
上記のすべての結果を見るために、我々はAを作成するつもりです
presentation
我々が我々の表示層を置くフォルダ.まず最初に、MaterialAppをサポートするウィジェットを作成します.// lib/presentation/app.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_data_layer_repository_pattern_v2/data.dart';
import 'package:flutter_data_layer_repository_pattern_v2/data/database/dao/recipe_dao.dart';
import 'package:flutter_data_layer_repository_pattern_v2/data/mapper.dart';
import 'package:flutter_data_layer_repository_pattern_v2/data/network/client/api_client.dart';
import 'package:flutter_data_layer_repository_pattern_v2/data/repository/recipe_repository.dart';
import 'package:flutter_data_layer_repository_pattern_v2/presentation/main_screen.dart';
import 'package:logger/logger.dart';
import 'package:provider/provider.dart';
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<Logger>(
create: (_) => Logger(
printer: PrettyPrinter(),
level: kDebugMode ? Level.verbose : Level.nothing,
),
),
Provider<RecipeRepository>(
create: (_) => RecipeRepository(
apiClient:
ApiClient(baseUrl: 'tasty.p.rapidapi.com', apiKey: apiKey),
mapper: Mapper(),
recipeDao: RecipeDao(),
),
),
],
child: MaterialApp(
home: MainScreen(),
),
);
}
}
私のケースでは、私はこれらの例Githubに公開しているので、私も作成してlib/data.dart
私のAPIキーを持つファイル.私は私のパスワードを秘密に保つためにこの方法を行う.コードを配布しない場合は、そのクラスで直接キーを書くことができます.変更する
lib/main.dart
次のようになります.// lib/main.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_data_layer_repository_pattern_v2/presentation/app.dart';
import 'package:sqflite/sqflite.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
// ignore: deprecated_member_use, avoid_redundant_argument_values
Sqflite.devSetDebugModeOn(kDebugMode);
runApp(App());
}
終わるために、私はAをつくるつもりですMainScreen
ウィジェットのレシピのリストを取得し、別のと呼ばれるRecipeDetail
これには、レシピの具体的な詳細が含まれます.// lib/presentation/main_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_data_layer_repository_pattern_v2/data/repository/recipe_repository.dart';
import 'package:flutter_data_layer_repository_pattern_v2/domain/recipe.dart';
import 'package:flutter_data_layer_repository_pattern_v2/presentation/recipe_details.dart';
import 'package:logger/logger.dart';
import 'package:provider/provider.dart';
class MainScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Data layer sample')),
body: FutureBuilder<List<Recipe>>(
future: Provider.of<RecipeRepository>(context).getRecipes(),
builder: (BuildContext context, AsyncSnapshot<List<Recipe>> snapshot) {
if (snapshot.hasData) {
return ListView.separated(
itemBuilder: (context, index) {
final recipe = snapshot.data![index];
return ListTile(
leading: SizedBox(
width: 48.0,
height: 48.0,
child: ClipOval(
child: Image.network(recipe.thumbnailUrl),
),
),
title: Text(recipe.name),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RecipeDetails(recipe: recipe),
),
),
);
},
separatorBuilder: (context, index) => const Divider(),
itemCount: snapshot.data!.length,
);
} else if (snapshot.hasError) {
Provider.of<Logger>(context)
.e('Error while fetching data: ${snapshot.error.toString()}');
return const Center(
child: Text('An error occurred while fetching data.'),
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_data_layer_repository_pattern_v2/domain/recipe.dart';
class RecipeDetails extends StatelessWidget {
final Recipe recipe;
const RecipeDetails({required this.recipe});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(recipe.name),
),
body: SafeArea(
child: Container(
padding: const EdgeInsets.only(
left: 16.0,
top: 24.0,
right: 16.0,
bottom: 24.0,
),
child: Text(recipe.description),
),
),
);
}
}
結論
これは、我々の小さな例が終わるところです.この例では2つのデータソースを実装していますが、他のリモートAPI、デバイスキャッシュなどのように実装する必要があります.
このコードにできるいくつかの改善は、より良いエラー処理であり、より洗練されたプレゼンテーション層を追加したり、データベース内のデータが常にAPI内のデータと同期していることを保証するための同期機構を追加することです.
具体例のソースコードを見ることができますhere .
この遠く、ハッピーコーディングを読んでくれてありがとう!
Reference
この問題について(あなたのAPIデータのローカルコピーを保つために、Flutter), 我々は、より多くの情報をここで見つけました https://dev.to/svprdga/data-layer-in-flutter-v2-use-the-repository-pattern-to-keep-a-local-copy-of-your-api-data-4fa6テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol