【Flutter】Masamuneで超速アプリ開発③「メモ帳アプリ作成 - メモの表示」

こんにちは。広瀬マサルです。

Flutterでアプリケーションを安定して高速に開発することのできるMasamuneフレームワークを紹介しました。

https://zenn.dev/mathru/articles/dcd9a52bcf3e94

その魅力と使い方を知っていただくためにこれから数回に分けて紹介記事を書きたいと思います。

Masamuneフレームワークを使いこなすことで超高速・安定・高品質なアプリを開発することができるでしょう。

前回は「Firestoreへの簡単接続」についてお話しました。

https://qiita.com/mathru/items/4ba93558baae7023f998

ローカルデータベースからFirestoreへシームレスに移行できるのは魅力ですね。

第３回から実際にアプリ開発を始めていきたいと思います。

一番シンプルな「**メモ帳アプリ**」を開発していきます。

# 基本的な考え方

アプリを作るに当たって基本的な考え方を事前に記載しておきます。

## CRUD（Create Read Update Delete）

アプリにおける基本的なデータの扱い方です。

`データを作成`、`データを取得`、`データの更新`、`データの削除`です。データベース＝アプリ間でこの処理を行うだけで大半のアプリは作成できると言われています。

MasamuneフレームワークではCRUDをとても扱いやすくする機能を提供しています。

## ドキュメントとコレクション

MasamuneフレームワークではFirestoreのキーと値のペアを複数集めた`ドキュメント`とそのドキュメントを複数まとめた`コレクション`というデータ構造を採用しています。

詳しくは下記をご覧ください。

https://zenn.dev/mathru/articles/90bab36d44f85b

https://firebase.google.com/docs/firestore/data-model

## Masamuneにおけるアプリの開発方法

Masamuneフレームワークでは基本的にどのような形でデータを保存するかを決めた`データモデル`とどのような形で画面を表示するかを決めた`ページ`を構築していくだけでCRUDアプリを制作していくことができます。

アプリの使い方から`データモデル`を先に考えるかアプリのイメージから`ページ`を先に考えるかは自由です。

今回は`データモデル`から考えていきたいと思います。

# メモ帳のデータモデルの作成

簡単なメモ帳を作るとなると下記のデータが必要ではないかと思います。

- メモのタイトル
- メモの本文
- メモの投稿日時

それではこれを実際に作っていきましょう。

[前回](https://qiita.com/mathru/items/4ba93558baae7023f998)のFlutterプロジェクトを開き**別ターミナル**で下記のコマンドを入力します。

```bash
katana code watch
```

これによりファイルの変更が監視され自動生成ファイルが即座に作成されるようになります。

さらにデータモデルの作成コマンドを入力します。

```bash
katana code collection memo
```

`models/memo.dart`に下記の`MemoModel`が作成されます。

```dart
// memo.dart

/// Value for model.
@freezed
@formValue
@immutable
// TODO: Set the path for the collection.
@CollectionModelPath("memo")
abstract class MemoModel with _$MemoModel {
  const factory MemoModel({
     // TODO: Set the data schema.
     
  }) = _MemoModel;
  const MemoModel._();

  factory MemoModel.fromJson(Map<String, Object?> json) => _$MemoModelFromJson(json);

  /// Query for document.
  ///
  /// ```dart
  /// appRef.model(MemoModel.document(id));       // Get the document.
  /// ref.app.model(MemoModel.document(id))..load();  // Load the document.
  /// ```
  static const document = _$MemoModelDocumentQuery();

  /// Query for collection.
  ///
  /// ```dart
  /// appRef.model(MemoModel.collection());       // Get the collection.
  /// ref.app.model(MemoModel.collection())..load();  // Load the collection.
  /// ref.app.model(
  ///   MemoModel.collection().data.equal(
  ///     "data",
  ///   )
  /// )..load(); // Load the collection with filter.
  /// ```
  static const collection = _$MemoModelCollectionQuery();

  /// Query for form value.
  ///
  /// ```dart
  /// ref.app.form(MemoModel.form(MemoModel()));    // Get the form controller in app scope.
  /// ref.page.form(MemoModel.form(MemoModel()));    // Get the form controller in page scope.
  /// ```
  static const form = _$MemoModelFormQuery();
}
```

この**コンストラクタ部分**に下記のパラメーターを追加します。

```dart
const factory MemoModel({
   // TODO: Set the data schema.
   required String title,
   required String text,
   @Default(ModelTimestamp()) ModelTimestamp createdAt,
}) = _MemoModel;
```

メモのタイトル（`title`）、メモの本文（`text`）、メモの投稿日時（`createdAt`）に対応しています。

`ModelTimestamp`はMasamuneフレームワーク内で日時を扱うための特殊クラスです。

ファイルを保存すると`katana code watch`により`memo.freezed.dart`、`memo.g.dart`、`memo.m.dart`が自動作成されます。

![](https://storage.googleapis.com/zenn-user-upload/58d16dd8cb61-20230701.png)

**これでデータモデルの定義は完了です。**

# メモ一覧ページの作成

続いて上記で作ったメモのデータを元にメモ一覧のページを作成します。

下記のコマンドでページを作成します。

```bash
katana code page memo_list
```

`pages/memo_list.dart`に下記の`MemoListPage`が作成されます。

```dart
// memo_list.dart

/// Page widget for MemoList.
@immutable
// TODO: Set the path for the page.
@PagePath("memo_list")
class MemoListPage extends PageScopedWidget {
  const MemoListPage({
    super.key,
    // TODO: Set parameters for the page.
    
  });

  // TODO: Set parameters for the page in the form [final String xxx].
  

  /// Used to transition to the MemoListPage screen.
  ///
  /// ```dart
  /// router.push(MemoListPage.query(parameters));    // Push page to MemoListPage.
  /// router.replace(MemoListPage.query(parameters)); // Replace page to MemoListPage.
  /// ```
  @pageRouteQuery
  static const query = _$MemoListPageQuery();

  @override
  Widget build(BuildContext context, PageRef ref) {
    // Describes the process of loading
    // and defining variables required for the page.
    // TODO: Implement the variable loading process.
    

    // Describes the structure of the page.
    // TODO: Implement the view.
    return UniversalScaffold();
  }
}
```

最初にメモ一覧データを読み込むための処理を`build`メソッド内に記述します。

```dart
// memo_list.dart

@override
Widget build(BuildContext context, PageRef ref) {
  // Describes the process of loading
  // and defining variables required for the page.
  // TODO: Implement the variable loading process.
  final memoList = ref.app.model(MemoModel.collection())..load();

  // Describes the structure of the page.
  // TODO: Implement the view.
  return UniversalScaffold();
}
```

`UniversalListView`と`ListTile`を使って`memoList`の内容を並べます。

（`UniversalListView`はMasamuneフレームワーク内で利用可能な`ListView`）

```dart
// memo_list.dart

// Describes the structure of the page.
// TODO: Implement the view.
return UniversalScaffold(
  appBar: UniversalAppBar(
    title: Text("Memo List"),
  ),
  body: UniversalListView(
    children: memoList.mapListenable((e) {
      return ListTile(
        title: Text(e.value?.title ?? ""),
        subtitle: Text(e.value?.createdAt.value.yyyyMMddHHmm() ?? ""),
      );
    }),
  ),
);
```

`memoList`を`mapListenable`で`ListTile`に変換しています。

`mapListenable`を利用することで`memoList`の各要素に更新があった場合、自動的に`ListTile`の中身が更新されるようになります。

これでメモ一覧画面が作成完了です。

# モックデータの作成とデバッグ

現在Firestoreに接続しているかと思いますが、アプリのデータが入った状態を確認するために一旦接続を解除し代わりにモックデータを表示するようにしましょう。

モックデータは`RuntimeModelAdapter`で取り扱うことが可能です。

`adapter.dart`を開き下記のように書き換えてみてください。

```dart
// adapter.dart
import 'models/memo.dart';

~~~~~~

final modelAdapter = RuntimeModelAdapter(
  initialValue: [
    MemoModelInitialCollection(
      {
        for (var i = 0; i < 10; i++)
          generateCode(32, seed: i): MemoModel(
            title: "メモタイトル$i",
            text: "メモテキスト$i",
            createdAt: ModelTimestamp(DateTime.now().subtract(i.h)),
          )
      },
    )
  ],
);
// final modelAdapter = FirestoreModelAdapter(
//   options: DefaultFirebaseOptions.currentPlatform,
// );
```

`RuntimeModelAdapter`の中の`initialValue`にモックデータを指定することができます。

各モックデータは自動で作成されたModelInitialCollectionデータの中にIDとModelオブジェクトのペアを含めて渡すことで定義されます。

IDはどう作成しても良いんですが、Masamuneフレームワーク内では32桁の文字列が利用されるので`generateCode`で32桁のランダムな文字列を作成してます。

また`router.dart`内で`initialQury`の値を`MemoListPage.query()`に書き換え、最初のページを`MemoListPage`に設定しましょう。

```dart
// router.dart
import 'pages/memo_list.dart';

~~~~~~~~

/// App Router.
///
/// ```dart
/// router.push(Page.query());  // Push page to Page.
/// router.pop();               // Pop page.
/// ```
final router = AppRouter(
  // TODO: Please configure the initial routing and redirection settings.
  boot: null,
  initialQuery: MemoListPage.query(),
  // initialQuery: HomePage.query(),
  redirect: [],
  pages: [
    // TODO: Add the page query to be used for routing.
  ],
);
```

それではこの状態でデバッグビルドしてみましょう。

![](https://storage.googleapis.com/zenn-user-upload/b1449510cea9-20230701.png)

メモの一覧が表示されてますね。

# メモの内容ページの作成

メモの中身を表示してみましょう。

下記のコマンドでページを作成します。

```bash
katana code page memo_detail
```

`pages/memo_detail.dart`に`MemoDetailPage`が作成されます。

```dart
// memo_detail.dart

/// Page widget for MemoDetail.
@immutable
// TODO: Set the path for the page.
@PagePath("memo_detail")
class MemoDetailPage extends PageScopedWidget {
  const MemoDetailPage({
    super.key,
    // TODO: Set parameters for the page.
    
  });

  // TODO: Set parameters for the page in the form [final String xxx].
  

  /// Used to transition to the MemoDetailPage screen.
  ///
  /// ```dart
  /// router.push(MemoDetailPage.query(parameters));    // Push page to MemoDetailPage.
  /// router.replace(MemoDetailPage.query(parameters)); // Replace page to MemoDetailPage.
  /// ```
  @pageRouteQuery
  static const query = _$MemoDetailPageQuery();

  @override
  Widget build(BuildContext context, PageRef ref) {
    // Describes the process of loading
    // and defining variables required for the page.
    // TODO: Implement the variable loading process.
    

    // Describes the structure of the page.
    // TODO: Implement the view.
    return UniversalScaffold();
  }
}
```

まず、表示する`メモID`が必要なので引数として指定します。

```dart
// memo_detail.dart

const MemoDetailPage({
  super.key,
  // TODO: Set parameters for the page.
  required this.memoId,
});

// TODO: Set parameters for the page in the form [final String xxx].
final String memoId;
```

メモIDを元に該当のメモデータを読み込むための処理を`build`メソッド内に記述します。

```dart
// memo_detail.dart

@override
Widget build(BuildContext context, PageRef ref) {
  // Describes the process of loading
  // and defining variables required for the page.
  // TODO: Implement the variable loading process.
  final memoData = ref.app.model(MemoModel.document(memoId))..load();

  // Describes the structure of the page.
  // TODO: Implement the view.
  return UniversalScaffold();
}
```

`memoData`を元にUIを作成します。

```dart
// memo_detail.dart

return UniversalScaffold(
  appBar: UniversalAppBar(
    title: Text(memoData.value?.title ?? ""),
  ),
  body: UniversalListView(
    padding: 16.p,
    children: [
      Text("投稿日時：${memoData.value?.createdAt.value.yyyyMMddHHmm() ?? ""}"),
      16.sy,
      Text(memoData.value?.text ?? ""),
    ],
  ),
);
```

`MemoListPage`の各要素から`MemoDetailPage`に移動できるようにします。

```dart
// memo_list.dart

// Describes the structure of the page.
// TODO: Implement the view.
return UniversalScaffold(
  appBar: UniversalAppBar(
    title: Text("Memo List"),
  ),
  body: UniversalListView(
    children: memoList.mapListenable((e) {
      return ListTile(
        title: Text(e.value?.title ?? ""),
        subtitle: Text(e.value?.createdAt.value.yyyyMMddHHmm() ?? ""),
        onTap: () {
          router.push(MemoDetailPage.query(memoId: e.uid));
        },
      );
    }),
  ),
);
```

各ドキュメントの`uid`がドキュメント自体に設定されているIDとなるのでそれをそのまま`memoId`として渡すことができます。

これでデバッグしてみましょう。

![](https://storage.googleapis.com/zenn-user-upload/fd9696b3dd9f-20230701.png)

メモの詳細ページが作成されました。

# おわりに

作成していて気づかれてるかと思いますがほぼUI周りの記述しかしていません。

このようにコマンドによる自動生成と`katana code watch`による自動生成を組み合わせることでデータ周りの実装を極力省くことができるのがMasamuneフレームワークです。

UI側も`GithubCopilot`などを利用していればサジェストしてくれるのでさらに実装を短縮化できる可能性があります。いい時代になりましたね！

次回はメモデータの追加・更新部分の実装を進めたいと思います。お楽しみに！

https://qiita.com/mathru/items/0a43fdc56ca70a953b1e

Masamuneフレームワークは[こちら](https://github.com/mathrunet/flutter_masamune)にソースを公開しています。issueやPullRequestをお待ちしてます！

また仕事の依頼等ございましたら、私の[Twitter](https://twitter.com/mathru)や[Webサイト](https://mathru.net)で直接ご連絡をお願いいたします！

https://mathru.net/ja/contact

# GitHub Sponsors

スポンサーを随時募集してます。ご支援お待ちしております！

https://github.com/sponsors/mathrunet