Want to Code Like a Senior Flutter Dev? Try These Extensions! (Part 3)
Teqani Blogs
Writer at Teqani
Debounced TextEditingController
Ever built a search box and realized your app is making a network call on every keystroke? A debounced TextEditingController waits for the user to stop typing before triggering the action, optimizing API usage and improving user experience.
It’s like sending a pizza order for each topping individually — inefficient and annoying.
That’s where a debounced TextEditingController comes in.
It waits for the user to stop typing before doing the thing, so your API (and your sanity) stay happy.
extension DebouncedTextController on TextEditingController {
void onChangedDebounced(void Function(String) callback,
{Duration delay = const Duration(milliseconds: 300)}) {
Timer? _debounce;
addListener(() {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(delay, () => callback(text));
});
}
}
Usage
controller.onChangedDebounced((value) {
print('Searching for $value');
});
Wait for Widget Size
Have you ever tried to get a widget’s size in initState only to get 0.0? Sometimes you need to wait until Flutter finishes laying things out before you can measure them. With the WaitForSize extension, you can retrieve the widget size after Flutter finishes rendering.
It’s like asking someone their height while they’re still putting on their shorts .
Sometimes you need to wait until Flutter finishes laying things out before you can measure them.
With a handy extension, you can say “Hey widget, tell me your size when you’re ready” — no more layout timing headaches.
extension WaitForSize on GlobalKey {
void onWidgetSize(Function(Size size) callback) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final context = currentContext;
if (context != null) {
final renderBox = context.findRenderObject() as RenderBox?;
if (renderBox != null) {
callback(renderBox.size);
}
}
});
}
}
Usage
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
final key = GlobalKey();
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Wait for Widget Size')),
body: Center(
child: Container(
key: key,
color: Colors.amber,
width: 200,
height: 100,
child: const Center(child: Text('Measure me!')),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
key.onWidgetSize((size) {
debugPrint('Widget size: $size');
});
},
child: const Icon(Icons.straighten),
),
),
);
}
}
Safe SetState
Avoid the dreaded "setState() called after dispose()" error. The SafeSetState extension ensures setState is only called if the widget is still mounted, preventing crashes and simplifying asynchronous operations.
We’ve all been there — you fire off an async call, the user navigates away, and bam!
Flutter yells at you: setState() called after dispose()
This tiny extension is like a bouncer for your widget’s state — it only lets setState in if the widget is still mounted. No more late-night debugging because you forgot to check.
extension SafeSetState on State {
void setStateSafe(VoidCallback fn) {
if (mounted) setState(fn);
}
}
Usage
setStateSafe(() => counter++);
Auto-Retry Futures with Delay
Handle transient API failures gracefully with the Auto-Retry Futures extension. This extension automatically retries failing API calls with a configurable delay, improving resilience and user experience. Use cautiously to avoid infinite loops!
Sometimes your API call just… fails.
Not because your code is bad (of course not ), but maybe the Wi-Fi took a coffee break.
This extension politely says, “Don’t worry, I’ll try again… and again… and again,”
with a delay between retries — until it finally works or gives up gracefully.
But a word of caution — this is your go-to for a single API call .
If you slap it on nested API calls, you’ll have retries inside retries, and soon your code will feel like Inception with network requests.
extension RetryFuture<T> on Future<T> {
Future<T> retry({
int retries = 3,
Duration delay = const Duration(seconds: 1),
}) async {
assert(retries > 0, "Retries must be greater than 0");
late Object lastError;
for (int attempt = 0; attempt < retries; attempt++) {
try {
return await this;
} catch (e) {
lastError = e;
if (attempt < retries - 1) {
await Future.delayed(delay);
}
}
}
throw lastError;
}
}
Usage.
try {
final result = await fakeApiCall(data)
.retry(retries: 3, delay: const Duration(seconds: 2));
setState(() => status = result);
} catch (e) {
setState(() => status = " Failed after retries: $e");
}
May your builds be fast, your widgets be stateless, and your extensions be magical.Until Next Time.
All blogs are certified by our company and reviewed by our specialists
Issue Number: #4870d4ca-2d00-4765-947c-cdc5cd239282