APIExample-Compose/
├── gradle.properties # rtc_sdk_version
├── AGENTS.md # Agent entry point — build commands, red lines, skill index
├── ARCHITECTURE.md # This file — directory layout, patterns, registration
├── .kiro/
│ ├── hooks/
│ │ └── build-on-task-complete.json # Runs assembleDebug after each spec task completes
│ ├── skills/
│ │ ├── add-new-case/SKILL.md # Step-by-step guide for adding a new Compose case
│ │ └── query-cases/SKILL.md # Query existing cases by API, group, or list position
│ └── steering/
│ ├── project-routing.md # Which sub-project to use; hard constraints (always included)
│ ├── coding-standards.md # RtcEngine lifecycle, Kotlin/Compose rules (always included)
│ └── complex-case-spec.md # Spec workflow for complex cases (manual inclusion)
└── app/src/main/
├── AndroidManifest.xml
├── assets/ # Audio/video sample files
├── res/
│ └── values/strings.xml # Display name strings (prefix: example_*)
└── java/io/agora/api/example/compose/
├── APIExampleApp.kt # Application class
├── MainActivity.kt # Single-Activity, sets content to NavGraph()
├── NavGraph.kt # Compose Navigation host — home / settings / example
│
├── model/
│ ├── Example.kt # data class: name: Int, content: @Composable
│ ├── Examples.kt # Hardcoded lists: BasicExampleList, AdvanceExampleList
│ └── Components.kt # Groups the two lists into Components for the home screen
│
├── samples/ # One .kt file per case — all @Composable
│ ├── JoinChannelVideoToken.kt # Basic: "Join Video Channel (With Token)"
│ ├── JoinChannelVideo.kt # Basic: "Join Video Channel" — canonical reference
│ ├── JoinChannelAudio.kt # Basic: "Join Audio Channel"
│ ├── LiveStreaming.kt # Advanced: "Live Streaming" — setClientRole
│ ├── RTMPStreaming.kt # Advanced: "RTMP Streaming" — push to CDN
│ ├── MediaMetadata.kt # Advanced: "Media Metadata" — send/receive metadata
│ ├── VoiceEffects.kt # Advanced: "Voice Effects" — voice beautifier/effects
│ ├── OriginAudioData.kt # Advanced: "Origin Audio Data" — raw audio processing
│ ├── CustomAudioSource.kt # Advanced: "Custom Audio Source" — push external audio
│ ├── CustomAudioRender.kt # Advanced: "Custom Audio Render" — pull audio rendering
│ ├── OriginVideoData.kt # Advanced: "Origin Video Data" — raw video processing
│ ├── CustomVideoSource.kt # Advanced: "Custom Video Source" — push external video
│ ├── CustomVideoRender.kt # Advanced: "Custom Video Render" — custom video rendering
│ ├── PictureInPicture.kt # Advanced: "Picture In Picture" — PiP mode
│ ├── JoinMultiChannel.kt # Advanced: "Join Multi Channel" — multi-channel join
│ ├── ChannelEncryption.kt # Advanced: "Channel Encryption" — built-in encryption
│ ├── PlayAudioFiles.kt # Advanced: "Play Audio Files" — audio mixing
│ ├── PreCallTest.kt # Advanced: "Pre Call Test" — network/device test
│ ├── MediaRecorder.kt # Advanced: "Media Recorder" — record media streams
│ ├── MediaPlayer.kt # Advanced: "Media Player" — play media files
│ ├── ScreenSharing.kt # Advanced: "Screen Sharing" — screen capture & share
│ ├── VideoProcessExtension.kt # Advanced: "Video Process Extension" — video filter
│ ├── RhythmPlayer.kt # Advanced: "Rhythm Player" — metronome playback
│ ├── LocalVideoTranscoding.kt # Advanced: "Local Video Transcoding" — local compositing
│ ├── SendDataStream.kt # Advanced: "Send Data Stream" — data channel messaging
│ ├── HostAcrossChannel.kt # Advanced: "Host Across Channel" — cross-channel relay
│ ├── SpatialSound.kt # Advanced: "Spatial Sound" — 3D spatial audio
│
├── ui/
│ ├── home/
│ │ └── Home.kt # Home screen — renders grouped example list
│ ├── example/
│ │ ├── Example.kt # Wrapper screen: calls example.content(back)
│ │ └── ExampleItem.kt # Single row in the example list
│ ├── settings/
│ │ └── Settings.kt # Settings screen (area, resolution, frame rate)
│ ├── common/
│ │ ├── APIExampleScaffold.kt # Shared scaffold with top bar
│ │ ├── APIExampleTopAppBar.kt
│ │ └── Widgets.kt # ChannelNameInput, VideoGrid, VideoStatsInfo, etc.
│ └── theme/
│ └── Theme.kt
│
├── data/
│ └── SettingPreferences.kt # DataStore-backed settings (area, resolution, frame rate)
│
└── utils/
├── TokenUtils.java # Fetches RTC tokens from Agora token server
├── AudioFileReader.java
├── AudioPlayer.java
├── VideoFileReader.java
├── FileUtils.java
├── YUVUtils.java
├── YuvFboProgram.java
├── YuvUploader.java
└── GLTextureView.java
Registration is manual — no reflection, no annotation scanning.
To add a case, edit exactly two files:
1. model/Examples.kt — append to BasicExampleList or AdvanceExampleList:
val AdvanceExampleList = listOf(
// … existing entries …
Example(R.string.example_my_new_case) { MyNewCase() }
)2. samples/MyNewCase.kt — create the Composable:
@Composable
fun MyNewCase() { … }No nav_graph.xml, no @Example annotation, no action ID. NavGraph.kt routes to cases by their
index in the list — the order in Examples.kt is the display order.
Every case follows a two-function structure. JoinChannelVideo.kt is the canonical reference.
MyNewCase() ← public, stateful: owns RtcEngine, state, permissions
└── MyNewCaseView(...) ← private, stateless: receives data + lambdas, pure UI
Engine creation and cleanup:
val rtcEngine = remember {
RtcEngine.create(RtcEngineConfig().apply {
mContext = context
mAppId = BuildConfig.AGORA_APP_ID
mEventHandler = object : IRtcEngineEventHandler() { … }
})
}
DisposableEffect(lifecycleOwner) { // key must be lifecycleOwner, not Unit
onDispose {
if (isJoined) rtcEngine.leaveChannel()
RtcEngine.destroy()
}
}Permissions:
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { grantedMap ->
if (grantedMap.values.all { it }) { /* join channel */ }
}
// trigger:
permissionLauncher.launch(arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA))State rules:
rememberSaveable— values that must survive rotation (channelName, isJoined, uid)remember— objects that must not be recreated (RtcEngine, collections)IRtcEngineEventHandlercallbacks can mutate Compose state directly — the snapshot system is thread-safe
TokenUtils.gen(channelName, uid) { token ->
rtcEngine.joinChannel(token, channelName, uid, options)
}