diff --git a/README.md b/README.md index 051c165..9917430 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,20 @@ # Easy Autocomplete -Buy Me A Pizza + + Buy Me A Pizza + A Flutter plugin to handle input autocomplete suggestions ## Preview -![Preview](https://raw.githubusercontent.com/4inka/flutter_easy_autocomplete/main/preview/preview.gif) +![Preview](https://raw.githubusercontent.com/4inka/flutter_easy_autocomplete/main/preview/preview1.gif) ## ToDo * Add validation functionality -* Add asynchronous suggestions fetch * Add possibility to show empty message when no suggestion is found +## Done +* Add asynchronous suggestions fetch ## Usage @@ -20,10 +23,12 @@ In the `pubspec.yaml` of your flutter project, add the following dependency: ``` yaml dependencies: ... - easy_autocomplete: ^1.1.0 + easy_autocomplete: ^1.2.0 ``` -You can create a simple autocomplete input widget with the following example: +### Basic example + +You can create a simple autocomplete input widget as shown in first preview with the following example: ``` dart import 'package:easy_autocomplete/easy_autocomplete.dart'; @@ -61,7 +66,8 @@ class MyApp extends StatelessWidget { } ``` -
+### Example with customized style + You can customize other aspects of the autocomplete widget such as the suggestions text style, background color and others as shown in example below: ``` dart @@ -127,12 +133,63 @@ The above example will generate something like below preview: ![Preview](https://raw.githubusercontent.com/4inka/flutter_easy_autocomplete/main/preview/preview2.gif) -
+### Example with asynchronous data fetch + +To create a autocomplete field that fetches data asynchronously you will need to use `asyncSuggestions` instead of `suggestions` +``` dart +import 'package:easy_autocomplete/easy_autocomplete.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + Future> _fetchSuggestions(String searchValue) async { + await Future.delayed(Duration(milliseconds: 750)); + List _suggestions = ['Afeganistan', 'Albania', 'Algeria', 'Australia', 'Brazil', 'German', 'Madagascar', 'Mozambique', 'Portugal', 'Zambia']; + List _filteredSuggestions = _suggestions.where((element) { + return element.toLowerCase().contains(searchValue.toLowerCase()); + }).toList(); + return _filteredSuggestions; + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Example', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: SafeArea( + child: Scaffold( + appBar: AppBar( + title: Text('Example') + ), + body: Container( + padding: EdgeInsets.all(10), + alignment: Alignment.center, + child: EasyAutocomplete( + asyncSuggestions: (searchValue) async => _fetchSuggestions(searchValue), + onChanged: (value) => print(value) + ) + ) + ) + ) + ); + } +} +``` + +The above example will generate something like below preview: + +![Preview](https://raw.githubusercontent.com/4inka/flutter_easy_autocomplete/main/preview/preview3.gif) ## API | Attribute | Type | Required | Description | Default value | |:---|:---|:---:|:---|:---| -| suggestions | `List` | :heavy_check_mark: | The list of suggestions to be displayed | | +| suggestions | `List` | :x: | The list of suggestions to be displayed | | +| asyncSuggestions | `Future> Function(String)` | :x: | Fetches list of suggestions from a Future | | | controller | `TextEditingController` | :x: | Text editing controller | | | decoration | `InputDecoration` | :x: | Can be used to decorate the input | | | onChanged | `Function(String)` | :x: | Function that handles the changes to the input | | @@ -142,8 +199,10 @@ The above example will generate something like below preview: | autofocus | `bool` | :x: | Determines if should gain focus on screen open | false | | keyboardType | `TextInputType` | :x: | Can be used to set different keyboardTypes to your field | TextInputType.text | | cursorColor | `Color` | :x: | Can be used to set a custom color to the input cursor | Colors.blue | +| inputTextStyle | `TextStyle` | :x: | Can be used to set custom style to the suggestions textfield | | | suggestionTextStyle | `TextStyle` | :x: | Can be used to set custom style to the suggestions list text | | | suggestionBackgroundColor | `Color` | :x: | Can be used to set custom background color to suggestions list | | +| debounceDuration | `Duration` | :x: | Used to set the debounce time for async data fetch | Duration(milliseconds: 400) | ## Issues & Suggestions If you encounter any issue you or want to leave a suggestion you can do it by filling an [issue](https://github.com/4inka/flutter_easy_autocomplete/issues). diff --git a/lib/easy_autocomplete.dart b/lib/easy_autocomplete.dart index c675b1e..d478d7d 100644 --- a/lib/easy_autocomplete.dart +++ b/lib/easy_autocomplete.dart @@ -27,13 +27,17 @@ library easy_autocomplete; +import 'dart:async'; + import 'package:easy_autocomplete/widgets/filterable_list.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class EasyAutocomplete extends StatefulWidget { /// The list of suggestions to be displayed - final List suggestions; + final List? suggestions; + /// Fetches list of suggestions from a Future + final Future> Function(String searchValue)? asyncSuggestions; /// Text editing controller final TextEditingController? controller; /// Can be used to decorate the input @@ -52,14 +56,19 @@ class EasyAutocomplete extends StatefulWidget { final TextInputType keyboardType; /// Can be used to set a custom color to the input cursor final Color? cursorColor; + /// Can be used to set custom style to the suggestions textfield + final TextStyle inputTextStyle; /// Can be used to set custom style to the suggestions list text final TextStyle suggestionTextStyle; /// Can be used to set custom background color to suggestions list final Color? suggestionBackgroundColor; + /// Used to set the debounce time for async data fetch + final Duration debounceDuration; /// Creates a autocomplete widget to help you manage your suggestions EasyAutocomplete({ - required this.suggestions, + this.suggestions, + this.asyncSuggestions, this.controller, this.decoration = const InputDecoration(), this.onChanged, @@ -69,10 +78,13 @@ class EasyAutocomplete extends StatefulWidget { this.textCapitalization = TextCapitalization.sentences, this.keyboardType = TextInputType.text, this.cursorColor, + this.inputTextStyle = const TextStyle(), this.suggestionTextStyle = const TextStyle(), - this.suggestionBackgroundColor + this.suggestionBackgroundColor, + this.debounceDuration = const Duration(milliseconds: 400) }) : assert(onChanged != null || controller != null, 'onChanged and controller parameters cannot be both null at the same time'), - assert(!(controller != null && initialValue != null), 'controller and initialValue cannot be used at the same time'); + assert(!(controller != null && initialValue != null), 'controller and initialValue cannot be used at the same time'), + assert(suggestions != null && asyncSuggestions == null || suggestions == null && asyncSuggestions != null, 'suggestions and asyncSuggestions cannot be both null or have values at the same time'); @override State createState() => _EasyAutocompleteState(); @@ -82,8 +94,11 @@ class _EasyAutocompleteState extends State { final LayerLink _layerLink = LayerLink(); late TextFormField _textFormField; bool _hasOpenedOverlay = false; + bool _isLoading = false; OverlayEntry? _overlayEntry; List _suggestions = []; + Timer? _debounce; + String _previousAsyncSearchText = ''; @override void initState() { @@ -96,6 +111,7 @@ class _EasyAutocompleteState extends State { textCapitalization: widget.textCapitalization, keyboardType: widget.keyboardType, cursorColor: widget.cursorColor ?? Colors.blue, + style: widget.inputTextStyle, onChanged: (value) { openOverlay(); widget.onChanged!(value); @@ -128,6 +144,7 @@ class _EasyAutocompleteState extends State { showWhenUnlinked: false, offset: Offset(0.0, size.height + 5.0), child: FilterableList( + loading: _isLoading, items: _suggestions, suggestionTextStyle: widget.suggestionTextStyle, suggestionBackgroundColor: widget.suggestionBackgroundColor, @@ -160,10 +177,31 @@ class _EasyAutocompleteState extends State { } } - void updateSuggestions(String input) { - _suggestions = widget.suggestions.where((element) { - return element.toLowerCase().contains(input.toLowerCase()); - }).toList(); + Future updateSuggestions(String input) async { + rebuildOverlay(); + if (widget.suggestions != null) { + _suggestions = widget.suggestions!.where((element) { + return element.toLowerCase().contains(input.toLowerCase()); + }).toList(); + rebuildOverlay(); + } + else if (widget.asyncSuggestions != null) { + setState(() => _isLoading = true); + if (_debounce != null && _debounce!.isActive) _debounce!.cancel(); + _debounce = Timer(widget.debounceDuration, () async { + if (_previousAsyncSearchText != input || _previousAsyncSearchText.isEmpty || input.isEmpty) { + _suggestions = await widget.asyncSuggestions!(input); + setState(() { + _isLoading = false; + _previousAsyncSearchText = input; + }); + rebuildOverlay(); + } + }); + } + } + + void rebuildOverlay() { if(_overlayEntry != null) { _overlayEntry!.markNeedsBuild(); } @@ -194,6 +232,7 @@ class _EasyAutocompleteState extends State { void dispose() { if (_overlayEntry != null) _overlayEntry!.dispose(); if (widget.controller != null) widget.controller!.dispose(); + if (_debounce != null) _debounce?.cancel(); super.dispose(); } } \ No newline at end of file diff --git a/lib/widgets/filterable_list.dart b/lib/widgets/filterable_list.dart index 83b61c1..9fed908 100644 --- a/lib/widgets/filterable_list.dart +++ b/lib/widgets/filterable_list.dart @@ -34,6 +34,7 @@ class FilterableList extends StatelessWidget { final double maxListHeight; final TextStyle suggestionTextStyle; final Color? suggestionBackgroundColor; + final bool loading; FilterableList({ required this.items, @@ -41,7 +42,8 @@ class FilterableList extends StatelessWidget { this.elevation = 5, this.maxListHeight = 150, this.suggestionTextStyle = const TextStyle(), - this.suggestionBackgroundColor + this.suggestionBackgroundColor, + this.loading = false }); @override @@ -53,12 +55,20 @@ class FilterableList extends StatelessWidget { maxHeight: maxListHeight ), child: Visibility( - visible: items.isNotEmpty, + visible: items.isNotEmpty || loading, child: ListView.builder( shrinkWrap: true, padding: EdgeInsets.zero, - itemCount: items.length, + itemCount: loading ? 1 : items.length, itemBuilder: (context, index) { + if (loading) { + return Container( + alignment: Alignment.center, + padding: EdgeInsets.all(10), + child: CircularProgressIndicator() + ); + } + return Material( color: suggestionBackgroundColor ?? Colors.transparent, child: InkWell( diff --git a/preview/preview.gif b/preview/preview1.gif similarity index 100% rename from preview/preview.gif rename to preview/preview1.gif diff --git a/preview/preview3.gif b/preview/preview3.gif new file mode 100644 index 0000000..78b866e Binary files /dev/null and b/preview/preview3.gif differ