Preservação de estado e armazenamento permanente

Jetpack Compose

No Jetpack Compose, o estado da interface geralmente é gerenciado usando remember e rememberSaveable. Embora rememberSaveable ofereça preservação automática do estado em todas as mudanças de configuração, os recursos integrados são limitados a tipos de dados primitivos e objetos que implementam Parcelable ou Serializable.

Para objetos personalizados, como Brush, que podem abranger estruturas e propriedades aninhadas complexas, são necessários mecanismos explícitos de serialização e desserialização. É aqui que um armazenamento de estado personalizado se torna útil. Ao definir um Saver personalizado para o objeto Brush, como demonstrado no exemplo fornecido com brushStateSaver usando a classe Converters, é possível garantir a preservação dos atributos essenciais do pincel, mesmo quando ocorrem mudanças de configuração.

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

Em seguida, use o Saver personalizado para preservar o estado do pincel selecionado de um usuário, como este:

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

Armazenamento persistente

Para ativar recursos como salvar, carregar documentos e possíveis colaborações em tempo real, armazene traços e dados associados em um formato serializado. Para a API Ink, a serialização e desserialização manual são necessárias.

Para restaurar um traço com precisão, salve o Brush e o [StrokeInputBatch].

Serialização básica

Defina uma estrutura de objeto de serialização que reflita os objetos da biblioteca Ink.

Codifique os dados serializados usando seu framework preferido, como Gson, Moshi, Protobuf e outros, e use a compactação para otimização.

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

}