Cómo diseñar un relleno de datos incrementales en una aplicación móvil

¡Hola! Mi nombre es Vita Sokolova, soy líder del equipo de Android en Surf .



En las aplicaciones móviles, hay formularios con un llenado complejo de varios pasos, por ejemplo, cuestionarios o solicitudes. El diseño de tales características generalmente causa un dolor de cabeza a los desarrolladores: se transfiere una gran cantidad de datos entre pantallas y se forman conexiones rígidas: quién, a quién, en qué orden deben transmitirse estos datos y qué pantalla abrir después de sí misma.



En este artículo, compartiré una forma conveniente de organizar el trabajo de una función paso a paso. Con su ayuda, es posible minimizar las conexiones entre pantallas y realizar fácilmente cambios en el orden de los pasos: agregar nuevas pantallas, cambiar su secuencia y la lógica de visualización al usuario.







* Por la palabra "función" en este artículo, me refiero a un conjunto de pantallas en una aplicación móvil que están conectadas lógicamente y representan una función para el usuario.



Por lo general, completar formularios y enviar solicitudes en aplicaciones móviles consta de varias pantallas secuenciales. Es posible que se necesiten datos de una pantalla en otra, y las cadenas de pasos a veces cambian según las respuestas. Por lo tanto, es útil permitir que el usuario guarde los datos "en borrador", para que pueda volver al proceso más tarde.



Puede haber muchas pantallas, pero de hecho el usuario completa un objeto grande con datos. En este artículo te diré cómo organizar cómodamente el trabajo con una cadena de pantallas que son un escenario.



Supongamos que un usuario solicita un trabajo y completa un formulario. Si se interrumpe en el medio, los datos ingresados ​​se guardarán en el borrador. Cuando el usuario vuelva a completar, la información del borrador se sustituirá automáticamente en los campos del cuestionario; no es necesario que complete todo desde cero.



Cuando el usuario complete todo el cuestionario, su respuesta será enviada al servidor.



El cuestionario consta de:



  • Paso 1: nombre completo, tipo de educación, experiencia laboral,

  • Paso 2 - lugar de estudio

  • Paso 3: lugar de trabajo o ensayo sobre usted,

  • Paso 4: las razones por las que la vacante está interesada.









El cuestionario cambiará dependiendo de si el usuario tiene educación y experiencia laboral. Si no hay educación, excluiremos el paso con llenar el lugar de estudio. Si no tiene experiencia laboral, pídale al usuario que escriba un poco sobre sí mismo.







En la etapa de diseño, tenemos que responder a varias preguntas:



  • Cómo hacer que el script de funciones sea flexible y poder agregar y eliminar pasos fácilmente.

  • Cómo asegurarse de que al abrir un paso, los datos necesarios ya estén llenos (por ejemplo, la pantalla "Educación" está esperando un tipo de educación ya conocido en la entrada para reconstruir la composición de sus campos).

  • Cómo agregar datos en un modelo común para transferirlos al servidor después del paso final.

  • Cómo guardar la aplicación en "borrador" para que el usuario pueda interrumpir el llenado y volver a él más tarde.



Como resultado, queremos obtener la siguiente funcionalidad: El







ejemplo completo está en mi repositorio en GitHub. 



Una solución obvia



Si desarrolla una función "en modo de ahorro de energía total", lo más obvio es crear un objeto de aplicación y transferirlo de pantalla a pantalla, rellenándolo en cada paso.



El color gris claro marcará los datos que no se necesitan en un paso en particular. Al mismo tiempo, se transmiten a cada pantalla para eventualmente ingresar a la aplicación final.







Por supuesto, todos estos datos deben empaquetarse en un objeto de aplicación. Veamos cómo quedará:



class Application(
    val name: String?,
    val surname: String?,
    val educationType : EducationType?,
    val workingExperience: Boolean?
    val education: Education?,
    val experience: Experience?,
    val motivation: List<Motivation>?
)


¡PERO!

Trabajando con tal objeto, condenamos nuestro código a ser cubierto con un número adicional innecesario de comprobaciones nulas. Por ejemplo, esta estructura de datos no garantiza de ninguna manera que el campo educationTypeya esté completado en la pantalla "Educación".



Como hacerlo mejor



Recomiendo mover la gestión de datos a un objeto separado, que proporcionará los datos no anulables necesarios como entrada para cada paso y guardará el resultado de cada paso en un borrador. A este objeto lo llamaremos interactor. Corresponde a la capa de Caso de Uso de la arquitectura pura de Robert Martin y para todas las pantallas se encarga de proporcionar datos recopilados de diversas fuentes (red, base de datos, datos de pasos anteriores, datos de un borrador de propuesta ...).



En nuestros proyectos, en Surf usamos Dagger. Por una serie de razones, es habitual hacer que los interactores @PerApplication alcancen: esto hace que nuestro interactor sea un singleton dentro de la aplicación. De hecho, el interactor puede ser un singleton dentro de una función, o incluso una activación, si todos sus pasos son fragmentos. Todo depende de la arquitectura general de su aplicación.



Más adelante en los ejemplos, asumiremos que tenemos una única instancia del interactor para toda la aplicación. Por lo tanto, todos los datos deben borrarse cuando finaliza el script.







Al configurar la tarea, además del almacenamiento centralizado de datos, queríamos organizar una gestión fácil de la composición y el orden de los pasos en la aplicación: dependiendo de lo que el usuario ya haya completado, pueden cambiar. Por lo tanto, necesitamos una entidad más: el escenario. Su área de responsabilidad es mantener el orden de los pasos que debe seguir el usuario.



La organización de una función paso a paso utilizando scripts y un interactor permite:



  • Es sencillo cambiar los pasos en el script: por ejemplo, superponer trabajo adicional, si durante la ejecución resulta que el usuario no puede enviar solicitudes o agregar pasos si se necesita más información.

  • Establecer contratos: qué datos deben estar en la entrada y salida de cada paso.

  • Organice guardar la aplicación en un borrador si el usuario no ha completado todas las pantallas.



Rellene previamente las pantallas con los datos guardados en borrador.



Entidades básicas



El mecanismo de la función consistirá en:



  • Un conjunto de modelos para describir un paso, entradas y salidas.

  • Escenario: entidad que describe qué pasos (pantallas) debe seguir el usuario.

  • Interaktora (ProgressInteractor): una clase responsable de almacenar información sobre el paso activo actual, agregar la información completa después de completar cada paso y emitir datos de entrada para iniciar un nuevo paso.

  • Draft (ApplicationDraft): una clase responsable de almacenar información completa. 



El diagrama de clases representa todas las entidades subyacentes de las que heredarán implementaciones concretas. Veamos cómo se relacionan.







Para la entidad Escenario, estableceremos una interfaz en la que describiremos qué lógica esperamos para cualquier escenario en la aplicación (contener una lista de pasos necesarios y reconstruirla después de completar el paso anterior, si es necesario.



La aplicación puede tener varias características, que consisten en muchas pantallas secuenciales, y cada una será Moviremos toda la lógica general que no depende de la característica o datos específicos a la clase base ProgressInteractor.



ApplicationDraft no está presente en las clases base, ya que es posible que no sea necesario guardar los datos que el usuario ha completado en un borrador. Por lo tanto, una implementación concreta de ProgressInteractor funcionará con el borrador. Los presentadores de pantalla también interactuarán con él.



Diagrama de clases para implementaciones específicas de clases base:







Todas estas entidades interactuarán entre sí y con los presentadores de pantalla de la siguiente manera: Hay







bastantes clases, así que analicemos cada bloque por separado usando el ejemplo de la característica del principio del artículo.



Descripción de pasos



Empecemos por el primer punto. Necesitamos entidades para describir los pasos:



// ,   ,    

interface Step




Para la función de nuestro ejemplo de solicitud de empleo, los pasos son los siguientes:



/**
 *     
 */
enum class ApplicationSteps : Step {
    PERSONAL_INFO,  //  
    EDUCATION,      // 
    EXPERIENCE,     //  
    ABOUT_ME,       //  " "
    MOTIVATION      //     
}



También necesitamos describir los datos de entrada para cada paso. Para hacer esto, usaremos clases selladas para su propósito previsto: crear una jerarquía de clases limitada.







Cómo se verá en el código
//   
interface StepInData


:



//,      
sealed class ApplicationStepInData : StepInData

//     
class EducationStepInData(val educationType: EducationType) : ApplicationStepInData()

//        
class MotivationStepInData(val values: List<Motivation>) : ApplicationStepInData()




Describimos la salida de manera similar:







Cómo se verá en el código
// ,   
interface StepOutData

//,    
sealed class ApplicationStepOutData : StepOutData

//    " "
class PersonalInfoStepOutData(
    val info: PersonalInfo
) : ApplicationStepOutData()

//   ""
class EducationStepOutData(
    val education: Education
) : ApplicationStepOutData()

//    " "
class ExperienceStepOutData(
    val experience: WorkingExperience
) : ApplicationStepOutData()

//   " "
class AboutMeStepOutData(
    val info: AboutMe
) : ApplicationStepOutData()

//   " "
class MotivationStepOutData(
    val motivation: List<Motivation>
) : ApplicationStepOutData()




Si no nos fijamos el objetivo de mantener las solicitudes sin completar en borradores, podríamos limitarnos a esto. Pero dado que cada pantalla puede abrirse no solo vacía, sino también llena desde el borrador, tanto los datos de entrada como los datos del borrador llegarán a la entrada del interactor, si el usuario ya ha ingresado algo.



Por lo tanto, necesitamos otro conjunto de modelos para reunir estos datos. Algunos pasos no necesitan información para ingresar y solo proporcionan un campo para los datos del borrador



Cómo se verá en el código
/**
 *     +   ,   
 */
interface StepData<I : StepInData, O : StepOutData>

sealed class ApplicationStepData : StepData<ApplicationStepInData,  ApplicationStepOutData> {
    class PersonalInfoStepData(
        val outData: PersonalInfoStepOutData?
    ) : ApplicationStepData()

    class EducationStepData(
        val inData: EducationStepInData,
        val outData: EducationStepOutData?
    ) : ApplicationStepData()

    class ExperienceStepData(
        val outData: ExperienceStepOutData?
    ) : ApplicationStepData()

    class AboutMeStepData(
        val outData: AboutMeStepOutData?
    ) : ApplicationStepData()

    class MotivationStepData(
        val inData: MotivationStepInData,
        val outData: MotivationStepOutData?
    ) : ApplicationStepData()
}




Actuamos según el guión



Con la descripción de los pasos y los datos de entrada / salida ordenados. Ahora arreglemos el orden de estos pasos en el script de funciones en el código. La entidad Escenario es responsable de administrar el orden actual de pasos. El guión se verá así:



/**
 * ,     ,     
 */
interface Scenario<S : Step, O : StepOutData> {
    
    //  
    val steps: List<S>

    /**
     *     
     *        
     */
    fun reactOnStepCompletion(stepOut: O)
}


En la implementación de nuestro ejemplo, el script será así:



class ApplicationScenario : Scenario<ApplicationStep, ApplicationStepOutData> {

    override val steps: MutableList<ApplicationStep> = mutableListOf(
        PERSONAL_INFO,
        EDUCATION,
        EXPERIENCE,
        MOTIVATION
    )

    override fun reactOnStepCompletion(stepOut: ApplicationStepOutData) {
        when (stepOut) {
            is PersonalInfoStepOutData -> {
                changeScenarioAfterPersonalStep(stepOut.info)
            }
        }
    }

    private fun changeScenarioAfterPersonalStep(personalInfo: PersonalInfo) {
        applyExperienceToScenario(personalInfo.hasWorkingExperience)
        applyEducationToScenario(personalInfo.education)
    }

    /**
     *    -       
     */
    private fun applyEducationToScenario(education: EducationType) {...}

    /**
     *      ,
     *           
     */
    private fun applyExperienceToScenario(hasWorkingExperience: Boolean) {...}
}


Debe tenerse en cuenta que cualquier cambio en el guión debe ser bidireccional. Digamos que quitas un paso. Asegúrese de que si el usuario regresa y selecciona una opción diferente, el paso se agrega al script.



¿Cómo, por ejemplo, se parece el código a la reacción a la presencia o ausencia de experiencia laboral?
/**
 *      ,
 *           
 */
private fun applyExperienceToScenario(hasWorkingExperience: Boolean) {
    if (hasWorkingExperience) {
        steps.replaceWith(
            condition = { it == ABOUT_ME },
            newElem = EXPERIENCE
        )
    } else {
        steps.replaceWith(
            condition = { it == EXPERIENCE },
            newElem = ABOUT_ME
        )
    }
}




Cómo funciona Interactor



Considere el siguiente bloque de construcción en la arquitectura de una función paso a paso: un interactor. Como dijimos anteriormente, su principal responsabilidad es atender el cambio entre pasos: proporcionar los datos necesarios a la entrada de los pasos y agregar los datos de salida en una solicitud de borrador.



Creemos una clase base para nuestro interactor y pongamos en ella el comportamiento común a todas las funciones paso a paso.



/**
 *      
 * S -  
 * I -    
 * O -    
 */
abstract class ProgressInteractor<S : Step, I : StepInData, O : StepOutData> 


El interactor debe trabajar con el escenario actual: notificarle sobre la finalización del siguiente paso para que el escenario pueda reconstruir su conjunto de pasos. Por lo tanto, declararemos un campo abstracto para nuestro script. Ahora se requerirá que cada interactor específico proporcione su propia implementación.



// ,      
protected abstract val scenario: Scenario<S, O>


El interactor también es responsable de almacenar el estado de qué paso está actualmente activo y cambiar al siguiente o al anterior. Debe notificar de inmediato a la pantalla raíz del cambio de paso para que pueda cambiar al fragmento deseado. Todo esto se puede organizar fácilmente mediante la transmisión de eventos, es decir, un enfoque reactivo. Además, los métodos de nuestro interactor a menudo realizarán operaciones asincrónicas (cargando datos desde la red o una base de datos), por lo que usaremos RxJava para comunicarnos con el interactor con los presentadores. Si aún no está familiarizado con esta herramienta, lea esta serie de artículos introductorios



Creemos un modelo que describa la información requerida por las pantallas sobre el paso actual y su posición en el guión:



/**
 *         
 */
class StepWithPosition<S : Step>(
    val step: S,
    val position: Int,
    val allStepsCount: Int
)


Iniciemos un BehaviorSubject en el interactor para emitir libremente información sobre el nuevo paso activo en él.



private val stepChangeSubject = BehaviorSubject.create<StepWithPosition<S>>()


Para que las pantallas puedan suscribirse a este flujo de eventos, crearemos una variable pública stepChangeObservable, que es un contenedor sobre nuestro stepChangeSubject.



val stepChangeObservable: Observable<StepWithPosition<S>> = stepChangeSubject.hide()


Durante el trabajo del interactor, a menudo es necesario conocer la posición del paso activo actual. Recomiendo crear una propiedad separada en el interactor - currentStepIndex y anular los métodos get () y set (). Esto nos brinda un acceso conveniente a esta información del tema.



Cómo se ve en el código
//   
private var currentStepIndex: Int
    get() = stepChangeSubject.value?.position ?: 0
    set(value) {
        stepChangeSubject.onNext(
            StepWithPosition(
                step = scenario.steps[value],
                position = value,
                allStepsCount = scenario.steps.count()
            )
        )
    }




Escribamos una parte general que funcionará igual independientemente de la implementación específica del interactor para la función.



Agreguemos métodos para inicializar y terminar el trabajo del interactor, haciéndolos abiertos para extensión en descendientes:



Métodos de inicialización y apagado
/**
 *   
 */
@CallSuper
open fun initProgressFeature() {
    currentStepIndex = 0
}

/**
 *   
 */
@CallSuper
open fun closeProgressFeature() {
    currentStepIndex = 0
}




Agreguemos las funciones que debe realizar cualquier interactor de características paso a paso:



  • getDataForStep (paso: S): proporciona datos como entrada para el paso S;

  • completeStep (stepOut: O): guarde la salida O y mueva el script al siguiente paso;

  • toPreviousStep (): mueve el script al paso anterior.



Comencemos con la primera función: procesar datos de entrada. Cada interactor determinará por sí mismo cómo y de dónde obtener los datos de entrada. Agreguemos un método abstracto responsable de esto:



/**
 *      
 */
protected abstract fun resolveStepInData(step: S): Single<out StepData<I, O>>




Para presentadores de pantallas específicas, agregue un método público que llamará resolveStepInData() :



/**
 *     
 */
fun getDataForStep(step: S): Single<out StepData<I, O>> = resolveStepInData(step)


Puede simplificar este código haciendo público el método resolveStepInData(). El método se getDataForStep()agrega por analogía con los métodos de finalización de pasos, que discutiremos a continuación.



Para completar un paso, creamos de manera similar un método abstracto en el que cada interactor específico guardará el resultado del paso.



/**
 *      
 */
protected abstract fun saveStepOutData(stepData: O): Completable


Y un método público. En él llamaremos al guardado de la información de salida. Cuando termine, dígale al guión que se ajuste a la información del paso final. También notificaremos a los suscriptores que estamos avanzando un paso.



/**
 *       
 */
fun completeStep(stepOut: O): Completable {
    return saveStepOutData(stepOut).doOnComplete {
        scenario.reactOnStepCompletion(stepOut)
        if (currentStepIndex != scenario.steps.lastIndex) {
            currentStepIndex += 1
        }
    }
}


Finalmente, implementamos un método para volver al paso anterior.



/**
 *    
 */
fun toPreviousStep() {
    if (currentStepIndex != 0) {
        currentStepIndex -= 1
    }
}


Veamos la implementación del interactor para nuestro ejemplo de solicitud de empleo. Como recordamos, es importante para nuestra función guardar datos en una solicitud de borrador, por lo tanto, en la clase ApplicationProgressInteractor, crearemos un campo adicional debajo del borrador.



/**
 *    
 */
@PerApplication
class ApplicationProgressInteractor @Inject constructor(
    private val dataRepository: ApplicationDataRepository
) : ProgressInteractor<ApplicationSteps, ApplicationStepInData, ApplicationStepOutData>() {

    //  
    override val scenario = ApplicationScenario()

    //  
    private val draft: ApplicationDraft = ApplicationDraft()

    //  
    fun applyDraft(draft: ApplicationDraft) {
        this.draft.apply {
            clear()
            outDataMap.putAll(draft.outDataMap)
        }
    }
    ...
}


Cómo se ve una clase de borrador
:



/**
 *  
 */
class ApplicationDraft(
    val outDataMap: MutableMap<ApplicationSteps, ApplicationStepOutData> = mutableMapOf()
) : Serializable {
    fun getPersonalInfoOutData() = outDataMap[PERSONAL_INFO] as? PersonalInfoStepOutData
    fun getEducationStepOutData() = outDataMap[EDUCATION] as? EducationStepOutData
    fun getExperienceStepOutData() = outDataMap[EXPERIENCE] as? ExperienceStepOutData
    fun getAboutMeStepOutData() = outDataMap[ABOUT_ME] as? AboutMeStepOutData
    fun getMotivationStepOutData() = outDataMap[MOTIVATION] as? MotivationStepOutData

    fun clear() {
        outDataMap.clear()
    }
}




Comencemos a implementar los métodos abstractos declarados en la clase padre. Comencemos con la función de finalización de pasos, es bastante simple con ella. Guardamos los datos de salida de un cierto tipo en un borrador bajo la clave requerida:



/**
 *      
 */
override fun saveStepOutData(stepData: ApplicationStepOutData): Completable {
    return Completable.fromAction {
        when (stepData) {
            is PersonalInfoStepOutData -> {
                draft.outDataMap[PERSONAL_INFO] = stepData
            }
            is EducationStepOutData -> {
                draft.outDataMap[EDUCATION] = stepData
            }
            is ExperienceStepOutData -> {
                draft.outDataMap[EXPERIENCE] = stepData
            }
            is AboutMeStepOutData -> {
                draft.outDataMap[ABOUT_ME] = stepData
            }
            is MotivationStepOutData -> {
                draft.outDataMap[MOTIVATION] = stepData
            }
        }
    }
}


Ahora veamos el método para obtener datos de entrada para un paso:



/**
 *     
 */
override fun resolveStepInData(step: ApplicationStep): Single<ApplicationStepData> {
    return when (step) {
        PERSONAL_INFO -> ...
        EXPERIENCE -> ...
        EDUCATION -> Single.just(
            EducationStepData(
                inData = EducationStepInData(
                    draft.getPersonalInfoOutData()?.info?.educationType
                    ?: error("Not enough data for EDUCATION step")
                ),
                outData = draft.getEducationStepOutData()
            )
        )
        ABOUT_ME -> Single.just(
            AboutMeStepData(
                outData = draft.getAboutMeStepOutData()
            )
        )
        MOTIVATION -> dataRepository.loadMotivationVariants().map { reasonsList ->
            MotivationStepData(
                inData = MotivationStepInData(reasonsList),
                outData = draft.getMotivationStepOutData()
            )
        }
    }
}


Al abrir un paso, hay dos opciones:



  • el usuario abre la pantalla por primera vez;

  • el usuario ya ha llenado la pantalla y hemos guardado datos en el borrador.



Para los pasos que no requieren nada para ingresar, pasaremos la información del borrador (si corresponde). 



 ABOUT_ME -> Single.just(
            AboutMeStepData(
                stepOutData = draft.getAboutMeStepOutData()
            )
        )


Si necesitamos datos de pasos anteriores como entrada, los sacaremos del borrador (nos aseguramos de guardarlos allí al final de cada paso). Y de manera similar, transferiremos datos a outData que se pueden usar para llenar la pantalla.



EDUCATION -> Single.just(
    EducationStepData(
        inData = EducationStepInData(
            draft.getPersonalInfoOutData()?.info?.educationType
            ?: error("Not enough data for EDUCATION step")
        ),
        outData = draft.getEducationStepOutData()
    )
)


También hay una situación más interesante: el último paso, donde es necesario indicar por qué el usuario está interesado en esta vacante en particular, requiere una lista de posibles motivos para ser descargado de la red. Este es uno de los momentos más convenientes de esta arquitectura. Podemos enviar una solicitud y, cuando recibamos una respuesta, combinarla con los datos del borrador y enviarla a la pantalla como entrada. La pantalla ni siquiera necesita saber de dónde provienen los datos y cuántas fuentes está recopilando.



MOTIVATION -> {
    dataRepository.loadMotivationVariants().map { reasonsList ->
        MotivationStepData(
            inData = MotivationStepInData(reasonsList),
            outData = draft.getMotivationStepOutData()
        )
    }
}




Este tipo de situaciones son otro argumento a favor de trabajar a través de interactores. A veces, para proporcionar un paso con datos, es necesario combinar varias fuentes de datos, por ejemplo, una descarga de la web y los resultados de los pasos anteriores.



En nuestro método, podemos combinar datos de muchas fuentes y proporcionar a la pantalla todo lo que necesitamos. Puede ser difícil tener una idea de por qué esto es genial en este ejemplo. En formularios reales, por ejemplo, al solicitar un préstamo, la pantalla potencialmente necesita enviar una gran cantidad de libros de referencia, información sobre el usuario de la base de datos interna, datos que completó 5 pasos atrás y una colección de las anécdotas más populares de 1970.



El código del presentador es mucho más fácil cuando la agregación se realiza mediante un método interactor separado que produce solo el resultado: datos o un error. Es más fácil para los desarrolladores realizar cambios y ajustes si está claro de inmediato dónde buscar todo.



Pero eso no es todo lo que hay en el interactor. Por supuesto, necesitamos un método para enviar la solicitud final, cuando se hayan pasado todos los pasos. Describamos la aplicación final y la capacidad de crearla usando el patrón "Builder"



Clase para presentar la solicitud final
/**
 *  
 */
class Application(
    val personal: PersonalInfo,
    val education: Education?,
    val experience: Experience,
    val motivation: List<Motivation>
) {

    class Builder {
        private var personal: Optional<PersonalInfo> = Optional.empty()
        private var education: Optional<Education?> = Optional.empty()
        private var experience: Optional<Experience> = Optional.empty()
        private var motivation: Optional<List<Motivation>> = Optional.empty()

        fun personalInfo(value: PersonalInfo) = apply { personal = Optional.of(value) }
        fun education(value: Education) = apply { education = Optional.of(value) }
        fun experience(value: Experience) = apply { experience = Optional.of(value) }
        fun motivation(value: List<Motivation>) = apply { motivation = Optional.of(value) }

        fun build(): Application {
            return try {
                Application(
                    personal.get(),
                    education.getOrNull(),
                    experience.get(),
                    motivation.get()
                )
            } catch (e: NoSuchElementException) {
                throw ApplicationIsNotFilledException(
                    """Some fields aren't filled in application
                        personal = {${personal.getOrNull()}}
                        experience = {${experience.getOrNull()}}
                        motivation = {${motivation.getOrNull()}}
                    """.trimMargin()
                )
            }
        }
    }
}




El método de envío de la aplicación en sí:



/**
 *  
 */
fun sendApplication(): Completable {
    val builder = Application.Builder().apply {
        draft.outDataMap.values.forEach { data ->
            when (data) {
                is PersonalInfoStepOutData -> personalInfo(data.info)
                is EducationStepOutData -> education(data.education)
                is ExperienceStepOutData -> experience(data.experience)
                is AboutMeStepOutData -> experience(data.info)
                is MotivationStepOutData -> motivation(data.motivation)
            }
        }
    }
    return dataRepository.loadApplication(builder.build())
}


Cómo usarlo todo en pantallas



Ahora vale la pena bajar al nivel de presentación y ver cómo interactúan los presentadores de pantalla con este interactor.



Nuestra función es una actividad con una pila de fragmentos en su interior.







El envío exitoso de la solicitud abre una actividad separada, donde se informa al usuario sobre el éxito del envío. La actividad principal será la encargada de mostrar el fragmento deseado, dependiendo del comando del interactor, y también de mostrar cuántos pasos ya se han dado en la barra de herramientas. Para hacer esto, en el presentador de actividad raíz, suscríbase al tema desde el interactor e implemente la lógica para cambiar fragmentos en la pila.



progressInteractor.stepChangeObservable.subscribe { stepData ->
    if (stepData.position > currentPosition) {
        //      FragmentManager
    } else {
        //   
    }
    //   -    
}


Ahora en el presentador de cada fragmento, al inicio de la pantalla, le pediremos al interactor que nos dé datos de entrada. Es mejor transferir los datos de recepción a un flujo separado porque, como se mencionó anteriormente, se puede asociar con la descarga de la red.



Por ejemplo, tomemos la pantalla para completar la información educativa.



progressInteractor.getDataForStep(EducationStep)
    .filter<ApplicationStepData.EducationStepData>()
    .subscribeOn(Schedulers.io())
    .subscribe { 
        val educationType = it.stepInData.educationType
 // todo:         

 it.stepOutData?.education?.let {
       // todo:      
  }
    }


Supongamos que completamos el paso "sobre educación" y el usuario quiere ir más allá. Todo lo que tenemos que hacer es formar un objeto con la salida y pasarlo al interactor.



progressInteractor.completeStep(EducationStepOutData(education)).subscribe {
                   //     ( )
               }


El interactor guardará los datos por sí mismo, iniciará cambios en el script, si es necesario, y le indicará a la actividad raíz que cambie al siguiente paso. Por lo tanto, los fragmentos no saben nada sobre su posición en el guión y pueden reorganizarse fácilmente si, por ejemplo, el diseño de una característica ha cambiado.



En el último fragmento, como reacción al exitoso guardado de datos, agregaremos el envío de la solicitud final, como recordamos, creamos un método para esto sendApplication()en el interactor.



progressInteractor.sendApplication()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(
                {
                    //    
                    activityNavigator.start(ThankYouRoute())
                },
                {
                    //  
                }
            )


En la pantalla final con información de que la solicitud se ha enviado correctamente, borraremos el interactor para que el proceso se pueda reiniciar desde cero.



progressInteractor.closeProgressFeature()


Eso es todo. Tenemos una función que consta de cinco pantallas. La pantalla "sobre educación" se puede omitir, la pantalla con completar la experiencia laboral - reemplazada por una pantalla para escribir un ensayo. Podemos interrumpir el llenado en cualquier paso y continuar más tarde, y todo lo que ingresamos se guardará en el borrador.



Un agradecimiento especial a Vasya Beglyanin @icebail , el autor de la primera implementación de este enfoque en el proyecto. Y también a Misha Zinchenko @midery , por su ayuda para llevar el borrador de la arquitectura a la versión final, que se describe en este artículo.



All Articles