How I Created An Instagram Clone With Flutter And Firebase

Firebase category image

Introduction

Learn how I created an Instagram clone with Flutter and Firebase. Fun fact: Creating the UI took the most time! However, it was really easy overall. Here is a deep dive for you!

Instagram is a well-known app with hundreds of millions of users. The newsfeed with posts, likes, comments, and images is the main feature that stands out. I wanted to know what it takes to build a small app with those features. So here is how I created an Instagram clone with Flutter and Firebase.

Video of my Instagram clone app built with Flutter and Firebase
Video of my Instagram clone app built with Flutter and Firebase

Tools

First, let’s talk about everything that I used during development. It’s not that much to be honest. And that’s good because simple things are easier to maintain and to enhance. I always head for simple solutions.

I use Firebase as my backend. The major benefit is that this cloud service doesn’t require any maintenance or updates. It just works out of the box. In addition, it has a generous free tier that makes developing and testing cheap. It’s always my first choice when I need to create a MVP.

Firebase Firestore is a document-based NoSQL database that I used to store all posts. Every post is a document. With the provided query language, it’s easy to get exactly the data you want. I published an introduction article about Firebase Firestore for Flutter developers if you want to know more.

Firebase Storage is a cloud storage service for all your files. No matter if it’s audio, video, PDF, ZIPs, or else, Cloud Storage can handle it. When users upload images in posts, they are stored here. Read my introduction article about it here.

Riverpod is my favorite state management tool. It took me a while to get the hang of it but I don’t think that there is a better solution currently available. It’s the glu in my app that connects models, services, and widgets.

Apart from that I use only a few other packages:

  • timeago for displaying nice time strings like “5 minutes ago”
  • equatable to compare object instances
  • An image picker to select and upload pictures from a device

These are my essentials for building an Instagram clone!

Goal

Of course, I don’t have the time (and probably not the knowledge and capabilities) to build an exact clone. That’s why I focused on those main features that my app should fulfill:

  • Create posts with text and images
    Users can enter text and upload an optional image.
  • Like feature
    Liking and unliking of posts should be possible. No dislike feature, although it would have a similar logic. Comments cannot be liked.
  • Comment option
    Crucial feature for social networks. A comment can only be text, no image uploading planned.
  • Author can delete own posts or comments
    Mistakes happen. That’s why people should be able to correct them by deleting their stuff again. In addition, one could think about an admin feature that can delete everything from everyone.
  • Updates should be minimal, data should be cached on the device
    Firebase offers 50,000 free read operations per day for Firestore. When there are 100 posts and 50 users and every user refreshes 10 times a day, the quota is depleted. That’s why I aim for caching and intelligent updates.
  • Authentication mechanism
    Users should be able to register, log in, reset passwords, and all the usual stuff that you know. This will be realized with Firebase Authentication.
  • Profile pictures
    All users can upload a profile picture that is displayed alongside their posts and comments. It will be stored in Firebase Storage.

The data model

I usually start with the data model since I am working alone on the project. In team projects, it’s better to define the interfaces first so that all developers can start on different tasks and integrate later.

I am going with two collections in Firebase Firestore for all posts and users. The reference between the two collections is the Firebase Authentication UID. It would also be possible to only go with a posts collection and store all information in there. But I want a bit more separation between those entities and that’s why I went with 2 collections. However, the Guest object is included and also updated in every post, like, or comment when the data changes.

Code for a Post object
Dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:equatable/equatable.dart';
import 'package:hochzeit/models/guest.dart';
import 'package:hochzeit/models/post_comment.dart';

class Post extends Equatable {
  final String id;
  final String text;
  final String imageId;
  final Guest author;
  final DateTime createdAt;
  final DateTime updatedAt;
  final List<PostComment> comments;
  final List<Guest> likes;
  final bool isDeleted;

  const Post(
      {required this.id,
      required this.text,
      required this.author,
      required this.createdAt,
      required this.updatedAt,
      required this.comments,
      required this.likes,
      required this.isDeleted,
      required this.imageId});

  Post update() => Post(
      id: id,
      createdAt: createdAt,
      text: text,
      author: author,
      comments: comments,
      likes: likes,
      imageId: imageId,
      isDeleted: isDeleted,
      updatedAt: DateTime.now());

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
        id: json.containsKey("id") ? json["id"] : "",
        text: json.containsKey("text") ? json["text"] : "",
        createdAt: json.containsKey("createdAt")
            ? (json["createdAt"] as Timestamp).toDate()
            : DateTime(1970),
        updatedAt: json.containsKey("updatedAt")
            ? (json["updatedAt"] as Timestamp).toDate()
            : DateTime(1970),
        author: json.containsKey("author")
            ? Guest.fromJson(json["author"])
            : Guest.empty(),
        imageId: json.containsKey("imageId") ? json["imageId"] : "",
        isDeleted: json["isDeleted"],
        comments: json.containsKey("comments")
            ? (json["comments"] as List)
                .map((item) => PostComment.fromJson(item))
                .toList()
            : <PostComment>[],
        likes: json.containsKey("likes")
            ? (json["likes"] as List)
                .map((item) => Guest.fromJson(item))
                .toList()
            : <Guest>[]);
  }

  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      "id": id,
      "text": text,
      "createdAt": Timestamp.fromDate(createdAt),
      "updatedAt": Timestamp.fromDate(updatedAt),          
      "author": author.toJson(),
      "imageId": imageId,
      "isDeleted": isDeleted,
      "comments": comments
          .map((comment) => comment.toJson())
          .toList(),
      "likes": likes.map((like) => like.toJson()).toList()
    };
  }

  @override
  List<Object?> get props => [id];
}
Code for a Guest object
Dart
import 'package:equatable/equatable.dart';

class Guest extends Equatable {
  final String id;
  final String uid;
  final String firstName;
  final String lastName;
  final String displayName;
  final String profilePicture;

  const Guest(
      {required this.id,
      required this.uid,
      required this.firstName,
      required this.lastName,
      required this.displayName,
      required this.profilePicture});
      
  Guest.empty()
      : this(
            id: "",
            uid: "",
            firstName: "",
            lastName: "",
            displayName: "",
            profilePicture: "");

  factory Guest.fromJson(Map<String, dynamic> json) {
    return Guest(
        id: json.containsKey("id") ? json["id"] : "",
        uid: json.containsKey("uid") ? json["uid"] : "",
        firstName: json.containsKey("firstName") ? json["firstName"] : "",
        lastName: json.containsKey("lastName") ? json["lastName"] : "",
        displayName: json.containsKey("displayName") ? json["displayName"] : "",
        profilePicture:
            json.containsKey("profilePicture") ? json["profilePicture"] : "");
  }

  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      "id": id,
      "uid": uid,
      "firstName": firstName,
      "lastName": lastName,
      "displayName": displayName,
      "profilePicture": profilePicture,
    };
  }

  @override
  List<Object?> get props => [id, uid];
}

These models contain all the information that we need to achieve the requirements discussed before. As you can see, the classes use equatable to compare instances via the unique id. The package saves me some boilerplate code. There are also fromJson and toJson methods so that I can read and write data easily with Firebase Firestore.

Authentication

My demo application of how I created an Instagram clone with Flutter and Firebase uses email/password authentication. Users enter their email and password on a login screen and are redirected to their news feed. This is pretty simple with the package firebase_auth.

I omitted features like registration and password reset. But they are not complex to implement. To register a new user, use the method createUserWithEmailAndPassword and that’s it. And for resetting a password, use the methods sendPasswordResetEmail, verifyPasswordResetCode, and confirmPasswordReset. You can also define the email templates to communicate with your users in Firebase.

Don’t forget to check out my ebook about Firebase Authentication with many more details about the system!

Firebase Authentication

Implement email/password authentication or use social providers like Google, Microsoft, and Facebook for your apps!

News feed

To keep it simple, we order the news feed chronogically. New stuff is at the top and older content at the bottom.

Show all posts

Firebase Cloud Firestore offers Streams of collections that can be easily integrated in your Flutter app. First, let’s define the Stream we want to subscribe to. In this example, we want all posts that are not deleted and have them ordered from new to old.

Dart
Stream<QuerySnapshot> _getPostsStream() {
  return FirebaseFirestore.instance
          .collection('posts')
          .where("isDeleted", isNotEqualTo: true)
          .orderBy("isDeleted", descending: true)
          .orderBy("createdAt", descending: true)
          .snapshots();
}

To display the data, we use a StreamBuilder. Here is how it could look like:

Dart
StreamBuilder(
    stream: _getPostsStream(),
    builder: (context, snapshot) {
      if (snapshot.hasError) {
        // handle error
      }

      if (snapshot.connectionState == ConnectionState.waiting) {
        return const Center(child: CircularProgressIndicator());
      }

      List<Widget> postCards = [];
      for (int index = 0;
          index < snapshot.data!.docs.length;
          index++) {
        final post = Post.fromJson(snapshot.data!.docs[index]
            .data() as Map<String, dynamic>);
        posts.add(post);
        postCards.add(PostCard(
          item: post,
        ));
      }

      postCards.add(_buildLoadingButton(snapshot.data));

      return ListView(
          padding: const EdgeInsets.fromLTRB(0, 0, 0, 80),
          physics: const AlwaysScrollableScrollPhysics(),
          children: postCards);
    })

By the way: Firebase Cloud Firestore automatically caches data if you are on iOS or Android. For Flutter web, you need to enable it manually. This can help you reduce read operations and also save costs. Read the article below for details:

If you need more insights to Firebase Cloud Firestore, grab a copy of my ebook!

Firebase Cloud Firestore

Learn about Firebase Firestore and write mobile apps with the power of a modern and fast NoSQL database.

Post new content

A post can either contain text and an image or only text. Users enter the content and publish it. The new post is added to the database, the optional image is uploaded to the cloud, and the news feed updates to reflect the changes.

Here is the user interface:

User interface screenshot of my Instagram clone with Flutter and Firebase
User interface screenshot of my Instagram clone with Flutter and Firebase

Ok, let’s start with the image. There is a nice package called image_picker which allows selecting an image from the camera or the gallery. The following code handles a selected image:

Dart
    final image = await ImagePicker().pickImage(source: ImageSource.gallery);

    if (image != null) {
      final imageFile = await image.readAsBytes();
      
      ...
    } 

Eventually, the user will publish the post. Then, we need to upload the image to get the url, create a new Post object, and insert it into the posts collection in Firestore. Here is how you can do exactly that:

Dart
// select an image
final image = await ImagePicker().pickImage(source: source);

if (image != null) {
  final imageFile = await image.readAsBytes();

	// upload the image
  final task = uploadFile(imageFile, image.name);

  task.snapshotEvents.listen((event) async {
    if (event.state == TaskState.success) {
	    // get the download url of the uploaded image
      _imagePath = await event.ref.getDownloadURL();
    } else if (event.state == TaskState.error) {
			// Handle error
    }
  });
}

// upload an image to Firebase Storage
// you need to pass in the file content and the name like "myImage.jpg"
UploadTask uploadFile(Uint8List file, String fileName) {
  final ref = FirebaseStorage.instance.ref();

  final child = ref.child("images/$fileName");
  return child.putData(file);
}

// add a new post to a Firebase Firestore collection
Future<Post> addPost(String text, String imageId, Guest author) async {
  final docRef = FirebaseFirestore.instance.collection("posts").doc();
  final newPost = Post(
      id: docRef.id,
      author: author,
      comments: const <PostComment>[],
      likes: const <Guest>[],
      updatedAt: DateTime.now(),
      createdAt: DateTime.now(),
      text: text,
      isDeleted: false,
      imageId: imageId);

  await FirebaseFirestore.instance
      .collection("posts")
      .doc(docRef.id)
      .set(newPost.toJson());

  return newPost;
}

With this implementation, users can add exactly one image. However, image_picker supports multi-select in case you want more. And if you want other file types than images, try file_picker.

Add a comment

A PostComment can be treated similar to a Post, but in my case only text is possible. Once a user created a comment, it is added to the comments list of the corresponding Post object. The last step is to update Firestore and replace the old Post with the new one.

Dart
Future addComment(Post post, PostComment comment) async {
  post.comments.add(comment);

	// this creates a new copy with the last updated timestamp set to now
  final postCopy = post.update();

  await FirebaseFirestore.instance
      .collection("posts")
      .doc(postCopy.id)
      .update(postCopy.toJson());
}

Like a post

Every Post contains a list of Likes which is just a reference to a user. When a user clicks on the like button in the app, then its ID is added to the list. Once again, we update the Post object in the database and the news feed will show the updated information.

Dart
Future likePost(Post post, Guest guest) async {
  post.likes.add(guest);

	// this creates a new copy with the last updated timestamp set to now
  final postCopy = post.update();

  await FirebaseFirestore.instance
      .collection("posts")
      .doc(postCopy.id)
      .update(postCopy.toJson());
}

You can also implement a toggle so that users can unlike again when they liked something before.

Delete your content

Deleting does not mean the content is gone, it just isn’t visible anymore. This can be done with a boolean flag isDeleted and content with that property is excluded from the news feed.

Why? Because then you can restore the content in case a user deleted by accident.

Dart
Future deletePost(Post post) async {
  final newPost = Post(
      id: post.id,
      author: post.author,
      comments: post.comments,
      likes: post.likes,
      updatedAt: DateTime.now(),
      createdAt: post.createdAt,
      text: post.text,
      isDeleted: true, // <-- here is the relevant flag
      imageId: post.imageId);

  await FirebaseFirestore.instance
      .collection("posts")
      .doc(post.id)
      .set(newPost.toJson());
}

In case you really want to delete stuff, there is a delete method for documents in Cloud Firestore.

Profile pictures

We already talked about images in posts in a previous section of this article about how I created an Instagram clone with Flutter and Firebase. Profile pictures follow the same pattern.

I build a UI for the user to select a profile picture, to change it, and to remove it using the image_picker package and Firebase Storage. It is very similar to what you have been before. But here is the catch:

Since we store the author information with every post, we need to update every post and include the author image. Otherwise, the image won’t be displayed!

Dart
// find all posts where the user is author, has liked, or has commented
// then we update the author information, the like information, or the comment 
// information if our user matches
Future updateGuestInPosts(
    List<Post> allPosts, Guest oldGuest, Guest newGuest) async {
  final postsToUpdate = allPosts
      .where((post) =>
          post.author == oldGuest ||
          post.comments.any((comment) => comment.author == oldGuest) ||
          post.likes.any((like) => like == oldGuest))
      .toList();

  for (var post in postsToUpdate) {
    final newPost = Post(
        id: post.id,
        text: post.text,
        author: post.author == oldGuest ? newGuest : oldGuest,
        createdAt: post.createdAt,
        updatedAt: DateTime.now(),
        isDeleted: post.isDeleted,
        imageId: post.imageId,
        likes: _replaceLikes(post, oldGuest, newGuest),
        comments: _replaceComments(post.comments, oldGuest, newGuest));

    await FirebaseFirestore.instance
        .collection("posts")
        .doc(newPost.id)
        .set(newPost.toJson());
  }
}

List<Guest> _replaceLikes(Post post, Guest oldGuest, Guest newGuest) {
  final index = post.likes.indexOf(oldGuest);

  if (index == -1) return post.likes;

  post.likes.removeAt(index);
  post.likes.insert(index, newGuest);

  return post.likes;
}

List<PostComment> _replaceComments(
    List<PostComment> comments, Guest oldGuest, Guest newGuest) {
  return comments
      .map((comment) => PostComment(
          id: comment.id,
          text: comment.text,
          createdAt: comment.createdAt,
          author: comment.author == oldGuest ? newGuest : oldGuest))
      .toList();
}

Check out the final result of what you can build with Flutter and Firebase!

Video of my Instagram clone app built with Flutter and Firebase
Video of my Instagram clone app built with Flutter and Firebase

To learn more about Firebase Storage, have a look at my ebook!

Firebase Cloud Storage

Upload and download user-generated content like on a file system. Firebase Cloud Storage makes file handling simple!

Conclusion

In this article, you learned how I created an Instagram clone with Flutter and Firebase. While I spent most of the time with the UI, the backend logic was rather simple. However, there is still room for improvement but I already achieved my goal.


Want More Flutter Content?

Join my bi-weekly newsletter that delivers small Flutter portions right in your inbox. A title, an abstract, a link, and you decide if you want to dive in!

Become A Firebase Expert!

Take a deep dive into Firebase and learn how to really handle their services. This ebook makes you an instant Firebase professional!

Flutter โค๏ธ Firebase

Get started with Firebase and learn how to use it in your Flutter apps. My detailed ebook delivers you everything you need to know! Flutter and Firebase are a perfect match!