こんにちは。広瀬マサルです。
Flutterでアプリケーションを安定して高速に開発することのできるMasamuneフレームワークを紹介しました。
その魅力と使い方を知っていただくためにこれから数回に分けて紹介記事を書きたいと思います。
Masamuneフレームワークを使いこなすことで超高速・安定・高品質なアプリを開発することができるでしょう。
前々回から「メモ帳アプリ」の作成を開始しまして、メモの一覧・詳細表示、メモの追加・編集・削除までを行えるようになりました。
今回はこのアプリに「テキスト検索」と「画像のアップロード」機能をつけてより使いやすくしていきたいと思います。
テキスト検索
MasamuneフレームワークではNoSQLを使ってFirestoreやローカルDBを構築しています。
Firestoreなどではテキスト検索はあまり得意でなくAlgoliaなどを併用するように推奨されています。
MasamuneフレームワークではAlgoliaなどを利用せずとも簡易的なテキスト検索を行える機能を提供しています。
まずはデータを検索対象にしたいと思います。
前回からのプロジェクトを開いて、別ターミナルで下記コマンドを実行しコードの変更を監視してください。
katana code watch
MemoModel
を開いて検索対象にしたいパラメーターに@searchParam
のアノテーションを付けましょう。
// memo.dart
const factory MemoModel({
// TODO: Set the data schema.
@searchParam required String title,
@searchParam required String text,
@Default(ModelTimestamp()) ModelTimestamp createdAt,
}) = _MemoModel;
const MemoModel._();
これだけでtitle
とtext
はテキスト検索の対象になります。
それでは続いて検索フォームを作成しましょう。
MemoListPage
を開いて下記のように書き換えます。
// 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.model(MemoModel.collection())..load();
final searchToggle = ref.page.watch((_) => ValueNotifier(false));
// Describes the structure of the page.
// TODO: Implement the view.
return UniversalScaffold(
appBar: UniversalAppBar(
title: searchToggle.value
? FormTextField(
autofocus: true,
hintText: "Search",
onChanged: (value) {
if (value == null) {
return;
}
memoList.search(value);
},
)
: Text("Memo List"),
actions: [
IconButton(
color: searchToggle.value ? theme.color.primary : null,
icon: const Icon(Icons.search),
onPressed: () {
searchToggle.value = !searchToggle.value;
},
),
],
),
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));
},
);
}),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
router.push(MemoEditAddPage.query());
},
child: const Icon(Icons.add),
),
);
}
UIとしては右上に検索ボタンが表示され、そこをタップすると検索ボックスが表示される仕様にしています。
下記のようにref.page.watch
を利用するとValueNotifier
やChangeNotifier
の状態をそのページ内だけで管理することができ、変更があった場合画面が更新されます。
final searchToggle = ref.page.watch((_) => ValueNotifier(false));
memoList
のsearch
メソッドに検索したい文字列を渡すことによりmemoList内の要素がフィルタリングされ画面が更新されます。
memoList.search(value);
これで実装完了です。デバッグしてみましょう。
検索ボタンを押すとテキストフィールドが出現します。
テキストを入力するとそれに応じて要素が表示されます。
画像のアップロード
それでは画像のアップロードを実装していきましょう。
モバイルで画像をアップロードしようとする場合、画像ライブラリへのアクセス権限が必要になります。
Masamuneフレームワークではそれらへのフォローも含めてまとめて設定可能です。
まずはkatana.yaml
を開きます。
app
→picker
の項目のenable
をtrue
にし、permission
を自身の言語コード(enやjaなど)をキーにした上でどのような目的で画像ライブラリを使用するかを記載してください。
# katana.yaml
app:
~~~~~~~~
# Describe the settings for using the file picker.
# Specify the permission message to use the library in IOS in [permission].
# Please include `en`, `ja`, etc. and write the message in that language there.
# ファイルピッカーを利用するための設定を記述します。
# [permission]にIOSでライブラリを利用するための権限許可メッセージを指定します。
# `en`や`ja`などを記載しそこにその言語でのメッセージを記述してください。
picker:
enable: true
permission:
ja: メモ画像用にライブラリを利用します。
記載後katana apply
コマンドを実行します。
katana apply
IOSで設定必須なNSPhotoLibraryUsageDescription
などの設定も上記コマンドでまとめて行われます。
次にmain.dart
を開きましょう。
Picker
オブジェクトの定義と、MasamuneAdapter
へPickerMasamuneAdapter
の追加を行います。
// main.dart
import 'package:masamune_picker/masamune_picker.dart';
~~~~~~~
/// Masamune adapter.
///
/// The Masamune framework plugin functions can be defined together.
// TODO: Add the adapters.
final masamuneAdapters = <MasamuneAdapter>[
const UniversalMasamuneAdapter(),
const PickerMasamuneAdapter(),
];
~~~~~~~
/// App picker.
final appPicker = Picker();
これで下準備は完了です。
続いてMemoModel
に画像のデータスキームを追加しましょう。
// memo.dart
const factory MemoModel({
// TODO: Set the data schema.
@searchParam required String title,
@searchParam required String text,
ModelImageUri? image,
@Default(ModelTimestamp()) ModelTimestamp createdAt,
}) = _MemoModel;
ModelImageUri
は画像用のURIを保存するための特殊クラスです。
Stringでも代用可能ですがModelImageUri
は画像としての取扱が良くなるので積極的に採用しましょう。
続いてmemo_edit.dart
に画像アップロード用のフォームを作成しましょう。
// memo_edit.dart
return UniversalScaffold(
appBar: UniversalAppBar(
title: Text("Editing memo"),
actions: [
IconButton(
icon: const Icon(Icons.check),
onPressed: () async {
final value = controller.validate();
if (value == null) {
return;
}
if (ref.isEditing) {
await memoData!.save(controller.value).showIndicator(context);
} else {
final memoData = ref.model(MemoModel.collection()).create();
await memoData.save(controller.value).showIndicator(context);
}
router.pop();
},
),
],
),
body: UniversalColumn(
children: [
FormLabel("Title"),
FormTextField(
form: controller,
hintText: "Title",
initialValue: controller.value.title,
onSaved: (value) => controller.value.copyWith(
title: value,
),
),
FormLabel("Text"),
Expanded(
child: FormTextField(
form: controller,
hintText: "Text",
expands: true,
initialValue: controller.value.text,
style: const FormStyle(textAlignVertical: TextAlignVertical.top),
onSaved: (value) => controller.value.copyWith(
text: value,
),
),
),
FormLabel("Image"),
FormMedia(
form: controller,
onTap: (ref) async {
final media = await appPicker.pickSingle(
type: PickerFileType.image,
);
final uploaded = await media.upload().showIndicator(context);
if (uploaded == null) {
return;
}
ref.update(uploaded, FormMediaType.image);
},
initialValue: controller.value.image?.toFormMediaValue(),
style: FormStyle(padding: 16.p),
builder: (context, media) {
return Image(
image: media.toImageProvider(),
fit: BoxFit.cover,
);
},
onSaved: (value) => controller.value.copyWith(
image: value.toModelImageUri(),
),
),
],
),
);
FormMedia
が画像などのメディアデータを入力させるためのフォームです。
form
にフォームコントローラーを渡します。
onTap
にタップしたときの動作を実装します。
今回は先程定義したappPicker
を利用しpickSingle
を実行することでOSのファイルピッカーを起動します。そこから取得できたmedia
をupload
メソッドを用いてアップロードし、onUpdate
を利用して更新します。
initialValue
にModelImageUri
をtoFormMediaValue
を用いながら変換し初期値として与えています。
builder
にはどのようにメディアデータを表示するかを実装します。
onSaved
でMemoModel
のimage
にデータを渡すかを記載します。
最後にmemo_detail.dart
を開き保存した画像を表示する仕組みを作りましょう。
単純にMemoModel
のimage
を利用すればよいです。toImageProvider()
メソッドを利用するとそのままImageProvider
として利用可能なので積極的に使いましょう。
return UniversalScaffold(
appBar: UniversalAppBar(
title: Text(memoData.value?.title ?? ""),
actions: [
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
Modal.confirm(
context,
submitText: "Delete",
cancelText: "Cancel",
title: "Confirmation",
text: "Delete the memo. Are you sure?",
onSubmit: () async {
await memoData.delete().showIndicator(context);
router.pop();
},
);
},
),
IconButton(
icon: const Icon(Icons.save),
onPressed: () {
router.push(MemoEditEditPage.query(editId: memoId));
},
),
],
),
body: UniversalListView(
padding: 16.p,
children: [
Text("投稿日時:${memoData.value?.createdAt.value.yyyyMMddHHmm() ?? ""}"),
16.sy,
Text(memoData.value?.text ?? ""),
if (memoData.value?.image != null) ...[
32.sy,
Image(
image: memoData.value!.image!.toImageProvider(),
),
],
],
),
);
デバッグしてみます。下記のようにフォームが表示されます。
画像部分をタップすると権限許可のダイアログが表示され、このようにOSのファイルピッカーが起動します。
入力し保存すると
このように画像が文章と共に表示されます。
番外編:Firebase Storageへの切り替え
画像アップロード機能を実装しました。
現在はローカル上に保存されているファイルをそのまま表示していますが、実際にCloud Storage for Firebase
を利用してアップロードを行いましょう。
まずは、すでに作成しているFirebaseのプロジェクトを開きます。
Storageを作成します。
最初は「Test Mode」で進めましょう。
RegionはFirestore作成時に設定したので変更できません。
続いてFlutterプロジェクトに戻りkatana.yaml
を書き換えます。
firebase
→ storage
→ enable
をtrue
に変更します。
# katana.yaml
firebase:
# Set the Firebase project ID.
# FirebaseのプロジェクトIDを設定します。
project_id: masamune-test
# Enable Firebase Firestore.
# Firebase Firestoreを有効にします。
firestore:
enable: true
overwrite_rule: false
# Enable Firebase Authentication.
# Firebase Authenticationを有効にします。
authentication:
enable: false
# Enable Cloud Storage for Firebase.
# Cloud Storage for Firebaseを有効にします。
storage:
enable: true # false -> true
overwrite_rule: false
下記のコマンドを実行します。
katana apply
続いてアダプターを切り替えましょう。ついでにデータベースもFirestore
に切り替えます。
main.dart
を開きます。
// main.dart
import 'package:katana_storage_firebase/katana_storage_firebase.dart';
~~~~~~~
/// App Model.
///
/// By replacing this with another adapter, the data storage location can be changed.
// TODO: Change the database.
final modelAdapter = FirestoreModelAdapter(
options: DefaultFirebaseOptions.currentPlatform,
);
// 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)),
// )
// },
// )
// ],
// );
/// App Auth.
///
/// Changing to another adapter allows you to change to another authentication mechanism.
// TODO: Change the authentication.
final authAdapter = RuntimeAuthAdapter();
/// App Storage.
///
/// Changing to another adapter allows you to change to another storage mechanism.
// TODO: Change the storage.
final storageAdapter = FirebaseStorageAdapter(
options: DefaultFirebaseOptions.currentPlatform,
);
// final storageAdapter = LocalStorageAdapter();
これで完了です。
デバッグしてみましょう。画像付きでメモを更新してみます。
画像が表示されました!
Storageのバケットにもファイルがアップロードされているのがわかります。
おわりに
実装が大変な「テキスト検索
」や「画像アップロード
」もMasamuneフレームワークであればいとも簡単に実装することができました。
このようにMasamuneフレームワークではアプリを作る上での様々な機能実装をサポートします。まだまだ深掘りできるところは沢山あるので楽しみにしてください!
次回からこのメモ帳アプリに「ユーザー管理
」「ユーザー登録・認証
」機能を加えて「マルチユーザーブログ
」アプリに進化させていきましょう。
Masamuneフレームワークはこちらにソースを公開しています。issueやPullRequestをお待ちしてます!
また仕事の依頼等ございましたら、私のTwitterやWebサイトで直接ご連絡をお願いいたします!
GitHub Sponsors
スポンサーを随時募集してます。ご支援お待ちしております!