<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alekseii Popkov</title>
    <description>The latest articles on DEV Community by Alekseii Popkov (@alexeyyupopkov).</description>
    <link>https://dev.to/alexeyyupopkov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3914363%2Fd652162e-d4b3-4b87-bbc2-9a8ce8e2644b.jpg</url>
      <title>DEV Community: Alekseii Popkov</title>
      <link>https://dev.to/alexeyyupopkov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alexeyyupopkov"/>
    <language>en</language>
    <item>
      <title>Flutter Navigation: Moving Transitions Out of Screens</title>
      <dc:creator>Alekseii Popkov</dc:creator>
      <pubDate>Mon, 21 Sep 2026 12:00:41 +0000</pubDate>
      <link>https://dev.to/alexeyyupopkov/flutter-navigation-moving-transitions-out-of-screens-l1a</link>
      <guid>https://dev.to/alexeyyupopkov/flutter-navigation-moving-transitions-out-of-screens-l1a</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I have worked in mobile development for more than ten years: first with iOS, then with Flutter. During all this time I have met the same problem. Navigation APIs are designed in such a way that following the common guidelines leads to poor architectural decisions.&lt;/p&gt;

&lt;p&gt;In iOS, the guidelines from Apple configure the next screen — and often create it — in the code of the previous one. This is true for every navigation method. For example, with storyboard segues the next screen is configured in &lt;code&gt;prepare(for:sender:)&lt;/code&gt; of the source view controller. With programmatic navigation, &lt;code&gt;pushViewController&lt;/code&gt; also takes a ready instance, and this instance is usually created in the code of the previous screen.&lt;/p&gt;

&lt;p&gt;Early SwiftUI repeated the same scheme: &lt;code&gt;NavigationLink(destination:)&lt;/code&gt; builds the next screen directly at the place of the tap. iOS 16 introduced &lt;code&gt;NavigationStack&lt;/code&gt;, where the destination is declared separately from the link, and the stack path can be stored outside the view. However, the guidelines still do not say that the decision about a transition is not the responsibility of the screen. For this reason the Coordinator pattern [1], [2], or its simplified version [3], remains common practice in iOS.&lt;/p&gt;

&lt;p&gt;Android came to the same conclusion from another direction. The official Jetpack Compose guide requires developers not to pass &lt;code&gt;navController&lt;/code&gt; into a composable, but to pass callbacks instead. This keeps screens reusable and testable [4].&lt;/p&gt;

&lt;p&gt;In Flutter the situation is more complex because of the web. Paths were added to transitions — the text that a user sees in the address bar. The built-in API (Navigator 2.0) is verbose, so developers rarely use it directly, mostly in simple cases. Third-party packages are more common; go_router and auto_route are the most popular ones. However, nothing changes with them. Neither the Flutter documentation nor the guides of these packages mention that defining a transition inside widget code is an architectural problem. On the contrary, it is presented as the normal way. A standard example from the go_router documentation looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;TextButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'/users/123'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;auto_route goes further and adds the extensions &lt;code&gt;context.router.push(...)&lt;/code&gt; and &lt;code&gt;context.pushRoute(...)&lt;/code&gt;, which make calls to navigation from a widget even more convenient.&lt;/p&gt;

&lt;p&gt;This leads to the following problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Complexity and a violation of SRP.&lt;/strong&gt; The screen is now responsible not only for the user interface and for reactions to user actions, but also for navigation, including the creation of other screens and passing dependencies to them. Two responsibilities are placed in one class, and the screen is no longer easy to read.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Poor reusability.&lt;/strong&gt; A screen that decides where to go next is difficult to place into another context, where the transition must be different.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tight coupling.&lt;/strong&gt; Every screen knows about the next screens. The architecture becomes fragile: a change in one screen requires changes in others.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Low testability.&lt;/strong&gt; It is not possible to check the reaction of a screen to a user action without navigation. The screen requires a configured router, and the result of a tap is not the fact of a request, but a transition that has actually happened. A unit test of a screen turns into an integration test.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;The problem appears because the previous screen owns information about the next one. The most obvious solution is to &lt;strong&gt;move the implementation of the transition out of the screen code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The next question is: where should it be moved? The answer is given by the &lt;strong&gt;Information Expert&lt;/strong&gt; pattern: a responsibility is assigned to the object that has the information required to fulfil it.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  More about the pattern
  &lt;p&gt;Information Expert is one of the GRASP principles (General Responsibility Assignment Software Patterns), described by Craig Larman in the book "Applying UML and Patterns" (1997). The principle answers the question of which object should receive a responsibility, and it is formulated as follows: assign a responsibility to the class that has the information necessary to fulfil it.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The screen does not have the information about where a tap leads: which tab the screen belongs to, which stack is below it, whether the next screen must be opened above the current one or the section must be switched. This information exists in the place where the application is assembled from screens. The screen itself only declares an abstract interface for such a transition. A typical transition then looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;FilledButton&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tonal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;coordinator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onUserPostsRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;userId:&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Posts by this user (&lt;/span&gt;&lt;span class="si"&gt;$postCount&lt;/span&gt;&lt;span class="s"&gt;)'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation of this interface should be kept as close as possible to the place where transitions are configured. In other words, it should be kept where the constructor of the current screen was called.&lt;/p&gt;

&lt;p&gt;The approach is not new. In iOS it is known as the Coordinator pattern [1], [2], and a similar approach for Flutter is described in [6]. One point is more important: &lt;strong&gt;it does not have to be adopted at once&lt;/strong&gt;. Transitions can be moved out of screens one screen at a time, without changing the navigation library and without changing the rest of the application. For this reason the approach can be applied not only to a new project, but also to existing code. Each processed screen immediately stops depending on where it leads, and the rest of the navigation continues to work as before.&lt;/p&gt;

&lt;p&gt;This article therefore has two goals.&lt;/p&gt;

&lt;p&gt;The first goal is to show the approach, and its variations, that follows the Information Expert principle and moves a transition out of widget code to the place where all the required information is available.&lt;/p&gt;

&lt;p&gt;The second goal, which is equally important, is to obtain a place in the code that describes the navigation in a reasonably clear way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is implemented
&lt;/h2&gt;

&lt;p&gt;The article consists of three parts. Each part contains the same implementation of a typical navigation structure of a mobile application: two tabs with their own stacks, and a shared screen that behaves differently in each tab. Part 1 uses the imperative approach without any third-party packages. Parts 2 and 3 implement the same behaviour with go_router and auto_route.&lt;/p&gt;

&lt;p&gt;Each part shows the same approach, but the form differs. In part 1 the transition is moved out of the screen with a callback; in parts 2 and 3 an abstract interface is used. The reason for this difference is that in part 1 a callback looks clearer in terms of syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example application
&lt;/h2&gt;

&lt;p&gt;The source code is available in the repository [5]. Each implementation is placed in a separate branch: &lt;code&gt;part-1-imperative&lt;/code&gt;, &lt;code&gt;part-2-gorouter&lt;/code&gt;, &lt;code&gt;part-3-autoroute&lt;/code&gt;. The &lt;code&gt;main&lt;/code&gt; branch contains the shared part — models, repository and presentation widgets — without navigation. The application is identical in all branches; only the navigation layer differs.&lt;/p&gt;

&lt;p&gt;In all three branches the navigation is placed in the same files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lib/router/
  app_router.dart            the tree of the application; the only place
                             that knows about both tabs
  feed_tab_router.dart       transitions of the Feed tab and answers to
                             requests of its screens
  contacts_tab_router.dart   the same for the Contacts tab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application has two tabs, and each tab has its own &lt;code&gt;Navigator&lt;/code&gt; and its own stack:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Figqm38y636oar35qm4ez.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Figqm38y636oar35qm4ez.png" alt="Screen structure and transitions" width="800" height="677"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Navigation requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;UserProfileScreen&lt;/code&gt; is used in both tabs;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PostListScreen&lt;/code&gt; is used twice: as the root of the Feed tab, and as the list of posts of a selected author;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UserPickerScreen&lt;/code&gt; is opened to return a result — the selected user — to the screen that started the request;&lt;/li&gt;
&lt;li&gt;the behaviour of the "Posts by this user" button on the profile screen depends on the tab in which this screen is opened. In the Feed tab the list of posts is added to the stack of the current tab. In the Contacts tab the application switches to the Feed tab and opens the list there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last requirement defines the structure of the navigation: the same screen handles the same user request in two different ways, depending on the context in which it was opened.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1. Imperative navigation
&lt;/h2&gt;

&lt;p&gt;This implementation uses no third-party packages: only &lt;code&gt;Navigator&lt;/code&gt; and &lt;code&gt;GlobalKey&lt;/code&gt;. The contract of a transition is expressed by a callback instead of an abstract interface, for reasons of syntax. The handler is written exactly where the screen is created, so the transition and its destination are visible in one place, without a separate implementation class.&lt;/p&gt;

&lt;p&gt;The four files below are listed in the order in which a developer reads them after opening the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  main.dart
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;appRouter:&lt;/span&gt; &lt;span class="n"&gt;AppRouter&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;AppRouter&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appRouter&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;
      &lt;span class="nl"&gt;home:&lt;/span&gt; &lt;span class="n"&gt;RootScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;appRouter:&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AppRouter&lt;/code&gt; is a plain object. It is created before the widget tree is built and is passed to the root screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  root_screen.dart
&lt;/h3&gt;

&lt;p&gt;The root screen is responsible for one level of navigation: switching between tabs. It does not know which screens are placed inside the tabs. The content of each tab is built by the corresponding tab router, which is passed through &lt;code&gt;rootBuilder&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Listing: root_screen.dart
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RootScreen&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;AppRouter&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;RootScreen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appRouter&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ValueListenableBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;valueListenable:&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentTab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentTab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;IndexedStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;index:&lt;/span&gt; &lt;span class="n"&gt;currentTab&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;TabNavigator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;navigatorKey:&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;feedNavigatorKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="nl"&gt;rootBuilder:&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;feedTabRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;buildRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;TabNavigator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;navigatorKey:&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contactsNavigatorKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="nl"&gt;rootBuilder:&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contactsTabRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;buildRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;bottomNavigationBar:&lt;/span&gt; &lt;span class="n"&gt;NavigationBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;selectedIndex:&lt;/span&gt; &lt;span class="n"&gt;currentTab&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;onDestinationSelected:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="n"&gt;appRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;switchTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppTab&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
          &lt;span class="nl"&gt;destinations:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[...],&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  app_router.dart
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;AppTab&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contacts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppRouter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;feedNavigatorKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GlobalKey&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NavigatorState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nl"&gt;debugLabel:&lt;/span&gt; &lt;span class="s"&gt;'feed'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;contactsNavigatorKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GlobalKey&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NavigatorState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;debugLabel:&lt;/span&gt; &lt;span class="s"&gt;'contacts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;currentTab&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ValueNotifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppTab&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;feedTabRouter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FeedTabRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;navigatorKey:&lt;/span&gt; &lt;span class="n"&gt;feedNavigatorKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;contactsTabRouter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ContactsTabRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;navigatorKey:&lt;/span&gt; &lt;span class="n"&gt;contactsNavigatorKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;onRoute:&lt;/span&gt; &lt;span class="n"&gt;_onContactsTabRoute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;switchTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppTab&lt;/span&gt; &lt;span class="n"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;currentTab&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;/// The last handler in the chain. A transition between tabs requires&lt;/span&gt;
  &lt;span class="c1"&gt;/// knowledge of both tabs, so it is handled here.&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_onContactsTabRoute&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ContactsTabRoute&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;UserPostsRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;switchTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppTab&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;feedTabRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;openUserPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the only place that knows about both tabs. The Contacts tab cannot open the posts of an author inside itself, because these posts belong to another tab. The tab passes the request here, and here the application switches the tab and opens the list in the feed.&lt;/p&gt;

&lt;h3&gt;
  
  
  feed_tab_router.dart
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FeedTabRouter&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;TabRouter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;GlobalKey&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NavigatorState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;navigatorKey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FeedTabRouter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;navigatorKey&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;buildRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_postList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;/// Entry point for a request that comes from the Contacts tab.&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;openUserPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_postList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;authorId:&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;_postList&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PostListScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;authorId:&lt;/span&gt; &lt;span class="n"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;onRoute:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;OpenPostRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_postDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;_postDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PostDetailsScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;postId:&lt;/span&gt; &lt;span class="n"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;onRoute:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;OpenAuthorRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_userProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;PickUserRoute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;_userPicker&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;_userProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;UserProfileScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;userId:&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;onRoute:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;OpenUserPostsRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;openUserPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;_userPicker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;UserPickerScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;onRoute:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;UserPickedRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file contains all transitions of the tab: one method per screen, and inside the method all directions to which this screen leads. The post list opens a post, a post opens the author profile or the user picker, and the profile opens the list of posts of the author.&lt;/p&gt;

&lt;h3&gt;
  
  
  contacts_tab_router.dart
&lt;/h3&gt;

&lt;p&gt;The Contacts tab is built in the same way, but it cannot handle one of the requests, because the posts of an author belong to another tab. For this reason the tab declares its own set of requests and passes such a request to the level above — to the object that knows about both tabs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ContactsTabRoute&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ContactsTabRoute&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserPostsRoute&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ContactsTabRoute&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Never&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;UserPostsRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;typedef&lt;/span&gt; &lt;span class="n"&gt;OnContactsTabRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ContactsTabRoute&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ContactsTabRouter&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;TabRouter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;OnContactsTabRoute&lt;/span&gt; &lt;span class="n"&gt;onRoute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;buildRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_contacts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;_contacts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ContactsScreen&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;

  &lt;span class="c1"&gt;/// The same screen and the same request as in the Feed tab, but the answer&lt;/span&gt;
  &lt;span class="c1"&gt;/// is different: there the posts stay in the stack, here the request is&lt;/span&gt;
  &lt;span class="c1"&gt;/// passed to the level above.&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;_userProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;UserProfileScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;userId:&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;onRoute:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;OpenUserPostsRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;onRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserPostsRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The profile screen here is the same class as in the feed, and it sends the same request. Only the answer is different, and this answer is located in the file of the tab, next to its other transitions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading order
&lt;/h3&gt;

&lt;p&gt;The navigation is read from top to bottom through the files, and each level shows exactly the layer for which this level is responsible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;main.dart&lt;/code&gt; — the application and the root screen;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;root_screen.dart&lt;/code&gt; — two tabs and switching between them;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app_router.dart&lt;/code&gt; — both tabs and transitions between them;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;feed_tab_router.dart&lt;/code&gt;, &lt;code&gt;contacts_tab_router.dart&lt;/code&gt; — all screens of a tab and transitions inside it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a level does not have the required information, it does not handle the request but passes it to the level above. A screen reports a request to the tab router, and the tab router reports it to &lt;code&gt;AppRouter&lt;/code&gt; if the request goes outside its stack. One file is enough to understand the navigation of a tab; four files, read one after another, are enough to understand the navigation of the application.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2. go_router
&lt;/h2&gt;

&lt;p&gt;This library provides a tree of locations, and a transition is a move to a location. The approach is the same, but the form of the contract is different: instead of a callback the screen takes an abstract interface. The visible tree of transitions, which the imperative approach had to assemble by hand, is here provided by the &lt;code&gt;GoRouter(routes: [...])&lt;/code&gt; constructor itself, and &lt;code&gt;onRoute&lt;/code&gt; handlers with their &lt;code&gt;switch&lt;/code&gt; written inside it would only clutter that picture. Nothing, however, prevents using callbacks here as well, exactly as in part 1.&lt;/p&gt;

&lt;p&gt;The order of the files is the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  main.dart
&lt;/h3&gt;

&lt;p&gt;The only difference from part 1 is one line: &lt;code&gt;routerConfig&lt;/code&gt; is passed instead of &lt;code&gt;home&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Listing: main.dart
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;appRouter:&lt;/span&gt; &lt;span class="n"&gt;AppRouter&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;AppRouter&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appRouter&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;
      &lt;span class="nl"&gt;routerConfig:&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  root_screen.dart
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RootScreen&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;StatefulNavigationShell&lt;/span&gt; &lt;span class="n"&gt;navigationShell&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;RootScreen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;navigationShell&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;navigationShell&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;bottomNavigationBar:&lt;/span&gt; &lt;span class="n"&gt;NavigationBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;selectedIndex:&lt;/span&gt; &lt;span class="n"&gt;navigationShell&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;onDestinationSelected:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;navigationShell&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;goBranch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;initialLocation:&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;navigationShell&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;destinations:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[...],&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The root screen is responsible for switching between tabs and does not know what is inside them.&lt;/p&gt;

&lt;h3&gt;
  
  
  app_router.dart
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppRouter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;GoRouter&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GoRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;initialLocation:&lt;/span&gt; &lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;routes:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="n"&gt;StatefulShellRoute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;indexedStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;navigationShell&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;RootScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;navigationShell:&lt;/span&gt; &lt;span class="n"&gt;navigationShell&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;branches:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="n"&gt;StatefulShellBranch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;routes:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;feedTabRoute&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
          &lt;span class="n"&gt;StatefulShellBranch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;routes:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;contactsTabRoute&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two branches, and each branch has its own stack. A transition between tabs requires no separate handling: the location belongs to another branch, and the shell switches the tab itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  feed_tab_router.dart
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;GoRoute&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;feedTabRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PostListScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;coordinator:&lt;/span&gt; &lt;span class="n"&gt;FeedPostListCoordinatorImpl&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="nl"&gt;routes:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;GoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PostDetailsScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;postId:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pathParameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AppRouterParam&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;coordinator:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FeedPostDetailsCoordinatorImpl&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;GoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;UserProfileScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;userId:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pathParameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AppRouterParam&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;coordinator:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FeedUserProfileCoordinatorImpl&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;GoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userPicker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;UserPickerScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;coordinator:&lt;/span&gt; &lt;span class="n"&gt;UserPickerCoordinatorImpl&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;GoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userPosts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PostListScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;authorId:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pathParameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AppRouterParam&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;coordinator:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FeedPostListCoordinatorImpl&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FeedPostListCoordinatorImpl&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;PostListScreenCoordinator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FeedPostListCoordinatorImpl&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;onPostRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;GoRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;postIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FeedPostDetailsCoordinatorImpl&lt;/span&gt;
    &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;PostDetailsScreenCoordinator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onPickUserRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GoRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userPickerIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;/// In the feed the posts of an author stay in the same stack — `push`.&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FeedUserProfileCoordinatorImpl&lt;/span&gt;
    &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;UserProfileScreenCoordinator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FeedUserProfileCoordinatorImpl&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;onUserPostsRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;GoRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userPostsIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file of a tab has two levels: the tree of locations above, and the implementations of the interfaces of its screens below.&lt;/p&gt;

&lt;h3&gt;
  
  
  contacts_tab_router.dart
&lt;/h3&gt;

&lt;p&gt;The file of the Contacts tab is built in exactly the same way, and it fits on one screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;GoRoute&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;contactsTabRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contacts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ContactsScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;coordinator:&lt;/span&gt; &lt;span class="n"&gt;ContactsCoordinatorImpl&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="nl"&gt;routes:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// The same profile screen as in the feed, with another implementation.&lt;/span&gt;
    &lt;span class="n"&gt;GoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;UserProfileScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;userId:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pathParameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AppRouterParam&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;coordinator:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ContactsUserProfileCoordinatorImpl&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ContactsCoordinatorImpl&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;ContactsScreenCoordinator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ContactsCoordinatorImpl&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;onUserProfileRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;GoRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contacts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;/// The same screen and the same request as in the Feed tab, but the answer is&lt;/span&gt;
&lt;span class="c1"&gt;/// different. `go` instead of `push`: the location belongs to another branch,&lt;/span&gt;
&lt;span class="c1"&gt;/// so the shell switches the tab itself, and the Contacts stack stays as it&lt;/span&gt;
&lt;span class="c1"&gt;/// was.&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ContactsUserProfileCoordinatorImpl&lt;/span&gt;
    &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;UserProfileScreenCoordinator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ContactsUserProfileCoordinatorImpl&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;onUserPostsRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;GoRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userPostsIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two tab files are built in the same way, the profile screen in them is the same class, and the whole difference between the requirements is one word in one implementation: &lt;code&gt;push&lt;/code&gt; in the feed and &lt;code&gt;go&lt;/code&gt; in the contacts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading order
&lt;/h3&gt;

&lt;p&gt;The reading order is preserved, and each level shows its own layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;main.dart&lt;/code&gt; — the application and the router configuration;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;root_screen.dart&lt;/code&gt; — two tabs and switching between them;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app_router.dart&lt;/code&gt; — both branches of the tree;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;feed_tab_router.dart&lt;/code&gt;, &lt;code&gt;contacts_tab_router.dart&lt;/code&gt; — all locations of a tab and the answers to the requests of its screens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference from part 1 is that a request does not have to be passed to a higher level: a transition to another tab is an ordinary location, and the difference between the tabs is expressed by one word in one implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3. auto_route
&lt;/h2&gt;

&lt;p&gt;The screens and their contracts are exactly the same as in part 2. There are two differences: the destination is a generated object instead of a string, and a layer of pages appears between a screen and the router. This layer keeps the annotations of the generator outside the screens.&lt;/p&gt;

&lt;p&gt;The order of the files is the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  main.dart
&lt;/h3&gt;

&lt;p&gt;The difference from part 2 is that the router configuration is taken from the generated &lt;code&gt;config()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Listing: main.dart
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;appRouter:&lt;/span&gt; &lt;span class="n"&gt;AppRouter&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;AppRouter&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appRouter&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;
      &lt;span class="nl"&gt;routerConfig:&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  root_page.dart
&lt;/h3&gt;

&lt;p&gt;The root screen is built from the tabs with &lt;code&gt;AutoTabsScaffold&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Listing: root_page.dart
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RoutePage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RootPage&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;RootPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;AutoTabsScaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;routes:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;feedTab&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;contactsTab&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
      &lt;span class="nl"&gt;bottomNavigationBuilder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tabsRouter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NavigationBar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;selectedIndex:&lt;/span&gt; &lt;span class="n"&gt;tabsRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;activeIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;onDestinationSelected:&lt;/span&gt; &lt;span class="n"&gt;tabsRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setActiveIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;destinations:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[...],&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The tabs themselves are empty nested routers, so they need no widgets of their own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app_tabs.dart&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;feedTab&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EmptyShellRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'FeedTab'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;contactsTab&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EmptyShellRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'ContactsTab'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  app_router.dart
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="nd"&gt;@AutoRouterConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppRouter&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;RootStackRouter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AutoRoute&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;AutoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;page:&lt;/span&gt; &lt;span class="n"&gt;RootRoute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;initial:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;feedTabRoute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contactsTabRoute&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  feed_tab_router.dart
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;AutoRoute&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;feedTabRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AutoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;page:&lt;/span&gt; &lt;span class="n"&gt;feedTab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;AutoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;page:&lt;/span&gt; &lt;span class="n"&gt;FeedRoute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;AutoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;page:&lt;/span&gt; &lt;span class="n"&gt;FeedPostDetailsRoute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;AutoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;page:&lt;/span&gt; &lt;span class="n"&gt;FeedUserProfileRoute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;AutoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userPosts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;page:&lt;/span&gt; &lt;span class="n"&gt;FeedUserPostsRoute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;AutoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userPicker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;page:&lt;/span&gt; &lt;span class="n"&gt;FeedUserPickerRoute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nd"&gt;@RoutePage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FeedUserProfilePage&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FeedUserProfilePage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@PathParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'userId'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;UserProfileScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;userId:&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;coordinator:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FeedUserProfileCoordinatorImpl&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FeedUserProfileCoordinatorImpl&lt;/span&gt;
    &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;UserProfileScreenCoordinator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FeedUserProfileCoordinatorImpl&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;onUserPostsRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;AutoRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FeedUserPostsRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;userId:&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are three levels here: the tree of locations, the pages, and the implementations of the interfaces. A page connects a screen with the implementation that this screen must receive in this tab.&lt;/p&gt;

&lt;p&gt;A transition to another tab requires a separate decision. A nested stack router cannot add a screen to a neighbouring tab, so the root router is used, and auto_route activates the required tab itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// contacts_tab_router.dart&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ContactsUserProfileCoordinatorImpl&lt;/span&gt;
    &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="n"&gt;UserProfileScreenCoordinator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ContactsUserProfileCoordinatorImpl&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;onUserPostsRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;AutoRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;root&lt;/span&gt;
          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;navigatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppRouterPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;feedUserPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reading order
&lt;/h3&gt;

&lt;p&gt;The reading order has not changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;main.dart&lt;/code&gt; — the application and the router configuration;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;root_page.dart&lt;/code&gt; — two tabs and switching between them;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app_router.dart&lt;/code&gt; — the whole tree;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;feed_tab_router.dart&lt;/code&gt; — the locations of a tab, its pages and the answers to the requests of its screens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code generation adds a layer of pages, but it changes neither the reading order nor the distribution of responsibilities between the levels.&lt;/p&gt;




&lt;h2&gt;
  
  
  Applicability
&lt;/h2&gt;

&lt;p&gt;The approach does not depend on a specific library. A screen reports a user request; the decision is made by the object that has the information required for this decision. The form of the contract — a callback or an abstract interface — and the mechanics of a transition are defined by the selected tool, but the distribution of responsibilities stays the same. This article shows three implementations: with the built-in &lt;code&gt;Navigator&lt;/code&gt;, with go_router and with auto_route. The same approach can be applied to any other library.&lt;/p&gt;

&lt;p&gt;This leads to a practical property: the approach is suitable for gradual refactoring. Transitions are moved out of screens one screen at a time, without a change of the navigation library and without changes in the rest of the application. Each step is complete in itself: a screen stops depending on where it leads and becomes suitable for reuse in another context. As a side effect, such a screen can be tested without a router, because a stub is passed to it instead of a real implementation of the transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://khanlou.com/2015/01/the-coordinator/" rel="noopener noreferrer"&gt;Khanlou, "The Coordinator" (2015)&lt;/a&gt; — the article that introduced the Coordinator pattern.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.hackingwithswift.com/articles/71/how-to-use-the-coordinator-pattern-in-ios-apps" rel="noopener noreferrer"&gt;Hudson, "How to use the coordinator pattern in iOS apps"&lt;/a&gt; — a detailed description of the Coordinator in practice.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://medium.com/@alexey.yu.popkov/ios-navigation-a-compact-router-approach-46cffa04a6ef" rel="noopener noreferrer"&gt;Popkov, "iOS Navigation: A Compact Router Approach"&lt;/a&gt; — a simplified version of the approach for iOS. Part 1 of this article is its port to Flutter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://developer.android.com/develop/ui/compose/navigation" rel="noopener noreferrer"&gt;Navigation with Compose&lt;/a&gt; — the official Android guide: do not pass &lt;code&gt;navController&lt;/code&gt; into a composable, pass callbacks instead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/AlexeyYuPopkov/example_routing_coordinator_flutter" rel="noopener noreferrer"&gt;example_routing_coordinator_flutter&lt;/a&gt; — the repository with the code of all three implementations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://medium.com/flutter-community/navigation-done-right-a-case-for-hierarchical-routing-with-flutter-ca0aac1275ad" rel="noopener noreferrer"&gt;Shevchuk, "Navigation done right: a case for hierarchical routing with Flutter" (2020)&lt;/a&gt; — a similar approach in Flutter: the decision about a transition is passed from a screen to its parent.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>flutter</category>
      <category>architecture</category>
      <category>mobile</category>
      <category>navigation</category>
    </item>
    <item>
      <title>Nostr as a backend out of the box: where it fits and where it doesn't</title>
      <dc:creator>Alekseii Popkov</dc:creator>
      <pubDate>Tue, 23 Jun 2026 13:44:51 +0000</pubDate>
      <link>https://dev.to/alexeyyupopkov/nostr-as-a-backend-out-of-the-box-where-it-fits-and-where-it-doesnt-465e</link>
      <guid>https://dev.to/alexeyyupopkov/nostr-as-a-backend-out-of-the-box-where-it-fits-and-where-it-doesnt-465e</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; Nostr is a ready-to-use, decentralized backend: it gives you an event model, signatures, encryption, and sync through relays. For notes, private chats, and simple feeds it covers almost everything — with no server of your own. But once you need a smart feed, roles, hierarchies, or complex moderation, the protocol hits one basic limit: only the owner of a key can change an event. After that, the extra workarounds quickly cost more than they save. Below I look at real cases: where Nostr works well, and where it is better to think about a separate backend.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;For a long time I wanted to build a simple cross-platform notes app.&lt;br&gt;
The most important use case for me was storing sensitive information, such as passwords, crypto wallet addresses, and so on. Another key requirement was reliable sync between devices, in particular iOS, Android, and macOS.&lt;/p&gt;

&lt;p&gt;But there was one big problem: for a long time I could not find a secure way to sync notes that did not depend on one specific provider.&lt;/p&gt;

&lt;p&gt;Some time ago I happened to join a project built around the &lt;a href="https://github.com/nostr-protocol/nips" rel="noopener noreferrer"&gt;Nostr&lt;/a&gt; protocol — that is how I first learned about it. For that project Nostr was, to be honest, not a great fit (you will see why later in the article). But for my notes task it turned out to be an almost perfect solution.&lt;/p&gt;

&lt;p&gt;The goal of this article is to review the good and the bad cases of using the &lt;a href="https://github.com/nostr-protocol/nips" rel="noopener noreferrer"&gt;Nostr&lt;/a&gt; protocol that I ran into myself. I also want to describe the tasks it is worth using for, and the tasks where you should look for other solutions.&lt;/p&gt;
&lt;h3&gt;
  
  
  What the Nostr protocol is and how it works
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/nostr-protocol/nips" rel="noopener noreferrer"&gt;Nostr&lt;/a&gt; protocol presents itself as a backend for building decentralized social networks that are hard to censor. But, as you will see below, it can be used for a wider range of tasks. That said, building a social network in the usual sense (with complex feed ranking and filtering, and good performance on complex post models) is, in my personal opinion, hard.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/nostr-protocol/nips" rel="noopener noreferrer"&gt;Nostr&lt;/a&gt; protocol is a set of rules (NIPs) for talking to a decentralized network of servers (relays). You talk to them over WebSockets. Each server keeps a database of events. An event is a record with a fixed structure; the client and the relay exchange it as a JSON object with a defined schema. A user can read and publish such events from one or more relays (and to one or more relays). Each event has an id — a hash of the event itself (more precisely, a SHA-256 of a serialized set of fields: &lt;code&gt;pubkey&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;, &lt;code&gt;kind&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;, and &lt;code&gt;content&lt;/code&gt;), not just of its content. So when you get the same event from different relays, the id is used to remove duplicates. Each event also has a signature. The signature is made from the event id and the sender's private key, and the JSON event has a field with the sender's public key. So to publish an event to one or more relays you need this key pair. (To read events you do not need any keys.)&lt;/p&gt;

&lt;p&gt;It is easy to see that a key pair can also be used for asymmetric encryption. The spec describes common ways to encrypt chat messages — for example, the current &lt;a href="https://github.com/nostr-protocol/nips/blob/master/44.md" rel="noopener noreferrer"&gt;Encrypted Payloads (NIP-44)&lt;/a&gt;, which the &lt;a href="https://github.com/nostr-protocol/nips/blob/master/17.md" rel="noopener noreferrer"&gt;Private Direct Messages (NIP-17)&lt;/a&gt; feature is built on. There is also an older &lt;a href="https://github.com/nostr-protocol/nips/blob/master/04.md" rel="noopener noreferrer"&gt;Encrypted Direct Message (NIP-04)&lt;/a&gt;: it is officially marked as deprecated and is not recommended for public, decentralized use (it uses unauthenticated encryption and leaks metadata).&lt;/p&gt;

&lt;p&gt;Still, it is important to understand that NIP-44 is not the only option. The protocol does not force a specific algorithm: clients are free to agree on any compatible way to encrypt content. So for practical tasks — say, if you run your own private relay or solve a specific business task — NIP-04 with plain AES, or any other scheme you like, can be a good fit.&lt;/p&gt;
&lt;h3&gt;
  
  
  Data model: events, &lt;code&gt;kind&lt;/code&gt;, and tags
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;If you already know Nostr (events, &lt;code&gt;kind&lt;/code&gt;, tags, the &lt;code&gt;REQ&lt;/code&gt; subscription model), feel free to skip this section and go straight to the cases.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Before we get to the cases, it makes sense to look a bit closer at how an event is built — we will keep coming back to it.&lt;/p&gt;

&lt;p&gt;Any Nostr event is JSON that looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1897f99..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pubkey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9c2e0a7..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1781787656&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"737f50..."&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9c2e0a7..."&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello, Nostr!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"41b737f50..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;kind&lt;/code&gt; field is an integer that sets the &lt;em&gt;rough&lt;/em&gt; meaning of an event, rather than a strict type. For example, &lt;code&gt;kind:0&lt;/code&gt; is a user profile, &lt;code&gt;kind:1&lt;/code&gt; is a short text note (a post), &lt;code&gt;kind:7&lt;/code&gt; is a reaction (a like). It is important that this is an agreement, not a strict schema: the meaning of each &lt;code&gt;kind&lt;/code&gt; is described in its NIP, and most NIPs have &lt;em&gt;draft&lt;/em&gt; status — so these are conventions, not a fixed standard. The relay does not validate the structure of the content: the clients that agreed with each other are fully responsible for how a &lt;code&gt;kind&lt;/code&gt; and the event format are read. You could say a &lt;code&gt;kind&lt;/code&gt; is something like a "table" in a normal database, but without a strict schema and without server-side validation.&lt;/p&gt;

&lt;p&gt;Besides meaning, &lt;code&gt;kind&lt;/code&gt; also sets how the relay stores the event. The exact behavior depends on the number range (the ranges are defined in &lt;a href="https://github.com/nostr-protocol/nips/blob/master/01.md" rel="noopener noreferrer"&gt;NIP-01&lt;/a&gt;). In simple terms, events fall into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;regular&lt;/strong&gt; — range &lt;code&gt;1000–9999&lt;/code&gt; (also &lt;code&gt;kind:1&lt;/code&gt;, &lt;code&gt;kind:4&lt;/code&gt;–&lt;code&gt;44&lt;/code&gt;); they just pile up and overwrite nothing;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;replaceable&lt;/strong&gt; — range &lt;code&gt;10000–19999&lt;/code&gt; (also &lt;code&gt;kind:0&lt;/code&gt; for a profile, &lt;code&gt;kind:3&lt;/code&gt; for a contact list); the relay keeps only the newest version for a given author, and a new event replaces the old one;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;addressable&lt;/strong&gt; (also called parameterized replaceable) — range &lt;code&gt;30000–39999&lt;/code&gt;; the same as replaceable, but the "coordinates" for replacing also include the value of the &lt;code&gt;d&lt;/code&gt; tag (see below). This is how updatable things with an identifier work — a long note (&lt;code&gt;kind:30023&lt;/code&gt;), a community definition (&lt;code&gt;kind:34550&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ephemeral&lt;/strong&gt; — range &lt;code&gt;20000–29999&lt;/code&gt;; the relay does not have to store them at all (useful for signaling).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For addressable events the "coordinates" are the triple &lt;code&gt;kind:pubkey:d&lt;/code&gt;, where &lt;code&gt;d&lt;/code&gt; is the value of a special &lt;code&gt;["d", "..."]&lt;/code&gt; tag — a stable identifier of the thing. The &lt;code&gt;a&lt;/code&gt; tag points to such events using the &lt;code&gt;kind:pubkey:d&lt;/code&gt; format (we will see this below).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;tags&lt;/code&gt; field is an array of arrays: the first item of each inner array is the tag name, and the rest are its values (&lt;code&gt;["e", "&amp;lt;event id&amp;gt;"]&lt;/code&gt;, &lt;code&gt;["p", "&amp;lt;pubkey&amp;gt;"]&lt;/code&gt;, and so on). Tags with single-letter names (&lt;code&gt;e&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;g&lt;/code&gt;, &lt;code&gt;d&lt;/code&gt;, …) are indexed by the relay, and you can filter queries by them — in a filter this is written as &lt;code&gt;#e&lt;/code&gt;, &lt;code&gt;#p&lt;/code&gt;, &lt;code&gt;#g&lt;/code&gt; (this will be useful in the feed section).&lt;/p&gt;

&lt;p&gt;Finally, about how events are requested. To get events, the client opens a subscription with a message like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"REQ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;subscription id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;filter&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;filter&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A filter is a JSON object with fields like &lt;code&gt;authors&lt;/code&gt;, &lt;code&gt;kinds&lt;/code&gt;, &lt;code&gt;since&lt;/code&gt;, &lt;code&gt;until&lt;/code&gt;, &lt;code&gt;limit&lt;/code&gt;, &lt;code&gt;#e&lt;/code&gt;, &lt;code&gt;#g&lt;/code&gt;. The relay returns all events that match at least one filter, and keeps the subscription open — it sends new matching events as they appear, until the client closes it with a &lt;code&gt;CLOSE&lt;/code&gt; message. We will come back to filters and their limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Notes
&lt;/h3&gt;

&lt;p&gt;While I was building my app, I did not really think about what the word Nostr stands for. Nostr is an acronym: Notes and Other Stuff Transmitted by Relays.&lt;/p&gt;

&lt;p&gt;So the idea of a notes app on top of this protocol was obvious. Nostr gives you sync, and it offers recommendations for strong asymmetric encryption — &lt;a href="https://github.com/nostr-protocol/nips/blob/master/44.md" rel="noopener noreferrer"&gt;Encrypted Payloads (Versioned)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So Nostr is a very good fit for this task.&lt;/p&gt;

&lt;p&gt;This is how my pet project appeared — &lt;strong&gt;Private Notes (Nostr)&lt;/strong&gt;: a cross-platform notes app with end-to-end encryption on the device (NIP-44), signing with secp256k1, and storage on relays the user picks. No server, no accounts — just a key pair. You can try it right now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🍎 &lt;strong&gt;iOS:&lt;/strong&gt; &lt;a href="https://apps.apple.com/bg/app/private-notes-nostr/id6757975921" rel="noopener noreferrer"&gt;App Store&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Web:&lt;/strong&gt; &lt;a href="https://alexeyyupopkov.github.io" rel="noopener noreferrer"&gt;alexeyyupopkov.github.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🤖 &lt;strong&gt;Android:&lt;/strong&gt; &lt;a href="https://play.google.com/store/apps/details?id=com.alekseii.yu.popkov.nostrNotes" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/AlexeyYuPopkov/nostr_notes" rel="noopener noreferrer"&gt;github.com/AlexeyYuPopkov/nostr_notes&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository is open — if you find a bug or have an idea, feel free to open an &lt;a href="https://github.com/AlexeyYuPopkov/nostr_notes/issues" rel="noopener noreferrer"&gt;Issue&lt;/a&gt;, I will be glad to hear from you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chats
&lt;/h3&gt;

&lt;p&gt;Many small mobile apps have a chat feature. For most such projects the chat backend is a third-party service like &lt;a href="https://www.twilio.com/" rel="noopener noreferrer"&gt;Twilio&lt;/a&gt;, &lt;a href="https://quickblox.com/" rel="noopener noreferrer"&gt;Quickblox&lt;/a&gt;, or a cloud database such as &lt;a href="https://firebase.google.com/docs/database/" rel="noopener noreferrer"&gt;Firebase Realtime Database&lt;/a&gt; or &lt;a href="https://firebase.google.com/docs/firestore/" rel="noopener noreferrer"&gt;Cloud Firestore&lt;/a&gt;. But this means extra costs for these subscriptions. In the second case you also need to design the database for chat data, and that means extra work and bugs. Nostr, on the other hand, gives you a ready backend that fits chats well. You can either use the many public relays that already work, or run your own on your own backend by downloading a popular implementation from GitHub.&lt;/p&gt;

&lt;p&gt;One more thing worth mentioning is media content in chat messages. A Nostr event is always JSON with a fixed schema, and the protocol does not expect you to store binary data (images, files) inside an event. To solve this there is &lt;a href="https://github.com/nostr-protocol/nips/blob/master/96.md" rel="noopener noreferrer"&gt;NIP-96&lt;/a&gt; — a standard that describes HTTP File Storage: a spec for file hosts compatible with Nostr. The client uploads a file to a NIP-96 server, gets a URL, and puts it in the event content. Public NIP-96 servers exist, and in general this works.&lt;/p&gt;

&lt;p&gt;But in practice, if a project already uses central infrastructure (AWS S3, Firebase Cloud Storage, and so on), it is simpler and safer to store media there and just put a link in the Nostr event. It is a plain approach, but for most products it is a reasonable one.&lt;/p&gt;

&lt;p&gt;For one-on-one (one2one) chats Nostr gives you everything out of the box. But for group chats there are limits in practice, and getting around them needs extra logic.&lt;/p&gt;

&lt;p&gt;I would point out two such problems:&lt;br&gt;
1) the performance of message encryption&lt;br&gt;
2) group moderation&lt;/p&gt;

&lt;p&gt;The first one is this. To use asymmetric encryption (&lt;a href="https://github.com/nostr-protocol/nips/blob/master/04.md" rel="noopener noreferrer"&gt;Encrypted Direct Message&lt;/a&gt; or &lt;a href="https://github.com/nostr-protocol/nips/blob/master/44.md" rel="noopener noreferrer"&gt;Encrypted Payloads (Versioned)&lt;/a&gt;), you have to encrypt the message separately for each user. As the number of group members grows, this can hurt both performance and the overall reliability of the approach. That said, from my own experience, for groups of 20 people or fewer I did not see real performance problems. I have read that up to 100 people should also be fine. At the same time, for extra reliability I would rather use central solutions outside the Nostr protocol to encrypt message content (even though that is less secure).&lt;/p&gt;

&lt;p&gt;The second problem is moderating the group and adding or removing members. The existing solution, &lt;a href="https://github.com/nostr-protocol/nips/blob/master/17.md" rel="noopener noreferrer"&gt;Private Direct Messages&lt;/a&gt;, is basically a group conversation without a separate, managed group object. The group's metadata (name, and so on) is not stored as one thing; it is computed from the tags of the messages themselves, and the tags of the newest message "win". As a result, you cannot "hard" add or remove members, and in fact any member can redefine the group metadata — just by sending a new message with the tags they want. The root of the problem is in the base of the Nostr protocol: only the creator of an event can change it. So if you create an event — a chat room model — and link messages to it by a reference to the room, then only its creator can moderate that event. That means there is only one moderator. And even this solution is a bit artificial — I have not seen a NIP (a Nostr standard) for it.&lt;/p&gt;

&lt;p&gt;There is also a solution to this problem — &lt;a href="https://github.com/nostr-protocol/nips/blob/master/29.md" rel="noopener noreferrer"&gt;NIP-29&lt;/a&gt;. It means running your own, separate relay for group moderation (though public relay-29 servers exist too, for example &lt;code&gt;wss://relay.groups.nip29.com&lt;/code&gt;). The relay itself checks whether a moderator is allowed to change the group state. Such a relay is responsible only for the group state, not for the users' messages, which are stored on normal relays. But this is, rather, a central solution (otherwise you lose the single source of truth and risk getting out of sync).&lt;/p&gt;

&lt;p&gt;Overall, relay-29 is a good central solution for group chats with roles and moderation, and it really works.&lt;/p&gt;
&lt;h3&gt;
  
  
  Feed, posts
&lt;/h3&gt;

&lt;p&gt;In my view, a social network is first of all posts, likes, comments, and reposts.&lt;/p&gt;

&lt;p&gt;Let's try to figure out how well the Nostr protocol fits these goals and what the limits are. We already know that relays store event models and can return their JSON over WebSockets.&lt;br&gt;
For clarity, here is an example of a possible post JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;

        &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1781787656&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1897f99..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"pubkey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9c2e0a7..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Some text..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"41b737f50..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"737f50..."&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1897f..."&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30023:f72...:abcd"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30023:er34...:abcd"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"g"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"u4pr"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"g"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"u4pru"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;  
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All tags are optional. Let's go over the ones in the example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;e&lt;/code&gt; — a reference to another event by its &lt;code&gt;id&lt;/code&gt;; used for threads, quotes, and replies&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;a&lt;/code&gt; — a reference to an addressable (replaceable) event in the &lt;code&gt;kind:pubkey:d&lt;/code&gt; format; in the example above these are references to &lt;code&gt;kind:30023&lt;/code&gt; events (long posts / articles per &lt;a href="https://github.com/nostr-protocol/nips/blob/master/23.md" rel="noopener noreferrer"&gt;NIP-23&lt;/a&gt;) by different authors&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;g&lt;/code&gt; — a &lt;a href="https://en.wikipedia.org/wiki/Geohash" rel="noopener noreferrer"&gt;geohash&lt;/a&gt; of the location the post is linked to; the longer the string, the more precise the coordinate (&lt;code&gt;u4pr&lt;/code&gt; is a rough region, &lt;code&gt;u4pru&lt;/code&gt; is a bit more precise)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, to understand the limits of Nostr, let's look at a few cases:&lt;br&gt;
1) To show, for example, posts (usually &lt;code&gt;kind:1&lt;/code&gt;) by specific authors (see &lt;code&gt;authors&lt;/code&gt;) in a feed, you send a request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"REQ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"feed-sub-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"9c2e0a7..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pubkey1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pubkey2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kinds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"until"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1718700000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So far so good. With &lt;code&gt;until + limit&lt;/code&gt; it is easy to set up time-based pagination here, and it will work reliably.&lt;/p&gt;

&lt;p&gt;2) Now let's make it harder. Say we need to build a feed that shows posts by specific authors OR posts by users with a nearby location:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"REQ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"feed-sub-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"9c2e0a7..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pubkey1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pubkey2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kinds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"until"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1718700000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kinds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"#g"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"u4pr"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"until"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1718700000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here a problem shows up right away. Inside one filter object all conditions work as AND — that is, authors + kinds + until must all match for one event. But to get OR between "posts by the authors we want" and "posts with the geohash we want", you have to use two separate filters in an array. The relay then returns the union of both results.&lt;br&gt;
And this is exactly where reliable &lt;code&gt;until+limit&lt;/code&gt; pagination breaks. Each filter in the array is limited on its own: in a typical implementation the relay applies limit: 20 to each of the two filters separately (the exact behavior when merging results from several filters depends on the relay implementation), and then mixes the results. So imagine the first filter (by authors) collected 20 newer posts, and the second (by geohash) also collected 20, but older ones. The final output — after sorting by time and cutting to a common limit on the client — may take only part of one source and miss part of the other. On the next request the new &lt;code&gt;until&lt;/code&gt; is computed from the oldest post you received. For one of the filters this &lt;code&gt;until&lt;/code&gt; will be wrong: it will either cover already shown posts again, or skip part of the events that never reached the client. You get holes in the feed, and they add up with each new load.&lt;/p&gt;

&lt;p&gt;A working but more time-consuming way is pagination through a time window (&lt;code&gt;since + until&lt;/code&gt;) that grows step by step — it widens if too few events fall into it. This approach has its own downsides: an unpredictable number of requests to the relay for one feed page, trouble showing a loading indicator, and the risk of a long "freeze" during quiet periods.&lt;/p&gt;

&lt;p&gt;A third problem comes up at the level of the geohash filter itself: matching the &lt;code&gt;g&lt;/code&gt; tag works by exact string equality, not by prefix. If an event has only &lt;code&gt;["g", "u4pr"]&lt;/code&gt; (a rough location), and the filter asks for &lt;code&gt;"#g": ["u4pruyd"]&lt;/code&gt; (a more precise one), the relay will not return that event — even though &lt;code&gt;u4pruyd&lt;/code&gt; is physically inside &lt;code&gt;u4pr&lt;/code&gt;. People get around this by publishing several geohash tags of different length (all the prefixes) on one event. But then the client that makes the request has to list all the prefixes of the area it wants — this scales poorly and bloats the filter when you try to cover a wide area.&lt;/p&gt;

&lt;p&gt;In the event model above you can see the &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;e&lt;/code&gt; tags. With them you can link a post to some other event (for example, to another post, if the post is a comment on it — usually through an &lt;code&gt;e&lt;/code&gt; tag, or a comment on some changeable event — through an &lt;code&gt;a&lt;/code&gt; tag). The &lt;code&gt;e&lt;/code&gt; and &lt;code&gt;a&lt;/code&gt; tags are described in more detail in &lt;a href="https://github.com/nostr-protocol/nips/blob/master/01.md" rel="noopener noreferrer"&gt;NIP-01&lt;/a&gt;. A post can also have likes, which are usually done through &lt;code&gt;kind=7&lt;/code&gt; events, and comments (&lt;code&gt;e&lt;/code&gt;). To get the number of likes and comments and show them in the feed, you need to open subscriptions to the matching events and then count them. So after you get the post and learn its &lt;code&gt;id&lt;/code&gt;, you send a request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"REQ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"post-stats-sub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kinds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"#e"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"1897f99..."&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But likes and comments are far from the only things that can reference (or be referenced by) a given post. Through &lt;code&gt;e&lt;/code&gt;/&lt;code&gt;a&lt;/code&gt; tags, a post can be pointed to by reposts (&lt;code&gt;kind=6/16&lt;/code&gt;), zap receipts (&lt;code&gt;kind=9735&lt;/code&gt;), reactions with custom content besides likes, quotes inside other posts, and so on. For each kind of link you basically need a separate subscription with its own &lt;code&gt;kinds&lt;/code&gt; filter, or a wider filter with no &lt;code&gt;kinds&lt;/code&gt; limit, which you then have to filter and parse on the client. Already at this stage you can see that to fully show one post card in a feed you may need not one but a whole set of parallel requests to the relay.&lt;br&gt;
There is also a less obvious danger here. Since &lt;code&gt;e&lt;/code&gt;/&lt;code&gt;a&lt;/code&gt; links are not limited by the protocol in any way, nothing stops events from referencing each other in a cycle — post A comments on post B, which itself is marked as a comment on post A (by accident, a client bug, or on purpose, as spam or an attack). A client that naively walks the tree of linked events (for example, to build a full thread) can fall into an endless loop if it does not add protection in advance — tracking the ids it has already visited, or limiting the recursion depth.&lt;/p&gt;

&lt;p&gt;So while Nostr is a good fit for simple feeds, more complex ones can run into trouble: very greedy content loading and, as a result, a very large number of requests and poor performance.&lt;/p&gt;

&lt;p&gt;As for the features we are used to in top social networks — a smart feed and many flexible filters — it seems you cannot build them with Nostr alone, without custom add-ons and without losing decentralization.&lt;/p&gt;

&lt;p&gt;Then again, maybe it is better without a smart feed: it is not great that you often cannot find a post you liked a second time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Community
&lt;/h3&gt;

&lt;p&gt;One of the features built on Nostr that I had to work with is &lt;a href="https://github.com/nostr-protocol/nips/blob/master/72.md" rel="noopener noreferrer"&gt;NIP-72&lt;/a&gt;.&lt;br&gt;
In short, &lt;a href="https://github.com/nostr-protocol/nips/blob/master/72.md" rel="noopener noreferrer"&gt;NIP-72&lt;/a&gt; is a NIP that describes moderated topic communities on top of normal Nostr events. A community is defined by an addressable event (&lt;code&gt;kind:34550&lt;/code&gt;), which lists its moderators. Any user can publish a post that links to this community — that is, propose a publication in the community. But for a post to actually count as part of the community and show up in its feed, one of the moderators must publish a separate approval event (&lt;code&gt;kind:4550&lt;/code&gt;). This event contains the original post in full and is signed by the moderator's key. When building the community feed, clients rely on these approval events, not on the posts with the tag. So selection and moderation here are fully on the side of public, verifiable signatures.&lt;/p&gt;

&lt;p&gt;In principle, this works. But when a manager comes and says that any moderator should be able to moderate the community without the creator's approval, a problem appears. &lt;a href="https://github.com/nostr-protocol/nips/blob/master/72.md" rel="noopener noreferrer"&gt;NIP-72&lt;/a&gt; does not assume that someone from the moderator list can edit the community definition itself. The moderator list in &lt;code&gt;kind:34550&lt;/code&gt; is just &lt;code&gt;p&lt;/code&gt; tags listed inside an event signed by its owner. Only the holder of the private key that originally signed it can change this event (and therefore the moderator list) — that is, the community creator, not any of the listed moderators. This is a direct result of a base Nostr limit: only the holder of the key that is authoritative for an event's state can edit or redefine that state, and the protocol does not split this authority into roles at all. Trying to solve this with Nostr alone, without going beyond the protocol, leads nowhere good in the end. In practice the problem was solved with a central add-on — an extra service that adds extra signatures to events on top of the protocol model. But even with this solution I was very unhappy both with the &lt;em&gt;communities&lt;/em&gt; feature itself and with the final implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  One more similar use case — organizations
&lt;/h3&gt;

&lt;p&gt;I had to deal with this case too. Here is a short description of the task. There are organizations and sub-organizations, and posts and other similar events are published on their behalf. Only members can publish on behalf of an organization or sub-organization. Members have roles, and depending on the role they can invite other members.&lt;/p&gt;

&lt;p&gt;Building this with Nostr alone is a doubtful idea. The protocol has no built-in model for roles, for an "organization → sub-organization" hierarchy, or for invitations; all it has is signing with one key as the only way to authorize. Here it is worth mentioning that Nostr did have a "native" way to delegate signing — &lt;a href="https://github.com/nostr-protocol/nips/blob/master/26.md" rel="noopener noreferrer"&gt;NIP-26 (Delegated Event Signing)&lt;/a&gt;, but it never caught on and is now marked as deprecated. So even the protocol's own delegation mechanism was, in the end, seen as a failure, and in practice the task is solved centrally — for example, with &lt;a href="https://github.com/erskingardner/keycast" rel="noopener noreferrer"&gt;keycast&lt;/a&gt;, a service for delegating signing in Nostr, made specially for team work with keys. But even keycast does not fully solve the task, in particular the problem of roles. To give teams control over who can sign what, its authors had to build their own system of policies and permissions around keys — something the protocol does not have. This says a lot: even a tool made specially for delegating signing ends up coming down to writing a full backend from scratch. So managing roles and an organization hierarchy will also need central solutions — and not only for delegating signing, but also separately for managing roles. The amount of such work is comparable to writing a separate backend for this task, and using Nostr as the backend, or even as part of it, looks unjustified here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusions
&lt;/h3&gt;

&lt;p&gt;If you go through all these cases — from notes and one2one chats to groups, feeds, and organizations — a fairly clear pattern appears.&lt;/p&gt;

&lt;p&gt;Nostr really is a ready backend out of the box, and in two forms at once. It can be a fully decentralized solution based on a network of public relays. Or it can be a central one — if you run your own private relay and use the protocol just as a handy, ready format for data, sync, and authentication on top of your own infrastructure. The second option, by the way, is not as much of a compromise as it may seem: even if you give up decentralization, you still get a ready event model, signatures, encryption, and many ready client libraries. That is, you save time not only on infrastructure, but also on designing the communication protocol itself.&lt;/p&gt;

&lt;p&gt;And for a number of tasks this solution really fits well — notes, chats (especially one2one), simple post feeds without claims to a complex feed. Here Nostr closes the classic problems (sync, encryption, identity) with almost no need to write your own backend at all.&lt;/p&gt;

&lt;p&gt;But along the way I also collected a few lessons for myself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not try to build a full social network on the level of Facebook, or any top social network, on Nostr.&lt;/strong&gt; As soon as a smart feed, flexible filtering, role models with hierarchy, and other features long standard for top social networks come into play, the protocol starts to fall apart. Not because the NIP authors missed something, but because the model itself (a relay as a dumb store of signed events, with no server that has the full and sole right to complex business logic) is not made for this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not try to get around the protocol's limits.&lt;/strong&gt; The temptation to "stretch" Nostr to the business logic you need with custom add-ons — be it a synthetic moderator model, extra signatures on top of events, or third-party delegation services — is strong, and almost always technically possible. But, as the NIP-72 and organizations cases show, at some point these add-ons start to weigh more than the protocol they were meant to make simpler. And, more importantly, getting around the limits is not free. Besides losing decentralization and compatibility with the rest of the Nostr ecosystem (other clients, relays, NIPs), such add-ons are always one more surface for bugs — bugs in code that no one but you has tested at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before you start building, weigh both paths honestly: use Nostr with add-ons, or think about a separate backend for the specific task.&lt;/strong&gt; Your own backend for roles, hierarchy, and moderation is written without looking back at someone else's protocol limits like "one key owner". It is often faster than building workarounds around Nostr — especially if the task fits the "signed events + relay" model poorly from the start.&lt;/p&gt;

&lt;p&gt;The bottom line: Nostr is a great tool when the task lands in its "native" niche — and surprisingly awkward when you try to stretch it onto something it was not meant for. As with any tool, the main thing is to understand in advance which of these two groups your task falls into, instead of finding out after a month of work.&lt;/p&gt;

&lt;h3&gt;
  
  
  The app from this article
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/AlexeyYuPopkov/nostr_notes" rel="noopener noreferrer"&gt;Private Notes (Nostr)&lt;/a&gt; is open source — you can look through the code to see the approach in practice.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;A Russian version of this article is available on Habr: &lt;a href="https://habr.com/ru/articles/1050348/" rel="noopener noreferrer"&gt;habr.com/ru/articles/1050348&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nostr</category>
      <category>decentralization</category>
      <category>backend</category>
      <category>flutter</category>
    </item>
  </channel>
</rss>
