Bagaimana merancang pengisian data inkremental di aplikasi seluler

Halo! Nama saya Vita Sokolova, saya Pimpinan Tim Android di Surf .



Dalam aplikasi seluler, ada formulir dengan pengisian multi-langkah yang kompleks - misalnya, kuesioner atau aplikasi. Merancang fitur seperti itu biasanya menyebabkan sakit kepala bagi pengembang: sejumlah besar data ditransfer antara layar dan koneksi kaku terbentuk - siapa, kepada siapa, dalam urutan apa data ini harus dikirim dan layar mana yang akan dibuka berikutnya setelah itu sendiri.



Pada artikel ini saya akan membagikan cara mudah untuk mengatur pekerjaan fitur langkah demi langkah. Dengan bantuannya, dimungkinkan untuk meminimalkan koneksi antar layar dan dengan mudah membuat perubahan pada urutan langkah-langkah: menambah layar baru, mengubah urutannya dan logika tampilan kepada pengguna.







* Yang saya maksud dengan kata "fitur" dalam artikel ini adalah serangkaian layar dalam aplikasi seluler yang terhubung secara logis dan mewakili satu fungsi untuk pengguna.



Biasanya, pengisian formulir dan pengajuan aplikasi dalam aplikasi seluler terdiri dari beberapa layar berurutan. Data dari satu layar mungkin diperlukan di layar lainnya, dan langkah-langkahnya terkadang berubah tergantung pada jawabannya. Oleh karena itu, sangat berguna untuk memungkinkan pengguna menyimpan data "dalam draf" sehingga dia dapat kembali ke proses nanti.



Mungkin ada banyak layar, tetapi sebenarnya pengguna mengisi satu objek besar dengan data. Pada artikel ini saya akan memberi tahu Anda cara mudah mengatur pekerjaan dengan rangkaian layar yang merupakan satu skenario.



Misalkan seorang pengguna melamar pekerjaan dan mengisi formulir. Jika terputus di tengah, data yang dimasukkan akan disimpan dalam draf. Saat pengguna kembali mengisi, informasi dari draf akan secara otomatis diganti ke dalam bidang kuesioner - dia tidak perlu mengisi semuanya dari awal.



Saat pengguna mengisi seluruh kuesioner, tanggapannya akan dikirim ke server.



Kuesioner terdiri dari:



  • Langkah 1 - nama, jenis pendidikan, pengalaman kerja,

  • Langkah 2 - tempat belajar,

  • Langkah 3 - tempat kerja atau esai tentang diri Anda,

  • Langkah 4 - alasan mengapa lowongan itu menarik.









Kuesioner akan berubah tergantung pada apakah pengguna memiliki pendidikan dan pengalaman kerja. Jika tidak ada pendidikan, kami akan mengecualikan langkah dengan mengisi tempat studi. Jika tidak ada pengalaman kerja, minta pengguna untuk menulis sedikit tentang dirinya.







Pada tahap desain, kami harus menjawab beberapa pertanyaan:



  • Cara membuat skrip fitur fleksibel dan dapat dengan mudah menambah dan menghapus langkah.

  • Bagaimana memastikan bahwa ketika Anda membuka langkah, data yang diperlukan sudah terisi (misalnya, layar "Pendidikan" di pintu masuk menunggu jenis pendidikan yang sudah dikenal untuk membangun kembali komposisi bidangnya).

  • Bagaimana menggabungkan data menjadi model umum untuk ditransfer ke server setelah langkah terakhir.

  • Bagaimana cara menyimpan aplikasi ke "draf" sehingga pengguna dapat menginterupsi pengisian dan kembali lagi nanti.



Hasilnya, kami ingin mendapatkan fungsi berikut:







Seluruh contoh ada di repositori saya di GitHub 



Solusi yang jelas



Jika Anda mengembangkan fitur "dalam mode hemat daya penuh", hal yang paling jelas adalah membuat objek aplikasi dan mentransfernya dari layar ke layar, mengisinya kembali di setiap langkah.



Warna abu-abu terang akan menandai data yang tidak dibutuhkan pada suatu langkah tertentu. Pada saat yang sama, mereka dikirim ke setiap layar untuk akhirnya masuk ke aplikasi terakhir.







Tentu saja, semua data ini harus dimasukkan ke dalam satu objek aplikasi. Mari kita lihat bagaimana tampilannya:



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


TAPI!

Bekerja dengan objek seperti itu, kita menghukum kode kita untuk ditutupi dengan sejumlah pemeriksaan nol ekstra yang tidak perlu. Misalnya, struktur data ini sama sekali tidak menjamin bahwa bidang educationTypetersebut sudah terisi di layar "Pendidikan".



Bagaimana melakukan yang lebih baik



Saya sarankan untuk memindahkan manajemen data ke objek terpisah, yang akan memberikan data yang tidak dapat dinihilkan sebagai masukan untuk setiap langkah dan menyimpan hasil dari setiap langkah ke draf. Kami akan menyebut objek ini sebagai interaksion. Ini sesuai dengan lapisan Use Case dari arsitektur murni Robert Martin dan untuk semua layar bertanggung jawab untuk menyediakan data yang dikumpulkan dari berbagai sumber (jaringan, database, data dari langkah sebelumnya, data dari proposal draf ...).



Dalam proyek kami, kami di Surf menggunakan Dagger. Karena sejumlah alasan, merupakan kebiasaan untuk membuat lingkup interaksor @PerApplication: ini membuat interaktor kita menjadi tunggal dalam aplikasi. Faktanya, interaksinya bisa berupa singleton dalam sebuah fitur, atau bahkan aktivasi - jika semua langkah Anda adalah fragmen. Itu semua tergantung pada arsitektur aplikasi Anda secara keseluruhan.



Lebih lanjut dalam contoh, kita akan berasumsi bahwa kita memiliki satu contoh interaktor untuk seluruh aplikasi. Oleh karena itu, semua data harus dibersihkan saat skrip berakhir.







Saat mengatur tugas, selain penyimpanan data terpusat, kami ingin mengatur pengelolaan yang mudah dari komposisi dan urutan langkah-langkah dalam aplikasi: tergantung pada apa yang telah diisi oleh pengguna, mereka dapat mengubahnya. Oleh karena itu, kita membutuhkan satu entitas lagi - Skenario. Area tanggung jawabnya adalah menjaga urutan langkah-langkah yang harus dilalui pengguna.



Pengaturan fitur langkah-demi-langkah menggunakan skrip dan interaksior memungkinkan:



  • Tidak ada rasa sakit untuk mengubah langkah-langkah dalam skrip: misalnya, pekerjaan lebih lanjut tumpang tindih jika ternyata selama eksekusi ternyata pengguna tidak dapat mengirimkan permintaan atau menambahkan langkah jika diperlukan lebih banyak informasi.

  • Tetapkan kontrak: data apa yang harus di input dan output di setiap langkah.

  • Atur penyimpanan aplikasi ke draf jika pengguna belum menyelesaikan semua layar.



Isi ulang layar dengan data yang disimpan dalam draf.



Entitas dasar



Mekanisme fitur tersebut terdiri dari:



  • Seperangkat model untuk menggambarkan langkah, masukan dan keluaran.

  • Skenario - entitas yang menjelaskan langkah-langkah (layar) mana yang harus dilalui pengguna.

  • Interaktora (ProgressInteractor) - kelas yang bertanggung jawab untuk menyimpan informasi tentang langkah aktif saat ini, menggabungkan informasi yang diisi setelah menyelesaikan setiap langkah dan mengeluarkan data masukan untuk memulai langkah baru.

  • Draft (ApplicationDraft) - kelas yang bertanggung jawab untuk menyimpan informasi yang diisi. 



Diagram kelas mewakili semua entitas yang mendasari dari mana implementasi konkret akan mewarisi. Mari kita lihat bagaimana mereka terkait.







Untuk entitas Skenario, kami akan menetapkan antarmuka di mana kami akan menjelaskan logika apa yang kami harapkan untuk setiap skenario dalam aplikasi (berisi daftar langkah-langkah yang diperlukan dan membangunnya kembali setelah menyelesaikan langkah sebelumnya, jika perlu.



Aplikasi mungkin memiliki beberapa fitur, yang terdiri dari banyak layar berurutan, dan masing-masing akan menjadi Kami akan memindahkan semua logika umum yang tidak bergantung pada fitur atau data tertentu ke dalam kelas dasar ProgressInteractor.



ApplicationDraft tidak ada di kelas dasar, karena menyimpan data yang telah diisi pengguna ke draf mungkin tidak diperlukan. Oleh karena itu, implementasi konkret dari ProgressInteractor akan bekerja dengan draf tersebut. Presenter layar akan berinteraksi dengannya.



Diagram kelas untuk implementasi tertentu dari kelas dasar:







Semua entitas ini akan berinteraksi satu sama lain dan dengan penyaji layar sebagai berikut: Ada







beberapa kelas, jadi mari kita analisis setiap blok secara terpisah menggunakan fitur dari awal artikel.



Deskripsi langkah



Mari kita mulai dengan poin pertama. Kami membutuhkan entitas untuk menggambarkan langkah-langkah:



// ,   ,    

interface Step




Untuk fitur dari contoh lamaran kerja kita, langkah-langkahnya adalah sebagai berikut:



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



Kita juga perlu mendeskripsikan data masukan untuk setiap langkah. Untuk melakukan ini, kami akan menggunakan kelas tertutup untuk tujuan yang dimaksudkan - untuk membuat hierarki kelas terbatas.







Bagaimana tampilannya dalam kode
//   
interface StepInData


:



//,      
sealed class ApplicationStepInData : StepInData

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

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




Kami menggambarkan output dengan cara yang sama:







Bagaimana tampilannya dalam kode
// ,   
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()




Jika kami tidak menetapkan tujuan untuk menyimpan aplikasi yang tidak terisi dalam draf, kami dapat membatasi diri untuk ini. Tetapi karena setiap layar dapat terbuka tidak hanya kosong, tetapi juga diisi dari draf, baik data input maupun data dari draf akan masuk ke input dari interaksor - jika pengguna sudah memasukkan sesuatu.



Oleh karena itu, kami memerlukan model lain untuk menyatukan data ini. Beberapa langkah tidak memerlukan informasi untuk masuk dan hanya menyediakan bidang untuk data dari draf



Bagaimana tampilannya dalam kode
/**
 *     +   ,   
 */
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()
}




Kami bertindak sesuai dengan naskah



Dengan deskripsi langkah-langkah dan data input / output yang diurutkan. Sekarang mari kita perbaiki urutan langkah-langkah ini di skrip fitur di kode. Entitas Skenario bertanggung jawab untuk mengelola urutan langkah-langkah saat ini. Skripnya akan terlihat seperti ini:



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

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


Dalam implementasi contoh kita, skripnya akan seperti ini:



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


Perlu diingat bahwa setiap perubahan dalam skrip harus dua arah. Katakanlah Anda menghapus satu langkah. Pastikan jika pengguna kembali dan memilih opsi yang berbeda, langkah tersebut ditambahkan ke skrip.



Bagaimana, misalnya, apakah kode tersebut terlihat seperti reaksi terhadap ada atau tidaknya pengalaman kerja
/**
 *      ,
 *           
 */
private fun applyExperienceToScenario(hasWorkingExperience: Boolean) {
    if (hasWorkingExperience) {
        steps.replaceWith(
            condition = { it == ABOUT_ME },
            newElem = EXPERIENCE
        )
    } else {
        steps.replaceWith(
            condition = { it == EXPERIENCE },
            newElem = ABOUT_ME
        )
    }
}




Bagaimana Interactor bekerja



Pertimbangkan blok penyusun berikutnya dalam arsitektur fitur langkah demi langkah - sebuah interaksior. Seperti yang kami katakan di atas, tanggung jawab utamanya adalah melayani peralihan antar langkah: memberikan data yang diperlukan untuk masukan ke langkah-langkah tersebut dan menggabungkan data keluaran ke dalam draf permintaan.



Mari buat kelas dasar untuk interaksi kita dan taruh di dalamnya perilaku yang umum untuk semua fitur langkah demi langkah.



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


Interactor harus bekerja dengan skrip saat ini: beri tahu tentang penyelesaian langkah berikutnya sehingga skrip dapat membangun kembali rangkaian langkahnya. Oleh karena itu, kami akan mendeklarasikan bidang abstrak untuk skrip kami. Sekarang masing-masing interaksior tertentu akan diminta untuk menyediakan implementasinya sendiri.



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


Interactor juga bertanggung jawab untuk menyimpan keadaan langkah mana yang saat ini aktif dan beralih ke langkah berikutnya atau sebelumnya. Ini harus segera memberi tahu layar root tentang perubahan langkah tersebut sehingga dapat beralih ke fragmen yang diinginkan. Semua ini dapat dengan mudah diatur menggunakan siaran acara, yaitu pendekatan reaktif. Selain itu, metode interaksi kami akan sering melakukan operasi asinkron (memuat data dari jaringan atau database), jadi kami akan menggunakan RxJava untuk berkomunikasi dengan interaktor dengan presenter. Jika Anda belum terbiasa dengan alat ini, bacalah seri artikel pengantar ini



Mari buat model yang mendeskripsikan informasi yang diperlukan oleh layar tentang langkah saat ini dan posisinya dalam skrip:



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


Mari kita mulai BehaviorSubject di interaksor untuk secara bebas memancarkan informasi tentang langkah aktif baru ke dalamnya.



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


Agar layar dapat berlangganan aliran kejadian ini, kita akan membuat variabel publik stepChangeObservable, yang merupakan pembungkus dari stepChangeSubject kita.



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


Selama pekerjaan interaktor, seringkali perlu untuk mengetahui posisi langkah aktif saat ini. Saya merekomendasikan membuat properti terpisah di interactor - currentStepIndex dan menimpa metode get () dan set (). Ini memberi kami akses mudah ke informasi ini dari subjek.



Bagaimana tampilannya dalam kode
//   
private var currentStepIndex: Int
    get() = stepChangeSubject.value?.position ?: 0
    set(value) {
        stepChangeSubject.onNext(
            StepWithPosition(
                step = scenario.steps[value],
                position = value,
                allStepsCount = scenario.steps.count()
            )
        )
    }




Mari kita tulis bagian umum yang akan berfungsi sama terlepas dari implementasi spesifik dari interaksor untuk fitur tersebut.



Mari tambahkan metode untuk menginisialisasi dan menghentikan pekerjaan interaksionor, membuatnya terbuka untuk ekstensi di turunan:



Metode untuk inisialisasi dan penghentian
/**
 *   
 */
@CallSuper
open fun initProgressFeature() {
    currentStepIndex = 0
}

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




Mari tambahkan fungsi yang harus dilakukan oleh setiap fitur interaksior langkah demi langkah:



  • getDataForStep (langkah: S) - berikan data sebagai input ke langkah S;

  • completeStep (stepOut: O) - simpan output O dan pindahkan skrip ke langkah berikutnya;

  • toPreviousStep () —- Pindahkan skrip ke langkah sebelumnya.



Mari kita mulai dengan fungsi pertama - memproses data masukan. Setiap interaksior sendiri akan menentukan bagaimana dan dari mana mendapatkan data masukan. Mari tambahkan metode abstrak yang bertanggung jawab untuk ini:



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




Untuk presenter layar tertentu, tambahkan metode publik yang akan memanggil resolveStepInData() :



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


Anda dapat menyederhanakan kode ini dengan membuat metode menjadi publik resolveStepInData(). Metode getDataForStep()ditambahkan untuk analogi dengan metode penyelesaian langkah, yang akan kita bahas di bawah ini.



Untuk menyelesaikan sebuah langkah, kami juga membuat metode abstrak di mana setiap interaksior tertentu akan menyimpan hasil dari langkah tersebut.



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


Dan metode publik. Di dalamnya kita akan memanggil penyimpanan informasi keluaran. Jika sudah selesai, beri tahu script untuk menyesuaikan dengan informasi dari langkah akhir. Kami juga akan memberi tahu pelanggan bahwa kami sedang melangkah maju.



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


Terakhir, kami menerapkan metode untuk kembali ke langkah sebelumnya.



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


Mari kita lihat implementasi interactor untuk contoh lamaran kerja kita. Seperti yang kita ingat, penting bagi fitur kita untuk menyimpan data ke permintaan draf, oleh karena itu, di kelas ApplicationProgressInteractor, kita akan membuat bidang tambahan di bawah draf.



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


Seperti apa kelas draf itu
:



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




Mari mulai mengimplementasikan metode abstrak yang dideklarasikan di kelas induk. Mari kita mulai dengan fungsi penyelesaian langkah - ini cukup sederhana. Kami menyimpan data keluaran dari jenis tertentu ke draf di bawah kunci yang diperlukan:



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


Sekarang mari kita lihat metode untuk mendapatkan data masukan untuk sebuah langkah:



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


Saat membuka langkah, ada dua opsi:



  • pengguna membuka layar untuk pertama kalinya;

  • pengguna telah mengisi layar, dan kami telah menyimpan data di draf.



Untuk langkah-langkah yang tidak memerlukan apa pun untuk masuk, kami akan meneruskan informasi dari draf (jika ada). 



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


Jika kami membutuhkan data dari langkah sebelumnya sebagai input, kami akan menariknya dari draf (kami memastikan untuk menyimpannya di sana pada akhir setiap langkah). Demikian pula, kami akan mentransfer data ke outData yang dapat digunakan untuk mengisi layar.



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


Ada juga situasi yang lebih menarik: langkah terakhir, di mana Anda perlu menunjukkan mengapa pengguna tertarik pada lowongan khusus ini, memerlukan daftar kemungkinan alasan untuk diunduh dari jaringan. Ini adalah salah satu momen ternyaman dalam arsitektur ini. Kami dapat mengirim permintaan dan, ketika kami menerima jawaban, menggabungkannya dengan data dari draf dan mengirimkannya ke layar sebagai input. Layar bahkan tidak perlu mengetahui dari mana data tersebut berasal dan berapa banyak sumber yang dikumpulkannya.



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




Situasi seperti itu adalah argumen lain yang mendukung bekerja melalui interaksion. Terkadang, untuk memberikan langkah dengan data, Anda perlu menggabungkan beberapa sumber data, misalnya, unduhan dari web dan hasil langkah sebelumnya.



Dalam metode kami, kami dapat menggabungkan data dari banyak sumber dan menyediakan layar dengan semua yang kami butuhkan. Mungkin sulit untuk memahami mengapa ini bagus dalam contoh ini. Dalam bentuk nyata - misalnya saat mengajukan pinjaman - layar berpotensi perlu mengirimkan banyak buku referensi, informasi tentang pengguna dari database internal, data yang ia isi 5 langkah ke belakang, dan kumpulan anekdot terpopuler dari tahun 1970.



Kode penyaji jauh lebih mudah ketika agregasi dilakukan dengan metode interaksior terpisah yang hanya menghasilkan hasil: data atau kesalahan. Lebih mudah bagi pengembang untuk membuat perubahan dan penyesuaian jika segera jelas di mana harus mencari semuanya.



Tapi tidak hanya itu yang ada di interaksinya. Tentu saja, kita membutuhkan metode untuk mengirim aplikasi terakhir - ketika semua langkah telah dilalui. Mari kita gambarkan aplikasi akhir dan kemampuan untuk membuatnya menggunakan pola "Builder"



Kelas untuk mengajukan aplikasi akhir
/**
 *  
 */
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()
                )
            }
        }
    }
}




Metode pengiriman aplikasinya sendiri:



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


Cara menggunakan semuanya di layar



Sekarang ada baiknya turun ke tingkat presentasi dan melihat bagaimana penyaji layar berinteraksi dengan interaksi ini.



Fitur kami adalah aktivitas dengan tumpukan fragmen di dalamnya.







Pengiriman aplikasi yang berhasil membuka aktivitas terpisah, di mana pengguna diberi tahu tentang keberhasilan pengiriman. Aktivitas utama akan bertanggung jawab untuk menampilkan fragmen yang diinginkan, bergantung pada perintah interaksior, dan juga untuk menampilkan berapa banyak langkah yang telah diambil di toolbar. Untuk melakukan ini, di penyaji aktivitas root, berlangganan subjek dari interaksior dan terapkan logika untuk mengalihkan fragmen dalam tumpukan.



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


Sekarang di penyaji setiap fragmen, di awal layar, kami akan meminta interaksior untuk memberi kami data masukan. Lebih baik mentransfer data penerimaan ke aliran terpisah, karena, seperti yang disebutkan sebelumnya, ini dapat dikaitkan dengan pengunduhan dari jaringan.



Sebagai contoh, mari kita ambil layar untuk mengisi informasi pendidikan.



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

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


Misalkan kita menyelesaikan langkah "tentang pendidikan" dan pengguna ingin melangkah lebih jauh. Yang perlu kita lakukan hanyalah membentuk objek dengan output dan meneruskannya ke interaksor.



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


Interactor akan menyimpan datanya sendiri, memulai perubahan pada skrip, jika perlu, dan memberi sinyal pada aktivitas root untuk beralih ke langkah berikutnya. Karenanya, fragmen tidak tahu apa-apa tentang posisinya di skrip: dan dapat dengan mudah diatur ulang jika, misalnya, desain fitur telah berubah.



Pada fragmen terakhir, sebagai reaksi terhadap penyimpanan data yang berhasil, kami akan menambahkan pengiriman permintaan akhir, seperti yang kita ingat, kami membuat metode untuk ini sendApplication()di interaksor.



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


Pada layar terakhir dengan informasi bahwa aplikasi telah berhasil dikirim, kami akan menghapus interaksinya sehingga proses dapat dimulai ulang dari awal.



progressInteractor.closeProgressFeature()


Itu saja. Kami memiliki fitur yang terdiri dari lima layar. Layar "tentang pendidikan" bisa dilewati, layar dengan isian pengalaman kerja - diganti dengan layar untuk menulis karangan. Kami dapat menghentikan pengisian pada langkah mana pun dan melanjutkannya nanti, dan semua yang telah kami masukkan akan disimpan dalam draf.



Terima kasih khusus kepada Vasya Beglyanin @icebail - penulis implementasi pertama pendekatan ini dalam proyek. Dan juga Misha Zinchenko @midery - untuk bantuan dalam membawa rancangan arsitektur ke versi final, yang dijelaskan dalam artikel ini.



All Articles