Day 15: Profile Navigation And Follow System In Connect

Profile Navigation And Follow System

Today, we’re making Connect feel even more interactive! 🚀

We’re enhancing the profile experience, allowing users to:
✅ Tap on profiles from posts and navigate seamlessly.
✅ Display profile images dynamically using cached data.
✅ Implement a follow/unfollow system to build connections.

This is a major step towards making Connect a social-first progress-sharing app. By the end of this, profiles will be fully functional, and users can start following others!

Let’s get started. 🔥

GitHub Code Link: https://github.com/maitry4/Connect/

Refer to this alongside as it’s tough to display everything here.

Profile Navigation And Follow System

STEP 1: Make Profiles Clickable in Post Tiles

Let’s start by making user profiles tappable in post tiles.

1️⃣ Wrap the username and avatar in a GestureDetector.
2️⃣ On tap, navigate to the profile page using Navigator.push().
3️⃣ Pass a fromHome boolean to the profile page:

  • If true, ensure returning to the home page reloads all posts instead of just the visited user’s posts.
//in profile page
final String uid;
  bool fromHome;
  CProfilePage({super.key, required this.uid, this.fromHome = false});
//When you pass it from post tile make it true.
 appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.surface,
            leading: widget.fromHome
                ? IconButton(
                    icon: const Icon(Icons.arrow_back),
                    onPressed: () {
                      Navigator.pop(context);
                      context
                          .read<CPostCubit>()
                          .fetchAllPosts(); // Refresh home on return
                    },
                  )
                : null,
          ),

Profile Navigation And Follow System Profile Navigation And Follow System Profile Navigation And Follow System Profile Navigation And Follow System

If the profile being viewed isn’t the current user’s, hide the Edit Profile button.

Pass another optional parameter for the current user ID.
Now, if the profile user matches the current user, display the edit profile button. Else do not.

if (widget.uid == widget.currentUserId)

Add this above the edit profile button.

And yes don’t forget to modify the profile card widget for the same on desktop.

STEP 2: Display Profile Images in Post Tiles

Now, let’s make posts feel more personal by showing the user’s profile image.

🔹 Modify CPostCubit to store a cache of user profiles (avoids excessive Firebase calls).
🔹 Add a method to fetch multiple users when loading posts.

Create a new state for multiple profiles:

class CProfilesLoadedState extends CProfileState {
  final List<CProfileUser> profiles;
  CProfilesLoadedState(this.profiles);
}

Profile Navigation And Follow System Profile Navigation And Follow System Profile Navigation And Follow System Profile Navigation And Follow System

In the home page initState modify:

postCubit.fetchAllPosts().then((_) {
  final posts = postCubit.state is CPostsLoadedState
      ? (postCubit.state as CPostsLoadedState).posts
      : [];

  final userIds = posts.map((post) => post.userId).toSet().toList();

  // Fetch all profiles at once
  context.read<CProfileCubit>().fetchUserProfiles(userIds);
});

💡 Now, pass profile images to both desktop and mobile post UI components and replace the placeholder with the user’s profile image.

  • in the bloc builder of the home page scaffold, we will have another bloc builder to provide profile images.
  • pass the profile images to the build mobile and build desktop methods.
  • now go ahead and pass these to your post-specific UI for both mobile and desktop(From the home page). and receive them here in the post UI pages.

In the profile page you won’t need to work really hard just do this:


                      .map((post) => CPostUi(post: post, profileImageUrl: usr.profileImageUrl,)).toList(),
                      
                          .map((post) => CPostUiDesktop(post: post, profileImageUrl: usr.profileImageUrl,))

And now let’s move on to the follow system in our app.

Profile Navigation And Follow System Profile Navigation And Follow System Profile Navigation And Follow System Profile Navigation And Follow System

STEP 3: Modify Post and Profile Entities for the Follow System

Next, let’s lay the groundwork for the follow/unfollow system.

🔹 Add a private boolean to CPost (for future private/public post settings).
🔹 Modify CProfileUser to include followers and following lists:

final List<String> followers;
final List<String> following;

CProfileUser copyWith({
  String? newBio, 
  String? newProfileImageUrl, 
  List<String>? newFollowers,
  List<String>? newFollowing,
}) {
  return CProfileUser(
    uid: uid, 
    email: email, 
    name: name, 
    bio: newBio ?? bio, 
    profileImageUrl: newProfileImageUrl ?? profileImageUrl,
    followers: newFollowers ?? followers,
    following: newFollowing ?? following,
  );
}

//Map<String, dynamic> toJson() 
'followers':followers,
      'following':following,
followers: List<String>.from(jsonUser['followers'] ?? []),
      following: List<String>.from(jsonUser['following'] ?? []),

STEP 4: Implement Follow/Unfollow in Firebase and ProfileCubit

Now, we’ll add the logic to follow and unfollow users.

🔹 First, add the method to profile_repo.dart:

Future<void> toggleFollow(String currentUid, String targetUid);

🔹 Implement it in firebase_profile_repo.dart:

Future<void> toggleFollow(String currentUid, String targetUid) async {
    try {
      // get both the users
      final currentUserDoc = await firebaseFirestore.collection('users').doc(currentUid).get();
      final targetUserDoc = await firebaseFirestore.collection('users').doc(targetUid).get();

      // see if they are present
      if (currentUserDoc.exists && targetUserDoc.exists) {
        // get data from both
        final currentUserData = currentUserDoc.data();
        final targetUserData = targetUserDoc.data();

        // check if the data is correct
        if (currentUserData != null && targetUserData != null){
          // get current followings
          final List<String> currentFollowing = List<String>.from(currentUserData['following'] ?? []);

          // check if the user is already following the target user.
          if(currentFollowing.contains(targetUid)) {
            // unfollow
            // remove from current user
            await firebaseFirestore.collection('users').doc(currentUid).update({
              'following':FieldValue.arrayRemove([targetUid]),
            });
            // remove from target user
            await firebaseFirestore.collection('users').doc(targetUid).update({
              'followers':FieldValue.arrayRemove([currentUid]),
            });
          } else {
            // follow
            // add to current user
            await firebaseFirestore.collection('users').doc(currentUid).update({
              'following':FieldValue.arrayUnion([targetUid]),
            });
            // remove from target user
            await firebaseFirestore.collection('users').doc(targetUid).update({
              'followers':FieldValue.arrayUnion([currentUid]),
            });
          }
        }
      }
    } catch(e) {

    }
  }

🔹 Now, update ProfileCubit to call this method:


Future<void> toggleFollow(String currentUid, String targetUid) async {
    try {
      await profileRepo.toggleFollow(currentUid, targetUid);
      await fetchUserProfile(targetUid);
    }
    catch(e) {
      emit(CProfileErrorState("Error : $e"));
    }
  }

STEP 5: Implement Follow/Unfollow in the UI

Create a CFollowButton component that takes:
✔️ onPressed callback
✔️ isFollowing boolean

Now, display it in the profile page (only if the user isn’t viewing their own profile):

if (widget.uid != widget.currentUserId)
  CFollowButton(
    onPressed: followButtonPress,
    isFollowing: usr.followers.contains(widget.currentUserId),
  ),

Make the Follow Button Functional

Add a method in the profile page to toggle follow/unfollow:

void followButtonPress() {
  final profileState = profileCubit.state;
  if (profileState is! CProfileLoadedState) return; // Couldn't load profile

  final profileUser = profileState.profileUser;
  final isFollowing = profileUser.followers.contains(widget.currentUserId);

  // Optimistically update UI
  setState(() {
    if (isFollowing) {
      profileUser.followers.remove(widget.currentUserId);
    } else {
      profileUser.followers.add(widget.currentUserId);
    }
  });

  // Perform follow/unfollow in Firebase
  profileCubit.toggleFollow(widget.currentUserId, widget.uid).catchError((error) {
    // Revert UI if error occurs
    setState(() {
      if (isFollowing) {
        profileUser.followers.add(widget.currentUserId);
      } else {
        profileUser.followers.remove(widget.currentUserId);
      }
    });
  });
}

Now, call followButtonPress inside the CFollowButton component in both desktop and mobile layouts.

You can also show the followers and following stats dynamically after this.

And yes don’t forget to add empty followers and following lists to all the users in your database. And a private boolean in all the posts too.

Conclusion

With today’s update, profiles in Connect are now interactive and dynamic. Users can now visit profiles, follow/unfollow, and engage more deeply.

Tomorrow, we’ll take it a step further by:
📌 Creating a Followers/Following list
📌 Adding a search user feature
📌 Prepping for UI improvements & deployment checks and a few more features we have planned on the way.

We’re just a few steps away from launching Connect—can’t wait to see it live! Do you have any more suggestions??

Let me know! 🚀💬

Leave a Reply