Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/example-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Example App

on:
pull_request:
push:
branches:
- main

jobs:
check:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
android-api-level: [ 29 ]

steps:
- name: checkout
uses: actions/checkout@v4

- name: Set up the JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v3

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- 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)
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,30 @@ realtimeClient.connection.on(ConnectionEvent.connected, connectionStateChange ->
```
---

## Live Objects

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.

### Installation

Add the following dependency to your `build.gradle` file:

```groovy
dependencies {
runtimeOnly("io.ably:live-objects:1.2.54")
}
```

### Documentation and Examples

- **[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

To run the example app, follow the setup instructions in the [examples README](./examples/README.md).

## Proxy support

Expand Down
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ plugins {
alias(libs.plugins.maven.publish) apply false
alias(libs.plugins.lombok) apply false
alias(libs.plugins.test.retry) apply false
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}

subprojects {
Expand Down
1 change: 1 addition & 0 deletions examples/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
107 changes: 107 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Example App using Live Objects

This demo app showcases Ably Live Objects functionality with two interactive features:

- **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
- **Task Management**: Collaborative task management where users can add, edit, and delete tasks that sync in real-time across all connected devices

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

## Prerequisites

Ensure you have the following installed:
- [Android Studio](https://developer.android.com/studio) (latest stable version)
- Java 17 or higher
- Android SDK with API Level 34 or higher

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

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

EXAMPLES_ABLY_KEY=xxxx:yyyyyy
```

## Steps to Run the App

1. Open in Android Studio

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

2. Sync Gradle

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

3. Configure an Emulator or Device

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

4. Run the App

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

5. View the App

Once the build is complete, the app will be installed and launched on the selected device or emulator.

## What You'll See

The app opens with two tabs:

1. **Color Voting Tab**:
- Vote for Red, Green, or Blue colors
- See real-time vote counts that update instantly across all devices
- Reset all votes with the "Reset all" button

2. **Task Management Tab**:
- Add new tasks using the text input and "Add Task" button
- Edit existing tasks by clicking the edit icon
- Delete individual tasks or remove all tasks at once
- See the total task count and real-time updates as tasks are modified

To see the real-time synchronization in action, run the app on multiple devices or emulators with the same Ably key.

## Building release APK

This is useful to check ProGuard rules, app size, etc.

1. Create signing keys for the Android app

```shell
keytool -genkey -v -keystore release.keystore \
-storepass <store-password> \
-alias <key-alias> \
-keypass <key-password> \
-keyalg RSA -keysize 2048 -validity 25000 -dname "CN=Ably Example App,OU=Examples,O=Ably,L=London,ST=England,C=GB"
```

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>
```

3. Build release APK

```shell
./gradlew :examples:assembleRelease
```

4. Install to the device

```shell
adb install -r examples/build/outputs/apk/release/examples-release.apk
```

## 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.
92 changes: 92 additions & 0 deletions examples/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import java.io.FileInputStream
import java.io.InputStreamReader
import java.util.Properties

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

android {
namespace = "com.ably.example"
compileSdk = 35

defaultConfig {
applicationId = "com.ably.example"
minSdk = 29
targetSdk = 35
versionCode = 1
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

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

buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true

proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")

val keystorePath = getLocalProperty("EXAMPLES_STORE_FILE")
keystorePath?.let {
signingConfig = signingConfigs.create("release") {
keyAlias = getLocalProperty("EXAMPLES_KEY_ALIAS")
keyPassword = getLocalProperty("EXAMPLES_KEY_PASSWORD")
storeFile = file(it)
storePassword = getLocalProperty("EXAMPLES_STORE_PASSWORD")
}
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
buildConfig = true
}
}

dependencies {
implementation(libs.core.ktx)
implementation(libs.lifecycle.runtime.ktx)
implementation(libs.activity.compose)
implementation(platform(libs.compose.bom))
implementation(libs.ui)
implementation(libs.ui.graphics)
implementation(libs.ui.tooling.preview)
implementation(libs.material3)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)

implementation(project(":live-objects"))
implementation(project(":android"))

implementation(libs.navigation.compose)

testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
androidTestImplementation(platform(libs.compose.bom))
androidTestImplementation(libs.ui.test.junit4)
debugImplementation(libs.ui.tooling)
debugImplementation(libs.ui.test.manifest)
}

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)
}
21 changes: 21 additions & 0 deletions examples/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 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

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

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

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.ably.example

import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class ColorVotingScreenTest {

@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()

@Test
fun incrementRedColor() {
// Navigate to Color Voting tab
composeTestRule.onNodeWithText("Color Voting").performClick()

// Wait for the screen to load
composeTestRule.waitForIdle()

// Find and click the Vote button for Red color
val redVoteButton = composeTestRule.onNodeWithTag("vote_button_red")

// Capture initial count
val initial = composeTestRule.onNodeWithTag("counter_red")
.fetchSemanticsNode()
.config[SemanticsProperties.Text].first().text.toInt()

composeTestRule.waitUntil(timeoutMillis = 10_000) {
SemanticsProperties.Disabled !in redVoteButton.fetchSemanticsNode().config
}

redVoteButton.performClick()

// Wait for the counter to update with 5-seconds timeout
composeTestRule.waitUntil(timeoutMillis = 5_000) {
val updated = composeTestRule.onNodeWithTag("counter_red")
.fetchSemanticsNode()
.config[SemanticsProperties.Text].first().text.toInt()
updated == initial + 1
}
}
}
23 changes: 23 additions & 0 deletions examples/src/androidTest/kotlin/com/ably/example/MainScreenTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.ably.example

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MainScreenTest {

@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()

@Test
fun tabsAreDisplayed() {
// Verify both tabs are displayed
composeTestRule.onNodeWithText("Color Voting").assertIsDisplayed()
composeTestRule.onNodeWithText("Task Management").assertIsDisplayed()
}
}
Loading
Loading