Skip to content
Open
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
4 changes: 3 additions & 1 deletion example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -221,14 +221,16 @@ class _ExampleAppState extends State<ExampleApp> {
await Future.delayed(const Duration(seconds: 5));
return actors;
},
asyncListFilter: (query, list) {
asyncListFilter: (query, list) async {
await Future.delayed(const Duration(seconds: 3));
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,
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();
}
}
85 changes: 62 additions & 23 deletions lib/searchable_listview.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// ignore_for_file: must_be_immutable

import 'dart:async';

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 @@ -82,6 +85,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 @@ -271,7 +275,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, List<T>)? asyncListFilter;
late Future<List<T>> Function(String, List<T>)? 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 Down Expand Up @@ -493,7 +501,9 @@ class _SearchableListState<T> extends State<SearchableList<T>> {
late List<T> filtredListResult = widget.initialList;
List<T> filtredAsyncListResult = [];
String searchText = '';
bool dataDownloaded = false;
int numberOfRequestLoading = 0;
bool asyncError = false;
late Debouncer _debouncer;
List<ExpansionTileController> expansionTileControllers = [];

@override
Expand All @@ -513,6 +523,29 @@ class _SearchableListState<T> extends State<SearchableList<T>> {
}
});
widget.searchTextController?.addListener(_textControllerListener);

if (widget.asyncListCallback != null) {
// Load the initial list for the async constructor
numberOfRequestLoading = 1;
WidgetsBinding.instance.addPostFrameCallback((_) async {
try {
final result = await widget.asyncListCallback!.call();
if (result != null) {
filtredAsyncListResult = result;
} else {
asyncError = true;
}
} catch (e) {
asyncError = true;
}
if (mounted) {
setState(() {
numberOfRequestLoading--;
});
}
});
_debouncer = Debouncer(milliseconds: widget.asyncDebounceTime);
}
}

@override
Expand Down Expand Up @@ -573,7 +606,7 @@ class _SearchableListState<T> extends State<SearchableList<T>> {
Expanded(
child: widget.isExpansionList
? renderExpandableListView()
: (widget.asyncListCallback != null && !dataDownloaded
: (widget.asyncListCallback != null
? renderAsyncListView()
: renderSearchableListView()),
),
Expand All @@ -582,21 +615,13 @@ class _SearchableListState<T> extends State<SearchableList<T>> {
}

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 (numberOfRequestLoading != 0) {
return widget.loadingWidget ?? const DefaultLoadingWidget();
}
return renderSearchableListView();
}

Widget renderSearchableListView() {
Expand Down Expand Up @@ -830,11 +855,25 @@ class _SearchableListState<T> extends State<SearchableList<T>> {
}
}
} else if (widget.asyncListCallback != null) {
setState(() {
filtredAsyncListResult = widget.asyncListFilter!(
value,
asyncListResult,
);
_debouncer.run(() async {
if (mounted) {
setState(() {
numberOfRequestLoading++;
});
}
try {
filtredAsyncListResult = await widget.asyncListFilter!(
value,
asyncListResult,
);
} catch (e) {
asyncError = true;
}
if (mounted) {
setState(() {
numberOfRequestLoading--;
});
}
});
} else {
setState(() {
Expand Down