Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -277,4 +277,5 @@ gradle-app.setting
### Gradle Patch ###
**/build/

# End of https://www.toptal.com/developers/gitignore/api/dart,flutter,intellij+all,git,macos,windows,linux,gradle
# End of https://www.toptal.com/developers/gitignore/api/dart,flutter,intellij+all,git,macos,windows,linux,gradle
/test/coverage_helper_test.dart
5 changes: 4 additions & 1 deletion lib/icapps_architecture.dart
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export 'package:icapps_architecture/provider/change_notifier_ex.dart';
export 'src/provider/change_notifier_ex.dart';
export 'src/extension/list_extensions.dart';
export 'src/extension/map_extensions.dart';
export 'src/extension/iterable_extensions.dart';
73 changes: 73 additions & 0 deletions lib/src/extension/iterable_extensions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'package:tuple/tuple.dart';

extension IterableExtension<T> on Iterable<T> {
/// Counts all elements for which [where] returns true
int count(bool Function(T) where) {
var c = 0;
for (final element in this) {
if (where(element)) {
++c;
}
}
return c;
}

/// Sums the result of [valueProvider] for each item
E sum<E extends num>(E Function(T) valueProducer) {
num value = 0;
forEach((e) => value = (value + valueProducer(e)) as E);
return value as E;
}

/// Finds the first item that matches [where], if no such item could be found
/// returns `null`
T? find(bool Function(T) where) {
for (final element in this) {
if (where(element)) return element;
}
return null;
}

/// Returns `true` if every item matches [where]
bool all(bool Function(T) where) {
for (final element in this) {
if (!where(element)) {
return false;
}
}
return true;
}

/// Create a map by mapping every element using [key]. Duplicate values
/// are discarded
Map<S, T> associateBy<S>(S Function(T) key) {
final map = <S, T>{};
for (final element in this) {
map[key(element)] = element;
}
return map;
}

/// Splits the elements according to [on]. Items for which [on] is true will
/// be stored in [Tuple2.item1], other items in [Tuple2.item2]
Tuple2<List<T>, List<T>> split(bool Function(T) on) {
final left = <T>[];
final right = <T>[];
forEach((element) {
if (on(element)) {
left.add(element);
} else {
right.add(element);
}
});

return Tuple2(left, right);
}

/// Same as [Iterable.map] except that the [mapper] function also receives
/// the index of the item being mapped
Iterable<R> mapIndexed<R>(R Function(int, T) mapper) {
var c = 0;
return map((element) => mapper(c++, element));
}
}
52 changes: 52 additions & 0 deletions lib/src/extension/list_extensions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
extension ListExtensions<T> on List<T> {
///Replaces all data in the list with [newData]
void replaceAll(List<T> newData) {
clear();
addAll(newData);
}

/// Sorts the list based on the comparable returned by [by]. By default
/// the sorting is [ascending]
void sortBy<R>(Comparable<R>? by(T item), {bool ascending = true}) {
sort((a, b) {
final byA = by(a);
final byB = by(b);
if (byA == null) return ascending ? -1 : 1;
if (byB == null) return ascending ? 1 : -1;
return _compareValues(byA, byB, ascending);
});
}

/// Sorts the list by comparing first comparing using [by] and if the items
/// are equal, by comparing them using [by2]. By default
/// the sorting is [ascending]
void sortBy2<R>(
Comparable<R>? by(T item),
Comparable<R>? by2(T item), {
bool ascending = true,
}) {
sort((a, b) {
final byA = by(a);
final byB = by(b);
if (byA == null && byB != null) return ascending ? -1 : 1;
if (byB == null && byA != null) return ascending ? 1 : -1;
if (byA != null && byB != null) {
final result = _compareValues(byA, byB, ascending);
if (result != 0) return result;
}

final byA2 = by2(a);
final byB2 = by2(b);
if (byA2 == null && byB2 == null) return 0;
if (byA2 == null) return ascending ? -1 : 1;
if (byB2 == null) return ascending ? 1 : -1;
return _compareValues(byA2, byB2, ascending);
});
}
}

int _compareValues<T extends Comparable<dynamic>>(T a, T b, bool ascending) {
if (identical(a, b)) return 0;
if (ascending) return a.compareTo(b);
return -a.compareTo(b);
}
21 changes: 21 additions & 0 deletions lib/src/extension/map_extensions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
extension MapExtension<K, V> on Map<K?, V> {
/// Removes all null keys from the map
Map<K, V> removeNullKeys() {
final map = <K, V>{};
forEach((key, value) {
if (key != null) map[key] = value;
});
return map;
}
}

extension MapExtension2<K, V> on Map<K, V?> {
/// Removes all null values from the map
Map<K, V> removeNullValues() {
final map = <K, V>{};
forEach((key, value) {
if (value != null) map[key] = value;
});
return map;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import 'dart:collection';

import 'package:flutter/foundation.dart';

/// Extended version of the foundation's [ChangeNotifier].
///
/// Has helper methods to determine if it has been disposed ([disposed]) and
/// convenience methods to register listeners that will be cleaned up when the
/// change notifier is disposed [registerDispose()] and [registerDisposeStream()]
mixin ChangeNotifierEx implements ChangeNotifier {
LinkedList<_ListenerEntry>? _listeners = LinkedList<_ListenerEntry>();
var _disposed = false;
final _cleanupList = <DisposeAware>[];

/// Returns `true` if this change notifier has been disposed
@protected
bool get disposed => _disposed;

Expand Down Expand Up @@ -47,6 +53,10 @@ mixin ChangeNotifierEx implements ChangeNotifier {
}
}

/// Registers an item for automatic cleanup when this item is [disposed]
///
/// If this ChangeNotifier has already been [disposed], [DisposeAware.dispose()]
/// will be called immediately before returning from this method
void registerDispose(DisposeAware toDispose) {
if (_disposed) {
toDispose.dispose();
Expand All @@ -55,10 +65,17 @@ mixin ChangeNotifierEx implements ChangeNotifier {
_cleanupList.add(toDispose);
}

/// Registers a stream for automatic cleanup when this item is [disposed].
///
/// In this case, cleanup refers to calling [StreamSubscription.cancel()]
///
/// If this ChangeNotifier has already been [disposed], [StreamSubscription.cancel()]
/// will be called immediately before returning from this method
void registerDisposeStream<T>(StreamSubscription<T> subscription) {
registerDispose(_StreamDisposer(subscription));
}

@override
@protected
@visibleForTesting
void notifyListeners() {
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies:
sdk: flutter
provider: ^5.0.0
mockito: ^5.0.3
tuple: ^2.0.0

dev_dependencies:
build_runner: ^1.11.0
Expand Down
59 changes: 59 additions & 0 deletions test/extension/iterable_extension_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:icapps_architecture/icapps_architecture.dart';

void main() {
late List<int> sut;
late List<double> sut2;

setUp(() {
sut = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
sut2 = [1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 8.1, 9.1, 10.1];
});
group('Iterable extension tests', () {
test('Test count', () {
expect(sut.count((e) => e < 100), 10);
expect(sut.count((e) => e < 5), 4);
});
test('Test sum', () {
expect(sut.sum((e) => e), 55);
expect(sut2.sum((e) => e).round(), 56);
});
test('Test find', () {
expect(sut.find((e) => e == 2), 2);
expect(sut.find((e) => e == 102), null);
});
test('Test all', () {
expect(sut.all((e) => e < 100), true);
expect(sut.all((e) => e < 5), false);
});
test('Test associate by', () {
expect(sut.associateBy((e) => e.toString()), {
'1': 1,
'2': 2,
'3': 3,
'4': 4,
'5': 5,
'6': 6,
'7': 7,
'8': 8,
'9': 9,
'10': 10,
});
});
test('Test split', () {
final result = sut.split((e) => e <= 7);
expect(result.item1, [1, 2, 3, 4, 5, 6, 7]);
expect(result.item2, [8, 9, 10]);
});
test('Test map indexed', () {
final seenIndexes = <int>[];
final seenValues = <int>[];
sut.mapIndexed((index, e) {
seenIndexes.add(index);
seenValues.add(e);
}).toList(growable: false);
expect(seenIndexes, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
expect(seenValues, sut);
});
});
}
113 changes: 113 additions & 0 deletions test/extension/list_extension_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:icapps_architecture/icapps_architecture.dart';
import 'package:tuple/tuple.dart';

void main() {
late List<int> sut;

setUp(() {
sut = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
});
group('List extension tests', () {
test('Test replace all', () {
expect(sut, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(sut..replaceAll([11, 12, 13]), [11, 12, 13]);
});
test('Test sort by', () {
sut.sortBy((e) => 10 - e);
expect(sut, [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]);
});
test('Test sort by null', () {
sut.sortBy((e) => e % 2 == 0 ? null : e);
expect(sut, [2, 4, 6, 8, 10, 1, 3, 5, 7, 9]);
});
test('Test sort by descending', () {
sut.sortBy((e) => e, ascending: false);
expect(sut, [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]);
});
test('Test sort by null descending', () {
sut.sortBy((e) => e % 2 == 0 ? null : e, ascending: false);
expect(sut, [9, 7, 5, 3, 1, 10, 8, 6, 4, 2]);
});
test('Test sort by2', () {
final complexSut = <Tuple2<String, int>>[
Tuple2("ABC", 2),
Tuple2("ABC", 1),
Tuple2("BCD", 25),
Tuple2("ABCDE", 25),
];

complexSut.sortBy2((e) => e.item1, (e) => e.item2);
expect(complexSut, <Tuple2<String, int>>[
Tuple2("ABC", 1),
Tuple2("ABC", 2),
Tuple2("ABCDE", 25),
Tuple2("BCD", 25),
]);
});
test('Test sort by2, descending', () {
final complexSut = <Tuple2<String, int>>[
Tuple2("ABC", 2),
Tuple2("ABC", 1),
Tuple2("BCD", 25),
Tuple2("ABCDE", 25),
];

complexSut.sortBy2((e) => e.item1, (e) => e.item2, ascending: false);
expect(complexSut, <Tuple2<String, int>>[
Tuple2("BCD", 25),
Tuple2("ABCDE", 25),
Tuple2("ABC", 2),
Tuple2("ABC", 1),
]);
});
test('Test sort by2 nullable', () {
final complexSut = <Tuple2<String?, int?>>[
Tuple2("ABC", 2),
Tuple2("ABC", 1),
Tuple2("BCD", 25),
Tuple2(null, null),
Tuple2("ABCDE", 25),
Tuple2(null, 25),
Tuple2(null, 24),
Tuple2("ABC", null),
];

complexSut.sortBy2((e) => e.item1, (e) => e.item2);
expect(complexSut, <Tuple2<String?, int?>>[
Tuple2(null, null),
Tuple2(null, 24),
Tuple2(null, 25),
Tuple2("ABC", null),
Tuple2("ABC", 1),
Tuple2("ABC", 2),
Tuple2("ABCDE", 25),
Tuple2("BCD", 25),
]);
});
test('Test sort by2 nullable descending', () {
final complexSut = <Tuple2<String?, int?>>[
Tuple2("ABC", 2),
Tuple2("ABC", 1),
Tuple2(null, null),
Tuple2("BCD", 25),
Tuple2("ABCDE", 25),
Tuple2(null, 25),
Tuple2(null, 24),
Tuple2("ABC", null),
];

complexSut.sortBy2((e) => e.item1, (e) => e.item2, ascending: false);
expect(complexSut, <Tuple2<String?, int?>>[
Tuple2("BCD", 25),
Tuple2("ABCDE", 25),
Tuple2("ABC", 2),
Tuple2("ABC", 1),
Tuple2("ABC", null),
Tuple2(null, 25),
Tuple2(null, 24),
Tuple2(null, null),
]);
});
});
}
Loading