LoginSignup
25
20

More than 3 years have passed since last update.

flutter + firebase auth + firestore + provider で匿名ユーザーごとにデータを管理する

Posted at

諸注意

オレオレ流です。誰か正解を教えて!
特に命名規則に自信がありません。〇〇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の設定

匿名ログインを有効にします。簡単ですね。
image.png
image.png

pubspec.yaml

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
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
TodoService.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
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
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);
          },
        ),
      ],
    );
  }
}

完成

右のゴミ箱ボタンで削除、右下の+ボタンで追加、編集機能はつけていません。
image.png
image.png

リンク

25
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
20