こんにちは、エンジニアの @muttsu_623 です。
最近、開発を頑張っている自分へのご褒美として念願だった『左右分離型キーボード』のMISTEL『Barocco MD770 静音赤軸』を購入しました。
Mistel BAROCCO MD770 RGB メカニカルキーボード 英語配列 85キー 左右分離型 CHERRY MX RGB 静音赤軸 ブラック MD770-PUSPDBBT1
- 発売日: 2020/01/30
- メディア: Personal Computers
購入してからまだ2週間くらいなのでまだ効果を実感できているわけではありませんが、肩が開かれた状態で姿勢良く開発を行うことができるため、長期的にみればいいお買い物になったかなと思います。
さて本題ですが、先日弊社の「求人サイト Green」のAndroidアプリをFlutterで作成しリリースしました。
Flutterでアプリをリリースしましたー🎉
— muttsu (@muttsu_623) February 8, 2021
弊社の主力事業GreenのAndroidアプリです!https://t.co/Qv6jG7cYmC
すでにリリースされているiOSアプリをFlutterに乗り換えることを考えていたり、Flutterの社内普及も考えているので、技術で会社を伸ばしていくことに興味ある方はぜひお話させてください🤗 pic.twitter.com/sWvCTGVLCL
Flutterのエンジニアの皆さんから想像以上の反響をいただきました。
私がFlutterの可能性に気づくことができ、ここまで着実にFlutterについてキャッチアップを行うことができたのは、私より先にFlutterコミュニティに貢献してくださった多くの方のおかげです。
ありがとうございます。
アプリをリリース後、今度は自分自身が実際にアプリをリリースした身として、 実際にどういう構成で運用しているのかをアウトプットすることにより、より多くの企業がFlutterでのアプリ制作を1つの選択肢に入れられ、 Flutterの採用事例が増えていき、よりコミュニティが活発になっていくのではないか と思いこの記事を執筆することにしました。
至らない点がありましたら、Flutterコミュニティを盛り上げていくためにも、ぜひ @muttsu_623 までご連絡いただけますと幸いです。
Flutterの基本的な書き方に関しては多くの記事が出ていると思うので今回はスキップします。
実際にどのような構成で作っているかに関して記載します。
- 宣伝
- 「求人メディアGreen」とは
- ディレクトリ構成
- 状態管理
- DI
- APIリクエスト
- ローカルDB
- 環境変数設定
- Flutterで開発を行なってみてよかった点
- Flutterで開発を行なってみてつらかった点
- 最後に
宣伝
先日、一緒に開発を行なった先輩と一緒に社内でインタビューしていただきました。
- なぜ Flutter で開発したのか? - リリースまでに苦労したことは? - 開発中にこだわった点は? - 今回の開発過程で凄かった点はズバリ何?
などを開発裏話を赤裸々に語っておりますので、ご興味ある方はぜひこちらもご覧ください😊
また、私が所属している株式会社アトラエはこんな会社です。
「求人メディアGreen」とは
株式会社アトラエで開発・運用している IT系人材に強みを持つ成功報酬型求人メディア です。
詳細は こちら をご覧ください。
ディレクトリ構成
lib/
: Flutterのコードが存在しているディレクトリdi/
: DIを行うためのファイルdomain/
: Flutterに依存しないピュアDartなクラス(Entity, Repository)infrastructure/
: HTTP通信のクラス、ローカルDBのクラス、Daoクラスpresentation/
: View, State, State Controllerクラスstate_controller/
: State Controllerクラスstate/
: Stateクラスui/
: Viewクラス
util/
: Utilクラスapp_error.dart
: 基本的にアプリ内で起きるエラーについては、AppError
型に変換しています。result.dart
: 非同期処理の結果はResult
型に変換して利用しています。
AppError
, Result<T>
についてはコチラにまとめましたので、ぜひご覧ください。
今回のFlutterでの開発には、私以外にサーバーサイドの経験が長いエンジニア2名、Androidの経験が長いエンジニアが1名参加しました。
参加してもらうエンジニア3名がスムーズにFlutterの開発に入れるよう、Flutterに依存したモジュールとFlutterに依存していない部分がわかりやすい構成になるように心がけました。
Controller, ViewModel, Usecase ⇄(Entity, Value Onject)⇄ Repository ⇄(Entity, Value Onject)⇄ Dao
上記の流れに関しては、開発しているプロジェクトがFlutterであっても、Goであっても、Kotlin(Android)であっても共通して行われる処理となり、基本的にはFlutterに依存したコードは薄くなります。
Repository
, Dao
それぞれは Interface
になっているため、これらを継承した実体を Injection する際はFlutterや、DIライブラリに依存したコードが存在しますが、それ以外はピュアなDartのファイルたちになります。
言語仕様は多少勉強してもらうことになりますが、それ以外は普段から慣れている構成で作っていただける状態にしています。
State Controller
, State
, View
は Flutterに依存したファイルたちになりますが、逆にいうと基本的にはそれらのみがFlutterに依存した状態になります。
状態管理
状態管理には StateNotifier
, Riverpod
を利用しています。
アプリ内で、求人情報State
, メッセージスレッドState
などのように扱いたい情報を State
として管理するようにしています。
State Controller
というクラスは、StateNotifier
を継承してつくっており、このクラスが Repository
を通してデータを取得や更新を行い、 State
を変更します。
また、State
の変更を扱いやすくするため、 flutter_hooks
というライブラリも利用しています。
class JobOfferPage extends StatelessWidget { ... return Center( child: HookBuilder(builder: (BuildContext context) { final companyName = useProvider(jobOfferStateControllerProvider.state.select((value) => value.companyName)); return Text(companyName); }), ); } class JobOfferState { const JobOfferState(this.id, this.companyName); final int id; final String companyName; ... } final jobOfferStateControllerProvider = StateNotifierProvider.autoDispose.family( (ref, int jobOfferId) => JobOfferStateController( JobOfferState(id: jobOfferId), ref.read(jobOfferRepositoryImplProvider), ), ); class JobOfferStateController extends StateNotifier { JobOfferStateController( JobOfferState state, this.repository, ): super(state); final JobOfferRepository repository; void getJobOffer() { ... } ... }
State
も State Controller
もあくまでデータの状態を管理するものとして、View
に関するロジックは含んでいません。
そのため、用意しているメソッドたちも
void onButtonPressed() {
...
}
のようなものは生えていなく、
void getJobOffer() {
...
}
のようなメソッドのみ生えています。
View
から直接 jobOfferStateController.getJobOffer
というメソッドを呼ぶ形で実装を行なっています。
View
は State Controller
から情報を得ることはなく、あくまでも State
の状態が変わった通知を受けて、 View
を再描画する MVC
のような構成になっています。
StateNotifier
の State
を変更する際に、メンバ変数が複数存在していると値を変更するのがめんどうなため、 freezed
を利用し、変更したい値のみを変更すればいいようにしています。
Aというクラスの b というメンバ変数のみを変更したい場合
freezedを利用しない場合
// in A class A { const A(this.b, this.c, this.d); final B b; final C c; final D d; } // in A Controller class AController extends StateNotifier { const AController(); fun change(B newB) { state = A(newB, state.c, state.d) } }
freezedを利用したい場合(freezedは)
// in A part 'a.freezed.dart' @freezed abstract class A with _$A { factory A({ B b, C c, D d, }) = _A; A._(); // in A Controller class AController extends StateNotifier { const AController(); fun change(B newB) { state = state.copyWith(b: b); } }
このように書くことができ、変更させたいメンバ変数のみを copyWith
というメソッドに渡すことによって新しい state をつくることができます。
以前こちらの資料でも解説したので、ぜひご覧ください。
※資料中で、State Controller
を Singleton
にしてしまっていると記載しましたが、こちらは Riverpod
の autoDispose
というメソッドにより解消されました。
DI
上記のように、 Repository
, Dao
は Interface
になっているため、実体を Injection する必要がありました。
これに関しては、上記で紹介した Riverpod
を利用しています。
Provider
というライブラリのほうが一般的で、Riverpod
はまだ v1.0.0
にもなっていないライブラリです。
しかしながら、
- 実際にコードを書いてみて致命的なバグがないこと
Provider
の作者が改めに開発したライブラリがRiverpod
であること- 個人的には
Riverpod
のほうが直感的でわかりやすかった
という理由から、 Riverpod
を問題なく利用しています。
Riverpodの具体的な使い方に関しては、こちらの記事が勉強になりました。
【神パッケージ】 Riverpod の使い方【Flutter】|いしかわ|note
APIリクエスト
APIリクエストでは、dio
というライブラリを利用しています。
一般的には、 http
を選択されているところもあるかもしれませんが、Interceptorなどが利用しやすく、dio
のほうがGitHub上でのstar数が圧倒的に多かったため、こちらを利用しています。
しかしながら、1点不安があり、Flutterのライブラリは現在null safety対応が進んでいるのですが、 dio
に関してはメインのContributerたちがメッキリGitHubに現れなくなっており、その対応が進んでいなさそうな点です。
気にかけている人はもちろんいて、flutterchinaからflutter communityに移りそうな雰囲気はありますが、こちらは様子見が必要だなと思っています。
ローカルDB
ローカルDBでは、hive
を利用しています。
こちらも一般的には、 sqflite
というライブラリが利用されているのかなと思います。
ただ、sqflite
はリファレンスを読んだときにどうしても直感的にわかりにくく、、
元々Androidの開発でRealmを利用していたこともあり、NoSQLのライブラリのほうが慣れていましたし、直感的にわかりやすいと思っていました。
色々とライブラリを見ましたが、 hive
のリファレンスがしっかりしていたことや、元々開発していた人の手から離れ、しっかりと運用されたライブラリになっていたため、信頼感もあったので hive
を利用することにしました。
ローカルDBの検討についてはこちらの記事が勉強になりました。
環境変数設定
よくある現象かと思いますが、ビルド環境を local
, dev
, qa
, stage
, production
, ...etc と分けたいニーズはあるかなと思います。
弊社のプロダクトも、APIのBase URLやFirebaseの設定ファイルなど、環境によって設定したい環境変数や読み込みたいファイルの違いがありました。
具体的な方法は参考にさせてもらった以下の記事に任せ、ここでは避けますが、build時に dart-define
というオプションをつけることにより、Flutter内やAndroid, iOSそれぞれのビルドスクリプト内で環境変数として読み取ることができます。
この環境変数を利用して、google-services.json
の読み込みなどを制限しています。
Flutterで開発を行なってみてよかった点
1. なんといっても1つのソースコードでiOS, Android両方のアプリがつくれる
今回はAndroidアプリのみをFlutterでリリースしましたが、今後はiOSもFlutterでリリースする予定です。
Android用につくっていたソースコードでそのままiOSビルドを行なったところ、ほぼ違和感がない状態でiOSでもスムーズにアプリが動きました。
自分はAndroid用にレイアウトを組み、ソースコードを書いていただけなのに、いざiOSでビルドしてもきれいに動いたときは本当に感動です。
Flutterを採用した理由の大きな1つとして、クロスプラットフォーム開発のチャレンジがありましたが、まさにクロスプラットフォームのメリットを感じた瞬間でした。
2. レイアウト構築が想像以上に楽
Reactなどの宣言的UIに慣れている人からしたら当たり前かもしれませんが、Androidで普段xmlでレイアウトを組んでいる身からするとこんなにも楽なのかと感動しました。
また、他の開発者の3人もすぐにレイアウト構築に取りかかることができたくらいハードルは低いと思っています。
3. ホットリロードが便利
こちらもReactの人にとっては馴染み深いと思いますが、これが非常に速いし便利だなと思っています。
AndroidでもApply Changesが存在しますが、それよりも断然早くUIやロジックの変更を反映することができるので楽です。
Flutterで開発を行なってみてつらかった点
1. ホットリロードがバグることがある
これはコードの書き方が悪く起こっている問題かもしれませんが、ホットリロードしてもうまく動かず、アプリをキルして再度リスタートさせるとうまくいくケースがあります。
あまりにもホットリロードを信じすぎるとそれによって沼にハマってしまうこともあるので注意してください。
2. 多少UIにニセモノ感がある
開発者でなければ気づかない気がしていますが、微妙にネイティブで標準で用意されているものと比べるとニセモノ感を感じてしまうUIはあります。
ほとんどないので気になりませんが、そういったものがあった場合はチームのデザイナーやPMと相談して決めるのがいいかなと思います。
3. UIをAndroidに寄せるか、iOSに寄せるか、出し分けるか
今回はAndroidアプリのみをFlutterで作成したため、すべて Material Design のものを利用しました。
しかしながら、日本においては iOSユーザーのほうが多いため、iOSもFlutterでつくるとなるとどちらのUIに寄せるべきかはチームで議論になると思います。
弊社としてもここは悩みどころになっています。
もしくは以下のリポジトリを参考にしながら、iOSとAndroidでUIを分けることも選択肢の1つになってくるかなと感じています。
最後に
Flutterで開発を行なったことにより、当初の目的通り、バラバラでソースコードを書くよりも、1つのソースコードに対して複数人が関わることができるようになったため、開発速度やリソースの流動性を生み出しやすい状況になったかなと思っています。
弊社だけでなく、他の多くの会社にとってFlutterを採用することで今までとは全然違う開発体験を送ることができると思っておりますので、Flutterを採用する企業が増えていくように今後も発信していければと思います。
GreenのAndroidアプリはリリースしましたが、正直まだまだFlutterを通して開発的にやりたいこと、事業としてやりたいことが盛り沢山な状況です。
会社を大きくしていく上で、事業を伸ばしていく上で、技術をトコトン信じて、トコトンレベルの高い技術を勉強して、使えるようになって、新たに生み出したいと思っている、そんな人たちと一緒に働きたいと思っています。
今、Greenという10年以上続いているサービスをアプリをFlutter、バックエンドをGoでゴリゴリ開発していますし、wevoxやYentaという事業部でもそれぞれ技術力を活かして、会社と伸ばしていこうと必死に働いているエンジニアがたくさんいます。
自分の技術力を大切な仲間のために使って、より高いレベルへどんどん挑戦していきたいというエンジニアの方は、ぜひ気軽に連絡くださると嬉しいです。
Twitter @muttsu_623 への連絡でも構いません。お待ちしています。
会社説明資料
中途採用募集ポジション一覧