La conservation de l'état et le stockage persistant sont des aspects non triviaux des applications d'encrage,en particulier dans Compose. Les objets de données principaux, tels que les propriétés du pinceau et les points qui forment un trait, sont complexes et ne sont pas conservés automatiquement. Cela nécessite une stratégie délibérée pour enregistrer l'état dans des scénarios tels que les changements de configuration et l'enregistrement permanent des dessins d'un utilisateur dans une base de données.
State preservation
In Jetpack Compose, UI state is typically managed using
remember
and
rememberSaveable.
While
rememberSaveable
offers automatic state preservation across configuration changes, its built-in
capabilities are limited to primitive data types and objects that implement
Parcelable or
Serializable.
For custom objects that contain complex properties, such as Brush, you need to define explicit serialization and deserialization mechanisms. A custom state saver is useful for this. By defining a custom Saver for the Brush object, you can preserve its essential attributes when configuration changes occur, as shown in the following brushStateSaver example.
fun brushStateSaver(converters: Converters): Saver<MutableState<Brush>, String> = Saver(
save = { state ->
converters.brushToString(state.value)
},
restore = { jsonString ->
val brush = converters.stringToBrush(jsonString)
mutableStateOf(brush)
}
)
You can then use the custom Saver to preserve the selected brush state:
val converters = Converters()
val currentBrush = rememberSaveable(saver = brushStateSaver(converters)) { mutableStateOf(defaultBrush) }
Stockage persistant
Pour activer des fonctionnalités telles que l'enregistrement et le chargement de documents, ainsi que la collaboration potentielle en temps réel, stockez les traits et les données associées dans un format sérialisé. Pour l'API Ink, la sérialisation et la désérialisation manuelles sont nécessaires.
Pour restaurer un trait avec précision, enregistrez ses Brush et StrokeInputBatch.
Brush: inclut les champs numériques (taille, epsilon), la couleur etBrushFamily.StrokeInputBatch: liste de points d'entrée avec des champs numériques.
Sérialisation de base
Définissez une structure d'objet de sérialisation qui reflète les objets de la bibliothèque Ink.
Encodez les données sérialisées à l'aide de votre framework préféré (Gson, Moshi, Protobuf, etc.) et utilisez la compression pour l'optimisation.
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)
}
}