DialogFragment ผ่านการเปลี่ยนแปลงการกำหนดค่าและการตายของกระบวนการ

Nov 29 2022
สวัสดีทุกคน เป็นเวลานานแล้วที่ฉันใช้สื่อเพื่อเรียนรู้สิ่งใหม่ๆ และตอนนี้เป็นเวลาที่ฉันจะได้เขียนบทความแรกของฉัน น่าจะเป็นจุดเริ่มต้นของใครอีกหลายคน

สวัสดีทุกคน,

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

บทนำ

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

บทความนี้เกี่ยวกับ DialogFragment แต่การสะท้อนจะเหมือนกันสำหรับ Fragment ใดๆ

เพื่อดำเนินการเปลี่ยนแปลงการกำหนดค่า

วิธีง่ายๆ ในการดำเนินการ "เปลี่ยนแปลงการกำหนดค่า" ในแอปของคุณคือการสลับระหว่างโหมดสว่าง/มืด กล่าวโดยสรุปคือ Fragments/Activity จะถูกสร้างขึ้นใหม่ด้วยอาร์กิวเมนต์ที่บันทึกไว้และที่บันทึก InstanceState แต่ ViewModels จะยังคงอยู่ ในกระบวนการตาย ViewModels กิจกรรม และ Fragments จะถูกสร้างขึ้นใหม่ทั้งหมด

เพื่อกระตุ้นการตายของกระบวนการ

- ตัวเลือกสำหรับนักพัฒนา (วิธีง่าย ๆ ) : ตัวเลือกสำหรับนักพัฒนา -> อย่าเก็บกิจกรรมไว้ -> เปิด
- ตัวเลือกสถานการณ์จริง: เรียกใช้แอพโลภจำนวนมากหรือสร้างแอพของคุณเองซึ่งจะใช้ความทรงจำจำนวนมาก (เช่นใช้การวนซ้ำ) จนกว่าอุปกรณ์จะประเมินว่าแอพที่เปิดก่อนหน้านี้ควรว่าง

ทุกครั้งที่คุณกลับมาที่แอปใดๆ ที่ปล่อยให้ว่าง แอปนั้นจะเปิดใหม่พร้อมกับอาร์กิวเมนต์/บันทึกInstanceState ที่บันทึกไว้ สิ่งเหล่านี้ควรเป็นแหล่งข้อมูลความจริงแหล่งเดียวที่คุณควรใส่ใจ

เป็นแนวปฏิบัติที่ดีเสมอที่จะทดสอบทุกหน้าจอในแอปของคุณโดยใช้การเปลี่ยนแปลงการกำหนดค่า / การหยุดทำงานของกระบวนการเพื่อให้แน่ใจว่าทุกอย่างทำงานตามที่คาดไว้ หากคุณพบข้อขัดข้องหรือพฤติกรรมแปลก ๆ เกี่ยวกับสิ่งนั้น อย่าเพิกเฉยเพราะมัน 'หายาก'...
ยิ่งกว่านั้น สำหรับอุปกรณ์บางรุ่นที่มีอายุการใช้งานแบตเตอรี่น้อยกว่า 15% อุปกรณ์จะเปลี่ยนเป็นโหมดมืดโดยอัตโนมัติเพื่อประหยัดแบตเตอรี่ ซึ่งหมายความว่าผู้ใช้สามารถดำเนินการบางอย่างแบบสุ่มในแอปของคุณ และแอปของคุณก็สามารถสร้างขึ้นใหม่ได้ในทันทีด้วยเหตุนี้

ฉันแนะนำข่าวร้ายอย่าทิ้งบทความไว้ :(

ถ้าฉันใช้เวลาอธิบายปัญหาเหล่านี้ นั่นเป็นเพราะมีบทความที่น่าสนใจมากมายที่นี่ซึ่งอธิบายวิธีเขียนโค้ดที่สวยงามใน kotlin โดยใช้ DSL พร้อมพารามิเตอร์ฟังก์ชันที่ส่งโดยตรงไปยังไดอะล็อก
ฉันไม่ใช่คนใหม่ในการพัฒนา Android และแม้ว่า Kotlin จะเป็นภาษาที่ยอดเยี่ยม แต่ฉันก็รักมันอย่างแน่นอน บางบทความควรดูแลการเปลี่ยนแปลงการกำหนดค่าหรือการตายของกระบวนการ มิฉะนั้น จะไม่ให้บริการนักพัฒนารุ่นเยาว์

สิ่งที่กำลังพูด

1°) ส่วนย่อย (และแน่นอน DialogFragment) ควรมีตัวสร้างว่างเสมอ ลองเปลี่ยนเป็นโหมดมืด/สว่างในขณะที่แฟรกเมนต์มีคอนสตรัคเตอร์พร้อมพารามิเตอร์ มันไม่ได้จบลงด้วยดี

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

2°) อย่าส่งฟังก์ชันเป็นอาร์กิวเมนต์ไปยังแฟรกเมนต์ของคุณผ่านบันเดิล ฟังก์ชันไม่สามารถแยกพัสดุ/จัดลำดับได้ แอปจะขัดข้องเมื่อมีการเปลี่ยนแปลงการกำหนดค่า

การส่งผ่านฟังก์ชันไปยังแฟรกเมนต์ใดๆ นั้นถือเป็นแนวทางปฏิบัติที่แย่มาก ยิ่งไปกว่านั้น อาจทำให้หน่วยความจำรั่วได้ ดังนั้นอย่าทำเช่นนั้น ฉันรู้ว่ามันง่ายที่จะพูดว่า: ' เฮ้ ถ้าคุณคลิกสำเร็จ ไปที่หน้าจอ X ' ด้วยแลมบ์ดา ฉันก็ทำเช่นนั้นเหมือนกันในตอนเริ่มต้นของการพัฒนา Android แต่เชื่อฉันเถอะ คุณไม่ต้องการส่งมอบผลิตภัณฑ์ที่ไม่เสถียรให้กับผู้ใช้ของคุณในเวอร์ชันที่ใช้งานจริง

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

วิธีการแก้

ดำเนินการร้องขอไปยัง DialogFragment ของคุณ รอผลลัพธ์ จากนั้นดำเนินการบางอย่างขึ้นอยู่กับผลลัพธ์ที่ได้รับ

กรณีการใช้งานที่เรียบง่าย:

คุณต้องการได้รับคำตอบจากผู้ใช้ของคุณสำหรับการอนุมัติการดำเนินการเฉพาะ การดำเนินการ 'ตกลง' หมายความว่าคุณจะไปยังหน้าจอถัดไปที่มีข้อมูลเฉพาะที่เกี่ยวข้องกับอินพุตของผู้ใช้
ในกรณีการใช้งานง่ายๆ ของเรา การกระทำของผู้ใช้อาจเป็น: 'ตกลง' 'ยกเลิก' (ปุ่ม UI 2 ปุ่ม) และ 'ปิด' (ไอคอนย้อนกลับ UI หรือการกดย้อนกลับของระบบ)
แต่โดยความเรียบง่าย เราจะถือว่า 'ปิด' ได้รับการจัดการเป็น 'ยกเลิก' ดังนั้น การโต้ตอบกับผู้ใช้จึงจำกัดการโต้ตอบไว้สองประเภท: 'ตกลง' หรือ 'ยกเลิก'

เราสามารถขอ DialogFragment ประเภทเดียวกันได้หลายครั้งในหนึ่งหรือหลายแฟรกเมนต์ขึ้นอยู่กับหลายบริบท นั่นเป็นเหตุผลที่เราจะให้ 'requestKey' ซึ่งโดยส่วนใหญ่แล้วจะไม่ซ้ำกับส่วนใดส่วนหนึ่ง สิ่งเดียวที่เราต้องการคือคำตอบของสิ่งที่เราขอ: 'ตกลง' หรือ 'ยกเลิก'

แนวคิดคือการส่ง 'requestKey' ไปยัง DialogFragment และรอการดำเนินการของผู้ใช้: 'ตกลง' หรือ 'ยกเลิก' สำหรับ 'requestKey' นี้

มีหลายวิธีในการสื่อสารระหว่างส่วนไดอะล็อกของคุณและส่วนย่อยที่ดำเนินการตามคำขอ

ฉันใช้เป็นการส่วนตัว:

- setFragmentResult จาก DialogFragment
- setFragmentResultListener ใน onCreate() จาก Fragment ของคุณที่ดำเนินการตามคำขอ

ฟังก์ชัน setFragmentResult มีพารามิเตอร์สองตัว: 'requestKey: String' และ 'result: Bundle' ฟังก์ชันนี้เผยแพร่ข้อมูลผ่าน parentFragmentManager ซึ่งหมายความว่าแฟรกเมนต์ใดๆ ที่ร้องขอด้วย requestKey เดียวกันจะไวต่อการตรวจจับผลลัพธ์

ด้านล่างนี้คือโค้ดที่ 'ทำให้ง่ายขึ้น' โดยใช้ส่วนขยายของฟังก์ชัน (ด้วยวิธีนี้ เราจะโฟกัสเฉพาะสิ่งที่สำคัญจริงๆ และเราสามารถเข้าใจได้ง่ายว่าใครเรียกอะไร ในทางปฏิบัติ แน่นอน ฟังก์ชันคัดลอก/วางเนื้อหาของฟังก์ชันแฟรกเมนต์/ไดอะล็อกแฟรกแฟรกเมนต์ที่มีอยู่):

enum class UserAction {
    OK, CANCEL
}

/*******************
 * DIALOG FRAGMENT *
 *******************/

/**
 * In your DialogFragment, the contract mentions that REQUEST_KEY_ARG_KEY is a mandatory arg.
 * You will notice that it works for Fragment too, not only DialogFragment
 * In our case, a device back press or UI back press will invoke onDismiss(), just override it and call this function
 * @param userAction 'Yes' or 'Cancel'
 */
fun DialogFragment.deliverUserAction(userAction: UserAction) {
    setFragmentResult(requireArguments().getString(REQUEST_KEY_ARG_KEY)!!,
                      bundleOf(USER_ACTION_RESULT_KEY to userAction,
                               REQUEST_DATA_BUNDLE_KEY to requireArguments().getBundle(REQUEST_DATA_BUNDLE_KEY)))
}

/**
 * Defined in public in your DialogFragment
 */
const val REQUEST_KEY_ARG_KEY = "request_key" // Mandatory in arg
const val REQUEST_DATA_BUNDLE_KEY = "request_data" // Optional in arg and result, it is just a way to forward contextual data
const val USER_ACTION_RESULT_KEY = "user_action" // Mandatory in result

/***********************************
 * FRAGMENT REQUESTING USER ACTION *
 ***********************************/

/**
 * Listen to a user action
 * Of cource, use it in real override onCreate function
 */
fun Fragment.fakeOverrideOnCreate() {
    setFragmentResultListener(REQUEST_KEY_TO_GO_TO_SCREEN_B) { _, bundle: Bundle ->
        // Called once in Lifecycle.State.ON_START
        val userAction: UserAction = bundle.getSerializable(USER_ACTION_RESULT_KEY) as UserAction // We know at this point that we will get an user action
        val requestData: Bundle? = bundle.getBundle(REQUEST_DATA_BUNDLE_KEY) // Optional because we don't necessary need data at this point for some usecase
        when (userAction) {
            UserAction.OK -> {
                TODO("Do something with ${requestData} if you need it to navigate to next screen")
            }
            UserAction.CANCEL -> TODO()
        }
    }
}

/**
 * Request a user action
 * @see fakeOverrideOnCreate directly related to it
 * @param requestKey requestKey is mandatory
 * @param requestData optional, some data you may need to forward to dialog fragment and get back in setFragmentResultListener
 */
fun Fragment.navigateToMyDialogFragmentX(requestKey: String, requestData: Bundle?) {
    bundleOf(
        REQUEST_KEY_ARG_KEY to requestKey,
        REQUEST_DATA_BUNDLE_KEY to requestData
    )
    // etc.
    // depending on if you are using Navigation component or not
}

fun Fragment.calledAnywhereInFragmentRequestingUserAction1() {
    navigateToMyDialogFragmentX(requestKey = REQUEST_KEY_TO_GO_TO_SCREEN_B,
                                requestData = bundleOf("input1" to "Here", "input2" to "we", "input3" to "go!"))
}

/**
 * Defined in private in your fragment requesting user action
 */
private const val REQUEST_KEY_TO_GO_TO_SCREEN_B = "request_key_to_go_to_screen_B"

ด้านล่างนี้คือการปรับปรุงบางอย่างโดยใช้โฟลว์

/***************************************
 * YOURFRAGMENT REQUESTING USER ACTION *
 ***************************************/

private val _userActionResultMutableStateFlow: MutableStateFlow<Pair<String, Bundle>?> = MutableStateFlow(null)
private val userActionResultStateFlow: StateFlow<Pair<String, Bundle>?> get() = _userActionResultMutableStateFlow

/**
 * Listen to a user action
 * Of cource, use it in real override onCreate function
 */
fun Fragment.fakeOverrideOnCreate() {
    setFragmentResultListener(REQUEST_KEY_TO_GO_TO_SCREEN_B) { requestKey, bundle: Bundle ->
        _userActionResultMutableStateFlow.value = requestKey to bundle
    }
}

/**
 * Listen to a user action and consume it only when fragment is the currentBackStackEntry
 * Of cource, use it in real override onViewCreated function
 */
fun Fragment.fakeOverrideOnViewCreated() {
    viewLifecycleOwner.lifecycleScope.launch {
        combine((findNavController().currentBackStackEntryFlow), userActionResultStateFlow) { currentBackStackEntry, userActionResult ->
            if (currentBackStackEntry.destination.id in R.id.YOURFRAGMENT_DEFINED_IN_NAV_GRAPH) {
                userActionResult
            } else {
                null
            }
        }.filterNotNull().collect { (requestKey, bundle) ->
            consumeUserActionResult(requestKey, bundle)
            _userActionResultMutableStateFlow.value = null // don't forget to notify that user action has been consumed
        }
    }
}

/**
 * consume user action result
 */
fun consumeUserActionResult(requestKey: String,
                            bundle: Bundle) {
    when (requestKey) {
        REQUEST_KEY_TO_GO_TO_SCREEN_B -> {
            val userAction: UserAction = bundle.getSerializable(USER_ACTION_RESULT_KEY) as UserAction // We know at this point that we will get an user action
            val requestData: Bundle? = bundle.getBundle(REQUEST_DATA_BUNDLE_KEY) // Optional because we don't necessary need data at this point for some usecase
            when (userAction) {
                UserAction.OK -> {
                    TODO("Do something with ${requestData} if you need it to navigate to next screen")
                }
                UserAction.CANCEL -> TODO()
            }
        }
    }
}

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

หากคุณเรียนรู้บางอย่างหรือถ้าคุณคุ้นเคยอยู่แล้ว แต่รู้สึกว่าคนอื่นควรรู้เกี่ยวกับสิ่งนี้ ปรบมือตามสบาย :)

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

เพียงเพื่อแบ่งปัน คนที่น่าสนใจสองสามคนที่ฉันแนะนำให้คุณเรียนรู้ด้วย:

เจค วอร์ตัน
- ทวิตเตอร์:https://twitter.com/JakeWharton
- ปานกลาง:https://medium.com/@techyourchance
Vasiliy Zukanov
- ทวิตเตอร์ :https://twitter.com/VasiliyZukanov
- ปานกลาง :https://medium.com/@techyourchance
Christophe Beyls
- ทวิตเตอร์:https://twitter.com/BladeCoder
- ปานกลาง:https://medium.com/@bladecoder
Gabor Varadi
- ทวิตเตอร์:https://twitter.com/Zhuinden
- ปานกลาง:https://medium.com/@zhuinden

หมายเหตุ: ฉันไม่รู้จักคนเหล่านี้เป็นการส่วนตัว ฉันแค่ติดตามพวกเขาเพราะพวกเขาเก่งและพวกเขาใช้งาน Twitter