Dans les applications mobiles, il existe des formulaires avec un remplissage complexe en plusieurs étapes - par exemple, des questionnaires ou des applications. La conception de telles fonctionnalités provoque généralement un casse-tête pour les développeurs: une grande quantité de données est transférée entre les écrans et des connexions rigides sont formées - qui, à qui, dans quel ordre ces données doivent-elles être transmises et quel écran ouvrir après lui-même.
Dans cet article, je vais partager un moyen pratique d'organiser le travail d'une fonctionnalité étape par étape. Avec son aide, il est possible de minimiser les connexions entre les écrans et d'effectuer facilement des modifications dans l'ordre des étapes: ajouter de nouveaux écrans, modifier leur séquence et la logique d'affichage à l'utilisateur.
* Par le mot «fonctionnalité» dans cet article, j'entends un ensemble d'écrans dans une application mobile qui sont logiquement connectés et représentent une fonction pour l'utilisateur.
Habituellement, remplir des questionnaires et soumettre des applications dans des applications mobiles se compose de plusieurs écrans séquentiels. Les données d'un écran peuvent être nécessaires sur un autre, et les étapes changent parfois en fonction des réponses. Par conséquent, il est utile de permettre à l'utilisateur de sauvegarder les données "en brouillon" - afin qu'il puisse revenir au processus plus tard.
Il peut y avoir de nombreux écrans, mais en fait, l'utilisateur remplit un seul gros objet avec des données. Dans cet article, je vais vous expliquer comment organiser facilement le travail avec une chaîne d'écrans qui ne forment qu'un seul scénario.
Disons qu'un utilisateur postule pour un emploi et remplit un formulaire. S'il est interrompu au milieu, les données saisies seront enregistrées dans le brouillon. Lorsque l'utilisateur recommence à remplir, les informations du brouillon seront automatiquement remplacées dans les champs du questionnaire - il n'a pas besoin de tout remplir à partir de zéro.
Lorsque l'utilisateur remplit l'intégralité du questionnaire, sa réponse sera envoyée au serveur.
Le questionnaire comprend:
- Étape 1 - Nom complet, type de formation, expérience de travail,
- Étape 2 - le lieu d'étude,
- Étape 3 - lieu de travail ou essai sur vous-même,
- Étape 4 - les raisons pour lesquelles le poste est intéressé.
Le questionnaire changera selon que l'utilisateur a une formation et une expérience de travail. S'il n'y a pas d'éducation, nous excluons l'étape de remplissage du lieu d'étude. S'il n'y a pas d'expérience professionnelle, demandez à l'utilisateur d'écrire un peu sur lui-même.
Au stade de la conception, nous devons répondre à plusieurs questions:
- Comment rendre le script de fonctionnalité flexible et pouvoir facilement ajouter et supprimer des étapes.
- Comment s'assurer que lorsque vous ouvrez une étape, les données requises seront déjà renseignées (par exemple, l'écran "Education" à l'entrée attend un type d'enseignement déjà connu pour reconstruire la composition de ses champs).
- Comment agréger les données dans un modèle commun pour le transfert vers le serveur après la dernière étape.
- Comment enregistrer l'application dans "brouillon" afin que l'utilisateur puisse interrompre le remplissage et y revenir plus tard.
En conséquence, nous voulons obtenir les fonctionnalités suivantes: L'
exemple complet est dans mon référentiel sur GitHub
Une solution évidente
Si vous développez une fonctionnalité "en mode économie d'énergie totale", le plus évident est de créer un objet application et de le transférer d'écran en écran, en le rechargeant à chaque étape.
La couleur gris clair marquera les données qui ne sont pas nécessaires à une étape particulière. Dans le même temps, ils sont transmis à chaque écran pour éventuellement entrer dans l'application finale.
Bien entendu, toutes ces données doivent être regroupées dans un seul objet d'application. Voyons à quoi cela ressemblera:
class Application(
val name: String?,
val surname: String?,
val educationType : EducationType?,
val workingExperience: Boolean?
val education: Education?,
val experience: Experience?,
val motivation: List<Motivation>?
)
MAIS!
En travaillant avec un tel objet, nous condamnons notre code à être couvert d'un nombre supplémentaire inutile de vérifications nulles. Par exemple, cette structure de données ne garantit en aucun cas que le champ
educationType
sera déjà rempli dans l'écran "Education".
Comment faire mieux
Je recommande de déplacer la gestion des données dans un objet séparé, qui fournira les données non nulles nécessaires à l'entrée de chaque étape et enregistrera le résultat de chaque étape dans un brouillon. Nous appellerons cet objet un interacteur. Il correspond à la couche Use Case issue de l'architecture pure de Robert Martin et se charge pour tous les écrans de fournir des données collectées à partir de différentes sources (réseau, base de données, données des étapes précédentes, données d'un projet de proposition ...).
Sur nos projets, chez Surf, nous utilisons Dagger. Pour un certain nombre de raisons, il est courant de créer des portées d'interacteurs @PerApplication: cela fait de notre interacteur un singleton au sein de l'application. En fait, l'interacteur peut être un singleton dans une fonctionnalité, ou même une activation - si toutes vos étapes sont des fragments. Tout dépend de l'architecture globale de votre application.
Plus loin dans les exemples, nous supposerons que nous avons une seule instance de l'interacteur pour l'ensemble de l'application. Par conséquent, toutes les données doivent être effacées à la fin du script.
Lors de la définition de la tâche, en plus du stockage centralisé des données, nous voulions organiser une gestion aisée de la composition et de l'ordre des étapes dans l'application: en fonction de ce que l'utilisateur a déjà rempli, elles peuvent changer. Par conséquent, nous avons besoin d'une autre entité - le scénario. Son domaine de responsabilité est de garder l'ordre des étapes que l'utilisateur doit franchir.
L'organisation d'une fonctionnalité pas à pas à l'aide de scripts et d'un interacteur permet:
- Il est facile de modifier les étapes du script: par exemple, le chevauchement des travaux supplémentaires s'il s'avère pendant l'exécution que l'utilisateur ne peut pas soumettre de demandes ou ajouter des étapes si des informations supplémentaires sont nécessaires.
- Définir les contrats: quelles données doivent être à l'entrée et à la sortie de chaque étape.
- Organisez l'enregistrement de l'application dans un brouillon si l'utilisateur n'a pas rempli tous les écrans.
Pré-remplissez les écrans avec les données enregistrées dans le brouillon.
Entités de base
Le mécanisme de la fonctionnalité comprendra:
- Un ensemble de modèles pour décrire une étape, des entrées et des sorties.
- Scénario - une entité qui décrit les étapes (écrans) que l'utilisateur doit suivre.
- Interaktora (ProgressInteractor) - une classe chargée de stocker des informations sur l'étape active en cours, d'agréger les informations remplies après l'achèvement de chaque étape et d'émettre des données d'entrée pour démarrer une nouvelle étape.
- Draft (ApplicationDraft) - une classe chargée de stocker les informations remplies.
Le diagramme de classes représente toutes les entités de base dont hériteront les implémentations concrètes. Voyons comment ils sont liés.
Pour l'entité Scénario, nous allons définir une interface dans laquelle nous décrivons la logique attendue pour tout scénario de l'application (contient une liste des étapes nécessaires et la reconstruit après avoir terminé l'étape précédente, si nécessaire.
L'application peut avoir plusieurs fonctionnalités, constituées de nombreux écrans séquentiels, et chacune sera Nous déplacerons toute la logique générale qui ne dépend pas de la fonctionnalité ou des données spécifiques dans la classe de base ProgressInteractor.
ApplicationDraft n'est pas présent dans les classes de base, car l'enregistrement des données que l'utilisateur a remplies dans un brouillon peut ne pas être nécessaire. Par conséquent, une mise en œuvre concrète de ProgressInteractor fonctionnera avec le projet. Les présentateurs d'écran interagiront également avec lui.
Diagramme de classes pour des implémentations spécifiques de classes de base:
Toutes ces entités interagiront les unes avec les autres et avec les présentateurs d'écran comme suit: Il existe
un certain nombre de classes, analysons donc chaque bloc séparément en utilisant la fonctionnalité du début de l'article.
Description des étapes
Commençons par le premier point. Nous avons besoin d'entités pour décrire les étapes:
// , ,
interface Step
Pour la fonctionnalité de notre exemple de demande d'emploi, les étapes sont les suivantes:
/**
*
*/
enum class ApplicationSteps : Step {
PERSONAL_INFO, //
EDUCATION, //
EXPERIENCE, //
ABOUT_ME, // " "
MOTIVATION //
}
Nous devons également décrire les données d'entrée pour chaque étape. Pour ce faire, nous utiliserons des classes scellées dans le but prévu - pour créer une hiérarchie de classes limitée.
À quoi cela ressemblera dans le code
:
//
interface StepInData
:
//,
sealed class ApplicationStepInData : StepInData
//
class EducationStepInData(val educationType: EducationType) : ApplicationStepInData()
//
class MotivationStepInData(val values: List<Motivation>) : ApplicationStepInData()
Nous décrivons la sortie de la même manière:
À quoi cela ressemblera dans le code
// ,
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 nous ne nous sommes pas fixé comme objectif de conserver les candidatures inachevées dans les brouillons, nous pourrions nous limiter à cela. Mais comme chaque écran peut s'ouvrir non seulement vide, mais également rempli à partir du brouillon, les données d'entrée et les données du brouillon viendront à l'entrée de l'interacteur - si l'utilisateur a déjà entré quelque chose.
Par conséquent, nous avons besoin d'un autre ensemble de modèles pour rassembler ces données. Certaines étapes ne nécessitent pas d'informations à saisir et ne fournissent qu'un champ pour les données du brouillon
À quoi cela ressemblera dans le code
/**
* + ,
*/
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()
}
Nous agissons selon le scénario
Avec la description des étapes et les données d'entrée / sortie triées. Maintenant, corrigeons l'ordre de ces étapes dans le script de fonctionnalité dans le code. L'entité Scénario est responsable de la gestion de l'ordre actuel des étapes. Le script ressemblera à ceci:
/**
* , ,
*/
interface Scenario<S : Step, O : StepOutData> {
//
val steps: List<S>
/**
*
*
*/
fun reactOnStepCompletion(stepOut: O)
}
Dans l'implémentation de notre exemple, le script sera comme ceci:
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) {...}
}
Il faut garder à l'esprit que toute modification du script doit être bidirectionnelle. Disons que vous supprimez une étape. Assurez-vous que si l'utilisateur revient en arrière et sélectionne une option différente, l'étape est ajoutée au script.
Comment, par exemple, le code ressemble-t-il à la réaction à la présence ou à l'absence d'expérience de travail
/**
* ,
*
*/
private fun applyExperienceToScenario(hasWorkingExperience: Boolean) {
if (hasWorkingExperience) {
steps.replaceWith(
condition = { it == ABOUT_ME },
newElem = EXPERIENCE
)
} else {
steps.replaceWith(
condition = { it == EXPERIENCE },
newElem = ABOUT_ME
)
}
}
Comment fonctionne Interactor
Considérez le bloc de construction suivant dans l'architecture d'une fonctionnalité étape par étape - un interacteur. Comme nous l'avons dit ci-dessus, sa responsabilité principale est d'assurer la commutation entre les étapes: donner les données nécessaires à l'entrée des étapes et agréger les données de sortie dans un projet de demande.
Créons une classe de base pour notre interacteur et mettons-y le comportement commun à toutes les fonctionnalités étape par étape.
/**
*
* S -
* I -
* O -
*/
abstract class ProgressInteractor<S : Step, I : StepInData, O : StepOutData>
L'interacteur doit travailler avec le scénario actuel: informez-le de l'achèvement de l'étape suivante afin que le scénario puisse reconstruire son ensemble d'étapes. Par conséquent, nous déclarerons un champ abstrait pour notre script. Désormais, chaque interacteur spécifique devra fournir sa propre implémentation.
// ,
protected abstract val scenario: Scenario<S, O>
L'interacteur est également chargé de stocker l'état de l'étape actuellement active et de passer à l'étape suivante ou précédente. Il doit informer rapidement l'écran racine du changement d'étape afin de pouvoir basculer vers le fragment souhaité. Tout cela peut être facilement organisé grâce à la diffusion d'événements, c'est-à-dire une approche réactive. De plus, les méthodes de notre interacteur effectueront souvent des opérations asynchrones (chargement de données depuis le réseau ou la base de données), nous utiliserons donc RxJava pour communiquer avec l'interacteur avec les présentateurs. Si vous n'êtes pas déjà familiarisé avec cet outil, lisez cette série d'articles d'introduction .
Créons un modèle qui décrit les informations requises par les écrans sur l'étape en cours et sa position dans le script:
/**
*
*/
class StepWithPosition<S : Step>(
val step: S,
val position: Int,
val allStepsCount: Int
)
Commençons par un BehaviorSubject dans l'interacteur pour y émettre librement des informations sur la nouvelle étape active.
private val stepChangeSubject = BehaviorSubject.create<StepWithPosition<S>>()
Pour que les écrans puissent s'abonner à ce flux d'événements, créons une variable publique stepChangeObservable, qui est un wrapper sur notre stepChangeSubject.
val stepChangeObservable: Observable<StepWithPosition<S>> = stepChangeSubject.hide()
Lors du travail de l'interacteur, il est souvent nécessaire de connaître la position de l'étape active courante. Je recommande de créer une propriété distincte dans l'interacteur - currentStepIndex et de remplacer les méthodes get () et set (). Cela nous donne un accès pratique à ces informations à partir du sujet.
À quoi ça ressemble dans le code
//
private var currentStepIndex: Int
get() = stepChangeSubject.value?.position ?: 0
set(value) {
stepChangeSubject.onNext(
StepWithPosition(
step = scenario.steps[value],
position = value,
allStepsCount = scenario.steps.count()
)
)
}
Écrivons une partie générale qui fonctionnera de la même manière quelle que soit l'implémentation spécifique de l'interacteur pour la fonctionnalité.
Ajoutons des méthodes pour initialiser et arrêter l'interacteur, en les ouvrant à l'extension dans les descendants:
Méthodes d'initialisation et d'arrêt
/**
*
*/
@CallSuper
open fun initProgressFeature() {
currentStepIndex = 0
}
/**
*
*/
@CallSuper
open fun closeProgressFeature() {
currentStepIndex = 0
}
Ajoutons les fonctions que tout interacteur de fonctionnalités étape par étape devrait exécuter:
- getDataForStep (étape: S) - fournir des données comme entrée à l'étape S;
- completeStep (stepOut: O) - enregistrer la sortie O et déplacer le script à l'étape suivante;
- toPreviousStep () —- Déplace le script à l'étape précédente.
Commençons par la première fonction - traiter les données d'entrée. Chaque interacteur déterminera lui-même comment et où obtenir les données d'entrée. Ajoutons une méthode abstraite responsable de cela:
/**
*
*/
protected abstract fun resolveStepInData(step: S): Single<out StepData<I, O>>
Pour les présentateurs d'écrans spécifiques, ajoutez une méthode publique qui appellera
resolveStepInData() :
/**
*
*/
fun getDataForStep(step: S): Single<out StepData<I, O>> = resolveStepInData(step)
Vous pouvez simplifier ce code en rendant la méthode publique
resolveStepInData()
. La méthode est getDataForStep()
ajoutée par analogie avec les méthodes d'achèvement d'étapes, que nous discuterons ci-dessous.
Pour terminer une étape, nous créons de la même manière une méthode abstraite dans laquelle chaque interacteur spécifique enregistrera le résultat de l'étape.
/**
*
*/
protected abstract fun saveStepOutData(stepData: O): Completable
Et une méthode publique. Dans ce document, nous appellerons la sauvegarde des informations de sortie. Une fois terminé, indiquez au script de s'adapter aux informations de l'étape de fin. Nous informerons également les abonnés que nous faisons un pas en avant.
/**
*
*/
fun completeStep(stepOut: O): Completable {
return saveStepOutData(stepOut).doOnComplete {
scenario.reactOnStepCompletion(stepOut)
if (currentStepIndex != scenario.steps.lastIndex) {
currentStepIndex += 1
}
}
}
Enfin, nous implémentons une méthode pour revenir à l'étape précédente.
/**
*
*/
fun toPreviousStep() {
if (currentStepIndex != 0) {
currentStepIndex -= 1
}
}
Regardons l'implémentation de l'interacteur pour notre exemple de candidature. Comme nous nous en souvenons, il est important que notre fonctionnalité enregistre les données dans le brouillon de l'application, par conséquent, dans la classe ApplicationProgressInteractor, nous allons créer un champ supplémentaire pour le brouillon.
/**
*
*/
@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)
}
}
...
}
À quoi ressemble une classe de brouillon
:
/**
*
*/
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()
}
}
Commençons par implémenter les méthodes abstraites déclarées dans la classe parente. Commençons par la fonction de complétion d'étape - c'est assez simple avec elle. Nous sauvegardons les données de sortie d'un certain type dans un brouillon sous la clé requise:
/**
*
*/
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
}
}
}
}
Regardons maintenant la méthode d'obtention des données d'entrée pour une étape:
/**
*
*/
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()
)
}
}
}
Lors de l'ouverture d'une étape, il existe deux options:
- l'utilisateur ouvre l'écran pour la première fois;
- l'utilisateur a déjà rempli l'écran et nous avons enregistré des données dans le brouillon.
Pour les étapes qui ne nécessitent rien pour entrer, nous transmettrons les informations du brouillon (le cas échéant).
ABOUT_ME -> Single.just(
AboutMeStepData(
stepOutData = draft.getAboutMeStepOutData()
)
)
Si nous avons besoin des données des étapes précédentes comme entrée, nous les retirerons du brouillon (nous nous sommes assurés de les enregistrer à la fin de chaque étape). Et de même, nous transférerons des données vers outData qui peuvent être utilisées pour remplir l'écran.
EDUCATION -> Single.just(
EducationStepData(
inData = EducationStepInData(
draft.getPersonalInfoOutData()?.info?.educationType
?: error("Not enough data for EDUCATION step")
),
outData = draft.getEducationStepOutData()
)
)
Il y a aussi une situation plus intéressante: la dernière étape, où il est nécessaire d'indiquer pourquoi l'utilisateur est intéressé par cette offre particulière, nécessite une liste de raisons possibles à télécharger depuis le réseau. C'est l'un des moments les plus propices de cette architecture. Nous pouvons envoyer une demande et, lorsque nous recevons une réponse, la combiner avec les données du brouillon et l'envoyer à l'écran comme entrée. L'écran n'a même pas besoin de savoir d'où proviennent les données et combien de sources il collecte.
MOTIVATION -> {
dataRepository.loadMotivationVariants().map { reasonsList ->
MotivationStepData(
inData = MotivationStepInData(reasonsList),
outData = draft.getMotivationStepOutData()
)
}
}
De telles situations sont un autre argument en faveur du travail via des interacteurs. Parfois, pour fournir une étape avec des données, vous devez combiner plusieurs sources de données, par exemple, un téléchargement à partir du Web et les résultats des étapes précédentes.
Dans notre méthode, nous pouvons combiner des données provenant de nombreuses sources et fournir à l'écran tout ce dont nous avons besoin. Il peut être difficile de comprendre pourquoi c'est génial dans cet exemple. Dans des formes réelles - par exemple, lors d'une demande de prêt - l'écran a potentiellement besoin de soumettre de nombreux ouvrages de référence, des informations sur l'utilisateur à partir de la base de données interne, les données qu'il a remplies 5 étapes en arrière et un recueil des anecdotes les plus populaires de 1970.
Le code du présentateur est beaucoup plus facile lorsque l'agrégation est effectuée par une méthode d'interaction distincte qui ne produit que le résultat: des données ou une erreur. Il est plus facile pour les développeurs d'apporter des modifications et des ajustements s'il est immédiatement clair où chercher tout.
Mais ce n'est pas tout ce qu'il y a dans l'interacteur. Bien sûr, nous avons besoin d'une méthode pour envoyer la candidature finale - lorsque toutes les étapes ont été franchies. Décrivons l'application finale et la possibilité de la créer à l'aide du modèle "Builder"
Classe pour soumettre la candidature finale
/**
*
*/
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()
)
}
}
}
}
La méthode d'envoi de l'application elle-même:
/**
*
*/
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())
}
Comment tout utiliser sur les écrans
Maintenant, cela vaut la peine de passer au niveau de la présentation et de voir comment les présentateurs d'écran interagissent avec cet interacteur.
Notre fonction est une activité avec une pile de fragments à l'intérieur.
La soumission réussie de la demande ouvre une activité distincte, où l'utilisateur est informé du succès de la soumission. L'activité principale sera chargée d'afficher le fragment souhaité, en fonction de la commande de l'interacteur, et également d'afficher le nombre d'étapes déjà effectuées dans la barre d'outils. Pour ce faire, dans le présentateur d'activité racine, abonnez-vous au sujet depuis l'interacteur et implémentez la logique de commutation des fragments dans la pile.
progressInteractor.stepChangeObservable.subscribe { stepData ->
if (stepData.position > currentPosition) {
// FragmentManager
} else {
//
}
// -
}
Maintenant, dans le présentateur de chaque fragment, au début de l'écran, nous allons demander à l'interacteur de nous donner des données d'entrée. Il est préférable de transférer les données de réception dans un flux séparé, car, comme mentionné précédemment, elles peuvent être associées au téléchargement à partir du réseau.
Par exemple, prenons l'écran pour remplir les informations sur l'éducation.
progressInteractor.getDataForStep(EducationStep)
.filter<ApplicationStepData.EducationStepData>()
.subscribeOn(Schedulers.io())
.subscribe {
val educationType = it.stepInData.educationType
// todo:
it.stepOutData?.education?.let {
// todo:
}
}
Supposons que nous terminions l'étape «sur l'éducation» et que l'utilisateur veuille aller plus loin. Tout ce que nous avons à faire est de former un objet avec la sortie et de le transmettre à l'interacteur.
progressInteractor.completeStep(EducationStepOutData(education)).subscribe {
// ( )
}
L'interacteur enregistrera les données lui-même, initiera des modifications dans le script, si nécessaire, et signalera à l'activité racine de passer à l'étape suivante. Ainsi, les fragments ne savent rien de leur position dans le script: et ils peuvent être facilement réorganisés si, par exemple, la conception d'une fonction a changé.
Sur le dernier fragment, en réaction à la sauvegarde réussie des données, nous ajouterons l'envoi de la requête finale, comme nous nous en souvenons, nous avons créé une méthode pour cela
sendApplication()
dans l'interacteur.
progressInteractor.sendApplication()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
//
activityNavigator.start(ThankYouRoute())
},
{
//
}
)
Sur l'écran final avec des informations indiquant que la demande a été envoyée avec succès, nous effacerons l'interacteur afin que le processus puisse être redémarré à partir de zéro.
progressInteractor.closeProgressFeature()
C'est tout. Nous avons une fonctionnalité composée de cinq écrans. L'écran "sur l'éducation" peut être ignoré, l'écran avec le remplissage de l'expérience de travail - remplacé par un écran pour rédiger un essai. Nous pouvons interrompre le remplissage à n'importe quelle étape et continuer plus tard, et tout ce que nous avons entré sera enregistré dans le brouillon.
Un merci spécial à Vasya Beglyanin @icebail - l'auteur de la première mise en œuvre de cette approche dans le projet. Et aussi Misha Zinchenko @midery - pour avoir aidé à amener le projet d'architecture à la version finale, qui est décrite dans cet article.