كيفية تصميم تعبئة البيانات الإضافية في تطبيق الهاتف المحمول

مرحبا! اسمي فيتا سوكولوفا ، أنا رئيس فريق Android في Surf .



في تطبيقات الهاتف المحمول ، توجد نماذج ذات تعبئة معقدة متعددة الخطوات - على سبيل المثال ، الاستبيانات أو التطبيقات. عادةً ما يتسبب تصميم مثل هذه الميزات في حدوث صداع للمطورين: يتم نقل كمية كبيرة من البيانات بين الشاشات ويتم تكوين اتصالات صلبة - لمن ، ولمن ، وبأي ترتيب يجب نقل هذه البيانات وأي شاشة يتم فتحها بعد ذلك.



في هذه المقالة سوف أشارك طريقة ملائمة لتنظيم عمل ميزة خطوة بخطوة. بفضل مساعدتها ، من الممكن تقليل الاتصالات بين الشاشات وإجراء تغييرات بسهولة على ترتيب الخطوات: إضافة شاشات جديدة ، وتغيير تسلسلها ومنطق العرض للمستخدم.







* بكلمة "ميزة" في هذه المقالة أعني مجموعة من الشاشات في تطبيق جوال مرتبطة منطقيًا وتمثل وظيفة واحدة للمستخدم.



عادة ما يتكون ملء الاستبيانات وتقديم الطلبات في تطبيقات الهاتف المحمول من عدة شاشات متسلسلة. قد تكون البيانات من شاشة واحدة مطلوبة على شاشة أخرى ، وتتغير الخطوات أحيانًا اعتمادًا على الإجابات. لذلك ، من المفيد السماح للمستخدم بحفظ البيانات "في المسودة" - حتى يتمكن من العودة إلى العملية لاحقًا.



يمكن أن يكون هناك العديد من الشاشات ، ولكن في الواقع يملأ المستخدم كائنًا كبيرًا بالبيانات. سأخبرك في هذه المقالة عن كيفية تنظيم العمل بشكل ملائم باستخدام سلسلة من الشاشات التي تمثل أحد السيناريوهات.



لنفترض أن المستخدم يتقدم لوظيفة ويملأ نموذجًا. إذا تمت مقاطعته في المنتصف ، فسيتم حفظ البيانات المدخلة في المسودة. عندما يعود المستخدم إلى الملء ، سيتم استبدال المعلومات من المسودة تلقائيًا في حقول الاستبيان - لا يحتاج إلى ملء كل شيء من البداية.



عندما يملأ المستخدم الاستبيان بالكامل ، سيتم إرسال رده إلى الخادم.



يتكون الاستبيان من:



  • الخطوة 1 - الاسم ونوع التعليم والخبرة العملية ،

  • الخطوة الثانية - مكان الدراسة

  • الخطوة 3 - مكان العمل أو مقال عن نفسك ،

  • الخطوة 4 - أسباب الاهتمام بالوظيفة الشاغرة.









سيتغير الاستبيان اعتمادًا على ما إذا كان المستخدم لديه خبرة تعليمية وعملية. إذا لم يكن هناك تعليم ، فسوف نستبعد الخطوة بملء مكان الدراسة. إذا لم تكن هناك خبرة في العمل ، اطلب من المستخدم أن يكتب القليل عن نفسه.







في مرحلة التصميم علينا الإجابة على عدة أسئلة:



  • كيفية جعل النص البرمجي للميزات مرنًا والقدرة على إضافة الخطوات وإزالتها بسهولة.

  • كيفية التأكد من أنه عند فتح خطوة ما ، سيتم بالفعل ملء البيانات المطلوبة (على سبيل المثال ، تنتظر شاشة "التعليم" عند المدخل نوعًا معروفًا من التعليم لإعادة تكوين تكوين الحقول الخاصة به).

  • كيفية تجميع البيانات في نموذج مشترك لنقلها إلى الخادم بعد الخطوة الأخيرة.

  • كيفية حفظ التطبيق في "المسودة" بحيث يمكن للمستخدم مقاطعة التعبئة والعودة إليه لاحقًا.



نتيجة لذلك ، نريد الحصول على الوظائف التالية:







المثال الكامل موجود في مستودعي على GitHub 



حل واضح



إذا قمت بتطوير ميزة "في وضع توفير الطاقة الكامل" ، فإن الشيء الأكثر وضوحًا هو إنشاء كائن تطبيق ونقله من شاشة إلى أخرى ، وإعادة تعبئته في كل خطوة.



سيميز اللون الرمادي الفاتح البيانات غير المطلوبة في خطوة معينة. في نفس الوقت ، يتم إرسالها إلى كل شاشة للدخول في النهاية إلى التطبيق النهائي.







بالطبع ، يجب حزم كل هذه البيانات في كائن تطبيق واحد. دعونا نرى كيف سيبدو:



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


لكن!

عند العمل مع مثل هذا الكائن ، فإننا نحكم على الكود الخاص بنا ليتم تغطيته بعدد إضافي غير ضروري من عمليات التحقق الفارغة. على سبيل المثال ، لا تضمن بنية البيانات هذه بأي حال educationTypeملء الحقل بالفعل في شاشة "التعليم".



كيف نفعل أفضل



أوصي بنقل إدارة البيانات إلى كائن منفصل ، والذي سيوفر البيانات الضرورية غير القابلة للإلغاء كمدخل لكل خطوة وحفظ نتيجة كل خطوة في مسودة. سوف نسمي هذا الكائن المتفاعل. إنها تتوافق مع طبقة حالة الاستخدام من الهندسة البحتة لروبرت مارتن وجميع الشاشات مسؤولة عن توفير البيانات التي تم جمعها من مصادر مختلفة (الشبكة ، قاعدة البيانات ، البيانات من الخطوات السابقة ، البيانات من مشروع الاقتراح ...



في مشاريعنا ، نحن في Surf نستخدم Dagger. لعدد من الأسباب ، من المعتاد إنشاء متفاعلاتPerApplication نطاقات: وهذا يجعل المتفاعل لدينا منفردًا داخل التطبيق. في الواقع ، يمكن أن يكون المتفاعل منفردًا داخل ميزة ، أو حتى تنشيطًا - إذا كانت جميع خطواتك عبارة عن أجزاء. كل هذا يتوقف على التصميم العام لتطبيقك.



علاوة على ذلك في الأمثلة ، سنفترض أن لدينا مثيلًا واحدًا للمتفاعل للتطبيق بأكمله. لذلك ، يجب مسح جميع البيانات عند انتهاء البرنامج النصي.







عند تعيين المهمة ، بالإضافة إلى تخزين البيانات المركزي ، أردنا تنظيم إدارة سهلة للتكوين وترتيب الخطوات في التطبيق: اعتمادًا على ما قام المستخدم بملئه بالفعل ، يمكنهم التغيير. لذلك ، نحتاج إلى كيان آخر - السيناريو. مجال مسؤوليتها هو الحفاظ على ترتيب الخطوات التي يجب أن يمر بها المستخدم.



يتيح تنظيم ميزة خطوة بخطوة باستخدام البرامج النصية والمتفاعل:



  • من السهل تغيير الخطوات في البرنامج النصي: على سبيل المثال ، تداخل المزيد من العمل إذا اتضح أثناء التنفيذ أن المستخدم لا يمكنه إرسال طلبات أو إضافة خطوات إذا كانت هناك حاجة إلى مزيد من المعلومات.

  • حدد العقود: ما هي البيانات التي يجب أن تكون عند مدخلات ومخرجات كل خطوة.

  • تنظيم حفظ التطبيق في مسودة إذا لم يكمل المستخدم جميع الشاشات.



ملء الشاشات بالبيانات المحفوظة في المسودة.



الكيانات الأساسية



تتكون آلية الميزة من:



  • مجموعة من النماذج لوصف الخطوة والمدخلات والمخرجات.

  • سيناريو - كيان يصف الخطوات (الشاشات) التي يحتاج المستخدم إلى المرور بها.

  • Interaktora (ProgressInteractor) - فئة مسؤولة عن تخزين المعلومات حول الخطوة النشطة الحالية ، وتجميع المعلومات المعبأة بعد الانتهاء من كل خطوة وإصدار بيانات الإدخال لبدء خطوة جديدة.

  • مسودة (ApplicationDraft) - فئة مسؤولة عن تخزين المعلومات المعبأة. 



يمثل مخطط الفصل جميع الكيانات الأساسية التي سترث منها التطبيقات الملموسة. دعونا نرى كيف ترتبط.







بالنسبة لكيان السيناريو ، سنقوم بتعيين واجهة سنصف فيها المنطق الذي نتوقعه لأي سيناريو في التطبيق (احتوى على قائمة بالخطوات الضرورية وأعد بنائه بعد إكمال الخطوة السابقة ، إذا لزم الأمر.



يمكن أن يحتوي التطبيق على العديد من الميزات التي تتكون من العديد من الشاشات المتسلسلة ، وكل منها سننقل كل المنطق العام الذي لا يعتمد على الميزة أو البيانات المحددة إلى الطبقة الأساسية ProgressInteractor.



ApplicationDraft غير موجود في الفئات الأساسية ، لأن حفظ البيانات التي قام المستخدم بملئها في المسودة قد لا يكون مطلوبًا. لذلك ، سيعمل تطبيق ProgressInteractor الملموس مع المسودة. سيتفاعل معها مقدمو الشاشة أيضًا.



مخطط الفصل لتطبيقات محددة للفئات الأساسية:







ستتفاعل جميع هذه الكيانات مع بعضها البعض ومع مقدمي الشاشة على النحو التالي: هناك عدد







غير قليل من الفئات ، لذلك دعونا نحلل كل كتلة على حدة باستخدام مثال الميزة من بداية المقالة.



وصف الخطوات



لنبدأ بالنقطة الأولى. نحتاج إلى كيانات لوصف الخطوات:



// ,   ,    

interface Step




بالنسبة للميزة من مثال طلب العمل لدينا ، فإن الخطوات كالتالي:



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



نحتاج أيضًا إلى وصف بيانات الإدخال لكل خطوة. للقيام بذلك ، سنستخدم الفئات المختومة للغرض المقصود منها - لإنشاء تسلسل هرمي محدود للفئة.







كيف سيبدو في الكود
//   
interface StepInData


:



//,      
sealed class ApplicationStepInData : StepInData

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

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




نصف المخرجات بطريقة مماثلة:







كيف سيبدو في الكود
// ,   
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()




إذا لم نحدد هدف الاحتفاظ بالطلبات غير المعبأة في المسودات ، فيمكننا قصر أنفسنا على ذلك. ولكن نظرًا لأنه لا يمكن فتح كل شاشة فارغة فحسب ، بل يتم ملؤها أيضًا من المسودة ، فإن بيانات الإدخال والبيانات من المسودة ستأتي إلى المدخلات من المتفاعل - إذا كان المستخدم قد أدخل شيئًا بالفعل.



لذلك ، نحتاج إلى مجموعة أخرى من النماذج لتجميع هذه البيانات معًا. لا تحتاج بعض الخطوات إلى معلومات للدخول وتوفر فقط حقلاً للبيانات من المسودة



كيف سيبدو في الكود
/**
 *     +   ,   
 */
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()
}




نحن نتصرف حسب السيناريو



مع وصف الخطوات وبيانات الإدخال / الإخراج مرتبة. الآن دعنا نصلح ترتيب هذه الخطوات في البرنامج النصي للميزة في الكود. كيان السيناريو مسؤول عن إدارة الترتيب الحالي للخطوات. سيبدو النص كما يلي:



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

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


في التنفيذ على سبيل المثال ، سيكون النص على النحو التالي:



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) {...}
}


يجب أن يؤخذ في الاعتبار أن أي تغيير في النص يجب أن يكون ذا اتجاهين. لنفترض أنك قمت بإزالة خطوة. تأكد من أنه إذا عاد المستخدم وحدد خيارًا مختلفًا ، فستتم إضافة الخطوة إلى البرنامج النصي.



كيف تبدو الكود ، على سبيل المثال ، كرد فعل على وجود أو عدم وجود خبرة في العمل
/**
 *      ,
 *           
 */
private fun applyExperienceToScenario(hasWorkingExperience: Boolean) {
    if (hasWorkingExperience) {
        steps.replaceWith(
            condition = { it == ABOUT_ME },
            newElem = EXPERIENCE
        )
    } else {
        steps.replaceWith(
            condition = { it == EXPERIENCE },
            newElem = ABOUT_ME
        )
    }
}




كيف يعمل Interactor



ضع في اعتبارك العنصر الأساسي التالي في بنية الميزة خطوة بخطوة - المتفاعل. كما قلنا أعلاه ، تتمثل مسؤوليتها الرئيسية في خدمة التبديل بين الخطوات: لإعطاء البيانات اللازمة للخطوات كمدخلات وتجميع بيانات المخرجات في طلب مسودة.



دعونا ننشئ فئة أساسية لمتفاعلنا ونضع فيه السلوك المشترك لجميع الميزات خطوة بخطوة.



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


يجب أن يعمل المتفاعل مع السيناريو الحالي: قم بإخطاره بإكمال الخطوة التالية حتى يتمكن السيناريو من إعادة بناء مجموعة الخطوات الخاصة به. لذلك ، سوف نعلن عن حقل مجرد لبرنامجنا النصي. الآن سيُطلب من كل متفاعل محدد تقديم التنفيذ الخاص به.



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


المتفاعل مسؤول أيضًا عن تخزين الحالة التي تكون فيها الخطوة نشطة حاليًا والتحول إلى الخطوة التالية أو السابقة. يجب عليه إخطار شاشة الجذر على الفور بتغيير الخطوة حتى تتمكن من التبديل إلى الجزء المطلوب. يمكن تنظيم كل هذا بسهولة باستخدام بث الحدث ، أي نهج تفاعلي. أيضًا ، غالبًا ما تؤدي أساليب المتفاعل لدينا عمليات غير متزامنة (تحميل البيانات من الشبكة أو قاعدة البيانات) ، لذلك سنستخدم RxJava للتواصل مع المتفاعل مع مقدمي العروض. إذا لم تكن معتادًا على هذه الأداة بالفعل ، فاقرأ هذه السلسلة من المقالات التمهيدية



لنقم بإنشاء نموذج يصف المعلومات المطلوبة بواسطة الشاشات حول الخطوة الحالية وموقعها في البرنامج النصي:



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


لنبدأ موضوع السلوك في المتفاعل لإصدار معلومات بحرية حول الخطوة النشطة الجديدة فيه.



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


حتى تتمكن الشاشات من الاشتراك في تدفق الأحداث هذا ، سننشئ متغيرًا عامًا متغيرًا قابل للمراقبة ، وهو عبارة عن غلاف فوق موضوع stepChangeSubject.



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


أثناء عمل المتفاعل ، غالبًا ما يكون من الضروري معرفة موضع الخطوة النشطة الحالية. أوصي بإنشاء خاصية منفصلة في المفاعل - currentStepIndex وتجاوز أساليب get () و set (). هذا يتيح لنا الوصول بسهولة إلى هذه المعلومات من الموضوع.



كيف يبدو في الكود
//   
private var currentStepIndex: Int
    get() = stepChangeSubject.value?.position ?: 0
    set(value) {
        stepChangeSubject.onNext(
            StepWithPosition(
                step = scenario.steps[value],
                position = value,
                allStepsCount = scenario.steps.count()
            )
        )
    }




دعنا نكتب جزءًا عامًا سيعمل بنفس الطريقة بغض النظر عن التنفيذ المحدد للمتفاعل للميزة.



دعنا نضيف طرقًا لتهيئة عمل المتفاعل وإنهائه ، مما يجعلها مفتوحة للتمديد في الأحفاد:



طرق التهيئة والإغلاق
/**
 *   
 */
@CallSuper
open fun initProgressFeature() {
    currentStepIndex = 0
}

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




دعنا نضيف الوظائف التي يجب أن يؤديها أي متفاعل للميزات خطوة بخطوة:



  • getDataForStep (الخطوة: S) - توفير البيانات كمدخلات للخطوة S ؛

  • CompleteStep (stepOut: O) - احفظ إخراج O وانقل البرنامج النصي إلى الخطوة التالية ؛

  • toPreviousStep () —- انقل البرنامج النصي إلى الخطوة السابقة.



لنبدأ بالوظيفة الأولى - معالجة بيانات الإدخال. سيحدد كل متفاعل نفسه كيف وأين تحصل على بيانات الإدخال. دعنا نضيف طريقة مجردة مسؤولة عن هذا:



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




لمقدمي شاشات معينة ، أضف طريقة عامة لاستدعاء resolveStepInData() :



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


يمكنك تبسيط هذا الرمز بجعل الطريقة عامة resolveStepInData(). getDataForStep()تمت إضافة الطريقة للقياس مع طرق إكمال الخطوة ، والتي سنناقشها أدناه.



لإكمال خطوة ما ، نقوم بالمثل بإنشاء طريقة مجردة يقوم فيها كل متفاعل محدد بحفظ نتيجة الخطوة.



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


وطريقة عامة. في ذلك سوف ندعو حفظ معلومات الإخراج. عند الانتهاء ، أخبر البرنامج النصي بالتكيف مع المعلومات من خطوة النهاية. سنقوم أيضًا بإخطار المشتركين بأننا نتقدم خطوة إلى الأمام.



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


أخيرًا ، نطبق طريقة للعودة إلى الخطوة السابقة.



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


دعنا نلقي نظرة على تنفيذ المتفاعل لمثال تطبيق الوظيفة الخاص بنا. كما نتذكر ، من المهم لميزتنا أن تحفظ البيانات في مسودة التطبيق ، لذلك ، في فئة ApplicationProgressInteractor ، سننشئ حقلاً إضافيًا للمسودة.



/**
 *    
 */
@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)
        }
    }
    ...
}


كيف تبدو مسودة الفصل
:



/**
 *  
 */
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()
    }
}




لنبدأ في تنفيذ الطرق المجردة المعلنة في الفصل الرئيسي. لنبدأ بوظيفة إكمال الخطوة - إنها بسيطة جدًا. نقوم بحفظ إخراج نوع معين في مسودة تحت المفتاح المطلوب:



/**
 *      
 */
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
            }
        }
    }
}


الآن دعنا نلقي نظرة على طريقة الحصول على بيانات الإدخال لخطوة:



/**
 *     
 */
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()
            )
        }
    }
}


عند فتح خطوة ، يوجد خياران:



  • يفتح المستخدم الشاشة لأول مرة ؛

  • قام المستخدم بالفعل بملء الشاشة ، وقمنا بحفظ البيانات في المسودة.



بالنسبة للخطوات التي لا تتطلب إدخال أي شيء ، سنقوم بتمرير المعلومات من المسودة (إن وجدت). 



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


إذا احتجنا إلى بيانات من الخطوات السابقة كمدخلات ، فسنقوم بسحبها من المسودة (تأكدنا من حفظها هناك في نهاية كل خطوة). وبالمثل ، سننقل البيانات إلى بيانات خارجية يمكننا بها ملء الشاشة مسبقًا.



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


هناك أيضًا موقف أكثر إثارة للاهتمام: تتطلب الخطوة الأخيرة ، حيث يكون من الضروري توضيح سبب اهتمام المستخدم بهذه الوظيفة الشاغرة تحديدًا ، قائمة بالأسباب المحتملة لتنزيلها من الشبكة. هذه واحدة من أكثر اللحظات ملاءمة في هذه العمارة. يمكننا إرسال طلب ، وعندما نتلقى إجابة ، نقوم بدمجه مع البيانات من المسودة وإرساله إلى الشاشة كمدخل. لا تحتاج الشاشة حتى إلى معرفة مصدر البيانات وعدد المصادر التي تجمعها.



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




مثل هذه المواقف هي حجة أخرى لصالح العمل من خلال المتفاعلين. في بعض الأحيان ، لتقديم خطوة مع البيانات ، تحتاج إلى دمج العديد من مصادر البيانات ، على سبيل المثال ، تنزيل من الويب ونتائج الخطوات السابقة.



في طريقتنا ، يمكننا دمج البيانات من العديد من المصادر وتزويد الشاشة بكل ما نحتاجه. قد يكون من الصعب التعرف على سبب أهمية ذلك في هذا المثال. في النماذج الحقيقية - على سبيل المثال ، عند التقدم بطلب للحصول على قرض - من المحتمل أن تحتاج الشاشة إلى إرسال الكثير من الكتب المرجعية ومعلومات حول المستخدم من قاعدة البيانات الداخلية والبيانات التي ملأها بخمس خطوات إلى الوراء ومجموعة من الحكايات الأكثر شيوعًا من عام 1970.



يكون كود المقدم أسهل بكثير عندما يتم التجميع بواسطة طريقة تفاعل منفصلة تنتج فقط النتيجة: بيانات أو خطأ. يسهل على المطورين إجراء التغييرات والتعديلات إذا كان من الواضح على الفور مكان البحث عن كل شيء.



لكن هذا ليس كل ما في المتفاعل. بالطبع ، نحتاج إلى طريقة لإرسال الطلب النهائي - بعد اجتياز جميع الخطوات. دعونا نصف التطبيق النهائي والقدرة على إنشائه باستخدام نمط "Builder"



فئة لتقديم الطلب النهائي
/**
 *  
 */
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()
                )
            }
        }
    }
}




طريقة ارسال الطلب نفسه:



/**
 *  
 */
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())
}


كيفية استخدامها كلها على الشاشات



الآن يستحق الأمر النزول إلى مستوى العرض ومعرفة كيف يتفاعل مقدمو الشاشة مع هذا المتفاعل.



ميزتنا هي نشاط به كومة من الأجزاء الداخلية.







يؤدي التقديم الناجح للتطبيق إلى فتح نشاط منفصل ، حيث يتم إبلاغ المستخدم بنجاح التقديم. سيكون النشاط الرئيسي مسؤولاً عن إظهار الجزء المطلوب ، اعتمادًا على أمر المتفاعل ، بالإضافة إلى عرض عدد الخطوات التي تم اتخاذها بالفعل في شريط الأدوات. للقيام بذلك ، في مقدم نشاط الجذر ، اشترك في الموضوع من المتفاعل وقم بتنفيذ منطق تبديل الأجزاء في المكدس.



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


الآن ، في مقدم كل جزء ، في بداية الشاشة ، سنطلب من المتفاعل أن يعطينا بيانات الإدخال. من الأفضل نقل البيانات المستقبلة إلى دفق منفصل ، لأنه ، كما ذكرنا سابقًا ، يمكن ربطها بالتنزيل من الشبكة.



على سبيل المثال ، لنأخذ الشاشة لملء معلومات التعليم.



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

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


لنفترض أننا أكملنا الخطوة "حول التعليم" ويريد المستخدم الذهاب إلى أبعد من ذلك. كل ما نحتاج إلى القيام به هو تكوين كائن مع الإخراج وتمريره إلى المتفاعل.



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


سيقوم المتفاعل بحفظ البيانات نفسها ، وبدء التغييرات في البرنامج النصي ، إذا لزم الأمر ، والإشارة إلى نشاط الجذر للتبديل إلى الخطوة التالية. وبالتالي ، لا تعرف الأجزاء أي شيء عن موضعها في البرنامج النصي: ويمكن إعادة ترتيبها بسهولة ، على سبيل المثال ، إذا تغير تصميم الميزة.



في الجزء الأخير ، كرد فعل على الحفظ الناجح للبيانات ، سنضيف إرسال الطلب النهائي ، كما نتذكر ، أنشأنا طريقة لهذا sendApplication()في المتفاعل.



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


في الشاشة الأخيرة مع المعلومات التي تفيد بأن الطلب قد تم إرساله بنجاح ، سنقوم بمسح المتفاعل بحيث يمكن إعادة تشغيل العملية من البداية.



progressInteractor.closeProgressFeature()


هذا كل شئ. لدينا ميزة تتكون من خمس شاشات. يمكن تخطي شاشة "حول التعليم" ، واستبدال الشاشة مع ملء خبرة العمل - بشاشة لكتابة مقال. يمكننا مقاطعة التعبئة في أي خطوة والمتابعة لاحقًا ، وسيتم حفظ كل ما أدخلناه في المسودة.



شكر خاص لفازيا Beglyanin icebail - مؤلف أول تنفيذ هذا النهج في المشروع. وأيضا ميشا زينتشينكو midery - للمساعدة في تحقيق مشروع العمارة إلى الإصدار النهائي، الذي يوصف في هذه المقالة.



All Articles