State preservation and persistent storage are non-trivial aspects of inking apps,especially in Compose. The core data objects, such as brush properties and the points that form a stroke, are complex and don't automatically persist. This requires a deliberate strategy for saving state during scenarios like configuration changes and permanently saving a user's drawings to a database.

## State preservation

In Jetpack Compose, UI state is typically managed using[`remember`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#remember)and[`rememberSaveable`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/saveable/package-summary#rememberSaveable(kotlin.Array,androidx.compose.runtime.saveable.Saver,kotlin.String,kotlin.Function0)). While[`rememberSaveable`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/saveable/package-summary#rememberSaveable(kotlin.Array,androidx.compose.runtime.saveable.Saver,kotlin.String,kotlin.Function0))offers automatic state preservation across configuration changes, its built-in capabilities are limited to primitive data types and objects that implement[`Parcelable`](https://developer.android.com/reference/kotlin/android/os/Parcelable)or[`Serializable`](https://developer.android.com/reference/java/io/Serializable).

For custom objects that contain complex properties, such as[`Brush`](https://developer.android.com/reference/kotlin/androidx/ink/brush/Brush), you need to define explicit serialization and deserialization mechanisms. A custom state saver is useful for this. By defining a custom[`Saver`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/saveable/Saver)for the`Brush`object, you can preserve its essential attributes when configuration changes occur, as shown in the following`brushStateSaver`example.  

    fun brushStateSaver(converters: Converters): Saver<MutableState<Brush>, String> = Saver(
        save = { state ->
            converters.brushToString(state.value)
        },
        restore = { jsonString ->
            val brush = converters.stringToBrush(jsonString)
            mutableStateOf(brush)
        }
    )

You can then use the custom[`Saver`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/saveable/Saver)to preserve the selected brush state:  

    val converters = Converters()
    val currentBrush = rememberSaveable(saver = brushStateSaver(converters)) { mutableStateOf(defaultBrush) }

## Persistent Storage

To enable features such as document saving, loading, and potential real-time collaboration, store strokes and associated data in a serialized format. For the Ink API, manual serialization and deserialization are necessary.

To accurately restore a stroke, save its`Brush`and`StrokeInputBatch`.

- [`Brush`](https://developer.android.com/reference/kotlin/androidx/ink/brush/Brush): Includes numeric fields (size, epsilon), color, and[`BrushFamily`](https://developer.android.com/reference/kotlin/androidx/ink/brush/BrushFamily).
- [`StrokeInputBatch`](https://developer.android.com/reference/kotlin/androidx/ink/strokes/StrokeInputBatch): A list of input points with numeric fields.

#### Basic serialization

Define a serialization object structure that mirrors the Ink library objects.

Encode the serialized data using your preferred framework like Gson, Moshi, Protobuf, and others, and use compression for optimization.  

    data class SerializedStroke(
      val inputs: SerializedStrokeInputBatch,
      val brush: SerializedBrush
    )

    data class SerializedBrush(
      val size: Float,
      val color: Long,
      val epsilon: Float,
      val stockBrush: SerializedStockBrush
    )

    enum class SerializedStockBrush {
      MARKER_V1,
      PRESSURE_PEN_V1,
      HIGHLIGHTER_V1
    }

    data class SerializedStrokeInputBatch(
      val toolType: SerializedToolType,
      val strokeUnitLengthCm: Float,
      val inputs: List<SerializedStrokeInput>
    )

    data class SerializedStrokeInput(
      val x: Float,
      val y: Float,
      val timeMillis: Float,
      val pressure: Float,
      val tiltRadians: Float,
      val orientationRadians: Float,
      val strokeUnitLengthCm: Float
    )

    enum class SerializedToolType {
      STYLUS,
      TOUCH,
      MOUSE,
      UNKNOWN
    }

    class Converters {

      private val gson: Gson = GsonBuilder().create()

      companion object {
        private val stockBrushToEnumValues =
          mapOf(
            StockBrushes.markerV1 to SerializedStockBrush.MARKER_V1,
            StockBrushes.pressurePenV1 to SerializedStockBrush.PRESSURE_PEN_V1,
            StockBrushes.highlighterV1 to SerializedStockBrush.HIGHLIGHTER_V1,
          )

        private val enumToStockBrush =
          stockBrushToEnumValues.entries.associate { (key, value) -> value to key }
      }

      private fun serializeBrush(brush: Brush): SerializedBrush {
        return SerializedBrush(
          size = brush.size,
          color = brush.colorLong,
          epsilon = brush.epsilon,
          stockBrush = stockBrushToEnumValues[brush.family] ?: SerializedStockBrush.MARKER_V1,
        )
      }

      private fun serializeStrokeInputBatch(inputs: StrokeInputBatch): SerializedStrokeInputBatch {
        val serializedInputs = mutableListOf<SerializedStrokeInput>()
        val scratchInput = StrokeInput()

        for (i in 0 until inputs.size) {
          inputs.populate(i, scratchInput)
          serializedInputs.add(
            SerializedStrokeInput(
              x = scratchInput.x,
              y = scratchInput.y,
              timeMillis = scratchInput.elapsedTimeMillis.toFloat(),
              pressure = scratchInput.pressure,
              tiltRadians = scratchInput.tiltRadians,
              orientationRadians = scratchInput.orientationRadians,
              strokeUnitLengthCm = scratchInput.strokeUnitLengthCm,
            )
          )
        }

        val toolType =
          when (inputs.getToolType()) {
            InputToolType.STYLUS -> SerializedToolType.STYLUS
            InputToolType.TOUCH -> SerializedToolType.TOUCH
            InputToolType.MOUSE -> SerializedToolType.MOUSE
            else -> SerializedToolType.UNKNOWN
          }

        return SerializedStrokeInputBatch(
          toolType = toolType,
          strokeUnitLengthCm = inputs.getStrokeUnitLengthCm(),
          inputs = serializedInputs,
        )
      }

      private fun deserializeStroke(serializedStroke: SerializedStroke): Stroke? {
        val inputs = deserializeStrokeInputBatch(serializedStroke.inputs) ?: return null
        val brush = deserializeBrush(serializedStroke.brush) ?: return null
        return Stroke(brush = brush, inputs = inputs)
      }

      private fun deserializeBrush(serializedBrush: SerializedBrush): Brush {
        val stockBrushFamily = enumToStockBrush[serializedBrush.stockBrush] ?: StockBrushes.markerV1

        return Brush.createWithColorLong(
          family = stockBrushFamily,
          colorLong = serializedBrush.color,
          size = serializedBrush.size,
          epsilon = serializedBrush.epsilon,
        )
      }

      private fun deserializeStrokeInputBatch(
        serializedBatch: SerializedStrokeInputBatch
      ): StrokeInputBatch {
        val toolType =
          when (serializedBatch.toolType) {
            SerializedToolType.STYLUS -> InputToolType.STYLUS
            SerializedToolType.TOUCH -> InputToolType.TOUCH
            SerializedToolType.MOUSE -> InputToolType.MOUSE
            else -> InputToolType.UNKNOWN
          }

        val batch = MutableStrokeInputBatch()

        serializedBatch.inputs.forEach { input ->
          batch.addOrThrow(
            type = toolType,
            x = input.x,
            y = input.y,
            elapsedTimeMillis = input.timeMillis.toLong(),
            pressure = input.pressure,
            tiltRadians = input.tiltRadians,
            orientationRadians = input.orientationRadians,
          )
        }

        return batch
      }

      fun serializeStrokeToEntity(stroke: Stroke): StrokeEntity {
        val serializedBrush = serializeBrush(stroke.brush)
        val serializedInputs = serializeStrokeInputBatch(stroke.inputs)
        return StrokeEntity(
          brushSize = serializedBrush.size,
          brushColor = serializedBrush.color,
          brushEpsilon = serializedBrush.epsilon,
          stockBrush = serializedBrush.stockBrush,
          strokeInputs = gson.toJson(serializedInputs),
        )
      }

      fun deserializeEntityToStroke(entity: StrokeEntity): Stroke {
        val serializedBrush =
          SerializedBrush(
            size = entity.brushSize,
            color = entity.brushColor,
            epsilon = entity.brushEpsilon,
            stockBrush = entity.stockBrush,
          )

        val serializedInputs =
          gson.fromJson(entity.strokeInputs, SerializedStrokeInputBatch::class.java)

        val brush = deserializeBrush(serializedBrush)
        val inputs = deserializeStrokeInputBatch(serializedInputs)

        return Stroke(brush = brush, inputs = inputs)
      }

      fun brushToString(brush: Brush): String {
            val serializedBrush = serializeBrush(brush)
            return gson.toJson(serializedBrush)
        }

      fun stringToBrush(jsonString: String): Brush {
            val serializedBrush = gson.fromJson(jsonString, SerializedBrush::class.java)
            return deserializeBrush(serializedBrush)
        }

    }