片段管理員

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

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

本主題說明如何存取 FragmentManagerFragmentManager 與您的活動和片段有關,請使用 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 的參照後,即可用其操作向使用者顯示的片段。

子項片段

一般來說,應用程式應含有應用程式專案中單一或少量活動,每個活動都代表一組相關畫面。該活動可能會提供放置頂層導覽的位置,以及將 ViewModels 和其他片段狀態劃分到片段之間的某個位置。應用程式中的每個個別到達網頁都應以片段表示。

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

子項片段的其他用途可能包括:

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

使用 FragmentManager

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

使用者在裝置上按下「返回」按鈕時,或呼叫 FragmentManager.popBackStack() 時,最頂端的片段交易會從堆疊中彈出。換句話說,交易會被撤銷。如果堆疊上沒有其他片段交易,而且您不使用子項片段,則返回事件對話框會上升至活動。如果您使用子項片段,請參閱子項與同層級片段的特別注意事項

當您在交易中呼叫 addToBackStack() 時,請注意,交易可以包含任意數量的作業,例如新增多個片段、取代多個容器中的片段等。彈出返回堆疊時,所有這些作業都會撤銷為單一 atomic 操作。如果您在 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() 為片段指派不重複的標記,並取得參照。您可以在版面配置中定義的片段上或在 FragmentTransaction 中的 add()replace() 操作期間使用 android:tag XML 屬性指派標記。

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

在這種情況下,您可以呼叫 saveState() 來儲存這個片段交易和 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 負責顯示您家鄉的熱門甜點。假設 DessertsFragment 依附於 DessertsRepository 類別,該類別會提供必要的資訊,以便向使用者顯示正確的使用者介面。

您可以定義 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
}

如要進一步瞭解這項測試程序和完整範例,請參閱測試應用程式的片段