Inject router and view models

Release-Notes: skip
Google-Bug-Id: b/244279450
Change-Id: I9968afcbd1c535670e369b6bb0f9b6923957fcd9
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
index 7485a6d..6fde9ca 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
@@ -54,7 +54,6 @@
 import {ParsedChangeInfo} from '../../../types/types';
 import {GrLinkedChip} from '../../shared/gr-linked-chip/gr-linked-chip';
 import {GrButton} from '../../shared/gr-button/gr-button';
-import {GrRouter} from '../../core/gr-router/gr-router';
 import {nothing} from 'lit';
 import {fixture, html, assert} from '@open-wc/testing';
 import {EventType} from '../../../types/events';
@@ -296,10 +295,9 @@
   });
 
   test('weblinks are visible when other weblinks', async () => {
-    const router = new GrRouter();
     sinon
       .stub(GerritNav, '_generateWeblinks')
-      .callsFake(router.generateWeblinks.bind(router));
+      .callsFake(() => [{name: 'test-name'}]);
 
     element.commitInfo = {
       ...createCommitInfoWithRequiredCommit(),
@@ -309,22 +307,12 @@
     const webLinks = element.webLinks!;
     assert.isFalse(webLinks.hasAttribute('hidden'));
     assert.equal(element.computeWebLinks().length, 1);
-    // With two non-gitiles weblinks, there are two returned.
-    element.commitInfo = {
-      ...createCommitInfoWithRequiredCommit(),
-      web_links: [
-        {...createWebLinkInfo(), name: 'test', url: '#'},
-        {...createWebLinkInfo(), name: 'test2', url: '#'},
-      ],
-    };
-    assert.equal(element.computeWebLinks().length, 2);
   });
 
   test('weblinks are visible when gitiles and other weblinks', async () => {
-    const router = new GrRouter();
     sinon
       .stub(GerritNav, '_generateWeblinks')
-      .callsFake(router.generateWeblinks.bind(router));
+      .callsFake(() => [{name: 'test-name'}]);
 
     element.commitInfo = {
       ...createCommitInfoWithRequiredCommit(),
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts
index ea4b202..92fc2bb 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts
@@ -13,7 +13,6 @@
   createServerInfo,
 } from '../../../test/test-data-generators';
 import {CommitId, RepoName} from '../../../types/common';
-import {GrRouter} from '../../core/gr-router/gr-router';
 import {fixture, html, assert} from '@open-wc/testing';
 import {waitEventLoop} from '../../../test/test-utils';
 
@@ -61,10 +60,9 @@
   });
 
   test('use web link when available', () => {
-    const router = new GrRouter();
-    sinon
-      .stub(GerritNav, '_generateWeblinks')
-      .callsFake(router.generateWeblinks.bind(router));
+    sinon.stub(GerritNav, 'getPatchSetWeblink').callsFake(() => {
+      return {name: 'test-name', url: 'test-url'};
+    });
 
     element.change = {...createChange(), labels: {}, project: '' as RepoName};
     element.commitInfo = {
@@ -75,65 +73,6 @@
     element.serverConfig = createServerInfo();
 
     assert.isOk(element._showWebLink);
-    assert.equal(element._webLink, 'link-url');
-  });
-
-  test('does not relativize web links that begin with scheme', () => {
-    const router = new GrRouter();
-    sinon
-      .stub(GerritNav, '_generateWeblinks')
-      .callsFake(router.generateWeblinks.bind(router));
-
-    element.change = {...createChange(), labels: {}, project: '' as RepoName};
-    element.commitInfo = {
-      ...createCommit(),
-      commit: 'commitsha' as CommitId,
-      web_links: [{name: 'gitweb', url: 'https://link-url'}],
-    };
-    element.serverConfig = createServerInfo();
-
-    assert.isOk(element._showWebLink);
-    assert.equal(element._webLink, 'https://link-url');
-  });
-
-  test('ignore web links that are neither gitweb nor gitiles', () => {
-    const router = new GrRouter();
-    sinon
-      .stub(GerritNav, '_generateWeblinks')
-      .callsFake(router.generateWeblinks.bind(router));
-
-    element.change = {...createChange(), project: 'project-name' as RepoName};
-    element.commitInfo = {
-      ...createCommit(),
-      commit: 'commit-sha' as CommitId,
-      web_links: [
-        {
-          name: 'ignore',
-          url: 'ignore',
-        },
-        {
-          name: 'gitiles',
-          url: 'https://link-url',
-        },
-      ],
-    };
-    element.serverConfig = createServerInfo();
-
-    assert.isOk(element._showWebLink);
-    assert.equal(element._webLink, 'https://link-url');
-
-    // Remove gitiles link.
-    element.commitInfo = {
-      ...createCommit(),
-      commit: 'commit-sha' as CommitId,
-      web_links: [
-        {
-          name: 'ignore',
-          url: 'ignore',
-        },
-      ],
-    };
-    assert.isNotOk(element._showWebLink);
-    assert.isNotOk(element._webLink);
+    assert.equal(element._webLink, 'test-url');
   });
 });
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index bac054f..8e46786 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -35,7 +35,7 @@
 } from '../../../types/common';
 import {AppElement, AppElementParams} from '../../gr-app-types';
 import {LocationChangeEventDetail} from '../../../types/events';
-import {GerritView} from '../../../services/router/router-model';
+import {GerritView, RouterModel} from '../../../services/router/router-model';
 import {firePageError} from '../../../utils/event-util';
 import {windowLocationReload} from '../../../utils/dom-util';
 import {
@@ -50,13 +50,46 @@
   LATEST_ATTEMPT,
   stringToAttemptChoice,
 } from '../../../models/checks/checks-util';
-import {AdminChildView} from '../../../models/views/admin';
-import {AgreementViewState} from '../../../models/views/agreement';
-import {RepoDetailView} from '../../../models/views/repo';
-import {GroupDetailView} from '../../../models/views/group';
-import {DiffViewState} from '../../../models/views/diff';
-import {ChangeViewState} from '../../../models/views/change';
-import {EditViewState} from '../../../models/views/edit';
+import {
+  AdminChildView,
+  AdminViewModel,
+  AdminViewState,
+} from '../../../models/views/admin';
+import {
+  AgreementViewModel,
+  AgreementViewState,
+} from '../../../models/views/agreement';
+import {
+  RepoDetailView,
+  RepoViewModel,
+  RepoViewState,
+} from '../../../models/views/repo';
+import {
+  GroupDetailView,
+  GroupViewModel,
+  GroupViewState,
+} from '../../../models/views/group';
+import {DiffViewModel, DiffViewState} from '../../../models/views/diff';
+import {ChangeViewModel, ChangeViewState} from '../../../models/views/change';
+import {EditViewModel, EditViewState} from '../../../models/views/edit';
+import {
+  DashboardViewModel,
+  DashboardViewState,
+} from '../../../models/views/dashboard';
+import {
+  SettingsViewModel,
+  SettingsViewState,
+} from '../../../models/views/settings';
+import {define} from '../../../models/dependency';
+import {Finalizable} from '../../../services/registry';
+import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
+import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
+import {
+  DocumentationViewModel,
+  DocumentationViewState,
+} from '../../../models/views/documentation';
+import {PluginViewModel, PluginViewState} from '../../../models/views/plugin';
+import {SearchViewModel, SearchViewState} from '../../../models/views/search';
 
 const RoutePattern = {
   ROOT: '/',
@@ -261,7 +294,9 @@
 
 type QueryStringItem = [string, string]; // [key, value]
 
-export class GrRouter {
+export const routerToken = define<GrRouter>('router');
+
+export class GrRouter implements Finalizable {
   readonly _app = app;
 
   _isRedirecting?: boolean;
@@ -270,11 +305,25 @@
   // and for first navigation in app after loaded from server (true).
   _isInitialLoad = true;
 
-  private readonly reporting = getAppContext().reportingService;
+  constructor(
+    private readonly reporting: ReportingService,
+    private readonly routerModel: RouterModel,
+    private readonly restApiService: RestApiService,
+    private readonly adminViewModel: AdminViewModel,
+    private readonly agreementViewModel: AgreementViewModel,
+    private readonly changeViewModel: ChangeViewModel,
+    private readonly dashboardViewModel: DashboardViewModel,
+    private readonly diffViewModel: DiffViewModel,
+    private readonly documentationViewModel: DocumentationViewModel,
+    private readonly editViewModel: EditViewModel,
+    private readonly groupViewModel: GroupViewModel,
+    private readonly pluginViewModel: PluginViewModel,
+    private readonly repoViewModel: RepoViewModel,
+    private readonly searchViewModel: SearchViewModel,
+    private readonly settingsViewModel: SettingsViewModel
+  ) {}
 
-  private readonly routerModel = getAppContext().routerModel;
-
-  private readonly restApiService = getAppContext().restApiService;
+  finalize(): void {}
 
   start() {
     if (!this._app) {
@@ -283,15 +332,15 @@
     this.startRouter();
   }
 
-  setParams(params: AppElementParams) {
+  setState(state: AppElementParams) {
     this.routerModel.updateState({
-      view: params.view,
-      changeNum: 'changeNum' in params ? params.changeNum : undefined,
-      patchNum: 'patchNum' in params ? params.patchNum ?? undefined : undefined,
+      view: state.view,
+      changeNum: 'changeNum' in state ? state.changeNum : undefined,
+      patchNum: 'patchNum' in state ? state.patchNum ?? undefined : undefined,
       basePatchNum:
-        'basePatchNum' in params ? params.basePatchNum ?? undefined : undefined,
+        'basePatchNum' in state ? state.basePatchNum ?? undefined : undefined,
     });
-    this.appElement().params = params;
+    this.appElement().params = state;
   }
 
   private appElement(): AppElement {
@@ -905,7 +954,7 @@
     this.mapRoute(
       RoutePattern.NEW_AGREEMENTS,
       'handleNewAgreementsRoute',
-      ctx => this.handleNewAgreementsRoute(ctx),
+      () => this.handleNewAgreementsRoute(),
       true
     );
 
@@ -1067,10 +1116,12 @@
           this.redirect('/q/owner:' + encodeURIComponent(data.params[0]));
         }
       } else {
-        this.setParams({
+        const state: DashboardViewState = {
           view: GerritView.DASHBOARD,
           user: data.params[0],
-        });
+        };
+        this.setState(state);
+        this.dashboardViewModel.updateState(state);
       }
     });
   }
@@ -1118,13 +1169,14 @@
     });
 
     if (sections.length > 0) {
-      // Custom dashboard view.
-      this.setParams({
+      const state: DashboardViewState = {
         view: GerritView.DASHBOARD,
         user: 'self',
         sections,
         title,
-      });
+      };
+      this.setState(state);
+      this.dashboardViewModel.updateState(state);
       return Promise.resolve();
     }
 
@@ -1135,11 +1187,13 @@
 
   handleProjectDashboardRoute(data: PageContextWithQueryMap) {
     const project = data.params[0] as RepoName;
-    this.setParams({
+    const state: DashboardViewState = {
       view: GerritView.DASHBOARD,
       project,
       dashboard: decodeURIComponent(data.params[1]) as DashboardId,
-    });
+    };
+    this.setState(state);
+    this.dashboardViewModel.updateState(state);
     this.reporting.setRepoName(project);
   }
 
@@ -1156,53 +1210,65 @@
   }
 
   handleGroupRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: GroupViewState = {
       view: GerritView.GROUP,
       groupId: data.params[0] as GroupId,
-    });
+    };
+    this.setState(state);
+    this.groupViewModel.updateState(state);
   }
 
   handleGroupAuditLogRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: GroupViewState = {
       view: GerritView.GROUP,
       detail: GroupDetailView.LOG,
       groupId: data.params[0] as GroupId,
-    });
+    };
+    this.setState(state);
+    this.groupViewModel.updateState(state);
   }
 
   handleGroupMembersRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: GroupViewState = {
       view: GerritView.GROUP,
       detail: GroupDetailView.MEMBERS,
       groupId: data.params[0] as GroupId,
-    });
+    };
+    this.setState(state);
+    this.groupViewModel.updateState(state);
   }
 
   handleGroupListOffsetRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: AdminViewState = {
       view: GerritView.ADMIN,
       adminView: AdminChildView.GROUPS,
       offset: data.params[1] || 0,
       filter: null,
       openCreateModal: data.hash === 'create',
-    });
+    };
+    this.setState(state);
+    this.adminViewModel.updateState(state);
   }
 
   handleGroupListFilterOffsetRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: AdminViewState = {
       view: GerritView.ADMIN,
       adminView: AdminChildView.GROUPS,
       offset: data.params['offset'],
       filter: data.params['filter'],
-    });
+    };
+    this.setState(state);
+    this.adminViewModel.updateState(state);
   }
 
   handleGroupListFilterRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: AdminViewState = {
       view: GerritView.ADMIN,
       adminView: AdminChildView.GROUPS,
       filter: data.params['filter'] || null,
-    });
+    };
+    this.setState(state);
+    this.adminViewModel.updateState(state);
   }
 
   handleProjectsOldRoute(data: PageContextWithQueryMap) {
@@ -1219,127 +1285,153 @@
 
   handleRepoCommandsRoute(data: PageContextWithQueryMap) {
     const repo = data.params[0] as RepoName;
-    this.setParams({
+    const state: RepoViewState = {
       view: GerritView.REPO,
       detail: RepoDetailView.COMMANDS,
       repo,
-    });
+    };
+    this.setState(state);
+    this.repoViewModel.updateState(state);
     this.reporting.setRepoName(repo);
   }
 
   handleRepoGeneralRoute(data: PageContextWithQueryMap) {
     const repo = data.params[0] as RepoName;
-    this.setParams({
+    const state: RepoViewState = {
       view: GerritView.REPO,
       detail: RepoDetailView.GENERAL,
       repo,
-    });
+    };
+    this.setState(state);
+    this.repoViewModel.updateState(state);
     this.reporting.setRepoName(repo);
   }
 
   handleRepoAccessRoute(data: PageContextWithQueryMap) {
     const repo = data.params[0] as RepoName;
-    this.setParams({
+    const state: RepoViewState = {
       view: GerritView.REPO,
       detail: RepoDetailView.ACCESS,
       repo,
-    });
+    };
+    this.setState(state);
+    this.repoViewModel.updateState(state);
     this.reporting.setRepoName(repo);
   }
 
   handleRepoDashboardsRoute(data: PageContextWithQueryMap) {
     const repo = data.params[0] as RepoName;
-    this.setParams({
+    const state: RepoViewState = {
       view: GerritView.REPO,
       detail: RepoDetailView.DASHBOARDS,
       repo,
-    });
+    };
+    this.setState(state);
+    this.repoViewModel.updateState(state);
     this.reporting.setRepoName(repo);
   }
 
   handleBranchListOffsetRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: RepoViewState = {
       view: GerritView.REPO,
       detail: RepoDetailView.BRANCHES,
       repo: data.params[0] as RepoName,
       offset: data.params[2] || 0,
       filter: null,
-    });
+    };
+    this.setState(state);
+    this.repoViewModel.updateState(state);
   }
 
   handleBranchListFilterOffsetRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: RepoViewState = {
       view: GerritView.REPO,
       detail: RepoDetailView.BRANCHES,
       repo: data.params['repo'] as RepoName,
       offset: data.params['offset'],
       filter: data.params['filter'],
-    });
+    };
+    this.setState(state);
+    this.repoViewModel.updateState(state);
   }
 
   handleBranchListFilterRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: RepoViewState = {
       view: GerritView.REPO,
       detail: RepoDetailView.BRANCHES,
       repo: data.params['repo'] as RepoName,
       filter: data.params['filter'] || null,
-    });
+    };
+    this.setState(state);
+    this.repoViewModel.updateState(state);
   }
 
   handleTagListOffsetRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: RepoViewState = {
       view: GerritView.REPO,
       detail: RepoDetailView.TAGS,
       repo: data.params[0] as RepoName,
       offset: data.params[2] || 0,
       filter: null,
-    });
+    };
+    this.setState(state);
+    this.repoViewModel.updateState(state);
   }
 
   handleTagListFilterOffsetRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: RepoViewState = {
       view: GerritView.REPO,
       detail: RepoDetailView.TAGS,
       repo: data.params['repo'] as RepoName,
       offset: data.params['offset'],
       filter: data.params['filter'],
-    });
+    };
+    this.setState(state);
+    this.repoViewModel.updateState(state);
   }
 
   handleTagListFilterRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: RepoViewState = {
       view: GerritView.REPO,
       detail: RepoDetailView.TAGS,
       repo: data.params['repo'] as RepoName,
       filter: data.params['filter'] || null,
-    });
+    };
+    this.setState(state);
+    this.repoViewModel.updateState(state);
   }
 
   handleRepoListOffsetRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: AdminViewState = {
       view: GerritView.ADMIN,
       adminView: AdminChildView.REPOS,
       offset: data.params[1] || 0,
       filter: null,
       openCreateModal: data.hash === 'create',
-    });
+    };
+    this.setState(state);
+    this.adminViewModel.updateState(state);
   }
 
   handleRepoListFilterOffsetRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: AdminViewState = {
       view: GerritView.ADMIN,
       adminView: AdminChildView.REPOS,
       offset: data.params['offset'],
       filter: data.params['filter'],
-    });
+    };
+    this.setState(state);
+    this.adminViewModel.updateState(state);
   }
 
   handleRepoListFilterRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: AdminViewState = {
       view: GerritView.ADMIN,
       adminView: AdminChildView.REPOS,
       filter: data.params['filter'] || null,
-    });
+    };
+    this.setState(state);
+    this.adminViewModel.updateState(state);
   }
 
   handleCreateProjectRoute(_: PageContextWithQueryMap) {
@@ -1359,54 +1451,66 @@
   }
 
   handlePluginListOffsetRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: AdminViewState = {
       view: GerritView.ADMIN,
       adminView: AdminChildView.PLUGINS,
       offset: data.params[1] || 0,
       filter: null,
-    });
+    };
+    this.setState(state);
+    this.adminViewModel.updateState(state);
   }
 
   handlePluginListFilterOffsetRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: AdminViewState = {
       view: GerritView.ADMIN,
       adminView: AdminChildView.PLUGINS,
       offset: data.params['offset'],
       filter: data.params['filter'],
-    });
+    };
+    this.setState(state);
+    this.adminViewModel.updateState(state);
   }
 
   handlePluginListFilterRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: AdminViewState = {
       view: GerritView.ADMIN,
       adminView: AdminChildView.PLUGINS,
       filter: data.params['filter'] || null,
-    });
+    };
+    this.setState(state);
+    this.adminViewModel.updateState(state);
   }
 
   handlePluginListRoute(_: PageContextWithQueryMap) {
-    this.setParams({
+    const state: AdminViewState = {
       view: GerritView.ADMIN,
       adminView: AdminChildView.PLUGINS,
-    });
+    };
+    this.setState(state);
+    this.adminViewModel.updateState(state);
   }
 
   handleQueryRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: SearchViewState = {
       view: GerritView.SEARCH,
       query: data.params[0],
       offset: data.params[2],
-    });
+    };
+    this.setState(state);
+    this.searchViewModel.updateState(state);
   }
 
   handleChangeIdQueryRoute(data: PageContextWithQueryMap) {
     // TODO(pcc): This will need to indicate that this was a change ID query if
     // standard queries gain the ability to search places like commit messages
     // for change IDs.
-    this.setParams({
+    const state: SearchViewState = {
       view: GerritView.SEARCH,
       query: data.params[0],
-    });
+    };
+    this.setState(state);
+    this.searchViewModel.updateState(state);
   }
 
   handleQueryLegacySuffixRoute(ctx: PageContextWithQueryMap) {
@@ -1420,7 +1524,7 @@
   handleChangeRoute(ctx: PageContextWithQueryMap) {
     // Parameter order is based on the regex group number matched.
     const changeNum = Number(ctx.params[1]) as NumericChangeId;
-    const params: ChangeViewState = {
+    const state: ChangeViewState = {
       project: ctx.params[0] as RepoName,
       changeNum,
       basePatchNum: convertToPatchSetNum(ctx.params[4]) as BasePatchSetNum,
@@ -1429,7 +1533,7 @@
     };
 
     if (ctx.queryMap.has('forceReload')) {
-      params.forceReload = true;
+      state.forceReload = true;
       history.replaceState(
         null,
         '',
@@ -1438,7 +1542,7 @@
     }
 
     if (ctx.queryMap.has('openReplyDialog')) {
-      params.openReplyDialog = true;
+      state.openReplyDialog = true;
       history.replaceState(
         null,
         '',
@@ -1447,53 +1551,56 @@
     }
 
     const tab = ctx.queryMap.get('tab');
-    if (tab) params.tab = tab;
+    if (tab) state.tab = tab;
     const filter = ctx.queryMap.get('filter');
-    if (filter) params.filter = filter;
+    if (filter) state.filter = filter;
     const attempt = stringToAttemptChoice(ctx.queryMap.get('attempt'));
-    if (attempt && attempt !== LATEST_ATTEMPT) params.attempt = attempt;
+    if (attempt && attempt !== LATEST_ATTEMPT) state.attempt = attempt;
 
-    assertIsDefined(params.project, 'project');
-    this.reporting.setRepoName(params.project);
+    assertIsDefined(state.project, 'project');
+    this.reporting.setRepoName(state.project);
     this.reporting.setChangeId(changeNum);
-    this.normalizePatchRangeParams(params);
-    this.setParams(params);
+    this.normalizePatchRangeParams(state);
+    this.setState(state);
+    this.changeViewModel.updateState(state);
   }
 
   handleCommentRoute(ctx: PageContextWithQueryMap) {
     const changeNum = Number(ctx.params[1]) as NumericChangeId;
-    const params: DiffViewState = {
+    const state: DiffViewState = {
       project: ctx.params[0] as RepoName,
       changeNum,
       commentId: ctx.params[2] as UrlEncodedCommentId,
       view: GerritView.DIFF,
       commentLink: true,
     };
-    this.reporting.setRepoName(params.project ?? '');
+    this.reporting.setRepoName(state.project ?? '');
     this.reporting.setChangeId(changeNum);
-    this.normalizePatchRangeParams(params);
-    this.setParams(params);
+    this.normalizePatchRangeParams(state);
+    this.setState(state);
+    this.diffViewModel.updateState(state);
   }
 
   handleCommentsRoute(ctx: PageContextWithQueryMap) {
     const changeNum = Number(ctx.params[1]) as NumericChangeId;
-    const params: ChangeViewState = {
+    const state: ChangeViewState = {
       project: ctx.params[0] as RepoName,
       changeNum,
       commentId: ctx.params[2] as UrlEncodedCommentId,
       view: GerritView.CHANGE,
     };
-    assertIsDefined(params.project);
-    this.reporting.setRepoName(params.project);
+    assertIsDefined(state.project);
+    this.reporting.setRepoName(state.project);
     this.reporting.setChangeId(changeNum);
-    this.normalizePatchRangeParams(params);
-    this.setParams(params);
+    this.normalizePatchRangeParams(state);
+    this.setState(state);
+    this.changeViewModel.updateState(state);
   }
 
   handleDiffRoute(ctx: PageContextWithQueryMap) {
     const changeNum = Number(ctx.params[1]) as NumericChangeId;
     // Parameter order is based on the regex group number matched.
-    const params: DiffViewState = {
+    const state: DiffViewState = {
       project: ctx.params[0] as RepoName,
       changeNum,
       basePatchNum: convertToPatchSetNum(ctx.params[4]) as BasePatchSetNum,
@@ -1503,13 +1610,14 @@
     };
     const address = this.parseLineAddress(ctx.hash);
     if (address) {
-      params.leftSide = address.leftSide;
-      params.lineNum = address.lineNum;
+      state.leftSide = address.leftSide;
+      state.lineNum = address.lineNum;
     }
-    this.reporting.setRepoName(params.project ?? '');
+    this.reporting.setRepoName(state.project ?? '');
     this.reporting.setChangeId(changeNum);
-    this.normalizePatchRangeParams(params);
-    this.setParams(params);
+    this.normalizePatchRangeParams(state);
+    this.setState(state);
+    this.diffViewModel.updateState(state);
   }
 
   handleChangeLegacyRoute(ctx: PageContextWithQueryMap) {
@@ -1537,7 +1645,7 @@
     // Parameter order is based on the regex group number matched.
     const project = ctx.params[0] as RepoName;
     const changeNum = Number(ctx.params[1]) as NumericChangeId;
-    const params: EditViewState = {
+    const state: EditViewState = {
       project,
       changeNum,
       // for edit view params, patchNum cannot be undefined
@@ -1546,8 +1654,9 @@
       lineNum: Number(ctx.hash),
       view: GerritView.EDIT,
     };
-    this.normalizePatchRangeParams(params);
-    this.setParams(params);
+    this.normalizePatchRangeParams(state);
+    this.setState(state);
+    this.editViewModel.updateState(state);
     this.reporting.setRepoName(project);
     this.reporting.setChangeId(changeNum);
   }
@@ -1556,7 +1665,7 @@
     // Parameter order is based on the regex group number matched.
     const project = ctx.params[0] as RepoName;
     const changeNum = Number(ctx.params[1]) as NumericChangeId;
-    const params: ChangeViewState = {
+    const state: ChangeViewState = {
       project,
       changeNum,
       patchNum: convertToPatchSetNum(ctx.params[3]) as RevisionPatchSetNum,
@@ -1565,16 +1674,16 @@
       tab: ctx.queryMap.get('tab') ?? '',
     };
     if (ctx.queryMap.has('forceReload')) {
-      params.forceReload = true;
+      state.forceReload = true;
       history.replaceState(
         null,
         '',
         location.href.replace(/[?&]forceReload=true/, '')
       );
     }
-    this.normalizePatchRangeParams(params);
-    this.setParams(params);
-
+    this.normalizePatchRangeParams(state);
+    this.setState(state);
+    this.changeViewModel.updateState(state);
     this.reporting.setRepoName(project);
     this.reporting.setChangeId(changeNum);
   }
@@ -1583,10 +1692,12 @@
     this.redirect('/settings/#Agreements');
   }
 
-  handleNewAgreementsRoute(data: PageContextWithQueryMap) {
-    data.params['view'] = GerritView.AGREEMENTS;
-    // TODO(TS): create valid object
-    this.setParams(data.params as unknown as AgreementViewState);
+  handleNewAgreementsRoute() {
+    const state: AgreementViewState = {
+      view: GerritView.AGREEMENTS,
+    };
+    this.setState(state);
+    this.agreementViewModel.updateState(state);
   }
 
   handleSettingsLegacyRoute(data: PageContextWithQueryMap) {
@@ -1594,18 +1705,22 @@
     // The parameter parsing replaces all '+' with a space,
     // undo that to have valid tokens.
     const token = data.params[0].replace(/ /g, '+');
-    this.setParams({
+    const state: SettingsViewState = {
       view: GerritView.SETTINGS,
       emailToken: token,
-    });
+    };
+    this.setState(state);
+    this.settingsViewModel.updateState(state);
   }
 
   handleSettingsRoute(_: PageContextWithQueryMap) {
-    this.setParams({view: GerritView.SETTINGS});
+    const state: SettingsViewState = {view: GerritView.SETTINGS};
+    this.setState(state);
+    this.settingsViewModel.updateState(state);
   }
 
   handleRegisterRoute(ctx: PageContextWithQueryMap) {
-    this.setParams({justRegistered: true});
+    this.setState({justRegistered: true});
     let path = ctx.params[0] || '/';
 
     // Prevent redirect looping.
@@ -1640,17 +1755,22 @@
   }
 
   handlePluginScreen(ctx: PageContextWithQueryMap) {
-    const view = GerritView.PLUGIN_SCREEN;
-    const plugin = ctx.params[0];
-    const screen = ctx.params[1];
-    this.setParams({view, plugin, screen});
+    const state: PluginViewState = {
+      view: GerritView.PLUGIN_SCREEN,
+      plugin: ctx.params[0],
+      screen: ctx.params[1],
+    };
+    this.setState(state);
+    this.pluginViewModel.updateState(state);
   }
 
   handleDocumentationSearchRoute(data: PageContextWithQueryMap) {
-    this.setParams({
+    const state: DocumentationViewState = {
       view: GerritView.DOCUMENTATION_SEARCH,
       filter: data.params['filter'] || null,
-    });
+    };
+    this.setState(state);
+    this.documentationViewModel.updateState(state);
   }
 
   handleDocumentationSearchRedirectRoute(data: PageContextWithQueryMap) {
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
index da57994..42659ca 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
@@ -16,6 +16,7 @@
 import {
   GrRouter,
   PageContextWithQueryMap,
+  routerToken,
   _testOnly_RoutePattern,
 } from './gr-router';
 import {GerritView} from '../../../services/router/router-model';
@@ -42,12 +43,15 @@
 import {EditViewState} from '../../../models/views/edit';
 import {ChangeViewState} from '../../../models/views/change';
 import {PatchRangeParams} from '../../../utils/url-util';
+import {DependencyRequestEvent} from '../../../models/dependency';
 
 suite('gr-router tests', () => {
   let router: GrRouter;
 
   setup(() => {
-    router = new GrRouter();
+    document.dispatchEvent(
+      new DependencyRequestEvent(routerToken, x => (router = x))
+    );
   });
 
   test('firstCodeBrowserWeblink', () => {
@@ -343,7 +347,7 @@
 
   suite('route handlers', () => {
     let redirectStub: sinon.SinonStub;
-    let setParamsStub: sinon.SinonStub;
+    let setStateStub: sinon.SinonStub;
     let handlePassThroughRoute: sinon.SinonStub;
 
     // Simple route handlers are direct mappings from parsed route data to a
@@ -355,7 +359,7 @@
       params: AppElementParams
     ) {
       (router as any)[methodName](data);
-      assert.deepEqual(setParamsStub.lastCall.args[0], params);
+      assert.deepEqual(setStateStub.lastCall.args[0], params);
     }
 
     function createPageContext(): PageContextWithQueryMap {
@@ -376,7 +380,7 @@
 
     setup(() => {
       redirectStub = sinon.stub(router, 'redirect');
-      setParamsStub = sinon.stub(router, 'setParams');
+      setStateStub = sinon.stub(router, 'setState');
       handlePassThroughRoute = sinon.stub(router, 'handlePassThroughRoute');
     });
 
@@ -400,10 +404,9 @@
     });
 
     test('handleNewAgreementsRoute', () => {
-      const params = createPageContext();
-      router.handleNewAgreementsRoute(params);
-      assert.isTrue(setParamsStub.calledOnce);
-      assert.equal(setParamsStub.lastCall.args[0].view, GerritView.AGREEMENTS);
+      router.handleNewAgreementsRoute();
+      assert.isTrue(setStateStub.calledOnce);
+      assert.equal(setStateStub.lastCall.args[0].view, GerritView.AGREEMENTS);
     });
 
     test('handleSettingsLegacyRoute', () => {
@@ -518,24 +521,24 @@
         const ctx = {...createPageContext(), params: {0: '/foo/bar'}};
         router.handleRegisterRoute(ctx);
         assert.isTrue(redirectStub.calledWithExactly('/foo/bar'));
-        assert.isTrue(setParamsStub.calledOnce);
-        assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
+        assert.isTrue(setStateStub.calledOnce);
+        assert.isTrue(setStateStub.lastCall.args[0].justRegistered);
       });
 
       test('no param', () => {
         const ctx = createPageContext();
         router.handleRegisterRoute(ctx);
         assert.isTrue(redirectStub.calledWithExactly('/'));
-        assert.isTrue(setParamsStub.calledOnce);
-        assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
+        assert.isTrue(setStateStub.calledOnce);
+        assert.isTrue(setStateStub.lastCall.args[0].justRegistered);
       });
 
       test('prevent redirect', () => {
         const ctx = {...createPageContext(), params: {0: '/register'}};
         router.handleRegisterRoute(ctx);
         assert.isTrue(redirectStub.calledWithExactly('/'));
-        assert.isTrue(setParamsStub.calledOnce);
-        assert.isTrue(setParamsStub.lastCall.args[0].justRegistered);
+        assert.isTrue(setStateStub.calledOnce);
+        assert.isTrue(setStateStub.lastCall.args[0].justRegistered);
       });
     });
 
@@ -663,7 +666,7 @@
         return router.handleDashboardRoute(data).then(() => {
           assert.isTrue(redirectToLoginStub.calledOnce);
           assert.isFalse(redirectStub.called);
-          assert.isFalse(setParamsStub.called);
+          assert.isFalse(setStateStub.called);
         });
       });
 
@@ -676,7 +679,7 @@
         };
         return router.handleDashboardRoute(data).then(() => {
           assert.isFalse(redirectToLoginStub.called);
-          assert.isFalse(setParamsStub.called);
+          assert.isFalse(setStateStub.called);
           assert.isTrue(redirectStub.calledOnce);
           assert.equal(redirectStub.lastCall.args[0], '/q/owner:foo');
         });
@@ -691,8 +694,8 @@
         return router.handleDashboardRoute(data).then(() => {
           assert.isFalse(redirectToLoginStub.called);
           assert.isFalse(redirectStub.called);
-          assert.isTrue(setParamsStub.calledOnce);
-          assert.deepEqual(setParamsStub.lastCall.args[0], {
+          assert.isTrue(setStateStub.calledOnce);
+          assert.deepEqual(setStateStub.lastCall.args[0], {
             view: GerritView.DASHBOARD,
             user: 'foo',
           });
@@ -714,7 +717,7 @@
           params: {0: ''},
         };
         return router.handleCustomDashboardRoute(data, '').then(() => {
-          assert.isFalse(setParamsStub.called);
+          assert.isFalse(setStateStub.called);
           assert.isTrue(redirectStub.called);
           assert.equal(redirectStub.lastCall.args[0], '/dashboard/self');
         });
@@ -730,8 +733,8 @@
           .handleCustomDashboardRoute(data, '?a=b&c&d=e')
           .then(() => {
             assert.isFalse(redirectStub.called);
-            assert.isTrue(setParamsStub.calledOnce);
-            assert.deepEqual(setParamsStub.lastCall.args[0], {
+            assert.isTrue(setStateStub.calledOnce);
+            assert.deepEqual(setStateStub.lastCall.args[0], {
               view: GerritView.DASHBOARD,
               user: 'self',
               sections: [
@@ -754,8 +757,8 @@
           .then(() => {
             assert.isFalse(redirectToLoginStub.called);
             assert.isFalse(redirectStub.called);
-            assert.isTrue(setParamsStub.calledOnce);
-            assert.deepEqual(setParamsStub.lastCall.args[0], {
+            assert.isTrue(setStateStub.calledOnce);
+            assert.deepEqual(setStateStub.lastCall.args[0], {
               view: GerritView.DASHBOARD,
               user: 'self',
               sections: [{name: 'a', query: 'b'}],
@@ -775,8 +778,8 @@
           .then(() => {
             assert.isFalse(redirectToLoginStub.called);
             assert.isFalse(redirectStub.called);
-            assert.isTrue(setParamsStub.calledOnce);
-            assert.deepEqual(setParamsStub.lastCall.args[0], {
+            assert.isTrue(setStateStub.calledOnce);
+            assert.deepEqual(setStateStub.lastCall.args[0], {
               view: GerritView.DASHBOARD,
               user: 'self',
               sections: [{name: 'a', query: 'is:open b'}],
@@ -1353,7 +1356,7 @@
 
         router.handleDiffEditRoute(ctx);
         assert.isFalse(redirectStub.called);
-        assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
+        assert.deepEqual(setStateStub.lastCall.args[0], appParams);
       });
 
       test('handleDiffEditRoute with lineNum', () => {
@@ -1379,7 +1382,7 @@
 
         router.handleDiffEditRoute(ctx);
         assert.isFalse(redirectStub.called);
-        assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
+        assert.deepEqual(setStateStub.lastCall.args[0], appParams);
       });
 
       test('handleChangeEditRoute', () => {
@@ -1404,7 +1407,7 @@
 
         router.handleChangeEditRoute(ctx);
         assert.isFalse(redirectStub.called);
-        assert.deepEqual(setParamsStub.lastCall.args[0], appParams);
+        assert.deepEqual(setStateStub.lastCall.args[0], appParams);
       });
     });
 
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index 52ff3db..96d2504 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -31,7 +31,7 @@
 import {getBaseUrl} from '../utils/url-util';
 import {GerritNav} from './core/gr-navigation/gr-navigation';
 import {getAppContext} from '../services/app-context';
-import {GrRouter} from './core/gr-router/gr-router';
+import {routerToken} from './core/gr-router/gr-router';
 import {AccountDetailInfo} from '../types/common';
 import {
   constructServerErrorMsg,
@@ -167,7 +167,7 @@
 
   @state() private themeEndpoint = 'app-theme-light';
 
-  readonly router = new GrRouter();
+  readonly getRouter = resolve(this, routerToken);
 
   private reporting = getAppContext().reportingService;
 
@@ -183,6 +183,7 @@
 
   constructor() {
     super();
+
     document.addEventListener(EventType.PAGE_ERROR, e => {
       this.handlePageError(e);
     });
@@ -244,7 +245,7 @@
 
     this.updateLoginUrl();
     this.reporting.appStarted();
-    this.router.start();
+    this.getRouter().start();
 
     this.restApiService.getAccount().then(account => {
       this.account = account;
diff --git a/polygerrit-ui/app/models/views/admin.ts b/polygerrit-ui/app/models/views/admin.ts
index 856d947..ecb2289 100644
--- a/polygerrit-ui/app/models/views/admin.ts
+++ b/polygerrit-ui/app/models/views/admin.ts
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {GerritView} from '../../services/router/router-model';
+import {define} from '../dependency';
 import {Model} from '../model';
 import {ViewState} from './base';
 
@@ -25,8 +26,12 @@
   adminView: AdminChildView.REPOS,
 };
 
+export const adminViewModelToken = define<AdminViewModel>('admin-view-model');
+
 export class AdminViewModel extends Model<AdminViewState> {
   constructor() {
     super(DEFAULT_STATE);
   }
+
+  finalize() {}
 }
diff --git a/polygerrit-ui/app/models/views/agreement.ts b/polygerrit-ui/app/models/views/agreement.ts
index 4f1763d..9f5a84e 100644
--- a/polygerrit-ui/app/models/views/agreement.ts
+++ b/polygerrit-ui/app/models/views/agreement.ts
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {GerritView} from '../../services/router/router-model';
+import {define} from '../dependency';
 import {Model} from '../model';
 import {ViewState} from './base';
 
@@ -13,8 +14,14 @@
 
 const DEFAULT_STATE: AgreementViewState = {view: GerritView.AGREEMENTS};
 
+export const agreementViewModelToken = define<AgreementViewModel>(
+  'agreement-view-model'
+);
+
 export class AgreementViewModel extends Model<AgreementViewState> {
   constructor() {
     super(DEFAULT_STATE);
   }
+
+  finalize() {}
 }
diff --git a/polygerrit-ui/app/models/views/change.ts b/polygerrit-ui/app/models/views/change.ts
index 94d465e6..71262b7 100644
--- a/polygerrit-ui/app/models/views/change.ts
+++ b/polygerrit-ui/app/models/views/change.ts
@@ -17,6 +17,7 @@
   getPatchRangeExpression,
 } from '../../utils/url-util';
 import {AttemptChoice} from '../checks/checks-util';
+import {define} from '../dependency';
 import {Model} from '../model';
 import {ViewState} from './base';
 
@@ -80,8 +81,13 @@
   }
 }
 
+export const changeViewModelToken =
+  define<ChangeViewModel>('change-view-model');
+
 export class ChangeViewModel extends Model<ChangeViewState> {
   constructor() {
     super(DEFAULT_STATE);
   }
+
+  finalize() {}
 }
diff --git a/polygerrit-ui/app/models/views/dashboard.ts b/polygerrit-ui/app/models/views/dashboard.ts
index dec53d1..0de6bf8 100644
--- a/polygerrit-ui/app/models/views/dashboard.ts
+++ b/polygerrit-ui/app/models/views/dashboard.ts
@@ -7,6 +7,7 @@
 import {GerritView} from '../../services/router/router-model';
 import {DashboardId} from '../../types/common';
 import {encodeURL} from '../../utils/url-util';
+import {define} from '../dependency';
 import {Model} from '../model';
 import {ViewState} from './base';
 
@@ -68,8 +69,14 @@
   }
 }
 
+export const dashboardViewModelToken = define<DashboardViewModel>(
+  'dashboard-view-model'
+);
+
 export class DashboardViewModel extends Model<DashboardViewState> {
   constructor() {
     super(DEFAULT_STATE);
   }
+
+  finalize() {}
 }
diff --git a/polygerrit-ui/app/models/views/diff.ts b/polygerrit-ui/app/models/views/diff.ts
index 68f416f..ce6760e 100644
--- a/polygerrit-ui/app/models/views/diff.ts
+++ b/polygerrit-ui/app/models/views/diff.ts
@@ -12,6 +12,7 @@
 import {GerritView} from '../../services/router/router-model';
 import {UrlEncodedCommentId} from '../../types/common';
 import {encodeURL, getPatchRangeExpression} from '../../utils/url-util';
+import {define} from '../dependency';
 import {Model} from '../model';
 import {ViewState} from './base';
 
@@ -58,8 +59,12 @@
   }
 }
 
+export const diffViewModelToken = define<DiffViewModel>('diff-view-model');
+
 export class DiffViewModel extends Model<DiffViewState> {
   constructor() {
     super(DEFAULT_STATE);
   }
+
+  finalize() {}
 }
diff --git a/polygerrit-ui/app/models/views/documentation.ts b/polygerrit-ui/app/models/views/documentation.ts
index 4273b13..4f33c6dd 100644
--- a/polygerrit-ui/app/models/views/documentation.ts
+++ b/polygerrit-ui/app/models/views/documentation.ts
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {GerritView} from '../../services/router/router-model';
+import {define} from '../dependency';
 import {Model} from '../model';
 import {ViewState} from './base';
 
@@ -16,8 +17,14 @@
   view: GerritView.DOCUMENTATION_SEARCH,
 };
 
+export const documentationViewModelToken = define<DocumentationViewModel>(
+  'documentation-view-model'
+);
+
 export class DocumentationViewModel extends Model<DocumentationViewState> {
   constructor() {
     super(DEFAULT_STATE);
   }
+
+  finalize() {}
 }
diff --git a/polygerrit-ui/app/models/views/edit.ts b/polygerrit-ui/app/models/views/edit.ts
index 102a7e0..7079630 100644
--- a/polygerrit-ui/app/models/views/edit.ts
+++ b/polygerrit-ui/app/models/views/edit.ts
@@ -11,6 +11,7 @@
 } from '../../api/rest-api';
 import {GerritView} from '../../services/router/router-model';
 import {encodeURL, getPatchRangeExpression} from '../../utils/url-util';
+import {define} from '../dependency';
 import {Model} from '../model';
 import {ViewState} from './base';
 
@@ -50,8 +51,12 @@
   }
 }
 
+export const editViewModelToken = define<EditViewModel>('edit-view-model');
+
 export class EditViewModel extends Model<EditViewState> {
   constructor() {
     super(DEFAULT_STATE);
   }
+
+  finalize() {}
 }
diff --git a/polygerrit-ui/app/models/views/group.ts b/polygerrit-ui/app/models/views/group.ts
index bac8eb5..3bce89d 100644
--- a/polygerrit-ui/app/models/views/group.ts
+++ b/polygerrit-ui/app/models/views/group.ts
@@ -6,6 +6,7 @@
 import {GerritView} from '../../services/router/router-model';
 import {GroupId} from '../../types/common';
 import {encodeURL, getBaseUrl} from '../../utils/url-util';
+import {define} from '../dependency';
 import {Model} from '../model';
 import {ViewState} from './base';
 
@@ -30,8 +31,12 @@
   return getBaseUrl() + url;
 }
 
+export const groupViewModelToken = define<GroupViewModel>('group-view-model');
+
 export class GroupViewModel extends Model<GroupViewState | undefined> {
   constructor() {
     super(undefined);
   }
+
+  finalize() {}
 }
diff --git a/polygerrit-ui/app/models/views/plugin.ts b/polygerrit-ui/app/models/views/plugin.ts
index 5b0e701..c122b19 100644
--- a/polygerrit-ui/app/models/views/plugin.ts
+++ b/polygerrit-ui/app/models/views/plugin.ts
@@ -4,6 +4,7 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 import {GerritView} from '../../services/router/router-model';
+import {define} from '../dependency';
 import {Model} from '../model';
 import {ViewState} from './base';
 
@@ -15,8 +16,13 @@
 
 const DEFAULT_STATE: PluginViewState = {view: GerritView.PLUGIN_SCREEN};
 
+export const pluginViewModelToken =
+  define<PluginViewModel>('plugin-view-model');
+
 export class PluginViewModel extends Model<PluginViewState> {
   constructor() {
     super(DEFAULT_STATE);
   }
+
+  finalize() {}
 }
diff --git a/polygerrit-ui/app/models/views/repo.ts b/polygerrit-ui/app/models/views/repo.ts
index d7e7a73..55d9438 100644
--- a/polygerrit-ui/app/models/views/repo.ts
+++ b/polygerrit-ui/app/models/views/repo.ts
@@ -6,6 +6,7 @@
 import {GerritView} from '../../services/router/router-model';
 import {RepoName} from '../../types/common';
 import {encodeURL, getBaseUrl} from '../../utils/url-util';
+import {define} from '../dependency';
 import {Model} from '../model';
 import {ViewState} from './base';
 
@@ -48,8 +49,12 @@
   return getBaseUrl() + url;
 }
 
+export const repoViewModelToken = define<RepoViewModel>('repo-view-model');
+
 export class RepoViewModel extends Model<RepoViewState> {
   constructor() {
     super(DEFAULT_STATE);
   }
+
+  finalize() {}
 }
diff --git a/polygerrit-ui/app/models/views/search.ts b/polygerrit-ui/app/models/views/search.ts
index d932b39..7c3d4c4 100644
--- a/polygerrit-ui/app/models/views/search.ts
+++ b/polygerrit-ui/app/models/views/search.ts
@@ -7,6 +7,7 @@
 import {GerritView} from '../../services/router/router-model';
 import {addQuotesWhen} from '../../utils/string-util';
 import {encodeURL} from '../../utils/url-util';
+import {define} from '../dependency';
 import {Model} from '../model';
 import {ViewState} from './base';
 
@@ -86,8 +87,13 @@
   view: GerritView.SEARCH,
 };
 
+export const searchViewModelToken =
+  define<SearchViewModel>('search-view-model');
+
 export class SearchViewModel extends Model<SearchViewState> {
   constructor() {
     super(DEFAULT_STATE);
   }
+
+  finalize() {}
 }
diff --git a/polygerrit-ui/app/models/views/settings.ts b/polygerrit-ui/app/models/views/settings.ts
index f2e67a4..6bbf4f3 100644
--- a/polygerrit-ui/app/models/views/settings.ts
+++ b/polygerrit-ui/app/models/views/settings.ts
@@ -5,6 +5,7 @@
  */
 import {GerritView} from '../../services/router/router-model';
 import {getBaseUrl} from '../../utils/url-util';
+import {define} from '../dependency';
 import {Model} from '../model';
 import {ViewState} from './base';
 
@@ -19,8 +20,14 @@
   return getBaseUrl() + '/settings';
 }
 
+export const settingsViewModelToken = define<SettingsViewModel>(
+  'settings-view-model'
+);
+
 export class SettingsViewModel extends Model<SettingsViewState> {
   constructor() {
     super(DEFAULT_STATE);
   }
+
+  finalize() {}
 }
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 2dca9a9..3a0079f 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -32,6 +32,31 @@
 import {PluginsModel} from '../models/plugins/plugins-model';
 import {HighlightService} from './highlight/highlight-service';
 import {AccountsModel} from '../models/accounts-model/accounts-model';
+import {
+  DashboardViewModel,
+  dashboardViewModelToken,
+} from '../models/views/dashboard';
+import {
+  SettingsViewModel,
+  settingsViewModelToken,
+} from '../models/views/settings';
+import {GrRouter, routerToken} from '../elements/core/gr-router/gr-router';
+import {AdminViewModel, adminViewModelToken} from '../models/views/admin';
+import {
+  AgreementViewModel,
+  agreementViewModelToken,
+} from '../models/views/agreement';
+import {ChangeViewModel, changeViewModelToken} from '../models/views/change';
+import {DiffViewModel, diffViewModelToken} from '../models/views/diff';
+import {
+  DocumentationViewModel,
+  documentationViewModelToken,
+} from '../models/views/documentation';
+import {EditViewModel, editViewModelToken} from '../models/views/edit';
+import {GroupViewModel, groupViewModelToken} from '../models/views/group';
+import {PluginViewModel, pluginViewModelToken} from '../models/views/plugin';
+import {RepoViewModel, repoViewModelToken} from '../models/views/repo';
+import {SearchViewModel, searchViewModelToken} from '../models/views/search';
 
 /**
  * The AppContext lazy initializator for all services
@@ -85,6 +110,50 @@
   const browserModel = new BrowserModel(appContext.userModel);
   dependencies.set(browserModelToken, browserModel);
 
+  const adminViewModel = new AdminViewModel();
+  dependencies.set(adminViewModelToken, adminViewModel);
+  const agreementViewModel = new AgreementViewModel();
+  dependencies.set(agreementViewModelToken, agreementViewModel);
+  const changeViewModel = new ChangeViewModel();
+  dependencies.set(changeViewModelToken, changeViewModel);
+  const dashboardViewModel = new DashboardViewModel();
+  dependencies.set(dashboardViewModelToken, dashboardViewModel);
+  const diffViewModel = new DiffViewModel();
+  dependencies.set(diffViewModelToken, diffViewModel);
+  const documentationViewModel = new DocumentationViewModel();
+  dependencies.set(documentationViewModelToken, documentationViewModel);
+  const editViewModel = new EditViewModel();
+  dependencies.set(editViewModelToken, editViewModel);
+  const groupViewModel = new GroupViewModel();
+  dependencies.set(groupViewModelToken, groupViewModel);
+  const pluginViewModel = new PluginViewModel();
+  dependencies.set(pluginViewModelToken, pluginViewModel);
+  const repoViewModel = new RepoViewModel();
+  dependencies.set(repoViewModelToken, repoViewModel);
+  const searchViewModel = new SearchViewModel();
+  dependencies.set(searchViewModelToken, searchViewModel);
+  const settingsViewModel = new SettingsViewModel();
+  dependencies.set(settingsViewModelToken, settingsViewModel);
+
+  const router = new GrRouter(
+    appContext.reportingService,
+    appContext.routerModel,
+    appContext.restApiService,
+    adminViewModel,
+    agreementViewModel,
+    changeViewModel,
+    dashboardViewModel,
+    diffViewModel,
+    documentationViewModel,
+    editViewModel,
+    groupViewModel,
+    pluginViewModel,
+    repoViewModel,
+    searchViewModel,
+    settingsViewModel
+  );
+  dependencies.set(routerToken, router);
+
   const changeModel = new ChangeModel(
     appContext.routerModel,
     appContext.restApiService,
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index 15857c8..eda4e58 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -37,6 +37,31 @@
   AccountsModel,
   accountsModelToken,
 } from '../models/accounts-model/accounts-model';
+import {
+  DashboardViewModel,
+  dashboardViewModelToken,
+} from '../models/views/dashboard';
+import {
+  SettingsViewModel,
+  settingsViewModelToken,
+} from '../models/views/settings';
+import {GrRouter, routerToken} from '../elements/core/gr-router/gr-router';
+import {AdminViewModel, adminViewModelToken} from '../models/views/admin';
+import {
+  AgreementViewModel,
+  agreementViewModelToken,
+} from '../models/views/agreement';
+import {ChangeViewModel, changeViewModelToken} from '../models/views/change';
+import {DiffViewModel, diffViewModelToken} from '../models/views/diff';
+import {
+  DocumentationViewModel,
+  documentationViewModelToken,
+} from '../models/views/documentation';
+import {EditViewModel, editViewModelToken} from '../models/views/edit';
+import {GroupViewModel, groupViewModelToken} from '../models/views/group';
+import {PluginViewModel, pluginViewModelToken} from '../models/views/plugin';
+import {RepoViewModel, repoViewModelToken} from '../models/views/repo';
+import {SearchViewModel, searchViewModelToken} from '../models/views/search';
 
 export function createTestAppContext(): AppContext & Finalizable {
   const appRegistry: Registry<AppContext> = {
@@ -92,6 +117,51 @@
   const browserModel = () => new BrowserModel(appContext.userModel);
   dependencies.set(browserModelToken, browserModel);
 
+  const adminViewModelCreator = () => new AdminViewModel();
+  dependencies.set(adminViewModelToken, adminViewModelCreator);
+  const agreementViewModelCreator = () => new AgreementViewModel();
+  dependencies.set(agreementViewModelToken, agreementViewModelCreator);
+  const changeViewModelCreator = () => new ChangeViewModel();
+  dependencies.set(changeViewModelToken, changeViewModelCreator);
+  const dashboardViewModelCreator = () => new DashboardViewModel();
+  dependencies.set(dashboardViewModelToken, dashboardViewModelCreator);
+  const diffViewModelCreator = () => new DiffViewModel();
+  dependencies.set(diffViewModelToken, diffViewModelCreator);
+  const documentationViewModelCreator = () => new DocumentationViewModel();
+  dependencies.set(documentationViewModelToken, documentationViewModelCreator);
+  const editViewModelCreator = () => new EditViewModel();
+  dependencies.set(editViewModelToken, editViewModelCreator);
+  const groupViewModelCreator = () => new GroupViewModel();
+  dependencies.set(groupViewModelToken, groupViewModelCreator);
+  const pluginViewModelCreator = () => new PluginViewModel();
+  dependencies.set(pluginViewModelToken, pluginViewModelCreator);
+  const repoViewModelCreator = () => new RepoViewModel();
+  dependencies.set(repoViewModelToken, repoViewModelCreator);
+  const searchViewModelCreator = () => new SearchViewModel();
+  dependencies.set(searchViewModelToken, searchViewModelCreator);
+  const settingsViewModelCreator = () => new SettingsViewModel();
+  dependencies.set(settingsViewModelToken, settingsViewModelCreator);
+
+  const routerCreator = () =>
+    new GrRouter(
+      appContext.reportingService,
+      appContext.routerModel,
+      appContext.restApiService,
+      resolver(adminViewModelToken),
+      resolver(agreementViewModelToken),
+      resolver(changeViewModelToken),
+      resolver(dashboardViewModelToken),
+      resolver(diffViewModelToken),
+      resolver(documentationViewModelToken),
+      resolver(editViewModelToken),
+      resolver(groupViewModelToken),
+      resolver(pluginViewModelToken),
+      resolver(repoViewModelToken),
+      resolver(searchViewModelToken),
+      resolver(settingsViewModelToken)
+    );
+  dependencies.set(routerToken, routerCreator);
+
   const changeModelCreator = () =>
     new ChangeModel(
       appContext.routerModel,