image

17 Jan 2026

9K

35K

Building an Editable Profile Card Widget in Flutter

Profile cards are ubiquitous in modern applications, providing a snapshot of user information. Enhancing these cards with editable fields significantly improves user experience, allowing for seamless updates without navigating to separate settings screens. This article will guide you through building a dynamic profile card widget in Flutter that supports toggling between display and edit modes.

Core Concepts

To achieve an editable profile card, we'll leverage several fundamental Flutter concepts:

  • StatefulWidget: Essential for managing internal state, such as whether the card is in edit mode or displaying saved data.
  • TextEditingController: Used to control and retrieve text input from TextFormField widgets. Each editable field will have its own controller.
  • setState: The mechanism to notify the Flutter framework that the internal state of a StatefulWidget has changed, triggering a rebuild of the UI.
  • Conditional Rendering: Displaying different widgets (e.g., Text for display, TextFormField for editing) based on the current state.

Step-by-Step Implementation

1. Project Setup

Start by creating a new Flutter project:


flutter create editable_profile_card
cd editable_profile_card

Replace the content of lib/main.dart with a basic app structure:


import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Editable Profile Card',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Profile'),
      ),
      body: Center(
        // ProfileCardWidget will go here
        child: Text('Loading Profile Card...'),
      ),
    );
  }
}

2. Define a User Profile Model

A simple data model will help manage user information:


// lib/models/user_profile.dart
class UserProfile {
  String name;
  String email;
  String bio;
  String imageUrl;

  UserProfile({
    required this.name,
    required this.email,
    required this.bio,
    this.imageUrl = 'https://via.placeholder.com/150', // Default image
  });

  // Optional: A copyWith method for immutability (useful in more complex state management)
  UserProfile copyWith({
    String? name,
    String? email,
    String? bio,
    String? imageUrl,
  }) {
    return UserProfile(
      name: name ?? this.name,
      email: email ?? this.email,
      bio: bio ?? this.bio,
      imageUrl: imageUrl ?? this.imageUrl,
    );
  }
}

3. Create the ProfileCardWidget

Now, let's build the ProfileCardWidget as a StatefulWidget to manage its editable state. Create a new file, e.g., lib/widgets/profile_card_widget.dart.


import 'package:flutter/material.dart';
import '../models/user_profile.dart'; // Import the UserProfile model

class ProfileCardWidget extends StatefulWidget {
  final UserProfile userProfile; // Initial profile data

  const ProfileCardWidget({Key? key, required this.userProfile}) : super(key: key);

  @override
  _ProfileCardWidgetState createState() => _ProfileCardWidgetState();
}

class _ProfileCardWidgetState extends State {
  // Local copy of the user profile that can be modified
  late UserProfile _currentUserProfile;
  bool _isEditing = false; // State to toggle edit mode

  // TextEditingControllers for each editable field
  late TextEditingController _nameController;
  late TextEditingController _emailController;
  late TextEditingController _bioController;

  @override
  void initState() {
    super.initState();
    // Initialize controllers with the initial user data
    _currentUserProfile = widget.userProfile;
    _nameController = TextEditingController(text: _currentUserProfile.name);
    _emailController = TextEditingController(text: _currentUserProfile.email);
    _bioController = TextEditingController(text: _currentUserProfile.bio);
  }

  @override
  void dispose() {
    // Dispose controllers to prevent memory leaks
    _nameController.dispose();
    _emailController.dispose();
    _bioController.dispose();
    super.dispose();
  }

  // Function to toggle between display and edit mode
  void _toggleEditMode() {
    setState(() {
      _isEditing = !_isEditing;
      if (!_isEditing) {
        // When exiting edit mode (saving changes)
        // Update the local UserProfile with current controller values
        _currentUserProfile = _currentUserProfile.copyWith(
          name: _nameController.text,
          email: _emailController.text,
          bio: _bioController.text,
        );
        // In a real application, you would send _currentUserProfile
        // to a backend service or global state management here.
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Profile saved!')),
        );
      } else {
        // When entering edit mode, ensure controllers reflect current data
        _nameController.text = _currentUserProfile.name;
        _emailController.text = _currentUserProfile.email;
        _bioController.text = _currentUserProfile.bio;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.all(16.0),
      elevation: 4.0,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            // Edit/Save button
            Align(
              alignment: Alignment.topRight,
              child: IconButton(
                icon: Icon(_isEditing ? Icons.save : Icons.edit),
                onPressed: _toggleEditMode,
                tooltip: _isEditing ? 'Save Profile' : 'Edit Profile',
              ),
            ),
            // Profile Picture
            CircleAvatar(
              radius: 50,
              backgroundImage: NetworkImage(_currentUserProfile.imageUrl),
              backgroundColor: Colors.grey.shade200, // Fallback background
              child: _currentUserProfile.imageUrl.isEmpty // If no image, show initial
                  ? Text(
                      _currentUserProfile.name.isNotEmpty
                          ? _currentUserProfile.name[0].toUpperCase()
                          : '?',
                      style: const TextStyle(fontSize: 40, color: Colors.white),
                    )
                  : null,
            ),
            const SizedBox(height: 16),
            // Name field
            _buildProfileField(
              context,
              label: 'Name',
              controller: _nameController,
              isEditable: _isEditing,
              currentValue: _currentUserProfile.name,
            ),
            // Email field
            _buildProfileField(
              context,
              label: 'Email',
              controller: _emailController,
              isEditable: _isEditing,
              currentValue: _currentUserProfile.email,
              keyboardType: TextInputType.emailAddress,
            ),
            // Bio field
            _buildProfileField(
              context,
              label: 'Bio',
              controller: _bioController,
              isEditable: _isEditing,
              currentValue: _currentUserProfile.bio,
              maxLines: 3,
            ),
          ],
        ),
      ),
    );
  }

  // Helper widget to build individual profile fields (Text or TextFormField)
  Widget _buildProfileField(
    BuildContext context, {
    required String label,
    required TextEditingController controller,
    required bool isEditable,
    required String currentValue,
    TextInputType keyboardType = TextInputType.text,
    int? maxLines = 1,
  }) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            label,
            style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey[600]),
          ),
          const SizedBox(height: 4),
          isEditable
              ? TextFormField(
                  controller: controller,
                  keyboardType: keyboardType,
                  maxLines: maxLines,
                  decoration: InputDecoration(
                    border: const OutlineInputBorder(),
                    isDense: true,
                    contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
                    hintText: 'Enter $label',
                  ),
                  style: Theme.of(context).textTheme.bodyLarge,
                )
              : Text(
                  currentValue.isNotEmpty ? currentValue : 'Not set',
                  style: Theme.of(context).textTheme.bodyLarge,
                ),
        ],
      ),
    );
  }
}

4. Integrate into HomeScreen

Finally, update lib/main.dart to display the ProfileCardWidget:


import 'package:flutter/material.dart';
import 'models/user_profile.dart'; // Import the model
import 'widgets/profile_card_widget.dart'; // Import the widget

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Editable Profile Card',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // Example UserProfile data
    final UserProfile initialProfile = UserProfile(
      name: 'Jane Doe',
      email: '[email protected]',
      bio: 'Flutter developer with a passion for beautiful UIs and robust applications.',
      imageUrl: 'https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500',
    );

    return Scaffold(
      appBar: AppBar(
        title: const Text('My Profile'),
      ),
      body: SingleChildScrollView( // Use SingleChildScrollView to prevent overflow on small screens
        child: Column(
          children: [
            const SizedBox(height: 20),
            ProfileCardWidget(userProfile: initialProfile),
            const SizedBox(height: 20),
            const Text('Scroll down for more content...', style: TextStyle(fontSize: 16)),
            const SizedBox(height: 500), // Filler to demonstrate scrolling
          ],
        ),
      ),
    );
  }
}

Explanation

  1. UserProfile Model: A simple data class holds the profile information, making it easy to pass and manage data.
  2. _ProfileCardWidgetState: This class holds the mutable state of our widget.
    • _currentUserProfile: A local copy of the profile data that reflects changes made in edit mode.
    • _isEditing: A boolean flag that determines if the card is currently in edit mode.
    • TextEditingControllers: Each editable field (name, email, bio) has its own controller initialized with the current profile data in initState.
  3. _toggleEditMode(): This function is called when the edit/save icon is pressed.
    • It toggles the _isEditing flag.
    • When switching from edit to display mode (_isEditing becomes false), it updates _currentUserProfile with the new values from the TextEditingControllers.
    • When switching from display to edit mode, it ensures the controllers are populated with the current _currentUserProfile data.
  4. build Method:
    • It renders a Card to contain the profile information.
    • An IconButton in the top right corner toggles the edit mode. Its icon changes dynamically between Icons.edit and Icons.save based on _isEditing.
    • CircleAvatar displays the profile picture.
    • The _buildProfileField helper method is used for each data field.
  5. _buildProfileField Helper: This method is crucial for conditional rendering.
    • It takes the field's label, its controller, and the isEditable flag.
    • If isEditable is true, it returns a TextFormField allowing user input.
    • If isEditable is false, it returns a simple Text widget to display the current value.
  6. dispose(): Crucially, TextEditingControllers must be disposed of when the widget is removed from the widget tree to prevent memory leaks.

Conclusion

You have successfully built a flexible and user-friendly profile card widget with editable fields in Flutter. This approach can be extended to include more complex fields, validation, and integration with backend services. By leveraging StatefulWidget, TextEditingController, and conditional rendering, you can create dynamic UI components that enhance the interactive experience of your Flutter applications.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is