Flutter has become a go-to framework for cross-platform mobile development, powering apps for Google, BMW, and Alibaba. But even experienced developers often get stuck when designing complex UIs, managing state, or crafting smooth animations. This guide provides 15 ready-to-use prompts that solve real-world problems, from basic widgets to advanced Bloc and Riverpod patterns.
Why Prompts Matter in Flutter Development
Prompts are structured instructions that guide AI tools (like GitHub Copilot or ChatGPT) to generate precise code. Instead of vague requests like "add animation," a good prompt specifies the widget type, state management pattern, and expected behavior. This approach reduces debugging time by up to 40%, according to a 2025 Stack Overflow developer survey.
Category 1: Widgets (Basic)
1. Adaptive Layout with Responsive Breakpoints
Task: Create a widget that adapts its layout based on screen width using LayoutBuilder and MediaQuery.
Prompt: "Write a Flutter widget that displays a card grid on tablets (>=600dp) and a list on phones. Use LayoutBuilder to detect breakpoints and GridView for the grid."
Example result:
class AdaptiveWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 600) {
return GridView.count(
crossAxisCount: 2,
children: List.generate(10, (index) => Card(child: Text('Item $index'))),
);
} else {
return ListView.builder(
itemCount: 10,
itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
);
}
},
);
}
}
Real-world case: A retail app used this pattern to reduce visual clutter on phones while maintaining feature parity on tablets, resulting in a 15% increase in user engagement.
2. Custom Animated Button with GestureDetector
Task: Build a button that scales when pressed using AnimatedContainer and GestureDetector.
Prompt: "Create a Flutter button that scales to 1.2x on press down and returns to 1.0x on release, with a 200ms animation duration."
Example result:
class ScaleButton extends StatefulWidget {
@override
_ScaleButtonState createState() => _ScaleButtonState();
}
class _ScaleButtonState extends State<ScaleButton> with SingleTickerProviderStateMixin {
double _scale = 1.0;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => setState(() => _scale = 1.2),
onTapUp: (_) => setState(() => _scale = 1.0),
child: AnimatedContainer(
duration: Duration(milliseconds: 200),
transform: Matrix4.identity()..scale(_scale),
child: ElevatedButton(onPressed: () {}, child: Text('Press me')),
),
);
}
}
Source: Official Flutter documentation on AnimatedContainer (flutter.dev/docs).
3. Form with Validation and Error Feedback
Task: Implement a login form with email and password validation, showing error messages below each field.
Prompt: "Write a Flutter form using Form widget, TextFormField, and FormState.validate(). Display red error text if email is empty or password is less than 6 characters."
Example result:
class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
String _email = '', _password = '';
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
validator: (value) => value!.isEmpty ? 'Email is required' : null,
onSaved: (value) => _email = value!,
),
TextFormField(
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
validator: (value) => value!.length < 6 ? 'Password too short' : null,
onSaved: (value) => _password = value!,
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
print('Login: $_email / $_password');
}
},
child: Text('Submit'),
),
],
),
);
}
}
Note: Always use obscureText: true for password fields to avoid security leaks.
Category 2: State Management (Advanced)
4. Simple Counter with Riverpod
Task: Create a counter app using Riverpod's StateProvider and ConsumerWidget.
Prompt: "Build a Flutter counter with Riverpod. Use StateProvider<int> for the count, a Consumer widget to display it, and two buttons to increment/decrement."
Example result:
final counterProvider = StateProvider<int>((ref) => 0);
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
children: [
Text('Count: $count'),
Row(
children: [
ElevatedButton(onPressed: () => ref.read(counterProvider.notifier).state++, child: Text('+')),
ElevatedButton(onPressed: () => ref.read(counterProvider.notifier).state--, child: Text('-')),
],
),
],
);
}
}
Source: Riverpod official documentation (riverpod.dev). Riverpod is used by over 30% of Flutter developers as of 2026.
5. Todo List with Bloc Pattern
Task: Implement a todo list using flutter_bloc with add/delete functionality.
Prompt: "Create a Bloc for a todo list. Events: AddTodo, DeleteTodo. State: ListBlocBuilder to render the list."
Example result:
// Events
abstract class TodoEvent {}
class AddTodo extends TodoEvent { final String task; AddTodo(this.task); }
class DeleteTodo extends TodoEvent { final int index; DeleteTodo(this.index); }
// Bloc
class TodoBloc extends Bloc<TodoEvent, List<String>> {
TodoBloc() : super([]);
@override
Stream<List<String>> mapEventToState(TodoEvent event) async* {
if (event is AddTodo) {
yield [...state, event.task];
} else if (event is DeleteTodo) {
yield [...state]..removeAt(event.index);
}
}
}
// UI
BlocProvider(
create: (context) => TodoBloc(),
child: BlocBuilder<TodoBloc, List<String>>(
builder: (context, todos) => ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) => ListTile(
title: Text(todos[index]),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => context.read<TodoBloc>().add(DeleteTodo(index)),
),
),
),
),
)
Real-world case: Google Ads app uses Bloc for managing campaign states, ensuring clear separation of concerns.
6. Async Data Fetching with Riverpod and FutureProvider
Task: Fetch user data from an API using Riverpod's FutureProvider and show loading/error states.
Prompt: "Write a Riverpod FutureProvider that fetches JSON from 'https://jsonplaceholder.typicode.com/users/1'. Display a CircularProgressIndicator while loading, and an error message if the request fails."
Example result:
final userProvider = FutureProvider<User>((ref) async {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users/1'));
if (response.statusCode != 200) throw Exception('Failed to load');
return User.fromJson(jsonDecode(response.body));
});
class UserWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return userAsync.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
data: (user) => Text('Name: ${user.name}'),
);
}
}
Source: JSONPlaceholder is a free fake API for testing (jsonplaceholder.typicode.com).
7. State Persistence with SharedPreferences and Provider
Task: Save app settings (e.g., dark mode toggle) using SharedPreferences and expose them via ChangeNotifierProvider.
Prompt: "Create a ThemeProvider extending ChangeNotifier that loads and saves a boolean 'isDarkMode' to SharedPreferences. Use Provider to inject it into the widget tree."
Example result:
class ThemeProvider extends ChangeNotifier {
bool _isDarkMode = false;
bool get isDarkMode => _isDarkMode;
Future<void> load() async {
final prefs = await SharedPreferences.getInstance();
_isDarkMode = prefs.getBool('isDarkMode') ?? false;
notifyListeners();
}
Future<void> toggle() async {
_isDarkMode = !_isDarkMode;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isDarkMode', _isDarkMode);
notifyListeners();
}
}
Note: Always call load() in main() before runApp() to ensure settings are ready.
Category 3: Animations (Expert)
8. Staggered List Animation with AnimatedList
Task: Animate items appearing in a list with a staggered delay using AnimatedList.
Prompt: "Create a Flutter AnimatedList where each new item slides in from the right with a 100ms delay between items."
Example result:
class StaggeredList extends StatefulWidget {
@override
_StaggeredListState createState() => _StaggeredListState();
}
class _StaggeredListState extends State<StaggeredList> {
final _listKey = GlobalKey<AnimatedListState>();
final _items = <String>[];
void _addItem() {
final index = _items.length;
_items.insert(0, 'Item $index');
_listKey.currentState!.insertItem(0, duration: Duration(milliseconds: 300));
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(onPressed: _addItem, child: Text('Add')),
Expanded(
child: AnimatedList(
key: _listKey,
initialItemCount: _items.length,
itemBuilder: (context, index, animation) {
return SlideTransition(
position: Tween<Offset>(
begin: Offset(1, 0),
end: Offset.zero,
).animate(CurvedAnimation(parent: animation, curve: Curves.easeInOut)),
child: ListTile(title: Text(_items[index])),
);
},
),
),
],
);
}
}
Source: Flutter cookbook on AnimatedList (flutter.dev/cookbook).
9. Hero Animation Between Screens
Task: Animate a shared element (e.g., an image) from a list to a detail page using Hero widget.
Prompt: "Wrap an Image widget in two screens with the same Hero tag. Use Navigator.push() to transition."
Example result:
// Screen 1
Hero(
tag: 'image_hero',
child: Image.network('https://picsum.photos/200'),
)
// Screen 2
Hero(
tag: 'image_hero',
child: Image.network('https://picsum.photos/600'),
)
Real-world case: E-commerce apps like eBay use Hero animations to create a seamless browsing-to-detail experience, increasing conversion rates.
10. Custom Paint Animation with AnimationController
Task: Draw a custom shape (e.g., a sine wave) that animates over time using CustomPainter and AnimationController.
Prompt: "Create a CustomPainter that draws a sine wave. Use AnimationController to shift the wave horizontally over 2 seconds, looping continuously."
Example result:
class SineWavePainter extends CustomPainter {
final double shift;
SineWavePainter(this.shift);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..strokeWidth = 2;
final path = Path();
for (double x = 0; x <= size.width; x++) {
double y = size.height / 2 + 50 * sin((x + shift) * 2 * pi / size.width);
if (x == 0) path.moveTo(x, y);
else path.lineTo(x, y);
}
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(SineWavePainter oldDelegate) => oldDelegate.shift != shift;
}
// In widget
class AnimatedSineWave extends StatefulWidget {
@override
_AnimatedSineWaveState createState() => _AnimatedSineWaveState();
}
class _AnimatedSineWaveState extends State<AnimatedSineWave> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: Duration(seconds: 2))..repeat();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) => CustomPaint(
painter: SineWavePainter(_controller.value * 100),
size: Size(300, 200),
),
);
}
}
Source: Dart's dart:math provides sin and cos for wave calculations.
11. Physics-Based Spring Animation
Task: Implement a spring effect when a widget is released after dragging using SpringDescription.
Prompt: "Use AnimatedPositioned with a SpringSimulation to animate a widget back to its original position after a drag gesture ends."
Example result:
class SpringBackWidget extends StatefulWidget {
@override
_SpringBackWidgetState createState() => _SpringBackWidgetState();
}
class _SpringBackWidgetState extends State<SpringBackWidget> with SingleTickerProviderStateMixin {
late AnimationController _controller;
double _dx = 0;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
}
void _onDragEnd(_) {
final simulation = SpringSimulation(SpringDescription(mass: 1, stiffness: 100, damping: 10), _dx, 0, 0);
_controller.animateWith(simulation);
_controller.addListener(() => setState(() => _dx = _controller.value));
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanUpdate: (details) => setState(() => _dx += details.delta.dx),
onPanEnd: _onDragEnd,
child: Transform.translate(
offset: Offset(_dx, 0),
child: Container(width: 100, height: 100, color: Colors.red),
),
);
}
}
Note: SpringSimulation is part of flutter/physics package.
12. Animated Switcher for Smooth Widget Transitions
Task: Toggle between two widgets (e.g., a star and a heart) with a fade transition using AnimatedSwitcher.
Prompt: "Use AnimatedSwitcher to switch between an Icon(Icons.star) and Icon(Icons.favorite) when a button is pressed, with a 500ms fade duration."
Example result:
bool _isStar = true;
AnimatedSwitcher(
duration: Duration(milliseconds: 500),
transitionBuilder: (child, animation) => FadeTransition(opacity: animation, child: child),
child: Icon(
_isStar ? Icons.star : Icons.favorite,
key: ValueKey(_isStar),
),
)
Source: Flutter's AnimatedSwitcher documentation.
Category 4: Integration Patterns (Expert)
13. Infinite Scroll with Riverpod and Pagination
Task: Load more items when scrolling to the bottom using ScrollController and Riverpod's StateNotifierProvider.
Prompt: "Implement an infinite scroll list that fetches 20 items per page from a mock API. Use StateNotifier to manage the list and loading state."
Example result:
class PaginationNotifier extends StateNotifier<List<String>> {
int _page = 0;
PaginationNotifier() : super([]);
Future<void> loadMore() async {
final response = await http.get(Uri.parse('https://api.example.com/items?page=$_page&limit=20'));
final newItems = List<String>.from(jsonDecode(response.body));
state = [...state, ...newItems];
_page++;
}
}
// In widget
ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100) {
context.read<PaginationNotifier>().loadMore();
}
});
}
Real-world case: Social media apps like Twitter use infinite scroll to keep users engaged longer.
14. Debounced Search with Provider
Task: Create a search bar that waits 300ms after the user stops typing before sending an API request.
Prompt: "Use Timer to implement debounce. The search text is stored in a ChangeNotifier that triggers a search after 300ms of inactivity."
Example result:
class SearchProvider extends ChangeNotifier {
Timer? _debounce;
String _query = '';
void onSearchChanged(String query) {
_debounce?.cancel();
_debounce = Timer(Duration(milliseconds: 300), () {
_query = query;
notifyListeners();
// Trigger API call here
});
}
@override
void dispose() {
_debounce?.cancel();
super.dispose();
}
}
Source: Dart's dart:async Timer class.
15. Multi-Provider with Dependency Injection
Task: Combine multiple providers (e.g., AuthProvider, CartProvider) using MultiProvider.
Prompt: "Wrap the app in MultiProvider with two providers: AuthProvider (ChangeNotifier) and CartProvider (ChangeNotifier). Access both in a widget."
Example result:
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthProvider()),
ChangeNotifierProvider(create: (_) => CartProvider()),
],
child: MaterialApp(
home: Consumer2<AuthProvider, CartProvider>(
builder: (context, auth, cart, child) {
return Text('User: ${auth.user}, Cart items: ${cart.items.length}');
},
),
),
)
Note: Consumer2 allows listening to two providers simultaneously without nesting.
Conclusion
These 15 prompts cover the most common Flutter development scenarios, from basic widgets to advanced state management and animations. By using structured prompts, you can reduce boilerplate code and focus on logic. Start by integrating Riverpod or Bloc into your next project and see how it improves code maintainability. Try one prompt today—your future self will thank you.
Comments