Penyimpanan persisten dan pelestarian status
Tetap teratur dengan koleksi
Simpan dan kategorikan konten berdasarkan preferensi Anda.
Jetpack Compose
Di Jetpack Compose, status UI biasanya dikelola menggunakan
remember
dan
rememberSaveable
Meskipun
rememberSaveable
menawarkan pelestarian status otomatis di seluruh perubahan konfigurasi, kemampuan bawaannya
terbatas pada jenis data dan objek primitif yang mengimplementasikan
Parcelable
atau
Serializable
.
Untuk objek kustom seperti
Brush
, yang dapat mencakup
struktur dan properti bersarang yang rumit, serialisasi eksplisit dan
mekanisme deserialisasi diperlukan. Di sinilah saver status kustom
menjadi berguna. Dengan menentukan kustom
Saver
untuk
objek Brush
, seperti
yang ditunjukkan dalam contoh yang diberikan dengan brushStateSaver
menggunakan contoh
Converters
, Anda dapat menjamin
mempertahankan atribut penting kuas
bahkan ketika konfigurasi berubah
terjadi.
fun brushStateSaver(converters: Converters): Saver<MutableState<Brush>, String> = Saver(
save = { state ->
converters.brushToString(state.value)
},
restore = { jsonString ->
val brush = converters.stringToBrush(jsonString)
mutableStateOf(brush)
}
)
Kemudian, Anda dapat menggunakan
Saver
ke
mempertahankan status kuas yang dipilih pengguna seperti:
val converters = Converters()
val currentBrush = rememberSaveable(saver = brushStateSaver(converters)) { mutableStateOf(defaultBrush) }
Penyimpanan persisten
Untuk mengaktifkan fitur seperti penyimpanan, pemuatan dokumen, dan potensi kolaborasi real-time, simpan goresan dan data terkait dalam format serial. Untuk
Ink API, serialisasi dan deserialisasi manual diperlukan.
Untuk memulihkan goresan secara akurat, simpanBrush
dan [StrokeInputBatch
].
Serialisasi dasar
Menentukan struktur objek serialisasi yang mencerminkan objek library Ink.
Enkode data serial menggunakan framework pilihan Anda seperti Gson, Moshi, Protobuf, dan lainnya, serta gunakan kompresi untuk pengoptimalan.
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)
}
}
Konten dan contoh kode di halaman ini tunduk kepada lisensi yang dijelaskan dalam Lisensi Konten. Java dan OpenJDK adalah merek dagang atau merek dagang terdaftar dari Oracle dan/atau afiliasinya.
Terakhir diperbarui pada 2025-07-27 UTC.
[[["Mudah dipahami","easyToUnderstand","thumb-up"],["Memecahkan masalah saya","solvedMyProblem","thumb-up"],["Lainnya","otherUp","thumb-up"]],[["Informasi yang saya butuhkan tidak ada","missingTheInformationINeed","thumb-down"],["Terlalu rumit/langkahnya terlalu banyak","tooComplicatedTooManySteps","thumb-down"],["Sudah usang","outOfDate","thumb-down"],["Masalah terjemahan","translationIssue","thumb-down"],["Masalah kode / contoh","samplesCodeIssue","thumb-down"],["Lainnya","otherDown","thumb-down"]],["Terakhir diperbarui pada 2025-07-27 UTC."],[],[],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 }"]]