material-components/material-components-android · error · IllegalStateException

TabLayoutMediator is already attached

Error message

TabLayoutMediator is already attached

What it means

TabLayoutMediator.link a TabLayout with a ViewPager2 and its lifecycle is one-shot: attach() throws IllegalStateException if called when the attached flag is already true. Re-attaching an already-attached mediator would double-register page change callbacks and tab listeners.

Source

Thrown at lib/java/com/google/android/material/tabs/TabLayoutMediator.java:113

      @NonNull TabConfigurationStrategy tabConfigurationStrategy) {
    this.tabLayout = tabLayout;
    this.viewPager = viewPager;
    this.autoRefresh = autoRefresh;
    this.smoothScroll = smoothScroll;
    this.tabConfigurationStrategy = tabConfigurationStrategy;
  }

  /**
   * Link the TabLayout and the ViewPager2 together. Must be called after ViewPager2 has an adapter
   * set. To be called on a new instance of TabLayoutMediator or if the ViewPager2's adapter
   * changes.
   *
   * @throws IllegalStateException If the mediator is already attached, or the ViewPager2 has no
   *     adapter.
   */
  public void attach() {
    if (attached) {
      throw new IllegalStateException("TabLayoutMediator is already attached");
    }
    adapter = viewPager.getAdapter();
    if (adapter == null) {
      throw new IllegalStateException(
          "TabLayoutMediator attached before ViewPager2 has an " + "adapter");
    }
    attached = true;

    // Add our custom OnPageChangeCallback to the ViewPager
    onPageChangeCallback = new TabLayoutOnPageChangeCallback(tabLayout);
    viewPager.registerOnPageChangeCallback(onPageChangeCallback);

    // Now we'll add a tab selected listener to set ViewPager's current item
    onTabSelectedListener = new ViewPagerOnTabSelectedListener(viewPager, smoothScroll);
    tabLayout.addOnTabSelectedListener(onTabSelectedListener);

    // Now we'll populate ourselves from the pager adapter, adding an observer if
    // autoRefresh is enabled

View on GitHub (pinned to ac7e18efee)

Solutions

  1. Create a new TabLayoutMediator in each onViewCreated and attach() once there.
  2. If you must reuse the instance, call detach() before attach() (detach clears listeners and the attached flag).
  3. When only the adapter's data changed, keep the mediator attached and notify the adapter instead of re-attaching.

Example fix

// before
mediator.attach(); // second call after re-create -> throws

// after (onViewCreated)
if (mediator != null) mediator.detach();
mediator = new TabLayoutMediator(tabLayout, viewPager2, configurator);
mediator.attach();
Defensive patterns

Strategy: try-catch

Try / catch

try {
  mediator.attach();
} catch (IllegalStateException e) {
  if (e.getMessage() != null && e.getMessage().contains("already attached")) {
    // harmless duplicate attach — ignore or detach+reattach
  } else throw e;
}

Prevention

When it happens

Trigger: Calling mediator.attach() twice; reusing one TabLayoutMediator field across fragment view recreations without creating a new mediator or calling detach().

Common situations: Fragment onDestroyView detaches (or not) and onViewCreated re-attaches the same stale mediator; adapter refresh code that calls attach() again after data changes instead of notifyDataSetChanged.

Related errors


AI-assisted analysis of material-components/material-components-android@ac7e18efee (2026-08-14). Data as JSON: /api/errors/418e7106ecfdd3b5. Report an issue: GitHub.