Quickstart: Android (Jetpack Compose)
Build the notes app with Kotlin and Jetpack Compose: keep the session in encrypted storage, follow sign-in state with auth.changes, and list and add the signed-in user's notes.
A Compose app (Android 8, API 26+) where people sign up, sign in, add notes and see only their own.
#Before you start
Do steps 1 to 3 of the Quickstart first: create a project, copy its project URL and anon key (Quickstart), and create the notes table with its row rules (Quickstart). Every signed-in user then sees and changes only their own notes, whatever the app code does.
#1. Create the app and add the dependency
In Android Studio create an Empty Activity project (Kotlin, Compose). Add the SDK and AndroidX Security for encrypted storage, and the internet permission.
dependencies {
implementation("in.sovaria:sovaria:1.0.0")
implementation("androidx.security:security-crypto:1.1.0-alpha06")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
}#2. Keep the session in encrypted storage
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import `in`.sovaria.SessionStorage
class KeystoreStore(context: Context) : SessionStorage {
private val prefs = EncryptedSharedPreferences.create(
context, "sovaria",
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
override suspend fun read(key: String): String? = prefs.getString(key, null)
override suspend fun write(key: String, value: String) { prefs.edit().putString(key, value).apply() }
override suspend fun delete(key: String) { prefs.edit().remove(key).apply() }
}#3. Connect and follow sign-in state
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import `in`.sovaria.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
class NotesViewModel(app: Application) : AndroidViewModel(app) {
val signedIn = MutableStateFlow(false)
val notes = MutableStateFlow<List<Map<String, Any?>>>(emptyList())
val error = MutableStateFlow("")
private lateinit var sovaria: Sovaria
init {
viewModelScope.launch {
// Restores the saved session (refreshing it if it's about to expire).
sovaria = Sovaria.initialize(
url = "https://<project-ref>.sovarias.in",
anonKey = "<anon-key>",
storage = KeystoreStore(app),
)
sovaria.auth.changes.collect { state -> // first event: INITIAL_SESSION
signedIn.value = state.session != null
if (signedIn.value) load()
}
}
}
private fun run(block: suspend () -> Unit) = viewModelScope.launch {
try { block() } catch (e: SovariaException) { error.value = e.message }
}
fun signIn(email: String, password: String, create: Boolean) = run {
if (create) sovaria.auth.signUp(email, password) else sovaria.auth.signInWithPassword(email, password)
}
// Rows come back keyed by column name. The row rules add "where owner_id = auth.uid()".
private suspend fun load() { notes.value = sovaria.db.sql("select id, body from notes order by id desc").rows }
fun add(body: String) = run {
sovaria.db.sql("insert into notes (body) values (\$1)", listOf(body))
load()
}
fun signOut() = run { sovaria.auth.signOut() }
}#4. The screens
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { MaterialTheme { NotesScreen() } }
}
}
@Composable
fun NotesScreen(vm: NotesViewModel = viewModel()) {
val signedIn by vm.signedIn.collectAsState()
val notes by vm.notes.collectAsState()
val error by vm.error.collectAsState()
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var body by remember { mutableStateOf("") }
Column(Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
if (!signedIn) {
OutlinedTextField(email, { email = it }, label = { Text("Email") })
OutlinedTextField(password, { password = it }, label = { Text("Password") },
visualTransformation = PasswordVisualTransformation())
Button({ vm.signIn(email, password, create = false) }) { Text("Sign in") }
TextButton({ vm.signIn(email, password, create = true) }) { Text("Create account") }
} else {
Row {
OutlinedTextField(body, { body = it }, Modifier.weight(1f), placeholder = { Text("Write a note") })
Button({ vm.add(body); body = "" }) { Text("Add") }
}
LazyColumn(Modifier.weight(1f)) { items(notes) { Text(it["body"]?.toString() ?: "") } }
TextButton({ vm.signOut() }) { Text("Sign out") }
}
if (error.isNotEmpty()) Text(error, color = MaterialTheme.colorScheme.error)
}
}In Kotlin strings $ starts a template, so the SQL placeholder is written \$1.
#5. Run it
Press Run with an emulator or device. Create an account, add a note, sign out and back in: the note is still there, and a second account never sees it.
#Next steps
- Check who can read and change
notesbefore you ship: Tutorial: prove your table is private before you ship. - Push new notes to other devices as they happen: Database change events.
- Google sign-in via deep links, push and Play Integrity: Android quickstart (Kotlin).
- Every type and method: Kotlin SDK.
#Coming from Firebase or Supabase?
| Platform | What this is called there |
|---|---|
| Firebase | Firebase Android (Kotlin) setup |
| Supabase | Supabase Kotlin quickstart |
| Sovaria | Quickstart: Android (Jetpack Compose) |
Moving an existing app? Migrate from Supabase or Firebase imports users (with their passwords), data, files and rules.