片段管理員

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

如果採用 Jetpack Navigation 程式庫,您可能不會與 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 Navigation 使用子項片段做為個別到達網頁。活動代管單一父項 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
}

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