Bảo tồn trạng thái và lưu trữ lâu dài

Việc lưu giữ trạng thái và bộ nhớ liên tục là những khía cạnh quan trọng của các ứng dụng viết bằng bút,đặc biệt là trong Compose. Các đối tượng dữ liệu cốt lõi (chẳng hạn như thuộc tính cọ và các điểm tạo thành nét vẽ) rất phức tạp và không tự động duy trì. Điều này đòi hỏi một chiến lược có chủ ý để lưu trạng thái trong các trường hợp như thay đổi cấu hình và lưu vĩnh viễn bản vẽ của người dùng vào cơ sở dữ liệu.

Lưu giữ trạng thái

Trong Jetpack Compose, trạng thái giao diện người dùng thường được quản lý bằng rememberrememberSaveable. Mặc dù rememberSaveable cung cấp khả năng tự động lưu giữ trạng thái trong quá trình thay đổi cấu hình, nhưng các chức năng tích hợp của thành phần này chỉ giới hạn ở các kiểu dữ liệu nguyên thuỷ và các đối tượng triển khai Parcelable hoặc Serializable.

Đối với các đối tượng tuỳ chỉnh có chứa các thuộc tính phức tạp, chẳng hạn như Brush, bạn cần xác định cơ chế tuần tự hoá và giải tuần tự hoá rõ ràng. Trình lưu trạng thái tuỳ chỉnh rất hữu ích trong trường hợp này. Bằng cách xác định Saver tuỳ chỉnh cho đối tượng Brush, bạn có thể giữ lại các thuộc tính thiết yếu của đối tượng đó khi xảy ra thay đổi về cấu hình, như minh hoạ trong ví dụ brushStateSaver sau đây.

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

Sau đó, bạn có thể dùng Saver tuỳ chỉnh để giữ nguyên trạng thái cọ đã chọn:

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

Bộ nhớ liên tục

Để bật các tính năng như lưu, tải tài liệu và cộng tác theo thời gian thực (nếu có), hãy lưu trữ nét vẽ và dữ liệu liên quan ở định dạng được chuyển đổi tuần tự. Đối với Ink API, bạn cần phải chuyển đổi tuần tự và huỷ chuyển đổi tuần tự theo cách thủ công.

Để khôi phục chính xác một nét vẽ, hãy lưu BrushStrokeInputBatch của nét vẽ đó.

  • Brush: Bao gồm các trường số (kích thước, epsilon), màu sắc và BrushFamily.
  • StrokeInputBatch: Một danh sách các điểm đầu vào có trường số.

Chuyển đổi tuần tự cơ bản

Xác định cấu trúc đối tượng tuần tự hoá phản ánh các đối tượng trong thư viện Ink.

Mã hoá dữ liệu được chuyển đổi tuần tự bằng khung hình ưu tiên của bạn, chẳng hạn như Gson, Moshi, Protobuf và các khung hình khác, đồng thời sử dụng tính năng nén để tối ưu hoá.

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

}