Các giai đoạn trong vòng đời hoạt động

1. Chào mừng mọi người

Giới thiệu

Trong lớp học lập trình này, bạn sẽ tìm hiểu thêm về một phần cơ bản của Android: hoạt động. Vòng đời hoạt động là tập hợp các trạng thái của một hoạt động trong suốt thời gian hoạt động. Vòng đời kéo dài từ khi hoạt động được tạo ra lúc ban đầu cho đến khi bị huỷ và khi hệ thống thu hồi tài nguyên của hoạt động đó. Khi người dùng di chuyển giữa các hoạt động trong ứng dụng của bạn (trong và ngoài ứng dụng), các hoạt động đó sẽ chuyển đổi giữa các trạng thái khác nhau trong vòng đời hoạt động.

Là nhà phát triển Android, bạn cần hiểu rõ vòng đời hoạt động. Nếu hoạt động không phản hồi chính xác với các thay đổi về trạng thái của vòng đời, ứng dụng có thể tạo ra các lỗi lạ, hành vi khó hiểu đối với người dùng hoặc sử dụng quá nhiều tài nguyên hệ thống của Android. Hiểu được vòng đời của Android và phản hồi chính xác với các thay đổi về trạng thái của vòng đời là điều rất quan trọng để trở thành công dân tốt của Android.

Những điều bạn nên biết

  • Hoạt động là gì và cách tạo hoạt động trong ứng dụng.
  • Phương thức onCreate() của hoạt động làm gì và loại thao tác được thực hiện trong phương thức đó.

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

  • Cách in thông tin ghi nhật ký vào Logcat.
  • Thông tin cơ bản trong vòng đời của Activity và các lệnh gọi lại được gọi khi hoạt động di chuyển giữa các trạng thái.
  • Cách ghi đè phương thức gọi lại trong vòng đời để 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.

Bạn sẽ thực hiện

  • Sửa đổi ứng dụng khởi đầu có tên là DessertClicker để thêm thông tin ghi nhật ký được xuất hiện trong Logcat.
  • Ghi đè phương thức gọi lại trong vòng đời và ghi lại các thay đổi vào trạng thái hoạt động.
  • Chạy ứng dụng và để ý thông tin ghi nhật ký xuất hiện khi hoạt động được khởi động, dừng và tiếp tục.
  • Triển khai phương thức onSaveInstanceState() để giữ lại dữ liệu ứng dụng có thể bị mất nếu cấu hình thiết bị thay đổi. Thêm mã để khôi phục dữ liệu đó khi ứng dụng khởi động lại.

2. Tổng quan về ứng dụng

Ở lớp học lập trình này, bạn làm việc với một ứng dụng khởi đầu có tên là DessertClicker. Trong ứng dụng này, mỗi lần người dùng nhấn vào một món tráng miệng trên màn hình, ứng dụng sẽ "mua" món tráng miệng đó cho người dùng. Ứng dụng sẽ cập nhật giá trị trong bố cục về số lượng món tráng miệng đã mua và tổng số tiền người dùng đã chi.

8216c20f5571fc04.png

Ứng dụng này chứa một số lỗi liên quan đến vòng đời của Android: Ví dụ: trong một số trường hợp, ứng dụng đặt lại các giá trị của món tráng miệng về 0. Khi nắm rõ vòng đời của ứng dụng Android, bạn sẽ hiểu được nguyên nhân của những vấn đề này và cách khắc phục.

Tải ứng dụng ban đầu

Tải mã ban đầu DessertClicker rồi mở mã này trong Android Studio.

Nếu bạn sử dụng đoạn mã khởi đầu lấy trên GitHub, hãy lưu ý tên thư mục là android-basics-kotlin-dessert-clicker-app-starter. Chọn thư mục này khi bạn mở dự án trong Android Studio.

Để lấy mã cho lớp học lập trình này và mở mã đó trong Android Studio, hãy làm như sau.

Lấy mã

  1. Nhấp vào URL được cung cấp. Thao tác này sẽ mở trang GitHub của dự án trong một trình duyệt.
  2. Trên trang GitHub của dự án, hãy nhấp vào nút Code (Mã), một hộp thoại sẽ xuất hiện.

5b0a76c50478a73f.png

  1. Trong hộp thoại này, hãy nhấp vào nút Download ZIP (Tải tệp ZIP xuống) để lưu dự án vào máy tính. Chờ quá trình tải xuống hoàn tất.
  2. Xác định vị trí của tệp trên máy tính (thường nằm trong thư mục Downloads (Tệp đã tải xuống)).
  3. Nhấp đúp vào tệp ZIP để giải nén. Thao tác này sẽ tạo một thư mục mới chứa các tệp dự án.

Mở dự án trong Android Studio

  1. Khởi động Android Studio.
  2. Trong cửa sổ Welcome to Android Studio (Chào mừng bạn đến với Android Studio), hãy nhấp vào Open an existing Android Studio project (Mở một dự án hiện có trong Android Studio).

36cc44fcf0f89a1d.png

Lưu ý: Nếu Android Studio đã mở sẵn thì thay vào đó, hãy chọn tuỳ chọn sau đây trong trình đơn File > New > Import Project (Tệp > Mới > Nhập dự án).

21f3eec988dcfbe9.png

  1. Trong hộp thoại Import Project (Nhập dự án), hãy chuyển đến nơi chứa thư mục dự án đã giải nén (thường nằm trong thư mục Downloads (Tệp đã tải xuống)).
  2. Nhấp đúp vào thư mục dự án đó.
  3. Chờ Android Studio mở dự án.
  4. Nhấp vào nút Run (Chạy) 11c34fc5e516fb1c.png để xây dựng và chạy ứng dụng. Hãy đảm bảo ứng dụng được dựng như mong đợi.
  5. Duyệt qua các tệp dự án trong cửa sổ công cụ Project (Dự án) để xem cách ứng dụng được thiết lập.

3. Khám phá các phương thức của vòng đời và thêm tính năng ghi nhật ký cơ bản

Mọi hoạt động đều có điều được gọi là vòng đời hoạt động. Đây là ám chỉ về vòng đời của thực vật và động vật, như vòng đời của bươm bướm—các trạng thái khác nhau cho thấy sự phát triển của bươm bướm từ khi sinh ra đến khi hoàn toàn trưởng thành cho đến khi chết.

c685f48ff799f0c9.png

Tương tự như vậy, vòng đời hoạt động bao gồm các trạng thái khác nhau mà một hoạt động có thể trải qua, từ khi hoạt động được khởi tạo lần đầu tiên cho đến khi hoạt động cuối cùng bị huỷ và thu hồi bộ nhớ bởi hệ thống. Khi người dùng khởi động ứng dụng, di chuyển giữa các hoạt động, di chuyển trong và ngoài ứng dụng, thì trạng thái của hoạt động sẽ thay đổi. Sơ đồ dưới đây cho thấy tất cả các trạng thái của vòng đời hoạt động. Đúng như tên gọi cho thấy, tên của các trạng thái này cho biết trạng thái của hoạt động.

c803811f4cb4034b.png

Thường thì bạn muốn thay đổi một số hành vi hoặc chạy một mã cụ thể khi trạng thái của vòng đời hoạt động thay đổi. Do đó, chính lớp Activity và bất kỳ lớp con nào của Activity, chẳng hạn như AppCompatActivity, sẽ triển khai một tập hợp các phương thức gọi lại trong vòng đời. Android gọi đến những lệnh gọi lại này khi hoạt động chuyển từ trạng thái này sang trạng thái khác và bạn có thể ghi đè các phương thức đó vào hoạt động của riêng mình để thực hiện các nhiệm vụ ứng với những thay đổi trạng thái vòng đời đó. Sơ đồ dưới đây cho thấy các trạng thái vòng đời cùng lệnh gọi lại có thể ghi đè.

f6b25a71cec4e401.png

Điều quan trọng là phải biết khi nào các lệnh gọi lại này được gọi và cần làm gì trong mỗi phương thức gọi lại. Nhưng cả hai sơ đồ này đều phức tạp và có thể gây nhầm lẫn. Trong lớp học lập trình này, thay vì chỉ đọc ý nghĩa của từng trạng thái và lệnh gọi lại, bạn sẽ thực hiện một số thao tác để tìm hiểu và xây dựng kiến thức về những gì đang xảy ra.

Bước 1: Kiểm tra phương thức onCreate() và thêm tính năng ghi nhật ký

Để tìm hiểu điều đang xảy ra với vòng đời của Android, bạn cần biết thời điểm mà các phương thức khác nhau của vòng đời được gọi. Điều này sẽ giúp bạn tìm ra nơi diễn ra sự cố trong DessertClicker.

Cách đơn giản để làm điều đó là sử dụng chức năng ghi nhật ký của Android. Tính năng ghi nhật ký cho phép bạn viết các thông báo ngắn vào bảng điều khiển trong khi ứng dụng chạy và bạn có thể sử dụng bảng điều khiển này để biết khi nào các lệnh gọi lại khác nhau được kích hoạt.

  1. Chạy ứng dụng Dessert Clicker và nhấn vài lần vào ảnh món tráng miệng. Lưu ý cách giá trị của Món tráng miệng được bán và tổng số tiền thay đổi.
  2. Mở MainActivity.kt và kiểm tra phương thức onCreate() của hoạt động này:
override fun onCreate(savedInstanceState: Bundle?) {
...
}

Trong sơ đồ vòng đời hoạt động, bạn có thể nhận ra phương thức onCreate(), vì bạn đã từng sử dụng lệnh gọi lại này. Đây là phương pháp mà mọi hoạt động đều phải triển khai. Phương thức onCreate() là nơi bạn nên thực hiện mọi thao tác khởi chạy một lần cho hoạt động của mình. Ví dụ: trong onCreate(), bạn mở rộng bố cục, xác định trình nghe lượt nhấp hoặc thiết lập tính năng liên kết chế độ xem.

9be2255ff49e0af8.png

Phương thức vòng đời onCreate() được gọi một lần, ngay sau khi hoạt động được khởi chạy (khi đối tượng Activity mới được tạo trong bộ nhớ). Sau khi onCreate() thực thi, hoạt động sẽ được coi là đã được tạo.

  1. Trong phương thức onCreate(), ngay sau lệnh gọi đến super.onCreate(), thêm vào dòng sau:
Log.d("MainActivity", "onCreate Called")
  1. Nhập lớp Log nếu cần (nhấn Alt+Enter hoặc Option+Enter trên máy Mac và chọn Import (Nhập)). Nếu bạn đã bật tính năng nhập tự động, thì quá trình này sẽ diễn ra một cách tự động.
import android.util.Log

Lớp Log ghi thông báo vào Logcat. Logcat là bảng điều khiển dành cho thông báo nhật ký. Các thông báo của Android về ứng dụng của bạn sẽ xuất hiện tại đây, bao gồm cả các thông báo bạn công khai gửi tới nhật ký bằng phương thức Log.d() hoặc các phương thức lớp Log khác.

Lệnh này có ba phần:

  • Mức độ ưu tiên của thông báo nhật ký, chính là mức độ quan trọng của thông báo. Trong trường hợp này, phương thức Log.d() sẽ viết một thông báo gỡ lỗi. Các phương thức khác trong lớp Log bao gồm Log.i() để thông báo thông tin, Log.e() để thông báo lỗi, Log.w() để cảnh báo hoặc Log.v() để thông báo chi tiết.
  • Nhật ký thẻ (tham số đầu tiên), trong trường hợp này là "MainActivity". Thẻ là một chuỗi cho phép bạn dễ dàng tìm thấy các thông báo nhật ký trong Logcat. Thẻ thường mang tên của lớp.
  • Thông báo nhật ký thực tế (tham số thứ hai) là một chuỗi ngắn, trong trường hợp này là "onCreate called".

Hằng số thời gian biên dịch là một giá trị không thay đổi. Sử dụng const trước một khai báo biến để đánh dấu biến là hằng số thời gian biên dịch.

  1. Biên dịch và chạy ứng dụng DessertClicker. Không có khác biệt nào về hành vi trong ứng dụng khi bạn nhấn vào món tráng miệng. Trong Android Studio, ở cuối màn hình, nhấp vào thẻ Logcat.

6463f587ac6997fe.png

  1. Trong cửa sổ Logcat, nhập D/MainActivity vào trường tìm kiếm.

bb0b78600cd47789.png

Logcat chứa nhiều thông báo, hầu hết đều không hữu ích với bạn. Bạn có thể lọc các mục logcat (entries Logcat) theo nhiều cách, nhưng tìm kiếm là cách dễ nhất. Do đã sử dụng MainActivity làm thẻ nhật ký trong mã, nên bạn có thể sử dụng thẻ đó để lọc nhật ký. Thêm D/ ngay từ đầu có nghĩa đây là thông báo gỡ lỗi do Log.d() tạo.

Thông báo nhật ký bao gồm ngày và giờ, tên gói (com.example.android.dessertclicker), thẻ nhật ký của bạn (với D/ ở trên cùng) và thông báo thực tế. Vì thông báo này xuất hiện trong nhật ký, nên bạn biết rằng onCreate() đã được thực thi.

Bước 2: Triển khai phương thức onStart()

Phương thức vòng đời onStart() được gọi ngay sau onCreate(). Sau khi onStart() chạy, hoạt động của bạn sẽ hiển thị trên màn hình. Không giống như onCreate(), chỉ được gọi một lần để khởi tạo hoạt động của bạn, onStart() có thể được gọi nhiều lần trong vòng đời của hoạt động.

385df4ce82ae2de9.png

Lưu ý rằng onStart() được ghép nối với phương thức vòng đời onStop() tương ứng. Nếu người dùng khởi động ứng dụng của bạn rồi quay lại màn hình chính của thiết bị, thì hoạt động sẽ dừng lại và không còn hiện trên màn hình.

  1. Trong Android Studio, với MainActivity.kt mở và con trỏ bên trong lớp MainActivity, chọn Code (Mã)> Override Methods (Ghi đè phương thức) hoặc nhấn vào Control+o (Command+o trên máy Mac). Một hộp thoại sẽ xuất hiện với danh sách khổng lồ tất cả các phương thức bạn có thể ghi đè trong lớp này.

e1f2460242b2ae.png

  1. Bắt đầu nhập onStart để tìm phương thức phù hợp. Để cuộn đến mục trùng khớp tiếp theo, sử dụng mũi tên xuống. Chọn onStart() trong danh sách và nhấp vào OK để chèn mã ghi đè theo nguyên mẫu. Mã sẽ có dạng như sau:
override fun onStart() {
   super.onStart()
}
  1. Thêm hằng số sau vào cấp cao nhất của MainActivity.kt, tức là phía trên phần khai báo lớp, class MainActivity.
const val TAG = "MainActivity"
  1. Bên trong phương thức onStart(), thêm một thông báo nhật ký:
override fun onStart() {
   super.onStart()
   Log.d(TAG, "onStart Called")
}
  1. Biên dịch và chạy ứng dụng DessertClicker, rồi mở ngăn Logcat. Nhập D/MainActivity vào trường tìm kiếm để lọc nhật ký. Lưu ý rằng cả hai phương thức onCreate()onStart() đều lần lượt được gọi và hoạt động của bạn được hiển thị trên màn hình.
  2. Nhấn vào nút Home (Màn hình chính) trên thiết bị, sau đó dùng màn hình gần đây để quay lại hoạt động. Lưu ý rằng hoạt động này sẽ lại tiếp tục từ nơi đã ngừng lại trước đó, với cùng các giá trị và onStart() được ghi lại lần thứ hai vào Logcat. Ngoài ra, lưu ý rằng phương thức onCreate() thường không được gọi lại.
16:19:59.125 31107-31107/com.example.android.dessertclicker D/MainActivity: onCreate Called
16:19:59.372 31107-31107/com.example.android.dessertclicker D/MainActivity: onStart Called
16:20:11.319 31107-31107/com.example.android.dessertclicker D/MainActivity: onStart Called

Bước 3: Thêm câu lệnh nhật ký khác

Tại bước này, bạn triển khai tính năng ghi nhật ký cho tất cả các phương thức vòng đời khác.

  1. Ghi đè phần còn lại của các phương thức vòng đời trong MainActivity và thêm câu lệnh nhật ký cho mỗi phương thức. Đây là mã:
override fun onResume() {
   super.onResume()
   Log.d(TAG, "onResume Called")
}

override fun onPause() {
   super.onPause()
   Log.d(TAG, "onPause Called")
}

override fun onStop() {
   super.onStop()
   Log.d(TAG, "onStop Called")
}

override fun onDestroy() {
   super.onDestroy()
   Log.d(TAG, "onDestroy Called")
}

override fun onRestart() {
   super.onRestart()
   Log.d(TAG, "onRestart Called")
}
  1. Biên dịch và chạy lại DessertClicker rồi kiểm tra Logcat. Lần này, ngoài onCreate()onStart(), để ý còn có thông báo nhật ký cho phương thức gọi lại trong vòng đời onResume().
2020-10-16 10:27:33.244 22064-22064/com.example.android.dessertclicker D/MainActivity: onCreate Called
2020-10-16 10:27:33.453 22064-22064/com.example.android.dessertclicker D/MainActivity: onStart Called
2020-10-16 10:27:33.454 22064-22064/com.example.android.dessertclicker D/MainActivity: onResume Called

Khi một hoạt động bắt đầu từ đầu, bạn sẽ thấy cả ba phương thức gọi lại trong vòng đời này đều được gọi theo thứ tự:

  • onCreate() để tạo ứng dụng.
  • onStart() để bắt đầu hoạt động và hiển thị hoạt động trên màn hình.
  • onResume() để tập trung vào hoạt động và giúp người dùng sẵn sàng tương tác với hoạt động đó.

Dù có tên, phương thức onResume() vẫn được gọi khi khởi chạy, ngay cả khi không có gì để tiếp tục.

160054d59f67519.png

4. Khám phá các trường hợp sử dụng vòng đời

Giờ đây khi ứng dụng DessertClicker đã được thiết lập để ghi nhật ký, bạn đã sẵn sàng để bắt đầu sử dụng ứng dụng theo nhiều cách và sẵn sàng khám phá cách phương thức gọi lại trong vòng đời được kích hoạt để đáp ứng các cách sử dụng đó.

Trường hợp sử dụng 1: Mở và đóng hoạt động

Bạn bắt đầu với trường hợp sử dụng cơ bản nhất, đó là khởi động ứng dụng lần đầu tiên, rồi hoàn toàn đóng ứng dụng.

  1. Biên dịch và chạy ứng dụng DessertClicker, nếu bạn vẫn chưa chạy ứng dụng này. Như đã thấy, phương thức gọi lại onCreate(), onStart()onResume() được gọi khi hoạt động khởi động lần đầu tiên.
2020-10-16 10:27:33.244 22064-22064/com.example.android.dessertclicker D/MainActivity: onCreate Called
2020-10-16 10:27:33.453 22064-22064/com.example.android.dessertclicker D/MainActivity: onStart Called
2020-10-16 10:27:33.454 22064-22064/com.example.android.dessertclicker D/MainActivity: onResume Called
  1. Nhấn vào bánh nướng một vài lần.
  2. Nhấn vào nút Back (Quay lại) trên thiết bị. Bạn có thể nhận thấy trong Logcat rằng onPause(), onStop()onDestroy() được gọi theo thứ tự đó.
2020-10-16 10:31:53.850 22064-22064/com.example.android.dessertclicker D/MainActivity: onPause Called
2020-10-16 10:31:54.620 22064-22064/com.example.android.dessertclicker D/MainActivity: onStop Called
2020-10-16 10:31:54.622 22064-22064/com.example.android.dessertclicker D/MainActivity: onDestroy Called

Trong trường hợp này, việc sử dụng nút Back (Quay lại) sẽ khiến hoạt động (và ứng dụng) đóng lại hoàn toàn. Việc thực thi phương thức onDestroy() có nghĩa là hoạt động đã hoàn toàn tắt và có thể thu thập rác. Thu thập rác tức là tính năng tự động dọn dẹp các đối tượng không còn được sử dụng nữa. Sau khi onDestroy() được gọi, hệ thống biết rằng các tài nguyên đó có thể được loại bỏ và hệ thống bắt đầu dọn dẹp bộ nhớ đó. 2dcc4d9c6478a9f4.png

Hoạt động cũng có thể được tắt hoàn toàn nếu mã của bạn thủ công gọi ra phương thức finish() của hoạt động hoặc nếu người dùng buộc thoát khỏi ứng dụng. (Ví dụ: người dùng có thể buộc thoát khỏi ứng dụng hoặc đóng ứng dụng trong màn hình gần đây.) Hệ thống Android cũng có thể tự tắt hoạt động nếu ứng dụng của bạn không hiển thị trên màn hình trong một thời gian dài. Android thực hiện việc này để duy trì thời lượng pin và cho phép ứng dụng khác dùng tài nguyên trên ứng dụng của bạn.

  1. Quay lại ứng dụng DessertClicker bằng cách tìm tất cả các ứng dụng đang mở trên màn hình Overview (Tổng quan). (Lưu ý rằng màn hình này còn được gọi là màn hình Gần đây hoặc ứng dụng gần đây.) Đây là Logcat:
2020-10-16 10:31:54.622 22064-22064/com.example.android.dessertclicker D/MainActivity: onDestroy Called
2020-10-16 10:38:00.733 22064-22064/com.example.android.dessertclicker D/MainActivity: onCreate Called
2020-10-16 10:38:00.787 22064-22064/com.example.android.dessertclicker D/MainActivity: onStart Called
2020-10-16 10:38:00.788 22064-22064/com.example.android.dessertclicker D/MainActivity: onResume Called

Hoạt động đã bị huỷ trong bước trước đó, do vậy khi quay lại ứng dụng, Android sẽ bắt đầu một hoạt động mới và gọi các phương thức onCreate(), onStart()onResume(). Lưu ý rằng không có nhật ký DessertClicker nào từ hoạt động trước đó được giữ lại.

Phương thức onCreate() là một bước quan trọng; đây chính là nơi tất cả quá trình khởi chạy lần đầu tiên diễn ra, là nơi bạn thiết lập bố cục lần đầu tiên bằng cách mở rộng bố cục và là nơi bạn khởi chạy các biến.

Trường hợp sử dụng 2: Rời khỏi và quay lại hoạt động

Bây giờ, khi đã khởi động và hoàn toàn đóng ứng dụng, bạn thấy hầu hết các trạng thái của vòng đời khi hoạt động được tạo ra lần đầu tiên. Bạn cũng thấy được tất cả trạng thái của vòng đời mà hoạt động trải qua khi hoàn toàn bị tắt và huỷ. Tuy nhiên, khi người dùng tương tác với thiết bị Android, họ chuyển đổi qua lại giữa các ứng dụng, quay về trang chủ, khởi động ứng dụng mới và xử lý các hoạt động gây gián đoạn bằng các hoạt động khác, như cuộc gọi điện thoại.

Hoạt động của bạn không bị đóng lại hoàn toàn mỗi khi người dùng rời khỏi hoạt động đó:

  • Khi hoạt động không còn hiển thị trên màn hình, việc này được gọi là đưa hoạt động vào chạy trong chế độ nền. (Ngược lại với điều này là khi hoạt động diễn ra trong nền trước (foreground) hoặc trên màn hình.)
  • Khi người dùng quay lại ứng dụng của bạn, hoạt động tương tự đó sẽ khởi động lại và xuất hiện trở lại. Phần này của vòng đời được gọi là vòng đời hiển thị của ứng dụng.

Khi chạy trong chế độ nền, ứng dụng của bạn không nên chủ động chạy để bảo tồn tài nguyên hệ thống và thời lượng pin. Có thể sử dụng vòng đời Activity và lệnh gọi lại để biết ứng dụng của bạn chuyển sang chạy trong chế độ nền khi nào để có thể tạm dừng mọi hoạt động đang diễn ra. Sau đó, khởi động lại các thao tác khi ứng dụng của bạn xuất hiện trên nền trước.

Tại bước này, bạn xem xét vòng đời hoạt động khi ứng dụng chuyển sang chạy ở chế độ nền và trở lại nền trước.

  1. Khi ứng dụng DessertClicker đang chạy, nhấp vào bánh nướng một vài lần.
  2. Nhấn nút Home (Màn hình chính) trên thiết bị của bạn và quan sát Logcat trong Android Studio. Khi trở lại màn hình chính, hãy đặt ứng dụng của bạn vào chế độ nền thay vì tắt ứng dụng. Lưu ý rằng phương thức onPause() và phương thức onStop() được gọi, còn onDestroy() thì không.
2020-10-16 10:41:05.383 22064-22064/com.example.android.dessertclicker D/MainActivity: onPause Called
2020-10-16 10:41:05.966 22064-22064/com.example.android.dessertclicker D/MainActivity: onStop Called

Khi onPause() được gọi, ứng dụng sẽ không còn được chú ý đến nữa. Sau onStop(), ứng dụng không còn hiển thị trên màn hình nữa. Dù hoạt động đã dừng, nhưng đối tượng Activity vẫn còn trong bộ nhớ, ở chế độ nền. Hoạt động không bị huỷ. Người dùng có thể quay lại ứng dụng, vì vậy, Android vẫn duy trì các tài nguyên hoạt động của bạn. b488b32801220b79.png

  1. Dùng màn hình gần đây để quay lại ứng dụng. Lưu ý trong Logcat rằng hoạt động được khởi động lại cùng onRestart()onStart(), rồi tiếp tục với onResume().
2020-10-16 10:42:18.144 22064-22064/com.example.android.dessertclicker D/MainActivity: onRestart Called
2020-10-16 10:42:18.158 22064-22064/com.example.android.dessertclicker D/MainActivity: onStart Called
2020-10-16 10:42:18.158 22064-22064/com.example.android.dessertclicker D/MainActivity: onResume Called

Khi hoạt động quay lại nền trước, phương thức onCreate() không được gọi lại. Đối tượng hoạt động không bị huỷ, do đó không cần được tạo lại. Thay vào onCreate(), thì phương thức onRestart() sẽ được gọi. Lần này, lưu ý rằng khi hoạt động quay lại nền trước, số lượng Món tráng miệng đã bán sẽ được giữ lại.

  1. Khởi động ít nhất một ứng dụng khác không phải là DessertClicker để thiết bị có một vài ứng dụng trong màn hình gần đây.
  2. Gọi màn hình gần đây lên và mở hoạt động gần đây khác. Sau đó, quay lại các ứng dụng gần đây và đưa DessertClicker trở lại nền trước.

Lưu ý rằng bạn cũng thấy các lệnh gọi lại này trong Logcat tại đây khi nhấn vào nút Home (Màn hình chính). onPause()onStop() được gọi khi ứng dụng chuyển sang chạy ở chế độ nền, sau đó gọi onRestart(), onStart()onResume() khi ứng dụng hoạt động trở lại.

Các phương thức này được gọi khi ứng dụng bị dừng và chuyển sang chạy ở chế độ nền, hoặc khi ứng dụng được khởi động lại khi quay lại trên nền trước. Nếu bạn cần thực hiện một số thao tác trong ứng dụng trong những trường hợp này, thì ghi đè phương thức gọi lại trong vòng đời có liên quan.

Thế còn onRestart() thì sao? Phương thức onRestart() rất giống với onCreate(). Hoặc onCreate() hoặc onRestart() được gọi trước khi hoạt động được hiển thị. Phương thức onCreate() chỉ được gọi lần đầu tiên và onRestart() được gọi sau đó. Phương thức onRestart() là nơi đặt mã mà bạn chỉ muốn gọi ra nếu hoạt động không được khởi động lần đầu tiên.

Trường hợp sử dụng 3: Ẩn một phần hoạt động

Bạn biết rằng khi một ứng dụng được khởi động và onStart() được gọi, ứng dụng này sẽ được hiển thị trên màn hình. Khi ứng dụng được tiếp tục và onResume() được gọi, ứng dụng sẽ thu hút sự chú ý của người dùng, nghĩa là người dùng có thể tương tác với ứng dụng. Một phần của vòng đời, mà trong đó ứng dụng được hiển thị trên toàn màn hình và thu hút sự chú ý của người dùng, được gọi là vòng đời tương tác.

Khi ứng dụng chuyển sang chạy trong chế độ nền, sự chú ý sẽ mất sau onPause() và ứng dụng không còn hiển thị sau onStop() nữa.

Sự khác biệt giữa sự chú ý và chế độ hiển thị là rất quan trọng vì một hoạt động có thể hiển thị một phần trên màn hình, nhưng lại được người dùng chú ý. Ở bước này, bạn xem xét trường hợp trong đó một hoạt động được hiển thị một phần nhưng không được người dùng chú ý.

  1. Khi ứng dụng DessertClicker đang chạy, nhấp vào nút Share (Chia sẻ) ở góc trên bên phải màn hình.

Hoạt động chia sẻ xuất hiện ở nửa dưới của màn hình nhưng hoạt động vẫn hiển thị ở nửa trên.

e2319779260eb5ee.png

9ddc8b1dc79b1bff.png

  1. Kiểm tra Logcat và lưu ý rằng chỉ onPause() mới được gọi.
2020-10-16 11:00:53.857 22064-22064/com.example.android.dessertclicker D/MainActivity: onPause Called

Trong trường hợp sử dụng này, onStop() không được gọi vì hoạt động vẫn chỉ được hiển thị một phần. Tuy nhiên, hoạt động không được người dùng chú ý và người dùng không thể tương tác với hoạt động đó. Hoạt động "chia sẻ" ở nền trước (foreground) có được sự chú ý của người dùng.

Tại sao sự khác biệt này lại quan trọng? Quá trình gián đoạn với riêng onPause() thường kéo dài trong một thời gian ngắn trước khi quay lại hoạt động của bạn hoặc chuyển đến hoạt động hay ứng dụng khác. Thông thường, bạn muốn tiếp tục cập nhật giao diện người dùng để phần còn lại của ứng dụng không tỏ ra bị treo.

Dù mã nào chạy trong onPause() thì đều chặn nội dung khác hiển thị, vì vậy, hãy bảo đảm mã trong onPause() có kích thước nhỏ. Ví dụ: nếu có cuộc gọi điện thoại đến, mã trong onPause() có thể trì hoãn thông báo cuộc gọi đến.

  1. Nhấp vào bên ngoài hộp thoại chia sẻ để quay lại ứng dụng và để ý rằng onResume() được gọi.

Cả onResume()onPause() đều liên quan đến sự chú ý. Phương thức onResume() được gọi khi hoạt động được chú ý và onPause() được gọi khi hoạt động mất đi sự chú ý.

5. Khám phá thay đổi về cấu hình

Có một trường hợp khác bạn cần nắm vững khi quản lý vòng đời hoạt độ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.

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 và độ dài chuỗi. 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 một 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. Cùng xem cách ứng dụng hoạt động trong tình huống này.

Mất dữ liệu khi xoay thiết bị

  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). 623fce7c623d42bd.png
  3. Kiểm tra kết quả đầu ra trong Logcat. Lọc kết quả đầu ra trên MainActivity.
2020-10-16 11:03:09.618 23206-23206/com.example.android.dessertclicker D/MainActivity: onCreate Called
2020-10-16 11:03:09.806 23206-23206/com.example.android.dessertclicker D/MainActivity: onStart Called
2020-10-16 11:03:09.808 23206-23206/com.example.android.dessertclicker D/MainActivity: onResume Called
2020-10-16 11:03:24.488 23206-23206/com.example.android.dessertclicker D/MainActivity: onPause Called
2020-10-16 11:03:24.490 23206-23206/com.example.android.dessertclicker D/MainActivity: onStop Called
2020-10-16 11:03:24.493 23206-23206/com.example.android.dessertclicker D/MainActivity: onDestroy Called
2020-10-16 11:03:24.520 23206-23206/com.example.android.dessertclicker D/MainActivity: onCreate Called
2020-10-16 11:03:24.569 23206-23206/com.example.android.dessertclicker D/MainActivity: onStart Called

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.

  1. Khi xoay thiết bị, hoạt động bị tắt và tạo lại, hoạt động khởi động với các giá trị mặc định, số lượng món tráng miệng đã bán và doanh thu được đặt lại bằng không.

Sử dụng onSaveInstanceState() để lưu dữ liệu gói

Phương thức onSaveInstanceState() là lệnh gọi lại được bạn sử dụng để lưu mọi dữ liệu cần thiết nếu Activity bị huỷ. Trong sơ đồ phương thức 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.

c259ab6beca0ca88.png

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ệ thống 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, ghi đè lệnh gọi lại onSaveInstanceState() và thêm một câu lệnh nhật ký.
override fun onSaveInstanceState(outState: Bundle) {
   super.onSaveInstanceState(outState)

   Log.d(TAG, "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():
2020-10-16 11:05:21.726 23415-23415/com.example.android.dessertclicker D/MainActivity: onPause Called
2020-10-16 11:05:22.382 23415-23415/com.example.android.dessertclicker D/MainActivity: onStop Called
2020-10-16 11:05:22.393 23415-23415/com.example.android.dessertclicker D/MainActivity: onSaveInstanceState Called
  1. Ở đầ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"

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. Bundle 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 dữ liệu đơn giản, 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 bộ nhớ, 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ị. Nếu lưu trữ quá nhiều dữ liệu, ứng dụng của bạn có thể gặp sự cố với TransactionTooLargeException.
  2. Trong onSaveInstanceState(), đặ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 cùng quy trình với số lượng món tráng miệng đã bán:
outState.putInt(KEY_DESSERT_SOLD, dessertsSold)

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

Có thể khôi phục trạng thái Hoạt động trong onCreate(Bundle) hoặc onRestoreInstanceState(Bundle) (Bundle được onSaveInstanceState() điền sẵn sẽ chuyển vào cả hai phương thức gọi lại trong vòng đờ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, Bundle 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(), ngay sau khi đặt biến binding:
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 doanh thu và số lượng món tráng miệng đã bán.
if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
   dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 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.
  2. Xoay thiết bị. Lưu ý rằng lần này ứng dụng hiển thị chính xác doanh thu và món tráng miệng được bán 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. 4179956182ffc634.png

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.

  1. 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 mà khôi phục trạng thái từ gói, gọi showCurrentDessert():
 if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
   dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
   showCurrentDessert()
}
  1. Biên dịch và chạy ứng dụng cũng như xoay màn hình. 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.

6. Tóm tắt

Vòng đời hoạt động

  • Vòng đời hoạt động là một tập hợp các trạng thái mà qua đó một hoạt động di chuyển. Vòng đời hoạt động bắt đầu khi hoạt động được tạo lần đầu tiên và kết thúc khi hoạt động bị huỷ.
  • Khi người dùng di chuyển giữa các hoạt động, bên trong và bên ngoài ứng dụng, mỗi hoạt động sẽ di chuyển giữa các trạng thái trong vòng đời hoạt động.
  • Mỗi trạng thái trong vòng đời hoạt động đều có một phương thức gọi lại tương ứng mà bạn có thể ghi đè trong lớp Activity. Tập hợp cốt lõi các phương thức vòng đời là: onCreate()onStart()onPause()onRestart()onResume()onStop()onDestroy()
  • Để thêm hành vi xảy ra khi hoạt động chuyển đổi sang trạng thái vòng đời, ghi đè phương thức gọi lại của trạng thái.
  • Để thêm phương thức ghi đè nòng cốt vào các lớp của bạn trong Android Studio, chọn Code (Mã) > Override Methods (Phương thức ghi đè) hoặc nhấn vào Control+o.

Ghi nhật ký bằng Nhật ký

  • API ghi nhật ký Android, đặc biệt là lớp Log, cho phép bạn viết các thông báo ngắn được hiển thị trong Logcat bên trong Android Studio.
  • Sử dụng Log.d() để viết thông báo gỡ lỗi. Phương thức này sẽ nhận hai đối số: thẻ nhật ký, thường là tên lớp và nhật ký thông báo, thường là một chuỗi ngắn.
  • Sử dụng cửa sổ Logcat trong Android Studio để xem nhật ký hệ thống, bao gồm cả các thông báo bạn viết.

Bảo tồn trạng thái hoạt động

  • Khi ứng dụng chạy ở chế độ nền, ngay sau khi onStop() được gọi, dữ liệu ứng dụng có thể đượ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 savedInstanceStatenull, 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à huỷ 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, lưu trạng thái của ứng dụng vào gói trong onSaveInstanceState().

7. Tìm hiểu thêm