Jetpack Compose
Jetpack Compose에서 UI 상태는 일반적으로 remember
및 rememberSaveable
를 사용하여 관리됩니다.
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
: 숫자 필드 (크기, epsilon), 색상,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)
}
}