Last active
June 11, 2025 01:03
-
-
Save rogeriomq/ed09233cb007c14e5dbcf0562c3c2667 to your computer and use it in GitHub Desktop.
Unhandled Exception - Autocomplete + Dialog
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:async'; | |
import 'package:flutter/material.dart'; | |
void main() => runApp(const DialogExampleApp()); | |
class DialogExampleApp extends StatelessWidget { | |
const DialogExampleApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
home: Scaffold( | |
appBar: AppBar(title: const Text('Dialog Sample')), | |
body: const Center(child: DialogExample()), | |
), | |
); | |
} | |
} | |
class DialogExample extends StatelessWidget { | |
const DialogExample({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
TextButton( | |
onPressed: () => showDialog<String>( | |
context: context, | |
builder: (BuildContext context) => Dialog( | |
child: Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
const Text('This is a typical dialog.'), | |
const SizedBox(height: 15), | |
AsyncAutocomplete(), | |
], | |
), | |
), | |
), | |
), | |
child: const Text('Show Dialog'), | |
), | |
const SizedBox(height: 10), | |
TextButton( | |
onPressed: () => showDialog<String>( | |
context: context, | |
builder: (BuildContext context) => Dialog.fullscreen( | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
const Text('This is a fullscreen dialog.'), | |
const SizedBox(height: 15), | |
TextButton( | |
onPressed: () { | |
Navigator.pop(context); | |
}, | |
child: const Text('Close'), | |
), | |
], | |
), | |
), | |
), | |
child: const Text('Show Fullscreen Dialog'), | |
), | |
], | |
); | |
} | |
} | |
const Duration fakeAPIDuration = Duration(seconds: 1); | |
const Duration debounceDuration = Duration(milliseconds: 500); | |
class AsyncAutocomplete extends StatefulWidget { | |
const AsyncAutocomplete({super.key}); | |
@override | |
State<AsyncAutocomplete> createState() => _AsyncAutocompleteState(); | |
} | |
class _AsyncAutocompleteState extends State<AsyncAutocomplete> { | |
// The query currently being searched for. If null, there is no pending | |
// request. | |
String? _currentQuery; | |
// The most recent options received from the API. | |
late Iterable<String> _lastOptions = <String>[]; | |
late final _Debounceable<Iterable<String>?, String> _debouncedSearch; | |
// Calls the "remote" API to search with the given query. Returns null when | |
// the call has been made obsolete. | |
Future<Iterable<String>?> _search(String query) async { | |
_currentQuery = query; | |
// In a real application, there should be some error handling here. | |
final Iterable<String> options = await _FakeAPI.search(_currentQuery!); | |
// If another search happened after this one, throw away these options. | |
if (_currentQuery != query) { | |
return null; | |
} | |
_currentQuery = null; | |
return options; | |
} | |
@override | |
void initState() { | |
super.initState(); | |
_debouncedSearch = _debounce<Iterable<String>?, String>(_search); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Column( | |
spacing: 20, | |
children: [ | |
TextButton( | |
onPressed: () { | |
Navigator.of(context).pop(); // Close the dialog | |
}, | |
child: Text('Close modal without autocomplete'), | |
), | |
Autocomplete<String>( | |
optionsBuilder: (TextEditingValue textEditingValue) async { | |
final Iterable<String>? options = await _debouncedSearch( | |
textEditingValue.text, | |
); | |
if (options == null) { | |
return _lastOptions; | |
} | |
_lastOptions = options; | |
return options; | |
}, | |
onSelected: (String selection) { | |
debugPrint('You just selected $selection'); | |
// When POP into autocomplete, generate unhandled error. | |
Navigator.of(context).pop(); // Close the dialog after selection | |
}, | |
), | |
], | |
); | |
} | |
} | |
// Mimics a remote API. | |
class _FakeAPI { | |
static const List<String> _kOptions = <String>[ | |
'aardvark', | |
'bobcat', | |
'chameleon', | |
]; | |
// Searches the options, but injects a fake "network" delay. | |
static Future<Iterable<String>> search(String query) async { | |
await Future<void>.delayed(fakeAPIDuration); // Fake 1 second delay. | |
if (query == '') { | |
return const Iterable<String>.empty(); | |
} | |
return _kOptions.where((String option) { | |
return option.contains(query.toLowerCase()); | |
}); | |
} | |
} | |
typedef _Debounceable<S, T> = Future<S?> Function(T parameter); | |
/// Returns a new function that is a debounced version of the given function. | |
/// | |
/// This means that the original function will be called only after no calls | |
/// have been made for the given Duration. | |
_Debounceable<S, T> _debounce<S, T>(_Debounceable<S?, T> function) { | |
_DebounceTimer? debounceTimer; | |
return (T parameter) async { | |
if (debounceTimer != null && !debounceTimer!.isCompleted) { | |
debounceTimer!.cancel(); | |
} | |
debounceTimer = _DebounceTimer(); | |
try { | |
await debounceTimer!.future; | |
} on _CancelException { | |
return null; | |
} | |
return function(parameter); | |
}; | |
} | |
// A wrapper around Timer used for debouncing. | |
class _DebounceTimer { | |
_DebounceTimer() { | |
_timer = Timer(debounceDuration, _onComplete); | |
} | |
late final Timer _timer; | |
final Completer<void> _completer = Completer<void>(); | |
void _onComplete() { | |
_completer.complete(); | |
} | |
Future<void> get future => _completer.future; | |
bool get isCompleted => _completer.isCompleted; | |
void cancel() { | |
_timer.cancel(); | |
_completer.completeError(const _CancelException()); | |
} | |
} | |
// An exception indicating that the timer was canceled. | |
class _CancelException implements Exception { | |
const _CancelException(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment