mirror of https://github.com/AMT-Cheif/drift.git
Implement diff algo, still struggling with the list though
This commit is contained in:
parent
9e1566186c
commit
11d6f5f9fe
|
@ -0,0 +1,77 @@
|
|||
/// A utility library to find an edit script that turns a list into another.
|
||||
/// This is useful when displaying a updating stream of immutable lists in a
|
||||
/// list that can be updated.
|
||||
library diff_util;
|
||||
|
||||
import 'package:sally/src/utils/android_diffutils_port.dart' as impl;
|
||||
|
||||
class EditAction {
|
||||
|
||||
/// The index of the first list on which this action should be applied. If
|
||||
/// this action [isDelete], that index and the next [amount] indices should be
|
||||
/// deleted. Otherwise, this index should be moved back by [amount] and
|
||||
/// entries from the second list (starting at [indexFromOther]) should be
|
||||
/// inserted into the gap.
|
||||
final int index;
|
||||
/// The amount of entries affected by this action
|
||||
final int amount;
|
||||
/// If this action [isInsert], this is the first index from the second list
|
||||
/// from where the items should be taken from.
|
||||
final int indexFromOther;
|
||||
|
||||
/// Whether this action should delete entries from the first list
|
||||
bool get isDelete => indexFromOther == null;
|
||||
/// Whether this action should insert entries into the first list
|
||||
bool get isInsert => indexFromOther != null;
|
||||
|
||||
EditAction(this.index, this.amount, this.indexFromOther);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (isDelete) {
|
||||
return 'EditAction: Delete $amount entries from the first list, starting '
|
||||
'at index $index';
|
||||
} else {
|
||||
return 'EditAction: Insert $amount entries into the first list, taking '
|
||||
'them from the second list starting at $indexFromOther. The entries '
|
||||
'should be written starting at index $index';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Finds the shortest edit script that turns list [a] into list [b].
|
||||
/// The implementation is ported from androids DiffUtil, which in turn
|
||||
/// implements a variation of Eugene W. Myer's difference algorithm. The
|
||||
/// algorithm is optimized for space and uses O(n) space to find the minimal
|
||||
/// 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
|
||||
/// to turn a into b.
|
||||
List<EditAction> diff<T>(List<T> a, List<T> b) {
|
||||
final snakes = impl.calculateDiff(impl.DiffInput(a, b));
|
||||
final actions = <EditAction>[];
|
||||
|
||||
var posOld = a.length;
|
||||
var posNew = b.length;
|
||||
for (var snake in snakes.reversed) {
|
||||
final snakeSize = snake.size;
|
||||
final endX = snake.x + snakeSize;
|
||||
final endY = snake.y + snakeSize;
|
||||
|
||||
if (endX < posOld) {
|
||||
// starting (including) with index endX, delete posOld - endX chars from a
|
||||
actions.add(EditAction(endX, posOld - endX, null));
|
||||
}
|
||||
if (endY < posNew) {
|
||||
// starting with index endX, insert posNex - endY characters into a. The
|
||||
// characters should be taken from b, starting (including) at the index
|
||||
// endY. The char that was at index endX should be pushed back.
|
||||
actions.add(EditAction(endX, posNew - endY, endY));
|
||||
}
|
||||
|
||||
posOld = snake.x;
|
||||
posNew = snake.y;
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
|
@ -76,12 +76,14 @@ abstract class GeneratedDatabase {
|
|||
|
||||
/// Starts an [InsertStatement] for a given table. You can use that statement
|
||||
/// to write data into the [table] by using [InsertStatement.insert].
|
||||
@protected @visibleForTesting
|
||||
InsertStatement<T> into<T>(TableInfo<dynamic, T> table) =>
|
||||
InsertStatement<T>(this, table);
|
||||
|
||||
/// Starts an [UpdateStatement] for the given table. You can use that
|
||||
/// statement to update individual rows in that table by setting a where
|
||||
/// clause on that table and then use [UpdateStatement.write].
|
||||
@protected @visibleForTesting
|
||||
UpdateStatement<Tbl, ReturnType> update<Tbl, ReturnType>(
|
||||
TableInfo<Tbl, ReturnType> table) =>
|
||||
UpdateStatement(this, table);
|
||||
|
@ -89,12 +91,14 @@ abstract class GeneratedDatabase {
|
|||
/// Starts a query on the given table. Queries can be limited with an limit
|
||||
/// or a where clause and can either return a current snapshot or a continuous
|
||||
/// stream of data
|
||||
@protected @visibleForTesting
|
||||
SelectStatement<Table, ReturnType> select<Table, ReturnType>(
|
||||
TableInfo<Table, ReturnType> table) {
|
||||
return SelectStatement<Table, ReturnType>(this, table);
|
||||
}
|
||||
|
||||
/// Starts a [DeleteStatement] that can be used to delete rows from a table.
|
||||
@protected @visibleForTesting
|
||||
DeleteStatement<Table> delete<Table>(TableInfo<Table, dynamic> table) =>
|
||||
DeleteStatement<Table>(this, table);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import 'package:sally/sally.dart';
|
|||
class StreamQueryStore {
|
||||
final List<_QueryStream> _activeStreams = [];
|
||||
|
||||
// todo cache streams (return same instance for same sql + variables)
|
||||
|
||||
StreamQueryStore();
|
||||
|
||||
/// Creates a new stream from the select statement.
|
||||
|
|
|
@ -0,0 +1,243 @@
|
|||
// ignore_for_file: cascade_invocations
|
||||
|
||||
/*
|
||||
This implementation is copied from the DiffUtil class of the android support
|
||||
library, available at https://chromium.googlesource.com/android_tools/+/refs/heads/master/sdk/sources/android-25/android/support/v7/util/DiffUtil.java
|
||||
It has the following license:
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class Snake {
|
||||
int x;
|
||||
int y;
|
||||
int size;
|
||||
bool removal;
|
||||
bool reverse;
|
||||
}
|
||||
|
||||
class Range {
|
||||
int oldListStart, oldListEnd;
|
||||
int newListStart, newListEnd;
|
||||
|
||||
Range.nullFields();
|
||||
Range(this.oldListStart, this.oldListEnd, this.newListStart, this.newListEnd);
|
||||
}
|
||||
|
||||
class DiffInput<T> {
|
||||
final List<T> from;
|
||||
final List<T> to;
|
||||
|
||||
DiffInput(this.from, this.to);
|
||||
|
||||
bool areItemsTheSame(int fromPos, int toPos) => from[fromPos] == to[toPos];
|
||||
}
|
||||
|
||||
List<Snake> calculateDiff(DiffInput input) {
|
||||
final oldSize = input.from.length;
|
||||
final newSize = input.to.length;
|
||||
|
||||
final snakes = <Snake>[];
|
||||
final stack = <Range>[];
|
||||
|
||||
stack.add(Range(0, oldSize, 0, newSize));
|
||||
|
||||
final max = oldSize + newSize + (oldSize - newSize).abs();
|
||||
|
||||
final forward = List<int>(max * 2);
|
||||
final backward = List<int>(max * 2);
|
||||
|
||||
final rangePool = <Range>[];
|
||||
|
||||
while (stack.isNotEmpty) {
|
||||
final range = stack.removeLast();
|
||||
final snake = _diffPartial(input, range.oldListStart, range.oldListEnd,
|
||||
range.newListStart, range.newListEnd, forward, backward, max);
|
||||
|
||||
if (snake != null) {
|
||||
if (snake.size > 0) {
|
||||
snakes.add(snake);
|
||||
}
|
||||
|
||||
// offset the snake to convert its coordinates from the Range's are to
|
||||
// global
|
||||
snake.x += range.oldListStart;
|
||||
snake.y += range.newListStart;
|
||||
|
||||
// add new ranges for left and right
|
||||
final left =
|
||||
rangePool.isEmpty ? Range.nullFields() : rangePool.removeLast();
|
||||
left.oldListStart = range.oldListStart;
|
||||
left.newListStart = range.newListStart;
|
||||
if (snake.reverse) {
|
||||
left.oldListEnd = snake.x;
|
||||
left.newListEnd = snake.y;
|
||||
} else {
|
||||
if (snake.removal) {
|
||||
left.oldListEnd = snake.x - 1;
|
||||
left.newListEnd = snake.y;
|
||||
} else {
|
||||
left.oldListEnd = snake.x;
|
||||
left.newListEnd = snake.y - 1;
|
||||
}
|
||||
}
|
||||
stack.add(left);
|
||||
|
||||
final right = range;
|
||||
if (snake.reverse) {
|
||||
if (snake.removal) {
|
||||
right.oldListStart = snake.x + snake.size + 1;
|
||||
right.newListStart = snake.y + snake.size;
|
||||
} else {
|
||||
right.oldListStart = snake.x + snake.size;
|
||||
right.newListStart = snake.y + snake.size + 1;
|
||||
}
|
||||
} else {
|
||||
right.oldListStart = snake.x + snake.size;
|
||||
right.newListStart = snake.y + snake.size;
|
||||
}
|
||||
stack.add(right);
|
||||
} else {
|
||||
rangePool.add(range);
|
||||
}
|
||||
}
|
||||
|
||||
snakes.sort((a, b) {
|
||||
final cmpX = a.x - b.x;
|
||||
return cmpX == 0 ? a.y - b.y : cmpX;
|
||||
});
|
||||
|
||||
// add root snake
|
||||
final first = snakes.isEmpty ? null : snakes.first;
|
||||
|
||||
if (first == null || first.x != 0 || first.y != 0) {
|
||||
snakes.insert(0, Snake()
|
||||
..x = 0
|
||||
..y = 0
|
||||
..removal = false
|
||||
..size = 0
|
||||
..reverse = false);
|
||||
}
|
||||
|
||||
return snakes;
|
||||
}
|
||||
|
||||
Snake _diffPartial(DiffInput input, int startOld, int endOld, int startNew,
|
||||
int endNew, List<int> forward, List<int> backward, int kOffset) {
|
||||
final oldSize = endOld - startOld;
|
||||
final newSize = endNew - startNew;
|
||||
|
||||
if (endOld - startOld < 1 || endNew - startNew < 1) return null;
|
||||
|
||||
final delta = oldSize - newSize;
|
||||
final dLimit = (oldSize + newSize + 1) ~/ 2;
|
||||
|
||||
forward.fillRange(kOffset - dLimit - 1, kOffset + dLimit + 1, 0);
|
||||
backward.fillRange(
|
||||
kOffset - dLimit - 1 + delta, kOffset + dLimit + 1 + delta, oldSize);
|
||||
|
||||
final checkInFwd = delta.isOdd;
|
||||
|
||||
for (var d = 0; d <= dLimit; d++) {
|
||||
for (var k = -d; k <= d; k += 2) {
|
||||
// find forward path
|
||||
// we can reach k from k - 1 or k + 1. Check which one is further in the
|
||||
// graph.
|
||||
int x;
|
||||
bool removal;
|
||||
|
||||
if (k == -d ||
|
||||
k != d && forward[kOffset + k - 1] < forward[kOffset + k + 1]) {
|
||||
x = forward[kOffset + k + 1];
|
||||
removal = false;
|
||||
} else {
|
||||
x = forward[kOffset + k - 1] + 1;
|
||||
removal = true;
|
||||
}
|
||||
|
||||
// set y based on x
|
||||
var y = x - k;
|
||||
|
||||
// move diagonal as long as items match
|
||||
while (x < oldSize &&
|
||||
y < newSize &&
|
||||
input.areItemsTheSame(startOld + x, startNew + y)) {
|
||||
x++;
|
||||
y++;
|
||||
}
|
||||
|
||||
forward[kOffset + k] = x;
|
||||
|
||||
if (checkInFwd && k >= delta - d + 1 && k <= delta + d - 1) {
|
||||
if (forward[kOffset + k] >= backward[kOffset + k]) {
|
||||
final outSnake = Snake()..x = backward[kOffset + k];
|
||||
outSnake
|
||||
..y = outSnake.x - k
|
||||
..size = forward[kOffset + k] - backward[kOffset + k]
|
||||
..removal = removal
|
||||
..reverse = false;
|
||||
|
||||
return outSnake;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var k = -d; k <= d; k += 2) {
|
||||
// find reverse path at k + delta, in reverse
|
||||
final backwardK = k + delta;
|
||||
int x;
|
||||
bool removal;
|
||||
|
||||
if (backwardK == d + delta ||
|
||||
backwardK != -d + delta &&
|
||||
backward[kOffset + backwardK - 1] <
|
||||
backward[kOffset + backwardK + 1]) {
|
||||
x = backward[kOffset + backwardK - 1];
|
||||
removal = false;
|
||||
} else {
|
||||
x = backward[kOffset + backwardK + 1] - 1;
|
||||
removal = true;
|
||||
}
|
||||
|
||||
// set y based on x
|
||||
var y = x - backwardK;
|
||||
// move diagonal as long as items match
|
||||
while (x > 0 &&
|
||||
y > 0 &&
|
||||
input.areItemsTheSame(startOld + x - 1, startNew + y - 1)) {
|
||||
x--;
|
||||
y--;
|
||||
}
|
||||
|
||||
backward[kOffset + backwardK] = x;
|
||||
|
||||
if (!checkInFwd && k + delta >= -d && k + delta <= d) {
|
||||
if (forward[kOffset + backwardK] >= backward[kOffset + backwardK]) {
|
||||
final outSnake = Snake()..x = backward[kOffset + backwardK];
|
||||
outSnake
|
||||
..y = outSnake.x - backwardK
|
||||
..size =
|
||||
forward[kOffset + backwardK] - backward[kOffset + backwardK]
|
||||
..removal = removal
|
||||
..reverse = true;
|
||||
|
||||
return outSnake;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw StateError("Unexpected case: Please make sure the lists don't change "
|
||||
'during a diff');
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import 'package:test_api/test_api.dart';
|
||||
import 'package:sally/diff_util.dart';
|
||||
|
||||
List<T> applyEditScript<T>(List<T> a, List<T> b, List<EditAction> actions) {
|
||||
final copy = List.of(a);
|
||||
|
||||
for (var action in actions) {
|
||||
if (action.isDelete) {
|
||||
final deleteStartIndex = action.index;
|
||||
copy.removeRange(deleteStartIndex, deleteStartIndex + action.amount);
|
||||
} else {
|
||||
final toAdd = b.getRange(action.indexFromOther, action.indexFromOther + action.amount);
|
||||
copy.insertAll(action.index, toAdd);
|
||||
}
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
void main() {
|
||||
final a = ['a', 'b', 'c', 'a', 'b', 'b', 'a'];
|
||||
final b = ['c', 'b', 'a', 'b', 'a', 'c'];
|
||||
|
||||
test('diff matcher should produce a correct edit script', () {
|
||||
expect(applyEditScript(a, b, diff(a, b)), b);
|
||||
});
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import 'package:sally_example/database.dart';
|
||||
|
||||
class TodoBloc {
|
||||
|
||||
final Database _db = Database();
|
||||
|
||||
Stream<List<TodoEntry>> get todosForHomepage => _db.todosWithoutCategories;
|
||||
|
||||
void createTodoEntry(String text) {
|
||||
_db.addTodoEntry(TodoEntry(
|
||||
content: text,
|
||||
));
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -43,30 +43,11 @@ class Database extends _$Database {
|
|||
}
|
||||
);
|
||||
|
||||
Stream<List<Category>> get definedCategories => select(categories).watch();
|
||||
|
||||
Stream<List<TodoEntry>> todosInCategories(List<Category> categories) {
|
||||
final ids = categories.map((c) => c.id);
|
||||
|
||||
return (select(todos)..where((t) => isIn(t.category, ids))).watch();
|
||||
Stream<List<TodoEntry>> allEntries() {
|
||||
return select(todos).watch();
|
||||
}
|
||||
|
||||
Future<void> deleteOldEntries() {
|
||||
return (delete(todos)..where((t) => year(t.targetDate).equals(2017))).go();
|
||||
}
|
||||
|
||||
Stream<List<TodoEntry>> watchEntriesInCategory(Category c) {
|
||||
return (select(todos)..where((t) => t.category.equals(c.id))).watch();
|
||||
}
|
||||
|
||||
Stream<List<TodoEntry>> get todosWithoutCategories =>
|
||||
(select(todos)..where((t) => isNull(t.category))).watch();
|
||||
|
||||
Future<List<TodoEntry>> sortEntriesAlphabetically() {
|
||||
return (select(todos)..orderBy([(u) => OrderingTerm(expression: u.title)])).get();
|
||||
}
|
||||
|
||||
Future addTodoEntry(TodoEntry entry) {
|
||||
Future addEntry(TodoEntry entry) {
|
||||
return into(todos).insert(entry);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sally_example/bloc.dart';
|
||||
import 'package:sally_example/widgets/homescreen.dart';
|
||||
import 'database.dart';
|
||||
import 'widgets/homescreen.dart';
|
||||
|
||||
void main() => runApp(MyApp());
|
||||
|
||||
|
@ -13,24 +13,18 @@ class MyApp extends StatefulWidget {
|
|||
|
||||
class MyAppState extends State<MyApp> {
|
||||
|
||||
TodoBloc _bloc;
|
||||
Database _db;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_bloc = TodoBloc();
|
||||
_db = Database();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_bloc.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
bloc: _bloc,
|
||||
return DatabaseProvider(
|
||||
db: _db,
|
||||
child: MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
|
@ -42,16 +36,16 @@ class MyAppState extends State<MyApp> {
|
|||
}
|
||||
}
|
||||
|
||||
class BlocProvider extends InheritedWidget {
|
||||
class DatabaseProvider extends InheritedWidget {
|
||||
|
||||
final TodoBloc bloc;
|
||||
final Database db;
|
||||
|
||||
BlocProvider({@required this.bloc, Widget child}) : super(child: child);
|
||||
DatabaseProvider({@required this.db, Widget child}) : super(child: child);
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(BlocProvider oldWidget) {
|
||||
return oldWidget.bloc != bloc;
|
||||
bool updateShouldNotify(DatabaseProvider oldWidget) {
|
||||
return oldWidget.db != db;
|
||||
}
|
||||
|
||||
static TodoBloc provideBloc(BuildContext ctx) => (ctx.inheritFromWidgetOfExactType(BlocProvider) as BlocProvider).bloc;
|
||||
static Database provideDb(BuildContext ctx) => (ctx.inheritFromWidgetOfExactType(DatabaseProvider) as DatabaseProvider).db;
|
||||
}
|
|
@ -1,40 +1,35 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sally_example/database.dart';
|
||||
import 'package:sally_example/main.dart';
|
||||
import 'package:sally_example/widgets/todo_card.dart';
|
||||
import 'package:sally_flutter/sally_flutter.dart';
|
||||
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
|
||||
class HomeScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = BlocProvider.provideBloc(context);
|
||||
final db = DatabaseProvider.provideDb(context);
|
||||
|
||||
return Scaffold(
|
||||
drawer: Text('Hi'),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
title: Text('TODO List'),
|
||||
),
|
||||
StreamBuilder<List<TodoEntry>>(
|
||||
stream: bloc.todosForHomepage,
|
||||
builder: (ctx, snapshot) {
|
||||
final data = snapshot.hasData ? snapshot.data : <TodoEntry>[];
|
||||
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(ctx, index) => Text(data[index].content),
|
||||
childCount: data.length,
|
||||
),
|
||||
appBar: AppBar(title: Text('Todo list'),),
|
||||
body: SallyAnimatedList<TodoEntry>(
|
||||
stream: db.allEntries(),
|
||||
itemBuilder: (ctx, TodoEntry item, animation) {
|
||||
return SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axis: Axis.vertical,
|
||||
child: TodoCard(item),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
removedItemBuilder: (_, __, ___) => Container(),
|
||||
),
|
||||
bottomSheet: Material(
|
||||
elevation: 12.0,
|
||||
child: TextField(
|
||||
onSubmitted: bloc.createTodoEntry,
|
||||
onSubmitted: (content) {
|
||||
db.addEntry(TodoEntry(content: content));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sally_example/database.dart';
|
||||
|
||||
class TodoCard extends StatelessWidget {
|
||||
|
||||
final TodoEntry entry;
|
||||
|
||||
TodoCard(this.entry) : super(key: ObjectKey(entry.id));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Text(entry.content),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import 'package:path/path.dart';
|
|||
import 'package:sally/sally.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
export 'package:sally_flutter/src/animated_list_old.dart';
|
||||
export 'package:sally/sally.dart';
|
||||
|
||||
/// A query executor that uses sqlfite internally.
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:sally_flutter/src/utils.dart';
|
||||
|
||||
typedef Widget AppearingAnimationBuilder<T>(
|
||||
BuildContext ctx, T item, Animation<double> animation,
|
||||
{int index});
|
||||
typedef Widget OutgoingAnimationBuilder<T>(
|
||||
BuildContext ctx, T item, Animation<double> animation,
|
||||
{int index});
|
||||
|
||||
/// A list that animatse
|
||||
class AnimatedStreamList<T> extends StatefulWidget {
|
||||
static const Widget _defaultPlaceholder = SizedBox();
|
||||
|
||||
/// Builder that builds widget as they appear on the list.
|
||||
final AppearingAnimationBuilder<T> appearing;
|
||||
|
||||
/// Builder that builds widgets as they leave the list.
|
||||
final OutgoingAnimationBuilder<T> leaving;
|
||||
|
||||
/// Widget that will be built when the stream emits an empty list after all
|
||||
/// remaining list items have animated away.
|
||||
final WidgetBuilder empty;
|
||||
|
||||
/// Widget that will be built when the stream has not yet emitted any item.
|
||||
final WidgetBuilder loading;
|
||||
|
||||
AnimatedStreamList(
|
||||
{@required this.appearing,
|
||||
@required this.leaving,
|
||||
this.empty,
|
||||
this.loading});
|
||||
|
||||
@override
|
||||
_AnimatedStreamListState<T> createState() => _AnimatedStreamListState<T>();
|
||||
}
|
||||
|
||||
const Duration _kDuration = Duration(milliseconds: 300);
|
||||
|
||||
class _AnimatedStreamListState<T> extends State<AnimatedStreamList<T>>
|
||||
with TickerProviderStateMixin {
|
||||
StreamSubscription _subscription;
|
||||
|
||||
List<T> _lastSnapshot;
|
||||
final List<_AnimatedItemState> _insertingItems = [];
|
||||
final List<_AnimatedItemState> _leavingItems = [];
|
||||
|
||||
void _handleDataReceived(List<T> data) {
|
||||
if (_lastSnapshot == null) {
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
_animateIncomingItem(i);
|
||||
}
|
||||
} else {
|
||||
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_lastSnapshot = data;
|
||||
});
|
||||
}
|
||||
|
||||
void _animateIncomingItem(int index) {
|
||||
final controller = AnimationController(vsync: this);
|
||||
final state = _AnimatedItemState(controller, true, index);
|
||||
|
||||
insertIntoSortedList<_AnimatedItemState>(_insertingItems, state,
|
||||
compare: (a, b) => a.itemIndex.compareTo(b.itemIndex));
|
||||
}
|
||||
|
||||
void _animateOutgoingItem(int index) {
|
||||
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AnimatedStreamList oldWidget) {}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var item in _insertingItems) {
|
||||
item._controller.dispose();
|
||||
}
|
||||
for (var item in _leavingItems) {
|
||||
item._controller.dispose();
|
||||
}
|
||||
|
||||
_subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_lastSnapshot == null) {
|
||||
// no data yet, show placeholder
|
||||
return widget.loading != null
|
||||
? widget.loading(context)
|
||||
: AnimatedStreamList._defaultPlaceholder;
|
||||
} else if (_lastSnapshot.isEmpty && _leavingItems.isEmpty) {
|
||||
return widget.empty != null
|
||||
? widget.empty(context)
|
||||
: AnimatedStreamList._defaultPlaceholder;
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemBuilder: (ctx, i) {},
|
||||
itemCount: _lastSnapshot.length + _leavingItems.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedItemState {
|
||||
final AnimationController _controller;
|
||||
final bool isInserting;
|
||||
int itemIndex;
|
||||
|
||||
_AnimatedItemState(this._controller, this.isInserting, this.itemIndex);
|
||||
}
|
||||
*/
|
|
@ -0,0 +1,118 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:sally/diff_util.dart';
|
||||
|
||||
typedef Widget ItemBuilder<T>(BuildContext context, T item, Animation<double> anim);
|
||||
typedef Widget RemovedItemBuilder<T>(BuildContext context, T item, Animation<double> anim);
|
||||
|
||||
/// An [AnimatedList] that shows the result of a sally query stream.
|
||||
class SallyAnimatedList<T> extends StatefulWidget {
|
||||
|
||||
final Stream<List<T>> stream;
|
||||
final ItemBuilder<T> itemBuilder;
|
||||
final RemovedItemBuilder<T> removedItemBuilder;
|
||||
|
||||
SallyAnimatedList({@required this.stream, @required this.itemBuilder, @required this.removedItemBuilder});
|
||||
|
||||
@override
|
||||
_SallyAnimatedListState createState() {
|
||||
return _SallyAnimatedListState();
|
||||
}
|
||||
}
|
||||
|
||||
class _SallyAnimatedListState<T> extends State<SallyAnimatedList<T>> {
|
||||
|
||||
List<T> _lastSnapshot;
|
||||
int _initialItemCount;
|
||||
|
||||
StreamSubscription _subscription;
|
||||
|
||||
final GlobalKey<AnimatedListState> _key = GlobalKey();
|
||||
AnimatedListState get listState => _key.currentState;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_setupSubscription();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _receiveData(List<T> data) {
|
||||
if (listState == null) {
|
||||
setState(() {
|
||||
_lastSnapshot = data;
|
||||
_initialItemCount = data.length;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (_lastSnapshot == null) {
|
||||
// no diff possible. Initialize lists instead of diffing
|
||||
_lastSnapshot = data;
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
listState.insertItem(i);
|
||||
}
|
||||
} else {
|
||||
final editScript = diff(_lastSnapshot, data);
|
||||
|
||||
for (var action in editScript) {
|
||||
if (action.isDelete) {
|
||||
// we need to delete action.amount items at index action.index
|
||||
for (var i = 0; i < action.amount; i++) {
|
||||
// i items have already been deleted, so + 1 for the index. Notice
|
||||
// that we don't have to do this when calling removeItem on the
|
||||
// animated list state, as it will reflect the operation immediately.
|
||||
final itemHere = _lastSnapshot[action.index + i];
|
||||
listState.removeItem(action.index, (ctx, anim) {
|
||||
return widget.removedItemBuilder(ctx, itemHere, anim);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < action.amount; i++) {
|
||||
listState.insertItem(action.index + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_lastSnapshot = data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _setupSubscription() {
|
||||
_subscription = widget.stream.listen(_receiveData);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(SallyAnimatedList<T> oldWidget) {
|
||||
_subscription?.cancel();
|
||||
_lastSnapshot = null;
|
||||
_setupSubscription();
|
||||
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_lastSnapshot == null)
|
||||
return const SizedBox();
|
||||
|
||||
return AnimatedList(
|
||||
key: _key,
|
||||
initialItemCount: _initialItemCount ?? 0,
|
||||
itemBuilder: (ctx, index, anim) {
|
||||
final item = _lastSnapshot[index];
|
||||
final child = widget.itemBuilder(ctx, item, anim);
|
||||
|
||||
return child;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
void insertIntoSortedList<T>(List<T> list, T entry, {int compare(T a, T b)}) {
|
||||
|
||||
}
|
|
@ -23,7 +23,7 @@ packages:
|
|||
source: hosted
|
||||
version: "1.1.2"
|
||||
collection:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
|
|
|
@ -10,6 +10,7 @@ environment:
|
|||
sdk: ">=2.0.0-dev.68.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
collection:
|
||||
sally:
|
||||
path: ../sally
|
||||
sqflite:
|
||||
|
|
Loading…
Reference in New Issue