שימור מצב ואחסון מתמיד

Jetpack פיתוח נייטיב

ב-Jetpack Compose, בדרך כלל מנהלים את מצב ממשק המשתמש באמצעות remember ו-rememberSaveable. ספריית rememberSaveable מציעה שמירת מצב אוטומטית במהלך שינויים בהגדרות, אבל היכולות המובנות שלה מוגבלות לסוגי נתונים פרימיטיביים ולאובייקטים שמטמיעים את Parcelable או את Serializable.

באובייקטים מותאמים אישית כמו Brush, שעשויים לכלול מבנים ומאפיינים מורכבים בתצוגת עץ, נדרשים מנגנונים מפורשים לסריאליזציה ולדה-סריאליזציה. כאן נכנס לתמונה שומר המצב המותאם אישית. כדי להבטיח שמאפייני המברשת החיוניים יישמרו גם כשיהיו שינויים בהגדרות, אפשר להגדיר Saver בהתאמה אישית לאובייקט Brush, כפי שמוצג בדוגמה עם brushStateSaver באמצעות סוג Converters לדוגמה.

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

לאחר מכן תוכלו להשתמש ב-Saver בהתאמה אישית כדי לשמור את מצב המברשת שנבחר על ידי המשתמש, באופן הבא:

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

אחסון קבוע

כדי להפעיל תכונות כמו שמירת מסמכים, טעינת מסמכים ושיתוף פעולה פוטנציאלי בזמן אמת, צריך לאחסן את הקווים והנתונים המשויכים בפורמט בסריאליזציה. ב-Ink API, צריך לבצע סריאליזציה ופענוח ידניים.

כדי לשחזר במדויק קו, שומרים את הערכים של Brush ושל [StrokeInputBatch].

  • Brush: כולל שדות מספריים (size,‏ epsilon), צבע ו-BrushFamily.
  • StrokeInputBatch: בעיקרון, רשימה של נקודות קלט עם שדות מספריים.

סריאליזציה בסיסית

מגדירים מבנה של אובייקט שרשור (serialization) שמשקף את האובייקטים בספריית Ink.

צריך לקודד את הנתונים בסדרה באמצעות המסגרת המועדפת עליכם, כמו Gson,‏ Moshi, ‏ Protobuf ועוד, ולהשתמש בקיצור כדי לבצע אופטימיזציה.

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

}