🎋

@JsonKeyはどう使う?

2023/07/23に公開

Freezedのよくわからない機能?

最近、Freezedを使っているのですけど、みんな書き方が違って@なんとかを使ってる人が多かったりして、自分はFreezedを有効に活用できてないなと思って、今回はいつもと違う書き方で、モデルクラスを作ってみました。

https://pub.dev/packages/freezed

🔤公式を翻訳してみた

JsonKeyアノテーションはどうですか?

コンストラクタのパラメータに渡されたすべてのデコレータは、生成されたプロパティにも「コピーペースト」されます。
そのため、次のように書くことができます:


class Example with _$Example {
  factory Example((name: 'my_property') String myProperty) = _Example;

  factory Example.fromJson(Map<String, dynamic> json) => _$ExampleFromJson(json);
}

JsonSerializableアノテーションはどうですか?

JsonSerializableアノテーションをコンストラクタの上に置くことで、@JsonSerializableアノテーションを渡すことができます:


class Example with _$Example {
  (explicitToJson: true)
  factory Example((name: 'my_property') SomeOtherClass myProperty) = _Example;

  factory Example.fromJson(Map<String, dynamic> json) => _$ExampleFromJson(json);
}

全てのクラスに対してカスタムjson_serializableフラグを定義したい場合(explicit_to_jsonやany_mapなど)、ここで説明されているように、build.yamlファイルを通して行うことができる。

デコレータのセクションも参照してください。

日本語の情報もあるので、リンク貼っておきます。
https://tech.excite.co.jp/entry/2022/12/03/090000

🎁まずはパッケージを追加しておく

name: freezed_app
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: "none" # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1

environment:
  sdk: ">=3.0.5 <4.0.0"

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  cupertino_icons: ^1.0.2
  dio: ^5.3.0
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.3.6
  freezed_annotation: ^2.2.0
  json_annotation: ^4.8.1

dev_dependencies:
  build_runner: ^2.4.6

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^2.0.0
  flutter_test:
    sdk: flutter
  freezed: ^2.3.5
  json_serializable: ^6.7.1

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true
  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg
  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware
  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages
  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

⚙️モデルクラスを作る

今回の使い方だと、@JsonKey(name: )がやってくれいることは、APIのオブジェクトのプロパティの名前を中に書いて、変数は別名(エイリアス)にしてるだけです。
例えば、APIのデータが、user_nameと書いてあったら、userNameにしてるだけ。
@JsonKey(name: 'user_name') String userNameといった感じですね。

// ignore_for_file: invalid_annotation_target

import 'package:freezed_annotation/freezed_annotation.dart';

part 'post.freezed.dart';
part 'post.g.dart';


class Post with _$Post {
  const factory Post({
    (0) (name: 'id') int postId,// APIのIDが数字なので、int型にしてる
    ('') (name: 'title') String postTitle,
  }) = _Post;

  factory Post.fromJson(Map<String, dynamic> json) =>
      _$PostFromJson(json);
}

コマンドを打って必要なファイルを自動生成する

dart run build_runner build

APIと通信するロジックを作る

APIと通信するためのロジックをdioを使って、おこなうレポジトリクラスを作ろうと思います。ベースURLなるものを定義すれば、dioでは、.getを使うときは、idを指定するパスを書くだけで良かったりする。httpにはこんな機能はなかったような?

import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_app/domain/post.dart';

// レポジトリをインスタンス化するグローバルなProvider
final dioClientProvider = Provider((ref) => DioClient(ref: ref));
// dioをインスタンス化するグローバルなProvider
final dioProvider = Provider((ref) => Dio(
  // ベースURLを設定していれば、getメソッドの引数にはパスのみを指定できる
      BaseOptions(
        baseUrl: 'https://jsonplaceholder.typicode.com/todos',
        // タイムアウトを5秒に設定する
        connectTimeout: const Duration(seconds: 5),
        receiveTimeout: const Duration(seconds: 5),
      ),
    ));

// APIと通信するレポジトリクラス
class DioClient {
  Ref ref;
  DioClient({required this.ref});

  // jsonplaceholderからタスクの一覧を取得する
  Future<List<Post>> getPost() async {
    // ベースURLがあれば、 .getの中は、('') でも良い。本来はこの中に、idなどのパスを指定する
    final response = await ref.read(dioProvider).get('');
    final posts = (response.data as List)
        .map((e) => Post.fromJson(e as Map<String, dynamic>))
        .toList();
    return posts;
  }
}

レポジトリクラスをUIにデータを表示するのに使うFutureProviderで呼び出す。

// DioClientを使うFutureProviderを作成する
// DioClientを使うFutureProviderを作成する
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_app/application/feature1/dio_client.dart';
import 'package:freezed_app/domain/post.dart';

// DioClientを使うFutureProviderを作成する
final postsProvider = FutureProvider<List<Post>>((ref) async {
  // DioClientを取得する
  final dioClient = ref.watch(dioClientProvider);
  // DioClientのgetTasksメソッドを呼び出す
  final posts = await dioClient.getPost();
  return posts;
});

🛜UIにAPIから取得したデータを表示

今回実験で作ったコードですので、reflistenは機能していません。これは自作したAPIで実験したかったので、使ってたんですけど、reander.comのAPIが使えなくなってたので、タイムアウトの実験ができませんでした。loadingのところに、ダイアログを書くと、10秒後に実行される現象は起きました?
タイムアウトはしてないはずなんですけどね。。。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_app/application/feature1/dio_provider.dart';

class PostList extends ConsumerWidget {
  const PostList({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final postAsyncValue = ref.watch(postsProvider);
    // タイムアウトしたら、ref.listenを使ってダイアログを出す
    ref.listen(postsProvider, (postAsyncValue, previousValue) {
      postAsyncValue!.when(
        data: (tasks) {
          Navigator.pop(context);
        },
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (error, stackTrace) {
          showDialog(
            context: context,
            builder: (context) {
              return AlertDialog(
                title: const Text('エラー'),
                content: Text(error.toString()),
                actions: [
                  TextButton(
                    onPressed: () => Navigator.pop(context),
                    child: const Text('OK'),
                  ),
                ],
              );
            },
          );
        },
      );
    });
    return Scaffold(
      appBar: AppBar(
        title: const Text('HTTP GET'),
      ),
      body: postAsyncValue.when(
        data: (posts) {
          return ListView.builder(
            itemCount: posts.length,
            itemBuilder: (context, index) {
              final post = posts[index];
              return ListTile(
            title: Text(post.postTitle),
            subtitle: Text(post.postId.toString()),
              );
            },
          );
        },
        loading: () => const Center(child: Text('読み込み中........')),
        error: (error, stackTrace) => Center(child: Text(error.toString())),
      ),
    );
  }
}

main.dartでimportしてビルドすれば、実行できます。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_app/applicaton/feature2/post_list.dart';


void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const PostList(),
    );
  }
}

実行結果

まとめ

Freezedは他にもたくさんの機能があって、まだ理解できていません。また何か新しい書き方を覚えたら記事にしようと思います。

Discussion