[Flutter]uaizu_appを支える技術

# はじめに
この記事は[Aizu Advent Calendar 2024](https://adventar.org/calendars/10858)に参加しています。
# 本題
8月から10月にかけて[uaizu_app](https://github.com/brqnko/uaizu_app)というスマホアプリを作成しました。この記事では、その中身や使用技術について書いていきたいと思います。なお、筆者はFlutter歴半年の未熟者ですので、温かい目で見て頂ければと思います。

# つくったもの

今回つくったアプリの主な機能は、大学の学務システムやMoodle、図書館データベースにアクセスすることです。また、課題の提出期限に通知を送信したりなどのサブ的な機能もいくつかあります。

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3834562/cc07b91c-0f91-bae8-d677-7c94cca4b21a.png"> <img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3834562/240fec8a-e562-0f72-4240-7515dac51571.png"> <img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3834562/b532854b-69c5-fd89-7bb8-eb1fd7e85133.png"> <img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3834562/0ad0484c-ac72-eb9b-e9fb-34b461add9cf.png"> <img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3834562/b032e8e5-dc86-87f8-a595-6496f7d2a78c.png"> <img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3834562/4ba0092b-712f-c9fc-eecd-ab44c286c2ce.png">

# 使用技術及びパッケージについて

## Flutter
クロスプラットフォームなアプリを開発するためのフレームワークとして、Flutterを採用しました。

https://flutter.dev/

## flutter_riverpod

状態管理を行なうことができます。依存性の注入にも使用しました。

https://pub.dev/packages/flutter_riverpod

## freezed

イミュータブルなクラスを簡単に作成することができます。

https://pub.dev/packages/freezed

## go_router

画面遷移を宣言的に行うことができます。

https://pub.dev/packages/go_router

## flutter_hooks

ひとつのUIで簡潔するような状態を管理するのに使用しました。

https://pub.dev/packages/flutter_hooks

## flutter_local_notifications

ローカル通知を出すことができます。

flutter_local_notifications

## その他パッケージ

上で紹介したもの以外に、様々なパッケージを使用しました。詳しくは、[こちら](https://github.com/brqnko/uaizu_app/blob/main/pubspec.yaml)をご覧ください。

# アーキテクチャについて
クリーンアーキテクチャというアーキテクチャでアプリを作成しました。クリーンアーキテクチャでは、Domain層、Infrastructure層、UseCase層、UI層の4つノレイヤーに分けてアプリを作成していきます。次に、それぞれのレイヤーで、どのようなコードを書いたのかについて説明します。

# Domain層

Domain層では主に、Entityの定義、DIコンテナの定義、Repositoryの定義を記述しました。

## Entityの定義

Entityの定義では、アプリ全体の基本的なクラスやEnumの定義をしました。Entityクラスのほとんどはイミュータブルで、freezedを使って定義されています。

下のコードは、freezedを使ったEntity定義の例です。

```dart
@freezed
class Book with _$Book {
  const factory Book({
    required String path,
    String? title,
    String? imageUrl,
    String? author,
    String? publisher,
    String? location,
    String? callMark,
    String? material,
    String? publication,
    String? form,
    String? alternative,
    String? countryOfPublication,
    String? titleLanguage,
    String? languageOfTexts,
    String? languageOfOriginal,
    String? isbn,
    String? ncid,
  }) = _Book;
}
```

## DIコンテナの定義

RiverpodのProviderを使用して、依存関係の解決の行いました。下にコード例を置いておきます。

```dart
final _bookDataSourceProvider = Provider((ref) {
  return BookDataSource(ref.watch(libraryClientProvider));
});

final bookRepositoryProvider = Provider((ref) {
  return BookRepositoryImpl(ref.watch(_bookDataSourceProvider));
});
```

## Repositoryの定義

データ操作に関するインターフェースを定義しています。現在のDart言語には、純粋なインターフェースの機能がないので、抽象クラスを使用しています。

下のコードは、Repository定義の実装例です。

```dart
abstract class BookRepository {
  Future<Book> fetchBookDetail(String path);

  Future<BookSearchResult> fetchBookSearchResult(BookSearchQuery query);

  Future<String?> fetchBookImageUrl(Book book);

  Future<List<Book>> fetchNewBooks();
}
```

# Infrastructure層

Infrastructure層では、HTTPクライアントの定義、データソースクラスの定義、その他もももろのコードを記述しています。

## HTTPクライアント

HTTP通信を行うために[http](https://pub.dev/packages/http)というパッケージを使用しました。ここでは、httpパッケージに足りない機能を追加した、独自のHTTPクライアントのラッパーを定義しています。具体的には、以下のような機能を追加しています。

- 単位時間に送れるリクエストの制限をかける

- デフォルトのヘッダーの追加

- クッキー情報の保管

## データソース

サーバーへのリクエスト送信やレスポンスの解析のコードはここに記述しています。どのようにレスポンスを解析するかについて優先順位をつけています。

- DOM抽出(優先度: 高)
- 正規表現(優先度: 中)
- 文字列操作(優先度: 低)

下のコードは、DOM抽出を用いたレスポンス解析の例です。

```dart
LibraryCalendarMonth parseOpeningCalendarFromBody(
    String responseBody,
    DateTime time,
  ) {
    final document = parse(responseBody);

    final calenderList = document
        .querySelector('table.library_calendar_table[data-striping="1"]')!
        .querySelectorAll('tr')
        .expand((element) => element.querySelectorAll('td'))
        .where(_isOpeningStateElement)
        .map((element) => _parseOpeningStateFromElement(element, time))
        .toList();

    final calender = Map.fromEntries(calenderList);

    return LibraryCalendarMonth(
      month: time,
      calender: calender,
      calenderColors: _calenderToOpeningColors(calender),
      locale: _client.locale,
    );
  }
```

## その他

上に書いたもの以外には、データを永続化するためのコードや、ローカル通知を送るためのコードを記述しています。

データベースは、2つのデータベースを使用しています。

- flutter_secure_storage

[flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage)は、データを暗号化された状態で永続化させることができます。パスワードなどの重要な情報はこれを使って保存しています。

- shared_preferences

[shared_preferences](https://pub.dev/packages/shared_preferences)は、単純なデータを保存するのに向いています。ローカル通知などの情報は、これを使って保存しています。

# UseCase層

UseCase層では、先ほどDomain層で定義したクラスなどを組み合わせて、ビジネスロジックを構築していきます。今回のアプリでは、あまり複雑なビジネスロジックを定義していません。

下のコードは、UseCaseクラスの実装例です。

```dart
class GetNewBooksUseCase extends UseCase<void, Future<List<Book>>> {
  GetNewBooksUseCase(this._repository);

  final BookRepository _repository;

  @override
  Future<List<Book>> call(void param) async {
    final books = await _repository.fetchNewBooks();

    return books;
  }
}
```

# UI層

Flutterのウィジェットを使って、UIを構築していきます。ここでのウィジェットは、dialogs、pages、res、router、widgetsに種類分けしてあります。

- dialog

ダイアログに関するウィジェットを定義しています。

- pages

画面全体を表すようなウィジェットを定義しています。

- res

カラーやフォントなどの情報を定義しています。

- router

go_routerなどを用いて、画面遷移まわりのコードを定義しています。

- widgets

独自のAppBarなどの細かなウィジェットを定義しています。

以下のコードは、とあるページの実装例です。

```dart
class SchedulePage extends HookWidget {
  const SchedulePage({super.key});

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    final focusedDay = useState(_flatToDay(DateTime.now()));

    return Scaffold(
      backgroundColor: colorScheme.surface,
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              CampusSquareCalendar(focusedDay: focusedDay),
              const SizedBox(height: 16),
              CampusSquareDetail(focusedDay: focusedDay),
            ],
          ),
        ),
      ),
    );
  }
}
```

# おわりに

以上で、解説を終わりたいと思います。本アプリのコード行数は、自動生成のものを除いて約6000行ほどあります。初心者の私でも6000行のプロジェクトをつくることができる、クリーンアーキテクチャはすごいと思いました。