LoginSignup
9
3

More than 1 year has passed since last update.

FlutterでAPI連携+HTMLの描画をする

Posted at

初めに

FlutterのサーバーサイドはFirebaseばかり触っていたのですが、
普通にAPI連携もやっとかないと!と思い備忘録として記事を書きました。

今回は、Google Books APIを用いて簡易的な書籍リストと一覧表示をします。
また、APIのレスポンスの中にHTMLが含まれているため、Flutterで簡易的にhtmlタグを表示する内容について記述していきます。

使用パッケージ

  • go_router: ^3.1.1
  • url_launcher: ^6.1.2
  • flutter_html: ^2.2.1
  • http: ^0.13.4

最終ゴール

まずは全体のコード

サクッとコードを見ていきましょう。
主要部分の解説は、コードの下に記述していきます。

main.dart
import 'package:flutter/material.dart';
import 'package:flutter_rest_api/detail_page.dart';
import 'package:go_router/go_router.dart';

import 'package:http/http.dart' as http;
import 'dart:convert';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  MyApp({Key? key}) : super(key: key);

  @override
  // GoRouterの設定
  Widget build(BuildContext context) => MaterialApp.router(
        routeInformationParser: _router.routeInformationParser,
        routerDelegate: _router.routerDelegate,
      );
  // ルーティングの設定
  final GoRouter _router = GoRouter(
    routes: <GoRoute>[
      GoRoute(
        path: '/',
        builder: (BuildContext context, GoRouterState state) =>
            const MyHomePage(title: 'Flutter REST API'),
      ),
      GoRoute(
        path: '/book/:id', // 本のIDを引き渡す
        builder: (context, state) {
          // パスパラメータの値を取得するには state.params を使用
          final String id = state.params['id']!;
          return BookDetailPage(id: id);
        },
      ),
    ],
    initialLocation: '/',
  );
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // APIのレスポンスを格納する
  List items = [];
  // APIリクエスト
  Future<void> getData() async {
    var response = await http.get(Uri.https(
        'www.googleapis.com', // ベースURL
        '/books/v1/volumes', // パス
        {'q': '{Flutter}', 'maxResults': '35', 'langRestrict': 'ja'})); // パラメーター

    var jsonResponse = jsonDecode(response.body);

    setState(() {
      items = jsonResponse['items'];
    });
  }

  @override
  void initState() {
    super.initState();
    getData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter REST API'),
      ),
      // データのリストを簡易的に表示
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (BuildContext context, int index) {
          return Card(
            child: Column(
              children: <Widget>[
                items[index] != null
                    ? GestureDetector(
                        child: ListTile(
                          // サムネ(書籍画像)
                          leading: Image.network(
                            items[index]['volumeInfo']['imageLinks']
                                    ['thumbnail'] ??
                                '',
                          ),
                          // 書籍タイトル
                          title:
                              Text(items[index]['volumeInfo']['title'] ?? ''),
                          // 発行年月日
                          subtitle: Text(items[index]['volumeInfo']
                                  ['publishedDate'] ??
                              ''),
                        ),
                        // IDを詳細画面に引き継ぐ
                        onTap: () {
                          // 詳細画面へ遷移
                          context.go('/book/${items[index]['id']}');
                        },
                      )
                    : const SizedBox(),
              ],
            ),
          );
        },
      ),
    );
  }
}
detail_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_rest_api/debug_function.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:url_launcher/url_launcher.dart';

class BookDetailPage extends StatefulWidget {
  const BookDetailPage({Key? key, required this.id}) : super(key: key);

  final String id;

  @override
  State<BookDetailPage> createState() => _BookDetailPageState();
}

class _BookDetailPageState extends State<BookDetailPage> {
  // APIのレスポンスを格納する
  late Map<String, dynamic> bookInfo;
  late Map<String, dynamic> saleInfo;
  bool isFetching = true;

  Future<void> getData() async {
    var response = await http.get(Uri.https(
        'www.googleapis.com',
        '/books/v1/volumes/${widget.id}'));
    var jsonResponse = jsonDecode(response.body);

    setState(() {
      bookInfo =  Map<String,dynamic>.from(jsonResponse['volumeInfo']);
      saleInfo =  Map<String,dynamic>.from(jsonResponse['saleInfo']);
      isFetching = false;
    });
  }

  // 外部リンクへの遷移
  void _launchURL(String linkText) async {
    var url = Uri.parse('https://developer.android.com/?hl=ja');
    // 正しいURLかチェックする
    if (await canLaunchUrl(url)) {
      // URLを開く
      await launchUrl(url);
    } else {
      throw 'Could not launch $url';
    }
  }

  String _authorsText(List authors){
    String text = '';
    int index = 1;
    for(final author in authors){
      text += author;
      if(index != authors.length){
        text += '/';
      }
      index ++;
    }
    return text;
  }

  @override
  void initState() {
    getData();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return isFetching ? Scaffold(
      appBar: AppBar(
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () => context.go('/'),
        ),
        title: const Text('Loading...'),
      ),
      body: const Center(child: CircularProgressIndicator()),
    ) : Scaffold(
      appBar: AppBar(
        title: const Text('書籍詳細'),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () => context.go('/'),
        ),
      ),
      body: ListView(
        children: <Widget>[
          const SizedBox(height: 15),
          Center(
            child: Text(bookInfo['title'], style: const TextStyle(
              fontSize: 20,
              fontWeight: FontWeight.w400,
            ),maxLines: 3,),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 20),
            child: SizedBox(
              height: 160,
              child: GestureDetector(
                // child: Container(color: Colors.red, width: 30, height: 30),
                child: Image.network(bookInfo['imageLinks']['small']),
                onTap: () {
                  _launchURL(saleInfo['buyLink']);
                }
              ),
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 8),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Text('販売価格'),
                const SizedBox(width: 20),
                Text(${saleInfo['listPrice']['amount'].toString()}'),
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 8),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Text('著者'),
                const SizedBox(width: 20),
                Text(_authorsText(bookInfo['authors'])),
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 8),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Text('出版社'),
                const SizedBox(width: 20),
                Text(bookInfo['publisher']),
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 15),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                const Padding(
                  padding: EdgeInsets.symmetric(vertical: 10),
                  child: Text('書籍概要', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
                ),
                Html(data: bookInfo['description']),
              ],
            ),
          ),

        ],
      ),
    );
  }
}

主要部分の解説

APIリクエスト

APIのリクエストをするには、httpパッケージを用いることで簡単にREST APIの処理が実装できます。作成段階ではまだ0系なものの、Likesの多さやPopularityを見たら問題はないかなと思います。

main.dart
  import 'package:http/http.dart' as http;
  import 'dart:convert';

  Future<void> getData() async {
    // GETリクエスト
    var response = await http.get(Uri.https(
        'www.googleapis.com', // ベースURL
        '/books/v1/volumes', // パス
        {'q': '{Flutter}', 'maxResults': '35', 'langRestrict': 'ja'})); // パラメータ

    var jsonResponse = jsonDecode(response.body);

    setState(() {
      items = jsonResponse['items']; // 配列に格納
    });
  }

外部ページへ遷移する

外部のページをブラウザで開きたい(htmlのaタグ的なことがしたい)場合には、url_launcherパッケージがおすすめです。

detal_page.dart
  import 'package:url_launcher/url_launcher.dart';

  void _launchURL(String linkText) async {
    var url = Uri.parse(linkText);
    // 正しいURLかチェックする
    if (await canLaunchUrl(url)) {
      // URLを開く
      await launchUrl(url);
    } else {
      throw 'Could not launch $url';
    }
  }

HTMLを描画する

Flutterの内部構造を深く知っているわけではないですが、HTMLの描画をするには一手間必要です。
今回はすごく簡易的で良かったため、flutter_htmlパッケージを利用してAPIから返ってくるHTMLを描画することができました。

detal_page.dart
import 'package:flutter_html/flutter_html.dart';

// Widget的な使い方です
Html(data: bookInfo['description']),

この書籍概要の部分は、Columnウィジェットなどは使っておらず、全てHTMLを変換したものです。

以上です。
flutter_htmlやhttpパッケージは、もっといろいろなことができます。
今回は簡易的な紹介でしたが、ぜひ興味を持った方は活用してみてください。

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3