How I Created An Instagram Clone With Flutter And Firebase

Introduction Link to heading

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 Link to heading

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 Link to heading

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 Link to heading

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 Link to heading

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 Link to heading

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 Link to heading

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.

News feed Link to heading

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 Link to heading

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:

How To Enable Firestore Cache In Flutter Web Apps Link to heading

Reduce database load and add offline capabilities to your apps. Here is how to enable Firestore cache in Flutter web apps.

Post new content Link to heading

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 Link to heading

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 Link to heading

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 Link to heading

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 Link to heading

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

Conclusion Link to heading

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.

Firebase WhatsApp Clone Built With Flutter in No Time! Link to heading

I built a WhatsApp clone with Flutter and Firebase in a few hours. Here is what I came up with.

I Built A Custom Search Function For My WordPress Blog With Flutter And Firebase Link to heading

Learn how I built a custom search function for my WordPress blog with Flutter and Firebase, and discover how you can do it too!

Building a simple text translation app with Flutter and Firebase Link to heading

With Firebase Extensions you can build a simple text translation app that does most of the work for you.