🦧

REST APIと通信をしてみる

2023/03/28に公開

Dioが更新されたので使ってみた

最近、Flutterでお仕事探しているんですけど、どこもREST API使っているみたいで、Firebase使ってない企業もあるみたいで困りました😇
でも師匠がREST APIの方が簡単だとおっしゃっていたので、また暇というわけではないのですが、キャッチアップしてみました。

今回は、Dioと呼ばれているパッケージを使います。こちらのパッケージは一時期メンテナンスがされなくなるのが話題になっていましたが、ある日突然更新されました!
https://pub.dev/packages/dio/versions/5.0.0

httpと違ってあまり情報がないので、調べながら色々今後は試していこうかなと思います。今回は、httpメソッドのGETを使うだけです。

今回使用したREST API
アメコミのヒーローのデータがJSON形式で送信されてくるようです。
https://protocoderspoint.com/jsondata/superheros.json

今回はとりあえず、海外の動画参考に勉強してみました。

https://www.youtube.com/watch?v=6x098QQM7Ew

home_page.dart
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  // finalだと使えない?lateをつけるとエラーが発生する
  var jsonList;
  // アプリが起動したときに実行する
  
  void initState() {
    // TODO: implement initState
    super.initState();
    getData();
  }

  void getData() async {
    try {
      // APIからの応答を保持する変数
      final response = await Dio()
          .get('https://protocoderspoint.com/jsondata/superheros.json');
      if (response.statusCode == 200) {
        setState(() {
          jsonList = response.data['superheros'] as List;
        });
      } else {
        print(response.statusCode);
      }
    } catch (e) {
      print(e);
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        itemBuilder: (BuildContext context, int index) {
          return Card(
            child: ListTile(
              // ネットワークから取得した画像を丸くす
              // 画像の幅と高さは50に設定する
              leading: ClipRRect(
                borderRadius: BorderRadius.circular(80),
                child: Image.network(
                  jsonList[index]['url'],
                  fit: BoxFit.fill,
                  width: 50,
                  height: 50,
                ),
              ),
              title: Text(jsonList[index]['name']),
              subtitle: Text(jsonList[index]['power']),
            ),
          );
        },
        itemCount: jsonList == null
            ? 0
            : jsonList.length, // nullだったら0で、データがあれば存在する数だけ取得する
      ),
    );
  }
}

アプリを実行するコード

main.dart
import 'package:flutter/material.dart';
import 'package:widget_example/home_page.dart';

void main() {
  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(
          appBarTheme: const AppBarTheme(
        backgroundColor: Colors.white,
        centerTitle: true,
        elevation: 0,
      )),
      home: const HomePage(),
    );
  }
}

表示できました🙌


でもこれだけだと、入門レベルですよね。GETするだけとはいえ、モデルクラスがないのは、問題だったりします。
こちらの記事を参考にRiverpod + Freezedでコードをリファクタリングしてみました。
仕組みさえ分かれば、Firebaseより簡単なのかも知れませんね。
https://terupro.net/flutter-api-dio-sample/

RiverpodとFreezedに必要なパッケージを追加します。

pabspec.yaml
name: widget_example
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: '>=2.19.4 <3.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:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  dio: ^5.0.0
  flutter_riverpod: ^2.3.2
  freezed: ^2.3.2
  freezed_annotation: ^2.2.0
  json_serializable: ^6.6.1
  build_runner: ^2.3.3

dev_dependencies:
  flutter_test:
    sdk: flutter

  # 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

# 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

モデルを作成する

次に、REST APIから取得したい内容のクラスを定義します。今回は、名前、パワー、画像のパスを取得します。「パワーってなんのことでしょうね?」
modelディレクトリを作成して、superheros.dartを作成します。

model/superheros.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';

part 'superheros.freezed.dart';
part 'superheros.g.dart';


class Superheros with _$Superheros {
  factory Superheros({
    String? name,
    String? power,
    String? url,
  }) = _Superheros;

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

モデルクラスを作成したら、Freezedのコマンドを実行して、toJSON,FromJSONを自動生成してくれるコマンドを実行します。
このコマンドはずっと実行されるので、Macの場合だと、 control + c を押して停止します。

flutter pub run build_runner watch --delete-conflicting-outputs

これにより、ファイルが自動生成されます。–delete-conflicting-outputsを加えることで、クラスに変更が加えられる度に再生成されます。


次にAPIを呼び出してデータを取得するためのクラスを作成します。
こちらで、Dioを使用します。
repositoryディレクトリを作成して、api_client.dartを作成してください。

repository/api_client.dart
import 'package:dio/dio.dart';
import 'package:widget_example/model/superheros.dart';

class ApiClient {
  Future<List<Superheros>?> fetchList() async {
    final dio = Dio();
    const url = 'https://protocoderspoint.com/jsondata/superheros.json';
    final response = await dio.get(url);

    if (response.statusCode == 200) {
      try {
        final data = response.data['superheros'] as List;
        final list = data.map((e) => Superheros.fromJson(e)).toList();
        return list;
      } catch (e) {
        throw e;
      }
    }
  }
}

APIを取得するためのメソッドをApiClientクラスから、呼び出します。
repository.dartを作成して、ロジックを記述します。

repository/repository.dart
import 'package:widget_example/repository/api_client.dart';

class Repository {
  final api = ApiClient();
  dynamic fetchList() async {
    return await api.fetchList();
  }
}

取得したAPIの状態を管理するためにRiverpodを使用します。非同期処理を扱うのに適しているFutureProviderを使用します。
provider.dartを作成して、状態を管理する処理を記述します。

repository/provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:widget_example/model/superheros.dart';
import 'package:widget_example/repository/repository.dart';

final repositoryProvider = Provider((ref) => Repository());

final listProvider = FutureProvider<List<Superheros>>((ref) async {
  final repository = ref.read(repositoryProvider);
  return await repository.fetchList();
});

APIのデータを画面に表示する

FutureProviderのwhenメソッドを使用して、ローディング、エラー処理、画面にデータの表示の3つの処理を書きます。

ui/home_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:widget_example/model/superheros.dart';

import '../repository/provider.dart';

class HomePage extends ConsumerWidget {
  const HomePage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncValue = ref.watch(listProvider); //取得したAPIデータの監視
    return Scaffold(
      body: Center(
        child: asyncValue.when(
          data: (data) {
            return data.isNotEmpty
                ? ListView(
                    children: data
                        .map(
                          (Superheros superheros) => Card(
                            child: ListTile(
                              leading: ClipRRect(
                                borderRadius: BorderRadius.circular(80),
                                child: Image.network(
                                  superheros.url.toString(),
                                  fit: BoxFit.fill,
                                  width: 50,
                                  height: 50,
                                ),
                              ),
                              title: Text(superheros.name!),
                              subtitle: Text(superheros.power!),
                            ),
                          ),
                        )
                        .toList(),
                  )
                : const Text('Data is empty.');
          },
          loading: () => const CircularProgressIndicator(),
          error: (error, _) => Text(error.toString()),
        ),
      ),
    );
  }
}

main.dartでアプリを実行する

main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:widget_example/ui/home_page.dart';

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(
          appBarTheme: const AppBarTheme(
        backgroundColor: Colors.white,
        centerTitle: true,
        elevation: 0,
      )),
      home: const HomePage(),
    );
  }
}

データの取得に成功したようです。これをやるだけでこんなにコード書くんですね😵

まとめ

今回は、REST APIの方が簡単だよと言われて久しぶりにアウトプットしてみましたが、まだまだ、知識が足りていないので、学習が必要かなと思いました。
こちらに完成品のソースコードを載せています。参考までに見てみてください。
https://github.com/sakurakotubaki/DIo5.0Example/tree/main/lib

Discussion