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

@@ -0,0 +1,108 @@
// 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 'dart:async';
import 'package:flutter/material.dart';
import '../api/api.dart';
/// Subscribes to the latest list of categories and allows the user to select
/// one.
class CategoryDropdown extends StatefulWidget {
final CategoryApi api;
final ValueChanged<Category> onSelected;
CategoryDropdown({
@required this.api,
@required this.onSelected,
});
@override
_CategoryDropdownState createState() => _CategoryDropdownState();
}
class _CategoryDropdownState extends State<CategoryDropdown> {
Category _selected;
Future<List<Category>> _future;
Stream<List<Category>> _stream;
void initState() {
super.initState();
// This widget needs to wait for the list of Categories, select the first
// Category, and emit an `onSelected` event.
//
// This could be done inside the FutureBuilder's `builder` callback,
// but calling setState() during the build is an error. (Calling the
// onSelected callback will also cause the parent widget to call
// setState()).
//
// Instead, we'll create a new Future that sets the selected Category and
// calls `onSelected` if necessary. Then, we'll pass *that* future to
// FutureBuilder. Now the selected category is set and events are emitted
// *before* the build is triggered by the FutureBuilder.
_future = widget.api.list().then((categories) {
if (categories.isEmpty) {
return categories;
}
_setSelected(categories.first);
return categories;
});
// Same here, we'll create a new stream that handles any potential
// setState() operations before we trigger our StreamBuilder.
_stream = widget.api.subscribe().map((categories) {
if (!categories.contains(_selected) && categories.isNotEmpty) {
_setSelected(categories.first);
}
return categories;
});
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<Category>>(
future: _future,
builder: (context, futureSnapshot) {
// Show an empty dropdown while the data is loading.
if (!futureSnapshot.hasData) {
return DropdownButton<Category>(items: [], onChanged: null);
}
return StreamBuilder<List<Category>>(
initialData: futureSnapshot.hasData ? futureSnapshot.data : [],
stream: _stream,
builder: (context, snapshot) {
var data = snapshot.hasData ? snapshot.data : <Category>[];
return DropdownButton<Category>(
value: _selected,
items: data.map(_buildDropdownItem).toList(),
onChanged: (category) {
_setSelected(category);
},
);
},
);
},
);
}
void _setSelected(Category category) {
if (_selected == category) {
return;
}
setState(() {
_selected = category;
});
widget.onSelected(_selected);
}
DropdownMenuItem<Category> _buildDropdownItem(Category category) {
return DropdownMenuItem<Category>(
child: Text(category.name), value: category);
}
}

View File

@@ -0,0 +1,111 @@
// 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:flutter/material.dart';
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:intl/intl.dart' as intl;
import '../api/api.dart';
import '../utils/chart_utils.dart' as utils;
import 'dialogs.dart';
// The number of days to show in the chart
const _daysBefore = 10;
class CategoryChart extends StatelessWidget {
final Category category;
final DashboardApi api;
CategoryChart({
@required this.category,
@required this.api,
});
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(category.name),
IconButton(
icon: Icon(Icons.settings),
onPressed: () {
showDialog<EditCategoryDialog>(
context: context,
builder: (context) {
return EditCategoryDialog(category: category);
},
);
},
),
],
),
),
Expanded(
// Load the initial snapshot using a FutureBuilder, and subscribe to
// additional updates with a StreamBuilder.
child: FutureBuilder<List<Entry>>(
future: api.entries.list(category.id),
builder: (context, futureSnapshot) {
if (!futureSnapshot.hasData) {
return _buildLoadingIndicator();
}
return StreamBuilder<List<Entry>>(
initialData: futureSnapshot.data,
stream: api.entries.subscribe(category.id),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return _buildLoadingIndicator();
}
return _BarChart(entries: snapshot.data);
},
);
},
),
),
],
);
}
Widget _buildLoadingIndicator() {
return Center(child: CircularProgressIndicator());
}
}
class _BarChart extends StatelessWidget {
final List<Entry> entries;
_BarChart({this.entries});
@override
Widget build(BuildContext context) {
return charts.BarChart(
[_seriesData()],
animate: false,
);
}
charts.Series<utils.EntryTotal, String> _seriesData() {
return charts.Series<utils.EntryTotal, String>(
id: 'Entries',
colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
domainFn: (entryTotal, _) {
if (entryTotal == null) return null;
var format = intl.DateFormat.Md();
return format.format(entryTotal.day);
},
measureFn: (total, _) {
if (total == null) return null;
return total.value;
},
data: utils.entryTotalsByDay(entries, _daysBefore),
);
}
}

View File

@@ -0,0 +1,103 @@
// 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:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:web_dashboard/src/api/api.dart';
import 'package:web_dashboard/src/app.dart';
class NewCategoryForm extends StatefulWidget {
@override
_NewCategoryFormState createState() => _NewCategoryFormState();
}
class _NewCategoryFormState extends State<NewCategoryForm> {
Category _category = Category('');
@override
Widget build(BuildContext context) {
var api = Provider.of<AppState>(context).api;
return EditCategoryForm(
category: _category,
onDone: (shouldInsert) {
if (shouldInsert) {
api.categories.insert(_category);
}
Navigator.of(context).pop();
},
);
}
}
class EditCategoryForm extends StatefulWidget {
final Category category;
final ValueChanged<bool> onDone;
EditCategoryForm({
@required this.category,
@required this.onDone,
});
@override
_EditCategoryFormState createState() => _EditCategoryFormState();
}
class _EditCategoryFormState extends State<EditCategoryForm> {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
initialValue: widget.category.name,
decoration: InputDecoration(
labelText: 'Name',
),
onChanged: (newValue) {
widget.category.name = newValue;
},
validator: (value) {
if (value.isEmpty) {
return 'Please enter a name';
}
return null;
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
child: RaisedButton(
child: Text('Cancel'),
onPressed: () {
widget.onDone(false);
},
),
),
Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
child: RaisedButton(
child: Text('OK'),
onPressed: () {
if (_formKey.currentState.validate()) {
widget.onDone(true);
}
},
),
),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,98 @@
// 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:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:web_dashboard/src/api/api.dart';
import 'package:web_dashboard/src/widgets/category_forms.dart';
import '../app.dart';
import 'edit_entry.dart';
class NewCategoryDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SimpleDialog(
title: Text('New Category'),
children: <Widget>[
NewCategoryForm(),
],
);
}
}
class EditCategoryDialog extends StatelessWidget {
final Category category;
EditCategoryDialog({
@required this.category,
});
@override
Widget build(BuildContext context) {
var api = Provider.of<AppState>(context).api;
return SimpleDialog(
title: Text('Edit Category'),
children: [
EditCategoryForm(
category: category,
onDone: (shouldUpdate) {
if (shouldUpdate) {
api.categories.update(category, category.id);
}
Navigator.of(context).pop();
},
),
],
);
}
}
class NewEntryDialog extends StatefulWidget {
@override
_NewEntryDialogState createState() => _NewEntryDialogState();
}
class _NewEntryDialogState extends State<NewEntryDialog> {
@override
Widget build(BuildContext context) {
return SimpleDialog(
title: Text('New Entry'),
children: [
NewEntryForm(),
],
);
}
}
class EditEntryDialog extends StatelessWidget {
final Category category;
final Entry entry;
EditEntryDialog({
this.category,
this.entry,
});
@override
Widget build(BuildContext context) {
var api = Provider.of<AppState>(context).api;
return SimpleDialog(
title: Text('Edit Entry'),
children: [
EditEntryForm(
entry: entry,
onDone: (shouldUpdate) {
if (shouldUpdate) {
api.entries.update(category.id, entry.id, entry);
}
Navigator.of(context).pop();
},
)
],
);
}
}

View File

@@ -0,0 +1,154 @@
// 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:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:web_dashboard/src/api/api.dart';
import 'package:intl/intl.dart' as intl;
import '../app.dart';
import 'categories_dropdown.dart';
class NewEntryForm extends StatefulWidget {
@override
_NewEntryFormState createState() => _NewEntryFormState();
}
class _NewEntryFormState extends State<NewEntryForm> {
Category _selected;
Entry _entry = Entry(0, DateTime.now());
@override
Widget build(BuildContext context) {
var api = Provider.of<AppState>(context).api;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: CategoryDropdown(
api: api.categories,
onSelected: (category) {
setState(() {
_selected = category;
});
},
),
),
EditEntryForm(
entry: _entry,
onDone: (shouldInsert) {
if (shouldInsert) {
api.entries.insert(_selected.id, _entry);
}
Navigator.of(context).pop();
},
),
],
);
}
}
class EditEntryForm extends StatefulWidget {
final Entry entry;
final ValueChanged<bool> onDone;
EditEntryForm({
@required this.entry,
@required this.onDone,
});
@override
_EditEntryFormState createState() => _EditEntryFormState();
}
class _EditEntryFormState extends State<EditEntryForm> {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(8),
child: TextFormField(
initialValue: widget.entry.value.toString(),
decoration: InputDecoration(labelText: 'Value'),
keyboardType: TextInputType.number,
validator: (value) {
try {
int.parse(value);
} catch (e) {
return "Please enter a whole number";
}
return null;
},
onChanged: (newValue) {
try {
widget.entry.value = int.parse(newValue);
} on FormatException {
print('Entry cannot contain "$newValue". Expected a number');
}
},
),
),
Padding(
padding: EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(intl.DateFormat('MM/dd/yyyy').format(widget.entry.time)),
RaisedButton(
child: Text('Edit'),
onPressed: () async {
var result = await showDatePicker(
context: context,
initialDate: widget.entry.time,
firstDate: DateTime.now().subtract(Duration(days: 365)),
lastDate: DateTime.now());
if (result == null) {
return;
}
setState(() {
widget.entry.time = result;
});
},
)
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
child: RaisedButton(
child: Text('Cancel'),
onPressed: () {
widget.onDone(false);
},
),
),
Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
child: RaisedButton(
child: Text('OK'),
onPressed: () {
if (_formKey.currentState.validate()) {
widget.onDone(true);
}
},
),
),
],
)
],
),
);
}
}