Documentation Index
Fetch the complete documentation index at: https://docs.privacyboost.io/llms.txt
Use this file to discover all available pages before exploring further.
Performance
This guide covers performance optimization strategies for the Privacy Boost Android SDK. The native binary is small enough that bundle size is rarely the bottleneck — the costs to manage are SDK init, proof generation, balance refresh frequency, and coroutine hygiene.
SDK Lifecycle
One Instance Per Process
PrivacyBoost holds a JWT, identity keys, and an internal note cache. Initialize it once and inject it via your DI container of choice (Hilt, Koin, manual Application-scoped singleton).
class App : Application() {
lateinit var privacyBoost: PrivacyBoost
private set
override fun onCreate() {
super.onCreate()
val config = PrivacyBoostConfig(
serverUrl = "https://test-api.privacyboost.io",
chainId = null,
shieldContractAddress = null,
wethContractAddress = "0x4200000000000000000000000000000000000006",
teePublicKey = null,
appId = "app_abc123xyz",
persistenceStorage = null,
persistenceUnlock = null,
)
privacyBoost = PrivacyBoost(config)
}
}
Re-instantiating the SDK on every screen would re-derive caches, refetch the merkle tree, and force a fresh JWT.
Defer Init Until First Use
If your app has flows that don’t touch the privacy pool (onboarding, marketing screens), defer SDK construction until the first gated route. The Rust core does some setup work on first call — keep it off the cold-start path.
class PrivacyBoostHolder(private val config: PrivacyBoostConfig) {
@Volatile private var instance: PrivacyBoost? = null
fun get(): PrivacyBoost =
instance ?: synchronized(this) {
instance ?: PrivacyBoost(config).also { instance = it }
}
}
Coroutine Scope Hygiene
All network/proof methods are suspend — call them from a structured scope, not a fire-and-forget GlobalScope.launch.
Use Lifecycle-Aware Scopes
class WalletViewModel(
private val sdk: PrivacyBoost,
) : ViewModel() {
fun deposit(token: String, amount: String) = viewModelScope.launch {
runCatching { sdk.shield(token, amount) }
.onSuccess { /* update UI */ }
.onFailure { /* show error */ }
}
}
viewModelScope cancels when the ViewModel is cleared, so a deposit that’s still proving when the user navigates away gets cleanly cancelled instead of leaking.
Run on the Right Dispatcher
The SDK does its own native work and is safe to call from Dispatchers.Main, but if you’re chaining heavy serialization or DB writes around it, hop to Dispatchers.Default or Dispatchers.IO:
val result = withContext(Dispatchers.IO) {
sdk.shield(token, amount)
}
Balance Refresh
Don’t Poll From Every Composable
getBalance(token) and getAllBalances() hit the network. Centralize refreshes through a single repository / StateFlow and let UI subscribe.
class BalanceRepository(
private val sdk: PrivacyBoost,
private val staleness: Duration = 30.seconds,
) {
private val _balances = MutableStateFlow<Map<String, TokenBalance>>(emptyMap())
val balances: StateFlow<Map<String, TokenBalance>> = _balances.asStateFlow()
private var lastFetch: Instant = Instant.DISTANT_PAST
private val mutex = Mutex()
suspend fun refresh(force: Boolean = false) = mutex.withLock {
val now = Clock.System.now()
if (!force && now - lastFetch < staleness) return@withLock
val fresh = sdk.getAllBalances().associateBy { it.tokenAddress }
_balances.value = fresh
lastFetch = now
}
}
The Mutex collapses concurrent calls — overlapping refreshes wait on the in-flight one.
Refresh on Resume, Not on a Timer
Tying refresh to a delay() loop drains battery. Tie it to lifecycle events:
@Composable
fun WalletScreen(repo: BalanceRepository) {
val lifecycleOwner = LocalLifecycleOwner.current
val balances by repo.balances.collectAsStateWithLifecycle()
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
lifecycleOwner.lifecycleScope.launch { repo.refresh() }
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
BalanceList(balances.values.toList())
}
For periodic background refresh outside the app, use WorkManager with a minimum-interval periodic request — not a foreground service.
Network
Parallelize Independent Reads
Sequential awaits serialize unnecessarily. Use async:
coroutineScope {
val balances = async { sdk.getAllBalances() }
val history = async { sdk.getTransactionHistory(null, 20u, null) }
val fees = async { sdk.getFees(tokenId) }
Triple(balances.await(), history.await(), fees.await())
}
Page History — Don’t Fetch It All
Pass a small limit and use the returned cursor for pagination:
val firstPage = sdk.getTransactionHistory(
txType = null,
limit = 20u,
cursor = null,
)
// later
firstPage.next?.let { cursor ->
val nextPage = sdk.getTransactionHistory(
txType = null,
limit = 20u,
cursor = cursor,
)
}
Memory
Trim When Backgrounded
If your app stays in the background long enough to receive onTrimMemory(TRIM_MEMORY_BACKGROUND) or higher, drop non-essential caches — pending transaction lists, off-screen history pages — but keep the SDK instance.
class App : Application(), ComponentCallbacks2 {
override fun onTrimMemory(level: Int) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
balanceRepo.evictNonCurrentTokens()
transactionRepo.evictBeyondVisibleWindow()
}
}
override fun onConfigurationChanged(newConfig: Configuration) {}
override fun onLowMemory() {}
}
Use Stable Keys in Lists
In LazyColumn, key transactions by txHash so Compose reuses items instead of rebuilding:
LazyColumn {
items(transactions, key = { it.txHash }) { tx ->
TransactionRow(tx)
}
}
Operation Timing
Wrap SDK calls to observe timing in development:
suspend inline fun <T> timed(label: String, block: () -> T): T {
val start = System.nanoTime()
return try {
block()
} finally {
val ms = (System.nanoTime() - start) / 1_000_000
Log.d("PB", "$label: ${ms}ms")
}
}
val result = timed("shield") { sdk.shield(token, amount) }
In production, route timings into your APM (Firebase Performance, Sentry Performance, etc.) to surface slow operations as percentile histograms.
What the SDK Already Optimizes
Things you do not need to layer on top:
- Note cache — the SDK caches unspent notes locally and only refetches new merkle tree leaves.
- Merkle tree pruning — proven nodes are kept; the rest are pruned.
- JWT reuse —
clearSession() keeps identity keys so the next authenticate() is fast.
Best Practices Summary
- One SDK instance per process — hold it in
Application or your DI graph.
- Defer SDK init until the first privacy-pool screen.
- Use
viewModelScope / lifecycleScope — never GlobalScope.
- Refresh balances on
ON_RESUME and after writes, not on a timer.
- Deduplicate concurrent refreshes with a
Mutex or single shared flow.
- Parallelize independent reads with
async.
- Page transaction history; don’t fetch it all.
- Use
clearSession() over logout() when re-auth is expected soon.
Next Steps