Android

Tạo Thiết lập trong Android

Ứng dụng Android của bạn đã gần xong và chỉ còn chờ phát hành. Để được cộng đồng đón nhận tốt hơn thì nó phải hỗ trợ cá nhân hóa qua thông qua việc cho người dùng tùy chỉnh các thiết lập. Tất nhiên, bạn có thể tự tạo các Button, các Switch hay CheckBox… bằng tay và tự gán events cho chúng, nhưng sẽ đơn giản hơn nhiều khi sử dụng các công cụ có sẵn của Android. Đó chính là các class trong gói thư viện Preference, bao gồm thư viện Framework và Support.

preferencescreen

1. SharedPreferences

Trong hình bên trên, bạn cung cấp cho người dùng tùy chỉnh kích thước font chữ và đương nhiên sẽ phải lưu lại thiết lập này. SharedPreferences là cách dễ nhất bạn nên dùng. Nguyên tắc hoạt động của nó vô cùng đơn giản. Mọi thiết lập sẽ được lưu vào một tập tin XML, có tên do bạn chỉ định trong thư mục shared_preferences của nội bộ ứng dụng, theo dạng Map<String, Object>. Để lấy giá trị của thiết lập đã lưu, bạn sẽ cần tới cái Key của nó. SharedPreferences cung cấp lưu trữ giá trị thiết lập với các kiểu dữ liệu gồm  int, long, float, boolean, String và Set<String>.

Một điểm cần lưu ý là bạn không khởi tạo instance của class android.content.SharedPreferences bằng new với constructor, mà luôn luôn phải gọi gián tiếp qua getSharedPreferences(String, int) thông qua Context, chẳng hạn Activity hay Service hay class nào được mở rộng từ Context, còn không thì phải yêu cầu truyền context làm một tham số trong một method. Lưu ý, Actvity#getSharedPreferences và Service#getSharedPreferences chỉ không null sau khi Activity.super.onCreate hoặc Service.super.onCreate chạy xong, còn nếu bạn gọi trước super thì methods sẽ return null. Dưới đây là ví dụ để lấy giá trị các thiết lập với các kiểu dữ liệu khác nhau:

Trong method context.getSharedPreferences(String, int), thì arg thứ nhất có dạng String là tên tập tin bạn muốn lưu các thiết lập vào, và tên này là do bạn đặt. Nó sẽ có dạng XML và nằm trong thư mục shared_preferences của ứng dụng được lưu trong /data/data/your.package.name; còn arg thứ hai có kiểu dữ liệu là int chính là “chế độ” mà bạn muốn đặt, sẽ là 1 hoặc tổ hợp của các Constants trong class Context bao gồm MODE_PRIVATE (= 0), MODE_WORLD_READABLE, MODE_WORLD_WRITEABLE hoặc MODE_MULTI_PROCESS. Trong đó, MODE_PRIVATE là cái được sử dụng phổ biến nhất.

Các tham số thứ nhất trong getBoolean, getInt, getString… chính là Key tương ứng mà bạn dùng để lưu value của các preferences, và điều quan trọng nhất, như bạn có thể hình dung, là chúng KHÔNG ĐƯỢC TRÙNG NHAU. Còn tham số thứ hai là giá trị mặc định sẽ được trả về nếu preference bạn cần chưa tồn tại. Vậy, làm sao để lưu được giá trị thiết lập?

Không có gì khó khăn, bạn sẽ cần nhờ tới một instance của interface Editor vốn là một inner class của SharedPreferences, thông qua method edit gọi từ SharedPreferences. Trong mỗi phương thức put, thì tham số đầu tiên là phần Key như tôi đã nói bên trên, còn tham số thứ hai chính là giá trị mà bạn muốn lưu thiết lập. Và cuối cùng, bạn phải yêu cầu apply hoặc commit trước khi đặt dấu chấm phẩy kết thúc câu lệnh. Điểm khác biệt giữa apply và commit là commit được thực hiện luôn trên Thread mà bạn gọi Editor, trong khi apply được thực hiện dưới nền trong một Thread khác. Ví dụ:

2. PreferenceActivity

SharedPreferences thực tế chỉ dùng trực tiếp khi bạn muốn thực hiện lưu và nạp các thiết lập mang tính… lẻ tẻ, hoặc không có một giao diện rõ ràng cho lắm. Còn nếu bạn muốn tạo hẳn một “màn hình” tập trung các thiết lập cho ứng dụng mang tính toàn cục, thì bạn sẽ dùng tới PreferenceActivity. Và giao diện của nó hoàn toàn tương đồng với cái ảnh màn hình mà tôi đưa ra trong đầu bài viết. Ở đây, tôi sẽ mặc định minSdk của project là 14, tức Android 4.0 Ice Cream Sandwich “đời đầu”, vì một phần là các Support Libraries đã nâng mức “ếch đi cày” tối thiểu lên 14 rồi, và phần khác là thị phần của các mức APIs cũ đã không còn đáng để bạn bận tâm nữa. Trước khi bạn đọc tiếp, thì vui lòng ghi nhớ, là mọi thứ từ dòng này trở xuống, đều được dựa trên SharedPreferences bên trên, dù được biểu hiện rõ hay được ẩn dưới hậu cảnh.

Đúng như tên của nó, PreferenceActivity cũng là một (và extends) Activity. Và việc khai báo nó trong Manifest cũng hoàn toàn tương tự như một Activity bình thường. Điều làm nên sự khác biệt, là phần giao diện, tức contentView của nó, đã được cấu hình sẵn, và bạn chỉ cần phải “chỉ trỏ” cho nó nạp cái nội dung nào lên mà thôi. Ở đây, việc bố trí nội dung lên giao diện cũng được cấu hình sẵn cho bạn luôn. Do đó bạn không cần và cũng không được tạo ListView/RecyclerView rồi gọi setAdapter như thông thường. Thay vào đó, bạn sẽ tạo một file XML gồm các Preferences, tôi tạm đặt tên nó là preferences.xml, và gọi một method hoàn toàn lạ lẫm với bạn: addPreferencesFromResource(int prefResource). Bạn sẽ đặt cái preferences.xml đó trong thư mục res/xml, và cái int prefResource, bạn cũng có thể đã đoán được, sẽ là R.xml.preferences.

Khoan bàn tới nội dung bạn sẽ viết gì trong tập tin preferences.xml, câu hỏi đặt ra là bạn sẽ gọi addPreferencesFromResource(int) ở chỗ nào trong Activity? Câu trả lời là trong onCreate(Bundle) hoặc onStart. Trong đa số các trường hợp thì để trong onCreate(Bundle) là đã “ngon lành” lắm rồi, tuy nhiên, nếu ý tưởng của bạn không thể làm khác được, thì bạn có thể đặt trong onStart hoặc thậm chí là onResume cũng được. Dưới đây là code trích ngang mang tính ví dụ:

Bây giờ, chúng ta sẽ quay lại phần quan trọng nhất của mục 2 này, là bạn sẽ viết những gì trong cái file preferences.xml kia. Trước hết, bạn hãy nhìn lại cái ảnh màn hình mẫu trong bài, và nếu bạn lười cuộn lên cuộn xuống, thì nó đây:

preferencescreen

Bạn sẽ nhìn thấy tất cả các thiết lập được trình bày dưới dạng danh sách, nói đúng hơn, chúng nằm trong một Multi ViewType ListView (và cái id của ListView này là android.R.id.list). Theo “tạo hóa” của Android, thì Google cung cấp cho chúng ta các loại preference sau, và thực tế thì bấy nhiêu đó là khá “đủ dùng” rồi. Chúng bao gồm: CheckBoxPreference, EditTextPreferenceListPreferenceMultiSelectListPreference, RingtonePreferenceSwitchPreference, và một base class của CheckBoxPreference và SwitchPreference, là TwoStatePreference, và base class của ListPreference và MultiSelectListPreference là DialogPreference. Tất cả chúng đều extends một class có tên là Preference. Bạn có thể dễ dàng nhận ra chúng chính là những kiểu widget và dialog rất đỗi quen thuộc với bạn qua cái những cái tên class. Điều lạ lùng duy nhất chính là cái từ “Preference” đi sau chúng mà thôi. Ở đây, điều này cũng không có gì phức tạp, chúng chỉ là những widget và dialog được cấu hình tự động để lưu thiết lập mà thôi. Chẳng hạn, bạn click vào một CheckBoxPreference thì cái CheckBox sẽ thay đổi giá trị (tức là checked = true hoặc false) và cái ứng dụng sẽ tự lưu giá trị của cái CheckBox đó vào tập tin trong bộ nhớ cứng của thiết bị, mà bạn không cần thiết phải tự viết code chỗ đó. Đơn giản là vậy thôi.

Bấy giờ, sang phần viết XML, bạn sẽ hình dung như bạn đang làm một BaseAdapter cho ListView, và bạn đang add các phần tử dần dần vào ArrayList nguồn của Adapter. Tuy nhiên, bạn không làm trực tiếp lấy cái ArrayList ra rồi add từng phần tử vào, mà sẽ viết cái preferences.xml theo công thức bên dưới. Ở đây, tôi sẽ viết trích ngang theo cái hình bên trên luôn để cho bạn dễ theo dõi. Các attrs khác của mỗi loại Preference, tôi sẽ trình bày sau. Chi tiết của bộ thư viện android.preference, bạn có thể tham khảo tại đây. Và bạn lưu ý là ở đây, bạn chỉ đang thao tác với các Preferences mà thôi, phần ActionBar/Toolbar kia, nếu bạn muốn thay đổi Title hay làm thao tác nào khác, thì vẫn làm trong Activity.

Và sau đây, là phần giải thích. Và bạn cũng có thể dễ dàng nhận ra là từng Preference một được sắp xếp theo đúng thứ tự từ trên xuống tương ứng với thứ tự bạn đặt chúng trong preferences.xml, và tên của mỗi preference tag cũng chính là tên class của mỗi Preference, chẳng hạn <SwitchPreference />.

Tất cả các Preference đều phải nằm trong một tag bao ngoài là PreferenceScreen. Nếu không có cặp tag bao ngoài này, thì việc PreferenceActivity (và phần dưới bài là PreferenceFragment) sẽ không parse được các Preference để đưa chúng vào cái ListView (android.R.id.list) được. Và cái bảng dừng quen thuộc sẽ được hiển thị. Vì vậy, bạn có thể quên cái PreferenceCategory bên trong, nhưng PreferenceScreen thì không được phép quên, nếu bạn chưa biết đọc Logcat.

Tiếp theo là PreferenceCategory. Class này giúp các bạn gom/nhóm các Preference cùng loại lại với nhau cho dễ quản lí. Về mặt giao diện thì nó được thể hiện như các Preference khác, không khác biệt gì cả. Nhưng nếu bạn đang có ý định cho phép người dùng remove luôn một mớ các preferences cùng lúc thì tên PreferenceCategory này lại vô cùng có ích.

Cuối cùng, cái quan trọng nhất: Mỗi Preference bao gồm một thuộc tính android:key, và cái key này chính là cái key trong SharedPreferences#get…(String key) như tôi đã nói bên trên, hoạt động của những Preference thực chất dựa trên SharedPreferences mà thôi. Chúng phải khác nhau (unique), và bạn phải định nghĩa key mỗi Preference, để ứng dụng biết mà lưu giá trị của Preference đó vào đâu, ngoại trừ PreferenceScreen và PreferenceCategory thì key mang tính không cần thiết vì chúng không có giá trị.

Ngoài ra, mỗi Preference còn có các thuộc tính, tức các attributes, và đương nhiên chúng nằm trong namespace android. Chúng gồm:

  • title: Chính là cái chữ lớn và có màu đen trong mỗi Preference trong ảnh màn hình bên trên.
  • summary: Chính là cái chữ nhỏ hơn và có màu xám trong mỗi Preference trong ảnh màn hình bên trên. Summary này mang tính “chung chung” và sẽ được dùng khi bạn không định nghĩa các entries/entryValues của DialogPreference hay summaryOn/summaryOff của các TwoStatePreference, hoặc các Preference khác không được thiết kế để hiển thị giá trị cụ thể mà nó đang lưu giữ.
  • defaultValue: Giá trị mặc định của Preference.

Còn các Preference được extends DialogPreference thì có các attrs riêng sau:

  • entries: Tên hiển thị của các tùy chọn (options) được hiện ra trong Dialog. Chẳng hạn đối với Font size bên trên, thì entries sẽ là { “Extra small”, “Small”, “Medium”, “Large”, “Extra large” }. Ở đây, trong tuyệt đại đa số các trường hợp, thì nó sẽ là một String[ ].
  • entryValues: Giá trị thực tế tương ứng với các tùy chọn bên trên, và Preference sẽ lưu vào. ListPreference và MultiSelectListPreference khi lưu thiết lập, là lưu (các) giá trị ở đây, mặc dù phần hiển thị, là hiển thị các entries bên trên.

Còn các Preference được extends TwoStatePreference thì có các attrs riêng sau:

  • summaryOn: Được sử dụng/hiển thị khi state của nó (tức checked) là true.
  • summaryOff: Được sử dụng/hiển thị khi state của nó (tức checked) là false.

Ngoài ra, cũng còn không ít các attrs khác mà bạn có thể tham khảo trên trang Android Developers. Hơn nữa, bạn có thể tự định nghĩa hành vi của các Preference thông qua Preference#setOnPreferenceClickListenerPreference#setOnPreferenceChangeListener hay gán Intent cho các Preference cụ thể với tag <intent>. Nhưng trong trường hợp bạn muốn khi click một dòng Preference, ứng dụng sẽ chuyển sang một Activity mới chứa các thiết lập con khác, thì bạn đừng xoắn não là phải tạo một Activity mới. Không cần đâu. Bạn chỉ cần bỏ các thiết lập con kia trong một PreferenceScreen con nữa mà thôi, tự nó chuyển cho bạn. Chẳng hạn:

Cái hay của PreferenceActivity là việc nó sẽ tự nạp các giá trị thiết lập vào, nghĩa là ban đầu, khi một Preference chưa được gán giá trị mới, tức là nó vẫn “còn nguyên vẹn” thì PreferenceActivity sẽ gán trạng thái cho nó theo giá trị defaultValue mà bạn đã chỉ định. Còn nếu nó đã có giá trị mới rồi, thì tự PreferenceActivity sẽ gán trạng thái cho Preference đó theo giá trị mà người dùng đã cá nhân hóa rồi. Và điều này diễn ra tự động, bạn không cần lo lắng mà code cho cái PreferenceActivity của bạn phải parse lại danh sách các thiết lập. Khi người dùng mở lại trang Thiết lập, mọi thứ sẽ tự động giống y chang như lần cuối họ thoát nó.

3. Sử dụng PreferenceFragment

Nếu bạn để ý thì nãy giờ, method PreferenceActivity#addPreferencesFromResource(int prefRes) đang bị gạch ngang (strike through), bởi vì nó đã bị deprecated. Hiện tại, mặc dù bạn có thể sử dụng nó như thường, nhưng mục đích chính của nó chỉ còn là để… làm cảnh cho vui, và Google khuyến khích các bạn sử dụng một cách tiếp cận khác: Sử dụng PreferenceFragment.

Như bạn đã biết, Fragment không bao giờ đi một mình, mà lúc nào cũng phải đi kèm với Activity, và PreferenceFragment cũng không là ngoại lệ. Và bạn đang nghĩ tới việc chứa PreferenceFragment trong một PreferenceActivity? Không. Câu trả lời là KHÔNG, KHÔNG và KHÔNG. Lí do, là giống như PreferenceActivity, PreferenceFragment được xây dựng giao diện sơ khai sẵn rồi, và nó có contentView hẳn hoi. Do đó bạn phải chứa nó trong một Activity chưa được định giao diện, trong khi PreferenceActivity đã có contentView rồi. Vì vậy, bạn chỉ cần đặt PreferenceFragment vào một Activity bình thường (android.app.Activity hay android.support.v7.app.AppCompatActivity) theo cách mà bạn đã, đang và sẽ đặt các Fragment khác thông qua FragmentManager, FragmentTransaction và một ViewGroup trên contentView của Activity.

Trong PreferenceFragment cũng có method addPreferencesFromResource(int prefRes) y chang như PreferenceActivity. Do đó, tôi sẽ không nói gì thêm về việc tạo preferences.xml nữa. Thay vào đó, tôi sẽ trả lời thắc mắc của bạn về việc tại sao nên dùng PreferenceFragment thay cho PreferenceActivity? Có phải tại The Big G thích vậy không? Cũng có thể. Nhưng tôi cho rằng lí do nằm ở tính linh động của Fragment. Vấn đề là bạn gọi addPreferencesFromResource(int) ở đâu? onCreate, onCreateView, onViewCreated hay onActivityCreated? Thông thường, khi sử dụng thư viện Framework thì Google ví dụ cho chúng ta đặt method trên trong onCreate như Activity. Dưới đây là code mẫu trích ngang:

4. Sử dụng thư viện Preference Support:

Cũng giống như nhiều gói thư viện khác, gói thư viện android.preference cũng có bộ Support Library tương ứng với nó, với mục đích là mang các tính năng mới nhất xuống các mức API thấp hơn. Có 2 loại gói thư viện Support, là android.support.v7.preferenceandroid.support.v14.preference, trong đó bộ V14 có bao gồm luôn bộ V7 rồi. Ngoài ra, chỉ có bộ V14 mới có khả năng mang giao diện Thiết lập kiểu Material Design xuống các phiên bản Android 4. Vì vậy, tôi sẽ hướng dẫn các bạn làm việc với bộ V14 này. Bộ thư viện này chỉ bao gồm PreferenceFragment(Compat) mà không gồm PreferenceActivityCompat. Để sử dụng bộ thư viện này, thì cũng như bao lần khác, bạn chỉ cần yêu cầu Gradle compile hoặc implement ‘com.android.support:preference-v14‘ với version mới nhất.

Việc tạo tập tin preferences.xml với bộ thư viện Support tuy khá tương đồng với khi tạo bằng thư viện Framework như tôi hướng dẫn bên trên. Song, vì bạn đang sử dụng thư viện Support, nên cũng có những khác biệt rõ rệt. Đầu tiên, bạn phải điền đầy đủ tên class bao gồm luôn package trong mỗi XML tag. Chẳng hạn:

Thứ hai, một số attr sẽ cần phải gọi từ namespace app (đại diện cho android.support.v14.preference) chứ không gọi từ namespace android, vì các thuộc tính liên quan, chẳng hạn như id, được đặt theo nội bộ của gói thư viện. Như bạn đã thấy, attr defaultValue được tôi gọi thông qua namespace app chứ không phải android như bên trên.  Thứ ba, việc addPreferencesFromResource nên được gọi trong method onCreatePreferences(Bundle, String). Thực chất, method này là method cuối cùng được gọi trong onCreate(Bundle). Bạn sẽ đang thắc mắc là vì sao lại “sinh đẻ” thêm cái method này nữa? Bởi vì Google muốn đảm bảo việc lấy preferenceTheme từ các attrs của theme mặc định phải được diễn ra trước. Và bạn phải định nghĩa cái preferenceTheme này trong styles/Theme mà cái Activity chứa Fragment này đang chạy, không thôi một IllegalStateException sẽ được thrown ra. Ở đây, trước mắt thì bạn cứ cóp pát phần code này vào styles.xml của bạn:

Khi bạn dùng PreferenceThemeOverlay.v14.Material thì tất cả các Preference sẽ được “Material Design hóa” trên Android 4, nếu bạn dùng PreferenceThemeOverlay (tức là của V7) thì sẽ xảy ra tình trạng lẫn lộn, với một số widget được Material Design hóa, trong khi một số khác lại vẫn chạy giao diện Holo, sẽ gây người dùng “ngứa mắt trái, đỏ mắt phải” vì trước mắt họ là một thứ giao diện “nửa nạc nửa mỡ”.

Bấy giờ, công đoạn cuối cùng là cho cho load Fragment vào Activity như bình thường. Lưu ý, là nếu bạn sử dụng android.support.v7.preference.PreferenceFragmentCompat, thì Activity chứa nó phải extends android.support.v4.app.FragmentActivity (AppCompatActivity được extends từ class này nên dùng thoải mái), bởi PrefrenceFragmentCompat được extends từ android.support.v4.app.Fragment, chỉ hoạt động với android.support.v4.app.FragmentManager qua FragmentActivity#getSupportFragmentManager(). Còn nếu bạn sử dụng android.support.v14.preference.PreferenceFragment, thì class này extends từ android.app.Fragment, và do đó, tương ứng với android.app.Activity#getFragmentManager().

5. Lấy giá trị của các Preferences đã lưu để sử dụng

Đương nhiên là việc lưu giá trị của một thiết lập phải đi kèm với việc lấy cái giá trị đó đem ra dùng ở đâu đó khác. Và với cụm “ở đâu đó khác”, ý tôi là lấy giá trị đó ra và dùng trong một Activity, Fragment khác hay Service nào đó, chứ nếu chỉ làm giao diện Thiết lập để cho có và chỉ thay đổi thiết lập trong màn hình đó thôi thì tốt hơn là không làm cho rồi. Vậy, lấy ra bằng cách nào? Như tôi đã nói, các Preference hoạt động dựa trên SharedPreferences. Do đó, bạn sẽ getInt, getBoolean, getString, v.v… thông qua một đối tượng SharedPreferences được định nghĩa như bên dưới.

Thực chất, PreferenceManager.getDefaultSharedPreferences(Context) có nội dung như bên dưới, và android.preference.PreferenceManager hay android.support.v7.preference.PreferenceManager đều là như nhau:

Như vậy, bạn cũng có thể dùng trực tiếp context.getSharedPreferences với String arg là tên gói ứng dụng của bạn nối thêm chuỗi “_preference”, không cần thông qua PreferenceManager. Và bây giờ là lúc bạn thực hành code thử một cái PreferenceActivity hay Fragment rồi. Sang bài sau, tôi sẽ giới thiệu về Preference Headers. Hẹn gặp lại.

4 thoughts on “Tạo Thiết lập trong Android”

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.