RP:スタブと分ける - Ki-Kobayashi/flutter_wiki GitHub Wiki

🟩 大きな構成は以下とする

🟡 データの流れ

main.dart ... ⇒ Screen ⇔ Controller ⇔ Service ⇔ Repository ⇔ DataSourceIF ⇔ DataSourceImpl(Stub / Remote)

.

🟡 infrastructure / data_source /

DataSourceIF を作成し、実装として、リモート と ローカル で分ける
repository では、DataSourceIF を見る ことで、 リモート と ローカル化を気にせず利用できる

📚 article_data_source.dart (抽象:データソースIF )

part 'article_rest_client.g.dart'; 🚨忘れずに!

@riverpod
ArticleDataSource articleDataSource(ArticleDataSourceRef ref) {
  throw UnimplementedError(); 👈 未実装状態にする(main.dartのProviderScopeで注入)
}

abstract class ArticleDataSource {
  Future<ApiResponse<CategoryNames>> getCategories();

  /// 記事リストを取得する
  Future<ApiResponse<ArticleResponse>> postCategoryArticles(
      FindArticleListRequest reqBody);
}

.

📚 articlerest_client.dart

抽象 :DIO + Retrofit 使用の実際のIF

part 'article_rest_client.g.dart'; 🚨忘れずに!

@riverpod
ArticleRestClient articleRestClient(ArticleRestClientRef ref) {
  return ArticleRestClient(ref.read(dioProvider));
}

@RestApi()
abstract class ArticleRestClient implements ArticleDataSource {
  factory ArticleRestClient(Dio dio) = _ArticleRestClient;

  @override
  @GET('/categories')
  Future<ApiResponse<CategoryNames>> getCategories();

  /// 記事リストを取得する
  @override
  @POST('/category-articles')
  Future<ApiResponse<ArticleResponse>> postCategoryArticles(
    @Body() FindArticleListRequest reqBody,
  );
}

.

📚 stub / stub_article_data_source.dart

実装:スタブ用のデータソースを返却するクラス)

part 'stub_article_data_source.g.dart'; 🚨忘れずに!

@riverpod
StubArticleDataSource stubArticleDataSource(StubArticleDataSourceRef ref) {
  return StubArticleDataSource();
}

class StubArticleDataSource implements ArticleDataSource {
  @override
  Future<ApiResponse<CategoryNams>> getCategories() async {
    final content = json.decode(await rootBundle
        .loadString('assets/stub/stub_categories.json'));
    // 👇Json変換がネストしている時は、以下のように書く
    return ApiResponse.fromJson(
      content,
      (Object? json) => CategoryNames.fromJson(json as Map<String, dynamic>),
    );
  }

  /// 記事リスト取得
  @override
  Future<ApiResponse<ArticleResponse>> getArticles(
      FindArticleListRequest reqBody) async {
    var dataPath = '';
    switch (reqBody.context) {
      case '.*':
        dataPath = 'assets/stub/stub_articles.json';
      case 'お気に入り':
        dataPath = 'assets/stub/stub_articles_favorite.json';
    }

    final content = json.decode(await rootBundle.loadString(dataPath));
    return ApiResponse.fromJson(
      content,
      (Object? json) => ArticleResponse.fromJson(json as Map<String, dynamic>),
    );
  }

💡TIPS💡 ネストしていなけば以下

 final content = json.decode(await rootBundle.loadString(
  'assets/stub/stub_articles.json')) as Iterable<Article>;
 // ✨List <Article> が返却
 return content.map((item) => Article.fromJson(item)).toList();

.

🟡 infrastructure / repository /

part 'article_repository.g.dart'; 🚨忘れずに!

// 🚨ArticleDataSource か ここ に以下を定義するかはプロジェクト次第
@riverpod
ArticleDataSource articleDataSource(ArticleDataSourceRef ref) {
  throw UnimplementedError();
}

@riverpod
ArticleRepository articleRepository(ArticleRepositoryRef ref) {
  final repo = ArticleRepository(client: ref.read(articleDataSourceProvider));
  return repo;
}

class ArticleRepository {
  ArticleRepository({
    required ArticleDataSource client,
  }) : _client = client;

  final ArticleDataSource _client;

  /// 記事リストを取得
  Future<ArticleResponse> fetchArticles({
    required FindArticleListRequest reqBody,
  }) async {
    try {
      logger.i('fetchArticles REQUEST: $reqBody');
      final res = await _client.getArticles(reqBody);
      if (res.statusCode != ApiStatusCode.success.statusCode) {
        logger.e(
            'fetchArticles statusCode: ${res.statusCode}, errMessage: ${res.message}');
        throw NetworkExceptions.getApiError(
          ApiStatusCodeError(apiStatusCode: res.statusCode),
        );
      }
      logger.i('fetchArticles RESPONSE: $res');
      return res.payload;
    } on Exception catch (e) {
      throw NetworkExceptions.getApiError(e);
    }
  }
}

.

🟡 main.dart

bool isDevArticle = true;

Future<void> main() async {
  // 💡UIの構築やライフサイクルイベント管理するもの
  //    ・ runApp()前に初期化したいものがあるなら必須
 //            (非同期処理、外部サービスの初期化時は必須)
  //     ・ 非同期処理完了前にウィジェットがビルドされることを防ぐ
  WidgetsFlutterBinding.ensureInitialized();

  // アプリ縦方向固定
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
  ]);

  // google fontsの設定
  GoogleFonts.config.allowRuntimeFetching = kDebugMode;

  runApp(
    ProviderScope(
      overrides: [
        sharedPreferencesProvider.overrideWithValue(
          await SharedPreferences.getInstance(),
        ),
        flutterSecureStorageProvider.overrideWithValue(
          const FlutterSecureStorage(),
        ),
        // 💡リリース時は、stubを見ない(RestClientのみにする)
        // 記事データソース
        articleDataSourceProvider.overrideWith((ref) => ref.watch(isDevArticle 👈ここ追加
            ? stubArticlesSourceProvider
            : articleRestClientProvider)),
      ],
      child: const Application(),
    ),
  );
}

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

🟩

🟡

.

⚠️ **GitHub.com Fallback** ⚠️