--- title: 'Build a Product Management Android App with Jetpack Compose' description: 'Learn how to use Supabase in your Android Kotlin App.' --- This tutorial demonstrates how to build a basic product management app. The app demonstrates management operations, photo upload, account creation and authentication using: - [Supabase Database](/docs/guides/database/overview) - a Postgres database for storing your user data and [Row Level Security](/docs/guides/auth#row-level-security) so data is protected and users can only access their own information. - [Supabase Auth](/docs/guides/auth) - users sign in through magic links sent to their email (without having to set up a password). - [Supabase Storage](/docs/guides/storage) - users can upload a profile photo. ![manage-product-cover](/docs/img/guides/kotlin/manage-product-cover.png) If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/hieuwu/product-sample-supabase-kt). <$Partial path="kotlin_project_setup.mdx" variables={{ "framework": "androidkotlin", "tab": "mobiles" }} /> ## Building the app ### Create new Android project Open Android Studio > New Project > Base Activity (Jetpack Compose). ![Android Studio new project](/docs/img/guides/kotlin/android-studio-new-project.png) ### Set up API key and secret securely #### Create local environment secret Create or edit the `local.properties` file at the root (same level as `build.gradle`) of your project. > **Note**: Do not commit this file to your source control, for example, by adding it to your `.gitignore` file! ```kotlin SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY SUPABASE_URL=YOUR_SUPABASE_URL ``` #### Read and set value to `BuildConfig` In your `build.gradle` (app) file, create a `Properties` object and read the values from your `local.properties` file by calling the `buildConfigField` method: ```kotlin defaultConfig { applicationId "com.example.manageproducts" minSdkVersion 22 targetSdkVersion 33 versionCode 5 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // Set value part Properties properties = new Properties() properties.load(project.rootProject.file("local.properties").newDataInputStream()) buildConfigField("String", "SUPABASE_PUBLISHABLE_KEY", "\"${properties.getProperty("SUPABASE_PUBLISHABLE_KEY")}\"") buildConfigField("String", "SECRET", "\"${properties.getProperty("SECRET")}\"") buildConfigField("String", "SUPABASE_URL", "\"${properties.getProperty("SUPABASE_URL")}\"") } ``` #### Use value from `BuildConfig` Read the value from `BuildConfig`: ```kotlin val url = BuildConfig.SUPABASE_URL val apiKey = BuildConfig.SUPABASE_PUBLISHABLE_KEY ``` ### Set up Supabase dependencies ![Gradle dependencies](/docs/img/guides/kotlin/gradle-dependencies.png) In the `build.gradle` (app) file, add these dependencies then press "Sync now." Replace the dependency version placeholders `$supabase_version` and `$ktor_version` with their respective latest versions. ```kotlin implementation "io.github.jan-tennert.supabase:postgrest-kt:$supabase_version" implementation "io.github.jan-tennert.supabase:storage-kt:$supabase_version" implementation "io.github.jan-tennert.supabase:auth-kt:$supabase_version" implementation "io.ktor:ktor-client-android:$ktor_version" implementation "io.ktor:ktor-client-core:$ktor_version" implementation "io.ktor:ktor-utils:$ktor_version" ``` Also in the `build.gradle` (app) file, add the plugin for serialization. The version of this plugin should be the same as your Kotlin version. ```kotlin plugins { ... id 'org.jetbrains.kotlin.plugin.serialization' version '$kotlin_version' ... } ``` ### Set up Hilt for dependency injection In the `build.gradle` (app) file, add the following: ```kotlin implementation "com.google.dagger:hilt-android:$hilt_version" annotationProcessor "com.google.dagger:hilt-compiler:$hilt_version" implementation("androidx.hilt:hilt-navigation-compose:1.0.0") ``` Create a new `ManageProductApplication.kt` class extending Application with `@HiltAndroidApp` annotation: ```kotlin // ManageProductApplication.kt @HiltAndroidApp class ManageProductApplication: Application() ``` Open the `AndroidManifest.xml` file, update name property of Application tag: ```xml ``` Create the `MainActivity`: ```kotlin @AndroidEntryPoint class MainActivity : ComponentActivity() { //This will come later } ``` ### Provide Supabase instances with Hilt To make the app easier to test, create a `SupabaseModule.kt` file as follows: ```kotlin @InstallIn(SingletonComponent::class) @Module object SupabaseModule { @Provides @Singleton fun provideSupabaseClient(): SupabaseClient { return createSupabaseClient( supabaseUrl = BuildConfig.SUPABASE_URL, supabaseKey = BuildConfig.SUPABASE_PUBLISHABLE_KEY ) { install(Postgrest) install(Auth) { flowType = FlowType.PKCE scheme = "app" host = "supabase.com" } install(Storage) } } @Provides @Singleton fun provideSupabaseDatabase(client: SupabaseClient): Postgrest { return client.postgrest } @Provides @Singleton fun provideSupabaseAuth(client: SupabaseClient): Auth { return client.auth } @Provides @Singleton fun provideSupabaseStorage(client: SupabaseClient): Storage { return client.storage } } ``` ### Create a data transfer object Create a `ProductDto.kt` class and use annotations to parse data from Supabase: ```kotlin @Serializable data class ProductDto( @SerialName("name") val name: String, @SerialName("price") val price: Double, @SerialName("image") val image: String?, @SerialName("id") val id: String, ) ``` Create a Domain object in `Product.kt` expose the data in your view: ```kotlin data class Product( val id: String, val name: String, val price: Double, val image: String? ) ``` ### Implement repositories Create a `ProductRepository` interface and its implementation named `ProductRepositoryImpl`. This holds the logic to interact with data sources from Supabase. Do the same with the `AuthenticationRepository`. Create the Product Repository: ```kotlin interface ProductRepository { suspend fun createProduct(product: Product): Boolean suspend fun getProducts(): List? suspend fun getProduct(id: String): ProductDto suspend fun deleteProduct(id: String) suspend fun updateProduct( id: String, name: String, price: Double, imageName: String, imageFile: ByteArray ) } ``` ```kotlin class ProductRepositoryImpl @Inject constructor( private val postgrest: Postgrest, private val storage: Storage, ) : ProductRepository { override suspend fun createProduct(product: Product): Boolean { return try { withContext(Dispatchers.IO) { val productDto = ProductDto( name = product.name, price = product.price, ) postgrest.from("products").insert(productDto) true } true } catch (e: java.lang.Exception) { throw e } } override suspend fun getProducts(): List? { return withContext(Dispatchers.IO) { val result = postgrest.from("products") .select().decodeList() result } } override suspend fun getProduct(id: String): ProductDto { return withContext(Dispatchers.IO) { postgrest.from("products").select { filter { eq("id", id) } }.decodeSingle() } } override suspend fun deleteProduct(id: String) { return withContext(Dispatchers.IO) { postgrest.from("products").delete { filter { eq("id", id) } } } } override suspend fun updateProduct( id: String, name: String, price: Double, imageName: String, imageFile: ByteArray ) { withContext(Dispatchers.IO) { if (imageFile.isNotEmpty()) { val imageUrl = storage.from("Product%20Image").upload( path = "$imageName.png", data = imageFile, upsert = true ) postgrest.from("products").update({ set("name", name) set("price", price) set("image", buildImageUrl(imageFileName = imageUrl)) }) { filter { eq("id", id) } } } else { postgrest.from("products").update({ set("name", name) set("price", price) }) { filter { eq("id", id) } } } } } // Because I named the bucket as "Product Image" so when it turns to an url, it is "%20" // For better approach, you should create your bucket name without space symbol private fun buildImageUrl(imageFileName: String) = "${BuildConfig.SUPABASE_URL}/storage/v1/object/public/${imageFileName}".replace(" ", "%20") } ``` Create the Authentication Repository: ```kotlin interface AuthenticationRepository { suspend fun signIn(email: String, password: String): Boolean suspend fun signUp(email: String, password: String): Boolean suspend fun signInWithGoogle(): Boolean } ``` ```kotlin class AuthenticationRepositoryImpl @Inject constructor( private val auth: Auth ) : AuthenticationRepository { override suspend fun signIn(email: String, password: String): Boolean { return try { auth.signInWith(Email) { this.email = email this.password = password } true } catch (e: Exception) { false } } override suspend fun signUp(email: String, password: String): Boolean { return try { auth.signUpWith(Email) { this.email = email this.password = password } true } catch (e: Exception) { false } } override suspend fun signInWithGoogle(): Boolean { return try { auth.signInWith(Google) true } catch (e: Exception) { false } } } ``` ### Implement screens To navigate screens, use the AndroidX navigation library. For routes, implement a `Destination` interface: ```kotlin interface Destination { val route: String val title: String } object ProductListDestination : Destination { override val route = "product_list" override val title = "Product List" } object ProductDetailsDestination : Destination { override val route = "product_details" override val title = "Product Details" const val productId = "product_id" val arguments = listOf(navArgument(name = productId) { type = NavType.StringType }) fun createRouteWithParam(productId: String) = "$route/${productId}" } object AddProductDestination : Destination { override val route = "add_product" override val title = "Add Product" } object AuthenticationDestination: Destination { override val route = "authentication" override val title = "Authentication" } object SignUpDestination: Destination { override val route = "signup" override val title = "Sign Up" } ``` This will help later for navigating between screens. Create a `ProductListViewModel`: ```kotlin @HiltViewModel class ProductListViewModel @Inject constructor( private val productRepository: ProductRepository, ) : ViewModel() { private val _productList = MutableStateFlow?>(listOf()) val productList: Flow?> = _productList private val _isLoading = MutableStateFlow(false) val isLoading: Flow = _isLoading init { getProducts() } fun getProducts() { viewModelScope.launch { val products = productRepository.getProducts() _productList.emit(products?.map { it -> it.asDomainModel() }) } } fun removeItem(product: Product) { viewModelScope.launch { val newList = mutableListOf().apply { _productList.value?.let { addAll(it) } } newList.remove(product) _productList.emit(newList.toList()) // Call api to remove productRepository.deleteProduct(id = product.id) // Then fetch again getProducts() } } private fun ProductDto.asDomainModel(): Product { return Product( id = this.id, name = this.name, price = this.price, image = this.image ) } } ``` Create the `ProductListScreen.kt`: ```kotlin @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Composable fun ProductListScreen( modifier: Modifier = Modifier, navController: NavController, viewModel: ProductListViewModel = hiltViewModel(), ) { val isLoading by viewModel.isLoading.collectAsState(initial = false) val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = isLoading) SwipeRefresh(state = swipeRefreshState, onRefresh = { viewModel.getProducts() }) { Scaffold( topBar = { TopAppBar( backgroundColor = MaterialTheme.colorScheme.primary, title = { Text( text = stringResource(R.string.product_list_text_screen_title), color = MaterialTheme.colorScheme.onPrimary, ) }, ) }, floatingActionButton = { AddProductButton(onClick = { navController.navigate(AddProductDestination.route) }) } ) { padding -> val productList = viewModel.productList.collectAsState(initial = listOf()).value if (!productList.isNullOrEmpty()) { LazyColumn( modifier = modifier.padding(padding), contentPadding = PaddingValues(5.dp) ) { itemsIndexed( items = productList, key = { _, product -> product.name }) { _, item -> val state = rememberDismissState( confirmStateChange = { if (it == DismissValue.DismissedToStart) { // Handle item removed viewModel.removeItem(item) } true } ) SwipeToDismiss( state = state, background = { val color by animateColorAsState( targetValue = when (state.dismissDirection) { DismissDirection.StartToEnd -> MaterialTheme.colorScheme.primary DismissDirection.EndToStart -> MaterialTheme.colorScheme.primary.copy( alpha = 0.2f ) null -> Color.Transparent } ) Box( modifier = modifier .fillMaxSize() .background(color = color) .padding(16.dp), ) { Icon( imageVector = Icons.Filled.Delete, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = modifier.align(Alignment.CenterEnd) ) } }, dismissContent = { ProductListItem( product = item, modifier = modifier, onClick = { navController.navigate( ProductDetailsDestination.createRouteWithParam( item.id ) ) }, ) }, directions = setOf(DismissDirection.EndToStart), ) } } } else { Text("Product list is empty!") } } } } @Composable private fun AddProductButton( modifier: Modifier = Modifier, onClick: () -> Unit, ) { FloatingActionButton( modifier = modifier, onClick = onClick, containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary ) { Icon( imageVector = Icons.Filled.Add, contentDescription = null, ) } } ``` Create the `ProductDetailsViewModel.kt`: ```kotlin @HiltViewModel class ProductDetailsViewModel @Inject constructor( private val productRepository: ProductRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { private val _product = MutableStateFlow(null) val product: Flow = _product private val _name = MutableStateFlow("") val name: Flow = _name private val _price = MutableStateFlow(0.0) val price: Flow = _price private val _imageUrl = MutableStateFlow("") val imageUrl: Flow = _imageUrl init { val productId = savedStateHandle.get(ProductDetailsDestination.productId) productId?.let { getProduct(productId = it) } } private fun getProduct(productId: String) { viewModelScope.launch { val result = productRepository.getProduct(productId).asDomainModel() _product.emit(result) _name.emit(result.name) _price.emit(result.price) } } fun onNameChange(name: String) { _name.value = name } fun onPriceChange(price: Double) { _price.value = price } fun onSaveProduct(image: ByteArray) { viewModelScope.launch { productRepository.updateProduct( id = _product.value?.id, price = _price.value, name = _name.value, imageFile = image, imageName = "image_${_product.value.id}", ) } } fun onImageChange(url: String) { _imageUrl.value = url } private fun ProductDto.asDomainModel(): Product { return Product( id = this.id, name = this.name, price = this.price, image = this.image ) } } ``` Create the `ProductDetailsScreen.kt`: ```kotlin @OptIn(ExperimentalCoilApi::class) @SuppressLint("UnusedMaterialScaffoldPaddingParameter") @Composable fun ProductDetailsScreen( modifier: Modifier = Modifier, viewModel: ProductDetailsViewModel = hiltViewModel(), navController: NavController, productId: String?, ) { val snackBarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() Scaffold( snackbarHost = { SnackbarHost(snackBarHostState) }, topBar = { TopAppBar( navigationIcon = { IconButton(onClick = { navController.navigateUp() }) { Icon( imageVector = Icons.Filled.ArrowBack, contentDescription = null, tint = MaterialTheme.colorScheme.onPrimary ) } }, backgroundColor = MaterialTheme.colorScheme.primary, title = { Text( text = stringResource(R.string.product_details_text_screen_title), color = MaterialTheme.colorScheme.onPrimary, ) }, ) } ) { val name = viewModel.name.collectAsState(initial = "") val price = viewModel.price.collectAsState(initial = 0.0) var imageUrl = Uri.parse(viewModel.imageUrl.collectAsState(initial = null).value) val contentResolver = LocalContext.current.contentResolver Column( modifier = modifier .padding(16.dp) .fillMaxSize() ) { val galleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> uri?.let { if (it.toString() != imageUrl.toString()) { viewModel.onImageChange(it.toString()) } } } Image( painter = rememberImagePainter(imageUrl), contentScale = ContentScale.Fit, contentDescription = null, modifier = Modifier .padding(16.dp, 8.dp) .size(100.dp) .align(Alignment.CenterHorizontally) ) IconButton(modifier = modifier.align(alignment = Alignment.CenterHorizontally), onClick = { galleryLauncher.launch("image/*") }) { Icon( imageVector = Icons.Filled.Edit, contentDescription = null, tint = MaterialTheme.colorScheme.primary ) } OutlinedTextField( label = { Text( text = "Product name", color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.titleMedium ) }, maxLines = 2, shape = RoundedCornerShape(32), modifier = modifier.fillMaxWidth(), value = name.value, onValueChange = { viewModel.onNameChange(it) }, ) Spacer(modifier = modifier.height(12.dp)) OutlinedTextField( label = { Text( text = "Product price", color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.titleMedium ) }, maxLines = 2, shape = RoundedCornerShape(32), modifier = modifier.fillMaxWidth(), value = price.value.toString(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), onValueChange = { viewModel.onPriceChange(it.toDouble()) }, ) Spacer(modifier = modifier.weight(1f)) Button( modifier = modifier.fillMaxWidth(), onClick = { if (imageUrl.host?.contains("supabase") == true) { viewModel.onSaveProduct(image = byteArrayOf()) } else { val image = uriToByteArray(contentResolver, imageUrl) viewModel.onSaveProduct(image = image) } coroutineScope.launch { snackBarHostState.showSnackbar( message = "Product updated successfully !", duration = SnackbarDuration.Short ) } }) { Text(text = "Save changes") } Spacer(modifier = modifier.height(12.dp)) OutlinedButton( modifier = modifier .fillMaxWidth(), onClick = { navController.navigateUp() }) { Text(text = "Cancel") } } } } private fun getBytes(inputStream: InputStream): ByteArray { val byteBuffer = ByteArrayOutputStream() val bufferSize = 1024 val buffer = ByteArray(bufferSize) var len = 0 while (inputStream.read(buffer).also { len = it } != -1) { byteBuffer.write(buffer, 0, len) } return byteBuffer.toByteArray() } private fun uriToByteArray(contentResolver: ContentResolver, uri: Uri): ByteArray { if (uri == Uri.EMPTY) { return byteArrayOf() } val inputStream = contentResolver.openInputStream(uri) if (inputStream != null) { return getBytes(inputStream) } return byteArrayOf() } ``` Create a `AddProductScreen`: ```kotlin @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn(ExperimentalMaterial3Api::class) @Composable fun AddProductScreen( modifier: Modifier = Modifier, navController: NavController, viewModel: AddProductViewModel = hiltViewModel(), ) { Scaffold( topBar = { TopAppBar( navigationIcon = { IconButton(onClick = { navController.navigateUp() }) { Icon( imageVector = Icons.Filled.ArrowBack, contentDescription = null, tint = MaterialTheme.colorScheme.onPrimary ) } }, backgroundColor = MaterialTheme.colorScheme.primary, title = { Text( text = stringResource(R.string.add_product_text_screen_title), color = MaterialTheme.colorScheme.onPrimary, ) }, ) } ) { padding -> val navigateAddProductSuccess = viewModel.navigateAddProductSuccess.collectAsState(initial = null).value val isLoading = viewModel.isLoading.collectAsState(initial = null).value if (isLoading == true) { LoadingScreen(message = "Adding Product", onCancelSelected = { navController.navigateUp() }) } else { SuccessScreen( message = "Product added", onMoreAction = { viewModel.onAddMoreProductSelected() }, onNavigateBack = { navController.navigateUp() }) } } } ``` Create the `AddProductViewModel.kt`: ```kotlin @HiltViewModel class AddProductViewModel @Inject constructor( private val productRepository: ProductRepository, ) : ViewModel() { private val _isLoading = MutableStateFlow(false) val isLoading: Flow = _isLoading private val _showSuccessMessage = MutableStateFlow(false) val showSuccessMessage: Flow = _showSuccessMessage fun onCreateProduct(name: String, price: Double) { if (name.isEmpty() || price <= 0) return viewModelScope.launch { _isLoading.value = true val product = Product( id = UUID.randomUUID().toString(), name = name, price = price, ) productRepository.createProduct(product = product) _isLoading.value = false _showSuccessMessage.emit(true) } } } ``` Create a `SignUpViewModel`: ```kotlin @HiltViewModel class SignUpViewModel @Inject constructor( private val authenticationRepository: AuthenticationRepository ) : ViewModel() { private val _email = MutableStateFlow("") val email: Flow = _email private val _password = MutableStateFlow("") val password = _password fun onEmailChange(email: String) { _email.value = email } fun onPasswordChange(password: String) { _password.value = password } fun onSignUp() { viewModelScope.launch { authenticationRepository.signUp( email = _email.value, password = _password.value ) } } } ``` Create the `SignUpScreen.kt`: ```kotlin @Composable fun SignUpScreen( modifier: Modifier = Modifier, navController: NavController, viewModel: SignUpViewModel = hiltViewModel() ) { val snackBarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() Scaffold( snackbarHost = { androidx.compose.material.SnackbarHost(snackBarHostState) }, topBar = { TopAppBar( navigationIcon = { IconButton(onClick = { navController.navigateUp() }) { Icon( imageVector = Icons.Filled.ArrowBack, contentDescription = null, tint = MaterialTheme.colorScheme.onPrimary ) } }, backgroundColor = MaterialTheme.colorScheme.primary, title = { Text( text = "Sign Up", color = MaterialTheme.colorScheme.onPrimary, ) }, ) } ) { paddingValues -> Column( modifier = modifier .padding(paddingValues) .padding(20.dp) ) { val email = viewModel.email.collectAsState(initial = "") val password = viewModel.password.collectAsState() OutlinedTextField( label = { Text( text = "Email", color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.titleMedium ) }, maxLines = 1, shape = RoundedCornerShape(32), modifier = modifier.fillMaxWidth(), value = email.value, onValueChange = { viewModel.onEmailChange(it) }, ) OutlinedTextField( label = { Text( text = "Password", color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.titleMedium ) }, maxLines = 1, shape = RoundedCornerShape(32), modifier = modifier .fillMaxWidth() .padding(top = 12.dp), value = password.value, onValueChange = { viewModel.onPasswordChange(it) }, ) val localSoftwareKeyboardController = LocalSoftwareKeyboardController.current Button(modifier = modifier .fillMaxWidth() .padding(top = 12.dp), onClick = { localSoftwareKeyboardController?.hide() viewModel.onSignUp() coroutineScope.launch { snackBarHostState.showSnackbar( message = "Create account successfully. Sign in now!", duration = SnackbarDuration.Long ) } }) { Text("Sign up") } } } } ``` Create a `SignInViewModel`: ```kotlin @HiltViewModel class SignInViewModel @Inject constructor( private val authenticationRepository: AuthenticationRepository ) : ViewModel() { private val _email = MutableStateFlow("") val email: Flow = _email private val _password = MutableStateFlow("") val password = _password fun onEmailChange(email: String) { _email.value = email } fun onPasswordChange(password: String) { _password.value = password } fun onSignIn() { viewModelScope.launch { authenticationRepository.signIn( email = _email.value, password = _password.value ) } } fun onGoogleSignIn() { viewModelScope.launch { authenticationRepository.signInWithGoogle() } } } ``` Create the `SignInScreen.kt`: ```kotlin @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable fun SignInScreen( modifier: Modifier = Modifier, navController: NavController, viewModel: SignInViewModel = hiltViewModel() ) { val snackBarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() Scaffold( snackbarHost = { androidx.compose.material.SnackbarHost(snackBarHostState) }, topBar = { TopAppBar( navigationIcon = { IconButton(onClick = { navController.navigateUp() }) { Icon( imageVector = Icons.Filled.ArrowBack, contentDescription = null, tint = MaterialTheme.colorScheme.onPrimary ) } }, backgroundColor = MaterialTheme.colorScheme.primary, title = { Text( text = "Login", color = MaterialTheme.colorScheme.onPrimary, ) }, ) } ) { paddingValues -> Column( modifier = modifier .padding(paddingValues) .padding(20.dp) ) { val email = viewModel.email.collectAsState(initial = "") val password = viewModel.password.collectAsState() androidx.compose.material.OutlinedTextField( label = { Text( text = "Email", color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.titleMedium ) }, maxLines = 1, shape = RoundedCornerShape(32), modifier = modifier.fillMaxWidth(), value = email.value, onValueChange = { viewModel.onEmailChange(it) }, ) androidx.compose.material.OutlinedTextField( label = { Text( text = "Password", color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.titleMedium ) }, maxLines = 1, shape = RoundedCornerShape(32), modifier = modifier .fillMaxWidth() .padding(top = 12.dp), value = password.value, onValueChange = { viewModel.onPasswordChange(it) }, ) val localSoftwareKeyboardController = LocalSoftwareKeyboardController.current Button(modifier = modifier .fillMaxWidth() .padding(top = 12.dp), onClick = { localSoftwareKeyboardController?.hide() viewModel.onGoogleSignIn() }) { Text("Sign in with Google") } Button(modifier = modifier .fillMaxWidth() .padding(top = 12.dp), onClick = { localSoftwareKeyboardController?.hide() viewModel.onSignIn() coroutineScope.launch { snackBarHostState.showSnackbar( message = "Sign in successfully !", duration = SnackbarDuration.Long ) } }) { Text("Sign in") } OutlinedButton(modifier = modifier .fillMaxWidth() .padding(top = 12.dp), onClick = { navController.navigate(SignUpDestination.route) }) { Text("Sign up") } } } } ``` ### Implement the `MainActivity` In the `MainActivity` you created earlier, show your newly created screens: ```kotlin @AndroidEntryPoint class MainActivity : ComponentActivity() { @Inject lateinit var supabaseClient: SupabaseClient @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ManageProductsTheme { // A surface container using the 'background' color from the theme val navController = rememberNavController() val currentBackStack by navController.currentBackStackEntryAsState() val currentDestination = currentBackStack?.destination Scaffold { innerPadding -> NavHost( navController, startDestination = ProductListDestination.route, Modifier.padding(innerPadding) ) { composable(ProductListDestination.route) { ProductListScreen( navController = navController ) } composable(AuthenticationDestination.route) { SignInScreen( navController = navController ) } composable(SignUpDestination.route) { SignUpScreen( navController = navController ) } composable(AddProductDestination.route) { AddProductScreen( navController = navController ) } composable( route = "${ProductDetailsDestination.route}/{${ProductDetailsDestination.productId}}", arguments = ProductDetailsDestination.arguments ) { navBackStackEntry -> val productId = navBackStackEntry.arguments?.getString(ProductDetailsDestination.productId) ProductDetailsScreen( productId = productId, navController = navController, ) } } } } } } } ``` ### Create the success screen To handle OAuth and OTP signins, create a new activity to handle the deep link you set in `AndroidManifest.xml`: ```xml ``` Then create the `DeepLinkHandlerActivity`: ```kotlin @AndroidEntryPoint class DeepLinkHandlerActivity : ComponentActivity() { @Inject lateinit var supabaseClient: SupabaseClient private lateinit var callback: (String, String) -> Unit override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) supabaseClient.handleDeeplinks(intent = intent, onSessionSuccess = { userSession -> Log.d("LOGIN", "Log in successfully with user info: ${userSession.user}") userSession.user?.apply { callback(email ?: "", createdAt.toString()) } }) setContent { val navController = rememberNavController() val emailState = remember { mutableStateOf("") } val createdAtState = remember { mutableStateOf("") } LaunchedEffect(Unit) { callback = { email, created -> emailState.value = email createdAtState.value = created } } ManageProductsTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { SignInSuccessScreen( modifier = Modifier.padding(20.dp), navController = navController, email = emailState.value, createdAt = createdAtState.value, onClick = { navigateToMainApp() } ) } } } } private fun navigateToMainApp() { val intent = Intent(this, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP } startActivity(intent) } } ```