Android Plumbing to Show Data
If you are coming from the world of web app development, you may be familiar with the MVC design pattern. Android + Kotlin has what appears to be a D(LA)A or Data(Layout and Activity)Adapter pattern.
Layout: Like front end of View: This controls the appearance of the app. It is written in XML. Unlike the other elements (that are all stored in java > your.project.name folder), the Layouts are stored in the res > layout folder.
Activity: Like back end of View: Backing Logic for the Layout (UI) (kind of like a code behind from back in the early C# days) - and strangely this is where you actually retrieve the data (not in the Data Class).
Adapter: like a Controller: Connects the Activity to the Data Class - gets data from Data Class, implements setOnClickListener
Data Class: like a Model: Defines the data structures, but the actual data acquisition (e.g., from Firestore( seems to be in the Activity).
For starters, we will build one data class, one adapter, and two primary Layouts+Activities beyond the activity_main: activity_scoops_list and activity_scoops_detail (technically, you build the Activities and Android Studio gives you the option to auto-generate the Layout). Let's go.
Note that a Scoop is the basic data object that we are working with. You can think of a Scoop as, well, anything that you want. You will see what it is if you follow along on future blogs :)
Start by Creating the Core Activities
Add a button to activity_main.xml with an id of scoopsButton
Note: if you are getting a warning about "This view is not constrained, it only has designtime positions, so it will jump to (0,0) unless you add constraints" (and your buttons are overlapping), this is a simple fix of clicking the magic wand icon ("Infer Constraints").
Right-click on the directory that has your MainActivity file, and click New > Activity > Empty Activity
Activity Name = ScoopList, select "Generate a Layout File" and click Finish
- Add this code to your onCreate function of your MainActivity.kt
val scoopsButton = findViewById<Button>(R.id.scoopsButton) as Button
scoopsButton.setOnClickListener {
Toast.makeText(this, "You scooped!", Toast.LENGTH_LONG).show()
val intent = Intent(this, ScoopList::class.java)
startActivity(intent)
}
Create a ScoopsDetail activity.
Just to verify you have everything wired, go ahead and add a button to your activity_scoop_list.xml that has an on click listener to your activity_scoop_detail.xml
- You will eventually delete this as this transition to the ScoopDetail will happen when a user clicks a Scoop from the ScoopList.
Assuming you got the list and detail pages and transitions to work, it is time to build the UI that will support the data. In Android, the list is called a Recycler View and it has a CardView. Let's build those now.
- Drag a RecyclerView to your onto your activity_scoop_list.xml. It should look like this now (note that I included a button - a little bit of an advance peek at what we will do next with adding content).
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ScoopList">
<Button
android:id="@+id/btnNewScoop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="New Scoop"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="5dp"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/scoopRecyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="1dp"
android:layout_marginStart="5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/btnNewScoop"
app:layout_constraintTop_toBottomOf="@id/btnNewScoop"
app:layout_constraintVertical_bias="1.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
Right-click on your layouts tap and select New > Layout Resource File
Enter scoop_item as the Name and type CardView as the root
- Add the following code to define the UI layout of each scoop_item
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_margin="16dp"
app:cardElevation="8dp"
app:cardCornerRadius="8dp"
>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginVertical="6dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Scoop Detail"
android:textSize="16dp" />
<TextView
android:id="@+id/tbFirstName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="Babysitting Coop"
android:textSize="16dp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
Now we are going to add the data class:
Right-click on the folder containing your Activities and click New > Kotlin Class/File
Call the class "Scoop" and add the following line to get started. I am not sure if this Parcelable is useful or not. The thinking is that it allows you to just write a Scoop object to say an intent, instead of having to write each of the separate properties as extra elements. May be more trouble than it is worth, but it does make the intent invocations cleaner.
@Parcelize
data class Scoop(
var scoopId: String? = null,
var scoopName: String? = null,
var scoopManager: String? = null,//uid for user
var description: String? = null,
var timestamp: Timestamp? = null,
val buyers: List<Buyer>? = null,
val sellers: List<Seller>? = null,
) : Parcelable
Finally, create an adapter that wires up the Class to the RecyclerView List
- The Adapter seems to be a bit light in function, responsible only for adding the onClickListeners. I may be doing something incorrectly here by not pulling the actual Firestore data in from the Data class (which would mean the Adapter would be doing some piping between the Data class and the Activities). Here is what my Adapter looks like:
class ScoopAdapter(private val scoopList: ArrayList<Scoop>, val clickListener: (Scoop) -> Unit) : RecyclerView.Adapter<ScoopAdapter.MyViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.scoop_item, parent, false)
return MyViewHolder(itemView)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val scoop : Scoop = scoopList[position]
holder.scoopName.text = scoop.scoopName
// Calling the clickListener sent by the constructor
holder?.itemView?.setOnClickListener { clickListener(scoop) }
}
override fun getItemCount(): Int {
return scoopList.size
}
public class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
{
val scoopName : TextView = itemView.findViewById(R.id.tbScoopName)
}
}
At this point, we have built the basic plumbing elements for the "list" parts of the list-detail Android application. Up next, we will add the detail page (and the create new Scoops page as well). Stay tuned.
More Info
Here is some additional content that you may find useful in getting your initial plumbing set up, and let me know if you have any problems getting this set up.
https://www.youtube.com/watch?v=Ly0xwWlUpVM&t=94s
Now the table is set and we are ready to get dangerous: start pulling data from Firestore.