Kiến thức cơ bản về Kotlin trên Android 04.2: Các tình huống liên quan đến vòng đời phức tạp

Lớp học lập trình này thuộc khoá học Kiến thức cơ bản về Kotlin cho Android. Bạn sẽ nhận được nhiều giá trị nhất qua khoá học này nếu thực hiện các lớp học lập trình theo trình tự. Tất cả lớp học lập trình của khoá học đều được liệt kê trên trang đích của lớp học lập trình Kiến thức cơ bản về cách tạo ứng dụng Android bằng Kotlin.

Giới thiệu

Trong lớp học lập trình gần đây nhất, bạn đã tìm hiểu về vòng đời ActivityFragment, đồng thời khám phá các phương thức được gọi khi trạng thái vòng đời thay đổi trong các hoạt động và mảnh. Trong lớp học lập trình này, bạn sẽ tìm hiểu chi tiết hơn về vòng đời hoạt động. Bạn cũng tìm hiểu về thư viện vòng đời của Android Jetpack. Thư viện này có thể giúp bạn quản lý các sự kiện trong vòng đời bằng mã được sắp xếp hợp lý và dễ bảo trì hơn.

Kiến thức bạn cần có

  • Hoạt động là gì và cách tạo hoạt động trong ứng dụng.
  • Thông tin cơ bản trong vòng đời của ActivityFragment, cũng như các lệnh gọi lại được gọi khi một hoạt động di chuyển giữa các trạng thái.
  • Cách ghi đè các phương thức gọi lại trong vòng đời onCreate()onStop() để thực hiện các thao tác tại những thời điểm khác nhau trong vòng đời hoạt động hoặc vòng đời mảnh.

Kiến thức bạn sẽ học được

  • Cách thiết lập, bắt đầu và dừng các phần của ứng dụng trong phương thức gọi lại vòng đời.
  • Cách sử dụng thư viện vòng đời Android để tạo một đối tượng theo dõi vòng đời và giúp quản lý vòng đời hoạt động và mảnh dễ dàng hơn.
  • Ảnh hưởng của việc tắt quy trình Android đến dữ liệu trong ứng dụng của bạn, cũng như cách tự động lưu và khôi phục dữ liệu đó khi Android đóng ứng dụng của bạn.
  • Cách xoay thiết bị và các thay đổi khác về cấu hình tạo ra những thay đổi đối với trạng thái vòng đời và ảnh hưởng đến trạng thái của ứng dụng.

Bạn sẽ thực hiện

  • Sửa đổi ứng dụng DessertClicker để thêm một hàm hẹn giờ, đồng thời bắt đầu và dừng hàm hẹn giờ đó tại nhiều thời điểm trong vòng đời hoạt động.
  • Sửa đổi ứng dụng để sử dụng thư viện vòng đời Android và chuyển đổi lớp DessertTimer thành một đối tượng theo dõi vòng đời.
  • Thiết lập và sử dụng Cầu gỡ lỗi Android (adb) để mô phỏng quá trình tắt ứng dụng và các phương thức gọi lại trong vòng đời xảy ra sau đó.
  • Triển khai phương thức onSaveInstanceState() để giữ lại dữ liệu ứng dụng có thể bị mất nếu ứng dụng bị đóng bất ngờ. Thêm mã để khôi phục dữ liệu đó khi ứng dụng khởi động lại.

Trong lớp học lập trình này, bạn sẽ mở rộng ứng dụng DessertClicker từ lớp học lập trình trước. Bạn thêm một bộ hẹn giờ chạy nền, sau đó chuyển đổi ứng dụng để sử dụng thư viện vòng đời Android.

Trong lớp học lập trình trước, bạn đã tìm hiểu cách theo dõi vòng đời hoạt động và vòng đời mảnh bằng cách ghi đè nhiều lệnh gọi lại trong vòng đời và ghi nhật ký khi hệ thống gọi các lệnh gọi lại đó. Trong nhiệm vụ này, bạn sẽ khám phá một ví dụ phức tạp hơn về cách quản lý các tác vụ trong vòng đời trong ứng dụng DessertClicker. Bạn sẽ sử dụng một bộ hẹn giờ in câu lệnh nhật ký mỗi giây, với số lượng giây mà bộ hẹn giờ đã chạy.

Bước 1: Thiết lập DessertTimer

  1. Mở ứng dụng DessertClicker của lớp học lập trình trước. (Bạn có thể tải DessertClickerLogs xuống tại đây nếu chưa có ứng dụng này.)
  2. Trong chế độ xem Project (Dự án), hãy mở rộng java > com.example.android.dessertclicker rồi mở DessertTimer.kt. Lưu ý rằng hiện tại tất cả mã đều được đánh dấu là nhận xét, nên mã này không chạy như một phần của ứng dụng.
  3. Chọn tất cả mã trong cửa sổ trình chỉnh sửa. Chọn Code > Comment with Line Comment (Mã > Nhận xét bằng nhận xét dòng) hoặc nhấn Control+/ (Command+/ trên máy Mac). Lệnh này sẽ bỏ chú thích tất cả mã trong tệp. (Android Studio có thể hiện lỗi tham chiếu chưa được giải quyết cho đến khi bạn tạo lại ứng dụng.)
  4. Lưu ý rằng lớp DessertTimer bao gồm startTimer()stopTimer(), dùng để bắt đầu và dừng bộ hẹn giờ. Khi startTimer() đang chạy, bộ tính giờ sẽ in một thông báo nhật ký mỗi giây, với tổng số giây mà thời gian đã chạy. Đến lượt, phương thức stopTimer() sẽ dừng đồng hồ hẹn giờ và các câu lệnh nhật ký.
  1. Mở MainActivity.kt. Ở đầu lớp, ngay bên dưới biến dessertsSold, hãy thêm một biến cho bộ hẹn giờ:
private lateinit var dessertTimer : DessertTimer;
  1. Di chuyển xuống onCreate() rồi tạo một đối tượng DessertTimer mới, ngay sau lệnh gọi đến setOnClickListener():
dessertTimer = DessertTimer()


Giờ đây, bạn đã có một đối tượng bộ tính giờ món tráng miệng. Hãy cân nhắc xem bạn nên bắt đầu và dừng bộ tính giờ ở đâu để bộ tính giờ chỉ chạy khi hoạt động đang diễn ra trên màn hình. Bạn sẽ xem xét một vài lựa chọn ở các bước tiếp theo.

Bước 2: Bắt đầu và dừng đồng hồ hẹn giờ

Phương thức onStart() được gọi ngay trước khi hoạt động hiển thị. Phương thức onStop() được gọi sau khi hoạt động ngừng hiển thị. Những lệnh gọi lại này có vẻ là lựa chọn phù hợp để xác định thời điểm bắt đầu và dừng đồng hồ hẹn giờ.

  1. Trong lớp MainActivity, hãy bắt đầu bộ hẹn giờ trong lệnh gọi lại onStart():
override fun onStart() {
   super.onStart()
   dessertTimer.startTimer()

   Timber.i("onStart called")
}
  1. Dừng hẹn giờ sau onStop():
override fun onStop() {
   super.onStop()
   dessertTimer.stopTimer()

   Timber.i("onStop Called")
}
  1. Biên dịch và chạy ứng dụng. Trong Android Studio, hãy nhấp vào ngăn Logcat. Trong hộp tìm kiếm Logcat, hãy nhập dessertclicker. Thao tác này sẽ lọc theo cả lớp MainActivityDessertTimer. Lưu ý rằng sau khi ứng dụng bắt đầu, bộ hẹn giờ cũng sẽ bắt đầu chạy ngay lập tức.
  2. Nhấp vào nút Quay lại và lưu ý rằng bộ hẹn giờ sẽ dừng lại. Bộ hẹn giờ dừng vì cả hoạt động và bộ hẹn giờ mà hoạt động đó kiểm soát đều đã bị huỷ.
  3. Dùng màn hình gần đây để quay lại ứng dụng. Lưu ý trong Logcat rằng bộ hẹn giờ khởi động lại từ 0.
  4. Nhấp vào nút Chia sẻ. Bạn có thể nhận thấy trong Logcat rằng bộ hẹn giờ vẫn đang chạy.

  5. Nhấp vào nút Trang chủ. Lưu ý trong Logcat rằng bộ hẹn giờ ngừng chạy.
  6. Dùng màn hình gần đây để quay lại ứng dụng. Lưu ý trong Logcat rằng bộ hẹn giờ sẽ khởi động lại từ điểm dừng trước đó.
  7. Trong MainActivity, trong phương thức onStop(), hãy nhận xét về lệnh gọi đến stopTimer(). Việc nhận xét stopTimer() minh hoạ trường hợp bạn bắt đầu một thao tác trong onStart() nhưng quên dừng lại trong onStop().
  8. Biên dịch và chạy ứng dụng, rồi nhấp vào nút Home sau khi bộ hẹn giờ bắt đầu. Mặc dù ứng dụng ở chế độ nền, nhưng đồng hồ hẹn giờ vẫn chạy và liên tục sử dụng tài nguyên hệ thống. Việc để bộ hẹn giờ tiếp tục chạy là một rò rỉ bộ nhớ cho ứng dụng của bạn và có thể không phải là hành vi bạn muốn.

    Mẫu chung là khi bạn thiết lập hoặc bắt đầu một việc gì đó trong một lệnh gọi lại, bạn sẽ dừng hoặc xoá việc đó trong lệnh gọi lại tương ứng. Bằng cách này, bạn sẽ tránh được tình trạng có ứng dụng hoặc dịch vụ đang chạy khi không còn cần thiết.
  1. Bỏ chú thích dòng trong onStop() nơi bạn dừng đồng hồ hẹn giờ.
  2. Cắt và dán lệnh gọi startTimer() từ onStart() sang onCreate(). Thay đổi này minh hoạ trường hợp bạn vừa khởi chạy vừa bắt đầu một tài nguyên trong onCreate(), thay vì dùng onCreate() để khởi chạy và onStart() để bắt đầu tài nguyên đó.
  3. Biên dịch và chạy ứng dụng. Lưu ý rằng bộ tính giờ bắt đầu chạy như bạn mong đợi.
  4. Nhấp vào Home để dừng ứng dụng. Bộ hẹn giờ sẽ dừng chạy như bạn mong đợi.
  5. Dùng màn hình gần đây để quay lại ứng dụng. Lưu ý rằng bộ hẹn giờ không khởi động lại trong trường hợp này, vì onCreate() chỉ được gọi khi ứng dụng khởi động, chứ không được gọi khi ứng dụng quay lại nền trước.

Những điểm chính cần nhớ:

  • Khi bạn thiết lập một tài nguyên trong một lệnh gọi lại vòng đời, hãy huỷ tài nguyên đó.
  • Thiết lập và huỷ trong các phương thức tương ứng.
  • Nếu bạn thiết lập một thứ gì đó trong onStart(), hãy dừng hoặc tháo dỡ lại trong onStop().

Trong ứng dụng DessertClicker, bạn có thể dễ dàng nhận thấy rằng nếu đã bắt đầu hẹn giờ trong onStart(), thì bạn cần dừng hẹn giờ trong onStop(). Chỉ có một đồng hồ hẹn giờ, nên bạn sẽ không khó nhớ cách dừng đồng hồ.

Trong một ứng dụng Android phức tạp hơn, bạn có thể thiết lập nhiều thứ trong onStart() hoặc onCreate(), sau đó xoá tất cả trong onStop() hoặc onDestroy(). Ví dụ: bạn có thể có các hình động, nhạc, cảm biến hoặc bộ hẹn giờ mà bạn cần thiết lập và tháo dỡ, cũng như khởi động và dừng. Nếu bạn quên một trong hai, điều đó sẽ dẫn đến lỗi và gây khó chịu.

Thư viện vòng đời (thuộc Android Jetpack) giúp đơn giản hoá nhiệm vụ này. Thư viện này đặc biệt hữu ích trong trường hợp bạn phải theo dõi nhiều thành phần chuyển động, một số thành phần ở các trạng thái vòng đời khác nhau. Thư viện này đảo ngược cách hoạt động của vòng đời: Thông thường, hoạt động hoặc mảnh sẽ cho một thành phần (chẳng hạn như DessertTimer) biết phải làm gì khi một lệnh gọi lại vòng đời xảy ra. Nhưng khi bạn sử dụng thư viện vòng đời, chính thành phần đó sẽ theo dõi các thay đổi trong vòng đời, sau đó thực hiện những việc cần thiết khi các thay đổi đó xảy ra.

Thư viện vòng đời có 3 phần chính:

  • Chủ sở hữu vòng đời là những thành phần có (và do đó "sở hữu") một vòng đời. ActivityFragment là chủ sở hữu vòng đời. Các đối tượng sở hữu vòng đời triển khai giao diện LifecycleOwner.
  • Lớp Lifecycle, chứa trạng thái thực tế của một chủ sở hữu vòng đời và kích hoạt các sự kiện khi có thay đổi về vòng đời.
  • Trình quan sát vòng đời, theo dõi trạng thái vòng đời và thực hiện các tác vụ khi vòng đời thay đổi. Các đối tượng tiếp nhận dữ liệu vòng đời triển khai giao diện LifecycleObserver.

Trong nhiệm vụ này, bạn sẽ chuyển đổi ứng dụng DessertClicker để sử dụng thư viện vòng đời Android và tìm hiểu cách thư viện này giúp bạn dễ dàng quản lý vòng đời hoạt động và mảnh của Android hơn.

Bước 1: Chuyển DessertTimer thành LifecycleObserver

Một phần quan trọng của thư viện vòng đời là khái niệm về hoạt động quan sát vòng đời. Hoạt động quan sát cho phép các lớp (chẳng hạn như DessertTimer) biết về vòng đời hoạt động hoặc vòng đời mảnh, đồng thời tự khởi động và dừng để phản hồi những thay đổi đối với các trạng thái vòng đời đó. Với trình quan sát vòng đời, bạn có thể loại bỏ trách nhiệm khởi động và dừng các đối tượng khỏi các phương thức hoạt động và mảnh.

  1. Mở lớp DesertTimer.kt.
  2. Thay đổi chữ ký lớp của lớp DessertTimer để có dạng như sau:
class DessertTimer(lifecycle: Lifecycle) : LifecycleObserver {

Định nghĩa lớp mới này thực hiện 2 việc:

  • Hàm dựng lấy một đối tượng Lifecycle, là vòng đời mà bộ hẹn giờ đang quan sát.
  • Định nghĩa lớp triển khai giao diện LifecycleObserver.
  1. Bên dưới biến runnable, hãy thêm một khối init vào định nghĩa lớp. Trong khối init, hãy dùng phương thức addObserver() để kết nối đối tượng vòng đời được truyền vào từ đối tượng sở hữu (hoạt động) với lớp này (trình quan sát).
 init {
   lifecycle.addObserver(this)
}
  1. Chú thích startTimer() bằng @OnLifecycleEvent annotation và sử dụng sự kiện vòng đời ON_START. Tất cả các sự kiện trong vòng đời mà trình quan sát vòng đời của bạn có thể quan sát đều nằm trong lớp Lifecycle.Event.
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun startTimer() {
  1. Làm tương tự với stopTimer(), sử dụng sự kiện ON_STOP:
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stopTimer()

Bước 2: Sửa đổi MainActivity

Lớp MainActivity của bạn đã là một đối tượng sở hữu vòng đời thông qua hoạt động kế thừa, vì siêu lớp FragmentActivity triển khai LifecycleOwner. Do đó, bạn không cần làm gì để hoạt động của mình nhận biết được vòng đời. Bạn chỉ cần truyền đối tượng vòng đời của hoạt động vào hàm khởi tạo DessertTimer.

  1. Mở MainActivity. Trong phương thức onCreate(), hãy sửa đổi quá trình khởi chạy DessertTimer để thêm this.lifecycle:
dessertTimer = DessertTimer(this.lifecycle)

Thuộc tính lifecycle của hoạt động chứa đối tượng Lifecycle mà hoạt động này sở hữu.

  1. Xoá lệnh gọi đến startTimer() trong onCreate() và lệnh gọi đến stopTimer() trong onStop(). Bạn không cần cho DessertTimer biết phải làm gì trong hoạt động nữa, vì DessertTimer hiện đang theo dõi chính vòng đời và tự động nhận được thông báo khi trạng thái vòng đời thay đổi. Giờ đây, tất cả những gì bạn làm trong các lệnh gọi lại này là ghi lại một thông báo.
  2. Biên dịch và chạy ứng dụng, sau đó mở Logcat. Lưu ý rằng đồng hồ hẹn giờ đã bắt đầu chạy như dự kiến.
  3. Nhấp vào nút trang chủ để đưa ứng dụng vào chế độ nền. Lưu ý rằng đồng hồ hẹn giờ đã dừng chạy như dự kiến.

Điều gì sẽ xảy ra với ứng dụng và dữ liệu của ứng dụng nếu Android tắt ứng dụng đó khi ứng dụng đang ở chế độ nền? Bạn cần hiểu rõ trường hợp đặc biệt này.

Khi ứng dụng của bạn chuyển sang chế độ nền, ứng dụng đó sẽ không bị huỷ mà chỉ dừng lại và chờ người dùng quay lại. Tuy nhiên, một trong những mối lo ngại chính của hệ điều hành Android là duy trì hoạt động trơn tru của hoạt động ở nền trước. Ví dụ: nếu người dùng đang sử dụng một ứng dụng GPS để giúp họ bắt xe buýt, thì bạn cần hiển thị ứng dụng GPS đó một cách nhanh chóng và tiếp tục chỉ đường. Việc giữ cho ứng dụng DessertClicker (mà người dùng có thể không xem trong vài ngày) chạy trơn tru ở chế độ nền là điều ít quan trọng hơn.

Android điều chỉnh các ứng dụng ở chế độ nền để ứng dụng trên nền trước có thể chạy mà không gặp vấn đề gì. Ví dụ: Android giới hạn mức xử lý mà các ứng dụng chạy ở chế độ nền có thể làm.

Đôi khi, Android thậm chí còn tắt toàn bộ quy trình ứng dụng, bao gồm cả mọi hoạt động liên quan đến ứng dụng. Android thực hiện dạng tắt này khi hệ thống gặp áp lực và có nguy cơ bị trễ hình ảnh, do đó, không có lệnh gọi lại hoặc mã bổ sung nào được chạy vào thời điểm này. Quy trình của ứng dụng chỉ cần lặng lẽ tắt khi ở trong chế độ nền. Nhưng với người dùng, có vẻ như ứng dụng vẫn chưa được đóng. Khi người dùng quay lại một ứng dụng đã bị hệ điều hành Android tắt, Android sẽ khởi động lại ứng dụng đó.

Trong nhiệm vụ này, bạn sẽ mô phỏng quá trình tắt quy trình Android và kiểm tra xem điều gì sẽ xảy ra với ứng dụng của bạn khi ứng dụng khởi động lại.

Bước 1: Sử dụng adb để mô phỏng quá trình tắt

Cầu gỡ lỗi Android (adb) là một công cụ dòng lệnh cho phép bạn gửi hướng dẫn đến các trình mô phỏng và thiết bị được kết nối với máy tính. Trong bước này, bạn sẽ dùng adb để đóng quy trình của ứng dụng và xem điều gì xảy ra khi Android tắt ứng dụng.

  1. Biên dịch và chạy ứng dụng của bạn. Nhấp vào bánh nướng một vài lần.
  2. Nhấn nút Màn hình chính để chuyển ứng dụng sang chế độ nền. Ứng dụng của bạn hiện đã dừng và có thể bị đóng nếu Android cần các tài nguyên mà ứng dụng đang sử dụng.
  3. Trong Android Studio, hãy nhấp vào thẻ Terminal để mở cửa sổ dòng lệnh.
  4. Nhập adb rồi nhấn phím Return.

    Nếu bạn thấy nhiều đầu ra bắt đầu bằng Android Debug Bridge version X.XX.X và kết thúc bằng tags to be used by logcat (see logcat —help), thì mọi thứ đều ổn. Nếu bạn thấy adb: command not found, hãy đảm bảo rằng lệnh adb có trong đường dẫn thực thi của bạn. Để biết hướng dẫn, hãy xem phần "Thêm adb vào đường dẫn thực thi" trong chương Tiện ích.
  5. Sao chép và dán nhận xét này vào dòng lệnh rồi nhấn phím Return:
adb shell am kill com.example.android.dessertclicker

Lệnh này yêu cầu mọi thiết bị hoặc trình mô phỏng được kết nối dừng quy trình có tên gói dessertclicker, nhưng chỉ khi ứng dụng ở chế độ nền. Vì ứng dụng của bạn ở chế độ nền, nên không có nội dung nào xuất hiện trên màn hình thiết bị hoặc trình mô phỏng để cho biết rằng quy trình của bạn đã dừng. Trong Android Studio, hãy nhấp vào thẻ Run (Chạy) để xem thông báo "Application terminated" (Ứng dụng đã bị chấm dứt). Nhấp vào thẻ Logcat để xem phương thức gọi lại onDestroy() chưa bao giờ chạy – hoạt động của bạn chỉ đơn giản là kết thúc.

  1. Dùng màn hình gần đây để quay lại ứng dụng. Ứng dụng của bạn sẽ xuất hiện trong danh sách gần đây cho dù ứng dụng đã được chuyển sang chế độ nền hay đã ngừng hoàn toàn. Khi bạn dùng màn hình gần đây để quay lại ứng dụng, hoạt động sẽ khởi động lại. Hoạt động này trải qua toàn bộ tập hợp các lệnh gọi lại khởi động vòng đời, bao gồm cả onCreate().
  2. Lưu ý rằng khi khởi động lại, ứng dụng sẽ đặt lại "điểm số" (cả số lượng món tráng miệng đã bán và tổng số tiền) về các giá trị mặc định (0). Nếu Android tắt ứng dụng của bạn, tại sao hệ thống không lưu trạng thái của ứng dụng?

    Khi hệ điều hành khởi động lại ứng dụng cho bạn, Android sẽ cố gắng hết sức để đặt lại ứng dụng về trạng thái trước đó. Android lấy trạng thái của một số khung hiển thị và lưu trạng thái đó vào một gói bất cứ khi nào bạn rời khỏi hoạt động. Một số ví dụ về dữ liệu được lưu tự động là văn bản trong EditText (miễn là chúng có một mã nhận dạng được đặt trong bố cục) và ngăn xếp quay lại của hoạt động.

    Tuy nhiên, đôi khi hệ điều hành Android không biết về tất cả dữ liệu của bạn. Ví dụ: nếu bạn có một biến tuỳ chỉnh như revenue trong ứng dụng DessertClicker, thì hệ điều hành Android không biết về dữ liệu này hoặc tầm quan trọng của dữ liệu này đối với hoạt động của bạn. Bạn cần tự thêm dữ liệu này vào gói.

Bước 2: Sử dụng onSaveInstanceState() để lưu dữ liệu gói

Phương thức onSaveInstanceState() là lệnh gọi lại mà bạn dùng để lưu mọi dữ liệu cần thiết nếu hệ điều hành Android huỷ ứng dụng của bạn. Trong sơ đồ lệnh gọi lại trong vòng đời, onSaveInstanceState() được gọi sau khi hoạt động bị dừng. Phương thức này được gọi mỗi khi ứng dụng chuyển sang chạy ở chế độ nền.

Coi cuộc gọi onSaveInstanceState() là biện pháp an toàn, nhờ đó, bạn có thể lưu một lượng nhỏ thông tin vào gói khi hoạt động thoát khỏi nền trước (foreground). Bây giờ, hệ thống lưu dữ liệu này vì nếu bạn đợi cho đến khi ứng dụng ngừng hoạt động, thì hệ điều hành có thể phải chịu áp lực về tài nguyên. Việc lưu dữ liệu mỗi lần đảm bảo rằng có thể khôi phục dữ liệu mới trong gói nếu cần.

  1. Trong MainActivity, hãy ghi đè lệnh gọi lại onSaveInstanceState() và thêm câu lệnh nhật ký Timber.
override fun onSaveInstanceState(outState: Bundle) {
   super.onSaveInstanceState(outState)

   Timber.i("onSaveInstanceState Called")
}
  1. Biên dịch và chạy ứng dụng rồi nhấp vào nút Home (Trang chủ) để đưa ứng dụng đó chạy trong chế độ nền. Lưu ý rằng lệnh gọi lại onSaveInstanceState() chỉ xảy ra sau onPause()onStop():
  2. Ở đầu tệp, ngay trước định nghĩa của lớp, thêm các hằng số sau:
const val KEY_REVENUE = "revenue_key"
const val KEY_DESSERT_SOLD = "dessert_sold_key"
const val KEY_TIMER_SECONDS = "timer_seconds_key"

Bạn sẽ sử dụng các khoá này để lưu và truy xuất dữ liệu từ gói trạng thái thực thể.

  1. Cuộn xuống onSaveInstanceState() và để ý tham số outState thuộc loại Bundle.

    Gói là một tập hợp các cặp khoá-giá trị, trong đó khoá luôn là chuỗi. Bạn có thể đặt các giá trị nguyên thuỷ, chẳng hạn như giá trị intboolean, vào gói này.
    Vì hệ thống duy trì gói này trong RAM, do vậy tốt nhất nên duy trì dung lượng của dữ liệu trong gói ở mức thấp. Kích thước của gói này cũng bị hạn chế, cho dù kích thước thay đổi tuỳ theo loại thiết bị. Thông thường, bạn nên lưu trữ ít hơn 100.000, nếu không, ứng dụng của bạn có thể gặp sự cố với lỗi TransactionTooLargeException.
  2. Trong onSaveInstanceState(), hãy đặt giá trị revenue (một số nguyên) vào gói bằng phương thức putInt():
outState.putInt(KEY_REVENUE, revenue)

Phương thức putInt() (và các phương thức tương tự từ lớp Bundle như putFloat()putString() sẽ nhận hai đối số: một chuỗi cho khoá (hằng số KEY_REVENUE) và giá trị thực tế để lưu lại.

  1. Lặp lại quy trình tương tự với số lượng món tráng miệng đã bán và trạng thái của bộ hẹn giờ:
outState.putInt(KEY_DESSERT_SOLD, dessertsSold)
outState.putInt(KEY_TIMER_SECONDS, dessertTimer.secondsCount)

Bước 3: Sử dụng onCreate() để khôi phục dữ liệu gói

  1. Cuộn lên đến onCreate() và kiểm tra chữ ký phương thức:
override fun onCreate(savedInstanceState: Bundle) {

Lưu ý rằng onCreate() nhận được Bundle mỗi khi được gọi. Khi hoạt động của bạn được khởi động lại do tắt quá trình, gói bạn đã lưu được chuyển vào onCreate(). Nếu hoạt động của bạn bắt đầu được làm mới, thì gói này trong onCreate() sẽ là null. Do vậy, nếu gói không phải là null, thì bạn biết bạn đang "tạo lại" hoạt động từ một điểm đã biết trước đó.

  1. Thêm mã này vào onCreate(), sau khi thiết lập DessertTimer:
if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
}

Quá trình kiểm tra null sẽ xác định xem có dữ liệu trong gói hay không hoặc liệu gói có phải là null hay không, từ đó cho biết ứng dụng được bắt đầu làm mới hay đã được tạo lại sau khi tắt. Kiểm thử này là một mô hình phổ biến để khôi phục dữ liệu từ gói.

Lưu ý rằng khoá bạn đã sử dụng tại đây (KEY_REVENUE) cũng là khoá bạn đã sử dụng cho putInt(). Để bảo đảm lần nào bạn cũng sử dụng cùng một khoá, phương pháp hay nhất là xác định các khoá đó là hằng số. Bạn sử dụng getInt() để lấy dữ liệu ra khỏi gói, giống như việc bạn dùng putInt() để đặt dữ liệu vào gói. Phương thức getInt() nhận hai đối số:

  • Một chuỗi đóng vai trò là khoá, ví dụ: "key_revenue" cho giá trị doanh thu.
  • Một giá trị mặc định trong trường hợp không có giá trị nào cho khoá đó trong gói.

Tiếp đó, số nguyên bạn nhận được từ gói sẽ được gán cho biến revenue và giao diện người dùng sẽ sử dụng giá trị đó.

  1. Thêm các phương thức getInt() để khôi phục số lượng món tráng miệng đã bán và giá trị của bộ hẹn giờ:
if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
   dessertTimer.secondsCount =
       savedInstanceState.getInt(KEY_TIMER_SECONDS, 0)
}
  1. Biên dịch và chạy ứng dụng. Nhấn vào bánh nướng ít nhất năm lần cho đến khi chuyển sang bánh donut. Nhấp vào Home (Trang chủ) để đưa ứng dụng vào chế độ nền.
  2. Trong thẻ Terminal (Thiết bị đầu cuối) của Android Studio, hãy chạy adb để tắt quy trình của ứng dụng.
adb shell am kill com.example.android.dessertclicker
  1. Dùng màn hình gần đây để quay lại ứng dụng. Lưu ý rằng lần này ứng dụng sẽ quay lại với doanh thu và số món tráng miệng được bán chính xác từ gói. Tuy nhiên cũng lưu ý rằng món tráng miệng đã hiển thị trở lại thành bánh nướng. Chỉ còn một việc cần phải làm để bảo đảm ứng dụng quay lại sau một lần tắt chính xác như khi bạn rời khỏi ứng dụng.
  2. Trong MainActivity, kiểm tra phương thức showCurrentDessert(). Lưu ý rằng phương thức này xác định ảnh món tráng miệng nào sẽ hiển thị trong hoạt động dựa trên số lượng món tráng miệng hiện được bán và danh sách món tráng miệng trong biến allDesserts.
for (dessert in allDesserts) {
   if (dessertsSold >= dessert.startProductionAmount) {
       newDessert = dessert
   }
    else break
}

Phương thức này dựa vào số lượng món tráng miệng được bán để chọn hình ảnh phù hợp. Do đó, bạn không cần phải làm gì để lưu trữ tham chiếu đến hình ảnh trong gói trong onSaveInstanceState(). Trong gói đó, bạn đã lưu trữ số lượng món tráng miệng bán được.

  1. Trong onCreate(), trong khối khôi phục trạng thái từ gói, hãy gọi showCurrentDessert():
 if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
   dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
   dessertTimer.secondsCount = 
      savedInstanceState.getInt(KEY_TIMER_SECONDS, 0)
   showCurrentDessert()                   
}
  1. Biên dịch và chạy ứng dụng, rồi đưa ứng dụng đó vào chế độ nền. Sử dụng adb để tắt quy trình. Dùng màn hình gần đây để quay lại ứng dụng. Lưu ý rằng cả giá trị của món tráng miệng được bán, tổng doanh thu và ảnh món tráng miệng đều được khôi phục chính xác.

Có một trường hợp đặc biệt cuối cùng trong việc quản lý vòng đời hoạt động và vòng đời mảnh mà bạn cần phải nắm vững, đó là tác động của các thay đổi trong cấu hình đối với vòng đời của hoạt động và mảnh.

Thay đổi cấu hình xảy ra khi trạng thái của thiết bị hoàn toàn thay đổi đến mức cách dễ nhất để hệ thống giải quyết thay đổi là hoàn toàn tắt đi và xây dựng lại hoạt động. Ví dụ: nếu người dùng thay đổi ngôn ngữ của thiết bị, thì toàn bộ bố cục cần phải thay đổi cho phù hợp với các hướng văn bản. Nếu người dùng cắm thiết bị vào đế sạc hoặc thêm bàn phím thực, thì bố cục ứng dụng có thể cần tận dụng bố cục hoặc kích thước hiển thị khác. Còn nếu hướng thiết bị thay đổi—nếu thiết bị được xoay từ dọc sang ngang hoặc quay lại theo hướng khác—bố cục có thể cần đổi để phù hợp với hướng mới.

Bước 1: Khám phá tính năng xoay thiết bị và các lệnh gọi lại trong vòng đời

  1. Biên dịch và chạy ứng dụng của bạn, sau đó mở Logcat.
  2. Xoay thiết bị hoặc trình mô phỏng sang chế độ ngang. Bạn có thể xoay trình mô phỏng sang trái hoặc sang phải bằng các nút xoay hoặc bằng Control và các phím mũi tên (Command và các phím mũi tên trên máy Mac).
  3. Kiểm tra kết quả đầu ra trong Logcat. Lọc kết quả đầu ra trên MainActivity.
    Lưu ý rằng khi thiết bị hoặc trình mô phỏng xoay màn hình, hệ thống sẽ gọi tất cả các phương thức gọi lại trong vòng đời để tắt hoạt động. Sau đó, khi hoạt động được tạo lại, hệ thống sẽ gọi tất cả các phương thức gọi lại trong vòng đời để khởi động hoạt động.
  4. Trong MainActivity, hãy thêm chú thích cho toàn bộ phương thức onSaveInstanceState().
  5. Biên dịch và chạy lại ứng dụng. Nhấp vào bánh nướng một vài lần rồi xoay thiết bị hoặc trình mô phỏng. Lần này, khi thiết bị xoay và hoạt động bị tắt rồi tạo lại, hoạt động sẽ khởi động với các giá trị mặc định.

    Khi có thay đổi về cấu hình, Android sẽ dùng cùng một gói trạng thái của thực thể mà bạn đã tìm hiểu trong tác vụ trước để lưu và khôi phục trạng thái của ứng dụng. Tương tự như khi tắt quy trình, hãy dùng onSaveInstanceState() để đưa dữ liệu của ứng dụng vào gói. Sau đó, hãy khôi phục dữ liệu trong onCreate() để tránh mất dữ liệu trạng thái hoạt động nếu thiết bị bị xoay.
  6. Trong MainActivity, hãy xoá dấu nhận xét phương thức onSaveInstanceState(), chạy ứng dụng, nhấp vào bánh cupcake rồi xoay ứng dụng hoặc thiết bị. Lần này, hãy lưu ý rằng dữ liệu món tráng miệng được giữ lại trong quá trình xoay hoạt động.

Dự án Android Studio: DessertClickerFinal

Mẹo về vòng đời

  • Nếu bạn thiết lập hoặc bắt đầu một thao tác trong một lệnh gọi lại trong vòng đời, hãy dừng hoặc xoá thao tác đó trong lệnh gọi lại tương ứng. Bằng cách dừng đối tượng, bạn đảm bảo rằng đối tượng đó không tiếp tục chạy khi không còn cần thiết nữa. Ví dụ: nếu thiết lập một bộ hẹn giờ trong onStart(), bạn cần tạm dừng hoặc dừng bộ hẹn giờ đó trong onStop().
  • Chỉ sử dụng onCreate() để khởi chạy những phần của ứng dụng chạy một lần, khi ứng dụng khởi động lần đầu. Sử dụng onStart() để bắt đầu các phần của ứng dụng chạy cả khi ứng dụng khởi động và mỗi khi ứng dụng quay lại nền trước.

Thư viện vòng đời

  • Sử dụng thư viện vòng đời Android để chuyển quyền kiểm soát vòng đời từ hoạt động hoặc mảnh sang thành phần thực tế cần nhận biết được vòng đời.
  • Chủ sở hữu vòng đời là những thành phần có (và do đó "sở hữu") vòng đời, bao gồm cả ActivityFragment. Các đối tượng sở hữu vòng đời triển khai giao diện LifecycleOwner.
  • Đối tượng tiếp nhận dữ liệu vòng đời chú ý đến trạng thái vòng đời hiện tại và thực hiện các tác vụ khi vòng đời thay đổi. Các đối tượng tiếp nhận dữ liệu vòng đời triển khai giao diện LifecycleObserver.
  • Các đối tượng Lifecycle chứa các trạng thái vòng đời thực tế và kích hoạt các sự kiện khi vòng đời thay đổi.

Cách tạo một lớp nhận biết vòng đời:

  • Triển khai giao diện LifecycleObserver trong các lớp cần nhận biết vòng đời.
  • Khởi chạy một lớp đối tượng tiếp nhận dữ liệu vòng đời bằng đối tượng vòng đời từ hoạt động hoặc mảnh.
  • Trong lớp đối tượng tiếp nhận dữ liệu vòng đời, hãy chú thích các phương thức nhận biết vòng đời bằng thay đổi trạng thái vòng đời mà chúng quan tâm.

    Ví dụ: chú thích @OnLifecycleEvent(Lifecycle.Event.ON_START) cho biết phương thức đang theo dõi sự kiện vòng đời onStart.

Tắt quy trình và lưu trạng thái hoạt động

  • Android điều chỉnh các ứng dụng chạy ở chế độ nền để ứng dụng trên nền trước có thể chạy mà không gặp vấn đề gì. Quy định này bao gồm việc giới hạn mức xử lý mà các ứng dụng ở chế độ nền có thể làm, và đôi khi thậm chí tắt toàn bộ quy trình của ứng dụng.
  • Người dùng không thể biết liệu hệ thống có tắt một ứng dụng ở chế độ nền hay không. Ứng dụng vẫn xuất hiện trên màn hình gần đây và sẽ khởi động lại ở trạng thái mà người dùng đã rời khỏi.
  • Cầu gỡ lỗi Android (adb) là một công cụ dòng lệnh cho phép bạn gửi hướng dẫn đến các trình mô phỏng và thiết bị được kết nối với máy tính. Bạn có thể dùng adb để mô phỏng quá trình tắt trong ứng dụng.
  • Khi Android tắt quy trình ứng dụng của bạn, phương thức vòng đời onDestroy() sẽ không được gọi. Ứng dụng chỉ dừng lại.

Bảo tồn trạng thái hoạt động và trạng thái mảnh

  • Khi ứng dụng của bạn chuyển sang chế độ nền, ngay sau khi onStop() được gọi, dữ liệu ứng dụng sẽ được lưu vào một gói. Một vài dữ liệu ứng dụng, chẳng hạn như nội dung của EditText, sẽ tự động được lưu cho bạn.
  • Gói này là một thực thể của Bundle, một tập hợp các khoá và giá trị. Các khoá luôn là chuỗi.
  • Sử dụng lệnh gọi lại onSaveInstanceState() để lưu dữ liệu khác vào gói mà bạn muốn giữ lại, ngay cả khi ứng dụng tự động tắt. Để đưa dữ liệu vào gói, sử dụng phương thức gói bắt đầu bằng put, chẳng hạn như putInt().
  • Bạn có thể lấy lại dữ liệu từ gói trong phương thức onRestoreInstanceState() hoặc phổ biến hơn là trong onCreate(). Phương thức onCreate() có tham số savedInstanceState chứa gói này.
  • Nếu biến savedInstanceState chứa null, hoạt động được bắt đầu mà không cần gói trạng thái và không có dữ liệu trạng thái nào để truy xuất.
  • Để truy xuất dữ liệu từ gói bằng khoá, sử dụng các phương thức Bundle bắt đầu bằng get, chẳng hạn như getInt().

Thay đổi cấu hình

  • Thay đổi cấu hình xảy ra khi trạng thái của thiết bị hoàn toàn thay đổi đến mức cách dễ nhất để hệ thống giải quyết thay đổi là tắt và tạo lại hoạt động.
  • Ví dụ phổ biến nhất về việc thay đổi cấu hình là khi người dùng xoay thiết bị từ chế độ dọc sang chế độ ngang, hoặc từ chế độ ngang sang chế độ dọc. Việc thay đổi cấu hình cũng có thể xảy ra khi thay đổi ngôn ngữ của thiết bị hoặc thiết bị được kết nối với bàn phím phần cứng.
  • Khi cấu hình thay đổi, Android sẽ gọi tất cả các lệnh gọi lại để tắt (shutdown callback) của vòng đời hoạt động. Sau đó, Android khởi động lại hoạt động từ đầu, chạy tất cả các lệnh gọi lại khởi động vòng đời.
  • Khi Android tắt ứng dụng do thay đổi cấu hình, ứng dụng sẽ khởi động lại hoạt động bằng gói trạng thái có trên onCreate().
  • Cũng giống như trong chế độ tắt quy trình, hãy lưu trạng thái của ứng dụng vào gói trong onSaveInstanceState().

Khoá học của Udacity:

Tài liệu dành cho nhà phát triển Android:

Khác:

Phần này liệt kê các bài tập về nhà cho học viên của lớp học lập trình này trong phạm vi khoá học có người hướng dẫn. Người hướng dẫn phải thực hiện các việc sau đây:

  • Giao bài tập về nhà nếu cần.
  • Trao đổi với học viên về cách nộp bài tập về nhà.
  • Chấm điểm bài tập về nhà.

Người hướng dẫn có thể sử dụng các đề xuất này ít hoặc nhiều tuỳ ý và nên giao cho học viên bất kỳ bài tập về nhà nào khác mà họ cảm thấy phù hợp.

Nếu bạn đang tự học các lớp học lập trình, hãy sử dụng những bài tập về nhà này để kiểm tra kiến thức của mình.

Thay đổi ứng dụng

Mở ứng dụng DiceRoller trong Bài 1. (Bạn có thể tải ứng dụng này xuống tại đây nếu chưa cài đặt.) Biên dịch và chạy ứng dụng, đồng thời lưu ý rằng nếu bạn xoay thiết bị, giá trị hiện tại của xúc xắc sẽ bị mất. Triển khai onSaveInstanceState() để giữ lại giá trị đó trong gói và khôi phục giá trị đó trong onCreate().

Trả lời các câu hỏi sau

Câu hỏi 1

Ứng dụng của bạn chứa một hoạt động mô phỏng thực tế yêu cầu sử dụng phép tính phức tạp để hiển thị. Sau đó, người dùng sẽ nhận được một cuộc gọi điện thoại. Câu nào sau đây là đúng?

  • Trong cuộc gọi điện thoại, bạn nên tiếp tục tính toán vị trí của các vật thể trong hoạt động mô phỏng vật lý.
  • Trong cuộc gọi điện thoại, bạn nên ngừng tính toán vị trí của các vật thể trong hoạt động mô phỏng vật lý.

Câu hỏi 2

Bạn nên ghi đè phương thức vòng đời nào để tạm dừng hoạt động mô phỏng khi ứng dụng không xuất hiện trên màn hình?

  • onDestroy()
  • onStop()
  • onPause()
  • onSaveInstanceState()

Câu hỏi 3

Để lớp này nhận biết được vòng đời thông qua thư viện vòng đời của Android, lớp nên triển khai giao diện nào?

  • Lifecycle
  • LifecycleOwner
  • Lifecycle.Event
  • LifecycleObserver

Câu hỏi 4

Phương thức onCreate() trong hoạt động của bạn nhận được Bundle có dữ liệu trong các trường hợp nào (tức là Bundle không phải null)? Có thể có nhiều câu trả lời đúng.

  • Hoạt động sẽ khởi động lại sau khi thiết bị được xoay.
  • Hoạt động này sẽ bắt đầu từ đầu.
  • Hoạt động sẽ tiếp tục sau khi người dùng trở lại từ nền.
  • Khởi động lại thiết bị.

Bắt đầu bài học tiếp theo: 5.1: ViewModel và ViewModelFactory

Để biết đường liên kết đến các lớp học lập trình khác trong khoá học này, hãy xem trang đích của lớp học lập trình Kiến thức cơ bản về cách tạo ứng dụng Android bằng Kotlin.