Appwrite, Android and Realtime

jakebarnby

Jake Barnby

Posted on September 6, 2021

Appwrite, Android and Realtime

🤔 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.

Social Media posts (7)

🤖 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:

  1. Click on the Add Collection button
  2. Inside the dialog:
    1. Set the collection name to Products
    2. 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

Products Collection

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'
Enter fullscreen mode Exit fullscreen mode

➕️ 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
)
Enter fullscreen mode Exit fullscreen mode

⚒️ 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. The ProductAdapter, which will handle creating and binding a view for each Product as they're added to the database. The DiffUtil.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 on DiffUtil 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)
            }
        )
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. The ProductViewHolder which describes a single Product view and metadata about it's position in the RecyclerView:
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)
    }
}
Enter fullscreen mode Exit fullscreen mode

💆 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)
    }
}


Enter fullscreen mode Exit fullscreen mode

🏎️ 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.

Products GIF

🥂 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

💖 💪 🙅 🚩
jakebarnby
Jake Barnby

Posted on September 6, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Appwrite, Android and Realtime
android Appwrite, Android and Realtime

September 6, 2021