Update example: Can now update items

This commit is contained in:
Simon Binder 2019-03-03 22:03:11 +01:00
parent 062deccb12
commit d818942c8d
No known key found for this signature in database
GPG Key ID: B807FDF954BA00CF
19 changed files with 243 additions and 81 deletions

View File

@ -265,6 +265,8 @@ Please note that a workaround for most on this list exists with custom statement
These aren't sorted by priority. If you have more ideas or want some features happening soon, These aren't sorted by priority. If you have more ideas or want some features happening soon,
let us know by creating an issue! let us know by creating an issue!
- Specify primary keys - Specify primary keys
- Support an simplified update that doesn't need an explicit where based on the primary key
- Data classes: Generate a `copyWith` method.
- Simple `COUNT(*)` operations (group operations will be much more complicated) - Simple `COUNT(*)` operations (group operations will be much more complicated)
- Support default values and expressions - Support default values and expressions
- Allow using DAOs or some other mechanism instead of having to put everything in the main - Allow using DAOs or some other mechanism instead of having to put everything in the main

View File

@ -48,8 +48,10 @@ class EditAction {
/// number of addition and removal operations between the two lists. It has /// number of addition and removal operations between the two lists. It has
/// O(N + D^2) time performance, where D is the minimum amount of edits needed /// O(N + D^2) time performance, where D is the minimum amount of edits needed
/// to turn a into b. /// to turn a into b.
List<EditAction> diff<T>(List<T> a, List<T> b) { List<EditAction> diff<T>(List<T> a, List<T> b,
final snakes = impl.calculateDiff(impl.DiffInput(a, b)); {bool Function(T a, T b) equals}) {
final defaultEquals = equals ?? (T a, T b) => a == b;
final snakes = impl.calculateDiff(impl.DiffInput<T>(a, b, defaultEquals));
final actions = <EditAction>[]; final actions = <EditAction>[];
var posOld = a.length; var posOld = a.length;

View File

@ -11,9 +11,8 @@ import 'package:sally/src/runtime/statements/update.dart';
/// This comes in handy to structure large amounts of database code better: The /// This comes in handy to structure large amounts of database code better: The
/// migration logic can live in the main [GeneratedDatabase] class, but code /// migration logic can live in the main [GeneratedDatabase] class, but code
/// can be extracted into [DatabaseAccessor]s outside of that database. /// can be extracted into [DatabaseAccessor]s outside of that database.
abstract class DatabaseAccessor<T extends GeneratedDatabase> extends DatabaseConnectionUser abstract class DatabaseAccessor<T extends GeneratedDatabase>
with QueryEngine { extends DatabaseConnectionUser with QueryEngine {
@protected @protected
final T db; final T db;
@ -154,7 +153,9 @@ abstract class GeneratedDatabase extends DatabaseConnectionUser
GeneratedDatabase(SqlTypeSystem types, QueryExecutor executor, GeneratedDatabase(SqlTypeSystem types, QueryExecutor executor,
{StreamQueryStore streamStore}) {StreamQueryStore streamStore})
: super(types, executor, streamQueries: streamStore); : super(types, executor, streamQueries: streamStore) {
executor.databaseInfo = this;
}
/// Creates a migrator with the provided query executor. We sometimes can't /// Creates a migrator with the provided query executor. We sometimes can't
/// use the regular [GeneratedDatabase.executor] because migration happens /// use the regular [GeneratedDatabase.executor] because migration happens

View File

@ -7,23 +7,18 @@ class UpdateStatement<T, D> extends Query<T, D> {
UpdateStatement(QueryEngine database, TableInfo<T, D> table) UpdateStatement(QueryEngine database, TableInfo<T, D> table)
: super(database, table); : super(database, table);
/// The object to update. The non-null fields of this object will be written Map<String, dynamic> _updatedFields;
/// into the rows matched by [whereExpr] and [limitExpr].
D _updateReference;
@override @override
void writeStartPart(GenerationContext ctx) { void writeStartPart(GenerationContext ctx) {
// TODO support the OR (ROLLBACK / ABORT / REPLACE / FAIL / IGNORE...) thing // TODO support the OR (ROLLBACK / ABORT / REPLACE / FAIL / IGNORE...) thing
final map = table.entityToSql(_updateReference)
..remove((_, value) => value == null);
ctx.buffer.write('UPDATE ${table.$tableName} SET '); ctx.buffer.write('UPDATE ${table.$tableName} SET ');
var first = true; var first = true;
map.forEach((columnName, variable) { _updatedFields.forEach((columnName, variable) {
if (!first) { if (!first) {
ctx.writeWhitespace(); ctx.buffer.write(', ');
} else { } else {
first = false; first = false;
} }
@ -39,12 +34,19 @@ class UpdateStatement<T, D> extends Query<T, D> {
/// means that, when you're not setting a where or limit expression /// means that, when you're not setting a where or limit expression
/// explicitly, this method will update all rows in the specific table. /// explicitly, this method will update all rows in the specific table.
Future<int> write(D entity) async { Future<int> write(D entity) async {
_updateReference = entity; if (!table.validateIntegrity(entity, false)) {
if (!table.validateIntegrity(_updateReference, false)) {
throw InvalidDataException( throw InvalidDataException(
'Invalid data: $entity cannot be written into ${table.$tableName}'); 'Invalid data: $entity cannot be written into ${table.$tableName}');
} }
_updatedFields = table.entityToSql(entity)
..remove((_, value) => value == null);
if (_updatedFields.isEmpty) {
// nothing to update, we're done
return Future.value(0);
}
final ctx = constructQuery(); final ctx = constructQuery();
final rows = await ctx.database.executor.doWhenOpened((e) async { final rows = await ctx.database.executor.doWhenOpened((e) async {
return await e.runUpdate(ctx.sql, ctx.boundVariables); return await e.runUpdate(ctx.sql, ctx.boundVariables);

View File

@ -38,10 +38,13 @@ class Range {
class DiffInput<T> { class DiffInput<T> {
final List<T> from; final List<T> from;
final List<T> to; final List<T> to;
final bool Function(T a, T b) equals;
DiffInput(this.from, this.to); DiffInput(this.from, this.to, this.equals);
bool areItemsTheSame(int fromPos, int toPos) => from[fromPos] == to[toPos]; bool areItemsTheSame(int fromPos, int toPos) {
return equals(from[fromPos], to[toPos]);
}
} }
List<Snake> calculateDiff(DiffInput input) { List<Snake> calculateDiff(DiffInput input) {

View File

@ -265,6 +265,8 @@ Please note that a workaround for most on this list exists with custom statement
These aren't sorted by priority. If you have more ideas or want some features happening soon, These aren't sorted by priority. If you have more ideas or want some features happening soon,
let us know by creating an issue! let us know by creating an issue!
- Specify primary keys - Specify primary keys
- Support an simplified update that doesn't need an explicit where based on the primary key
- Data classes: Generate a `copyWith` method.
- Simple `COUNT(*)` operations (group operations will be much more complicated) - Simple `COUNT(*)` operations (group operations will be much more complicated)
- Support default values and expressions - Support default values and expressions
- Allow using DAOs or some other mechanism instead of having to put everything in the main - Allow using DAOs or some other mechanism instead of having to put everything in the main

View File

@ -0,0 +1,14 @@
import 'dart:async';
import 'package:sally_example/database/database.dart';
class TodoAppBloc {
final Database db;
Stream<List<TodoEntry>> get allEntries => db.allEntries();
TodoAppBloc() : db = Database();
void addEntry(TodoEntry entry) {
db.addEntry(entry);
}
}

View File

@ -8,8 +8,6 @@ part 'database.g.dart';
class Todos extends Table { class Todos extends Table {
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 4, max: 16).nullable()();
TextColumn get content => text()(); TextColumn get content => text()();
DateTimeColumn get targetDate => dateTime().nullable()(); DateTimeColumn get targetDate => dateTime().nullable()();
@ -54,10 +52,12 @@ class Database extends _$Database {
// each category // each category
return customSelectStream( return customSelectStream(
'SELECT *, (SELECT COUNT(*) FROM todos WHERE category = c.id) AS "amount" FROM categories c;', 'SELECT *, (SELECT COUNT(*) FROM todos WHERE category = c.id) AS "amount" FROM categories c;',
readsFrom: {todos, categories}).map((rows) { readsFrom: {todos, categories})
.map((rows) {
// when we have the result set, map each row to the data class // when we have the result set, map each row to the data class
return rows return rows
.map((row) => CategoryWithCount(Category.fromData(row.data, this), row.readInt('amount'))) .map((row) => CategoryWithCount(
Category.fromData(row.data, this), row.readInt('amount')))
.toList(); .toList();
}); });
} }
@ -73,4 +73,14 @@ class Database extends _$Database {
Future deleteEntry(TodoEntry entry) { Future deleteEntry(TodoEntry entry) {
return (delete(todos)..where((t) => t.id.equals(entry.id))).go(); return (delete(todos)..where((t) => t.id.equals(entry.id))).go();
} }
Future updateContent(int id, String content) {
return (update(todos)..where((t) => t.id.equals(id)))
.write(TodoEntry(content: content));
}
Future updateDate(int id, DateTime dueDate) {
return (update(todos)..where((t) => t.id.equals(id)))
.write(TodoEntry(targetDate: dueDate));
}
} }

View File

@ -8,19 +8,16 @@ part of 'database.dart';
class TodoEntry { class TodoEntry {
final int id; final int id;
final String title;
final String content; final String content;
final DateTime targetDate; final DateTime targetDate;
final int category; final int category;
TodoEntry( TodoEntry({this.id, this.content, this.targetDate, this.category});
{this.id, this.title, this.content, this.targetDate, this.category});
factory TodoEntry.fromData(Map<String, dynamic> data, GeneratedDatabase db) { factory TodoEntry.fromData(Map<String, dynamic> data, GeneratedDatabase db) {
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 dateTimeType = db.typeSystem.forDartType<DateTime>();
return TodoEntry( return TodoEntry(
id: intType.mapFromDatabaseResponse(data['id']), id: intType.mapFromDatabaseResponse(data['id']),
title: stringType.mapFromDatabaseResponse(data['title']),
content: stringType.mapFromDatabaseResponse(data['content']), content: stringType.mapFromDatabaseResponse(data['content']),
targetDate: dateTimeType.mapFromDatabaseResponse(data['target_date']), targetDate: dateTimeType.mapFromDatabaseResponse(data['target_date']),
category: intType.mapFromDatabaseResponse(data['category']), category: intType.mapFromDatabaseResponse(data['category']),
@ -28,8 +25,7 @@ class TodoEntry {
} }
@override @override
int get hashCode => int get hashCode =>
((((id.hashCode) * 31 + title.hashCode) * 31 + content.hashCode) * 31 + (((id.hashCode) * 31 + content.hashCode) * 31 + targetDate.hashCode) *
targetDate.hashCode) *
31 + 31 +
category.hashCode; category.hashCode;
@override @override
@ -37,7 +33,6 @@ class TodoEntry {
identical(this, other) || identical(this, other) ||
(other is TodoEntry && (other is TodoEntry &&
other.id == id && other.id == id &&
other.title == title &&
other.content == content && other.content == content &&
other.targetDate == targetDate && other.targetDate == targetDate &&
other.category == category); other.category == category);
@ -50,11 +45,6 @@ class $TodosTable extends Todos implements TableInfo<Todos, TodoEntry> {
GeneratedIntColumn get id => GeneratedIntColumn get id =>
GeneratedIntColumn('id', false, hasAutoIncrement: true); GeneratedIntColumn('id', false, hasAutoIncrement: true);
@override @override
GeneratedTextColumn get title => GeneratedTextColumn(
'title',
true,
);
@override
GeneratedTextColumn get content => GeneratedTextColumn( GeneratedTextColumn get content => GeneratedTextColumn(
'content', 'content',
false, false,
@ -70,8 +60,7 @@ class $TodosTable extends Todos implements TableInfo<Todos, TodoEntry> {
true, true,
); );
@override @override
List<GeneratedColumn> get $columns => List<GeneratedColumn> get $columns => [id, content, targetDate, category];
[id, title, content, targetDate, category];
@override @override
Todos get asDslTable => this; Todos get asDslTable => this;
@override @override
@ -79,7 +68,6 @@ class $TodosTable extends Todos implements TableInfo<Todos, TodoEntry> {
@override @override
bool validateIntegrity(TodoEntry instance, bool isInserting) => bool validateIntegrity(TodoEntry instance, bool isInserting) =>
id.isAcceptableValue(instance.id, isInserting) && id.isAcceptableValue(instance.id, isInserting) &&
title.isAcceptableValue(instance.title, isInserting) &&
content.isAcceptableValue(instance.content, isInserting) && content.isAcceptableValue(instance.content, isInserting) &&
targetDate.isAcceptableValue(instance.targetDate, isInserting) && targetDate.isAcceptableValue(instance.targetDate, isInserting) &&
category.isAcceptableValue(instance.category, isInserting); category.isAcceptableValue(instance.category, isInserting);
@ -96,9 +84,6 @@ class $TodosTable extends Todos implements TableInfo<Todos, TodoEntry> {
if (d.id != null) { if (d.id != null) {
map['id'] = Variable<int, IntType>(d.id); map['id'] = Variable<int, IntType>(d.id);
} }
if (d.title != null) {
map['title'] = Variable<String, StringType>(d.title);
}
if (d.content != null) { if (d.content != null) {
map['content'] = Variable<String, StringType>(d.content); map['content'] = Variable<String, StringType>(d.content);
} }

View File

@ -9,8 +9,12 @@ part 'todos_dao.g.dart';
class TodosDao extends DatabaseAccessor<Database> with _TodosDaoMixin { class TodosDao extends DatabaseAccessor<Database> with _TodosDaoMixin {
TodosDao(Database db) : super(db); TodosDao(Database db) : super(db);
Stream<List<TodoEntry>> todosWithoutCategory() { Stream<List<TodoEntry>> todosInCategory(Category category) {
return null; if (category == null) {
return (select(todos)..where((t) => isNull(t.category))).watch();
} else {
return (select(todos)..where((t) => t.category.equals(category.id)))
.watch();
}
} }
} }

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sally_example/database/database.dart'; import 'package:sally_example/bloc.dart';
import 'widgets/homescreen.dart'; import 'widgets/homescreen.dart';
void main() => runApp(MyApp()); void main() => runApp(MyApp());
@ -14,18 +14,18 @@ class MyApp extends StatefulWidget {
// We use this widget to set up the material app and provide an InheritedWidget that // We use this widget to set up the material app and provide an InheritedWidget that
// the rest of this simple app can then use to access the database // the rest of this simple app can then use to access the database
class MyAppState extends State<MyApp> { class MyAppState extends State<MyApp> {
Database _db; TodoAppBloc bloc;
@override @override
void initState() { void initState() {
_db = Database(); bloc = TodoAppBloc();
super.initState(); super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DatabaseProvider( return BlocProvider(
db: _db, bloc: bloc,
child: MaterialApp( child: MaterialApp(
title: 'Sally Demo', title: 'Sally Demo',
theme: ThemeData( theme: ThemeData(
@ -37,17 +37,16 @@ class MyAppState extends State<MyApp> {
} }
} }
class DatabaseProvider extends InheritedWidget { class BlocProvider extends InheritedWidget {
final Database db; final TodoAppBloc bloc;
DatabaseProvider({@required this.db, Widget child}) : super(child: child); BlocProvider({@required this.bloc, Widget child}) : super(child: child);
@override @override
bool updateShouldNotify(DatabaseProvider oldWidget) { bool updateShouldNotify(BlocProvider oldWidget) {
return oldWidget.db != db; return oldWidget.bloc != bloc;
} }
static Database provideDb(BuildContext ctx) => static TodoAppBloc provideBloc(BuildContext ctx) =>
(ctx.inheritFromWidgetOfExactType(DatabaseProvider) as DatabaseProvider) (ctx.inheritFromWidgetOfExactType(BlocProvider) as BlocProvider).bloc;
.db;
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sally_example/bloc.dart';
import 'package:sally_example/database/database.dart'; import 'package:sally_example/database/database.dart';
import 'package:sally_example/main.dart'; import 'package:sally_example/main.dart';
import 'package:sally_example/widgets/todo_card.dart'; import 'package:sally_example/widgets/todo_card.dart';
@ -13,12 +14,12 @@ class HomeScreen extends StatefulWidget {
} }
} }
/// Shows a list of todo entries and displays a text input to add another one /// Shows a list of todos and displays a text input to add another one
class HomeScreenState extends State<HomeScreen> { class HomeScreenState extends State<HomeScreen> {
// we only use this to reset the input field at the bottom when a entry has been added // we only use this to reset the input field at the bottom when a entry has been added
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
Database get db => DatabaseProvider.provideDb(context); TodoAppBloc get bloc => BlocProvider.provideBloc(context);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -29,9 +30,14 @@ class HomeScreenState extends State<HomeScreen> {
// A SallyAnimatedList automatically animates incoming and leaving items, we only // A SallyAnimatedList automatically animates incoming and leaving items, we only
// have to tell it what data to display and how to turn data into widgets. // have to tell it what data to display and how to turn data into widgets.
body: SallyAnimatedList<TodoEntry>( body: SallyAnimatedList<TodoEntry>(
stream: db.allEntries(), // we want to show an updating stream of all todo entries stream: bloc
.allEntries, // we want to show an updating stream of all entries
// consider items equal if their id matches. Otherwise, we'd get an
// animation of an old item leaving and another one coming in every time
// the content of an item changed!
equals: (a, b) => a.id == b.id,
itemBuilder: (ctx, item, animation) { itemBuilder: (ctx, item, animation) {
// When a new item arrives, it will expand verticallly // When a new item arrives, it will expand vertically
return SizeTransition( return SizeTransition(
key: ObjectKey(item.id), key: ObjectKey(item.id),
sizeFactor: animation, sizeFactor: animation,
@ -46,15 +52,15 @@ class HomeScreenState extends State<HomeScreen> {
sizeFactor: animation, sizeFactor: animation,
axis: Axis.vertical, axis: Axis.vertical,
child: AnimatedBuilder( child: AnimatedBuilder(
animation: CurvedAnimation(parent: animation, curve: Curves.easeOut), animation:
CurvedAnimation(parent: animation, curve: Curves.easeOut),
child: TodoCard(item), child: TodoCard(item),
builder: (context, child) { builder: (context, child) {
return Opacity( return Opacity(
opacity: animation.value, opacity: animation.value,
child: child, child: child,
); );
} }),
),
); );
}, },
), ),
@ -95,7 +101,7 @@ class HomeScreenState extends State<HomeScreen> {
if (controller.text.isNotEmpty) { if (controller.text.isNotEmpty) {
// We write the entry here. Notice how we don't have to call setState() // We write the entry here. Notice how we don't have to call setState()
// or anything - sally will take care of updating the list automatically. // or anything - sally will take care of updating the list automatically.
db.addEntry(TodoEntry(content: controller.text)); bloc.addEntry(TodoEntry(content: controller.text));
controller.clear(); controller.clear();
} }
} }

View File

@ -1,8 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sally_example/database/database.dart'; import 'package:sally_example/database/database.dart';
import 'package:sally_example/main.dart'; import 'package:sally_example/main.dart';
import 'package:sally_example/widgets/todo_edit_dialog.dart';
import 'package:intl/intl.dart';
/// Card that displays a todo entry and an icon button to delete that entry final DateFormat _format = DateFormat.yMMMd();
/// Card that displays an entry and an icon button to delete that entry
class TodoCard extends StatelessWidget { class TodoCard extends StatelessWidget {
final TodoEntry entry; final TodoEntry entry;
@ -10,20 +14,70 @@ class TodoCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget dueDate;
if (entry.targetDate == null) {
dueDate = const Text(
'No due date set',
style: TextStyle(color: Colors.grey, fontSize: 12),
);
} else {
dueDate = Text(
_format.format(entry.targetDate),
style: const TextStyle(fontSize: 12),
);
}
return Card( return Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Row( child: Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: <Widget>[ children: <Widget>[
Expanded(child: Text(entry.content)), Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(entry.content),
dueDate,
],
),
),
IconButton(
icon: const Icon(Icons.calendar_today),
color: Colors.green,
onPressed: () async {
final dateTime = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2019),
lastDate: DateTime(3038));
await BlocProvider.provideBloc(context)
.db
.updateDate(entry.id, dateTime);
},
),
IconButton(
icon: const Icon(Icons.edit),
color: Colors.blue,
onPressed: () {
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => TodoEditDialog(
entry: entry,
),
);
},
),
IconButton( IconButton(
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
color: Colors.red, color: Colors.red,
onPressed: () { onPressed: () {
// We delete the entry here. Again, notice how we don't have to call setState() or // We delete the entry here. Again, notice how we don't have to call setState() or
// inform the parent widget. The animated list will take care of this automatically. // inform the parent widget. The animated list will take care of this automatically.
DatabaseProvider.provideDb(context).deleteEntry(entry); BlocProvider.provideBloc(context).db.deleteEntry(entry);
}, },
) )
], ],

View File

@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:sally_example/database/database.dart';
import 'package:sally_example/main.dart';
class TodoEditDialog extends StatefulWidget {
final TodoEntry entry;
const TodoEditDialog({Key key, this.entry}) : super(key: key);
@override
_TodoEditDialogState createState() => _TodoEditDialogState();
}
class _TodoEditDialogState extends State<TodoEditDialog> {
final TextEditingController textController = TextEditingController();
@override
void initState() {
textController.text = widget.entry.content;
super.initState();
}
@override
void dispose() {
textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Edit entry'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: textController,
decoration: InputDecoration(
hintText: 'What needs to be done?',
helperText: 'Content of entry',
),
),
],
),
actions: [
FlatButton(
child: const Text('Cancel'),
textColor: Colors.red,
onPressed: () {
Navigator.pop(context);
},
),
FlatButton(
child: const Text('Save'),
onPressed: () {
final entry = widget.entry;
if (textController.text.isNotEmpty) {
BlocProvider.provideBloc(context)
.db
.updateContent(entry.id, textController.text);
}
Navigator.pop(context);
},
),
],
);
}
}

View File

@ -9,6 +9,7 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
intl:
cupertino_icons: ^0.1.2 cupertino_icons: ^0.1.2
rxdart: 0.20.0 rxdart: 0.20.0
sally_flutter: sally_flutter:

View File

@ -14,10 +14,17 @@ class SallyAnimatedList<T> extends StatefulWidget {
final ItemBuilder<T> itemBuilder; final ItemBuilder<T> itemBuilder;
final RemovedItemBuilder<T> removedItemBuilder; final RemovedItemBuilder<T> removedItemBuilder;
/// A function that decides whether two items are considered equal. By
/// default, `a == b` will be used. A customization is useful if the content
/// of items can change (e.g. when a title changes, you'd only want to change
/// one text and not let the item disappear to show up again).
final bool Function(T a, T b) equals;
SallyAnimatedList( SallyAnimatedList(
{@required this.stream, {@required this.stream,
@required this.itemBuilder, @required this.itemBuilder,
@required this.removedItemBuilder}); @required this.removedItemBuilder,
this.equals});
@override @override
_SallyAnimatedListState<T> createState() { _SallyAnimatedListState<T> createState() {
@ -56,7 +63,7 @@ class _SallyAnimatedListState<T> extends State<SallyAnimatedList<T>> {
listState.insertItem(i); listState.insertItem(i);
} }
} else { } else {
final editScript = diff(_lastSnapshot, data); final editScript = diff(_lastSnapshot, data, equals: widget.equals);
for (var action in editScript) { for (var action in editScript) {
if (action.isDelete) { if (action.isDelete) {

View File

@ -1 +0,0 @@
void insertIntoSortedList<T>(List<T> list, T entry, {int compare(T a, T b)}) {}

View File

@ -45,7 +45,8 @@ class DaoGenerator extends GeneratorForAnnotation<UseDao> {
'DatabaseAccessor<${dbImpl.displayName}> {\n'); 'DatabaseAccessor<${dbImpl.displayName}> {\n');
for (var table in tableTypes) { for (var table in tableTypes) {
final infoType = tableInfoNameForTableClass(table.element as ClassElement); final infoType =
tableInfoNameForTableClass(table.element as ClassElement);
final getterName = ReCase(table.name).camelCase; final getterName = ReCase(table.name).camelCase;
buffer.write('$infoType get $getterName => db.$getterName;\n'); buffer.write('$infoType get $getterName => db.$getterName;\n');

View File

@ -18,4 +18,5 @@ class SpecifiedTable {
{this.fromClass, this.columns, this.sqlName, this.dartTypeName}); {this.fromClass, this.columns, this.sqlName, this.dartTypeName});
} }
String tableInfoNameForTableClass(ClassElement fromClass) => '\$${fromClass.name}Table'; String tableInfoNameForTableClass(ClassElement fromClass) =>
'\$${fromClass.name}Table';