株式会社Neverのshoheiです。
株式会社Neverは「NEVER STOP CREATE 作りつづけること」をビジョンに掲げ、理想を実現するためにプロダクトを作り続ける組織です。モバイルアプリケーションの受託開発、技術支援、コンサルティングを行っております。アプリ開発のご依頼や開発面でのお困りの際はお気楽にお問合せください。
概要
弊社のコーディング規約を紹介します。弊社はNotionで管理しています。
参考元: 実装方針 & コードレビューのやり方
具体例を交えて全て紹介するとかなりのボリュームになるので、その中で大事な5選を紹介します。
コーディング規約の目的
- ソフトウェア品質を良くする
- 不具合の発生を抑え、パフォーマンスを良くするため
- ソースコードの可読性を良くする
- コードレビューの負担を下げ、レビューの生産性を高めるため
- 実装に迷った時の判断材料にする
- 規約に沿う事で、実装者の心理的安全性が保たれ、自信を持って実装できるため
コーディグ規約5選
弊社が定めるFlutterアプリ開発のコーディング規約5選を紹介します。
ソースコードが命名規則通りに統一されている事
命名規則を統一することでソースコードの可読性が良くなり、コードレビューの生産性が高くなります。命名規則は以下の通りです。1
項目 | 規則 | 具体例 |
---|---|---|
ディレクトリ名 | スネークケース | home_page |
ファイル名 | スネークケース | home_page.dart |
クラス名 | アッパーキャメルケース | class HomePage |
変数名 | キャメルケース | var userName |
定数名 | キャメルケース | final userName |
基本的には形容詞 or 名詞
始まりの構成で命名します。理由は複数形を考慮するためです。操作系は動詞
始まりの構成で命名します。2
画面、コンポーネント
項目 | 命名例 | 説明 |
---|---|---|
画面 |
ArticlePage ArticleScreen ArticlesPage ArticlesScreen ArticleListPage ArticleListScreen
|
名詞 + Page 名詞 + Screen で構成する。一目見てどの画面かわかる名前、suffixに画面を示す単語を命名。 |
コンポーネント |
ArticleTile ArticleCard ArticleListView
|
名詞 + Widgetの種類名 例えばCard をカプセル化した場合、ArticleCard と命名する。注意として、ArticleWidget のようなsuffixにWidget を付けるのは避ける、何のWidgetで構成されているのか分からないため。 |
データクラス
項目 | 命名例 | 説明 |
---|---|---|
記事 |
Article LatestArticle PrivateArticle
|
名詞 形容詞 + 名詞 名詞 + 名詞 クラス内のプロパティを持つ者として相応しい名前を命名する。 |
データソースを扱う層
項目 | 命名例 | 説明 |
---|---|---|
APIクライアント | GitHubClient |
名詞 + Client で構成する。 |
データソース先をカプセル化したクラス |
ArticleRepository ArticleDataSource
|
名詞 + Repository 名詞 + DataSource で構成する。 |
ビジネスロジック
1クラス = 1ロジックを担うクラス
動詞 + 名詞
の構成でクラス名を命名します。
項目 | 命名例 |
---|---|
記事を作成する |
CreateArticle UploadArticle SaveArticle
|
記事を取得する |
FetchArticle GetArticle
|
記事を更新する | UpdateArticle |
記事を削除する |
DeleteArticle RemoveArticle
|
関数名はcall
を使います。Dartではcall
を定義すると、そのクラスオブジェクトは関数として扱われ3、1クラス = 1ロジックの粒度として扱いやすくなるためです。関数として扱われるので、call
を省略して利用できます。
final createArticle = CreateArticle();
createArticle.call();
createArticle(); // callを省略できる
また、取得系において個人的にはFetch
がオススメです。 Fetch
は遠く(データソース)から取得し、Get
は近く(メモリキャッシュ)から取得するイメージを持つためです(get
はgetter
の要素が強く感じる)
1状態 = 1管理者(操作関数を持つクラス)
名詞 + 名詞
の構成でクラス名を命名します。
項目 | 命名例 | 説明 |
---|---|---|
記事の状態管理 + CRUD操作を提供する | ArticleController |
名詞 + Controller で構成する。FlutterではxxxController で命名されたものが多いため合わせる。 |
関数は 動詞 + 名詞
の構成で命名します。
項目 | 命名例 |
---|---|
記事を作成する |
createArticle uploadArticle saveArticle
|
boolean
is + 形容詞
has + 過去分詞
三単現動詞 + 名詞
助動詞 + 動詞
で構成し、YES or Noで判断できるよう命名します。4
項目 | 命名例 | 説明 |
---|---|---|
読み込み状態 | isLoading |
is + 形容詞 |
有効状態 | isEnabled |
is + 形容詞 |
表示状態 | isVisible |
is + 形容詞 |
保持状態 | hasArticled |
has + 過去分詞 |
存在有無 | existsArticle |
三単現動詞 + 名詞 |
可否状態 | canUpload |
助動詞 + 動詞 |
その他
項目 | 命名例 | 由来 |
---|---|---|
拡張関数 |
StringExtension StringExt
|
拡張元 + Extension 拡張元 + Ext で構成する。suffixは拡張関数を示す単語で構成する。 |
変換クラス | TimestampDateConverter |
名詞 + Converter で構成する。 どのような状態を変換できるのかわかるよう命名する。 |
ストリームクラス | ArticleStream |
名詞 + Stream で構成する。購読すると、どのような状態がストリーム経由で得られるのかわかるよう命名する。 |
便利クラス |
Validation ValidationUtil ValidationHelper
|
かっこよく名詞 のみで完結したい。xxxUtil xxxHelper の命名は最終手段。 |
Exceptionクラス | GitHubException |
名詞 + Exception で構成する。Exceptionの発生元から具体的な名前を命名する。 |
抽象化を採用する場合、抽象クラスを実装したクラス名のsuffixにImpl
(Implementation
の略称)を付けます。Repository
の場合はその実装部としてDataSource
でも良いです。5
抽象クラス例 | 実装クラス例 |
---|---|
ArticleRepository |
ArticleRepositoryImpl ArticleDataSource
|
CreateArticle |
CreateArticleImpl |
静的解析で指摘事項がない事
静的解析のパッケージを導入することで、規約に合わない実装をワーニング / エラーとして指摘してくれます。解説ドキュメントはこちら
Flutterプロジェクトを作成すると flutter_lints が標準で導入されていますが、ルールは比較的甘めなので、より厳しくする場合はカスタマイズが必要です。
カスタマイズがややめんどうなので、より厳しいルールを定めた外部パッケージを使うことをお勧めします。
オススメのパッケージは以下の通りです。
必要に応じて外部パッケージ専用のlint(riverpod_lintなど)を導入しても良いです。
導入後、ビルド前にIDEの静的解析が実行されるので、実行後の結果でワーニングやエラーがあれば修正します。修正方法は指摘内容にリンクが記載されているので確認して修正します。
静的解析の指摘を解消することで、不具合の混入リスクが軽減し、ソフトウェアの品質が上がります。
リスト表示で遅延処理が正しく実装されている事
APIやDBから取得したデータをリスト表示する場合、遅延処理が実施されるWidgetを使います。これを怠ると、全てのリストデータがメモリ上に保持され続け、OOM(Out Of Memory)発生のリスクが高くなります。
良くある事例として、重たい画像を含んだ多くのデータをリスト表示したらアプリが落ちるといった現象です。
代表的な遅延処理があるリスト系Widgetは以下の通りです。
- リスト
- グリッド
- 外部パッケージ
実装してみて遅延処理がされているか不安な場合は、スクロールするたびにリスト内の描画対象となるStateインスタンスが生成と破棄を繰り返していることをログで確認します。
// リスト表示するWidget
class Tile extends StatefulWidget {
const Tile(this.index, {super.key});
final int index;
@override
TileState createState() => TileState();
}
class TileState extends State<Tile> {
@override
void dispose() {
debugPrint('dispose: ${widget.index}'); // 追加
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(
widget.index.toString(),
),
);
}
}
例として、1,000件のリスト表示を3通りの方法で検証してみます。
// 1️⃣ ✅ ListView.builder 👉 破棄される、仕込んだdisposeログが表示
class SamplePage1 extends StatelessWidget {
const SamplePage1({super.key});
@override
Widget build(BuildContext context) {
final items = List.generate(1000, (index) => index);
return Scaffold(
appBar: AppBar(),
body: ListView.builder(
itemBuilder: (context, item) => Tile(item),
itemCount: items.length,
),
);
}
}
// 2️⃣ ✅ ListView 👉 破棄される、仕込んだdisposeログが表示
class SamplePage2 extends StatelessWidget {
const SamplePage2({super.key});
@override
Widget build(BuildContext context) {
final items = List.generate(1000, (index) => index);
return Scaffold(
appBar: AppBar(),
body: ListView(
children: items.map(Tile.new).toList(),
),
);
}
}
// 3️⃣ ❌ SingleChildScrollView + Column 👉 破棄されない、仕込んだdisposeログは表示されない
class SamplePage3 extends StatelessWidget {
const SamplePage3({super.key});
@override
Widget build(BuildContext context) {
final items = List.generate(1000, (index) => index);
return Scaffold(
appBar: AppBar(),
body: SingleChildScrollView(
child: Column(
children: items.map(Tile.new).toList(),
),
),
);
}
}
Dev Toolsでメモリ割り当て状況を確認します。
1️⃣ ✅ ListView.builder 👉 WidgetとStateは画面範囲 + cacheExtentの領域のみ保持
2️⃣ ✅ ListView 👉 Widgetは全て保持されるが、Stateは画面範囲 + cacheExtentの領域のみ保持
3️⃣ ❌ SingleChildScrollView + Column 👉 WidgetもStateも全て保持される
例外ケースでも耐えれる実装ができている事
Nullableデータを正しく扱えている事
データが確定しない状態がある(確保されたメモリ上にデータが無い場合がある)ことをNullable
として扱います。
Nullableデータは必ずNULLチェック
をして扱います。
int? fetchData() {
...
}
// ❌ NG dataがnullの場合、null強制参照エラーが発生する。
final data = fetchData();
print(data!.toString());
// ✅ OK
final data = fetchData();
if (data != null) {
print(data.toString());
} else {
print('no data');
}
null強制参照をすると静的解析が教えてくれます。強制参照のシンボルである!
は避けるよう実装することがベターです。
また、?
とnull合体演算子??
の組み合わせを使う事で、Nullableデータを安全に扱うことができます。
final data = fetchData();
print(data?.toString() ?? 'no data');
文字数が多い場合に画面が崩れない事
Layout overflow
🚧になっていないか確認します。特に、Row
やColumn
で子Widgetのサイズが大きくなることを考慮していないと発生します。
また、端末の設定よりフォントサイズを大きくすると、通常時は問題なくても、大きくなった場合にLayout overflow
🚧になります。
フォントサイズは以下の手順で変更できます。
- iOS
- 設定 → アクセシビリティ → 画面表示とテキストサイズ → さらに大きな文字
- Android
- 設定 → ディスプレイ → 表示サイズとテキスト → フォントサイズ
対応として子Widgetのサイズの指定を実施しますが、子Widgetのサイズが拡張した際にどのようなデザインであるべきかで対応が変わります。
例としてはTextのWidgetにFlexible
をセットします。
class SamplePage1 extends StatelessWidget {
const SamplePage1({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Flexibleで拡張時の比率を設定
Flexible(
child: Text('タイトル' * 6),
),
FilledButton(
onPressed: () {},
child: const Text('ボタン'),
),
],
),
),
);
}
}
ここは実現したいデザインと相談しながら実装しましょう。
Exceptionを適切に扱えている事
Exception
がthrow
された時の例外処理が考慮できているか確認します。
主にAPIクライアントや外部パッケージのエラーレスポンスの仕様を確認して、try - catch
でエラーハンドリングするものか確認します。
エラーがthrow
される仕様の場合は、try - catch
でエラーを捕捉できるよう実装します。6
void post() {
... // throw Exception(); or throw FormatException();
}
try {
post();
} on Exception catch (e) {
// 全てのExceptionを捕捉できる
}
try {
post();
} on FormatException catch (e) {
// FormatExceptionを捕捉できる
} on Exception catch (e) {
// FormatException以外のExceptionを捕捉できる
}
catch節ではエラー発生時の正しい処理を実装しましょう。無理やりcatch節でException
を丸め込んでしまった場合、ログからException
の原因が特定できず、不具合調査が難航する場合があるので注意しましょう。
なお、Exception
以外にError
型がありますが、こちらはプログラム修正すべきものなので、catchはせずにエラーを発生させて、プログラムの修正を強制します。78
非同期処理のローディング中や機内モード中のエラーハンドリングの実装ができている事
ネットワーク通信など時間がかかる処理を実行した際に、ユーザーに処理中を伝えるためと、ユーザーからの多重実行を防ぐための考慮ができているか確認します。
- ローディング状態のフラグで制御
-
isLoading
等のstateを使い、処理が開始したらisLoading
をtrue
, 終了したらfalse
、処理中にリクエストがあってもisLoading
がtrue
であれば早期return
をする。 - ローディング状態に合わせて、CircularProgressIndicatorやCupertinoActivityIndicatorを表示する
-
-
Dialog
を表示して処理中を伝え、ユーザーからの操作を制御する - AsyncCacheで多重実行を制御9
- AsyncCache.ephemeral()で非同期処理が終了するまでリクエストがあっても処理しない
また、端末の機内モードを使い、ネットワーク接続できない状態でも問題なく動作するか確認します。前述のExceptionの例外処理ができているか確認します。
最後に
コーディング規約をチーム内で共有し、チームで楽しく良いアプリを作っていきましょう👍