Redis
 Computer >> コンピューター >  >> プログラミング >> Redis

Flutter、Serverless Framework、Upstash(Redis)で構築するフルスタックサーバーレスアプリ 第2部

チュートリアルシリーズ第2部へようこそ。第1部では、UpstashとServerless Framework、Redisを使ってREST APIを構築する方法を解説しました。

この第2部では、そのREST APIのエンドポイントを利用するモバイルアプリケーションをFlutterで構築していきます。

それでは始めましょう 🙃

事前準備

まず、お使いのコンピュータにFlutterがインストールされ、正常に動作している必要があります。

  • Flutter

IDEで新しいFlutterプロジェクトを作成し、任意の名前を付けてください。

依存パッケージの追加

プロジェクトのルートディレクトリにあるpubspec.yamlファイルを開き、dev_dependenciesの下に以下の依存関係を追加します。

timeago: ^3.1.0
shared_preferences: ^2.0.6
http: ^0.13.4

追加後は次のようになります。

dev_dependencies:
  flutter_test:
    sdk: flutter

  timeago: ^3.1.0
  shared_preferences: ^2.0.6
  http: ^0.13.4

各ライブラリの役割は以下の通りです。

  • timeago: Unixタイムスタンプ(例:1636824843)を「1分前」「5分前」のような人間が読みやすい形式に変換します。
  • shared_preferences: ユーザーアカウント作成後に、userIdなどの詳細情報を端末内に保存して追跡するために使用します。
  • http: HTTP通信を行うために使用します。

それでは実装に進みましょう。

ユーザー作成画面

最初に構築するのはユーザー作成画面です。この画面では「ユーザー作成エンドポイント」を呼び出します。

画面のイメージは以下の通りです。

Flutter、Serverless Framework、Upstash(Redis)で構築するフルスタックサーバーレスアプリ 第2部
ウサギの画像については気にしないでください。ImageView用のプレースホルダーです。

libフォルダの中にaccountというフォルダを作成し、その中にcreate_profile_screen.dartという新しいファイルを作ります。

最終的なlibフォルダの構成は以下のようになります。
Flutter、Serverless Framework、Upstash(Redis)で構築するフルスタックサーバーレスアプリ 第2部

新しいユーザーを作成するには、以下の情報が必要です。

  • プロフィール画像のURL
  • 名(ファーストネーム)
  • 姓(ラストネーム)
  • ユーザー名
  • エンドポイントURL

コードを見てみましょう。

static const String CREATE_USER_PROFILE_URL = "https://5vafvrk8kj.execute-api.us-east-1.amazonaws.com/dev/user";
bool _loading = false;

Future<void> createUserProfile() async {
  setState(() {
    _loading = true;
  });
  print(usernameController.text);
  print(firstNameController.text);
  print(lastNameController.text);
  print(profilePicUrl);
  await http.post(Uri.parse(CREATE_USER_PROFILE_URL),
      body: convert.jsonEncode({'username': usernameController.text,
        "firstName": firstNameController.text, "lastName": lastNameController.text,
        "profilePic": profilePicUrl})).then((response) async {

    var jsonResponse =
    convert.jsonDecode(response.body) as Map<String, dynamic>;

    setState(() {
      _loading = false;
    });
    if(response.statusCode == 400){
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(padding: EdgeInsets.all(10), backgroundColor: Colors.red, content: Text(jsonResponse['message'])));
    } else if(response.statusCode == 200) {
      print('user id is :' + jsonResponse['userId']);
      await saveUserId(jsonResponse['userId']);
      Navigator.push(context, MaterialPageRoute(builder: (context){
        return HomeScreen();
      }));
    }
  });
}

FutureはDartのコアクラスで、非同期処理を扱うために使われます。Futureオブジェクトは、将来のある時点で利用可能になる値またはエラーを表します。

http.Responseクラスには、HTTPリクエストが成功した際に受け取ったデータが格納されます。

上記のコードでは、http postメソッドを使って「ユーザー作成エンドポイント」にPOSTリクエストを送信し、レスポンスを待ちます。

レスポンスのステータスコードが200であればリクエストは成功です。作成されたUserIdをshared_preferencesに保存し、ホーム画面へ遷移します。ステータスコードが400の場合は、SnackBarでエラーメッセージを表示します。

この画面の完全なソースコードはこちら:Create Profile Screen

投稿作成機能

APIには、ユーザーが投稿を作成できるエンドポイントも含まれています。投稿作成画面は以下のようなイメージです。

Flutter、Serverless Framework、Upstash(Redis)で構築するフルスタックサーバーレスアプリ 第2部

投稿を作成するには、以下の情報が必要です。

  • userId
  • テキスト
  • imageUrl

なお、ここではデモンストレーションの目的上、既存のimageUrlを使用しています。実際のアプリでは、ユーザーが画像を選択し、サーバーにアップロードして画像URLを取得した後、そのURLを使って投稿を作成する流れになります。

CreatePostメソッドは、先ほどのCreateUserメソッドとよく似た構造になっています。

Future<void> createPost(String userId) async {
  await http
      .post(Uri.parse(CREATE_USER_POST_URL),
          body: convert.jsonEncode({
            'userId': userId,
            "postText": postTextController.text,
            "postImage": _postPicUrl[i]
          }))
      .then((response) async {
    var jsonResponse =
        convert.jsonDecode(response.body) as Map<String, dynamic>;

    setState(() {
      _loading = false;
    });
    if (response.statusCode == 400) {
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          padding: EdgeInsets.all(10),
          backgroundColor: Colors.red,
          content: Text(jsonResponse['message'])));
    } else if (response.statusCode == 200) {
      print('post id is :' + jsonResponse['id']);
      Navigator.of(context).pop();
    }
  });
}

全投稿の一覧表示

アプリのホーム画面では、作成されたすべての投稿を一覧表示します。

イメージは以下の通りです。

Flutter、Serverless Framework、Upstash(Redis)で構築するフルスタックサーバーレスアプリ 第2部

すべての投稿をストレスなく取得するためには、まず単一の投稿を表すカスタムDartオブジェクトを作成する必要があります。

class Post {
  String? postText;
  String? userId;
  String? createdOn;
  String? id;
  String? postImage;
  PostAdmin? postAdmin;

  Post(
      {this.postText,
      this.userId,
      this.createdOn,
      this.id,
      this.postImage,
      this.postAdmin});

  Post.fromJson(Map<String, dynamic> json) {
    postText = json['postText'];
    userId = json['userId'];
    createdOn = json['createdOn'];
    id = json['id'];
    postImage = json['postImage'];
    postAdmin = json['postAdmin'] != null
        ? PostAdmin.fromJson(json['postAdmin'])
        : null;
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['postText'] = this.postText;
    data['userId'] = this.userId;
    data['createdOn'] = this.createdOn;
    data['id'] = this.id;
    data['postImage'] = this.postImage;
    if (this.postAdmin != null) {
      data['postAdmin'] = this.postAdmin!.toJson();
    }
    return data;
  }
}

class PostAdmin {
  String? timestamp;
  String? userId;
  String? username;
  String? firstName;
  String? lastName;
  String? profilePic;

  PostAdmin(
      {this.timestamp,
      this.userId,
      this.username,
      this.firstName,
      this.lastName,
      this.profilePic});

  PostAdmin.fromJson(Map<String, dynamic> json) {
    timestamp = json['timestamp'];
    userId = json['userId'];
    username = json['username'];
    firstName = json['firstName'];
    lastName = json['lastName'];
    profilePic = json['profilePic'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['timestamp'] = this.timestamp;
    data['userId'] = this.userId;
    data['username'] = this.username;
    data['firstName'] = this.firstName;
    data['lastName'] = this.lastName;
    data['profilePic'] = this.profilePic;
    return data;
  }
}

次に、http.ResponseをこのカスタムDartオブジェクトに変換します。

List<Post> parsePosts(String responseBody) {
  final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();

  return parsed.map<Post>((json) => Post.fromJson(json)).toList();
}

Future<List<Post>> fetchPosts(http.Client client) async {
  final response = await client
      .get(Uri.parse(GET_POSTS));

  return compute(parsePosts, response.body);
}

fetchPostsメソッドの戻り値の型はFuture<List<Post>>です。

スペックの低いデバイスでfetchPosts()関数を実行すると、JSONの解析・変換中にアプリが一瞬フリーズすることに気づくかもしれません。これがいわゆる「ジャンク(jank)」で、取り除くべきものです。

ジャンクを解消するには、解析と変換の処理をcompute関数を使ってバックグラウンドに移動します。

compute(parsePosts, response.body);

compute()関数は、負荷の高い処理をバックグラウンドのアイソレート(isolate)で実行し、その結果を返します。

FutureBuilderによる非同期表示

ホーム画面のファイルでは、FutureBuilderウィジェットを使って、データベースからすべての投稿をリストとして非同期に取得します。

FutureBuilderには2つのパラメータを渡す必要があります。

  • 扱いたいFuture。今回はfetchPosts()関数が返すFutureです。
  • Futureの状態(ロード中、成功、エラー)に応じて、何を描画するかをFlutterに伝えるビルダー関数。

snapshot.hasDataは、スナップショットにnull以外のデータが含まれている場合にのみtrueを返すことに注意してください。

fetchPostsはnull以外の値のみを返すため、「404 Not Found」などのサーバーレスポンスの場合でも例外をスローするようにします。例外がスローされるとsnapshot.hasErrorがtrueになり、これを使ってエラーメッセージを表示できます。

それ以外の場合は、ローディングスピナーが表示されます。

Expanded(child: FutureBuilder<List<Post>>(
  future: _posts,
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      List<Post>? posts = snapshot.data;
      if(posts != null){
        return ListView.builder(itemBuilder: (context,index){
          return Card(
            child: Container(
              padding: EdgeInsets.all(10),
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  ClipRRect(
                    borderRadius: BorderRadius.circular(1000),
                    child: Image.network(
                      posts[index].postAdmin!.profilePic!,
                      fit: BoxFit.cover,
                      height: 40,
                      width: 40,
                    ),
                  ),
                  Expanded(
                    child: Container(
                      padding: EdgeInsets.only(left: 10),
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.start,
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(posts[index].postAdmin!.username!, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),),
                          Text(posts[index].postText!),
                          ClipRRect(
                            borderRadius: BorderRadius.circular(10),
                            child: Image.network(
                              posts[index].postImage!,
                              fit: BoxFit.cover,
                              height: 150,
                              width: size.width,
                            ),
                          ),
                        ],
                      ),
                    ),
                  )
                ],
              ),
            ),
          );
        }, itemCount: posts.length,);
      }
    } else if (snapshot.hasError) {
      return Text("${snapshot.error}");
    }

    // デフォルトではローディングスピナーを表示
    return Container(
        height: 40,
        width: 40,
        child: Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).colorScheme.secondary))));
  },
))

initStateメソッド内でfetchPostsを呼び出します。

late Future<List<Post>> _posts;

@override
void initState() {
  super.initState();
  _posts = fetchPosts(http.Client());
}

buildメソッドではなくinitStatefetchPostsを呼び出す理由は、Flutterはビュー内の何かを変更する必要があるたびにbuild()メソッドを呼び出し、しかもそれが驚くほど頻繁に発生するからです。build()メソッド内にフェッチ処理を残しておくと、不要なAPIコールが大量に発生し、アプリの速度が低下してしまいます。

完全なソースコードはこちらから確認できます:complete source code

まだインターフェースを作成すべきエンドポイントがいくつか残っていますが、良いチュートリアルには演習問題がつきものですよね 😂

まとめ

このシリーズでは、Upstashを使ってサーバーレスのREST APIを構築し、それをモバイルアプリケーションから利用する方法を見てきました。

皆さんがUpstashで次に何を構築するのか、またこのチュートリアルをどのように自分のユースケースに合わせて発展させるのか、ぜひ見てみたいです。

この記事が役に立ったと思ったら、SNSでシェアしてください。

質問がありますか?コメントを残してください。

誤りを見つけたら、コメントでお知らせください。すぐに対応します。

Happy Coding ✌🏿

参考資料

  • Upstash Docs
  • Redis
  • Flutter
  • Fetching data from the internet(インターネットからのデータ取得)
  1. Nuxt 3とサーバーレスRedis(Upstash)で始めるページ訪問カウント実装

    Nuxt 3とサーバーレスRedis(Upstash)で始めるページ訪問カウント実装 アプリケーションの利用状況を追跡したり、リソース利用を制限したり、キャッシュからデータを取得してパフォーマンスを向上させたりする必要がある場合、Redisがその答えとなります。Redisはインメモリのキー・バリュー型データベースであり、オープンソースで「Remote Dictionary Server」の略称です。 この記事では、サーバーレスRedisサービスであるUpstashと、Vue SSRフレームワークの最新ベータ版であるNuxt 3を組み合わせた基本的なアプリケーション構築を通じて、Redisの基礎

  2. Flutter、サーバーレスフレームワーク、Upstash(REDIS)を備えたフルスタックサーバーレスアプリ-パート1

    この投稿では、データを保存するためのFlutter、Serverless Framework、Upstash、Redisを使用してサーバーレスモバイルアプリケーションを構築します。 Upstashとは? Upstashは、Redis用のサーバーレスデータベースです。 Upstashを使用すると、リクエストごとに支払います。これは、データベースが使用されていないときに課金されないことを意味します。 Upstashはデータベースを構成および管理します。これは、DynamoDBやFaunaなどの他のデータベースの強力な代替手段であり、などの利点があります。 低レイテンシ REDISAPIと同