Build a Campus Marketplace Home Screen
Translate a Figma design into a Flutter home screen with cards and navigation.
Introduction
30 Second Summary
You have a beautiful Figma design for your campus marketplace app. Now you need to turn it into a real, working screen on your phone. The gap between a static mockup and running code can feel overwhelming when you are staring at a blank editor.
In this project, you will translate your Figma home screen design into a working Flutter app running on your Android phone. You will learn a repeatable framework for decomposing any design into Flutter widgets.
What You'll Build
A fully styled campus marketplace home screen built by systematically breaking your Figma design into Flutter widgets like Row, Column, Container, and ListView.
By the end of this project, you'll have:
- A new Flutter project with a clean folder structure for screens and widgets.
- A complete home screen with a search bar, horizontally scrollable category chips, and a vertical feed of service cards.
- A reusable ServiceCard widget with proper parameterization that you can extend across your app.
- Secret Mission: Add an Explore screen with a grid layout and wire it to the bottom navigation for tab switching.
Are there any prerequisites?
You should have VS Code and the Flutter SDK already installed on your machine, with an Android phone available for USB debugging. Basic familiarity with Dart syntax is helpful but not required.
You should also have your Figma designs ready for the home screen so you can reference them as you build.
Create the Project and Set Up Structure
Translating a Figma design into code starts with a clean, well-organized project. Your marketplace home screen has multiple sections (search bar, categories, service cards, navigation), so getting the folder structure right now saves headaches later.
In this step, you will create a new Flutter project using the empty template, set up dedicated folders for screens and widgets, and confirm the app runs on your phone.
In this step, get ready to:
- Create a new Flutter project from the empty template.
- Set up a folder structure with separate directories for screens and widgets.
- Wire up the entry point and confirm the app runs on your Android phone.
Create a new Flutter project
The flutter create --empty command generates a minimal project with no demo counter-app boilerplate. This gives you a clean starting point to build from.
- In VS Code, open the integrated terminal by pressing Ctrl+`.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the Flutter project and move into it by running these commands:
flutter create --empty campus_marketplace
cd campus_marketplace
What does this command do?
The --empty flag tells Flutter to skip the default counter-app demo. You get a bare main.dart that shows Hello World! on screen. The project name campus_marketplace uses the required lowercase_with_underscores naming convention for Flutter projects.
- Confirm the project was created by running:
ls
- You should see folders like android/, lib/, test/, and a pubspec.yaml file in the output.
Seeing an error during project creation?
- Make sure you typed campus_marketplace with underscores, not hyphens. Flutter project names cannot contain dashes.
- If you see flutter: command not found, your Flutter SDK is not in your system PATH. Reopen VS Code and try again.
Still stuck? Help me troubleshoot the flutter create command
- Now open the project folder in VS Code. Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the campus_marketplace folder, and click Open.
VS Code reloads with the project as your workspace. You should see the lib/ folder in the Explorer sidebar on the left.
- Open the integrated terminal again by pressing Ctrl+`.
- Run the app on your connected Android phone by running:
flutter run
- You should see a white screen with Hello World! displayed on your phone.
Not seeing the app on your phone?
- Make sure USB debugging is enabled on your phone. Check under Settings > Developer options > USB debugging.
- If the terminal says No devices found, unplug your USB cable and plug it back in. Accept any permission prompts on your phone.
- Run flutter devices to confirm Flutter can see your phone.
Still stuck? Help me connect my Android phone to Flutter
Set up the folder structure
A clean folder structure keeps your project organized as it grows. You will separate your screens (full pages) from your reusable widgets (smaller UI components).
- Stop the running app by pressing q in the terminal.
- Create the folders for screens and widgets by running these commands:
mkdir lib/screens
mkdir lib/widgets
Why separate folders?
The screens/ folder holds full-page layouts like your home screen. The widgets/ folder holds smaller, reusable pieces like service cards. This separation makes your code easier to find and maintain as you add more screens later.
- Create the empty files for your home screen and service card widget by running:
macOS
touch lib/screens/home_screen.dart
touch lib/widgets/service_card.dart
Windows
New-Item lib/screens/home_screen.dart
New-Item lib/widgets/service_card.dart
- Confirm the files exist by running:
ls lib/screens
ls lib/widgets
- You should see home_screen.dart in the first output and service_card.dart in the second.
Wire up the entry point and verify the app runs
Right now, main.dart still has the default Hello World code. You will replace it with your own app class that points to the new HomeScreen.
- In VS Code, open lib/screens/home_screen.dart from the Explorer sidebar.
- Paste the following code into the file:
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
// Return an empty Scaffold as a placeholder
return const Scaffold();
}
}
What does this code do?
- HomeScreen is a StatelessWidget. Stateless widgets display fixed content that does not change over time.
- Scaffold provides the basic page structure for a Flutter screen. Right now it is empty, but you will fill it with your marketplace UI in the next steps.
- Now open lib/main.dart from the Explorer sidebar.
- Select all the existing code and replace it with the following:
import 'package:flutter/material.dart';
import 'screens/home_screen.dart';
void main() {
runApp(const CampusMarketplace());
}
class CampusMarketplace extends StatelessWidget {
const CampusMarketplace({super.key});
@override
Widget build(BuildContext context) {
// Set up the app with Material Design theming
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Campus Marketplace',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const HomeScreen(),
);
}
}
What does this code do?
- CampusMarketplace is your app's root widget. Every Flutter app starts with a runApp() call that takes a top-level widget.
- MaterialApp sets up Material Design features like theming and navigation.
- debugShowCheckedModeBanner: false removes the red "DEBUG" banner from the corner of the screen.
- home: const HomeScreen() tells the app to display your HomeScreen when it launches.
- Save both main.dart and home_screen.dart.
- In the terminal, run the app again:
flutter run
- Your phone should now show a blank white screen with no errors in the terminal.
Seeing a red error screen on your phone?
- Check that home_screen.dart is saved inside lib/screens/ and not directly in lib/. The import path in main.dart expects screens/home_screen.dart.
- Make sure the class name HomeScreen in home_screen.dart matches the import in main.dart exactly, including capitalization.
- If you see a compilation error, check the terminal output for the specific line number.
Still stuck? Help me fix the red error screen in my Flutter app
✔️ Awesome, I've got everything!
Great. Double check you have saved both files before moving on.
ⓧ I'd like to double check the full code
Here is what each file should look like at this point.
lib/main.dart:
import 'package:flutter/material.dart';
import 'screens/home_screen.dart';
void main() {
runApp(const CampusMarketplace());
}
class CampusMarketplace extends StatelessWidget {
const CampusMarketplace({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Campus Marketplace',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const HomeScreen(),
);
}
}
lib/screens/home_screen.dart:
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold();
}
}
lib/widgets/service_card.dart should be an empty file. You will add code here in the next step.
Your project skeleton is ready. Next up, you will build the search bar and category chips at the top of your home screen.
Build the Search Bar and Category Chips
Your blank Scaffold is ready for its first real content. In Flutter, every visual element on screen is a widget. Building a UI means nesting widgets inside each other to form a widget tree.
In this step, you'll translate the top section of your Figma design into Flutter code. The search bar maps to a Container with a TextField inside, and the category chips become styled Container widgets lined up in a horizontal Row.
In this step, get ready to:
- Identify the Flutter widgets that map to your Figma search bar and category chips.
- Build a styled search bar with a Container and TextField.
- Build horizontally scrollable category chips.
Identify the widget structure from your Figma design
Before writing any code, look at the top section of your Figma home screen design. You need to identify which Flutter widget maps to each visual element.
- Open your Figma design and look at the top area of the home screen.
- The search bar is a rounded rectangle with a search icon and placeholder text inside.
- The category filters below it are small pill-shaped labels arranged horizontally.
How does a design map to Flutter widgets?
A rounded rectangle with a background color becomes a Container with BoxDecoration. A text input field becomes a TextField. Horizontal arrangements become a Row, and vertical stacking becomes a Column.
The whole screen wraps in a SafeArea to keep content from hiding behind the phone's status bar or notch.
Build the search bar widget
The search bar needs a Container for the rounded shape and shadow, with a TextField inside for user input. You will also set up the outer Scaffold and Column structure that holds the entire screen.
- In VS Code, open lib/screens/home_screen.dart from the Explorer sidebar.
- Replace the entire file contents with the following code:
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
// Light grey background for contrast against white cards
backgroundColor: Colors.grey[50],
body: SafeArea(
child: Column(
children: [
// Search bar section
Padding(
padding: const EdgeInsets.all(16.0),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: const TextField(
decoration: InputDecoration(
hintText: 'Search services...',
prefixIcon: Icon(Icons.search, color: Colors.grey),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
),
),
),
],
),
),
);
}
}
What does this code do?
- Scaffold is the base structure for a screen. The backgroundColor is set to light grey so white cards stand out.
- SafeArea pushes content below the phone's status bar and above the navigation gestures area.
- Column stacks its children vertically. Right now it only has the search bar.
- The Container uses BoxDecoration for white background, rounded corners (radius 12), and a subtle drop shadow.
- InputBorder.none removes the default underline from the TextField so the Container's shape is the only visible boundary.
- Save home_screen.dart.
- Press r in the terminal where flutter run is active to hot reload.
- On your phone, you should see a white rounded search bar with a grey magnifying glass icon and "Search services..." hint text on a light grey background.
Don't see the search bar?
- Make sure you saved the file. Hot reload only picks up saved changes.
- Check for red underlines in VS Code. A missing comma or bracket will prevent the widget tree from compiling.
- Confirm your terminal still shows the running app. If it crashed, press R (capital) for a full restart.
Help me debug my search bar widget.
Build the category chips
Category chips let users filter services at a glance. In Flutter, a horizontally scrollable row of items uses a SingleChildScrollView with its scroll direction set to horizontal.
- In home_screen.dart, find the closing ), of the Padding widget (the search bar's outermost wrapper). It's inside the children: [ list.
- Add the following code directly after that closing ), and before the ], that closes the children list:
SizedBox(
height: 50,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
_buildCategoryChip('All', isSelected: true),
const SizedBox(width: 8),
_buildCategoryChip('Tutoring'),
const SizedBox(width: 8),
_buildCategoryChip('Laundry'),
const SizedBox(width: 8),
_buildCategoryChip('Food'),
const SizedBox(width: 8),
_buildCategoryChip('Design'),
const SizedBox(width: 8),
_buildCategoryChip('Printing'),
],
),
),
),
This code calls a helper method called _buildCategoryChip that does not exist yet. You will create it next.
- Scroll down to the very end of the HomeScreen class, just before the final closing }.
- Add the following helper method below the build method's closing }:
Widget _buildCategoryChip(String label, {bool isSelected = false}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? Colors.deepPurple : Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected ? Colors.deepPurple : Colors.grey[300]!,
),
),
child: Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
);
}
What does this code do?
- SizedBox(height: 50) gives the chip row a fixed height so it does not expand to fill the screen.
- SingleChildScrollView with Axis.horizontal makes the Row scrollable sideways when the chips overflow the screen width.
- _buildCategoryChip is a private helper that returns a styled Container for each chip. The isSelected parameter toggles the color between deepPurple (selected) and white (unselected).
- SizedBox(width: 8) between each chip adds consistent horizontal spacing.
- Save home_screen.dart and press r in the terminal to hot reload.
- On your phone, you should see the search bar at the top and a row of category chips below it. The "All" chip is purple with white text. The others are white with a grey border.
- Swipe the chip row sideways. You should see "Printing" appear as you scroll right.
Seeing a red error screen on your phone?
- Check that _buildCategoryChip is inside the HomeScreen class but outside the build method. It should be a sibling of build, not nested inside it.
- Make sure the SizedBox widget is placed after the Padding widget with a comma separating them. Missing commas between children cause compile errors.
- Look for a ! after Colors.grey[300] in the border color. This null assertion operator is required because subscript access on Colors returns a nullable type.
Help me fix my category chips code.
✔️ Awesome, I've got everything!
Great. Double check you have saved home_screen.dart and your phone shows the search bar with category chips below it.
ⓧ I'd like to double check the full code
Here is the complete home_screen.dart file at the end of this step:
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
body: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: const TextField(
decoration: InputDecoration(
hintText: 'Search services...',
prefixIcon: Icon(Icons.search, color: Colors.grey),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
),
),
),
SizedBox(
height: 50,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
_buildCategoryChip('All', isSelected: true),
const SizedBox(width: 8),
_buildCategoryChip('Tutoring'),
const SizedBox(width: 8),
_buildCategoryChip('Laundry'),
const SizedBox(width: 8),
_buildCategoryChip('Food'),
const SizedBox(width: 8),
_buildCategoryChip('Design'),
const SizedBox(width: 8),
_buildCategoryChip('Printing'),
],
),
),
),
],
),
),
);
}
Widget _buildCategoryChip(String label, {bool isSelected = false}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? Colors.deepPurple : Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected ? Colors.deepPurple : Colors.grey[300]!,
),
),
child: Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
);
}
}
Your top section is looking sharp. Next up, you'll build the service card widget that displays each marketplace listing in the scrollable feed below the chips.
Build the Service Card Widget
Your search bar and category chips are looking great. Now it's time to tackle the most important visual element on the home screen: the service card.
Each card in your Figma design represents a campus service someone is offering. In this step, you'll build a reusable ServiceCard widget that you can stamp out for every listing in the feed.
In this step, get ready to:
- Decompose the card design into a Flutter widget tree.
- Build the ServiceCard widget with image, title, provider, price, and rating.
- Import the card into your home screen and display it on your phone.
Decompose the card from your Figma design
Before writing any code, look at the service card in your Figma design. Every visual element maps to a specific Flutter widget.
- The card itself is a Container with rounded corners and a subtle shadow.
- Inside the card, a Column stacks the image on top and the text details below.
- The image area uses ClipRRect to round only the top corners.
- The details section is another Column with the title, provider name, and a Row that places the price on the left and the star rating on the right.
What is widget tree decomposition?
Every screen in Flutter is a tree of nested widgets. Decomposing means looking at your design and identifying which widget handles each piece: Container for styling, Column for vertical stacking, Row for horizontal arrangement.
Once you can see the tree in your head, the code practically writes itself.
Build the ServiceCard widget
Now that you know the structure, translate it into Dart code. The ServiceCard will be a reusable StatelessWidget that accepts data through its constructor.
- Open lib/widgets/service_card.dart in VS Code and paste the following code:
import 'package:flutter/material.dart';
class ServiceCard extends StatelessWidget {
// Parameters the card needs to display a service
final String title;
final String provider;
final String price;
final String rating;
final Color imageColor;
const ServiceCard({
super.key,
required this.title,
required this.provider,
required this.price,
required this.rating,
required this.imageColor,
});
@override
Widget build(BuildContext context) {
return Container(
// Card shape with rounded corners and shadow
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image placeholder with rounded top corners
ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
child: Container(
height: 140,
width: double.infinity,
color: imageColor,
child: const Center(
child: Icon(
Icons.image,
size: 48,
color: Colors.white70,
),
),
),
),
// Text details section
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
provider,
style: TextStyle(
fontSize: 13,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
// Price on left, rating on right
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
price,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: Colors.deepPurple,
),
),
Row(
children: [
const Icon(
Icons.star,
size: 16,
color: Colors.amber,
),
const SizedBox(width: 4),
Text(
rating,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
],
),
),
],
),
);
}
}
What does this code do?
The ServiceCard class declares five required parameters (title, provider, price, rating, imageColor). This makes the widget reusable for any service listing.
- ClipRRect rounds only the top corners of the image area, so the card keeps its shape.
- The colored Container acts as a placeholder for a real image. You can swap in Image.network later when you have real assets.
- The bottom Row uses MainAxisAlignment.spaceBetween to push the price to the left and the star rating to the right.
- Save lib/widgets/service_card.dart.
You won't see a change on your phone yet. The next substep imports this widget into your home screen.
Import and display the card on your home screen
The ServiceCard widget exists in its own file, but your home screen doesn't know about it yet. You need to import it and add a single card instance to the Column to verify it renders correctly.
- In lib/screens/home_screen.dart, find the existing import line at the top:
import 'package:flutter/material.dart';
- Add this import directly below it:
import '../widgets/service_card.dart';
- Now scroll down to the Column's children list. Find the closing parenthesis of the SizedBox(height: 50) that wraps your category chips.
- Add the following code after that SizedBox widget (still inside the children list):
// Spacer between chips and card
const SizedBox(height: 16),
// Test card to verify the widget renders
ServiceCard(
title: 'Math Tutoring',
provider: 'Sarah Johnson',
price: '\$15/hr',
rating: '4.8',
imageColor: Colors.blue[100]!,
),
What does this code do?
The SizedBox(height: 16) adds vertical spacing between the category chips and the card.
The ServiceCard instance passes sample data for one listing. The ! after Colors.blue[100] tells Dart you're certain this value isn't null, which satisfies the non-nullable Color parameter.
- Save lib/screens/home_screen.dart.
- Hot reload your app (press r in the terminal where flutter run is active).
- You should see a card appear below the category chips with a blue placeholder image, the title "Math Tutoring", the provider "Sarah Johnson", the price in purple, and a star rating on the right.
Don't see the card on your phone?
- Check that the import path is exactly ../widgets/service_card.dart. A wrong path causes an unresolved import error in the terminal.
- Make sure the ServiceCard is placed inside the children: [] list of the Column, not outside it.
- If you see an overflow error (yellow/black stripes), the card may be too tall for the remaining space. This is expected for now and will be fixed in the next step when we add scrolling.
Help me debug why my ServiceCard isn't showing up after hot reload.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the full lib/widgets/service_card.dart:
import 'package:flutter/material.dart';
class ServiceCard extends StatelessWidget {
final String title;
final String provider;
final String price;
final String rating;
final Color imageColor;
const ServiceCard({
super.key,
required this.title,
required this.provider,
required this.price,
required this.rating,
required this.imageColor,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
child: Container(
height: 140,
width: double.infinity,
color: imageColor,
child: const Center(
child: Icon(
Icons.image,
size: 48,
color: Colors.white70,
),
),
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
provider,
style: TextStyle(
fontSize: 13,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
price,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: Colors.deepPurple,
),
),
Row(
children: [
const Icon(
Icons.star,
size: 16,
color: Colors.amber,
),
const SizedBox(width: 4),
Text(
rating,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
],
),
),
],
),
);
}
}
Here is the full lib/screens/home_screen.dart:
import 'package:flutter/material.dart';
import '../widgets/service_card.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
body: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: const TextField(
decoration: InputDecoration(
hintText: 'Search services...',
prefixIcon: Icon(Icons.search, color: Colors.grey),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
),
),
),
SizedBox(
height: 50,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
_buildCategoryChip('All', isSelected: true),
const SizedBox(width: 8),
_buildCategoryChip('Tutoring'),
const SizedBox(width: 8),
_buildCategoryChip('Laundry'),
const SizedBox(width: 8),
_buildCategoryChip('Food'),
const SizedBox(width: 8),
_buildCategoryChip('Design'),
const SizedBox(width: 8),
_buildCategoryChip('Printing'),
],
),
),
),
const SizedBox(height: 16),
ServiceCard(
title: 'Math Tutoring',
provider: 'Sarah Johnson',
price: '\$15/hr',
rating: '4.8',
imageColor: Colors.blue[100]!,
),
],
),
),
);
}
Widget _buildCategoryChip(String label, {bool isSelected = false}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? Colors.deepPurple : Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected ? Colors.deepPurple : Colors.grey[300]!,
),
),
child: Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
);
}
}
Your reusable card widget is complete. Next up, you'll assemble the full scrollable home screen by feeding a list of sample services into a ListView.builder and adding a bottom navigation bar.
Assemble the Full Scrollable Home Screen
You have a single service card rendering on your phone. A real marketplace feed needs to display many services and let the user scroll through them.
In this step, you will replace the hardcoded single card with a scrollable list powered by sample data. You will also add a bottom navigation bar so users can switch between screens.
In this step, get ready to:
- Replace the single card with a scrollable feed using ListView.builder.
- Create sample service data to populate the feed.
- Add a BottomNavigationBar with tab switching.
Combine all sections into a scrollable layout
Your home screen already uses SafeArea and a Column to stack the search bar and category chips vertically. The remaining piece is making the card feed scrollable.
Right now, the single ServiceCard sits directly inside the Column. To display multiple cards that scroll independently of the search bar, you need two things:
- An Expanded widget that tells the feed to take up all remaining vertical space below the chips.
- A ListView.builder inside it that creates cards on demand as the user scrolls.
Why ListView.builder instead of a plain Column?
A Column renders every child at once, even if it is off-screen. ListView.builder only creates the widgets that are currently visible. For a feed that could grow to dozens of items, this keeps your app smooth and memory-efficient.
Populate the feed with sample data
Before the list can build cards dynamically, it needs a data source. You will create a list of maps containing sample campus services, then wire it into ListView.builder.
- In home_screen.dart, scroll to the very bottom of the file (below the closing brace of the class).
- Add the following sample data list outside the class:
final List<Map<String, dynamic>> sampleServices = [
{
'title': 'Math Tutoring',
'provider': 'Sarah Johnson',
'price': '\$15/hr',
'rating': '4.8',
'color': Colors.blue[100],
},
{
'title': 'Laundry Pickup',
'provider': 'CleanCampus',
'price': '\$8/load',
'rating': '4.5',
'color': Colors.green[100],
},
{
'title': 'Graphic Design',
'provider': 'Mike Chen',
'price': '\$25/project',
'rating': '4.9',
'color': Colors.orange[100],
},
{
'title': 'Food Delivery',
'provider': 'CampusEats',
'price': '\$3 fee',
'rating': '4.3',
'color': Colors.red[100],
},
{
'title': 'Printing Service',
'provider': 'QuickPrint Hub',
'price': '\$0.10/page',
'rating': '4.6',
'color': Colors.purple[100],
},
];
What does this data do?
Each map represents one campus service with a title, provider name, price, rating, and a placeholder color for the image area. The ListView.builder will read from this list to generate cards.
- Now find the single ServiceCard widget inside the Column's children list:
ServiceCard(
title: 'Math Tutoring',
provider: 'Sarah Johnson',
price: '\$15/hr',
rating: '4.8',
imageColor: Colors.blue[100]!,
),
- Replace that entire ServiceCard(...) block with the following Expanded widget:
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: sampleServices.length,
itemBuilder: (context, index) {
final service = sampleServices[index];
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: ServiceCard(
title: service['title']!,
provider: service['provider']!,
price: service['price']!,
rating: service['rating']!,
imageColor: service['color'] as Color,
),
);
},
),
),
What does this code do?
Expanded tells the list to fill all remaining vertical space below the chips. Without it, Flutter would not know how tall to make the scrollable area.
ListView.builder uses itemCount and itemBuilder to generate only the cards visible on screen. Each card pulls its data from sampleServices by index.
- Save home_screen.dart and hot reload.
- On your phone, you should see 5 service cards stacked vertically. Scroll up and down to see them all.
Cards not showing or seeing a red error?
Make sure the sampleServices list is placed outside the class braces (after the final } of the widget class), not inside them.
If you see a vertical overflow error, confirm you wrapped ListView.builder in an Expanded widget. Without it, the Column does not know how much space the list should take.
Still stuck? Help me fix my ListView.builder. I replaced the single ServiceCard with an Expanded widget wrapping ListView.builder but I'm seeing an error.
Add the BottomNavigationBar
Your Figma design has navigation tabs at the bottom of the screen. In Flutter, this is a BottomNavigationBar widget that sits below the main body.
To highlight the active tab when the user taps it, the widget needs to track which tab is selected. This requires converting your StatelessWidget into a StatefulWidget so it can call setState and redraw.
- In home_screen.dart, replace the entire class declaration and build method with the following code. This converts the widget to a StatefulWidget and adds the bottom nav bar:
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// Track which tab is currently selected
int _selectedIndex = 0;
// Update the selected tab when user taps
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
body: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: const TextField(
decoration: InputDecoration(
hintText: 'Search services...',
prefixIcon: Icon(Icons.search, color: Colors.grey),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
),
),
),
SizedBox(
height: 50,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
_buildCategoryChip('All', isSelected: true),
const SizedBox(width: 8),
_buildCategoryChip('Tutoring'),
const SizedBox(width: 8),
_buildCategoryChip('Laundry'),
const SizedBox(width: 8),
_buildCategoryChip('Food'),
const SizedBox(width: 8),
_buildCategoryChip('Design'),
const SizedBox(width: 8),
_buildCategoryChip('Printing'),
],
),
),
),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: sampleServices.length,
itemBuilder: (context, index) {
final service = sampleServices[index];
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: ServiceCard(
title: service['title']!,
provider: service['provider']!,
price: service['price']!,
rating: service['rating']!,
imageColor: service['color'] as Color,
),
);
},
),
),
],
),
),
// Bottom navigation bar with four tabs
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: _selectedIndex,
onTap: _onItemTapped,
selectedItemColor: Colors.deepPurple,
unselectedItemColor: Colors.grey,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.explore), label: 'Explore'),
BottomNavigationBarItem(icon: Icon(Icons.message), label: 'Messages'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
// Helper to build a styled category chip
Widget _buildCategoryChip(String label, {bool isSelected = false}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? Colors.deepPurple : Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected ? Colors.deepPurple : Colors.grey[300]!,
),
),
child: Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
);
}
}
What changed here?
HomeScreen now extends StatefulWidget instead of StatelessWidget. The actual UI lives in _HomeScreenState, which can hold mutable data.
_selectedIndex stores which tab is active. When the user taps a tab, _onItemTapped calls setState to update the index and trigger a rebuild so the highlight moves.
The bottomNavigationBar property on Scaffold places the nav bar below the body. Setting type: BottomNavigationBarType.fixed ensures all four items are always visible.
- Save home_screen.dart and hot reload.
- Your phone should now show the full home screen: search bar at the top, scrollable category chips, a scrollable list of 5 service cards, and a bottom navigation bar with four tabs.
- Tap each tab in the bottom nav. The highlight color should change to purple on the active tab.
Bottom nav not appearing?
Make sure bottomNavigationBar is a property of Scaffold (at the same level as body), not nested inside the body's Column.
If you see a red error about createState, confirm your class declaration says extends StatefulWidget and the state class says extends State<HomeScreen>.
Still stuck? Help me debug my BottomNavigationBar. I converted HomeScreen to a StatefulWidget and added a bottomNavigationBar to the Scaffold but something is wrong.
✔️ Awesome, I've got everything!
Great. Make sure you have saved home_screen.dart and confirmed the full home screen is working on your phone.
ⓧ I'd like to double check the full code
Here is the complete home_screen.dart at this point:
import 'package:flutter/material.dart';
import '../widgets/service_card.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
body: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: const TextField(
decoration: InputDecoration(
hintText: 'Search services...',
prefixIcon: Icon(Icons.search, color: Colors.grey),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
),
),
),
SizedBox(
height: 50,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
_buildCategoryChip('All', isSelected: true),
const SizedBox(width: 8),
_buildCategoryChip('Tutoring'),
const SizedBox(width: 8),
_buildCategoryChip('Laundry'),
const SizedBox(width: 8),
_buildCategoryChip('Food'),
const SizedBox(width: 8),
_buildCategoryChip('Design'),
const SizedBox(width: 8),
_buildCategoryChip('Printing'),
],
),
),
),
const SizedBox(height: 16),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: sampleServices.length,
itemBuilder: (context, index) {
final service = sampleServices[index];
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: ServiceCard(
title: service['title']!,
provider: service['provider']!,
price: service['price']!,
rating: service['rating']!,
imageColor: service['color'] as Color,
),
);
},
),
),
],
),
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: _selectedIndex,
onTap: _onItemTapped,
selectedItemColor: Colors.deepPurple,
unselectedItemColor: Colors.grey,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.explore), label: 'Explore'),
BottomNavigationBarItem(icon: Icon(Icons.message), label: 'Messages'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
Widget _buildCategoryChip(String label, {bool isSelected = false}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: isSelected ? Colors.deepPurple : Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected ? Colors.deepPurple : Colors.grey[300]!,
),
),
child: Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
);
}
}
final List<Map<String, dynamic>> sampleServices = [
{
'title': 'Math Tutoring',
'provider': 'Sarah Johnson',
'price': '\$15/hr',
'rating': '4.8',
'color': Colors.blue[100],
},
{
'title': 'Laundry Pickup',
'provider': 'CleanCampus',
'price': '\$8/load',
'rating': '4.5',
'color': Colors.green[100],
},
{
'title': 'Graphic Design',
'provider': 'Mike Chen',
'price': '\$25/project',
'rating': '4.9',
'color': Colors.orange[100],
},
{
'title': 'Food Delivery',
'provider': 'CampusEats',
'price': '\$3 fee',
'rating': '4.3',
'color': Colors.red[100],
},
{
'title': 'Printing Service',
'provider': 'QuickPrint Hub',
'price': '\$0.10/page',
'rating': '4.6',
'color': Colors.purple[100],
},
];
Your home screen is fully assembled. You have a polished layout that matches your Figma design with search, categories, a scrollable feed, and navigation tabs. Next up, you will tackle the Secret Mission and wire a second screen into the bottom nav.
Secret mission
Add the Explore Screen
Your bottom navigation bar has an Explore tab, but tapping it does nothing yet. In this secret mission, you will create a second screen with a grid layout and wire real navigation logic so tapping between tabs switches views.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files to continue building, or delete them if you were just experimenting.
Resources you used:
- The campus_marketplace Flutter project folder on your Desktop.
Keep everything running
No action needed. Choose this if you plan to keep building the remaining screens (login, profile, messages) for your campus marketplace app.
- Your project stays in the campus_marketplace folder on your Desktop, ready to open in VS Code any time.
- To resume development later, open the folder in VS Code and run flutter run with your phone connected.
Pause - I'll come back to this later
Since everything is local, there is nothing to pause. Your project files sit on disk without consuming resources while idle.
- If you have flutter run still active in your terminal, press q to quit the running app.
- Disconnect your Android phone from USB.
Delete - I don't want to use this again
Remove the project folder entirely to free up disk space.
- If you have flutter run still active in your terminal, press q to quit first.
- Delete the project folder by running the command for your operating system:
macOS
cd ~/Desktop
rm -rf campus_marketplace
Windows
cd ~\Desktop
Remove-Item -Recurse -Force campus_marketplace
- Confirm the folder is gone by running ls (macOS) or dir (Windows) in your Desktop directory. You should no longer see campus_marketplace in the output.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully styled campus marketplace home screen by translating your Figma design into Flutter code, and you have it running on your actual phone.
You've learned how to:
- Decompose a Figma design into a Flutter widget tree using Row, Column, Container, and ListView to build a complete home screen layout.
- Create a reusable ServiceCard widget with proper parameterization for title, provider, price, rating, and image color.
- Implement a scrollable feed with ListView.builder and a functioning BottomNavigationBar with StatefulWidget state management.
- Secret Mission: Wired a second Explore screen with GridView and bottom nav tab switching.
Ready to quiz yourself?