1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-13 00:08:24 +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,67 @@
// 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 '../api/api.dart';
import '../app.dart';
import '../widgets/category_chart.dart';
class DashboardPage extends StatelessWidget {
Widget build(BuildContext context) {
var appState = Provider.of<AppState>(context);
return FutureBuilder<List<Category>>(
future: appState.api.categories.list(),
builder: (context, futureSnapshot) {
if (!futureSnapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
return StreamBuilder<List<Category>>(
initialData: futureSnapshot.data,
stream: appState.api.categories.subscribe(),
builder: (context, snapshot) {
if (snapshot.data == null) {
return Center(
child: CircularProgressIndicator(),
);
}
return Dashboard(snapshot.data);
},
);
},
);
}
}
class Dashboard extends StatelessWidget {
final List<Category> categories;
Dashboard(this.categories);
@override
Widget build(BuildContext context) {
var api = Provider.of<AppState>(context).api;
return Scrollbar(
child: GridView(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
childAspectRatio: 2,
maxCrossAxisExtent: 500,
),
children: [
...categories.map(
(category) => Card(
child: CategoryChart(
category: category,
api: api,
),
),
)
],
),
);
}
}

View File

@@ -0,0 +1,161 @@
// 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:intl/intl.dart' as intl;
import '../api/api.dart';
import '../app.dart';
import '../widgets/categories_dropdown.dart';
import '../widgets/dialogs.dart';
class EntriesPage extends StatefulWidget {
@override
_EntriesPageState createState() => _EntriesPageState();
}
class _EntriesPageState extends State<EntriesPage> {
Category _selected;
@override
Widget build(BuildContext context) {
var appState = Provider.of<AppState>(context);
return Column(
children: [
CategoryDropdown(
api: appState.api.categories,
onSelected: (category) => setState(() => _selected = category)),
Expanded(
child: _selected == null
? Center(child: CircularProgressIndicator())
: EntriesList(
category: _selected,
api: appState.api.entries,
),
),
],
);
}
}
class EntriesList extends StatefulWidget {
final Category category;
final EntryApi api;
EntriesList({
@required this.category,
@required this.api,
}) : super(key: ValueKey(category.id));
@override
_EntriesListState createState() => _EntriesListState();
}
class _EntriesListState extends State<EntriesList> {
@override
Widget build(BuildContext context) {
if (widget.category == null) {
return _buildLoadingIndicator();
}
return FutureBuilder<List<Entry>>(
future: widget.api.list(widget.category.id),
builder: (context, futureSnapshot) {
if (!futureSnapshot.hasData) {
return _buildLoadingIndicator();
}
return StreamBuilder<List<Entry>>(
initialData: futureSnapshot.data,
stream: widget.api.subscribe(widget.category.id),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return _buildLoadingIndicator();
}
return ListView.builder(
itemBuilder: (context, index) {
return EntryTile(
category: widget.category,
entry: snapshot.data[index],
);
},
itemCount: snapshot.data.length,
);
},
);
},
);
}
Widget _buildLoadingIndicator() {
return Center(child: CircularProgressIndicator());
}
}
class EntryTile extends StatelessWidget {
final Category category;
final Entry entry;
EntryTile({
this.category,
this.entry,
});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(entry.value.toString()),
subtitle: Text(intl.DateFormat('MM/dd/yy h:mm a').format(entry.time)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
FlatButton(
child: Text('Edit'),
onPressed: () {
showDialog<void>(
context: context,
builder: (context) {
return EditEntryDialog(category: category, entry: entry);
},
);
},
),
FlatButton(
child: Text('Delete'),
onPressed: () async {
var shouldDelete = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Delete entry?'),
actions: [
FlatButton(
child: Text('Cancel'),
onPressed: () => Navigator.of(context).pop(false),
),
FlatButton(
child: Text('Delete'),
onPressed: () => Navigator.of(context).pop(true),
),
],
),
);
if (shouldDelete) {
await Provider.of<AppState>(context, listen: false)
.api
.entries
.delete(category.id, entry.id);
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Entry deleted'),
),
);
}
},
),
],
),
);
}
}

View File

@@ -3,47 +3,79 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../api/api.dart';
import 'item_details.dart';
import '../widgets/dialogs.dart';
import '../widgets/third_party/adaptive_scaffold.dart';
import 'dashboard.dart';
import 'entries.dart';
class HomePage extends StatelessWidget {
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _pageIndex = 0;
@override
Widget build(BuildContext context) {
var api = Provider.of<DashboardApi>(context);
return Scaffold(
body: StreamProvider<List<Item>>(
initialData: [],
create: (context) => api.items.allItemsStream(),
child: Consumer<List<Item>>(
builder: (context, items, child) {
return ListView.builder(
itemBuilder: (context, idx) {
return ListTile(
title: Text(items[idx].name),
onTap: () {
_showDetails(items[idx], context);
},
);
},
itemCount: items.length,
);
},
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
api.items.insert(Item('Coffees Drank'));
},
),
return AdaptiveScaffold(
currentIndex: _pageIndex,
destinations: [
AdaptiveScaffoldDestination(title: 'Home', icon: Icons.home),
AdaptiveScaffoldDestination(title: 'Entries', icon: Icons.list),
AdaptiveScaffoldDestination(title: 'Settings', icon: Icons.settings),
],
body: _pageAtIndex(_pageIndex),
onNavigationIndexChange: (newIndex) {
setState(() {
_pageIndex = newIndex;
});
},
floatingActionButton:
_hasFloatingActionButton ? _buildFab(context) : null,
);
}
void _showDetails(Item item, BuildContext context) {
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return ItemDetailsPage(item);
}));
bool get _hasFloatingActionButton {
if (_pageIndex == 2) return false;
return true;
}
FloatingActionButton _buildFab(BuildContext context) {
return FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => _handleFabPressed(),
);
}
void _handleFabPressed() {
if (_pageIndex == 0) {
showDialog<NewCategoryDialog>(
context: context,
builder: (context) => NewCategoryDialog(),
);
return;
}
if (_pageIndex == 1) {
showDialog<NewEntryDialog>(
context: context,
builder: (context) => NewEntryDialog(),
);
return;
}
}
Widget _pageAtIndex(int index) {
if (index == 0) {
return DashboardPage();
}
if (index == 1) {
return EntriesPage();
}
return Center(child: Text('Settings page'));
}
}

View File

@@ -1,22 +0,0 @@
// 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:web_dashboard/src/api/api.dart';
class ItemDetailsPage extends StatelessWidget {
final Item item;
ItemDetailsPage(this.item);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text('${item.name}'),
),
);
}
}

View File

@@ -0,0 +1,46 @@
// 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 '../auth/auth.dart';
class SignInPage extends StatefulWidget {
final Auth auth;
final ValueChanged<User> onSuccess;
SignInPage({
@required this.auth,
@required this.onSuccess,
});
@override
_SignInPageState createState() => _SignInPageState();
}
class _SignInPageState extends State<SignInPage> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text('Sign In'),
onPressed: () async {
var user = await widget.auth.signIn();
if (user != null) {
widget.onSuccess(user);
} else {
throw ('Unable to sign in');
}
},
),
),
);
}
}