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
class TodoEntry extends DataClass implements Insertable<TodoEntry> {
class Entry extends DataClass implements Insertable<Entry> {
final int id;
final String content;
final DateTime creationDate;
TodoEntry(
{@required this.id, @required this.content, @required this.creationDate});
factory TodoEntry.fromData(Map<String, dynamic> data, GeneratedDatabase db,
final bool done;
Entry({@required this.id, @required this.content, @required this.done});
factory Entry.fromData(Map<String, dynamic> data, GeneratedDatabase db,
{String prefix}) {
final effectivePrefix = prefix ?? '';
final intType = db.typeSystem.forDartType<int>();
final stringType = db.typeSystem.forDartType<String>();
final dateTimeType = db.typeSystem.forDartType<DateTime>();
return TodoEntry(
final boolType = db.typeSystem.forDartType<bool>();
return Entry(
id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']),
content:
stringType.mapFromDatabaseResponse(data['${effectivePrefix}content']),
creationDate: dateTimeType
.mapFromDatabaseResponse(data['${effectivePrefix}creation_date']),
done: boolType.mapFromDatabaseResponse(data['${effectivePrefix}done']),
);
}
factory TodoEntry.fromJson(Map<String, dynamic> json,
factory Entry.fromJson(Map<String, dynamic> json,
{ValueSerializer serializer = const ValueSerializer.defaults()}) {
return TodoEntry(
return Entry(
id: serializer.fromJson<int>(json['id']),
content: serializer.fromJson<String>(json['content']),
creationDate: serializer.fromJson<DateTime>(json['creationDate']),
done: serializer.fromJson<bool>(json['done']),
);
}
@override
@ -41,64 +39,61 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
return {
'id': serializer.toJson<int>(id),
'content': serializer.toJson<String>(content),
'creationDate': serializer.toJson<DateTime>(creationDate),
'done': serializer.toJson<bool>(done),
};
}
@override
T createCompanion<T extends UpdateCompanion<TodoEntry>>(bool nullToAbsent) {
T createCompanion<T extends UpdateCompanion<Entry>>(bool nullToAbsent) {
return TodoEntriesCompanion(
id: id == null && nullToAbsent ? const Value.absent() : Value(id),
content: content == null && nullToAbsent
? const Value.absent()
: Value(content),
creationDate: creationDate == null && nullToAbsent
? const Value.absent()
: Value(creationDate),
done: done == null && nullToAbsent ? const Value.absent() : Value(done),
) as T;
}
TodoEntry copyWith({int id, String content, DateTime creationDate}) =>
TodoEntry(
Entry copyWith({int id, String content, bool done}) => Entry(
id: id ?? this.id,
content: content ?? this.content,
creationDate: creationDate ?? this.creationDate,
done: done ?? this.done,
);
@override
String toString() {
return (StringBuffer('TodoEntry(')
return (StringBuffer('Entry(')
..write('id: $id, ')
..write('content: $content, ')
..write('creationDate: $creationDate')
..write('done: $done')
..write(')'))
.toString();
}
@override
int get hashCode => $mrjf($mrjc(
$mrjc($mrjc(0, id.hashCode), content.hashCode), creationDate.hashCode));
int get hashCode => $mrjf(
$mrjc($mrjc($mrjc(0, id.hashCode), content.hashCode), done.hashCode));
@override
bool operator ==(other) =>
identical(this, other) ||
(other is TodoEntry &&
(other is Entry &&
other.id == id &&
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<String> content;
final Value<DateTime> creationDate;
final Value<bool> done;
const TodoEntriesCompanion({
this.id = const Value.absent(),
this.content = const Value.absent(),
this.creationDate = const Value.absent(),
this.done = const Value.absent(),
});
}
class $TodoEntriesTable extends TodoEntries
with TableInfo<$TodoEntriesTable, TodoEntry> {
with TableInfo<$TodoEntriesTable, Entry> {
final GeneratedDatabase _db;
final String _alias;
$TodoEntriesTable(this._db, [this._alias]);
@ -122,19 +117,17 @@ class $TodoEntriesTable extends TodoEntries
);
}
final VerificationMeta _creationDateMeta =
const VerificationMeta('creationDate');
GeneratedDateTimeColumn _creationDate;
final VerificationMeta _doneMeta = const VerificationMeta('done');
GeneratedBoolColumn _done;
@override
GeneratedDateTimeColumn get creationDate =>
_creationDate ??= _constructCreationDate();
GeneratedDateTimeColumn _constructCreationDate() {
return GeneratedDateTimeColumn('creation_date', $tableName, false,
defaultValue: currentDateAndTime);
GeneratedBoolColumn get done => _done ??= _constructDone();
GeneratedBoolColumn _constructDone() {
return GeneratedBoolColumn('done', $tableName, false,
defaultValue: const Constant(true));
}
@override
List<GeneratedColumn> get $columns => [id, content, creationDate];
List<GeneratedColumn> get $columns => [id, content, done];
@override
$TodoEntriesTable get asDslTable => this;
@override
@ -156,13 +149,11 @@ class $TodoEntriesTable extends TodoEntries
} else if (content.isRequired && isInserting) {
context.missing(_contentMeta);
}
if (d.creationDate.present) {
if (d.done.present) {
context.handle(
_creationDateMeta,
creationDate.isAcceptableValue(
d.creationDate.value, _creationDateMeta));
} else if (creationDate.isRequired && isInserting) {
context.missing(_creationDateMeta);
_doneMeta, done.isAcceptableValue(d.done.value, _doneMeta));
} else if (done.isRequired && isInserting) {
context.missing(_doneMeta);
}
return context;
}
@ -170,9 +161,9 @@ class $TodoEntriesTable extends TodoEntries
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
TodoEntry map(Map<String, dynamic> data, {String tablePrefix}) {
Entry map(Map<String, dynamic> data, {String tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null;
return TodoEntry.fromData(data, _db, prefix: effectivePrefix);
return Entry.fromData(data, _db, prefix: effectivePrefix);
}
@override
@ -184,9 +175,8 @@ class $TodoEntriesTable extends TodoEntries
if (d.content.present) {
map['content'] = Variable<String, StringType>(d.content.value);
}
if (d.creationDate.present) {
map['creation_date'] =
Variable<DateTime, DateTimeType>(d.creationDate.value);
if (d.done.present) {
map['done'] = Variable<bool, BoolType>(d.done.value);
}
return map;
}
@ -197,10 +187,38 @@ class $TodoEntriesTable extends TodoEntries
}
}
class HiddenEntryCountResult {
final int entries;
HiddenEntryCountResult({
this.entries,
});
}
abstract class _$Database extends GeneratedDatabase {
_$Database(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e);
$TodoEntriesTable _todoEntries;
$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
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:
moor: ^1.4.0
flutter_web: any
dev_dependencies:
moor_generator:
@ -17,3 +18,12 @@ dependency_overrides:
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>
<html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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>
<meta charset="UTF-8">
<title></title>
<script defer src="main.dart.js" type="application/javascript"></script>
<script src="sql-wasm.js"></script>
</head>
<body>
<div id="output"></div>
<form id="add_todo_form">
<input type="text" id="description" />
<input type="submit">
</form>
</body>
</html>

View File

@ -1,18 +1,7 @@
import 'dart:html';
import 'package:moor/moor_web.dart';
import 'database.dart';
import 'package:example_web/main.dart';
import 'package:flutter_web_ui/ui.dart' as ui;
void main() async {
final db = Database(WebDatabase('database', logStatements: true));
db.watchEntries().listen(print);
(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 = '';
});
await ui.webOnlyInitializePlatform();
launchApp();
}

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),
OpeningDetails(version, databaseInfo.schemaVersion));
});
if (upgradeNeeded) {
// assume that a schema version was written in an upgrade => save db
_storeDb();
}
}
}