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 fromTextFormFieldwidgets. Each editable field will have its own controller.setState: The mechanism to notify the Flutter framework that the internal state of aStatefulWidgethas changed, triggering a rebuild of the UI.- Conditional Rendering: Displaying different widgets (e.g.,
Textfor display,TextFormFieldfor 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
UserProfileModel: A simple data class holds the profile information, making it easy to pass and manage data._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 ininitState.
_toggleEditMode(): This function is called when the edit/save icon is pressed.- It toggles the
_isEditingflag. - When switching from edit to display mode (
_isEditingbecomesfalse), it updates_currentUserProfilewith the new values from theTextEditingControllers. - When switching from display to edit mode, it ensures the controllers are populated with the current
_currentUserProfiledata.
- It toggles the
buildMethod:- It renders a
Cardto contain the profile information. - An
IconButtonin the top right corner toggles the edit mode. Its icon changes dynamically betweenIcons.editandIcons.savebased on_isEditing. CircleAvatardisplays the profile picture.- The
_buildProfileFieldhelper method is used for each data field.
- It renders a
_buildProfileFieldHelper: This method is crucial for conditional rendering.- It takes the field's label, its controller, and the
isEditableflag. - If
isEditableistrue, it returns aTextFormFieldallowing user input. - If
isEditableisfalse, it returns a simpleTextwidget to display the current value.
- It takes the field's label, its controller, and the
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.