10 Essential Flutter Prompts: From Widgets and State Management to Advanced Animations
Building a production-grade Flutter application is rarely about writing every line from scratch. The real craft lies in knowing what to ask — whether you’re querying a code assistant, an AI pair programmer, or just structuring your own thought process. This article is a curated collection of 10 essential prompts for Flutter development, organized by difficulty. Each prompt is a task that turns a vague idea into executable code, covering widgets, state management patterns (Bloc, Riverpod), and high-performance animations.
We’ll walk through real-world scenarios, explain the reasoning behind each prompt, and show you the expected output. By the end, you’ll have a reusable toolkit of prompts that save hours of boilerplate and debugging.
Why Prompts Matter in Flutter
Flutter’s declarative UI paradigm and rich widget library give developers immense power — but also complexity. A well-structured prompt does three things:
- Reduces context switching: Instead of jumping between docs and code, you get a focused solution.
- Enforces best practices: Prompts can encode patterns like separation of concerns, immutable state, and reactive updates.
- Accelerates prototyping: A single prompt can generate an entire widget tree with state management wiring.
Consider a typical scenario: you need a reusable form with validation, a loading indicator, and error handling. Without a prompt, you’d write 100+ lines of code. With a precise prompt, you get a scaffold in seconds.
How to Use These Prompts
Each prompt follows the format:
- Task: A clear, measurable goal.
- Prompt: The exact text you’d feed into an AI coding assistant (like GitHub Copilot, Cursor, or an LLM).
- Example Result: A simplified code snippet or widget tree that demonstrates the output.
We assume you have a basic Flutter environment set up. For state management, we focus on Bloc and Riverpod — the two most popular solutions as of mid-2026, according to the official Flutter package repository stats (pub.dev).
Let’s dive in.
Basic Prompts: Widgets and Layout
1. Responsive Breakpoint Layout
Task: Create a layout that adapts from a single-column mobile view to a two-column tablet view.
Prompt:
Write a Flutter widget that displays a list of cards. On screens narrower than 600dp, show one card per row. On wider screens, show two cards per row. Use LayoutBuilder and provide a sample card with an icon, title, and subtitle.
Example Result:
import 'package:flutter/material.dart';
class ResponsiveCardGrid extends StatelessWidget {
final List<Map<String, String>> items;
ResponsiveCardGrid({required this.items});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 600) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => _buildCard(items[index]),
);
} else {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 3,
),
itemCount: items.length,
itemBuilder: (context, index) => _buildCard(items[index]),
);
}
},
);
}
Widget _buildCard(Map<String, String> item) {
return Card(
margin: EdgeInsets.all(8),
child: ListTile(
leading: Icon(Icons.article),
title: Text(item['title'] ?? ''),
subtitle: Text(item['subtitle'] ?? ''),
),
);
}
}
Why this works: LayoutBuilder gives you the parent’s constraints, making it the most reliable way to create breakpoints without media query assumptions.
2. Custom TextFormField with Validation
Task: Build an email input field that validates on submit and shows an error message.
Prompt:
Create a reusable Flutter widget called EmailInputField. It should have a TextEditingController, a FormFieldValidator that checks for a valid email pattern, and display an error text in red below the field. Include a clear button when the field has text.
Example Result:
class EmailInputField extends StatelessWidget {
final TextEditingController controller;
EmailInputField({required this.controller});
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: 'Email',
suffixIcon: IconButton(
icon: Icon(Icons.clear),
onPressed: () => controller.clear(),
),
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter an email';
}
final emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+$');
if (!emailRegex.hasMatch(value)) {
return 'Please enter a valid email';
}
return null;
},
);
}
}
Key insight: Using TextFormField inside a Form widget automatically enables validation on submit, but the prompt explicitly asks for a validator — ensuring the widget is self-contained.
Intermediate Prompts: State Management with Bloc and Riverpod
3. Bloc Counter (Classic Example, Real-World Twist)
Task: Implement a counter with increment, decrement, and reset actions using the Bloc pattern. Include loading and error states.
Prompt:
Write a Flutter Bloc that manages an integer counter. The states should be: CounterInitial, CounterLoading, CounterValue(int value), CounterError(String message). The events: Increment, Decrement, Reset. Provide a simple UI that dispatches these events and displays the state. Use flutter_bloc package.
Example Result:
// counter_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
// States
abstract class CounterState {}
class CounterInitial extends CounterState {}
class CounterLoading extends CounterState {}
class CounterValue extends CounterState {
final int value;
CounterValue(this.value);
}
class CounterError extends CounterState {
final String message;
CounterError(this.message);
}
// Events
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}
class Reset extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterInitial()) {
on<Increment>((event, emit) {
emit(CounterLoading());
try {
final current = state is CounterValue ? (state as CounterValue).value : 0;
emit(CounterValue(current + 1));
} catch (e) {
emit(CounterError(e.toString()));
}
});
on<Decrement>((event, emit) {
emit(CounterLoading());
try {
final current = state is CounterValue ? (state as CounterValue).value : 0;
emit(CounterValue(current - 1));
} catch (e) {
emit(CounterError(e.toString()));
}
});
on<Reset>((event, emit) {
emit(CounterInitial());
});
}
}
Real‑world nuance: The prompt explicitly asks for loading and error states. In production, you’d often wrap async operations (e.g., API calls) with these states to show spinners and error banners. ASI Biont supports connecting to external APIs through Bloc patterns — see asibiont.com/courses for more details.
4. Riverpod AsyncNotifier for API Fetch
Task: Fetch a list of users from a REST API using Riverpod’s AsyncNotifier, handle loading/error/data states, and display them in a ListView.
Prompt:
Using Flutter Riverpod (riverpod package), create an AsyncNotifier that fetches a list of users from 'https://jsonplaceholder.typicode.com/users'. Define a User model with id, name, and email. Build a widget that shows a CircularProgressIndicator while loading, an error message on failure, and a ListView of User tiles on success. Use family provider if you want to pass a search query.
Example Result:
// user_model.dart
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
}
// user_provider.dart
import 'package:riverpod/riverpod.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
class UserNotifier extends AsyncNotifier<List<User>> {
@override
Future<List<User>> build() async {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
if (response.statusCode == 200) {
final List<dynamic> jsonList = json.decode(response.body);
return jsonList.map((e) => User.fromJson(e)).toList();
} else {
throw Exception('Failed to load users');
}
}
}
final userProvider = AsyncNotifierProvider<UserNotifier, List<User>>(() => UserNotifier());
// user_list_screen.dart
class UserListScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(userProvider);
return usersAsync.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
);
},
),
);
}
}
Why Riverpod: Riverpod’s AsyncNotifier eliminates the need for separate state classes — the when method handles loading/error/data automatically. This is a cleaner pattern than Bloc for simple API calls.
5. Debounced Search with Riverpod
Task: Create a search field that debounces input by 300ms and filters a list of items.
Prompt:
Implement a Riverpod provider that takes a search query string and returns a filtered list of strings. The query should be debounced by 300 milliseconds. Create a text field that updates the query, and display the filtered results in a list. Use StateNotifier or AsyncNotifier.
Example Result:
// search_provider.dart
final searchQueryProvider = StateProvider<String>((ref) => '');
final debouncedQueryProvider = FutureProvider<String>((ref) async {
final query = ref.watch(searchQueryProvider);
await Future.delayed(Duration(milliseconds: 300));
return query;
});
final filteredItemsProvider = FutureProvider<List<String>>((ref) {
final debouncedQuery = ref.watch(debouncedQueryProvider).value ?? '';
final allItems = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry', 'Fig', 'Grape'];
if (debouncedQuery.isEmpty) return allItems;
return allItems.where((item) => item.toLowerCase().contains(debouncedQuery.toLowerCase())).toList();
});
// usage in widget
final query = ref.watch(searchQueryProvider.notifier);
TextField(
onChanged: (value) => query.state = value,
);
ref.watch(filteredItemsProvider).when(
data: (items) => ListView(...),
...
);
Real‑world application: Debouncing is critical for search-as-you-type UIs to avoid hammering APIs. The prompt encodes a clean separation: query state, debounce timer, and filtered computation.
Advanced Prompts: Animations and Performance
6. Custom Implicit Animation with TweenAnimationBuilder
Task: Animate a widget’s opacity and scale simultaneously when a button is pressed, using TweenAnimationBuilder.
Prompt:
Create a Flutter widget that contains a blue square. When the user taps a button, the square should animate to double its size and become fully transparent, then back to original size and full opacity. Use TweenAnimationBuilder with a duration of 500ms and easeInOut curve. The animation should be toggled on each button press.
Example Result:
class AnimatedSquare extends StatefulWidget {
@override
_AnimatedSquareState createState() => _AnimatedSquareState();
}
class _AnimatedSquareState extends State<AnimatedSquare> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 1.0, end: _expanded ? 2.0 : 1.0),
duration: Duration(milliseconds: 500),
curve: Curves.easeInOut,
builder: (context, scale, child) {
return Opacity(
opacity: 1.0 - (scale - 1.0), // map scale 1..2 to opacity 1..0
child: Transform.scale(
scale: scale,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
);
},
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => setState(() => _expanded = !_expanded),
child: Text('Animate'),
),
],
);
}
}
Expert note: TweenAnimationBuilder is an implicit animation widget — you don’t need to manage an AnimationController. This is perfect for simple, self-contained animations.
7. Staggered Animation with AnimationController
Task: Create a staggered entrance animation for a list of items, where each item fades in and slides up with a delay of 100ms between items.
Prompt:
Write a Flutter widget that displays a column of 5 text items. When the widget first appears, each item should animate in sequentially: first item starts at 0ms, second at 100ms, third at 200ms, etc. Each item should fade from 0 to 1 opacity and slide up from 20 pixels below its final position. Use an AnimationController with a total duration of 1 second.
Example Result:
class StaggeredList extends StatefulWidget {
@override
_StaggeredListState createState() => _StaggeredListState();
}
class _StaggeredListState extends State<StaggeredList> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
)..forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final items = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5'];
return Column(
children: List.generate(items.length, (index) {
final delay = index * 0.1; // 100ms delay per item
final intervalStart = delay;
final intervalEnd = delay + 0.2; // each item animates over 200ms
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
final double? value = _controller.value;
final double? clamped = ((value - intervalStart) / (intervalEnd - intervalStart)).clamp(0.0, 1.0);
return Opacity(
opacity: clamped,
child: Transform.translate(
offset: Offset(0, 20 * (1 - clamped)),
child: Text(items[index], style: TextStyle(fontSize: 24)),
),
);
},
);
}),
);
}
}
Performance tip: For long lists, consider using SliverAnimatedList or a ListView with item animations, as building many AnimatedBuilder widgets can impact frame rate.
8. Hero Animation Between Screens
Task: Animate a shared image from a list screen to a detail screen using Hero widget.
Prompt:
Create two Flutter screens. Screen 1 shows a grid of product images. Screen 2 shows a detailed view of the selected image. Use Hero widget with a unique tag to animate the image transition. Images can be placeholder NetworkImages.
Example Result:
// screen1.dart
Hero(
tag: 'product_${product.id}',
child: Image.network(product.imageUrl),
)
// screen2.dart
Scaffold(
appBar: AppBar(),
body: Hero(
tag: 'product_${product.id}',
child: Image.network(product.imageUrl, width: double.infinity, fit: BoxFit.cover),
),
)
Key requirement: Both Hero widgets must share the exact same tag string. The prompt makes this explicit by using a unique identifier.
Expert Prompts: Complex State Flows and Custom RenderObjects
9. Bloc with Side Effects (BlocListener)
Task: When a user logs in successfully, navigate to a home screen and show a snackbar. Use BlocListener to handle side effects without rebuilding the UI.
Prompt:
Implement a Bloc for login. States: LoginInitial, LoginLoading, LoginSuccess(String token), LoginFailure(String error). Events: LoginSubmitted(String username, String password). In the UI, use BlocListener to navigate to a new screen on LoginSuccess and show a SnackBar on LoginFailure. Use flutter_bloc package.
Example Result:
// login_bloc.dart
class LoginBloc extends Bloc<LoginEvent, LoginState> {
LoginBloc() : super(LoginInitial()) {
on<LoginSubmitted>((event, emit) async {
emit(LoginLoading());
try {
// Simulate API call
await Future.delayed(Duration(seconds: 2));
if (event.username == 'admin' && event.password == 'admin') {
emit(LoginSuccess('fake_token_123'));
} else {
emit(LoginFailure('Invalid credentials'));
}
} catch (e) {
emit(LoginFailure(e.toString()));
}
});
}
}
// UI
BlocListener<LoginBloc, LoginState>(
listener: (context, state) {
if (state is LoginSuccess) {
Navigator.pushReplacementNamed(context, '/home');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Login successful')),
);
} else if (state is LoginFailure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.error)),
);
}
},
child: BlocBuilder<LoginBloc, LoginState>(
builder: (context, state) {
if (state is LoginLoading) return CircularProgressIndicator();
return Form(...);
},
),
);
Why separate listener: BlocListener runs only when the state changes, not on every rebuild — perfect for one-time side effects like navigation.
10. Custom Painter for Real-Time Graph
Task: Draw a smooth line chart that updates every second with random data points, animating smoothly.
Prompt:
Create a Flutter CustomPainter that draws a line chart. The data is a list of double values. The painter should draw a smooth path using quadratic bezier curves, fill the area under the line with a semi-transparent color, and draw circles at each data point. Then create a StatefulWidget that updates the data list every second and calls repaint. Use an AnimationController to interpolate between old and new data.
Example Result:
class LineChartPainter extends CustomPainter {
final List<double> data;
final double animationValue; // 0.0 to 1.0
LineChartPainter({required this.data, required this.animationValue});
@override
void paint(Canvas canvas, Size size) {
final paintLine = Paint()
..color = Colors.blue
..strokeWidth = 2
..style = PaintingStyle.stroke;
final paintFill = Paint()
..color = Colors.blue.withOpacity(0.2)
..style = PaintingStyle.fill;
final path = Path();
final stepX = size.width / (data.length - 1);
final maxY = data.reduce((a, b) => a > b ? a : b);
for (int i = 0; i < data.length; i++) {
final x = i * stepX;
final y = size.height - (data[i] / maxY) * size.height * animationValue;
if (i == 0) {
path.moveTo(x, y);
} else {
// Quadratic bezier for smooth curve
final prevX = (i - 1) * stepX;
final prevY = size.height - (data[i - 1] / maxY) * size.height * animationValue;
final controlX = (prevX + x) / 2;
path.quadraticBezierTo(controlX, prevY, x, y);
}
}
// Fill area under curve
canvas.drawPath(path, paintFill);
// Draw line
canvas.drawPath(path, paintLine);
// Draw dots
for (int i = 0; i < data.length; i++) {
final x = i * stepX;
final y = size.height - (data[i] / maxY) * size.height * animationValue;
canvas.drawCircle(Offset(x, y), 4, Paint()..color = Colors.blue);
}
}
@override
bool shouldRepaint(covariant LineChartPainter oldDelegate) => true;
}
Expert insight: Using animationValue to interpolate between data sets creates a smooth morphing effect — far better than just redrawing raw data.
Conclusion: Crafting Prompts Like a Senior Engineer
The difference between a junior and a senior Flutter developer often comes down to how they decompose problems. The prompts above are not just code generators — they are templates for thinking. Each one encapsulates a design pattern: separation of concerns (Bloc), dependency injection (Riverpod), declarative animation (TweenAnimationBuilder), or performance optimization (CustomPainter).
Next steps:
- Try modifying the prompts to add your own business logic.
- Combine multiple prompts — for example, use the debounced search (Prompt 5) with the staggered list animation (Prompt 7) for a polished search results page.
- Explore the official Flutter documentation at docs.flutter.dev for deeper dives into each widget.
Remember: a great prompt is specific, includes examples, and defines the expected output. As you build more complex apps, refine your prompts to include error boundaries, testing considerations, and accessibility. That’s how you turn a prompt from a shortcut into a craft.
Comments