狀態保留與永久儲存空間
透過集合功能整理內容
你可以依據偏好儲存及分類內容。
Jetpack Compose
在 Jetpack Compose 中,UI 狀態通常使用以下方式管理:
remember
和
rememberSaveable
。
雖然
rememberSaveable
提供在設定變更期間自動保留狀態的功能
功能僅限於基本的資料類型和物件
Parcelable
或
Serializable
。
對於可能包含複雜巢狀結構和屬性的自訂物件 (例如 Brush
),您必須使用明確的序列化和反序列化機制。也就是自訂狀態儲存工具
變得實用定義自訂 Deployment
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
]。
基本序列化
定義與 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)
}
}
這個頁面中的內容和程式碼範例均受《內容授權》中的授權所規範。Java 與 OpenJDK 是 Oracle 和/或其關係企業的商標或註冊商標。
上次更新時間:2025-07-27 (世界標準時間)。
[null,null,["上次更新時間:2025-07-27 (世界標準時間)。"],[],[],null,["# State preservation and persistent storage\n\nJetpack Compose\n---------------\n\nIn Jetpack Compose, UI state is typically managed using\n[`remember`](/reference/kotlin/androidx/compose/runtime/package-summary#remember(kotlin.Function0))\nand\n[`rememberSaveable`](/reference/kotlin/androidx/compose/runtime/saveable/package-summary#rememberSaveable(kotlin.Array,androidx.compose.runtime.saveable.Saver,kotlin.String,kotlin.Function0)).\nWhile\n[`rememberSaveable`](/reference/kotlin/androidx/compose/runtime/saveable/package-summary#rememberSaveable(kotlin.Array,androidx.compose.runtime.saveable.Saver,kotlin.String,kotlin.Function0))\noffers automatic state preservation across configuration changes, its built-in\ncapabilities are limited to primitive data types and objects that implement\n[`Parcelable`](/reference/kotlin/android/os/Parcelable) or\n[`Serializable`](/reference/java/io/Serializable).\n\nFor custom objects such as\n[`Brush`](/reference/kotlin/androidx/ink/brush/Brush), which may encompass\nintricate nested structures and properties, explicit serialization and\ndeserialization mechanisms are necessary. This is where a custom state saver\nbecomes useful. By defining a custom\n[`Saver`](/reference/kotlin/androidx/compose/runtime/saveable/Saver) for\nthe `Brush` object, as\ndemonstrated in the provided example with `brushStateSaver`using the example\n`Converters`class, you can guarantee the\npreservation of the brush's essential attributes even when configuration changes\noccur. \n\n fun brushStateSaver(converters: Converters): Saver\u003cMutableState\u003cBrush\u003e, String\u003e = Saver(\n save = { state -\u003e\n converters.brushToString(state.value)\n },\n restore = { jsonString -\u003e\n val brush = converters.stringToBrush(jsonString)\n mutableStateOf(brush)\n }\n )\n\nYou can then use the custom\n[`Saver`](/reference/kotlin/androidx/compose/runtime/saveable/Saver) to\npreserve a user's selected brush state like so: \n\n val converters = Converters()\n val currentBrush = rememberSaveable(saver = brushStateSaver(converters)) { mutableStateOf(defaultBrush) }\n\n### Persistent storage\n\nTo enable features such as document saving, loading, and potential real-time\ncollaboration, store strokes and associated data in a serialized format. For the\nInk API, manual serialization and deserialization are necessary.\n\nTo accurately restore a stroke, save its`Brush` and \\[`StrokeInputBatch`\\].\n\n- [**`Brush`**](/reference/kotlin/androidx/ink/brush/Brush): Includes numeric fields (size, epsilon), color, and [`BrushFamily`](/reference/kotlin/androidx/ink/brush/BrushFamily).\n- [**`StrokeInputBatch`**](/reference/kotlin/androidx/ink/strokes/StrokeInputBatch): Essentially a list of input points with numeric fields.\n\n#### Basic serialization\n\nDefine a serialization object structure that mirrors the Ink library objects.\n\nEncode the serialized data using your preferred framework like Gson, Moshi,\nProtobuf, and others, and use compression for optimization. \n\n data class SerializedStroke(\n val inputs: SerializedStrokeInputBatch,\n val brush: SerializedBrush\n )\n\n data class SerializedBrush(\n val size: Float,\n val color: Long,\n val epsilon: Float,\n val stockBrush: SerializedStockBrush\n )\n\n enum class SerializedStockBrush {\n MARKER_V1,\n PRESSURE_PEN_V1,\n HIGHLIGHTER_V1\n }\n\n data class SerializedStrokeInputBatch(\n val toolType: SerializedToolType,\n val strokeUnitLengthCm: Float,\n val inputs: List\u003cSerializedStrokeInput\u003e\n )\n\n data class SerializedStrokeInput(\n val x: Float,\n val y: Float,\n val timeMillis: Float,\n val pressure: Float,\n val tiltRadians: Float,\n val orientationRadians: Float,\n val strokeUnitLengthCm: Float\n )\n\n enum class SerializedToolType {\n STYLUS,\n TOUCH,\n MOUSE,\n UNKNOWN\n }\n\n class Converters {\n\n private val gson: Gson = GsonBuilder().create()\n\n companion object {\n private val stockBrushToEnumValues =\n mapOf(\n StockBrushes.markerV1 to SerializedStockBrush.MARKER_V1,\n StockBrushes.pressurePenV1 to SerializedStockBrush.PRESSURE_PEN_V1,\n StockBrushes.highlighterV1 to SerializedStockBrush.HIGHLIGHTER_V1,\n )\n\n private val enumToStockBrush =\n stockBrushToEnumValues.entries.associate { (key, value) -\u003e value to key }\n }\n\n private fun serializeBrush(brush: Brush): SerializedBrush {\n return SerializedBrush(\n size = brush.size,\n color = brush.colorLong,\n epsilon = brush.epsilon,\n stockBrush = stockBrushToEnumValues[brush.family] ?: SerializedStockBrush.MARKER_V1,\n )\n }\n\n private fun serializeStrokeInputBatch(inputs: StrokeInputBatch): SerializedStrokeInputBatch {\n val serializedInputs = mutableListOf\u003cSerializedStrokeInput\u003e()\n val scratchInput = StrokeInput()\n\n for (i in 0 until inputs.size) {\n inputs.populate(i, scratchInput)\n serializedInputs.add(\n SerializedStrokeInput(\n x = scratchInput.x,\n y = scratchInput.y,\n timeMillis = scratchInput.elapsedTimeMillis.toFloat(),\n pressure = scratchInput.pressure,\n tiltRadians = scratchInput.tiltRadians,\n orientationRadians = scratchInput.orientationRadians,\n strokeUnitLengthCm = scratchInput.strokeUnitLengthCm,\n )\n )\n }\n\n val toolType =\n when (inputs.getToolType()) {\n InputToolType.STYLUS -\u003e SerializedToolType.STYLUS\n InputToolType.TOUCH -\u003e SerializedToolType.TOUCH\n InputToolType.MOUSE -\u003e SerializedToolType.MOUSE\n else -\u003e SerializedToolType.UNKNOWN\n }\n\n return SerializedStrokeInputBatch(\n toolType = toolType,\n strokeUnitLengthCm = inputs.getStrokeUnitLengthCm(),\n inputs = serializedInputs,\n )\n }\n\n private fun deserializeStroke(serializedStroke: SerializedStroke): Stroke? {\n val inputs = deserializeStrokeInputBatch(serializedStroke.inputs) ?: return null\n val brush = deserializeBrush(serializedStroke.brush) ?: return null\n return Stroke(brush = brush, inputs = inputs)\n }\n\n private fun deserializeBrush(serializedBrush: SerializedBrush): Brush {\n val stockBrushFamily = enumToStockBrush[serializedBrush.stockBrush] ?: StockBrushes.markerV1\n\n return Brush.createWithColorLong(\n family = stockBrushFamily,\n colorLong = serializedBrush.color,\n size = serializedBrush.size,\n epsilon = serializedBrush.epsilon,\n )\n }\n\n private fun deserializeStrokeInputBatch(\n serializedBatch: SerializedStrokeInputBatch\n ): StrokeInputBatch {\n val toolType =\n when (serializedBatch.toolType) {\n SerializedToolType.STYLUS -\u003e InputToolType.STYLUS\n SerializedToolType.TOUCH -\u003e InputToolType.TOUCH\n SerializedToolType.MOUSE -\u003e InputToolType.MOUSE\n else -\u003e InputToolType.UNKNOWN\n }\n\n val batch = MutableStrokeInputBatch()\n\n serializedBatch.inputs.forEach { input -\u003e\n batch.addOrThrow(\n type = toolType,\n x = input.x,\n y = input.y,\n elapsedTimeMillis = input.timeMillis.toLong(),\n pressure = input.pressure,\n tiltRadians = input.tiltRadians,\n orientationRadians = input.orientationRadians,\n )\n }\n\n return batch\n }\n\n fun serializeStrokeToEntity(stroke: Stroke): StrokeEntity {\n val serializedBrush = serializeBrush(stroke.brush)\n val serializedInputs = serializeStrokeInputBatch(stroke.inputs)\n return StrokeEntity(\n brushSize = serializedBrush.size,\n brushColor = serializedBrush.color,\n brushEpsilon = serializedBrush.epsilon,\n stockBrush = serializedBrush.stockBrush,\n strokeInputs = gson.toJson(serializedInputs),\n )\n }\n\n fun deserializeEntityToStroke(entity: StrokeEntity): Stroke {\n val serializedBrush =\n SerializedBrush(\n size = entity.brushSize,\n color = entity.brushColor,\n epsilon = entity.brushEpsilon,\n stockBrush = entity.stockBrush,\n )\n\n val serializedInputs =\n gson.fromJson(entity.strokeInputs, SerializedStrokeInputBatch::class.java)\n\n val brush = deserializeBrush(serializedBrush)\n val inputs = deserializeStrokeInputBatch(serializedInputs)\n\n return Stroke(brush = brush, inputs = inputs)\n }\n\n fun brushToString(brush: Brush): String {\n val serializedBrush = serializeBrush(brush)\n return gson.toJson(serializedBrush)\n }\n\n fun stringToBrush(jsonString: String): Brush {\n val serializedBrush = gson.fromJson(jsonString, SerializedBrush::class.java)\n return deserializeBrush(serializedBrush)\n }\n\n }"]]