# How to Build a Custom User Profile Page in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 20-30 min
- Compatibility: FlutterFlow Free+ (FlutterFlowUploadButton for avatar/cover)
- Last updated: March 2026

## TL;DR

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).

## Before you start

- 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

### 1. 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.

### 2. 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.

### 3. 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.

### 4. 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.

### 5. 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 code example

File: `User Profile Page Architecture`

```text
Firestore Data Model:
├── users/{uid}
│   ├── displayName: String ("Jane Smith")
│   ├── bio: String ("Fitness enthusiast & designer")
│   ├── avatarUrl: String (Storage URL)
│   ├── coverPhotoUrl: String (Storage URL)
│   ├── location: String ("San Francisco, CA")
│   ├── postsCount: Integer (42) [denormalized]
│   ├── followerCount: Integer (128) [denormalized]
│   ├── followingCount: Integer (95) [denormalized]
│   └── createdAt: Timestamp
└── users/{uid}/following/{followedUid}
    └── timestamp: Timestamp

ProfilePage (receives optional userId param):
├── Stack (cover photo + avatar)
│   ├── Container (h: 200, DecorationImage: coverPhotoUrl)
│   │   └── Camera IconButton overlay [if own profile]
│   └── Positioned (bottom: -50, center)
│       └── Stack
│           ├── CircleImage (avatarUrl, 100x100, white border)
│           └── Positioned (bottom-right)
│               └── Camera IconButton [if own profile]
├── SizedBox (h: 60) — space for avatar overflow
├── Column (user info)
│   ├── IF !isEditing: Text(displayName, headlineSmall, bold)
│   │   IF isEditing: TextField(editDisplayName)
│   ├── IF !isEditing: Text(bio, bodyMedium, secondary)
│   │   IF isEditing: TextField(editBio, multiline)
│   ├── IF !isEditing: Row: Icon(location_on) + Text(location)
│   │   IF isEditing: TextField(editLocation)
│   └── IF own profile:
│       ├── Button: "Edit Profile" [if !isEditing]
│       └── Row: Button("Save") + Button("Cancel") [if isEditing]
├── Row (stats)
│   ├── Column: Text(postsCount, bold) + Text("Posts")
│   ├── Column: Text(followerCount, bold) + Text("Followers")
│   │   └── On Tap → FollowersListPage
│   └── Column: Text(followingCount, bold) + Text("Following")
│       └── On Tap → FollowingListPage
├── Button: Follow/Unfollow [if other user's profile]
│   ├── Follow → Create following doc + increment both counts
│   └── Unfollow → Delete following doc + decrement both counts
└── TabBar
    ├── Posts Tab: ListView (posts where authorId==userId)
    └── About Tab: joined date + location + bio
```

## Common mistakes

- **Fetching follower and following counts by querying the entire subcollection each time** — Querying all documents in a following subcollection just to count them is O(n) reads. A user with 500 followers costs 500 reads every time their profile is loaded. At scale, this is prohibitively expensive and slow. Fix: 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.
- **Using Page State for avatar URL after upload without updating Firestore** — The uploaded image URL is only stored in Page State, which resets on navigation. The avatar reverts to the old image when the user returns to the profile page because Firestore still has the old URL. Fix: 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.
- **Not checking whether the profile belongs to the current user before showing edit controls** — Without this check, the Edit Profile button and camera upload icons appear on every user's profile — visitors could attempt to edit someone else's profile (though Firestore rules should block the write). Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-build-a-custom-user-profile-page-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-build-a-custom-user-profile-page-in-flutterflow
