Build an Instagram-Like Android App Using Google Firebase

A lot of people use Instagram and you probably think it’s something very hard to build by your own. In the next tutorial I will show you that it actually can be quite easy. We will build an Instagram-like app (not the fully featured Instagram, but a solid starting point) using Google’s Firebase to keep our data.

Project Setup

As always, we will start from scratch, by creating a new Android Studio project. Let’s name it My Instagram App (inspiring, I know) and set the minimum SDK version to API 16. We will use an Empty Activity for starters, with the default MainActivity name for the Java class and activity_main for the layout.

Let’s also restrict the activity’s orientation to portrait in the AndroidManifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.dragosholban.myinstagramapp">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity"
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Check the code on GitHub.

Adding Firebase to Our Android Project

To use all the goodies Google has to ofer in the Firebase platform we need to add it to our project. First, go to the Firebase Console and create a new project. I’ll name mine  My Instagram App, like before.

Next, select Add Firebase to your Android app and follow the wizard. When you’re done, proceed to the next section.

Check the code on GitHub.

Using Firebase Auth to Authenticate Users

The first thing our users will need to do, is to authenticate themselves by entering an email and a password. Firebase offers this type of authentication (and much more) and even the interface for it, so we won’t have to code all the involved screens and dialogs. To use it in our app, first go to the Firebase Console, select our newly created project and, from the left menu, choose Authentication and click on the Set Up Sign-in Method button. From the list of Sign-in Providers, activate the Email/Password one.

Next, we need to add firebase-ui-auth to the app/build.gradle file:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'com.google.firebase:firebase-core:15.0.0'
    implementation 'com.firebaseui:firebase-ui-auth:3.3.1'
}

apply plugin: 'com.google.gms.google-services'

To start the authentication process, we will add a button in the activity_main.xml layout file:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout ...>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Sign In"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:onClick="signIn"/>

</android.support.constraint.ConstraintLayout>

This button will call the signIn method from our MainActivity class, that will start the Firebase Auth activity:

private static final int RC_SIGN_IN = 123;

public void signIn(View view) {
    startActivityForResult(
        // Get an instance of AuthUI based on the default app
        AuthUI.getInstance().createSignInIntentBuilder().build(),
        RC_SIGN_IN);
}

The authentication will return to our app in the onActivityResult method below. For now, we will only show a Toast with the user’s name or the error’s message if that’s the case.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode == RC_SIGN_IN) {
        IdpResponse response = IdpResponse.fromResultIntent(data);

        if (resultCode == RESULT_OK) {
            // Successfully signed in
            FirebaseUser fbUser = FirebaseAuth.getInstance().getCurrentUser();
            Toast.makeText(this, "Authenticated as " + fbUser.getDisplayName(), Toast.LENGTH_SHORT).show();
        } else {
            // Sign in failed, check response for error code
            if (response != null) {
                Toast.makeText(this, response.getError().getMessage(), Toast.LENGTH_SHORT).show();
            }
        }
    }
}

Run the app now to see how everything works. You even have a Recover password option without writing a single line of code for it!

Check the code on GitHub.

Creating the Feed Activity

After the user successfully authenticates to our app, he or she will need to be sent to the feed activity, to see the latest uploaded images. Let’s create a new, empty activity named FeedActivity. We will also add an uploadImage method, to be used later, when we will upload images to our app:

public class FeedActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_feed);
    }

    public void uploadImage(View view) {

    }
}

In the activity_feed.xml layout file, add a button at the bottom that will be used to upload new images to our app:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout...>

    <Button
        android:id="@+id/uploadButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:onClick="uploadImage"
        android:text="+"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</android.support.constraint.ConstraintLayout>

From the MainActivity, start this new one, if the user is already authenticated, or when the authentication succeeds:

public class MainActivity extends AppCompatActivity {

    // ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        FirebaseUser fbUser = FirebaseAuth.getInstance().getCurrentUser();
        if(fbUser != null) {
            // User already signed in
            // go to feed activity
            Intent intent = new Intent(this, FeedActivity.class);
            startActivity(intent);
        }
    }

    // ...

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == RC_SIGN_IN) {
            IdpResponse response = IdpResponse.fromResultIntent(data);

            if (resultCode == RESULT_OK) {
                // Successfully signed in
                // go to feed activity
                Intent intent = new Intent(this, FeedActivity.class);
                startActivity(intent);
            } else {
                // Sign in failed, check response for error code
                if (response != null) {
                    Toast.makeText(this, response.getError().getMessage(), Toast.LENGTH_SHORT).show();
                }
            }
        }
    }
}

Check the code on GitHub.

Uploading Images to Firebase Storage

We will use the Firebase Storage to keep the images uploaded by our users in the app. Like we did before for authentication, we need to go back to the Firebase Console and enable the Storage option:

We will keep the default security rules that allow all our authenticated users to read and write files to the provided storage.

Back in the FeedActivity, let’s add the code to load an image from the device’s gallery. You could also add code to take images using the camera, but, as this is out of this tutorial scope, we will skip this part. You can see how this can be done by following the How to Build a Jigsaw Puzzle Android Game tutorial published earlier.

Start by adding the READ_EXTERNAL_STORAGE permission to the AndroidManifest.xml file:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.dragosholban.myinstagramclone">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
        ... >
        ...
    </application>

</manifest>

The actual code from the FeedActivity, that requests and receives an image from the device’s storage, follows:

public class FeedActivity extends AppCompatActivity {

    static final int RC_PERMISSION_READ_EXTERNAL_STORAGE = 1;
    static final int RC_IMAGE_GALLERY = 2;

    // ...

    public void uploadImage(View view) {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.READ_EXTERNAL_STORAGE}, RC_PERMISSION_READ_EXTERNAL_STORAGE);
        } else {
            Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
            intent.setType("image/*");
            startActivityForResult(intent, RC_IMAGE_GALLERY);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == RC_PERMISSION_READ_EXTERNAL_STORAGE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
                intent.setType("image/*");
                startActivityForResult(intent, RC_IMAGE_GALLERY);
            }
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == RC_IMAGE_GALLERY && resultCode == RESULT_OK) {
            Uri uri = data.getData();
        }
    }
}

Once we get the image Uri, we can upload it to the Firebase storage. For this, we need yet another dependency added to our app’s build.gradle file:

dependencies {
    ...
    implementation 'com.google.firebase:firebase-core:15.0.0'
    implementation 'com.firebaseui:firebase-ui-auth:3.3.1'
    implementation 'com.google.firebase:firebase-storage:15.0.0'
}

Finally, in the FeedActivity, Firebase is ready to handle the upload with a few lines of code:

public class FeedActivity extends AppCompatActivity {

    FirebaseUser fbUser;

    // ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_feed);

        fbUser = FirebaseAuth.getInstance().getCurrentUser();
        if (fbUser == null) {
            finish();
        }
    }

    // ...

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == RC_IMAGE_GALLERY && resultCode == RESULT_OK) {
            Uri uri = data.getData();

            StorageReference storageRef = FirebaseStorage.getInstance().getReference();
            StorageReference imagesRef = storageRef.child("images");
            StorageReference userRef = imagesRef.child(fbUser.getUid());
            String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
            String filename = fbUser.getUid() + "_" + timeStamp;
            StorageReference fileRef = userRef.child(filename);

            UploadTask uploadTask = fileRef.putFile(uri);
            uploadTask.addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception exception) {
                    // Handle unsuccessful uploads
                    Toast.makeText(FeedActivity.this, "Upload failed!\n" + exception.getMessage(), Toast.LENGTH_LONG).show();
                }
            }).addOnSuccessListener(new OnSuccessListener<UploadTask.TaskSnapshot>() {
                @Override
                public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) {
                    Uri downloadUrl = taskSnapshot.getDownloadUrl();
                    Toast.makeText(FeedActivity.this, "Upload finished!", Toast.LENGTH_SHORT).show();
                }
            });
        }
    }
}

Run the app now, click on the button and select an image. After a few moments it will be uploaded to the Firebase servers and you can open the console to see the result:

Check the code on GitHub.

Saving Data to the Firebase Database

If we want to list the uploaded images in our feed activity, we need to keep track of all the uploaded files and the users who own them. For this we will use another Firebase service, the Realtime Database. To use it, activate it from the Firebase Console and choose the locked mode security rules when prompted.

To be able to save data, we need to change the security rules to allow our authenticate users to read and write to the database. Select the Rules tab and change the security rules to the following:

{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null"
  }
}

Now, add the necessary dependency to the app’s build.gradle file and sync the project:

dependencies {
    ...
    implementation 'com.google.firebase:firebase-core:15.0.0'
    implementation 'com.firebaseui:firebase-ui-auth:3.3.1'
    implementation 'com.google.firebase:firebase-storage:15.0.0'
    implementation 'com.google.firebase:firebase-database:15.0.0'
}

Before actually saving anything, we will ned to define some Java classes that will hold objects to be saved in the database (our models). First, we’ll define the User model:

@IgnoreExtraProperties
public class User {
    public String uid;
    public String displayName;
    public String token;

    public User() {
        // Default constructor required for calls to DataSnapshot.getValue(User.class)
    }

    public User(String uid, String displayName, String token) {
        this.uid = uid;
        this.displayName = displayName;
        this.token = token;
    }
}

Next, follows the Image class:

@IgnoreExtraProperties
public class Image {
    public String key;
    public String userId;
    public String downloadUrl;

    // these properties will not be saved to the database
    @Exclude
    public User user;

    @Exclude
    public int likes = 0;

    @Exclude
    public boolean hasLiked = false;

    @Exclude
    public String userLike;

    public Image() {
        // Default constructor required for calls to DataSnapshot.getValue(User.class)
    }

    public Image(String key, String userId, String downloadUrl) {
        this.key = key;
        this.userId = userId;
        this.downloadUrl = downloadUrl;
    }

    public void addLike() {
        this.likes++;
    }

    public void removeLike() {
        this.likes--;
    }
}

Finally, the Like class:

@IgnoreExtraProperties
public class Like {
    public String imageId;
    public String userId;

    public Like() {
        // Default constructor required for calls to DataSnapshot.getValue(Like.class)
    }

    public Like(String imageId, String userId) {
        this.imageId = imageId;
        this.userId = userId;
    }
}

We’ll start by saving our users to the database. This is done in the MainActivity class, after the user signs in:

public class MainActivity extends AppCompatActivity {

    DatabaseReference database;

    private static final int RC_SIGN_IN = 123;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        database = FirebaseDatabase.getInstance().getReference();

        FirebaseUser fbUser = FirebaseAuth.getInstance().getCurrentUser();
        if(fbUser != null) {
            // User already signed in

            // get the FCM token
            String token = FirebaseInstanceId.getInstance().getToken();

            // save the user info in the database to users/UID/
            // we'll use the UID as part of the path
            User user = new User(fbUser.getUid(), fbUser.getDisplayName(), token);
            database.child("users").child(user.uid).setValue(user);

            // go to feed activity
            Intent intent = new Intent(this, FeedActivity.class);
            startActivity(intent);
        }
    }

    // ...

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == RC_SIGN_IN) {
            IdpResponse response = IdpResponse.fromResultIntent(data);

            if (resultCode == RESULT_OK) {
                // Successfully signed in

                // get the Firebase user
                FirebaseUser fbUser = FirebaseAuth.getInstance().getCurrentUser();

                // get the FCM token
                String token = FirebaseInstanceId.getInstance().getToken();

                // save the user info in the database to users/UID/
                // we'll use the UID as part of the path
                User user = new User(fbUser.getUid(), fbUser.getDisplayName(), token);
                database.child("users").child(user.uid).setValue(user);

                // go to feed activity
                Intent intent = new Intent(this, FeedActivity.class);
                startActivity(intent);
            } else {
                // Sign in failed, check response for error code
                if (response != null) {
                    Toast.makeText(this, response.getError().getMessage(), Toast.LENGTH_SHORT).show();
                }
            }
        }
    }
}

Run the app now and sign in again using Firebase authentication. Then go to the console and check the database section. You should see a new user added:

Let’s continue by saving the uploaded images URLs to our database. This is done in our FeedActivity class, after an upload has finished (this part can be improved by cropping and resizing the images before uploading):

public class FeedActivity extends AppCompatActivity {

    FirebaseUser user;
    DatabaseReference database;

    // ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // ...

        database = FirebaseDatabase.getInstance().getReference();
    }

    // ...

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == RC_IMAGE_GALLERY && resultCode == RESULT_OK) {
            Uri uri = data.getData();

            // ...

            UploadTask uploadTask = fileRef.putFile(uri);
            uploadTask.addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception exception) {
                    // Handle unsuccessful uploads
                    Toast.makeText(FeedActivity.this, "Upload failed!\n" + exception.getMessage(), Toast.LENGTH_LONG).show();
                }
            }).addOnSuccessListener(new OnSuccessListener<UploadTask.TaskSnapshot>() {
                @Override
                public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) {
                    Uri downloadUrl = taskSnapshot.getDownloadUrl();
                    Toast.makeText(FeedActivity.this, "Upload finished!", Toast.LENGTH_SHORT).show();

                    // save image to database
                    String key = database.child("images").push().getKey();
                    Image image = new Image(key, fbUser.getUid(), downloadUrl.toString());
                    database.child("images").child(key).setValue(image);
                }
            });
        }
    }
}

That’s all! Run the app again and upload some images. Then check the database using the Firebase Console. You will see them added under the /images path:

Check the code on GitHub.

Loading Data from the Firebase Database

Our users can now add photos but there is no way they can see the images. Let’s fix this in this section.

First let’s add a RecyclerView to our activity_feed.xml layout file, to have a place to show images in our app:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout ...">

    <Button
        ... />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toTopOf="@+id/uploadButton"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

We will also need a new layout file for the elements loaded by the RecyclerView. Create a new layout file named image_view.xml with the following contents:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        app:layout_constraintBottom_toTopOf="@+id/textView2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:adjustViewBounds="true" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/likeButton"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/likeButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginStart="8dp"
        android:text="Like"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/textView2" />

</android.support.constraint.ConstraintLayout>

Next, we need an adapter for our RecyclerView, create a new ImageAdapter Java class for it:

public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> {
    private ArrayList<Image> mDataset;
    private FeedActivity mActivity;

    public static class ViewHolder extends RecyclerView.ViewHolder {
        public TextView mTextView;
        public ImageView mImageView;
        public Button mLikeButton;

        public ViewHolder(View v) {
            super(v);
            mTextView = v.findViewById(R.id.textView2);
            mImageView = v.findViewById(R.id.imageView);
            mLikeButton = v.findViewById(R.id.likeButton);
        }
    }

    public ImageAdapter(ArrayList<Image> myDataset, FeedActivity activity) {
        mDataset = myDataset;
        mActivity = activity;
    }

    // Create new views (invoked by the layout manager)
    @Override
    public ImageAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
                                                      int viewType) {
        // create a new view
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.image_view, parent, false);

        ViewHolder vh = new ViewHolder(v);
        return vh;
    }

    // Replace the contents of a view (invoked by the layout manager)
    @Override
    public void onBindViewHolder(ViewHolder holder, final int position) {
        final Image image = (Image) mDataset.get(position);
        if (image.user != null) {
            holder.mTextView.setText(image.user.displayName);
        }
        Picasso.get().load(image.downloadUrl).into(holder.mImageView);
    }

    // Return the size of your dataset (invoked by the layout manager)
    @Override
    public int getItemCount() {
        return mDataset.size();
    }

    public void addImage(Image image) {
        mDataset.add(0, image);
        notifyDataSetChanged();
    }
}

This uses the Picasso library to load images from the internet, so add it to the app’s build.gradle file:

dependencies {
    ...
    implementation 'com.google.firebase:firebase-core:15.0.0'
    implementation 'com.firebaseui:firebase-ui-auth:3.3.1'
    implementation 'com.google.firebase:firebase-storage:15.0.0'
    implementation 'com.google.firebase:firebase-database:15.0.0'
    implementation 'com.squareup.picasso:picasso:2.71828'
}

Finally, in the FeedActivity class, add the code to load data from the Firebase database:

public class FeedActivity extends AppCompatActivity {

    FirebaseUser user;
    DatabaseReference database;
    RecyclerView recyclerView;
    RecyclerView.LayoutManager mLayoutManager;
    ImageAdapter mAdapter;
    ArrayList<Image> images = new ArrayList<>();

    // ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ...

        database = FirebaseDatabase.getInstance().getReference();

        // Setup the RecyclerView
        recyclerView = findViewById(R.id.recyclerView);
        mLayoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(mLayoutManager);
        mAdapter = new ImageAdapter(images, this);
        recyclerView.setAdapter(mAdapter);

        // Get the latest 100 images
        Query imagesQuery = database.child("images").orderByKey().limitToFirst(100);
        imagesQuery.addChildEventListener(new ChildEventListener() {
            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String s) {

                // A new image has been added, add it to the displayed list
                final Image image = dataSnapshot.getValue(Image.class);

                // get the image user
                database.child("users/" + image.userId).addListenerForSingleValueEvent(new ValueEventListener() {
                    @Override
                    public void onDataChange(DataSnapshot dataSnapshot) {
                        User user = dataSnapshot.getValue(User.class);
                        image.user = user;
                        mAdapter.notifyDataSetChanged();
                    }

                    @Override
                    public void onCancelled(DatabaseError databaseError) {

                    }
                });

                mAdapter.addImage(image);
            }

            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, String s) {

            }

            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {

            }

            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, String s) {

            }

            @Override
            public void onCancelled(DatabaseError databaseError) {

            }
        });
    }

    // ...
}

Run the app again and notice how the latest images are displayed in the feed. You can even use another device to upload new images and see how the feed is updated in realtime, no refresh needed. That’s something not even the original Instagram app has!

Check the code on GitHub.

Implementing the Like Functionality

What would be Instagram (or any other social app) without the “like” button? We definitely have to let the user express their appreciation for the images they see, so let’s implement the “like” functionality in our app too. You noticed that we already added the Like button and the Like model class, so all we need is to make it work.

First, we’ll add the code to save new likes to the database, in the FeedActivity class:

public void setLiked(Image image) {
    if(!image.hasLiked) {
        // add new Like
        image.hasLiked = true;
        Like like = new Like(image.key, fbUser.getUid());
        String key = database.child("likes").push().getKey();
        database.child("likes").child(key).setValue(like);
        image.userLike = key;
    } else {
        // remove Like
        image.hasLiked = false;
        if (image.userLike != null) {
            database.child("likes").child(image.userLike).removeValue();
        }
    }
}

This method will be called each time the user clicks on the Like button. We’ll add a listener for this in our ImageAdapter:

// Replace the contents of a view (invoked by the layout manager)
@Override
public void onBindViewHolder(ViewHolder holder, final int position) {
    final Image image = mDataset.get(position);
    if (image.user != null) {
        holder.mTextView.setText(image.user.displayName);
    }
    Picasso.get().load(image.downloadUrl).into(holder.mImageView);

    holder.mLikeButton.setText("Like (" + image.likes + ")");
    if(image.hasLiked) {
        holder.mLikeButton.setBackgroundColor(mActivity.getResources().getColor(R.color.colorAccent));
    } else {
        holder.mLikeButton.setBackgroundColor(mActivity.getResources().getColor(R.color.colorPrimary));
    }
    holder.mLikeButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            mActivity.setLiked(image);
        }
    });
}

The last thing to do is to load the likes every time we receive a new image from the Firebase database:

// Get the latest 100 images
Query imagesQuery = database.child("images").orderByKey().limitToFirst(100);
imagesQuery.addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String s) {

        // A new image has been added, add it to the displayed list
        final Image image = dataSnapshot.getValue(Image.class);

        // get the image user
        // ...

        // get image likes
        Query likesQuery = database.child("likes").orderByChild("imageId").equalTo(image.key);
        likesQuery.addChildEventListener(new ChildEventListener() {
            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String s) {
                Like like = dataSnapshot.getValue(Like.class);
                image.addLike();
                if(like.userId.equals(fbUser.getUid())) {
                    image.hasLiked = true;
                    image.userLike = dataSnapshot.getKey();
                }
                mAdapter.notifyDataSetChanged();
            }

            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, String s) {

            }

            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {
                Like like = dataSnapshot.getValue(Like.class);
                image.removeLike();
                if(like.userId.equals(fbUser.getUid())) {
                    image.hasLiked = false;
                    image.userLike = null;
                }
                mAdapter.notifyDataSetChanged();
            }

            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, String s) {

            }

            @Override
            public void onCancelled(DatabaseError databaseError) {

            }
        });

        mAdapter.addImage(image);
    }

    // ...
});

Run the app on two or more devices, sign in with different users, upload some images and start clicking on that Like button! You will see everything updating in realtime, on all the devices. How cool is that?

Check the code on GitHub.

Sending Push Notifications Using Firebase Functions

The last thing we’ll do for our Instagram-like app, is to send a push notification whenever somebody “likes” an image, to the user that posted that awesome image. We’ll use the Firebase Functions service that allows you to write and run code in the cloud, without having to manage or worry about any backend server at all.

Go to the Firebase Console one more time and click on the Get Started button from the Functions section. Follow the instructions to get you started. We’ll use Javascript to write our code that will watch the database for new likes and send a push notification every time a new like is created.

In the created Firebase project, open the functions/index.js file and add the following code to it:

const functions = require('firebase-functions');
const admin = require('firebase-admin');

admin.initializeApp();

// Listens for new likes added to /likes/:pushId and sends a push notification
exports.sendLikeNotification = functions.database.ref('/likes/{pushId}')
    .onCreate((snapshot, context) => {
        // Grab the current value of what was written to the Realtime Database.
        const like = snapshot.val();

        // Get the image liked
        admin.database().ref(`/images/${like.imageId}`).once("value", (snapshot) => {
            const image = snapshot.val();

            // get the user that liked the image
            const getUserLikedPromise = admin.database().ref(`/users/${like.userId}`).once('value');
            // get the user that posted the image
            const getUserImagePromise = admin.database().ref(`/users/${image.userId}`).once('value');

            return Promise.all([getUserLikedPromise, getUserImagePromise]).then(results => {
                const userLiked = results[0].val();
                const userImage = results[1].val();

                if (userLiked.uid === userImage.uid) {
                    return console.log("User liked his own image.");
                }

                if (userImage.token) {
                    // Notification details.
                    const payload = {
                        notification: {
                            title: 'You have a new like!',
                            body: `${userLiked.displayName} liked your image.`
                        }
                    };

                    console.log("Will send notification to user: " + userImage.uid);

                    return admin.messaging().sendToDevice(userImage.token, payload);
                }

                return console.log("No device token for user: " + user.uid);
            });
        }, (errorObject) => {
            return console.log("The read failed: " + errorObject.code);
        });
    });

Now deploy the code using the firebase deploy command (run this twice if it gives you an unexpected error the first time).

Back to our Android project, we need to make a few more changes so the device token will be updated to the Firebase database every time it changes.

To be able to receive push notifications, we need to add the FCM dependency to the app’s build.gradle file:

dependencies {
    // ...
    implementation 'com.google.firebase:firebase-core:15.0.0'
    implementation 'com.firebaseui:firebase-ui-auth:3.3.1'
    implementation 'com.google.firebase:firebase-storage:15.0.0'
    implementation 'com.google.firebase:firebase-database:15.0.0'
    implementation 'com.squareup.picasso:picasso:2.71828'
    implementation 'com.google.firebase:firebase-messaging:15.0.2'
}

In the AndroidManifest.xml file, add the following service:

<?xml version="1.0" encoding="utf-8"?>
<manifest...>

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application
        ...>
        ...
        <service
            android:name=".MyFirebaseInstanceIDService">
            <intent-filter>
                <action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
            </intent-filter>
        </service>
    </application>

</manifest>

Now, create a new Java class named MyFirebaseInstanceIDService with the following contents:

public class MyFirebaseInstanceIDService extends FirebaseInstanceIdService {

    /**
     * Called if InstanceID token is updated. This may occur if the security of
     * the previous token had been compromised. Note that this is called when the InstanceID token
     * is initially generated so this is where you would retrieve the token.
     */
    // [START refresh_token]
    @Override
    public void onTokenRefresh() {
        // Get updated InstanceID token.
        String refreshedToken = FirebaseInstanceId.getInstance().getToken();

        // If you want to send messages to this application instance or
        // manage this apps subscriptions on the server side, send the
        // Instance ID token to your app server.
        sendRegistrationToServer(refreshedToken);
    }
    // [END refresh_token]

    /**
     * Persist token to third-party servers.
     *
     * Modify this method to associate the user's FCM InstanceID token with any server-side account
     * maintained by your application.
     *
     * @param token The new token.
     */
    private void sendRegistrationToServer(String token) {
        FirebaseUser firebaseUser = FirebaseAuth.getInstance().getCurrentUser();
        if (firebaseUser != null) {
            User user = new User(firebaseUser.getUid(), firebaseUser.getDisplayName(), token);
            DatabaseReference database = FirebaseDatabase.getInstance().getReference();
            database.child("users").child(user.uid).setValue(user);
        }
    }
}

This will update the token when needed so we have the latest information in our database and be able to send push notifications.

Now start using the app again. Users will start to receive push notifications when somebody “likes” one of their uploaded images.

Check the code on GitHub.

That’s it! We now have a starting point for the next Instagram-like app. Of course, there are many things that can be, and should be, done to have a fully featured app, but now you should be able to add as many features as you want. Please let me know in the comments what else you added and leave a link to your published app if you have one.

I hope you enjoyed building this and learned a lot along the way. Please let me know in the comments how it was for you, if you found any problems or if you have any improvements to suggest. Also, if this tutorial helped you build some other Android apps, please add some links in the comments and tell all of us about them.

You can get the final code from GitHub, in case something goes terribly wrong and you don’t manage to fix it. See you at the next Android tutorial!

12 Replies to “Build an Instagram-Like Android App Using Google Firebase”

  1. Brilliant… Just what i needed … A way of adding the bottom navigation to activities … would be a greate way to suppliment ( helping a long way) on this tutorial.

  2. thanks for the great guide that you have here.

    wonder if i can make the activity_feed only display current user or selected user’s images only.

    1. hey i noticed the pics are gone when you scroll. how do you make it stay in place after they’re loaded?

      1. found the problem. it’s because the image from my camera is too big. probably too big for the cache? idk about the technical side of things.

        when i add bitmap compression and display the compressed image, they stay in place.

        also added another upload task, gallery access, ditched the like function but made a delete button instead etc etc.

        gosh, im learning so much from this. thanks man.

        1. Hi, I’m glad you find this useful. If you end up building something, please leave us a link in the comments so we can check it out. Thanks!

  3. Great dude You are awesome !

    And i have a question, Since we using Firebase app.. how much it cost for Storage ? (If i publish a real apps)
    I mean, is Firebase free storage is enough or what?

    Many thanks to you of you answer this

  4. Hi Dragos, this is an excellent project, very well explained and easy to follow. Thank you!!
    A few things has changed: FirebaseInstanceId.getInstance().getToken() and taskSnapshot.getDownloadUrl() have been deprecated.
    The User class exists already inside the FireBase library and gave me an error. I replaced the class with another name (DBUser) and worked perfectly.
    Thank you!

  5. Hi Dragos, Thank you so much for this excellent program. I have a question though, I do not see my notification on my device, although in Firebase, it says it sent the notification for user id. My device notification is set to on. Any thoughts please?
    Trino

Leave a Reply

Your email address will not be published. Required fields are marked *