Building & Running a Complete Starter App
Capstone: Personal Profile Card App
In this final lesson of the Flutter Setup & First App tutorial, you will build a complete personal profile card app from scratch. This capstone project combines everything you’ve learned: creating a project, configuring pubspec.yaml, building a widget tree, adding custom fonts and colors, using hot reload, debugging, and building for release. Follow along step by step to create a polished, professional-looking app.
Step 1: Create the Project
Let’s start by creating a new Flutter project with a custom organization name.
Creating the Project
# Create the project
flutter create --org com.example profile_card_app
# Navigate into the project
cd profile_card_app
# Open in your IDE
code . # VS Code
# or
studio . # Android Studio
Project Structure Overview
After creation, your project has this structure:
Project Directory Structure
profile_card_app/
├── android/ # Android-specific configuration
├── ios/ # iOS-specific configuration
├── lib/ # Your Dart code lives here
│ └── main.dart # App entry point
├── test/ # Test files
├── web/ # Web platform files
├── pubspec.yaml # Project configuration
├── pubspec.lock # Dependency lock file
├── analysis_options.yaml # Lint rules
└── README.md
Step 2: Configure pubspec.yaml
Before writing any code, let’s configure our project’s dependencies and assets.
Updated pubspec.yaml
name: profile_card_app
description: A personal profile card app built with Flutter.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.6
google_fonts: ^6.1.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.1
flutter:
uses-material-design: true
assets:
- assets/images/
Set Up the Assets Directory
Create the assets directory and add a placeholder profile image:
Creating Asset Directories
# Create the assets directory
mkdir -p assets/images
# Add a profile image to assets/images/
# You can use any image file named profile.jpg or profile.png
# For now, we'll use a placeholder from the network
# Install dependencies
flutter pub get
assets/images/ and use Image.asset().Step 3: Define the Color Scheme and Theme
Let’s create a clean, professional color scheme for our app. We’ll define our theme in main.dart.
lib/main.dart - App Entry Point and Theme
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
void main() {
runApp(const ProfileCardApp());
}
class ProfileCardApp extends StatelessWidget {
const ProfileCardApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Profile Card',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1A73E8),
brightness: Brightness.light,
),
textTheme: GoogleFonts.poppinsTextTheme(),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1A73E8),
brightness: Brightness.dark,
),
textTheme: GoogleFonts.poppinsTextTheme(
ThemeData.dark().textTheme,
),
useMaterial3: true,
),
home: const ProfileScreen(),
);
}
}
ColorScheme.fromSeed() which is the Material 3 way to generate a harmonious color palette from a single seed color. This ensures all your colors look good together automatically.Step 4: Build the Profile Screen
Now let’s build the main profile screen with a Scaffold, AppBar, and a scrollable body.
ProfileScreen Widget
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('My Profile'),
centerTitle: true,
backgroundColor: colorScheme.primaryContainer,
foregroundColor: colorScheme.onPrimaryContainer,
elevation: 0,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const SizedBox(height: 20),
const ProfileHeader(),
const SizedBox(height: 24),
const ProfileInfoCard(),
const SizedBox(height: 16),
const SkillsCard(),
const SizedBox(height: 16),
const ContactCard(),
const SizedBox(height: 20),
],
),
),
);
}
}
Step 5: Create the Profile Header
The profile header displays the user’s avatar, name, and title.
ProfileHeader Widget
class ProfileHeader extends StatelessWidget {
const ProfileHeader({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Column(
children: [
// Profile Avatar
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: colorScheme.primary,
width: 3,
),
boxShadow: [
BoxShadow(
color: colorScheme.primary.withOpacity(0.3),
blurRadius: 12,
spreadRadius: 2,
),
],
),
child: CircleAvatar(
radius: 60,
backgroundColor: colorScheme.primaryContainer,
child: Icon(
Icons.person,
size: 60,
color: colorScheme.onPrimaryContainer,
),
),
),
const SizedBox(height: 16),
// Name
Text(
'Edrees Salih',
style: textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 4),
// Title
Text(
'Flutter & Web Developer',
style: textTheme.titleMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
// Location
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.location_on_outlined,
size: 16,
color: colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Text(
'Saudi Arabia',
style: textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
],
);
}
}
Step 6: Create the Profile Info Card
This card shows a brief bio and key statistics.
ProfileInfoCard Widget
class ProfileInfoCard extends StatelessWidget {
const ProfileInfoCard({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section Title
Row(
children: [
Icon(Icons.info_outline, color: colorScheme.primary),
const SizedBox(width: 8),
Text(
'About Me',
style: textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const Divider(height: 24),
// Bio Text
Text(
'Passionate developer with experience in Flutter, Laravel, '
'and modern web technologies. I love building beautiful, '
'functional applications that solve real problems.',
style: textTheme.bodyLarge?.copyWith(
height: 1.6,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 20),
// Stats Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStat(context, '5+', 'Years Exp'),
_buildStat(context, '50+', 'Projects'),
_buildStat(context, '10+', 'Courses'),
],
),
],
),
),
);
}
Widget _buildStat(BuildContext context, String value, String label) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Column(
children: [
Text(
value,
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
const SizedBox(height: 4),
Text(
label,
style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
);
}
}
Step 7: Create the Skills Card
The skills card displays technology proficiencies using Chip widgets inside a Wrap widget.
SkillsCard Widget
class SkillsCard extends StatelessWidget {
const SkillsCard({super.key});
static const List<Map<String, dynamic>> skills = [
{'name': 'Flutter', 'icon': Icons.phone_android},
{'name': 'Dart', 'icon': Icons.code},
{'name': 'Laravel', 'icon': Icons.web},
{'name': 'PHP', 'icon': Icons.data_object},
{'name': 'JavaScript', 'icon': Icons.javascript},
{'name': 'React', 'icon': Icons.display_settings},
{'name': 'MySQL', 'icon': Icons.storage},
{'name': 'Git', 'icon': Icons.merge_type},
{'name': 'Firebase', 'icon': Icons.cloud},
{'name': 'REST API', 'icon': Icons.api},
];
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section Title
Row(
children: [
Icon(Icons.build_outlined, color: colorScheme.primary),
const SizedBox(width: 8),
Text(
'Skills',
style: textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const Divider(height: 24),
// Skills Chips
Wrap(
spacing: 8,
runSpacing: 8,
children: skills.map((skill) {
return Chip(
avatar: Icon(
skill['icon'] as IconData,
size: 18,
color: colorScheme.onSecondaryContainer,
),
label: Text(skill['name'] as String),
backgroundColor: colorScheme.secondaryContainer,
labelStyle: TextStyle(
color: colorScheme.onSecondaryContainer,
),
side: BorderSide.none,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
);
}).toList(),
),
],
),
),
);
}
}
Step 8: Create the Contact Card
The contact card displays contact information with interactive buttons.
ContactCard Widget
class ContactCard extends StatelessWidget {
const ContactCard({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section Title
Row(
children: [
Icon(Icons.contact_mail_outlined, color: colorScheme.primary),
const SizedBox(width: 8),
Text(
'Contact',
style: textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const Divider(height: 24),
// Contact Items
_buildContactItem(
context,
Icons.email_outlined,
'Email',
'edrees@example.com',
),
const SizedBox(height: 12),
_buildContactItem(
context,
Icons.language,
'Website',
'esb1995.com',
),
const SizedBox(height: 12),
_buildContactItem(
context,
Icons.code,
'GitHub',
'github.com/edrees',
),
const SizedBox(height: 20),
// Action Buttons
Row(
children: [
Expanded(
child: FilledButton.icon(
onPressed: () {
debugPrint('Send message tapped');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Message feature coming soon!'),
),
);
},
icon: const Icon(Icons.send),
label: const Text('Message'),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: () {
debugPrint('Share profile tapped');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Share feature coming soon!'),
),
);
},
icon: const Icon(Icons.share),
label: const Text('Share'),
),
),
],
),
],
),
),
);
}
Widget _buildContactItem(
BuildContext context,
IconData icon,
String label,
String value,
) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
size: 20,
color: colorScheme.onPrimaryContainer,
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
Text(
value,
style: textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
],
),
],
);
}
}
Step 9: Use Hot Reload to Iterate
Now that your app is built, let’s practice using hot reload to make changes instantly.
Hot Reload Workflow
# Run the app
flutter run
# Make a change in code, for example change the name:
# 'Edrees Salih' -> 'Your Name Here'
# Press 'r' in the terminal for hot reload
# OR press Ctrl+S (VS Code) / Cmd+S (macOS) if auto-save is enabled
# The change appears instantly without losing app state!
# Try these changes and hot reload each one:
# 1. Change the seed color: Color(0xFF1A73E8) -> Color(0xFF6750A4)
# 2. Change the bio text
# 3. Add a new skill to the skills list
# 4. Change the contact information
R) instead.Step 10: Debug a Deliberate Bug
Let’s intentionally introduce a bug and practice debugging it. This exercise teaches you how to read error messages and fix common issues.
Introducing a Layout Bug
// Replace the Stats Row in ProfileInfoCard with this buggy code:
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStat(context, '5+', 'Years Experience in Development'),
_buildStat(context, '50+', 'Completed Projects Delivered'),
_buildStat(context, '10+', 'Online Courses Published'),
],
)
// This causes a RenderFlex overflow because the long text
// doesn't fit in the Row!
// ERROR: A RenderFlex overflowed by 42 pixels on the right.
// The relevant error-causing widget was: Row
// FIX: Make the stat labels shorter, or wrap each stat
// in an Expanded widget:
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(child: _buildStat(context, '5+', 'Years\nExp')),
Expanded(child: _buildStat(context, '50+', 'Projects')),
Expanded(child: _buildStat(context, '10+', 'Courses')),
],
)
Debugging with Breakpoints
// Add a breakpoint in ProfileInfoCard.build()
// 1. Click the gutter next to: final colorScheme = ...
// 2. Run in debug mode (F5)
// 3. Navigate to the profile screen
// 4. Execution pauses at your breakpoint
// In the debug sidebar, inspect:
// - context.widget.runtimeType
// - colorScheme.primary (see the actual color)
// - textTheme.titleLarge (see font properties)
// Add a watch expression:
// MediaQuery.of(context).size.width
// This shows you the screen width, useful for responsive design
Step 11: Build for Release
Now that your app is working correctly, let’s build it for release.
Building the Release Version
# First, analyze the code for any issues
flutter analyze
# Run all tests
flutter test
# Build a release APK (Android)
flutter build apk --split-per-abi
# Build an App Bundle for Play Store (Android)
flutter build appbundle
# Build for web
flutter build web
# Build for iOS (macOS only)
flutter build ios
Finding Your Build Output
# APK files (split by architecture):
# build/app/outputs/flutter-apk/app-arm64-v8a-release.apk (~15 MB)
# build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk (~12 MB)
# build/app/outputs/flutter-apk/app-x86_64-release.apk (~15 MB)
# App Bundle:
# build/app/outputs/bundle/release/app-release.aab
# Web output:
# build/web/index.html (and supporting files)
Step 12: The Complete main.dart File
Here is the complete main.dart file with all the widgets we built. You can copy this entire file to get the working app:
Complete lib/main.dart
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
void main() {
runApp(const ProfileCardApp());
}
class ProfileCardApp extends StatelessWidget {
const ProfileCardApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Profile Card',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1A73E8),
brightness: Brightness.light,
),
textTheme: GoogleFonts.poppinsTextTheme(),
useMaterial3: true,
),
home: const ProfileScreen(),
);
}
}
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('My Profile'),
centerTitle: true,
backgroundColor: colorScheme.primaryContainer,
foregroundColor: colorScheme.onPrimaryContainer,
),
body: const SingleChildScrollView(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
SizedBox(height: 20),
ProfileHeader(),
SizedBox(height: 24),
ProfileInfoCard(),
SizedBox(height: 16),
SkillsCard(),
SizedBox(height: 16),
ContactCard(),
SizedBox(height: 20),
],
),
),
);
}
}
// ... (all widget classes from Steps 5-8 above)
Customization Ideas
Now that you have a working profile card app, here are ideas to make it your own:
Enhancement Ideas
// 1. Add your own profile photo
// Replace the Icon with an actual image:
CircleAvatar(
radius: 60,
backgroundImage: AssetImage('assets/images/profile.jpg'),
)
// 2. Add an Experience section with a Timeline
// 3. Add a Projects showcase with screenshots
// 4. Add dark mode toggle button
// 5. Add animation to the profile avatar
// 6. Add social media links with url_launcher
// 7. Make it responsive for tablets and web
// 8. Add a floating action button for quick actions
What You Built
Congratulations! In this capstone lesson, you built a complete Flutter app from scratch that includes:
- Project creation with
flutter create --org - pubspec.yaml configuration with dependencies and assets
- Material 3 theming with
ColorScheme.fromSeed()and Google Fonts - Widget composition using Scaffold, AppBar, Column, Card, Row, Wrap, and more
- Custom widgets split into reusable, focused components
- Icons and text styling using the theme system
- User interaction with buttons and SnackBars
- Hot reload workflow for rapid iteration
- Debugging practice with intentional bugs
- Release builds for Android, iOS, and web
Tutorial Summary
Over the course of this tutorial, you’ve mastered the fundamentals of Flutter setup and development:
- Lesson 1-4: Installing Flutter, configuring your IDE, and understanding the project structure
- Lesson 5-8: Core widgets, layouts, styling, and the widget tree
- Lesson 9: Debugging techniques for finding and fixing errors efficiently
- Lesson 10: Managing dependencies and assets with pubspec.yaml
- Lesson 11: Flutter CLI commands for every stage of development
- Lesson 12: Building a complete app that ties everything together
You are now ready to move on to more advanced Flutter topics like state management, navigation, and building complete production applications.