Chapter 04

HTTPのPOSTメソッドを使う

JboyHashimoto
JboyHashimoto
2022.10.14に更新

データを追加してみる。

モックサーバーにデータを追加するサンプルも作ってみました。モデルが違うと画面にデータを表示できなかったので、別のアプリ作りました。
json-serverは、Flutterのプロジェクトの中に今回は配置しています。起動するときは、VScodeか、ターミナルでコマンドを入力して起動します。
Widgetを公式のサンプルでは、切り分けていたので、試してみたのですが、同じページにクラスを書かないと、プロパティを使えないようなので、同じページに、TextEditingControllerが使えるクラスを書いています。

こちらが完成品です
https://github.com/sakurakotubaki/Flutter-HTTP-POST

まずは、モデルを作成します。データを取得するget_model.dartとデータを保存するpost_model.dartを作成します。

Flutterのプロジェクトを作る

新しくプロジェクトを作ったら、その中で今回はmock-serverディレクトリを作成して、その中にcdコマンドで移動して、json-serverの環境構築を行います。

db.jsonですが、今回は空っぽにしておきます。後でPOSTすると、データが追加されていきます。

db.json
{
  "bookList": [
  ],
  "profile": {
    "name": "typicode"
  }
}

POSTメソッドで使うファイル

公式ドキュメントに載っていたものを参考にしました。やはり勉強するなら、公式ドキュメントですね。

model/post_model.dart
class PostModel {
  final int id;
  final String title;

  const PostModel({required this.id, required this.title});

  factory PostModel.fromJson(Map<String, dynamic> json) {
    return PostModel(
      id: json['id'],
      title: json['title'],
    );
  }
}

GETメソッドで使うファイル

以前作ったものを修正しただけですね。プロパティが減っただけです。シンプルにしすぎた...

model/get_model.dart
import 'dart:convert' as convert;
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;

/// [mocker-serverから、データを取得するクラスを定義]
/// [JSONのデータの中のオブジェクトと同じ名前にする]
class GetModel {
  final int id; // idは数字なのでint型
  final String title; // titleは文字なのでstring型

  // コンストラクターを定義
  GetModel({required this.id, required this.title});

  // 新しいインスタンスを作成し、jsonを解析してデータを
  // 新しいインスタンスに配置します。(公式を翻訳)
  GetModel.fromJson(Map<String, dynamic> json)
      : id = json['id'],
        title = json['title']; //最後のところはセミコロンをつける

  // インスタンスをマップに変換するtoJson()メソッド
  Map<String, dynamic> toJson() => {'id': id, 'title': title};
}

/// [BookListを型に使いモックサーバーからデータを取得する関数.]
List<GetModel> parseBook(String responseBody) {
  // 引数をキャストしてMap型に変換
  final parsed = convert.jsonDecode(responseBody).cast<Map<String, dynamic>>();
  // 配列をmapメソッドでループさせる。
  return parsed.map<GetModel>((json) => GetModel.fromJson(json)).toList();
}

// インターネットからデータを取得するメソッド
Future<List<GetModel>> fetchPosts() async {
  const endpoint = 'http://localhost:3000/bookList/';
  var url = Uri.parse(endpoint);

  var response = await http.get(url);
  if (response.statusCode == 200) {
    // ステータスコードを表示する
    print('ステータスコード: ${response.statusCode}');
    // JSONのデータを表示する
    print('JSONのデータ:  ${response.body}');
    return compute(parseBook, response.body);
  } else {
    throw Exception('Failed to load book');
  }
}

HTTPを呼び出すためのファイル

前回使ったProviderとHTTPのGETをするロジックを書いたファイルをdomainディレクトリに配置しています。

POSTをするためのファイル

domain/book_domain.dart
import 'package:http_post_app/model/post_model.dart';
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<PostModel> createBook(String title) async {
  print('Riverpodで実行');
  final response = await http.post(
    Uri.parse('http://localhost:3000/bookList/'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title,
    }),
  );

  if (response.statusCode == 201) {
    print(response.statusCode);
    print(response.body);
    // サーバーが 201 CREATED レスポンスを返した場合。
    // JSON をパースします。
    return PostModel.fromJson(jsonDecode(response.body));
  } else {
    // サーバーが 201 CREATED レスポンスを返さなかった場合。
    // 例外が発生する。
    throw Exception('Failed to create BookList.');
  }
}

前回使ったGETで使うProviderが書かれたファイル

domain/book_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http_post_app/model/get_model.dart';

// 自動でデータを再取得するよう設定するFutureProvider.
final booksProvider = FutureProvider<List<GetModel>>((ref) async {
  // model.dartの関数を呼び出す.
  return fetchPosts();
});

アプリの画面

今回は入力フォームをmain.dartに配置して、画面にデータを表示するのは、book_page.dartで行います。
データの表示なのですが、1度だけしか呼び出せないようなので、アプリを再起動しないと、追加されたデータが見れないようでして、このような場合は、StreamProviderやStreamBuilderを使うとよさそうですね。
今回はモックにデータを追加するのを体験するだけなので、比較するものまでは作ってないです。
すいません🙇‍♂️

main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http_post_app/book_page.dart';
import 'package:http_post_app/domain/book_domain.dart';
import 'package:http_post_app/model/post_model.dart';

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

// 切り分けたWidgetで使うためのクラスを同じページに定義
// _futureBookでmodel.dartのクラスを引数に使う
class PostMethod {
  final TextEditingController _controller = TextEditingController();
  Future<PostModel>? _futureBook;
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Create Data Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const PostPage(),
    );
  }
}

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    final postClass = new PostMethod();
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.blueGrey,
        title: const Text('POSTメソッドでデータを追加'),
      ),
      body: Container(
        alignment: Alignment.center,
        padding: const EdgeInsets.all(8.0),
        child: _HTTPWidget(postClass: postClass),
      ),
    );
  }
}

class _HTTPWidget extends StatelessWidget {
  const _HTTPWidget({
    Key? key,
    required this.postClass,
  }) : super(key: key);

  final PostMethod postClass;

  
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        TextField(
          controller: postClass._controller,
          decoration: const InputDecoration(hintText: '本のタイトルを入力'),
        ),
        ElevatedButton(
          onPressed: () {
            postClass._futureBook = createBook(postClass._controller.text);
          },
          child: const Text('本を追加'),
        ),
        ElevatedButton(
            onPressed: () {
              Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => const BookPage()),
              );
            },
            child: const Text('BookPage'))
      ],
    );
  }
}

追加されたデータを表示するページ。アプリを起動して、一度しかプログラムが実行されないので、Runボタンを押して、アプリを再起動すると、追加されたデータが見れるようになります。
今回はモックにデータを追加するのが、目的なので、これ以上のことはしません。

book_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http_post_app/domain/book_provider.dart';

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    // このページで使用するriverpodを呼び出す変数を定義
    final value = ref.watch(booksProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('mock-serverからデータを習得する'),
        backgroundColor: Colors.blueGrey,
      ),
      body: Center(
        child: value.when(
            data: (books) {
              return ListView.builder(
                // 配列のデータを描画するWidget
                itemCount: books.length, // 配列の数をカウント
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text(books[index].title), // 配列のtitleプロパティを表示
                  );
                },
              );
            },
            // データが習得できなかったら、ローディングされる.
            error: (err, stack) => Center(child: Text(err.toString())),
            loading: () => const Center(child: CircularProgressIndicator())),
      ),
    );
  }
}

では、アプリを立ち上げてデータを追加していきましよう!

追加前のdb.json

追加後のdb.json

データを追加していく

画面にデータが表示されました。今回の実装だと1回しかプログラムを実行できないので、追加したら画面にデータが増えていく動作をみるのはできないですね。

世の中のアプリは、このようにHTTP通信を行なって、サーバーにデータを保存して、サーバーにリクエストを送って、文字や画像のデータを取得してアプリの画面に表示しているようです。
すごいですね。PayPayやSlackなどは、このような仕組みで動いているのでしょうね。
でないと、Web版、デスクトップ版、モバイル版が全て同じデータを表示できるわけないですもんね。