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

Jetpack Compose

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 tính năng tự động lưu giữ trạng thái trong các thay đổi về cấu hình, nhưng các chức năng tích hợp sẵn của lớp này chỉ giới hạn ở các kiểu dữ liệu và đối tượng nguyên thuỷ triển khai Parcelable hoặc Serializable.

Đối với các đối tượng tuỳ chỉnh như Brush, có thể bao gồm các cấu trúc và thuộc tính lồng nhau phức tạp, bạn cần có cơ chế chuyển đổi tuần tự và huỷ chuyển đổi tuần tự rõ ràng. Đây là lúc trình lưu trạng thái tuỳ chỉnh phát huy tác dụng. Bằng cách xác định một Saver tuỳ chỉnh cho đối tượng Brush, như minh hoạ trong ví dụ được cung cấp với brushStateSaver bằng cách sử dụng lớp Converters mẫu, bạn có thể đảm bảo việc giữ lại các thuộc tính thiết yếu của bút vẽ ngay cả khi cấu hình thay đổi.

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ể sử dụng Saver tuỳ chỉnh để lưu giữ trạng thái bút vẽ đã chọn của người dùng như sau:

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à khả năng cộng tác theo thời gian thực, hãy lưu trữ các nét vẽ và dữ liệu liên quan ở định dạng tuần tự. Đối với API Ink, 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ưuBrush và [StrokeInputBatch] 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: Về cơ bản là danh sách các điểm đầu vào có trường số.

Tuần tự hoá 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 thư viện Ink.

Mã hoá dữ liệu được tuần tự hoá bằng khung mà bạn muốn như Gson, Moshi, Protobuf và các khung 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)
    }

}