Appwrite, Android and Realtime
Jake Barnby
Posted on September 6, 2021
🤔 What is Appwrite?
Appwrite is a new open-source, end-to-end, backend server for web and mobile developers that allows you to build apps much faster. It abstracts and simplifies common development tasks behind REST APIs and tools, helping you build advanced apps faster.
🤖 Appwrite for Android
Appwrite provides sleek REST APIs for various common features that are required for web and mobile application development such as cloud functions, database, storage, as well as realtime support for each service so that we as developers can focus on our applications rather than on backend implementations. This makes Appwrite very suitable for those of us who want to build Android applications. In this tutorial we'll build an Android realtime product application using Appwrite and the new realtime API, let's get started.
📝 Prerequisites
In order to continue with this tutorial, you need to have access to an Appwrite console, and either an existing project, or permission to create one. If you have not already installed Appwrite, please do so. Installing Appwrite is really simple following Appwrite's official installation docs. Installation should only take around 2 minutes. Once installed, login to your console and create a new Project.
💾 Database Setup
In the Appwrite console, let's select the project that we will be using for our Android app. If you don't have a project yet, you can easily create one by clicking on the Create Project button. Once inside, select Database from the left sidebar. On the database page:
- Click on the Add Collection button
- Inside the dialog:
- Set the collection name to Products
- Click the Create button
This will then create a collection, and you will be redirected to the new collection's page where we can define its rules. Define the following rules, then click the Update button.
-
Name
- label: Name
- Key: name
- Rule Type: Text
- Required: true
- Array: false
-
Description
- label: Description
- Key: description
- Rule Type: Text
- Required: true
- Array: false
-
SKU
- label: SKU
- Key: sku
- Rule Type: Text
- Required: true
- Array: false
-
Price
- label: Price
- Key: price
- Rule Type: Numeric
- Required: true
- Array: false
-
Image URL
- label: Image URL
- Key: imageUrl
- Rule Type: Text
- Required: true
- Array: false
Now that the collection is created, we can move on to setting up the Android application.
⚙️ Setup Android Project and Dependencies
Using Android Studio, create a new Android Application project choosing the Empty Activity template. Once the project is created, add the following dependencies to your app's build.gradle(.kts)
file:
// Appwrite
implementation("io.appwrite:sdk-for-android:0.2.0")
// Appcompat, LiveData, ViewModel and Activity extensions
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
implementation 'androidx.activity:activity-ktx:1.3.1'
// JSON
implementation 'com.google.code.gson:gson:2.8.7'
// Image loading
implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'com.github.bumptech.glide:compiler:4.12.0'
➕️ Add Android Platform
To initialize the Appwrite SDK and start interacting with Appwrite services, you first need to add a new Android platform to your project. To add a new platform, go to your Appwrite console, select your project (or create one if you haven't already), and click the Add Platform button on the project Dashboard.
From the options, choose to add a new Android platform and add your app credentials.
Add your app name and package name. Your package name is generally the applicationId
in your app-level build.gradle
file. You may also find your package name in your AndroidManifest.xml
file. By registering a new platform, you are allowing your app to communicate with the Appwrite API.
🧱 Create the Product Model
We now need to create a model to represent a product on the Android side. Create a new Kotlin file Product.kt
and declare a simple data class:
data class Product(
val name: String,
val sku: String,
val price: Double,
val imageUrl: String
)
⚒️ Building Views
Now, open your app/src/main/res/layout/activity_main.xml
and update the layout as following:
<?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=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerProducts"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toTopOf="@id/btnSubscribe"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:spanCount="3"/>
<Button
android:id="@+id/btnSubscribe"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="Subscribe to products"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/recyclerProducts"/>
</androidx.constraintlayout.widget.ConstraintLayout>
You'll notice that the activity is quite simple; we only have a Button
to subscribe and a RecyclerView
. The RecyclerView
will be used to display the product collection in realtime as we add new products. We'll now need to define a separate view that will be used to represent each of the individual products.
Create a new layout from File > New > Layout Resource File
, name it item_product.xml
and add the following:
<?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"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_margin="2dp">
<ImageView
android:id="@+id/imgProduct"
android:layout_width="match_parent"
android:layout_height="120dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<View
android:layout_width="match_parent"
android:layout_height="34dp"
android:alpha="0.6"
android:background="@color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="@+id/txtName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="@+id/txtPrice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
All the pieces of our RecyclerView
are in place, and we can move on to setting up the ViewModel
, where most of the heavy lifting happens.
👩🔧 Create View Model
Create app/src/main/java/com/example/realtimestarter/RealtimeViewModel.kt
and update it with following code. Make sure to replace all the property values near the top of the file with your own values which can be found in your Appwrite console.
package io.appwrite.realtimestarter
import android.content.Context
import android.util.Log
import androidx.lifecycle.*
import io.appwrite.Client
import io.appwrite.extensions.toJson
import io.appwrite.models.RealtimeResponseEvent
import io.appwrite.models.RealtimeSubscription
import io.appwrite.services.Account
import io.appwrite.services.Database
import io.appwrite.services.Realtime
import kotlinx.coroutines.launch
class RealtimeViewModel : ViewModel(), LifecycleObserver {
private val endpoint = "YOUR_ENDPOINT" // Replace with your endpoint
private val projectId = "YOUR_PROJECT_ID" // Replace with your project ID
private val collectionId = "YOUR_COLLECTION_ID" // Replace with your product collection ID
private val realtime by lazy { Realtime(client!!) }
private val account by lazy { Account(client!!) }
private val db by lazy { Database(client!!) }
private val _productStream = MutableLiveData<Product>()
val productStream: LiveData<Product> = _productStream
private val _productDeleted = MutableLiveData<Product>()
val productDeleted: LiveData<Product> = _productDeleted
private var client: Client? = null
var subscription: RealtimeSubscription? = null
private set
fun subscribeToProducts(context: Context) {
buildClient(context)
viewModelScope.launch {
// Create a session so that we are authorized for realtime
createSession()
// Attach an error logger to our realtime instance
realtime.doOnError { Log.e(this::class.java.name, it.message.toString()) }
// Subscribe to document events for our collection and attach the handle product callback
subscription = realtime.subscribe(
"collections.${collectionId}.documents",
payloadType = Product::class.java,
callback = ::handleProductMessage
)
//createDummyProducts()
}
}
private fun handleProductMessage(message: RealtimeResponseEvent<Product>) {
when (message.event) {
in
"database.documents.create",
"database.documents.update" -> {
_productStream.postValue(message.payload!!)
}
"database.documents.delete" -> {
_productDeleted.postValue(message.payload!!)
}
}
}
private suspend fun createDummyProducts() {
// For testing; insert 100 products while subscribed
val url = "https://dummyimage.com/600x400/cde/fff"
for (i in 1 until 100) {
db.createDocument(
collectionId,
Product("iPhone $i", "sku-$i", i.toDouble(), url).toJson(),
listOf("*"),
listOf("*")
)
}
}
private fun buildClient(context: Context) {
if (client == null) {
client = Client(context)
.setEndpoint(endpoint)
.setProject(projectId)
}
}
private suspend fun createSession() {
try {
account.createAnonymousSession()
} catch (ex: Exception) {
ex.printStackTrace()
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun closeSocket() {
// Activity is being destroyed; close our socket connection if it's open
subscription?.close()
}
}
The ViewModel
contains a function to call the realtime API and subscribe to notifications for create/update/delete events relating to any documents within the collection with id collectionId
, that is also visible to our user.
To allow observing the incoming realtime updates from outside itself, the ViewModel
also exposes the LiveData
property productStream
, which we'll utilize in our Activity
later on to get realtime updates in our RecyclerView
.
♻️ Recycler View
There's 2 more files we need to add to get our RecyclerView
working:
- The
ProductAdapter
, which will handle creating and binding a view for eachProduct
as they're added to the database. TheDiffUtil.ItemCallback
provided in the constructor will be used to calculate list update diffs on a background thread, then post any changes on the UI thread, perfect for realtime! You can find more information onDiffUtil
here.
package io.appwrite.realtimestarter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
class ProductAdapter :
ListAdapter<Product, ProductViewHolder>(object : DiffUtil.ItemCallback<Product>() {
override fun areItemsTheSame(oldItem: Product, newItem: Product) =
oldItem.sku == newItem.sku
override fun areContentsTheSame(oldItem: Product, newItem: Product) =
oldItem == newItem
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_product, parent, false)
return ProductViewHolder(view)
}
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
val item = currentList[position]
holder.setName(item.name)
holder.setPrice(item.price.toString())
holder.setProductImage(item.imageUrl)
}
fun submitNext(product: Product) {
val current = currentList.toMutableList()
val index = currentList.indexOfFirst {
it.sku == product.sku
}
if (index != -1) {
current[index] = product
} else {
current.add(product)
}
submitList(current)
}
fun submitDeleted(product: Product) {
submitList(
currentList.toMutableList().apply {
remove(product)
}
)
}
}
- The
ProductViewHolder
which describes a singleProduct
view and metadata about it's position in theRecyclerView
:
package io.appwrite.realtimestarter
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private var nameView: TextView = itemView.findViewById(R.id.txtName)
private var priceView: TextView = itemView.findViewById(R.id.txtPrice)
private var imageView: ImageView = itemView.findViewById(R.id.imgProduct)
fun setName(name: String) {
nameView.text = name
}
fun setPrice(price: String) {
priceView.text = "\$$price"
}
fun setProductImage(url: String) {
Glide.with(itemView)
.load(url)
.centerCrop()
.into(imageView)
}
}
💆 Activity
With everything else in place, lets tie it all together in our MainActivity
. Open app/src/main/java/com/example/realtimestarter/MainActivity.kt
and update like so:
package io.appwrite.realtimestarter
import android.os.Bundle
import android.widget.Button
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
class RealtimeActivity : AppCompatActivity() {
private val viewModel by viewModels<RealtimeViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_realtime)
val button = findViewById<Button>(R.id.btnSubscribe)
button.setOnClickListener {
viewModel.subscribeToProducts(this)
}
val adapter = ProductAdapter()
val recycler = findViewById<RecyclerView>(R.id.recyclerProducts)
recycler.adapter = adapter
viewModel.productStream.observe(this) {
adapter.submitNext(it)
}
viewModel.productDeleted.observe(this) {
adapter.submitDeleted(it)
}
lifecycle.addObserver(viewModel)
}
override fun onStop() {
super.onStop()
lifecycle.removeObserver(viewModel)
}
}
🏎️ Let's Get Realtime
That's it, all that's left is to run the application, then add some documents. When running on your emulator or device, click Subscribe to start listening for realtime updates.
Head back to your Appwrite console and navigate to the Products
collection we created earlier. From here, we can start adding new documents and see them appear in our app.
As soon you add a product in the console, you'll see them appear in the UI of your app! That's the real beauty of Appwrite Realtime, as shown below.
🥂 Conclusion
I hope you enjoyed building this Realtime Application with Appwrite and Android. The full source for this application is available in the Demo Realtime Application repository. Let us know if you have any feedback or suggestions. Looking forward to seeing what the community can create with Appwrite Realtime and Android!
🎓 Learn More
Posted on September 6, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.