Skip to content

Conversation

@ttypic
Copy link
Contributor

@ttypic ttypic commented Aug 18, 2025

Added an Android Compose-based example application demonstrating Ably Live Objects integration. Includes tasks management and color voting screens.

Screen_recording_20250819_111640.webm

Summary by CodeRabbit

  • New Features

    • Added an Android Jetpack Compose example app showcasing real-time Color Voting and Task Management with multi-device sync, theming, icons, splash, sandbox auth fallback, and UI for adding/editing tasks.
  • Documentation

    • Example app setup/run/build instructions and troubleshooting added; Live Objects overview added to main README.
  • Chores

    • Bumped Android Gradle Plugin, added Android/Kotlin/Compose plugin aliases, and included the examples module.
  • Tests

    • Added instrumented UI tests and a CI workflow to run emulator tests.

@ttypic ttypic requested a review from sacOO7 August 18, 2025 18:52
@coderabbitai
Copy link

coderabbitai bot commented Aug 18, 2025

Walkthrough

Adds a new Android "examples" Compose app module (UI, resources, tests, CI) and registers it in settings; updates the root Gradle plugin aliases and version catalog (AGP bumped to 8.6.1 and many Compose/AndroidX deps/plugins added); introduces Ably Live Objects utilities, Sandbox auth helper, and Compose screens.

Changes

Cohort / File(s) Summary
Root build & catalog
build.gradle.kts, gradle/libs.versions.toml
Adds plugin aliases alias(libs.plugins.android.application) apply false, alias(libs.plugins.kotlin.android) apply false, alias(libs.plugins.kotlin.compose) apply false; bumps agp to 8.6.1 and adds many Compose/AndroidX library and plugin entries to the version catalog.
Project settings
settings.gradle.kts
Registers new module: include("examples").
Examples: build & CI
examples/build.gradle.kts, examples/proguard-rules.pro, examples/.gitignore, .github/workflows/example-app.yml
New module Gradle config (Compose enabled, BuildConfig ABLY_KEY from local.properties, optional signing config, Java/Kotlin targets, deps including Compose BOM and project modules); ProGuard template; ignores /build; CI workflow to run connected Android tests on emulator.
Examples: manifest & resources
examples/src/main/AndroidManifest.xml, examples/src/main/res/...
Adds exported launcher MainActivity and resources: splash background, adaptive icons, colors, ic_launcher_background, app name string, theme definitions.
Examples: app entry & auth sandbox
examples/src/main/kotlin/com/ably/example/MainActivity.kt, examples/src/main/kotlin/com/ably/example/Sandbox.kt
MainActivity lazily creates/configures AblyRealtime (uses BuildConfig.ABLY_KEY or sandbox token flow), manages lifecycle, and sets Compose content; Sandbox creates/caches sandbox app credentials via an HttpClient with retry/backoff.
Examples: realtime utilities & bridges
examples/src/main/kotlin/com/ably/example/Utils.kt
Adds coroutine wrappers and Compose helpers for Ably Live Objects (LiveMap/LiveCounter), channel setup, suspend bridges, and composable observers integrating lifecycle.
Examples: UI screens
examples/src/main/kotlin/com/ably/example/screen/*
Adds MainScreen (tabs), ColorVotingScreen & ColorVoteCard, TaskManagementScreen & TaskItemCard — wiring live counters/maps to UI and vote/add/edit/delete actions.
Examples: theme & typography
examples/src/main/kotlin/com/ably/example/ui/theme/*
Adds color constants, Material3 AblyTheme with dynamic color support, and Typography settings.
Examples: tests
examples/src/androidTest/kotlin/com/ably/example/*
Adds instrumentation tests: MainScreenTest (tabs displayed) and ColorVotingScreenTest (increment red vote UI test).
Docs
examples/README.md, README.md
Adds examples README with run/signing/troubleshooting instructions and updates root README with Live Objects section referencing the example app.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant Activity as MainActivity
  participant UI as Compose UI
  participant Ably as AblyRealtime
  participant Sandbox as Sandbox (optional)
  participant Channel as Channel
  participant LO as Live Objects

  User->>Activity: launch app
  Activity->>Ably: build client (use BuildConfig.ABLY_KEY or request Sandbox token)
  note right of Sandbox #e6ffed: Sandbox creates/caches appId & apiKey via HTTP with retries
  Activity->>Ably: connect()
  Activity->>UI: setContent(MainScreen)

  UI->>Ably: getRealtimeChannel("objects-live-*")
  Ably->>Channel: create/configure channel (attachOnSubscribe=false, object modes)
  Channel->>LO: attach / resolve root LiveMap / counters / maps
  UI->>LO: observe counters/maps (compose observers)
  alt User votes
    UI->>LO: counter.incrementCoroutine(1)
    LO-->>UI: updated value → recomposition
  end
  alt User modifies tasks
    UI->>LO: map.setCoroutine / removeCoroutine
    LO-->>UI: updated entries → recomposition
  end

  Activity-->>Ably: close() on stop/pause
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

Hop hop — a module springs from the ground,
Tabs and counters make a cheerful sound.
Tasks that wobble, votes that climb,
Channels hum in realtime rhyme.
Gradle aligned, plugins in tow — rabbit builds on time. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/lo-example-app

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot temporarily deployed to staging/pull/1150/features August 18, 2025 18:53 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1150/javadoc August 18, 2025 18:54 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (26)
example/.gitignore (1)

1-1: LGTM. Anchored ignore correctly targets the module’s build dir

Using "/build" in a submodule .gitignore anchors to example/build, which is what we want.

If you prefer the conventional pattern used across many Android projects, consider "build/" which is also clear and ignores the directory (not files named build).

-/build
+build/
example/src/main/res/values/strings.xml (1)

1-3: Externalize user-visible strings used in UI components

The app currently hardcodes several UI strings in composables. To support localization and consistency, add them here and reference via stringResource in code.

Apply this diff to add the missing strings:

 <resources>
     <string name="app_name">Ably Example</string>
+    <string name="vote_title">Vote for your favorite color</string>
+    <string name="color_red">Red</string>
+    <string name="color_blue">Blue</string>
+    <string name="color_green">Green</string>
+    <string name="action_vote">Vote</string>
+    <string name="action_reset_all">Reset all</string>
 </resources>
example/src/main/java/com/ably/example/screen/ColorVotingScreen.kt (6)

22-24: Remove unnecessary ExperimentalMaterial3Api opt-in

Nothing in this file appears to use experimental Material3 APIs. Dropping the opt-in avoids widening the experimental surface.

-@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun ColorVotingScreen(realtimeClient: AblyRealtime) {

25-27: Extract channel name to a constant to avoid magic strings

This improves maintainability and reusability if the channel name changes or is shared across screens.

-  val channel = getRealtimeChannel(realtimeClient, "objects-live-counter")
+  val channel = getRealtimeChannel(realtimeClient, CHANNEL_NAME)

Add these declarations near the top of the file (outside the composable):

private const val CHANNEL_NAME = "objects-live-counter"

Optionally, do the same for the counter keys:

private const val KEY_RED = "red"
private const val KEY_BLUE = "blue"
private const val KEY_GREEN = "green"

And then use KEY_RED/KEY_BLUE/KEY_GREEN below.


30-32: Nit: add a space after the comma for readability

Minor formatting improves readability and consistency.

-  val (redCount, redCounter, resetRed) = observeCounter(channel, root,"red")
-  val (blueCount, blueCounter, resetBlue) = observeCounter(channel, root,"blue")
-  val (greenCount, greenCounter, resetGreen) = observeCounter(channel, root,"green")
+  val (redCount, redCounter, resetRed) = observeCounter(channel, root, "red")
+  val (blueCount, blueCounter, resetBlue) = observeCounter(channel, root, "blue")
+  val (greenCount, greenCounter, resetGreen) = observeCounter(channel, root, "green")

If you applied constants from the previous comment:

-  val (redCount, redCounter, resetRed) = observeCounter(channel, root, "red")
-  val (blueCount, blueCounter, resetBlue) = observeCounter(channel, root, "blue")
-  val (greenCount, greenCounter, resetGreen) = observeCounter(channel, root, "green")
+  val (redCount, redCounter, resetRed) = observeCounter(channel, root, KEY_RED)
+  val (blueCount, blueCounter, resetBlue) = observeCounter(channel, root, KEY_BLUE)
+  val (greenCount, greenCounter, resetGreen) = observeCounter(channel, root, KEY_GREEN)

3-15: Externalize hardcoded strings and use stringResource

This screen hardcodes user-facing text. Switch to stringResource to enable localization and consistency with Android best practices. Also import stringResource and R.

Add imports:

 import androidx.compose.ui.unit.sp
+import androidx.compose.ui.res.stringResource
+import com.ably.example.R

Replace literals:

-    Text(
-      text = "Vote for your favorite color",
+    Text(
+      text = stringResource(R.string.vote_title),
       fontSize = 24.sp,
       fontWeight = FontWeight.Bold,
       textAlign = TextAlign.Center,
       modifier = Modifier.padding(vertical = 16.dp)
     )
-    ColorVoteCard(
+    ColorVoteCard(
       color = Color.Red,
-      colorName = "Red",
+      colorName = stringResource(R.string.color_red),
       count = redCount ?: 0,
-    ColorVoteCard(
+    ColorVoteCard(
       color = Color.Blue,
-      colorName = "Blue",
+      colorName = stringResource(R.string.color_blue),
       count = blueCount ?: 0,
-    ColorVoteCard(
+    ColorVoteCard(
       color = Color.Green,
-      colorName = "Green",
+      colorName = stringResource(R.string.color_green),
       count = greenCount ?: 0,
-          Text(
-            text = "Vote",
+          Text(
+            text = stringResource(R.string.action_vote),
             fontWeight = FontWeight.Medium
           )

Note: Add the corresponding strings to values/strings.xml as suggested in the other comment.

Also applies to: 41-47, 50-53, 60-64, 71-75, 149-152


91-95: Avoid hardcoding text color on Button to preserve theme compatibility

Hardcoding Color.White can reduce contrast in dark/light themes. Let MaterialTheme provide the appropriate content color.

-      Text(
-        text = "Reset all",
-        color = Color.White,
-        fontWeight = FontWeight.Medium
-      )
+      Text(
+        text = stringResource(R.string.action_reset_all),
+        fontWeight = FontWeight.Medium
+      )

If you choose to keep a custom color, prefer MaterialTheme.colorScheme.onPrimary over Color.White.


54-57: Optional: add basic error handling around network operations

If incrementCoroutine throws (e.g., network failure), the coroutine will cancel silently. Consider wrapping with runCatching or try/catch to surface errors (e.g., Snackbar/Toast) in the example.

-        scope.launch {
-          redCounter?.incrementCoroutine(1)
-        }
+        scope.launch {
+          runCatching { redCounter?.incrementCoroutine(1) }
+            .onFailure { /* TODO: surface error */ }
+        }

Apply similarly to blue/green if desired.

Also applies to: 65-68, 76-79

example/src/main/java/com/ably/example/Utils.kt (5)

190-197: Simplify reset and avoid unnecessary coroutineScope/launch nesting

The lambda is already suspend; no need to nest another scope + launch.

-  return CounterState(counterValue, counter) {
-    coroutineScope {
-      launch {
-        counter = channel.objects.createCounterCoroutine().also {
-          root?.setCoroutine(path, LiveMapValue.of(it))
-        }
-      }
-    }
-  }
+  return CounterState(counterValue, counter) {
+    val newCounter = channel.objects.createCounterCoroutine()
+    root?.setCoroutine(path, LiveMapValue.of(newCounter))
+    counter = newCounter
+  }

256-259: Only fetch the root object when the channel is attached

Fetching the root while in initialized may race or fail on some setups. Prefer waiting for attached.

-    if (channelState == ChannelState.attached || channelState == ChannelState.initialized) {
+    if (channelState == ChannelState.attached) {
       root = channel.objects.getRootCoroutines()
     }

91-101: Harden updateOptions error path to handle null ErrorInfo

onError can be invoked with a null reason. Avoid NPE in fromErrorInfo.

 suspend fun Channel.updateOptions(options: ChannelOptions): Unit = suspendCancellableCoroutine { continuation ->
   setOptions(options, object : io.ably.lib.realtime.CompletionListener {
     override fun onSuccess() {
       continuation.resume(Unit)
     }
 
     override fun onError(reason: ErrorInfo?) {
-      continuation.cancel(AblyException.fromErrorInfo(reason))
+      val cause: Throwable = reason?.let(AblyException::fromErrorInfo)
+        ?: IllegalStateException("setOptions failed: no reason provided")
+      continuation.cancel(cause)
     }
   })
 }

269-279: Move setOptions/attach to LaunchedEffect and use the suspending wrapper to avoid races

Setting channel options and attaching immediately inside DisposableEffect risks a race and ignores failures. Use LaunchedEffect with your suspending updateOptions and keep detach in DisposableEffect.

-  DisposableEffect(channel) {
-    channel.setOptions(ChannelOptions().apply {
-      attachOnSubscribe = false
-      modes = arrayOf(ChannelMode.object_publish, ChannelMode.object_subscribe)
-    })
-
-    channel.attach()
-
-    onDispose {
-      channel.detach()
-    }
-  }
+  LaunchedEffect(channel) {
+    try {
+      channel.updateOptions(ChannelOptions().apply {
+        attachOnSubscribe = false
+        modes = arrayOf(ChannelMode.object_publish, ChannelMode.object_subscribe)
+      })
+    } catch (_: Throwable) {
+      // For the example app, we ignore/log; production apps should surface this.
+    }
+    channel.attach()
+  }
+  DisposableEffect(channel) {
+    onDispose { channel.detach() }
+  }

103-112: Optional: require non-null root in helpers to enforce correct call sites

Making root non-null at the API level prevents accidental creation of orphaned child objects before the root is available.

-suspend fun getOrCreateCounter(channel: Channel, root: LiveMap?, path: String): LiveCounter {
-  val mapValue = root?.get(path)
+suspend fun getOrCreateCounter(channel: Channel, root: LiveMap, path: String): LiveCounter {
+  val mapValue = root.get(path)
   if (mapValue == null) {
     val counter = channel.objects.createCounterCoroutine()
-    root?.setCoroutine(path, LiveMapValue.of(counter))
+    root.setCoroutine(path, LiveMapValue.of(counter))
     return counter
   } else {
     return mapValue.asLiveCounter
   }
 }
@@
-suspend fun getOrCreateMap(channel: Channel, root: LiveMap?, path: String): LiveMap {
-  val mapValue = root?.get(path)
+suspend fun getOrCreateMap(channel: Channel, root: LiveMap, path: String): LiveMap {
+  val mapValue = root.get(path)
   if (mapValue == null) {
     val map = channel.objects.createMapCoroutine()
-    root?.setCoroutine(path, LiveMapValue.of(map))
+    root.setCoroutine(path, LiveMapValue.of(map))
     return map
   } else {
     return mapValue.asLiveMap
   }
 }

Then keep the guarded call sites you added in LaunchedEffect(root) { root?.let { ... } }.

Also applies to: 114-123

example/build.gradle.kts (2)

24-25: Avoid leaking secrets and ensure safe string insertion for BuildConfig.

Current approach injects ABLY_KEY into all build types and doesn’t escape quotes/backslashes. Prefer debug-only injection and escape the value.

Apply one of the following:

Option A (minimal): keep in defaultConfig, but escape and support env/Gradle property fallback.

-        buildConfigField("String", "ABLY_KEY", "\"${getLocalProperty("ABLY_KEY") ?: ""}\"")
+        val ablyKey = getLocalProperty("ABLY_KEY")
+            ?: System.getenv("ABLY_KEY")
+            ?: (findProperty("ABLY_KEY") as String? ?: "")
+        buildConfigField("String", "ABLY_KEY", "\"${escapeForBuildConfig(ablyKey)}\"")

Option B (preferred): move to debug buildType to avoid shipping keys in release builds.

-        buildConfigField("String", "ABLY_KEY", "\"${getLocalProperty("ABLY_KEY") ?: ""}\"")
     buildTypes {
+        debug {
+            val ablyKey = getLocalProperty("ABLY_KEY")
+                ?: System.getenv("ABLY_KEY")
+                ?: (findProperty("ABLY_KEY") as String? ?: "")
+            buildConfigField("String", "ABLY_KEY", "\"${escapeForBuildConfig(ablyKey)}\"")
+        }
         release {
             isMinifyEnabled = false
             proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+            // Intentionally not including ABLY_KEY by default to avoid shipping secrets
+            buildConfigField("String", "ABLY_KEY", "\"\"")
         }
     }

Support function (added below getLocalProperty):

// Escapes for Java string literal in BuildConfig
fun escapeForBuildConfig(value: String): String =
    value.replace("\\", "\\\\").replace("\"", "\\\"")

Also ensure example/local.properties is in .gitignore (or place ABLY_KEY only in root local.properties). I can open a small PR to add that ignore if you want.


70-78: Read local.properties from the project root and simplify I/O.

The current function likely resolves local.properties relative to the module, not the repo root, and manually manages streams.

Apply this refactor and helper addition:

-import java.io.FileInputStream
-import java.io.InputStreamReader
+import java.io.File
 import java.util.Properties
@@
-fun getLocalProperty(key: String, file: String = "local.properties"): String? {
-    val properties = Properties()
-    val localProperties = File(file)
-    if (!localProperties.isFile) return null
-    InputStreamReader(FileInputStream(localProperties), Charsets.UTF_8).use { reader ->
-        properties.load(reader)
-    }
-    return properties.getProperty(key)
-}
+fun getLocalProperty(key: String, file: String = "local.properties"): String? {
+    val properties = Properties()
+    // Resolve against repo root to match common Android convention
+    val localProperties = File(rootDir, file)
+    if (!localProperties.isFile) return null
+    localProperties.bufferedReader(Charsets.UTF_8).use { reader ->
+        properties.load(reader)
+    }
+    return properties.getProperty(key)
+}
+
+// Escapes for Java string literal in BuildConfig
+fun escapeForBuildConfig(value: String): String =
+    value.replace("\\", "\\\\").replace("\"", "\\\"")
example/src/main/java/com/ably/example/ui/theme/Theme.kt (1)

37-43: Consider defaulting dynamicColor to false for consistent branding.

Dynamic color can drift away from branded colors (Purple/Pink tokens). For showcase apps that demonstrate a product, fixed palettes often work better. Optional tweak.

example/src/main/java/com/ably/example/screen/MainScreen.kt (5)

13-13: Remove unnecessary ExperimentalMaterial3Api opt-in.

TopAppBar in Material3 is stable; the opt-in is likely no longer required.

Apply:

-@OptIn(ExperimentalMaterial3Api::class)

16-16: Persist selected tab across configuration changes.

Use rememberSaveable to keep the user’s selection on rotate/process death.

-    var selectedTab by remember { mutableIntStateOf(0) }
+    var selectedTab by androidx.compose.runtime.saveable.rememberSaveable { mutableIntStateOf(0) }

If you prefer, add an import:

import androidx.compose.runtime.saveable.rememberSaveable

26-31: Externalize user-visible strings for localization/testing.

"Ably Live Objects Demo", "Color Voting", "Task Management" should live in strings.xml and be referenced via stringResource.

I can follow up with a patch adding example/src/main/res/values/strings.xml and wiring stringResource() usage if you want.

Also applies to: 39-44


48-51: Guard against future out-of-range tab indices.

Add an else branch to keep the UI resilient if tabs change or state restores an invalid index.

         when (selectedTab) {
             0 -> ColorVotingScreen(realtimeClient)
             1 -> TaskManagementScreen(realtimeClient)
+            else -> ColorVotingScreen(realtimeClient)
         }

15-21: Minor: Make Tab definitions stable.

tabs is cheap but recreates on every recomposition. Consider moving to a top-level val or remember-ing it.

example/src/main/java/com/ably/example/MainActivity.kt (2)

34-37: Avoid duplicate connect/close churn; keep lifecycle wiring to one pair (either Start/Stop or Resume/Pause)

Calling connect in both onStart and onResume, and close in both onStop and onPause, can cause unnecessary reconnects and extra auth/attach churn. Prefer one pair; Start/Stop is usually the right granularity for networking.

Apply this diff to remove the redundant Resume/Pause handlers:

   override fun onStart() {
     super.onStart()
     realtimeClient.connect()
   }

-  override fun onResume() {
-    super.onResume()
-    realtimeClient.connect()
-  }
-
-  override fun onPause() {
-    super.onPause()
-    realtimeClient.close()
-  }

Also applies to: 44-51


16-20: Gate verbose logging to debug builds

Keep Ably logs quiet in release builds.

-        logLevel = Log.VERBOSE
+        logLevel = if (BuildConfig.DEBUG) Log.VERBOSE else Log.ERROR
example/src/main/java/com/ably/example/screen/TaskManagementScreen.kt (3)

80-83: Use UUIDs for task IDs to avoid collisions

Timestamps can collide across devices or rapid inserts. Prefer UUIDs.

-                  val taskId = "task_${System.currentTimeMillis()}"
+                  val taskId = "task_${java.util.UUID.randomUUID()}"

95-100: Snapshot keys before bulk delete to avoid mid-iteration inconsistencies

Capture keys to a list before issuing remote deletes. This avoids surprises if the backing map updates during the loop.

-              scope.launch {
-                taskIdToTask.forEach { task ->
-                  liveTasks?.removeCoroutine(task.key)
-                }
-              }
+              scope.launch {
+                val keys = taskIdToTask.keys.toList()
+                keys.forEach { key ->
+                  liveTasks?.removeCoroutine(key)
+                }
+              }

76-92: Consider basic error handling for set/remove operations

These suspending calls may fail (e.g., network/auth). Wrapping in try/catch and surfacing a short Snackbar would improve UX for the example without much complexity. If you want, I can draft a minimal SnackbarHost-based pattern.

Also applies to: 153-168

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2c36548 and 81f6a5a.

📒 Files selected for processing (23)
  • build.gradle.kts (1 hunks)
  • example/.gitignore (1 hunks)
  • example/build.gradle.kts (1 hunks)
  • example/proguard-rules.pro (1 hunks)
  • example/src/main/AndroidManifest.xml (1 hunks)
  • example/src/main/java/com/ably/example/MainActivity.kt (1 hunks)
  • example/src/main/java/com/ably/example/Utils.kt (1 hunks)
  • example/src/main/java/com/ably/example/screen/ColorVotingScreen.kt (1 hunks)
  • example/src/main/java/com/ably/example/screen/MainScreen.kt (1 hunks)
  • example/src/main/java/com/ably/example/screen/TaskManagementScreen.kt (1 hunks)
  • example/src/main/java/com/ably/example/ui/theme/Color.kt (1 hunks)
  • example/src/main/java/com/ably/example/ui/theme/Theme.kt (1 hunks)
  • example/src/main/java/com/ably/example/ui/theme/Type.kt (1 hunks)
  • example/src/main/res/drawable/ic_launcher_background.xml (1 hunks)
  • example/src/main/res/drawable/ic_launcher_foreground.xml (1 hunks)
  • example/src/main/res/mipmap-anydpi/ic_launcher.xml (1 hunks)
  • example/src/main/res/mipmap-anydpi/ic_launcher_round.xml (1 hunks)
  • example/src/main/res/values/colors.xml (1 hunks)
  • example/src/main/res/values/strings.xml (1 hunks)
  • example/src/main/res/values/themes.xml (1 hunks)
  • gradle.properties (1 hunks)
  • gradle/libs.versions.toml (4 hunks)
  • settings.gradle.kts (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-06-05T10:27:53.946Z
Learnt from: sacOO7
PR: ably/ably-java#1095
File: gradle/libs.versions.toml:24-26
Timestamp: 2025-06-05T10:27:53.946Z
Learning: The ably-java project prefers to use the latest available versions of testing dependencies (including pre-release versions) when they contain relevant bug fixes, rather than sticking strictly to stable releases.

Applied to files:

  • gradle.properties
📚 Learning: 2025-08-01T09:53:16.730Z
Learnt from: sacOO7
PR: ably/ably-java#1120
File: live-objects/src/test/kotlin/io/ably/lib/objects/integration/DefaultLiveObjectsTest.kt:0-0
Timestamp: 2025-08-01T09:53:16.730Z
Learning: In the ably-java LiveObjects test code, extension properties with capital letter names (like `State`, `ObjectId`) are defined in live-objects/src/test/kotlin/io/ably/lib/objects/integration/helpers/Utils.kt to provide access to internal fields of concrete implementations through their public interfaces. For example, `LiveObjects.State` casts to `DefaultLiveObjects` to access the internal `state` field for testing purposes.

Applied to files:

  • example/src/main/java/com/ably/example/Utils.kt
🧬 Code Graph Analysis (4)
example/src/main/java/com/ably/example/screen/TaskManagementScreen.kt (1)
example/src/main/java/com/ably/example/Utils.kt (3)
  • getRealtimeChannel (265-283)
  • observeRootObject (251-263)
  • observeMap (221-249)
example/src/main/java/com/ably/example/screen/ColorVotingScreen.kt (1)
example/src/main/java/com/ably/example/Utils.kt (3)
  • getRealtimeChannel (265-283)
  • observeRootObject (251-263)
  • observeCounter (149-198)
example/src/main/java/com/ably/example/MainActivity.kt (4)
lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java (1)
  • AblyRealtime (31-317)
lib/src/main/java/io/ably/lib/types/ClientOptions.java (1)
  • ClientOptions (19-392)
example/src/main/java/com/ably/example/ui/theme/Theme.kt (1)
  • AblyTheme (35-57)
example/src/main/java/com/ably/example/screen/MainScreen.kt (1)
  • MainScreen (13-53)
example/src/main/java/com/ably/example/Utils.kt (2)
live-objects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt (1)
  • decrementAsync (50-52)
lib/src/main/java/io/ably/lib/types/ChannelOptions.java (1)
  • ChannelOptions (13-131)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: check (29)
  • GitHub Check: check (21)
  • GitHub Check: check (24)
  • GitHub Check: check (19)
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-rest-okhttp
  • GitHub Check: build
  • GitHub Check: check-rest
  • GitHub Check: check
  • GitHub Check: check-liveobjects
  • GitHub Check: check-realtime
🔇 Additional comments (16)
gradle.properties (1)

2-2: Approve SNAPSHOT bump

The -SNAPSHOT suffix is correct—our only publishing workflow (.github/workflows/release.yaml) runs manually via workflow_dispatch on tagged releases and enforces a strict VERSION_NAME == TAG check before invoking ./gradlew publishAndReleaseToMavenCentral. Since SNAPSHOT will never match a Git tag, no artifacts will be published unintentionally. No other workflows publish on merges to main.

settings.gradle.kts (1)

19-19: Example module isolated from publishing tasks

  • settings.gradle.kts includes example as intended.
  • example/build.gradle.kts only applies Android and Kotlin/Compose plugins; no maven-publish, publishing, sonatype or signing references found.

All clear—no publishing plugins picked up.

example/src/main/res/values/themes.xml (1)

3-3: Theme.Material.Light.NoActionBar is safe
MainActivity extends ComponentActivity (see MainActivity.kt:13) and minSdk = 30 (>21), so using Theme.Material.Light.NoActionBar for a Compose-first app poses no runtime issues.

example/src/main/res/drawable/ic_launcher_foreground.xml (1)

1-14: Launcher foreground vector looks good

Dimensions and path data are standard for 108dp adaptive icons; colors align with brand.

example/proguard-rules.pro (1)

1-22: ProGuard/R8 config template looks fine for now

No active rules are added, which is appropriate for an example app unless you plan to enable minification. If/when you turn on minify for release, ensure the module’s build.gradle wires this file via proguardFiles and verify Ably SDK works without extra keep rules.

If you intend to minify the example’s release build, please confirm proguardFiles is configured in example/build.gradle.kts and run a release build to catch potential keep rule needs.

example/src/main/java/com/ably/example/ui/theme/Type.kt (1)

10-34: Typography setup aligns with Compose templates

This mirrors the standard Material3 starter. No functional concerns.

example/src/main/res/values/colors.xml (1)

1-10: Baseline color palette added correctly

Resource definitions are valid and consistent with common Compose templates.

example/src/main/java/com/ably/example/ui/theme/Color.kt (1)

5-11: LGTM — theme color tokens look good

The color constants follow the standard Compose template pattern and are ready to be consumed by your Theme.kt.

example/src/main/res/mipmap-anydpi/ic_launcher_round.xml (1)

2-6: LGTM — adaptive round icon is correctly defined

Background, foreground, and monochrome layers are wired as expected.

example/src/main/res/mipmap-anydpi/ic_launcher.xml (1)

2-6: LGTM — adaptive launcher icon is correctly defined

Matches the standard adaptive icon structure and aligns with the manifest declarations.

example/src/main/res/drawable/ic_launcher_background.xml (1)

2-74: LGTM: Vector launcher background looks correct and well-formed.

Static asset; paths, viewport, and strokes are valid. Pairs as expected with the foreground for adaptive icons.

build.gradle.kts (2)

11-13: LGTM: Exposing app/Kotlin/Compose plugins via aliases at the root is correct.

This enables the example module to opt-in without applying at the root. Good alignment with version catalogs.


11-13: Plugin aliases and versions validated

  • Found in gradle/libs.versions.toml:
    • android-application → id "com.android.application", version.ref "agp" (8.6.1)
    • kotlin-android → id "org.jetbrains.kotlin.android", version.ref "kotlin" (2.1.10)
    • kotlin-compose → id "org.jetbrains.kotlin.plugin.compose", version.ref "kotlin" (2.1.10)

All aliases used in build.gradle.kts match, and AGP (8.6.1) and Kotlin (2.1.10) versions are declared. No further changes required.

example/build.gradle.kts (1)

11-21: Toolchain compatibility confirmed

The following versions satisfy compileSdk/targetSdk 35 without changes:

  • AGP: 8.6.1 supports API 35
  • Kotlin (and the org.jetbrains.kotlin.plugin.compose alias): 2.1.10
  • Compose BOM: 2024.09.00 aligns with the Kotlin Compose compiler

No further updates required.

example/src/main/java/com/ably/example/ui/theme/Theme.kt (1)

35-57: LGTM: Theme setup with dynamic color fallback is clean.

Good handling of dynamic color on S+ and sane defaults for pre-S devices. No issues spotted.

example/src/main/java/com/ably/example/MainActivity.kt (1)

17-18: ABLY_KEY is properly configured and local.properties is gitignored

  • example/build.gradle.kts (line 24) defines
    buildConfigField("String", "ABLY_KEY", "\"${getLocalProperty("ABLY_KEY") ?: ""}\"")
    so BuildConfig.ABLY_KEY is populated from local.properties at build time.
  • .gitignore (line 10) includes local.properties, ensuring credentials aren’t checked in.

No further action needed.

Copy link
Collaborator

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ttypic ttypic force-pushed the feat/lo-example-app branch from 81f6a5a to 62c4c84 Compare August 19, 2025 10:03
@github-actions github-actions bot temporarily deployed to staging/pull/1150/features August 19, 2025 10:03 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1150/javadoc August 19, 2025 10:05 Inactive
@ttypic ttypic force-pushed the feat/lo-example-app branch from 62c4c84 to 74ada11 Compare August 19, 2025 10:11
@github-actions github-actions bot temporarily deployed to staging/pull/1150/features August 19, 2025 10:12 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1150/javadoc August 19, 2025 10:13 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
gradle/libs.versions.toml (2)

2-2: AGP 8.6.1 requires Gradle 8.10+ — ensure wrapper is updated

This was already flagged earlier; reiterating to avoid CI failures.

If not already done, update gradle/wrapper/gradle-wrapper.properties to Gradle 8.10+ and regenerate the wrapper JAR:

#!/bin/bash
set -euo pipefail
echo "Current wrapper:"
fd -a gradle-wrapper.properties | xargs -I{} sh -c 'echo "--- {} ---"; cat "{}"'
echo
echo "AGP in catalog:"
rg -n '^\s*agp\s*=' gradle/libs.versions.toml
echo
echo "Tip to upgrade wrapper:"
echo "./gradlew wrapper --gradle-version 8.10.2 --distribution-type all"

65-77: Compose libraries have no explicit versions — ensure the BOM is applied in modules

Declaring Compose artifacts without versions is correct only if modules import the BOM. A previous review noted the BOM wasn’t applied in module build scripts.

Add in each Compose-using module:

  • dependencies: implementation(platform(libs.compose.bom))
  • then reference libs like libs.ui, libs.material3, etc., without versions.

Optional convenience: define a bundle to reduce repetition when pulling UI libs:

[bundles]
# add alongside existing bundles
compose-ui = ["ui", "ui-graphics", "material3"]
🧹 Nitpick comments (3)
example/src/main/java/com/ably/example/ui/theme/Theme.kt (2)

3-12: Nit: remember the computed colorScheme to avoid recomputation on trivial recompositions

This is a micro-optimization. Memoizing the computed scheme reduces redundant work during recompositions while keeping the same behavior. Include context as a key so changes re-evaluate correctly.

Apply this diff:

-import androidx.compose.runtime.Composable
-import androidx.compose.ui.platform.LocalContext
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
@@
-) {
-    val colorScheme = when {
-        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
-            val context = LocalContext.current
-            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
-        }
-
-        darkTheme -> DarkColorScheme
-        else -> LightColorScheme
-    }
+) {
+    val context = LocalContext.current
+    val colorScheme = remember(darkTheme, dynamicColor, context) {
+        when {
+            dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+                if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+            }
+            darkTheme -> DarkColorScheme
+            else -> LightColorScheme
+        }
+    }

Also applies to: 31-39


41-45: Nit: Prefer trailing lambda for MaterialTheme content for readability

This is purely stylistic; both are equivalent. The trailing lambda style is commonly used in Compose samples.

-    MaterialTheme(
-        colorScheme = colorScheme,
-        typography = Typography,
-        content = content
-    )
+    MaterialTheme(
+        colorScheme = colorScheme,
+        typography = Typography
+    ) {
+        content()
+    }
gradle/libs.versions.toml (1)

28-28: Typo: “jetbrains-annoations” -> “jetbrains-annotations”; also rename the library alias

Minor but confusing; fix the spelling in [versions] and align the library alias for clarity.

Apply this diff:

--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@
-jetbrains-annoations = "26.0.2"
+jetbrains-annotations = "26.0.2"
@@
-jetbrains = { group = "org.jetbrains", name = "annotations", version.ref = "jetbrains-annoations" }
+jetbrains-annotations = { group = "org.jetbrains", name = "annotations", version.ref = "jetbrains-annotations" }

Note: Update any usages of libs.jetbrains to libs.jetbrains.annotations (or libs.jetbrains-annotations) in build scripts accordingly.

Also applies to: 64-66

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 62c4c84 and 74ada11.

⛔ Files ignored due to path filters (3)
  • example/src/main/res/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png
  • example/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png is excluded by !**/*.png
📒 Files selected for processing (22)
  • build.gradle.kts (1 hunks)
  • example/.gitignore (1 hunks)
  • example/build.gradle.kts (1 hunks)
  • example/proguard-rules.pro (1 hunks)
  • example/src/main/AndroidManifest.xml (1 hunks)
  • example/src/main/java/com/ably/example/MainActivity.kt (1 hunks)
  • example/src/main/java/com/ably/example/Utils.kt (1 hunks)
  • example/src/main/java/com/ably/example/screen/ColorVotingScreen.kt (1 hunks)
  • example/src/main/java/com/ably/example/screen/MainScreen.kt (1 hunks)
  • example/src/main/java/com/ably/example/screen/TaskManagementScreen.kt (1 hunks)
  • example/src/main/java/com/ably/example/ui/theme/Color.kt (1 hunks)
  • example/src/main/java/com/ably/example/ui/theme/Theme.kt (1 hunks)
  • example/src/main/java/com/ably/example/ui/theme/Type.kt (1 hunks)
  • example/src/main/res/drawable/launch_background.xml (1 hunks)
  • example/src/main/res/mipmap-anydpi/ic_launcher.xml (1 hunks)
  • example/src/main/res/mipmap-anydpi/ic_launcher_round.xml (1 hunks)
  • example/src/main/res/values/colors.xml (1 hunks)
  • example/src/main/res/values/ic_launcher_background.xml (1 hunks)
  • example/src/main/res/values/strings.xml (1 hunks)
  • example/src/main/res/values/themes.xml (1 hunks)
  • gradle/libs.versions.toml (4 hunks)
  • settings.gradle.kts (1 hunks)
✅ Files skipped from review due to trivial changes (3)
  • example/src/main/res/values/ic_launcher_background.xml
  • example/src/main/res/drawable/launch_background.xml
  • example/proguard-rules.pro
🚧 Files skipped from review as they are similar to previous changes (17)
  • example/src/main/res/values/strings.xml
  • example/.gitignore
  • settings.gradle.kts
  • build.gradle.kts
  • example/src/main/java/com/ably/example/screen/MainScreen.kt
  • example/build.gradle.kts
  • example/src/main/java/com/ably/example/Utils.kt
  • example/src/main/java/com/ably/example/ui/theme/Color.kt
  • example/src/main/java/com/ably/example/screen/ColorVotingScreen.kt
  • example/src/main/res/mipmap-anydpi/ic_launcher.xml
  • example/src/main/java/com/ably/example/MainActivity.kt
  • example/src/main/res/mipmap-anydpi/ic_launcher_round.xml
  • example/src/main/AndroidManifest.xml
  • example/src/main/res/values/colors.xml
  • example/src/main/java/com/ably/example/screen/TaskManagementScreen.kt
  • example/src/main/res/values/themes.xml
  • example/src/main/java/com/ably/example/ui/theme/Type.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: check-liveobjects
  • GitHub Check: check-realtime
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-rest
  • GitHub Check: check-rest-okhttp
  • GitHub Check: check
  • GitHub Check: build
  • GitHub Check: check (21)
  • GitHub Check: check (19)
  • GitHub Check: check (24)
  • GitHub Check: check (29)
🔇 Additional comments (5)
example/src/main/java/com/ably/example/ui/theme/Theme.kt (2)

25-46: Solid Material3 theme setup with proper dynamic color gating

The dynamic color handling (Android 12+), dark theme fallback, and MaterialTheme wiring look correct and idiomatic. This should integrate cleanly with the rest of the Compose UI.


13-23: All referenced color constants and Typography exist
The color values (Purple80, PurpleGrey80, Pink80, Purple40, PurpleGrey40, Pink40) are defined in
example/src/main/java/com/ably/example/ui/theme/Color.kt, and val Typography is declared in
example/src/main/java/com/ably/example/ui/theme/Type.kt. No further changes needed.

gradle/libs.versions.toml (3)

7-7: Navigation-Compose pinned explicitly (good; not covered by Compose BOM)

androidx.navigation is not part of the Compose BOM, so keeping an explicit version for navigation-compose is correct.

Also applies to: 40-40


92-94: Compose compiler plugin alias is exposed and no Compose modules detected

  • The kotlin-compose entry is correctly mapped in gradle/libs.versions.toml.
  • No Gradle scripts reference alias(libs.plugins.kotlin.compose) or id("org.jetbrains.kotlin.plugin.compose").
  • No modules declare Compose UI dependencies (androidx.compose.*) or composeOptions blocks.

Since there are currently no Compose–based modules, no further changes are needed.


29-34: Confirmed AndroidX versions and AGP/Kotlin compatibility
All specified artifacts in gradle/libs.versions.toml (lines 29–34) are published in Google’s Maven repository, and Android Gradle Plugin 8.6.1 is compatible with Kotlin 2.1.10 and the matching Compose compiler plugin 2.1.10. No changes required.

@ttypic ttypic force-pushed the feat/lo-example-app branch from 74ada11 to 9b7cbb1 Compare August 19, 2025 10:20
@github-actions github-actions bot temporarily deployed to staging/pull/1150/features August 19, 2025 10:21 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1150/javadoc August 19, 2025 10:23 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
settings.gradle.kts (1)

19-19: Module naming: consider examples or a more specific module name

If this is the first of potentially multiple samples (Android, Java, etc.), consider examples (aggregator) or a specific name like example-android-live-objects for clarity and future scalability.

gradle/libs.versions.toml (2)

2-2: AGP 8.6.1 requires Gradle 8.10+—verify wrapper

Ensure gradle/wrapper/gradle-wrapper.properties points to Gradle 8.10+; otherwise the build will fail.

Run:

#!/bin/bash
set -euo pipefail
echo "AGP in catalog:"
rg -nP '^\s*agp\s*=\s*".*"' gradle/libs.versions.toml || true
echo
echo "Gradle wrapper properties:"
fd -a gradle-wrapper.properties | xargs -I{} sh -c 'echo "--- {} ---"; sed -n "1,200p" "{}"'
echo
echo "Distribution URL(s) (expect 8.10+ for AGP 8.6.x):"
rg -n 'distributionUrl=' -g '**/gradle-wrapper.properties' || true

70-77: Compose BOM is declared—ensure it’s actually imported in module build scripts

Without importing the BOM via platform(libs.compose.bom), these unversioned Compose libs won’t be pinned.

In example/build.gradle.kts, ensure:

plugins {
  alias(libs.plugins.android.application)
  alias(libs.plugins.kotlin.android)
  alias(libs.plugins.kotlin.compose)
}

dependencies {
  implementation(platform(libs.compose.bom))
  androidTestImplementation(platform(libs.compose.bom))

  implementation(libs.ui)
  implementation(libs.ui.graphics)
  implementation(libs.material3)
  // ... other Compose libs without versions
  debugImplementation(libs.ui.tooling)
  debugImplementation(libs.ui.test.manifest)
  androidTestImplementation(libs.ui.test.junit4)
}

You can verify BOM usage with:

#!/bin/bash
rg -nP 'platform\\(libs\\.compose\\.bom\\)|org\\.jetbrains\\.kotlin\\.plugin\\.compose' -g '!**/build/**'
🧹 Nitpick comments (3)
example/README.md (2)

12-18: Add a security note about Ably keys in client apps

Good for a demo, but embedding an API key in a client app is unsafe for production. Recommend documenting Token Auth and reminding not to commit local.properties.

Apply this diff right after the properties block:

 ABLY_KEY=xxxx:yyyyyy

+> Security note
+> - Never commit local.properties (it’s gitignored by default).
+> - For production, do not embed an Ably API key in client apps. Use Token Auth (ephemeral tokens issued by your backend) instead.


---

`24-26`: **Fix list indentation (Markdownlint MD007) and minor style**

Unindent list items so they render consistently and pass MD007.

Apply this diff:

```diff
-  - Open Android Studio.
-  - Select File > Open and navigate to the cloned repository.
-  - Open the project.
+- Open Android Studio.
+- Select File > Open and navigate to the cloned repository.
+- Open the project.

-  - Wait for Gradle to sync automatically.
-  - If it doesn’t, click on Sync Project with Gradle Files in the toolbar.
+- Wait for Gradle to sync automatically.
+- If it doesn’t, click on Sync Project with Gradle Files in the toolbar.

-  - Set up an emulator or connect a physical Android device.
-  - Ensure the device is configured with at least Android 5.0 (API 21).
+- Set up an emulator or connect a physical Android device.
+- Ensure the device is configured with at least Android 5.0 (API 21).

-  - Select your emulator or connected device in the device selector dropdown.
-  - Click on the Run button ▶️ in the toolbar or press Shift + F10.
+- Select your emulator or connected device in the device selector dropdown.
+- Click on the Run button ▶️ in the toolbar or press Shift + F10.

Also applies to: 30-31, 35-36, 40-41

gradle/libs.versions.toml (1)

28-28: Typo in version alias: jetbrains-annoations

Minor nit: fix the alias spelling to jetbrains-annotations for consistency/readability.

Apply this diff:

-jetbrains-annoations = "26.0.2"
+jetbrains-annotations = "26.0.2"

And update the library to reference the corrected alias:

-jetbrains = { group = "org.jetbrains", name = "annotations", version.ref = "jetbrains-annoations" }
+jetbrains = { group = "org.jetbrains", name = "annotations", version.ref = "jetbrains-annotations" }

Also applies to: 64-64

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 74ada11 and 9b7cbb1.

⛔ Files ignored due to path filters (3)
  • example/src/main/res/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png
  • example/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png is excluded by !**/*.png
📒 Files selected for processing (23)
  • build.gradle.kts (1 hunks)
  • example/.gitignore (1 hunks)
  • example/README.md (1 hunks)
  • example/build.gradle.kts (1 hunks)
  • example/proguard-rules.pro (1 hunks)
  • example/src/main/AndroidManifest.xml (1 hunks)
  • example/src/main/java/com/ably/example/MainActivity.kt (1 hunks)
  • example/src/main/java/com/ably/example/Utils.kt (1 hunks)
  • example/src/main/java/com/ably/example/screen/ColorVotingScreen.kt (1 hunks)
  • example/src/main/java/com/ably/example/screen/MainScreen.kt (1 hunks)
  • example/src/main/java/com/ably/example/screen/TaskManagementScreen.kt (1 hunks)
  • example/src/main/java/com/ably/example/ui/theme/Color.kt (1 hunks)
  • example/src/main/java/com/ably/example/ui/theme/Theme.kt (1 hunks)
  • example/src/main/java/com/ably/example/ui/theme/Type.kt (1 hunks)
  • example/src/main/res/drawable/launch_background.xml (1 hunks)
  • example/src/main/res/mipmap-anydpi/ic_launcher.xml (1 hunks)
  • example/src/main/res/mipmap-anydpi/ic_launcher_round.xml (1 hunks)
  • example/src/main/res/values/colors.xml (1 hunks)
  • example/src/main/res/values/ic_launcher_background.xml (1 hunks)
  • example/src/main/res/values/strings.xml (1 hunks)
  • example/src/main/res/values/themes.xml (1 hunks)
  • gradle/libs.versions.toml (4 hunks)
  • settings.gradle.kts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • example/.gitignore
🚧 Files skipped from review as they are similar to previous changes (18)
  • example/src/main/res/mipmap-anydpi/ic_launcher_round.xml
  • example/src/main/res/values/ic_launcher_background.xml
  • example/proguard-rules.pro
  • example/src/main/res/values/colors.xml
  • build.gradle.kts
  • example/src/main/java/com/ably/example/MainActivity.kt
  • example/src/main/res/values/themes.xml
  • example/src/main/res/drawable/launch_background.xml
  • example/src/main/java/com/ably/example/ui/theme/Color.kt
  • example/src/main/java/com/ably/example/screen/TaskManagementScreen.kt
  • example/src/main/java/com/ably/example/screen/MainScreen.kt
  • example/build.gradle.kts
  • example/src/main/AndroidManifest.xml
  • example/src/main/java/com/ably/example/Utils.kt
  • example/src/main/java/com/ably/example/ui/theme/Type.kt
  • example/src/main/java/com/ably/example/screen/ColorVotingScreen.kt
  • example/src/main/res/values/strings.xml
  • example/src/main/res/mipmap-anydpi/ic_launcher.xml
🧰 Additional context used
🪛 LanguageTool
example/README.md

[grammar] ~3-~3: There might be a mistake here.
Context: ... Follow the steps below to get started with chat example app ## Prerequisites Ens...

(QB_NEW_EN)


[grammar] ~7-~7: There might be a mistake here.
Context: ...Ensure you have the following installed: - [Android Studio](https://developer.androi...

(QB_NEW_EN)


[grammar] ~8-~8: There might be a mistake here.
Context: ...e following installed: - Android Studio (latest stable version) - Java 17 or hi...

(QB_NEW_EN)


[grammar] ~8-~8: There might be a mistake here.
Context: ...roid.com/studio) (latest stable version) - Java 17 or higher - Android SDK with API...

(QB_NEW_EN)


[grammar] ~9-~9: There might be a mistake here.
Context: ...test stable version) - Java 17 or higher - Android SDK with API Level 34 or higher ...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
example/README.md

24-24: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


25-25: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


26-26: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


30-30: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


31-31: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


35-35: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


36-36: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


40-40: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


41-41: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: check-liveobjects
  • GitHub Check: check-rest-okhttp
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-rest
  • GitHub Check: check-realtime
  • GitHub Check: check
  • GitHub Check: check (19)
  • GitHub Check: build
  • GitHub Check: check (29)
  • GitHub Check: check (24)
  • GitHub Check: check (21)
🔇 Additional comments (3)
example/README.md (1)

33-37: Clarify min/target SDK expectations

Prerequisites mention installing API Level 34+, but the device requirement says API 21+. Please confirm the module’s minSdk/targetSdk and reflect both clearly to avoid confusion.

Would you like me to patch the README once you confirm the actual minSdk/targetSdk?

example/src/main/java/com/ably/example/ui/theme/Theme.kt (1)

25-46: LGTM: correct dynamic color gating with sensible fallbacks

Theme selection correctly gates on API 31+ for dynamic colors and falls back to static schemes; Material3 is applied with typography. Looks good.

gradle/libs.versions.toml (1)

92-94: LGTM: plugin aliases align with Kotlin 2.1.10 and Compose plugin

Aliases for android application, kotlin android, and kotlin compose are correctly cataloged and version-aligned.

@ttypic ttypic force-pushed the feat/lo-example-app branch from 9b7cbb1 to e421e7e Compare August 19, 2025 12:52
@github-actions github-actions bot temporarily deployed to staging/pull/1150/features August 19, 2025 12:52 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1150/javadoc August 19, 2025 12:54 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1150/features August 19, 2025 12:56 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1150/javadoc August 19, 2025 12:59 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (2)
gradle/libs.versions.toml (2)

2-2: AGP 8.6.1 → ensure Gradle wrapper is 8.10+

Android Gradle Plugin 8.6.1 requires Gradle 8.10 or newer. Please verify and bump the wrapper if needed.

Run:

#!/bin/bash
set -euo pipefail
echo "Gradle wrapper(s) found:"
fd -a gradle-wrapper.properties | while read -r f; do
  echo "--- $f ---"
  rg -n 'distributionUrl' "$f"
done
echo
echo "Expected: gradle-8.10 (or newer) in distributionUrl for AGP 8.6.1."

34-34: Compose BOM declared—ensure it’s actually imported and compose plugin aligns with Kotlin 2.1.10

  • Import the BOM in each Compose-using module: implementation(platform(libs.compose.bom)).
  • Keep the Compose compiler plugin version equal to Kotlin (you already expose it via libs.plugins.kotlin.compose at 2.1.10).

To confirm:

#!/bin/bash
set -euo pipefail
echo "Checking Example module for BOM import and Compose compiler plugin..."
fd -a --hidden --follow --exclude '**/build/**' 'build.gradle.kts' | rg -n '^examples/.*build\.gradle\.kts$' -N || true
echo
rg -nP -C2 'plugins\s*\{[^}]*\b(alias\(libs\.plugins\.kotlin\.compose\)|id\("org\.jetbrains\.kotlin\.plugin\.compose"\))' examples/ -S || true
echo
rg -nP -C2 'implementation\s*\(\s*platform\s*\(\s*libs\.compose\.bom\s*\)\s*\)' examples/ -S || true

Also applies to: 70-77

🧹 Nitpick comments (16)
examples/src/main/res/values/themes.xml (1)

3-3: Prefer a Material3/AppCompat-based parent theme for Compose interoperability

Using the framework theme parent android:Theme.Material.Light.NoActionBar can cause inconsistencies with Material Components and Compose Material3. Consider basing the app theme on Material Components (or Material3) for better compatibility.

Apply this minimal change:

-    <style name="Theme.Ably" parent="android:Theme.Material.Light.NoActionBar"/>
+    <style name="Theme.Ably" parent="Theme.Material3.DayNight.NoActionBar"/>

Optionally, if you want a transparent status bar with Compose, expand the style and add:

<style name="Theme.Ably" parent="Theme.Material3.DayNight.NoActionBar">
    <item name="android:statusBarColor">@android:color/transparent</item>
</style>
examples/README.md (3)

8-8: Punctuation: end the sentence with a period

Minor readability tweak.

-Follow the steps below to get started with the Live Objects demo app
+Follow the steps below to get started with the Live Objects demo app.

29-31: Fix markdown list indentation to satisfy MD007 (ul-indent) linter

The unordered list items are indented by two spaces; the linter expects 0. Flatten these bullets.

-  - Open Android Studio.
-  - Select File > Open and navigate to the cloned repository.
-  - Open the project.
+ - Open Android Studio.
+ - Select File > Open and navigate to the cloned repository.
+ - Open the project.

-  - Wait for Gradle to sync automatically.
-  - If it doesn’t, click on Sync Project with Gradle Files in the toolbar.
+ - Wait for Gradle to sync automatically.
+ - If it doesn’t, click on Sync Project with Gradle Files in the toolbar.

-  - Set up an emulator or connect a physical Android device.
-  - Ensure the device is configured with at least Android 5.0 (API 21).
+ - Set up an emulator or connect a physical Android device.
+ - Ensure the device is configured with at least Android 5.0 (API 21).

-  - Select your emulator or connected device in the device selector dropdown.
-  - Click on the Run button ▶️ in the toolbar or press Shift + F10.
+ - Select your emulator or connected device in the device selector dropdown.
+ - Click on the Run button ▶️ in the toolbar or press Shift + F10.

Also applies to: 35-36, 40-41, 45-46


17-23: Global .gitignore already covers local.properties—no gitignore updates needed

The root .gitignore (line 10) already ignores local.properties in every module, so you don’t need to add entries under android/ or examples/. To prevent any accidental commits of your Ably key, though, add a short note in the examples README:

• File: examples/README.md (after line 23)

> **Note:** `local.properties` (and your `ABLY_KEY`) is already listed in `.gitignore` and must not be committed.
examples/src/main/java/com/ably/example/Utils.kt (3)

190-198: Simplify reset implementation (avoid redundant coroutineScope/launch)

The reset lambda is already suspend. The extra coroutineScope + launch adds indirection without benefit.

Apply this diff:

   return CounterState(counterValue, counter) {
-    coroutineScope {
-      launch {
-        counter = channel.objects.createCounterCoroutine().also {
-          root?.setCoroutine(path, LiveMapValue.of(it))
-        }
-      }
-    }
+    counter = channel.objects.createCounterCoroutine().also {
+      root?.setCoroutine(path, LiveMapValue.of(it))
+    }
   }

270-275: Prefer suspending updateOptions with error propagation, keep attach/detach in DisposableEffect

You already defined Channel.updateOptions; use it in a LaunchedEffect so failures surface via coroutine cancellation, and keep DisposableEffect solely for attach/detach lifecycle.

Apply this diff:

-  DisposableEffect(channel) {
-    channel.setOptions(ChannelOptions().apply {
-      attachOnSubscribe = false
-      modes = arrayOf(ChannelMode.object_publish, ChannelMode.object_subscribe)
-    })
-
-    channel.attach()
+  LaunchedEffect(channel) {
+    channel.updateOptions(ChannelOptions().apply {
+      attachOnSubscribe = false
+      modes = arrayOf(ChannelMode.object_publish, ChannelMode.object_subscribe)
+    })
+  }
+  DisposableEffect(channel) {
+    channel.attach()

31-41: Avoid !! on callbacks; cancel continuation when result is null

While Ably should return non-null results on success, defensive handling avoids potential NPEs and keeps the examples resilient.

Apply this diff:

   getRootAsync(object : ObjectsCallback<LiveMap> {
     override fun onSuccess(result: LiveMap?) {
-      continuation.resume(result!!)
+      if (result != null) {
+        continuation.resume(result)
+      } else {
+        continuation.cancel(IllegalStateException("getRootAsync returned null"))
+      }
     }
   createCounterAsync(object : ObjectsCallback<LiveCounter> {
     override fun onSuccess(result: LiveCounter?) {
-      continuation.resume(result!!)
+      if (result != null) {
+        continuation.resume(result)
+      } else {
+        continuation.cancel(IllegalStateException("createCounterAsync returned null"))
+      }
     }
   createMapAsync(object : ObjectsCallback<LiveMap> {
     override fun onSuccess(result: LiveMap?) {
-      continuation.resume(result!!)
+      if (result != null) {
+        continuation.resume(result)
+      } else {
+        continuation.cancel(IllegalStateException("createMapAsync returned null"))
+      }
     }

Also applies to: 43-53, 55-65

examples/src/main/java/com/ably/example/screen/ColorVotingScreen.kt (1)

82-96: Run resets concurrently to improve UX

Resets run sequentially inside a single coroutine. Launch them in parallel to reduce perceived latency.

Apply this diff:

-    Button(
-      onClick = {
-        scope.launch {
-          resetRed()
-          resetBlue()
-          resetGreen()
-        }
-      },
+    Button(
+      onClick = {
+        // Fire-and-forget in parallel; each reset is a suspend function
+        scope.launch { resetRed() }
+        scope.launch { resetGreen() }
+        scope.launch { resetBlue() }
+      },
     ) {
       Text(
         text = "Reset all",
-        color = Color.White,
         fontWeight = FontWeight.Medium
       )
     }

Optional: omit the explicit text color and rely on Button’s contentColor (Material 3) for better theming across light/dark.

examples/src/main/java/com/ably/example/screen/TaskManagementScreen.kt (1)

139-171: Use keyed items() with a stable list instead of indexing into entries

Indexing via entries.elementAt(index) is O(n) per access and lacks stable keys, which hurts performance and state retention on list updates. Materialize the entries once and use items(items, key).

Apply this diff:

-        } else {
-          LazyColumn(
-            verticalArrangement = Arrangement.spacedBy(8.dp)
-          ) {
-            items(taskIdToTask.entries.size) { index ->
-              val task = taskIdToTask.entries.elementAt(index)
-              TaskItemCard(
-                task = task,
-                isEditing = editingTaskId == task.key,
-                editingText = editingText,
-                onEditingTextChange = { editingText = it },
-                onEdit = {
-                  editingTaskId = task.key
-                  editingText = task.value
-                },
-                onSave = {
-                  scope.launch {
-                    liveTasks?.setCoroutine(task.key, LiveMapValue.of(editingText.trim()))
-                    editingTaskId = null
-                    editingText = ""
-                  }
-                },
-                onCancel = {
-                  editingTaskId = null
-                  editingText = ""
-                },
-                onDelete = {
-                  scope.launch {
-                    liveTasks?.removeCoroutine(task.key)
-                  }
-                }
-              )
-            }
-          }
-        }
+        } else {
+          val entries = taskIdToTask.entries.toList()
+          LazyColumn(
+            verticalArrangement = Arrangement.spacedBy(8.dp)
+          ) {
+            items(items = entries, key = { it.key }) { task ->
+              TaskItemCard(
+                task = task,
+                isEditing = editingTaskId == task.key,
+                editingText = editingText,
+                onEditingTextChange = { editingText = it },
+                onEdit = {
+                  editingTaskId = task.key
+                  editingText = task.value
+                },
+                onSave = {
+                  scope.launch {
+                    liveTasks?.setCoroutine(task.key, LiveMapValue.of(editingText.trim()))
+                    editingTaskId = null
+                    editingText = ""
+                  }
+                },
+                onCancel = {
+                  editingTaskId = null
+                  editingText = ""
+                },
+                onDelete = {
+                  scope.launch {
+                    liveTasks?.removeCoroutine(task.key)
+                  }
+                }
+              )
+            }
+          }
+        }
gradle/libs.versions.toml (1)

28-28: Typo in version key: “jetbrains-annoations” → “jetbrains-annotations”

Minor, but can cause confusion when scanning the catalog.

Apply:

-[versions]
-jetbrains-annoations = "26.0.2"
+[versions]
+jetbrains-annotations = "26.0.2"
-jetbrains = { group = "org.jetbrains", name = "annotations", version.ref = "jetbrains-annoations" }
+jetbrains = { group = "org.jetbrains", name = "annotations", version.ref = "jetbrains-annotations" }

Also applies to: 64-64

examples/src/main/java/com/ably/example/screen/MainScreen.kt (1)

16-16: Persist tab selection across config changes and avoid re-allocating tabs list

Use rememberSaveable for selectedTab and remember the static tabs list.

Apply:

-    var selectedTab by remember { mutableIntStateOf(0) }
+    var selectedTab by rememberSaveable { mutableIntStateOf(0) }
-    val tabs = listOf(
-        TabItem("Color Voting", Icons.Default.Favorite),
-        TabItem("Task Management", Icons.AutoMirrored.Filled.List),
-    )
+    val tabs = remember {
+        listOf(
+            TabItem("Color Voting", Icons.Default.Favorite),
+            TabItem("Task Management", Icons.AutoMirrored.Filled.List),
+        )
+    }

And add the import:

import androidx.compose.runtime.saveable.rememberSaveable

Also applies to: 18-21

examples/src/main/java/com/ably/example/MainActivity.kt (2)

14-22: Validate ABLY_KEY early and construct ClientOptions(key) to surface misconfiguration clearly

Constructing the client with an empty or malformed key will throw at runtime (AblyRealtime constructor can throw AblyException). Guard against this and use the ClientOptions(key) ctor which validates the key format. This gives a clear failure message for contributors running the sample without local.properties.

   private val realtimeClient: AblyRealtime by lazy {
-    AblyRealtime(
-      ClientOptions().apply {
-        key = BuildConfig.ABLY_KEY
-        logLevel = Log.VERBOSE
-        autoConnect = false
-      }
-    )
+    val key = BuildConfig.ABLY_KEY
+    require(key.isNotBlank()) { "ABLY_KEY is missing. Add ABLY_KEY=your_api_key to local.properties (not committed)." }
+    AblyRealtime(
+      ClientOptions(key).apply {
+        logLevel = Log.VERBOSE
+        autoConnect = false
+      }
+    )
   }

15-21: Avoid embedding API keys in client apps; prefer Token Auth

Even for an examples app, shipping a build with a real API key in BuildConfig makes it trivially extractable from the APK. Prefer token auth (authUrl or authCallback) for client-side usage. If you keep the local.properties flow for dev-only builds, ensure release builds do not embed the key.

For example (MainActivity) using a token endpoint:

val opts = ClientOptions().apply {
  authUrl = "https://your.example/token" // simple demo endpoint
  autoConnect = false
  logLevel = Log.VERBOSE
}
// AblyRealtime(opts)

And in Gradle, only set ABLY_KEY for debug builds (see separate comment in build.gradle.kts).

examples/build.gradle.kts (3)

24-25: Safely inject ABLY_KEY into BuildConfig (escape quotes and backslashes)

If the key contains characters like quotes or backslashes, the generated BuildConfig line can be malformed. Escape them before injection.

-        buildConfigField("String", "ABLY_KEY", "\"${getLocalProperty("ABLY_KEY") ?: ""}\"")
+        val ablyKey = getLocalProperty("ABLY_KEY") ?: ""
+        buildConfigField("String", "ABLY_KEY", "\"${ablyKey.replace("\\", "\\\\").replace("\"", "\\\"")}\"")

27-32: Do not embed secrets in release builds; restrict ABLY_KEY to debug or require Token Auth

Avoid shipping a release variant with a real ABLY key in BuildConfig. For a safer example setup, only define ABLY_KEY for debug builds, and set an empty (or placeholder) value for release. Alternatively, remove this entirely and switch to Token Auth.

Example:

buildTypes {
    debug {
        isMinifyEnabled = false
        proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        val ablyKey = getLocalProperty("ABLY_KEY") ?: ""
        buildConfigField("String", "ABLY_KEY", "\"${ablyKey.replace("\\", "\\\\").replace("\"", "\\\"")}\"")
    }
    release {
        isMinifyEnabled = false
        proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        // Do not embed secrets in release builds
        buildConfigField("String", "ABLY_KEY", "\"\"")
    }
}

15-21: Optional: lower minSdk for broader device coverage

minSdk 30 limits the sample to Android 11+. Compose supports much lower API levels; consider 24 or even 21 if aligned with the rest of the project, to make the examples runnable on more devices.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9b7cbb1 and 153d444.

⛔ Files ignored due to path filters (3)
  • examples/src/main/res/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png
  • examples/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • examples/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png is excluded by !**/*.png
📒 Files selected for processing (23)
  • build.gradle.kts (1 hunks)
  • examples/.gitignore (1 hunks)
  • examples/README.md (1 hunks)
  • examples/build.gradle.kts (1 hunks)
  • examples/proguard-rules.pro (1 hunks)
  • examples/src/main/AndroidManifest.xml (1 hunks)
  • examples/src/main/java/com/ably/example/MainActivity.kt (1 hunks)
  • examples/src/main/java/com/ably/example/Utils.kt (1 hunks)
  • examples/src/main/java/com/ably/example/screen/ColorVotingScreen.kt (1 hunks)
  • examples/src/main/java/com/ably/example/screen/MainScreen.kt (1 hunks)
  • examples/src/main/java/com/ably/example/screen/TaskManagementScreen.kt (1 hunks)
  • examples/src/main/java/com/ably/example/ui/theme/Color.kt (1 hunks)
  • examples/src/main/java/com/ably/example/ui/theme/Theme.kt (1 hunks)
  • examples/src/main/java/com/ably/example/ui/theme/Type.kt (1 hunks)
  • examples/src/main/res/drawable/launch_background.xml (1 hunks)
  • examples/src/main/res/mipmap-anydpi/ic_launcher.xml (1 hunks)
  • examples/src/main/res/mipmap-anydpi/ic_launcher_round.xml (1 hunks)
  • examples/src/main/res/values/colors.xml (1 hunks)
  • examples/src/main/res/values/ic_launcher_background.xml (1 hunks)
  • examples/src/main/res/values/strings.xml (1 hunks)
  • examples/src/main/res/values/themes.xml (1 hunks)
  • gradle/libs.versions.toml (4 hunks)
  • settings.gradle.kts (1 hunks)
✅ Files skipped from review due to trivial changes (5)
  • examples/src/main/res/values/strings.xml
  • examples/src/main/res/values/ic_launcher_background.xml
  • examples/src/main/res/mipmap-anydpi/ic_launcher_round.xml
  • examples/proguard-rules.pro
  • examples/.gitignore
🚧 Files skipped from review as they are similar to previous changes (1)
  • settings.gradle.kts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-01T09:53:16.730Z
Learnt from: sacOO7
PR: ably/ably-java#1120
File: live-objects/src/test/kotlin/io/ably/lib/objects/integration/DefaultLiveObjectsTest.kt:0-0
Timestamp: 2025-08-01T09:53:16.730Z
Learning: In the ably-java LiveObjects test code, extension properties with capital letter names (like `State`, `ObjectId`) are defined in live-objects/src/test/kotlin/io/ably/lib/objects/integration/helpers/Utils.kt to provide access to internal fields of concrete implementations through their public interfaces. For example, `LiveObjects.State` casts to `DefaultLiveObjects` to access the internal `state` field for testing purposes.

Applied to files:

  • examples/src/main/java/com/ably/example/Utils.kt
📚 Learning: 2025-08-07T07:17:33.340Z
Learnt from: sacOO7
PR: ably/ably-java#1137
File: live-objects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/LiveMapManagerTest.kt:6-6
Timestamp: 2025-08-07T07:17:33.340Z
Learning: In the ably-java LiveObjects test code, there are extension properties defined in TestHelpers.kt that provide access to private fields of classes for testing purposes. For example, `internal var DefaultLiveMap.LiveMapManager: LiveMapManager` allows tests to access the private `liveMapManager` field. These extension imports (like `import io.ably.lib.objects.unit.LiveMapManager`) should not be removed as they are necessary for test functionality and are not conflicting imports.

Applied to files:

  • examples/src/main/java/com/ably/example/Utils.kt
🧬 Code Graph Analysis (5)
examples/src/main/java/com/ably/example/MainActivity.kt (4)
lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java (1)
  • AblyRealtime (31-317)
lib/src/main/java/io/ably/lib/types/ClientOptions.java (1)
  • ClientOptions (19-392)
examples/src/main/java/com/ably/example/ui/theme/Theme.kt (1)
  • AblyTheme (25-46)
examples/src/main/java/com/ably/example/screen/MainScreen.kt (1)
  • MainScreen (13-53)
examples/src/main/java/com/ably/example/screen/MainScreen.kt (2)
examples/src/main/java/com/ably/example/screen/ColorVotingScreen.kt (1)
  • ColorVotingScreen (22-98)
examples/src/main/java/com/ably/example/screen/TaskManagementScreen.kt (1)
  • TaskManagementScreen (27-176)
examples/src/main/java/com/ably/example/screen/TaskManagementScreen.kt (1)
examples/src/main/java/com/ably/example/Utils.kt (3)
  • getRealtimeChannel (266-284)
  • observeRootObject (252-264)
  • observeMap (222-250)
examples/src/main/java/com/ably/example/screen/ColorVotingScreen.kt (1)
examples/src/main/java/com/ably/example/Utils.kt (3)
  • getRealtimeChannel (266-284)
  • observeRootObject (252-264)
  • observeCounter (149-199)
examples/src/main/java/com/ably/example/Utils.kt (2)
live-objects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt (1)
  • decrementAsync (50-52)
lib/src/main/java/io/ably/lib/types/ChannelOptions.java (1)
  • ChannelOptions (13-131)
🪛 LanguageTool
examples/README.md

[grammar] ~12-~12: There might be a mistake here.
Context: ...Ensure you have the following installed: - [Android Studio](https://developer.androi...

(QB_NEW_EN)


[grammar] ~13-~13: There might be a mistake here.
Context: ...e following installed: - Android Studio (latest stable version) - Java 17 or hi...

(QB_NEW_EN)


[grammar] ~13-~13: There might be a mistake here.
Context: ...roid.com/studio) (latest stable version) - Java 17 or higher - Android SDK with API...

(QB_NEW_EN)


[grammar] ~14-~14: There might be a mistake here.
Context: ...test stable version) - Java 17 or higher - Android SDK with API Level 34 or higher ...

(QB_NEW_EN)


[grammar] ~56-~56: There might be a mistake here.
Context: ...with two tabs: 1. Color Voting Tab: - Vote for Red, Green, or Blue colors -...

(QB_NEW_EN)


[grammar] ~61-~61: There might be a mistake here.
Context: ...all" button 2. Task Management Tab: - Add new tasks using the text input and "...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
examples/README.md

29-29: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


30-30: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


31-31: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


35-35: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


36-36: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


40-40: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


41-41: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


45-45: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


46-46: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: check
  • GitHub Check: check (29)
  • GitHub Check: check (24)
  • GitHub Check: check (21)
  • GitHub Check: check (19)
  • GitHub Check: check-rest
  • GitHub Check: build
  • GitHub Check: check-liveobjects
  • GitHub Check: check-rest-okhttp
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-realtime
🔇 Additional comments (10)
examples/src/main/res/values/themes.xml (1)

3-3: Wire up the splash background (if not already via a launch theme)

The project adds launch_background.xml, but I don’t see it referenced here. If you intend to use a custom launch screen pre-Android 12, add it as the window background. For Android 12+ use SplashScreen attributes.

You can update Theme.Ably like this (only if not already configured elsewhere):

<style name="Theme.Ably" parent="Theme.Material3.DayNight.NoActionBar">
    <!-- Pre-Android 12 -->
    <item name="android:windowBackground">@drawable/launch_background</item>

    <!-- Android 12+ SplashScreen API (optional, if you have a dedicated splash asset) -->
    <item name="android:windowSplashScreenBackground">@android:color/white</item>
    <!-- Prefer a monochrome/vector icon here -->
    <item name="android:windowSplashScreenAnimatedIcon">@mipmap/ic_launcher_foreground</item>
</style>
examples/src/main/res/drawable/launch_background.xml (1)

3-12: Confirm this drawable is actually used as the launch background

This looks good and matches the standard template. Please confirm it’s referenced either via a dedicated launch theme or Theme.Ably’s windowBackground (or via the Android 12+ SplashScreen attributes).

If it isn’t yet wired, see my comment in examples/src/main/res/values/themes.xml for how to attach it to Theme.Ably.

examples/src/main/java/com/ably/example/ui/theme/Color.kt (2)

5-11: LGTM — standard Compose template color palette

These constants are fine as-is.


5-11: No unused color definitions found
All six palette colors are referenced in Theme.kt (used as primary/secondary/tertiary in both dark and light schemes), so there’s nothing to remove or narrow in visibility here.

examples/src/main/res/values/colors.xml (1)

3-9: LGTM — standard Material palette added

These resources are fine for a starter theme. No issues spotted.

examples/src/main/java/com/ably/example/ui/theme/Type.kt (1)

10-34: LGTM — Typography scaffold matches Material 3 defaults

The override for bodyLarge looks correct and integrates cleanly with your Theme.kt.

gradle/libs.versions.toml (1)

29-34: All specified AndroidX artifacts are available on Google Maven

Verified that each artifact version in gradle/libs.versions.toml (lines 29–34) returns HTTP 200 from Google Maven. No further action is needed.

build.gradle.kts (1)

11-13: LGTM: plugin aliases exposed to subprojects

Providing android-application, kotlin-android, and kotlin-compose aliases at the root (apply false) is the right setup for the examples module to consume them.

examples/src/main/AndroidManifest.xml (1)

11-21: LGTM: correctly exported launcher Activity (API 31+ compliance)

android:exported="true" with a MAIN/LAUNCHER intent filter is correct. Manifest basics look good for the sample app.

examples/src/main/java/com/ably/example/ui/theme/Theme.kt (1)

25-46: LGTM: idiomatic dynamic color + light/dark fallback

Theme selection logic matches recommended patterns; guards dynamic colors on S+, and cleanly applies Typography.

@ttypic ttypic force-pushed the feat/lo-example-app branch from 153d444 to 420b3ef Compare August 19, 2025 17:02
@github-actions github-actions bot temporarily deployed to staging/pull/1150/features August 19, 2025 17:03 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1150/javadoc August 19, 2025 17:05 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
gradle/libs.versions.toml (3)

34-34: Compose BOM declared—ensure it’s imported in module build scripts

Declaring compose-bom here is good, and Compose libraries have no explicit versions (correct). Ensure each Compose-using module has implementation(platform(libs.compose.bom)) in dependencies; otherwise the versions won’t be pinned.

If helpful, I can provide the exact Gradle snippet for your modules.

Also applies to: 71-77


2-2: AGP 8.6.1 requires Gradle 8.10+—bump the Gradle wrapper

Confirm gradle/wrapper/gradle-wrapper.properties uses Gradle 8.10 or newer; otherwise the build will fail early.

You can regenerate via:

  • ./gradlew wrapper --gradle-version 8.10 --distribution-type all

70-77: Add material-icons-extended to match UI icon imports

The example UI uses androidx.compose.material.icons.* which requires androidx.compose.material:material-icons-extended. Add it to the catalog so modules can depend on it.

Apply this diff:

 material3 = { group = "androidx.compose.material3", name = "material3" }
+material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
🧹 Nitpick comments (12)
examples/README.md (4)

29-31: Fix list indentation to satisfy markdownlint (MD007).

The nested bullet points under the numbered steps are indented by two spaces and are being flagged (Expected: 0; Actual: 2). Either outdent them to top-level bullets, or keep them as nested items but convert the step titles into headings to avoid MD007 conflicts. Suggested minimal fix: remove the extra spaces before the dashes.

-  - Open Android Studio.
-  - Select File > Open and navigate to the cloned repository.
-  - Open the project.
+- Open Android Studio.
+- Select File > Open and navigate to the cloned repository.
+- Open the project.

-  - Wait for Gradle to sync automatically.
-  - If it doesn’t, click on Sync Project with Gradle Files in the toolbar.
+ - Wait for Gradle to sync automatically.
+ - If it doesn’t, click on Sync Project with Gradle Files in the toolbar.

-  - Set up an emulator or connect a physical Android device.
-  - Ensure the device is configured with at least Android 5.0 (API 21).
+ - Set up an emulator or connect a physical Android device.
+ - Ensure the device is configured with at least Android 5.0 (API 21).

-  - Select your emulator or connected device in the device selector dropdown.
-  - Click on the Run button ▶️ in the toolbar or press Shift + F10.
+ - Select your emulator or connected device in the device selector dropdown.
+ - Click on the Run button ▶️ in the toolbar or press Shift + F10.

If you prefer keeping them as nested bullets, switching step titles to headings (e.g., “### 1. Open in Android Studio”) also resolves MD007.

Also applies to: 35-36, 40-41, 45-46


8-8: Add missing punctuation.

End the sentence with a period.

-Follow the steps below to get started with the Live Objects demo app
+Follow the steps below to get started with the Live Objects demo app.

17-23: Strengthen ABLY_KEY guidance and secrecy note.

Add a short note on where to obtain the key and to avoid committing secrets. This improves developer onboarding and security hygiene.

 Add your Ably key to the `local.properties` file:
 
 ```properties
 sdk.dir=/path/to/android/sdk
 
 ABLY_KEY=xxxx:yyyyyy

+Notes:
+- You can create and manage Ably API keys in the Ably Dashboard.
+- Do not commit local.properties to source control. Treat ABLY_KEY as a secret.
+- The example app reads this value from local.properties and exposes it to the app via BuildConfig.



Do you want me to cross-check that `examples/build.gradle.kts` correctly reads `ABLY_KEY` from `local.properties` and wires it into `BuildConfig`?

---

`69-73`: **Add an ABLY_KEY troubleshooting hint.**

Users often forget the key or mistype its format. Add a quick pointer in Troubleshooting.

```diff
 ## Troubleshooting
 
 - SDK Not Found: Install missing SDK versions from File > Settings > Appearance & Behavior > System Settings > Android SDK.
 - Build Failures: Check the error logs and resolve dependencies or configuration issues.
+ - ABLY_KEY not found or invalid: Ensure `ABLY_KEY` is set in `local.properties` with the correct format (`xxxxx:yyyyyy`), and that the examples module is picking it up via BuildConfig.
examples/src/main/java/com/ably/example/screen/TaskManagementScreen.kt (8)

145-176: Use LazyColumn items with stable keys instead of index-based access.

Indexing into a precomputed list with elementAt(index) is less efficient and provides no stable item keys, which can cause unnecessary recompositions. Switch to items(items, key = { it.key }).

-          LazyColumn(
-            verticalArrangement = Arrangement.spacedBy(8.dp)
-          ) {
-            items(taskIdToTask.size) { index ->
-              val task = taskEntries.elementAt(index)
-              TaskItemCard(
-                task = task,
-                isEditing = editingTaskId == task.key,
-                editingText = editingText,
-                onEditingTextChange = { editingText = it },
-                onEdit = {
-                  editingTaskId = task.key
-                  editingText = task.value
-                },
-                onSave = {
-                  scope.launch {
-                    liveTasks?.setCoroutine(task.key, LiveMapValue.of(editingText.trim()))
-                    editingTaskId = null
-                    editingText = ""
-                  }
-                },
-                onCancel = {
-                  editingTaskId = null
-                  editingText = ""
-                },
-                onDelete = {
-                  scope.launch {
-                    liveTasks?.removeCoroutine(task.key)
-                  }
-                }
-              )
-            }
-          }
+          LazyColumn(
+            verticalArrangement = Arrangement.spacedBy(8.dp)
+          ) {
+            items(items = taskEntries, key = { it.key }) { task ->
+              TaskItemCard(
+                task = task,
+                isEditing = editingTaskId == task.key,
+                editingText = editingText,
+                onEditingTextChange = { editingText = it },
+                onEdit = {
+                  editingTaskId = task.key
+                  editingText = task.value
+                },
+                onSave = {
+                  scope.launch {
+                    liveTasks?.setCoroutine(task.key, LiveMapValue.of(editingText.trim()))
+                    editingTaskId = null
+                    editingText = ""
+                  }
+                },
+                onCancel = {
+                  editingTaskId = null
+                  editingText = ""
+                },
+                onDelete = {
+                  scope.launch {
+                    liveTasks?.removeCoroutine(task.key)
+                  }
+                }
+              )
+            }
+          }

32-35: Persist UI state across configuration changes.

Use rememberSaveable for simple String state so edits survive rotations and process recreation.

-  var taskText by remember { mutableStateOf("") }
-  var editingTaskId by remember { mutableStateOf<String?>(null) }
-  var editingText by remember { mutableStateOf("") }
+  var taskText by rememberSaveable { mutableStateOf("") }
+  var editingTaskId by rememberSaveable { mutableStateOf<String?>(null) }
+  var editingText by rememberSaveable { mutableStateOf("") }

82-97: Gate the Add button with enabled= and simplify onClick.

Disable the button for blank input and drop the inner guard for cleaner intent.

-          Button(
-            onClick = {
-              if (taskText.isNotBlank()) {
-                scope.launch {
-                  val taskId = "${System.currentTimeMillis()}_${Uuid.random().toHexString()}"
-                  liveTasks?.setCoroutine(taskId, LiveMapValue.of(taskText.trim()))
-                  taskText = ""
-                }
-              }
-            },
-            modifier = Modifier.weight(1f)
-          ) {
+          Button(
+            onClick = {
+              scope.launch {
+                val taskId = "${System.currentTimeMillis()}_${Uuid.random().toHexString()}"
+                liveTasks?.setCoroutine(taskId, LiveMapValue.of(taskText.trim()))
+                taskText = ""
+              }
+            },
+            enabled = taskText.isNotBlank(),
+            modifier = Modifier.weight(1f)
+          ) {
             Icon(Icons.Default.Add, contentDescription = "Add")
             Spacer(modifier = Modifier.width(8.dp))
             Text("Add Task")
           }

236-240: Prevent long task text from overflowing the row.

Add maxLines and ellipsis for better layout resilience.

-        Text(
-          text = task.value,
-          modifier = Modifier.weight(1f),
-          fontSize = 16.sp
-        )
+        Text(
+          text = task.value,
+          modifier = Modifier.weight(1f),
+          fontSize = 16.sp,
+          maxLines = 1,
+          overflow = TextOverflow.Ellipsis
+        )

You’ll also need the import below.


3-18: Add missing import for TextOverflow used in TaskItemCard.

 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow

43-45: Consider sorting newest-first for better UX (optional).

With keys prefixed by milliseconds, showing the latest task at the top can be more intuitive.

-  val taskEntries = remember(taskIdToTask) {
-    taskIdToTask.entries.sortedBy { it.key }
-  }
+  val taskEntries = remember(taskIdToTask) {
+    taskIdToTask.entries.sortedByDescending { it.key }
+  }

100-110: Optional: Add a confirm affordance before “Remove All”.

Bulk deletion is destructive. Consider a confirmation dialog or disabling the button when there are no tasks. If you want, I can propose a lightweight Material3 AlertDialog integration.


85-90: Handle failures from Live Objects operations.

setCoroutine/removeCoroutine are suspending calls. If they throw (network issues, permissions), the UI silently ignores it. Consider wrapping in runCatching { ... } and surfacing a snackbar/toast/log for visibility.

Example pattern (apply where you call setCoroutine/removeCoroutine):

scope.launch {
  runCatching {
    liveTasks?.setCoroutine(taskId, LiveMapValue.of(taskText.trim()))
  }.onFailure { e ->
    // TODO: route to Snackbar/Toast; for now:
    // Log.e("TaskManagement", "Failed to add task", e)
  }
}

If you prefer, I can wire a Scaffold + SnackbarHost to show errors.

Also applies to: 101-105, 160-165, 171-173

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 153d444 and 420b3ef.

⛔ Files ignored due to path filters (3)
  • examples/src/main/res/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png
  • examples/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • examples/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png is excluded by !**/*.png
📒 Files selected for processing (23)
  • build.gradle.kts (1 hunks)
  • examples/.gitignore (1 hunks)
  • examples/README.md (1 hunks)
  • examples/build.gradle.kts (1 hunks)
  • examples/proguard-rules.pro (1 hunks)
  • examples/src/main/AndroidManifest.xml (1 hunks)
  • examples/src/main/java/com/ably/example/MainActivity.kt (1 hunks)
  • examples/src/main/java/com/ably/example/Utils.kt (1 hunks)
  • examples/src/main/java/com/ably/example/screen/ColorVotingScreen.kt (1 hunks)
  • examples/src/main/java/com/ably/example/screen/MainScreen.kt (1 hunks)
  • examples/src/main/java/com/ably/example/screen/TaskManagementScreen.kt (1 hunks)
  • examples/src/main/java/com/ably/example/ui/theme/Color.kt (1 hunks)
  • examples/src/main/java/com/ably/example/ui/theme/Theme.kt (1 hunks)
  • examples/src/main/java/com/ably/example/ui/theme/Type.kt (1 hunks)
  • examples/src/main/res/drawable/launch_background.xml (1 hunks)
  • examples/src/main/res/mipmap-anydpi/ic_launcher.xml (1 hunks)
  • examples/src/main/res/mipmap-anydpi/ic_launcher_round.xml (1 hunks)
  • examples/src/main/res/values/colors.xml (1 hunks)
  • examples/src/main/res/values/ic_launcher_background.xml (1 hunks)
  • examples/src/main/res/values/strings.xml (1 hunks)
  • examples/src/main/res/values/themes.xml (1 hunks)
  • gradle/libs.versions.toml (4 hunks)
  • settings.gradle.kts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • examples/src/main/res/drawable/launch_background.xml
🚧 Files skipped from review as they are similar to previous changes (19)
  • examples/src/main/java/com/ably/example/ui/theme/Theme.kt
  • examples/src/main/res/values/themes.xml
  • examples/src/main/res/values/colors.xml
  • examples/src/main/res/mipmap-anydpi/ic_launcher.xml
  • examples/src/main/res/values/strings.xml
  • examples/src/main/res/values/ic_launcher_background.xml
  • examples/src/main/res/mipmap-anydpi/ic_launcher_round.xml
  • examples/src/main/java/com/ably/example/screen/ColorVotingScreen.kt
  • settings.gradle.kts
  • build.gradle.kts
  • examples/.gitignore
  • examples/build.gradle.kts
  • examples/src/main/java/com/ably/example/screen/MainScreen.kt
  • examples/src/main/java/com/ably/example/MainActivity.kt
  • examples/src/main/java/com/ably/example/ui/theme/Color.kt
  • examples/src/main/AndroidManifest.xml
  • examples/src/main/java/com/ably/example/ui/theme/Type.kt
  • examples/proguard-rules.pro
  • examples/src/main/java/com/ably/example/Utils.kt
🧰 Additional context used
🧬 Code Graph Analysis (1)
examples/src/main/java/com/ably/example/screen/TaskManagementScreen.kt (1)
examples/src/main/java/com/ably/example/Utils.kt (3)
  • getRealtimeChannel (266-284)
  • observeRootObject (252-264)
  • observeMap (222-250)
🪛 LanguageTool
examples/README.md

[grammar] ~12-~12: There might be a mistake here.
Context: ...Ensure you have the following installed: - [Android Studio](https://developer.androi...

(QB_NEW_EN)


[grammar] ~13-~13: There might be a mistake here.
Context: ...e following installed: - Android Studio (latest stable version) - Java 17 or hi...

(QB_NEW_EN)


[grammar] ~13-~13: There might be a mistake here.
Context: ...roid.com/studio) (latest stable version) - Java 17 or higher - Android SDK with API...

(QB_NEW_EN)


[grammar] ~14-~14: There might be a mistake here.
Context: ...test stable version) - Java 17 or higher - Android SDK with API Level 34 or higher ...

(QB_NEW_EN)


[grammar] ~56-~56: There might be a mistake here.
Context: ...with two tabs: 1. Color Voting Tab: - Vote for Red, Green, or Blue colors -...

(QB_NEW_EN)


[grammar] ~61-~61: There might be a mistake here.
Context: ...all" button 2. Task Management Tab: - Add new tasks using the text input and "...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
examples/README.md

29-29: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


30-30: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


31-31: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


35-35: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


36-36: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


40-40: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


41-41: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


45-45: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


46-46: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: check (29)
  • GitHub Check: check-realtime
  • GitHub Check: check-rest-okhttp
  • GitHub Check: check-liveobjects
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: build
  • GitHub Check: check (19)
  • GitHub Check: check-rest
  • GitHub Check: check (24)
  • GitHub Check: check
  • GitHub Check: check (21)
🔇 Additional comments (3)
gradle/libs.versions.toml (2)

92-94: Kotlin Compose plugin alias aligned with Kotlin 2.1.10—LGTM

Using org.jetbrains.kotlin.plugin.compose with the same version as Kotlin is the recommended setup for Kotlin 2.x.


7-7: All declared AndroidX/Compose versions are published on Google Maven

Confirmed via Google Maven metadata that the following versions exist:

  • androidx.navigation:navigation-compose 2.7.6
  • androidx.core:core-ktx 1.16.0
  • androidx.test.ext:junit 1.3.0
  • androidx.test.espresso:espresso-core 3.7.0
  • androidx.lifecycle:lifecycle-runtime-ktx 2.9.2
  • androidx.activity:activity-compose 1.10.1
  • androidx.compose:compose-bom 2024.09.00

No changes required—just ensure your Gradle repositories include google() and mavenCentral().

examples/src/main/java/com/ably/example/screen/TaskManagementScreen.kt (1)

26-30: Verify Kotlin 2.0+ plugin and stdlib versions

Ensure the project is using Kotlin 2.0 or higher—both the Gradle plugin and the stdlib—so that kotlin.uuid.ExperimentalUuidApi and Uuid are available. Please confirm the version declarations in:

  • build.gradle / build.gradle.kts (plugins block or id("org.jetbrains.kotlin") version "…").
  • settings.gradle.kts (pluginManagement block), if you manage plugin versions there.
  • gradle.properties or your version catalog (libs.versions.toml), if you declare Kotlin versions centrally.

If any of these are below 2.0, bump them (and align stdlib) to avoid compilation errors with kotlin.uuid.

@github-actions github-actions bot temporarily deployed to staging/pull/1150/javadoc August 20, 2025 15:52 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (20)
examples/README.md (4)

29-31: Fix unordered list indentation to satisfy markdownlint (MD007)

Bulleted lines are indented by two spaces at top-level. Remove the leading spaces to comply with MD007 and render consistently.

Apply these diffs:

-  - Open Android Studio.
-  - Select File > Open and navigate to the cloned repository.
-  - Open the project.
+- Open Android Studio.
+- Select File > Open and navigate to the cloned repository.
+- Open the project.
-  - Wait for Gradle to sync automatically.
-  - If it doesn’t, click on Sync Project with Gradle Files in the toolbar.
+- Wait for Gradle to sync automatically.
+- If it doesn’t, click on Sync Project with Gradle Files in the toolbar.
-  - Set up an emulator or connect a physical Android device.
-  - Ensure the device is configured with at least Android 5.0 (API 21).
+- Set up an emulator or connect a physical Android device.
+- Ensure the device is configured with at least Android 5.0 (API 21).
-  - Select your emulator or connected device in the device selector dropdown.
-  - Click on the Run button ▶️ in the toolbar or press Shift + F10.
+- Select your emulator or connected device in the device selector dropdown.
+- Click on the Run button ▶️ in the toolbar or press Shift + F10.

Also applies to: 35-36, 40-41, 45-46


15-15: Clarify compileSdk vs minSdk to avoid confusion

You mention SDK API Level 34+ (build requirement) and device Android 5.0/API 21 (runtime requirement). Make this explicit.

Apply this diff:

-- Android SDK with API Level 34 or higher
+- Android SDK (compileSdk) 34 or higher
-  - Ensure the device is configured with at least Android 5.0 (API 21).
+- Ensure the device runs Android 5.0+ (API 21+) or higher.

Also applies to: 41-41


17-23: Link to where to obtain the Ably API key

A direct link reduces onboarding friction.

Apply this diff:

-Add your Ably key to the `local.properties` file:
+Add your Ably key to the `local.properties` file (you can create/find keys in the Ably dashboard):
+https://ably.com/dashboard

If you want, I can add a brief “How to create a key” section with capabilities guidance.


8-8: Minor punctuation fix for readability

Add a period to complete the sentence.

-Follow the steps below to get started with the Live Objects demo app
+Follow the steps below to get started with the Live Objects demo app.
examples/src/main/kotlin/com/ably/example/ui/theme/Type.kt (2)

10-17: Consider renaming the Typography instance to avoid shadowing the class name.

Using val Typography = Typography(...) is the default template but it shadows the imported class, which can be confusing in larger codebases. Consider AppTypography for clarity.


18-33: Remove or convert commented-out style overrides into actionable TODO.

Large commented blocks tend to rot. Either remove them or keep a short TODO pointing to a follow-up change where you define concrete overrides.

examples/src/main/kotlin/com/ably/example/MainActivity.kt (2)

34-52: Avoid double connect/close: use either onStart/onStop or onResume/onPause, not both.

Right now connect() is called in both onStart and onResume, and close() in both onStop and onPause. These redundant calls add noise and can cause unnecessary state churn. Prefer onStart/onStop for visibility-bound connections.

Apply this diff to remove the duplication:

   override fun onStart() {
     super.onStart()
     realtimeClient.connect()
   }
 
-  override fun onStop() {
-    super.onStop()
-    realtimeClient.close()
-  }
-
-  override fun onResume() {
-    super.onResume()
-    realtimeClient.connect()
-  }
-
-  override fun onPause() {
-    super.onPause()
-    realtimeClient.close()
-  }
+  override fun onStop() {
+    super.onStop()
+    realtimeClient.close()
+  }

14-22: Consider handling missing/invalid ABLY_KEY and construction failures gracefully.

AblyRealtime(ClientOptions) can throw and a missing/invalid BuildConfig.ABLY_KEY will crash at first access. For a demo app, surfacing a friendly error UI makes troubleshooting easier.

Example approach:

private val realtimeClient: AblyRealtime by lazy {
  runCatching {
    AblyRealtime(
      ClientOptions().apply {
        key = BuildConfig.ABLY_KEY
        logLevel = Log.VERBOSE
        autoConnect = false
      }
    )
  }.getOrElse { throwable ->
    // TODO: route to an error screen/snackbar, or finish activity with a message
    throw throwable
  }
}

Optionally gate logging verbosity by build type:

logLevel = if (BuildConfig.DEBUG) Log.VERBOSE else Log.WARN
examples/src/main/kotlin/com/ably/example/screen/MainScreen.kt (3)

16-16: Persist selected tab across configuration changes.

Use rememberSaveable so the selected tab survives rotations and process recreation.

Apply this diff:

-    var selectedTab by remember { mutableIntStateOf(0) }
+    var selectedTab by rememberSaveable { mutableIntStateOf(0) }

18-21: Move user-facing strings to resources for i18n/accessibility.

Hardcoded strings make localization harder. Use string resources and stringResource().

For example:

val tabs = listOf(
  TabItem(stringResource(R.string.tab_color_voting), Icons.Default.Favorite),
  TabItem(stringResource(R.string.tab_task_management), Icons.AutoMirrored.Filled.List),
)

And in TopAppBar title:

Text(stringResource(R.string.app_title))

23-33: Optional: add scroll behavior for TopAppBar.

If content scrolls, adding a scroll behavior gives standard M3 elevation treatment.

Example:

val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
TopAppBar(
  title = { Text(stringResource(R.string.app_title)) },
  colors = TopAppBarDefaults.topAppBarColors(
    containerColor = MaterialTheme.colorScheme.primaryContainer,
    titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
  ),
  scrollBehavior = scrollBehavior
)
examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt (3)

91-95: Avoid hardcoding Button text color; let Material theme decide

Material3 Button provides appropriate content color based on the current theme. Hardcoding white may reduce contrast in some themes. Let the theme handle this.

Apply this diff:

-      Text(
-        text = "Reset all",
-        color = Color.White,
-        fontWeight = FontWeight.Medium
-      )
+      Text(
+        text = "Reset all",
+        fontWeight = FontWeight.Medium
+      )

49-59: Disable “Vote” until the counter is ready to improve UX

Currently, tapping “Vote” before the counter is available is a no-op. Make this visible in the UI by disabling the button until the counter is non-null.

Apply these diffs:

  1. Pass enabled flag from the screen:
   ColorVoteCard(
     color = Color.Red,
     colorName = "Red",
     count = redCount ?: 0,
+    enabled = redCounter != null,
     onVote = {
       scope.launch {
         redCounter?.incrementCoroutine(1)
       }
     }
   )
   ColorVoteCard(
     color = Color.Green,
     colorName = "Green",
     count = greenCount ?: 0,
+    enabled = greenCounter != null,
     onVote = {
       scope.launch {
         greenCounter?.incrementCoroutine(1)
       }
     }
   )
   ColorVoteCard(
     color = Color.Blue,
     colorName = "Blue",
     count = blueCount ?: 0,
+    enabled = blueCounter != null,
     onVote = {
       scope.launch {
         blueCounter?.incrementCoroutine(1)
       }
     }
   )
  1. Update ColorVoteCard signature and button:
 @Composable
 fun ColorVoteCard(
   color: Color,
   colorName: String,
   count: Int,
+  enabled: Boolean = true,
   onVote: () -> Unit
 ) {
 ...
-        OutlinedButton(
-          onClick = onVote,
-        ) {
+        OutlinedButton(
+          onClick = onVote,
+          enabled = enabled,
+        ) {

Also applies to: 60-70, 71-80, 101-106, 146-148


3-15: Add accessibility semantics for the color swatch

Expose a content description for the color preview to support TalkBack/VoiceOver users.

Apply these diffs:

  1. Imports:
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
  1. Semantics on the color swatch:
         Box(
           modifier = Modifier
             .size(40.dp)
-            .background(color, RoundedCornerShape(8.dp))
+            .background(color, RoundedCornerShape(8.dp))
+            .semantics { contentDescription = "$colorName color swatch" }
         )

Also applies to: 125-134

examples/src/main/kotlin/com/ably/example/screen/TaskManagementScreen.kt (2)

82-97: Disable “Add” and “Save” when input is blank

Prevents accidental no-ops and clarifies UI state.

Apply these diffs:

           Button(
             onClick = {
               if (taskText.isNotBlank()) {
                 scope.launch {
                   val taskId = "${System.currentTimeMillis()}_${Uuid.random().toHexString()}"
                   liveTasks?.setCoroutine(taskId, LiveMapValue.of(taskText.trim()))
                   taskText = ""
                 }
               }
-            },
+            },
+            enabled = taskText.isNotBlank(),
             modifier = Modifier.weight(1f)
           ) {
           Button(
-            onClick = onSave,
+            onClick = onSave,
+            enabled = editingText.isNotBlank(),
             modifier = Modifier.weight(1f)
           ) {
             Text("Save")
           }

Also applies to: 214-219


43-46: Consider showing newest tasks first

You sort ascending by key (timestamp_prefix). For better UX, consider reverse order so recently added tasks appear on top.

Apply this diff:

-  val taskEntries = remember(taskIdToTask) {
-    taskIdToTask.entries.sortedBy { it.key }
-  }
+  val taskEntries = remember(taskIdToTask) {
+    taskIdToTask.entries.sortedByDescending { it.key }
+  }
examples/src/main/kotlin/com/ably/example/Utils.kt (4)

175-181: Nit: misleading variable name in root update handler

The variable counterHasBeenRemoved checks for UPDATED, then re-reads the counter. Rename to reflect intent.

Apply this diff:

-    val listener: (LiveMapUpdate) -> Unit = { rootUpdate ->
-      val counterHasBeenRemoved = rootUpdate.update
+    val listener: (LiveMapUpdate) -> Unit = { rootUpdate ->
+      val counterUpdatedAtPath = rootUpdate.update
         .filter { (_, change) -> change == UPDATED }
         .any { (keyName) -> keyName == path }

-      if (counterHasBeenRemoved) root?.get(path)?.asLiveCounter?.let { counter = it }
+      if (counterUpdatedAtPath) root?.get(path)?.asLiveCounter?.let { counter = it }
     }

257-263: Clear root when channel detaches

When the channel leaves the attached state, clearing root prevents stale references.

Apply this diff:

   LaunchedEffect(channelState) {
-    if (channelState == ChannelState.attached) {
-      root = channel.objects.getRootCoroutines()
-    }
+    root = if (channelState == ChannelState.attached) {
+      channel.objects.getRootCoroutines()
+    } else {
+      null
+    }
   }

270-276: Consider using the asynchronous setOptions with error handling

setOptions might be asynchronous and/or throw; using the CompletionListener variant avoids blocking and lets you handle errors without crashing a Composable.

Apply this diff:

-    channel.setOptions(ChannelOptions().apply {
-      attachOnSubscribe = false
-      modes = arrayOf(ChannelMode.object_publish, ChannelMode.object_subscribe)
-    })
+    channel.setOptions(
+      ChannelOptions().apply {
+        attachOnSubscribe = false
+        modes = arrayOf(ChannelMode.object_publish, ChannelMode.object_subscribe)
+      },
+      object : io.ably.lib.realtime.CompletionListener {
+        override fun onSuccess() { /* no-op for example */ }
+        override fun onError(reason: ErrorInfo?) {
+          // Optionally log/handle, but don't crash the Composable
+        }
+      }
+    )

190-199: Simplify reset implementation; coroutineScope + launch is redundant

Since reset is already a suspend function, launching within coroutineScope adds complexity without benefit.

Apply this diff:

-  return CounterState(counterValue, counter) {
-    coroutineScope {
-      launch {
-        counter = channel.objects.createCounterCoroutine().also {
-          root?.setCoroutine(path, LiveMapValue.of(it))
-        }
-      }
-    }
-  }
+  return CounterState(counterValue, counter) {
+    val newCounter = channel.objects.createCounterCoroutine()
+    root?.setCoroutine(path, LiveMapValue.of(newCounter))
+    counter = newCounter
+  }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 420b3ef and ea2981e.

📒 Files selected for processing (10)
  • examples/README.md (1 hunks)
  • examples/build.gradle.kts (1 hunks)
  • examples/src/main/kotlin/com/ably/example/MainActivity.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/Utils.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/screen/MainScreen.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/screen/TaskManagementScreen.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/ui/theme/Color.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/ui/theme/Theme.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/ui/theme/Type.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/build.gradle.kts
🧰 Additional context used
🧬 Code Graph Analysis (5)
examples/src/main/kotlin/com/ably/example/MainActivity.kt (4)
lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java (1)
  • AblyRealtime (31-317)
lib/src/main/java/io/ably/lib/types/ClientOptions.java (1)
  • ClientOptions (19-392)
examples/src/main/kotlin/com/ably/example/ui/theme/Theme.kt (1)
  • AblyTheme (25-46)
examples/src/main/kotlin/com/ably/example/screen/MainScreen.kt (1)
  • MainScreen (13-53)
examples/src/main/kotlin/com/ably/example/screen/MainScreen.kt (2)
examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt (1)
  • ColorVotingScreen (22-98)
examples/src/main/kotlin/com/ably/example/screen/TaskManagementScreen.kt (1)
  • TaskManagementScreen (29-182)
examples/src/main/kotlin/com/ably/example/Utils.kt (2)
live-objects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt (1)
  • decrementAsync (50-52)
lib/src/main/java/io/ably/lib/types/ChannelOptions.java (1)
  • ChannelOptions (13-131)
examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt (1)
examples/src/main/kotlin/com/ably/example/Utils.kt (3)
  • getRealtimeChannel (266-284)
  • observeRootObject (252-264)
  • observeCounter (149-199)
examples/src/main/kotlin/com/ably/example/screen/TaskManagementScreen.kt (1)
examples/src/main/kotlin/com/ably/example/Utils.kt (3)
  • getRealtimeChannel (266-284)
  • observeRootObject (252-264)
  • observeMap (222-250)
🪛 LanguageTool
examples/README.md

[grammar] ~12-~12: There might be a mistake here.
Context: ...Ensure you have the following installed: - [Android Studio](https://developer.androi...

(QB_NEW_EN)


[grammar] ~13-~13: There might be a mistake here.
Context: ...e following installed: - Android Studio (latest stable version) - Java 17 or hi...

(QB_NEW_EN)


[grammar] ~13-~13: There might be a mistake here.
Context: ...roid.com/studio) (latest stable version) - Java 17 or higher - Android SDK with API...

(QB_NEW_EN)


[grammar] ~14-~14: There might be a mistake here.
Context: ...test stable version) - Java 17 or higher - Android SDK with API Level 34 or higher ...

(QB_NEW_EN)


[grammar] ~56-~56: There might be a mistake here.
Context: ...with two tabs: 1. Color Voting Tab: - Vote for Red, Green, or Blue colors -...

(QB_NEW_EN)


[grammar] ~61-~61: There might be a mistake here.
Context: ...all" button 2. Task Management Tab: - Add new tasks using the text input and "...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
examples/README.md

29-29: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


30-30: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


31-31: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


35-35: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


36-36: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


40-40: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


41-41: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


45-45: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


46-46: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: check
  • GitHub Check: check-rest-okhttp
  • GitHub Check: build
  • GitHub Check: check (21)
  • GitHub Check: check-rest
  • GitHub Check: check (24)
  • GitHub Check: check (29)
  • GitHub Check: check (19)
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-liveobjects
  • GitHub Check: check-realtime
🔇 Additional comments (9)
examples/README.md (1)

75-83: Fix unclosed code block in “Create signing keys” section

The fenced code block starting at Line 75 is not closed, which will break rendering for the remainder of the document.

Apply this diff to close the fence:

 -keyalg RSA -keysize 2048 -validity 25000 -dname "CN=Ably Example App,OU=Examples,O=Ably,L=London,ST=England,C=GB"
+```

Likely an incorrect or invalid review comment.

examples/src/main/kotlin/com/ably/example/ui/theme/Color.kt (1)

5-11: LGTM: Theme color constants look good.

Clear, conventional palette constants for M3 theming. No issues spotted.

examples/src/main/kotlin/com/ably/example/MainActivity.kt (1)

17-19: ABLY_KEY sourcing verified

No hardcoded Ably keys were found in the repository. In examples/build.gradle.kts, ABLY_KEY is defined via

buildConfigField("String", "ABLY_KEY", "\"${getLocalProperty("EXAMPLES_ABLY_KEY") ?: ""}\"")

which pulls from a local/CI secret. A repository-wide search did not surface any literal AppKeys or Base64-style tokens.

No changes needed.

examples/src/main/kotlin/com/ably/example/screen/MainScreen.kt (1)

55-58: LGTM: Simple TabItem model works well here.

The minimal data class is appropriate for this usage.

examples/src/main/kotlin/com/ably/example/ui/theme/Theme.kt (2)

31-39: Dynamic color selection and static fallbacks are correctly implemented.

The S+ gate for dynamic colors and clean fallback to static schemes look good.


41-45: LGTM: MaterialTheme wiring is correct.

Passing both colorScheme and Typography provides a consistent theme across the app.

examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt (1)

24-33: Overall: solid, idiomatic Compose + LiveObjects wiring

Channel acquisition, root observation, and per-counter observation look clean. The voting and reset flows are straightforward and match the helpers in Utils.kt.

examples/src/main/kotlin/com/ably/example/screen/TaskManagementScreen.kt (2)

31-41: Nicely structured and readable UI

The example makes good use of Compose state and coroutines, and the LiveMap integration is clear and idiomatic for an examples module.


26-28: Kotlin version confirmed (≥ 2.0), ExperimentalUuidApi is supported.

• The project’s version catalog (gradle/libs.versions.toml line 23) declares
kotlin = "2.1.10", which is ≥ 2.0.
• No further changes needed—the kotlin.uuid.ExperimentalUuidApi import is safe.

ttypic added 3 commits August 21, 2025 10:59
Added an Android Compose-based example application demonstrating Ably Live Objects integration. Includes tasks management and color voting screens.
@ttypic ttypic force-pushed the feat/lo-example-app branch from ea2981e to 6525e03 Compare August 21, 2025 12:15
@github-actions github-actions bot temporarily deployed to staging/pull/1150/features August 21, 2025 12:16 Inactive
@ttypic ttypic force-pushed the feat/lo-example-app branch from 6525e03 to 494ec11 Compare August 21, 2025 12:18
@github-actions github-actions bot temporarily deployed to staging/pull/1150/features August 21, 2025 12:18 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1150/javadoc August 21, 2025 12:21 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (5)
examples/proguard-rules.pro (1)

22-22: Clarify Live Objects keep-rules strategy; add fallback commented rules (only if consumer rules aren’t present)

Per earlier discussion, Live Objects needed keep rules for DefaultObjectsPlugin and DefaultObjectsSerializer. That was addressed at the library level (consumer rules) in a separate PR. Let’s: (a) document that this example relies on those consumer rules, and (b) provide commented fallback rules here for folks copying the example without the library’s consumer rules.

Proposed append after Line 22:

+
+# --- Ably Live Objects -------------------------------------------------------
+# The Ably Android/Java library provides consumer ProGuard/R8 rules to keep
+# reflection/serialization entry points used by Live Objects. This example app
+# relies on those consumer rules. If you are using a local module or a custom
+# build where consumer rules are not packaged, uncomment the fallback rules:
+#-keep class io.ably.lib.objects.DefaultObjectsPlugin { *; }
+#-keep class io.ably.lib.objects.serialization.DefaultObjectsSerializer { *; }
+# -----------------------------------------------------------------------------

To verify the setup, please run:

#!/bin/bash
# 1) Check that the examples module actually wires this rules file and enables shrink/obfuscation for release builds.
echo "== Checking examples Gradle configs =="
fd -a -t f -g "build.gradle*" examples | while read -r f; do
  echo "---- $f ----"
  rg -nP "minifyEnabled\\s*true|isMinifyEnabled\\s*=\\s*true|proguardFiles|getDefaultProguardFile\\(|proguard-rules\\.pro|consumerProguardFiles" "$f" -n
done

# 2) Confirm the library module ships consumer rules (so the example needn't duplicate them).
echo
echo "== Searching for library consumer rules =="
rg -nP "consumerProguardFiles|proguard\\.txt|consumer-rules" -C2
examples/README.md (2)

15-16: Align stated SDK levels with the module’s build config

The README says “API Level 34+” and device “API 21+”, but the examples module was noted to use compileSdk = 35 and minSdk = 30. Update the docs or lower minSdk in build to match; otherwise newcomers will pick incompatible emulators/devices.

Suggested edits:

- - Android SDK with API Level 34 or higher
+ - Android SDK with API Level 35 or higher
...
-  - Ensure the device is configured with at least Android 5.0 (API 21).
+  - Ensure the device/emulator runs Android 11 (API 30) or higher.

If you intend to support API 21, lower minSdk in examples/build.gradle(.kts) accordingly and keep README as-is. Please confirm the intended minSdk so we can finalize the correct wording.

Also applies to: 40-42


19-23: Add explicit “do not commit secrets” warnings under local.properties examples

Prevent accidental key/keystore leaks by adding visible warnings right below both example blocks.

 sdk.dir=/path/to/android/sdk

 EXAMPLES_ABLY_KEY=xxxx:yyyyyy

+> Security note: Do not commit local.properties or any API keys to version control. Use test keys for demos and rotate keys if exposed.


```diff
 RELEASE_KEY_PASSWORD=<key-password>

+> Security note: Never commit keystores or these credentials. Keep release.keystore out of the repo and store secrets in a secure vault.



Also applies to: 83-90

</blockquote></details>
<details>
<summary>examples/src/main/kotlin/com/ably/example/Utils.kt (2)</summary><blockquote>

`25-35`: **Avoid result!! and unsafe continuation.resume/cancel — make suspend bridges cancellation-safe**

Force unwraps (result!!) and calling resume/cancel without checking continuation state can crash (NPE, “Already resumed”) when callbacks fire after cancellation. Prefer resumeWithException, check isActive, and handle null results as failures.



Apply this pattern (representative diffs shown; repeat similarly for all listed functions):

getRootCoroutines:
```diff
 private suspend fun RealtimeObjects.getRootCoroutines(): LiveMap = suspendCancellableCoroutine { continuation ->
   getRootAsync(object : ObjectsCallback<LiveMap> {
     override fun onSuccess(result: LiveMap?) {
-      continuation.resume(result!!)
+      if (continuation.isActive) {
+        result?.let { continuation.resume(it) }
+          ?: continuation.resumeWithException(IllegalStateException("getRootAsync returned null"))
+      }
     }
     override fun onError(exception: AblyException?) {
-      continuation.cancel(exception)
+      if (continuation.isActive) {
+        continuation.resumeWithException(exception ?: IllegalStateException("getRootAsync error"))
+      }
     }
   })
 }

incrementCoroutine:

 suspend fun LiveCounter.incrementCoroutine(amount: Int): Unit = supressCoroutineExceptions {
   suspendCancellableCoroutine { continuation ->
     incrementAsync(amount, object : ObjectsCallback<Void> {
       override fun onSuccess(result: Void?) {
-        continuation.resume(Unit)
+        if (continuation.isActive) continuation.resume(Unit)
       }
       override fun onError(exception: AblyException?) {
-        continuation.cancel(exception)
+        if (continuation.isActive) {
+          continuation.resumeWithException(exception ?: IllegalStateException("incrementAsync error"))
+        }
       }
     })
   }
 }

Also applies to: 37-49, 50-60, 62-74, 76-88, 90-102, 126-138, 140-152


159-163: Prevent creating orphaned LiveObjects when root is null

Both observeCounter and observeMap call getOrCreate… even when root is null, creating objects that are never inserted into the root map.

   LaunchedEffect(root) {
     supressCoroutineExceptions {
-      counter = getOrCreateCounter(channel, root, path)
+      if (root != null) {
+        counter = getOrCreateCounter(channel, root, path)
+      }
     }
   }
...
   LaunchedEffect(root) {
     supressCoroutineExceptions {
-      map = getOrCreateMap(channel, root, path)
+      if (root != null) {
+        map = getOrCreateMap(channel, root, path)
+      }
     }
   }

Also applies to: 234-238

🧹 Nitpick comments (24)
examples/.gitignore (1)

1-1: Nit: clarify directory intent and add common Android artifact

A trailing slash conveys the intent to ignore a directory, and captures is a common module-level folder created by Android Studio for test artifacts.

-/build
+/build/
+/captures/
examples/proguard-rules.pro (3)

1-7: Modernize header to reference R8 and Gradle(.kts); tighten wording

ProGuard has been superseded by R8 in AGP. Let’s modernize the header and make it DSL-agnostic.

-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-#   http://developer.android.com/guide/developing/tools/proguard.html
+# Add project-specific shrinker (R8/ProGuard) rules for the examples module here.
+# You can control which rule files are applied via buildTypes.proguardFiles
+# in build.gradle or build.gradle.kts.
+#
+# For more details, see the Android Gradle Plugin / R8 documentation.

15-17: Preserve line numbers in release to get actionable crash stack traces

Keeping LineNumberTable/SourceFile dramatically improves crash triage for sample app users.

-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
+# Preserve line number information for debugging stack traces.
+-keepattributes SourceFile,LineNumberTable

8-14: If WebView JS interfaces are used, replace the placeholder with your FQCN

If the example doesn’t use WebView JS bridges, no action needed. If it does, you should replace the placeholder with the actual interface class.

Would you confirm whether any screen uses WebView + addJavascriptInterface? If yes, I’ll propose the exact keep rule with the correct FQCN.

examples/src/main/kotlin/com/ably/example/ui/theme/Color.kt (2)

5-11: Scope these tokens to the module (use internal) to avoid leaking example-only API.

These are implementation details of the examples module. Marking them internal keeps the public surface clean and prevents accidental reuse from other modules.

-val Purple80 = Color(0xFFD0BCFF)
-val PurpleGrey80 = Color(0xFFCCC2DC)
-val Pink80 = Color(0xFFEFB8C8)
+internal val Purple80 = Color(0xFFD0BCFF)
+internal val PurpleGrey80 = Color(0xFFCCC2DC)
+internal val Pink80 = Color(0xFFEFB8C8)

-val Purple40 = Color(0xFF6650a4)
-val PurpleGrey40 = Color(0xFF625b71)
-val Pink40 = Color(0xFF7D5260)
+internal val Purple40 = Color(0xFF6650A4)
+internal val PurpleGrey40 = Color(0xFF625B71)
+internal val Pink40 = Color(0xFF7D5260)

5-11: Consider adopting Material 3 token naming for clarity and long‑term maintainability.

Template-style names (Purple40/80, etc.) make it harder to reason about roles across light/dark. Using role-based tokens (e.g., md_theme_light_primary, md_theme_dark_primary, etc.) aligns with M3 guidance and scales when you add surfaces/containers/error/inverse colors. If you want, I can generate a follow-up patch that renames these and updates references.

examples/src/main/kotlin/com/ably/example/ui/theme/Theme.kt (3)

31-39: Minor readability refactor: compute supportsDynamicColor once.

This keeps the branching intent obvious and avoids repeating the SDK check.

-    val colorScheme = when {
-        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+    val supportsDynamicColor = dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+    val colorScheme = when {
+        supportsDynamicColor -> {
             val context = LocalContext.current
             if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
         }
 
         darkTheme -> DarkColorScheme
         else -> LightColorScheme
     }

13-23: Consider defining more roles than primary/secondary/tertiary in the static schemes.

Relying on defaults is fine for a sample, but if the examples aim to showcase production-quality theming, explicitly provide values for surfaces, containers, outlines, error, inverse, etc., to guarantee consistent contrast and brand alignment regardless of OEM defaults.

If you’d like, I can propose a full lightColorScheme/darkColorScheme mapping using your tokens (or Ably brand colors) and run through WCAG contrast checks.


25-30: Add brief KDoc to clarify dynamicColor behavior and intended usage.

Helps readers understand why dynamic color defaults to true and how it behaves on pre‑S devices and in screenshots/tests.

Example:

/**
 * App theme wrapper.
 *
 * @param darkTheme Whether to use dark mode; defaults to system setting.
 * @param dynamicColor When true and running on Android 12+ (API 31), uses Material You dynamic color.
 *                     Falls back to the static [LightColorScheme]/[DarkColorScheme] otherwise.
 */
.github/workflows/example-app.yml (1)

41-42: Improve failure diagnostics: include crash buffer and Gradle stacktrace

Only printing System.out at INFO level may miss crashes. Capture crash buffer and show Gradle stacktrace for faster triage.

-          # Print emulator logs if tests fail
-          script: ./gradlew :examples:connectedAndroidTest || (adb logcat -d System.out:I && exit 1)
+          # Print logs if tests fail: Gradle stacktrace + crash + filtered System.out
+          script: ./gradlew :examples:connectedAndroidTest --stacktrace || (\
+            echo '--- adb crash buffer ---' && adb logcat -b crash -d && \
+            echo '--- adb System.out (INFO) ---' && adb logcat -d System.out:I && exit 1)
examples/README.md (1)

29-31: Fix list indentation to satisfy markdownlint (MD007)

Some list items are indented by two spaces, triggering MD007. Unindent to level 0 for consistency.

-  - Open Android Studio.
-  - Select File > Open and navigate to the cloned repository.
-  - Open the project.
+- Open Android Studio.
+- Select File > Open and navigate to the cloned repository.
+- Open the project.
...
-  - Wait for Gradle to sync automatically.
-  - If it doesn’t, click on Sync Project with Gradle Files in the toolbar.
+- Wait for Gradle to sync automatically.
+- If it doesn’t, click on Sync Project with Gradle Files in the toolbar.
...
-  - Set up an emulator or connect a physical Android device.
-  - Ensure the device is configured with at least Android 5.0 (API 21).
+- Set up an emulator or connect a physical Android device.
+- Ensure the device/emulator runs Android 11 (API 30) or higher.
...
-  - Select your emulator or connected device in the device selector dropdown.
-  - Click on the Run button ▶️ in the toolbar or press Shift + F10.
+- Select your emulator or connected device in the device selector dropdown.
+- Click on the Run button ▶️ in the toolbar or press Shift + F10.

Also applies to: 35-36, 40-41, 45-46

examples/src/main/kotlin/com/ably/example/Sandbox.kt (4)

14-27: Tighten retry policy and add explicit timeouts

Currently you retry on any non-2xx (including 4xx) and rely on timeout exceptions without configuring timeouts. Add HttpTimeout and retry only on transient 5xx to avoid hammering on client errors.

 private val client = HttpClient(CIO) {
+  install(HttpTimeout) {
+    connectTimeoutMillis = 10_000
+    requestTimeoutMillis = 30_000
+    socketTimeoutMillis = 30_000
+  }
   install(HttpRequestRetry) {
     maxRetries = 5
-    retryIf { _, response ->
-      !response.status.isSuccess()
-    }
+    retryIf { _, response -> response.status.value in setOf(500, 502, 503, 504) }
     retryOnExceptionIf { _, cause ->
       cause is ConnectTimeoutException ||
         cause is HttpRequestTimeoutException ||
         cause is SocketTimeoutException
     }
     exponentialDelay()
   }
 }

31-51: Make getInstance() concurrency-safe (avoid double creation)
Concurrent callers can race and create multiple sandboxes. Guard with a Mutex.

 class Sandbox private constructor(val appId: String, val apiKey: String) {
   companion object {
-    private var cachedInstance: Sandbox? = null
+    private var cachedInstance: Sandbox? = null
+    private val mutex = kotlinx.coroutines.sync.Mutex()
@@
-    suspend fun getInstance(): Sandbox {
-      cachedInstance?.let { return it }
-      val created = createInstance()
-      cachedInstance = created
-      return created
-    }
+    suspend fun getInstance(): Sandbox = mutex.lock().let {
+      try {
+        cachedInstance ?: createInstance().also { cachedInstance = it }
+      } finally {
+        mutex.unlock()
+      }
+    }
   }
 }

33-44: Harden JSON parsing and status handling for createInstance()

Check the HTTP status and fail with a clear message; avoid NPEs if fields are missing.

     suspend fun createInstance(): Sandbox {
-      val response: HttpResponse = client.post("https://sandbox.realtime.ably-nonprod.net/apps") {
+      val response: HttpResponse = client.post("https://sandbox.realtime.ably-nonprod.net/apps") {
         contentType(ContentType.Application.Json)
         setBody(loadAppCreationRequestBody().toString())
       }
-      val body = JsonParser.parseString(response.bodyAsText())
+      require(response.status.isSuccess()) { "Sandbox app creation failed: ${'$'}{response.status}" }
+      val body = JsonParser.parseString(response.bodyAsText())
       return Sandbox(
-        appId = body.asJsonObject["appId"].asString,
-        apiKey = body.asJsonObject["keys"].asJsonArray[0].asJsonObject["keyStr"].asString,
+        appId = body.asJsonObject.getAsJsonPrimitive("appId")?.asString
+          ?: error("Response missing appId"),
+        apiKey = body.asJsonObject.getAsJsonArray("keys")?.get(0)?.asJsonObject?.getAsJsonPrimitive("keyStr")?.asString
+          ?: error("Response missing keys[0].keyStr"),
       )
     }

7-8: Redundant import

You import io.ktor.client.network.sockets.* and SocketTimeoutException explicitly. One can go.

-import io.ktor.client.network.sockets.*
-import io.ktor.client.network.sockets.SocketTimeoutException
+import io.ktor.client.network.sockets.SocketTimeoutException
examples/src/main/kotlin/com/ably/example/Utils.kt (2)

281-291: Ensure options are applied before attach; avoid racing setOptions vs attach

Use a LaunchedEffect to call the suspend updateOptions wrapper, then attach; keep detach in onDispose.

-  DisposableEffect(channel) {
-    channel.setOptions(ChannelOptions().apply {
-      attachOnSubscribe = false
-      modes = arrayOf(ChannelMode.object_publish, ChannelMode.object_subscribe)
-    })
-
-    channel.attach()
-
-    onDispose {
-      channel.detach()
-    }
-  }
+  // Apply options and attach asynchronously when channel changes
+  LaunchedEffect(channel) {
+    val options = ChannelOptions().apply {
+      attachOnSubscribe = false
+      modes = arrayOf(ChannelMode.object_publish, ChannelMode.object_subscribe)
+    }
+    supressCoroutineExceptions { channel.updateOptions(options) }
+    channel.attach()
+  }
+  DisposableEffect(channel) {
+    onDispose { channel.detach() }
+  }

297-302: Rename typo and add minimal logging in suppressCoroutineExceptions

The helper function and its call sites currently use the misspelled name supressCoroutineExceptions. Please rename it to suppressCoroutineExceptions everywhere to improve readability and to avoid confusion. While you’re here, consider logging suppressed exceptions at a warning level to aid future debugging.

Locations to update (in examples/src/main/kotlin/com/ably/example/Utils.kt):

  • Line 62: LiveCounter.incrementCoroutine
  • Line 76: LiveCounter.decrementCoroutine
  • Line 90: Channel.updateOptions
  • Line 126: LiveMap.setCoroutine
  • Line 140: LiveMap.removeCoroutine
  • Line 160: inline suppression block
  • Line 235: inline suppression block
  • Line 268: inline suppression block
  • Line 297: function definition

Suggested diffs:

--- a/examples/src/main/kotlin/com/ably/example/Utils.kt
+++ b/examples/src/main/kotlin/com/ably/example/Utils.kt
@@ -297,7 +297,7 @@
- suspend fun supressCoroutineExceptions(block: suspend () -> Unit) {
+ suspend fun suppressCoroutineExceptions(block: suspend () -> Unit) {
   try {
     block()
   } catch (e: Exception) {
     // Optionally log suppressed exceptions
     android.util.Log.w("Examples", "Suppressed coroutine exception", e)
   }
 }

And for each call site, e.g.:

- suspend fun LiveCounter.incrementCoroutine(amount: Int): Unit = supressCoroutineExceptions {
+ suspend fun LiveCounter.incrementCoroutine(amount: Int): Unit = suppressCoroutineExceptions {
examples/src/main/kotlin/com/ably/example/screen/MainScreen.kt (3)

16-16: Preserve tab selection across configuration changes with rememberSaveable.

Using remember loses state on rotation/process death. Swap to rememberSaveable to keep the selected tab stable.

Apply this diff:

@@
-fun MainScreen(realtimeClient: AblyRealtime) {
-    var selectedTab by remember { mutableIntStateOf(0) }
+fun MainScreen(realtimeClient: AblyRealtime) {
+    var selectedTab by rememberSaveable { mutableIntStateOf(0) }

Add the missing import:

@@
-import androidx.compose.runtime.*
+import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.rememberSaveable

19-22: Avoid recreating the tabs list on every recomposition.

Wrap the list in remember. Cheap here, but it’s a habitual win and removes minor churn.

-    val tabs = listOf(
+    val tabs = remember {
+        listOf(
         TabItem("Color Voting", Icons.Default.Favorite),
         TabItem("Task Management", Icons.AutoMirrored.Filled.List),
-    )
+        )
+    }

27-27: Consider string resources for UI text.

"Ably Live Objects Demo" and "(sandbox)" are hardcoded. If you foresee localization or UI tests asserting by resource IDs, move these to string resources.

examples/src/androidTest/kotlin/com/ably/example/ColorVotingScreenTest.kt (1)

28-30: No-op finder for "Red" — assert or remove.

This call doesn’t assert anything. Either assert it’s displayed or drop it.

-    // Get initial red count (should be 0 initially or some value)
-    composeTestRule.onNodeWithText("Red")
+    // Ensure the Red card is visible
+    composeTestRule.onNodeWithText("Red").assertIsDisplayed()
examples/src/main/kotlin/com/ably/example/MainActivity.kt (1)

20-37: Blocking in authCallback via runBlocking — acceptable for demo, consider prefetching for UX.

runBlocking inside TokenCallback is fine for an example, but can block the requesting thread. If you want smoother UX, prefetch the sandbox key earlier (e.g., in onCreate via lifecycleScope) and capture it for the callback.

examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt (2)

41-47: Optional: externalize strings and improve a11y labels.

"Vote for your favorite color" and "Reset all" are hardcoded. Also consider using contentDescription like "Vote for Red" for better accessibility.

Example:

@@
-    Text(
-      text = "Vote for your favorite color",
+    Text(
+      text = stringResource(R.string.vote_header),
@@
-    Button(
+    Button(
       enabled = redCounter != null && greenCounter != null && blueCounter != null,
@@
-      Text(
-        text = "Reset all",
+      Text(
+        text = stringResource(R.string.reset_all),

And in ColorVoteCard’s button:

-        OutlinedButton(
+        OutlinedButton(
           onClick = onVote,
           enabled = enabled,
         ) {
-          Text(
-            text = "Vote",
+          Text(
+            text = stringResource(R.string.vote),
             fontWeight = FontWeight.Medium
           )
         }

You’d need to add the string resources and import stringResource.

Also applies to: 85-100


49-83: DRY: three nearly identical ColorVoteCard blocks.

You can eliminate duplication by mapping over a list of descriptors (color, name, state). This will also make it easier to add more choices later.

Sketch:

val cards = listOf(
  Triple(Color.Red, "Red", Triple(redCount, redCounter, ::resetRed)),
  Triple(Color.Green, "Green", Triple(greenCount, greenCounter, ::resetGreen)),
  Triple(Color.Blue, "Blue", Triple(blueCount, blueCounter, ::resetBlue)),
)

cards.forEach { (color, name, state) ->
  val (count, counter, _) = state
  ColorVoteCard(
    color = color,
    colorName = name,
    count = count ?: 0,
    enabled = counter != null,
    onVote = { scope.launch { counter?.incrementCoroutine(1) } }
  )
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ea2981e and 494ec11.

⛔ Files ignored due to path filters (3)
  • examples/src/main/res/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png
  • examples/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • examples/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png is excluded by !**/*.png
📒 Files selected for processing (27)
  • .github/workflows/example-app.yml (1 hunks)
  • build.gradle.kts (1 hunks)
  • examples/.gitignore (1 hunks)
  • examples/README.md (1 hunks)
  • examples/build.gradle.kts (1 hunks)
  • examples/proguard-rules.pro (1 hunks)
  • examples/src/androidTest/kotlin/com/ably/example/ColorVotingScreenTest.kt (1 hunks)
  • examples/src/androidTest/kotlin/com/ably/example/MainScreenTest.kt (1 hunks)
  • examples/src/main/AndroidManifest.xml (1 hunks)
  • examples/src/main/kotlin/com/ably/example/MainActivity.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/Sandbox.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/Utils.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/screen/MainScreen.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/screen/TaskManagementScreen.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/ui/theme/Color.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/ui/theme/Theme.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/ui/theme/Type.kt (1 hunks)
  • examples/src/main/res/drawable/launch_background.xml (1 hunks)
  • examples/src/main/res/mipmap-anydpi/ic_launcher.xml (1 hunks)
  • examples/src/main/res/mipmap-anydpi/ic_launcher_round.xml (1 hunks)
  • examples/src/main/res/values/colors.xml (1 hunks)
  • examples/src/main/res/values/ic_launcher_background.xml (1 hunks)
  • examples/src/main/res/values/strings.xml (1 hunks)
  • examples/src/main/res/values/themes.xml (1 hunks)
  • gradle/libs.versions.toml (4 hunks)
  • settings.gradle.kts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (14)
  • examples/src/main/res/values/themes.xml
  • examples/src/main/res/drawable/launch_background.xml
  • examples/src/main/res/values/strings.xml
  • examples/src/main/res/values/ic_launcher_background.xml
  • examples/src/main/kotlin/com/ably/example/ui/theme/Type.kt
  • examples/src/main/res/mipmap-anydpi/ic_launcher_round.xml
  • settings.gradle.kts
  • examples/src/main/res/values/colors.xml
  • examples/build.gradle.kts
  • examples/src/main/kotlin/com/ably/example/screen/TaskManagementScreen.kt
  • examples/src/main/res/mipmap-anydpi/ic_launcher.xml
  • examples/src/main/AndroidManifest.xml
  • build.gradle.kts
  • gradle/libs.versions.toml
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-06T09:28:12.298Z
Learnt from: sacOO7
PR: ably/ably-java#1095
File: live-objects/src/test/kotlin/io/ably/lib/objects/integration/setup/IntegrationTest.kt:87-89
Timestamp: 2025-06-06T09:28:12.298Z
Learning: The Sandbox.kt file in ably-java live-objects module already has comprehensive HTTP retry mechanism using HttpRequestRetry with 5 retries, exponential backoff, and automatic retry on non-success responses and timeout exceptions.

Applied to files:

  • examples/src/main/kotlin/com/ably/example/Sandbox.kt
🧬 Code graph analysis (4)
examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt (1)
examples/src/main/kotlin/com/ably/example/Utils.kt (3)
  • getRealtimeChannel (277-295)
  • observeRootObject (261-275)
  • observeCounter (154-206)
examples/src/main/kotlin/com/ably/example/Utils.kt (2)
live-objects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt (1)
  • decrementAsync (50-52)
lib/src/main/java/io/ably/lib/types/ChannelOptions.java (1)
  • ChannelOptions (13-131)
examples/src/main/kotlin/com/ably/example/screen/MainScreen.kt (2)
examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt (1)
  • ColorVotingScreen (22-102)
examples/src/main/kotlin/com/ably/example/screen/TaskManagementScreen.kt (1)
  • TaskManagementScreen (29-182)
examples/src/main/kotlin/com/ably/example/MainActivity.kt (2)
lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java (1)
  • AblyRealtime (31-317)
lib/src/main/java/io/ably/lib/types/ClientOptions.java (1)
  • ClientOptions (19-392)
🪛 actionlint (1.7.7)
.github/workflows/example-app.yml

22-22: the runner of "actions/setup-java@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🪛 LanguageTool
examples/README.md

[grammar] ~12-~12: There might be a mistake here.
Context: ...Ensure you have the following installed: - [Android Studio](https://developer.androi...

(QB_NEW_EN)


[grammar] ~13-~13: There might be a mistake here.
Context: ...e following installed: - Android Studio (latest stable version) - Java 17 or hi...

(QB_NEW_EN)


[grammar] ~13-~13: There might be a mistake here.
Context: ...roid.com/studio) (latest stable version) - Java 17 or higher - Android SDK with API...

(QB_NEW_EN)


[grammar] ~14-~14: There might be a mistake here.
Context: ...test stable version) - Java 17 or higher - Android SDK with API Level 34 or higher ...

(QB_NEW_EN)


[grammar] ~56-~56: There might be a mistake here.
Context: ...with two tabs: 1. Color Voting Tab: - Vote for Red, Green, or Blue colors -...

(QB_NEW_EN)


[grammar] ~61-~61: There might be a mistake here.
Context: ...all" button 2. Task Management Tab: - Add new tasks using the text input and "...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
examples/README.md

29-29: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


30-30: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


31-31: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


35-35: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


36-36: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


40-40: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


41-41: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


45-45: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


46-46: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: check (19)
  • GitHub Check: check (24)
  • GitHub Check: check (29)
  • GitHub Check: check (21)
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-rest-okhttp
  • GitHub Check: check-rest
  • GitHub Check: check (29)
  • GitHub Check: check-realtime
🔇 Additional comments (9)
examples/.gitignore (2)

1-1: LGTM: correct module-scoped ignore

Using a leading slash inside a module-level .gitignore correctly anchors the ignore to examples/build. No functional issues.


1-1: No redundant /build ignore patterns found—retain examples/.gitignore entry

Verification confirmed that none of the existing .gitignore files include patterns like **/build/, /build/, or build/, so the /build rule in examples/.gitignore is not redundant and should be kept as is.

examples/src/main/kotlin/com/ably/example/ui/theme/Color.kt (2)

5-11: LGTM on the actual values.

Color constants are correctly specified in ARGB (0xAARRGGBB) and map cleanly to light/dark roles via the theme. No functional issues.


1-3: Action Required: Verify Detekt/Lint Configuration for MagicNumber Rule

It appears there’s a detekt-config.yaml at the project root but no explicit MagicNumber entry in it, and no detekt/ktlint plugin references were found in your Gradle scripts. That suggests one of two things:

  • If Detekt (with the default MagicNumber rule) or KtLint is indeed applied to the examples module, these hard-coded hex literals in examples/src/main/kotlin/com/ably/example/ui/theme/Color.kt will trigger “MagicNumber” warnings.
  • If neither tool is configured for this module, the suppression is unnecessary.

Please confirm your lint setup. If you do need to suppress these known, deliberate literals, add at the top of the file:

@file:Suppress("MagicNumber")
package com.ably.example.ui.theme

import androidx.compose.ui.graphics.Color

Otherwise, you can safely ignore this suggestion.

Optional: To double-check where Detekt/KtLint is applied, you might search your Gradle scripts with:

rg -nP 'detekt|ktlint' -g '*.gradle*'
examples/src/main/kotlin/com/ably/example/ui/theme/Theme.kt (1)

41-45: LGTM: Correct Material3 setup and safe dynamic color gating.

The theme correctly gates dynamic color by API level, applies Typography, and wraps content via MaterialTheme. No functional issues.

.github/workflows/example-app.yml (1)

15-15: No change required: emulator API level aligns with minSdk

The examples/build.gradle.kts file sets minSdk = 29, and the workflow uses android-api-level: [ 29 ], so the emulator API level already matches the module’s minimum SDK. You can ignore the suggestion to bump to API 30.

Likely an incorrect or invalid review comment.

examples/src/androidTest/kotlin/com/ably/example/MainScreenTest.kt (1)

17-22: LGTM — simple, stable UI assertion for the two main tabs

The test is focused and should be robust. No changes needed.

examples/src/main/kotlin/com/ably/example/MainActivity.kt (1)

35-36: Good: explicit autoConnect=false with manual lifecycle control.

examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt (1)

146-159: Add test tags to ColorVoteCard for reliable UI testing

In examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt (around lines 146–159), introduce stable test hooks by adding a testTag modifier to both the count Text and the OutlinedButton. This will let your instrumentation tests target each color’s vote controls unambiguously.

• Import the testTag extension:

import androidx.compose.ui.platform.testTag
``` ([developer.android.com](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/package-summary?utm_source=chatgpt.com))Add `modifier = Modifier.testTag("count_${colorName.lowercase()}")` to the vote count `Text`.  
• Add `modifier = Modifier.testTag("vote_${colorName.lowercase()}")` to the `OutlinedButton`. ([developer.android.com](https://developer.android.com/develop/ui/compose/testing/interoperability?utm_source=chatgpt.com))

```diff
@@ examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt
 import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.platform.testTag

@@
         Text(
           text = count.toString(),
           fontSize = 24.sp,
-          fontWeight = FontWeight.Bold
+          fontWeight = FontWeight.Bold,
+          modifier = Modifier.testTag("count_${colorName.lowercase()}")
         )
@@
         OutlinedButton(
           onClick = onVote,
-          enabled = enabled,
+          enabled = enabled,
+          modifier = Modifier.testTag("vote_${colorName.lowercase()}"),
         ) {
           Text(
             text = "Vote",
             fontWeight = FontWeight.Medium
           )
         }

Afterward, refactor your tests to use:

composeTestRule.onNodeWithTag("vote_red")…
composeTestRule.onNodeWithTag("count_red")…

Then run the instrumentation suite to confirm improved test stability.

@github-actions github-actions bot temporarily deployed to staging/pull/1150/features August 21, 2025 13:41 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1150/javadoc August 21, 2025 13:43 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
README.md (5)

122-124: Unify list marker style to satisfy markdownlint (MD004).

The rest of the README predominantly uses asterisks; switch these dashes to asterisks for consistency.

-- **[Live Objects Documentation](https://ably.com/docs/liveobjects)** - Complete guide to using Live Objects with code examples and API reference
-- **[Example App](./examples)** - Interactive demo showcasing Live Objects with realtime color voting and collaborative task management
+* **[Live Objects Documentation](https://ably.com/docs/liveobjects)** – Complete guide to using Live Objects with code examples and API reference.
+* **[Example App](./examples)** – Interactive demo showcasing Live Objects with realtime color voting and collaborative task management.

125-128: Use consistent list markers and tighten copy.

Switch to asterisks (MD004) and end sentences with periods; minor copy edits for parallelism.

-The example app demonstrates:
-- **Color Voting**: Realtime voting system with live vote counts synchronized across all devices
-- **Task Management**: Collaborative task management where users can add, edit, and delete tasks that sync in realtime
+The example app demonstrates:
+* **Color Voting**: Realtime voting with live vote counts synchronized across all devices.
+* **Task Management**: Collaborative tasks where users can add, edit, and delete items that sync in realtime.

108-109: Minor grammar tweak for product-name subject agreement.

Treat “Ably Live Objects” as a singular product name.

-Ably Live Objects provide realtime, collaborative data structures that automatically synchronize state across all connected clients. Build interactive applications with shared data that updates instantly across devices.
+Ably Live Objects provides realtime, collaborative data structures that automatically synchronize state across all connected clients. Build interactive applications with shared data that updates instantly across devices.

114-117: Avoid hardcoding a specific version in top-level README, or add a freshness hint.

Versions drift; readers copy-paste stale numbers. Either reference a version catalog variable (for this repo) or link to Maven Central with a short note to use the latest.

Two options:

  • Keep explicit version and add a note:
-    implementation("io.ably:live-objects:1.2.54")
+    implementation("io.ably:live-objects:<latest>")
+    // Check Maven Central for the latest version:
+    // https://central.sonatype.com/artifact/io.ably/live-objects
  • Or, if you prefer using the repo’s version catalog, show the alias:
-    implementation("io.ably:live-objects:1.2.54")
+    implementation(libs.ably.liveobjects)

(Adjust alias to match your libs.versions.toml.)


120-129: Consider adding a minimal usage snippet to bridge from install → first lines of code.

A two- or three-line snippet (e.g., creating a Live Objects client and reading/writing a field) helps readers validate the dependency wiring before jumping to the examples module.

I can draft a minimal “Hello Live Objects” snippet consistent with the public API in this PR if you want it included here.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 494ec11 and 5c41f7a.

📒 Files selected for processing (1)
  • README.md (1 hunks)
🧰 Additional context used
🪛 LanguageTool
README.md

[grammar] ~122-~122: There might be a mistake here.
Context: ...cts with code examples and API reference - Example App - Interactive...

(QB_NEW_EN)


[grammar] ~125-~125: There might be a mistake here.
Context: ...anagement The example app demonstrates: - Color Voting: Realtime voting system w...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
README.md

122-122: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


123-123: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


126-126: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


127-127: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-rest-okhttp
  • GitHub Check: check-liveobjects
  • GitHub Check: check-realtime
  • GitHub Check: check-rest
  • GitHub Check: build
  • GitHub Check: check (19)
  • GitHub Check: check
  • GitHub Check: check (29)
  • GitHub Check: check (29)
  • GitHub Check: check (21)
  • GitHub Check: check (24)

@ttypic ttypic force-pushed the feat/lo-example-app branch from 5c41f7a to 9c5e6e1 Compare August 21, 2025 13:45
@github-actions github-actions bot temporarily deployed to staging/pull/1150/features August 21, 2025 13:46 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1150/javadoc August 21, 2025 13:48 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (12)
README.md (1)

112-118: Live Objects should be a compile-time dependency (implementation), not runtimeOnly.

Consumers will reference Live Objects classes in their code; with runtimeOnly their builds will fail due to missing types on the compile classpath.

Apply this diff:

dependencies {
-    runtimeOnly("io.ably:live-objects:1.2.54")
+    implementation("io.ably:live-objects:1.2.54")
}
.github/workflows/example-app.yml (1)

22-26: Update setup-java to v4 (Node20) — v3 jobs are deprecated and will break.

actions/setup-java@v3 runs on Node16 which GitHub Actions has deprecated. Use v4 to avoid failures.

Apply:

-      - name: Set up the JDK
-        uses: actions/setup-java@v3
+      - name: Set up the JDK
+        uses: actions/setup-java@v4
         with:
           java-version: '17'
           distribution: 'temurin'
examples/README.md (3)

15-15: Align README Android SDK requirements with build.gradle.kts.

The examples module is currently configured with
• compileSdk = 35
• minSdk = 30

Please update the prerequisites in examples/README.md to match:

• At line ~15, change
"Android SDK with API Level 34 or higher"
→ "Android SDK with API Level 35 or higher"

• Add or update a bullet for minimum supported Android version:
"Android device with API Level 30 or higher (Android 11+)"

Alternatively, if you intend to support down to API 21, lower minSdk in examples/build.gradle.kts to 21.


17-23: Add an explicit "do not commit secrets" note near local.properties examples.

Readers may accidentally commit API keys or signing credentials. A visible warning helps prevent leaks.

Proposed additions:

Add your Ably key to the `local.properties` file:

```properties
sdk.dir=/path/to/android/sdk

EXAMPLES_ABLY_KEY=xxxx:yyyyyy

+> Security note: Do not commit local.properties or any API keys to version control. Use test keys for demos and rotate keys if exposed.


---

`83-90`: **Add an explicit "do not commit secrets" note near local.properties examples.**



```diff
2. Update `local.properties` file:

```properties
EXAMPLES_STORE_FILE=/absolute/path/to/release.keystore
EXAMPLES_STORE_PASSWORD=<store-password>
EXAMPLES_KEY_ALIAS=<key-alias>
EXAMPLES_KEY_PASSWORD=<key-password>

+> Security note: Never commit keystores or these credentials. Keep release.keystore out of the repo and store secrets in a secure vault.


</blockquote></details>
<details>
<summary>examples/build.gradle.kts (1)</summary><blockquote>

`84-92`: **Bug: local.properties is read from the module directory (likely missing); read from rootProject instead.**

Using `File(file)` will look under the examples module, not the repository root where local.properties normally resides. This results in ABLY_KEY defaulting to empty and the client failing to authenticate.



```diff
fun getLocalProperty(key: String, file: String = "local.properties"): String? {
    val properties = Properties()
-    val localProperties = File(file)
+    val localProperties = rootProject.file(file)
    if (!localProperties.isFile) return null
    InputStreamReader(FileInputStream(localProperties), Charsets.UTF_8).use { reader ->
        properties.load(reader)
    }
    return properties.getProperty(key)
}
examples/src/main/kotlin/com/ably/example/Utils.kt (4)

27-35: Harden coroutine bridges: avoid force unwraps and resume-after-cancel crashes.

Multiple suspendCancellableCoroutine bridges use result!! and resume/cancel without checking if the continuation is still active. If the enclosing scope is cancelled (e.g., user navigates away), callbacks may fire later and cause IllegalStateException ("Already resumed") or NPEs.

Pattern to apply in each callback:

  • Avoid result!!; handle null by failing the continuation.
  • Guard with continuation.isActive before resuming.
  • Prefer resumeWithException on error.

Apply representative diffs (repeat similar changes in all listed functions):

  1. getRootCoroutines:
  getRootAsync(object : ObjectsCallback<LiveMap> {
    override fun onSuccess(result: LiveMap?) {
-     continuation.resume(result!!)
+     if (continuation.isActive) {
+       result?.let { continuation.resume(it) }
+         ?: continuation.resumeWithException(IllegalStateException("getRootAsync returned null"))
+     }
    }

    override fun onError(exception: AblyException?) {
-     continuation.cancel(exception)
+     if (continuation.isActive) {
+       continuation.resumeWithException(exception ?: IllegalStateException("getRootAsync error"))
+     }
    }
  })

154-163: Bug: Creating LiveObjects before root is available can leak unreferenced objects.

Both observeCounter and observeMap call getOrCreate… even when root is null, which creates a LiveObject that is never inserted into the root map. This can lead to orphaned objects and inconsistent UI.

Guard the calls with a null check so creation happens only after root is available.

Apply these diffs:

  LaunchedEffect(root) {
-   counter = getOrCreateCounter(channel, root, path)
+   if (root != null) {
+     counter = getOrCreateCounter(channel, root, path)
+   }
  }

181-189: Fix change detection: handle insert/update/remove and clear stale reference.

Variable name implies "removed" but you only filter UPDATED; worse, when the key is removed you never null out the local counter.

  DisposableEffect(root) {
    val listener: (LiveMapUpdate) -> Unit = { rootUpdate ->
-     val counterHasBeenRemoved = rootUpdate.update
-       .filter { (_, change) -> change == UPDATED }
-       .any { (keyName) -> keyName == path }
-
-     if (counterHasBeenRemoved) root?.get(path)?.asLiveCounter?.let { counter = it }
+     val keyChanged = rootUpdate.update.any { (keyName, _) -> keyName == path }
+     if (keyChanged) {
+       // Update to the current value in root; becomes null if removed
+       counter = root?.get(path)?.asLiveCounter
+       // counter change triggers re-subscription via DisposableEffect(counter)
+     }
    }

234-238: Bug: Creating LiveObjects before root is available can leak unreferenced objects.

  LaunchedEffect(root) {
-   map = getOrCreateMap(channel, root, path)
+   if (root != null) {
+     map = getOrCreateMap(channel, root, path)
+   }
  }
examples/src/androidTest/kotlin/com/ably/example/ColorVotingScreenTest.kt (2)

34-38: Stop indexing into onAllNodesWithText("Vote")[0]; use a stable testTag and assertIsEnabled().

Index-based matching is brittle and can break on UI reordering. Prefer a tagged node and a direct enable assertion.

Apply this diff:

-    // Find and click the Vote button for Red color
-    // The Vote button should be near the Red text
-    // Use the first Vote button which corresponds to Red (first ColorVoteCard)
-    val redVoteButton = composeTestRule.onAllNodesWithText("Vote")[0]
-
-    composeTestRule.waitUntil(timeoutMillis = 10_000) {
-      SemanticsProperties.Disabled !in redVoteButton.fetchSemanticsNode().config
-    }
+    // Prefer a stable tag from the UI (e.g., Modifier.testTag("vote_red"))
+    val redVoteButton = composeTestRule.onNodeWithTag("vote_red")
+    // Wait for the button to appear (realtime init may delay composition)
+    composeTestRule.waitUntil(timeoutMillis = 10_000) {
+      composeTestRule.onAllNodesWithTag("vote_red").fetchSemanticsNodes().isNotEmpty()
+    }
+    redVoteButton.assertIsEnabled()

If you don’t want to tag the UI, use a structural matcher (fallback, no UI changes needed):

// imports needed:
// import androidx.compose.ui.test.hasAnySibling
// import androidx.compose.ui.test.hasText
// import androidx.compose.ui.test.assertIsEnabled

val redVoteButton = composeTestRule.onNode(hasText("Vote") and hasAnySibling(hasText("Red")))
composeTestRule.waitUntil(10_000) {
  SemanticsProperties.Disabled !in redVoteButton.fetchSemanticsNode().config
}
redVoteButton.assertIsEnabled()

28-30: Assert a relative increment for the Red counter; avoid the absolute "1" check and 1s timeout.

The counter may not start at 0 and network latency can exceed 1s. Capture the initial value for Red, click, then wait until it increments by 1.

Apply this diff:

-    // Get initial red count (should be 0 initially or some value)
-    composeTestRule.onNodeWithText("Red")
+    // Capture initial Red count from a stable tag on the Red counter Text
+    composeTestRule.waitUntil(timeoutMillis = 10_000) {
+      composeTestRule.onAllNodesWithTag("count_red").fetchSemanticsNodes().isNotEmpty()
+    }
+    val initialRed = composeTestRule.onNodeWithTag("count_red")
+      .fetchSemanticsNode()
+      .config[SemanticsProperties.Text]!!.first().text.toString().toInt()
@@
-    redVoteButton.performClick()
+    redVoteButton.performClick()
@@
-    // Wait for the counter to update with 1 second timeout
-    composeTestRule.waitUntil(timeoutMillis = 1_000) {
-      composeTestRule.onAllNodesWithText("1").fetchSemanticsNodes().isNotEmpty()
-    }
-
-    composeTestRule.onNodeWithText("1").assertIsDisplayed()
+    // Wait for the Red counter to increment
+    composeTestRule.waitUntil(timeoutMillis = 10_000) {
+      val updated = composeTestRule.onNodeWithTag("count_red")
+        .fetchSemanticsNode()
+        .config[SemanticsProperties.Text]!!.first().text.toString().toInt()
+      updated == initialRed + 1
+    }
+    composeTestRule.onNodeWithTag("count_red").assertIsDisplayed()

Note: This assumes the UI exposes testTags "vote_red" and "count_red". If you prefer not to modify the UI, we can derive the count via a structural matcher, but tags are far more robust and readable.

Also applies to: 40-47

🧹 Nitpick comments (8)
examples/src/main/kotlin/com/ably/example/ui/theme/Theme.kt (3)

10-11: Cache colorScheme with remember to avoid re-allocations on recomposition

Minor perf/readability win: computing a new ColorScheme instance on every recomposition can cause needless allocations (and can make MaterialTheme think colors changed by identity). Cache it with remember keyed by darkTheme, dynamicColor, and context.

Apply this diff:

 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalContext
@@
-) {
-    val colorScheme = when {
-        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
-            val context = LocalContext.current
-            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
-        }
-
-        darkTheme -> DarkColorScheme
-        else -> LightColorScheme
-    }
+) {
+    val context = LocalContext.current
+    val colorScheme = remember(darkTheme, dynamicColor, context) {
+        when {
+            dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+                if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+            }
+            darkTheme -> DarkColorScheme
+            else -> LightColorScheme
+        }
+    }

Also applies to: 31-39


28-28: Confirm default dynamicColor = true is intentional for brand consistency

On Android 12+, dynamic color will override the brand palette by default. For an example app this is fine, but confirm it’s intentional and won’t confuse users comparing screenshots across OS versions. If you want a stable brand by default, consider defaulting dynamicColor to false in public entry points and enabling it in specific screens/demos.


41-45: Optional: expose shapes if you plan to brand component geometry

If you later introduce custom Shapes, consider threading a shapes parameter through MaterialTheme for consistency across screens. Safe to defer for an example app.

README.md (1)

122-129: Fix Markdown list style inconsistency.

The documentation uses dashes for list items while the rest of the document uses asterisks. This creates style inconsistency and triggers linting warnings.

Apply this diff to maintain consistency:

-- **[Live Objects Documentation](https://ably.com/docs/liveobjects)** - Complete guide to using Live Objects with code examples and API reference
-- **[Example App](./examples)** - Interactive demo showcasing Live Objects with realtime color voting and collaborative task management
+* **[Live Objects Documentation](https://ably.com/docs/liveobjects)** - Complete guide to using Live Objects with code examples and API reference
+* **[Example App](./examples)** - Interactive demo showcasing Live Objects with realtime color voting and collaborative task management

The example app demonstrates:
-- **Color Voting**: Realtime voting system with live vote counts synchronized across all devices
-- **Task Management**: Collaborative task management where users can add, edit, and delete tasks that sync in realtime
+* **Color Voting**: Realtime voting system with live vote counts synchronized across all devices
+* **Task Management**: Collaborative task management where users can add, edit, and delete tasks that sync in realtime
examples/src/androidTest/kotlin/com/ably/example/ColorVotingScreenTest.kt (4)

20-49: Avoid coupling tests to shared realtime state; isolate the channel or inject a fake for hermetic tests.

Using the live “objects-live-counter” channel can make the starting counts non-deterministic and the test flaky across CI runs or parallel devices. Prefer one of:

  • Inject a LiveObjects abstraction and provide a Fake in androidTest.
  • Or make the channel name unique per test run (e.g., suffix with a UUID), and let the UI read it from a test-only BuildConfig or DI.

I can draft a small interface and a test Fake, or wire a unique-channel provider for the sample app’s tests if you want.


22-27: Replace waitForIdle with condition-based waits where possible.

waitForIdle doesn’t synchronize with network/Ably flows. The explicit waits added for button appearance/enabling and count update already cover readiness; you can remove this call after adopting them.


21-21: Rename test for intent clarity (optional).

Consider renaming to incrementsRedCountOnVoteClick for clearer behavior intent.


34-38: UI tagging snippet (outside this file).

Add stable tags in the UI to support the test changes. Example for ColorVoteCard/ColorVotingScreen:

// In ColorVoteCard(colorName: String, onVote: () -> Unit, count: Int, ...)
Button(
  modifier = Modifier.testTag("vote_${colorName.lowercase()}"),
  onClick = onVote,
  enabled = isReadyToVote,
) { Text("Vote") }

Text(
  text = count.toString(),
  modifier = Modifier.testTag("count_${colorName.lowercase()}"),
)

Want me to open a follow-up patch adding these tags across Red/Green/Blue?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5c41f7a and 9c5e6e1.

📒 Files selected for processing (15)
  • .github/workflows/example-app.yml (1 hunks)
  • README.md (1 hunks)
  • examples/README.md (1 hunks)
  • examples/build.gradle.kts (1 hunks)
  • examples/src/androidTest/kotlin/com/ably/example/ColorVotingScreenTest.kt (1 hunks)
  • examples/src/androidTest/kotlin/com/ably/example/MainScreenTest.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/MainActivity.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/Sandbox.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/Utils.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/screen/MainScreen.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/screen/TaskManagementScreen.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/ui/theme/Color.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/ui/theme/Theme.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/ui/theme/Type.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • examples/src/main/kotlin/com/ably/example/Sandbox.kt
  • examples/src/androidTest/kotlin/com/ably/example/MainScreenTest.kt
  • examples/src/main/kotlin/com/ably/example/screen/TaskManagementScreen.kt
  • examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt
  • examples/src/main/kotlin/com/ably/example/MainActivity.kt
  • examples/src/main/kotlin/com/ably/example/ui/theme/Color.kt
  • examples/src/main/kotlin/com/ably/example/ui/theme/Type.kt
  • examples/src/main/kotlin/com/ably/example/screen/MainScreen.kt
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-07T07:19:59.979Z
Learnt from: sacOO7
PR: ably/ably-java#1139
File: live-objects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt:86-92
Timestamp: 2025-08-07T07:19:59.979Z
Learning: In DefaultLiveCounter.notifyUpdated method, sacOO7 prefers to keep the unchecked cast `update as LiveCounterUpdate` without type safety checks, as they are confident the type system guarantees the correct type will always be passed.

Applied to files:

  • examples/src/main/kotlin/com/ably/example/Utils.kt
📚 Learning: 2025-08-14T10:43:48.159Z
Learnt from: sacOO7
PR: ably/ably-java#1148
File: lib/src/main/java/io/ably/lib/objects/ObjectsHelper.java:0-0
Timestamp: 2025-08-14T10:43:48.159Z
Learning: In the ably-java codebase, some "LiveObjectsPlugin" references in log messages, Javadoc, build files, and workflows are intentionally retained due to ongoing internal technical debate about isolating API naming from product naming, as discussed in LODR-033. These are not considered incomplete refactoring work but deliberate exceptions to the broader effort to eliminate "liveobject" keyword usage.

Applied to files:

  • README.md
📚 Learning: 2025-08-07T07:17:33.340Z
Learnt from: sacOO7
PR: ably/ably-java#1137
File: live-objects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/LiveMapManagerTest.kt:6-6
Timestamp: 2025-08-07T07:17:33.340Z
Learning: In the ably-java LiveObjects test code, there are extension properties defined in TestHelpers.kt that provide access to private fields of classes for testing purposes. For example, `internal var DefaultLiveMap.LiveMapManager: LiveMapManager` allows tests to access the private `liveMapManager` field. These extension imports (like `import io.ably.lib.objects.unit.LiveMapManager`) should not be removed as they are necessary for test functionality and are not conflicting imports.

Applied to files:

  • README.md
🧬 Code graph analysis (1)
examples/src/main/kotlin/com/ably/example/Utils.kt (1)
lib/src/main/java/io/ably/lib/types/ChannelOptions.java (1)
  • ChannelOptions (13-131)
🪛 LanguageTool
README.md

[grammar] ~122-~122: There might be a mistake here.
Context: ...cts with code examples and API reference - Example App - Interactive...

(QB_NEW_EN)


[grammar] ~125-~125: There might be a mistake here.
Context: ...anagement The example app demonstrates: - Color Voting: Realtime voting system w...

(QB_NEW_EN)

examples/README.md

[grammar] ~12-~12: There might be a mistake here.
Context: ...Ensure you have the following installed: - [Android Studio](https://developer.androi...

(QB_NEW_EN)


[grammar] ~13-~13: There might be a mistake here.
Context: ...e following installed: - Android Studio (latest stable version) - Java 17 or hi...

(QB_NEW_EN)


[grammar] ~13-~13: There might be a mistake here.
Context: ...roid.com/studio) (latest stable version) - Java 17 or higher - Android SDK with API...

(QB_NEW_EN)


[grammar] ~14-~14: There might be a mistake here.
Context: ...test stable version) - Java 17 or higher - Android SDK with API Level 34 or higher ...

(QB_NEW_EN)


[grammar] ~56-~56: There might be a mistake here.
Context: ...with two tabs: 1. Color Voting Tab: - Vote for Red, Green, or Blue colors -...

(QB_NEW_EN)


[grammar] ~61-~61: There might be a mistake here.
Context: ...all" button 2. Task Management Tab: - Add new tasks using the text input and "...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
README.md

122-122: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


123-123: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


126-126: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


127-127: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

examples/README.md

29-29: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


30-30: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


31-31: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


35-35: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


36-36: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


40-40: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


41-41: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


45-45: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)


46-46: Unordered list indentation
Expected: 0; Actual: 2

(MD007, ul-indent)

🪛 actionlint (1.7.7)
.github/workflows/example-app.yml

22-22: the runner of "actions/setup-java@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: build
  • GitHub Check: check (29)
  • GitHub Check: check (19)
  • GitHub Check: check (24)
  • GitHub Check: check (21)
  • GitHub Check: check-liveobjects
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-rest
  • GitHub Check: check (29)
  • GitHub Check: check-rest-okhttp
  • GitHub Check: check
  • GitHub Check: check-realtime
🔇 Additional comments (4)
examples/src/main/kotlin/com/ably/example/ui/theme/Theme.kt (2)

25-46: LGTM: Idiomatic Material 3 theme wrapper with proper dynamic-color gating

The overall structure is clean and matches current Compose M3 patterns. System dark-mode detection and S+ dynamic color checks are correct, and the fallback schemes are sensible.


13-23: Contrast ratios verified – onColors are sufficient
The six custom brand tones all meet WCAG 2.1 AA (4.5:1) with their optimal “on” fallback (black for the 80-series, white for the 40-series). No override of onPrimary/onSecondary/onTertiary is required:

  • Purple80, PurpleGrey80, Pink80 → contrast vs black ≥ 12.3 → onBlack
  • Purple40, PurpleGrey40, Pink40 → contrast vs white ≥ 6.4 → onWhite

You may still opt to explicitly specify primaryContainer/secondaryContainer (and their on-colors) if you want tighter brand control, but defaults remain WCAG-compliant here.

.github/workflows/example-app.yml (1)

36-42: LGTM!

The Android emulator configuration is properly set up for headless CI execution. The optimizations for no-snapshot-save, no-window, and software graphics acceleration are appropriate for GitHub Actions environment. The error handling with logcat output on test failure will aid in debugging.

examples/build.gradle.kts (1)

58-82: LGTM!

The dependency configuration is well-structured with proper separation of implementation, test, and debug dependencies. The inclusion of both :live-objects and :android modules is appropriate for the example app functionality. Compose BOM usage ensures version consistency across Compose libraries.

ttypic added 2 commits August 22, 2025 00:21
Introduce Compose-based UI tests for MainScreen and ColorVotingScreen. Add GitHub Actions workflow to run tests on pull requests and pushes to `main`.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
README.md (1)

112-118: Use implementation, not runtimeOnly, for Live Objects.

Consumers import Live Objects types at compile time; using runtimeOnly will cause missing symbols on the compile classpath.

Apply this diff:

 ```groovy
 dependencies {
-    runtimeOnly("io.ably:live-objects:1.2.54")
+    implementation("io.ably:live-objects:1.2.54")
 }

Optional (Kotlin DSL):

```diff
+// build.gradle.kts
+dependencies {
+    implementation("io.ably:live-objects:1.2.54")
+}
.github/workflows/example-app.yml (1)

21-26: Update setup-java to v4 (Node20); v3 will break on ubuntu-latest

actions/setup-java@v3 runs on deprecated Node16 and is flagged by actionlint. Switch to v4.

@@
-      - name: Set up the JDK
-        uses: actions/setup-java@v3
+      - name: Set up the JDK
+        uses: actions/setup-java@v4
         with:
           java-version: '17'
           distribution: 'temurin'
🧹 Nitpick comments (8)
README.md (2)

122-127: Unify bullet style with the rest of the README (asterisks instead of dashes).

Earlier lists use asterisks; switch these to keep markdownlint happy and maintain consistency.

- - **[Live Objects Documentation](https://ably.com/docs/liveobjects)** - Complete guide to using Live Objects with code examples and API reference
- - **[Example App](./examples)** - Interactive demo showcasing Live Objects with realtime color voting and collaborative task management
+ * **[Live Objects Documentation](https://ably.com/docs/liveobjects)** - Complete guide to using Live Objects with code examples and API reference
+ * **[Example App](./examples)** - Interactive demo showcasing Live Objects with realtime color voting and collaborative task management
@@
- - **Color Voting**: Realtime voting system with live vote counts synchronized across all devices
- - **Task Management**: Collaborative task management where users can add, edit, and delete tasks that sync in realtime
+ * **Color Voting**: Realtime voting system with live vote counts synchronized across all devices
+ * **Task Management**: Collaborative task management where users can add, edit, and delete tasks that sync in realtime

129-129: Fix the “examples README” path if the module is singular.

Align this link with the actual module directory to avoid a broken navigation path.

-To run the example app, follow the setup instructions in the [examples README](./examples/README.md).
+To run the example app, follow the setup instructions in the [example README](./example/README.md).
examples/src/androidTest/kotlin/com/ably/example/MainScreenTest.kt (2)

20-21: Make tab assertions more robust by matching the Tab role, not just text

This avoids false positives if the same text appears elsewhere and documents the intent more clearly.

@@
-        composeTestRule.onNodeWithText("Color Voting").assertIsDisplayed()
-        composeTestRule.onNodeWithText("Task Management").assertIsDisplayed()
+        composeTestRule.onNode(hasText("Color Voting").and(hasRole(Role.Tab))).assertIsDisplayed()
+        composeTestRule.onNode(hasText("Task Management").and(hasRole(Role.Tab))).assertIsDisplayed()

Add the required imports:

@@
-import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.test.hasRole
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.onNode

17-22: Optional: Add a second test that switches tabs and verifies content changes

Current test checks presence; consider also asserting behavior (e.g., click “Task Management” and verify a screen-specific element). I can draft that if helpful.

.github/workflows/example-app.yml (2)

36-43: Capture artifacts (reports + full logcat) to debug emulator/test flakiness

The current script only prints System.out logs and drops context. Persist full logcat and upload connected test reports so failures are actionable.

@@
-      - uses: reactivecircus/android-emulator-runner@v2
+      - uses: reactivecircus/android-emulator-runner@v2
         with:
           api-level: ${{ matrix.android-api-level }}
           emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
           disable-animations: true
           # Print emulator logs if tests fail
-          script: ./gradlew :examples:connectedAndroidTest || (adb logcat -d System.out:I && exit 1)
+          script: |
+            ./gradlew --stacktrace :examples:connectedAndroidTest \
+            || (mkdir -p build/artifacts && adb logcat -v time -d > build/artifacts/emulator-logcat.txt && exit 1)
+
+      - name: Upload androidTest reports and logs
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: androidTest-api${{ matrix.android-api-level }}
+          path: |
+            examples/build/reports/androidTests/connected
+            examples/build/outputs/androidTest-results/connected
+            build/artifacts/emulator-logcat.txt
+          retention-days: 7

15-16: Optional: Broaden matrix to a modern API level (and/or add a second one)

API 29-only can miss regressions. Consider adding 33/34, or rotating a second level via a nightly workflow.

examples/src/androidTest/kotlin/com/ably/example/ColorVotingScreenTest.kt (2)

37-41: Minor: Assert enabled state after waiting for it

Readability win and clearer failure when the button never becomes enabled.

@@
-    composeTestRule.waitUntil(timeoutMillis = 10_000) {
-      SemanticsProperties.Disabled !in redVoteButton.fetchSemanticsNode().config
-    }
-
-    redVoteButton.performClick()
+    composeTestRule.waitUntil(timeoutMillis = 10_000) {
+      SemanticsProperties.Disabled !in redVoteButton.fetchSemanticsNode().config
+    }
+    redVoteButton.assertIsEnabled()
+    redVoteButton.performClick()

Add the import:

@@
-import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEnabled

33-36: Safer parsing for counter text to avoid brittle failures

If text is missing/non-numeric, fail with a clear message instead of a cryptic exception.

@@
-    val initial = composeTestRule.onNodeWithTag("counter_red")
-      .fetchSemanticsNode()
-      .config[SemanticsProperties.Text].first().text.toInt()
+    val initial = composeTestRule.onNodeWithTag("counter_red")
+      .fetchSemanticsNode()
+      .config[SemanticsProperties.Text]
+      .firstOrNull()?.text?.toIntOrNull()
+      ?: error("counter_red text is missing or not an integer")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9c5e6e1 and f4fed1c.

📒 Files selected for processing (6)
  • .github/workflows/example-app.yml (1 hunks)
  • README.md (1 hunks)
  • examples/build.gradle.kts (1 hunks)
  • examples/src/androidTest/kotlin/com/ably/example/ColorVotingScreenTest.kt (1 hunks)
  • examples/src/androidTest/kotlin/com/ably/example/MainScreenTest.kt (1 hunks)
  • examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt
  • examples/build.gradle.kts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-14T10:43:48.159Z
Learnt from: sacOO7
PR: ably/ably-java#1148
File: lib/src/main/java/io/ably/lib/objects/ObjectsHelper.java:0-0
Timestamp: 2025-08-14T10:43:48.159Z
Learning: In the ably-java codebase, some "LiveObjectsPlugin" references in log messages, Javadoc, build files, and workflows are intentionally retained due to ongoing internal technical debate about isolating API naming from product naming, as discussed in LODR-033. These are not considered incomplete refactoring work but deliberate exceptions to the broader effort to eliminate "liveobject" keyword usage.

Applied to files:

  • README.md
📚 Learning: 2025-08-07T07:17:33.340Z
Learnt from: sacOO7
PR: ably/ably-java#1137
File: live-objects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/LiveMapManagerTest.kt:6-6
Timestamp: 2025-08-07T07:17:33.340Z
Learning: In the ably-java LiveObjects test code, there are extension properties defined in TestHelpers.kt that provide access to private fields of classes for testing purposes. For example, `internal var DefaultLiveMap.LiveMapManager: LiveMapManager` allows tests to access the private `liveMapManager` field. These extension imports (like `import io.ably.lib.objects.unit.LiveMapManager`) should not be removed as they are necessary for test functionality and are not conflicting imports.

Applied to files:

  • README.md
🪛 actionlint (1.7.7)
.github/workflows/example-app.yml

22-22: the runner of "actions/setup-java@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🪛 LanguageTool
README.md

[grammar] ~122-~122: There might be a mistake here.
Context: ...cts with code examples and API reference - Example App - Interactive...

(QB_NEW_EN)


[grammar] ~125-~125: There might be a mistake here.
Context: ...anagement The example app demonstrates: - Color Voting: Realtime voting system w...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
README.md

122-122: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


123-123: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


126-126: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)


127-127: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: check-rest-okhttp
  • GitHub Check: check-liveobjects
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-rest
  • GitHub Check: check-realtime
  • GitHub Check: check (29)
  • GitHub Check: check (19)
  • GitHub Check: check (21)
  • GitHub Check: check (24)
  • GitHub Check: check
  • GitHub Check: check (29)
  • GitHub Check: build
🔇 Additional comments (3)
README.md (2)

106-109: Nice, concise overview of Live Objects.

Clear value proposition; reads well and is consistent with terminology used elsewhere in the README.


122-124: Ignore link correction; examples directory is correct
Verified that the directory is named examples (not example), as shown by the presence of examples/README.md. The existing link ./examples in README.md will correctly resolve. No changes needed.

Likely an incorrect or invalid review comment.

examples/src/androidTest/kotlin/com/ably/example/ColorVotingScreenTest.kt (1)

29-36: LGTM: Stable tags + relative increment = less flaky tests

Good move using test tags (vote_button_red, counter_red) and asserting an increment relative to the initial value instead of hard-coding “1”.

Also applies to: 43-49

Copy link
Collaborator

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@ttypic ttypic merged commit 75541b5 into main Aug 26, 2025
14 checks passed
@ttypic ttypic deleted the feat/lo-example-app branch August 26, 2025 12:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants