3
votes

Hello I am a self taught Flutter dev coding my first app. I am building a basic quotes application in Flutter which utilizes Firebase for auth and FireStore as database. Here is the FireStore noSQL schema:

There's a Stories collection which is read only data that provides image links & text as shown below. Note the document ID is auto-generated in the Stories Collection:

Stories Collection

Then there's a Users collection that stores user data such as Username, email, account creation dateTime, and a likes array [to potentially store liked stories]. In this collection, the document's ID is UID (Unique Id) of the authenticated (logged in) user.

User's collection

So here's the user story that I am chasing and my approach toward it:

Whenever a user taps on the favorite icon below, the like has to be saved in to the users collection where User's uid is the document ID and store the liked story in the likes array.

Simulator Screenshot of UI before like

Then utilize an if statement saying, if the authenticated user has stories in the likes array turn the outlined white heart into a red one like this:

Simulator Screenshot of UI after like

However, there's a bug in my code which turns all the Stories' hearts red at once whenever a user taps the favorite icon. Can somebody please help? Here's a snippet of the code:

class FirestoreSlideshowState extends State<FirestoreSlideshow> {
  static const likedKey = 'liked_key';

  bool liked;

  final PageController ctrl = PageController(viewportFraction: 0.8);

  final Firestore db = Firestore.instance;
  Stream slides;

  String activeTag = 'favs';

  // Keep track of current page to avoid unnecessary renders

  int currentPage = 0;


@override

  void initState() {

    super.initState();
    _restorePersistedPreference();
    _queryDb();

    // Set state when page changes
    ctrl.addListener(() {
      int next = ctrl.page.round();

      if (currentPage != next) {
        setState(() {
          currentPage = next;
        });
      }
    });
  }

  void _restorePersistedPreference() async {
    var preferences = await SharedPreferences.getInstance();
    var liked = preferences.getBool(likedKey) ?? false;
    setState(() {
      this.liked = liked;
    });
  }

  void _persistPreference() async {
    setState(() {
      liked = !liked;
    });
    var preferences = await SharedPreferences.getInstance();
    preferences.setBool(likedKey, liked);
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
        stream: slides,
        initialData: [],
        builder: (context, AsyncSnapshot snap) {
          List slideList = snap.data.toList();

          return PageView.builder(
              controller: ctrl,
              itemCount: slideList.length,
              itemBuilder: (context, int currentIdx) {
                if (slideList.length >= currentIdx) {
                  // Active page
                  bool active = currentIdx == currentPage;
                  return _buildStoryPage(slideList[currentIdx], active);
                }
              });
        });
  }

  Stream _queryDb({String tag = 'favs'}) {
    // Make a Query
    Query query = db.collection('Stories').where('tags', arrayContains: tag);

    // Map the documents to the data payload
    slides =
        query.snapshots().map((list) => list.documents.map((doc) => doc.data));

    // Update the active tag
    setState(() {
      activeTag = tag;
    });
  }

  _buildStoryPage(Map data, bool active) {

    final _width = MediaQuery.of(context).size.width;
    final _height = MediaQuery.of(context).size.height;
    // Animated Properties
    final double blur = active ? 20 : 0;
    final double offset = active ? 20 : 0;
    final double top = active ? 75 : 150;

    return AnimatedContainer(
        duration: Duration(milliseconds: 500),
        curve: Curves.easeOutQuint,
        width: _width / 2,
        height: _height,
        margin: EdgeInsets.only(top: top, bottom: 20, right: 20),
        decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(40),
            image: DecorationImage(
              fit: BoxFit.fill,
              image: NetworkImage(data['img']),
            ),
            boxShadow: [
              BoxShadow(
                  color: Colors.black87,
                  blurRadius: blur,
                  offset: Offset(offset, offset))
            ]),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.end,
          children: [
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Center(
                child: Text(data['quote'],
                    style: TextStyle(
                        fontSize: 20,
                        color: Colors.white,
                        fontFamily: "Typewriter")),
              ),
            ),
            SizedBox(height: 20),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Text(data['author'],
                  style: TextStyle(
                      fontSize: 20,
                      color: Colors.white,
                      fontFamily: "Typewriter")),
            ),
            SizedBox(height: 20),
            Row(mainAxisAlignment: MainAxisAlignment.end, children: [

              IconButton(
            icon: Icon(liked ? Icons.favorite : Icons.favorite_border,
                color: liked ? Colors.red : Colors.grey[50]),
            onPressed: (){
              
   
              
              _persistPreference();
              

            } ),
             
              
              IconButton(
                  icon: Icon(
                    Icons.share,
                    color: Colors.white,
                  ),
                  onPressed: () {
                    share();
                  })
            ])
          ],
        ));
  }
}


2

2 Answers

1
votes

I would recommend making another collection for likes. If this app were to scale you would be limiting your users to how many quotes they would like due to the size limit of a Firestore document of 1 MB. So here would be a proposed solution:

Document structure for /likes/{likeId}

{
    'userId': string
    'quoteId': string
}

Then if you wanted to load a list of liked quotes you could do a query like:

_db.collection(likes).where('userId', isEqualTo: currentUserId).get()

From there you could then load the quotes from the quote ID included in the likes object according to the structure above. To avoid loading tons of documents for no reason you could also implement pagination in your query.

So now when someone would like a quote you would create a new like object in the likes collection with the quote id & user id as shown above.

For deletion / unliking you will need to find a document that matched BOTH userId & quoteId to delete it

var snap = _db.collection(likes).where('userId', isEqualTo: currentUserId).where('quoteId', isEqualTo: quoteId).get();
if(snap.docs != null && snap.docs.isNotEmpty){
    //the record exists
    snap.docs.first.ref.delete() // this will delete the first record in the database that matches this like. Ideally there will only ever be one (you should check to make sure a document doesn't already exist before creating a new one)
}

On the flutter side, you would likely have a ListView that shows a list of these. I'd recommend making a component that would load the quote from the id using a future builder.

If you need more of an example of the Flutter-side code let me know. Goodluck!

0
votes

In this line of code: liked = !liked;, you are setting all your instances of liked with their inverted value and this is causing it to apply the like to all records.

Try removing that setState() as a whole and apply the inverted liked outside that, so like this:

void _persistPreference() async {
    var preferences = await SharedPreferences.getInstance();
    preferences.setBool(likedKey, !liked);
}

NOTE: Ideally the value of liked would have to come from the users colleciton in Firestore but I understand that this is likely a next step in your project, so this workaround should work for this stage. For getting it from Firestore you would have to populate the iconbutton with something like this (not tested so might need to be adapted):

//user is a preloaded variable with the current user data
IconButton(icon: Icon(user.data['likes'].contains(data['uid']) ? Icons.favorite ...))

and operate that in the same _persistPreference() function while updating that to Firestore.