## Jetpack Compose

In Jetpack Compose, UI state is typically managed using
[`remember`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#remember(kotlin.Function0))
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 such as
[`Brush`](https://developer.android.com/reference/kotlin/androidx/ink/brush/Brush), which may encompass
intricate nested structures and properties, explicit serialization and
deserialization mechanisms are necessary. This is where a custom state saver
becomes useful. By defining a custom
[`Saver`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/saveable/Saver) for
the `Brush` object, as
demonstrated in the provided example with `brushStateSaver`using the example
`Converters`class, you can guarantee the
preservation of the brush's essential attributes even when configuration changes
occur.  

    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 a user's selected brush state like so:  

    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): Essentially 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)
        }

    }