Skip to content

Commit 75541b5

Browse files
authored
Merge pull request #1150 from ably/feat/lo-example-app
feat[live-objects]: add `example` module showcasing LiveObjects features
2 parents b12fabb + f4fed1c commit 75541b5

31 files changed

Lines changed: 1468 additions & 1 deletion

.github/workflows/example-app.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Example App
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
jobs:
10+
check:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
android-api-level: [ 29 ]
16+
17+
steps:
18+
- name: checkout
19+
uses: actions/checkout@v4
20+
21+
- name: Set up the JDK
22+
uses: actions/setup-java@v3
23+
with:
24+
java-version: '17'
25+
distribution: 'temurin'
26+
27+
- name: Set up Gradle
28+
uses: gradle/actions/setup-gradle@v3
29+
30+
- name: Enable KVM
31+
run: |
32+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
33+
sudo udevadm control --reload-rules
34+
sudo udevadm trigger --name-match=kvm
35+
36+
- uses: reactivecircus/android-emulator-runner@v2
37+
with:
38+
api-level: ${{ matrix.android-api-level }}
39+
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
40+
disable-animations: true
41+
# Print emulator logs if tests fail
42+
script: ./gradlew :examples:connectedAndroidTest || (adb logcat -d System.out:I && exit 1)

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,30 @@ realtimeClient.connection.on(ConnectionEvent.connected, connectionStateChange ->
103103
```
104104
---
105105

106+
## Live Objects
107+
108+
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.
109+
110+
### Installation
111+
112+
Add the following dependency to your `build.gradle` file:
113+
114+
```groovy
115+
dependencies {
116+
runtimeOnly("io.ably:live-objects:1.2.54")
117+
}
118+
```
119+
120+
### Documentation and Examples
121+
122+
- **[Live Objects Documentation](https://ably.com/docs/liveobjects)** - Complete guide to using Live Objects with code examples and API reference
123+
- **[Example App](./examples)** - Interactive demo showcasing Live Objects with realtime color voting and collaborative task management
124+
125+
The example app demonstrates:
126+
- **Color Voting**: Realtime voting system with live vote counts synchronized across all devices
127+
- **Task Management**: Collaborative task management where users can add, edit, and delete tasks that sync in realtime
128+
129+
To run the example app, follow the setup instructions in the [examples README](./examples/README.md).
106130

107131
## Proxy support
108132

build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ plugins {
88
alias(libs.plugins.maven.publish) apply false
99
alias(libs.plugins.lombok) apply false
1010
alias(libs.plugins.test.retry) apply false
11+
alias(libs.plugins.android.application) apply false
12+
alias(libs.plugins.kotlin.android) apply false
13+
alias(libs.plugins.kotlin.compose) apply false
1114
}
1215

1316
subprojects {

examples/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

examples/README.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Example App using Live Objects
2+
3+
This demo app showcases Ably Live Objects functionality with two interactive features:
4+
5+
- **Color Voting**: Real-time voting system where users can vote for their favorite color (Red, Green, Blue) and see live vote counts synchronized across all devices
6+
- **Task Management**: Collaborative task management where users can add, edit, and delete tasks that sync in real-time across all connected devices
7+
8+
Follow the steps below to get started with the Live Objects demo app
9+
10+
## Prerequisites
11+
12+
Ensure you have the following installed:
13+
- [Android Studio](https://developer.android.com/studio) (latest stable version)
14+
- Java 17 or higher
15+
- Android SDK with API Level 34 or higher
16+
17+
Add your Ably key to the `local.properties` file:
18+
19+
```properties
20+
sdk.dir=/path/to/android/sdk
21+
22+
EXAMPLES_ABLY_KEY=xxxx:yyyyyy
23+
```
24+
25+
## Steps to Run the App
26+
27+
1. Open in Android Studio
28+
29+
- Open Android Studio.
30+
- Select File > Open and navigate to the cloned repository.
31+
- Open the project.
32+
33+
2. Sync Gradle
34+
35+
- Wait for Gradle to sync automatically.
36+
- If it doesn’t, click on Sync Project with Gradle Files in the toolbar.
37+
38+
3. Configure an Emulator or Device
39+
40+
- Set up an emulator or connect a physical Android device.
41+
- Ensure the device is configured with at least Android 5.0 (API 21).
42+
43+
4. Run the App
44+
45+
- Select your emulator or connected device in the device selector dropdown.
46+
- Click on the Run button ▶️ in the toolbar or press Shift + F10.
47+
48+
5. View the App
49+
50+
Once the build is complete, the app will be installed and launched on the selected device or emulator.
51+
52+
## What You'll See
53+
54+
The app opens with two tabs:
55+
56+
1. **Color Voting Tab**:
57+
- Vote for Red, Green, or Blue colors
58+
- See real-time vote counts that update instantly across all devices
59+
- Reset all votes with the "Reset all" button
60+
61+
2. **Task Management Tab**:
62+
- Add new tasks using the text input and "Add Task" button
63+
- Edit existing tasks by clicking the edit icon
64+
- Delete individual tasks or remove all tasks at once
65+
- See the total task count and real-time updates as tasks are modified
66+
67+
To see the real-time synchronization in action, run the app on multiple devices or emulators with the same Ably key.
68+
69+
## Building release APK
70+
71+
This is useful to check ProGuard rules, app size, etc.
72+
73+
1. Create signing keys for the Android app
74+
75+
```shell
76+
keytool -genkey -v -keystore release.keystore \
77+
-storepass <store-password> \
78+
-alias <key-alias> \
79+
-keypass <key-password> \
80+
-keyalg RSA -keysize 2048 -validity 25000 -dname "CN=Ably Example App,OU=Examples,O=Ably,L=London,ST=England,C=GB"
81+
```
82+
83+
2. Update `local.properties` file:
84+
85+
```properties
86+
EXAMPLES_STORE_FILE=/absolute/path/to/release.keystore
87+
EXAMPLES_STORE_PASSWORD=<store-password>
88+
EXAMPLES_KEY_ALIAS=<key-alias>
89+
EXAMPLES_KEY_PASSWORD=<key-password>
90+
```
91+
92+
3. Build release APK
93+
94+
```shell
95+
./gradlew :examples:assembleRelease
96+
```
97+
98+
4. Install to the device
99+
100+
```shell
101+
adb install -r examples/build/outputs/apk/release/examples-release.apk
102+
```
103+
104+
## Troubleshooting
105+
106+
- SDK Not Found: Install missing SDK versions from File > Settings > Appearance & Behavior > System Settings > Android SDK.
107+
- Build Failures: Check the error logs and resolve dependencies or configuration issues.

examples/build.gradle.kts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import java.io.FileInputStream
2+
import java.io.InputStreamReader
3+
import java.util.Properties
4+
5+
plugins {
6+
alias(libs.plugins.android.application)
7+
alias(libs.plugins.kotlin.android)
8+
alias(libs.plugins.kotlin.compose)
9+
}
10+
11+
android {
12+
namespace = "com.ably.example"
13+
compileSdk = 35
14+
15+
defaultConfig {
16+
applicationId = "com.ably.example"
17+
minSdk = 29
18+
targetSdk = 35
19+
versionCode = 1
20+
versionName = "1.0"
21+
22+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
23+
24+
buildConfigField("String", "ABLY_KEY", "\"${getLocalProperty("EXAMPLES_ABLY_KEY") ?: ""}\"")
25+
}
26+
27+
buildTypes {
28+
release {
29+
isMinifyEnabled = true
30+
isShrinkResources = true
31+
32+
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
33+
34+
val keystorePath = getLocalProperty("EXAMPLES_STORE_FILE")
35+
keystorePath?.let {
36+
signingConfig = signingConfigs.create("release") {
37+
keyAlias = getLocalProperty("EXAMPLES_KEY_ALIAS")
38+
keyPassword = getLocalProperty("EXAMPLES_KEY_PASSWORD")
39+
storeFile = file(it)
40+
storePassword = getLocalProperty("EXAMPLES_STORE_PASSWORD")
41+
}
42+
}
43+
}
44+
}
45+
compileOptions {
46+
sourceCompatibility = JavaVersion.VERSION_11
47+
targetCompatibility = JavaVersion.VERSION_11
48+
}
49+
kotlinOptions {
50+
jvmTarget = "11"
51+
}
52+
buildFeatures {
53+
compose = true
54+
buildConfig = true
55+
}
56+
}
57+
58+
dependencies {
59+
implementation(libs.core.ktx)
60+
implementation(libs.lifecycle.runtime.ktx)
61+
implementation(libs.activity.compose)
62+
implementation(platform(libs.compose.bom))
63+
implementation(libs.ui)
64+
implementation(libs.ui.graphics)
65+
implementation(libs.ui.tooling.preview)
66+
implementation(libs.material3)
67+
implementation(libs.ktor.client.core)
68+
implementation(libs.ktor.client.cio)
69+
70+
implementation(project(":live-objects"))
71+
implementation(project(":android"))
72+
73+
implementation(libs.navigation.compose)
74+
75+
testImplementation(libs.junit)
76+
androidTestImplementation(libs.ext.junit)
77+
androidTestImplementation(libs.espresso.core)
78+
androidTestImplementation(platform(libs.compose.bom))
79+
androidTestImplementation(libs.ui.test.junit4)
80+
debugImplementation(libs.ui.tooling)
81+
debugImplementation(libs.ui.test.manifest)
82+
}
83+
84+
fun getLocalProperty(key: String, file: String = "local.properties"): String? {
85+
val properties = Properties()
86+
val localProperties = File(file)
87+
if (!localProperties.isFile) return null
88+
InputStreamReader(FileInputStream(localProperties), Charsets.UTF_8).use { reader ->
89+
properties.load(reader)
90+
}
91+
return properties.getProperty(key)
92+
}

examples/proguard-rules.pro

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.ably.example
2+
3+
import androidx.compose.ui.semantics.SemanticsProperties
4+
import androidx.compose.ui.test.assertIsDisplayed
5+
import androidx.compose.ui.test.junit4.createAndroidComposeRule
6+
import androidx.compose.ui.test.onAllNodesWithText
7+
import androidx.compose.ui.test.onNodeWithTag
8+
import androidx.compose.ui.test.onNodeWithText
9+
import androidx.compose.ui.test.performClick
10+
import androidx.test.ext.junit.runners.AndroidJUnit4
11+
import org.junit.Rule
12+
import org.junit.Test
13+
import org.junit.runner.RunWith
14+
15+
@RunWith(AndroidJUnit4::class)
16+
class ColorVotingScreenTest {
17+
18+
@get:Rule
19+
val composeTestRule = createAndroidComposeRule<MainActivity>()
20+
21+
@Test
22+
fun incrementRedColor() {
23+
// Navigate to Color Voting tab
24+
composeTestRule.onNodeWithText("Color Voting").performClick()
25+
26+
// Wait for the screen to load
27+
composeTestRule.waitForIdle()
28+
29+
// Find and click the Vote button for Red color
30+
val redVoteButton = composeTestRule.onNodeWithTag("vote_button_red")
31+
32+
// Capture initial count
33+
val initial = composeTestRule.onNodeWithTag("counter_red")
34+
.fetchSemanticsNode()
35+
.config[SemanticsProperties.Text].first().text.toInt()
36+
37+
composeTestRule.waitUntil(timeoutMillis = 10_000) {
38+
SemanticsProperties.Disabled !in redVoteButton.fetchSemanticsNode().config
39+
}
40+
41+
redVoteButton.performClick()
42+
43+
// Wait for the counter to update with 5-seconds timeout
44+
composeTestRule.waitUntil(timeoutMillis = 5_000) {
45+
val updated = composeTestRule.onNodeWithTag("counter_red")
46+
.fetchSemanticsNode()
47+
.config[SemanticsProperties.Text].first().text.toInt()
48+
updated == initial + 1
49+
}
50+
}
51+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.ably.example
2+
3+
import androidx.compose.ui.test.assertIsDisplayed
4+
import androidx.compose.ui.test.junit4.createAndroidComposeRule
5+
import androidx.compose.ui.test.onNodeWithText
6+
import androidx.test.ext.junit.runners.AndroidJUnit4
7+
import org.junit.Rule
8+
import org.junit.Test
9+
import org.junit.runner.RunWith
10+
11+
@RunWith(AndroidJUnit4::class)
12+
class MainScreenTest {
13+
14+
@get:Rule
15+
val composeTestRule = createAndroidComposeRule<MainActivity>()
16+
17+
@Test
18+
fun tabsAreDisplayed() {
19+
// Verify both tabs are displayed
20+
composeTestRule.onNodeWithText("Color Voting").assertIsDisplayed()
21+
composeTestRule.onNodeWithText("Task Management").assertIsDisplayed()
22+
}
23+
}

0 commit comments

Comments
 (0)