Skip to content

Commit ae84663

Browse files
authored
refactor: Filters/FilterState and add disposables (algolia#31)
1 parent 36bb336 commit ae84663

File tree

11 files changed

+529
-243
lines changed

11 files changed

+529
-243
lines changed

helper_dart/lib/algolia_helper.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
/// experiences at a deeper level, in pure Dart.
55
library algolia_helper;
66

7+
export 'src/disposable.dart';
78
export 'src/exception.dart';
89
export 'src/facet_list.dart';
910
export 'src/filter.dart';
@@ -12,7 +13,6 @@ export 'src/filter_state.dart';
1213
export 'src/filters.dart';
1314
export 'src/highlighting.dart';
1415
export 'src/hits_searcher.dart';
15-
export 'src/immutable_filters.dart';
1616
export 'src/search_response.dart';
1717
export 'src/search_state.dart';
1818
export 'src/selectable_item.dart';
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import 'disposable_mixin.dart';
2+
3+
/// Represents an object able to release it's resources.
4+
abstract class Disposable {
5+
/// Whether this [Disposable] has already released its resources.
6+
bool get isDisposed;
7+
8+
/// Releases this [Disposable] resources.
9+
void dispose();
10+
}
11+
12+
/// Acts as a container for multiple disposables that can be canceled at once.
13+
///
14+
/// Can be cleared or disposed. When disposed, cannot be used again.
15+
/// ### Example
16+
/// // init your subscriptions
17+
/// composite.add(hitsSearcher)
18+
/// ..add(filterState)
19+
/// ..add(facetList);
20+
///
21+
/// // clear them all at once
22+
/// composite.clear();
23+
abstract class CompositeDisposable implements Disposable {
24+
/// Creates [CompositeDisposable] instance.
25+
factory CompositeDisposable() => _CompositeDisposable();
26+
27+
/// Returns the total amount of currently added [Disposable]s
28+
int get length;
29+
30+
/// Checks if there currently are no [Disposable]s added
31+
bool get isEmpty;
32+
33+
/// Adds [disposable] to this composite.
34+
/// Throws an exception if this composite was disposed
35+
Disposable add(Disposable disposable);
36+
37+
/// Remove [disposable] from this composite and cancel it if it has been
38+
/// removed.
39+
void remove(Disposable disposable, {bool shouldDispose = true});
40+
41+
/// Cancels all disposables added to this composite.
42+
/// Clears disposables collection.
43+
/// This composite can be reused after calling this method.
44+
void clear();
45+
}
46+
47+
/// Default implementation of [CompositeDisposable].
48+
class _CompositeDisposable with DisposableMixin implements CompositeDisposable {
49+
/// List of [Disposable]s.
50+
final List<Disposable> _disposables = [];
51+
52+
@override
53+
int get length => _disposables.length;
54+
55+
@override
56+
bool get isEmpty => _disposables.isEmpty;
57+
58+
@override
59+
Disposable add(Disposable disposable) {
60+
if (isDisposed) {
61+
throw StateError(
62+
'This $runtimeType was disposed, consider checking `isDisposed` or'
63+
' try to use new instance instead');
64+
}
65+
_disposables.add(disposable);
66+
return disposable;
67+
}
68+
69+
@override
70+
void remove(
71+
Disposable disposable, {
72+
bool shouldDispose = true,
73+
}) =>
74+
_disposables.remove(disposable) && shouldDispose
75+
? disposable.dispose()
76+
: null;
77+
78+
@override
79+
void clear() {
80+
doDispose();
81+
_disposables.clear();
82+
}
83+
84+
@override
85+
void doDispose() {
86+
for (final disposable in _disposables) {
87+
if (!disposable.isDisposed) disposable.dispose();
88+
}
89+
}
90+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import 'disposable.dart';
2+
3+
/// Mixin to abstract away `isDisposed` logic.
4+
mixin DisposableMixin implements Disposable {
5+
@override
6+
bool isDisposed = false;
7+
8+
@override
9+
void dispose() {
10+
if (!isDisposed) {
11+
doDispose();
12+
isDisposed = true;
13+
}
14+
}
15+
16+
void doDispose();
17+
}

helper_dart/lib/src/facet_list.dart

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import 'package:logging/logging.dart';
44
import 'package:meta/meta.dart';
55
import 'package:rxdart/rxdart.dart';
66

7+
import 'disposable.dart';
8+
import 'disposable_mixin.dart';
79
import 'extensions.dart';
810
import 'filter.dart';
911
import 'filter_group.dart';
1012
import 'filter_state.dart';
13+
import 'filters.dart';
1114
import 'hits_searcher.dart';
12-
import 'immutable_filters.dart';
1315
import 'logger.dart';
1416
import 'search_response.dart';
1517
import 'selectable_item.dart';
@@ -57,7 +59,7 @@ import 'selectable_item.dart';
5759
/// ```
5860
@experimental
5961
@sealed
60-
abstract class FacetList {
62+
abstract class FacetList implements Disposable {
6163
/// Create [FacetList] instance.
6264
factory FacetList({
6365
required HitsSearcher searcher,
@@ -102,9 +104,6 @@ abstract class FacetList {
102104

103105
/// Select/deselect the provided facet value depending on the current selection state.
104106
void toggle(String value);
105-
106-
/// Dispose the component.
107-
void dispose();
108107
}
109108

110109
/// Elements selection mode.
@@ -113,8 +112,8 @@ enum SelectionMode { single, multiple }
113112
/// [Facet] with selection status.
114113
typedef SelectableFacet = SelectableItem<Facet>;
115114

116-
/// Implementation of [FacetList].
117-
class _FacetList implements FacetList {
115+
/// Default implementation of [FacetList].
116+
class _FacetList with DisposableMixin implements FacetList {
118117
/// Create [_FacetList] instance.
119118
_FacetList({
120119
required this.searcher,
@@ -124,6 +123,14 @@ class _FacetList implements FacetList {
124123
required this.selectionMode,
125124
required this.persistent,
126125
}) {
126+
if (searcher.isDisposed) {
127+
_log.warning('creating an instance with disposed searcher');
128+
}
129+
130+
if (filterState.isDisposed) {
131+
_log.warning('creating an instance with disposed filter state');
132+
}
133+
127134
// Setup search state by adding `attribute` to the search state
128135
searcher.applyState(
129136
(state) => state.copyWith(
@@ -274,8 +281,8 @@ class _FacetList implements FacetList {
274281
}
275282
}
276283

277-
/// Clear filters from [ImmutableFilters] depending
278-
Future<ImmutableFilters> _clearFilters(ImmutableFilters filters) async {
284+
/// Clear filters from [StatelessFilters] depending
285+
Future<StatelessFilters> _clearFilters(StatelessFilters filters) async {
279286
switch (selectionMode) {
280287
case SelectionMode.single:
281288
return filters.clear([groupID]);
@@ -300,7 +307,7 @@ class _FacetList implements FacetList {
300307
}
301308

302309
@override
303-
void dispose() {
310+
void doDispose() {
304311
_log.finest('FacetList disposed');
305312
_subscriptions.cancel();
306313
}

helper_dart/lib/src/filter_state.dart

Lines changed: 91 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,116 @@
11
import 'dart:async';
22

33
import 'package:logging/logging.dart';
4+
import 'package:meta/meta.dart';
45
import 'package:rxdart/rxdart.dart';
56

7+
import 'disposable.dart';
8+
import 'disposable_mixin.dart';
69
import 'filter.dart';
710
import 'filter_group.dart';
811
import 'filters.dart';
9-
import 'immutable_filters.dart';
1012
import 'logger.dart';
1113

1214
/// [FilterState] holds one or several filters, organized in groups.
1315
/// [filters] streams filters changes of added or removed filters,
1416
/// which will be applied to searches performed by the connected Searcher.
15-
class FilterState {
17+
@sealed
18+
abstract class FilterState implements Disposable {
19+
/// FilterState's factory.
20+
factory FilterState() => _FilterState();
21+
22+
/// Filters groups stream (facet, tag, numeric and hierarchical).
23+
Stream<Filters> get filters;
24+
25+
/// Adds [filters] to the provided [groupID].
26+
void add(FilterGroupID groupID, Iterable<Filter> filters);
27+
28+
/// Overrides [filters] with the provided [map].
29+
void set(Map<FilterGroupID, Set<Filter>> map);
30+
31+
/// Removes [filters] from [groupID].
32+
void remove(FilterGroupID groupID, Iterable<Filter> filters);
33+
34+
/// Toggles [filter] in given [groupID].
35+
void toggle(FilterGroupID groupID, Filter filter);
36+
37+
/// Checks if [filter] exists in [groupID].
38+
bool contains(FilterGroupID groupID, Filter filter);
39+
40+
/// Adds [hierarchicalFilter] to given [attribute].
41+
void addHierarchical(
42+
String attribute,
43+
HierarchicalFilter hierarchicalFilter,
44+
);
45+
46+
/// Removes [HierarchicalFilter] of given [attribute].
47+
void removeHierarchical(String attribute);
48+
49+
/// Clears [groupIDs].
50+
/// If none provided, all filters will be cleared.
51+
void clear([Iterable<FilterGroupID>? groupIDs]);
52+
53+
/// Clears all except [groupIDs].
54+
void clearExcept(Iterable<FilterGroupID> groupIDs);
55+
56+
/// Get current [filters] value.
57+
Filters snapshot();
58+
59+
/// **Asynchronous** updates [filters] by applying [builder] to current
60+
/// filters value.
61+
/// Useful to apply multiple consecutive update operations without firing
62+
/// multiple filters events.
63+
Future<void> modify(AsyncFiltersBuilder builder);
64+
}
65+
66+
/// Asynchronous stateless filters builder.
67+
typedef AsyncFiltersBuilder = Future<StatelessFilters> Function(
68+
StatelessFilters filters,
69+
);
70+
71+
/// Default implementation of [FilterState].
72+
class _FilterState with DisposableMixin implements FilterState {
1673
/// Filters groups stream (facet, tag, numeric and hierarchical).
74+
@override
1775
Stream<Filters> get filters => _filters.stream.distinct();
1876

1977
/// Events logger
2078
final Logger _log = algoliaLogger('FilterState');
2179

22-
/// Hot stream controller of [ImmutableFilters].
23-
final BehaviorSubject<ImmutableFilters> _filters =
24-
BehaviorSubject.seeded(const ImmutableFilters());
80+
/// Hot stream controller of [StatelessFilters].
81+
final BehaviorSubject<StatelessFilters> _filters =
82+
BehaviorSubject.seeded(StatelessFilters());
2583

2684
/// Adds [filters] to the provided [groupID].
85+
@override
2786
void add(FilterGroupID groupID, Iterable<Filter> filters) {
2887
_modify((it) => it.add(groupID, filters));
2988
}
3089

3190
/// Overrides [filters] with the provided [map].
91+
@override
3292
void set(Map<FilterGroupID, Set<Filter>> map) {
3393
_modify((it) => it.set(map));
3494
}
3595

3696
/// Removes [filters] from [groupID].
97+
@override
3798
void remove(FilterGroupID groupID, Iterable<Filter> filters) {
3899
_modify((it) => it.remove(groupID, filters));
39100
}
40101

41102
/// Toggles [filter] in given [groupID].
103+
@override
42104
void toggle(FilterGroupID groupID, Filter filter) =>
43105
_modify((it) => it.toggle(groupID, filter));
44106

45107
/// Checks if [filter] exists in [groupID].
108+
@override
46109
bool contains(FilterGroupID groupID, Filter filter) =>
47110
_filters.value.contains(groupID, filter);
48111

49112
/// Adds [hierarchicalFilter] to given [attribute].
113+
@override
50114
void addHierarchical(
51115
String attribute,
52116
HierarchicalFilter hierarchicalFilter,
@@ -55,49 +119,61 @@ class FilterState {
55119
}
56120

57121
/// Removes [HierarchicalFilter] of given [attribute].
122+
@override
58123
void removeHierarchical(String attribute) {
59124
_modify((it) => it.removeHierarchical(attribute));
60125
}
61126

62127
/// Clears [groupIDs].
63128
/// If none provided, all filters will be cleared.
129+
@override
64130
void clear([Iterable<FilterGroupID>? groupIDs]) {
65131
_modify((it) => it.clear(groupIDs));
66132
}
67133

68134
/// Clears all except [groupIDs].
135+
@override
69136
void clearExcept(Iterable<FilterGroupID> groupIDs) {
70137
_modify((it) => it.clearExcept(groupIDs));
71138
}
72139

73140
/// Get current [filters] value.
141+
@override
74142
Filters snapshot() => _filters.value;
75143

76144
/// Dispose of underlying resources.
77-
void dispose() {
145+
@override
146+
void doDispose() {
78147
_log.finest('FilterState disposed');
79148
_filters.close();
80149
}
81150

82-
/// Updates [filters] by applying [action] to current filters value.
151+
/// **Asynchronous** updates [filters] by applying [builder] to current
152+
/// filters value.
83153
/// Useful to apply multiple consecutive update operations without firing
84154
/// multiple filters events.
85-
void _modify(ImmutableFilters Function(ImmutableFilters filters) action) {
155+
@override
156+
Future<void> modify(AsyncFiltersBuilder builder) async {
157+
if (_filters.isClosed) {
158+
_log.warning('modifying disposed instance');
159+
return;
160+
}
86161
final current = _filters.value;
87-
final updated = action(current);
162+
final updated = await builder(current);
88163
_filters.sink.add(updated);
89164
_log.finest('FilterState updated: $updated');
90165
}
91166

92-
/// **Asynchronous** updates [filters] by applying [action] to current filters
93-
/// value.
167+
/// Updates [filters] by applying [builder] to current filters value.
94168
/// Useful to apply multiple consecutive update operations without firing
95169
/// multiple filters events.
96-
Future<void> modify(
97-
Future<ImmutableFilters> Function(ImmutableFilters filters) action,
98-
) async {
170+
void _modify(StatelessFilters Function(StatelessFilters) builder) {
171+
if (_filters.isClosed) {
172+
_log.warning('modifying disposed instance');
173+
return;
174+
}
99175
final current = _filters.value;
100-
final updated = await action(current);
176+
final updated = builder(current);
101177
_filters.sink.add(updated);
102178
_log.finest('FilterState updated: $updated');
103179
}

0 commit comments

Comments
 (0)