Code Your Own Instagram Clone With Flutter and Feeds

66 min read

In this blog post, we will show you how you can easily create a clone of Instagram using Stream Feeds and Flutter.

Gordon H.
Gordon H.
Published March 21, 2022 Updated March 22, 2022

We will, very creatively, call our clone Stream-agram. You’ll also learn a lot of Flutter concepts to improve animations, gestures, transitions, and state management.

The video linked above walks you through this entire blog post, step by step, with additional code instructions. If you get stuck, or if you prefer to learn through video, then the video is there to help you!

This clone will allow you to:

  • sign in using different user accounts
  • add and change profile pictures
  • add photo posts to your own user feed (activities)
  • subscribe/unsubscribe to other users’ feeds
  • add comments and likes (reactions)
Instagram Clone App Preview

Instagram is a complicated application, and we can’t hope to replicate it all. But we will do our best, and in the process create an awesome Flutter app - complete with state management, gestures, animations, and transitions.

Whether you’re a beginner or experienced Flutter engineer, this article will have something of value to you. You will also learn the following Flutter concepts by completing this tutorial:

  • How to neatly structure and organize your code.
  • How to use Provider and basic Flutter components to easily manage your application state.
  • Theming your app to support light and dark mode.
  • Custom page transitions.
  • Hero animations and how to customize them.
  • When to use implicit animations and when to use explicit animations.
  • How to create your own explicit animations.
  • Performance considerations.
  • A lot of Flutter layout and other tips.
  • Extension methods.
  • Using TextEditingController, PageController and FocusNode.
  • Using AutomaticKeepAliveClientMixin to cache pages.
Stream Instagram Clone Preview

What Is an Activity Feed?

To understand what Stream’s Flutter Feed SDK offers you, you first need to understand what an activity feed is.

Sometimes called a newsfeed or activity stream, an activity feed is a real-time list of actions performed by users on an app or website. Activity feeds display information from a user’s online community such as likes, follows, comments, posts, and content shares.

Stream takes the complexity of managing these activity feeds and makes it simple, and the Stream Feed Flutter SDK makes it simple from a front-end perspective.

Building an Instagram Clone With Feeds

Let’s discuss how an activity feed will work in the context of building your Instagram clone. You will make a feed of user-generated picture posts (activities). These posts should also show user engagement and reactions in the form of likes and comments. Users should be allowed to follow specific users they are interested in and see a timeline of posts from all those they follow. They should also be able to see all of the posts that they’ve made.

With some of the higher-level concepts out of the way, we can finally start.

https://media.giphy.com/media/An7elHGhaAfekLMgdS/giphy.gif

Create a Flutter Application

The first step on this journey is... to create a new Flutter application 🚀.

Run the following command in your terminal, in a directory of your choosing:

bash
$ flutter create stream_agram

You can change the application name to whatever you want.

Open the folder in your preferred IDE.

Package Dependencies

Open the pubspec.yaml file and add the following dependencies:

yaml
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  stream_feed_flutter_core: ^0.7.0+1
  provider: ^6.0.2
  google_fonts: ^2.3.1
  image_picker: ^0.8.4+9
  cached_network_image: ^3.2.0
  transparent_image: ^2.0.0
  jiffy: ^5.0.0

⚠️ NOTE: There may be updated versions of these packages at the time you’re reading this article. If you want to follow along exactly, it will be safer to use the versions mentioned above. Once you are familiar with all of the code, you can run flutter pub outdated to see which packages require updating.

Stream Setup

Before you can get to the code part, you will first need to create a Stream application to handle all of the feed infrastructure.

Creating an app on Stream is really easy and doesn’t take long. It’s also free and doesn’t require a credit card 💸 with the 30-day trial, or you can register for a Maker Account and access Stream Chat for free indefinitely.

Step One - Create an Account

To get started, register for an account.

Create Stream Account

Step Two - Create an App

After creating your account, create an app and name it:

  1. Go to your Stream dashboard.
  2. Select Create App.
  3. Enter an App Name (for example, Streamagram).
  4. Set your Server Location.
  5. Set the Environment to Development.
  6. Select Create App.

After creating your app, you should see it listed in your app’s dashboard. From here you can edit the app, create users and feed groups, access data, and do a lot of advanced operations. If you want to become familiar with Feeds, take a look at the Feeds 101 documentation.

Stream Feeds Dashboard

Step Three - Create User and Timeline Instagram Feeds

To give your Instagram clone the same feeds interactivity you experience when using the app, you will create two Flat Feeds:

  • user
  • timeline

Flat feeds are the only feeds that can be followed, and therefore are a good type to set up for adding activities. Flat feeds can also be used to consume activities from other feeds - in a "timeline"-like manner.

Each user in your Instagram application will have their own unique feeds. The user flat feed will be the feed that users post their pictures to. These posts are called activities.

The timeline flat feed will be used to subscribe to other users, and our own, user feeds.

In summary, if you sign in to the Stream-agram application, the user feed will show all of your posts, and the timeline feed will show all the user feeds that you have subscribed to.

Note: You can name these feeds anything you want, but user and timeline are two standard names, and you will be referencing these names later in your code.

To learn more about feed groups see the documentation on the different feeds types (Flat, Aggregated, Notification).

Now, click on the Add Feed Group button, and create a “user” and “timeline” flat feed. An example of creating the user feed is shown below.

Stream Feed Add Group

After creating the feeds you should now see:

Stream Feed Group Overview

Great! Your feeds are set up. If you scroll down you should also see your application’s Key and Secret.

Stream App Access Keys

Your API Key is only an app identifier and safe to share publicly. Your Secret helps generate authenticated user tokens and perform sensitive API operations. The Secret should be kept private at all times - it is the equivalent of a password.

Please take note of your application’s Key and Secret, you will need them in the next section. 🕵️‍♀️

Step Four - Creating Frontend Tokens

Creating, and updating, users in Stream Feeds is simple. However, for your client application to communicate with the Stream Feeds API, you will first need authentication tokens to validate your server requests. You would normally do this by creating your own custom backend application that can execute various sensitive server operations and keep track of all your users.

However, for demonstration purposes, you will hardcode your users and their tokens in the application.

❗️ NOTE: This should only be done in a development environment - tokens should never be hardcoded in a production application.

Stream Feed offers a number of different backend-clients, including Dart! Seeing as we are Flutter developers, we will use the Dart client to generate tokens.

This Github repository contains code that shows you how you can use the Dart stream-feed package as a server-client to generate frontend tokens using your CLI.

If you’re interested in how the code works you can inspect it yourself. It is really straightforward, it:

  • Reads in your API key and secret
  • Reads in usernames
  • It generates frontend tokens for the given usernames

To run the CLI, do the following:

  1. git clone https://github.com/HayesGordon/stream-feed-cli
  2. cd stream-feed-cli
  3. dart run
  4. Follow the prompts

You can create as many users as you want and name them as you wish in your Instagram clone. For this article, we will generate four users, with the following usernames (IDs):

  • sacha-arbonel
  • gordon-hayes
  • reuben-turner
  • sahil-kumar

Take note of the frontend tokens for all of the users. You will need it in the next section.

It may be simpler to follow along exactly and create these users as well, as it will require fewer code changes later on.

Coding Your Instagram Clone

You’re at the fun part of the tutorial.

https://media.giphy.com/media/rMS1sUPhv95f2/giphy.gif

Before continuing, let's quickly discuss our approach to building our Instagram clone.

Our Stream-agram app will consist of a few major UI components that we'll build off of throughout the rest of this tutorial:

  • Login Screen: Where you'll login (A mock representation).
  • Home Screen: Providing various navigation controls for other areas of the application (default showing the Timline Page).
  • Timeline Page: Where you’ll see the feeds of users you follow.
  • Profile Page: Where you’ll see profile information and all the posts that you’ve made.
  • Search Page: Where you can follow and unfollow other users.
  • Edit Profile Screen: Where you can edit your profile information by changing the profile picture.
  • Add Post Screen: Where you can add new posts (activities) to your user feed.

We'll start with simplified versions of each of these screens and iterate on them as we add more complexity and functionality layer by layer. The end result will be a (near) identical Instagram clone.

Flutter File and Folder Structure

Each application is unique and will require its own folder structure. What is used in this tutorial is by no means better than any other method. It’s simply a convenient way to organize the code of this particular application.

This application groups everything under two high-level folders: app and components.

A component is anything that will be represented in UI, or you can think of it as a piece of the app that can operate in isolation, for example, pages, buttons, and widgets. The component directory also contains a folder called app_widgets, which contains general widgets used across the application.

The app directory contains classes that relate to the whole application, or code that multiple components will use. For example, app state and navigation.

This tutorial uses barrel files to neatly organize imports.

A barrel is a way to roll up imports from several files into a single convenient file.

By the end of this tutorial your file and folder layout should look like this. You can create all of this now, or as you follow the steps in the tutorial:

├── lib
|   ├── app
│   │   ├── navigation
│   │   │   └── custom_rect_tween.dart
|   |   |   └── hero_dialog_route.dart
|   |   |   └── navigation.dart *
|   |   ├── state
|   |   |   ├── models
|   |   |   |   └── models.dart *
|   |   |   |   └── user.dart
|   |   |   └── app_state.dart
|   |   |   └── demo_users.dart
|   |   |   └── state.dart *
|   |   └── app.dart *
|   |   └── stream_agram.dart
|   |   └── theme.dart
|   |   └── utils.dart
|   ├── components
│   │   ├── app_widgets
│   │   │   └── app_widgets.dart *
|   |   |   └── avatars.dart
|   |   |   └── comment_box.dart
|   |   |   └── favorite_icon.dart
|   |   |   └── tap_fade_icon.dart
|   |   ├── comments
|   |   |   ├── state
|   |   |   |   └── comment_state.dart *
|   |   |   |   └── state.dart
|   |   |   └── comment_screen.dart
|   |   |   └── comments.dart *
│   │   ├── home
│   │   │   └── home_screen.dart
|   |   |   └── home.dart *
|   |   ├── login
|   |   |   └── login_screen.dart
|   |   |   └── login.dart *
│   │   ├── new_post
│   │   │   └── new_post_screen.dart
|   |   |   └── new_post.dart *
|   |   ├── profile
|   |   |   └── edit_profile_screen.dart
|   |   |   └── profile_page.dart
|   |   |   └── profile.dart *
│   │   ├── search
│   │   │   └── search_page.dart
|   |   |   └── search.dart *
|   |   └── timeline
|   |   |   ├── widgets
|   |   |   |   └── post_card.dart
|   |   |   |   └── widgets.dart *
|   |       └── timeline_page.dart
|   |       └── timeline.dart *
│   └── main.dart
├── pubspec.lock
├── pubspec.yaml

⚠️ Note that Barrel files are indicated with an *

Creating Demo Instagram Users

As mentioned earlier, for demonstration purposes, in this tutorial you will hardcode demo users.

Create the file, app/state/demo_users.dart, and add the following:

dart
import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';

/// Demo application users.
enum DemoAppUser {
  sahil,
  sacha,
  reuben,
  gordon,
}

/// Convenient class Extension on [DemoAppUser] enum
extension DemoAppUserX on DemoAppUser {
  /// Convenient method Extension to generate an [id] from [DemoAppUser] enum
  String? get id => {
        DemoAppUser.sahil: 'sahil-kumar',

There are a few TODOs in this file, as you will need to add the String tokens that you generated earlier. Be sure to add the correct token for the correct user.

If you are using different usernames then you will need to update other parts of this file as needed.

The code above has an enum class called DemoAppUser, with an extension, DemoAppUserX, to access various other data, such as the token and user name. This file isn’t too important, and you can choose to create your demo users in whatever way you wish to.

Create the app/state/state.dart barrel file and add the export:

dart
export 'demo_users.dart';

Then create a higher level barrel file, called app/app.dart, and add the following export:

dart
export 'state/state.dart';

Theming Your Instagram Clone

You will notice we use the same fonts and color schemes you see in Instagram, giving your clone the look and feel of the real-world app. Lastly, it’s important to note that our clone also includes a light ☀️ and a dark 🌚 mode. The preferred mode is based on the platform preferences. Getting your phone to switch modes based on the time of day, though, may be a bit outside the scope of this tutorial 😉

Create app/theme.dart and add the following:

dart
import 'package:flutter/material.dart';

/// Global reference to application colors.
abstract class AppColors {
  /// Dark color.
  static const dark = Colors.black;

  static const light = Color(0xFFFAFAFA);

  /// Grey background accent.
  static const grey = Color(0xFF262626);

  /// Primary text color
  static const primaryText = Colors.white;

You can review this at your own pace, or explore it as you use some of these definitions later on in the tutorial.

Update the state/state.dart barrel file:

dart
export 'state/state.dart';
export 'theme.dart'; // ADD THIS

Models (User)

Can you believe that this big application will only require you to create one model class? 😱

Well, believe it, as most of the models will come from the Stream Feed packages.

Create app/state/models/user.dart and add the following:

dart
import 'dart:convert';

import 'package:flutter/material.dart';

/// Data model for a feed user's extra data.

class StreamagramUser {
  /// Data model for a feed user's extra data.
  const StreamagramUser({
    required this.firstName,
    required this.lastName,
    required this.fullName,
    required this.profilePhoto,
    required this.profilePhotoResized,
    required this.profilePhotoThumbnail,

This is your User Data Class. It stores a number of fields and provides convenience methods, such as toMap, fromMap, and copyWith. These will come in handy later on. The reason you are creating this class is to easily create an object from the extra data that will be stored for each Stream Feed User.

Even though we only have one model, we will be good citizens and create our barrel file anyway.

Create app/state/models/models.dart and add the following:

dart
export 'user.dart';

And in the app/state/state.dart barrel file add:

dart
export 'demo_users.dart';
export 'app_state.dart';
export 'models/models.dart'; // ADD THIS

State Management (Provider)

A hot topic for debate.

For this Instagram Clone you will only need to manage basic application state, so that you can easily propagate changes to the UI.

The majority of the state will be managed by the stream_feed_flutter_core package, however, you still need an easy way to pass around some state in your application, for example, the current authenticated used.

This article is not trying to be opinionated about state management. A simple InheritedWidget is sometimes all you need, and Provider makes inherited widgets extremely easy, as well as giving some other great functionality that we will explore more in this tutorial.

Create the file app/state/app_state.dart and add the following:

dart
import 'package:flutter/material.dart';
import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';
import 'models/models.dart';

import 'demo_users.dart';

/// State related to Stream-agram app.
///
/// Manages the connection and stores a references to the [StreamFeedClient]
/// and [StreamagramUser].
///
/// Provides various convenience methods.
class AppState extends ChangeNotifier {
  /// Create new [AppState].
  AppState({

This class isn’t too complicated. Later on, you will need to expand it a bit more.

For now, it only manages the local user and their data in a responsive way. A ChangeNotifier class is an easy way to manage state in a Flutter application. You store mutable variables in the class that you can then update; once updated you can notify any listeners by calling the notifyListeners method.

In this class, you will see a variable called _streamagramUser that is initially null, which will be set once you call the connect method, which requires a DemoAppUser.

The StreamFeedClient is probably the most important in this class. It has a variety of methods to easily interact with the Stream Feeds API. The setUser method, for example, is exactly that, it sets the current Stream Feeds user for the application locally, and then proceeds to either create or read the user on the Stream Feeds backend (depending on whether the user already exists).

The setUser method on StreamFeedClient requires a User object, which takes in:

  • a unique user id (our username)
  • extra data that will be used to set the user information on the server (name, profile picture URL, or anything you want)
  • and the frontend token

This method is an asynchronous Future and once complete you update the local _streamagramUser object by calling the fromMap method, which creates a new object from the Map. After that, you call notifyListeners to update any listeners of the change.

Update the app/state/state.dart barrel file:

dart
export 'demo_users.dart';
export 'app_state.dart'; // ADD THIS

Initialization and Providers

Create a new file, app/stream_agram.dart, and add the following:

dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:stream_agram/app/app.dart';
import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';

import '../components/login/login.dart';

/// {@template app}
/// Main entry point to the Stream-agram application.
/// {@endtemplate}
class StreamagramApp extends StatefulWidget {
  /// {@macro app}
  const StreamagramApp({
    Key? key,
    required this.appTheme,

There are a few important things that you are doing in this file:

  • Initialize the StreamFeedClient with your unique Stream application’s API Key.
  • Create the AppState class, passing in the StreamFeedClient.
  • Create a FeedBloc, which is the business logic for your Stream Feeds applications. This class comes from the Stream Feed package and you will be using it a lot later on in the tutorial.
  • Wrap MaterialApp with a ChangeNotifierProvider exposing your AppState object. This will ensure that the whole application can easily access your application state. It is important to use the .value factory, as that exposes the already created appState, object.
  • Set the home argument to be the LoginScreen. You will create this screen soon.
  • Use the builder argument to wrap your application with a FeedProvider, which exposes the feedBloc object to the whole application. The FeedProvider is needed by the Stream Feeds widgets to easily access the FeedBloc, wherever they are in the widget tree.

Update your app/app.dart barrel file:

dart
export 'state/state.dart';
export 'theme.dart';
export 'stream_agram.dart'; // ADD THIS

Extension Classes - Utilities

Next, create an app/utils.dart file that will contain some helpful extensions that you’ll use throughout the application. These extensions will make certain operation easier in your application’s UI, and reduce the amount of code needed to execute repetitive operations.

These extensions will allow you to:

  • Remove and show a Snackbar with a provided message.
  • Access AppState from BuildContext.

Add the following:

dart
import 'package:flutter/material.dart';

import 'package:provider/provider.dart';

import 'state/app_state.dart';

/// Extension method on [BuildContext] to easily perform snackbar operations.
extension Snackbar on BuildContext {
  /// Removes the current active [SnackBar], and replaces it with a new snackbar
  /// with content of [message].
  void removeAndShowSnackbar(final String message) {
    ScaffoldMessenger.of(this).removeCurrentSnackBar();
    ScaffoldMessenger.of(this).showSnackBar(
      SnackBar(content: Text(message)),
    );

Now, update the app/app.dart barrel file for the last time 🥳: (for now, at least):

dart
export 'state/state.dart';
export 'theme.dart';
export 'stream_agram.dart';
export 'utils.dart'; // ADD THIS

Create Your Mock Instagram Login Screen

Next, you will create a “login screen”, a mock screen to select one of the demo users.

Create the file components/login/login_screen.dart and add the following:

dart
import 'package:flutter/material.dart';

import '../../app/app.dart';
import '../home/home.dart';

/// {@template login_screen}
/// Screen that presents an option of users to authenticate as.
/// {@endtemplate}
class LoginScreen extends StatefulWidget {
  /// {@macro login_screen}
  const LoginScreen({Key? key}) : super(key: key);

  
  _LoginScreenState createState() => _LoginScreenState();
}

You can explore this file in detail on your own. As a quick summary this widget does the following:

  • Displays buttons for each demo user, showing their name. On click, it will attempt to authenticate as that user.
  • Calls connect on AppState, and if the response is true navigate to the HomeScreen, otherwise display an error message in the Snackbar.
  • Shows Snackbar messages indicating the state of the connection.

Once you run your application it will look something like this:

Instagram clone mock login screen

Remember to create your login barrel file, components/login/login.dart:

dart
export 'login_screen.dart';

Creating Your Instagram-like Home Screen

This will be the main screen of your application, that is used to navigate users to various other areas of the application.

Opening the real-world Instagram app you’re presented with functionality to access various pages, through a bottom navigation bar:

  • Timeline page (default)
  • Search page
  • Reels page
  • Shopping page
  • Profile page

The application also allows you to easily access commonly used features in the application bar, such as:

  • Create a new post
  • Navigate to activity timeline
  • Navigate to messaging

For the time being, we will create a simplified version. Later on, you will expand the code to make some improvements in how the Home Screen looks and functions.

To begin, your Home screen will have navigation similar to the real-world Instagram clone by using a Flutter PageView and BottomNavigationBar to have the following pages:

  • Timeline page (default)
  • Search page
  • Profile page

For the time being, all of these pages will have simple text placeholders, which you will expand later.

You’ll also create an AppBar, which, for now, will only contain the title. You’ll expand this later with more controls.

Create components/home/home_screen.dart and add the following:

dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

import '../../app/app.dart';

/// HomeScreen of the application.
///
/// Provides Navigation to various pages in the application and maintains their
/// state.
///
/// Default first page is [TimelinePage].
class HomeScreen extends StatefulWidget {
  /// Creates a new [HomeScreen]
  const HomeScreen({Key? key}) : super(key: key);

Let’s break this down:

  • This screen is split into three separate parts: AppBar, PageView, and BottomNavigationBar.
  • A list of widgets is stored in the _homePages variable. For now, they’re simple Text widget placeholders.
  • A PageController to navigate between the different PageView pages. This is used in the _StreamagramBottomNavBar. There is also a listener added to the controller to update the UI when it changes.
  • The physics of the PageView is set to NeverScrollableScrollPhysics. This ensures that the only way to update the PageView is by pressing one of the bottom navigation bar items.
  • Use the grandHotel font from the GoogleFonts package to create the AppBar title.

If you run the application now, the screen should look like this:

Instagram Clone Home Screen

And you should be able to switch between the different pages in the PageView.

Application Entry Point (

The final piece of the puzzle. Delete everything in main.dart and replace it with the following:

dart
import 'package:flutter/material.dart';

import 'app/app.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  final theme = AppTheme();
  runApp(StreamagramApp(appTheme: theme));
}

You should now have a login screen, be able to select a user, and navigate to the Home screen. If you’re running into errors, make sure that the users’ tokens, ids, and your API Key are correct.

Awesome 🚀. With the basic building blocks done, you can proceed to the more advanced parts.

https://media.giphy.com/media/8xgqLTTgWqHWU/giphy.gif

Creating an Instagram Avatar Widget

There are some global widgets that you will use throughout your Instagram clone. You will add a few of these over the course of this tutorial. For now, you’ll begin by creating a simple Avatar widget that displays the user’s profile image if set, or just their initials if no image is set.

Create components/app_widgets/avatars.dart, and add:

dart
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';

import '../../app/app.dart';

/// An avatar that displays a user's profile picture.
///
/// Supports different sizes:
/// - `Avatar.tiny`
/// - `Avatar.small`
/// - `Avatar.medium`
/// - `Avatar.big`
/// - `Avatar.huge`
class Avatar extends StatelessWidget {
  /// Creates a tiny avatar.

You can explore this widget yourself. What is important is that it supports a few different named constructors with different default sizes (tiny, large, huge, etc.). This widget will be used wherever you show a user avatar. It also makes use of the cached_network_image package to cache images.

Create the components/app_widgets/app_widgets.dart barrel file, and export the file:

dart
export 'avatars.dart';

Creating Your Instagram-like Profile Page

An Instagram profile page, at a minimum, requires the following:

  • An Avatar and User information.
  • Followers and Following information.
  • The functionality to easily edit profile information.
  • A list of user created posts (activities).
  • The ability to easily add new posts.

The following sections will discuss each of these steps in detail.

Create a Main Instagram Profile Page

Create the file components/profile/profile_page.dart and add:

dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';

import '../../app/app.dart';
import '../app_widgets/app_widgets.dart';

/// {@template profile_page}
/// User profile page. List of user created posts.
/// {@endtemplate}
class ProfilePage extends StatelessWidget {
  /// {@macro profile_page}
  const ProfilePage({Key? key}) : super(key: key);

  

In this file, you’re finally using some of the stream_chat_flutter_core widgets to easily display feed activities. You can explore the UI code at your own pace. Let’s discuss the most important parts:

  • FlatFeedCore requires you to specify the feedGroup, which will be “user” for the profile page. Remember that this is what you set at the beginning of this tutorial.
  • FlatFeedCore has four builders you need to give to handle the different states (loadingBuilder, errorBuilder, emptyBuilder, feedBuilder).
  • emptyBuilder returns a CustomScrollView, that displays the _ProfileHeader, _EditProfileButton, and _NoPostsMessage widgets.
  • You will update feedBuilder later on.
  • The _ProfileHeader widget uses Provider to watch the state of AppState.
  • There are a few TODOs that you will complete later.

The FlatFeedCore widget deals with all of the complexity of retrieving a specific feed’s activities and making it easy for you to display them how you want to. There are a number of different arguments you can give to this widget to help with pagination or filtering of feed content. For now, however, we won’t go into all of that detail. You can explore that after you’ve finished this tutorial.

Create the components/profile/profile.dart barrel file and add:

dart
export 'profile_page.dart';

Then go back to components/home/home_screen.dart and modify the _homePages variable to show the ProfilePage. It should look like this:

dart
...
import '../profile/profile.dart'; // ADD IMPORT
...
/// List of pages available from the home screen.
  static const List<Widget> _homePages = <Widget>[
    Center(child: Text('TimelinePage')),
    Center(child: Text('SearchPage')),
    ProfilePage(), // MODIFY THIS
  ];

If you run your Instagram clone now and navigate to the ProfilePage, you should see something similar to this:

Instagram Profile Screen

Create an Edit Profile Screen

The Edit Profile Screen displays user information (like your name) and allows users to update their profile picture. Of course, you can extend this page to add more functionality if you wish. But for now, we just want to display the user’s avatar, name, and username.

Updating the App State

Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

Before creating the UI, you need to update the AppState class.

Open app/state/app_state.dart and modify it as follows:

dart
class AppState extends ChangeNotifier {

...

    var isUploadingProfilePicture = false;

...

    /// Uploads a new profile picture from the given [filePath].
  ///
  /// This will call [notifyListeners] and update the local [_streamagramUser] state.
  Future<void> updateProfilePhoto(String filePath) async {
    // Upload the original image
    isUploadingProfilePicture = true;
    notifyListeners();

The above code creates an isUploadingProfilePicture state variable that is initially set to false.

The updateProfilePhoto method does a few things:

  1. It takes in the file path to the image.
  2. Sets isUploadingProfilePicture to true. This variable will be observed in your UI.
  3. Uploads that file using the StreamFeedClient by giving an AttachmentFile. This uploads the file to Stream’s CDN.
  4. Creates resized versions of the profile picture using the getResized method. These are smaller versions of the profile picture that the application will use when the Avatar is displayed as tiny or small.
  5. Updates the local _streamagramUser with the new profile photo URLs.
  6. Sets isUploadingProfilePicture to false.
  7. Calls the update method on the currentUser to update the user on the Stream database as well.

The code also contains a few calls to notifyListeners to update the UI of changes.

Create an Instagram-like Image Picker

To select an image from the device’s photos for your Instagram clone profile picture, you will use the image_picker package.

See here for installation instructions. Depending on whether you are on Android or iOS, different steps may be required.

UI - Edit Profile Screen

Create components/profile/edit_profile_screen.dart and add:

dart
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:provider/provider.dart';

import '../../app/app.dart';
import '../app_widgets/app_widgets.dart';

/// {@template edit_profile_page}
/// Screen to edit a user's profile info.
/// {@endtemplate}
class EditProfileScreen extends StatelessWidget {
  /// {@macro edit_profile_page}
  const EditProfileScreen({
    Key? key,
  }) : super(key: key);

Let’s break down the above code:

  • There is a custom PageRouteBuilder, which uses a SlideTransition to slide the page in from the bottom. Similar to how Instagram does it.
  • Various UI and layout widgets to display user information.
  • A _changePicture method that uses the image_picker package to select an image from the gallery and upload it using the updateProfilePhoto method you made earlier.
  • You also specify the maxWidth, maxHeight, and imageQuality to reduce the image size that has to be uploaded.

The page should look something like this:

Instagram Clone Edit Profile Screen

Modify the components/profile/profile.dart barrel file:

dart
export 'edit_profile_screen.dart'; // ADD THIS
export 'profile_page.dart';

And in components/profile/profile_page.dart, modify the TODO in the _EditProfileButton widget:

dart
...

import 'package:stream_agram/components/profile/edit_profile_screen.dart';

...

class _EditProfileButton extends StatelessWidget {
  const _EditProfileButton({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      child: OutlinedButton(
        onPressed: () {

Success!! 🥳

If you completed all of the steps above, your Instagram clone profile should let you update your placeholder Avatar with an image from your device:

Update profile picture

Create a

Time for another global app widget. This widget is just a simple icon widget that fades in and out when tapped.

Create components/app_widgets/tap_fade_icon.dart and add:

dart
import 'package:flutter/material.dart';

/// {@template tap_fade_icon}
/// A tappable icon that fades colors when tapped and held.
/// {@endtemplate}
class TapFadeIcon extends StatefulWidget {
  /// {@macro tap_fade_icon}
  const TapFadeIcon({
    Key? key,
    required this.onTap,
    required this.icon,
    required this.iconColor,
    this.size = 22,
  }) : super(key: key);

You can inspect this widget on your own.

Add the class to the correct barrel file. Open components/app_widgets/app_widgets.dart and add:

dart
export 'avatars.dart';
export 'tap_fade_icon.dart'; // ADD THIS

Building Your Instagram User Feed

As mentioned earlier, our Instagram clone can be broken down into a user feed and a timeline feed. The “user” feed is unique to each user and will show all of the posts that an individual user has made. While the “timeline” feed is a combination of users’ “user” feeds.

In this section you will to create the functionality to push Activities directly to a “user” feed. Activities are the content that appear within a feed, for example, posts in an Instagram clone.

In order to code our Stream-agram app so that it looks, feels, and interacts like its real-world counterpart, we need to first finish the user feed by coding the following:

  • Build out a screen for adding new posts
  • Add a PictureViewer with interactivity and transition animations (hero animation)
  • Update the AppBar so we can continue adding more posts
  • Enable following and unfollowing other user feeds

With these parts out of the way, we can then pivot to building your Instagram timeline feed.

Creating the Instagram New Post Screen

This is the screen where you will add a photo, with a description, to your user feed.

Create the file components/new_post/new_post_screen.dart and add:

dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';
import 'package:transparent_image/transparent_image.dart';

import '../../app/app.dart';
import '../app_widgets/app_widgets.dart';

/// Screen to choose photos and add a new feed post.
class NewPostScreen extends StatefulWidget {
  /// Create a [NewPostScreen].
  const NewPostScreen({Key? key}) : super(key: key);

  /// Material route to this screen.

Let’s break this screen down:

  • In a similar way as before, you’re using the image_picker package to select an image. Once an image is selected you set the local _pickedFile value.
  • You create a TextFormField that requires you to enter a description for the image.
  • Once an image has been picked and the description has been entered, then a user can press the Share button. If these conditions aren’t met, then a warning message is displayed.
  • The Share button calls the _postImage method, which does a few things:
    1. Sets the loading state to true.
    2. Decodes the image - as this is needed to get the image size.
    3. Uploads the image to the Stream CDN.
    4. Creates a resized, smaller, version of the image, using getResized.
    5. Uses FeedProvider.of(context).bloc to retrieve the FeedBloc class and creates a new activity using onAddActivity.

For our Instagram clone, an activity will require three pieces of information: an actor, a verb, and an object. The actor is the entity performing the action (the user). The verb is the type of action (a post). The object is the content of the activity itself (often a reference, but we will just say an image).

So basically, the current-user is posting an image to the “user” feed. Then you’re also specifying some extra data, which most notably includes:

  • The image description
  • The image aspect ratio (you will use this later on)
  • The image and resized image URLs

For more information, see the Adding Activities documentation. Activities and feeds can be quite complex. But that complexity means you can do some advanced things! For now, however, the above is all you need.

The screen should look like this:

Instagram new post screen

Create the components/new_post/new_post.dart barrel file:

dart
export 'new_post_screen.dart';

Add Navigation and Display Instagram Posts (Activities)

Open components/profile/profile_page.dart .

Modify the _NoPostsMessage widget to navigate to the NewPostScreen:

dart
...

import '../new_post/new_post.dart';

...

class _NoPostsMessage extends StatelessWidget {
  const _NoPostsMessage({
    Key? key,
  }) : super(key: key);

  
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,

Modify the ProfilePage widget’s feedBuilder to the following:

dart

import 'package:cached_network_image/cached_network_image.dart';

...

feedBuilder: (context, activities) {
  return RefreshIndicator(
    onRefresh: () async {
      await FeedProvider.of(context)
          .bloc
          .currentUser!
          .get(withFollowCounts: true);
      return FeedProvider.of(context)
          .bloc
          .queryEnrichedActivities(feedGroup: 'user');

Now that your Instagram clone has the ability to add activities (posts), you need to handle the scenario when there are activities to show. The above builder:

  1. Creates a CustomScrollView.
  2. Displays the _ProfileHeader with the number of posts set to the number of activities.
  3. Displays the _EditProfileButton.
  4. Displays a SliverGrid of all the activities, that returns a CachedNetworkImage, with the URL retrieved from the activity. Be sure to display the resized_image_url as it will be more performant to display smaller images, instead of Flutter decoding and caching large images.
  5. Wraps the list in a RefreshIndicator, that will allow users to pull the list down and initiate a page refresh, by getting the latest server data. This is achieved by calling currentUser.get and bloc.queryEnrichedActivities. queryEnrichedActivities will update the FeedBloc state for the given feed group.

That’s it! You should now be able to log in as a user and upload an image to their user feed. Below is a video showing a demo:

Upload post preview

Adding Picture Viewer Transition Animations

Before moving on to the timeline feed, let’s skill up a bit with page transitions.

When using Instagram, you'll notice the app provides seamless transitions when selecting an image for a post. To replicate this behavior in our Instagram clone, we'll create a picture viewer that performs a hero transition when an image is selected, while dynamically switching the low-resolution image for the higher resolution one. This will create a seamless transition from the grid view to the full screen. You’ll also add functionality to pan and zoom the picture.

There are a few steps needed to achieve the above described behavior:

  1. Creating a CustomRectTween for a custom Hero animation
  2. Creating a PageRoute that creates a FadeTransition
  3. Updating the UI in components/profile/profile_page.dart to perform navigation
  4. Make use of CachedNetworkImage to fade in the low-resolution cached image, for the high-resolution image
  5. Use the InteractiveViewer widget to add pan and zoom functionality

First, you will need to create some helper classes for the transitions.

Create app/navigation/custom_rect_tween.dart and add the following:

dart
import 'dart:ui';

import 'package:flutter/widgets.dart';

/// {@template custom_rect_tween}
/// Linear RectTween with a [Curves.easeOut] curve.
///
/// Less dramatic than the regular [RectTween] used in [Hero] animations.
/// {@endtemplate}
class CustomRectTween extends RectTween {
  /// {@macro custom_rect_tween}
  CustomRectTween({
    required Rect? begin,
    required Rect? end,
  }) : super(begin: begin, end: end);

This class extends RectTween to create a custom lerp. You will use this later to replace the standard Hero transition. “lerp” is a term used to describe the interpolation between a start and an end value over time (t). This linearly transforms a Rect from a begin to an end value.

Next, create app/navigation/hero_dialog_route.dart and add the following:

dart
import 'package:flutter/material.dart';

/// {@template hero_dialog_route}
/// Custom [PageRoute] that creates an overlay dialog (popup effect).
///
/// Best used with a [Hero] animation.
/// {@endtemplate}
class HeroDialogRoute<T> extends PageRoute<T> {
  /// {@macro hero_dialog_route}
  HeroDialogRoute({
    required WidgetBuilder builder,
    RouteSettings? settings,
    bool fullscreenDialog = false,
  })  : _builder = builder,
        super(settings: settings, fullscreenDialog: fullscreenDialog);

This is a custom PageRoute that performs a FadeTransition and has a black background. There are other ways you could also perform the same transition effect. Explore this class and play around with the overrides to see what you can create. Earlier in this tutorial you also made use of PageRouteBuilder, which is an alternative way to do the above. However, extending the class gives you more control.

Create the app/navigation/navigation.dart barrel file:

dart
export 'custom_rect_tween.dart';
export 'hero_dialog_route.dart';

Update the app/app.dart barrel file:

dart
export 'state/state.dart';
export 'theme.dart';
export 'stream_agram.dart';
export 'utils.dart';
export 'navigation/navigation.dart'; // ADD THIS

UI

Open the components/profile/profile_page.dart file and add the following to the bottom:

dart
...

class _PictureViewer extends StatelessWidget {
  const _PictureViewer({
    Key? key,
    required this.activity,
  }) : super(key: key);

  final EnrichedActivity activity;

  
  Widget build(BuildContext context) {
    final resizedUrl = activity.extraData!['resized_image_url'] as String?;
    final fullSizeUrl = activity.extraData!['image_url'] as String;
    final aspectRatio = activity.extraData!['aspect_ratio'] as double?;

The above code is quite special. It does a few things:

  • Retrieves the image and resized image URLs, as well as the aspect ratio.
  • Returns a CachedNetworkImage which uses the image URL (full resolution), and specifies the placeholder to be the current CachedNetworkImage for the resized URL that has already been cached.
  • Using the AspectRatio widget will ensure that both the cached image (resized URL), and the full resolution image, fill up the same amount of space.
  • Sets the fadeInDuration to Duration.zero.
  • Wraps everything in a Hero widget with, using the CustomRectTween for the createRectTween argument.
  • Wraps everything in an InteractiveViewer to allow user’s to pan and zoom the image.

All of the above code will ensure the image smoothly fades from the small cached version to the full resolution one once it is retrieved – while also doing a hero transition at the same time! 🥳

Now, in the ProfilePage widget, update the SliverChildBuilderDelegate to the following:

dart
...

delegate: SliverChildBuilderDelegate(
  (context, index) {
    final activity = activities[index];
    final url =
        activity.extraData!['resized_image_url'] as String;
    return GestureDetector(
      onTap: () {
        Navigator.of(context).push(
          HeroDialogRoute(
            builder: (context) {
              return _PictureViewer(activity: activity);
            },
          ),

The above wraps the CachedNetworkImage in a Hero widget and a GestureDetector that performs a navigation using the newly created HeroDialogRoute, which opens the _PictureViewer widget.

⚠️ Ensure that the Hero tags are the same across the pages for the transition to work correctly. Also, ensure you do not show duplicate tags on the same screen - which is why you use the activity.id to make it unique.

You should now have an awesome animation when clicking on an image. See the video below:

Picture Viewer Demo

Update Your Instagram App Bar Actions

At the moment the AppBar is a little empty. You may have noted as well that there is currently no way to add more posts after the first post has been created.

Let’s change that.

Open components/home/home_screen.dart and update the AppBar with an actions argument:

dart
...

import '../app_widgets/app_widgets.dart';
import '../new_post/new_post.dart';

...

AppBar(

...

    actions: [
    Padding(
      padding: const EdgeInsets.all(8),
      child: TapFadeIcon(

This uses the TapFadeIcon you created earlier. Two of these icons are only for decoration, and not part of the demo app. However, the first will open the NewPostScreen when tapped.

The AppBar should now look like this, and it should be possible to add multiple posts:

Instagram User Feed

Follow/Unfollow Instagram Users

Following and unfollowing other users (feeds) is a core Instagram feature. Without it, your users will only be posting pictures for themselves to see. 🤪

Create Your Instagram-like Search Page

Create the components/search/search_page.dart file, and add:

dart
import 'package:flutter/material.dart';
import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';

import '../../app/app.dart';
import '../app_widgets/app_widgets.dart';

/// Page to find other users and follow/unfollow.
class SearchPage extends StatelessWidget {
  /// Create a new [SearchPage].
  const SearchPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    final users = List<DemoAppUser>.from(DemoAppUser.values)
      ..removeWhere((it) => it.id == context.appState.user.id);

This is quite a bit of code and might be easier for you to explore at your own pace. To summarize it, this code snippet:

  1. Gets all demo users, and removes the currently authenticated user from that list.
  2. Displays all the users using the _UserProfile widget.
  3. Calls the getUser method in the _UserProfile widget state:
    • This method gets the latest user information from the server.
    • Checks to see whether the current user is already following that user, using the isFollowingFeed method on the FeedBloc.
  4. Uses a FutureBuilder widget to await the result of getUser.
  5. Returns a _ProfileTile widget for each user that neatly displays the user information, and makes it possible to follow/unfollow a user, by calling the followOrUnfollowUser method. The FeedBloc class is used to easily follow/unfollow certain feeds based on ID.
  6. The followFeed and unfollowFeed methods on FeedBloc have default values set to use the timeline and user feeds. You can customize these if needed if you used different names.

The followOrUnfollowUser method also executes:

dart
FeedProvider.of(context).bloc.queryEnrichedActivities(
          feedGroup: 'timeline',
          flags: EnrichmentFlags()
            ..withOwnReactions()
            ..withRecentReactions()
            ..withReactionCounts(),
        );

This forces the timeline feed to update. The flags argument is interesting: it tells the API to “enrich” the activities with these flags. What that basically means is that it will retrieve the activities and the reactions added to those activities. You will explore this later on in the tutorial.

Next, create the barrel file, components/search/search.dart:

dart
export 'search_page.dart';

Finally, you will need a way to navigate to this page. Open components/home/home_screen.dart and in the HomeScreen widget modify the _homePages variable:

dart
...

import 'package:stream_agram/components/search/search.dart';

...

/// List of pages available from the home screen.
static const List<Widget> _homePages = <Widget>[
  Center(child: Text('TimelinePage')),
  SearchPage(), // ADD THIS
  ProfilePage(),
];

...

If everything works correctly, you should now be able to navigate to the Search Page and see:

Instagram Follow Screen

⚠️ NOTE: If you’re getting a “Could not load profile” error, then that probably means you have not created the user account yet. Be sure to sign in with each user account at least once!

Building Your Instagram Timeline Feed

Your Stream-agram users can post to their user feed and they can follow/unfollow specific user feeds, just like the real-world Instagram app. That means you’re now finally ready to display the timeline feed.

However, to get the UI and animation just right can be difficult. So instead of jumping into the deep end, let’s first create some of the UI widgets that this class relies on.

Creating a Favorite Icon and Comment Box

What would a social media app like Instagram be without the ability to react to posts? To add this functionality to our Instagram clone, you’ll need to create a widget that handles “favoriting” (or “liking”) a post and a widget to add comments to a post (a TextField).

Let’s start with the FavoriteIconButton.

Create components/app_widgets/favorite_icon.dart and:

dart
import 'package:flutter/material.dart';
import 'package:stream_agram/app/theme.dart';

/// {@template favorite_icon_button}
/// Animated button to indicate if a post/comment is liked.
///
/// Pass in onPressed to
/// {@endtemplate}
class FavoriteIconButton extends StatefulWidget {
  /// {@macro favorite_icon_button}
  const FavoriteIconButton({
    Key? key,
    required this.isLiked,
    this.size = 22,
    required this.onTap,

This is a simple widget that highlights a heart icon when tapped and calls the onTap callback. This widget is used to indicate that a post or comment has been liked.

Instagram Clone Like Button

Next, you’ll create the CommentBox widget. Create components/app_widgets/comment_box.dart and add:

dart
import 'package:flutter/material.dart';

import '../../app/app.dart';
import 'app_widgets.dart';

/// Displays a text field styled to easily add comments to posts.
///
/// Quickly add emoji reactions.
class CommentBox extends StatelessWidget {
  /// Creates a [CommentBox].
  const CommentBox({
    Key? key,
    required this.commenter,
    required this.textEditingController,
    required this.focusNode,

As a summary, this widget:

  • Takes in a StreamagramUser, which will be the current user, or commenter, to display the user profile image. It also takes in a FocusNode, TextEditingController, and a callback method named onSubmitted, which are all used in the text editing process. The onSubmitted callback will be called when the message is sent (Done button, or on return).
  • Provides various UI and styling. For example, enabling and disabling the Done button.
  • Creates a convenient emoji box that updates the TextEditingController, with the selected emoji value, when pressed.
Instagram Clone Comment Box

Finally, expose the files in the components/app_widgets/app_widgets.dart barrel file:

dart
export 'avatars.dart';
export 'tap_fade_icon.dart';
export 'favorite_icon.dart'; // ADD THIS
export 'comment_box.dart'; // ADD THIS

Creating a Post Card Widget

This is a big one. This widget will show everything related to a “post”, or activity.

This widget will take too long to explain in detail 😅. Instead, let's just see what it looks like:

Post Card Widget

Everything in the above image is part of the PostCard widget:

  • A header with the name and picture of the poster.
  • The image/activity.
  • A bar to like and add comments (the share and bookmark icons are only for UI).
  • An Image description.
  • A comment box.

To begin, create components/timeline/widgets/post_card.dart and add the following:

dart
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:provider/provider.dart';
import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';

import '../../../app/app.dart';
import '../../app_widgets/app_widgets.dart';

typedef OnAddComment = void Function(
  EnrichedActivity activity, {
  String? message,
});

/// {@template post_card}

There is too much to cover in detail in this file. The important bits are:

  • You pass in an EnrichedActivity to PostCard. Which is basically an Activity on steroids. This contains extra information, like child activities and reactions. A reaction can be anything added to an activity, such as a comment or a like.
  • Pass in an OnAddComment callback. This will be called when the button is pressed to add a comment to this post. The reason you have this is so that when a user clicks one of the emojis, you can immediately fill in your CommentBox widget with that value.
  • You locally manage the state of the like reactions that are added to an activity. The reason for this is that it is simpler to manage it yourself than to update the StreamFeedBloc directly, which will result in the entire list rebuilding when a post is liked.

It is worth taking a deeper look at the _addLikeReaction method in the _PictureCarousal widget.

If you need more information be sure to watch the companion video.

Create the components/timeline/widgets/widgets.dart barrel file and add:

dart
export 'post_card.dart';

Creating Your Instagram-like Timeline Page

Another big widget. On this page, you will use the FlatFeedCore widget to display the “timeline” feed. For each activity in the feed, you’ll display a PostCard. There is also some extra magic to create a floating CommentBox, that’s visibility is animated when creating a comment.

Create components/timeline/timeline_page.dart and add:

dart
import 'package:flutter/material.dart';
import 'package:stream_agram/app/app.dart';
import 'package:stream_agram/components/app_widgets/app_widgets.dart';
import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';

import 'widgets/widgets.dart';

/// {@template timeline_page}
/// Page to display a timeline of user created posts. Global 'timeline'
/// {@endtemplate}
class TimelinePage extends StatefulWidget {
  /// {@macro timeline_page}
  const TimelinePage({Key? key}) : super(key: key);

  

The most important parts of this page:

  • You create a Stack, with the first item a FlatFeedCore, with the feedGroup set to “timeline”. This is similar to what you used in the Profile Page, however now you are:
    • Returning a PostCard in the feedBuilder.
    • Setting the flags argument, with EnrichmentFlags. This will ensure that the FlatFeedCore creates EnrichedActivities that contains all of the current users reactions (withOwnReactions), all the recent reactions (withRecentReactions), and all reaction counts (withReactionCounts).
  • The second, and final item, of the stack is a _CommentBox. This is private widget that does a fancy custom animation to show and hide the CommentBox widget that you created earlier.
    • The animation is triggered when the _showCommentBox's value changes. This is a ValueNotifier.
  • The addComment method uses the FeedBloc to add a reaction on the activity. The reaction that you’re adding is a comment (type), on the timeline (feed group), with a String message as extra data.
  • The openCommentBox method gets called when a users presses on the area to add a comment, or if they select one of the emojis. This does a number of different things to ensure the CommentBox is displayed and in focus.
  • The reason that _CommentBox uses a custom explicit animation is that you also want to set the Visibility to false when the animation is done and the comment box should be hidden. This improves performance as you do not want Flutter to go through the process of creating the CommentBox, and setting the opacity to zero when it is not needed. Applying opacity in Flutter can be quite expensive, especially when animating. The use of the FadeTransion widget is the most optimal way to animate opacity in Flutter.

Phew 😮‍💨. Okay, that was intense. Almost done.

Create a barrel file components/timeline/timeline.dart and add:

dart
export 'timeline_page.dart';

Then open components/home/home_screen.dart and modify the _homePages variable as follows:

dart
...

import 'package:stream_agram/components/timeline/timeline_page.dart';

...

/// List of pages available from the home screen.
static const List<Widget> _homePages = <Widget>[
  TimelinePage(), // ADD THIS
  SearchPage(),
  ProfilePage(),
];

...

You can finally run your Instagram clone and you should see some activity on the timeline (after you follow someone that has already posted something). You’ll also be able to like posts and add comments and have the feed update automatically 🥳.

Instagram clone timeline screen

Adding More Instagram Likes and Comments

In Instagram, you'd expect to see all the comments for a given activity. For our Instagram clone, we'll create a comments screen that will expand and show all the comments related to an activity, and also allow users to add a comment to a comment, and a like to comment 😱. These are called child reactions.

This section is a combination of everything that you’ve learned so far!

Comment State

You will need a bit more complex state management for this section, as you need to keep track whether a user is adding a comment (reaction) to an activity, or if they’re adding a comment to a comment (child reaction).

Create components/comments/state/comment_state.dart and add:

dart
import 'package:flutter/material.dart';
import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';

import '../../../app/state/models/models.dart';

/// Indicates the type of comment that was made.
/// Can be:
/// - Activity comment
/// - Reaction comment
enum TypeOfComment {
  /// Comment on an activity
  activityComment,

  /// Comment on a reaction
  reactionComment,

This ChangeNotifier manages what type of comment it is - TypeOfComment:

  • A comment/like on an activity (reaction on a post).
  • A comment/like on a reaction (child reaction on a comment).

This will be easier to understand after looking at the UI code.

Expose this class in your barrel file, components/comments/state/state.dart:

dart
export 'comment_state.dart';

Create Your Instagram-like Comments Screen

Create components/comments/comment_screen.dart, and add:

dart
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:provider/provider.dart';
import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';

import '../../app/app.dart';
import '../app_widgets/app_widgets.dart';
import 'state/comment_state.dart';

/// Screen that shows all comments for a given post.
class CommentsScreen extends StatefulWidget {
  /// Creates a new [CommentsScreen].
  const CommentsScreen({
    Key? key,
    required this.enrichedActivity,

A few important things in this file:

  1. You expose a FocusNode and a CommentState using a MultiProvider, to all of the child widgets of the CommentScreen.
  2. You create a Stack with a private _CommentList and _CommentBox widgets.
  3. The _CommentList uses a ReactionListCore to display all the reactions for a specific Activity. Similar to earlier, you specify EnrichmentFlags.
  4. Make use of AnimatedSwitcher to nicely show and hide a popup that indicates if you’re replying to someone else’s comment.
  5. Handle liking a comment in the _handleFavorite method.
  6. Display the time since the comment using the Jiffy package.
  7. In handleSubmit decide whether a comment should be a reaction, or a child reaction, and add it accordingly using the BlocFeed.
Instagram Comments Screen

Create the components/comments/comments.dart barrel file:

dart
export 'comment_screen.dart';

Then update the TODOs in components/timeline/widgets/post_card.dart to navigate to the CommentsScreen:

dart
...

import '../../comments/comments.dart';

...

Padding(
  padding: iconPadding,
  child: TapFadeIcon(
    onTap: () {
            // ADD THIS
      final map = widget.enrichedActivity.actor!.data!;

            // AND THIS
      Navigator.of(context).push(

Following Your Own Instagram User Feed

You may have noticed this by yourself by now. But when you look at the timeline, the current logged-in user’s posts don’t show up?

Well, that is because you will need to tell Stream Feeds that you want to follow your own user feed.

Open app/state/app_state.dart and modify it as follows:

dart
...

/// Current user's [FlatFeed] with name 'user'.
///
/// This feed contains all of a user's personal posts.
FlatFeed get currentUserFeed => _client.flatFeed('user', user.id);

/// Current user's [FlatFeed] with name 'timeline'.
///
/// This contains all posts that a user has subscribed (followed) to.
FlatFeed get currentTimelineFeed => _client.flatFeed('timeline', user.id);

...

And modify the connect method to also follow your own feed:

dart
Future<bool> connect(DemoAppUser demoUser) async {
    final currentUser = await _client.setUser(
      User(id: demoUser.id),
      demoUser.token!,
      extraData: demoUser.data,
    );
    if (currentUser.data != null) {
      _streamagramUser = StreamagramUser.fromMap(currentUser.data!);
      await currentTimelineFeed.follow(currentUserFeed); // ADD THIS -> Follow own feed
      notifyListeners();
      return true;
    } else {
      return false;
    }
  }

Now, if you restart the application and connect as a user, you should see their posts also show up on the timeline.

Caching your Instagram Pages State

Something else that you’ve probably noticed at this point is that the pages are recreated each time you navigate in the application (using the PageView). Annoying, isn’t it?

Well, luckily that is an easy fix.

Open components/home/home_screen.dart and at the bottom of the file add this class:

dart
class _KeepAlivePage extends StatefulWidget {
  const _KeepAlivePage({
    Key? key,
    required this.child,
  }) : super(key: key);

  final Widget child;

  
  _KeepAlivePageState createState() => _KeepAlivePageState();
}

class _KeepAlivePageState extends State<_KeepAlivePage>
    with AutomaticKeepAliveClientMixin {
  

This uses the AutomaticKeepAliveClientMixin to keep the widget alive.

All you need to do now, is wrap all of the pages in a _KeepAlivePage widget. Update the _homePages widget to the following:

dart
/// List of pages available from the home screen.
static const List<Widget> _homePages = <Widget>[
  _KeepAlivePage(child: TimelinePage()),
  _KeepAlivePage(child: SearchPage()),
  _KeepAlivePage(child: ProfilePage()),
];

Do a Hot Restart, and you should see that the pages are kept alive as you navigate in the app.

What is next?

Your application is finished 🥳

Using Flutter and Stream Feeds you were able to create an Instagram clone. Congratulation for making it all the way to the end.

https://media.giphy.com/media/IwAZ6dvvvaTtdI8SD5/giphy.gif

As discussed throughout the tutorial, managing activity feeds can be quite complex, and Stream Feeds has a lot to offer that we have not explored in this tutorial.

The next step is to take what you’ve learned and create your own activity feeds application! Be sure to read up more on Aggregated and Notification feeds, and see if you can incorporate those.

We value your feedback and would be glad to take suggestions for content you would like to see in the future.

You can get the full source code on Github.

decorative lines
Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->