Heavily inspired bei Android's DiffUtil class, the code was adopted for Dart.
Uses Myers algorithm internally. For an explanation of how the algorithm works internally, for details I recommend this great series of articles: https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/
There are often situations, where an app displays a list of items, which are fetched from an external data source like a server endpoint or a database, and updates are not sent as delta, but as a whole new list.
It can be useful to take the old list and the new list and calculate the difference between those two, for example when animating the insertion and removal of new items in the displayed list (See diffutil_sliverlist).
This package does exactly that: It takes two lists and calculates the difference (or to be more accurate: edit script) between those two lists as list of Insert, Remove, Change (only when using custom delegates) and Move (when enabled) operations.
Simple usage:
final diffResult = calculateListDiff<int>([1, 2 ,3], [1, 3, 4]);
Custom equality:
final diffResult = calculateListDiff<YourClassWithId>(oldList, newList, (o1, o2) => o1.id == o2.id);
If you don't want to use plain old Dart lists (for example if you're using built_value or kt.dart), and don't want to convert your custom list to standard lists, you can use the calculateDiff
function and implement your own DiffDelegate
easily.
Or use calculateCustomListDiff
and CustomListDiffDelegate
.
Move detection is disabled by default.
Call .getUpdates()
on the diffResult
to get a List of DiffUpdate
objects. These are sealed classes of type Insert, Remove, Change or Move. Move operations are only calculated if calculateListDiff
was called with detectMoves: true
The order of these operations matters! They describe how to turn the previous list into the new list, when applied in the order as they are returned.
for (final update in diffResult.getUpdates())
update.when(
insert: (pos, count) => print("inserted $count on $pos"),
remove: (pos, count) => print("removed $count on $pos"),
change: (pos, payload) => print("changed on $pos with payload $payload"),
move: (from, to) => print("move $from to $to"),
);
By default, Insert
and Remove
Operations are batched. (e.g. multiple consecutive inserts or removes are represented by a single Insert
/Remove
object with a count
field > 1). If you want to turn off edit script batching, call getUpdates(batch: false)
. This means, every Insert
and Remove
operation will have a count of 1 and the edit script of []
and [1, 2]
will be
[Insert(position: 0, count : 1), Insert(position: 0, count :1 )]
instead of
[Insert(position: 0, count: 2)]
If you need the concrete items that have been inserted/removed/changed/moved, call getUpdatesWithData()
.
diffutil.calculateListDiff([1, 2, 3], [1, 0, 3]).getUpdatesWithData();
returns
[
DataRemove(position: 1, data: 2),
DataInsert(position: 1, data: 0)
];
The result of getUpdatesWithData()
cannot be batched. You can use the resulting Iterable<DataDiffUpdate>
like this:
for (final update in updates) {
update.when(
insert: (pos, data) => print('insert $pos $data'),
remove: (pos, data) => print('remove $pos $data'),
change: (pos, oldData, newData) => print('change on $pos from $oldData to $newData'),
move: (from, to, data) => print('move $data from $from to $to'),
);
}
Note that if you implement you own DiffDelegate
and call calculateDiff()
directly, the DiffDelegate
also needs to implement IndexableItemDiffDelegate
if you want to call getUpdatesWithData()
.
For data objects that have some meaningful identity (like an unique id
field), you can enable the calculation of Change
operations. For this, you need to implement your own DiffDelegate
and override the areItemsTheSame
method.
Let's say we have a class called DataObject
which is defined like this:
class DataObject {
final int id;
final int? payload;
DataObject({required this.id, this.payload});
}
The delegate can be implemented like this:
class DataObjectListDiff extends diffutil.ListDiffDelegate<DataObject> {
DataObjectListDiff(List<DataObject> oldList, List<DataObject> newList)
: super(oldList, newList);
@override
bool areContentsTheSame(int oldItemPosition, int newItemPosition) {
return equalityChecker(oldList[oldItemPosition], newList[newItemPosition]);
}
@override
bool areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldList[oldItemPosition].id == newList[newItemPosition].id;
}
}
With this, Change
Operations are emitted if the id of an object stays the same between oldList and newList, but it's data changes.
Same as Android's DiffUtil:
The edit script is the smallest set of operations needed to transform the first list into the second list.
Run this command:
With Dart:
$ dart pub add diffutil_dart
With Flutter:
$ flutter pub add diffutil_dart
This will add a line like this to your package's pubspec.yaml (and run an implicit dart pub get
):
dependencies:
diffutil_dart: ^4.0.0
Alternatively, your editor might support dart pub get
or flutter pub get
. Check the docs for your editor to learn more.
Now in your Dart code, you can use:
import 'package:diffutil_dart/diffutil.dart';
import 'package:diffutil_dart/diffutil.dart' as diffutil;
/// Note: This is a dart-only project, running flutter pub get will fail with an error message of a missing
/// pubspec.yaml in the example folder. Just run pub get instead.
void main() {
final oldList = [1, 2, 3];
final newList = [2, 3, 4];
print('difference between $oldList and $newList, without move detection:');
final listDiff = diffutil.calculateListDiff(oldList, newList).getUpdates();
// use the diff using a list of diff objects
for (final update in listDiff) {
update.when(
insert: (pos, count) => print('inserted $count item on $pos'),
remove: (pos, count) => print('removed $count item on $pos'),
change: (pos, payload) => print('changed on $pos with payload $payload'),
move: (from, to) => print('move from $from to $to'),
);
}
print('changeset: $listDiff');
final oldList2 = [1, 2, 3];
final newList2 = [1, 3, 2];
print('\n');
print('difference between $oldList2 and $newList2, without move detection:');
final listDiff2 = diffutil
.calculateListDiff(oldList2, newList2, detectMoves: false)
.getUpdates();
print('changeset: $listDiff2');
print('\n');
print('difference between $oldList2 and $newList2, with move detection:');
final listDiff3 = diffutil
.calculateListDiff(oldList2, newList2, detectMoves: true)
.getUpdates();
print('changeset: $listDiff3');
print('\n');
print(
'difference between $oldList and $newList, with data, with move detection:');
final listDiff4 = diffutil
.calculateListDiff(oldList, newList, detectMoves: true)
.getUpdatesWithData();
print('changeset: $listDiff4');
print('\n');
final oldList3 = [];
final newList3 = [1, 2, 3];
print('difference between $oldList3 and $newList3, batched:');
final listDiff5 =
diffutil.calculateListDiff(oldList3, newList3).getUpdates(batch: true);
print('changeset: $listDiff5');
print('\n');
print('difference between $oldList3 and $newList3, unbatched:');
final listDiff6 =
diffutil.calculateListDiff(oldList3, newList3).getUpdates(batch: false);
print('changeset: $listDiff6');
print('\n');
final dataObjectList1 = [DataObject(id: 1, payload: 0)];
final dataObjectList2 = [DataObject(id: 1, payload: 1)];
print(
'data object diff between $dataObjectList1 and $dataObjectList2, default behaviour');
print(diffutil
.calculateListDiff(dataObjectList1, dataObjectList2)
.getUpdatesWithData());
print('\n');
print(
'data object diff $dataObjectList1 and $dataObjectList2, with custom delegate with that respects identity');
print(diffutil
.calculateDiff(DataObjectListDiff(dataObjectList1, dataObjectList2))
.getUpdatesWithData());
}
class DataObject {
final int id;
final int? payload;
DataObject({required this.id, this.payload});
@override
String toString() {
return 'DataObject{id: $id, payload: $payload}';
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is DataObject &&
runtimeType == other.runtimeType &&
id == other.id &&
payload == other.payload;
@override
int get hashCode => id.hashCode ^ payload.hashCode;
}
class DataObjectListDiff extends diffutil.ListDiffDelegate<DataObject> {
DataObjectListDiff(List<DataObject> oldList, List<DataObject> newList)
: super(oldList, newList);
@override
bool areContentsTheSame(int oldItemPosition, int newItemPosition) {
return equalityChecker(oldList[oldItemPosition], newList[newItemPosition]);
}
@override
bool areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldList[oldItemPosition].id == newList[newItemPosition].id;
}
}
Download details:
Author: littlebat.dev
Source: https://github.com/knaeckeKami/diffutil.dart