片段管理員

FragmentManager 是負責對應用程式片段執行操作的類別,例如新增、移除或取代這些片段,並將其新增至返回堆疊。

如果您採用 Jetpack 導覽 程式庫,請務必和 FragmentManager 搭配使用,否則可能無法直接與 FragmentManager 互動。不過,使用片段的任何應用程式在某些層級都使用 FragmentManager,因此請務必瞭解什麼是程式碼和運作方式。

本頁面將說明:

  • 如何存取 FragmentManager
  • 與活動和片段相關的 FragmentManager 角色。
  • 如何使用 FragmentManager 管理返回堆疊。
  • 如何為片段提供資料和依附元件。

存取 FragmentManager

您可以透過活動或片段存取 FragmentManager

FragmentActivity 及其子類別 (例如 AppCompatActivity) 都能透過 getSupportFragmentManager() 方法存取 FragmentManager

片段可代管一或多個子項片段。在片段中,您可以透過 getChildFragmentManager() 取得用來管理片段子項的 FragmentManager 參照。如果您需要存取其主機 FragmentManager,可以使用 getParentFragmentManager()

以下列舉幾個範例,用來說明片段、片段的主機,以及與每個片段相關聯的 FragmentManager 例項彼此的關係。

兩個 UI 版面配置範例,顯示片段及其主機活動之間的關係
圖 1 兩個 UI 版面配置範例,顯示片段與主機活動之間的關係。

圖 1 顯示兩個範例,每個範例都只有單一活動主機。這兩個示例中的主機活動都會以 BottomNavigationView 的形式向使用者顯示頂層導覽,該元素負責在應用程式中使用不同的螢幕畫面換出主機片段,每個畫面都會實作為獨立的片段。

範例 1 中的主機片段代管兩個子項片段,兩個片段組成分割檢視螢幕畫面。範例 2 中的主機片段代管一個子項片段,該片段組成滑動檢視畫面的顯示片段。

透過這項設定,您可以將每個主機視為皆具備與其相關聯的 FragmentManager,用於管理主機的子項片段。圖 2 顯示了相關說明,以及 supportFragmentManagerparentFragmentManagerchildFragmentManager 之間的屬性對應。

每個主機都有專屬的 FragmentManager,用於管理其子項片段
圖 2. 每個主機都有專屬的 FragmentManager,且用於管理子項片段。

應參照的 FragmentManager 屬性取決於呼叫網站在片段階層中的位置,以及您嘗試存取的片段管理員。

取得 FragmentManager 的參照後,即可用其操作向使用者顯示的片段。

子項片段

一般而言,應用程式是由應用程式專案中的單一或少量活動組成,每個活動都代表一組相關的螢幕畫面。該活動可能會提供一個放置頂層導覽的位置,並提供另一個位置來限制 ViewModel 物件和片段之間其他檢視畫面狀態的範圍。片段代表應用程式中的個別目的地。

如果您想一次顯示多個片段 (例如在分割檢視畫面或資訊主頁中顯示),可以使用由目的地片段及其子項片段管理員管理的子項片段。

子項片段的其他用途如下:

  • 螢幕滑動:使用父項片段中的 ViewPager2,用於管理一系列子項片段檢視畫面。
  • 在一組相關的畫面中進行附屬導覽。
  • Jetpack 導航使用子項片段做為個別到達網頁。活動代管單一父項 NavHostFragment,並在使用者瀏覽應用程式時,填入不同的子項到達網頁片段。

使用 FragmentManager

FragmentManager 會管理片段返回堆疊。FragmentManager 可以在執行階段執行返回堆疊,例如新增或移除片段,以回覆使用者互動。每一組異動都會以一個單位 (稱為 FragmentTransaction) 的形式提交。如要深入瞭解片段交易,請參閱片段交易指南

當使用者輕觸裝置上的返回按鈕,或呼叫 FragmentManager.popBackStack() 時,最頂端的片段交易會從堆疊中彈出。如果堆疊上沒有其他片段交易,且您未使用子項片段,則返回事件會向上傳遞至活動。如果您「有」使用子項片段,請參閱「子項片段和同層級片段的特別注意事項

當您在交易中呼叫 addToBackStack() 時,交易可以包含任意數量的作業,例如新增多個片段,或取代多個容器中的片段

彈出返回堆疊時,所有作業都會撤銷,成為不可中斷的單項作業。不過,如果您在呼叫 popBackStack() 前提交了其他交易,且您「並未」對交易使用 addToBackStack(),則這些操作「不會」撤銷。因此,在單一 FragmentTransaction 中,請避免將會影響返回堆疊的交易,以及不會影響返回堆疊的交易交錯處理。

執行交易

如要在版面配置容器中顯示片段,請使用 FragmentManager 來建立 FragmentTransaction。然後,您可以在交易中對容器執行 add()replace() 作業。

舉例來說,簡易的 FragmentTransaction 可能如下所示:

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();

在這個範例中,ExampleFragment 會取代目前由 R.id.fragment_container ID 識別的版面配置容器中的片段 (如有)。將片段的類別提供給 replace() 方法,可讓 FragmentManager 使用其 FragmentFactory 處理例項化。詳情請參閱「為片段提供依附元件」一節。

setReorderingAllowed(true) 會最佳化交易中片段的狀態變更,因此動畫和轉場可以正常運作。如要進一步瞭解如何使用動畫和轉場效果,請參閱片段交易使用動畫瀏覽各個片段

呼叫 addToBackStack() 會將交易傳輸到返回堆疊。使用者稍後可以輕觸「返回」按鈕來撤銷交易,恢復前一個片段。如果您在單一交易中新增或移除了多個片段,彈出返回堆疊時,所有這些操作都會撤銷。addToBackStack() 呼叫中提供的選用名稱可讓您使用 popBackStack(),彈回至該特定交易。

如果您在執行移除片段的交易時呼叫 addToBackStack(),則在進行交易時,系統會刪除已移除的片段,且使用者無法返回。如果您在移除片段時呼叫 addToBackStack(),則該片段只有 STOPPED,而使用者返回時,其片段為 RESUMED。在這種情況下,該檢視畫面會遭到刪除。詳情請參閱片段生命週期一文。

找出現有片段

您可以使用 findFragmentById() 取得版面配置容器中目前片段的參照。使用 XML 時,請使用 findFragmentById() 查詢指定 ID 的片段;如要新增標記,請在 FragmentTransaction 中加入容器 ID。範例如下:

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);

或者,您也可以使用 findFragmentByTag() 為片段指派不重複的標記,並取得參照。您可以在版面配置中定義的片段上使用 android:tag XML 屬性指派標記,或在 FragmentTransaction 中的 add()replace() 操作期間進行這項作業。

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");

子項和同層片段的特別注意事項

在任何特定時間內,只有一個 FragmentManager 可以控製片段返回堆疊。如果應用程式同時在螢幕畫面上顯示多個同層級片段,或使用子項片段,則須指定一個 FragmentManager 來處理應用程式的主要導覽。

如要定義片段交易中的主要導覽片段,請在交易中呼叫 setPrimaryNavigationFragment() 方法,並傳入一個片段的例項,該例項的 childFragmentManager 應具有主要控制權。

請將導覽結構視為一系列的圖層,並將活動放置在最外層,最後將每層子項片段納入下方。每個圖層都有一個主要導覽片段。

發生「返回」事件時,最靠近圖層的控制項會控制導覽行為。一旦最內層沒有彈出其他片段交易,控制項就會返回到下一層,並重複此流程,直到您到達活動為止。

同時顯示兩個以上的片段時,只有一個可以是主要導覽片段。如果將片段設為主要導覽片段,系統就會移除先前片段的標示。在上述示例中,如果將詳細資料片段設為主要導覽片段,系統會移除對主要片段的標示。

支援多個返回堆疊

在某些情況下,您的應用程式可能需要支援多個返回堆疊。最常見的例子是應用程式使用底部導覽列。FragmentManager 可讓您使用 saveBackStack()restoreBackStack() 方法支援多個返回堆疊。這些方法可讓您儲存一個返回堆疊並還原不同的堆疊,藉此在返回堆疊間切換。

saveBackStack() 的運作方式與呼叫 popBackStack() 相同,其中包含選用的 name 參數:彈出指定交易和堆疊上之後的所有交易。差別在於 saveBackStack()儲存彈出交易中所有片段的狀態

舉例來說,假設您先前藉由使用 addToBackStack() 提交 FragmentTransaction,將片段新增至返回堆疊,如以下範例所示:

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();

在這種情況下,您可以呼叫 saveBackStack() 來儲存這個片段交易和 ExampleFragment 的狀態:

Kotlin

supportFragmentManager.saveBackStack("replacement")

Java

supportFragmentManager.saveBackStack("replacement");

您可以呼叫名稱相同的 restoreBackStack() 來還原所有彈出交易和所有已儲存的片段狀態:

Kotlin

supportFragmentManager.restoreBackStack("replacement")

Java

supportFragmentManager.restoreBackStack("replacement");

為片段提供依附元件

新增片段時,您可以手動將片段執行個體化,並將其新增至 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();

提交片段交易時,系統便會使用您建立的片段執行個體。但是,在設定變更期間,系統會將您的活動和所有片段都刪除,並重新建立最適用的 Android 資源FragmentManager 會為您處理以下所有操作:重新建立片段的例項、將其連接至主機,並重新建立返回堆疊狀態。

根據預設,FragmentManager 會使用架構提供的 FragmentFactory,將新的片段執行個體執行個體化。這個預設工廠會利用反射來查找並叫用片段的無引數建構函式。也就是說,您無法使用這個預設工廠為片段提供依附元件。這表示在預設情況下,您在首次建立片段時使用的任何自訂建構函式都不會在重新建立期間使用

如要為片段提供依附元件,或是使用任何自訂建構函式,請改為建立自訂 FragmentFactory 子類別,然後覆寫 FragmentFactory.instantiate。接著,您可以使用自訂工廠覆寫 FragmentManager 的預設工廠,然後用它為片段進行例項化。

假設您有負責顯示您家鄉熱門甜點的 DessertsFragment,且 DessertsFragmentDessertsRepository 類別上有依附元件,而該類別會為其提供向使用者顯示正確 UI 所需的資訊。

您可以定義 DessertsFragment,以在其建構函式中使用 DessertsRepository 執行個體。

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.

    ...
}

實作簡單的 FragmentFactory 可能如下所示。

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);
        }
    }
}

這個範例子類別 FragmentFactory 覆寫 instantiate() 方法,以便提供 DessertsFragment 的自訂片段建立邏輯。其他片段類別是透過 super.instantiate()FragmentFactory 的預設行為進行處理。

然後,您可以在 FragmentManager 上設定屬性,將 MyFragmentFactory 指定為用來建構應用程式片段的工廠。您必須在活動的 super.onCreate() 之前設定這個屬性,以確保在重新建立片段時會使用 MyFragmentFactory

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);
    }
}

在活動中設定 FragmentFactory,會覆寫整個活動的片段階層中的片段建立。換句話說,您新增的所有子項片段 childFragmentManager 都會使用在此設定的自訂片段工廠,除非在較低層級覆寫該片段。

使用 FragmentFactory 進行測試

請在單一活動架構中使用 FragmentScenario 類別單獨測試片段。由於您無法依賴活動的自訂 onCreate 邏輯,請改將 FragmentFactory 做為引數傳送至片段測試,如以下範例所示:

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

如要進一步瞭解這項測試程序和完整範例,請參閱「測試片段」。