As we all battle this Covid-19 pandemic, humanity is relearning how fragile we all are. Sure, we all knew that our biological clock is ticking,but this isn’t what we expected. For the first time in our generation, we can’t even mourn or celebrate our family members who’ve passed away. In one clean sweep, they’ve become annonymous, no eulogies, no elaborate ceremonies to mark their passing.How depressing?

Family,Love and Death are being by dictated to one thing: CoronaVirus.People who’ve never experienced loneliness are now being forced to self isolation. And they do have no choice.Fancy weddings, family re-unions and everything in between are being suspended indefinately. If you are in a city and a family member in another city contracts the disease and dies, in countries like mine, you have no choice but to grieve hundreds of miles apart. The Family is one of those societal structures that being affected the most.

I thought about this and decided to create an app that can bring smiles and happiness to families again. Even if we are hundreds of miles apart, we can still share quality moments with family members. There are technologies that allow us to create tools and apps that faciltate this type of sharing.

We want a family gallery app. This app allows only family members to share moments using images. Only family members can view these moments. These moments are stored in Firebase realtime database and firebase cloud storage. You sign in as a family member, capture a moment as a photo via camera, and upload. Other family members will be able to view it when they login in. You can also delete your own moments. You can view the images you’ve shared in your account page. Go ahead and read the features of this app as well as technologies used to make.

This app is designed for students who want to learn how to create a real world app using Kotlin, Firebase Realtime Database, Firebase cloud storage and firebase authentication. You can use it as a template for implementing all types of ideas.

Technologies and Features

1. Kotlin Programming Language

Yet again we use Kotlin, the defacto Programming Language for android these days. It’s a very sweet language once you start getting to grips with it. If you are new to Kotlin then this is almost the perfect project to practice. Code is clean and quite short for a project with the functionalities we include.

2. Clean Architecture

We implement the clean architecture principals in this app. We start by using MVVM(Model View ViewModel) design pattern. You will learn how to use several classes included in the lifecycle extensions like the ViewModel and LiveData. In the end we create a high quality, maintenable and extensible project. By seperating logic from UI, we keep our classes testable. By using defining several functionalities once, then using them in several classes, we not only shorten our code, but more importantly we reduce chances of errors.

3. Firebase Cloud Storage

The cloud storage for storing static data like images and audio, Firebase Cloud storage gives us an important functionality in our app which would have otherwise been difficult. It is vital to this app like we are creating given that we need to store our images somewhere we all our family members can securely access them. The image urls will then be returned for us and we will save them to Firebase Realtime Database.

4. Firebase Realtime Database

This app wouldn’t be complete if we used only Firebase Cloud storage. Yeah we need to upload images but then we also the image URLs. These image urls will be stored for us in Firebase Realtime database. Not only the image urls but also other properties like date of capturing the image,the id of the image, the uploader etc. Firebase realtime database will give us this important capability of storing metadata associated with the image.

5. Firebase Authentication

As we said earlier, this an app for family members. Meaning that only family members will use the app. Even if another person has the APK, he will need to login. However he cannot create an account since we’ve intentionally ommited that functionality. Family members will be added by the admin at Firebase Console. This makes the app highly secure and the family will never have to deal with spam. We implement email/password form of authentication.

6. Gallery

We will use a beautiful image gallery library that gives us ability to easily list images based on URLs, then when an image is clicked we show it in a different activity which has the zoom capability.

7. Slider

We will also have a beautiful image slider in our homepage. The slider also requires only an array of image urls to be passed to it. Then we can load our images using Picasso or any other library like Glide or Fresco. The image auto-slide, and can also be swiped manually. This, among other properties can be changed. For example you can set the timing,colors etc.

8. Camera

Family Members will be able to easily capture images via the Camera. Of course they can also pick images via the Gallery or even directly via the File Explorer. This is seamless and easy. The picked image will be uploaded immediately.

9. Runtime Permissions

Yeah, we implement runtime permissions as well, in the easiest way we’ve seen over the internet. We use a library known as Dexter. Not only do we check the permissions, but we also give the user the ability to proceed to settings and assign us the permission we need.

10. Material Design

We create a beautiful app using material design. We have some really beautiful pages,use material design colors and also use custom fonts.

11. Easiest Adapter

We implement an adapter in the easiest way possible using the EasyAdapter library. This massively eliminates the boilerplate code we always need to create even the most basic adapter. EasyAdapter leverages data binding power to allow us create an adappter with as few lines as currently possible.

12. “List My Photos”

We implement the ability to lits or view you own photos in your account. You can then choose to delete your photo. Obviously you can add more photos.

13. View Even Offline

By making use of Firebase realtime database’s offline peristence capability, family members can view the photos even while completely offline. Obviously to upload an image you need internet connectivity.

14. Periodic Random Family Quotes with Animations

To put us more into the Family Mood, we also include some loving family quotes which are randomly displayed using animated textviews. We create animation files and load them in our textviews. We use Timer to show the quotes after a fixed interval.

Well let’s start.

Defining a Photo Object

We need to define what we mean by a Photo, in a programmatic sense. That means we need to create our data object class. Instances of this class are what will be peristed to our database.

Here is our we define our Photo.

/**
 * Our Photo Class. It's roles are:
 * 1. Define the properties of our Photo item.
 * 2. Assign Default Photo values using the elvis operator
 */
class Photo : Serializable {

    var datePublished: String? = ""
    var publisher: String? = ""
    var imageURL: String? = ""

    @get:Exclude
    @set:Exclude
    var key: String? = ""

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

}

We’ve specified properties that are essential to our photos. We don’t want to clutter our instance with many properties as this app intends to be photo-centric. Date published shall be picked automatically using java.util.Date class when we are publishing our photo. We shall also pick the publisher email as users have to login to upload or even view the images. The image url will be returned to us once we’ve uploaded our image to Firebase Cloud storage.

Take note of the annotations we’ve decorated on our key. What we are simply saying is that when we are posting our Photo to our realtime database, we want to ommit the key. This is because the key is auto-generated by Firebase Realtime database. It will be generated even if we are offline.

If you cast the class to a string we will return to you the image url.

Representing our Firebase Request

The upload,download and delete operations, or rather the crud operations we perform against Firebase are what we mean by our requests. For instance if you are uploading an image to firebase then we call that a request. We need one class to represent this requests.

Here are the properties of our requests:

  1. Status – The status of the request.
  2. Message – associated with the requests e.g progress message,error message,success message.
  3. Photos – List of photos,if any, associated with the request.

Here is how we representing these requests:

This class will represent a single request or Firebase operation


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

How to Download the Photos from Firebase Cloud storage

Well, in reality it’s not from Firebase Cloud strorage, instead it’s from Firebase realtime database. The image urls get alongside other properties get stored in the realtime database. The actual images will be downloaded via Picasso later on. But for now let’s see how to fetch our photo objects from firebase realtime database.

We are using MVVM(Model View ViewModel) design pattern. Therefore the perfect place to write logic code like this is in a repository class. So we start by creating one:

“`kotlin
class PhotoRepository {

I will call this method select():

    fun select(): MutableLiveData<RequestCall> {

You are seeing from the above line that we are returning a MutableLiveData object with a RequestCall generic parameter. That requestcall object is what will hold for us the data we download. It will also hold for us the status of the request as well as the message associated with the request. The MutableLiveData is an observable object. It will be observed by our subscribers within our UI. Those subscribers will get notified if any change occurs in the data held by our RequestCall object.

So let’s instantiate the MutableLiveData object we will be returning:

        val mLiveData = MutableLiveData<RequestCall>()

Then the RequestCall it will hold:

        val r = RequestCall()

Let’s now set the initial data of our requestcall:

        r.status = IN_PROGRESS
        r.message = "Fetching Photo Please Wait.."

Then set the RequestCall instance as the value property of our MutableLiveData:

        mLiveData.value = r

Next we will attach an event listener to our Firebase Realtime Database reference:

        DB.addValueEventListener(object : ValueEventListener {

The first method of our annonymous class we override is the ondataChange. This is the method that will give us our data snapshot:

            override fun onDataChange(dataSnapshot: DataSnapshot) {

Am going to clear the list which will tenporarily be holding our the data we’ve downloaded:

                memCache.clear()

We will then validate our DataSnapshot, making sure it exists and that it has some
children:

                if (dataSnapshot.exists() && dataSnapshot.childrenCount > 0) {

If this validation passes our test, then we simply loop through the dataSnapshot’s children, which are indeed our Photo objects, adding them to our arraylist

                    for (ds in dataSnapshot.children) { //Now get Photo Objects and populate our arraylist.
                        val news = ds.getValue(Photo::class.java)
                        if (news != null) {
                            news.key = ds.key
                            memCache.add(news)
                        }
                    }

In that cases our request is over and it is a success:

                    r.status = SUCCEEDED
                    r.message = "DOWNLOAD COMPLETE"

However sometimes we succeed even if we don’t manage to find any data, Maybe the user hasn’t uploaded any or he/she has deleted them all:

                else {
                    r.status = SUCCEEDED
                    r.message = "NO DATA FOUND"
                }

We now need to attach the downloaded data to our RequestCall then post the change via our MutableLiveData:

                r.photos = memCache
                mLiveData.postValue(r)

It is possible that a failure can occur,for example if you don’t have any internet or hasn’t specified the right permission:

            override fun onCancelled(databaseError: DatabaseError) {
                Log.d("CAMPOSHA", databaseError.message)
                r.status = FAILED
                r.message = databaseError.message
                mLiveData.postValue(r)
            }

Last but not least, we shouldn’t forget to return our LiveData:

        return mLiveData

Here is the whole code for this method:

    fun select(): MutableLiveData<RequestCall> {
        val mLiveData = MutableLiveData<RequestCall>()
        val r = RequestCall()
        r.status = IN_PROGRESS
        r.message = "Fetching Photo Please Wait.."
        mLiveData.value = r
        DB.addValueEventListener(object : ValueEventListener {
            override fun onDataChange(dataSnapshot: DataSnapshot) {
                memCache.clear()
                if (dataSnapshot.exists() && dataSnapshot.childrenCount > 0) {
                    for (ds in dataSnapshot.children) { //Now get Photo Objects and populate our arraylist.
                        val news = ds.getValue(Photo::class.java)
                        if (news != null) {
                            news.key = ds.key
                            memCache.add(news)
                        }
                    }
                    r.status = SUCCEEDED
                    r.message = "DOWNLOAD COMPLETE"
                } else {
                    r.status = SUCCEEDED
                    r.message = "NO DATA FOUND"
                }
                r.photos = memCache
                mLiveData.postValue(r)
            }

            override fun onCancelled(databaseError: DatabaseError) {
                Log.d("CAMPOSHA", databaseError.message)
                r.status = FAILED
                r.message = databaseError.message
                mLiveData.postValue(r)
            }
        })
        return mLiveData
    }

How to Save Text Locally/Online

We will now write a simple method that will be able to save text data either locally, if there is no internet or to datbasse online if there is internet.

We start by creating the method and preparing our request call as well as the liveData:

    fun saveTextLocally(photo: Photo): MutableLiveData<RequestCall> {
        val r = RequestCall()
        r.status = IN_PROGRESS
        r.message = "Saving ..."
        mutableLiveData.value = r

You can see we are receiving a photo object as a parameter.

Now we mean both creating and updating when we mean saving. The key will help us identify if the intention is creation or update:

              if (photo.key.isNullOrEmpty()){
                    DB.push().setValue(photo);
                }else{
                    DB.child(photo.key!!).setValue(photo)
                }
                r.status = SUCCEEDED
                r.message = "Congrats!. Successfully Saved."

If the key is null or empty, it means it’s a new creation. If one exists, then we know the intention is update.

Here is the full method:

    fun saveTextLocally(photo: Photo): MutableLiveData<RequestCall> {
        val r = RequestCall()
        r.status = IN_PROGRESS
        r.message = "Saving ..."
        mutableLiveData.value = r
        return run {
            r.status = IN_PROGRESS
            r.message = "Saving...Please wait.."
            mutableLiveData.postValue(r)
            //push data to FirebaseDatabase. Table or Child called Photo will be created.
            try{
                if (photo.key.isNullOrEmpty()){
                    DB.push().setValue(photo);
                }else{
                    DB.child(photo.key!!).setValue(photo)
                }
                r.status = SUCCEEDED
                r.message = "Congrats!. Successfully Saved."

            }catch (e: Exception){
                r.status = FAILED
                r.message= e.message!!
            }
            mutableLiveData.postValue(r)
            mutableLiveData
        }
    }

Well we also have the following methods in our repository class:

  1. Method for uploading only text while online.
  2. Method for updating only text while online.
  3. Method for updating image and text.
  4. Method for deleting image and text
  5. Method for signing in using email and password
  6. Method for filtering uploads.

Unlike the earlier methods we had defined, the abobe methods need internet connectivity. Especially the ones involving modifying firebase cloud storage. Saving text or selecting data don’t need internet connectivity.

How to specify our Database and Storage Locations

The firebase realtime database will be storing text while the cloud storage will be storing images. Both are different services. We need to define paths pointing to where we want our data stored in them.

For Firebase realtime database we start by instantiating the FirebaseDatabase using the factory method getInstance(), then we obtain the reference and pass a string we want to use as the name of the node to store our data:

    //The following line points us to the location of our database
    @JvmField
    val DB = FirebaseDatabase.getInstance().getReference("family_gallery_db")

For Firebase Cloud storage, we instantiate the FirebaseStorage and also obtain it’s reference:

    //The following points us to where our images will be stored
    @JvmField
    val IMAGES_DB = FirebaseStorage.getInstance().getReference("family_gallery_images_db")

We specify the above in our Constants class.

Example of Test family members

In our Constants class, I have specified some test family members I have stored in my account. You can use them to test the app. Login into the app using any of them:

    //You can specify family members here. You make sure you have added them
    // as users in the firebase console under the authentication tab
    //Here are some test family members for demo purposes
    val FAMILY_MEMBER_1 = "clarkkent@gmail.com" //Password is 123456
    val FAMILY_MEMBER_2 = "johndoe@gmail.com" //Password is also 123456
    val FAMILY_MEMBER_3 = "jamesweb@gmail.com" //Password is also 123456

How to show animated family Quotes after a given interval

We will show some family quotes in the app. You can find plenty of quotes online. Here are some of the best I found:

    @JvmField
    val FAMILY_QUOTES = arrayOf(
        "A happy family is but an earlier heaven.",
        "The family is the test of freedom; because the family is the only thing that the free man makes for himself and by himself.",
        "The most important thing in the world is family and love.",
        "Nothing is better than going home to family and eating good food and relaxing.",
        "To us, family means putting your arms around each other and being there.",
        "You leave home to seek your fortune and, when you get it, you go home and share it with your family.",
        "Call it a clan, call it a network, call it a tribe, call it a family: Whatever you call it, whoever you are, you need one.",
        "The family is one of nature’s masterpieces",
        "Families are the compass that guides us. They are the inspiration to reach great heights, and our comfort when we occasionally falter.",
        "Other things may change us, but we start and end with the family.",
        "Family is not an important thing, it’s everything.",
        "Family means no one gets left behind or forgotten.",
        "The only rock I know that stays steady, the only institution I know that works, is the family.",
        "Without a family, man, alone in the world, trembles with the cold.",
        "Rejoice with your family in the beautiful land of life.",
        "Other things may change us, but we start and end with the family",
        "Having somewhere to go is home. Having someone to love is family. And having both is a blessing.",
        "In time of test, family is best"
    )

I have defined them in the Constants class.

Now the next thing we need to learn is how to animate our textview. We will flipping a quote afetr a given number of seconds. We are going to apply either of two animations, we choose the animation randomly. The first thing is to define the animations as XML files in our anim folder under the resources directory:

(a). Fading In Our TextView

The first animation is fade_in animation. We fade in our textview when switching to the next quote from our list:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:fromAlpha="0.0"
        android:toAlpha="1.0"
        android:duration="1500"
        />
</set>

(b). Dropping Our TextView

The next animation is the drop animation. We slightly drop the textview when switching to the next animation.

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:fromYDelta="-150%"
        android:toYDelta="0%"
        android:duration="500"
        android:repeatCount="0" />
</set>

Now that we’ve defined our animations, we actually need to apply them. But before applying them, we need to create a timer which will post to us the a string containing our quote after a given interval. We want a fixed timer, meaning that the timer will post the quote after a fixed interval. We will create this timer in our Utils class:

    @JvmStatic
    fun createTimer(): MutableLiveData<String>{
        val timer = Timer()
        val mLiveData = MutableLiveData<String>()
        //Schedule the specified task for repeated fixed-rate execution,
        // beginning after the specified delay. Subsequent executions
        // take place at approximately regular intervals, separated by
        // the specified period.
        //Schedule the specified task for repeated fixed-rate execution,
        // beginning after the specified delay. Subsequent executions
        // take place at approximately regular intervals, separated by
        // the specified period.
        timer.scheduleAtFixedRate(object : TimerTask() {
            override fun run() {
                mLiveData.postValue("'${Constants.FAMILY_QUOTES[Random().nextInt(Constants.FAMILY_QUOTES.size)]}'")
            }
        }, 5000, 7000)
        return mLiveData
    }

As you can see we’ve used the scheduleAtFixedRate() method. And yeah we are returning a LiveData object which will be observed by our subscribe, the latter receiving the chosen quote.
NB/= Don’t place a UI widget in the run() method of our scheduleAtFixedRate() Timer method. UI widgets need to be executed in the UI thread, and the above run() method runs in the background in the UI thread.

Good. Now remember those animations we had defined earlier, let’s also create another utility method to load them and apply them to a passed widget:

    @JvmStatic
    fun loadAndApplyAnimation(c: Context, view: View){
        val animations = arrayOf( R.anim.fade_in, R.anim.drop)
        val currentAnim = AnimationUtils.loadAnimation(c,animations[Random().nextInt(animations.size)])
        view.startAnimation(currentAnim)
    }

That’s it. So all we now need is to go to our activity, say inside the onCreate() method and add the following method:

        Utils.createTimer().observe(this@HomeActivity, Observer {
            Utils.loadAndApplyAnimation(this,quotesTV)
            quotesTV.text=it
        })

How to create a beautiful Gallery easily

There is an old but beautiful library in github that can easily give us a portable gallery, one that we can easily inject into our app. This way we avoid re-inventing the wheel. The library is written in Java but works seamlessly with Kotlin. It can give us the following:

  1. Image Gallery just by supplying a bunch of URLs.
  2. Zoomable PhotoView when a single image is clicked.
  3. We can use custom themes and apply custom toolbar titles.
  4. It works with any image loader like Picasso and Glide.

Let’s install it:

    implementation 'com.github.lawloretienne:imagegallery:0.1.0'

Then create it a theme as follows in our styles.xml:


    <style name="Base.Theme.ImageGallery" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="colorButtonNormal">@color/primary</item>
        <item name="colorControlActivated">@color/primary</item>
        <item name="colorControlHighlight">@color/grey_500</item>
        <item name="android:textColorPrimary">@color/primary_text</item>
        <item name="android:textColorSecondary">@color/secondary_text</item>
        <item name="android:windowAnimationStyle">@style/MyActivityAnimations</item>
    </style>
    <style name="Theme.ImageGallery" parent="Base.Theme.ImageGallery" />

Because it will give us an already defined ImageGalleryActivity activity to use, we need to register that activity in our android manifest:

        <activity
            android:name="com.etiennelawlor.imagegallery.library.activities.ImageGalleryActivity"
            android:configChanges="orientation|keyboardHidden|screenSize"
            android:label="Family Gallery"
            android:theme="@style/Theme.ImageGallery" />

Now we come to our HomeActivity, where we will use the Gallery and implement two interfaces:

class HomeActivity : BaseActivity(), ImageGalleryAdapter.ImageThumbnailLoader, FullScreenImageGalleryAdapter.FullScreenImageLoader {

We’ve implemented the above two interfaces since the library will give us two activities:

  1. One for showing image thumbnails in a recyclerview. When a single thumbnail is clicked we open the second activity. Here is the method we implement to load and resize our images as thumbnails:
    override fun loadImageThumbnail(iv: ImageView?, imageUrl: String?, dimension: Int) {
        if(imageUrl!!.isNotEmpty()){
            Picasso.get().load(imageUrl).resize(dimension,dimension).placeholder(R.drawable.load_dots).error(R.drawable.image_not_found).into(iv)
        }
    }

Note that resizing is mandatory. Whatever image loader library you are using, make sure to resize.

2. That second activity is our full screen photoView. You can zoom in and out the image beautifully.

    override fun loadFullScreenImage(iv: ImageView?, imageUrl: String?, width: Int, bglinearLayout: LinearLayout?) {
        if(imageUrl!!.isNotEmpty()){
            Picasso.get()
                .load(imageUrl)
                .resize(width, 0)
                .into(iv, object : Callback {
                    override fun onSuccess() {
                        val bitmap: Bitmap = (iv!!.drawable as BitmapDrawable).getBitmap()
                        Palette.from(bitmap).generate { }
                    }
                    override fun onError(e: Exception?) {
                        show(e?.message)
                    }
                })
        }

Alright. The above methods were interface methods we were overriding. Now to actually pass the urls of our images and open the gallery we will create another method:

    private fun openGallery(){
        if(!networkImages.isNullOrEmpty()){
            val intent = Intent(this, ImageGalleryActivity::class.java)
            val bundle = Bundle()
            bundle.putStringArrayList(
                ImageGalleryActivity.KEY_IMAGES,
                ArrayList(listOf(*networkImages))
            )
            bundle.putString(ImageGalleryActivity.KEY_TITLE, "Family Gallery")
            intent.putExtras(bundle)
            startActivity(intent)
        }else{
            show("Please Upload some Images first")
        }
    }

Hey and don’t forget to set the thumbnail loader and fullscreen image loader as this, given we’ve just implemented two interfaces. Do it maybe in your onResume or onCreate method:

        ImageGalleryActivity.setImageThumbnailLoader(this)
        FullScreenImageGalleryActivity.setFullScreenImageLoader(this)

That’s it. Now to show the gallery all you need to do is:

        viewCard!!.setOnClickListener {
            openGallery()
        }

How to Create a Beautiful Image Carousel

An image Carousel is different from a gallery. Image carousel are basically auto-sliders. I like using image carousel’s in almost any app I create involving images. Let’s use one of my favorite slider libraries.

Start by installing the library:

    implementation 'com.synnapps:carouselview:0.1.4'

Go to your layout where you want to show the carousel and add the following code:

    <com.synnapps.carouselview.CarouselView
        android:id="@+id/carouselView"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@color/colorPrimary"
        app:animateOnBoundary="true"
        app:autoPlay="true"
        app:fillColor="#FFFFFFFF"
        app:indicatorGravity="top"
        app:indicatorVisibility="visible"
        app:pageColor="#00000000"
        app:pageTransformInterval="1500"
        app:pageTransformer="depth"
        app:radius="6dp"
        app:slideInterval="3000"
        app:strokeColor="#FF777777"
        app:strokeWidth="1dp" />

Clearly you can see there is an opportunity for making customizations. Things like the page transform interval, whether to autoPlay, the fillColor, indicator visibility etc. This library is super easy to use because these properties can be filled declaratively.

Now go to your activity where you intend to use the carousel. Initialize an array maybe as an instance field. That array will hold the image urls we will be rendering in the image slider. Try to include atleast one image url, maybe a dummy one in the array:

    private var networkImages = arrayOf<String?>(
        "https://images.unsplash.com/photo-1580458072512-96ced1f43991?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=751&q=80"
    )

Now let’s prepare an image listener. This is where we will load our images. The current image url will be passed to us after the interval we specified has collapsed, and we simply load the image using our favorite image loader.

    private var imageListener = ImageListener(fun(position: Int, imageView: ImageView) {
        if (!networkImages[position].isNullOrEmpty()) {
            Picasso.get().load(networkImages[position]).placeholder(R.drawable.placeholder)
                .error(R.drawable.image_not_found).fit()
                .centerCrop().into(imageView)
        }else{
            Picasso.get().load(R.drawable.image_not_found)
                .error(R.drawable.image_not_found).fit()
                .centerCrop().into(imageView)
        }
    })

NB/= Be careful to handle null values as we’ve done above no matter what image loader you are using. If you don’t you may encounter runtime crashes when null is passed to the image loader.

OK now go ahead and setup the carousel:

    private fun setupCarousel() {
        carouselView!!.setImageListener(imageListener)
        carouselView.setImageClickListener { position: Int ->
            Toast.makeText(this, "Clicked item: $position", Toast.LENGTH_SHORT)
                .show()
        }
        carouselView.pageCount = networkImages.size
    }

Great. So what we need now is to extract the image urls from the photo objects that have been downloaded from firebase. We will create a helper method in our Utils class to do that:

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

Basically we pass it a list of photos and it returns us the image urls as a string.

Then once you’ve downloaded the data from Firebase, you do the following:

                    networkImages = getImageURLs(photos)
                    setupCarousel()

Basically we’ve assigned the extracted image urls to our network images string array. Then invoked the setupcarousel to update the carosuelView page count and urls.

How to handle runtime Permission via Dexter

Dexter is a library for handling runtime permissions. I like how concise it is. It’s one of my favorite runtime permissions library. We will use it here to check runtime permissions.

Start by installing it as below:

    implementation 'com.karumi:dexter:4.2.0'

Now this isn’t a requirement but what about if we create a nice dialog that will prompt the user to grant us permission at runtime:

    protected fun showSettingDialog() {
        Log.i("PERMISSION", "Showing Permission Setting Dialog")
        val builder =
            AlertDialog.Builder(this)
        builder.setTitle("Assign Permissions")
        builder.setMessage("This app needs permission to use this feature. You can grant them in app settings.")
        builder.setPositiveButton("GO TO SETTING") { dialog: DialogInterface, _: Int ->
            dialog.cancel()
            openSetting()
        }
        builder.setNegativeButton("Cancel") { dialog: DialogInterface, which: Int -> dialog.cancel() }
        builder.show()
    }

Also let’s come and define the method to open for us settings page in the android device so that we can be granted the permission we need at runtime:

    private fun openSetting() {
        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
        val uri = Uri.fromParts("package", packageName, null)
        intent.data = uri
        startActivityForResult(intent, 101)
    }

Then here is how we check for the needed permission at runtime:

private fun checkPermissionsThenPickImage() {
        Dexter.withActivity(this)
            .withPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
            .withListener(object : PermissionListener {
                override fun onPermissionGranted(response: PermissionGrantedResponse) {
                    show("Good...READ EXTERNAL PERMISSION GRANTED")
                    captureImage()
                }

                override fun onPermissionDenied(response: PermissionDeniedResponse) {
                    show("WHOOPS! PERMISSION DENIED: Please grant permission first")
                    if (response.isPermanentlyDenied) {
                        showSettingDialog()
                    }
                }

                override fun onPermissionRationaleShouldBeShown(
                    permission: PermissionRequest,
                    token: PermissionToken
                ) {
                    Log.i("DEXTER PERMISSION", "Permission Rationale Should be shown")
                    token.continuePermissionRequest()
                }
            }).check()
    }

How to Capture Image from Camera,Choose from Gallery or File Explorer

You go out for a hike. You see a large snake wobbling across the river. It is the biggest snake you’ve ever seen and you have to share this with your family. Snakes are good swimmers so if you don’t pull out your app fast and capture it, it will go. In such a scenario you just want an app that has integration with the Camera. Well this app provides one. You can capture that large snake directly from the device Camera and upload it immediately.

Moreover you can select already captured images via the Gallery or the File explorer. Because this is a gallery app, we don’t want or need edittexts or spinners for entering data. We will capture or select our image and send it to cloud storage immediately. The other meta data like time and user will be picked automatically.

How to Authenticate Family Members using Firebase Authentication

Well we’ve included the ability to regulate the users of this app. If we create an app for family members only we don’t want to find obnoxious uploads by annonymous users. Family members can be added by one admin via Firebase console. Thus even if a random person gets the app, he/she can’t login.

So first navigate over to the firebase console and sign in using your gmail account. Go over to the authentication tab:

Now go ahead and enable the Email/Password authentication as a sign-in method.

Now easily add a family members you want to be able to use the app.

Folks won’t be able to sign in if you don’t add them. We’ve ommitted signing up from the app to avoid spam and imposters.

OK, am assuming you’ve done that, now come and design a pretty login page:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@color/colorPrimary"
    android:orientation="vertical"
    android:padding="16dp">

    <include layout="@layout/_state" />

    <ImageView
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginBottom="10dp"
        android:src="@drawable/home_lock" />

    <com.google.android.material.textfield.TextInputLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content">

        <EditText
            android:id="@+id/emailTxt"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:hint="Email"
            android:imeActionId="@+id/login"
            android:imeActionLabel="Sign In"
            android:imeOptions="actionUnspecified"
            android:inputType="textEmailAddress"
            android:maxLines="1"
            android:singleLine="true"
            android:textColor="@android:color/white"
            android:textColorHint="@android:color/white"
            tools:ignore="InvalidImeActionId" />

    </com.google.android.material.textfield.TextInputLayout>

    <com.google.android.material.textfield.TextInputLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content">

        <EditText
            android:id="@+id/passwordTxt"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:hint="Password"
            android:imeActionId="@+id/login"
            android:imeActionLabel="Sign In"
            android:imeOptions="actionUnspecified"
            android:inputType="textPassword"
            android:maxLines="1"
            android:singleLine="true"
            android:textColor="@android:color/white"
            android:textColorHint="@android:color/white"
            tools:ignore="InvalidImeActionId" />

    </com.google.android.material.textfield.TextInputLayout>

    <Button
        android:id="@+id/loginBtn"
        style="?android:textAppearanceSmall"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:background="@color/colorAccent"
        android:text="Login"
        android:textColor="@android:color/white"
        android:textStyle="bold" />

</LinearLayout>

So in our LoginActivity, here is a simple validation of email/password fields:

    private fun validate(): Boolean {
        if (emailTxt.text.isNullOrEmpty() || emailTxt.text.isBlank()){
            emailTxt.error = "Invalid Value"
            return false;
        }
        if (passwordTxt.text.isNullOrEmpty() || passwordTxt.text.isBlank()){
            passwordTxt.error = "Invalid Value"
            return false;
        }
        return true
    }

Then here is how we login, by invoking the method we had defined in our ViewModel class:

    private fun login() {
        if (!validate()){
            return
        }
        photosViewModel().login(emailTxt.text.toString(),passwordTxt.text.toString()).observe(this, Observer {
            if (makeRequest(it,"LOGIN")== Constants.SUCCEEDED){
                CacheManager.CURRENT_USER= FirebaseAuth.getInstance().currentUser?.email!!;
                if (PermissionManager.isLoggedIn){
                    openPage(HomeActivity::class.java)
                    finish()
                }else{
                    show("Something is wrong. Email is null or empty")
                }
            }
        })
    }

You can see that if the login succeeds we open the home activity then finish the current activity. Also every important is the below line:

                CacheManager.CURRENT_USER= FirebaseAuth.getInstance().currentUser?.email!!;

We caching the user email in memory. We cache it statically. This will allow us to use the email as the identifier of an uploader. We will need that identifier when uploading. We will also need it when filtering a given family member’s uploads.

In the demo, we are going to provide an email and password for you to test the app. Just download the apk and run it. You will find email and password already set up for you. We do this inside the onResume():

        emailTxt.setText(Constants.FAMILY_MEMBER_1)
        passwordTxt.setText("123456")

By the way you won’t need to be signing into the app all the time. Even if you restart the app, you will be auto-logged in even without internet connectivity. Firebase Authentication has the capability to cache a firebase user in the device. So we will simply be checking if we have an already cached user. If so then we simply load him and proceed to the HoemActivity:

        if(FirebaseAuth.getInstance().currentUser != null){
            CacheManager.CURRENT_USER= FirebaseAuth.getInstance().currentUser!!.email!!
            openPage(HomeActivity::class.java)
            finish()
        }

Creating Current User Account Page

Because this is a gallery app for Family Members. We want every family member to be able to view his account. In his account listed are his email address and the uploads he’s made. We will show different sections beautifully using TabLayout.

Showing his email is pretty simple. We have cached the current user in memory so we can just obtain his email address and render it in a textview:

    override fun onResume() {
        super.onResume()

        emailTV.text = CacheManager.CURRENT_USER
    }

Then we will filter the current user’s posts or uploads using firebase query apis. We can obtain the method to do so by invoking the filter method defined in our ViewModel class:

    private fun downloadMyPhotos(){
        photosViewModel().filter(CacheManager.CURRENT_USER).observe(this, Observer { r ->
            if (makeRequest(r, "PHOTOS DOWNLOAD") == Constants.SUCCEEDED) {
                val photos: ArrayList<Photo> = r.photos as ArrayList<Photo>
                if (photos.isNotEmpty()) {
                    createStateCard(
                        "Successfully Fetched My Photos",
                        "I have "+photos.size.toString() + " Photos",
                        isShowing = true,
                        isLoading = false,
                        STATE = Constants.SUCCEEDED
                    )
                    setupRecyclerView(photos)

                } else {
                    createStateCard(
                        "Successfully Connected",
                        "However it seems you haven't posted any photo",
                        isShowing = true,
                        isLoading = false,
                        STATE = Constants.SUCCEEDED
                    )
                }
            }
        })

    }

We are also giving the user the ability to delete his or her uploads.He can delete them simply by clicking a button next to the item:

                binding.mDeleteBtn.setOnClickListener {
                    photosViewModel().delete(n).observe(this@AccountActivity, Observer { r ->
                        if(makeRequest(r,"PHOTO DELETE")==Constants.SUCCEEDED){
                            show("Deleted Successfully")
                            downloadMyPhotos()
                        }
                    })
                }

test

Download

 

Download the APK below:

Download  and Test APK