This file holds the shared Android patterns that cut across Chat, Video, and Feeds. Load it before product-specific references when you need lifecycle, auth, or architecture guidance.
Client model: one-per-process vs one-per-user. Chat and Video run one client per process:
ChatClient.Builder(...).build()andStreamVideoBuilder(...).build()register process-wide singletons (ChatClient.instance()/StreamVideo.instance()), and you swap the identity on that single client viaconnectUser/disconnect(Chat) orlogOut+StreamVideo.removeClient+ a fresh builder (Video). Feeds is different:FeedsClientis constructed with theUserandtokenProviderbaked in, there is noinstance()accessor, and there is noconnectUsercall to change identity afterwards. The Feeds client is bound to one user for its lifetime — to operate as a different user you mustdisconnect()the currentFeedsClientand build a new one with the newUser+tokenProvider. Hold the reference yourself (lateinit onApplication, Hilt@Singleton, Koinsingle); the SDK won't keep it alive for you.
Match the project that already exists:
- Compose-only app: ownership usually starts in
Applicationand a single-Activity host with aNavHost - XML / Views app: ownership usually starts in
Application, anAppCompatActivity, or a Fragment-based shell - Mixed app: keep Stream setup at the shared boundary (
Applicationor app-scoped DI) instead of duplicating it per screen
Do not rewrite the app into a different UI framework unless the user asks.
Create each Stream client once, store it in an owned object, and pass or inject it from there.
Good ownership points:
Application.onCreate()- a Hilt
@Singletonprovider, Koinsingle, or other app-scoped DI binding - a composition root or
Appinitializer that runs before any Stream UI is rendered
Bad ownership points:
- inside a
@Composablebody - inside a
remember { ... }factory - inside a leaf
ActivityorFragmentthat does not own app lifecycle
Exact builder shapes and singleton accessors live in the matching reference file.
Use the simplest token shape that matches the user's environment:
- Backend exists: prefer a backend-issued Stream token via a
TokenProvider- the SDK will call it again automatically when the token expires. - No backend / demo flow: generate a token with the Stream CLI (binary is
stream- seecredentials.md). Never-expiring:getstream token <user_id>. Expiring:getstream token <user_id> --ttl 1h(units:s/m/h/d). - User pastes their own: accept it and pass it to the client.
Keep the split clear:
- client: API key,
User(id,name,image), user token - server: API secret and token minting (the CLI handles this automatically)
If the app already has its own auth system, extend that flow instead of adding a second login model beside it. Connect / build once per user session, not on every screen entry.
Stateful SDK helpers should have explicit ownership:
- Compose: obtain ViewModels via
viewModels { factory }(Activity / Fragment) orhiltViewModel()(Hilt). Pass them into Composables; neverremember { SomeViewModel(...) }. - Views: retain ViewModels via
ViewModelProvideror DI; collect SDKFlows inrepeatOnLifecycle(Lifecycle.State.STARTED).
Avoid creating query objects, channel controllers, message lists, or call objects inside Composables — hoist them into a ViewModel.
Keep UI updates on the main dispatcher. In Compose, collect Flow state with collectAsStateWithLifecycle(); in Activities / Fragments, use lifecycleScope.launch { repeatOnLifecycle(STARTED) { ... } }.
Preserve the project's concurrency style. If the app already uses coroutines + Flow, stay there. If parts of it still use callbacks, bridge carefully instead of rewriting unrelated code.
The Android Chat and Video modules don't collide at the type level (User, theming, and DI primitives live under different package names). Build both clients in Application.onCreate() with the same API key and JWT token.
Before calling the work done, confirm:
- the right Stream artifact is added to the right module's
build.gradle.kts - each Stream client is built before any Stream UI renders
- for Video,
CAMERA/RECORD_AUDIOare requested at runtime before the firstcall.join(...) - the requested user can connect without leaking the API secret
- the edited flow works within the existing navigation structure (Compose
NavHost, Navigation Component, or Fragment back stack) - user switching or logout tears down the previous session cleanly