О проблеме

В разработке под платформу Android с самых первых его версий существует сложность в управлении жизненным циклом activity. Activity - это один из основных компонентов Android-приложения. Он представляет один, независимый от других, экран. Во время работы приложения операционная система может выгружать activity из оперативной памяти в следующих ситуациях ситуациях:

Объект activity является jvm классом, который пересоздается при наступлении вышеописанных условий, следовательно все поля этого объекта теряют свои актуальные значения.

Стандартное решение

Для решения данной проблемы разработчики добавили в базовый класс activity дополнительный callback метод onSaveInstanceState. Чтобы лучше понять принцип работы этого механизма рассмотрим диаграммы, иллюстрирующие жизненный цикл activity (рис. 1):

Рисунок 1 – Жизненный цикл activity

В случае уничтожения объекта системой после вызова onStop и перед вызовом onDestroy вызывается метод onSaveInstanceState, который позволяет сохранить необходимые данные. Далее при пересоздании в метод onCreate передается объект bundle в который ранее данные были сохранены. Однако, сохранение информации в bundle приводит к необходимости написания однотипного кода, что чревато ошибками на этапе сопровождния.

Рисунок 2 – Логгирование методов жизненного цикла activity (анимация: 16 кадров, циклов повторения – замкнутый, размер 377 Кб)

Кодогенерация

Так как код сохранения/восстановления состояния достаточно однотипен, его можно сгенерировать с помощью 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. Для успешной кодогенерации аннотированные свойства должны отмечать следующим критериям:

На выходе создается класс 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)