DEV Community

Hardik Sachan
Hardik Sachan

Posted on

Introduction to Appwrite Cloud Functions with Android and Kotlin

Originally posted on hashnode

In this article, we'll take a look at Cloud Functions in appwrite.io and how to use them with Kotlin or Android.

Here's an overview of what we'll be doing.

  • We'll create a classic "Hello, World!" program with functions
  • We'll write a function to get Covid-19 stats from Covid19Api
  • We'll build an android app around the above function

Before we start, if you like to dive directly into the code, here's the github repo

Creating a new Appwrite project

Before we dive into creating functions, let's set up a new Appwrite project to which we will add our functions. If you already have an appwrite project you can skip this section.

To get Appwrite up and running, follow these instructions and create a new account.

Next, follow the onscreen instructions to create a new project -
New project name

Hello World with Appwrite Functions

This is a pretty basic project to implement but it will make us familiar with the process of creating a new Appwrite Function. Let's get started -

First of all, we need a new Kotlin project, I'm going to use Intellij Idea as an IDE with gradle build system.

Here's the configuration I'm using to create a new project -
New project config

Next, let's create a new file named Main.kt in hello-world/src/main/kotlin and put the following code in it -

fun main() {
    println("Hello, world!")
}
Enter fullscreen mode Exit fullscreen mode

...and we are done!

Now let's add this to an Appwrite function.

Appwrite requires us to create a "fat jar" which is just a fancy way of saying that we need to include all runtime dependencies in our jar file. To achieve this we have to add the following lines to build.gradle.kts -

tasks.withType<Jar>() {
    manifest {
        attributes["Main-Class"] = "MainKt"
    }

    from(sourceSets.main.get().output)

    dependsOn(configurations.runtimeClasspath)
    from({
        configurations.runtimeClasspath.get()
            .filter { it.name.endsWith("jar") }
            .map { zipTree(it) }
    })
}
Enter fullscreen mode Exit fullscreen mode

These lines basically instruct Gradle to build a "fat jar". Now to actually build a jar, we need to run the following task -

Build jar task

This will create the required jar file in hello-world/build/libs/hello-world-1.0-SNAPSHOT.jar

Now, to upload this to Appwrite we need to package this in a tar file. To do this run the following command inside the libs directory -

tar -zcvf code.tar.gz .\hello-world-1.0-SNAPSHOT.jar
Enter fullscreen mode Exit fullscreen mode

This will create a file named code.tar.gz in the libs directory. We will need this file later.

Creating a new Appwrite Function

Now, let's create a new appwrite function from the appwrite console. Navigate to Functions tab on Left pane, and click on Add Function button. Give it a name, in this case I'm using hello-world, and for the runtime, choose Java 16.0

Next, let's create a new deploy tag -

Hello World Deploy tag

Here's the Command -

java -jar hello-world-1.0-SNAPSHOT.jar
Enter fullscreen mode Exit fullscreen mode

Here we upload the tar file we created earlier. Click Activate and voila, we're done. :D

Let's test out the function. Click on Execute Now we don't need to pass any data to this function. Navigate to logs and check the output

Hello World successful execution

And you just created your first appwrite function. Cheers 🍻

A little upgrade

Now let's make this function a little more useful. Instead of printing Hello, world! all the time, we'll pass a name to this function.

A note on APPWRITE_FUNCTION_DATA and APPWRITE_FUNCTION_EVENT_DATA

As mentioned in the documentation, these environment variables contains the information pertinent to execution of a custom appwrite function.

We use APPWRITE_FUNCTION_DATA when we trigger the function through Appwrite console or via SDK or HTTP API. This variable contains the data passed in those executions.

APPWRITE_FUNCTION_EVENT_DATA is used when the function is triggered by some event like inserting a new document. This variable contains information regarding that event.

Let's get back to the upgrade

Now we need to read the name from the APPWRITE_FUNCTION_DATA variable, to do this we add a new function -

fun getNameFromEnv(): String =
    System.getenv("APPWRITE_FUNCTION_DATA")
Enter fullscreen mode Exit fullscreen mode

and update the println statement as follows -

println("Hello, ${getNameFromEnv()}!")
Enter fullscreen mode Exit fullscreen mode

Let's build the jar and add a new deploy tag to our appwrite console. You can follow the same instructions from above. And then execute the function passing you name as input.

Let's test it out.

Hello Hardik Input
Hello Hardik output

Yay! We're done. πŸš€

Display Covid-19 stats

By creating the previous project, we got familiar with the process of creating a new Appwrite function with Kotlin. Now, let's build another project which is a little more complex. In this project, we'll see how we can integrate an Appwrite Function with a third-party API. Let's get started.

We will be using Covid19Api to get the data.

Before we dive into implementing the function, let's take a quick look at the requirements of what we'll be building -

  • First, we need to read the country from APPWRITE_FUNCTION_DATA.
  • Now, we need to check if the country is valid.
  • If the country is valid, we return the stats for that country.
  • If the country is not valid, we return global stats, with a message indicating the country was not valid.

Let's first create a new project. I'll name it get-covid-stats. Then we need a Main.kt file in get-covid-stats/src/main/kotlin.

Now we need to read the country name from APPWRITE_FUNCTION_DATA. Let's do that -

fun readCountryFromEnv(): String = 
    System.getenv("APPWRITE_FUNCTION_DATA")
Enter fullscreen mode Exit fullscreen mode

and in main let's call it -

suspend fun main() {
    val country = readCountryFromEnv()
}
Enter fullscreen mode Exit fullscreen mode

Next, we need to install some dependencies. We need to make Network Requests for that, I'm going to use Ktor HTTP Client and to parse json let's use kotlinx.serialization. If you don't know these libraries, don't worry, I'll explain how they work. To install these dependencies add the following lines to build.gradle.kts -

dependencies {
    // ....
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0")
    implementation("io.ktor:ktor-client-core:1.6.4")
    implementation("io.ktor:ktor-client-cio:1.6.4")
    implementation("io.ktor:ktor-client-serialization:1.6.4")
}
Enter fullscreen mode Exit fullscreen mode

There is one more line we need to add for kotlinx.serialization to work properly. You can read more about it here. Let's add the following line to build.gradle.kts -

plugins {
    // ...
    kotlin("plugin.serialization") version "1.5.31"
}
Enter fullscreen mode Exit fullscreen mode

Next, we need some classes to hold responses we receive from Covid19Api. We'll save the classes in model package. Let's create them -

First, let's create an interface with data we need to return in get-covid-stats/src/main/kotlin/model/ICovidStats.kt

package model

interface ICovidStats {
    val newConfirmed: Int
    val totalConfirmed: Int
    val newDeaths: Int
    val totalDeaths: Int
    val newRecovered: Int
    val totalRecovered: Int
}
Enter fullscreen mode Exit fullscreen mode

Now, if we take a look at data returned from the endpoint we see we need three classes Response.kt, GlobalStats.kt and CountryStats.kt. Let's create them -

// GlobalStats.kt

package model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class GlobalStats(
    @SerialName("NewConfirmed") override val newConfirmed: Int,
    @SerialName("TotalConfirmed") override val totalConfirmed: Int,
    @SerialName("NewDeaths") override val newDeaths: Int,
    @SerialName("TotalDeaths") override val totalDeaths: Int,
    @SerialName("NewRecovered") override val newRecovered: Int,
    @SerialName("TotalRecovered") override val totalRecovered: Int,
) : ICovidStats
Enter fullscreen mode Exit fullscreen mode

@Serializable tells kotlinx.serialization that this class can be parsed to/from JSON and @SerialName is used to indicate the JSON field name.

// CountryStats.kt

package model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class CountryStats(
    @SerialName("Country") val country: String,
    @SerialName("CountryCode") val countryCode: String,
    @SerialName("Slug") val slug: String,
    @SerialName("NewConfirmed") override val newConfirmed: Int,
    @SerialName("TotalConfirmed") override val totalConfirmed: Int,
    @SerialName("NewDeaths") override val newDeaths: Int,
    @SerialName("TotalDeaths") override val totalDeaths: Int,
    @SerialName("NewRecovered") override val newRecovered: Int,
    @SerialName("TotalRecovered") override val totalRecovered: Int,
) : ICovidStats
Enter fullscreen mode Exit fullscreen mode
// Response.kt

package model

import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName

@Serializable
data class Response(
    @SerialName("Global") val global : GlobalStats,
    @SerialName("Countries") val countries : List<CountryStats>,
)
Enter fullscreen mode Exit fullscreen mode

Okay 🀯, these are the response models we need.

We also need an object which we will return from this function. Let's do that -

// FunctionResult.kt

package model

import kotlinx.serialization.Serializable

@Serializable
data class FunctionResult(
    val isGlobal: Boolean,
    val newConfirmed: Int,
    val totalConfirmed: Int,
    val newDeaths: Int,
    val totalDeaths: Int,
    val newRecovered: Int,
    val totalRecovered: Int,
)

Enter fullscreen mode Exit fullscreen mode

We will also need a JSON Parser, Let's set up kotlinx.serialization json Parser -

val jsonParser = Json {
    isLenient = true
    ignoreUnknownKeys = true
}
Enter fullscreen mode Exit fullscreen mode

Next, let's get the stats from the API. First, we need an HTTP Client to make the requests. Let's do that -

HttpClient() {
    install(JsonFeature) {
        serializer = KotlinxSerializer(json = jsonParser)
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, we also install JsonFeature which automatically parses the response to classes we created earlier. Now, let's use this client to make a get request to https://api.covid19api.com/summary.

HttpClient() {
    // ...
}.use { client ->
    val response: Response = client.get("https://api.covid19api.com/summary")
}
Enter fullscreen mode Exit fullscreen mode

Now let's get the country or global data from this and create a FunctionResult object -

val result: FunctionResult = response.countries.find {
    it.country.equals(country, ignoreCase = true) ||
            it.countryCode.equals(country, ignoreCase = true) ||
            it.slug.equals(country, ignoreCase = true)
}?.run {
    FunctionResult(
        false, newConfirmed, totalConfirmed, newDeaths,
        totalDeaths, newRecovered, totalRecovered
    )
} ?: response.global.run {
    FunctionResult(
        true, newConfirmed, totalConfirmed, newDeaths,
        totalDeaths, newRecovered, totalRecovered
    )
}
Enter fullscreen mode Exit fullscreen mode

Let's put this information in stdout -

println(jsonParser.encodeToString(result))
Enter fullscreen mode Exit fullscreen mode

Here's the complete code -

import io.ktor.client.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.request.*
import io.ktor.utils.io.core.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import model.FunctionResult
import model.Response

suspend fun main() {
    val country = readCountryFromEnv()

    val jsonParser = Json {
        isLenient = true
        ignoreUnknownKeys = true
    }

    HttpClient() {
        install(JsonFeature) {
            serializer = KotlinxSerializer(json = jsonParser)
        }
    }.use { client ->
        val response: Response = client.get("https://api.covid19api.com/summary")

        val result: FunctionResult = response.countries.find {
            it.country.equals(country, ignoreCase = true) ||
                    it.countryCode.equals(country, ignoreCase = true) ||
                    it.slug.equals(country, ignoreCase = true)
        }?.run {
            FunctionResult(
                false, newConfirmed, totalConfirmed, newDeaths,
                totalDeaths, newRecovered, totalRecovered
            )
        } ?: response.global.run {
            FunctionResult(
                true, newConfirmed, totalConfirmed, newDeaths,
                totalDeaths, newRecovered, totalRecovered
            )
        }

        println(jsonParser.encodeToString(result))
    }
}

fun readCountryFromEnv(): String =
    System.getenv("APPWRITE_FUNCTION_DATA")
Enter fullscreen mode Exit fullscreen mode

Whew, that was a lot of code. Let's add our function to appwrite console (see steps in above example) and test it out.

Get Covid Stats Input

iGet Covid Stats Output

Here's what it prints -

{"isGlobal":false,"newConfirmed":14623,"totalConfirmed":34108996,"newDeaths":197,"totalDeaths":452651,"newRecovered":0,"totalRecovered":0}
Enter fullscreen mode Exit fullscreen mode

Alright! In this article we learned the basics of Appwrite Function Service. In the next post, I'll show you how to connect an Appwrite Function to an android application. Stay tuned for that.

Discussion (0)