DialogFragment ผ่านการเปลี่ยนแปลงการกำหนดค่าและการตายของกระบวนการ
สวัสดีทุกคน,
เป็นเวลานานแล้วที่ฉันใช้สื่อเพื่อเรียนรู้สิ่งใหม่ๆ และตอนนี้เป็นเวลาที่ฉันเขียนบทความแรก น่าจะเป็นจุดเริ่มต้นของใครอีกหลายคน
บทนำ
ทำไมฉันถึงพูดถึง 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