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.
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
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
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 Stream
s 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.
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:
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:
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:
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:
// 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.
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 Like
s 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.
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.
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!
// 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!
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.