Getting started with Appwrite's Android SDK

One of the major highlights of Appwrite 0.9 is the official support for Android. We've also released a brand new Android SDK to go alongside it πŸ˜‰!

In this tutorial, we'll learn to setup Appwrite's Android SDK, interact with Appwrite's Accounts API and also learn to setup OAuth Logins in your App! We'll extensively use JetPack components like Fragments, ViewModels, Data Binding, LiveData and Kotlin Coroutines! I hope you're as excited as I am! Let's get started!

πŸ“ Prerequisites

At this stage, we assume that you already have an Appwrite instance up and running. If you do not have Appwrite setup yet, you can follow the super easy installation step over at appwrite.io. It's not a typo. There really is only 1 step!

You should have also setup an OAuth provider with Appwrite to be able to follow the OAuth section of this tutorial. You can learn to setup OAuth providers in Appwrite by going through our tutorial.

πŸ› οΈ Create a new Android Project

Setup your Android project and choose the Empty Activity Template. Give your project a name, Choose Android 6.0 ( Marshmallow ) as your minimum SDK and click Finish.

Now is also a good time to add our Android App as a platform in the Appwrite Console. Head over to your AndroidManifest.xml and locate the package name of your app. It should look something like com.example.myapplication.

In your Appwrite dashboard, click on Add Platform and select a New Android App. Give your app a name, add the package name and click Register.

Once this is complete, it's time to head back to our Android project add our dependencies.

πŸ‘· Setup Appwrite's Android SDK

Appwrite's Android SDK is hosted on Maven Central, so you'll first need to add the mavenCentral() repository to your project's build.gradle file ( if it's not already present ).

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}

Then add the Appwrite Android SDK to your app's build.gradle file.

dependencies {
    // ... Other dependencies
    implementation 'io.appwrite:sdk-for-android:0.0.1'
}

We'll also be using Data Binding and we need to enable that in our app's build.gradle file

android {
    // ...
    buildFeatures {
        dataBinding true
    }
}

Sync your gradle dependencies and if there are no errors, we're ready to proceed!

⌨️ Create helper classes

The first step is to initialise the Client() class from the Appwrite SDK. For this, we will create a new file utils/Client.kt where we will create a Singleton object to use across our App.

package com.example.myapplication.utils

import android.content.Context
import io.appwrite.Client

object Client {
    lateinit var client : Client

    fun create(context: Context) {
        client = Client(context)
             // Replace with your own endpoint and project ID
            .setEndpoint("https://demo.appwrite.io/v1")
            .setProject("6070749e6acd4")
    }
}

If your Appwrite setup is running on localhost, you will need to make sure localhost is accessible from your emulator by using a proxy. Else, you can replace localhost with your private IP address which you can find using the command hostname -I on UNIX systems.

If you're using Appwrite on localhost, you need to set the setSelfSigned(true) flag to true.

client = Client(context)
            .setEndpoint("https://192.168.1.35/v1")
            .setProject("6070749e6acd4")
            .setSelfSigned(true)

Next, we're going to create another utility class called utils/Event.kt. This utility class will allow us to handle LiveData Events ( Changes that need to be observed only once ).

package com.example.myapplication.utils

/**
 * Used as a wrapper for data that is exposed via a LiveData that represents an event.
 */
open class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

πŸ—οΈ Create the layouts

Now, we're going to update the layout in activity_main.xml to allow us to host fragments. Replace the contents of activity_main.xml with this snippet.

<?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.fragment.app.FragmentContainerView
        android:id="@+id/fragment_container_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Now, create layout file for our Fragment at layout/fragment_account.xml

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/constraint_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="16dp">

            <EditText
                android:id="@+id/responseTV"
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:background="@null"
                android:enabled="true"
                android:fadeScrollbars="false"
                android:focusable="true"
                android:longClickable="true"
                android:scrollbars="vertical"
                android:textIsSelectable="true"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <EditText
                android:id="@+id/email"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:hint="Email"
                android:inputType="textEmailAddress"
                android:text="[email protected]"
                app:layout_constraintStart_toStartOf="@id/responseTV"
                app:layout_constraintTop_toBottomOf="@id/responseTV" />


            <EditText
                android:id="@+id/password"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:hint="password"
                android:inputType="textPassword"
                android:text="testtest"
                app:layout_constraintStart_toStartOf="@id/email"
                app:layout_constraintTop_toBottomOf="@id/email" />

            <EditText
                android:id="@+id/name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="16dp"
                android:hint="name"
                android:inputType="text"
                app:layout_constraintStart_toStartOf="@id/password"
                app:layout_constraintTop_toBottomOf="@id/password" />

            <Button
                android:id="@+id/login"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:text="Login"
                app:layout_constraintEnd_toStartOf="@+id/signup"
                app:layout_constraintHorizontal_bias="0.5"
                app:layout_constraintHorizontal_chainStyle="spread"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/name" />

            <Button
                android:id="@+id/signup"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:text="Signup"
                app:layout_constraintEnd_toStartOf="@id/getUser"
                app:layout_constraintHorizontal_bias="0.5"
                app:layout_constraintStart_toEndOf="@+id/login"
                app:layout_constraintTop_toBottomOf="@+id/name" />

            <Button
                android:id="@+id/getUser"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:text="Get User"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="0.5"
                app:layout_constraintStart_toEndOf="@+id/signup"
                app:layout_constraintTop_toBottomOf="@+id/name" />

            <Button
                android:id="@+id/oAuth"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:text="Login with Facebook"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintEnd_toStartOf="@+id/logout"
                app:layout_constraintHorizontal_bias="0.5"
                app:layout_constraintHorizontal_chainStyle="spread"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/signup" />

            <Button
                android:id="@+id/logout"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Logout"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="0.5"
                app:layout_constraintStart_toEndOf="@+id/oAuth"
                app:layout_constraintTop_toTopOf="@id/oAuth" />

        </androidx.constraintlayout.widget.ConstraintLayout>
    </ScrollView>

</layout>

πŸ”¨ Create the ViewModel

Let's now create a ViewModel at ui/accounts/AccountsViewModel.kt to manage our app state.

package com.example.myapplication.ui.accounts

import android.text.Editable
import androidx.activity.ComponentActivity
import androidx.lifecycle.*
import com.example.myapplication.utils.Client.client
import com.example.myapplication.utils.Event
import io.appwrite.exceptions.AppwriteException
import io.appwrite.services.Account
import kotlinx.coroutines.launch
import org.json.JSONObject


class AccountsViewModel : ViewModel() {

    private val _error = MutableLiveData<Event<Exception>>().apply {
        value = null
    }
    val error: LiveData<Event<Exception>> = _error

    private val _response = MutableLiveData<Event<String>>().apply {
        value = null
    }
    val response: LiveData<Event<String>> = _response

    private val accountService by lazy {
        Account(client)
    }

    fun onLogin(email: Editable , password : Editable) {
        viewModelScope.launch {
            try {
                var response = accountService.createSession(email.toString(), password.toString())
                var json = response.body?.string() ?: ""
                json = JSONObject(json).toString(8)
                _response.postValue(Event(json))
            } catch (e: AppwriteException) {
                _error.postValue(Event(e))
            }
        }

    }

    fun onSignup(email: Editable , password : Editable, name: Editable) {
        viewModelScope.launch {
            try {
                var response = accountService.create(email.toString(), password.toString(), name.toString())
                var json = response.body?.string() ?: ""
                json = JSONObject(json).toString(2)
                _response.postValue(Event(json))
            } catch (e: AppwriteException) {
                _error.postValue(Event(e))
            }
        }

    }

    fun onGetUser() {
        viewModelScope.launch {
            try {
                var response = accountService.get()
                var json = response.body?.string() ?: ""
                json = JSONObject(json).toString(2)
                _response.postValue(Event(json))
            } catch (e: AppwriteException) {
                _error.postValue(Event(e))
            }
        }
    }

    fun onLogout() {
        viewModelScope.launch {
            try {
                var response = accountService.deleteSession("current")
                var json = response.body?.string()?.ifEmpty { "{}" }
                json = JSONObject(json).toString(4)
                _response.postValue(Event(json))
            } catch (e: AppwriteException) {
                _error.postValue(Event(e))
            }
        }
    }

}

Our ViewModel has 2 LiveData Objects: One to keep track of errors and one to keep track of the response.

We also have 4 functions

  • onLogin - onClick handler for the Login Button
  • onSignup - onClick handler for the Signup Button
  • onLogout - onClick handler for the Logout Button
  • onGetUser - onClick handler for the Get User Button

πŸ“ Create the Fragment

Awesome! Let's move on to create our Fragment at ui/accounts/AccountsFragment.kt

package com.example.myapplication.ui.accounts

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.example.myapplication.R
import com.example.myapplication.databinding.FragmentAccountBinding


class AccountsFragment : Fragment() {

    private lateinit var binding: FragmentAccountBinding
    private lateinit var viewModel: AccountsViewModel

    override fun onCreateView(
        inflater: LayoutInflater ,
        container: ViewGroup? ,
        savedInstanceState: Bundle?
    ): View {
        viewModel = ViewModelProvider(this).get(AccountsViewModel::class.java)
        binding = DataBindingUtil.inflate(
            inflater,
            R.layout.fragment_account,
            container,
            false
        )
        binding.lifecycleOwner = viewLifecycleOwner
        binding.login.setOnClickListener{
            viewModel.onLogin(binding.email.text, binding.password.text)
        }

        binding.signup.setOnClickListener{
            viewModel.onSignup(binding.email.text, binding.password.text, binding.name.text)
        }

        binding.getUser.setOnClickListener{
            viewModel.onGetUser()
        }

        binding.logout.setOnClickListener{
            viewModel.onLogout()
        }

        viewModel.error.observe(viewLifecycleOwner, Observer { event ->
            event?.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
                Toast.makeText(requireContext(), it.message , Toast.LENGTH_SHORT).show()
            }
        })

        viewModel.response.observe(viewLifecycleOwner, Observer { event ->
            event?.getContentIfNotHandled()?.let {
                binding.responseTV.setText(it)
            }
        })

        return binding.root
    }
}

πŸ”§ Update Main Activity

Finally, let's update our MainActivity.kt, that will initialise our Client singleton and add the AccountsFragment to the FragmentContainerView.

package com.example.myapplication

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.fragment.app.add
import androidx.fragment.app.commit
import com.example.myapplication.utils.Client

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Client.create(applicationContext)

        if (savedInstanceState == null) {
            supportFragmentManager.commit {
                setReorderingAllowed(true)
                add<AccountsFragment>(R.id.fragment_container_view)
            }
        }
    }
}

You should now be able to run your app and create users, login, logout and get information about the currently logged in user!

πŸ” Adding OAuth Support

You would have noticed that we have a Login With Facebook button in our UI but it doesn't really do anything yet. So let's now add Facebook OAuth to our app!

The first step is to add a callback activity in the AndroidManifest.xml file.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapplication">
    <application

        <!-- Other Activities -->
        <activity android:name="io.appwrite.views.CallbackActivity" >
            <intent-filter android:label="android_web_auth">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="appwrite-callback-6070749e6acd4"  />
            </intent-filter>
        </activity>

    </application>
</manifest>

Make sure you replace the Project ID in <data android:scheme="appwrite-callback-[PROJECT-ID]" /> with your own.

Next, we'll add a function to our ViewModel to call the createOAuth2Session() method of the Appwrite SDK.

fun oAuthLogin(activity: ComponentActivity) {
        viewModelScope.launch {
            try {
                accountService.createOAuth2Session(activity, "facebook", "appwrite-callback-6070749e6acd4://demo.appwrite.io/auth/oauth2/success", "appwrite-callback-6070749e6acd4://demo.appwrite.io/auth/oauth2/failure")
            } catch (e: Exception) {
                _error.postValue(Event(e))
            } catch (e: AppwriteException) {
                _error.postValue(Event(e))
            }
        }
    }

The success and failure URLs take the following form
appwrite-callback-[PROJECT-ID]://[YOUR APPWRITE ENDPOINT]/auth/oauth2/[ success | failure ]

Make sure you replace the Project ID and Endpoint with that of your own.

The last step is to invoke this function from AccountsFragment.kt

override fun onCreateView(
        inflater: LayoutInflater ,
        container: ViewGroup? ,
        savedInstanceState: Bundle?
    ): View? {

        // ... Existing Methods

        binding.oAuth.setOnClickListener{
            viewModel.oAuthLogin(activity as ComponentActivity)
        }
    }

Re-run your app and you should now be able to trigger your Facebook OAuth Flow! With that, you now know how to interact with Appwrite's Accounts API in your Android Apps!

We've built a complete app that interacts with all of Appwrite's APIs, which you can find over at our Github Repo. If you'd like to learn more about Appwrite or how Appwrite works under the hood, we've just curated all the resources for you during 30 Days of Appwrite.

✨️ Credits

Hope you enjoyed this article! We love contributions and encourage you to take a look at our open isuses and ongoing RFCs.

If you get stuck anywhere, feel free to reach out to us on our friendly support channels run by humans πŸ‘©β€πŸ’».

Here are some handy links for more information:

23