Appwrite, Android and Realtime

Appwrite, Android and Realtime:

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'

āž•ļø 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:




    

    


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:




    

    

    

    


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()
    val productStream: LiveData = _productStream

    private val _productDeleted = MutableLiveData()
    val productDeleted: LiveData = _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) {
        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:

  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(object : DiffUtil.ItemCallback() {
        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)
            }
        )
    }
}
  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)
    }
}

šŸ’† 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()

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

        val button = findViewById(R.id.btnSubscribe)
        button.setOnClickListener {
            viewModel.subscribeToProducts(this)
        }

        val adapter = ProductAdapter()
        val recycler = findViewById(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.

from Tumblr https://generouspiratequeen.tumblr.com/post/661648134147014656

Leave a comment