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:
@@ -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>';
|
||||
}
|
||||
}
|
||||
|
||||
33
experimental/web_dashboard/lib/src/api/api.g.dart
Normal file
33
experimental/web_dashboard/lib/src/api/api.g.dart
Normal 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),
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user