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 commandhostname -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="test@test.com"
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:
Top comments (0)