🎯

サーバサイドDartとFlutterを絡めたデモアプリ作ってみた所感

2022/09/23に公開

はじめに

Flutterにハマっている皆様、サーバサイドDartのServerpodというフレームワークはご存じでしょうか。
https://serverpod.dev/

前々から気になっていたので軽く触ってみました。

クライアントサイドがFlutterに対するサーバサイドの選定

普及している選択肢として、JVM言語やGo、Nodeなどを用いてサーバを実装するか、Firebaseを利用して実装するかが多いと思います。

その際に、煩わしさとしてAPIレスポンスをdartでモデル定義する点が挙げられます。
しかし、ここについては「JSONからdartのモデルへと自動で一括変換してくれるツール」や「protobufを利用して自動生成コードとしてモデルを扱う」などといったアプローチで煩わしさを解決出来ます。

ただ、このServerpodを使うことでそういったアプローチの出番はなくなります。サーバとクライアントで言語が同一なため、レイヤー間での言語コミュニケーションのコストが削減されます。(Node × ReactNativeと同様ですね)

まだまだ実用的ではない所感ですが、これから小規模なスピード感溢れる現場では新たな選択肢として挙げられるかもしれません。選択肢が増えることはワクワクです🌝

実装内容

DeviceInfoのプラグインを利用して端末情報を参照し、そのパラメータをServerpodで作ったAPIを介してDBに入れたり消したり引っ張ったりします。

↓先に今回作ったrepositoryを貼っておきます。
https://github.com/Yokoi-K/device-info-pod

Serverpodの導入

ここについては公式のGet Startedを順に進めるだけなので割愛します。

ただ、事前にDockerをインストールは必要です
↓よしなにbrewで叩きましょう

brew install --cask docker

今回のプロジェクト名はdevice_info_podとしました。

それでは、「サーバサイド」と「Flutter」の実装を見ていきましょう。

サーバサイド

/XXX_serverがサーバサイドのパッケージです。

DB

前提として、ServerpodはデフォルトでPostgreSQLRedisが疎通出来るように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で扱うための自動生成コードなのでノータッチで大丈夫です。

↓こういう完成系です

完成系のgif

謝辞

雑で簡易的なアーキテクチャにしています。細かい箇所は突っ込まないでください🙇‍♂️(小時間なので何卒お許しを)
あくまでイメージコードです。

フロー

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に反映するのと、各ハンドラの実装ぐらいです。

/lib/ui/page/home_page.dart

当然ですが、ここでもサーバサイドで定義したAPIレスポンスの型をそのままUIで参照しています。

所感

冒頭で綴った通り、APIレスポンスの型がそのまま扱えるのは最高な利点でした。優れた開発体験です。
そして、サーバサイド未経験の方でもFlutterを書いていれば比較的簡単に読み書き出来るのでそういった面でも重宝しそうです。

今後のserverpodの成長を応援しています🍺

余談ですが、AWSへデプロイするgithub-actionsも自動で作られて驚きました。
https://github.com/Yokoi-K/device-info-pod/blob/main/.github/workflows/deployment-aws.yml

Discussion