状態の保持と永続ストレージ

Jetpack Compose

Jetpack Compose では、通常、UI の状態は rememberrememberSaveable を使用して管理されます。rememberSaveable は構成の変更にわたって状態を自動的に保持しますが、その組み込み機能はプリミティブ データ型と、Parcelable または Serializable を実装するオブジェクトに限定されています。

複雑なネストされた構造とプロパティを含む可能性がある Brush などのカスタム オブジェクトの場合、明示的なシリアル化と逆シリアル化のメカニズムが必要です。ここでカスタム状態セーバーが役立ちます。Brush オブジェクトにカスタム Saver を定義すると、Converters クラスの例で brushStateSaver を使用して示されているように、構成が変更されてもブラシの重要な属性が確実に保持されます。

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: 数値フィールド(サイズ、イプシロン)、色、BrushFamily が含まれます。
  • StrokeInputBatch: 基本的に、数値フィールドを含む入力ポイントのリストです。

基本的なシリアル化

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

}