Android

Tạo Thiết lập trong Android (tiếp)

Sau bài trước, tôi hi vọng các bạn đã có thể tạo thành công một PreferenceActivity hoặc một PreferenceFragment(Compat) nằm trong một Activity. Trong bài này, tôi sẽ giới thiệu thêm về các Preference cũng như hướng dẫn bạn tạo giao diện PreferenceActivity theo hướng 2 cột để tối ưu hóa giao diện cho máy tính bảng.

6. Thao tác với các Preference trong Java

Mặc dù tự các Preference trong Activity/Fragment, sau khi được nạp, đã tự thân chúng đã được cấu hình sẵn để hiển thị các trạng thái theo các giá trị thiết lập trước đó, cũng như lưu giá trị mới khi người dùng thao tác, và người viết code không cần thiết phải tự “chỉ trỏ” cho mỗi Preference là “Nè, khi người dùng thay đổi giá trị thì mày nhớ lưu vào đó”, tuy nhiên trong một số các trường hợp – và cũng không phải là hiếm – bạn vẫn cần phải yêu cầu cái Preference làm thêm một số việc khác. Và may mắn thay, bạn không cần phải tốn công sức làm một bộ class để cụ thể hóa ý tưởng đó, mà bạn có thể thao tác trực tiếp với Preference đó trong phần Java logic.

preferencescreen

Bấy giờ, giả sử tôi muốn Toast khi người dùng toggle vào SwitchPreference Use external browser, thì trước hết, tôi phải tìm được tới cái Preference đó trước. Sau đó, tôi sẽ dùng một trong hai methods Preference#setOnPreferenceChangeListener(Preference.OnPreferenceChangeListener) hoặc Preference#setOnPreferenceClickListener(Preference.OnPreferenceClickListener). Tôi nghĩ là bạn có thể đoán được mục đích của hai methods này qua tên gọi của chúng. Chẳng hạn, ở trường hợp này thì code như sau, và phần code này nên nằm sau addPreferencesFromResource(int) ở cả Fragment và Activity:

Chắc bạn đang thắc mắc là 2 methods Preference.OnPreferenceChangeListener#onPreferenceChange(Preference p, Object newValue) và Preference.OnPreferenceClickListener#onPreferenceClick (Preference p) sẽ return ra boolean true hay false. Cũng như các boolean khác trong Android Event interfaces, thì nếu bạn muốn duy trì hành vi mặc định của mỗi Preference, tức là bạn chỉ muốn “chèn thêm” một số đoạn mã của bạn mà thôi, thì bạn cho return true. Còn ngược lại, nếu bạn muốn “ngăn cản” hành vi mặc định của mỗi Preference, muốn kiểm soát hoàn toàn hành vi của Preference đó, thì cho return false. Ở đoạn code bên trên, tôi chỉ muốn thêm mỗi cái Toast vào cái SwitchPreference, còn hành vi mặc định của nó thì vẫn sẽ được duy trì, nên tôi sẽ chọn return true.

7. Gán Intent vào Preference:

Tất nhiên, bạn hoàn toàn có thể dùng Preference.OnPreferenceChangeListener#onPreferenceChange(Preference p, Object newValue) hoặc Preference.OnPreferenceClickListener#onPreferenceClick (Preference p) và gán một Intent vào event mà bạn muốn, chẳng hạn để khi người dùng click vào Preference, thì Activity hiện tại sẽ chuyển cảnh sang Activity khác. Thông thường nhất, là việc bạn tích hợp mời người dùng ghé thăm trang web của bạn trong phần Thiết lập – và ở đây thì thông thường, người ta dùng class Preference nguyên thủy thay cho TwoStatePreference hay DialogPreference. Nhưng bạn cũng có thể “đặt cứng” Intent này vào trong phần XML preferences luôn. Chẳng hạn:

8. Sự khác biệt giữa android.preference.PreferenceFragment, android.support.v14.preference.PreferenceFragment và android.support.v7.preference.PreferenceFragmentCompat:

  • android.preference.PreferenceFragment trong gói thư viện Framework được extends từ Fragment trong thư viện Framework, tức android.app.Fragment. Do đó, khi thao tác với tên này, trong Activity, bạn phải gọi các methods từ thư viện Framework. Bên cạnh đó, bạn gọi các attrs của Preference từ namespace android. Một điểm lưu ý cho các bạn muốn thay đổi cả cái ListView hiển thị các Preference, thì nó là một ListView có id là android.R.id.list.
  • android.support.v7.preference.PreferenceFragmentCompat trong gói thư viện Support V7 được extends từ Fragment trong thư viện Support, tức android.support.v4.app.Fragment. Do đó, khi thao tác với tên này, trong Activity, bạn phải gọi các methods từ thư viện Support. Bên cạnh đó, bạn gọi một số attrs của Preference từ namespace android, trong khi một số attrs khác từ namespace app. Một điểm lưu ý cho các bạn muốn thay đổi cả cái ListView hiển thị các Preference, thì nó không phải là một ListView đâu. Nó là một RecyclerView có id là android.support.v7.R.id.list. Tuy nhiên, mặc định trong PreferenceFragment(Compat) cung cấp sẵn một số methods để thao tác với RecyclerView đó mà không cần lấy nó ra, chẳng hạn setDivider(Drawable) và setDividerHeight(int)…
  • android.support.v14.preference.PreferenceFragment là trung gian giữa hai tên bên trên. Nó là kết quả của việc extends android.app.Fragment theo hướng biến/extends android.support.v4.app.Fragment ra android.support.v7.app.PreferenceFragment. Chẳng hạn, nó sử dụng RecyclerView thay cho ListView, và id của tên RV này là android.support.v7.R.id.list, cùng các tùy chọn giống như PreferenceFragmentCompat.

9. Preference Headers:

Đây là một cách “đường mương” khá mệt mỏi và dễ làm bạn hoang mang tới mức não xoắn trong lần đầu thao tác. Tất nhiên, có những người có ít nhiều kinh nghiệm (như tôi chẳng hạn :D) biết rõ phần giao diện trước khi đánh phím đầu tiên, nên mức độ xoắn não cũng không quá cao, nhưng đối với những bạn có câu cửa miệng là “Em mới học”, “Em non kinh nghiệm lắm” mà muốn dùng đao to búa lớn ngay từ đầu mà không “thèm” học những cái cơ bản nhất – mà đa số các bạn này chưa biết rành về giao diện mà cứ thích “code sao cho đẹp” – sẽ rơi vào tình trạng hoang mang tột đỉnh và đi hỏi khắp nơi.

Mục đích của Preference Headers là tối ưu giao diện Thiết lập cho các thiết bị có màn hình lớn, chẳng hạn máy tính bảng và TV. Tất nhiên, bạn có thể làm bằng tay với kiến thức khá cơ bản về Fragment là đã đủ rồi, và tôi thích tự “làm ăn” theo kiểu đó hơn. Nhưng đã lỡ nói về Preferences thì thôi, tôi giải bày phần này luôn cho nó đủ bộ.

android_settings_tablet

Bấy giờ, bạn đang thấy một giao diện Thiết lập rất đẹp, đẹp tới mức “lung linh” và bạn đang nghĩ làm ra nó cũng tốn kém nhiều công sức lắm. Và thực tế cũng không hẳn là tốn công lắm. Bên trái là danh sách các nhóm thiết lập. Và với từ “các nhóm”, tôi không có ý giới hạn ở mỗi PreferenceCategory, và thực chất mấy cái tên nhỏ nhỏ như “Wireless & Networks” hay “Device” cũng chẳng phải PreferenceCategory như bạn đang “ngộ nhận” – không giống như trong bài trước đâu. Ở đây, “các nhóm” bao gồm “Wireless & Network”, “Wifi”, “Bluetooth”, “Data usage”, “More”, “Device”, “Display”, “Sound & notification”, v.v… Và ở đây, bạn sẽ phải lên giao diện cho rõ ràng trước, phân bổ các thiết lập con sao cho hợp lí nhất trước khi nhấn phím lần đầu tiên.

Tất cả những tên tôi vừa nói sẽ nằm trong một tập tin có “mô típ” như bên dưới. Và không cần nói, bạn cũng có thể đoán ngay cái “đuôi” của tập tin này là gì, và vị trí của nó trong project. Trước khi thắc mắc vì sao tôi chỉ làm có vài cái, tôi gửi bạn câu trả lời trước: Vì tôi lười.

Đấy là cái XML của cái bên trái, và tôi tạm gọi tên là preference_headers.xml cho bạn tiện theo dõi. Ngoài titleicon, thì nó còn có thêm một attr trỏ tới cái Fragment tương ứng khi người dùng click vào một header. Thông tin thêm, mỗi <header> chính là một PreferenceActivity.Header instance, và như các bạn đang dần đoán ra, <preference-headers> chính là một instance của List<PreferenceActivity.Header>. Một điều quan trọng nữa, là mỗi cái Fragment sẽ phải extends android.preference.PreferenceFragment.

“What the truong giang? PreferenceActivity.Header à? Như vậy là tôi lại quay lại PreferenceAcitivity sao?” – Chuẩn rồi. Bạn đang quay lại PreferenceActivity đây, do đó chỉ có mỗi cái method addPreferencesFromResource(int prefRes) và vài cái liên quan của PreferenceActivity là bị deprecated thôi, chứ nguyên cả cái class đâu có bị deprecated! Và như vậy, bạn đã thấy được điểm ức chế đầu tiên rồi đó, đó là “Hôm trước ông nói là nên dùng cách tiếp cận bằng PreferenceFragment(Compat) thay cho PreferenceActivity, sao nay lại dùng lại Activity nữa?” – Vì nó Android nó đã là như vậy rồi. Một thắc mắc khác mà có thể các bạn đang muốn hỏi, là liệu ở đây có thư viện Support tương ứng không, vì PreferenceActivity chỉ có trong thư viện Framework, và bạn không thấy class tương ứng với nó trong cả V7 và V14 của thư viện Support. Cái đó thì khoan, tôi sẽ nói sau.

Bây giờ, tôi sẽ mời bạn đọc lại cái XML tôi vừa viết bên trên trước khi chuyển tới đoạn kế tiếp, và bạn vui lòng đọc kĩ từng dòng một. Nào, 1, 2, 3. Quay lên!

Sau khi đọc lại xong, tôi nghĩ bạn sẽ thốt lên: “Ối làng xóm ơi! Vậy tôi phải làm hàng hà sa số Fragment tương ứng với số lượng Header sao?”. Đúng vậy, đấy là ức chế thứ hai đó. Và quả thật, ức chế này còn lớn hơn cái lúc nãy tới vài lần. Bình tĩnh nào, bình tĩnh nào, không phải không có cách lách luật đâu. Bình tĩnh nào. À hèm.

Mỗi header có cung cấp cho chúng ta một meta tag có tên là <extra>. Và khi thấy tới tag này thì bạn sẽ nghĩ tới gì nào? 1, 2, 3 ——-> BUNDLE – giống giống Activity#getIntent().getExtras() đúng không? Nhưng ở đây, cái Bundle này chính là Bundle arguments của Fragment đó. Arguments, arguments, arguments… nghe quen quen… nó chính là Fragment#getArguments() đó thôi.

Android Bundle tương tự như Map<K,V>, tức là có 2 phần. Một phần là Key, và phần kia là Value, quá quen thuộc rồi đúng không. Và thẻ extra cũng có 2 attrs tương ứng là key và value, được gọi từ namespace android. Chẳng hạn, tôi “đính” tag extra vào vài cái headers: – Và đừng hỏi vì sao tôi chỉ làm với vài cái? Là vì tôi lười!

Nếu bạn đang thắc mắc “What the tran thanh? Thêm cái extra để làm gì cho tốn công?” thì đây: Nó giúp bạn tiết kiệm công sức ở chỗ khác. Đúng hơn là tiết kiệm số Fragment. Lưu ý chỉ là số Fragment mà thôi, chứ không phải là số preferences.xml tương ứng với các PreferenceFragment đâu. Chúng vẫn vậy thôi. Giả sử tôi “tứ long nhất thể” mấy cái PreferenceFragment kia lại, có tên là SettingsNetworkFragment, thì tôi làm như sau:

Và thay vì có tới 4 cái addPreferencesFromResource riêng rẽ, tôi “quy tựu” chúng lại 1 chỗ duy nhất. Một lần nữa, tôi nhắc lại, bạn vẫn duy trì số lượng XML như cũ:

Đỡ rườm rà, vướng víu hơn rồi đúng không? Tất nhiên, bạn chỉ có thể “hợp thể” các PreferenceFragment có cùng một mô típ giao diện với nhau lại mà thôi. Đối với những PreferenceFragment cần có giao diện riêng, chẳng hạn như cái PreferenceFragment về lượng pin như trong hình, thì bạn cần dựng giao diện cho nó.

android_settings_tablet

Và như các bạn có thể hình dung được, các headers là những tên nằm phía bên trái trong hình trên, và những cái Fragment sẽ nằm bên phải, ví dụ như Fragment về lượng pin đang nằm ở bên phải. Khoan đã! Bạn sẽ làm gì với các headers đây? Gọi addPreferencesFromResource(int headerPrefs) trong onCreate(Bundle)? Không đâu. Mấy cái headers không “chơi” với method đó, mà nó sẽ cần phải được load trong một life cycle method hoàn toàn mới mẻ với bạn, có tên là onBuildHeaders(List<PreferenceActivity.Header> target). Vai trò của argument target khá tương đồng với argumnent menu trong onCreateOptionsMenu(Menu menu). Để load các headers, bạn sẽ dùng một method tuy lạ nhưng không lại có tên là loadHeadersFromResource(int headerPref, List<PreferenceActivity.Header> target). Và bạn có thể đã nhận ra, cái target sẽ được mang từ trên xuống dưới, từ ngoài vào trong:

Vậy là xong xuôi vụ headers. Như bạn đã thấy, phần headers này tốn khá nhiều thời gian để trình bày, và trên thực tế, khi làm nó cũng mất thời gian với tỉ lệ tương đương với thời gian bạn làm các PreferenceScreen. “Và dường như bạn nhận ra một điều, bạn đã yêu tablet nhiều lắm” khi dùng headers, vì giao diện trên điện thoại cũng chia thành 2 cột như trên và tất cả sẽ nhìn như một mớ hổ lốn. Không đâu, Google tính cho bạn vụ này rồi. Trên điện thoại, thì màn hình của PreferenceActivity chỉ hiển thị cột bên trái, và khi người dùng nhấn vào một mục, nó sẽ chuyển sang một Activity mới chứa Fragment tương ứng. Một điều hay ho nữa, là bạn chả cần quan tâm mà cấu hình cái Activity này, PreferenceActivity đã lo sẵn rồi. Bạn chỉ cần “xì tốp” ở việc tạo headers và các Fragments tương ứng là được rồi.

Lưu ý là bạn mấy cái key và value của <extra> trong <header> chỉ là “lưu hành nội bộ” để PreferenceActivity phân phối giao diện và nếu bạn dùng SharedPreferences#get*(String key, Object defVal) với key của các <extra> thì defVal luôn được return về, trừ khi bạn đã edit và commit cái value khác. Đơn giản, cái key đó đâu phải là key của Preference nào đâu!

10. Dùng thư viện Support cho Preference headers:

Như tôi đã nói, PreferenceActivity không có class tương ứng trong gói thư viện Preference Support, kể cả V7 và V14. Vì vậy, bạn sẽ phải chọn 1 trong số những con đường sau:

  • Extends AppCompatActivity theo hướng PreferenceActivity extends Activity: Cách này nghe dễ, nhưng làm khó. Tuy nhiên, vẫn còn dễ chán so với cách bên dưới.
  • Extends PreferenceActivity theo hướng AppCompatActivity extends Activity: Cách này là vô cùng khó. Nhưng sau khi bạn thành công, thì bạn sẽ trở thành “Bóng đèn sáng nhất của Điện quang”, chinh chiến mọi địa hình trên Android. Và lỡ như bạn cạo trọc đầu và được các Android devs khác thách thức, thì cứ bình tĩnh mà trả lời: “Thưa mấy thí chủ cùi bắp cùi mía, Bần tăng chưa ngán ai bao giờ”.
  • Sử dụng cách của Android Studio cung cấp sẵn: Extends PreferenceActivity theo hướng áp dụng các AppCompatDelegate instances. Cách này là một vơ sần “nửa mùa” của cách bên trên, vì nó chỉ “Support hóa” mỗi PreferenceActivity mà thôi, còn để sử dụng được android.support.v7.preference.PreferenceFragmentCompat thì không được. Nên bạn có thể “né” bằng cách sử dụng android.support.v14.preference.PreferenceFragment. Xét về độ dễ: Cách này dễ ẹc, vì Android Studio nó sẽ làm sẵn Base class có tên là AppCompatPreferenceActivity, bạn chỉ cần chọn New -> Activity -> Settings là nó sẽ cho bạn class này sẵn, với 1 class mẫu extends class này. Chỗ này, họ hay ở việc đặt tên, AppCompatPreferenceActivity thay cho PreferenceActivityCompat.

Tuy nhiên, cá nhân tôi lại thích làm theo cách của mình hơn. Chỉ đơn giản xuất phát từ việc tối ưu hóa giao diện cho tablet theo hướng hai cột mà thôi. Trên thực tế, thì ngay cả Google cũng rất lười biếng sử dụng preference headers, bởi tỉ trọng của các Android tablets là không cao. Ngoài các ứng dụng về phía hệ thống Android, không có nhiều các ứng dụng dịch vụ của Google có phần Thiết lập được tối ưu hóa cho tablet, trong khi The Big G không tiếc sức “hôn hít phần sau” của Apple khi khá nhiều các ứng dụng dịch vụ tương ứng lại có phần Thiết lập rất đẹp trên iPad. Vì vậy, nếu lười dùng preference headers thì bạn cũng đừng quá lo lắng, vì ai cũng như ai thôi.

11. AppCompatPreferenceActivity:

Như tôi đã nói, khi bạn chọn New -> Activity -> Settings thì Android Studio sẽ generate ra 2 class: AppCompatPreferenceActivity và một class mẫu với mấy cái XML preferences bạn chả làm công việc nào khác ngoài việc delete. Nên tôi cung cấp cho bạn nội dung của class này luôn cộng với một method. Nhiệm vụ của các bạn là cóp, pát và extends class này mà thôi.

 

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.