This guide helps you migrate from FirebaseUI Auth 9.x (View-based) to 10.x (Compose-based). Note that 10.x is a beta release.
FirebaseUI Auth 10.x is a complete rewrite built with Jetpack Compose and Material Design 3. The architecture has changed significantly from the View-based system to a modern, declarative UI framework.
- 9.x: Android Views, Activities, Fragments
- 10.x: Jetpack Compose, Composables
- 9.x: Builder pattern (
createSignInIntentBuilder()) - 10.x: Kotlin DSL (
authUIConfiguration {})
- 9.x:
IdpConfig.EmailBuilder().build() - 10.x:
AuthProvider.Email()
- 9.x: Activity-based with
startActivityForResult()andActivityResultLauncher - 10.x: Composable screens with direct callbacks OR
AuthFlowControllerfor Activity-based apps
- 9.x: XML theme resources (
R.style.AppTheme) - 10.x:
AuthUIThemewith Material 3 color schemes
- 9.x:
AuthStateListenercallbacks - 10.x: Reactive
Flow<AuthState>
Old (9.x):
dependencies {
implementation("com.firebaseui:firebase-ui-auth:9.1.1")
}New (10.x):
dependencies {
// FirebaseUI Auth
// Check Maven Central for the latest version:
// https://central.sonatype.com/artifact/com.firebaseui/firebase-ui-auth/versions
implementation("com.firebaseui:firebase-ui-auth:10.0.0-beta05")
// Required: Jetpack Compose
implementation(platform("androidx.compose:compose-bom:2026.06.01"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
}Old (9.x) - Activity-based:
public class SignInActivity extends AppCompatActivity {
private final ActivityResultLauncher<Intent> signInLauncher =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
IdpResponse response = IdpResponse.fromResultIntent(result.getData());
if (result.getResultCode() == RESULT_OK) {
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
// User signed in
} else {
// Sign in failed
}
}
);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_signin);
Intent signInIntent = AuthUI.getInstance()
.createSignInIntentBuilder()
.setAvailableProviders(Arrays.asList(
new AuthUI.IdpConfig.EmailBuilder().build(),
new AuthUI.IdpConfig.GoogleBuilder().build()
))
.setTheme(R.style.AppTheme)
.build();
signInLauncher.launch(signInIntent);
}
}New (10.x) - Compose-based:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
val authTheme = AuthUITheme.fromMaterialTheme()
val configuration = remember(authTheme) {
authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Email())
provider(AuthProvider.Google())
}
theme = authTheme
}
}
FirebaseAuthScreen(
configuration = configuration,
onSignInSuccess = { result ->
val user = result.user
// User signed in
},
onSignInFailure = { exception ->
// Sign in failed
},
onSignInCancelled = {
finish()
}
)
}
}
}
}Old (9.x):
List<AuthUI.IdpConfig> providers = Arrays.asList(
new AuthUI.IdpConfig.EmailBuilder()
.setRequireName(true)
.build(),
new AuthUI.IdpConfig.GoogleBuilder()
.build(),
new AuthUI.IdpConfig.PhoneBuilder()
.build()
);New (10.x):
context is required — the builder throws without it. applicationContext is an Activity
property; from a composable use LocalContext.current.applicationContext instead.
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Email(isDisplayNameRequired = true))
provider(
AuthProvider.Google(
scopes = listOf("email"),
serverClientId = "YOUR_CLIENT_ID"
)
)
provider(AuthProvider.Phone(defaultCountryCode = "US"))
}
}Old (9.x) - XML styles:
<style name="AppTheme" parent="FirebaseUI">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent</item>
</style>.setTheme(R.style.AppTheme)New (10.x) - Material 3:
val configuration = authUIConfiguration {
context = applicationContext
providers { provider(AuthProvider.Email()) }
theme = AuthUITheme(
colorScheme = lightColorScheme(
primary = Color(0xFF6200EE),
onPrimary = Color.White,
secondary = Color(0xFF03DAC6)
)
)
}Or inherit from your app theme:
MyAppTheme {
val localContext = LocalContext.current
val authTheme = AuthUITheme.fromMaterialTheme()
val configuration = remember(localContext, authTheme) {
authUIConfiguration {
context = localContext
providers { provider(AuthProvider.Email()) }
theme = authTheme
}
}
FirebaseAuthScreen(configuration = configuration, ...)
}Old (9.x):
AuthUI.getInstance()
.signOut(this)
.addOnCompleteListener(task -> {
// User signed out
});New (10.x):
lifecycleScope.launch {
FirebaseAuthUI.getInstance().signOut(context)
// User signed out
}Old (9.x):
AuthUI.getInstance()
.delete(this)
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
// Account deleted
} else {
// Deletion failed
}
});New (10.x):
lifecycleScope.launch {
try {
FirebaseAuthUI.getInstance().delete(context)
// Account deleted
} catch (e: Exception) {
// Deletion failed
}
}Old (9.x):
FirebaseAuth.getInstance().addAuthStateListener(firebaseAuth -> {
FirebaseUser user = firebaseAuth.getCurrentUser();
if (user != null) {
// User is signed in
} else {
// User is signed out
}
});New (10.x):
@Composable
fun AuthGate() {
val authUI = remember { FirebaseAuthUI.getInstance() }
// remember the flow: authStateFlow() builds a new one per call, and without this every
// recomposition would restart collection and re-register the underlying Firebase listener.
val authStateFlow = remember(authUI) { authUI.authStateFlow() }
val authState by authStateFlow.collectAsState(initial = AuthState.Idle)
when (authState) {
is AuthState.Success -> {
// User is signed in
MainAppScreen()
}
else -> {
// Show authentication
FirebaseAuthScreen(...)
}
}
}Old (9.x):
new AuthUI.IdpConfig.EmailBuilder()
.setRequireName(true)
.setAllowNewAccounts(true)
.enableEmailLinkSignIn()
.setActionCodeSettings(actionCodeSettings)
.build()New (10.x):
AuthProvider.Email(
isDisplayNameRequired = true,
isNewAccountsAllowed = true,
isEmailLinkSignInEnabled = true,
emailLinkActionCodeSettings = actionCodeSettings {
url = "https://example.com/auth"
handleCodeInApp = true
setAndroidPackageName(packageName, true, null)
}
)Old (9.x):
new AuthUI.IdpConfig.GoogleBuilder()
.setScopes(Arrays.asList("email", "profile"))
.build()New (10.x):
AuthProvider.Google(
scopes = listOf("email", "profile"),
serverClientId = "YOUR_CLIENT_ID"
)Old (9.x):
new AuthUI.IdpConfig.PhoneBuilder()
.setDefaultNumber("US", "+1 123-456-7890")
.build()New (10.x):
AuthProvider.Phone(
defaultCountryCode = "US",
defaultNumber = "+11234567890"
)If you have an existing Activity-based app and want to keep using Activities:
New (10.x) - Low-Level API:
class AuthActivity : ComponentActivity() {
private lateinit var controller: AuthFlowController
private val authLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val authUI = FirebaseAuthUI.getInstance()
val configuration = authUIConfiguration {
context = applicationContext
providers {
provider(AuthProvider.Email())
provider(AuthProvider.Google())
}
}
controller = authUI.createAuthFlow(configuration)
// Only on a fresh start. Launching unguarded would start a second flow every time the
// Activity is recreated — a rotation, or a restore after process death.
if (savedInstanceState == null) {
authLauncher.launch(controller.createIntent(this))
}
}
override fun onDestroy() {
super.onDestroy()
controller.dispose()
}
}The flow runs in its own Activity, so the outcome arrives as an Activity result rather than a
return value. That result only says the flow ended, so to follow it in detail — loading, errors,
MFA — collect controller.authStateFlow alongside the launcher. Dispose the controller in
onDestroy: a disposed controller cannot be reused, and disposal is the contract its other
members are documented against.
Solution: Ensure you have the correct import:
import com.firebase.ui.auth.configuration.authUIConfigurationSolution: Compose FirebaseAuthScreen and use its callbacks — no ActivityResultLauncher
needed. The exception is the Activity-based route above: AuthFlowController hands you an Intent,
so that one still launches through a result contract.
Solution: Use content slots for custom UI. EmailAuthScreen renders the mode it is given and
never changes it on its own, so the host owns the mode and navigates between them — give each mode
its own destination, or the fields of the one you leave carry over into the one you arrive at:
@Serializable
data class EmailModeKey(val mode: EmailAuthMode, val email: String = "") : NavKey
val backStack = rememberNavBackStack(EmailModeKey(EmailAuthMode.SignIn))
NavDisplay(
backStack = backStack,
// NavDisplay throws on an empty back stack, and throws from recomposition, so the first
// mode must not pop.
onBack = { if (backStack.size > 1) backStack.removeLastOrNull() },
entryProvider = entryProvider {
entry<EmailModeKey> { key ->
EmailAuthScreen(
context = context,
configuration = emailConfig,
authUI = authUI,
prefillEmail = key.email.ifEmpty { null },
mode = key.mode,
// The typed address travels with the switch, so the mode you arrive at can
// prefill it. Replace a mode already on the stack rather than revisiting it
// with a stale address.
onNavigateToMode = { mode, email ->
val existing = backStack.indexOfFirst { it is EmailModeKey && it.mode == mode }
backStack.add(EmailModeKey(mode, email))
if (existing >= 0) {
while (backStack.size > existing + 1) backStack.removeAt(existing)
}
},
onSuccess = { /* ... */ },
onError = { /* ... */ },
onCancel = { /* ... */ },
) { state ->
// Your custom UI here
CustomSignInUI(state)
}
}
},
)PhoneAuthScreen and MfaEnrollmentScreen work the same way, with step / onNavigateToStep /
onNavigateBack plus a flowState from rememberPhoneAuthFlowState or
rememberMfaEnrollmentFlowState remembered above the NavDisplay. If you would rather not own any
of this, use FirebaseAuthScreen, which owns it for you.
A back-stack key must be @Serializable to survive process death, so add the
org.jetbrains.kotlin.plugin.serialization plugin to the module hosting these screens.
Solution: Convert XML themes to Kotlin code using AuthUITheme:
val configuration = authUIConfiguration {
context = applicationContext
providers { provider(AuthProvider.Email()) }
theme = AuthUITheme(
colorScheme = lightColorScheme(
primary = Color(0xFF6200EE),
// ... other colors
)
)
}- Build the app - Ensure it compiles without errors
- Test all auth flows - Sign in, sign up, password reset
- Test all providers - Email, Google, Phone, etc.
- Test sign out - Verify users can sign out
- Test account deletion - Verify accounts can be deleted
- Test error handling - Verify errors are handled gracefully
- Test theming - Verify UI matches your design
- Updated dependency to
firebase-ui-auth:10.0.0-beta05 - Migrated to Jetpack Compose
- Converted Activities to ComponentActivities with
setContent {} - Replaced
createSignInIntentBuilder()withauthUIConfiguration {} - Updated all provider configurations
- Converted XML themes to
AuthUITheme - Updated error handling from result codes to exceptions
- Replaced
ActivityResultLauncherwithFirebaseAuthScreencallbacks (Activity-based apps keep one, forAuthFlowController.createIntent()) - Updated sign-out to use suspend functions
- Updated account deletion to use suspend functions
- Tested all authentication flows
- Tested on multiple Android versions