Creating a Recipe Stepper Widget with Image Carousel in Flutter
Building intuitive and engaging user interfaces is crucial for any successful mobile application. When it comes to displaying multi-step content, such as recipes, tutorials, or onboarding flows, a stepper widget provides a structured and easy-to-follow experience. Integrating an image carousel at the top can further enhance this by visually showcasing the end product or key ingredients as the user progresses.
In this article, we'll walk through the process of creating a dynamic recipe stepper widget in Flutter, complete with an image carousel at the top, allowing users to navigate through recipe steps with ease.
Prerequisites
- Basic understanding of Flutter and Dart.
- Flutter SDK installed and configured.
- A code editor like VS Code or Android Studio.
Project Setup
First, let's create a new Flutter project. Open your terminal or command prompt and run:
flutter create recipe_stepper_app
cd recipe_stepper_app
Now, open the project in your preferred IDE.
Designing the Data Model
For our recipe, we'll need a list of steps (instructions) and a list of images to display in the carousel. We'll keep it simple by using String lists for both.
Implementing the Image Carousel
We'll use Flutter's built-in PageView widget for the image carousel. PageView allows users to swipe horizontally between pages, which is perfect for displaying a gallery of images. We'll also add simple dot indicators to show the current page.
Here's how we can set up the image carousel:
import 'package:flutter/material.dart';
class RecipeStepperPage extends StatefulWidget {
@override
_RecipeStepperPageState createState() => _RecipeStepperPageState();
}
class _RecipeStepperPageState extends State {
final PageController _pageController = PageController();
int _currentImagePage = 0;
final List _recipeImages = [
'https://via.placeholder.com/600x400/FF5733/FFFFFF?text=Delicious+Meal+1',
'https://via.placeholder.com/600x400/33FF57/FFFFFF?text=Fresh+Ingredients',
'https://via.placeholder.com/600x400/3357FF/FFFFFF?text=Ready+to+Cook',
'https://via.placeholder.com/600x400/FFFF33/000000?text=Serving+Suggestion',
];
// ... (rest of the state and build method will come here)
}
Inside the build method, we'll integrate the PageView and the page indicators:
// ... inside _RecipeStepperPageState's build method
Scaffold(
appBar: AppBar(
title: Text('Recipe Stepper'),
backgroundColor: Colors.deepOrange,
),
body: Column(
children: [
// Image Carousel
Container(
height: 250,
child: PageView.builder(
controller: _pageController,
itemCount: _recipeImages.length,
onPageChanged: (index) {
setState(() {
_currentImagePage = index;
});
},
itemBuilder: (context, index) {
return Image.network(
_recipeImages[index],
fit: BoxFit.cover,
);
},
),
),
// Image Carousel Indicators
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(_recipeImages.length, (index) {
return AnimatedContainer(
duration: Duration(milliseconds: 150),
margin: EdgeInsets.symmetric(horizontal: 4.0),
height: 8.0,
width: _currentImagePage == index ? 24.0 : 8.0,
decoration: BoxDecoration(
color: _currentImagePage == index
? Colors.deepOrange
: Colors.grey.withOpacity(0.5),
borderRadius: BorderRadius.circular(4.0),
),
);
}),
),
),
// ... (Stepper content will go here)
],
),
);
Implementing the Recipe Stepper Core
Next, we'll define the recipe steps and implement the stepper logic. This involves managing the current step index and providing "Next" and "Previous" buttons to navigate through the steps.
// ... inside _RecipeStepperPageState
int _currentStep = 0;
final List _recipeSteps = [
'Step 1: Gather all your fresh ingredients. Make sure everything is prepped and ready to go.',
'Step 2: Preheat your oven to 200°C (392°F). Prepare your baking dish as instructed.',
'Step 3: Combine dry ingredients in one bowl and wet ingredients in another. Mix them separately.',
'Step 4: Gently fold the wet ingredients into the dry ingredients until just combined. Do not overmix!',
'Step 5: Pour the mixture into the prepared baking dish and bake for 30-40 minutes, or until golden brown.',
'Step 6: Let it cool for 10 minutes before serving. Enjoy your delicious homemade meal!',
];
void _nextStep() {
setState(() {
if (_currentStep < _recipeSteps.length - 1) {
_currentStep++;
} else {
// Optionally, handle "finish" or "restart" here
print('Recipe Finished!');
}
});
}
void _previousStep() {
setState(() {
if (_currentStep > 0) {
_currentStep--;
}
});
}
// ... (rest of the build method after carousel)
Now, let's add the UI for displaying the current step's instruction and the navigation buttons below the carousel.
// ... inside _RecipeStepperPageState's build method, after the image carousel indicators
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Step ${_currentStep + 1} of ${_recipeSteps.length}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.deepOrange,
),
),
SizedBox(height: 10),
Text(
_recipeSteps[_currentStep],
style: TextStyle(fontSize: 16),
),
Spacer(), // Pushes buttons to the bottom
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ElevatedButton(
onPressed: _currentStep > 0 ? _previousStep : null,
style: ElevatedButton.styleFrom(
primary: Colors.grey, // Disabled color
onSurface: Colors.deepOrange, // Enabled color
),
child: Text('Previous'),
),
ElevatedButton(
onPressed: _currentStep < _recipeSteps.length - 1
? _nextStep
: null,
style: ElevatedButton.styleFrom(
primary: Colors.deepOrange,
),
child: Text(
_currentStep < _recipeSteps.length - 1 ? 'Next' : 'Finish',
),
),
],
),
SizedBox(height: 16), // Bottom padding for buttons
],
),
),
),
],
),
);
Putting It All Together
Finally, let's integrate everything into a complete Flutter application. Replace the contents of your main.dart file with the following:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Recipe Stepper App',
theme: ThemeData(
primarySwatch: Colors.deepOrange,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: RecipeStepperPage(),
);
}
}
class RecipeStepperPage extends StatefulWidget {
@override
_RecipeStepperPageState createState() => _RecipeStepperPageState();
}
class _RecipeStepperPageState extends State {
final PageController _pageController = PageController();
int _currentImagePage = 0;
int _currentStep = 0;
final List _recipeImages = [
'https://via.placeholder.com/600x400/FF5733/FFFFFF?text=Delicious+Meal+1',
'https://via.placeholder.com/600x400/33FF57/FFFFFF?text=Fresh+Ingredients',
'https://via.placeholder.com/600x400/3357FF/FFFFFF?text=Ready+to+Cook',
'https://via.placeholder.com/600x400/FFFF33/000000?text=Serving+Suggestion',
];
final List _recipeSteps = [
'Step 1: Gather all your fresh ingredients. Make sure everything is prepped and ready to go.',
'Step 2: Preheat your oven to 200°C (392°F). Prepare your baking dish as instructed.',
'Step 3: Combine dry ingredients in one bowl and wet ingredients in another. Mix them separately.',
'Step 4: Gently fold the wet ingredients into the dry ingredients until just combined. Do not overmix!',
'Step 5: Pour the mixture into the prepared baking dish and bake for 30-40 minutes, or until golden brown.',
'Step 6: Let it cool for 10 minutes before serving. Enjoy your delicious homemade meal!',
];
void _nextStep() {
setState(() {
if (_currentStep < _recipeSteps.length - 1) {
_currentStep++;
} else {
// Optionally, handle "finish" or "restart" here
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Recipe Completed!')),
);
}
});
}
void _previousStep() {
setState(() {
if (_currentStep > 0) {
_currentStep--;
}
});
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Recipe Stepper'),
backgroundColor: Colors.deepOrange,
elevation: 0,
),
body: Column(
children: [
// Image Carousel
Container(
height: 250,
child: PageView.builder(
controller: _pageController,
itemCount: _recipeImages.length,
onPageChanged: (index) {
setState(() {
_currentImagePage = index;
});
},
itemBuilder: (context, index) {
return Image.network(
_recipeImages[index],
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Center(child: Icon(Icons.broken_image, size: 50, color: Colors.grey));
},
);
},
),
),
// Image Carousel Indicators
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(_recipeImages.length, (index) {
return AnimatedContainer(
duration: Duration(milliseconds: 150),
margin: EdgeInsets.symmetric(horizontal: 4.0),
height: 8.0,
width: _currentImagePage == index ? 24.0 : 8.0,
decoration: BoxDecoration(
color: _currentImagePage == index
? Colors.deepOrange
: Colors.grey.withOpacity(0.5),
borderRadius: BorderRadius.circular(4.0),
),
);
}),
),
),
// Stepper Content
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Step ${_currentStep + 1} of ${_recipeSteps.length}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.deepOrange,
),
),
SizedBox(height: 10),
Expanded( // Use Expanded for the text to allow scrolling if content is long
child: SingleChildScrollView(
child: Text(
_recipeSteps[_currentStep],
style: TextStyle(fontSize: 16),
),
),
),
SizedBox(height: 20), // Spacer before buttons
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ElevatedButton(
onPressed: _currentStep > 0 ? _previousStep : null,
style: ElevatedButton.styleFrom(
primary: Colors.blueGrey, // Use a distinct color for previous
onSurface: Colors.blueGrey.shade200, // Disabled color
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
'Previous',
style: TextStyle(color: Colors.white),
),
),
ElevatedButton(
onPressed: _currentStep < _recipeSteps.length - 1
? _nextStep
: (_currentStep == _recipeSteps.length - 1 ? _nextStep : null),
style: ElevatedButton.styleFrom(
primary: Colors.deepOrange,
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
_currentStep < _recipeSteps.length - 1 ? 'Next' : 'Finish',
style: TextStyle(color: Colors.white),
),
),
],
),
SizedBox(height: 16), // Bottom padding
],
),
),
),
],
),
);
}
}
Conclusion and Further Enhancements
You've successfully built a recipe stepper widget with an integrated image carousel in Flutter! This setup provides a clean and interactive way for users to follow multi-step instructions while visually engaging with related content.
Here are some ideas for further enhancements:
- Complex Data Model: Enhance the
_recipeStepsto be a list of custom objects, each containing not just text but also step-specific images, timers, or ingredient lists. - Animations: Add more sophisticated animations for step transitions or image changes.
- Accessibility: Implement proper semantics for screen readers.
- Error Handling: Implement more robust error handling for network images.
- Persist Progress: Use local storage (e.g.,
shared_preferencesor a database) to save the user's progress through a recipe. - Third-party Packages: Explore packages like
carousel_sliderfor more advanced carousel features orsmooth_page_indicatorfor more indicator styles. - Gestures: Allow swiping left/right on the recipe text area to navigate steps as well.
This widget forms a solid foundation for any app requiring a guided, multi-step user experience. Happy coding!