✨スポットバイトルがリリースされました✨
2024年10月、空いた時間に気軽に働けるスキマバイトを探せるアプリ「スポットバイトル」がリリースされました。
ネイティブアプリにFlutter・バックエンドにGoを採用しました。
どちらも未経験だった筆者が、半年間の開発を通じてどのように感じたかお伝えします。
📝 筆者のバックグラウンド
筆者はこれまでWEBを中心とした開発をしており、以下のような技術スタックでした。
- フロントエンド: Vue.js(Nuxt)・React(Next.js)・jQuery。TypeScriptが好き
- バックエンド: PHP(Laravel・Fuel PHP・Codeigniter・Symfonyとフレームワークいっぱい)・Node.js
これらの言語・フレームワークとの比較になります。
📱 Flutterの楽しいところ・戸惑ったところ
Flutterを触った初日は、自由度の高さとリッチなUIを簡単に構築できる点に感動しました。これでクロスプラットフォームなのだから驚きです。
しかし、いくつか戸惑う点や工夫が必要な点も見えてきました。
1. ネストが深くなりがちなウィジェットツリー
Flutterでは、UIを構築する際にウィジェットを階層的に記述します。
そのため、複雑な画面になるとネストが深くなり、慣れないと驚きます。以下は簡単な例ですが、実際にはもっと深いネストになる場面が多いです。
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Hello, Flutter!'),
ElevatedButton(
onPressed: () {},
child: Text('Click Me'),
),
],
),
),
),
);
}
}
幸いIDEやLinterでわかりやすく表示・補正してくれるため、戸惑いはあれど見づらさはありませんでした。ツール群が揃っているのは最近のフレームワークのいいところですね。
ネストが深くなってくると、ウィジェットを分割してコンポーネントとして管理することで、コードを整理しやすくなりました。このあたりはWEB開発でも同様ですね。
また、Flutter Outlineを活用して、ツリー構造を視覚的に確認する方法も効果的でした。
2. Riverpodによる状態管理が便利
Flutterの状態管理にはいくつか選択肢がありますが、デファクトスタンダードっぽい?Riverpodを採用しました。
特に、FutureProvider
を利用すると非同期データの取得が簡潔に書けます。
final dataListProvider = FutureProvider<List<String>>((ref) async {
await Future.delayed(Duration(seconds: 2));
return ['Item 1', 'Item 2', 'Item 3'];
});
状態の再評価や破棄も基本お任せして自動でやってくれますし、レンダリング・ローディング・エラーハンドリングといった前後の処理も気軽に書くことができます。
今回はAPI通信に組み込んで、さまざまなところで活躍しています。
ただし、データの再フェッチや複数のProviderを連携する場合には癖があると感じることもありました。
例えばマスタAとマスタBをそれぞれ非同期で取得し、それをまとめて扱うようなケースです。
今回は、以下のようにFuture.wait
を利用しました。
(この使い方を知るまでに時間がかかりました…)
final masterAProvider = FutureProvider<List<String>>((ref) async {
await Future.delayed(Duration(seconds: 2));
return ['Master A1', 'Master A2'];
});
final masterBProvider = FutureProvider<List<String>>((ref) async {
await Future.delayed(Duration(seconds: 3));
return ['Master B1', 'Master B2'];
});
final combinedDataProvider = FutureProvider<Map<String, List<String>>>((ref) async {
final masterA = ref.watch(masterAProvider.future);
final masterB = ref.watch(masterBProvider.future);
final results = await Future.wait([masterA, masterB]);
return {
'masterA': results[0] as List<String>,
'masterB': results[1] as List<String>,
};
});
この方法を用いることで、複数の非同期処理を気軽に扱えるようになり、コードがかなりスッキリしました。
👍 Goを書いてて気持ちよかったところ
Goはシンプルな言語仕様と強力な型が印象的でした。
ほどよく堅牢性があり、ほどよく直感的に書けるので、洗礼された言語だと思います。
1. 型安全で明示的なエラー処理
Goでは例外処理を使わず、エラーを明示的に返す文化があります。
file, err := os.Open("example.txt")
if err != nil {
log.Fatalf("Failed to open file: %v", err)
}
defer file.Close()
最初は冗長に感じましたが、「早期Returnみたいなものかな」と思えば、次第に読みやすくなっていきました。
早々にエラーハンドリングすることで、細かく意識せずに済むのはいいことですね。
2. ライブラリ選定と独自実装のバランス
今回はHTTPのルーティング・ミドルウェアにgo-chi/chi
を採用するなどして、ライブラリを使いながらもフレームワークにはお任せしない方針にしています。
負債を作り込まないシンプルな作りにもできるためチューニングもやりやすく、Goの実行効率と相性がいいように思えます。
3. ユニットテストにおけるDBの扱い
(これはGoに限らない話ですが)
テスト環境でDBをどうするかはいつも悩ましいものです。
なるべくスタブを用意するべきだとは思いますが、
- 依存関係によりモックのためのモックができていく
- 記述量が増える・実態と離れていくなどなんのためのテストかわからなくなる
- 単純にめんどくさい
といった理由から、いつも導入には慎重になってしまいます。
また、今回のプロダクト開発ではDBAを含む複数チームでDDLの変更が行われており、データ構造の変更のたびにモックを修正するのも手間がかかりそうでした。
そこで、今回はローカル環境にDBを立てDDLを流し込む方式を採用しました。
Seedデータのみ用意する必要はありますが、StagingのDBからdumpデータを流し込むこともできるため、取り回しやすかったです。
注意点として、トランザクションを貼ってロールバックするなど、テストにあたってDBのクリーンアップを確実に行う必要があります。
ここが不完全で、1度目は通るが2度目が通らないユニットテストを作ってしまい、原因に悩むこともありました。
🎉 おわりに
FlutterとGoを初めて触った経験は、非常に学びの多いものでした。
開発者体験もよく、コーディングが楽しいため数日でメキメキ開発できるようになりました。未経験ながらチャレンジして良かったと思います。
FlutterはリッチなUIを効率的に構築できる一方、独自のお作法もあり少々慣れが必要だと思いました。
Goはシンプルながら指向性がハッキリしていて、誰が書いてもブレが少ないコードが出来上がると感じています。
🚀 この記事は…
ディップAdventCalendar 2024の23日目の記事でした
他の記事もぜひご覧ください!
次回はこちら↓
前回はこちら↓
それでは、楽しいクリスマスを!