FlutterでMixinを使う
mixinとは何か?
公式を翻訳
クラスへの機能追加:Mixin
ミキシンは、クラスのコードを複数のクラス階層で再利用するための方法です。
ミキシンを使用するには、with キーワードに続けて 1 つ以上のミキシン名を使用します。次の例では、ミキシンを使用する 2 つのクラスを示しています。
class Musician extends Performer with Musical {
// ···
}
class Maestro extends Person with Musical, Aggressive, Demented {
Maestro(String maestroName) {
name = maestroName;
canConduct = true;
}
}
ミキシンを実装するには、Object を継承し、コンストラクタを宣言していないクラスを作成します。ミキシンを通常のクラスとして使用したいのでなければ、class の代わりに mixin キーワードを使用します。たとえば、以下のようになります。
mixin Musical {
bool canPlayPiano = false;
bool canCompose = false;
bool canConduct = false;
void entertainMe() {
if (canPlayPiano) {
print('Playing piano');
} else if (canConduct) {
print('Waving hands');
} else {
print('Humming to self');
}
}
}
どんなことができるかと言いますと、クラスに機能を作って後から追加できるってことです。書き方が独特なので、慣れるまでわからないですね。
今回は、Flutterで使ってる人をあまり見かけないので、これは多分推奨されてないと思いますが、mixin使って、FireStoreを操作するコードを書いてみました。
最近、コードを書きすぎて、ファイルが増えすぎたので、プロジェクトごとGithubにpushせずに、コードだけ公開しました。
今回作成したアプリ
Androidのエミュレーターが動作が遅いのか動きがカクカクですけど、アプリの動作は確認できます。
参考までにどうぞ。
今回は、Mixinの設定ファイルをおくmixin_fileディレクトリと、データの追加・表示・削除をするファイルをおくmovie_pageディレクトリを作成してください。
Mixinの設定ファイルを作成
最近使ってみた、withConverterを今回は使用してみようと思います。
コードを書いてて気づいたのですけど、このファイルにwithConverterのコードを書いてないと、mixinの中で、importができませんでした🤔
import 'package:cloud_firestore/cloud_firestore.dart';
class Movie {
Movie({required this.title, required this.genre, required this.createdAt});
Movie.fromJson(Map<String, Object?> json)
: this(
title: json['title']! as String, // Stringにcastする.
genre: json['genre']! as String, // Stringにcastする.
createdAt: json['createdAt']! as Timestamp, // Timestampにcastする.
);
final String title; // 映画のタイトル.
final String genre; // 映画のジャンル.
final Timestamp createdAt; // データが作成された時間.
Map<String, Object?> toJson() {
return {
'title': title,
'genre': genre,
'createdAt': createdAt,
};
}
}
final moviesRef =
FirebaseFirestore.instance.collection('movies').withConverter<Movie>(
fromFirestore: (snapshot, _) => Movie.fromJson(snapshot.data()!),
toFirestore: (movie, _) => movie.toJson(),
);
withConverterを使用して、データの追加・表示・削除を行う機能をmixinに定義しておく。
使用するときは、mixinを継承しているDataStoreクラスから呼び出す。
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/cupertino.dart';
import 'package:timelineview_app/mixin_file/movie_model.dart';
mixin MoviesConverter {
// Formで入力に使うコントローラー.
final TextEditingController titleController = TextEditingController();
final TextEditingController genreController = TextEditingController();
// FireStoreの値を全て取得する.
final moviesConverter =
FirebaseFirestore.instance.collection('movies').withConverter<Movie>(
fromFirestore: (snapshot, _) => Movie.fromJson(snapshot.data()!),
toFirestore: (movie, _) => movie.toJson(),
);
// FireStoreにデータを追加する.
Future<void> addMovies(String title, String genre) async {
// 作成時刻を取得.
final now = DateTime.now();
// 映画情報を追加.
await moviesConverter.add(
Movie(title: title, genre: genre, createdAt: Timestamp.fromDate(now)),
);
}
// FireStoreのデータを削除する.
Future<void> removeMovies(dynamic document) async {
// 映画情報を追加.
await moviesConverter.doc(document).delete();
}
}
DataStoreクラスを作成。アプリ側ではこのクラスを呼び出せば、mixinで追加したロジックも使用することができる。
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:timelineview_app/mixin_file/movie_model.dart';
import 'package:timelineview_app/mixin_file/movies_converter.dart';
// mixinを継承したクラス。アプリではこのクラスを呼び出す!
class DataStore with MoviesConverter {
// falseにすると、昇順でデータを並び替える.
final moviesOrderBy = FirebaseFirestore.instance
.collection('movies')
.orderBy('createdAt', descending: false)
.withConverter<Movie>(
fromFirestore: (snapshot, _) => Movie.fromJson(snapshot.data()!),
toFirestore: (movie, _) => movie.toJson(),
);
}
アプリを実行してみる
mixinでFirebase使ったことないので、どうなるのかためしにやってみたのですが、操作することはできるみたいです。
といっても普段は、RiverpodのStateNotire使うので、これは必要かなと考えますね。
Riverpod使わない人もいるので、何か参考にならないかなと思って作ってみました。
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:timelineview_app/ui/movie_page/add_movie.dart';
import 'package:timelineview_app/ui/movie_page/orderby_page.dart';
import 'package:timelineview_app/ui/movie_page/stream_page.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SelectPage(),
);
}
}
class SelectPage extends StatelessWidget {
const SelectPage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ページを選択'),
),
body: Center(
child: Column(
children: [
SizedBox(height: 50),
ElevatedButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => StreamPage()));
},
child: Text('Stream')),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => OrderByPage()));
},
child: Text('OrderBy')),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => AddMovie()));
},
child: Text('AddMovie')),
],
),
),
);
}
}
データを追加するページ
import 'package:flutter/material.dart';
import 'package:timelineview_app/service/mixin_file/data_store.dart';
class AddMovie extends StatelessWidget {
const AddMovie({Key? key}) : super(key: key);
Widget build(BuildContext context) {
final dataStore = DataStore();
final titleAdd = dataStore.titleController;
final genreAdd = dataStore.genreController;
return Scaffold(
appBar: AppBar(
title: Text('映画を追加する'),
),
body: Center(
child: Column(
children: [
TextField(
controller: titleAdd,
decoration: InputDecoration(
hintText: "映画名を入力",
),
),
TextField(
controller: genreAdd,
decoration: InputDecoration(
hintText: "映画のジャンルを入力",
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
final addFunction =
dataStore.addMovies(titleAdd.text, genreAdd.text);
},
child: Text('映画を追加'))
],
),
),
);
}
}
Streamで表示するページ
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:timelineview_app/service/mixin_file/data_store.dart';
import 'package:timelineview_app/service/mixin_file/movie_model.dart';
class StreamPage extends StatelessWidget {
const StreamPage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
final dataStore = DataStore().moviesConverter.snapshots();
return Scaffold(
appBar: AppBar(
title: const Text('StreamPage'),
),
body: StreamBuilder<QuerySnapshot<Movie>>(
// クラスを型として使う.
stream: dataStore, // withConverterのデータをstreamで流す.
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(snapshot.error.toString()),
);
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final data = snapshot.requireData;
return ListView.builder(
// ListView.builderで画面に描画する.
itemCount: data.size,
itemBuilder: (context, index) {
return ListTile(
trailing: IconButton(
onPressed: () {
// ランダムに生成されたidを取得.
final document = data.docs[index].id;
// 削除するメソッドを実行する.
final removeData = DataStore().removeMovies(document);
},
icon: const Icon(Icons.delete),
),
title: Text(
data.docs[index].data().title), // Movieクラスのtitleプロパティを使う.
subtitle: Row(
children: [
Text(data.docs[index].data().genre),
const SizedBox(width: 20),
// .toStringだと変な文字になるので、.toDate()の後に
// .と入力してString型に変換するコードを選ぶ.
Text(data.docs[index]
.data()
.createdAt
.toDate()
.toIso8601String())
],
), // Movieクラスのgenreプロパティを使う.
);
},
);
},
),
);
}
}
OrderByで並び替えて表示するページ
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:timelineview_app/service/mixin_file/data_store.dart';
import 'package:timelineview_app/service/mixin_file/movie_model.dart';
class OrderByPage extends StatelessWidget {
const OrderByPage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
final dataStore = DataStore().moviesOrderBy.snapshots();
return Scaffold(
appBar: AppBar(
title: const Text('StreamPage'),
),
body: StreamBuilder<QuerySnapshot<Movie>>(
// クラスを型として使う.
stream: dataStore, // withConverterのデータをstreamで流す.
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(snapshot.error.toString()),
);
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final data = snapshot.requireData;
return ListView.builder(
// ListView.builderで画面に描画する.
itemCount: data.size,
itemBuilder: (context, index) {
return ListTile(
trailing: IconButton(
onPressed: () {
// ランダムに生成されたidを取得.
final document = data.docs[index].id;
// 削除するメソッドを実行する.
final removeData = DataStore().removeMovies(document);
},
icon: const Icon(Icons.delete),
),
title: Text(
data.docs[index].data().title), // Movieクラスのtitleプロパティを使う.
subtitle: Row(
children: [
Text(data.docs[index].data().genre),
const SizedBox(width: 20),
// .toStringだと変な文字になるので、.toDate()の後に
// .と入力してString型に変換するコードを選ぶ.
Text(data.docs[index]
.data()
.createdAt
.toDate()
.toIso8601String())
],
), // Movieクラスのgenreプロパティを使う.
);
},
);
},
),
);
}
}
最後に
最近、Riverpodばかりでコード書いてたので、Riverpod使わないコードが書けないのに気づきました😅
お恥ずかしい💦
自作のクラス使ったり、関数作って、FireStoreのデータを取得するのもやってみたいですね。
Discussion