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:
67
experimental/web_dashboard/lib/src/pages/dashboard.dart
Normal file
67
experimental/web_dashboard/lib/src/pages/dashboard.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
161
experimental/web_dashboard/lib/src/pages/entries.dart
Normal file
161
experimental/web_dashboard/lib/src/pages/entries.dart
Normal 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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
46
experimental/web_dashboard/lib/src/pages/sign_in.dart
Normal file
46
experimental/web_dashboard/lib/src/pages/sign_in.dart
Normal 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');
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user