Flutter Setup & First App

Building & Running a Complete Starter App

55 min Lesson 12 of 12

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.

Note: This is a hands-on lesson. Open your IDE and follow each step. By the end, you’ll have a fully functional profile card app that you can customize with your own information and use as a portfolio piece.

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
Tip: For this tutorial, we’ll use a network image so you don’t need to add a local image file. In a real app, you’d place your photo in 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(),
    );
  }
}
Note: We use 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
Tip: Hot reload preserves your app’s current state (scroll position, form inputs, etc.). If you make structural changes like adding new state variables or changing the widget tree hierarchy, you may need to use hot restart (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
Warning: Always check the Debug Console when your app shows a red or gray error screen. The console provides the exact file, line number, and description of what went wrong. The red screen in debug mode is actually helpful -- it disappears in release mode where a gray screen appears instead.

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)
Note: The release build is significantly smaller and faster than the debug build. Debug builds include debugging tools, assertions, and source maps that make the APK much larger. A typical profile card app like this will be around 12-15 MB as a release APK.

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
Tip: This profile card app is a great foundation for a portfolio project. Expand it with navigation to multiple screens, API integration, and persistent storage to create a full-featured personal app. Every concept you’ve learned in this tutorial -- from project setup to debugging to deployment -- will serve you throughout your Flutter development journey.

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.

Tutorial Complete!

Congratulations! You have completed all lessons in this tutorial.