はじめに
今回は Flutter で http 通信を扱うサンプルとしてメッセージを DB に保存、リスト表示するだけのシンプルなアプリケーションを作成します。http 通信先としては Firebase のRealtime Databaseを使用して async+await での非同期通信を実装します。
その他小技集
Flutter 初心者が知りたい小技集 ① ウィジェットの分割
Flutter 初心者が知りたい小技集 ② ナビゲーションの書き方
Flutter 初心者が知りたい小技集 ④ SharedPreferencesでMapを扱う
プロジェクト作成
【Command Line】
flutter create http_sample
パッケージ追加
【Code】
// pubspec.yaml
.
.
dependencies:
  provider: ^4.3.1
  http: ^0.12.2
.
.
Firebase ではテスト用にプロジェクトを一つ用意して Realtime Database を作成します。
 
赤枠で隠してますが URL をコピーしておきます。
前準備
設定ファイル作成
【Command Line】
$ mkdir lib/constants
$ touch lib/constants/my_config.dart
【Code】
// lib/constants/my_config.dart
class MyConfig {
  static const dbUrl = "コピーしたRealtime DatabaseのURL";
}
Provider の追加
画面とロジックを分離するために Provider を使用します。
【Command Line】
$ mkdir lib/provider
$ touch lib/provider/messages.dart
【Code】
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart';
import '../constants/my_config.dart';
// モデル
class Message with ChangeNotifier {
  final id;
  final String title;
  final String body;
  Message({@required this.id, @required this.title, @required this.body});
}
// Provider本体
class Messages with ChangeNotifier {
  List<Message> _items = [];
  // ゲッター
  List<Message> get items {
    return [..._items];
  }
  // 読み込み用
  Future<void> fetchAndSetMessages() async {
    const url = MyConfig.dbUrl + "/messages.json";
    try {
      final response = await http.get(url);
      // とりあえず出力だけ
      print(json.decode(response.body));
    } catch (error) {
      throw (error);
    }
  }
  // 追加用
  Future<void> addMessage(Message message) async {
    const url = MyConfig.dbUrl + "/messages.json";
    try {
      final response = await http.post(
        url,
        body: json.encode({'title': message.title, 'body': message.body}),
      );
      final newMessage = Message(
          id: json.decode(response.body)['name'],
          title: message.title,
          body: message.body);
      _items.add(newMessage);
      notifyListeners();
    } catch (error) {
      throw error;
    }
  }
}
自身がつまづいたポイントを挙げると
print(json.decode(response.body));
とありますが response の内容をそのまま print しようとすると中身をうまく表示できません。
import 'dart:convert';
をインポートしてデコードすることによって中身まで確認できます。
また追加処理ではデータベースに登録した際の返り値を id として取得しています。
実装
ウィジェットの分割
screens フォルダを追加して my_home_page.dart として main.dart ファイルから画面を分離します。
【Command Line】
$ mkdir lib/screens
$ touch lib/screens/my_home_page.dart
main.dart ファイルには Provider を適用しています。
【Code】
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import './provider/messages.dart';
import './screens/my_home_page.dart';
void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: ChangeNotifierProvider(
          create: (ctx) => Messages(),
          child: MyHomePage(),
        ));
  }
}
// lib/screens/my_home_page.dart
import 'package:flutter/material.dart';
class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("MyApp"),
      ),
      body: Center(
        child: Text(
          'You have pushed the button this many times:',
        ),
      ),
    );
  }
}
リファクタリング
 
my_home_page.dart を StatefulWidget に変換します。vscode でしたら右クリック → リファクターで一発で変換可能です。
【Code】
// lib/screens/my_home_page.dart
import 'package:flutter/material.dart';
class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("MyApp"),
      ),
      body: Center(
        child: Text(
          'You have pushed the button this many times:',
        ),
      ),
    );
  }
}
追加処理
メッセージ追加用のボタンを AppBar におきます。
【Code】
// lib/screens/my_home_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../provider/messages.dart';
class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  var _message = Message(id: null, title: 'Test Title', body: 'Test Body');
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("MyApp"),
        actions: <Widget>[
          Consumer<Messages>(
            builder: (_, messages, child) => Center(
              child: IconButton(
                icon: Icon(Icons.add),
                onPressed: () {
                  messages.addMessage(_message);
                },
              ),
            ),
          ),
        ],
      ),
      body: Text("SHOW LIST"),
    );
  }
}
ボタンを押してみるとデータベースに値が追加されると思います。
 
表示
追加したデータを表示しましょう
フォルダを作成してリスト表示するためのウィジェットを追加します。
【Command Line】
$ mkdir lib/widgets
$ touch lib/widgets/message_list.dart
【Code】
// lib/widgets/message_list.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../provider/messages.dart';
class MessageList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final messagesData = Provider.of<Messages>(context);
    final messages = messagesData.items;
    return ListView.builder(
      itemCount: messages.length,
      itemBuilder: (ctx, i) => ChangeNotifierProvider.value(
        value: messages[i],
        child: ListTile(
          title: Text(messages[i].title),
          subtitle: Text(messages[i].body),
        ),
      ),
    );
  }
}
 
ボタンを押すたびに画面に項目が表示されるはずです。
読み込み処理
現在はデータベースから読み込んでいるわけではないのでリロードすると消えてしまいます。データベースから読み込んで値をセットするように修正しましょう。
【Code】
// lib/screens/my_home_page.dart
.
.
class _MyHomePageState extends State<MyHomePage> {
  // 追加
  var _isInit = true;
  var _isLoading = false;
  // 追加
  @override
  void didChangeDependencies() {
    // 最初の一回だけ読み込み
    if (_isInit) {
      setState(() {
        _isLoading = true;
      });
      Provider.of<Messages>(context).fetchAndSetMessages().then((_) {
        // 読み込み処理を行い、終了次第ローディングフラグをfalseに
        setState(() {
          _isLoading = false;
        });
      });
    }
    _isInit = false;
    super.didChangeDependencies();
  }
.
.
      body: _isLoading ? CircularProgressIndicator() : MessageList(),
.
.
didChangeDependencies に_isInit フラグを合わせることで Provider に書いた読み込み処理を一度だけ実行するように書きます。読み込み中はグルグルを表示させましょう。
Provider の読み込み処理を修正して値をセットするようにします。
【Code】
// lib/provider/messages.dart
.
.
  // 読み込み用
  Future<void> fetchAndSetMessages() async {
    const url = MyConfig.dbUrl + "/messages.json";
    try {
      final response = await http.get(url);
      final extractedData = json.decode(response.body) as Map<String, dynamic>;
      final List<Message> loadedMessages = [];
      extractedData.forEach((messageId, messageData) {
        loadedMessages.add(Message(
          id: messageId,
          title: messageData['title'],
          body: messageData['body'],
        ));
      });
      _items = loadedMessages;
      notifyListeners();
    } catch (error) {
      throw (error);
    }
  }
.
.
グルグルを中央に、また追加処理時にも表示させたいですね。エラーハンドリングもしつつ修正したものが以下です。
// lib/screens/my_home_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../provider/messages.dart';
import '../widgets/message_list.dart';
class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  var _isInit = true;
  var _isLoading = false;
  // 追加
  Future<void> _addToDB() async {
    setState(() {
      _isLoading = true;
    });
    try {
      var _message = Message(id: null, title: 'Test Title', body: 'Test Body');
      await Provider.of<Messages>(context, listen: false).addMessage(_message);
    } catch (error) {
      // 失敗したときにはダイアログを表示
      await showDialog<Null>(
        context: context,
        builder: (ctx) => AlertDialog(
          title: Text('An error occurred!'),
          content: Text('Something went wrong.'),
          actions: <Widget>[
            FlatButton(
              child: Text('Okay'),
              onPressed: () {
                Navigator.of(ctx).pop();
              },
            ),
          ],
        ),
      );
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }
  @override
  void didChangeDependencies() {
    // 最初の一回だけ読み込み
    if (_isInit) {
      setState(() {
        _isLoading = true;
      });
      Provider.of<Messages>(context).fetchAndSetMessages().then((_) {
        // 読み込み処理を行い、終了次第ローディングフラグをfalseに
        setState(() {
          _isLoading = false;
        });
      });
    }
    _isInit = false;
    super.didChangeDependencies();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("MyApp"),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.add),
            onPressed: () {
              _addToDB();
            },
          ),
        ],
      ),
      body: _isLoading
          ? Center(
              child: CircularProgressIndicator(),
            )
          : MessageList(),
    );
  }
}
以上になります。
最後に
コード量も多くややこしくなってしまいました。今回は小技も少なく、ほぼ自身の備忘録のようなものですが一部でも参考になるコードがあれば幸いです。
ここまで読んでくださりありがとうございました。