諸注意
オレオレ流です。誰か正解を教えて!
特に命名規則に自信がありません。〇〇serviceや〇〇processなど
lib配下に本来は用途ごとにディレクトリ分けるべきですが(pagesとかviewsとか)、この記事では分けていません。
この記事ですること
flutterにおいてユーザー単位でデータを保持するにはどうしたらよいかという記事です。
データを保持するためにfirestoreを使用し、ユーザー単位でデータを持つために匿名ログインを使用します。
また、状態管理としてproviderを使用します。
サンプルとしてaddとdeleteができるTODOアプリをつくります。
この記事でしないこと
firebaseやflutterの設定はしません。
firebaseはプロジェクトの作成とfirestoreの設定をしておいてください。
firestoreのセキュリティルールはテストモードにしてください。(記事内で正しく設定し直します)
本番環境と開発環境を分離したいって人はflutter + firebaseで本番環境と開発環境を切り替えるを参考にしてください。
この記事では本番と開発は分けていませんが、一応はflutter + firebaseで本番環境と開発環境を切り替えるの続きを想定した記事です。
firestoreのセキュリティルールの設定
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ユーザーが認証されている場合はTRUE
function isUserAuthenticated(userId) {
return request.auth.uid == userId;
}
// ユーザー認証されている場合に各権限を許可する
match /users/{userId}/todo/{docId} {
allow read, write: if isUserAuthenticated(userId);
}
}
}
authの設定
pubspec.yaml
cupertino_icons: ^0.1.2
cloud_firestore: ^0.13.4+2
firebase_auth: ^0.15.4
apple_sign_in: ^0.1.0
provider: ^4.0.4
flutter
/lib
配下に以下のファイルを作ってください。
auth_service.dart
todo_service.dart
todo_model.dart
main.dart
ソースコード貼り付けます。長いので折りたたんでいます。
auth_service.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
enum Status { uninitialized, authenticated, authenticating, unauthenticated }
// https://medium.com/flutter-community/flutter-firebase-login-using-provider-package-54ee4e5083c7
class AuthService with ChangeNotifier {
final FirebaseAuth _auth;
FirebaseUser _user;
Status _status = Status.uninitialized;
AuthService.instance() : _auth = FirebaseAuth.instance{
_auth.onAuthStateChanged.listen(_onAuthStateChanged);
}
FirebaseUser get user => _user;
FirebaseAuth get auth => _auth;
Status get status => _status;
// firebase auth側の匿名認証を有効にするのを忘れずに
Future<void> signInAnonymously() async {
try {
_status = Status.authenticating;
notifyListeners();
await _auth.signInAnonymously();
_status = Status.authenticated;
notifyListeners();
} catch (e) {
print(e);
_status = Status.unauthenticated;
notifyListeners();
}
}
Future<void> _onAuthStateChanged(FirebaseUser firebaseUser) async {
if (firebaseUser == null) {
_status = Status.unauthenticated;
} else {
_user = firebaseUser;
_status = Status.authenticated;
}
notifyListeners();
}
}
todo_service.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:how_many_minutes_left/todo_model.dart';
class TodoService extends ChangeNotifier {
String uid;
List _todo;
TodoService();
CollectionReference get dataPath =>
Firestore.instance.collection('users/$uid/todo');
List get todo => _todo;
void init(List<DocumentSnapshot> documents) {
_todo = documents.map((doc) => TodoModel.fromMap(doc)).toList();
}
void addTitle(title) {
dataPath.document().setData({'title': title, 'createAt': DateTime.now()});
}
void deleteDocument(docId) {
dataPath.document(docId).delete();
}
}
todo_model.dart
import 'package:cloud_firestore/cloud_firestore.dart';
class TodoModel {
String _docId;
String _title;
Timestamp _createAt;
TodoModel(
this._docId,
this._title,
this._createAt,
);
String get docId => _docId;
String get title => _title;
Timestamp get createAt => _createAt;
TodoModel.fromMap(map) {
_docId = map.documentID;
_title = map['title'];
_createAt = map['createAt'];
}
Map<String, dynamic> toMap() {
var map = <String, dynamic>{};
map['title'] = _title;
map['createAt'] = _createAt;
return map;
}
}
main.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:how_many_minutes_left/todo_service.dart';
import 'package:provider/provider.dart';
import 'auth_service.dart';
// 匿名ユーザーごとに処理を分ける
void main() {
runApp(
// providerを複数使うときはMultiProviderを使う。
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthService.instance()),
ChangeNotifierProvider(create: (_) => TodoService()),
],
child: MyApp(),
),
);
}
// 本番かリリースかを判断するには bool.fromEnvironment('dart.vm.product')を使う。
// よりわかりやすくするためにラップして使っている。
bool isRelease() {
bool _bool;
bool.fromEnvironment('dart.vm.product') ? _bool = true : _bool = false;
return _bool;
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SignProcess(),
);
}
}
class SignProcess extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer(
builder: (context, AuthService authService, _) {
// ログインの状態に応じて処理を遷移させる。
switch (authService.status) {
case Status.uninitialized:
print('uninitialized');
return Center(child: CircularProgressIndicator());
case Status.unauthenticated:
case Status.authenticating:
print('anonymously');
authService.signInAnonymously();
return Center(child: CircularProgressIndicator());
case Status.authenticated:
print("authenticated");
break; // DbProcess();へ進む
}
return DbProcess();
},
);
}
}
class DbProcess extends StatelessWidget {
@override
Widget build(BuildContext context) {
final authService = Provider.of<AuthService>(context);
final todoService = Provider.of<TodoService>(context);
// firestoreのデータはuidごとに分けているので、データの取得前にtodoServiceにuidを渡してあげる
todoService.uid = authService.user.uid;
// streamのデータ(firestore)のデータが変更される度に自動でリビルドしてくれる
return StreamBuilder<QuerySnapshot>(
// firestoreからデータを拾ってくる
stream: todoService.dataPath.orderBy("createAt").snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
switch (snapshot.connectionState) {
case ConnectionState.waiting: // データの取得まち
return CircularProgressIndicator();
default:
// streamからデータを取得できたので、使いやすい形にかえてあげる
todoService.init(snapshot.data.documents);
return ViewPage();
}
},
);
}
}
class ViewPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final todoService = Provider.of<TodoService>(context);
return Scaffold(
appBar: AppBar(
title: Center(
child: Text(
'firestoreの動作確認\n(${isRelease() ? 'リリース' : 'デバック'}モード)'))),
body: Center(
child: ListView.builder(
itemCount: todoService.todo.length,
itemBuilder: (BuildContext context, int index) {
final _date = todoService.todo[index].createAt.toDate();
return ListTile(
title: Text(todoService.todo[index].title),
subtitle: Text(_date.toString()),
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () {
todoService.deleteDocument(todoService.todo[index].docId);
},
),
);
},
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return NameInputDialog();
},
);
},
),
);
}
}
class NameInputDialog extends StatefulWidget {
@override
_NameInputDialogState createState() => _NameInputDialogState();
}
class _NameInputDialogState extends State<NameInputDialog> {
TextEditingController _nameController = TextEditingController();
@override
Widget build(BuildContext context) {
final todoService = Provider.of<TodoService>(context);
return AlertDialog(
title: Text('予定を入力してください'),
content: TextField(
controller: _nameController,
autofocus: true,
decoration: InputDecoration(
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(16))),
),
actions: <Widget>[
FlatButton(
child: Text('クリア'),
onPressed: _nameController.clear,
),
FlatButton(
child: Text('決定'),
onPressed: () {
todoService.addTitle(_nameController.text);
Navigator.pop(context);
},
),
],
);
}
}
完成
右のゴミ箱ボタンで削除、右下の+ボタンで追加、編集機能はつけていません。