🤩

アニメ引用アプリ AnimeChan APIとFlutter(Part 1)

2022/09/19に公開

日本語版:

今日はAnimeChan Api、riverpod、http、flutterを使ってアニメ引用を表示するアプリを作ってみましょう。

まず、必要な依存関係を追加します。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
    
  http: ^0.13.5
  flutter_riverpod: ^2.0.0-dev.9

次に、main.dart ファイルを少し変更してみましょう。

main
void main() {
  runApp(
    // ウィジェットがプロバイダを読み込めるようにするために、アプリケーション全体を "ProviderScope" ウィジェットでラップする必要があります。
    // ここに、プロバイダの状態が保存されます。
    const ProviderScope(
      child: Navi(),
    ),
  );
}

ここで、Navi() は、アプリケーション内でナビゲーションを行うためのクラスです。

そして、このクラスは navi.dart ファイルに記述されます。

navi.dart
class Navi extends ConsumerWidget {
  const Navi({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(
      routes: {
        '/': (context) => const RandomQuote(),
      },
    );
  }
}

そこで、riverpodパッケージからインポートした ConsumerWidget を使用します。

ConsumerWidget

プロバイダをリッスンすることができる [StatelessWidget] です。
[ConsumerWidget] を使うことで、ウィジェットツリーがプロバイダの変更をリスンできるようになり、必要なときに UI が自動的に更新されるようになります。

RandomQuote() クラスでは、APIを使用してランダムなアニメの引用を表示することを試みます。しかし、このクラスの作成前に、プロバイダを格納する providers.dart と、定数値を書き込む const.dart 、APIから取得した情報を格納する anime.dart を作成しましょう。

最初に const.dart

const.dart
const baseUrl = "https://animechan.vercel.app/api";

次に anime.dart

anime.dart
class Anime {
  final String anime;
  final String character;
  final String quote;

  Anime({
    required this.anime,
    required this.character,
    required this.quote,
  });

  factory Anime.fromJson(Map<String, dynamic> json) {
    return Anime(
      anime: json['anime'],
      character: json['character'],
      quote: json['quote'],
    );
  }
}

最後に providers.dart

providers.dart
final actionProvider = StateProvider<AutoDisposeFutureProvider>((ref) {
  return randomQuote;
});

final randomQuote = FutureProvider.autoDispose(
  (ref) async {
    final response = await http.get(Uri.parse('$baseUrl/random'));

    if (response.statusCode == 200) {
      // サーバーが200 OKレスポンスを返した場合、JSONをパース(解析)します。
      return Anime.fromJson(jsonDecode(response.body));
    } else {
      // サーバーが200 OKレスポンスを返しなかった場合、例外をスローします。
      throw Exception('Failed to load random quote');
    }
  },
);
.autoDispose

一般的なユースケースは、プロバイダが使用されなくなったときに、その状態を破棄することです。

ナイス!さて、プロバイダを用意したら、次は RandomQuote クラスを作成してみましょう。

random.dart
class RandomQuote extends ConsumerStatefulWidget {
  const RandomQuote({Key? key}) : super(key: key);

  
  ConsumerState<ConsumerStatefulWidget> createState() => _RandomQuoteState();
}

class _RandomQuoteState extends ConsumerState<RandomQuote> {

  
  Widget build(BuildContext context) {
    final actionProv = ref.read(actionProvider);
    final itemValue = ref.watch(actionProv);
    return Scaffold(
      appBar: AppBar(
        title: Text("ランダム引用"),
        centerTitle: true,
      ),
      body: itemValue.when(
        data: (item) {
          return GestureDetector(
              onTap: () => ref.refresh(randomQuote.future),
              child: Center(
                child: ListTile(
                  title: Text(
                    item.quote,
                    textAlign: TextAlign.center,
                  ),
                  subtitle: Text(
                    "${item.character}${item.anime})",
                    textAlign: TextAlign.center,
                  ),
                ),
              ));
        },
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (e, st) => Center(child: Text(e.toString())),
      ),
    );
  }
}

難しいかもしれませんが、最初から説明しましょう。

AutoDisposeFutureProviderを返すactionProviderを使っているので、まずその値を読む必要があります。ですからactionProvを使うです。
次に、itemValueを使用して、actionProviderから返された値がどのように動作するかを観察します。
riverpodのドキュメントによると、FutureProviderを使うときは、Fututre Builderの代わりにwhen() を使うのが良いそうです。

引用文がより美しく見えるように、ListTile ウィジェットを使用しています。
タイトルには引用文を、サブタイトルにはキャラクター名とアニメ名を使用します。

データの読み込み中は、画面に CircularProgressIndicator() が表示されます。
また、エラーが発生した場合は、中央揃えでエラーテキストを表示します。

なぜ GestureDetector が必要なのでしょうか?
私たちのAPIでは、1回のリクエストにつき1つの結果しか得られません。
そのため、私たちが見積をタップしようとすると、もう一回リクエストを行い、新しい素晴らしい見積を取得することになります。
そのために、ref.refresh() メソッドを使用します。

そして、私たちのプログラムをエミュレートしようとした時に、こういうものが出てくるはずです。

うん、なかなか手強いエラーですね。でも、心配しないで、コードを書き足すだけで解決できますよ。

main.dart


+ class PostHttpOverrides extends HttpOverrides {
  
  HttpClient createHttpClient(context) {
    return super.createHttpClient(context)
      ..badCertificateCallback =
          (X509Certificate cert, String host, int port) => true;
  }
}

void main() {
+ HttpOverrides.global = PostHttpOverrides();
  runApp(
    const ProviderScope(
      child: Navi(),
    ),
  );
}

これらを追加した後、私たちのアプリを真似てみてください。

おめでとうございます、あなたはランダム引用アニメアプリを作りました。:D

追加事項として、ユーザがタップして引用を変更できることがわかるように、スナックバーを追加しましょう。

random.dart
class _RandomQuoteState extends ConsumerState<RandomQuote> {
+ var snackBar = SnackBar(
     content: const Text('Click on Text to change Quote'),
     action: SnackBarAction(
       label: 'Undo',
       onPressed: () {},
     ),
   );

  
+  void initState() {
    // TODO: implement initState
    super.initState();
+   Future.delayed(
      Duration.zero,
      () {
        ScaffoldMessenger.of(context).showSnackBar(snackBar);
      },
    );
  }

Future.delayed(Duration.zero,() {} のおかげで、initState()でSnackBarを使用することができます。

参考:
riverpood
animechan

English ver:

Today we will try to create an application to display a anime quotes using AnimeChan Api, riverpod, http and flutter.

First let's add necessary dependencies.

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
    
  http: ^0.13.5
  flutter_riverpod: ^2.0.0-dev.9

Next, let's change the main.dart file a bit.

main
void main() {
  runApp(
    // For widgets to be able to read providers, we need to wrap the entire
    // application in a "ProviderScope" widget.
    // This is where the state of our providers will be stored.
    const ProviderScope(
      child: Navi(),
    ),
  );
}

Here the Navi() is a Class that we will use to navigate in our application.

And that class we will write in navi.dart file.

navi.dart
class Navi extends ConsumerWidget {
  const Navi({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(
      routes: {
        '/': (context) => const RandomQuote(),
      },
    );
  }
}

So we use ConsumerWidget that we imported from riverpod package.

ConsumerWidget

A [StatelessWidget] that can listen to providers.
Using [ConsumerWidget], this allows the widget tree to listen to changes on provider, so that the UI automatically updates when needed.

In the RandomQuote() class we will try to show a random anime quote by using our API. But before we move on to creating this class, let's create providers.dart in which we will store providers, const.dart where we will write const values and anime.dart which we store information that we took from API.

First const.dart

const.dart
const baseUrl = "https://animechan.vercel.app/api";

Then anime.dart

anime.dart
class Anime {
  final String anime;
  final String character;
  final String quote;

  Anime({
    required this.anime,
    required this.character,
    required this.quote,
  });

  factory Anime.fromJson(Map<String, dynamic> json) {
    return Anime(
      anime: json['anime'],
      character: json['character'],
      quote: json['quote'],
    );
  }
}

and finally providers.dart

providers.dart
final actionProvider = StateProvider<AutoDisposeFutureProvider>((ref) {
  return randomQuote;
});

final randomQuote = FutureProvider.autoDispose(
  (ref) async {
    final response = await http.get(Uri.parse('$baseUrl/random'));

    if (response.statusCode == 200) {
      // If the server did return a 200 OK response,
      // then parse the JSON.
      return Anime.fromJson(jsonDecode(response.body));
    } else {
      // If the server did not return a 200 OK response,
      // then throw an exception.
      throw Exception('Failed to load random quote');
    }
  },
);
.autoDispose

A common use case is to destroy the state of a provider when it is no-longer used.

Nice! Now when we prepared our providers we can try to create RandomQuote class

random.dart
class RandomQuote extends ConsumerStatefulWidget {
  const RandomQuote({Key? key}) : super(key: key);

  
  ConsumerState<ConsumerStatefulWidget> createState() => _RandomQuoteState();
}

class _RandomQuoteState extends ConsumerState<RandomQuote> {

  
  Widget build(BuildContext context) {
    final actionProv = ref.read(actionProvider);
    final itemValue = ref.watch(actionProv);
    return Scaffold(
      appBar: AppBar(
        title: Text("Random Quote"),
        centerTitle: true,
      ),
      body: itemValue.when(
        data: (item) {
          return GestureDetector(
              onTap: () => ref.refresh(randomQuote.future),
              child: Center(
                child: ListTile(
                  title: Text(
                    item.quote,
                    textAlign: TextAlign.center,
                  ),
                  subtitle: Text(
                    "${item.character}${item.anime})",
                    textAlign: TextAlign.center,
                  ),
                ),
              ));
        },
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (e, st) => Center(child: Text(e.toString())),
      ),
    );
  }
}

I know there is a lot going on here, but let's start from the beginning.

Because we use actionProvider that return AutoDisposeFutureProvider, first we need to read that value that's why we use actionProv.
Then we use itemValue to watch how returned value from actionProvider behave.
Based on the documentation provided by riverpod, when we use FutureProvider, it is better to use when() instead of Fututre builder.

We use ListTile widget, because our quote will look more aesthetic.
As a title, we use our quote and as a subtitle, character name and anime name.

When the data is loading the screen will show CircularProgressIndicator().
And in error case, centered error text.

Why do we need GestureDetector ?
Our API gives us only one result per one request.
So when we gonna tap on our quote, we will make one more request and we will get new amazing quote.
To make it, we use ref.refresh() method.

And when are you going to try to emulate our program, you will see this.

Yeah, it's pretty tough error. But don't worry we can solve it just by adding some things to our code.

main.dart


+ class PostHttpOverrides extends HttpOverrides {
  
  HttpClient createHttpClient(context) {
    return super.createHttpClient(context)
      ..badCertificateCallback =
          (X509Certificate cert, String host, int port) => true;
  }
}

void main() {
+ HttpOverrides.global = PostHttpOverrides();
  runApp(
    const ProviderScope(
      child: Navi(),
    ),
  );
}

After adding those things you can try to emulate our app.

Congratulations you made your random quote anime app! :D

As a something extra let's add a snackbar for user, so that he/she will knows that through tap he/she can change quote.

random.dart
class _RandomQuoteState extends ConsumerState<RandomQuote> {
+ var snackBar = SnackBar(
     content: const Text('Click on Text to change Quote'),
     action: SnackBarAction(
       label: 'Undo',
       onPressed: () {},
     ),
   );

  
+  void initState() {
    // TODO: implement initState
    super.initState();
+   Future.delayed(
      Duration.zero,
      () {
        ScaffoldMessenger.of(context).showSnackBar(snackBar);
      },
    );
  }

Thanks to Future.delayed(Duration.zero,() {} we can use SnackBar in initState().

References:
riverpood
animechan

Discussion