> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gorules.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Android Rules Engine

> Integrate GoRules into your Android application.

Install the ZEN Engine and evaluate your first decision in Android.

## Installation

```kotlin theme={null}
dependencies {
    implementation("io.gorules:zen-engine-kotlin-android:2.0.0")
}
```

## Basic usage

```kotlin theme={null}
import io.gorules.zen_engine.kotlin_android.ZenEngine
import io.gorules.zen_engine.kotlin_android.JsonBuffer
import kotlinx.coroutines.runBlocking

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        runBlocking {
            val ruleJson = assets.open("rules/pricing.json").readBytes()

            ZenEngine(null, null).use { engine ->
                val decision = engine.createDecision(JsonBuffer(ruleJson))

                val input = JsonBuffer("""
                    {
                        "customer": { "tier": "gold", "yearsActive": 3 },
                        "order": { "subtotal": 150, "items": 5 }
                    }
                """)

                val response = decision.evaluate(input, null)

                Log.d("ZenEngine", response.result.toString())
                // => {"discount":0.15,"freeShipping":true}
            }
        }
    }
}
```

## Loader

`ZenEngine` accepts an optional `ZenLoader` that serves decisions by key. Use `Static`, `Filesystem`, or `Zip` for common backends, or `Callback` for custom loading logic.

### Static

Register decisions in memory. Use this for rules bundled in your app's assets:

```kotlin theme={null}
import io.gorules.zen_engine.kotlin_android.ZenEngine
import io.gorules.zen_engine.kotlin_android.ZenLoader
import io.gorules.zen_engine.kotlin_android.JsonBuffer

fun createEngine(context: Context): ZenEngine {
    val pricing = context.assets.open("rules/pricing.json").readBytes()

    val loader = ZenLoader.Static(mapOf("pricing.json" to JsonBuffer(pricing)))
    return ZenEngine(loader, null)
}
```

### File system

Load decisions from files under a root directory, such as internal storage. Keys resolve to paths relative to the root:

```kotlin theme={null}
import io.gorules.zen_engine.kotlin_android.ZenEngine
import io.gorules.zen_engine.kotlin_android.ZenLoader
import java.io.File

fun createEngine(context: Context): ZenEngine {
    val loader = ZenLoader.Filesystem(File(context.filesDir, "rules").absolutePath)
    return ZenEngine(loader, null)
}
```

### Zip archive

Pass the bytes of a zip archive. Every `.json` entry becomes a decision keyed by its path within the archive. This pairs naturally with BRMS release ZIPs - download the release once and hand the bytes to the engine:

```kotlin theme={null}
import io.gorules.zen_engine.kotlin_android.ZenEngine
import io.gorules.zen_engine.kotlin_android.ZenLoader
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File

val client = OkHttpClient()

fun createEngine(context: Context): ZenEngine {
    val cacheFile = File(context.cacheDir, "decisions.zip")

    val zipBytes = if (cacheFile.exists()) {
        cacheFile.readBytes()
    } else {
        val request = Request.Builder()
            .url("https://api.example.com/decisions.zip")
            .build()
        val bytes = client.newCall(request).execute().body!!.bytes()
        cacheFile.parentFile?.mkdirs()
        cacheFile.writeBytes(bytes)
        bytes
    }

    return ZenEngine(ZenLoader.Zip(zipBytes), null)
}
```

### Custom loader

For any other backend, implement `ZenDecisionLoaderCallback` and wrap it in `ZenLoader.Callback`:

```kotlin theme={null}
import io.gorules.zen_engine.kotlin_android.ZenEngine
import io.gorules.zen_engine.kotlin_android.ZenLoader
import io.gorules.zen_engine.kotlin_android.ZenDecisionLoaderCallback
import io.gorules.zen_engine.kotlin_android.JsonBuffer
import com.google.firebase.remoteconfig.FirebaseRemoteConfig

val remoteConfig = FirebaseRemoteConfig.getInstance()

fun createEngine(): ZenEngine {
    val callback = object : ZenDecisionLoaderCallback {
        override suspend fun load(key: String): JsonBuffer? {
            val json = remoteConfig.getString("decision_$key")
            return if (json.isEmpty()) null else JsonBuffer(json)
        }
    }
    return ZenEngine(ZenLoader.Callback(callback), null)
}
```

Returning `null` from the callback reports the key as not found.

## Batch evaluation

Evaluate many requests in one call. Each result reports its own success or failure, so one bad input never fails the batch:

```kotlin theme={null}
val results = engine.evaluateBatch(
    listOf(
        ZenBatchRequest("pricing.json", JsonBuffer("""{ "amount": 100 }""")),
        ZenBatchRequest("pricing.json", JsonBuffer("""{ "amount": 250 }"""))
    ),
    null
)

for (result in results) {
    if (result.success) {
        println(result.data?.result)
    } else {
        println("Evaluation failed: ${result.error}")
    }
}
```

## Coroutines

Evaluation functions are `suspend` functions, integrating natively with Kotlin coroutines:

```kotlin theme={null}
import kotlinx.coroutines.*

lifecycleScope.launch {
    val results = inputs.map { input ->
        async(Dispatchers.Default) {
            decision.evaluate(JsonBuffer(input), null)
        }
    }.awaitAll()

    results.forEach { Log.d("ZenEngine", it.result.toString()) }
}
```

## Error handling

```kotlin theme={null}
import io.gorules.zen_engine.kotlin_android.ZenException

try {
    val response = decision.evaluate(input, null)
    Log.d("ZenEngine", response.result.toString())
} catch (e: ZenException) {
    Log.e("ZenEngine", "Evaluation failed: ${e.message}")
}
```

## Tracing

Enable tracing to inspect decision execution:

```kotlin theme={null}
import io.gorules.zen_engine.kotlin_android.ZenEvaluateOptions

val options = ZenEvaluateOptions(trace = true, maxDepth = null)

val response = decision.evaluate(input, options)

Log.d("ZenEngine", "Trace: ${response.trace}")
Log.d("ZenEngine", "Performance: ${response.performance}")
```

## Expression utilities

Evaluate ZEN expressions outside of a decision context:

```kotlin theme={null}
import io.gorules.zen_engine.kotlin_android.evaluateExpression
import io.gorules.zen_engine.kotlin_android.evaluateUnaryExpression
import io.gorules.zen_engine.kotlin_android.JsonBuffer

// Standard expressions
val result = evaluateExpression("a + b", JsonBuffer("""{ "a": 5, "b": 3 }"""))
// => 8

val total = evaluateExpression("sum(items)", JsonBuffer("""{ "items": [1, 2, 3, 4] }"""))
// => 10

// Unary expressions (comparison against $)
val isValid = evaluateUnaryExpression(">= 5", JsonBuffer("""{ "$": 10 }"""))
// => true

val inList = evaluateUnaryExpression("'US', 'CA', 'MX'", JsonBuffer("""{ "$": "US" }"""))
// => true
```

## Performance note

<Note>
  The Android bindings use JNA (Java Native Access) for interoperability with the native Rust engine. This introduces some overhead compared to native Rust or direct bindings. We plan to revisit this when the FFM (Foreign Function & Memory) API becomes more widely adopted.
</Note>

## Best practices

**Use `.use {}` for resource management.** `ZenEngine` implements `AutoCloseable` to release native resources.

```kotlin theme={null}
ZenEngine(null, null).use { engine ->
    // use engine
}
```

**Initialize the engine once.** Create a single `ZenEngine` instance at application startup and reuse it for all evaluations.

**Cache decisions persistently.** Use internal storage or SharedPreferences to cache downloaded decisions for offline use.

**Evaluate on background threads.** Use `Dispatchers.Default` or `Dispatchers.IO` to avoid blocking the main thread.

**Bundle fallback decisions.** Include decisions in assets as fallback for first launch or network failures.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.