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?