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通信を行うために使用します。
それでは実装に進みましょう。
ユーザー作成画面
最初に構築するのはユーザー作成画面です。この画面では「ユーザー作成エンドポイント」を呼び出します。
画面のイメージは以下の通りです。

ウサギの画像については気にしないでください。ImageView用のプレースホルダーです。
libフォルダの中にaccountというフォルダを作成し、その中にcreate_profile_screen.dartという新しいファイルを作ります。
最終的なlibフォルダの構成は以下のようになります。
新しいユーザーを作成するには、以下の情報が必要です。
- プロフィール画像の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には、ユーザーが投稿を作成できるエンドポイントも含まれています。投稿作成画面は以下のようなイメージです。

投稿を作成するには、以下の情報が必要です。
- 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();
}
});
}
全投稿の一覧表示
アプリのホーム画面では、作成されたすべての投稿を一覧表示します。
イメージは以下の通りです。

すべての投稿をストレスなく取得するためには、まず単一の投稿を表すカスタム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メソッドではなくinitStateでfetchPostsを呼び出す理由は、Flutterはビュー内の何かを変更する必要があるたびにbuild()メソッドを呼び出し、しかもそれが驚くほど頻繁に発生するからです。build()メソッド内にフェッチ処理を残しておくと、不要なAPIコールが大量に発生し、アプリの速度が低下してしまいます。
完全なソースコードはこちらから確認できます:complete source code。
まだインターフェースを作成すべきエンドポイントがいくつか残っていますが、良いチュートリアルには演習問題がつきものですよね 😂
まとめ
このシリーズでは、Upstashを使ってサーバーレスのREST APIを構築し、それをモバイルアプリケーションから利用する方法を見てきました。
皆さんがUpstashで次に何を構築するのか、またこのチュートリアルをどのように自分のユースケースに合わせて発展させるのか、ぜひ見てみたいです。
この記事が役に立ったと思ったら、SNSでシェアしてください。
質問がありますか?コメントを残してください。
誤りを見つけたら、コメントでお知らせください。すぐに対応します。
Happy Coding ✌🏿
参考資料
- Upstash Docs
- Redis
- Flutter
- Fetching data from the internet(インターネットからのデータ取得)
-
Nuxt 3とサーバーレスRedis(Upstash)で始めるページ訪問カウント実装
Nuxt 3とサーバーレスRedis(Upstash)で始めるページ訪問カウント実装 アプリケーションの利用状況を追跡したり、リソース利用を制限したり、キャッシュからデータを取得してパフォーマンスを向上させたりする必要がある場合、Redisがその答えとなります。Redisはインメモリのキー・バリュー型データベースであり、オープンソースで「Remote Dictionary Server」の略称です。 この記事では、サーバーレスRedisサービスであるUpstashと、Vue SSRフレームワークの最新ベータ版であるNuxt 3を組み合わせた基本的なアプリケーション構築を通じて、Redisの基礎
-
Flutter、サーバーレスフレームワーク、Upstash(REDIS)を備えたフルスタックサーバーレスアプリ-パート1
この投稿では、データを保存するためのFlutter、Serverless Framework、Upstash、Redisを使用してサーバーレスモバイルアプリケーションを構築します。 Upstashとは? Upstashは、Redis用のサーバーレスデータベースです。 Upstashを使用すると、リクエストごとに支払います。これは、データベースが使用されていないときに課金されないことを意味します。 Upstashはデータベースを構成および管理します。これは、DynamoDBやFaunaなどの他のデータベースの強力な代替手段であり、などの利点があります。 低レイテンシ REDISAPIと同