1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-10 23:08:59 +00:00

Add firebase support to web_dashboard (#421)

* add mock data, app state, model classes

* Set up app without ChangeNotifier

* refactor

* add experiments to experimental/

* Add project-agnostic Firebase authentication code

* add sign in button

* add stub firebase API

* add firestore

* refactor code for google_sign_in

* update pubspec.lock

* switch to mocks for non-firebase version

* Add firebase instructions to the README

* fix README

* sign in silently if the user is already signed in

* add json_serializable

* update README

* ignore 'id' field on types

* Implement FirebaseItemApi

* Add build_runner instructions to README

* remove experiments directory

* add EditItemForm

* move types.dart into api.dart

* move mock and firebase configuration into the constructor

* add main_mock entrypoint

* add copyright checks to grinder script

* fix fix-copyright task

* run grind fix-copyright

* add run and generate tasks

* add run tasks to grind script

* add fillWithMockData() fix delete() in mock API

* add edit / new form dialogs

* Add charts that display entries from Firebase

* Add Entries list without editing

* refactor home page

* format

* Add entries page functionality

* Show current day in charts

* cleanup: pubspec.lock, remove type annotation

* Remove _selectedItem from Home page

Add ItemsDropdown
Use ItemsDropdown in NewEntryDialog / NewEntryForm

* rename item-category

* don't wait to show snackbar on delete

* fix circular progress indicator

* Move dialogs into dialogs.dart

* run grind fix-copyright

* remove unused import

* Refactor entry total calculation, add chart_utils library

* fix bug in chart_utils.dart

* convert CategoryChart to a stateless widget

* use a const for number of days in chart

* code review updates

- rename stream -> subscribe
- timeStamp -> timestamp
- remove latest() from API
- use FutureBuilder and StreamBuilder instead of stateful widget
- rename variables in mock_service_test.dart

* use a single collection reference in firebase API

* remove reference to stream in mock API

* Use a new type,  _EntriesEvent to improve filtering in mock API

* add analysis_options.yaml and fix (most) issues

* fix avoid_types_on_closure_parameters lint warnings

* use spread operator in dashboard.dart

* handle case where selected item in the category dropdown goes away

* use StreamBuilder + FutureBuilder on Entries page

* rename method

* use fake firebase configuration

* update pubspec.lock

* update README

* Change categories_dropdown to FutureBuilder + StreamBuilder

* Update minSdkVersion in build.gradle

SDK version 16 was failing: "The number of method references in a .dex
file cannot exceed 64K."

* update README

* Use a collection reference in FirebaseEntryApi

Already added to FirebaseCategoryApi

* Invoke onSelected in CategoriesDropdown when necessary

Also, avoid calling onSelected during a build.

* fix misnamed var

* remove unused import

* Use relative imports

* Use extension methods for DateTime utilities

* remove forms.dart

* Make Firebase instructions specific for this sample

* add copyright headers

* fix grammar

* dartfmt

* avoid setState() during build phase in CategoryDropdown

* add empty test to material_theme_builder
This commit is contained in:
John Ryan
2020-05-26 13:14:21 -07:00
committed by GitHub
parent b518c322cc
commit 395ae8c0bb
39 changed files with 2730 additions and 220 deletions

View File

@@ -2,44 +2,106 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:json_annotation/json_annotation.dart';
part 'api.g.dart';
/// Manipulates app data,
abstract class DashboardApi {
ItemApi get items;
CategoryApi get categories;
EntryApi get entries;
}
/// Manipulates [Item] data.
abstract class ItemApi {
Future<Item> delete(String id);
Future<Item> get(String id);
Future<Item> insert(Item item);
Future<List<Item>> list();
Future<Item> update(Item item, String id);
Stream<List<Item>> allItemsStream();
}
/// Manipulates [Category] data.
abstract class CategoryApi {
Future<Category> delete(String id);
/// Something being tracked.
class Item {
final String name;
String id;
Future<Category> get(String id);
Item(this.name);
Future<Category> insert(Category category);
Future<List<Category>> list();
Future<Category> update(Category category, String id);
Stream<List<Category>> subscribe();
}
/// Manipulates [Entry] data.
abstract class EntryApi {
Future<Entry> delete(String itemId, String id);
Future<Entry> insert(String itemId, Entry entry);
Future<List<Entry>> list(String itemId);
Future<Entry> update(String itemId, String id, Entry entry);
Stream<List<Entry>> allEntriesStream(String itemId);
Future<Entry> delete(String categoryId, String id);
Future<Entry> get(String categoryId, String id);
Future<Entry> insert(String categoryId, Entry entry);
Future<List<Entry>> list(String categoryId);
Future<Entry> update(String categoryId, String id, Entry entry);
Stream<List<Entry>> subscribe(String categoryId);
}
/// Something that's being tracked, e.g. Hours Slept, Cups of water, etc.
@JsonSerializable()
class Category {
String name;
@JsonKey(ignore: true)
String id;
Category(this.name);
factory Category.fromJson(Map<String, dynamic> json) =>
_$CategoryFromJson(json);
Map<String, dynamic> toJson() => _$CategoryToJson(this);
@override
operator ==(Object other) => other is Category && other.id == id;
@override
int get hashCode => id.hashCode;
@override
String toString() {
return '<Category id=$id>';
}
}
/// A number tracked at a point in time.
@JsonSerializable()
class Entry {
final int value;
final DateTime time;
int value;
@JsonKey(fromJson: _timestampToDateTime, toJson: _dateTimeToTimestamp)
DateTime time;
@JsonKey(ignore: true)
String id;
Entry(this.value, this.time);
factory Entry.fromJson(Map<String, dynamic> json) => _$EntryFromJson(json);
Map<String, dynamic> toJson() => _$EntryToJson(this);
static DateTime _timestampToDateTime(Timestamp timestamp) {
return DateTime.fromMillisecondsSinceEpoch(
timestamp.millisecondsSinceEpoch);
}
static Timestamp _dateTimeToTimestamp(DateTime dateTime) {
return Timestamp.fromMillisecondsSinceEpoch(
dateTime.millisecondsSinceEpoch);
}
@override
operator ==(Object other) => other is Entry && other.id == id;
@override
int get hashCode => id.hashCode;
@override
String toString() {
return '<Entry id=$id>';
}
}

View File

@@ -0,0 +1,33 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'api.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Category _$CategoryFromJson(Map<String, dynamic> json) {
return Category(
json['name'] as String,
);
}
Map<String, dynamic> _$CategoryToJson(Category instance) => <String, dynamic>{
'name': instance.name,
};
Entry _$EntryFromJson(Map<String, dynamic> json) {
return Entry(
json['value'] as int,
Entry._timestampToDateTime(json['time'] as Timestamp),
);
}
Map<String, dynamic> _$EntryToJson(Entry instance) => <String, dynamic>{
'value': instance.value,
'time': Entry._dateTimeToTimestamp(instance.time),
};

View File

@@ -1,3 +1,151 @@
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'api.dart';
class FirebaseDashboardApi implements DashboardApi {
@override
final EntryApi entries;
@override
final CategoryApi categories;
FirebaseDashboardApi(Firestore firestore, String userId)
: entries = FirebaseEntryApi(firestore, userId),
categories = FirebaseCategoryApi(firestore, userId);
}
class FirebaseEntryApi implements EntryApi {
final Firestore firestore;
final String userId;
final CollectionReference _categoriesRef;
FirebaseEntryApi(this.firestore, this.userId)
: _categoriesRef = firestore.collection('users/$userId/categories');
@override
Stream<List<Entry>> subscribe(String categoryId) {
var snapshots = _categoriesRef
.document('$categoryId')
.collection('entries')
.snapshots();
var result = snapshots.map((querySnapshot) {
return querySnapshot.documents.map((snapshot) {
return Entry.fromJson(snapshot.data)..id = snapshot.documentID;
}).toList();
});
return result;
}
@override
Future<Entry> delete(String categoryId, String id) async {
var document = _categoriesRef.document('$categoryId/entries/$id');
var entry = await get(categoryId, document.documentID);
await document.delete();
return entry;
}
@override
Future<Entry> insert(String categoryId, Entry entry) async {
var document = await _categoriesRef
.document('$categoryId')
.collection('entries')
.add(entry.toJson());
return await get(categoryId, document.documentID);
}
@override
Future<List<Entry>> list(String categoryId) async {
var entriesRef =
_categoriesRef.document('$categoryId').collection('entries');
var querySnapshot = await entriesRef.getDocuments();
var entries = querySnapshot.documents
.map((doc) => Entry.fromJson(doc.data)..id = doc.documentID)
.toList();
return entries;
}
@override
Future<Entry> update(String categoryId, String id, Entry entry) async {
var document = _categoriesRef.document('$categoryId/entries/$id');
await document.setData(entry.toJson());
var snapshot = await document.get();
return Entry.fromJson(snapshot.data)..id = snapshot.documentID;
}
@override
Future<Entry> get(String categoryId, String id) async {
var document = _categoriesRef.document('$categoryId/entries/$id');
var snapshot = await document.get();
return Entry.fromJson(snapshot.data)..id = snapshot.documentID;
}
}
class FirebaseCategoryApi implements CategoryApi {
final Firestore firestore;
final String userId;
final CollectionReference _categoriesRef;
FirebaseCategoryApi(this.firestore, this.userId)
: _categoriesRef = firestore.collection('users/$userId/categories');
@override
Stream<List<Category>> subscribe() {
var snapshots = _categoriesRef.snapshots();
var result = snapshots.map((querySnapshot) {
return querySnapshot.documents.map((snapshot) {
return Category.fromJson(snapshot.data)..id = snapshot.documentID;
}).toList();
});
return result;
}
@override
Future<Category> delete(String id) async {
var document = _categoriesRef.document('$id');
var categories = await get(document.documentID);
await document.delete();
return categories;
}
@override
Future<Category> get(String id) async {
var document = _categoriesRef.document('$id');
var snapshot = await document.get();
return Category.fromJson(snapshot.data)..id = snapshot.documentID;
}
@override
Future<Category> insert(Category category) async {
var document = await _categoriesRef.add(category.toJson());
return await get(document.documentID);
}
@override
Future<List<Category>> list() async {
var querySnapshot = await _categoriesRef.getDocuments();
var categories = querySnapshot.documents
.map((doc) => Category.fromJson(doc.data)..id = doc.documentID)
.toList();
return categories;
}
@override
Future<Category> update(Category category, String id) async {
var document = _categoriesRef.document('$id');
await document.setData(category.toJson());
var snapshot = await document.get();
return Category.fromJson(snapshot.data)..id = snapshot.documentID;
}
}

View File

@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:math';
import 'package:uuid/uuid.dart' as uuid;
@@ -13,48 +14,67 @@ class MockDashboardApi implements DashboardApi {
final EntryApi entries = MockEntryApi();
@override
final ItemApi items = MockItemApi();
final CategoryApi categories = MockCategoryApi();
MockDashboardApi();
/// Creates a [MockDashboardApi] filled with mock data for the last 30 days.
Future<void> fillWithMockData() async {
await Future<void>.delayed(Duration(seconds: 1));
var category1 = await categories.insert(Category('Coffee (oz)'));
var category2 = await categories.insert(Category('Running (miles)'));
var category3 = await categories.insert(Category('Git Commits'));
var monthAgo = DateTime.now().subtract(Duration(days: 30));
for (var category in [category1, category2, category3]) {
for (var i = 0; i < 30; i++) {
var date = monthAgo.add(Duration(days: i));
var value = Random().nextInt(6) + 1;
await entries.insert(category.id, Entry(value, date));
}
}
}
}
class MockItemApi implements ItemApi {
Map<String, Item> _storage = {};
StreamController<List<Item>> _streamController =
StreamController<List<Item>>.broadcast();
class MockCategoryApi implements CategoryApi {
Map<String, Category> _storage = {};
StreamController<List<Category>> _streamController =
StreamController<List<Category>>.broadcast();
@override
Future<Item> delete(String id) async {
Future<Category> delete(String id) async {
var removed = _storage.remove(id);
_emit();
return _storage.remove(id);
return removed;
}
@override
Future<Item> get(String id) async {
Future<Category> get(String id) async {
return _storage[id];
}
@override
Future<Item> insert(Item item) async {
Future<Category> insert(Category category) async {
var id = uuid.Uuid().v4();
var newItem = Item(item.name)..id = id;
_storage[id] = newItem;
var newCategory = Category(category.name)..id = id;
_storage[id] = newCategory;
_emit();
return newItem;
return newCategory;
}
@override
Future<List<Item>> list() async {
Future<List<Category>> list() async {
return _storage.values.toList();
}
@override
Future<Item> update(Item item, String id) async {
_storage[id] = item;
return item..id = id;
Future<Category> update(Category category, String id) async {
_storage[id] = category;
_emit();
return category..id = id;
}
Stream<List<Item>> allItemsStream() {
return _streamController.stream;
}
Stream<List<Category>> subscribe() => _streamController.stream;
void _emit() {
_streamController.add(_storage.values.toList());
@@ -63,44 +83,64 @@ class MockItemApi implements ItemApi {
class MockEntryApi implements EntryApi {
Map<String, Entry> _storage = {};
StreamController<List<Entry>> _streamController =
StreamController<List<Entry>>.broadcast();
StreamController<_EntriesEvent> _streamController =
StreamController.broadcast();
@override
Future<Entry> delete(String itemId, String id) async {
_emit();
return _storage.remove('$itemId-$id');
Future<Entry> delete(String categoryId, String id) async {
_emit(categoryId);
return _storage.remove('$categoryId-$id');
}
@override
Future<Entry> insert(String itemId, Entry entry) async {
Future<Entry> insert(String categoryId, Entry entry) async {
var id = uuid.Uuid().v4();
var newEntry = Entry(entry.value, entry.time)..id = id;
_storage['$itemId-$id'] = newEntry;
_emit();
_storage['$categoryId-$id'] = newEntry;
_emit(categoryId);
return newEntry;
}
@override
Future<List<Entry>> list(String itemId) async {
Future<List<Entry>> list(String categoryId) async {
return _storage.keys
.where((k) => k.startsWith(itemId))
.where((k) => k.startsWith(categoryId))
.map((k) => _storage[k])
.toList();
}
@override
Future<Entry> update(String itemId, String id, Entry entry) async {
_storage['$itemId-$id'] = entry;
Future<Entry> update(String categoryId, String id, Entry entry) async {
_storage['$categoryId-$id'] = entry;
_emit(categoryId);
return entry..id = id;
}
@override
Stream<List<Entry>> allEntriesStream(String itemId) {
return _streamController.stream;
Stream<List<Entry>> subscribe(String categoryId) {
return _streamController.stream
.where((event) => event.categoryId == categoryId)
.map((event) => event.entries);
}
void _emit() {
_streamController.add(_storage.values.toList());
void _emit(String categoryId) {
var entries = _storage.keys
.where((k) => k.startsWith(categoryId))
.map((k) => _storage[k])
.toList();
_streamController.add(_EntriesEvent(categoryId, entries));
}
@override
Future<Entry> get(String categoryId, String id) async {
return _storage['$categoryId-$id'];
}
}
class _EntriesEvent {
final String categoryId;
final List<Entry> entries;
_EntriesEvent(this.categoryId, this.entries);
}