状态保留和永久性存储
使用集合让一切井井有条
根据您的偏好保存内容并对其进行分类。
Jetpack Compose
在 Jetpack Compose 中,界面状态通常使用
remember
和
rememberSaveable
。
虽然
rememberSaveable
提供在配置更改后自动保存状态的功能,
功能仅限于原始数据类型和实现
Parcelable
或
Serializable
。
对于可能包含复杂嵌套结构和属性的自定义对象(例如 Brush
),必须使用显式序列化和反序列化机制。这时,自定义状态 Saver 就派上用场了。通过为 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
]。
基本序列化
定义镜像 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 和/或其关联公司的注册商标。
最后更新时间 (UTC):2025-07-27。
[null,null,["最后更新时间 (UTC):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 }"]]