Build a user profile page with a CircleImage avatar that opens ImagePicker on tap for uploading to Firebase Storage, a cover photo Container, display name and bio Text fields that swap to TextFields in edit mode via a Page State isEditing toggle, a stats row showing posts, followers, and following counts denormalized on the user document, an activity feed ListView of recent posts, and a follow/unfollow button for viewing other users' profiles.
User profile with avatar upload, edit mode, stats, and follow system
A user profile page is the public-facing identity of each user in your app. This tutorial builds a complete profile: a cover photo and circular avatar with tap-to-change uploads, display name and bio that switch between display mode and edit mode, a stats row showing posts, followers, and following counts, a tabbed activity feed showing recent posts, and a follow/unfollow system for viewing other users' profiles. This is distinct from a settings page (app preferences) or a signup form (initial account creation).
Prerequisites
- A FlutterFlow project with Firebase/Firestore connected
- Firebase Authentication enabled with user accounts
- Firebase Storage enabled for avatar and cover photo uploads
- Users collection in Firestore with basic user data fields
Step-by-step guide
Set up the user document structure with profile and stats fields
Set up the user document structure with profile and stats fields
Ensure your Firestore users collection has the following fields: displayName (String), bio (String, default empty), avatarUrl (String — Firebase Storage URL), coverPhotoUrl (String — Firebase Storage URL), location (String), postsCount (Integer, default 0), followerCount (Integer, default 0), followingCount (Integer, default 0), and createdAt (Timestamp). Create a subcollection users/{uid}/following where each document ID is the UID of a followed user (with a timestamp field for when the follow happened). The stats counts are denormalized — updated atomically whenever a follow/unfollow or post action occurs. Fill in test data for at least two users.
Expected result: User documents have all profile fields plus denormalized counter fields for posts, followers, and following.
Build the profile header with avatar and cover photo upload
Build the profile header with avatar and cover photo upload
Create a ProfilePage that receives an optional userId parameter (empty means current user). At the top, add a Stack for the cover photo and avatar. Bottom layer: a Container (height: 200, full width) with a DecorationImage bound to the user's coverPhotoUrl field (use a default gradient if empty). Top layer: a Positioned CircleImage (diameter: 100, border: 3px white) centered at the bottom edge of the cover photo (bottom: -50, centerHorizontal). For the current user's own profile, overlay a small camera IconButton on the avatar's bottom-right corner. On Tap of the camera icon, trigger FlutterFlowUploadButton configured for Firebase Storage path users/{uid}/avatar.jpg. On upload complete, update the user document's avatarUrl field. Similarly, add a camera icon on the cover photo that uploads to users/{uid}/cover.jpg and updates coverPhotoUrl.
Expected result: Profile displays the cover photo and circular avatar. Tapping the camera icons opens the image picker and uploads to Storage.
Implement edit mode toggle for display name, bio, and location
Implement edit mode toggle for display name, bio, and location
Below the avatar, add a Column for user info. Create a Page State variable isEditing (Boolean, default false). For each field (displayName, bio, location), use Conditional Visibility to switch between display and edit widgets: when isEditing is false, show a Text widget with the field value; when isEditing is true, show a TextField pre-filled with the current value bound to Page State edit variables (editDisplayName, editBio, editLocation). Add an Edit Profile button (visible when isEditing is false and the profile is the current user's) that sets isEditing to true. Add Save and Cancel buttons (visible when isEditing is true): Save triggers an Update Document action on the user document with the edited values and sets isEditing back to false; Cancel discards changes and sets isEditing to false. For other users' profiles, hide the edit button entirely.
Expected result: Tapping Edit Profile swaps static text for editable TextFields. Save updates Firestore and returns to display mode.
Display the stats row with posts, followers, and following counts
Display the stats row with posts, followers, and following counts
Below the user info, add a Row with three equally spaced Column widgets for the stats. Each Column contains: a Text with the count value (headlineSmall, bold) and a Text label below it (bodySmall, secondary color). Column 1: postsCount + 'Posts'. Column 2: followerCount + 'Followers'. Column 3: followingCount + 'Following'. Tap on Followers navigates to a FollowersListPage that queries users whose following subcollection contains this user's UID. Tap on Following navigates to a FollowingListPage that queries the user's following subcollection and resolves each UID to a user document. These counts are denormalized on the user document for instant display — no subcollection count queries needed.
Expected result: A stats row shows posts, followers, and following counts. Tapping followers or following navigates to the respective lists.
Build the follow/unfollow button and activity feed tab
Build the follow/unfollow button and activity feed tab
For other users' profiles (userId != currentUser.uid), add a Follow/Unfollow button below the stats row. Check if the current user follows this profile by querying users/{currentUser.uid}/following/{profileUserId}. If the document exists, show an Unfollow button (outlined style); if not, show a Follow button (filled primary style). Follow action: create a document at users/{currentUser.uid}/following/{profileUserId} with timestamp, then atomically increment the current user's followingCount and the profile user's followerCount using FieldValue.increment(1). Unfollow reverses this: delete the following document and decrement both counts. Below the button, add a TabBar with 'Posts' and 'About' tabs. Posts tab: ListView of recent posts where authorId == profileUserId ordered by timestamp desc. About tab: joined date, location, and bio in a simple Column layout.
Expected result: Visiting another user's profile shows a Follow/Unfollow button that updates counts atomically. The activity feed shows their recent posts.
Complete working example
1Firestore Data Model:2├── users/{uid}3│ ├── displayName: String ("Jane Smith")4│ ├── bio: String ("Fitness enthusiast & designer")5│ ├── avatarUrl: String (Storage URL)6│ ├── coverPhotoUrl: String (Storage URL)7│ ├── location: String ("San Francisco, CA")8│ ├── postsCount: Integer (42) [denormalized]9│ ├── followerCount: Integer (128) [denormalized]10│ ├── followingCount: Integer (95) [denormalized]11│ └── createdAt: Timestamp12└── users/{uid}/following/{followedUid}13 └── timestamp: Timestamp1415ProfilePage (receives optional userId param):16├── Stack (cover photo + avatar)17│ ├── Container (h: 200, DecorationImage: coverPhotoUrl)18│ │ └── Camera IconButton overlay [if own profile]19│ └── Positioned (bottom: -50, center)20│ └── Stack21│ ├── CircleImage (avatarUrl, 100x100, white border)22│ └── Positioned (bottom-right)23│ └── Camera IconButton [if own profile]24├── SizedBox (h: 60) — space for avatar overflow25├── Column (user info)26│ ├── IF !isEditing: Text(displayName, headlineSmall, bold)27│ │ IF isEditing: TextField(editDisplayName)28│ ├── IF !isEditing: Text(bio, bodyMedium, secondary)29│ │ IF isEditing: TextField(editBio, multiline)30│ ├── IF !isEditing: Row: Icon(location_on) + Text(location)31│ │ IF isEditing: TextField(editLocation)32│ └── IF own profile:33│ ├── Button: "Edit Profile" [if !isEditing]34│ └── Row: Button("Save") + Button("Cancel") [if isEditing]35├── Row (stats)36│ ├── Column: Text(postsCount, bold) + Text("Posts")37│ ├── Column: Text(followerCount, bold) + Text("Followers")38│ │ └── On Tap → FollowersListPage39│ └── Column: Text(followingCount, bold) + Text("Following")40│ └── On Tap → FollowingListPage41├── Button: Follow/Unfollow [if other user's profile]42│ ├── Follow → Create following doc + increment both counts43│ └── Unfollow → Delete following doc + decrement both counts44└── TabBar45 ├── Posts Tab: ListView (posts where authorId==userId)46 └── About Tab: joined date + location + bioCommon mistakes when building a Custom User Profile Page in FlutterFlow
Why it's a problem: Fetching follower and following counts by querying the entire subcollection each time
How to avoid: Denormalize: store followerCount and followingCount as Integer fields directly on the user document. Increment and decrement these atomically using FieldValue.increment(1) and FieldValue.increment(-1) on follow/unfollow actions. The profile page reads one document for all counts.
Why it's a problem: Using Page State for avatar URL after upload without updating Firestore
How to avoid: After FlutterFlowUploadButton completes, immediately update the user document's avatarUrl field in Firestore with the new Storage URL. The profile page always reads from Firestore, ensuring the latest image persists.
Why it's a problem: Not checking whether the profile belongs to the current user before showing edit controls
How to avoid: Add Conditional Visibility on all edit controls: only show when the userId parameter equals currentUser.uid (or when userId is empty, meaning the current user's own profile). This provides a clean read-only experience for visitors.
Best practices
- Denormalize followerCount and followingCount on the user document — never count subcollection docs on every profile load
- Use FieldValue.increment and FieldValue.increment(-1) for atomic count updates on follow/unfollow
- Resize uploaded images (512x512 for avatars, 1200x400 for covers) to keep Storage costs and load times low
- Use Conditional Visibility to cleanly separate edit mode and display mode without duplicate widgets where possible
- Always update Firestore after image upload — do not rely on Page State for persistent avatar URLs
- Add a settings gear icon in the AppBar that navigates to the settings page for account management
- Test the follow/unfollow flow between two accounts to verify counts increment and decrement correctly
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Design a Firestore data model for a user profile system with display name, bio, avatar, cover photo, and denormalized counts for posts, followers, and following. Include a following subcollection and write the logic for atomic follow/unfollow operations that increment and decrement the count fields on both user documents.
Create a user profile page with a cover photo at the top, a circular avatar overlapping the bottom of the cover, display name and bio below, a row showing posts count, followers count, and following count, and a Follow button. Add an edit mode where tapping Edit Profile turns the text fields into editable inputs.
Frequently asked questions
How do I handle the avatar overlapping the cover photo?
Use a Stack widget. Place the cover photo Container as the bottom layer. Add the CircleImage as a Positioned widget with bottom: -50 (half the avatar height) to create the overlap effect. Add a SizedBox with height 60 below the Stack to create space for the overflowing avatar in the page layout.
Can I add a profile completion percentage indicator?
Yes. Create a Custom Function that checks which fields are filled (avatar, cover, bio, location) and calculates a percentage. Display it as a LinearPercentIndicator on the profile page with a prompt like 'Complete your profile: add a bio' for missing fields.
How do I prevent users from following themselves?
The Follow button should only appear when the profile userId does not equal currentUser.uid. Add this as a Conditional Visibility check. As a safety net, add a Firestore Security Rule on the following subcollection that rejects writes where the document ID equals the parent user's UID.
How do I show a user's posts on their profile?
Add a ListView in the Posts tab bound to a Backend Query on your posts collection where authorId equals the profile user's UID, ordered by timestamp descending. Reuse the same PostCard component from your main feed for consistency.
What image dimensions should I use for avatars and cover photos?
For avatars, resize to 512x512 pixels — they display at 100x100 or smaller, so higher resolution is wasted storage and bandwidth. For cover photos, resize to 1200x400 pixels to match the wide aspect ratio. Use JPEG compression at 80% quality.
Can RapidDev help build a social profile system with verification and privacy settings?
Yes. A production profile system with identity verification badges, granular privacy settings (who can see activity, follow requests), profile analytics, and content recommendation requires Cloud Functions and custom logic. RapidDev can build the complete social profile platform.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation