Flutter: FriendRepository & Integration Guide
Hey everyone! Today, we're diving deep into creating a robust FriendRepository in your Flutter app. This is super important for building any social features, like friend requests, friend lists, and more. We'll walk through the process, covering everything from the interface and data models to unit tests and error handling. Let's get started!
🎯 Goal: Building a Solid Friend Repository
Our main goal is to build a solid FriendRepository that can handle all friend-related logic in our Flutter app. We're going to follow the existing repository pattern, ensuring consistency across our project. This repository will act as a single source of truth for all friend-related operations, making it easier to manage and maintain our codebase. Think of it as a central hub for all things friends!
This will include handling friend requests, accepting, declining, and removing friends, and also retrieving a user's friend list. We'll be using Cloud Functions to handle the backend logic and Firestore for data storage. We'll also cover creating FriendRequestEntity and implementing error handling to provide a seamless user experience.
Why is a FriendRepository Important?
A FriendRepository is crucial because it encapsulates all the friend-related operations in one place. This improves code organization and makes it easier to test and maintain. It also helps to keep your UI code clean and focused on presentation, as the repository handles all the data interactions.
It provides a clear separation of concerns, making debugging easier and allowing for flexibility in the future. For example, if you decide to change your backend from Firestore to another service, you only need to modify the repository implementation without affecting the rest of your app. This separation is a key principle of good software design.
📦 Repository Interface: The Blueprint
First, let's look at the FriendRepository interface. This interface defines the contract for all friend-related operations. It outlines what functions our repository will provide. The interface ensures that any concrete implementation of the repository adheres to this contract.
// lib/core/domain/repositories/friend_repository.dart
abstract class FriendRepository {
  /// Send a friend request to a user
  Future<void> sendFriendRequest(String targetUserId);
  
  /// Accept a friend request
  Future<void> acceptFriendRequest(String friendshipId);
  
  /// Decline a friend request
  Future<void> declineFriendRequest(String friendshipId);
  
  /// Remove a friend (delete friendship)
  Future<void> removeFriend(String friendshipId);
  
  /// Get list of accepted friends
  Stream<List<UserEntity>> getFriends(String userId);
  
  /// Get pending friend requests (sent or received)
  Future<List<FriendRequestEntity>> getPendingRequests({
    required FriendRequestType type,
  });
}
enum FriendRequestType { sent, received }
As you can see, this interface lays out all the essential operations: sending friend requests, accepting and declining them, removing friends, getting a list of friends, and retrieving pending friend requests. The getFriends method is designed to provide a real-time stream of friend updates using Firestore changes, which is a key feature for a responsive user experience.
📊 Data Models: Defining the Data
Next, let's look at the data models. These models define the structure of the data we'll be working with. We have the FriendRequestEntity and the FriendshipStatus enum. These models help us to keep our data organized.
// lib/core/domain/entities/friend_request_entity.dart
@freezed
class FriendRequestEntity with _$FriendRequestEntity {
  const factory FriendRequestEntity({
    required String friendshipId,
    required String requesterId,
    required String recipientId,
    required String requesterName,
    required String recipientName,
    required FriendshipStatus status,
    required DateTime createdAt,
    required DateTime updatedAt,
  }) = _FriendRequestEntity;
}
enum FriendshipStatus { pending, accepted, declined }
The FriendRequestEntity contains all the relevant information about a friend request, like who sent it, who received it, the status of the request, and timestamps. The FriendshipStatus enum defines the possible statuses of a friendship: pending, accepted, or declined. These models are crucial for representing the friend data correctly in your app.
✅ Acceptance Criteria: Ensuring Quality
To make sure our repository is up to par, we need to meet some acceptance criteria:
FriendRepositoryinterface defined.FirestoreFriendRepositoryimplemented.- All methods call Cloud Functions (no direct Firestore access).
 FriendRequestEntityandFriendshipStatusenum created.- Repository registered in service locator.
 - Unit tests for repository (90%+ coverage).
 - Mock Cloud Function responses using 
mocktail. - Error handling for all Cloud Function error codes.
 
These criteria are important because they give a clear idea of what has to be done to ensure the repository works correctly and meets the project's requirements. These steps make certain that our code is tested, has error handling in place, and is well-organized.
Diving Deeper into Acceptance Criteria
Let's break down these acceptance criteria further. The FirestoreFriendRepository implementation will handle the communication with Firestore and Cloud Functions. This will include implementing all the methods defined in the FriendRepository interface. It's crucial that all data operations are done through Cloud Functions. This ensures data security and allows for server-side logic and validation.
Next, we need to register the repository in the service locator. This makes the repository available throughout our app. Unit tests are very important because they allow you to verify the behavior of the repository in isolation. You want at least 90% coverage. These tests should cover all the methods and scenarios, including success and failure cases.
Finally, we will use mocktail to mock the Cloud Function responses. This makes it possible to test the repository without making actual calls to Cloud Functions. Make sure that there is proper error handling in place to deal with any issues. This will help you identify any problems that might come up while interacting with Cloud Functions.
🔧 Technical Notes: Tips and Tricks
Here are some technical notes to keep in mind:
- Follow existing repository patterns.
 - Use 
FirebaseFunctionsExceptionfor error handling. - Stream 
getFriendsshould listen to Firestore changes in real-time. - Integrate with the existing 
UserEntitymodel. 
Following existing repository patterns will help maintain consistency. It also makes it easier for other developers to understand your code. You should use FirebaseFunctionsException for error handling, which allows you to catch and handle errors from Cloud Functions correctly. The getFriends method should use Firestore's real-time updates to keep the friend list updated instantly. Make sure the repository integrates seamlessly with your existing UserEntity model.
Detailed Technical Notes
Let's expand on these notes. The repository pattern involves separating data access logic from the rest of your app. This makes your code more modular and testable. The FirebaseFunctionsException is essential for handling errors that might occur when calling Cloud Functions. By catching these exceptions, you can handle errors gracefully and provide helpful feedback to the user.
The use of real-time updates for getFriends is important. It uses Firestore's real-time capabilities to keep the friend list in sync automatically. When a friend is added or removed, the UI updates immediately without needing a manual refresh. Integration with the UserEntity model is important, this will require mapping data between the model and the data stored in Firestore.
📁 Files to Create/Modify: Where the Magic Happens
Here's a list of the files we'll be working with:
lib/core/domain/repositories/friend_repository.dart(new)lib/core/data/repositories/firestore_friend_repository.dart(new)lib/core/domain/entities/friend_request_entity.dart(new)lib/core/data/models/friend_request_model.dart(new)lib/core/services/service_locator.dart(update)test/unit/core/data/repositories/firestore_friend_repository_test.dart(new)test/unit/core/data/models/friend_request_model_test.dart(new)
We'll be creating the FriendRepository interface, the FirestoreFriendRepository implementation, the FriendRequestEntity data model, and a model for the friend requests. The service_locator.dart file will be updated to register the repository. We'll write unit tests to test the FirestoreFriendRepository and FriendRequestModel.
File Breakdown
Let's break down each file.
friend_repository.dart: This file will contain the abstract class for theFriendRepositoryinterface. It's the blueprint that defines the contract for all friend-related operations.firestore_friend_repository.dart: This file will contain the implementation of theFriendRepositoryinterface using Firestore. This class will handle communication with Cloud Functions and Firestore to manage friend data.friend_request_entity.dart: This file will hold theFriendRequestEntitydata model. This model will represent a friend request, including details like the requester, recipient, and status.friend_request_model.dart: This file will contain the data model used to map data from Firestore to theFriendRequestEntity.service_locator.dart: This file will be updated to register theFirestoreFriendRepositoryso that it can be accessed throughout the application.firestore_friend_repository_test.dart: This file will contain the unit tests for theFirestoreFriendRepository. This is where you'll test the repository's methods, like sending and accepting friend requests.friend_request_model_test.dart: This file will contain unit tests for theFriendRequestModel, verifying data mapping and transformations.
🚀 Conclusion: Building Social Features with Confidence
And that's it! By implementing a robust FriendRepository, you'll have a strong foundation for adding social features to your Flutter app. Remember to focus on writing clean, testable code and handling errors gracefully. This guide gives you a solid base to start building exciting features like friend lists and friend requests. Good luck, and happy coding, everyone!
This guide has shown you how to build a FriendRepository in your Flutter app, including creating the interface, defining data models, implementing a Firestore-based repository, writing unit tests, and handling errors. Following these steps, you can create a reliable and scalable friend system. This allows you to build a great user experience and provides a scalable solution for all your friend features.
Final Thoughts and Best Practices
Here are some best practices to keep in mind:
- Always write unit tests to make sure your code works as expected.
 - Handle errors gracefully and provide informative messages to the user.
 - Use real-time updates to provide a great user experience.
 - Keep your code clean and organized. Make sure to adhere to established coding standards.
 - Regularly refactor and improve your code.
 
By following these practices, you can build a FriendRepository that's maintainable, scalable, and provides a great user experience. Remember to always prioritize testing and error handling, making sure your app is robust and reliable. You've got this, and happy coding!