Skip to content

R8 code shrinking now works with the Appwrite Android SDK_

Appwrite Android SDK 29.0.0 works with R8 full mode, fixing queries, Realtime, and saved sessions in release builds ahead of Google Play's 2027 DEX rule.

Turn on R8 in a release build with an older version of the Appwrite Android SDK and nothing fails at compile time. The build succeeds, the app launches, and then the problems start. Database queries reach the server as {}. Realtime subscriptions connect but never deliver an event. Users who install your next release find themselves signed out.

Until now, the workaround was to leave R8 off or write your own keep rules for the SDK. That stops being an option in February 2027, when Google Play starts requiring a minimum level of code optimization for apps with more than 10 MB of DEX code.

Appwrite Android SDK 29.0.0 works with R8 full mode. Queries, operators, Realtime, and saved sessions behave the same in a shrunk release build as they do in debug. Here's what broke, why, and what to change in your own app.

What changed in Android SDK 29.0.0

Version 29.0.0 of the Android SDK makes these R8 changes:

  • Queries and operators serialize to correct JSON under R8 instead of {}.
  • Realtime delivers events in shrunk builds, and Realtime errors keep their message and code.
  • Session cookies are saved with fixed key names, so a new build of your app can still read them.
  • The end-to-end test suite now also runs against R8-shrunk code.

The SDK ships consumer R8 rules, and the only code you might need to touch is your own model classes, covered later in this post.

Google Play's DEX optimization requirement

Google announced in August 2026 that, starting in February 2027, apps on Google Play must be "optimized with a minimum of 25% coverage across optimization, shrinking, and obfuscation." Apps and games that miss the threshold "may see reduced app visibility and publishing capabilities on Google Play."

Google's DEX code optimization page sets the scope. Android vitals calculates these metrics for every app, but only alerts apps whose bundle contains at least 10 MB of uncompressed DEX code, or 50 MB for games. Any shrinker counts. For most Android teams, that means R8.

Two details make this a library problem as well as an app problem:

  • Play reads the scores from your bundle. For apps built with AGP 8.10 or later, the percentages come from the r8.json file R8 embeds in the app bundle. They describe the final build, every dependency included.
  • Library keep rules become your keep rules. Consumer rules shipped inside a library's AAR merge into your app's R8 configuration. A broad keep rule in a dependency stops R8 from renaming or optimizing that code, and the cost shows up in your app's scores.

An SDK that breaks under R8 leaves you two workarounds, and both cost you. Turn R8 off and you miss the threshold outright. Keep the whole SDK with a blanket rule and none of its code gets renamed or optimized, which drags your scores down.

Why R8 full mode broke the Android SDK

R8 full mode has been the default since Android Gradle Plugin 8.0. It makes stricter assumptions about reflection than ProGuard compatibility mode. If R8 can't see your code use a class, constructor, or field, it's free to remove, rename, or restructure it.

Earlier versions of the SDK relied on Gson reflection in a few internal paths. Gson reads and writes fields by name at runtime, which R8 can't trace. Each of the three bugs below comes from that gap, and each matches a failure mode in Gson's troubleshooting guide.

Queries and operators serialized to {}

The SDK turns Query and Operator values into JSON before they go to the API. It did this by handing the object to Gson, which reflected over its method, attribute, and values fields. Under R8, Gson found nothing to serialize, and every query reached Appwrite as an empty object.

That affected every list call that took queries: filters, pagination, ordering, and the nested Query.and, Query.or, and Query.elemMatch helpers. Operators for atomic updates had the same problem.

Realtime connected but delivered nothing

Realtime parses each WebSocket message into a RealtimeResponse before routing it. That class was only ever created by Gson, never by SDK code, so R8 full mode treated it as never instantiated and made it abstract. Every incoming message threw Abstract classes can't be instantiated. The socket stayed open and subscription callbacks never fired.

Realtime errors failed more quietly. The SDK cast error payloads into an AppwriteException through Gson, and under R8 the exception arrived with a null message and code. Your error handler ran, but it had nothing to log.

Saved sessions broke after an app update

The Android SDK persists session cookies in SharedPreferences so users stay signed in across launches. It wrote them by serializing an internal cookie class with Gson. Once R8 renamed that class's fields, cookies were saved with obfuscated keys like {"c":…,"d":…} instead of name, value, and domain.

Obfuscated names aren't stable across builds. When you shipped a new version and the mapping changed, the new build couldn't read the cookies the old one had saved, and users lost their session on update. This is the hardest of the three to catch, because it only shows up when one release build reads data written by another.

How 29.0.0 fixes it

The SDK no longer lets Gson reflect over its own classes. It converts queries, operators, Realtime messages, and session cookies to and from plain maps whose keys are string literals in the code. R8 can rename the fields, and the JSON stays the same.

The cookie keys match what earlier SDK versions wrote in builds without R8, so sessions saved by those builds still load after you upgrade. Query and Operator also expose the conversion as public toMap() and toJson() methods, which you can use to log a query.

These bugs went unnoticed because the SDK's end-to-end tests only ran against unshrunk code. The suite now also runs against an R8-shrunk build of the SDK. Pointed at the old code, it fails the same way apps did, so a regression like this one now fails CI before it ships.

How to use the Appwrite Android SDK with R8

Update to 29.0.0

Update the dependency in your app-level build.gradle.kts:

Kotlin
implementation("io.appwrite:sdk-for-android:29.0.0")

Version 29.0.0 raises the minimum SDK from 23 to 24 (Android 7.0). If your app still supports Android 6.0, raise minSdk before you upgrade. The 29.0.0 release notes list every other change, including fixes for Android 7.x.

Turn on R8 in your release build

On AGP 9.3 and later, use the optimization block:

Kotlin
android {
    buildTypes {
        release {
            optimization {
                enable = true
            }
        }
    }
}

On earlier AGP versions, set isMinifyEnabled and isShrinkResources:

Kotlin
android {
    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro",
            )
        }
    }
}

You don't need to add any Appwrite rules to proguard-rules.pro.

Annotate your own model classes with @Keep

The SDK's own classes no longer depend on reflection, but yours might. When you pass a class as the nestedType of a document, row, or preferences call, or as the payloadType of a Realtime subscription, the SDK hands it to Gson to fill from JSON. R8 can't see that, so annotate those classes with @Keep:

Kotlin
import androidx.annotation.Keep

@Keep
data class Note(val title: String, val body: String)

val rows = tablesDB.listRows(
    databaseId = "<DATABASE_ID>",
    tableId = "<TABLE_ID>",
    queries = listOf(Query.equal("pinned", true)),
    nestedType = Note::class.java,
)
rows.rows.forEach { Log.d("Notes", it.data.title) }

val subscription = realtime.subscribe(
    Channel.tablesdb("<DATABASE_ID>").table("<TABLE_ID>").row(),
    payloadType = Note::class.java,
) { event ->
    Log.d("Notes", event.payload.title)
}

Without @Keep, the same R8 behavior that broke the SDK breaks your models: fields come back empty, or Gson throws when it tries to create the class. Limit @Keep to the classes the SDK fills from JSON so R8 can still optimize the rest of your app. Calls that omit nestedType return Map<String, Any> and need no annotation.

Check your R8 scores

Read the numbers Play reads by extracting r8.json from your release bundle:

Bash
unzip -p app-release.aab BUNDLE-METADATA/com.android.tools/r8.json

Play Console shows the same percentages for each bundle you upload, in the app bundle explorer. If a score is lower than you expect, the R8 Configuration Analyzer points to the keep rules responsible. Its scores reflect your keep rules before optimization, so they can differ from r8.json.

Then test the release build itself, not only debug. Sign in, install a second release build over the first, and confirm the session survives. Tests against a debug build never cover that path.

Getting started with R8 and the Appwrite Android SDK

R8 is moving from optional to expected on Google Play. With 29.0.0, the Appwrite Android SDK works in R8 full mode, so you can turn R8 on without writing any rules for it.

Create a project on Appwrite Cloud, add the SDK, and ship your next release with R8 on. Self-hosted Appwrite works the same way.

Read next

Ready to build?_