If you know my man Sam Stern who’s popped up a couple times around my Reddit, you probably heard his talk with Kiana McNellis on Integrating back-end systems with Firebase for better app management. This sparked an idea for what could be a new series here on this blog, Firebase In Use where I go over frequently used app implementations of Firebase for you to learn and implement in your own app.

And what better way for me to start that than to do what I do best: copy and wash... except it won’t be! Correct me if I’m wrong, but the use of the Firebase Admin SDK allowed the setting of specific users as Admins via the server… but this ain’t so dynamic.

This is going to be my version of the integrating back-end systems talk that the Firebase guys did, but this will be dynamic. This will allow user roles to change as they use the app… and this is a challenge to my boys at Firebase. Who’s done it better? Allow me to demand £100, three cups of coffee and a part-time job there if I win the vote.

 

The App we’re making

We’re going to making a simple group management panel, like the ones of group chats in Messenger where users can be assigned one of two roles:

Admin – These have the power to invite, kick, and change the role of other users, on top of anything a normal user can do.

User – These have the power to view the other members of the group.

The Dependencies we’re importing

Of course, I expect that you’ve already connected your app to Firebase. If you haven’t, what are you doing here? Just go…

implementation 'com.google.firebase:firebase-auth:16.1.0'
implementation 'com.google.firebase:firebase-firestore:17.1.3'
implementation 'com.firebaseui:firebase-ui-auth:4.3.0'

On top of that, import the Authentication, Firestore, and FirebaseUI-Auth dependencies above. We won’t be needing anything else. In fact, we’re just using FirebaseUI so we don’t have to make our own authentication screen. Otherwise, you can do without it

Also make sure Email/Password provider is enabled in your Firebase Console.

The Things you’re… copy-pasting

Do you really want me to explain every single list view and text view here? That’s tedious for both us. Just create these classes and layouts and dump them in your app.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ListView
        android:id="@+id/lv_groups"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:layout_gravity="bottom|end"
        android:src="@drawable/ic_add_white"
        android:layout_margin="16dp"/>

</FrameLayout>

list_item_member.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp">

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:textColor="#424242" />

    <ImageView
        android:id="@+id/iv_menu"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:src="@drawable/ic_more_vert_grey"
        android:layout_gravity="end|center_vertical" />

</FrameLayout>

menu_main.xml

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

    <item
        android:id="@+id/ic_sign_out"
        android:title="Sign Out" />

</menu>

menu_user.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/ic_delete_user"
        android:title="Delete user"
        android:orderInCategory="50"
        app:showAsAction="never"/>

</menu>

User.kt

class User(val id: String, val role: Int)

UsersAdapter.kt

class UsersAdapter(context: Context, val resource: Int, val users: ArrayList<User>): ArrayAdapter<User>(context, resource, users) {

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val user = users[position]
        var view = convertView

        if (view == null) {
            view = LayoutInflater.from(context).inflate(resource, parent, false)
        }

        // Prepare the text and the popup menu
        view!!.tv_name.text = user.id
        view.iv_menu.setOnClickListener {
            val popup = PopupMenu(context, it)
            popup.menuInflater.inflate(R.menu.menu_user, popup.menu)
            popup.setOnMenuItemClickListener { item ->
                // TODO: Add code later
                true
            }
            popup.show()
        }

        return view
    }

}

The Code you’re actually writing

First things first, let’s get authentication out the way. We start with this in MainActivity

MainActivity.kt

class MainActivity : AppCompatActivity() {

    val RC_SIGN_IN = 0

    val auth = FirebaseAuth.getInstance()
    var providers = Arrays.asList(AuthUI.IdpConfig.EmailBuilder().build())

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (auth.currentUser == null) { launchAuthentication() }
    }

    private fun launchAuthentication() {
        startActivityForResult(AuthUI.getInstance()
                .createSignInIntentBuilder()
                .setAvailableProviders(providers)
                .build(), RC_SIGN_IN)
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        // Sign Out and Restart Activity
        if (item?.itemId == R.id.ic_sign_out) {
            val intent = Intent(this, MainActivity::class.java)
            finish()
            startActivity(intent)
        }

        return true
    }
}

To explain things a bit, this launches the FirebaseUI Auth if the user isn’t currently authenticated. It also handles inflating the menu which contains a sign out function. That just about wraps up the authentication part.

Adding New Group (FAB)

Create a new activity called GroupActivity where we’ll handle the creation and viewing of groups. Let the FAB in MainActivity send the intent. Note the extra with the intent.

MainActivity.kt (Snippet)

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Launch authentication if not logged in
        if (auth.currentUser == null) { 
            launchAuthentication() 
            return
        }

        // FAB Listener to create a new group
        fab.setOnClickListener {
            val intent = Intent(this, GroupActivity::class.java)
            intent.putExtra("INTENT_NEW_GROUP", true)
            startActivity(intent)
        }
    }

Now in GroupActivity, write a new function createNewGroup() that creates a new group under the “groups” collection of Firestore and stores the current user as an admin of the group. Let it run only if the intent extra is given.

GroupActivity.kt

class GroupActivity : AppCompatActivity() {

    val firestore = FirebaseFirestore.getInstance()
    val auth = FirebaseAuth.getInstance()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_group)

        if (intent.hasExtra("INTENT_NEW_GROUP")) { createNewGroup() }
    }

    private fun createNewGroup() {
        // Prepare an empty map for a new group document
        val groupMap = HashMap<String, Any>()

        // Prepare a new map for the current user as an admin (int 0)
        val userMap = HashMap<String, Any>()
        userMap["id"] = auth.uid!!
        userMap["role"] = 0

        // Add the group map and then the user map
        firestore.collection("groups").add(groupMap)
                .addOnSuccessListener {
                    // Init the users subcollection
                    it.collection("users").document(auth.uid!!).set(userMap)

                    // Add the group to the user's group subcollection
                    val groupMap = HashMap<String, Any>()
                    groupMap["id"] = it.id
                    firestore.collection("users").document(auth.uid!!)
                            .collection("groups").document(it.id).set(groupMap)
                }
    }
}

As soon as the group is created, I also add the same group to the user’s subcollection so that the groups can show up in the user’s MainActivity.

It’s all looking good in our database. So that we can go back into the groups we created, we’ll go back into MainActivity and add the function getGroupsList() and invoke at the end of your OnCreate method.

MainActivity.kt (Snippet)

private fun getGroupsList() {
        firestore.collection("users").document(auth.uid!!)
                .collection("groups").addSnapshotListener { groupSnapshots, firebaseFirestoreException ->
                    for (group in groupSnapshots!!) { groups.add(group.id) }

                    val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, groups)
                    lv_groups.adapter = adapter
                    lv_groups.setOnItemClickListener { adapterView, view, i, l ->
                        val intent = Intent(this, GroupActivity::class.java)
                        intent.putExtra("INTENT_ID", groups[i])
                        startActivity(intent)
                    }
                }
    }

And now we have our groups showing in the MainActivity.

Now we have the user as an admin, now we need a way to invite other users to fill in the “user” role. (On another note, ignore the skewed FAB).

I changed the FAB in MainActivity to instead show a menu dialog that gives the choice of creating and joining a group. (Yes, I made it so you have to enter the group ID to join. Yes, I’m sadistic. Face it)

MainActivity.kt (Snippet)

private fun launchAddGroupDialog() {
        val items = arrayOf("Create New Group", "Join Group")
        AlertDialog.Builder(this)
                .setItems(items) { dialogInterface, i ->
                    when(i) {
                        0 -> { createNewGroup() }
                        1 -> { joinGroup() }
                    }
                }
                .show()
    }

    private fun createNewGroup() {
        val intent = Intent(this, GroupActivity::class.java)
        intent.putExtra("INTENT_NEW_GROUP", true)
        startActivity(intent)
    }

    private fun joinGroup() {
        val editText = EditText(this)
        editText.tag = "groupid"
        AlertDialog.Builder(this)
                .setTitle("Enter Group ID")
                .setView(editText)
                .setNegativeButton("Cancel", null)
                .setPositiveButton("Join") { dialogInterface, i ->
                    val groupID = editText.text.toString()
                    val intent = Intent(this, GroupActivity::class.java)
                    intent.putExtra("INTENT_ID", groupID)
                    intent.putExtra("INTENT_JOINING_GROUP", true)
                    startActivity(intent)
                }
                .show()
    }

Now in GroupActivity, we’re going to get this ID and use it to inflate a list using the UsersAdapter we created much earlier.

activity_group.xml

<?xml version="1.0" encoding="utf-8"?>
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/lv_users"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".GroupActivity" />

GroupActivity.kt (Snippet)

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_group)

        val groupID = intent.getStringExtra("INTENT_ID")
        if (intent.hasExtra("INTENT_NEW_GROUP")) { createNewGroup() }
        else { loadGroup(groupID) }
    }

    private fun loadGroup(groupID: String) {
        firestore.collection("groups").document(groupID).collection("users")
                .addSnapshotListener { userSnapshot, firebaseFirestoreException ->
                    val users = ArrayList<User>()

                    if (userSnapshot == null || userSnapshot.isEmpty) {
                        Toast.makeText(this, "Group does not exist", Toast.LENGTH_SHORT).show()
                        finish()
                        return@addSnapshotListener
                    }

                    for (user in userSnapshot) { users.add(User(user.id, (user.get("role") as Long).toInt())) }
                    val adapter = UsersAdapter(this, R.layout.list_item_member, users)
                    lv_users.adapter = adapter
                }
    }

We’re almost there! In GroupActivity, we can now see the list of members, but joining a group only opens it up and doesn’t add the user to that group. Let’s go fix that.

I created the function joinGroup() to add the user to the group document and the group to the user document. This time, the user is added as a “user” instead of an admin.

GroupActivity.kt (Snippet)

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_group)

        val groupID = intent.getStringExtra("INTENT_ID")
        if (intent.hasExtra("INTENT_NEW_GROUP")) { createNewGroup() }
        else {
            if (intent.hasExtra("INTENT_JOINING_GROUP")) { joinGroup(groupID) }
            loadGroup(groupID)
        }
    }

private fun joinGroup(groupID: String) {
        // Add the user with role as user (int 1)
        val usersMap = HashMap<String, Any>()
        usersMap["id"] = auth.uid!!
        usersMap["role"] = 1
        firestore.collection("groups").document(groupID)
                .collection("users").document(auth.uid!!).set(usersMap)

        // Add the group to the user's group subcollection
        val groupMap = HashMap<String, Any>()
        firestore.collection("users").document(auth.uid!!).collection("groups")
                .document(groupID).set(groupMap)
    }

Now we can have other users added into the group as, well, users.

Now our roles are set, but they don’t do anything. Let’s go into UsersAdapter and add a delete user function there.

UsersAdapter.kt (Snippet)

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
            ...
            popup.setOnMenuItemClickListener { item ->
                when(item.itemId) {
                    R.id.ic_delete_user -> { deleteUser(users[position].id) }
                }
                true
            }
            ...
    }

    private fun deleteUser(uid: String) {
        val role = (context as GroupActivity).role
        val groupID = (context as GroupActivity).groupID

        if (role != 0) {
            Toast.makeText(context, "You do not have permission to do this", Toast.LENGTH_SHORT).show()
            return
        }

        // Delete group from users ref
        firestore.collection("users").document(uid)
                .collection("groups").document(groupID).delete()

        // Delete user from group ref
        firestore.collection("groups").document(groupID)
                .collection("users").document(uid).delete()
    }

GroupActivity.kt (Snippet)

    var role = -1
    var groupID = ""

    private fun joinGroup(groupID: String) {
        // Add the user with role as user (int 1)
        val usersMap = HashMap<String, Any>()
        usersMap["id"] = auth.uid!!
        usersMap["role"] = 1
        ...
    }

Now when I try to delete a user from a user role, I get this message.

But doing the same on another account with the permissions allows the delete to go through…. And that’s roles!

The Message I’m Concluding With

If you want to see the full app and tinker with it yourself, I shared it on Github. You’re welcome.

So that’s Firebase In Use, an installment to this blog that would be interesting for me at the very least. It’s really out of my comfort zone to make these demo apps but if that’s the best way to go about certain topics and use cases, so be it. We both learn.

So how was it? Was it too much code and too little explaining? Just right perhaps? Let me know. How about the series in general? Do you want to see more Firebase In Use?

 

Newsletter

Subscribe to the Newsletter