Skip to main content
Manage authentication state and wallet updates with Kotlin Flow.

State Flows

The SDK provides flows that emit updates:
  • authenticatedUserChanges - User login/logout
  • tokenChanges - Auth token updates
  • userWalletsChanges - Wallet changes
  • isAuthenticatedFlow - Auth status
Your UI updates automatically when you collect these flows.

Implementation Patterns

1. Basic Session Management

Start with a simple session management setup using a ViewModel:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.dynamic.sdk.android.DynamicSDK
import com.dynamic.sdk.android.Models.BaseWallet
import com.dynamic.sdk.android.Models.UserProfile
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class SessionViewModel : ViewModel() {
    private val sdk = DynamicSDK.getInstance()

    private val _isAuthenticated = MutableStateFlow(false)
    val isAuthenticated: StateFlow<Boolean> = _isAuthenticated.asStateFlow()

    private val _isLoading = MutableStateFlow(true)
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()

    private val _user = MutableStateFlow<UserProfile?>(null)
    val user: StateFlow<UserProfile?> = _user.asStateFlow()

    private val _wallets = MutableStateFlow<List<BaseWallet>>(emptyList())
    val wallets: StateFlow<List<BaseWallet>> = _wallets.asStateFlow()

    init {
        setupObservers()
    }

    private fun setupObservers() {
        // Observe authentication state
        viewModelScope.launch {
            sdk.auth.authenticatedUserChanges.collect { user ->
                _isAuthenticated.value = user != null
                _user.value = user
                _isLoading.value = false
            }
        }

        // Observe wallet changes
        viewModelScope.launch {
            sdk.wallets.userWalletsChanges.collect { wallets ->
                _wallets.value = wallets
            }
        }
    }
}

2. Using Session Manager in Jetpack Compose

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import com.dynamic.sdk.android.DynamicSDK
import com.dynamic.sdk.android.UI.DynamicUI
import com.dynamic.sdk.android.core.ClientProps
import com.dynamic.sdk.android.core.LoggerLevel

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Initialize SDK at app launch
        val props = ClientProps(
            environmentId = "your-environment-id",
            appLogoUrl = "https://your-app.com/logo.png",
            appName = "Your App",
            redirectUrl = "yourapp://",
            appOrigin = "https://your-app.com",
            logLevel = LoggerLevel.DEBUG
        )
        DynamicSDK.initialize(props, applicationContext, this)

        setContent {
            MaterialTheme {
                Surface(modifier = Modifier.fillMaxSize()) {
                    Box(modifier = Modifier.fillMaxSize()) {
                        AppContent()
                        DynamicUI()
                    }
                }
            }
        }
    }
}

@Composable
fun AppContent() {
    val viewModel: SessionViewModel = viewModel()
    val isLoading by viewModel.isLoading.collectAsState()
    val isAuthenticated by viewModel.isAuthenticated.collectAsState()

    when {
        isLoading -> LoadingScreen()
        isAuthenticated -> MainAppScreen(viewModel)
        else -> LoginScreen()
    }
}

@Composable
fun LoadingScreen() {
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = androidx.compose.ui.Alignment.Center) {
        CircularProgressIndicator()
    }
}

3. Complete Session Manager

For production apps, implement a comprehensive session manager:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.dynamic.sdk.android.DynamicSDK
import com.dynamic.sdk.android.Models.BaseWallet
import com.dynamic.sdk.android.Models.UserProfile
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class SessionManager : ViewModel() {
    private val sdk = DynamicSDK.getInstance()

    private val _isAuthenticated = MutableStateFlow(false)
    val isAuthenticated: StateFlow<Boolean> = _isAuthenticated.asStateFlow()

    private val _user = MutableStateFlow<UserProfile?>(null)
    val user: StateFlow<UserProfile?> = _user.asStateFlow()

    private val _wallets = MutableStateFlow<List<BaseWallet>>(emptyList())
    val wallets: StateFlow<List<BaseWallet>> = _wallets.asStateFlow()

    private val _token = MutableStateFlow<String?>(null)
    val token: StateFlow<String?> = _token.asStateFlow()

    private val _isCreatingWallets = MutableStateFlow(false)
    val isCreatingWallets: StateFlow<Boolean> = _isCreatingWallets.asStateFlow()

    private val _errorMessage = MutableStateFlow<String?>(null)
    val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow()

    init {
        setupObservers()
    }

    private fun setupObservers() {
        // Initial values
        _isAuthenticated.value = sdk.auth.isAuthenticated()
        _user.value = sdk.auth.authenticatedUser
        _wallets.value = sdk.wallets.userWallets
        _token.value = sdk.auth.token

        // Check if wallets are being created
        if (_user.value != null && _wallets.value.isEmpty()) {
            _isCreatingWallets.value = true
        }

        // Observe authentication state
        viewModelScope.launch {
            sdk.auth.authenticatedUserChanges.collect { user ->
                _isAuthenticated.value = user != null
                _user.value = user

                if (user == null) {
                    // User logged out
                    _wallets.value = emptyList()
                    _isCreatingWallets.value = false
                } else if (_wallets.value.isEmpty()) {
                    // User just authenticated, wallets being created
                    _isCreatingWallets.value = true
                }
            }
        }

        // Observe wallet changes
        viewModelScope.launch {
            sdk.wallets.userWalletsChanges.collect { wallets ->
                _wallets.value = wallets

                // Wallets appeared, stop showing loading
                if (wallets.isNotEmpty()) {
                    _isCreatingWallets.value = false
                }
            }
        }

        // Observe token changes
        viewModelScope.launch {
            sdk.auth.tokenChanges.collect { token ->
                _token.value = token
            }
        }
    }

    fun logout() {
        viewModelScope.launch {
            try {
                sdk.auth.logout()
            } catch (e: Exception) {
                _errorMessage.value = "Logout failed: ${e.message}"
            }
        }
    }

    fun showUserProfile() {
        sdk.ui.showUserProfile()
    }
}

4. Home Screen with Session Management

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.unit.dp
import com.dynamic.sdk.android.Models.BaseWallet

@Composable
fun MainAppScreen(sessionManager: SessionManager) {
    val user by sessionManager.user.collectAsState()
    val wallets by sessionManager.wallets.collectAsState()
    val isCreatingWallets by sessionManager.isCreatingWallets.collectAsState()
    val errorMessage by sessionManager.errorMessage.collectAsState()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        // User info
        user?.let { userProfile ->
            Text(
                text = "Welcome, ${userProfile.email ?: "User"}!",
                style = MaterialTheme.typography.headlineMedium
            )
        }

        Spacer(modifier = Modifier.height(16.dp))

        // Wallets section
        when {
            isCreatingWallets -> {
                Row(
                    horizontalArrangement = Arrangement.spacedBy(8.dp),
                    verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
                ) {
                    CircularProgressIndicator(modifier = Modifier.size(24.dp))
                    Text("Creating wallets...")
                }
            }
            wallets.isEmpty() -> {
                Text("No wallets")
            }
            else -> {
                Text(
                    text = "Your Wallets",
                    style = MaterialTheme.typography.titleMedium
                )
                Spacer(modifier = Modifier.height(8.dp))
                LazyColumn {
                    items(wallets) { wallet ->
                        WalletCard(wallet)
                    }
                }
            }
        }

        Spacer(modifier = Modifier.height(16.dp))

        // Error message
        errorMessage?.let { error ->
            Text(
                text = error,
                color = MaterialTheme.colorScheme.error,
                style = MaterialTheme.typography.bodySmall
            )
        }

        Spacer(modifier = Modifier.height(16.dp))

        // Actions
        Button(
            onClick = { sessionManager.showUserProfile() },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("Show Profile")
        }

        Button(
            onClick = { sessionManager.logout() },
            modifier = Modifier.fillMaxWidth(),
            colors = ButtonDefaults.buttonColors(
                containerColor = MaterialTheme.colorScheme.error
            )
        ) {
            Text("Logout")
        }
    }
}

@Composable
fun WalletCard(wallet: BaseWallet) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 8.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = wallet.chain.uppercase(),
                style = MaterialTheme.typography.labelMedium,
                color = MaterialTheme.colorScheme.primary
            )
            Text(
                text = wallet.address,
                style = MaterialTheme.typography.bodyMedium,
                maxLines = 1
            )
        }
    }
}

5. Navigation Based on Auth State

Handle navigation when authentication state changes:
import androidx.compose.runtime.*
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.dynamic.sdk.android.DynamicSDK
import kotlinx.coroutines.launch

@Composable
fun NavigationRoot() {
    val sdk = DynamicSDK.getInstance()
    val navController = rememberNavController()
    var isAuthenticated by remember { mutableStateOf(sdk.auth.isAuthenticated()) }

    // Listen for auth changes
    LaunchedEffect(Unit) {
        sdk.auth.authenticatedUserChanges.collect { user ->
            isAuthenticated = user != null
            if (user != null) {
                navController.navigate("home") {
                    popUpTo("login") { inclusive = true }
                }
            } else {
                navController.navigate("login") {
                    popUpTo("home") { inclusive = true }
                }
            }
        }
    }

    NavHost(
        navController = navController,
        startDestination = if (isAuthenticated) "home" else "login"
    ) {
        composable("login") {
            LoginScreen()
        }
        composable("home") {
            MainAppScreen(SessionManager())
        }
    }
}

Best Practices

1. Always Collect on Main Dispatcher

When updating UI state from flows, make sure to use the main dispatcher:
viewModelScope.launch {
    sdk.auth.authenticatedUserChanges.collect { user ->
        // This automatically runs on Main dispatcher in ViewModelScope
        _isAuthenticated.value = user != null
    }
}

2. Initialization Order

Always initialize the SDK at app launch:
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Initialize SDK before setting content
        val props = ClientProps(
            environmentId = "your-env-id",
            appName = "Your App",
            redirectUrl = "yourapp://",
            appOrigin = "https://your-app.com"
        )
        DynamicSDK.initialize(props, applicationContext, this)

        setContent {
            // Now safe to use SDK
            AppContent()
        }
    }
}

3. Check Initial State

Always check the current state before setting up listeners:
fun startListening() {
    // Check current state first
    _user.value = sdk.auth.authenticatedUser
    _wallets.value = sdk.wallets.userWallets

    // Then set up listeners
    viewModelScope.launch {
        sdk.auth.authenticatedUserChanges.collect { ... }
    }
}

4. Handle Wallet Creation Loading State

Wallets are created asynchronously after authentication:
// Show loading state when user is authenticated but wallets haven't appeared yet
if (user != null && wallets.isEmpty()) {
    _isCreatingWallets.value = true
}

// Clear loading state when wallets appear
viewModelScope.launch {
    sdk.wallets.userWalletsChanges.collect { newWallets ->
        if (newWallets.isNotEmpty()) {
            _isCreatingWallets.value = false
        }
    }
}

Troubleshooting

Common Issues

State not updating
  • Ensure you’re using viewModelScope.launch for coroutine collection
  • Verify flows are collected in the correct scope
  • Check that your ViewModel is using StateFlow and collectAsState() properly
Navigation not working after login
  • Make sure you’re subscribed to authenticatedUserChanges before the user authenticates
  • Check that your navigation logic handles the case where user is already authenticated
Wallets not appearing
  • Wallets are created asynchronously after authentication
  • Collect from userWalletsChanges to receive updates
  • Check that embedded wallets are enabled in your Dynamic dashboard
  • Ensure proper Flow collection with collectAsState() in Compose

Debug Session State

Add logging to understand state changes:
viewModelScope.launch {
    sdk.auth.authenticatedUserChanges.collect { user ->
        android.util.Log.d("Session", "Auth state changed: ${user != null}")
        user?.let {
            android.util.Log.d("Session", "User ID: ${it.userId}")
        }
    }
}

viewModelScope.launch {
    sdk.wallets.userWalletsChanges.collect { wallets ->
        android.util.Log.d("Session", "Wallets updated: ${wallets.size} wallets")
        wallets.forEach { wallet ->
            android.util.Log.d("Session", "  - ${wallet.chain}: ${wallet.address}")
        }
    }
}

What’s Next

Now that you have session management set up, you can:
  1. Authentication Guide - Implement user authentication flows
  2. Wallet Operations - Work with wallet balances and signing
  3. EVM Operations - Perform EVM transactions
  4. Solana Operations - Send Solana transactions