A child is an island of curiosity surrounded by a sea of question marks. So they say. Since my childhood, I have always had a deep fasicination with astronomy. Think celestial bodies like stars,planets,galaxies etc. Then in the year 2009 NASA sent out the Kepler space probe to discover Earth-size planets orbiting other stars. Now retired, the spacecraft has discovered and catalogued thousands of planets, alien planets. And from what we’ve seen,this Universe hosts some really weird planets. So recently I have decided to create an app that will allow me to catalogue these alien planets.

Here is the demo:

There comes the Alien Planets App. This an app meant to teach students some important technologies like:

No. Technology Value
1. Programming Languages Kotlin, Java
2. Design Pattern MVVM(Model View ViewModel)
3. Database Firebase Realtime Database,Firebase Cloud Storage
4. Offline-First Approach Using firebase caching.
5. Runtime Permssions How to handle runtime permissions in a flexible way
6. FULL App Learn how to create a full android app you can use as a template.

Here are the concepts and technologies you will learn from this app in more detail:

(a). Programming Languages

There are two apps. One written in Kotlin and the other in Java. Both point to the same firebase database location. Both apps are similar in their functionalities. If you are pioneering in Kotlin, then a great way to learn is to compare the two projects. It may take you time to get used to Kotlin idioms but you’ll start seeing why this language is highly rated.

(b). Design Pattern

We want to teach how to design an app using Model View ViewModel. You will see how neat and organized the app is. Data is separated from UI, UI is separated from logic. Logic is defined once and can be re-used whenever needed. We end up with an app easy to understand and maintain. An app that you can use as a template for creating other apps.

 

(c). Firebase Realtime Database

This is a great app to learn how to POST, UPDATE, RETRIEVE and DELETE agains Firebase Realtime Database. We show how to perform all these operations.

(d). Firebase Cloud Storage

This project will teach how to efficiently UPLOAD,UPDATE, RETRIEVE and DELETE both images and text. This is super important as most apps need this capability. However doing it in a clean and reliable way is not easy, no wonder it’s difficult to find projects online that give you high quality code that does all these operations. We’ve created one class that you can from now on use to achieve these operations. You can just copy it to your project, expose it to your UI via ViewModel and you are good to go.

(e). Capture,Pick Images

We want to teach you how to capture images from camera, or select from gallery or from file picker and upload them alongside text to firebase. We do this in the easiest way possible.

(f). Runtime Permissions

We also show how to handle runtime permissions in the easiest way and most flexible way we’ve found. This is through the library Dexter, a mature runtime permissions library for android.

(g). Simple Search

We see how to perform a simple local search of our firebase data. We also highlight the search results in our recyclerview.

Ok, let’s start.

1. Setting up Gradle

A sizeable android app or app with significant complexity will have a couple of dependencies. Dependencies are libraries, third party or otherwise, which add functionalities to our apps. They save us time. We don’t have to re-implement things that have already been implemented and packaged by other smart developers. Dependencies as well as the compilation and building of the app is handled by Gradle, android studio’s build system.

(a). Enabling Kotlin

Yeah we are using Kotlin so we’ve gotta add it as a dependency. In our build.gradle’s dependencies closure,we will add:

    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.0.2'

We will also need to apply two plugins at the top our app level’s build.gradle:

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

Then in the root level build.gradle, we need to add the following classpath under the dependencies closure:

        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

In the same root build.gradle file, you need to specify the kotlin version:

    ext.kotlin_version = '1.3.61'

You do this in the same buildscript closure:

buildscript {
    ext.kotlin_version = '1.3.61'
    //continue

(b). Enabling Jitpack

Some third party libraries will be hosted in jcenter which android studio uses as the default repository. However some will be fetched from jitpack. Thus we need to register jitpack also as a repository. Then if gradle fails to find any of our third part dependencies from maven, it will search in jitpack.

allprojects {
    repositories {
        google()
        jcenter()
        maven { url "https://jitpack.io" }
    }
}

(c). Enabling Java8

With the Java8, we will enable Java8 features. Mostly we are interested in lambda expressions as we use them extensively instead of verbose annonymous classes for our event handlers.
To enable Java8, go to the app level build.gradle and inside the android closure:

android{
    //blah blah .....

    compileOptions {
        targetCompatibility 1.8
        sourceCompatibility 1.8
    }
}

Here is an example of us using lambda expression to open a detail activity:

        holder.setItemClickListener(pos -> Utils.sendPlanetToActivity(c, DetailActivity.class);

(d). Enabling Data Binding

We intend to utilize data binding in our Java project. This eliminates the boilerplate findViewById() calls. We can simple bind our model classes directly to textviews or edittexts. We can still access the widgets when we need to do something custom to them.

To enable data binding, all you need to do is add the following in your android closure in the app level build.gradle:

    dataBinding {
        enabled = true
    }

How to enable and Use androidx Libraries

AndroidX package structure to make it clearer which packages are bundled with the Android operating system, and which are packaged with your app’s APK. We will be using androidx libraries.

First go to gradle.properties file and add the following

android.useAndroidX=true

Then add androidx libraries:

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.recyclerview:recyclerview:1.0.0'
    implementation 'com.google.android.material:material:1.0.0'
    implementation 'androidx.cardview:cardview:1.0.0'

How to Install and use Lifecycle Components

These components include the LiveData and ViewModel classes that are vital to the MVVM design pattern we are using. Let’s install them:

    // Lifecycle components
    implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
    annotationProcessor 'androidx.lifecycle:lifecycle-compiler:2.1.0'

How to Install Firebase

Let’s see how to install firebase realtime database and firebase cloud storage. Start by adding the following in your app level build.gradle file:

    //Firebase dependencies
    implementation 'com.google.firebase:firebase-database:19.1.0'
    implementation 'com.google.firebase:firebase-storage:19.1.0'

Below the dependencies closure, apply our google services plugin:

dependencies
    //Your dependencies
}

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

If you forget the above then you will be getting FirebaseApp not initialized errors at runtime.

Now go to your root-level build.gradle:

buildscript {
    repositories {
        //repositories here

    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.2'
        //add google services class path
        classpath 'com.google.gms:google-services:4.3.2'
    }
}

And add the google services classpath as specified above. Versions may differ.

Now you need to create a firebase app in the firebase console. Luckily, you can do this easily using android studio.

From android studio menu bar, choose Tools –> Firebase

A Firebase Asssitant will be shown like below, so choose Firebase Realtime Database.

Then Click Connect To Firebase button.

A Firebase Connection Dialog will be shown. If you don’t have a project create a new one, otherwise you can use an existing one.

Once you have your project created, go and enable public writing so that your app can write to and read from your database without any authentication. Go to your firebase console in the browser, and under the Firebase realtime database, under the Rules tab, allow read and writes as below:

To a Programmer, What is a Planet?

To a physicist, a planet is an astronomical body orbiting a star or stellar remnant that is massive enough to be rounded by its own gravity, is not massive enough to cause thermonuclear fusion, and has cleared its neighbouring region of planetesimals. That’s according to Wikipedia.

We, programmers are however like gods. To us a planet is anything we want it to be. We don’t have a fixed definition of a planet. The definition of a planet to us is subjective. You can give it any definition you want. We don’t use English. We use languages like Kotlin and Java.’

Here is my definition of a planet in Kotlin:

class Planet : Serializable {
    var name: String? = ""
    var description: String? = ""
    var galaxy: String? = ""
    var type: String? = ""
    var dod: String? = ""
    var imageURL: String? = ""
    @get:Exclude
    @set:Exclude
    var key: String? = ""

    override fun toString(): String {
        return name!!
    }

}

And here is how I would define it in Java:

public class Planet implements Serializable {

    private String name,description,galaxy,type,dod, imageURL,key;

    public Planet() {
        //empty constructor needed
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }

    public String getGalaxy() {
        return galaxy;
    }
    public void setGalaxy(String galaxy) {
        this.galaxy = galaxy;
    }

    public String getType() {
        return type;
    }
    public void setType(String type) {
        this.type = type;
    }

    public String getDod() {
        return dod;
    }
    public void setDod(String dod) {
        this.dod = dod;
    }

    public String getImageURL() {
        return imageURL;
    }
    public void setImageURL(String imageURL) {
        this.imageURL = imageURL;
    }

    @Override
    public String toString() {
        return getName();
    }
    @Exclude
    public String getKey() {
        return key;
    }
    @Exclude
    public void setKey(String key) {
        this.key = key;
    }

}

Aha! Much more verbose in java but more or less similar to the Kotlin definition. Beware of that definition in Kotlin. It looks simple but it’s design will be fundamental in solving serious time-consuming problem, the infamous NullPointerExceptions. The java one won’t. However it can be made to by creating a method that checks for null and returns a default value.

In the Kotlin data object, we’ve used what is called an elvis operator. We are assigning default blank values to to attributes. Thus later if we can do things like this:

            nameTV.text = receivedPlanet!!.name
            descriptionTV.text = receivedPlanet!!.description
            galaxyTV.text = receivedPlanet!!.galaxy

We are fearlessly retrieving a name from our non-null receivedPlanet to show it in a textview inside our detail page. As long as our received planet is not null, there is nothing to fear since even if the name was null, a default blank value will be returned.

Representing a Firebase DB Call/Operation

We will be making calls agains our Firebase realtime database. We want to define a data object class to represent these calls or requests. Think about it, what would such a call involve? Things are important to us. Well here are three things I can think of now:

  1. Status – The status of that operation. Is it in progress or completed. If completed, did it complete successfully or with an error.
  2. Message – I would need some message that I can show to my user. Whether it is an error message, a success message or even a progress message.
  3. Data – Yeah, some list of data, if the operation involves downloading data. If there is no data then I simply have an empty list.

Here is the class to define these programmatically:

class RequestCall {
    //A single request will have the following attributes
    var status = 0
    var message: String = "NO MESSAGE"
    var planets: List<Planet> = ArrayList()

}

And in Java.

public class RequestCall {
    //A single request will have the following attributes
    private int status;
    private String message;
    private List<Planet> planets;
    //And the getters and setters...
}

Again we’ve initialized our attributes in kotlin with default values.

How to Upload Only Text to Firebase

The overall aim is to UPLOAD UPDATE RETRIEVE and DELETE images and text to and from firebase realtime database and firebase cloud storage. However, as we promised earlier on, we want to decouple stuff as much as possible. So we start by creating amethod that will upload us only text. Then we’ll use it after we’ve uploaded image and now we want to save the text details to our realtime database. By the way this method, like any other CRUD method we will define will be placed in our repository class.

    private fun uploadOnlyText(planet: Planet): MutableLiveData<RequestCall> {
        val r = RequestCall()
        r.status = IN_PROGRESS
        r.message = "Performing Validation"
        mutableLiveData.value = r
        return run {
            r.status = IN_PROGRESS
            r.message = "Inserting Planet Text...Please wait.."
            mutableLiveData.postValue(r)
            //push data to FirebaseDatabase. Table or Child called Planet will be created.
            DB.push().setValue(planet)
                .addOnCompleteListener { task ->
                    if (task.isSuccessful) {
                        r.status = SUCCEEDED
                        if (planet.imageURL != null && planet.imageURL!!.isNotEmpty()) {
                            r.message = "Congrats! Both Text and Image Inserted Successfully"
                        } else {
                            r.message =
                                "Text Successfully Saved. However Image was not Uploaded."
                        }
                    } else {
                        r.status = FAILED
                        if (planet.imageURL != null && planet.imageURL!!.isNotEmpty()) {
                            r.message =
                                "Unfortunaletly Text Was Not Inserted. However Image was uploaded. ERROR: " + task.exception!!.message
                        } else {
                            r.message =
                                "Unfortunaletly! Both Text and Image Were Not Inserted: " + task.exception!!.message
                        }
                    }
                    mutableLiveData.postValue(r)
                }
            mutableLiveData
        }
    }

The MutableLiveData is allowing us to update our UI with progress message all the way until completion. The magic method is actually simple: DB.push().setValue(planet).Yeah, that alone is enough to push data to firebase. But in a real world app you need to show progress,sucess and error messages. You also need to check null and empty values. You also need to design a proper app by decoupling logic from the UI.

Here is how to do the same thing in Java:

    public MutableLiveData<RequestCall> uploadOnlyText(Planet planet) {
        if(mutableLiveData == null){
            mutableLiveData = new MutableLiveData<>();
        }
        RequestCall r = new RequestCall();
        r.setStatus(Constants.IN_PROGRESS);
        r.setMessage("Performing Validation");
        mutableLiveData.setValue(r);

        if (planet == null) {
            r.setStatus(FAILED);
            r.setMessage("VALIDATION FAILED: Null Planet Received");
            mutableLiveData.postValue(r);
            return mutableLiveData;
        } else {

            r.setStatus(Constants.IN_PROGRESS);
            r.setMessage("Inserting Planet Text...Please wait..");
            mutableLiveData.postValue(r);

            //push data to FirebaseDatabase. Table or Child called Planet will be created.
            DB.push().setValue(planet).addOnCompleteListener(new OnCompleteListener<Void>() {
                @Override
                public void onComplete(@NonNull Task<Void> task) {
                    if (task.isSuccessful()) {
                        r.setStatus(SUCCEEDED);
                        if(planet.getImageURL() != null && !planet.getImageURL().isEmpty()){
                            r.setMessage("Congrats! Both Text and Image Inserted Successfully");
                        }else{
                            r.setMessage("Text Successfully Saved. However Image was not Uploaded.");
                        }
                    } else {
                        r.setStatus(FAILED);
                        if(planet.getImageURL() != null && !planet.getImageURL().isEmpty()){
                            r.setMessage("Unfortunaletly Text Was Not Inserted. However Image was uploaded. ERROR: "+task.getException().getMessage());
                        }else{
                            r.setMessage("Unfortunaletly! Both Text and Image Were Not Inserted: "+task.getException().getMessage());
                        }
                    }
                    mutableLiveData.postValue(r);
                }
            });
            return mutableLiveData;
        }
    }

In the Kotlin code, we don’t need to check for null Planet. The method will force the caller to pass a safe value. Also take note that both methods have been declared private, signalling that we are using them just within the current class. However you can make them public, then update the messages and call them from elsewhere independently.

How to Upload Image and Text to Firebase

Let’s now see how to upload both images and text. To upload image and text, you need the planet as well as image uri.

In Kotlin here is how we upload both images and text cleanly and safely, while sending progress messages to the UI:

    fun uploadImageText(planet: Planet, mImageUri: Uri): MutableLiveData<RequestCall> {
        mutableLiveData = MutableLiveData()
        val r = RequestCall()
        r.status = IN_PROGRESS
        r.message = "Uploading Planet"
        mutableLiveData.value = r
        return (run {
            if (mUploadTask != null && mUploadTask!!.isInProgress) {
                r.status = IN_PROGRESS
                r.message = "An Upload is already in Progress.Please be patient"
                mutableLiveData.postValue(r)
                return mutableLiveData
            }
            r.status = IN_PROGRESS
            r.message = "Now Uploading Image...Please wait.."
            mutableLiveData.postValue(r)
            val imageRef: StorageReference = IMAGES_DB.child(mImageUri.lastPathSegment!!)
            mUploadTask = imageRef.putFile(mImageUri)
            (mUploadTask as UploadTask).continueWithTask(
                (Continuation { task: Task<UploadTask.TaskSnapshot?> ->
                    if (!task.isSuccessful) {
                        r.status = IN_PROGRESS
                        r.message = "ERROR ENCOUNTERED: " + task.exception!!.message
                        mutableLiveData.postValue(r)
                    }
                    imageRef.downloadUrl
                } as Continuation<UploadTask.TaskSnapshot?, Task<Uri>>)
            )
                .addOnCompleteListener { task: Task<Uri?> ->
                    if (task.isSuccessful) {
                        if (task.isSuccessful) {
                            val downloadUri = task.result
                            val url =downloadUri!!.toString()
                            planet.imageURL = url
                            r.status =
                                IN_PROGRESS
                            r.message =
                                "Image Upload successful. We are now saving text details"
                        } else {
                            r.status =
                                IN_PROGRESS
                            r.message =
                                "Unfortunately Image Could not be uploaded: FULL DETAILS: " + task.exception!!.message
                        }
                        mutableLiveData.postValue(r)
                        mutableLiveData = uploadOnlyText(planet)
                    } else {
                        r.status = FAILED
                        r.message =
                            "Unfortunately Image Could not be uploaded: FULL DETAILS: " + task.exception!!.message
                        mutableLiveData.postValue(r)
                    }
                }
            mutableLiveData
        })
    }

We specify the location where images are uploaded using this line:

            val imageRef: StorageReference = IMAGES_DB.child(mImageUri.lastPathSegment!!)

IMAGES_DB is defined in our Constants class, it is a StorageReference object:

    @JvmField
    val IMAGES_DB = FirebaseStorage.getInstance().getReference("alien_planets_images_db")

If you think the method looks long, it actually isn’t. Most of the lines are simple statements responsible for sending progress to the UI with appropriate and helpful messages to the user. Moreover we are doing it safely and cleanly using clean architecture. We could easily condense this code by half but it wouldn’t be of appropriate quality.

And by the way, it would be much longer. Note that we are re-using the earlier defined uploadOnlyText() method:

  mutableLiveData = uploadOnlyText(planet)

How to Update Only Text

Well this method is important and we’ll re-use it twice. First if the user doesn’t change the image, then this is the method that will be invoked. There is no reason to re-upload the same image again if the user clicks update button. So we’ll check the selected image uri, if it is null, then we know the user hasn’t captured or selected an image. In that case we upload only text using the following code:

    fun updateOnlyText(planet: Planet, mLiveData: MutableLiveData<RequestCall>): MutableLiveData<RequestCall> {
        val r = RequestCall()
        r.status = IN_PROGRESS
        r.message = "Performing Update Validation"
        mLiveData.value = r
        return run {
            r.status = IN_PROGRESS
            r.message = "Updating Planet...Please wait.."
            mLiveData.postValue(r)
            val finalLiveData: MutableLiveData<RequestCall> = mLiveData
            DB.child(planet.key!!).setValue(planet)
                .addOnCompleteListener { task ->
                    if (task.isSuccessful) {
                        r.status = SUCCEEDED
                        if (planet.imageURL != null && planet.imageURL!!.isNotEmpty()) {
                            r.message = "Congrats! Both Text and Image Updated Successfully"
                        } else {
                            r.message =
                                "Text Successfully Updated. However Image was not Uploaded."
                        }
                    } else {
                        r.status = FAILED
                        if (planet.imageURL != null && planet.imageURL!!.isNotEmpty()) {
                            r.message =
                                "Unfortunaletly Text Was Not Updated. However Image was uploaded. ERROR: " + task.exception!!.message
                        } else {
                            r.message =
                                "Unfortunaletly! Both Text and Image Were Not Updated: " + task.exception!!.message
                        }
                    }
                    finalLiveData.postValue(r)
                }
            finalLiveData
        }
    }

The magic part is this: DB.child(planet.key!!).setValue(planet). It is enough to do the update. The key will specify the node we want to update. The planet is new planet to replace the older one. Note that the key will remain the same.

We are using the method also when updating images and text. In that case once the image has been replaced, then we call it to update the text part.

Here is the equivalent method in Java:


    public MutableLiveData<RequestCall> updateOnlyText(Planet planet, MutableLiveData<RequestCall> mLiveData) {
        if(mLiveData == null){
            mLiveData = new MutableLiveData<>();
        }
        RequestCall r = new RequestCall();
        r.setStatus(Constants.IN_PROGRESS);
        r.setMessage("Performing Update Validation");
        mLiveData.setValue(r);

        if (planet == null) {
            r.setStatus(FAILED);
            r.setMessage("VALIDATION FAILED: Null Planet Received");
            mLiveData.setValue(r);
            return mLiveData;
        } else {

            r.setStatus(Constants.IN_PROGRESS);
            r.setMessage("Updating Planet...Please wait..");
            mLiveData.postValue(r);

            MutableLiveData<RequestCall> finalLiveData = mLiveData;
            DB.child(planet.getKey()).setValue(planet).addOnCompleteListener(new OnCompleteListener<Void>() {
                @Override
                public void onComplete(@NonNull Task<Void> task) {
                    if (task.isSuccessful()) {
                        r.setStatus(SUCCEEDED);
                        if(planet.getImageURL() != null && !planet.getImageURL().isEmpty()){
                            r.setMessage("Congrats! Both Text and Image Updated Successfully");
                        }else{
                            r.setMessage("Text Successfully Updated. However Image was not Uploaded.");
                        }
                    } else {
                        r.setStatus(FAILED);
                        if(planet.getImageURL() != null && !planet.getImageURL().isEmpty()){
                            r.setMessage("Unfortunaletly Text Was Not Updated. However Image was uploaded. ERROR: "+task.getException().getMessage());
                        }else{
                            r.setMessage("Unfortunaletly! Both Text and Image Were Not Updated: "+task.getException().getMessage());
                        }
                    }
                    finalLiveData.postValue(r);
                }
            });
            return finalLiveData;
        }

    }

How to Update Both Images and Text

Let’s see how to update both images and text. We want the replacement image, once uploaded in the cloud storage,its url to be saved alongside other texts in firebase realtime database.Here is how we update the images and text in Kotlin:

    fun updateImageText(planet: Planet, mImageUri: Uri): MutableLiveData<RequestCall> {
        mutableLiveData = MutableLiveData()
        val r = RequestCall()
        r.status = IN_PROGRESS
        r.message = "Performing Validation"
        mutableLiveData.value = r
        return (run {
            if (mUploadTask != null && mUploadTask!!.isInProgress) {
                r.status = IN_PROGRESS
                r.message = "An Upload is already in Progress.Please be patient"
                mutableLiveData.postValue(r)
                return mutableLiveData as MutableLiveData<RequestCall>
            }
            r.status = IN_PROGRESS
            r.message = "Now Uploading Image...Please wait.."
            mutableLiveData.postValue(r)
            val imageRef: StorageReference = IMAGES_DB.child(mImageUri.lastPathSegment!!)
            mUploadTask = imageRef.putFile(mImageUri)
            (mUploadTask as UploadTask).continueWithTask(
                (Continuation { task: Task<UploadTask.TaskSnapshot?> ->
                    if (!task.isSuccessful) {
                        r.status = IN_PROGRESS
                        r.message = "ERROR ENCOUNTERED: " + task.exception!!.message
                    }
                    imageRef.downloadUrl
                } as Continuation<UploadTask.TaskSnapshot?, Task<Uri>>)
            )
                .addOnCompleteListener { task: Task<Uri?> ->
                    if (task.isSuccessful) {
                        val downloadUri = task.result
                        val url = downloadUri.toString()
                        planet.imageURL = url
                        r.status = IN_PROGRESS
                        r.message = "Image Upload successful. We are now saving text details"
                    } else {
                        r.status = FAILED
                        r.message =
                            "Unfortunately Image Could not be uploaded: FULL DETAILS: " + task.exception!!.message
                    }
                    mutableLiveData.postValue(r)
                    mutableLiveData = updateOnlyText(planet, mutableLiveData)
                }
            mutableLiveData
        })
    }

Again you can see we’ve re-used the uploadOnlyText() method we had defined earlier.This is great as we reduce redundancy in our code,reducing errors and improving maintenability.

Here is the method to upate images and text in java:


    public MutableLiveData<RequestCall> updateImageText(Planet planet, Uri mImageUri) {
        mutableLiveData = new MutableLiveData<>();
        RequestCall r = new RequestCall();
        r.setStatus(Constants.IN_PROGRESS);
        r.setMessage("Performing Validation");
        mutableLiveData.setValue(r);

        if (planet == null) {
            r.setStatus(FAILED);
            r.setMessage("VALIDATION FAILED: Null Planet Received");
            mutableLiveData.setValue(r);
            return mutableLiveData;
        }else if (mImageUri == null) {
            r.setStatus(FAILED);
            r.setMessage("VALIDATION FAILED: Image Uri is Empty.Please pick an image");
            mutableLiveData.postValue(r);
            return mutableLiveData;

        } else {
            if (mUploadTask != null && mUploadTask.isInProgress()) {
                r.setStatus(Constants.IN_PROGRESS);
                r.setMessage("An Upload is already in Progress.Please be patient");
                mutableLiveData.postValue(r);
                return mutableLiveData;
            }

            r.setStatus(Constants.IN_PROGRESS);
            r.setMessage("Now Uploading Image...Please wait..");
            mutableLiveData.postValue(r);

            StorageReference imageRef = IMAGES_DB.child(mImageUri.getLastPathSegment());
            mUploadTask = imageRef.putFile(mImageUri);

            mUploadTask.continueWithTask((
                    Continuation<UploadTask.TaskSnapshot, Task<Uri>>) task -> {
                if (!task.isSuccessful()) {
                    r.setStatus(Constants.IN_PROGRESS);
                    r.setMessage("ERROR ENCOUNTERED: "+task.getException().getMessage());
                }
                // Continue with the task to get the download URL
                return imageRef.getDownloadUrl();
            }).addOnCompleteListener((OnCompleteListener<Uri>) task -> {
                if (task.isSuccessful()) {
                        Uri downloadUri = task.getResult();
                        String url = downloadUri.toString();
                        planet.setImageURL(url);
                        r.setStatus(IN_PROGRESS);
                        r.setMessage("Image Upload successful. We are now saving text details");

                }else{
                    r.setStatus(FAILED);
                    r.setMessage("Unfortunately Image Could not be uploaded: FULL DETAILS: "+task.getException().getMessage());
                }
                mutableLiveData.postValue(r);
                mutableLiveData = updateOnlyText(planet,mutableLiveData);

            });

            return mutableLiveData;
        }

    }

How to Delete Images Only

The method we will look at shortly is de-signed to be used by other methods. It can be used by the updateImageText() method, if you want to remove the old image completely while replacing it with the new image. Or it can be used if you are deleting both images and text. Normally after you have deleted an image, you want to make sure that firebase realtime database is either updated with the new image url,if you uploaded new one, or the text is also deleted. You don’t want a situation where you’ve already removed image but it’s url is still saved in the database.

However even if that was to occur, nothing will crash for us. We will simply show a placeholder image. Then you can update the planet,assigning it new image, or you can simply delete the whole planet.

Here is the method to delete only images in kotlin:

    private fun deleteOnlyImage(imageURL: String,textWasDeleted: Boolean): MutableLiveData<RequestCall> {
        val r = RequestCall()
        r.status = IN_PROGRESS
        r.message = "Deleting Image..Now"
        mutableLiveData.value = r
        if (imageURL.isEmpty()) {
            r.status = FAILED
            if (textWasDeleted) {
                r.message =
                    "While Text was deleted Image could not because of Null Image URL"
            } else {
                r.message = "Image cannot not be deleted because of Null Image URL."
            }
            return mutableLiveData
        }
        if (!textWasDeleted) {
            r.status = FAILED
            r.message =
                "Image Not Deleted Because it's URL is still saved in Database. This means Text deletion was not successful"
            return mutableLiveData
        }
        val imageRef: StorageReference
        try {
            imageRef = mStorage.getReferenceFromUrl(imageURL)
        } catch (e: Exception) {
            r.status = FAILED
            r.message =
                "While Text was deleted, image could not because a Wrong Image URL was specified. DETAILS: " + e.message
            return mutableLiveData
        }
        r.status = IN_PROGRESS
        r.message = "Text Already Deleted...Now Deleting Image. Please wait..."
        imageRef.delete().addOnCompleteListener { task ->
            if (task.isSuccessful) {
                r.status = SUCCEEDED
                r.message = "Congrats! Both Image and Text Deleted Successfully"
            } else {
                r.status = FAILED
                r.message =
                    "While Text was Deleted Successfully, Image Was Not Deleted. DETAILS: " + task.exception!!.message
            }
            mutableLiveData.postValue(r)
        }
        return mutableLiveData
    }

Here is how to delete image from Firebase Cloud Strorage in Java:


    public MutableLiveData<RequestCall> deleteOnlyImage(final String imageURL,boolean textWasDeleted) {
        if(mutableLiveData == null){
            mutableLiveData = new MutableLiveData<>();
        }

        RequestCall r = new RequestCall();
        r.setStatus(Constants.IN_PROGRESS);
        r.setMessage("Deleting Image..Now");
        mutableLiveData.setValue(r);

        if(imageURL == null || imageURL.isEmpty()){
            r.setStatus(FAILED);
            if(textWasDeleted){
                r.setMessage("While Text was deleted Image could not because Null Image URL of null image url.");
            }else{
                r.setMessage("Image cannot not be deleted because of Null Image URL.");
            }
            return mutableLiveData;
        }
        if(!textWasDeleted){
            r.setStatus(FAILED);
            r.setMessage("Image Not Be Deleted Because it's URL is still saved in Database. This means Text deletion was not successful");
            return mutableLiveData;
        }

        StorageReference imageRef;
        try {
            imageRef = mStorage.getReferenceFromUrl(imageURL);
        }catch (Exception e){
            r.setStatus(FAILED);
            r.setMessage("While Text was deleted, image could not because a Wrong Image URL was specified. DETAILS: "+e.getMessage());
            return mutableLiveData;
        }
        r.setStatus(IN_PROGRESS);
        r.setMessage("Text Already Deleted...Now Deleting Image. Please wait...");

        imageRef.delete().addOnCompleteListener(new OnCompleteListener<Void>() {
            @Override
            public void onComplete(@NonNull Task<Void> task) {
                if (task.isSuccessful()) {
                    r.setStatus(SUCCEEDED);
                    r.setMessage("Congrats! Both Image and Text Deleted Successfully");
                } else {
                    r.setStatus(FAILED);
                    r.setMessage("While Text was Deleted Successfully, Image Was Not Deleted. DETAILS: "+task.getException().getMessage());
                }
                mutableLiveData.postValue(r);
            }
        });
        return mutableLiveData;
    }

How to Delete Both Images and Text

We have already defined a method that can delete for us an image from firebase cloud storage. We now need a method that can delete for us both text and images.

Here is the method to delete both image and text from firebase using kotlin:

    fun deleteImageText(selectedPlanet: Planet): MutableLiveData<RequestCall> {
        mutableLiveData = MutableLiveData()
        val r = RequestCall()
        r.status = IN_PROGRESS
        r.message = "Performing Validation"
        mutableLiveData.value = r
        return run {
            r.status = IN_PROGRESS
            r.message = "Deleting Planet...Please wait.."
            mutableLiveData.postValue(r)
            val selectedStarKey = selectedPlanet.key
            DB.child(selectedStarKey!!).removeValue()
                .addOnCompleteListener { task ->
                    if (task.isSuccessful) {
                        r.status = IN_PROGRESS
                        r.message =
                            selectedPlanet.name + " text REMOVED DELETED SUCCESSFULLY..Now deleting image"
                        mutableLiveData = deleteOnlyImage(selectedPlanet.imageURL!!, true)
                    } else {
                        r.status = FAILED
                        r.message = "UNSUCCESSFUL: " + task.exception!!.message
                    }
                    mutableLiveData.postValue(r)
                }
            mutableLiveData
        }
    }

Here is the method to delete both image and text from firebase using java:

    public MutableLiveData<RequestCall> deleteImageText(Planet selectedPlanet) {
        mutableLiveData = new MutableLiveData<>();
        RequestCall r = new RequestCall();
        r.setStatus(Constants.IN_PROGRESS);
        r.setMessage("Performing Validation");
        mutableLiveData.setValue(r);

        if (selectedPlanet == null) {
            r.setStatus(FAILED);
            r.setMessage("VALIDATION FAILED: Null Planet Received");
            mutableLiveData.postValue(r);
            return mutableLiveData;
        } else {
            r.setStatus(Constants.IN_PROGRESS);
            r.setMessage("Deleting Planet...Please wait..");
            mutableLiveData.postValue(r);
            final String selectedStarKey = selectedPlanet.getKey();

            DB.child(selectedStarKey).removeValue().addOnCompleteListener(new OnCompleteListener<Void>() {
                @Override
                public void onComplete(@NonNull Task<Void> task) {
                    if (task.isSuccessful()) {
                        r.setStatus(IN_PROGRESS);
                        r.setMessage(selectedPlanet.getName() + " text REMOVED DELETED SUCCESSFULLY..Now deleting image");

                        mutableLiveData = deleteOnlyImage(selectedPlanet.getImageURL(),true);
                    } else {
                        r.setStatus(FAILED);
                        r.setMessage("UNSUCCESSFUL: " + task.getException().getMessage());
                    }
                    mutableLiveData.postValue(r);
                }
            });
            return mutableLiveData;
        }

    }

Creating an About us Page

This is a static page meant to give you a design you can use to create an about us page for your full app:

class AboutUsActivity : BaseActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_about_us)
        setSupportActionBar(aboutUsToolBar)
        backArrow.setOnClickListener {finish()}
    }
}

Dashboard Page

We are using this as our central navigation activity. Users navigate to other pages from here:

class DashboardActivity : BaseActivity() {
    private fun initializeWidgets() { //We have 4 cards in the dashboard
        viewCard!!.setOnClickListener {
            openActivity(this, ListingActivity::class.java)
        }
        addCard!!.setOnClickListener {
            openActivity(this, UploadActivity::class.java)
        }
        aboutUs!!.setOnClickListener {
            openActivity(this, AboutUsActivity::class.java)
        }
        closeCard!!.setOnClickListener { finish() }
        mCollapsingToolbarLayout!!.setExpandedTitleColor(resources.getColor(R.color.white))
        mCollapsingToolbarLayout.setBackgroundResource(
            LOCAL_IMAGES[Random().nextInt(LOCAL_IMAGES.size)]
        )
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_dashboard)
        initializeWidgets()
    }
}

If you are new to Kotlin, you may wonder, where are the findViewById() methods? Well this is Kotlin, all we needed to do was add this import:f

import kotlinx.android.synthetic.main.activity_dashboard.*

And that’s referenced for us all widgets in the activity_dashboard layout.

However, we’ve also simplified stuff in java using data binding:

public class DashboardActivity extends BaseActivity {

    private ActivityDashboardBinding b;

    private void initializeWidgets() {
        //We have 4 cards in the dashboard
        b.viewCard.setOnClickListener(v -> Utils.openActivity(this,
                ListingActivity.class));
        b.addCard.setOnClickListener(v -> Utils.openActivity(this,
                UploadActivity.class));
        b.aboutUs.setOnClickListener(v -> Utils.openActivity(this,
                AboutUsActivity.class));
        b.closeCard.setOnClickListener(v -> finish());

        b.mCollapsingToolbarLayout.setExpandedTitleColor(getResources().
                getColor(R.color.white));
        b.mCollapsingToolbarLayout.setBackgroundResource(LOCAL_IMAGES[new Random().nextInt(
                LOCAL_IMAGES.length)]);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        b = DataBindingUtil.setContentView(this, R.layout.activity_dashboard);
        b.setCallback(this);

        this.initializeWidgets();
    }
}
//end

Detail Page

An app is nothing if you don’t have a readingView or a detail view. In these types views, you render details of a data object. Like in our case we need a readingview or detail view for our Planet. That’s where our DetailActivity comes in place.

In Kotlin, here is how we rexeive data:

    private fun receiveAndShowData() {
        receivedPlanet = receive(intent, this@DetailActivity)
        if (receivedPlanet != null) {
            nameTV.text = receivedPlanet!!.name
            descriptionTV.text = receivedPlanet!!.description
            galaxyTV.text = receivedPlanet!!.galaxy
            typeTV.text = receivedPlanet!!.type
            dodTV.text = receivedPlanet!!.dod
            mCollapsingToolbarLayout.title = receivedPlanet!!.name
            mCollapsingToolbarLayout.setExpandedTitleColor(resources.getColor(R.color.white))
            loadImageFromNetwork(receivedPlanet!!.imageURL!!,
                LOCAL_IMAGES[Random().nextInt(LOCAL_IMAGES.size)], dImageView
            )
        }

        editFAB.setOnClickListener {
            sendPlanetToActivity(this, receivedPlanet, UploadActivity::class.java)
            finish()
        }
    }

Listing View

We will be listing our planets using Recyclerview with cardviews. You can use any layout manager like GridLayoutManager or LinearLayoutManager.

We also show a toolbar searchview which allows users to search data:

    override fun onQueryTextChange(query: String): Boolean {
        Utils.searchString = query
        val adapter = MyAdapter()
        adapter.addAll(Utils.MEMORY_CACHE)
        adapter.filter.filter(query)
        rv.layoutManager = LinearLayoutManager(this)
        rv.adapter = adapter
        return false
    }

CarouselView

That beautiful image slider up top our Listings Page is the carouselView. We will extract image urls from our Planets and feed it.
Here is how we extract the URLs from our Planet objects:

In Kotlin using a forEach function call:

    /**
     * Extract image URL from downloaded data
     * @param planets - the lists
     * @return
     */
    @JvmStatic
    fun getImageURLs(planets: List<Planet>): Array<String?> {
        val imageURLs = arrayOfNulls<String>(planets.size)
        planets.withIndex().forEach { (i, planet) ->
            imageURLs[i] = planet.imageURL
        }
        return imageURLs
    }

You can also use the normal for loop in kotlin:

    @JvmStatic
    fun getImageURLs(planets: List<Planet>): Array<String?> {
        val imageURLs = arrayOfNulls<String>(planets.size)
        for ((i, planet) in planets.withIndex()) {
            imageURLs[i] = planet.imageURL
        }
        return imageURLs
    }

In Java the equivalent for loop is:

    public static String[] getImageURLs(List<Planet> planets){
        String[] imageURLs=new String[planets.size()];

        int i = 0;
        for (Planet planet : planets){
            imageURLs[i] = planet.getImageURL();
            i++;
        }
        return imageURLs;
    }

Splash Activity

You can brand your app using the splash page. We also apply some animations on our textviews and logo.

    /**
     * Let's show our Splash animation using Animation class. We fade in our widgets.
     */
    private fun showSplashAnimation() {
        val animation =
            AnimationUtils.loadAnimation(
                this,
                R.anim.drop
            )
        mLogo.startAnimation(animation)
        val fadeIn =
            AnimationUtils.loadAnimation(this, R.anim.fade_in)
        mainTitle.startAnimation(fadeIn)
        subTitle.startAnimation(fadeIn)
    }

Filtering

We have a class called FilterHelper that will allow us perform actual filtering:

    /*
    - The following method will allow us to Perform the actual filtering.
     */
    override fun performFiltering(constraint: CharSequence): FilterResults {
        var constraint: CharSequence? = constraint
        val filterResults = FilterResults()
        if (constraint != null && constraint.isNotEmpty()) { //CHANGE TO UPPER
            constraint = constraint.toString().toUpperCase()
            //HOLD FILTERS WE FIND
            val foundFilters = ArrayList<Planet>()
            var name: String?
            var galaxy: String?
            var description: String?
            //ITERATE CURRENT LIST
            for (i in currentList!!.indices) {
                name = currentList!![i].name
                galaxy = currentList!![i].galaxy
                description = currentList!![i].description
                //SEARCH
                when {
                    name!!.toUpperCase().contains(constraint) -> {
                        foundFilters.add(currentList!![i])
                    }
                    galaxy!!.toUpperCase().contains(constraint) -> {
                        foundFilters.add(currentList!![i])
                    }
                    description!!.toUpperCase().contains(constraint) -> {
                        foundFilters.add(currentList!![i])
                    }
                }
            }
            //SET RESULTS TO FILTER LIST
            filterResults.count = foundFilters.size
            filterResults.values = foundFilters
        } else { //NO ITEM FOUND.LIST REMAINS INTACT
            filterResults.count = currentList!!.size
            filterResults.values = currentList
        }
        //RETURN RESULTS
        return filterResults
    }

How to enable permanent Offline-Persistence

Firebase Realtime Database has the capability to allow us to permanently cache data offline. This is very powerful since it makes our app operational even if offline. However, it is important to note that this pertains to Firebase Realtime Database and not Firebase Cloud Storage. You need internet connectivity to upload images. You can neither upload images offline nor even when connection is regained. Because of this, if you are uploading new data, you will need internet connectivity atleast until image is uploaded and it’s url returned. Then even if the device goes offline, we will be able to insert the details into Firebase Realtime Database automatically when connection is resumed.

Enabling this offline persistence is pretty easy:

In our application’s app class, inside the onCreate() method we will start by initializing the FirebaseApp:

FirebaseApp.initializeApp(this)

Then we will enable persistence by passing true to the setPersistenceEnabled() method:

        FirebaseDatabase.getInstance().setPersistenceEnabled(true)