О проблеме
В разработке под платформу Android с самых первых его версий существует сложность в управлении жизненным циклом activity. Activity - это один из основных компонентов Android-приложения. Он представляет один, независимый от других, экран. Во время работы приложения операционная система может выгружать activity из оперативной памяти в следующих ситуациях ситуациях:
- При повороте экрана
- Если activity находится в фоне и для работы активных приложений ресурсов недостаточно
Объект activity является jvm классом, который пересоздается при наступлении вышеописанных условий, следовательно все поля этого объекта теряют свои актуальные значения.
Стандартное решение
Для решения данной проблемы разработчики добавили в базовый класс activity дополнительный callback метод
onSaveInstanceState
. Чтобы лучше понять принцип работы этого механизма рассмотрим диаграммы,
иллюстрирующие жизненный цикл activity (рис. 1):
В случае уничтожения объекта системой после вызова onStop
и перед вызовом
onDestroy
вызывается метод onSaveInstanceState
, который позволяет сохранить
необходимые данные. Далее при пересоздании в метод onCreate
передается объект bundle в который
ранее данные были сохранены. Однако, сохранение информации в bundle приводит к необходимости написания
однотипного кода, что чревато ошибками на этапе сопровождния.
Кодогенерация
Так как код сохранения/восстановления состояния достаточно однотипен, его можно сгенерировать с помощью annotation processor. Annotation processor - jvm модуль, подключаемый к основному проекту. В его задчи входит обработка пользовательских аннотаций и генерация кода на их основе. Основным классом, через который осуществляется обработка является AbstractProcessor. Для процессинга аннотаций нужно унаследоваться от него и переопределить ряд методов.
Так как в настоящее время наиболее актуальным и перспективным языком разработки под Android является kotlin, то и процессор будет обрабатывать kotlin-код. Все примеры кода будут приведены также на котлине. Однако, на данный момент существует API только для обработки java-кода, в связи с этим для обработки кода на котлине необходимо вручную считывать и анализировать метаданные. Метаданные kotlin расширений хранятся в аннотации @Metadata в бинарном формате ProtoBuf. Существует библиотека, выполняющая парсинг сырых байт в ProtoBuf объекты. Это в значительной мере облегчает обработку, но, тем не менее, все еще требуется дополнительная обработка. Для этого были написаны следующие вспомогательные классы и методы:
fun NameResolver.getJvmName(i: Int) = getQualifiedClassName(i).replace("/", ".") fun Element.toKotlinClass(): KotlinClass { val metadata = kotlinMetadata ?: throw RuntimeException("$simpleName does not have metadata") val classMetadata = metadata as KotlinClassMetadata val resolver = classMetadata.data.nameResolver val proto = classMetadata.data.classProto return KotlinClass(metadata, asTypeElement) } fun Element.toKotlinClassOrNull(): KotlinClass? { val metadata = kotlinMetadata ?: return null val classMetadata = if (metadata is KotlinClassMetadata) metadata else return null val resolver = classMetadata.data.nameResolver val proto = classMetadata.data.classProto val typeElement = if (this is TypeElement) this else null return KotlinClass(metadata, this.asTypeElement) } class KotlinClass( private val metadata: KotlinClassMetadata, val jvmTypeElement: TypeElement ) { private val resolver = metadata.data.nameResolver val name: String = resolver.getJvmName(metadata.data.classProto.fqName) val pkg: String = name.substring(0, name.lastIndexOf(".")) val simpleName: String = name.substring(name.lastIndexOf(".") + 1, name.length) val properties: List<KotlinProperty> = metadata.data.classProto.propertyList .map { KotlinProperty(resolver, it) } val functions: List<KotlinFunction> = metadata.data.classProto.functionList .map { KotlinFunction(resolver, it) } val isDataClass: Boolean = metadata.data.classProto.isDataClass fun toTypeName(): TypeName = ClassName(pkg, simpleName) } class KotlinProperty( private val resolver: NameResolver, private val property: ProtoBuf.Property ) { val name: String = resolver.getJvmName(property.name) val returnType: KotlinType? = if (property.hasReturnType()) KotlinType(resolver, property.returnType) else null val receiverType: KotlinType? = if (property.hasReceiverType()) KotlinType(resolver, property.receiverType) else null val hasAnnotations: Boolean = property.hasAnnotations val getterHasAnnotations: Boolean = property.getterHasAnnotations val setterHasAnnotations: Boolean = property.setterHasAnnotations } class KotlinFunction( private val resolver: NameResolver, private val function: ProtoBuf.Function ) { val name = resolver.getJvmName(function.name) val returnType: KotlinType? = if (function.hasReturnType()) KotlinType(resolver, function.returnType) else null val receiverType: KotlinType? = if (function.hasReceiverType()) KotlinType(resolver, function.receiverType) else null val valueParameters: List<KotlinValueParameter> = function.valueParameterList.map { KotlinValueParameter(resolver, it) } val hasAnnotations: Boolean = function.hasAnnotations } class KotlinValueParameter( private val resolver: NameResolver, private val parameter: ProtoBuf.ValueParameter ) { val name = resolver.getJvmName(parameter.name) val type: KotlinType? = parameter.type.takeIf { parameter.hasType() } ?.let { KotlinType(resolver, it) } } class KotlinType( private val resolver: NameResolver, private val type: ProtoBuf.Type ) { val name: String = resolver.getJvmName(type.className) val arguments: List<KotlinTypeArgument> = type.argumentList.map { KotlinTypeArgument(resolver, it) } val nullable: Boolean = type.nullable val pkg: String = name.substring(0, name.lastIndexOf(".")) val simpleName: String = name.substring(name.lastIndexOf(".") + 1, name.length) fun toTypeName(): TypeName = if (arguments.isNotEmpty()) { ClassName(pkg, simpleName) .parameterizedBy(*arguments.map { it.type.toTypeName() }.toTypedArray()) .nullable(nullable) } else { ClassName(pkg, simpleName).nullable(nullable) } fun isSimpleType(): Boolean = name in SIMPLE_TYPES.map { it.fullName } fun isInt(): Boolean = name == kt.Int.fullName fun isLong(): Boolean = name == kt.Long.fullName fun isString(): Boolean = name == kt.String.fullName fun isFloat(): Boolean = name == kt.Float.fullName fun isDouble(): Boolean = name == kt.Double.fullName fun isBoolean(): Boolean = name == kt.Boolean.fullName companion object { val SIMPLE_TYPES = listOf( kt.Boolean, kt.Float, kt.Double, kt.Int, kt.Long, kt.String ) } } class KotlinTypeArgument( private val resolver: NameResolver, private val typeArgument: ProtoBuf.Type.Argument ) { val type: KotlinType = KotlinType(resolver, typeArgument.type) }
Также для некоторых возможностей требуется объект RoundEnvironment, поэтому часть вспомогательных расширений написана в дочернем классе KotlinAbstractProcessor
protected fun KotlinType.toTypeElement(): TypeElement = elementUtils.getTypeElement(name.toJvmName()) protected fun KotlinClass.getExecutableElement(function: KotlinFunction): ExecutableElement = elementUtils.getTypeElement(name.toJvmName()).enclosedElements .first { it.simpleName.toString() == function.name } .asExecutableElement protected fun KotlinType.toKotlinClass(): KotlinClass = toTypeElement() .toKotlinClass() protected fun KotlinType.toKotlinClassOrNull(): KotlinClass? = toTypeElement() .toKotlinClassOrNull() protected fun KotlinType?.implements(name: String) = this!! .toTypeElement() .implements(name) protected fun KotlinType.canSerialize(): Boolean = when { this.isSimpleType() -> true this.isSerializable() -> true this.implements(java.util.List::class.java.canonicalName) -> arguments[0].type.canSerialize() this.implements(java.util.Set::class.java.canonicalName) -> arguments[0].type.canSerialize() else -> false } protected fun KotlinType.isSerializable(): Boolean = this.toTypeElement().getAnnotation(kotlinx.serialization.Serializable::class.java) != null protected fun KotlinType.implementsObservable(): Boolean = this.implements(caf.observable.IObservable.fullName) protected fun KotlinType.isSerializableList(): Boolean = when { this.implements(java.util.List::class.java.canonicalName) -> arguments[0].type.isSimpleType() || arguments[0].type.isSerializable() || arguments[0].type.isSerializableList() else -> false }
С помощью данных расширений можно приступить к решению задачи кодогенерации
@AutoService(Processor::class) class AnnotationProcessor : BaseAnnotationProcessor() { override fun getSupportedAnnotationTypes(): MutableSet<String> { return mutableSetOf( Presenter::class.java.name ) } override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest() override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean = try { roundEnv.annotatedClasses<Presenter>() .mapNotNull { it.toKotlinClassOrNull() } .forEach { kotlinClass -> processPresenter(kotlinClass) } false } catch (t: Throwable) { error(t) true } private fun processPresenter(kotlinClass: KotlinClass) { FileSpec.builder(kotlinClass.pkg, "Generated${kotlinClass.simpleName}") .addType(kotlinClass.getPersistableWrapper()) .addFunction(kotlinClass.getPersistableWrapperFactoryFunc()) .build() .writeTo(this.generatedDir!!) } private fun KotlinClass.getPersistableWrapper(): TypeSpec { val className = simpleName val classPackage = pkg val persistableWrapperName = ClassName(classPackage, "Persistable${className}Wrapper") return TypeSpec.classBuilder(persistableWrapperName) .primaryConstructor( FunSpec.constructorBuilder() .addParameter("presenter", ClassName(classPackage, className)) .build() ) .addProperty( PropertySpec.builder("presenter", ClassName(classPackage, className)) .addModifiers(KModifier.PRIVATE) .initializer("presenter") .build() ) .addSuperinterface(caf.Persistable.className) .addFunction(getSaveFunc()) .addFunction(getRestoreFunc()) .build() } private fun KotlinClass.getPersistableWrapperFactoryFunc(): FunSpec = FunSpec.builder("persistable") .receiver(toTypeName()) .returns(caf.Persistable.className) .addStatement("return %T(this)", ClassName(pkg, "Persistable${simpleName}Wrapper")) .build() private fun KotlinClass.getSaveFunc(): FunSpec { val annotatedGetterNames = getAnnotatedGetterNames<Persist>() val block = buildCodeBlock { wrap("val jsonRes = %M {", ktx.serialization.json.json.memberName) { properties .filter { property -> property.name.toGetterName() in annotatedGetterNames } .onEach { property -> if (property.returnType == null) { return@onEach } val isLateInit = property.returnType.name == caf.observable.LateinitObservable.fullName conditionalWrapIf(isLateInit, "presenter.${property.name}.isInitialized") { add("%S to ", property.name) generateSerializer( "presenter.${property.name}.value", property.returnType.arguments[0].type ) } } } addStatement("return jsonRes.toString()") } return FunSpec.builder("onSaveInstanceState") .addModifiers(KModifier.OVERRIDE) .returns(kt.String.className) .addCode(block) .build() } private fun KotlinClass.getRestoreFunc(): FunSpec { val annotatedGetterNames = getAnnotatedGetterNames<Persist>() val block = buildCodeBlock { wrap( "%T.${currentSerializationType.memberName}.parseJson(strState).jsonObject.apply", ktx.serialization.json.Json.className ) { properties .filter { property -> property.name.toGetterName() in annotatedGetterNames } .onEach { property -> if (property.returnType == null) { return@onEach } val isLateInit = property.returnType.name == caf.observable.LateinitObservable.fullName conditionalWrapIf(isLateInit, "containsKey(%S)", property.name) { val propertyTypeArg = property.returnType.arguments[0].type val propertyGetter = when { propertyTypeArg.isSimpleType() -> "getPrimitive" propertyTypeArg.isSerializable() -> if (propertyTypeArg.nullable) "getObjectOrNull" else "getObject" propertyTypeArg.isSerializableList() -> if (propertyTypeArg.nullable) "getArrayOrNull" else "getArray" else -> { error("property type ${propertyTypeArg.name} is not supported") throw fatal() } } wrap("presenter.${property.name}.value = $propertyGetter(%S).let", property.name) { generateDeserializer("it", propertyTypeArg) } } } } } return FunSpec.builder("onRestoreInstanceState") .addModifiers(KModifier.OVERRIDE) .addParameter("strState", kt.String.className) .addCode(block) .build() } protected fun CodeBlock.Builder.generateSerializer(propertyGetter: String, type: KotlinType) { when { type.isSimpleType() -> { conditionalWrapIfElse(type.nullable, "$propertyGetter != null", propertyGetter, next = { if (type.isInt() || type.isLong() || type.isFloat() || type.isDouble()) { addStatement("($propertyGetter!! as Number)") } else { addStatement("$propertyGetter!!") } }, nextElse = { addStatement("null") }) } type.isSerializable() -> { conditionalWrapIfElse(type.nullable, "$propertyGetter != null", next = { addStatement( "%T.${currentSerializationType.memberName}.toJson(%T.serializer(), $propertyGetter!!)", ktx.serialization.json.Json.className, type.toTypeName().nullable(false) ) }, nextElse = { addStatement("%T", ktx.serialization.json.JsonNull.className) }) } type.isSerializableList() -> { conditionalWrapIfElse(type.nullable, "$propertyGetter != null", next = { wrap("%M", ktx.serialization.json.jsonArray.memberName) { wrapLambda("$propertyGetter!!.forEach") { add("+ ") generateSerializer("it", type.arguments[0].type) } } }, nextElse = { addStatement("%T", ktx.serialization.json.JsonNull.className) }) } else -> { error("Unable to generate serializer for type ${type.name}") throw fatal() } } } protected fun CodeBlock.Builder.generateDeserializer(propertyGetter: String, type: KotlinType) { when { type.isInt() -> if (type.nullable) { addStatement("$propertyGetter.primitive.intOrNull") } else { addStatement("$propertyGetter.primitive.int") } type.isFloat() -> if (type.nullable) { addStatement("$propertyGetter.primitive.floatOrNull") } else { addStatement("$propertyGetter.primitive.float") } type.isLong() -> if (type.nullable) { addStatement("$propertyGetter.primitive.longOrNull") } else { addStatement("$propertyGetter.primitive.long") } type.isDouble() -> if (type.nullable) { addStatement("$propertyGetter.primitive.doubleOrNull") } else { addStatement("$propertyGetter.primitive.double") } type.isString() -> if (type.nullable) { addStatement("$propertyGetter.primitive.contentOrNull") } else { addStatement("$propertyGetter.primitive.content") } type.isBoolean() -> if (type.nullable) { addStatement("$propertyGetter.primitive.booleanOrNull") } else { addStatement("$propertyGetter.primitive.boolean") } type.isSerializable() -> { conditionalWrapIfElse( type.nullable, "$propertyGetter != null && !$propertyGetter!!.isNull", next = { addStatement( "%T.${currentSerializationType.memberName}.fromJson(%T.serializer(), $propertyGetter)", ktx.serialization.json.Json.className, type.toTypeName().nullable(false) ) }, nextElse = { addStatement("null") }) } type.isSerializableList() -> { conditionalWrapIfElse( type.nullable, "$propertyGetter != null && !$propertyGetter!!.isNull", next = { wrap("it.jsonArray.map") { generateDeserializer("it", type.arguments[0].type) } }, nextElse = { addStatement("null") }) } } } private inline fun <reified T : Annotation> KotlinClass.getAnnotatedGetterNames() = jvmTypeElement .enclosedElements .filter { it.getAnnotation(T::class.java) != null } .mapNotNull { val getterName = it.simpleName.split("$")[0].toGetterName() val getterElement = jvmTypeElement.enclosedElements .firstOrNull { it.simpleName.toString() == getterName } if (getterElement == null) { error("No public getter for @Persist annotated element exists") throw RuntimeException("Fatal error") } if (!getterElement.modifiers.contains(Modifier.PUBLIC)) { error("@Persist annotation can be applied only to public getters") throw RuntimeException("Fatal error") } if (!getterElement.type.asExecutable.returnType.asDeclared.typeElement.implements(caf.observable.IMutableObservable.fullName)) { error("@Persist annotation can be applied only to properties that implements ${caf.observable.IMutableObservable.fullName}") throw RuntimeException("Fatal error") } getterElement } .map { it.simpleName.toString() } private inline fun <reified T : Annotation> KotlinClass.getAnnotatedFunctionNames() = jvmTypeElement .enclosedElements .filter { it.getAnnotation(T::class.java) != null } .mapNotNull { functionElement -> if (functionElement.kind != ElementKind.METHOD) { error("Only functions can be annotated with @InteractorExecutor") throw fatal() } functionElement as ExecutableElement val functionReturnTypeElement = functionElement.returnType?.asDeclared?.typeElement if (functionReturnTypeElement?.qualifiedName?.toString() != caf.interactor.Task.fullName) { error("@InteractorExecutor functions should return ${caf.interactor.Task.fullName} objects") throw RuntimeException("Fatal error") } functionElement } .map { it.simpleName.toString() } }
Результат
Теперь любой класс, состояние которого нужно сериализовать, можно описать слеюующим образом:
@Presenter class SomeClass { @Persist val value1 = Observable<Int>(20) @Persist val value2 = Observable<List<String>>(listOf()) }
Следует отметить, что данный генератор разрабатывался под определенную задачу, в которой состояние хранилось в объектах класса Observable, поэтому он обрабатывает только свойства с типом этих классов. Класс, объекты которого требуют сериализации, помечаются аннотацией @Presenter, свойства, требующие сериализации отмечаются аннотацией @Persist. Для успешной кодогенерации аннотированные свойства должны отмечать следующим критериям:
- Свойство должно иметь публичный getter
- Тип свойства толжен реализовывать интерфейс IMutableObservable (его описание выходит за рамки данной статьи)
- Аргумент дженерика должен быть либо простым типом (Int, Long, Float, Double, Boolean, String), либо этот тип должен быть помечен аннотацией @Serizlizable из kotlin-serialization, либо являться списком вышеперечисленных типов
На выходе создается класс PersistableSomeClassWrapper имеющий методы onSaveInstanceState():
String
и onRestoreInstanceState(strState: String)
. Первый возвращает строку, хранящую
состояние объекта, второй - по переданной строке это состояние восстанавливает. Так же создается extension
метод SomeClass.persistableWrapper(): PersistableSomeClassWrapper
, для более удобного создания
враппера.
class PersistableSomeClassWrapper(private val presenter: SomeClass) : Persistable { override fun onSaveInstanceState(): String { val jsonRes = json { "value1" to (presenter.value1.value!! as Number) "value2" to jsonArray { presenter.value2.value!!.forEach { + it!! } } } return jsonRes.toString() } override fun onRestoreInstanceState(strState: String) { Json.indented.parseJson(strState).jsonObject.apply { presenter.value1.value = getPrimitive("value1").let { it.primitive.int } presenter.value2.value = getArray("value2").let { it.jsonArray.map { it.primitive.content } } } } } fun SomeClass.persistable(): Persistable = PersistableSomeClassWrapper(this)