如何使用Firebase与Flutter实现食谱APP的收藏切换功能
Got it, let's walk through fixing and implementing your favorite toggle feature step by step, based on your existing code and requirements.
Step 1: Fix the Document ID Strategy
Your current updateFavourite method uses doc().set(), which generates a random document ID every time. This will create duplicate entries for the same recipe when a user toggles the favorite button multiple times. Instead, we'll use a unique ID for each user-recipe pair to ensure only one entry exists per user per recipe.
Add this helper function to generate the ID:
// Generate a unique document ID for each user-recipe combination String getFavouriteDocId(String userId, String recipeId) { return '$userId-$recipeId'; }
Step 2: Implement the Toggle Favorite Core Method
Replace your existing updateFavourite and isFavourite methods with this single toggle function. It will check if the recipe is already in the user's favorites, then either remove it or add it accordingly.
Option 1: Delete Document When Unfavouriting (Most Efficient)
This approach removes the document entirely when the user unfavourites, keeping your collection clean:
Future<void> toggleFavouriteRecipe(FavouriteRecipesModel recipe) async { final docId = getFavouriteDocId(recipe.userId, recipe.recipeId); final docRef = favouriteDataCollection.doc(docId); final docSnapshot = await docRef.get(); if (docSnapshot.exists) { // Remove from favorites await docRef.delete(); } else { // Add to favorites await docRef.set({ kFavouriteTitle: recipe.title, kFavouriteCookingTime: recipe.cookingTime, kFavouriteIngredients: recipe.ingredients, kFavouritePreparation: recipe.preparation, kFavouritePublishedDate: recipe.dateTime, kFavouriteNumOfLikes: recipe.numOfLikes, kFavouriteRecipeImage: recipe.imgPath, kFavouriteServings: recipe.servings, kIsFavourite: true, // Mark as favourite when adding kRecipeId: recipe.recipeId, kFavouriteRecipeCat: recipe.recipeBundleName, 'uid': recipe.userId, }); } }
Option 2: Toggle isFavourite Field (If You Need to Keep the Document)
If you want to retain the document (e.g., for tracking historical preferences), update the isFavourite field instead of deleting:
Future<void> toggleFavouriteRecipe(FavouriteRecipesModel recipe) async { final docId = getFavouriteDocId(recipe.userId, recipe.recipeId); final docRef = favouriteDataCollection.doc(docId); final docSnapshot = await docRef.get(); if (docSnapshot.exists) { // Toggle the favourite status final currentStatus = docSnapshot.data()[kIsFavourite] as bool; await docRef.update({kIsFavourite: !currentStatus}); } else { // Add as favourite await docRef.set({ // ... all fields as above kIsFavourite: true, }); } }
Step 3: Adjust the Favorite Recipes Stream
Update your stream to correctly fetch the user's favorites based on your chosen approach:
For Option 1 (Delete on Unfavourite)
Since we remove documents when unfavouriting, we only need to filter by the user's ID:
Stream<List<FavouriteRecipesModel>> get favouriteRecipes { return favouriteDataCollection .where('uid', isEqualTo: uid) .snapshots() .map(getFavouriteRecipes); }
For Option 2 (Toggle isFavourite Field)
Keep the filter for isFavourite: true to only show active favorites:
Stream<List<FavouriteRecipesModel>> get favouriteRecipes { return favouriteDataCollection .where(kIsFavourite, isEqualTo: true) .where('uid', isEqualTo: uid) .snapshots() .map(getFavouriteRecipes); }
Step 4: Implement the UI Toggle Button
In your recipe detail screen, add a button that checks the current favorite status and triggers the toggle method. Use a FutureBuilder to fetch the status asynchronously:
First, add a helper method to check if a recipe is favourited:
Future<bool> isRecipeFavourite(String userId, String recipeId) async { final docId = getFavouriteDocId(userId, recipeId); final docSnapshot = await favouriteDataCollection.doc(docId).get(); // For Option 1: Check if the document exists return docSnapshot.exists; // For Option 2: Check if the document exists AND is marked as favourite // return docSnapshot.exists && docSnapshot.data()[kIsFavourite] as bool; }
Then add the button to your UI:
// Inside your Recipe Detail Screen widget FutureBuilder<bool>( future: isRecipeFavourite(currentUserId, recipe.recipeId), // Replace with your user ID and current recipe ID builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(size: 24); } final isFavourited = snapshot.data ?? false; return IconButton( icon: Icon( isFavourited ? Icons.favorite : Icons.favorite_border, color: isFavourited ? Colors.redAccent : null, size: 32, ), onPressed: () { // Convert your RecipeModel to FavouriteRecipesModel final favouriteRecipe = FavouriteRecipesModel( title: recipe.title, cookingTime: recipe.cookingTime, ingredients: recipe.ingredients, preparation: recipe.preparation, dateTime: recipe.dateTime, numOfLikes: recipe.numOfLikes, imgPath: recipe.imgPath, servings: recipe.servings, recipeId: recipe.recipeId, recipeBundleName: recipe.recipeBundleName, userId: currentUserId, isFavourite: true, // Toggle will handle the actual status favouriteId: '', // Will be populated by the document ID ); // Trigger the toggle toggleFavouriteRecipe(favouriteRecipe); }, ); }, )
Key Notes
- Make sure
currentUserIdis the authenticated user's ID (from Firebase Auth). - Add error handling with
try-catchblocks in the toggle method if you want to handle Firebase exceptions (e.g., network errors). - Your existing
getFavouriteRecipesmapper function works perfectly with this setup, since it usesdoc.idforfavouriteId.
内容的提问来源于stack exchange,提问作者Mohammed Nadim

