> ## 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:0.4.7")
}
```

## Basic usage

```kotlin theme={null}
import io.gorules.zen_engine.kotlin.ZenEngine
import io.gorules.zen_engine.kotlin.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

The loader pattern enables dynamic decision loading from any storage backend. Implement the `ZenDecisionLoaderCallback` interface with a `suspend fun load(key: String): JsonBuffer?` method. Use `ConcurrentHashMap` to cache decisions for optimal performance.

### Assets

```kotlin theme={null}
import io.gorules.zen_engine.kotlin.ZenEngine
import io.gorules.zen_engine.kotlin.ZenDecisionLoaderCallback
import io.gorules.zen_engine.kotlin.JsonBuffer
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.runBlocking

val cache = ConcurrentHashMap<String, ByteArray>()

fun createEngine(context: Context): ZenEngine {
    val loader = object : ZenDecisionLoaderCallback {
        override suspend fun load(key: String): JsonBuffer? {
            val bytes = cache.computeIfAbsent(key) {
                context.assets.open("rules/$key").readBytes()
            }
            return JsonBuffer(bytes)
        }
    }
    return ZenEngine(loader, null)
}
```

### Internal storage

```kotlin theme={null}
import io.gorules.zen_engine.kotlin.ZenEngine
import io.gorules.zen_engine.kotlin.ZenDecisionLoaderCallback
import io.gorules.zen_engine.kotlin.JsonBuffer
import java.io.File
import java.util.concurrent.ConcurrentHashMap

val cache = ConcurrentHashMap<String, ByteArray>()

fun createEngine(context: Context): ZenEngine {
    val loader = object : ZenDecisionLoaderCallback {
        override suspend fun load(key: String): JsonBuffer? {
            val bytes = cache.computeIfAbsent(key) {
                File(context.filesDir, "rules/$key").readBytes()
            }
            return JsonBuffer(bytes)
        }
    }
    return ZenEngine(loader, null)
}
```

### Firebase Remote Config

```kotlin theme={null}
import io.gorules.zen_engine.kotlin.ZenEngine
import io.gorules.zen_engine.kotlin.ZenDecisionLoaderCallback
import io.gorules.zen_engine.kotlin.JsonBuffer
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import java.util.concurrent.ConcurrentHashMap

val remoteConfig = FirebaseRemoteConfig.getInstance()
val cache = ConcurrentHashMap<String, ByteArray>()

fun createEngine(): ZenEngine {
    val loader = object : ZenDecisionLoaderCallback {
        override suspend fun load(key: String): JsonBuffer? {
            val bytes = cache.computeIfAbsent(key) {
                remoteConfig.getString("decision_$key").toByteArray()
            }
            return JsonBuffer(bytes)
        }
    }
    return ZenEngine(loader, null)
}
```

### Remote URL with caching

```kotlin theme={null}
import io.gorules.zen_engine.kotlin.ZenEngine
import io.gorules.zen_engine.kotlin.ZenDecisionLoaderCallback
import io.gorules.zen_engine.kotlin.JsonBuffer
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.util.concurrent.ConcurrentHashMap

val client = OkHttpClient()
val cache = ConcurrentHashMap<String, ByteArray>()

fun createEngine(context: Context): ZenEngine {
    val loader = object : ZenDecisionLoaderCallback {
        override suspend fun load(key: String): JsonBuffer? {
            val bytes = cache.computeIfAbsent(key) {
                val cacheFile = File(context.cacheDir, "rules/$key")

                if (cacheFile.exists()) {
                    cacheFile.readBytes()
                } else {
                    val request = Request.Builder()
                        .url("https://api.example.com/rules/$key")
                        .build()
                    val responseBytes = client.newCall(request).execute().body!!.bytes()
                    cacheFile.parentFile?.mkdirs()
                    cacheFile.writeBytes(responseBytes)
                    responseBytes
                }
            }
            return JsonBuffer(bytes)
        }
    }
    return ZenEngine(loader, null)
}
```

### Remote URL with zip

```kotlin theme={null}
import io.gorules.zen_engine.kotlin.ZenEngine
import io.gorules.zen_engine.kotlin.ZenDecisionLoaderCallback
import io.gorules.zen_engine.kotlin.JsonBuffer
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.util.zip.ZipInputStream
import java.io.ByteArrayInputStream

val client = OkHttpClient()
val rules = mutableMapOf<String, ByteArray>()

fun initializeRules(context: Context) {
    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 responseBytes = client.newCall(request).execute().body!!.bytes()
        cacheFile.parentFile?.mkdirs()
        cacheFile.writeBytes(responseBytes)
        responseBytes
    }

    // Extract all decisions from zip
    ZipInputStream(ByteArrayInputStream(zipBytes)).use { zis ->
        var entry = zis.nextEntry
        while (entry != null) {
            if (!entry.isDirectory) {
                rules[entry.name] = zis.readBytes()
            }
            entry = zis.nextEntry
        }
    }
}

fun createEngine(): ZenEngine {
    val loader = object : ZenDecisionLoaderCallback {
        override suspend fun load(key: String): JsonBuffer? {
            return rules[key]?.let { JsonBuffer(it) }
        }
    }
    return ZenEngine(loader, null)
}
```

### Firebase Storage with zip

```kotlin theme={null}
import io.gorules.zen_engine.kotlin.ZenEngine
import io.gorules.zen_engine.kotlin.ZenDecisionLoaderCallback
import io.gorules.zen_engine.kotlin.JsonBuffer
import com.google.firebase.storage.FirebaseStorage
import java.io.File
import java.util.zip.ZipInputStream
import java.io.ByteArrayInputStream
import kotlinx.coroutines.tasks.await

val storage = FirebaseStorage.getInstance()
val rules = mutableMapOf<String, ByteArray>()

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

    val zipBytes = if (cacheFile.exists()) {
        cacheFile.readBytes()
    } else {
        // Download from Firebase Storage
        val ref = storage.reference.child("decisions.zip")
        val maxSize: Long = 10 * 1024 * 1024 // 10MB max
        val bytes = ref.getBytes(maxSize).await()
        cacheFile.parentFile?.mkdirs()
        cacheFile.writeBytes(bytes)
        bytes
    }

    // Extract all decisions from zip
    ZipInputStream(ByteArrayInputStream(zipBytes)).use { zis ->
        var entry = zis.nextEntry
        while (entry != null) {
            if (!entry.isDirectory) {
                rules[entry.name] = zis.readBytes()
            }
            entry = zis.nextEntry
        }
    }
}

fun createEngine(): ZenEngine {
    val loader = object : ZenDecisionLoaderCallback {
        override suspend fun load(key: String): JsonBuffer? {
            return rules[key]?.let { JsonBuffer(it) }
        }
    }
    return ZenEngine(loader, null)
}
```

## 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.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.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.evaluateExpression
import io.gorules.zen_engine.kotlin.evaluateUnaryExpression
import io.gorules.zen_engine.kotlin.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.
