Trình quản lý mảnh

FragmentManager là lớp chịu trách nhiệm thực hiện hành động trên các mảnh của ứng dụng, ví dụ: thêm, xoá hoặc thay thế mảnh, cũng như thêm mảnh vào ngăn xếp lui.

Bạn không thể tương tác trực tiếp với FragmentManager nếu đang sử dụng thư viện Điều hướng của Jetpack, vì thư viện này hoạt động với FragmentManager thay mặt bạn. Tuy nhiên, bất cứ ứng dụng nào sử dụng mảnh cũng sẽ sử dụng FragmentManager ở mức độ nào đó. Bởi vậy, bạn cần hiểu được khái niệm và cách thức hoạt động của trình quản lý mảnh.

Trang này bao gồm:

  • Cách truy cập FragmentManager.
  • Vai trò của FragmentManager trong các hoạt động và mảnh.
  • Cách quản lý ngăn xếp lui bằng FragmentManager.
  • Cách cung cấp dữ liệu và phần phụ thuộc cho các mảnh.

Truy cập FragmentManager

Bạn có thể truy cập FragmentManager từ một hoạt động hoặc từ một mảnh.

FragmentActivity và các lớp con của nó, chẳng hạn như AppCompatActivity, có quyền truy cập vào FragmentManager thông qua phương thức getSupportFragmentManager().

Các mảnh có thể lưu trữ một hoặc nhiều mảnh con. Bên trong một mảnh, bạn có thể lấy thông tin tham chiếu đến FragmentManager quản lý các mảnh con của mảnh thông qua getChildFragmentManager(). Nếu cần truy cập vào FragmentManager của máy chủ, bạn có thể sử dụng getParentFragmentManager().

Dưới đây là một vài ví dụ để thấy mối quan hệ giữa các mảnh, máy chủ lưu trữ và các thực thể FragmentManager được liên kết với nhau.

hai ví dụ về bố cục của giao diện người dùng cho thấy mối quan hệ giữa mảnh và hoạt động của máy chủ.
Hình 1. Hai ví dụ về bố cục giao diện người dùng cho thấy mối quan hệ giữa mảnh và hoạt động của máy chủ.

Hình 1 cho thấy hai ví dụ, mỗi ví dụ có một máy chủ hoạt động duy nhất. Hoạt động của máy chủ trong cả hai ví dụ này hiển thị thành phần điều hướng cấp cao nhất cho người dùng dưới dạng BottomNavigationView chịu trách nhiệm hoán đổi mảnh máy chủ có nhiều màn hình trong ứng dụng. Mỗi màn hình được triển khai dưới dạng một mảnh riêng.

Mảnh máy chủ trong Ví dụ 1 lưu trữ hai mảnh con tạo thành một màn hình xem phân tách. Mảnh máy chủ trong Ví dụ 2 lưu trữ một mảnh con duy nhất tạo thành mảnh hiển thị của khung hiển thị vuốt.

Với chế độ thiết lập này, bạn có thể coi mỗi máy chủ được liên kết với một FragmentManager có chức năng quản lý các mảnh con. Điều này được minh hoạ trong hình 2, cùng với các liên kết thuộc tính giữa supportFragmentManager, parentFragmentManagerchildFragmentManager.

mỗi máy chủ đều được liên kết với FragmentManager riêng có chức năng quản lý các mảnh con
Hình 2. Mỗi máy chủ đều được liên kết với FragmentManager riêng có chức năng quản lý các mảnh con.

Thuộc tính FragmentManager thích hợp để tham chiếu phụ thuộc vào vị trí gọi trong hệ thống phân cấp mảnh, cũng như trình quản lý mảnh mà bạn đang tìm cách truy cập.

Sau khi có một tham chiếu đến FragmentManager, bạn có thể sử dụng tham chiếu đó để điều khiển các mảnh hiển thị cho người dùng.

Mảnh con

Nói chung, ứng dụng của bạn nên bao gồm một hoặc một số ít hoạt động trong dự án ứng dụng, với mỗi hoạt động đại diện cho một nhóm các màn hình liên quan. Hoạt động có thể cung cấp một điểm để đặt thành phần điều hướng ở cấp cao nhất và một vị trí để thu hẹp phạm vi của ViewModel cũng như trạng thái xem khác giữa các mảnh. Mảnh biểu thị cho một đích đến riêng lẻ trong ứng dụng.

Nếu muốn hiển thị nhiều mảnh cùng một lúc, chẳng hạn như trong thành phần hiển thị phân tách hoặc trang tổng quan, bạn cần sử dụng các mảnh con được mảnh đích quản lý và trình quản lý mảnh con.

Sau đây là các trường hợp sử dụng khác cho mảnh con:

  • Chuyển màn hình, bằng ViewPager2 trong một mảnh mẹ để quản lý một loạt khung hiển thị mảnh con.
  • Điều hướng phụ trong một nhóm các màn hình liên quan.
  • Thư viện Điều hướng của Jetpack sử dụng các mảnh con làm đích đến riêng lẻ. Một hoạt động lưu trữ một NavHostFragment mẹ và lấp đầy không gian bằng nhiều mảnh đích đến con khi người dùng di chuyển trong ứng dụng của bạn.

Sử dụng FragmentManager

FragmentManager quản lý ngăn xếp lui của mảnh. Trong thời gian chạy, FragmentManager có thể thực hiện các thao tác ngăn xếp lui như thêm hoặc xoá các mảnh để phản hồi tương tác của người dùng. Mỗi tập hợp thay đổi được xác nhận cùng nhau dưới dạng một đơn vị duy nhất gọi là FragmentTransaction. Để thảo luận sâu hơn về giao dịch mảnh, hãy xem hướng dẫn giao dịch mảnh.

Khi người dùng nhấn vào nút "Quay lại" trên thiết bị hoặc khi bạn gọi FragmentManager.popBackStack(), giao dịch mảnh ở trên cùng sẽ bật ra khỏi ngăn xếp. Nếu không có thêm giao dịch mảnh nào trong ngăn xếp và nếu bạn không sử dụng các mảnh con, thì sự kiện quay lại sẽ xuất hiện trong hoạt động. Nếu bạn đang sử dụng các mảnh con, hãy xem bài viết những điểm cần đặc biệt cân nhắc đối với mảnh con và mảnh đồng cấp.

Khi bạn gọi addToBackStack() trên một giao dịch, giao dịch đó có thể bao gồm nhiều hoạt động, chẳng hạn như thêm nhiều mảnh hoặc thay thế các mảnh trong nhiều vùng chứa.

Khi ngăn xếp lui được đẩy, tất cả các thao tác này sẽ đảo ngược thành một hành động ở cấp nguyên tử. Tuy nhiên, nếu bạn đã thực hiện các giao dịch bổ sung trước lệnh gọi popBackStack() và nếu bạn không sử dụng addToBackStack() cho giao dịch này, thì các thao tác này sẽ không đảo ngược. Do đó, trong một FragmentTransaction, hãy tránh xen kẽ các giao dịch ảnh hưởng đến ngăn xếp lui với những giao dịch không ảnh hưởng đến ngăn xếp lui.

Thực hiện giao dịch

Để hiển thị một mảnh trong vùng chứa bố cục, hãy sử dụng FragmentManager để tạo một FragmentTransaction. Sau đó, trong giao dịch, bạn có thể thực hiện thao tác add() hoặc replace() trên vùng chứa.

Ví dụ: một mã FragmentTransaction đơn giản có thể trông như sau:

Kotlin

supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container)
   setReorderingAllowed(true)
   addToBackStack("name") // Name can be null
}

Java

FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, ExampleFragment.class, null)
    .setReorderingAllowed(true)
    .addToBackStack("name") // Name can be null
    .commit();

Trong ví dụ này, ExampleFragment sẽ thay thế mảnh (nếu có) hiện ở trong vùng chứa bố cục được xác định bằng mã R.id.fragment_container. Việc cung cấp lớp của mảnh cho phương thức replace() cho phép FragmentManager xử lý việc tạo thực thể bằng cách sử dụng FragmentFactory. Để biết thêm thông tin, hãy xem phần Cung cấp các phần phụ thuộc cho mảnh.

setReorderingAllowed(true) tối ưu hoá các thay đổi về trạng thái của các mảnh có liên quan trong giao dịch để các ảnh động và hiệu ứng chuyển tiếp hoạt động chính xác. Để biết thêm thông tin về cách di chuyển bằng ảnh động và hiệu ứng chuyển tiếp, hãy xem Giao dịch mảnhDi chuyển giữa các mảnh bằng ảnh động.

Việc gọi addToBackStack() sẽ xác nhận giao dịch vào ngăn xếp lui. Sau đó, người dùng có thể đảo ngược giao dịch và khôi phục mảnh trước đó bằng cách nhấn nút Quay lại. Nếu bạn đã thêm hoặc xoá nhiều mảnh trong một giao dịch, thì tất cả các thao tác đó sẽ bị huỷ khi ngăn xếp lui bị đẩy ra. Tên tuỳ chọn đã cung cấp trong lệnh gọi addToBackStack() cho phép bạn quay lại giao dịch cụ thể đó bằng popBackStack().

Nếu bạn không gọi addToBackStack() khi thực hiện giao dịch xoá một mảnh, thì mảnh đã xoá sẽ bị huỷ khi xác nhận giao dịch và người dùng sẽ không thể quay lại mảnh đó. Nếu bạn gọi addToBackStack() khi xoá một mảnh, thì mảnh đó sẽ chỉ STOPPED và sau đó sẽ được RESUMED khi người dùng quay lại. Khung hiển thị của mảnh này bị phá huỷ trong trường hợp này. Để biết thêm thông tin, hãy xem bài viết Vòng đời của mảnh.

Tìm một mảnh hiện có

Bạn có thể lấy thông tin tham chiếu đến mảnh hiện tại trong vùng chứa bố cục bằng cách sử dụng findFragmentById(). Hãy sử dụng findFragmentById() để tìm một mảnh theo mã nhận dạng nhất định khi được tăng cường từ XML hoặc theo mã vùng chứa khi được thêm vào một FragmentTransaction. Sau đây là ví dụ:

Kotlin

supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container)
   setReorderingAllowed(true)
   addToBackStack(null)
}
...
val fragment: ExampleFragment =
        supportFragmentManager.findFragmentById(R.id.fragment_container) as ExampleFragment

Java

FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, ExampleFragment.class, null)
    .setReorderingAllowed(true)
    .addToBackStack(null)
    .commit();
...
ExampleFragment fragment =
        (ExampleFragment) fragmentManager.findFragmentById(R.id.fragment_container);

Ngoài ra, bạn có thể gán một thẻ duy nhất cho một mảnh và lấy thông tin tham chiếu bằng findFragmentByTag(). Bạn có thể gán một thẻ bằng thuộc tính XML android:tag trên các mảnh được xác định trong bố cục hoặc trong khi thực hiện thao tác add() hay replace() trong một FragmentTransaction.

Kotlin

supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container, "tag")
   setReorderingAllowed(true)
   addToBackStack(null)
}
...
val fragment: ExampleFragment =
        supportFragmentManager.findFragmentByTag("tag") as ExampleFragment

Java

FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, ExampleFragment.class, null, "tag")
    .setReorderingAllowed(true)
    .addToBackStack(null)
    .commit();
...
ExampleFragment fragment = (ExampleFragment) fragmentManager.findFragmentByTag("tag");

Các điểm cần đặc biệt lưu ý đối với mảnh con và mảnh đồng cấp

Chỉ một FragmentManager có thể kiểm soát ngăn xếp lui của mảnh tại một thời điểm bất kỳ. Nếu ứng dụng của bạn hiển thị nhiều mảnh đồng cấp trên màn hình cùng một lúc hoặc dùng các mảnh con, thì bạn cần chỉ định một FragmentManager để xử lý hoạt động điều hướng chính trong ứng dụng.

Để xác định mảnh điều hướng chính bên trong một giao dịch mảnh, hãy gọi phương thức setPrimaryNavigationFragment() trên giao dịch, chuyển vào thực thể của mảnh mà childFragmentManager của mảnh đó có quyền kiểm soát chính.

Xem cấu trúc điều hướng là một chuỗi các lớp, với hoạt động là lớp ngoài cùng, bọc từng lớp mảnh con bên dưới. Mỗi lớp có một mảnh điều hướng chính duy nhất.

Khi sự kiện Quay lại xuất hiện, lớp bên trong cùng sẽ kiểm soát hành vi di chuyển. Khi lớp trong cùng không còn giao dịch mảnh nào xuất hiện từ đó, quyền kiểm soát quay lại lớp bên ngoài tiếp theo, quy trình này lặp lại cho đến khi bạn truy cập vào hoạt động.

Khi hai hoặc nhiều mảnh hiển thị cùng một lúc, chỉ một trong số đó là mảnh điều hướng chính. Việc đặt một mảnh làm mảnh điều hướng chính sẽ xoá thông tin chỉ định khỏi mảnh trước đó. Trong ví dụ trước, nếu bạn đặt mảnh chi tiết làm mảnh điều hướng chính, thì thông tin chỉ định của mảnh chính sẽ bị xoá.

Hỗ trợ nhiều ngăn xếp lui

Trong một số trường hợp, ứng dụng của bạn có thể cần hỗ trợ nhiều ngăn xếp lui. Một ví dụ phổ biến là trường hợp ứng dụng sử dụng thanh điều hướng ở dưới cùng. FragmentManager cho phép bạn hỗ trợ nhiều ngăn xếp lui bằng phương thức saveBackStack()restoreBackStack(). Các phương thức này cho phép bạn hoán đổi giữa các ngăn xếp lui bằng cách lưu một ngăn xếp lui và khôi phục một ngăn xếp lui khác.

saveBackStack() hoạt động tương tự như cách gọi popBackStack() bằng tham số name tuỳ chọn: giao dịch đã chỉ định và tất cả các giao dịch sau giao thức này trên ngăn xếp sẽ được đẩy ra. Điểm khác biệt là saveBackStack() lưu trạng thái của tất cả các mảnh trong các giao dịch đã được đẩy ra.

Ví dụ: giả sử trước đây bạn đã thêm một mảnh vào ngăn xếp lui bằng cách xác nhận một FragmentTransaction bằng addToBackStack(), như trong ví dụ sau:

Kotlin

supportFragmentManager.commit {
  replace<ExampleFragment>(R.id.fragment_container)
  setReorderingAllowed(true)
  addToBackStack("replacement")
}

Java

supportFragmentManager.beginTransaction()
  .replace(R.id.fragment_container, ExampleFragment.class, null)
  // setReorderingAllowed(true) and the optional string argument for
  // addToBackStack() are both required if you want to use saveBackStack()
  .setReorderingAllowed(true)
  .addToBackStack("replacement")
  .commit();

Trong trường hợp đó, bạn có thể lưu giao dịch mảnh này và trạng thái của ExampleFragment bằng cách gọi saveBackStack():

Kotlin

supportFragmentManager.saveBackStack("replacement")

Java

supportFragmentManager.saveBackStack("replacement");

Bạn có thể gọi restoreBackStack() với cùng tham số tên để khôi phục tất cả giao dịch đã được đẩy ra và mọi trạng thái mảnh đã lưu:

Kotlin

supportFragmentManager.restoreBackStack("replacement")

Java

supportFragmentManager.restoreBackStack("replacement");

Cung cấp phần phụ thuộc cho mảnh

Khi thêm một mảnh, bạn có thể tạo bản sao mảnh theo cách thủ công và thêm bản sao mảnh đó vào FragmentTransaction.

Kotlin

fragmentManager.commit {
    // Instantiate a new instance before adding
    val myFragment = ExampleFragment()
    add(R.id.fragment_view_container, myFragment)
    setReorderingAllowed(true)
}

Java

// Instantiate a new instance before adding
ExampleFragment myFragment = new ExampleFragment();
fragmentManager.beginTransaction()
    .add(R.id.fragment_view_container, myFragment)
    .setReorderingAllowed(true)
    .commit();

Khi bạn xác nhận giao dịch mảnh, bản sao của mảnh bạn đã tạo là thực thể được sử dụng. Tuy nhiên, trong quá trình thay đổi cấu hình, hoạt động và tất cả các mảnh của hoạt động sẽ bị huỷ, sau đó được tạo lại bằng các tài nguyên Android thích hợp nhất. FragmentManager xử lý toàn bộ việc này cho bạn: nó tạo lại các thực thể của mảnh, đính kèm các thực thể đó vào máy chủ và tạo lại trạng thái ngăn xếp lui.

Theo mặc định, FragmentManager sử dụng một FragmentFactory mà khung cung cấp để tạo thực thể mới của mảnh. Nhà máy mặc định này sử dụng sự phản chiếu để tìm và gọi một hàm khởi tạo không đối số cho mảnh. Điều này có nghĩa là bạn không thể sử dụng nhà máy mặc định này để cung cấp các phần phụ thuộc cho mảnh. Điều này cũng có nghĩa là mọi hàm khởi tạo tuỳ chỉnh mà bạn sử dụng để tạo mảnh lần đầu sẽ không được dùng trong quá trình tạo lại theo mặc định.

Để cung cấp các phần phụ thuộc cho mảnh hoặc để sử dụng bất kỳ hàm khởi tạo tuỳ chỉnh nào, bạn phải tạo một lớp con FragmentFactory tuỳ chỉnh, sau đó ghi đè FragmentFactory.instantiate. Sau đó, bạn có thể ghi đè factory mặc định của FragmentManager bằng factory tuỳ chỉnh của mình. Sau đó, factory này được dùng để tạo thực thể cho các mảnh.

Giả sử bạn có DessertsFragment chịu trách nhiệm hiển thị các món tráng miệng phổ biến tại quê nhà và DessertsFragment có phần phụ thuộc vào một lớp DessertsRepository giúp cung cấp thông tin mà lớp đó cần để hiển thị đúng giao diện người dùng cho người dùng.

Bạn có thể xác định DessertsFragment để yêu cầu bản sao của DessertsRepository trong hàm khởi tạo.

Kotlin

class DessertsFragment(val dessertsRepository: DessertsRepository) : Fragment() {
    ...
}

Java

public class DessertsFragment extends Fragment {
    private DessertsRepository dessertsRepository;

    public DessertsFragment(DessertsRepository dessertsRepository) {
        super();
        this.dessertsRepository = dessertsRepository;
    }

    // Getter omitted.

    ...
}

Cách triển khai FragmentFactory đơn giản có thể tương tự như sau.

Kotlin

class MyFragmentFactory(val repository: DessertsRepository) : FragmentFactory() {
    override fun instantiate(classLoader: ClassLoader, className: String): Fragment =
            when (loadFragmentClass(classLoader, className)) {
                DessertsFragment::class.java -> DessertsFragment(repository)
                else -> super.instantiate(classLoader, className)
            }
}

Java

public class MyFragmentFactory extends FragmentFactory {
    private DessertsRepository repository;

    public MyFragmentFactory(DessertsRepository repository) {
        super();
        this.repository = repository;
    }

    @NonNull
    @Override
    public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {
        Class<? extends Fragment> fragmentClass = loadFragmentClass(classLoader, className);
        if (fragmentClass == DessertsFragment.class) {
            return new DessertsFragment(repository);
        } else {
            return super.instantiate(classLoader, className);
        }
    }
}

Ví dụ này phân lớp FragmentFactory, ghi đè phương thức instantiate() để cung cấp logic tạo mảnh tuỳ chỉnh cho DessertsFragment. Các lớp mảnh khác được xử lý bởi hành vi mặc định của FragmentFactory thông qua super.instantiate().

Sau đó, bạn có thể chỉ định MyFragmentFactory làm nhà máy để sử dụng khi tạo các mảnh của ứng dụng bằng cách đặt một thuộc tính trên FragmentManager. Bạn phải đặt thuộc tính này trước super.onCreate() của hoạt động để đảm bảo rằng MyFragmentFactory được sử dụng khi tạo lại các mảnh.

Kotlin

class MealActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager.fragmentFactory = MyFragmentFactory(DessertsRepository.getInstance())
        super.onCreate(savedInstanceState)
    }
}

Java

public class MealActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        DessertsRepository repository = DessertsRepository.getInstance();
        getSupportFragmentManager().setFragmentFactory(new MyFragmentFactory(repository));
        super.onCreate(savedInstanceState);
    }
}

Việc đặt FragmentFactory trong hoạt động sẽ ghi đè hoạt động tạo mảnh trong hệ phân cấp mảnh của hoạt động. Nói cách khác, childFragmentManager của bất kỳ mảnh con nào mà bạn thêm vào sẽ sử dụng factory mảnh tuỳ chỉnh được đặt ở đây trừ trường hợp bị ghi đè ở cấp thấp hơn.

Kiểm thử với FragmentFactory

Trong một cấu trúc hoạt động đơn lẻ, hãy kiểm thử các mảnh để tách biệt bằng cách sử dụng lớp FragmentScenario. Vì không thể dựa vào logic onCreate tuỳ chỉnh của hoạt động, nên bạn có thể chuyển FragmentFactory dưới dạng một đối số cho phần kiểm thử mảnh của mình, như minh hoạ trong ví dụ sau:

// Inside your test
val dessertRepository = mock(DessertsRepository::class.java)
launchFragment<DessertsFragment>(factory = MyFragmentFactory(dessertRepository)).onFragment {
    // Test Fragment logic
}

Để biết thông tin chi tiết về quy trình kiểm thử này và xem các ví dụ đầy đủ, vui lòng xem bài viết Kiểm thử các mảnh.