DialogFragment thông qua thay đổi cấu hình và chết tiến trình.

Nov 29 2022
Xin chào mọi người, Đã lâu rồi tôi mới sử dụng phương tiện để tiếp tục học hỏi những điều mới và bây giờ là lúc để tôi viết bài viết đầu tiên của mình. Có lẽ là sự khởi đầu của nhiều người khác.

Chào mọi người,

Đã lâu rồi tôi không sử dụng phương tiện để tiếp tục học những điều mới và bây giờ là lúc để tôi viết bài báo đầu tiên của mình. Có lẽ là sự khởi đầu của nhiều người khác.

Giới thiệu

Tại sao tôi sắp nói về DialogFragment liên quan đến thay đổi cấu hình và quá trình chết? Bởi vì chúng tôi quan tâm đến trải nghiệm người dùng và chúng tôi không thích các báo cáo sự cố hoặc hành vi lạ khi xảy ra sự cố.

Bài viết này là về DialogFragment nhưng phản xạ là như nhau đối với bất kỳ Fragment nào.

Để thực hiện thay đổi cấu hình

Một cách dễ dàng để thực hiện "thay đổi cấu hình" trong ứng dụng của bạn là chuyển đổi giữa chế độ sáng/tối. Nói tóm lại, Fragment/Activities sẽ được tạo lại với các đối số đã lưu và saveInstanceState nhưng ViewModels sẽ vẫn tồn tại. Trong một quá trình chết, ViewModels, Activity và Fragments sẽ được tạo lại.

Để kích động một quá trình chết

- Tùy chọn nhà phát triển (cách đơn giản): Tùy chọn nhà phát triển -> Không giữ hoạt động -> bật.
- Tùy chọn tình huống thực tế: chạy một loạt ứng dụng tham lam hoặc tạo ứng dụng của riêng bạn sẽ tiêu tốn rất nhiều bộ nhớ (ví dụ: sử dụng vòng lặp) cho đến khi thiết bị ước tính rằng các ứng dụng đã mở trước đó sẽ được giải phóng.

Mỗi khi bạn quay lại bất kỳ ứng dụng nào đã được giải phóng, ứng dụng đó sẽ được khởi chạy lại với các đối số / saveInstanceState được lưu. Những luận điểm này phải là nguồn dữ liệu sự thật duy nhất mà bạn thực sự nên quan tâm.

Luôn luôn là một phương pháp hay để kiểm tra mọi màn hình trong ứng dụng của bạn bằng cách sử dụng thay đổi cấu hình/chết quá trình để đảm bảo rằng mọi thứ hoạt động như mong đợi. Nếu bạn bị treo máy hay có những biểu hiện lạ về việc đó thì đừng bỏ qua vì đây là 'hiếm'...
Hơn nữa, đối với một số thiết bị còn dưới 15% pin, thiết bị sẽ tự động chuyển sang chế độ tối để tiết kiệm pin. điều đó có nghĩa là người dùng có thể thực hiện một số hành động ngẫu nhiên trong ứng dụng của bạn và ứng dụng của bạn đột nhiên có thể được tạo lại vì điều đó.

À, mình giới thiệu một số tin xấu, đừng bỏ bài nhé :(

Nếu tôi dành thời gian để giải thích các vấn đề này, thì đó là bởi vì có rất nhiều bài báo hay ở đây giải thích cách viết một số mã đẹp trong kotlin bằng DSL với các tham số chức năng được gửi trực tiếp đến hộp thoại.
Tôi không phải là người mới trong lĩnh vực phát triển Android và mặc dù thực tế rằng Kotlin là một ngôn ngữ tuyệt vời, nhưng tôi chắc chắn yêu thích nó, một số bài viết nên quan tâm đến việc thay đổi cấu hình hoặc xử lý chết, nếu không, chúng không phục vụ các nhà phát triển cơ sở.

Những điều đang được nói

1°) Một đoạn (và tất nhiên là DialogFragment) phải luôn có một hàm tạo trống. Cố gắng chuyển sang chế độ tối/sáng trong khi một đoạn có hàm tạo có tham số; nó không kết thúc tốt.

Ngoài ra, khi điều hướng đến một DialogFragment, không chuyển các hàm cho DialogFragment này thông qua các hàm được xác định trong DialogFragment của bạn để lưu nó trong biến cục bộ vì khi thay đổi cấu hình, đoạn này sẽ được khôi phục nhưng không có các hàm này được gọi; vì vậy các biến cục bộ của bạn sẽ không được khởi tạo. Chỉ hàm tạo rỗng và các hàm được ghi đè (như onCreate, onCreateDialog, v.v.) từ DialogFragment mới được gọi. Các đối số và đã lưuInstanceState chỉ nên là mối quan tâm của bạn.

2°) Không chuyển các hàm làm đối số cho đoạn của bạn thông qua một gói. Một chức năng không thể chia thành từng phần/tuần tự hóa, ứng dụng sẽ gặp sự cố khi thay đổi cấu hình.

Chuyển các chức năng cho bất kỳ đoạn nào là một cách làm rất tệ; Hơn nữa, nó có thể dẫn đến rò rỉ bộ nhớ, vì vậy đừng làm điều đó. Tôi biết, thật dễ dàng để nói: ' Này, nếu bạn nhấp vào thành công, hãy chuyển đến màn hình X ' bằng lambda. Tôi cũng đã làm điều đó khi bắt đầu phát triển Android nhưng tin tôi đi, bạn không muốn cung cấp một sản phẩm không ổn định cho người dùng của mình trong quá trình sản xuất.

3°) Sử dụng ViewModel được chia sẻ? Trời ơi không ! Đầu tiên, vì quá trình chết và sau đó là vì bạn không muốn đau đầu trong khi gỡ lỗi vì lý do tại sao nó không hoạt động trong một số trường hợp. Chế độ xem được chia sẻ là khái niệm dễ bị lỗi nhất. Nó có thể hữu ích trong một vài trường hợp nhưng nhiều nhất, đừng sử dụng nó.

Dung dịch

Thực hiện một yêu cầu đối với DialogFragment của bạn, đợi kết quả và sau đó thực hiện điều gì đó tùy thuộc vào kết quả nhận được.

Một trường hợp sử dụng đơn giản:

Bạn muốn nhận được câu trả lời từ người dùng của mình để phê duyệt một hành động cụ thể. Hành động 'Ok' ngụ ý rằng bạn sẽ điều hướng đến màn hình tiếp theo với dữ liệu cụ thể liên quan đến đầu vào của người dùng.
Trong trường hợp sử dụng đơn giản của chúng tôi, hành động của người dùng có thể là: 'Ok', 'Cancel' (2 nút giao diện người dùng) và 'Dismiss' (biểu tượng quay lại giao diện người dùng hoặc nhấn quay lại hệ thống).
Nhưng để đơn giản, chúng tôi sẽ xem xét rằng 'Loại bỏ' được xử lý như một hành động 'Hủy'. Vì vậy, tương tác của người dùng được giới hạn ở hai loại tương tác: 'Ok' hoặc 'Cancel'.

Chúng tôi có khả năng có thể yêu cầu nhiều lần cùng một loại DialogFragment trong một hoặc một số đoạn tùy thuộc vào nhiều ngữ cảnh. Đó là lý do tại sao chúng tôi sẽ cung cấp một 'requestKey', trong hầu hết các trường hợp, là duy nhất cho một đoạn cụ thể. Điều duy nhất chúng tôi muốn là câu trả lời cho những gì chúng tôi đã yêu cầu: 'Ok' hoặc 'Cancel'.

Ý tưởng là phát ra 'requestKey' cho DialogFragment và chờ hành động của người dùng: 'Ok' hoặc 'Cancel' cho 'requestKey' này.

Có nhiều cách để giao tiếp giữa đoạn hộp thoại của bạn và đoạn thực hiện yêu cầu.

Cá nhân tôi đang sử dụng:

- setFragmentResult từ DialogFragment
- setFragmentResultListener trong onCreate() từ Fragment của bạn đang thực hiện một yêu cầu

hàm setFragmentResult có hai tham số: 'requestKey: String' và 'result: Bundle'. Hàm này truyền dữ liệu thông qua parentFragmentManager, điều đó có nghĩa là bất kỳ đoạn nào đang yêu cầu với cùng một requestKey đều có thể nhận được kết quả.

Dưới đây là một số mã 'đơn giản hóa' bằng cách sử dụng các phần mở rộng hàm (theo cách đó, chúng tôi chỉ tập trung vào những gì thực sự quan trọng và chúng tôi có thể dễ dàng hiểu ai gọi cái gì. Tất nhiên, trong thực tế, sao chép/dán hàm phần thân của hàm phân đoạn/đoạn hộp thoại hiện có):

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"

Dưới đây là một số cải tiến khi sử dụng các luồng

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

Bạn sẽ nhận thấy rằng có rất nhiều mã không quá nhiều. Hơn nữa, bạn có thể nghĩ rằng bạn sẽ cần sao chép mã soạn sẵn của đề tài trên mỗi đoạn bằng cách sử dụng đoạn hội thoại của bạn. Có và không. Đại biểu là một công cụ mạnh mẽ và sẽ cho phép bạn viết mã này chỉ một lần. Hãy cho tôi biết nếu bạn quan tâm đến một cải tiến như vậy để thêm vào bài viết này hay không.

Nếu bạn học được điều gì đó hoặc nếu bạn đã quen thuộc nhưng cảm thấy rằng những người khác nên biết về điều này, hãy vỗ tay :).

Tôi thích chia sẻ kiến ​​thức và nếu bạn không hiểu điều gì đó hoặc nếu tôi viết sai điều gì đó, đừng ngần ngại bình luận bên dưới. Tôi sẽ trả lời nó và tôi sẽ sửa đổi nếu có sai sót.

Chỉ là để chia sẻ, một vài người thú vị tôi khuyên bạn nên tìm hiểu với:

Jake Wharton
- Twitter:https://twitter.com/JakeWharton
- Vừa phải:https://medium.com/@techyourchance
Vasiliy Zukanov
- Twitter :https://twitter.com/VasiliyZukanov
- Vừa phải :https://medium.com/@techyourchance
Christophe Beyls
- Twitter:https://twitter.com/BladeCoder
- Vừa phải:https://medium.com/@bladecoder
Gabor Varadi
- Twitter:https://twitter.com/Zhuinden
- Vừa phải:https://medium.com/@zhuinden

Lưu ý: Tôi không biết cá nhân những người này, tôi chỉ theo dõi họ vì họ tốt và họ đang hoạt động tích cực trên Twitter.