Flutter — Firebase CRUD list view
--
Today we’re going to create a BLoC patterned list where a user can ‘create, read, update and delete’ I’m going to mostly focus on the ListView as the only UI element, and I won’t be explaining much to do with the BLoC pattern itself — just using it, along with some dependency injection, JsonSerialization.. and a few other things.
So to begin, we’re going to store data in the “user” database of Firebase. So we’ll first setup an Authentication repo:
abstract class Authentication {
Stream<String> getUid();
Future<FirebaseUser> loadUser();
void refresh();
}
class AuthenticationImpl extends Authentication {
final String _auth = FirebaseAuth.instance;
final StreamController<String> _uidStream = BehaviorSubject<String>();
AuthenticationImpl() {
refresh();
}
@override
Future<String> loadUser() async {
return _auth.currentUser().uid;
}
@override
Stream<String> getUid() => _uidStream.stream;
@override
void refresh() => loadUser()
.then((user) =>
_uidStream.add(user != null ? user.uid : null));
}
And I’ll also dependency injection that at run time:
Ioc().bind("auth", (ioc) => AuthenticationImpl(), singleton: true);
Now I can tell if a user is logged in or not. If they are logged in, they’ll get a string returned from the getUid() or if I want to be updated, it’ll be added to the UID Stream.
If they’re logged in, they can add notes. Notes are basically stored in Firebase’s Firestore. It’ll be done at “/users/<uid>” and it’ll contain a list of Notes.
/// Representing a single note in the database
class Note {
/// Actual note data
final String note;
/// Date the note was logged. Also used as a key.
final DateTime date;
Note(this.note, this.date);
factory Note.fromJson(Map<String, dynamic> json) => _$NoteFromJson(json);
Map<String, dynamic> toJson() => _$NoteToJson(this);
}
An example of the data from Firebase
Storage
Storage is going to be split up into two components: the bloc, and the repository.
Here’s the repository: (Omitting a lot of the rubbish for brevity)
abstract class UserSettingsRepo {
Stream<List<Note>> get stream;
void storeNote(Note note);
void removeNote(Note note);}
class UserSettingsRepoImpl implements UserSettingsRepo {
StreamController<List<Note>> _streamController =
BehaviorSubject<List<Note>>();
Authentication _auth = Ioc().use<Authentication>("auth");
// Cache of users store
List<Note> _map = List();
UserSettingsRepoImpl() {
_streamController.add(_map);
_auth.getUid().listen((uid) {
if (uid != null) {
Firestore.instance
.collection("users")
.document(uid)
.snapshots()
.listen((l) {
_map = l.data.map(/** Map to note list.../*);
_streamController.add(UserSettings(_map));
});
}
});
}
@override
Stream<UserSettings> get stream => _streamController.stream;
@override
void storeNote(Technique technique, Note note) async {_map.removeWhere((currentNote) => note.date == currentNote.date);
techniques.add(note);
_updateFirestoreWithCache(); }@override
void removeNote(Technique technique, Note note) {
_map.removeWhere((currentNote) => note.date == currentNote.date);
_updateFirestoreWithCache();
}
void _updateFirestoreWithCache() async {
var user = await _auth.loadUser();
Firestore.instance.collection("users").document(user.uid).setData(
_map.map((k, v) => MapEntry(k, v.map((n) => n.toJson()).toList())));
}}
StoreNote will check if the item already exists (in this case, if the DateTime which is the ‘key’ already exists, then it’s an update. Note: I should use the firebase update function here — but I haven’t :))
So, I’m going to avoid doing the add / update UI etc and leave that to you. Let’s now display a list. Here’s a simple BLoC:
class NoteListBloc extends Bloc {
UserSettingsRepo _repo = Ioc().use<List<Note>>("UserSettingsRepo");
Stream<List<Note>> get stream => _repo.stream;
@override
void dispose() {
stream.drain();
}
void addNote(Note note) {
_repo.storeNote(note);
}
void removeNote(Note note) {
_repo.removeNote(technique, note);
}
}
And the List itself
class NotesList extends StatefulWidget {
final Technique technique;
const NotesList({Key key, @required this.technique}) : super(key: key);
@override
_NotesListState createState() => _NotesListState();
}
class _NotesListState extends State<NotesList> {
NoteListBloc _bloc;
@override
Widget build(BuildContext context) {
return StreamBuilder<UserSettings>(
stream: _bloc.stream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Container(
child: Text(
"You don't have any notes"
));
}
return ListView(
shrinkWrap: true,
children: snapshot.data
.map((note) => NoteTile(
note: note,
onClick: () => addNoteModal(
context, _bloc.addNote,
note: note),
))
.toList(),
);
});
}
@override
void didChangeDependencies() {
_bloc = NoteListBloc();
super.didChangeDependencies();
}
@override
void dispose() {
_bloc.dispose();
super.dispose();
}
}
Since I’m not going to show you the create / update UI — just a note on what “addNoteModal” does — it creates a Modal screen which allows you to input text. If you pass in a note, then it will be an ‘update’ whereby the text is set by default for the existing note text. As we’ve seen previously, storeNote will take care of whether or not this is an update or a save depending on the date.
The interesting bit: the ListTiles:
class NoteTile extends StatelessWidget {
final Note note;
final Function onClick;
const NoteTile({Key key, @required this.note, @required this.onClick})
: super(key: key);
@override
Widget build(BuildContext context) => Dismissible(
key: Key(note.date.toIso8601String()),
confirmDismiss: confirm,
child: ListTile(
onTap: onClick,
title: Text(
note.note
),
leading: Icon(Icons.note),
subtitle: Text(displayableDate(note.date)),
trailing:
Icon(Icons.navigate_next),
),
);
Future<bool> confirm(DismissDirection direction) {
return Future.value(false);
}
}
So I’ve added the dismissable widget here. If you run this as is, then you will see a list of your notes, and then you can swipe to dismiss, but because ‘confirm’ returns ‘false’ it will just pop back in.
Obviously what i want to do here is actually display a confirmation dialog before swiping away. And thus:
confirm becomes:
Future<bool> confirm(DismissDirection direction, BuildContext context) async {
return await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Confirm delete"),
content: const Text("Are you sure you wish to delete this note?"),
actions: <Widget>[
FlatButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text("Delete")),
FlatButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text("Cancel"),
)
],
);
});
}
also change:
confirmDismiss: (direction) async => await confirm(direction, context),
So to actually dismiss. Add this:
class NoteTile extends StatelessWidget {
final Note note;
final Function onClick;
final Function onDelete;
const NoteTile(
{Key key, @required this.note, @required this.onClick, @required this.onDelete})
: super(key: key);...
onDismissed: (_) => onDelete(),
...}
And now construct the tile like this:
NoteTile(
note: note,
onDelete: () => _bloc.removeNote(note),
onClick: () =>
addNoteModal(
context, _bloc.addNote,
note: note),
)
And there you have it, alert dialog with a delete function and a confirmation dialog.
Let me know what you think.