Computer >> คอมพิวเตอร์ >  >> ระบบ >> Android

วิธีใช้ Model-View-ViewModel บน Android อย่างมืออาชีพ

เป้าหมายของฉันในบทความนี้คือการอธิบายว่าทำไมรูปแบบสถาปัตยกรรม Model-View-ViewModel จึงนำเสนอการแยกข้อกังวลที่น่าอึดอัดใจในบางสถานการณ์เกี่ยวกับตรรกะการนำเสนอของสถาปัตยกรรม GUI

เราจะสำรวจ MVVM สองรูปแบบ (มี ไม่ เพียงวิธีเดียวเท่านั้น) และเหตุผลที่คุณอาจชอบตัวแปรหนึ่งมากกว่าตัวแปรอื่น โดยขึ้นอยู่กับข้อกำหนดของโครงการ

MVVM เทียบกับ MVP/MVC หรือไม่

มีความเป็นไปได้ค่อนข้างมากที่คำถามที่พบบ่อยที่สุดที่ฉันถูกถามระหว่างเซสชันถาม &ตอบในวันอาทิตย์แบบสดของฉันคือ:

MVVM เทียบกับ MVP/MVC?

เมื่อใดก็ตามที่ฉันถูกถามคำถามนี้ ฉันเน้นอย่างรวดเร็วว่าไม่มีสถาปัตยกรรม GUI ใดที่ทำงานได้ดีในทุกสถานการณ์

ทำไมคุณอาจถาม? สถาปัตยกรรมที่ดีที่สุด (หรืออย่างน้อยก็เป็นทางเลือกที่ดี) สำหรับแอปพลิเคชันที่กำหนดขึ้นอยู่กับข้อกำหนดที่มีอยู่

ให้เราคิดสั้น ๆ เกี่ยวกับคำนี้ว่า ข้อกำหนด จริงๆแล้วหมายถึง:

  • UI ของคุณซับซ้อนแค่ไหน โดยทั่วไป UI แบบธรรมดาไม่ต้องการตรรกะที่ซับซ้อนในการประสานงาน ในขณะที่ UI ที่ซับซ้อนอาจต้องการตรรกะที่กว้างขวางและการควบคุมที่ละเอียดเพื่อให้ทำงานได้อย่างราบรื่น
  • คุณสนใจเรื่องการทดสอบมากแค่ไหน โดยทั่วไป คลาสที่เชื่อมต่อกับเฟรมเวิร์กและ OS อย่างแน่นหนา (โดยเฉพาะ อินเทอร์เฟซผู้ใช้ ) ต้องการงานพิเศษเพื่อทดสอบ
  • คุณต้องการส่งเสริมการใช้งานซ้ำและนามธรรมมากน้อยเพียงใด จะเป็นอย่างไรถ้าคุณต้องการแชร์แบ็คเอนด์ โดเมน และแม้แต่ตรรกะการนำเสนอของแอปพลิเคชันของคุณในแพลตฟอร์มต่างๆ
  • คุณโดยธรรมชาติหรือเปล่า ในทางปฏิบัติ , พวกชอบความสมบูรณ์แบบ , ขี้เกียจ หรือทั้งหมดข้างต้น ในเวลาต่างกัน ในสถานการณ์ต่างกัน

ฉันชอบที่จะเขียนบทความที่ฉันพูดคุยอย่างละเอียดว่า MVVM ทำงานอย่างไรโดยคำนึงถึงข้อกำหนดและข้อกังวลที่ระบุไว้ข้างต้น น่าเสียดาย ที่พวกคุณบางคนอาจเข้าใจผิดคิดว่ามีทางเดียวเท่านั้นที่จะทำ MVVM

แต่ฉันจะพูดถึงสองแนวทางที่แตกต่างกันในแนวคิดทั่วไปของ MVVM ซึ่งมีข้อดีและข้อเสียที่แตกต่างกันมาก แต่ก่อนอื่น เรามาเริ่มด้วยแนวคิดทั่วไปกันก่อน

คุณจะไม่อ้างอิงถึงคลาสของมุมมองของคุณ

สำหรับเพื่อนๆ ที่ไม่สามารถอ่านภาษาอังกฤษแบบเก่าได้:คุณไม่สามารถอ้างอิงคลาสการดู ."

นอกเหนือจากการใช้ชื่อ ViewModel (ซึ่งทำให้สับสนหากคลาสเต็มไปด้วย ตรรกะ ) กฎเหล็กหุ้มข้อหนึ่งของสถาปัตยกรรม MVVM คือคุณไม่สามารถอ้างอิง View จาก ViewModel ได้

ตอนนี้ ความสับสนช่วงแรกอาจเกิดขึ้นจากคำว่า "การอ้างอิง" ซึ่งฉันจะพูดใหม่โดยใช้ศัพท์แสงที่แตกต่างกันหลายระดับ:

  • ViewModels ของคุณต้องไม่มีการอ้างอิงใดๆ (ตัวแปรสมาชิก คุณสมบัติ ฟิลด์ที่เปลี่ยนแปลง/เปลี่ยนไม่ได้) ไปยัง Views ใดๆ
  • ViewModels ของคุณอาจไม่ขึ้นอยู่กับการดูใดๆ
  • ViewModels ของคุณอาจไม่พูดคุยกับ Views ของคุณโดยตรง

ตอนนี้ บนแพลตฟอร์ม Android สาเหตุของกฎนี้ไม่ใช่แค่การฝ่าฝืนกฎเท่านั้นที่แย่ เพราะคนที่ดูเหมือนจะรู้เกี่ยวกับสถาปัตยกรรมซอฟต์แวร์บอกคุณว่ากฎนี้ไม่ดี

เมื่อใช้คลาส ViewModel จาก Architecture Components (ซึ่งได้รับการออกแบบให้มีอินสแตนซ์ คงอยู่ นานกว่าวงจรชีวิตของ Fragment/Activity ตามความเหมาะสม ) การอ้างอิง View กำลังขอการรั่วไหลของหน่วยความจำที่ร้ายแรง .

สำหรับสาเหตุที่ MVVM โดยทั่วไปไม่อนุญาตให้มีการอ้างอิงดังกล่าว เป้าหมายคือ สมมุติ เพื่อให้ทั้ง View และ ViewModel ง่ายต่อการทดสอบและเขียน

คนอื่นๆ อาจชี้ให้เห็นด้วยว่าคุณลักษณะนี้ส่งเสริมความสามารถในการใช้ซ้ำของ ViewModels แต่นี่เป็นตรงที่สิ่งต่างๆ แตกสลายด้วยรูปแบบนี้ .

ก่อนที่เราจะดูโค้ดนี้ โปรดทราบว่า โดยส่วนตัวแล้วฉันไม่ได้ใช้ LiveData ในรหัสการผลิตของฉันเอง ฉันชอบที่จะเขียนรูปแบบผู้เผยแพร่-สมาชิกของฉันเองในทุกวันนี้ แต่สิ่งที่ฉันพูดด้านล่างนี้ใช้ได้กับไลบรารีใดๆ ที่อนุญาตให้ลิงก์รูปแบบ PubSub/Observer จาก ViewModel ไปยังมุมมอง

บทความนี้มาพร้อมกับวิดีโอแนะนำที่ครอบคลุมแนวคิดเดียวกันมากมายที่นี่:

ViewLogic + ViewModel หรือ View + ViewModelController?

เมื่อฉันพูดว่า "พัง" ในส่วนก่อนหน้า ฉันไม่ได้หมายถึงว่ารูปแบบนั้นแตกจริงๆ ฉันหมายความว่ามันแบ่งออกเป็น (อย่างน้อย) สองแนวทางที่แตกต่างกันซึ่งมีรูปลักษณ์ ประโยชน์ และผลที่ตามมาที่แตกต่างกันมาก

ให้เราพิจารณาสองแนวทางนี้ และเมื่อคุณต้องการจะเลือกวิธีใดวิธีหนึ่งมากกว่ากัน

วิธีใช้ Model-View-ViewModel บน Android อย่างมืออาชีพ
โบโรเมียร์อธิบายว่า MVVM ไม่ใช่ไม้กายสิทธิ์ที่ทำให้ตรรกะการนำเสนอของแอปพลิเคชันของคุณหายไป

แนวทางแรก:จัดลำดับความสำคัญของ ViewModels ที่นำกลับมาใช้ใหม่ได้

เท่าที่ฉันสามารถบอกได้ คนส่วนใหญ่ที่ใช้ MVVM ตั้งเป้าที่จะส่งเสริมความสามารถในการใช้งานซ้ำของ ViewModels เพื่อให้สามารถนำมาใช้ซ้ำสำหรับ n จำนวนการดูที่แตกต่างกัน (อัตราส่วนหลายต่อหนึ่ง)

พูดง่ายๆ ก็คือ มีสองวิธีที่คุณสามารถใช้ซ้ำได้:

  • โดยไม่ได้อ้างอิงข้อมูลพร็อพเพอร์ตี้เฉพาะ หวังว่านี่ไม่ใช่ข่าวสำหรับคุณในตอนนี้
  • โดย รู้ ให้น้อยที่สุดเกี่ยวกับรายละเอียดของ UI โดยทั่วไป

จุดที่สองอาจฟังดูคลุมเครือหรือขัดกับสัญชาตญาณ (มันจะรู้อะไรเกี่ยวกับสิ่งที่ไม่ได้อ้างอิงได้อย่างไร) ดังนั้นฉันคิดว่าถึงเวลาต้องดูโค้ดแล้ว:

class NoteViewModel(val repo: NoteRepo): ViewModel(){
    //Note: you may also publish data to the View via Databinding, RxJava Observables, and other approaches. Although I do not like to use LiveData in back end classes, it works great with Android front end with AAC
    val noteState: MutableLiveData<Note>()
    //...
    fun handleEvent(event: NoteEvent) {
        when (event) {
            is NoteEvent.OnStart -> getNote(event.noteId)
            //...
        }
    }
    private fun getNote(noteId: String){
        noteState.value = repo.getNote(noteId)
    }
}

แม้ว่านี่จะเป็นตัวอย่างที่ง่ายมาก แต่ประเด็นก็คือสิ่งเดียวที่ ViewModel นี้เปิดเผยต่อสาธารณะ (นอกเหนือจากฟังก์ชัน handleEvent) เป็นวัตถุ Note แบบธรรมดา:

data class Note(val creationDate:String,
                val contents:String,
                val imageUrl: String,
                val creator: User?)

ด้วยวิธีการเฉพาะนี้ ViewModel จึงสามารถแยกออกจาก View ใด View หนึ่งได้อย่างแท้จริง แต่ยังรวมถึงรายละเอียด และโดยการขยาย ตรรกะการนำเสนอ ของมุมมองเฉพาะใดๆ

หากสิ่งที่ฉันพูดยังคงคลุมเครือ ฉันสัญญาว่ามันจะชัดเจนเมื่อฉันอธิบายวิธีอื่น

แม้ว่าหัวข้อก่อนหน้าของฉัน “ViewLogic + ViewModel… ” ไม่ได้มีไว้เพื่อใช้หรือดำเนินการอย่างจริงจัง ฉันหมายความว่าโดยการมี ViewModels ที่แยกส่วนและนำกลับมาใช้ใหม่ได้มาก ตอนนี้เราจึงขึ้นอยู่กับตัว View เองในการหาวิธีแสดงผล/ผูกวัตถุ Note นี้บนหน้าจอ

พวกเราบางคนไม่ชอบเติม View class ด้วย Logic

นี่คือจุดที่สิ่งต่างๆ กลายเป็นโคลนและขึ้นอยู่กับข้อกำหนดของโครงการ . ฉันไม่ได้บอกว่าการเติมดูคลาสด้วยตรรกะเช่น…:

private fun observeViewModel() {
    viewModel.notes.observe(
        viewLifecycleOwner,
        Observer { notes: List<Note> ->
            if (notes.isEmpty()) showEmptyState()
            else showNoteList(notes)
        }
    )
   //..
}

…คือ เสมอ เป็นสิ่งที่ไม่ดี แต่คลาสที่เชื่อมต่อกับแพลตฟอร์มอย่างแน่นหนา (เช่น Fragments) นั้นยากต่อการทดสอบ และคลาสที่มีตรรกะในนั้นคือคลาสที่สำคัญที่สุดในการทดสอบ!

พูดง่ายๆ ก็คือ การนำสิ่งที่ฉันคิดว่าเป็นหลักการสีทองของสถาปัตยกรรมที่ดีมาใช้นั้น ถือเป็นความล้มเหลว:การแยกข้อกังวล

ความคิดเห็นส่วนตัวของฉันคือควรใช้การแยกข้อกังวลในระดับที่สูงมาก แต่อย่าพลาดว่าแอปพลิเคชั่น Cash cow จำนวนมากเขียนขึ้นโดยผู้ที่ไม่มีเงื่อนงำน้อยที่สุดเกี่ยวกับความหมาย

แนวทางที่เราจะพูดถึงต่อไปในขณะที่มีผลข้างเคียงของตัวเอง อีกครั้งจะลบตรรกะการนำเสนอออกจากมุมมอง

ส่วนใหญ่ก็แล้วแต่

แนวทางที่สอง:Humble View, Control-Freak ViewModel

บางครั้งไม่มีการควบคุมการดูของคุณอย่างละเอียด (ซึ่งเป็นผลมาจากการจัดลำดับความสำคัญของความสามารถในการใช้งานซ้ำของ ViewModels) จริง ๆ แล้วแย่จัง

เพื่อทำให้ฉันไม่ค่อยกระตือรือร้นที่จะใช้วิธีการก่อนหน้านี้โดยไม่เลือกปฏิบัติ ฉันพบว่าฉันบ่อยครั้ง อย่า จำเป็นต้องใช้ ViewModel ซ้ำ .

แดกดัน “สิ่งที่เป็นนามธรรมมากเกินไป” เป็นการวิจารณ์ทั่วไปของ MVP เหนือ MVVM

อย่างที่กล่าวไปแล้ว เราไม่สามารถเพิ่มการอ้างอิงกลับเข้าไปใน ViewModel เพื่อที่จะได้กลับมาควบคุมมุมมองที่ละเอียดนี้อีกครั้ง โดยพื้นฐานแล้วจะเป็นเพียงแค่ MVP + หน่วยความจำรั่ว (สมมติว่าคุณยังคงใช้ ViewModel จาก AAC)

ทางเลือกอื่นคือสร้าง ViewModels ของคุณให้มีพฤติกรรมเกือบทั้งหมด , สถานะ และ ตรรกะการนำเสนอ ของมุมมองที่กำหนด มุมมองจะต้องยังคงผูกกับ ViewModel แน่นอน แต่มีรายละเอียดเพียงพอเกี่ยวกับ View อยู่ใน ViewModel ที่ฟังก์ชั่นของ View จะลดลงเหลือหนึ่งซับ (มีข้อยกเว้นเล็กน้อย)

ในการตั้งชื่อแบบแผนการตั้งชื่อของ Martin Fowler สิ่งนี้เรียกว่า Passive View/Screen ชื่อที่ใช้กันโดยทั่วไปสำหรับแนวทางนี้คือ รูปแบบวัตถุที่ต่ำต้อย .

เพื่อให้บรรลุสิ่งนี้ คุณต้องให้ ViewModel ของคุณมีฟิลด์ที่สังเกตได้ (อย่างไรก็ตาม คุณทำได้ – การผูกข้อมูล, Rx, LiveData, อะไรก็ตาม) สำหรับทุกการควบคุมหรือวิดเจ็ตที่มีอยู่ในมุมมอง:

class UserViewModel(
    val repo: IUserRepository,
){

    //The actual data model is kept private to avoid unwanted tampering
    private val userState = MutableLiveData<User>()

    //Control Logic
    internal val authAttemptState = MutableLiveData<Unit>()
    internal val startAnimation = MutableLiveData<Unit>()

    //UI Binding
    internal val signInStatusText = MutableLiveData<String>()
    internal val authButtonText = MutableLiveData<String>()
    internal val satelliteDrawable = MutableLiveData<String>()

    private fun showErrorState() {
        signInStatusText.value = LOGIN_ERROR
        authButtonText.value = SIGN_IN
        satelliteDrawable.value = ANTENNA_EMPTY
    }
    //...
}

ต่อจากนี้ View ยังคงต้องเชื่อมต่อกับ ViewModel แต่ฟังก์ชันที่จำเป็นในการทำเช่นนั้นจะกลายเป็นเรื่องง่ายในการเขียน:

class LoginView : Fragment() {

    private lateinit var viewModel: UserViewModel
    //...
    
    //Create and bind to ViewModel
    override fun onStart() {
        super.onStart()
        viewModel = ViewModelProviders.of(
        //...   
        ).get(UserViewModel::class.java)

        //start background anim
        (root_fragment_login.background as AnimationDrawable).startWithFade()

        setUpClickListeners()
        observeViewModel()

        viewModel.handleEvent(LoginEvent.OnStart)
    }

    private fun setUpClickListeners() {
      //...
    }

    private fun observeViewModel() {
        viewModel.signInStatusText.observe(
            viewLifecycleOwner,
            Observer {
                //"it" is the value of the MutableLiveData object, which is inferred to be a String automatically
                lbl_login_status_display.text = it
            }
        )

        viewModel.authButtonText.observe(
            viewLifecycleOwner,
            Observer {
                btn_auth_attempt.text = it
            }
        )

        viewModel.startAnimation.observe(
            viewLifecycleOwner,
            Observer {
                imv_antenna_animation.setImageResource(
                    resources.getIdentifier(ANTENNA_LOOP, "drawable", activity?.packageName)
                )
                (imv_antenna_animation.drawable as AnimationDrawable).start()
            }
        )

        viewModel.authAttemptState.observe(
            viewLifecycleOwner,
            Observer { startSignInFlow() }
        )

        viewModel.satelliteDrawable.observe(
            viewLifecycleOwner,
            Observer {
                imv_antenna_animation.setImageResource(
                    resources.getIdentifier(it, "drawable", activity?.packageName)
                )
            }
        )
    }

คุณสามารถค้นหาโค้ดแบบเต็มสำหรับตัวอย่างนี้ได้ที่นี่

ตามที่คุณอาจสังเกตเห็น เราอาจจะไม่ใช้ ViewModel นี้ซ้ำ ที่อื่น . นอกจากนี้ มุมมองของเรายังถ่อมตัวมากพอ (ขึ้นอยู่กับมาตรฐานและความชอบของคุณสำหรับการครอบคลุมโค้ด) และเขียนได้ง่ายมาก

บางครั้ง คุณอาจเจอสถานการณ์ที่คุณต้องหามาตรการครึ่งหนึ่งระหว่างการแจกแจงตรรกะของการนำเสนอ ระหว่าง Views และ ViewModels ซึ่งไม่ปฏิบัติตามแนวทางใดแนวทางหนึ่งเหล่านี้อย่างเคร่งครัด

ฉันไม่ได้สนับสนุนแนวทางหนึ่งมากกว่าวิธีอื่น แต่สนับสนุนให้คุณมีความยืดหยุ่นในแนวทางของคุณ โดยพิจารณาจากข้อกำหนดที่มีอยู่

เลือกสถาปัตยกรรมของคุณตามการตั้งค่าและข้อกำหนด

ประเด็นของบทความนี้คือการดูสองแนวทางที่แตกต่างกันซึ่งนักพัฒนาซอฟต์แวร์สามารถใช้ในแง่ของการสร้างสถาปัตยกรรม GUI สไตล์ MVVM บนแพลตฟอร์ม Android (โดยบางส่วนจะส่งต่อไปยังแพลตฟอร์มอื่น)

อันที่จริง เราอาจเจาะจงมากขึ้นเกี่ยวกับความแตกต่างเล็กๆ น้อยๆ แม้จะอยู่ในสองแนวทางนี้

  • หากมุมมองสังเกตฟิลด์สำหรับวิดเจ็ต/การควบคุมแต่ละรายการที่มี หรือควรสังเกตฟิลด์หนึ่งซึ่งเผยแพร่ โมเดล เดียว เพื่อแสดงมุมมองใหม่ทุกครั้งหรือไม่
  • บางทีเราอาจไม่ต้องทำให้ ViewModels ของเราเป็นแบบตัวต่อตัว ในขณะที่รักษามุมมองของเราให้เป็นวัตถุที่อ่อนน้อมถ่อมตน เพียงแค่เพิ่มบางอย่าง เช่น ผู้นำเสนอหรือตัวควบคุมลงในมิกซ์

การพูดคุยมีราคาถูก และฉันขอแนะนำอย่างยิ่งให้คุณลองเรียนรู้สิ่งเหล่านี้ ในโค้ด เพื่อจะได้ไม่ต้องพึ่งคนอย่างผมมาบอกว่าต้องทำยังไง

ท้ายที่สุด ฉันคิดว่าสององค์ประกอบที่สร้างสถาปัตยกรรมที่ยอดเยี่ยมนั้นมาจากข้อควรพิจารณาต่อไปนี้:

ประการแรก เล่นกับหลายวิธีจนกว่าคุณจะพบวิธีที่คุณชอบ . ทำได้ดีที่สุดโดยการสร้างแอปพลิเคชันจริง (อาจเป็นเรื่องง่าย) ในแต่ละสไตล์ และดูว่ารู้สึกใช่อย่างไร .

ประการที่สอง เข้าใจว่าการตั้งค่านอกเหนือจากความชอบ สไตล์ที่แตกต่างกันมักจะเน้นถึงประโยชน์ที่แตกต่างกันเพื่อแลกกับการขาดดุลที่แตกต่างกัน ในที่สุด คุณจะสามารถเลือกทางเลือกที่ดีโดยพิจารณาจากความเข้าใจในข้อกำหนดของโครงการมากกว่าความเชื่อที่มองไม่เห็น .

เรียนรู้เพิ่มเติมเกี่ยวกับสถาปัตยกรรมซอฟต์แวร์:

โซเชียล

https://www.instagram.com/rkay301/
https://www.facebook.com/wiseassblog/
https://twitter.com/wiseass301
https://wiseassblog.com/