Skip to content
Closed
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
25 changes: 21 additions & 4 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,11 @@ class _ExampleAppState extends State<ExampleApp> {
],
),
filter: (query) {
filteredActors = actors.where((element) => element.name.toLowerCase().contains(query.toLowerCase()) || element.lastName.toLowerCase().contains(query.toLowerCase())).toList();
filteredActors = actors
.where((element) =>
element.name.toLowerCase().contains(query.toLowerCase()) ||
element.lastName.toLowerCase().contains(query.toLowerCase()))
.toList();
return filteredActors;
},
initialList: actors,
Expand Down Expand Up @@ -238,17 +242,25 @@ class _ExampleAppState extends State<ExampleApp> {
await Future.delayed(const Duration(seconds: 5));
return actors;
},
asyncListFilter: (query, list) {
var result = actors.where((element) => element.name.contains(query) || element.lastName.contains(query)).toList();
asyncListFilter: (query, list) async {
var result = actors
.where((element) =>
element.name.contains(query) ||
element.lastName.contains(query))
.toList();
return result;
},
asyncDebounceTime: 200,
separatorBuilder: (context, index) {
return Container(
height: 30,
);
},
textStyle: const TextStyle(fontSize: 25),
emptyWidget: const EmptyView(),
loadingWidget: const Center(
child: CircularProgressIndicator(),
),
inputDecoration: InputDecoration(
labelText: "Search Actor",
fillColor: Colors.white,
Expand Down Expand Up @@ -277,7 +289,12 @@ class _ExampleAppState extends State<ExampleApp> {
);
},
filterExpansionData: (p0) {
final filteredMap = {for (final entry in mapOfActors.entries) entry.key: (mapOfActors[entry.key] ?? []).where((element) => element.name.contains(p0)).toList()};
final filteredMap = {
for (final entry in mapOfActors.entries)
entry.key: (mapOfActors[entry.key] ?? [])
.where((element) => element.name.contains(p0))
.toList()
};
return filteredMap;
},
textStyle: const TextStyle(fontSize: 25),
Expand Down
20 changes: 20 additions & 0 deletions lib/resources/debouncer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'dart:async';
import 'dart:ui';

class Debouncer {
final int milliseconds;
Timer? _timer;

Debouncer({required this.milliseconds});

void run(VoidCallback action) {
if (_timer?.isActive ?? false) {
_timer?.cancel();
}
_timer = Timer(Duration(milliseconds: milliseconds), action);
}

void dispose() {
_timer?.cancel();
}
}
130 changes: 94 additions & 36 deletions lib/searchable_listview.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// ignore_for_file: must_be_immutable

import 'package:async/async.dart';
import 'package:flutter/material.dart';
import 'package:searchable_listview/resources/arrays.dart';
import 'package:searchable_listview/resources/debouncer.dart';
import 'package:searchable_listview/widgets/default_error_widget.dart';
import 'package:searchable_listview/widgets/default_loading_widget.dart';
import 'package:searchable_listview/widgets/list_view_rendering.dart';
Expand Down Expand Up @@ -84,6 +86,7 @@ class SearchableList<T> extends StatefulWidget {
required this.asyncListCallback,
required this.asyncListFilter,
required this.itemBuilder,
this.asyncDebounceTime = 0,
this.loadingWidget,
this.errorWidget,
this.searchTextController,
Expand Down Expand Up @@ -279,7 +282,11 @@ class SearchableList<T> extends StatefulWidget {
/// Callback invoked when filtring the searchable list
/// used when providing [asyncListCallback]
/// can't be null when [asyncListCallback] isn't null
late List<T> Function(String query, List<T> list)? asyncListFilter;
late Future<List<T>> Function(String query, List<T> list)? asyncListFilter;

/// Debouncing time when typing in search field in milliseconds
/// Wait [asyncDebounceTime] milliseconds without any new entry before invoking [asyncListFilter]
int asyncDebounceTime = 0;

/// Loading widget displayed when [asyncListCallback] is loading
/// if nothing is provided in [loadingWidget] searchable list will display a [CircularProgressIndicator]
Expand All @@ -296,7 +303,8 @@ class SearchableList<T> extends StatefulWidget {
/// [expansionGroupIndex] : expansion group index
/// [listItem] the current item model that will be rendered.
/// Used only for expansion list constructor
late Widget Function(int expansionGroupIndex, T listItem)? expansionListBuilder;
late Widget Function(int expansionGroupIndex, T listItem)?
expansionListBuilder;

/// The widget to be displayed when the filter returns an empty list.
/// Defaults to `const SizedBox.shrink()`.
Expand Down Expand Up @@ -498,29 +506,46 @@ class SearchableList<T> extends StatefulWidget {
class _SearchableListState<T> extends State<SearchableList<T>> {
/// Create scroll controller instance
/// attached to the listview widget
late ScrollController scrollController = widget.scrollController ?? ScrollController();
late ScrollController scrollController =
widget.scrollController ?? ScrollController();
List<T> asyncListResult = [];
late List<T> filtredListResult = widget.initialList;
List<T> filtredAsyncListResult = [];
String searchText = '';
bool dataDownloaded = false;
bool asyncError = false;
// Current async task running, null otherwise
CancelableOperation<List<T>?>? _activeOperation;
late Debouncer? _debouncer;
List<ExpansionTileController> expansionTileControllers = [];

@override
void initState() {
super.initState();
scrollController.addListener(() {
if (widget.closeKeyboardWhenScrolling && widget.focusNode?.hasFocus == true) {
if (widget.closeKeyboardWhenScrolling &&
widget.focusNode?.hasFocus == true) {
FocusScope.of(context).requestFocus(FocusNode());
}
if (widget.onPaginate != null && scrollController.position.pixels == scrollController.position.maxScrollExtent) {
if (widget.onPaginate != null &&
scrollController.position.pixels ==
scrollController.position.maxScrollExtent) {
setState(() {
widget.onPaginate?.call();
});
}
});

if (widget.asyncListCallback != null) {
// Load the initial list for the async constructor
WidgetsBinding.instance.addPostFrameCallback((_) async {
asyncFilter("");
if (mounted) setState(() {});
});
_debouncer = Debouncer(milliseconds: widget.asyncDebounceTime);
}
if (widget.searchMode == SearchMode.onEdit) {
widget.searchTextController?.addListener(_textControllerListener);
widget.searchTextController?.addListener(textControllerListener);
}
}

Expand All @@ -529,12 +554,12 @@ class _SearchableListState<T> extends State<SearchableList<T>> {
if (widget.scrollController == null) {
scrollController.dispose();
}
widget.searchTextController?.removeListener(_textControllerListener);
widget.searchTextController?.removeListener(textControllerListener);
super.dispose();
}

@override
void didUpdateWidget(covariant SearchableList<T> oldWidget) {
void didUpdateWidget(SearchableList<T> oldWidget) {
super.didUpdateWidget(oldWidget);

// Reload the list if the initialList changed
Expand All @@ -547,7 +572,8 @@ class _SearchableListState<T> extends State<SearchableList<T>> {
filterList(searchText);
}
}
if (widget.isExpansionList && oldWidget.expansionListData != widget.expansionListData) {
if (widget.isExpansionList &&
oldWidget.expansionListData != widget.expansionListData) {
filterList(searchText);
}
}
Expand All @@ -557,7 +583,10 @@ class _SearchableListState<T> extends State<SearchableList<T>> {
return widget.sliverScrollEffect
? renderSliverEffect()
: Column(
verticalDirection: widget.searchTextPosition == SearchTextPosition.top ? VerticalDirection.down : VerticalDirection.up,
verticalDirection:
widget.searchTextPosition == SearchTextPosition.top
? VerticalDirection.down
: VerticalDirection.up,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.showSearchField)
Expand All @@ -570,32 +599,30 @@ class _SearchableListState<T> extends State<SearchableList<T>> {
),
),
Expanded(
child: widget.isExpansionList ? renderExpandableListView() : (widget.asyncListCallback != null && !dataDownloaded ? renderAsyncListView() : renderSearchableListView()),
child: widget.isExpansionList
? renderExpandableListView()
: (widget.asyncListCallback != null && !dataDownloaded
? renderAsyncListView()
: renderSearchableListView()),
),
],
);
}

Widget renderAsyncListView() {
return FutureBuilder(
future: widget.asyncListCallback!.call(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return widget.loadingWidget ?? const DefaultLoadingWidget();
}
dataDownloaded = true;
if (snapshot.data == null) {
return widget.errorWidget ?? const DefaultErrorWidget();
}
asyncListResult = snapshot.data as List<T>;
filtredAsyncListResult = asyncListResult;
return renderSearchableListView();
},
);
if (asyncError) {
return widget.errorWidget ?? const DefaultErrorWidget();
}
if (_activeOperation != null) {
return widget.loadingWidget ?? const DefaultLoadingWidget();
}
return renderSearchableListView();
}

Widget renderSearchableListView() {
List<T> renderedList = widget.asyncListCallback != null ? filtredAsyncListResult : filtredListResult;
List<T> renderedList = widget.asyncListCallback != null
? filtredAsyncListResult
: filtredListResult;
return buildSearchableListView(
list: renderedList,
);
Expand Down Expand Up @@ -649,7 +676,8 @@ class _SearchableListState<T> extends State<SearchableList<T>> {
}

Widget renderExpandableListView() {
if (widget.expansionListData.isEmpty || widget.expansionListData.values.every((element) => element.isEmpty)) {
if (widget.expansionListData.isEmpty ||
widget.expansionListData.values.every((element) => element.isEmpty)) {
return widget.emptyWidget ?? const SizedBox.shrink();
} else {
expansionTileControllers.addAll(
Expand Down Expand Up @@ -698,7 +726,10 @@ class _SearchableListState<T> extends State<SearchableList<T>> {
return widget.scrollDirection == Axis.horizontal
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
verticalDirection: widget.searchTextPosition == SearchTextPosition.top ? VerticalDirection.down : VerticalDirection.up,
verticalDirection:
widget.searchTextPosition == SearchTextPosition.top
? VerticalDirection.down
: VerticalDirection.up,
children: [
if (widget.showSearchField)
Padding(
Expand Down Expand Up @@ -755,7 +786,8 @@ class _SearchableListState<T> extends State<SearchableList<T>> {
searchText = value;
if (widget.isExpansionList) {
setState(() {
widget.expansionListData = widget.filterExpansionData?.call(value) ?? {};
widget.expansionListData =
widget.filterExpansionData?.call(value) ?? {};
});
for (var controller in expansionTileControllers) {
try {
Expand All @@ -766,11 +798,11 @@ class _SearchableListState<T> extends State<SearchableList<T>> {
}
}
} else if (widget.asyncListCallback != null) {
setState(() {
filtredAsyncListResult = widget.asyncListFilter!(
value,
asyncListResult,
);
// Debouncer always has a value in this case
_debouncer!.run(() async {
_activeOperation?.cancel();
asyncFilter(value);
if (mounted) setState(() {});
});
} else {
setState(() {
Expand All @@ -791,12 +823,38 @@ class _SearchableListState<T> extends State<SearchableList<T>> {
}
}

void _textControllerListener() {
void textControllerListener() {
if (searchText != widget.searchTextController?.text) {
filterList(widget.searchTextController?.text ?? '');
}
}

Future<void> asyncFilter(String value) async {
final operation = CancelableOperation.fromFuture(
widget.asyncListFilter!(
value,
asyncListResult,
),
);
_activeOperation = operation;
try {
final result = await operation.valueOrCancellation();
if (result != null && !operation.isCanceled) {
filtredAsyncListResult = result;
}
} catch (e) {
if (!operation.isCanceled) {
asyncError = true;
}
} finally {
// Make sure to not interrupt an other operation
if (_activeOperation == operation) {
_activeOperation = null;
}
}
setState(() {});
}

/// Builds and returns a SearchTextField widget with all necessary parameters
Widget _buildSearchTextField() {
return SearchTextField(
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ environment:
sdk: '>=2.12.0 <4.0.0'
flutter: '>=1.17.0'
dependencies:
async: ^2.13.1
flutter:
sdk: flutter
dev_dependencies:
Expand Down
Loading
Loading