Field Decomposition
A document is never stored as one serialized blob. Instead, every top-level field becomes its
own MMKV entry, keyed {document}::{field}:
// Note(title = "Pick up milk", body = "2%, not whole", done = false)
// stored as:
note-1::title -> bytes
note-1::body -> bytes
note-1::done -> bytes
The :: separator is reserved for this purpose, which is why it's rejected in a
document key — see Opening Documents.
Why decompose
Decomposition is what makes field-level reactivity and truly cheap single-field writes
possible. update(prop, value) and field delegates each touch exactly one decomposed
key — a single putBytes or getBytes call, independent of how many other
fields the document has, with no read or re-encoding of anything else.
update { current -> ... } is a different story: it's a full read-modify-write of
the whole document — every field is decoded on the read side, and every field is
re-encoded and rewritten on the write side, even when the builder only changes one of them. See
Read & Write for when to reach for each shape, and
Benchmarks for the measured cost difference between the two.
Nested types
A field whose type is itself a @Serializable object — like a nested
Player inside a save file, or an address inside a user profile — is stored as a
single serialized sub-blob under that one field's key, to any nesting depth. Nested types are not
decomposed further in v1; only top-level fields get their own key.
How fields are discovered
The library walks each type's fields using kotlinx.serialization's SerialDescriptor
— never reflection, which keeps this working identically on Kotlin/Native (iOS) as it does on the
JVM. A custom encoder/decoder writes and reads each element to and from its own
{doc}::{field} key instead of a single shared output buffer.