🧬

abstractでREST APIのプログラムを作る

2022/10/05に公開

抽象クラスでプログラムの使い回しをやってみる

以前、abstractクラスでプログラムを作る学習をしたのだが、個人開発では、全然使う機会がなかった😅
色々なプログラムを見てきたが、使い回すプログラムがないと、abstractもmixinも使う機会がない。
なので、今回は、改めて勉強のために、HTTPでインターネットのデータを取得して、ページ毎に違うデータを表示するのをやってみた。
ログを出力する関数だと面白くないなと思いやってみました🧑‍💻

今回使用したDartのパッケージはこちら。
https://pub.dev/packages/http

dioというものがあるのですが、2.12以降更新されていないようなので、LIKE数も多いhttpを選びました。

こちらが完成品です
mvvmぽくしたかったんで、フォルダとファイル分けました。
https://github.com/sakurakotubaki/HTTTP-GET-Abstract

アプリのフォルダ構成はロジックとUIと抽象クラスを分けています。

lib
├── main.dart
├── model
│   └── model.dart
├── repository
│   └── base_url.dart
├── screen
│   ├── page_three.dart
│   └── page_two.dart
└── view_model
    └── view_model.dart

Listで使うモデルクラスを定義

model/model.dart
/// モデルとなるAlbumクラスを定義する
class Album {
  final int userId;
  final int id;
  final String title;

  const Album({
    required this.userId,
    required this.id,
    required this.title,
  });
  // 常に新しいインスタンスを作成しないときは、factoryを使用する。
  // シングルトンと呼ばれている
  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      userId: json['userId'],
      id: json['id'],
      title: json['title'],
    );
  }
}

ViewModelで使う抽象クラスを定義したファイル

repository/base_url.dart
import 'package:http_class/model/model.dart';

/// ロジックを持たない抽象メソッドをもつクラス抽象を定義する
abstract class BaseUrl {
  /// こちらの抽象メソッドはロジックを持たず、継承したクラスで上書きして、ロジックを変更する。
  /// モデルであるAlbumクラスを型に使って、fetchAlbumメソッドで
  /// [http通信]でデータを[GET]するときに使う.
  Future<Album> fetchAlbum();
}

ViewModelを定義したファイル
こちらで、抽象クラスを継承して、抽象クラスの抽象メソッドをoverrideで上書きして、ロジックを変更する。

view_model.dart
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:http_class/repository/base_url.dart';
import '../model/model.dart';

class UrlOne implements BaseUrl {
  
  Future<Album> fetchAlbum() async {
    print('抽象クラスを継承したプログラム1を実行');
    final response = await http
        .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));

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

class UrlTwo implements BaseUrl {
  
  Future<Album> fetchAlbum() async {
    print('抽象クラスを継承したプログラム2を実行');
    final response = await http
        .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/2'));

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

class UrlThree implements BaseUrl {
  
  Future<Album> fetchAlbum() async {
    print('抽象クラスを継承したプログラム3を実行');
    final response = await http
        .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/3'));

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

アプリのプログラムを実行するmain.dartで、JSONPlaceholderというREST APIから取得した、データを表示します。
1ページだけだと、変化がわからないと思うので、2ページと3ページを作成して、ページ毎に、メソッドを上書きして、違うURLから、REST APIを取得して画面に表示できるようにしました。

1ページ目

main.dart
import 'package:flutter/material.dart';
import 'package:http_class/screen/page_three.dart';
import 'package:http_class/screen/page_two.dart';
import 'view_model/view_model.dart';
import 'model/model.dart';

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

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

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

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

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

class _HomePageState extends State<HomePage> {
  /// [late]は変数の初期を遅らせる。nullでない変数を宣言する。
  late Future<Album> futureAlbum;

  /// Widgetの初期化時に一度だけ呼ばれるメソッド
  
  void initState() {
    super.initState();
    // 画面にHTTPでGETしたデータを表示するクラスをインスタンス化する。
    futureAlbum = new UrlOne().fetchAlbum();
    // futureAlbum = fetchAlbum();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.indigo,
        title: const Text('HTTP GET1'),
      ),
      // 他のページに移動するDrawer
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: [
            const DrawerHeader(
              decoration: BoxDecoration(
                color: Colors.blue,
              ),
              child: Text('他のページへ移動'),
            ),
            ListTile(
              title: const Text('HomePageTwoへ移動'),
              onTap: () {
                Navigator.push(context,
                    MaterialPageRoute(builder: (context) => HomePageTwo()));
              },
            ),
            ListTile(
              title: const Text('HomePageThreeへ移動'),
              onTap: () {
                Navigator.push(context,
                    MaterialPageRoute(builder: (context) => HomePageThree()));
              },
            ),
          ],
        ),
      ),
      body: Center(
        // FutureBuilderを使って画面にListのデータを描画する
        child: FutureBuilder<Album>(
          future: futureAlbum,
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              return Text(snapshot.data!.title);
            } else if (snapshot.hasError) {
              return Text('${snapshot.error}');
            }

            // By default, show a loading spinner.
            return const CircularProgressIndicator();
          },
        ),
      ),
    );
  }
}

2ページ目

screen/path_two.dart
import 'package:flutter/material.dart';
import 'package:http_class/model/model.dart';
import 'package:http_class/view_model/view_model.dart';

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

  
  State<HomePageTwo> createState() => _HomePageTwoState();
}

class _HomePageTwoState extends State<HomePageTwo> {
  late Future<Album> futureAlbum;

  
  void initState() {
    super.initState();
    futureAlbum = new UrlTwo().fetchAlbum();
    // futureAlbum = fetchAlbum();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.indigo,
        title: const Text('HTTP GET2'),
      ),
      body: Center(
        child: FutureBuilder<Album>(
          future: futureAlbum,
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              return Text(snapshot.data!.title);
            } else if (snapshot.hasError) {
              return Text('${snapshot.error}');
            }

            // By default, show a loading spinner.
            return const CircularProgressIndicator();
          },
        ),
      ),
    );
  }
}

3ページ目

screen/path_three.dart
import 'package:flutter/material.dart';
import 'package:http_class/model/model.dart';
import 'package:http_class/view_model/view_model.dart';

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

  
  State<HomePageThree> createState() => _HomePageThreeState();
}

class _HomePageThreeState extends State<HomePageThree> {
  late Future<Album> futureAlbum;

  
  void initState() {
    super.initState();
    futureAlbum = new UrlThree().fetchAlbum();
    // futureAlbum = fetchAlbum();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.indigo,
        title: const Text('HTTP GET3'),
      ),
      body: Center(
        child: FutureBuilder<Album>(
          future: futureAlbum,
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              return Text(snapshot.data!.title);
            } else if (snapshot.hasError) {
              return Text('${snapshot.error}');
            }

            // By default, show a loading spinner.
            return const CircularProgressIndicator();
          },
        ),
      ),
    );
  }
}

スクリーンショット

1ページの画面

drawerの画面

2ページの画面

3ページの画面

最後に

今回、REST APIを操作するのをやってみたのは、開発現場ではFirebaseはあまり使ってないので、HTTP通信を使ったアプリの知識を身につけたいと思ったからです。覚えないといけないことは、たくさんあるので、調べながら、APIを操作するプログラムを最近作ってみたりしてますね。

Discussion