Create a real example for the web backend

This commit is contained in:
Simon Binder 2019-07-05 22:00:02 +02:00
parent c806a4a7b1
commit 311a47c704
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
14 changed files with 350 additions and 129 deletions

View File

@ -0,0 +1,11 @@
# See https://github.com/dart-lang/build/tree/master/build_web_compilers#configuration
targets:
$default:
builders:
build_web_compilers|entrypoint:
generate_for:
- web/**.dart
options:
dart2js_args:
- --no-source-maps
- -O4

View File

@ -0,0 +1,47 @@
import 'package:moor/moor_web.dart';
part 'database.g.dart';
const int _doneEntriesCount = 20;
@DataClassName('Entry')
class TodoEntries extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get content => text()();
BoolColumn get done => boolean().withDefault(const Constant(false))();
}
@UseMoor(tables: [
TodoEntries
], queries: {
'hiddenEntryCount': 'SELECT COUNT(*) - $_doneEntriesCount AS entries '
'FROM todo_entries WHERE done'
})
class Database extends _$Database {
Database() : super(WebDatabase('app', logStatements: true));
@override
final int schemaVersion = 1;
Stream<List<Entry>> incompleteEntries() {
return (select(todoEntries)..where((e) => not(e.done))).watch();
}
Stream<List<Entry>> newestDoneEntries() {
return (select(todoEntries)
..where((e) => e.done)
..orderBy(
[(e) => OrderingTerm(expression: e.id, mode: OrderingMode.desc)])
..limit(_doneEntriesCount))
.watch();
}
Future createTodoEntry(String desc) {
return into(todoEntries).insert(TodoEntriesCompanion(content: Value(desc)));
}
Future setCompleted(Entry entry, bool done) {
return update(todoEntries).write(entry.copyWith(done: done));
}
}

View File

@ -7,32 +7,30 @@ part of 'database.dart';
// ************************************************************************** // **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps // ignore_for_file: unnecessary_brace_in_string_interps
class TodoEntry extends DataClass implements Insertable<TodoEntry> { class Entry extends DataClass implements Insertable<Entry> {
final int id; final int id;
final String content; final String content;
final DateTime creationDate; final bool done;
TodoEntry( Entry({@required this.id, @required this.content, @required this.done});
{@required this.id, @required this.content, @required this.creationDate}); factory Entry.fromData(Map<String, dynamic> data, GeneratedDatabase db,
factory TodoEntry.fromData(Map<String, dynamic> data, GeneratedDatabase db,
{String prefix}) { {String prefix}) {
final effectivePrefix = prefix ?? ''; final effectivePrefix = prefix ?? '';
final intType = db.typeSystem.forDartType<int>(); final intType = db.typeSystem.forDartType<int>();
final stringType = db.typeSystem.forDartType<String>(); final stringType = db.typeSystem.forDartType<String>();
final dateTimeType = db.typeSystem.forDartType<DateTime>(); final boolType = db.typeSystem.forDartType<bool>();
return TodoEntry( return Entry(
id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']), id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']),
content: content:
stringType.mapFromDatabaseResponse(data['${effectivePrefix}content']), stringType.mapFromDatabaseResponse(data['${effectivePrefix}content']),
creationDate: dateTimeType done: boolType.mapFromDatabaseResponse(data['${effectivePrefix}done']),
.mapFromDatabaseResponse(data['${effectivePrefix}creation_date']),
); );
} }
factory TodoEntry.fromJson(Map<String, dynamic> json, factory Entry.fromJson(Map<String, dynamic> json,
{ValueSerializer serializer = const ValueSerializer.defaults()}) { {ValueSerializer serializer = const ValueSerializer.defaults()}) {
return TodoEntry( return Entry(
id: serializer.fromJson<int>(json['id']), id: serializer.fromJson<int>(json['id']),
content: serializer.fromJson<String>(json['content']), content: serializer.fromJson<String>(json['content']),
creationDate: serializer.fromJson<DateTime>(json['creationDate']), done: serializer.fromJson<bool>(json['done']),
); );
} }
@override @override
@ -41,64 +39,61 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
return { return {
'id': serializer.toJson<int>(id), 'id': serializer.toJson<int>(id),
'content': serializer.toJson<String>(content), 'content': serializer.toJson<String>(content),
'creationDate': serializer.toJson<DateTime>(creationDate), 'done': serializer.toJson<bool>(done),
}; };
} }
@override @override
T createCompanion<T extends UpdateCompanion<TodoEntry>>(bool nullToAbsent) { T createCompanion<T extends UpdateCompanion<Entry>>(bool nullToAbsent) {
return TodoEntriesCompanion( return TodoEntriesCompanion(
id: id == null && nullToAbsent ? const Value.absent() : Value(id), id: id == null && nullToAbsent ? const Value.absent() : Value(id),
content: content == null && nullToAbsent content: content == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(content), : Value(content),
creationDate: creationDate == null && nullToAbsent done: done == null && nullToAbsent ? const Value.absent() : Value(done),
? const Value.absent()
: Value(creationDate),
) as T; ) as T;
} }
TodoEntry copyWith({int id, String content, DateTime creationDate}) => Entry copyWith({int id, String content, bool done}) => Entry(
TodoEntry(
id: id ?? this.id, id: id ?? this.id,
content: content ?? this.content, content: content ?? this.content,
creationDate: creationDate ?? this.creationDate, done: done ?? this.done,
); );
@override @override
String toString() { String toString() {
return (StringBuffer('TodoEntry(') return (StringBuffer('Entry(')
..write('id: $id, ') ..write('id: $id, ')
..write('content: $content, ') ..write('content: $content, ')
..write('creationDate: $creationDate') ..write('done: $done')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@override @override
int get hashCode => $mrjf($mrjc( int get hashCode => $mrjf(
$mrjc($mrjc(0, id.hashCode), content.hashCode), creationDate.hashCode)); $mrjc($mrjc($mrjc(0, id.hashCode), content.hashCode), done.hashCode));
@override @override
bool operator ==(other) => bool operator ==(other) =>
identical(this, other) || identical(this, other) ||
(other is TodoEntry && (other is Entry &&
other.id == id && other.id == id &&
other.content == content && other.content == content &&
other.creationDate == creationDate); other.done == done);
} }
class TodoEntriesCompanion extends UpdateCompanion<TodoEntry> { class TodoEntriesCompanion extends UpdateCompanion<Entry> {
final Value<int> id; final Value<int> id;
final Value<String> content; final Value<String> content;
final Value<DateTime> creationDate; final Value<bool> done;
const TodoEntriesCompanion({ const TodoEntriesCompanion({
this.id = const Value.absent(), this.id = const Value.absent(),
this.content = const Value.absent(), this.content = const Value.absent(),
this.creationDate = const Value.absent(), this.done = const Value.absent(),
}); });
} }
class $TodoEntriesTable extends TodoEntries class $TodoEntriesTable extends TodoEntries
with TableInfo<$TodoEntriesTable, TodoEntry> { with TableInfo<$TodoEntriesTable, Entry> {
final GeneratedDatabase _db; final GeneratedDatabase _db;
final String _alias; final String _alias;
$TodoEntriesTable(this._db, [this._alias]); $TodoEntriesTable(this._db, [this._alias]);
@ -122,19 +117,17 @@ class $TodoEntriesTable extends TodoEntries
); );
} }
final VerificationMeta _creationDateMeta = final VerificationMeta _doneMeta = const VerificationMeta('done');
const VerificationMeta('creationDate'); GeneratedBoolColumn _done;
GeneratedDateTimeColumn _creationDate;
@override @override
GeneratedDateTimeColumn get creationDate => GeneratedBoolColumn get done => _done ??= _constructDone();
_creationDate ??= _constructCreationDate(); GeneratedBoolColumn _constructDone() {
GeneratedDateTimeColumn _constructCreationDate() { return GeneratedBoolColumn('done', $tableName, false,
return GeneratedDateTimeColumn('creation_date', $tableName, false, defaultValue: const Constant(true));
defaultValue: currentDateAndTime);
} }
@override @override
List<GeneratedColumn> get $columns => [id, content, creationDate]; List<GeneratedColumn> get $columns => [id, content, done];
@override @override
$TodoEntriesTable get asDslTable => this; $TodoEntriesTable get asDslTable => this;
@override @override
@ -156,13 +149,11 @@ class $TodoEntriesTable extends TodoEntries
} else if (content.isRequired && isInserting) { } else if (content.isRequired && isInserting) {
context.missing(_contentMeta); context.missing(_contentMeta);
} }
if (d.creationDate.present) { if (d.done.present) {
context.handle( context.handle(
_creationDateMeta, _doneMeta, done.isAcceptableValue(d.done.value, _doneMeta));
creationDate.isAcceptableValue( } else if (done.isRequired && isInserting) {
d.creationDate.value, _creationDateMeta)); context.missing(_doneMeta);
} else if (creationDate.isRequired && isInserting) {
context.missing(_creationDateMeta);
} }
return context; return context;
} }
@ -170,9 +161,9 @@ class $TodoEntriesTable extends TodoEntries
@override @override
Set<GeneratedColumn> get $primaryKey => {id}; Set<GeneratedColumn> get $primaryKey => {id};
@override @override
TodoEntry map(Map<String, dynamic> data, {String tablePrefix}) { Entry map(Map<String, dynamic> data, {String tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null;
return TodoEntry.fromData(data, _db, prefix: effectivePrefix); return Entry.fromData(data, _db, prefix: effectivePrefix);
} }
@override @override
@ -184,9 +175,8 @@ class $TodoEntriesTable extends TodoEntries
if (d.content.present) { if (d.content.present) {
map['content'] = Variable<String, StringType>(d.content.value); map['content'] = Variable<String, StringType>(d.content.value);
} }
if (d.creationDate.present) { if (d.done.present) {
map['creation_date'] = map['done'] = Variable<bool, BoolType>(d.done.value);
Variable<DateTime, DateTimeType>(d.creationDate.value);
} }
return map; return map;
} }
@ -197,10 +187,38 @@ class $TodoEntriesTable extends TodoEntries
} }
} }
class HiddenEntryCountResult {
final int entries;
HiddenEntryCountResult({
this.entries,
});
}
abstract class _$Database extends GeneratedDatabase { abstract class _$Database extends GeneratedDatabase {
_$Database(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e); _$Database(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e);
$TodoEntriesTable _todoEntries; $TodoEntriesTable _todoEntries;
$TodoEntriesTable get todoEntries => _todoEntries ??= $TodoEntriesTable(this); $TodoEntriesTable get todoEntries => _todoEntries ??= $TodoEntriesTable(this);
HiddenEntryCountResult _rowToHiddenEntryCountResult(QueryRow row) {
return HiddenEntryCountResult(
entries: row.readInt('entries'),
);
}
Future<List<HiddenEntryCountResult>> hiddenEntryCount(
{QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
'SELECT COUNT(*) - 20 AS entries FROM todo_entries WHERE done',
variables: []).then((rows) => rows.map(_rowToHiddenEntryCountResult).toList());
}
Stream<List<HiddenEntryCountResult>> watchHiddenEntryCount() {
return customSelectStream(
'SELECT COUNT(*) - 20 AS entries FROM todo_entries WHERE done',
variables: [],
readsFrom: {todoEntries})
.map((rows) => rows.map(_rowToHiddenEntryCountResult).toList());
}
@override @override
List<TableInfo> get allTables => [todoEntries]; List<TableInfo> get allTables => [todoEntries];
} }

View File

@ -0,0 +1,32 @@
import 'package:example_web/widgets/home_screen.dart';
import 'package:flutter_web/material.dart';
import 'database/database.dart';
void launchApp() {
runApp(
DatabaseProvider(
db: Database(),
child: MaterialApp(
title: 'Moor web!',
home: HomeScreen(),
),
),
);
}
class DatabaseProvider extends InheritedWidget {
final Database db;
DatabaseProvider({@required this.db, @required Widget child})
: super(child: child);
@override
bool updateShouldNotify(InheritedWidget oldWidget) => false;
static Database provide(BuildContext ctx) {
return (ctx.inheritFromWidgetOfExactType(DatabaseProvider)
as DatabaseProvider)
?.db;
}
}

View File

@ -0,0 +1,43 @@
import 'package:flutter_web/material.dart';
import '../main.dart';
class CreateEntryBar extends StatefulWidget {
@override
_CreateEntryBarState createState() => _CreateEntryBarState();
}
class _CreateEntryBarState extends State<CreateEntryBar> {
final TextEditingController _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(
border: OutlineInputBorder(),
),
),
),
IconButton(
icon: Icon(Icons.add),
onPressed: () {
final text = _controller.text;
_controller.clear();
DatabaseProvider.provide(context).createTodoEntry(text);
},
),
],
);
}
}

View File

@ -0,0 +1,59 @@
import 'package:example_web/database/database.dart';
import 'package:flutter_web/material.dart';
import '../main.dart';
class SliverEntryList extends StatelessWidget {
final Stream<List<Entry>> entries;
const SliverEntryList({Key key, this.entries}) : super(key: key);
@override
Widget build(BuildContext context) {
return StreamBuilder<List<Entry>>(
stream: entries,
builder: (context, snapshot) {
final entries = snapshot.data ?? const [];
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final entry = entries[index];
return _EntryCard(
key: ObjectKey(entry.id),
entry: entry,
);
},
childCount: entries.length,
),
);
},
);
}
}
class _EntryCard extends StatelessWidget {
final Entry entry;
const _EntryCard({Key key, this.entry}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Text(entry.content),
Spacer(),
Checkbox(
value: entry.done,
onChanged: (checked) {
DatabaseProvider.provide(context).setCompleted(entry, checked);
},
),
],
),
);
}
}

View File

@ -0,0 +1,55 @@
import 'package:example_web/main.dart';
import 'package:example_web/widgets/entry_list.dart';
import 'package:flutter_web/material.dart';
import 'create_entry_bar.dart';
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final db = DatabaseProvider.provide(context);
final headerTheme = Theme.of(context).textTheme.title;
return Scaffold(
appBar: AppBar(
title: const Text('Moor Web'),
),
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 800),
child: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: CreateEntryBar(),
),
SliverToBoxAdapter(
child: Text('In progress', style: headerTheme),
),
SliverEntryList(
entries: db.incompleteEntries(),
),
SliverToBoxAdapter(
child: Text('Complete', style: headerTheme),
),
SliverEntryList(
entries: db.newestDoneEntries(),
),
SliverToBoxAdapter(
child: StreamBuilder<List<int>>(
builder: (context, snapshot) {
final hiddenCount = snapshot?.data?.single ?? 0;
return Text('Not showing $hiddenCount older entries');
},
),
),
],
),
),
),
),
);
}
}

View File

@ -6,6 +6,7 @@ environment:
dependencies: dependencies:
moor: ^1.4.0 moor: ^1.4.0
flutter_web: any
dev_dependencies: dev_dependencies:
moor_generator: moor_generator:
@ -17,3 +18,12 @@ dependency_overrides:
path: ../ path: ../
moor_generator: moor_generator:
path: ../../moor_generator path: ../../moor_generator
flutter_web_ui:
git:
url: https://github.com/flutter/flutter_web.git
path: packages/flutter_web_ui
flutter_web:
git:
url: https://github.com/flutter/flutter_web.git
path: packages/flutter_web

View File

@ -0,0 +1,10 @@
[
{
"family": "MaterialIcons",
"fonts": [
{
"asset": "https://fonts.gstatic.com/s/materialicons/v42/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2"
}
]
}
]

View File

@ -1,28 +0,0 @@
import 'package:moor/moor.dart';
part 'database.g.dart';
@DataClassName('TodoEntry')
class TodoEntries extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get content => text()();
DateTimeColumn get creationDate =>
dateTime().withDefault(currentDateAndTime)();
}
@UseMoor(tables: [TodoEntries])
class Database extends _$Database {
Database(QueryExecutor e) : super(e);
@override
int get schemaVersion => 1;
Stream<List<TodoEntry>> watchEntries() {
return select(todoEntries).watch();
}
Future<int> insert(String text) {
return into(todoEntries).insert(TodoEntriesCompanion(content: Value(text)));
}
}

View File

@ -1,27 +1,11 @@
<!DOCTYPE html> <!doctype html>
<html lang="en">
<html>
<head> <head>
<meta charset="utf-8"> <meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <title></title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <script defer src="main.dart.js" type="application/javascript"></script>
<meta name="scaffolded-by" content="https://github.com/google/stagehand">
<title>example_web</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" href="favicon.ico">
<script defer src="main.dart.js"></script>
<script src="sql-wasm.js"></script> <script src="sql-wasm.js"></script>
</head> </head>
<body> <body>
<div id="output"></div>
<form id="add_todo_form">
<input type="text" id="description" />
<input type="submit">
</form>
</body> </body>
</html> </html>

View File

@ -1,18 +1,7 @@
import 'dart:html'; import 'package:example_web/main.dart';
import 'package:flutter_web_ui/ui.dart' as ui;
import 'package:moor/moor_web.dart';
import 'database.dart';
void main() async { void main() async {
final db = Database(WebDatabase('database', logStatements: true)); await ui.webOnlyInitializePlatform();
db.watchEntries().listen(print); launchApp();
(querySelector('#add_todo_form') as FormElement).onSubmit.listen((e) {
final content = querySelector('#description') as InputElement;
e.preventDefault();
db.insert(content.value).then((insertId) => print('inserted #$insertId'));
content.value = '';
});
} }

View File

@ -1,14 +0,0 @@
@import url(https://fonts.googleapis.com/css?family=Roboto);
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
font-family: 'Roboto', sans-serif;
}
#output {
padding: 20px;
text-align: center;
}

View File

@ -223,6 +223,11 @@ class WebDatabase extends _DatabaseUser {
return databaseInfo.beforeOpenCallback(_BeforeOpenExecutor(_state), return databaseInfo.beforeOpenCallback(_BeforeOpenExecutor(_state),
OpeningDetails(version, databaseInfo.schemaVersion)); OpeningDetails(version, databaseInfo.schemaVersion));
}); });
if (upgradeNeeded) {
// assume that a schema version was written in an upgrade => save db
_storeDb();
}
} }
} }