サーバサイドDartとFlutterを絡めたデモアプリ作ってみた所感
はじめに
Flutterにハマっている皆様、サーバサイドDartのServerpodというフレームワークはご存じでしょうか。
前々から気になっていたので軽く触ってみました。
クライアントサイドがFlutterに対するサーバサイドの選定
普及している選択肢として、JVM言語やGo、Nodeなどを用いてサーバを実装するか、Firebaseを利用して実装するかが多いと思います。
その際に、煩わしさとしてAPIレスポンスをdartでモデル定義する点が挙げられます。
しかし、ここについては「JSONからdartのモデルへと自動で一括変換してくれるツール」や「protobufを利用して自動生成コードとしてモデルを扱う」などといったアプローチで煩わしさを解決出来ます。
ただ、このServerpodを使うことでそういったアプローチの出番はなくなります。サーバとクライアントで言語が同一なため、レイヤー間での言語コミュニケーションのコストが削減されます。(Node × ReactNativeと同様ですね)
まだまだ実用的ではない所感ですが、これから小規模なスピード感溢れる現場では新たな選択肢として挙げられるかもしれません。選択肢が増えることはワクワクです🌝
実装内容
DeviceInfoのプラグインを利用して端末情報を参照し、そのパラメータをServerpodで作ったAPIを介してDBに入れたり消したり引っ張ったりします。
↓先に今回作ったrepositoryを貼っておきます。
Serverpodの導入
ここについては公式のGet Startedを順に進めるだけなので割愛します。
ただ、事前にDockerをインストールは必要です
↓よしなにbrewで叩きましょう
brew install --cask docker
今回のプロジェクト名はdevice_info_podとしました。
それでは、「サーバサイド」と「Flutter」の実装を見ていきましょう。
サーバサイド
/XXX_server
がサーバサイドのパッケージです。
DB
前提として、ServerpodはデフォルトでPostgreSQLとRedisが疎通出来るようにDocker-composeの設定があります。(すごく楽)
Tableの実装
yamlを書きます。
/lib/src/protocol/device_info.yaml
今回は、「抜粋した端末情報」とお馴染みの「createdAt」をColumnとして定義します。
class: DeviceInfoModel
table: device_info
fields:
version: int?
brand: String?
device: String?
hardware: String?
manufacturer: String?
model: String?
supported64BitAbis: List<String?>
isPhysicalDevice: bool?
createdAt: DateTime?
ここまでで一旦、generateします
serverpod generate
そうすると、DeviceInfoModelのdartファイルと
/lib/src/generated/device_info.dart
そのスキーマをPostgreSQLに作るためのSQLが生成されます。
/generated/tables.pgsql
CREATE TABLE device_info (
"id" serial,
"version" integer,
"brand" text,
"device" text,
"hardware" text,
"manufacturer" text,
"model" text,
"supported64BitAbis" json NOT NULL,
"isPhysicalDevice" boolean,
"createdAt" timestamp without time zone
);
ALTER TABLE ONLY device_info
ADD CONSTRAINT device_info_pkey PRIMARY KEY (id);
ちなみに、yamlではid
を明示的に定義していないですが、オートインクリメントとして自動で定義されるようです。
PostgreSQLにSQLを流す
ここに関してドキュメントには何も言及されていなかったのですが、今回はdocker exec
で先ほど生成されたSQLを流していきましょう。
docker exec -i device_info_pod_server_postgres_1 psql -U postgres device_info_pod < generated/tables.pgsql
「DockerContainer名」、「DB名」、「流すSQLへのパス」は適宜修正してください。
このコマンドを叩くと、パスワードを求められますのでdocker-compose.yamlに記載されているPostgreSQLへのパスワードを入力します
/docker-compose.yaml
POSTGRES_PASSWORD: "vv8NFSA--ePf0Y3EYp_xUA9saw7WfbfX"
Endpointの実装
/lib/src/endpoints/device_info_endpoint.dart
Endpointを継承したクラスを実装します。これが自動生成されてクライアントからアクセスします。
今回は、とりあえず4つ用意しました。
先ほど生成したDeviceInfoModel
をそのまま参照出来るところが肝です。
class DeviceInfoEndpoint extends Endpoint {
Future<List<DeviceInfoModel>> findAll(Session session) {
return DeviceInfoModel.find(
session,
orderBy: DeviceInfoModel.t.createdAt,
);
}
Future<void> insert(Session session, DeviceInfoModel model) {
return DeviceInfoModel.insert(
session,
model..createdAt = DateTime.now(),
);
}
Future<int> deleteAll(Session session) {
return DeviceInfoModel.delete(
session,
where: (t) => t.createdAt <= DateTime.now(),
);
}
Future<int> count(Session session) {
return DeviceInfoModel.count(session);
}
}
ぼやき
deleteAll
のメソッドが生えていなかったので、whereを駆使して全削除してます(絶対にちゃんとした方法はあるはず。。。)
あと、insert
でcreatedAtはサーバ側のタイムスタンプを使いたいのでモデルに代入していますが、クライアントでこのパラメータは指定出来ないようにしたい。。。(現時点では方法が無さそうでした👀)
完
今回のサーバサイド実装は、TableとEndpointのみなのでここまで完成したら再度generateしてFlutterの実装に進みます
serverpod generate
Flutter
/XXX_flutter
がFlutterのパッケージです。
ちなみに、/XXX_client
のパッケージはFlutterで扱うための自動生成コードなのでノータッチで大丈夫です。
↓こういう完成系です
謝辞
雑で簡易的なアーキテクチャにしています。細かい箇所は突っ込まないでください🙇♂️(小時間なので何卒お許しを)
あくまでイメージコードです。
フロー
Repository → State → UI
の順で今回は引っ張っています。
お馴染みのriverpodでDIしています。
Repository
こんな感じです。
ちなみに、countは用意しただけでstate以降は使っていません。
/lib/repository/device_info_repository.dart
class DeviceInfoRepository {
DeviceInfoRepository(this._client);
final Client _client;
Future<List<DeviceInfoModel>> fetch() {
return _client.deviceInfo.findAll();
}
Future<void> add(DeviceInfoModel model) {
return _client.deviceInfo.insert(model);
}
Future<int> clear() {
return _client.deviceInfo.deleteAll();
}
Future<int> count() {
return _client.deviceInfo.count();
}
}
Clientというクラスを介して、サーバサイドで自動生成したEndpointなどにアクセス出来ます。
文字列でのパス定義は不要で、補完が働きます。
ポイント
なんということでしょう。
DeviceInfoModel
という先ほどサーバサイドで使っていた型をそのまま参照しています。
この要領でAPIレスポンスの型定義は不要となります!
localhostの注意点
Clientの初期化の際に、hostを指定しますがデフォルトはlocalhostが入っています。
Client('http://localhost:8080/'),
Androidエミュレータでは、そのままだとlocalhostが働かないので↓このようにしてください。
localhost指定だとSocketExceptionが発生します。
Client('http://10.0.2.2:8080/'),
State
StateNotifierで絡めただけなので割愛します。
/lib/state/device_info_state.dart
UI
後はAPIレスポンスの内容をUIに反映するのと、各ハンドラの実装ぐらいです。
当然ですが、ここでもサーバサイドで定義したAPIレスポンスの型をそのままUIで参照しています。
所感
冒頭で綴った通り、APIレスポンスの型がそのまま扱えるのは最高な利点でした。優れた開発体験です。
そして、サーバサイド未経験の方でもFlutterを書いていれば比較的簡単に読み書き出来るのでそういった面でも重宝しそうです。
今後のserverpodの成長を応援しています🍺
余談ですが、AWSへデプロイするgithub-actionsも自動で作られて驚きました。
Discussion