はじめに
この記事はAizu Advent Calendar 2024に参加しています。
本題
8月から10月にかけてuaizu_appというスマホアプリを作成しました。この記事では、その中身や使用技術について書いていきたいと思います。なお、筆者はFlutter歴半年の未熟者ですので、温かい目で見て頂ければと思います。
つくったもの
今回つくったアプリの主な機能は、大学の学務システムやMoodle、図書館データベースにアクセスすることです。また、課題の提出期限に通知を送信したりなどのサブ的な機能もいくつかあります。
使用技術及びパッケージについて
Flutter
クロスプラットフォームなアプリを開発するためのフレームワークとして、Flutterを採用しました。
flutter_riverpod
状態管理を行なうことができます。依存性の注入にも使用しました。
freezed
イミュータブルなクラスを簡単に作成することができます。
go_router
画面遷移を宣言的に行うことができます。
flutter_hooks
ひとつのUIで簡潔するような状態を管理するのに使用しました。
flutter_local_notifications
ローカル通知を出すことができます。
flutter_local_notifications
その他パッケージ
上で紹介したもの以外に、様々なパッケージを使用しました。詳しくは、こちらをご覧ください。
アーキテクチャについて
クリーンアーキテクチャというアーキテクチャでアプリを作成しました。クリーンアーキテクチャでは、Domain層、Infrastructure層、UseCase層、UI層の4つノレイヤーに分けてアプリを作成していきます。次に、それぞれのレイヤーで、どのようなコードを書いたのかについて説明します。
Domain層
Domain層では主に、Entityの定義、DIコンテナの定義、Repositoryの定義を記述しました。
Entityの定義
Entityの定義では、アプリ全体の基本的なクラスやEnumの定義をしました。Entityクラスのほとんどはイミュータブルで、freezedを使って定義されています。
下のコードは、freezedを使ったEntity定義の例です。
@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を使用して、依存関係の解決の行いました。下にコード例を置いておきます。
final _bookDataSourceProvider = Provider((ref) {
return BookDataSource(ref.watch(libraryClientProvider));
});
final bookRepositoryProvider = Provider((ref) {
return BookRepositoryImpl(ref.watch(_bookDataSourceProvider));
});
Repositoryの定義
データ操作に関するインターフェースを定義しています。現在のDart言語には、純粋なインターフェースの機能がないので、抽象クラスを使用しています。
下のコードは、Repository定義の実装例です。
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というパッケージを使用しました。ここでは、httpパッケージに足りない機能を追加した、独自のHTTPクライアントのラッパーを定義しています。具体的には、以下のような機能を追加しています。
-
単位時間に送れるリクエストの制限をかける
-
デフォルトのヘッダーの追加
-
クッキー情報の保管
データソース
サーバーへのリクエスト送信やレスポンスの解析のコードはここに記述しています。どのようにレスポンスを解析するかについて優先順位をつけています。
- DOM抽出(優先度: 高)
- 正規表現(優先度: 中)
- 文字列操作(優先度: 低)
下のコードは、DOM抽出を用いたレスポンス解析の例です。
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は、データを暗号化された状態で永続化させることができます。パスワードなどの重要な情報はこれを使って保存しています。
- shared_preferences
shared_preferencesは、単純なデータを保存するのに向いています。ローカル通知などの情報は、これを使って保存しています。
UseCase層
UseCase層では、先ほどDomain層で定義したクラスなどを組み合わせて、ビジネスロジックを構築していきます。今回のアプリでは、あまり複雑なビジネスロジックを定義していません。
下のコードは、UseCaseクラスの実装例です。
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などの細かなウィジェットを定義しています。
以下のコードは、とあるページの実装例です。
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行のプロジェクトをつくることができる、クリーンアーキテクチャはすごいと思いました。