Skip to content

Instantly share code, notes, and snippets.

@jaumard
Last active March 24, 2021 21:05
Show Gist options
  • Save jaumard/13681f287314e16e46942e57b520d15e to your computer and use it in GitHub Desktop.
Save jaumard/13681f287314e16e46942e57b520d15e to your computer and use it in GitHub Desktop.
Simple Autocomplete Flutter
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Test'),),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: AutocompleteField<String>(
decoration: InputDecoration(labelText: 'Test'),
delegate: (query) async {
return ['Test', 'Test2', 'Test3'];
},
itemBuilder: (context, entry) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text(entry),
);
},
onItemSelected: (entry) {
print(entry);
},
),
),
),
);
}
}
typedef AutocompleteDelegate<T> = Future<List<T>> Function(String query);
class AutocompleteField<T> extends StatefulWidget {
final InputDecoration decoration;
final FocusNode focusNode;
final bool autofocus;
final int maxLines;
final int itemExtent;
final TextInputType keyboardType;
final TextEditingController controller;
final Widget Function(BuildContext context, T entry) itemBuilder;
final AutocompleteDelegate<T> delegate;
final Function(T entry) onItemSelected;
const AutocompleteField(
{Key key, this.itemExtent, this.keyboardType, this.maxLines = 1, this.autofocus = false, this.controller, @required this.onItemSelected, @required this.itemBuilder, @required this.delegate, this.focusNode, this.decoration})
: super(key: key);
@override
_AutocompleteFieldState<T> createState() => _AutocompleteFieldState<T>();
}
class _AutocompleteFieldState<T> extends State<AutocompleteField<T>> {
FocusNode _focusNode;
TextEditingController _controller;
AutocompleteBloc _bloc;
OverlayEntry _overlayEntry;
final LayerLink _layerLink = LayerLink();
@override
void initState() {
_controller = widget.controller ?? TextEditingController(text: '');
_bloc = AutocompleteBloc<T>(widget.delegate);
_bloc.query.add(_controller.text);
_focusNode = widget.focusNode ?? FocusNode();
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
if (_controller.text.length >= 3) {
_showOverlay();
}
} else {
_hideOverlay();
}
});
super.initState();
}
void _showOverlay() {
if (_overlayEntry != null) {
_hideOverlay();
}
_overlayEntry = _createOverlayEntry();
Overlay.of(context).insert(_overlayEntry);
}
void _hideOverlay() {
if (_overlayEntry != null) {
_overlayEntry.remove();
_overlayEntry = null;
}
}
OverlayEntry _createOverlayEntry() {
// ignore: avoid_as
final renderBox = context.findRenderObject() as RenderBox;
final size = renderBox.size;
return OverlayEntry(
builder: (context) => Positioned(
width: size.width,
child: CompositedTransformFollower(
link: _layerLink,
showWhenUnlinked: false,
offset: Offset(0.0, size.height + 5.0),
child: Material(
elevation: 4.0,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: (widget.itemExtent ?? size.height) * 3),
child: StreamBuilder<List<T>>(
stream: _bloc.results,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
return Scrollbar(
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemBuilder: (context, index) {
final data = snapshot.data;
final entry = data[index];
return InkWell(
child: widget.itemBuilder(context, entry),
onTap: () {
widget.onItemSelected(entry);
_hideOverlay();
},
);
},
itemCount: snapshot.data.length,
),
);
},
),
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: _layerLink,
child: TextField(
focusNode: _focusNode,
maxLines: widget.maxLines,
autofocus: widget.autofocus,
keyboardType: widget.keyboardType,
controller: _controller,
onChanged: (value) {
if (value.length >= 3) {
_bloc.query.add(value);
if (_overlayEntry == null) {
_showOverlay();
}
} else {
_hideOverlay();
}
},
decoration: widget.decoration,
),
);
}
@override
void dispose() {
_bloc.dispose();
super.dispose();
}
}
class AutocompleteBloc<T> {
final AutocompleteDelegate<T> delegate;
final _query = BehaviorSubject<String>();
final _results = BehaviorSubject<List<T>>();
AutocompleteBloc(this.delegate) {
_query.distinct().debounceTime(Duration(milliseconds: 500)).listen(_search);
}
void _search(String query) async {
final results = await delegate(query);
_results.add(results);
}
Sink<String> get query => _query.sink;
Stream<List<T>> get results => _results.stream;
Future<void> dispose() async {
await _query.close();
await _results.close();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment