mirror of https://github.com/AMT-Cheif/drift.git
Update example: Can now update items
This commit is contained in:
parent
062deccb12
commit
d818942c8d
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()();
|
||||||
|
@ -53,11 +51,13 @@ class Database extends _$Database {
|
||||||
// select all categories and load how many associated entries there are for
|
// select all categories and load how many associated entries there are for
|
||||||
// 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
child: TodoCard(item),
|
CurvedAnimation(parent: animation, curve: Curves.easeOut),
|
||||||
builder: (context, child) {
|
child: TodoCard(item),
|
||||||
return Opacity(
|
builder: (context, child) {
|
||||||
opacity: animation.value,
|
return Opacity(
|
||||||
child: child,
|
opacity: animation.value,
|
||||||
);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
|
@ -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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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:
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
void insertIntoSortedList<T>(List<T> list, T entry, {int compare(T a, T b)}) {}
|
|
|
@ -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');
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in New Issue