diff --git a/core/core.services.yml b/core/core.services.yml
index aa87929664..1a0c38b2c0 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -472,8 +472,9 @@ services:
   theme.negotiator:
     class: Drupal\Core\Theme\ThemeNegotiator
     arguments: ['@access_check.theme']
+    parent: container.trait
     tags:
-      - { name: service_collector, tag: theme_negotiator, call: addNegotiator }
+      - { name: service_id_collector, tag: theme_negotiator }
   theme.negotiator.default:
     class: Drupal\Core\Theme\DefaultNegotiator
     arguments: ['@config.factory']
diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php
index bb87d5e484..c92cd5dd08 100644
--- a/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php
+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/TaggedHandlersPass.php
@@ -10,24 +10,28 @@
 /**
  * Collects services to add/inject them into a consumer service.
  *
- * This mechanism allows a service to get multiple processor services injected,
- * in order to establish an extensible architecture.
+ * This mechanism allows a service to get multiple processor services or just
+ * their IDs injected, in order to establish an extensible architecture.
  *
- * It differs from the factory pattern in that processors are not lazily
- * instantiated on demand; the consuming service receives instances of all
- * registered processors when it is instantiated. Unlike a factory service, the
- * consuming service is not ContainerAware.
+ * The service collector differs from the factory pattern in that processors are
+ * not lazily instantiated on demand; the consuming service receives instances
+ * of all registered processors when it is instantiated. Unlike a factory
+ * service, the consuming service is not ContainerAware. It differs from regular
+ * service definition arguments (constructor injection) in that a consuming
+ * service MAY allow further processors to be added dynamically at runtime. This
+ * is why the called method (optionally) receives the priority of a processor as
+ * second argument.
  *
- * It differs from plugins in that all processors are explicitly registered by
+ * To lazily instantiate services the service ID collector pattern can be used,
+ * but the consumer service needs to be ContainerAware. As constructor injection
+ * is used, processors cannot be added at runtime via this method. However, a
+ * consuming service could have setter methods to allow runtime additions.
+ *
+ * These differ from plugins in that all processors are explicitly registered by
  * service providers (driven by declarative configuration in code); the mere
  * availability of a processor (cf. plugin discovery) does not imply that a
  * processor ought to be registered and used.
  *
- * It differs from regular service definition arguments (constructor injection)
- * in that a consuming service MAY allow further processors to be added
- * dynamically at runtime. This is why the called method (optionally) receives
- * the priority of a processor as second argument.
- *
  * @see \Drupal\Core\DependencyInjection\Compiler\TaggedHandlersPass::process()
  */
 class TaggedHandlersPass implements CompilerPassInterface {
@@ -35,26 +39,34 @@ class TaggedHandlersPass implements CompilerPassInterface {
   /**
    * {@inheritdoc}
    *
-   * Finds services tagged with 'service_collector', then finds all
-   * corresponding tagged services and adds a method call for each to the
+   * Finds services tagged with 'service_collector' or 'service_id_collector',
+   * then finds all corresponding tagged services.
+   *
+   * The service collector adds a method call for each to the
    * consuming/collecting service definition.
    *
-   * Supported 'service_collector' tag attributes:
+   * The service ID collector will collect an array of service IDs and add them
+   * as a constructor argument.
+   *
+   * Supported tag attributes:
    * - tag: The tag name used by handler services to collect. Defaults to the
    *   service ID of the consumer.
-   * - call: The method name to call on the consumer service. Defaults to
-   *   'addHandler'. The called method receives two arguments:
-   *   - The handler instance as first argument.
-   *   - Optionally the handler's priority as second argument, if the method
-   *     accepts a second parameter and its name is "priority". In any case, all
-   *     handlers registered at compile time are sorted already.
    * - required: Boolean indicating if at least one handler service is required.
    *   Defaults to FALSE.
    *
+   * Additional tag attributes supported by 'service_collector' only:
+   *   - call: The method name to call on the consumer service. Defaults to
+   *     'addHandler'. The called method receives two arguments:
+   *     - The handler instance as first argument.
+   *     - Optionally the handler's priority as second argument, if the method
+   *       accepts a second parameter and its name is "priority". In any case,
+   *       all handlers registered at compile time are sorted already.
+   *
    * Example (YAML):
    * @code
    * tags:
    *   - { name: service_collector, tag: breadcrumb_builder, call: addBuilder }
+   *   - { name: service_id_collector, tag: theme_negotiator }
    * @endcode
    *
    * Supported handler tag attributes:
@@ -77,91 +89,141 @@ class TaggedHandlersPass implements CompilerPassInterface {
   public function process(ContainerBuilder $container) {
     foreach ($container->findTaggedServiceIds('service_collector') as $consumer_id => $passes) {
       foreach ($passes as $pass) {
-        $interface = NULL;
-        $tag = isset($pass['tag']) ? $pass['tag'] : $consumer_id;
-        $method_name = isset($pass['call']) ? $pass['call'] : 'addHandler';
-        $required = isset($pass['required']) ? $pass['required'] : FALSE;
+        $this->processServiceCollectorPass($pass, $consumer_id, $container);
+      }
+    }
+
+    foreach ($container->findTaggedServiceIds('service_id_collector') as $consumer_id => $passes) {
+      foreach ($passes as $pass) {
+        $this->processServiceIdCollectorPass($pass, $consumer_id, $container);
+      }
+    }
+  }
+
+  /**
+   * Processes a service collector service pass.
+   *
+   * @param array $pass
+   *   The service collector pass data.
+   * @param string $consumer_id
+   *   The consumer service ID.
+   * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
+   *   The service container.
+   */
+  protected function processServiceCollectorPass(array $pass, $consumer_id, ContainerBuilder $container) {
+    $tag = isset($pass['tag']) ? $pass['tag'] : $consumer_id;
+    $method_name = isset($pass['call']) ? $pass['call'] : 'addHandler';
+    $required = isset($pass['required']) ? $pass['required'] : FALSE;
+
+    // Determine parameters.
+    $consumer = $container->getDefinition($consumer_id);
+    $method = new \ReflectionMethod($consumer->getClass(), $method_name);
+    $params = $method->getParameters();
 
-        // Determine parameters.
-        $consumer = $container->getDefinition($consumer_id);
-        $method = new \ReflectionMethod($consumer->getClass(), $method_name);
-        $params = $method->getParameters();
+    $interface_pos = 0;
+    $id_pos = NULL;
+    $priority_pos = NULL;
+    $extra_params = [];
+    foreach ($params as $pos => $param) {
+      if ($param->getClass()) {
+        $interface = $param->getClass();
+      }
+      else if ($param->getName() === 'id') {
+        $id_pos = $pos;
+      }
+      else if ($param->getName() === 'priority') {
+        $priority_pos = $pos;
+      }
+      else {
+        $extra_params[$param->getName()] = $pos;
+      }
+    }
+    // Determine the ID.
+
+    if (!isset($interface)) {
+      throw new LogicException(vsprintf("Service consumer '%s' class method %s::%s() has to type-hint an interface.", [
+        $consumer_id,
+        $consumer->getClass(),
+        $method_name,
+      ]));
+    }
+    $interface = $interface->getName();
 
-        $interface_pos = 0;
-        $id_pos = NULL;
-        $priority_pos = NULL;
-        $extra_params = [];
-        foreach ($params as $pos => $param) {
-          if ($param->getClass()) {
-            $interface = $param->getClass();
-          }
-          elseif ($param->getName() === 'id') {
-            $id_pos = $pos;
-          }
-          elseif ($param->getName() === 'priority') {
-            $priority_pos = $pos;
-          }
-          else {
-            $extra_params[$param->getName()] = $pos;
-          }
-        }
-        // Determine the ID.
+    // Find all tagged handlers.
+    $handlers = [];
+    $extra_arguments = [];
+    foreach ($container->findTaggedServiceIds($tag) as $id => $attributes) {
+      // Validate the interface.
+      $handler = $container->getDefinition($id);
+      if (!is_subclass_of($handler->getClass(), $interface)) {
+        throw new LogicException("Service '$id' for consumer '$consumer_id' does not implement $interface.");
+      }
+      $handlers[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
+      // Keep track of other tagged handlers arguments.
+      foreach ($extra_params as $name => $pos) {
+        $extra_arguments[$id][$pos] = isset($attributes[0][$name]) ? $attributes[0][$name] : $params[$pos]->getDefaultValue();
+      }
+    }
 
-        if (!isset($interface)) {
-          throw new LogicException(vsprintf("Service consumer '%s' class method %s::%s() has to type-hint an interface.", [
-            $consumer_id,
-            $consumer->getClass(),
-            $method_name,
-          ]));
-        }
-        $interface = $interface->getName();
+    if ($required && empty($handlers)) {
+      throw new LogicException(sprintf("At least one service tagged with '%s' is required.", $tag));
+    }
 
-        // Find all tagged handlers.
-        $handlers = [];
-        $extra_arguments = [];
-        foreach ($container->findTaggedServiceIds($tag) as $id => $attributes) {
-          // Validate the interface.
-          $handler = $container->getDefinition($id);
-          if (!is_subclass_of($handler->getClass(), $interface)) {
-            throw new LogicException("Service '$id' for consumer '$consumer_id' does not implement $interface.");
-          }
-          $handlers[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
-          // Keep track of other tagged handlers arguments.
-          foreach ($extra_params as $name => $pos) {
-            $extra_arguments[$id][$pos] = isset($attributes[0][$name]) ? $attributes[0][$name] : $params[$pos]->getDefaultValue();
-          }
-        }
-        if (empty($handlers)) {
-          if ($required) {
-            throw new LogicException(sprintf("At least one service tagged with '%s' is required.", $tag));
-          }
-          continue;
-        }
-        // Sort all handlers by priority.
-        arsort($handlers, SORT_NUMERIC);
+    // Sort all handlers by priority.
+    arsort($handlers, SORT_NUMERIC);
 
-        // Add a method call for each handler to the consumer service
-        // definition.
-        foreach ($handlers as $id => $priority) {
-          $arguments = [];
-          $arguments[$interface_pos] = new Reference($id);
-          if (isset($priority_pos)) {
-            $arguments[$priority_pos] = $priority;
-          }
-          if (isset($id_pos)) {
-            $arguments[$id_pos] = $id;
-          }
-          // Add in extra arguments.
-          if (isset($extra_arguments[$id])) {
-            // Place extra arguments in their right positions.
-            $arguments += $extra_arguments[$id];
-          }
-          // Sort the arguments by position.
-          ksort($arguments);
-          $consumer->addMethodCall($method_name, $arguments);
-        }
+    // Add a method call for each handler to the consumer service
+    // definition.
+    foreach ($handlers as $id => $priority) {
+      $arguments = [];
+      $arguments[$interface_pos] = new Reference($id);
+      if (isset($priority_pos)) {
+        $arguments[$priority_pos] = $priority;
       }
+      if (isset($id_pos)) {
+        $arguments[$id_pos] = $id;
+      }
+      // Add in extra arguments.
+      if (isset($extra_arguments[$id])) {
+        // Place extra arguments in their right positions.
+        $arguments += $extra_arguments[$id];
+      }
+      // Sort the arguments by position.
+      ksort($arguments);
+      $consumer->addMethodCall($method_name, $arguments);
+    }
+  }
+
+  /**
+   * Processes a service collector ID service pass.
+   *
+   * @param array $pass
+   *   The service collector pass data.
+   * @param string $consumer_id
+   *   The consumer service ID.
+   * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
+   *   The service container.
+   */
+  protected function processServiceIdCollectorPass(array $pass, $consumer_id, ContainerBuilder $container) {
+    $tag = isset($pass['tag']) ? $pass['tag'] : $consumer_id;
+    $required = isset($pass['required']) ? $pass['required'] : FALSE;
+
+    $consumer = $container->getDefinition($consumer_id);
+
+    // Find all tagged handlers.
+    $handlers = [];
+    foreach ($container->findTaggedServiceIds($tag) as $id => $attributes) {
+      $handlers[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
     }
+
+    if ($required && empty($handlers)) {
+      throw new LogicException(sprintf("At least one service tagged with '%s' is required.", $tag));
+    }
+
+    // Sort all handlers by priority.
+    arsort($handlers, SORT_NUMERIC);
+
+    $consumer->addArgument(array_keys($handlers));
   }
 
 }
diff --git a/core/lib/Drupal/Core/Theme/ThemeNegotiator.php b/core/lib/Drupal/Core/Theme/ThemeNegotiator.php
index c932e89d53..78e68ca305 100644
--- a/core/lib/Drupal/Core/Theme/ThemeNegotiator.php
+++ b/core/lib/Drupal/Core/Theme/ThemeNegotiator.php
@@ -3,6 +3,8 @@
 namespace Drupal\Core\Theme;
 
 use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\DependencyInjection\ContainerAware;
+use Symfony\Component\DependencyInjection\ContainerAwareTrait;
 
 /**
  * Provides a class which determines the active theme of the page.
@@ -12,23 +14,16 @@
  */
 class ThemeNegotiator implements ThemeNegotiatorInterface {
 
+  use ContainerAwareTrait;
+
   /**
-   * Holds arrays of theme negotiators, keyed by priority.
+   * Holds an array of theme negotiator IDs, sorted by priority.
    *
-   * @var array
+   * @var string[]
    */
   protected $negotiators = [];
 
   /**
-   * Holds the array of theme negotiators sorted by priority.
-   *
-   * Set to NULL if the array needs to be re-calculated.
-   *
-   * @var array|null
-   */
-  protected $sortedNegotiators;
-
-  /**
    * The access checker for themes.
    *
    * @var \Drupal\Core\Theme\ThemeAccessCheck
@@ -40,43 +35,12 @@ class ThemeNegotiator implements ThemeNegotiatorInterface {
    *
    * @param \Drupal\Core\Theme\ThemeAccessCheck $theme_access
    *   The access checker for themes.
+   * @param string[] $negotiators
+   *   An array of negotiator IDs.
    */
-  public function __construct(ThemeAccessCheck $theme_access) {
+  public function __construct(ThemeAccessCheck $theme_access, array $negotiators) {
     $this->themeAccess = $theme_access;
-  }
-
-  /**
-   * Adds a active theme negotiation service.
-   *
-   * @param \Drupal\Core\Theme\ThemeNegotiatorInterface $negotiator
-   *   The theme negotiator to add.
-   * @param int $priority
-   *   Priority of the theme negotiator.
-   */
-  public function addNegotiator(ThemeNegotiatorInterface $negotiator, $priority) {
-    $this->negotiators[$priority][] = $negotiator;
-    // Force the negotiators to be re-sorted.
-    $this->sortedNegotiators = NULL;
-  }
-
-  /**
-   * Returns the sorted array of theme negotiators.
-   *
-   * @return array|\Drupal\Core\Theme\ThemeNegotiatorInterface[]
-   *   An array of theme negotiator objects.
-   */
-  protected function getSortedNegotiators() {
-    if (!isset($this->sortedNegotiators)) {
-      // Sort the negotiators according to priority.
-      krsort($this->negotiators);
-      // Merge nested negotiators from $this->negotiators into
-      // $this->sortedNegotiators.
-      $this->sortedNegotiators = [];
-      foreach ($this->negotiators as $builders) {
-        $this->sortedNegotiators = array_merge($this->sortedNegotiators, $builders);
-      }
-    }
-    return $this->sortedNegotiators;
+    $this->negotiators = $negotiators;
   }
 
   /**
@@ -90,7 +54,9 @@ public function applies(RouteMatchInterface $route_match) {
    * {@inheritdoc}
    */
   public function determineActiveTheme(RouteMatchInterface $route_match) {
-    foreach ($this->getSortedNegotiators() as $negotiator) {
+    foreach ($this->negotiators as $negotiator_id) {
+      $negotiator = $this->container->get($negotiator_id);
+
       if ($negotiator->applies($route_match)) {
         $theme = $negotiator->determineActiveTheme($route_match);
         if ($theme !== NULL && $this->themeAccess->checkAccess($theme)) {
diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/TaggedHandlersPassTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/TaggedHandlersPassTest.php
index 7e7381f92d..ed013b5145 100644
--- a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/TaggedHandlersPassTest.php
+++ b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/TaggedHandlersPassTest.php
@@ -61,6 +61,26 @@ public function testProcessRequiredHandlers() {
   }
 
   /**
+   * Tests a required consumer with no handlers.
+   *
+   * @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException
+   * @expectedExceptionMessage At least one service tagged with 'consumer_id' is required.
+   * @covers ::process
+   * @covers ::processServiceIdCollectorPass
+   */
+  public function testIdCollectorProcessRequiredHandlers() {
+    $container = $this->buildContainer();
+    $container
+      ->register('consumer_id', __NAMESPACE__ . '\ValidConsumer')
+      ->addTag('service_id_collector', [
+        'required' => TRUE,
+      ]);
+
+    $handler_pass = new TaggedHandlersPass();
+    $handler_pass->process($container);
+  }
+
+  /**
    * Tests consumer with missing interface in non-production environment.
    *
    * @covers ::process
@@ -105,6 +125,32 @@ public function testProcess() {
   }
 
   /**
+   * Tests one consumer and two handlers with service ID collection.
+   *
+   * @covers ::process
+   */
+  public function testserviceIdProcess() {
+    $container = $this->buildContainer();
+    $container
+      ->register('consumer_id', __NAMESPACE__ . '\ValidConsumer')
+      ->addTag('service_id_collector');
+
+    $container
+      ->register('handler1', __NAMESPACE__ . '\ValidHandler')
+      ->addTag('consumer_id');
+    $container
+      ->register('handler2', __NAMESPACE__ . '\ValidHandler')
+      ->addTag('consumer_id');
+
+    $handler_pass = new TaggedHandlersPass();
+    $handler_pass->process($container);
+
+    $arguments = $container->getDefinition('consumer_id')->getArguments();
+    $this->assertCount(1, $arguments);
+    $this->assertCount(2, $arguments[0]);
+  }
+
+  /**
    * Tests handler priority sorting.
    *
    * @covers ::process
@@ -136,6 +182,39 @@ public function testProcessPriority() {
   }
 
   /**
+   * Tests handler priority sorting for service ID collection.
+   *
+   * @covers ::process
+   */
+  public function testserviceIdProcessPriority() {
+    $container = $this->buildContainer();
+    $container
+      ->register('consumer_id', __NAMESPACE__ . '\ValidConsumer')
+      ->addTag('service_id_collector');
+
+    $container
+      ->register('handler1', __NAMESPACE__ . '\ValidHandler')
+      ->addTag('consumer_id');
+    $container
+      ->register('handler2', __NAMESPACE__ . '\ValidHandler')
+      ->addTag('consumer_id', [
+        'priority' => 20,
+      ]);
+    $container
+      ->register('handler3', __NAMESPACE__ . '\ValidHandler')
+      ->addTag('consumer_id', [
+        'priority' => 10,
+      ]);
+
+    $handler_pass = new TaggedHandlersPass();
+    $handler_pass->process($container);
+
+    $arguments = $container->getDefinition('consumer_id')->getArguments();
+    $this->assertCount(1, $arguments);
+    $this->assertSame(['handler2', 'handler3', 'handler1'], $arguments[0]);
+  }
+
+  /**
    * Tests consumer method without priority parameter.
    *
    * @covers ::process
diff --git a/core/tests/Drupal/Tests/Core/Theme/ThemeNegotiatorTest.php b/core/tests/Drupal/Tests/Core/Theme/ThemeNegotiatorTest.php
index 4b33fcbfcd..6eb8b74566 100644
--- a/core/tests/Drupal/Tests/Core/Theme/ThemeNegotiatorTest.php
+++ b/core/tests/Drupal/Tests/Core/Theme/ThemeNegotiatorTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\Core\Theme;
 
+use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Routing\RouteMatch;
 use Drupal\Core\Theme\ThemeNegotiator;
 use Drupal\Tests\UnitTestCase;
@@ -21,6 +22,13 @@ class ThemeNegotiatorTest extends UnitTestCase {
   protected $themeAccessCheck;
 
   /**
+   * The container builder.
+   *
+   * @var \Drupal\Core\DependencyInjection\ContainerBuilder
+   */
+  protected $container;
+
+  /**
    * The request stack.
    *
    * @var \Symfony\Component\HttpFoundation\RequestStack
@@ -34,11 +42,14 @@ class ThemeNegotiatorTest extends UnitTestCase {
    */
   protected $themeNegotiator;
 
+  /**
+   * {@inheritdoc}
+   */
   protected function setUp() {
     $this->themeAccessCheck = $this->getMockBuilder('\Drupal\Core\Theme\ThemeAccessCheck')
       ->disableOriginalConstructor()
       ->getMock();
-    $this->themeNegotiator = new ThemeNegotiator($this->themeAccessCheck);
+    $this->container = new ContainerBuilder();
   }
 
   /**
@@ -55,14 +66,16 @@ public function testDetermineActiveTheme() {
       ->method('applies')
       ->will($this->returnValue(TRUE));
 
-    $this->themeNegotiator->addNegotiator($negotiator, 0);
+    $this->container->set('test_negotiator', $negotiator);
+
+    $negotiators = ['test_negotiator'];
 
     $this->themeAccessCheck->expects($this->any())
       ->method('checkAccess')
       ->will($this->returnValue(TRUE));
 
     $route_match = new RouteMatch('test_route', new Route('/test-route'), [], []);
-    $theme = $this->themeNegotiator->determineActiveTheme($route_match);
+    $theme = $this->createThemeNegotiator($negotiators)->determineActiveTheme($route_match);
 
     $this->assertEquals('example_test', $theme);
   }
@@ -73,6 +86,8 @@ public function testDetermineActiveTheme() {
    * @see \Drupal\Core\Theme\ThemeNegotiator::determineActiveTheme()
    */
   public function testDetermineActiveThemeWithPriority() {
+    $negotiators = [];
+
     $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
     $negotiator->expects($this->once())
       ->method('determineActiveTheme')
@@ -81,7 +96,7 @@ public function testDetermineActiveThemeWithPriority() {
       ->method('applies')
       ->will($this->returnValue(TRUE));
 
-    $this->themeNegotiator->addNegotiator($negotiator, 10);
+    $negotiators['test_negotiator_1'] = $negotiator;
 
     $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
     $negotiator->expects($this->never())
@@ -89,14 +104,18 @@ public function testDetermineActiveThemeWithPriority() {
     $negotiator->expects($this->never())
       ->method('applies');
 
-    $this->themeNegotiator->addNegotiator($negotiator, 0);
+    $negotiators['test_negotiator_2'] = $negotiator;
+
+    foreach ($negotiators as $id => $negotiator) {
+      $this->container->set($id, $negotiator);
+    }
 
     $this->themeAccessCheck->expects($this->any())
       ->method('checkAccess')
       ->will($this->returnValue(TRUE));
 
     $route_match = new RouteMatch('test_route', new Route('/test-route'), [], []);
-    $theme = $this->themeNegotiator->determineActiveTheme($route_match);
+    $theme = $this->createThemeNegotiator(array_keys($negotiators))->determineActiveTheme($route_match);
 
     $this->assertEquals('example_test', $theme);
   }
@@ -107,6 +126,8 @@ public function testDetermineActiveThemeWithPriority() {
    * @see \Drupal\Core\Theme\ThemeNegotiator::determineActiveTheme()
    */
   public function testDetermineActiveThemeWithAccessCheck() {
+    $negotiators = [];
+
     $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
     $negotiator->expects($this->once())
       ->method('determineActiveTheme')
@@ -115,7 +136,7 @@ public function testDetermineActiveThemeWithAccessCheck() {
       ->method('applies')
       ->will($this->returnValue(TRUE));
 
-    $this->themeNegotiator->addNegotiator($negotiator, 10);
+    $negotiators['test_negotiator_1'] = $negotiator;
 
     $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
     $negotiator->expects($this->once())
@@ -125,7 +146,11 @@ public function testDetermineActiveThemeWithAccessCheck() {
       ->method('applies')
       ->will($this->returnValue(TRUE));
 
-    $this->themeNegotiator->addNegotiator($negotiator, 0);
+    $negotiators['test_negotiator_2'] = $negotiator;
+
+    foreach ($negotiators as $id => $negotiator) {
+      $this->container->set($id, $negotiator);
+    }
 
     $this->themeAccessCheck->expects($this->at(0))
       ->method('checkAccess')
@@ -138,7 +163,7 @@ public function testDetermineActiveThemeWithAccessCheck() {
       ->will($this->returnValue(TRUE));
 
     $route_match = new RouteMatch('test_route', new Route('/test-route'), [], []);
-    $theme = $this->themeNegotiator->determineActiveTheme($route_match);
+    $theme = $this->createThemeNegotiator(array_keys($negotiators))->determineActiveTheme($route_match);
 
     $this->assertEquals('example_test2', $theme);
   }
@@ -149,6 +174,8 @@ public function testDetermineActiveThemeWithAccessCheck() {
    * @see \Drupal\Core\Theme\ThemeNegotiatorInterface
    */
   public function testDetermineActiveThemeWithNotApplyingNegotiator() {
+    $negotiators = [];
+
     $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
     $negotiator->expects($this->never())
       ->method('determineActiveTheme');
@@ -156,7 +183,7 @@ public function testDetermineActiveThemeWithNotApplyingNegotiator() {
       ->method('applies')
       ->will($this->returnValue(FALSE));
 
-    $this->themeNegotiator->addNegotiator($negotiator, 10);
+    $negotiators['test_negotiator_1'] = $negotiator;
 
     $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
     $negotiator->expects($this->once())
@@ -166,16 +193,33 @@ public function testDetermineActiveThemeWithNotApplyingNegotiator() {
       ->method('applies')
       ->will($this->returnValue(TRUE));
 
-    $this->themeNegotiator->addNegotiator($negotiator, 0);
+    $negotiators['test_negotiator_2'] = $negotiator;
+
+    foreach ($negotiators as $id => $negotiator) {
+      $this->container->set($id, $negotiator);
+    }
 
     $this->themeAccessCheck->expects($this->any())
       ->method('checkAccess')
       ->will($this->returnValue(TRUE));
 
     $route_match = new RouteMatch('test_route', new Route('/test-route'), [], []);
-    $theme = $this->themeNegotiator->determineActiveTheme($route_match);
+    $theme = $this->createThemeNegotiator(array_keys($negotiators))->determineActiveTheme($route_match);
 
     $this->assertEquals('example_test2', $theme);
   }
 
+  /**
+   * Creates a new theme negotiator instance.
+   *
+   * @param array $negotiators
+   *   An array of negotiator IDs.
+   *
+   * @return \Drupal\Core\Theme\ThemeNegotiator
+   */
+  protected function createThemeNegotiator(array $negotiators) {
+    $theme_negotiator = new ThemeNegotiator($this->themeAccessCheck, $negotiators);
+    $theme_negotiator->setContainer($this->container);
+    return $theme_negotiator;
+  }
 }
