Save the date! Android Dev Summit is coming to Mountain View, CA on November 7-8, 2018.

Xử lý Thay đổi Thời gian chạy

Một số cấu hình thiết bị có thể thay đổi trong thời gian chạy (chẳng hạn như hướng màn hình, sự sẵn có của bàn phím, và ngôn ngữ). Khi sự thay đổi như vậy diễn ra, Android sẽ khởi động lại việc chạy Activity (onDestroy() sẽ được gọi, sau đó là onCreate()). Hành vi khởi động lại được thiết kế để giúp ứng dụng điều chỉnh phù hợp với cấu hình mới bằng cách tự động tải lại ứng dụng của bạn bằng các tài nguyên thay thế khớp với cấu hình thiết bị mới.

Để xử lý khởi động lại cho đúng, điều quan trọng là hoạt động của bạn khôi phục lại trạng thái trước đó của nó thông qua vòng đời Hoạt động thông thường, trong đó Android sẽ gọi onSaveInstanceState() trước khi nó hủy hoạt động của bạn sao cho bạn có thể lưu dữ liệu về trạng thái của ứng dụng. Khi đó, bạn có thể khôi phục trạng thái trong khi onCreate() hoặc onRestoreInstanceState().

Để kiểm tra xem ứng dụng của bạn có tự khởi động lại mà giữ nguyên trạng thái ứng dụng hay không, bạn cần gọi ra các thay đổi cấu hình (chẳng hạn như thay đổi hướng màn hình) trong khi thực hiện các tác vụ khác nhau trong ứng dụng của bạn. Ứng dụng của bạn sẽ có thể khởi động lại vào bất cứ lúc nào mà không bị mất dữ liệu của người dùng hay trạng thái để xử lý các sự kiện như thay đổi cấu hình hoặc khi người dùng nhận được một cuộc gọi đến rồi quay lại ứng dụng của bạn muộn hơn nhiều sau khi tiến trình ứng dụng của bạn có thể đã bị hủy. Để tìm hiểu về cách bạn có thể khôi phục trạng thái hoạt động của mình, hãy đọc về Vòng đời của hoạt động.

Tuy nhiên, bạn có thể gặp phải một tình huống trong đó việc khởi động lại ứng dụng của bạn và khôi phục phần lớn dữ liệu có thể tốn kém và tạo nên trải nghiệm người dùng kém. Trong tình huống như vậy, bạn có hai tùy chọn:

  1. Giữ lại một đối tượng trong khi thay đổi cấu hình

    Cho phép hoạt động của bạn khởi động lại khi cấu hình thay đổi, nhưng mang theo một đối tượng có trạng thái tới thực thể mới của hoạt động của bạn.

  2. Tự mình xử lý thay đổi cấu hình

    Ngăn không cho hệ thống khởi động lại hoạt động của bạn trong những thay đổi cấu hình nhất định, nhưng nhận một lệnh gọi lại khi cấu hình thay đổi, sao cho bạn có thể cập nhật thủ công hoạt động của mình nếu cần.

Giữ lại một Đối tượng trong khi Thay đổi Cấu hình

Nếu việc khởi động lại hoạt động của bạn yêu cầu bạn phải khôi phục nhiều tập hợp dữ liệu lớn, hãy thiết lập lại kết nối mạng, hoặc thực hiện các thao tác tăng cường khác, khi đó khởi động lại hoàn toàn do thay đổi cấu hình có thể gây ra trải nghiệm người dùng chậm chạp. Đồng thời, có thể bạn sẽ không thể hoàn toàn khôi phục được trạng thái hoạt động của mình với Bundle mà hệ thống lưu cho bạn bằng phương pháp gọi lại onSaveInstanceState()—nó không được thiết kế để mang các đối tượng lớn (chẳng hạn như bitmap) và dữ liệu trong nó phải được nối tiếp hóa rồi bỏ nối tiếp hóa, điều này có thể tiêu tốn nhiều bộ nhớ và khiến việc thay đổi cấu hình diễn ra chậm. Trong một tình huống như vậy, bạn có thể gỡ bỏ gánh nặng khởi tạo lại hoạt động của mình bằng cách giữ lại Fragment khi hoạt động của bạn được khởi động lại do thay đổi cấu hình. Phân đoạn này có thể chứa các tham chiếu tới đối tượng có trạng thái mà bạn muốn giữ lại.

Khi hệ thống Android tắt hoạt động của bạn do một thay đổi cấu hình, các phân đoạn của hoạt động mà bạn đã đánh dấu để giữ lại sẽ không bị hủy. Bạn có thể thêm các phân đoạn này vào hoạt động của mình để giữ lại các đối tượng có trạng thái.

Để giữ lại các đối tượng có trạng thái trong một phân đoạn trong khi thay đổi cấu hình thời gian chạy:

  1. Mở rộng lớp Fragment và khai báo các tham chiếu tới đối tượng có trạng thái của bạn.
  2. Gọi setRetainInstance(boolean) khi phân đoạn được tạo.
  3. Thêm phân đoạn vào hoạt động của bạn.
  4. Sử dụng FragmentManager để truy xuất phân đoạn khi hoạt động được khởi động lại.

Ví dụ, định nghĩa phân đoạn của bạn như sau:

public class RetainedFragment extends Fragment {

    // data object we want to retain
    private MyDataObject data;

    // this method is only called once for this fragment
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // retain this fragment
        setRetainInstance(true);
    }

    public void setData(MyDataObject data) {
        this.data = data;
    }

    public MyDataObject getData() {
        return data;
    }
}

Chú ý: Trong khi bạn có thể lưu trữ bất kỳ đối tượng nào, bạn không nên chuyển một đối tượng được gắn với Activity, chẳng hạn như Drawable, Adapter, View hay bất kỳ đối tượng nào khác đi kèm với một Context. Nếu bạn làm vậy, nó sẽ rò rỉ tất cả dạng xem và tài nguyên của thực thể hoạt động gốc. (Rò rỉ tài nguyên có nghĩa là ứng dụng của bạn duy trì việc lưu giữ tài nguyên và chúng không thể được thu dọn bộ nhớ rác, vì thế rất nhiều bộ nhớ có thể bị mất.)

Khi đó, hãy sử dụng FragmentManager để thêm phân đoạn vào hoạt động. Bạn có thể thu được đối tượng dữ liệu từ phân đoạn khi hoạt động bắt đầu lại trong khi thay đổi cấu hình thời gian chạy. Ví dụ, định nghĩa hoạt động của bạn như sau:

public class MyActivity extends Activity {

    private RetainedFragment dataFragment;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // find the retained fragment on activity restarts
        FragmentManager fm = getFragmentManager();
        dataFragment = (DataFragment) fm.findFragmentByTag(“data”);

        // create the fragment and data the first time
        if (dataFragment == null) {
            // add the fragment
            dataFragment = new DataFragment();
            fm.beginTransaction().add(dataFragment, “data”).commit();
            // load the data from the web
            dataFragment.setData(loadMyData());
        }

        // the data is available in dataFragment.getData()
        ...
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // store the data in the fragment
        dataFragment.setData(collectMyLoadedData());
    }
}

Trong ví dụ này, onCreate() thêm một phân đoạn hoặc khôi phục một tham chiếu đến nó. onCreate() cũng lưu trữ đối tượng có trạng thái bên trong thực thể phân đoạn đó. onDestroy() cập nhật đối tượng có trạng thái bên trong thực thể phân đoạn được giữ lại.

Tự mình Xử lý Thay đổi Cấu hình

Nếu ứng dụng của bạn không cần cập nhật các tài nguyên trong một thay đổi cấu hình cụ thể bạn có giới hạn về hiệu năng yêu cầu bạn phải tránh khởi động lại hoạt động, khi đó bạn có thể khai báo rằng hoạt động của bạn tự mình xử lý thay đổi cấu hình , làm vậy sẽ tránh cho hệ thống khởi động lại hoạt động của bạn.

Lưu ý: Việc tự mình xử lý thay đổi cấu hình có thể khiến việc sử dụng các tài nguyên thay thế khó khăn hơn nhiều, vì hệ thống không tự động áp dụng chúng cho bạn. Kỹ thuật này nên được coi là giải pháp cuối cùng khi bạn phải tránh khởi động lại do một thay đổi cấu hình và không được khuyến cáo đối với hầu hết ứng dụng.

Để khai báo rằng hoạt động của bạn xử lý một thay đổi cấu hình, hãy chỉnh sửa phần tử <activity> phù hợp trong tệp bản kê khai của bạn để bao gồm thuộc tính android:configChanges với một giá trị có chức năng biểu diễn cấu hình mà bạn muốn xử lý. Các giá trị có thể được liệt kê trong tài liệu dành cho thuộc tính android:configChanges (các giá trị thường được sử dụng nhất là "orientation" để ngăn khởi động lại khi hướng màn hình thay đổi và "keyboardHidden" để ngăn khởi động lại khi tính sẵn có của bàn phím thay đổi). Bạn có thể khai báo nhiều giá trị cấu hình trong thuộc tính bằng cách tách chúng bằng một ký tự | đường dẫn nối.

Ví dụ, đoạn mã bản kê khai sau khai báo một hoạt động có chức năng xử lý cả thay đổi về hướng màn hình và thay đổi về tính sẵn có của bàn phím:

<activity android:name=".MyActivity"
          android:configChanges="orientation|keyboardHidden"
          android:label="@string/app_name">

Lúc này, khi một trong những cấu hình này thay đổi, MyActivity không khởi động lại. Thay vào đó, MyActivity nhận được một lệnh gọi tới onConfigurationChanged(). Phương pháp này được chuyển bởi một đối tượng Configuration mà quy định cấu hình thiết bị mới. Bằng cách đọc các trường trong Configuration, bạn có thể xác định cấu hình mới và thực hiện những thay đổi phù hợp bằng cách cập nhật tài nguyên được sử dụng trong giao diện của bạn. Tại thời điểm phương pháp này được gọi, đối tượng Resources của hoạt động của bạn được cập nhật để trả về các tài nguyên dựa trên cấu hình mới, sao cho bạn có thể dễ dàng đặt lại các phần tử trong UI của mình mà không để hệ thống khởi động lại hoạt động của bạn.

Chú ý: Bắt đầu với Android 3.2 (API mức 13), "kích cỡ màn hình" cũng thay đổi khi thiết bị chuyển giữa hướng dọc và khổ ngang . Vì thế, nếu bạn muốn ngăn cản việc khởi động lại vào thời gian chạy do thay đổi hướng khi phát triển cho API mức 13 hoặc cao hơn (như được khai báo bởi các thuộc tính minSdkVersiontargetSdkVersion ), bạn phải bao gồm giá trị "screenSize" bên cạnh giá trị "orientation". Cụ thể, bạn phải khai báo android:configChanges="orientation|screenSize". Tuy nhiên, nếu ứng dụng của bạn nhắm tới API mức 12 hoặc thấp hơn, khi đó hoạt động của bạn luôn tự mình xử lý thay đổi cấu hình này (thay đổi cấu hình này không khởi động lại hoạt động của bạn, ngay cả khi đang chạy trên một thiết bị phiên bản Android 3.2 hoặc cao hơn).

Ví dụ, việc triển khai onConfigurationChanged() sau sẽ kiểm tra hướng thiết bị hiện tại:

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    // Checks the orientation of the screen
    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
        Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
    }
}

Đối tượng Configuration biểu diễn tất cả cấu hình hiện tại, không chỉ những cấu hình đã thay đổi. Trong phần lớn thời gian, bạn sẽ không quan tâm chính xác xem cấu hình đã thay đổi như thế nào và có thể đơn giản gán lại tất cả tài nguyên của mình với chức năng cung cấp nội dung thay thế cho cấu hình mà bạn đang xử lý. Ví dụ, do đối tượng Resources nay đã được cập nhật, bạn có thể đặt lại bất kỳImageView nào với setImageResource() và tài nguyên phù hợp cho cấu hình mới được sử dụng (như được mô tả trong phần Cung cấp Tài nguyên).

Lưu ý rằng giá trị từ các trường Configuration là những số nguyên khớp với hằng số cụ thể từ lớp Configuration. Đối với tài liệu về những hằng số cần sử dụng với mỗi trường, hãy tham khảo trường phù hợp trong tham chiếu Configuration.

Hãy ghi nhớ: Khi bạn khai báo hoạt động của mình để xử lý một thay đổi cấu hình, bạn có trách nhiệm đặt lại bất kỳ phần tử nào mà bạn cung cấp nội dung thay thế cho. Nếu bạn khai báo hoạt động của mình để xử lý thay đổi hướng và có những hình ảnh nên thay đổi giữa khổ ngang và hướng dọc, bạn phải gán lại từng tài nguyên cho từng phần tử trong khi onConfigurationChanged().

Nếu bạn không cần cập nhật ứng dụng của mình dựa trên những thay đổi cấu hình này, thay vào đó bạn có thể không triển khai onConfigurationChanged(). Trong trường hợp đó, tất cả tài nguyên được sử dụng trước khi thay đổi cấu hình sẽ vẫn được sử dụng và bạn chỉ mới tránh được việc khởi động lại hoạt động của mình. Tuy nhiên, ứng dụng của bạn cần luôn có khả năng tắt và khởi động lại với trạng thái trước đó của nó được giữ nguyên, vì thế bạn không nên coi kỹ thuật này như một cách để thoát khỏi việc giữ lại trạng thái của mình trong vòng đời của hoạt động bình thường. Không chỉ bởi có những thay đổi cấu hình khác mà bạn không thể ngăn không cho khởi động lại ứng dụng của mình, mà cả bởi vì bạn nên xử lý những sự kiện như là khi người dùng rời khỏi ứng dụng của bạn và nó bị hủy trước khi người dùng quay lại.

Để biết thêm về những thay đổi cấu hình nào mà bạn có thể xử lý trong hoạt động của mình, hãy xem tài liệu android:configChanges và lớp Configuration .