37
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

株式会社NeverのFlutterコーディング規約5選

Last updated at Posted at 2023-12-31

株式会社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は近く(メモリキャッシュ)から取得するイメージを持つためです(getgetterの要素が強く感じる)

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にImplImplementationの略称)を付けます。Repositoryの場合はその実装部としてDataSourceでも良いです。5

抽象クラス例 実装クラス例
ArticleRepository ArticleRepositoryImpl ArticleDataSource
CreateArticle CreateArticleImpl

静的解析で指摘事項がない事

静的解析のパッケージを導入することで、規約に合わない実装をワーニング / エラーとして指摘してくれます。解説ドキュメントはこちら

Flutterプロジェクトを作成すると flutter_lints が標準で導入されていますが、ルールは比較的甘めなので、より厳しくする場合はカスタマイズが必要です。

カスタマイズがややめんどうなので、より厳しいルールを定めた外部パッケージを使うことをお勧めします。

オススメのパッケージは以下の通りです。

必要に応じて外部パッケージ専用のlint(riverpod_lintなど)を導入しても良いです。

導入後、ビルド前にIDEの静的解析が実行されるので、実行後の結果でワーニングやエラーがあれば修正します。修正方法は指摘内容にリンクが記載されているので確認して修正します。

スクリーンショット 2023-12-31 16.47.05.png

静的解析の指摘を解消することで、不具合の混入リスクが軽減し、ソフトウェアの品質が上がります。

リスト表示で遅延処理が正しく実装されている事

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の領域のみ保持
スクリーンショット 2023-12-31 17.20.41.png

2️⃣ ✅ ListView 👉 Widgetは全て保持されるが、Stateは画面範囲 + cacheExtentの領域のみ保持
スクリーンショット 2023-12-31 17.18.52.png

3️⃣ ❌ SingleChildScrollView + Column 👉 WidgetもStateも全て保持される
スクリーンショット 2023-12-31 17.24.05.png

例外ケースでも耐えれる実装ができている事

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🚧になっていないか確認します。特に、RowColumnで子Widgetのサイズが大きくなることを考慮していないと発生します。

また、端末の設定よりフォントサイズを大きくすると、通常時は問題なくても、大きくなった場合にLayout overflow🚧になります。

フォントサイズは以下の手順で変更できます。

  • iOS
    • 設定 → アクセシビリティ → 画面表示とテキストサイズ → さらに大きな文字
  • Android
    • 設定 → ディスプレイ → 表示サイズとテキスト → フォントサイズ

Screenshot_20231231_174101.png

対応として子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('ボタン'),
            ),
          ],
        ),
      ),
    );
  }
}

Screenshot_20231231_174144.png

ここは実現したいデザインと相談しながら実装しましょう。

Exceptionを適切に扱えている事

Exceptionthrowされた時の例外処理が考慮できているか確認します。

主に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を使い、処理が開始したらisLoadingtrue, 終了したらfalse、処理中にリクエストがあってもisLoadingtrueであれば早期returnをする。
    • ローディング状態に合わせて、CircularProgressIndicatorCupertinoActivityIndicatorを表示する
  • Dialogを表示して処理中を伝え、ユーザーからの操作を制御する
  • AsyncCacheで多重実行を制御9

また、端末の機内モードを使い、ネットワーク接続できない状態でも問題なく動作するか確認します。前述のExceptionの例外処理ができているか確認します。

最後に

コーディング規約をチーム内で共有し、チームで楽しく良いアプリを作っていきましょう👍

  1. Effective Dart: Style

  2. モデルやメソッドに名前を付けるときは英語の品詞に気をつけよう

  3. Callable objects

  4. booleanメソッドの命名規則

  5. データレイヤ - このガイドにおける命名規則

  6. Error handling

  7. Error class

  8. 【Dart】Exception(例外)とError(間違い)の使い方

  9. AsyncCacheのススメ(非同期処理の多重実行防止のための個人的ベタープラクティス)

37
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?