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

TabLayoutMediator attached before ViewPager2 has an adapter

Error message

TabLayoutMediator attached before ViewPager2 has an adapter

What it means

TabLayoutMediator.attach() reads viewPager.getAdapter() and throws IllegalStateException when it is null, because the mediator's callbacks and tab population are driven entirely by adapter callbacks. The doc contract states attach() must run after ViewPager2 has an adapter set.

Source

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

    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
    if (autoRefresh) {
      // Register our observer on the new adapter
      pagerAdapterObserver = new PagerAdapterObserver();
      adapter.registerAdapterDataObserver(pagerAdapterObserver);

View on GitHub (pinned to ac7e18efee)

Solutions

  1. Set the adapter first: viewPager2.setAdapter(adapter); then mediator.attach().
  2. If the adapter arrives asynchronously, create and attach the mediator only after the adapter is set (e.g. inside the observer that delivers the adapter).
  3. Alternatively use RecyclerView.Adapter state restoration carefully: keep mediator creation lazy until both pager and adapter are ready.

Example fix

// before
new TabLayoutMediator(tabLayout, viewPager2, true, configurator).attach();
viewPager2.setAdapter(adapter); // attach ran too early

// after
viewPager2.setAdapter(adapter);
new TabLayoutMediator(tabLayout, viewPager2, true, configurator).attach();
Defensive patterns

Strategy: validation

Validate before calling

if (viewPager2.getAdapter() != null) {
  new TabLayoutMediator(tabLayout, viewPager2, configurator).attach();
} else {
  // set adapter first, or defer attach until the adapter is delivered
}

Prevention

When it happens

Trigger: Calling new TabLayoutMediator(tabLayout, viewPager2, ...).attach() before viewPager2.setAdapter(adapter).

Common situations: Adapter set asynchronously (loaded from a ViewModel LiveData whose observer fires after attach); initialization-order mistakes in onViewCreated; adapter assignment behind a data-load callback while attach ran eagerly.

Related errors


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