Em aplicativos móveis, existem formulários com preenchimento complexo de várias etapas - por exemplo, questionários ou aplicativos. O design de tais recursos geralmente causa uma dor de cabeça para os desenvolvedores: uma grande quantidade de dados é transferida entre telas e conexões rígidas são formadas - quem, para quem, em que ordem esses dados devem ser transmitidos e qual tela abrir depois dela.
Neste artigo, compartilharei uma maneira conveniente de organizar o trabalho de um recurso passo a passo. Com a sua ajuda, é possível minimizar as conexões entre as telas e facilmente fazer alterações na ordem dos passos: adicionar novas telas, alterar sua sequência e a lógica de exibição ao usuário.
* Com a palavra "recurso" neste artigo, quero dizer um conjunto de telas em um aplicativo móvel que estão logicamente conectadas e representam uma função para o usuário.
Normalmente, o preenchimento de questionários e o envio de aplicativos em aplicativos móveis consiste em várias telas sequenciais. Os dados de uma tela podem ser necessários em outra, e as cadeias de etapas às vezes mudam dependendo das respostas. Portanto, é útil permitir ao usuário salvar os dados "em rascunho" para que ele possa retornar ao processo posteriormente.
Pode haver muitas telas, mas na verdade o usuário preenche um grande objeto com dados. Neste artigo, explicarei como organizar convenientemente o trabalho com uma cadeia de telas que são um cenário.
Imagine que um usuário se inscreve para um emprego e preenche um questionário. Se for interrompido no meio, os dados inseridos serão salvos no rascunho. Quando o usuário voltar a preencher, as informações do rascunho serão automaticamente substituídas nos campos do questionário - ele não precisa preencher tudo do zero.
Quando o usuário preencher todo o questionário, sua resposta será enviada para o servidor.
O questionário consiste em:
- Etapa 1 - nome, tipo de educação, experiência de trabalho,
- Etapa 2 - local de estudo
- Etapa 3 - local de trabalho ou ensaio sobre você,
- Etapa 4 - os motivos do interesse da vaga.
O questionário mudará dependendo se o usuário possui educação e experiência de trabalho. Se não houver educação, excluiremos a etapa com o preenchimento do local de estudo. Se não houver experiência de trabalho, peça ao usuário para escrever um pouco sobre si mesmo.
Na fase de design, temos que responder a várias perguntas:
- Como tornar o script de recurso flexível e ser capaz de adicionar e remover etapas facilmente.
- Como garantir que ao abrir uma etapa, os dados necessários já estarão preenchidos (por exemplo, a tela "Educação" está aguardando um tipo de ensino já conhecido na entrada para reconstruir a composição de seus campos).
- Como agregar dados em um modelo comum para transferir para o servidor após a etapa final.
- Como salvar a aplicação em "rascunho" para que o usuário interrompa o preenchimento e volte a ela posteriormente.
Como resultado, queremos obter a seguinte funcionalidade: O
exemplo completo está em meu repositório no GitHub
Uma solução óbvia
Se você desenvolver um recurso "no modo de economia de energia total", o mais óbvio é criar um objeto aplicativo e transferi-lo de uma tela para outra, recarregando-o a cada etapa.
A cor cinza claro marcará os dados que não são necessários em uma etapa específica. Ao mesmo tempo, são transmitidos a cada tela para eventualmente entrar na aplicação final.
Claro, todos esses dados devem ser compactados em um objeto de aplicativo. Vamos ver como ficará:
class Application(
val name: String?,
val surname: String?,
val educationType : EducationType?,
val workingExperience: Boolean?
val education: Education?,
val experience: Experience?,
val motivation: List<Motivation>?
)
MAS!
Trabalhando com tal objeto, condenamos nosso código a ser coberto com um número extra desnecessário de verificações nulas. Por exemplo, esta estrutura de dados não garante de forma alguma que o campo
educationTypejá estará preenchido na tela "Educação".
Como fazer melhor
Recomendo mover o gerenciamento de dados para um objeto separado, que fornecerá os dados não anuláveis necessários na entrada de cada etapa e salvará o resultado de cada etapa em um rascunho. Chamaremos esse objeto de interator. Corresponde à camada de Caso de Uso da arquitetura pura de Robert Martin e para todas as telas é responsável por fornecer os dados coletados de várias fontes (rede, banco de dados, dados de etapas anteriores, dados de um rascunho de proposta ...).
Em nossos projetos, nós da Surf usamos o Dagger. Por uma série de razões, é comum tornar os interatores @PerApplication escopos: isso torna nosso interator um único dentro do aplicativo. Na verdade, o interator pode ser um singleton em um recurso ou até mesmo uma ativação - se todas as suas etapas forem fragmentos. Tudo depende da arquitetura geral do seu aplicativo.
Mais adiante nos exemplos, assumiremos que temos uma única instância do interator para todo o aplicativo. Portanto, todos os dados devem ser apagados quando o script termina.
Na hora de definir a tarefa, além do armazenamento centralizado dos dados, queríamos organizar o gerenciamento fácil da composição e da ordem das etapas na aplicação: dependendo do que o usuário já preencheu, elas podem mudar. Portanto, precisamos de mais uma entidade - o Cenário. Sua área de responsabilidade é manter a ordem das etapas pelas quais o usuário deve passar.
A organização de um recurso passo a passo usando scripts e um interagente permite:
- É indolor alterar as etapas no script: por exemplo, sobrepor mais trabalho, se durante a execução, o usuário não puder enviar solicitações ou adicionar etapas se precisar de mais informações.
- Definir contratos: quais dados devem estar na entrada e na saída de cada etapa.
- Organize o salvamento do aplicativo em um rascunho se o usuário não tiver concluído todas as telas.
Preencha as telas com os dados salvos no rascunho.
Entidades básicas
O mecanismo do recurso consistirá em:
- Um conjunto de modelos para descrever uma etapa, entradas e saídas.
- Cenário - uma entidade que descreve quais etapas (telas) o usuário precisa percorrer.
- Interaktora (ProgressInteractor) - classe responsável por armazenar informações sobre a etapa ativa atual, agregando as informações preenchidas após a conclusão de cada etapa e emitindo dados de entrada para iniciar uma nova etapa.
- Rascunho (ApplicationDraft) - uma classe responsável por armazenar as informações preenchidas.
O diagrama de classes representa todas as entidades subjacentes das quais as implementações concretas herdarão. Vamos ver como eles estão relacionados.
Para a entidade Cenário, definiremos uma interface na qual descreveremos a lógica que esperamos para qualquer cenário no aplicativo (conter uma lista de etapas necessárias e reconstruí-la após concluir a etapa anterior, se necessário.
O aplicativo pode ter vários recursos consistindo em várias telas sequenciais, e cada uma irá Moveremos toda a lógica geral que não depende do recurso ou dos dados específicos para a classe base ProgressInteractor.
O ApplicationDraft não está presente nas classes base, pois pode não ser necessário salvar os dados que o usuário preencheu em um rascunho. Portanto, uma implementação concreta do ProgressInteractor funcionará com o rascunho. Apresentadores de tela também irão interagir com ele.
Diagrama de classes para implementações específicas de classes base:
Todas essas entidades irão interagir umas com as outras e com os apresentadores de tela da seguinte maneira: Existem
algumas classes, então vamos analisar cada bloco separadamente usando o recurso do início do artigo.
Descrição das etapas
Vamos começar com o primeiro ponto. Precisamos de entidades para descrever as etapas:
// , ,
interface Step
Para o recurso de nosso exemplo de formulário de emprego, as etapas são as seguintes:
/**
*
*/
enum class ApplicationSteps : Step {
PERSONAL_INFO, //
EDUCATION, //
EXPERIENCE, //
ABOUT_ME, // " "
MOTIVATION //
}
Também precisamos descrever os dados de entrada para cada etapa. Para fazer isso, usaremos classes seladas para a finalidade pretendida - criar uma hierarquia de classes limitada.
Como ficará no código
:
//
interface StepInData
:
//,
sealed class ApplicationStepInData : StepInData
//
class EducationStepInData(val educationType: EducationType) : ApplicationStepInData()
//
class MotivationStepInData(val values: List<Motivation>) : ApplicationStepInData()
Descrevemos a saída de maneira semelhante:
Como ficará no 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()
Se não definíssemos a meta de manter os aplicativos não preenchidos em rascunhos, poderíamos nos limitar a isso. Mas como cada tela pode ser aberta não apenas vazia, mas também preenchida com o rascunho, tanto os dados de entrada quanto os dados do rascunho virão para a entrada do interator - se o usuário já tiver inserido algo.
Portanto, precisamos de outro conjunto de modelos para reunir esses dados. Algumas etapas não precisam de informações para entrar e fornecem apenas um campo para os dados do rascunho
Como ficará no 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()
}
Agimos de acordo com o roteiro
Com a descrição das etapas e dados de entrada / saída ordenados. Agora vamos corrigir a ordem dessas etapas no script do recurso no código. A entidade Cenário é responsável por gerenciar a ordem atual das etapas. O script será semelhante a este:
/**
* , ,
*/
interface Scenario<S : Step, O : StepOutData> {
//
val steps: List<S>
/**
*
*
*/
fun reactOnStepCompletion(stepOut: O)
}
Na implementação de nosso exemplo, o script será assim:
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) {...}
}
Deve-se ter em mente que qualquer mudança no script deve ser bidirecional. Digamos que você remova uma etapa. Certifique-se de que, se o usuário voltar e selecionar uma opção diferente, a etapa seja adicionada ao script.
Como, por exemplo, o código parece uma reação à presença ou ausência de experiência de trabalho
/**
* ,
*
*/
private fun applyExperienceToScenario(hasWorkingExperience: Boolean) {
if (hasWorkingExperience) {
steps.replaceWith(
condition = { it == ABOUT_ME },
newElem = EXPERIENCE
)
} else {
steps.replaceWith(
condition = { it == EXPERIENCE },
newElem = ABOUT_ME
)
}
}
Como funciona o interagente
Considere o próximo bloco de construção na arquitetura de um recurso passo a passo - um interator. Como dissemos acima, sua principal responsabilidade é atender à alternância entre as etapas: fornecer os dados necessários para a entrada das etapas e agregar os dados de saída em uma solicitação de rascunho.
Vamos criar uma classe base para nosso interator e colocar nela o comportamento comum a todos os recursos passo a passo.
/**
*
* S -
* I -
* O -
*/
abstract class ProgressInteractor<S : Step, I : StepInData, O : StepOutData>
O interagente deve trabalhar com o script atual: notifique-o sobre a conclusão da próxima etapa para que o script possa reconstruir seu conjunto de etapas. Portanto, iremos declarar um campo abstrato para nosso script. Agora, cada interagente específico deverá fornecer sua própria implementação.
// ,
protected abstract val scenario: Scenario<S, O>
O interator também é responsável por armazenar o estado de qual etapa está ativa no momento e alternar para a próxima ou anterior. Ele deve notificar imediatamente a tela raiz da mudança de etapa para que possa alternar para o fragmento desejado. Tudo isso pode ser facilmente organizado usando a transmissão de eventos, ou seja, uma abordagem reativa. Além disso, os métodos de nosso interator frequentemente realizarão operações assíncronas (carregamento de dados da rede ou banco de dados), portanto, usaremos RxJava para nos comunicarmos com o interator e os apresentadores. Se você ainda não está familiarizado com esta ferramenta, leia esta série de artigos introdutórios .
Vamos criar um modelo que descreva as informações exigidas pelas telas sobre a etapa atual e sua posição no script:
/**
*
*/
class StepWithPosition<S : Step>(
val step: S,
val position: Int,
val allStepsCount: Int
)
Vamos iniciar um BehaviorSubject no interator para emitir livremente informações sobre a nova etapa ativa nele.
private val stepChangeSubject = BehaviorSubject.create<StepWithPosition<S>>()
Para que as telas possam se inscrever nesse fluxo de eventos, criaremos uma variável pública stepChangeObservable, que é um wrapper sobre nosso stepChangeSubject.
val stepChangeObservable: Observable<StepWithPosition<S>> = stepChangeSubject.hide()
Durante o trabalho do interator, muitas vezes é necessário saber a posição da etapa ativa atual. Eu recomendo criar uma propriedade separada no interator - currentStepIndex e substituir os métodos get () e set (). Isso nos dá acesso conveniente a essas informações do assunto.
Como fica no 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()
)
)
}
Vamos escrever uma parte geral que funcionará da mesma forma, independentemente da implementação específica do interator para o recurso.
Vamos adicionar métodos para inicializar e encerrar o trabalho do interator, tornando-os abertos para extensão nos descendentes:
Métodos de inicialização e desligamento
/**
*
*/
@CallSuper
open fun initProgressFeature() {
currentStepIndex = 0
}
/**
*
*/
@CallSuper
open fun closeProgressFeature() {
currentStepIndex = 0
}
Vamos adicionar as funções que qualquer interator de recursos passo a passo deve executar:
- getDataForStep (etapa: S) - fornece dados como entrada para a etapa S;
- completeStep (stepOut: O) - salva a saída O e move o script para a próxima etapa;
- toPreviousStep () —- Mova o script para a etapa anterior.
Vamos começar com a primeira função - processamento de dados de entrada. Cada interagente determinará por si mesmo como e de onde obter os dados de entrada. Vamos adicionar um método abstrato responsável por isso:
/**
*
*/
protected abstract fun resolveStepInData(step: S): Single<out StepData<I, O>>
Para apresentadores de telas específicas, adicione um método público que chamará
resolveStepInData() :
/**
*
*/
fun getDataForStep(step: S): Single<out StepData<I, O>> = resolveStepInData(step)
Você pode simplificar esse código tornando o método público
resolveStepInData(). O método é getDataForStep()adicionado por analogia com os métodos de conclusão de etapas, que discutiremos a seguir.
Para completar uma etapa, criamos de forma semelhante um método abstrato no qual cada interagente específico salvará o resultado da etapa.
/**
*
*/
protected abstract fun saveStepOutData(stepData: O): Completable
E um método público. Nele chamaremos o salvamento das informações de saída. Quando terminar, diga ao script para se ajustar às informações da etapa final. Também notificaremos os assinantes de que estamos avançando um passo.
/**
*
*/
fun completeStep(stepOut: O): Completable {
return saveStepOutData(stepOut).doOnComplete {
scenario.reactOnStepCompletion(stepOut)
if (currentStepIndex != scenario.steps.lastIndex) {
currentStepIndex += 1
}
}
}
Por fim, implementamos um método para retornar à etapa anterior.
/**
*
*/
fun toPreviousStep() {
if (currentStepIndex != 0) {
currentStepIndex -= 1
}
}
Vejamos a implementação do interator em nosso exemplo de aplicação de emprego. Como lembramos, é importante que nosso recurso salve dados no rascunho da aplicação, portanto, na classe ApplicationProgressInteractor, criaremos um campo adicional para o rascunho.
/**
*
*/
@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)
}
}
...
}
Como é uma aula de draft
:
/**
*
*/
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()
}
}
Vamos começar a implementar os métodos abstratos declarados na classe pai. Vamos começar com a função de conclusão de etapas - é bem simples com ela. Salvamos a saída de um determinado tipo em um rascunho com a chave desejada:
/**
*
*/
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
}
}
}
}
Agora, vamos examinar o método de obtenção de dados de entrada para uma etapa:
/**
*
*/
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()
)
}
}
}
Ao abrir uma etapa, existem duas opções:
- o usuário abre a tela pela primeira vez;
- o usuário já preencheu a tela e salvamos os dados no rascunho.
Para etapas que não exigem nada para entrar, passaremos as informações do rascunho (se houver).
ABOUT_ME -> Single.just(
AboutMeStepData(
stepOutData = draft.getAboutMeStepOutData()
)
)
Se precisarmos de dados das etapas anteriores como entrada, vamos retirá-los do rascunho (garantimos salvá-los lá no final de cada etapa). E da mesma forma, vamos transferir dados para outData que podem ser usados para preencher a tela.
EDUCATION -> Single.just(
EducationStepData(
inData = EducationStepInData(
draft.getPersonalInfoOutData()?.info?.educationType
?: error("Not enough data for EDUCATION step")
),
outData = draft.getEducationStepOutData()
)
)
Há também uma situação mais interessante: a última etapa, em que é necessário indicar o motivo do interesse do usuário por essa vaga em particular, exige uma lista de possíveis motivos para fazer o download da rede. Este é um dos momentos mais convenientes desta arquitetura. Podemos enviar um pedido e, ao recebermos uma resposta, combiná-lo com os dados do rascunho e enviá-lo para a tela como entrada. A tela nem precisa saber de onde vêm os dados e quantas fontes estão coletando.
MOTIVATION -> {
dataRepository.loadMotivationVariants().map { reasonsList ->
MotivationStepData(
inData = MotivationStepInData(reasonsList),
outData = draft.getMotivationStepOutData()
)
}
}
Tais situações são outro argumento a favor do trabalho por meio de interatores. Às vezes, para fornecer dados a uma etapa, você precisa combinar várias fontes de dados, por exemplo, um download da web e os resultados das etapas anteriores.
Em nosso método, podemos combinar dados de muitas fontes e fornecer à tela tudo o que precisamos. Pode ser difícil entender por que isso é ótimo neste exemplo. Em formulários reais - por exemplo, ao solicitar um empréstimo - a tela potencialmente precisa enviar muitos livros de referência, informações sobre o usuário do banco de dados interno, dados que ele preencheu 5 etapas atrás e uma coleção das anedotas mais populares de 1970.
O código do apresentador é muito mais fácil quando a agregação é feita por um método de interação separado que produz apenas o resultado: dados ou um erro. É mais fácil para os desenvolvedores fazer alterações e ajustes se for imediatamente claro onde procurar tudo.
Mas isso não é tudo que existe no interator. Claro, precisamos de um método para enviar o aplicativo final - quando todas as etapas tiverem sido aprovadas. Vamos descrever o aplicativo final e a capacidade de criá-lo usando o padrão "Builder"
Aula para enviar a aplicação 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()
)
}
}
}
}
O método de envio do próprio aplicativo:
/**
*
*/
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())
}
Como usar tudo nas telas
Agora vale a pena descer ao nível da apresentação e ver como os apresentadores de tela interagem com esse interator.
Nosso recurso é uma atividade com uma pilha de fragmentos dentro.
O envio bem-sucedido do aplicativo abre uma atividade separada, onde o usuário é informado sobre o sucesso do envio. A atividade principal será responsável por mostrar o fragmento desejado, dependendo do comando do interator, e também por mostrar quantos passos já foram executados na barra de ferramentas. Para fazer isso, no apresentador de atividades raiz, inscreva-se no assunto do interator e implemente a lógica para trocar fragmentos na pilha.
progressInteractor.stepChangeObservable.subscribe { stepData ->
if (stepData.position > currentPosition) {
// FragmentManager
} else {
//
}
// -
}
Agora, no apresentador de cada fragmento, no início da tela, pediremos ao interator que nos forneça os dados de entrada. É melhor transferir os dados recebidos para um fluxo separado, porque, como mencionado anteriormente, ele pode ser associado ao download da rede.
Por exemplo, vamos pegar a tela de preenchimento de informações educacionais.
progressInteractor.getDataForStep(EducationStep)
.filter<ApplicationStepData.EducationStepData>()
.subscribeOn(Schedulers.io())
.subscribe {
val educationType = it.stepInData.educationType
// todo:
it.stepOutData?.education?.let {
// todo:
}
}
Suponha que concluamos a etapa "sobre educação" e o usuário deseja ir mais longe. Tudo o que precisamos fazer é formar um objeto com a saída e passá-la para o interator.
progressInteractor.completeStep(EducationStepOutData(education)).subscribe {
// ( )
}
O interagente salvará os próprios dados, iniciará as alterações no script, se necessário, e sinalizará à atividade raiz para passar para a próxima etapa. Assim, os fragmentos não sabem nada sobre sua posição no script: e podem ser facilmente reorganizados se, por exemplo, o design de um recurso mudou.
No último fragmento, como reação ao sucesso do salvamento dos dados, adicionaremos o envio da requisição final, como lembramos, criamos um método para isso
sendApplication()no interator.
progressInteractor.sendApplication()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
//
activityNavigator.start(ThankYouRoute())
},
{
//
}
)
Na tela final com a informação de que o pedido foi enviado com sucesso, liberaremos o interator para que o processo seja reiniciado do zero.
progressInteractor.closeProgressFeature()
Isso é tudo. Temos um recurso que consiste em cinco telas. A tela "sobre educação" pode ser pulada, a tela com o preenchimento da experiência de trabalho - substituída por uma tela para escrever um ensaio. Podemos interromper o preenchimento em qualquer etapa e continuar depois, e tudo o que inserimos será salvo no rascunho.
Agradecimentos especiais a Vasya Beglyanin @icebail - o autor da primeira implementação desta abordagem no projeto. E também a Misha Zinchenko @midery - pela ajuda em trazer o rascunho da arquitetura para a versão final, que é descrita neste artigo.