---
id: initialize_stream_chat_widget_tree
title: Initialize Stream Chat in Part of the Widget Tree
---

If you’re creating a full-scale chat application, you probably want to have Stream Chat Flutter initialized at the top of your widget tree and the Stream user connected as soon as they open the application.

However, if you only need chat functionality in a part of your application, then it’ll be better to delay Stream Chat initialization to when it’s needed.
This guide demonstrates three alternative ways for you to initialize Stream Chat Flutter for a part of your widget tree and to only connect a user when needed.

## What To Keep In Mind?

Before investigating potential solutions, let’s first take a look at the relevant Stream Chat widgets and classes.

Most of the Stream Chat Flutter UI widgets rely on having a [StreamChat](../../customization/stream_chat_and_theming/) ancestor in the widget tree.
The **StreamChat** widget is an [InheritedWidget](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html) that exposes the **StreamChatClient** through **BuildContext**.
This widget also initializes the [StreamChatCore](../04-stream_chat_flutter_core/stream_chat_core.mdx) widget and the **StreamChatTheme**.

**StreamChatCore** is a **StatefulWidget** used to react to life cycle changes and system updates.
When the app goes into the background, the WebSocket connection is closed.
Conversely, a new connection is initiated when the app is back in the foreground.

What is important to take note of is that **a connection is only established if a user is connected**.

This means that if you have not yet called `client.connectUser(user, token)`, no connection will be made, and only background listeners will be registered to determine the app's foreground state.

## Option 1: Builder and Connect/Disconnect User

This option requires you to wrap your whole application with the **StreamChat** widget and to call `connectUser` and `disconnectUser` as needed.

This option is the easiest, however, it requires **StreamChat** to be at the top of each route and as a result, will have a slight overhead as it’ll create the above-mentioned Stream widgets that may not yet be needed.

### Exposing the StreamChat Widget

First, you must expose the client and base Stream Chat widgets to the whole application.

```dart
void main() {
  final client = StreamChatClient(
    'q29npdvqjr99',
    logLevel: Level.OFF,
  );

  runApp(MyApp(client: client));
}

class MyApp extends StatelessWidget {
  const MyApp({
    Key? key,
    required this.client,
  }) : super(key: key);

  final StreamChatClient client;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: (context, child) {
        return StreamChat(client: client, child: child);
      },
      home: const HomeScreen(),
    );
  }
}
```

In the above code, you:

1. Create a **StreamChatClient** instance
2. Pass the instance to the **StreamChat** widget
3. Expose **StreamChat** to the whole application within the **MaterialApp** `builder`

A few important things to note:

- The `builder` wraps the **StreamChat** widget for every route of our application. No matter where you are in the widget tree, you’ll be able to call `StreamChat.of(context)`.
- The state will only be created once for our application, as **StreamChat** is a **StatefulWidget** and the position of **StreamChat** in the widget tree remains the same throughout the application lifecycle.
- No connection will be made until you call `connectUser`.

### Connecting and Disconnecting Users

In **MaterialApp** above, the home page is set to **HomeScreen**. This screen could look something like the following:

```dart
class HomeScreen extends StatelessWidget {
  const HomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: const Center(
        child: Text('Home Screen'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.of(context).push(
              MaterialPageRoute(builder: (context) => const ChatSetup()));
        },
        child: const Icon(
          Icons.message,
        ),
      ),
    );
  }
}
```

This screen shows a floating action button that on click navigates the user to the **ChatSetup**. We want to connect and disconnect a Stream user only when they go to the chat setup screen.

```dart
class ChatSetup extends StatefulWidget {
  const ChatSetup({
    super.key,
  });

  @override
  State<ChatSetup> createState() => _ChatSetupState();
}

class _ChatSetupState extends State<ChatSetup> {
  late final Future<OwnUser> connectionFuture;
  late final client = StreamChat.of(context).client;

  @override
  void initState() {
    super.initState();
    connectionFuture = client.connectUser(
      User(id: 'USER_ID'),
      'TOKEN',
    );
  }

  @override
  void dispose() {
    client.disconnectUser();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: FutureBuilder(
        future: connectionFuture,
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          switch (snapshot.connectionState) {
            case ConnectionState.waiting:
              return const Center(
                child: CircularProgressIndicator(),
              );
            default:
              if (snapshot.hasError) {
                return Text('Error: ${snapshot.error}');
              } else {
                return const ChannelListPage();
              }
          }
        },
      ),
    );
  }
}
```

Within `initState` you call `connectUser`; once the future for `connectUser` has completed a connection to the Stream API is established, you can then display the relevant Stream Chat UI widgets.

Once the **ChatScreen** widget is disposed of, then `disconnectUser` will be called within the `dispose` method.

### Caution

In this example, `disconnectUser` will **only** be called when the **ChatSetup** widget is disposed.

You need to ensure that this widget (route) is completely disposed of, or you need to call `disconnectUser` and `connectUser` manually when navigating to relevant parts of your application.

For example, let’s say you have a button within one of the chat screens to navigate to a completely different part of your app, then you want to make sure the **ChatScreen** route is disposed of by forcing it to be removed:

```dart
Navigator.of(context).pushAndRemoveUntil(
  MaterialPageRoute(
    builder: ((context) => const HomeScreen()),
  ),
  (Route<dynamic> route) => false,
);
```

Or let’s say you wanted to pop back to the first route:

```dart
Navigator.of(context).popUntil((route) => route.isFirst);
```

Both of these will ensure the route is disposed of and the user is disconnected as a result.

## Option 2: A Nested Navigator

Another approach would be to introduce a new [Navigator](https://api.flutter.dev/flutter/widgets/Navigator-class.html).
This has the benefit that everything related to Stream chat is contained to a specific part of the widget tree.

### Defining Routes and Nested Routes

In this example, our application has the following routes.

```dart
const routeHome = '/';
const routePrefixChat = '/chat/';
const routeChatHome = '$routePrefixChat$routeChatChannels';
const routeChatChannels = 'chat_channels';
const routeChatChannel = 'chat_channel';
```

For the `/chat/` nested routes (**routePrefixChat**), this approach initializes Stream Chat in our application and introduces a nested navigator.

Let’s explore the code:

```dart
void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({
    Key? key,
  }) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  @override
  Widget build(BuildContext context) {
    return Provider.value(
      value: navigatorKey,
      child: MaterialApp(
        navigatorKey: navigatorKey,
        initialRoute: routeHome,
        onGenerateRoute: (settings) {
          late Widget page;
          if (settings.name == routeHome) {
            page = const HomeScreen();
          } else if (settings.name!.startsWith(routePrefixChat)) {
            final subRoute = settings.name!.substring(routePrefixChat.length);
            page = ChatSetup(
              setupChatRoute: subRoute,
            );
          } else {
            throw Exception('Unknown route: ${settings.name}');
          }

          return MaterialPageRoute<dynamic>(
            builder: (context) {
              return page;
            },
            settings: settings,
          );
        },
      ),
    );
  }
}
```

In the above code you’re:

1. Creating a navigator key, passing it to `MaterialApp`, and exposing it to the whole application using Provider (you can expose it however you want).
2. Creating `onGenerateRoute` that specifies what page to show depending on the route. Most importantly, if the route contains the **routePrefixChat,** it navigates to the **ChatSetup** page and passes in the remainder of the route.

For example, `Navigator.pushNamed(context, routeChatHome)` will navigate to the **ChatSetup** page and pass in the nested route **routeChatChannels.**

### Stream Chat Initialization, User Connection, and Nested Navigation

Within the **ChatSetup** widget, we’ll initialize Stream chat, connect a user, and create a new Navigator that handles the sub-navigation for the chat-specific pages.

```dart
class ChatSetup extends StatefulWidget {
  const ChatSetup({Key? key, required this.setupChatRoute}) : super(key: key);

  final String setupChatRoute;

  @override
  State<ChatSetup> createState() => _ChatSetupState();
}

class _ChatSetupState extends State<ChatSetup> {
  late final Future<OwnUser> connectionFuture;
  late final client = StreamChatClient(
    'KEY',
    logLevel: Level.OFF,
  );

  @override
  void initState() {
    super.initState();
    connectionFuture = client.connectUser(
      User(id: 'USER_ID'),
      'TOKEN',
    );
  }

  @override
  void dispose() {
    client.disconnectUser();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      child: StreamChat(
        client: client,
        child: FutureBuilder(
          future: connectionFuture,
          builder: (BuildContext context, AsyncSnapshot snapshot) {
            switch (snapshot.connectionState) {
              case ConnectionState.waiting:
                return const Center(
                  child: CircularProgressIndicator(),
                );
              default:
                if (snapshot.hasError) {
                  return Text('Error: ${snapshot.error}');
                } else {
                  return Navigator(
                    initialRoute: widget.setupChatRoute,
                    onGenerateRoute: _onGenerateRoute,
                  );
                }
            }
          },
        ),
      ),
    );
  }

  Route _onGenerateRoute(RouteSettings settings) {
    late Widget page;
    switch (settings.name) {
      case routeChatChannels:
        page = ChannelListPage(
          client: client,
        );
        break;
      case routeChatChannel:
        final channel = settings.arguments as Channel;
        page = StreamChannel(
          channel: channel,
          child: const ChannelPage(),
        );
        break;
      default:
        throw Exception('Unknown route: ${settings.name}');
    }
    return MaterialPageRoute<dynamic>(
      builder: (context) {
        return page;
      },
      settings: settings,
    );
  }
}
```

This **ChatSetup** widget is similar to what it was in the first option, the only difference is that we’re also introducing a nested navigator and handling those nested routes.

The steps for the above code are:

1. Create a **StreamChatClient** instance
2. Call `connectUser` within `initState` and await the result using a **FutureBuilder**
3. Introduce a **StreamChat** widget into the widget tree
4. Introduce a new **Navigator** for the chat-specific routes
5. Call `disconnectUser` within `dispose`

### Displaying Stream Chat UI Widgets and Global Navigation

Then finally, the **ChannelListPage** could look something like the following:

```dart
class ChannelListPage extends StatefulWidget {
  const ChannelListPage({
    super.key,
    required this.client,
  });

  final StreamChatClient client;

  @override
  State<ChannelListPage> createState() => _ChannelListPageState();
}

class _ChannelListPageState extends State<ChannelListPage> {
  late final _controller = StreamChannelListController(
    client: widget.client,
    filter: Filter.in_(
      'members',
      [StreamChat.of(context).currentUser!.id],
    ),
    channelStateSort: const [SortOption('last_message_at')],
  );

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _popChatPages() {
    final nav = context.read<GlobalKey<NavigatorState>>();
    nav.currentState!.pop();
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () async {
        _popChatPages();
        return true;
      },
      child: Scaffold(
        appBar: AppBar(
          leading: BackButton(
            onPressed: () {
              _popChatPages();
            },
          ),
        ),
        body: RefreshIndicator(
          onRefresh: _controller.refresh,
          child: StreamChannelListView(
            controller: _controller,
            onChannelTap: (channel) {
              Navigator.pushNamed(context, routeChatChannel,
                  arguments: channel);
            },
          ),
        ),
      ),
    );
  }
}
```

There are two important things to note in the above code:

- We’re accessing the global **NavigatorState** using Provider and calling `pop()` on the back press. This will ensure that this entire route is disposed of and that the Stream connection is closed.
- Within `onChannelTap` we’re calling `pushNamed` and passing in the route to display a single channel page. This uses the navigator introduced in **ChatSetup**, which is the closest navigator within the widget tree.

A few things to take note of:

- You’ll always need to access the global navigator if you want to dispose of this route. you need to ensure that this route is popped or replaced, otherwise, the Stream connection will remain active for as long as the chat route is on the stack (or manually disconnected).

## Option 3: Navigator 2.0 - Using GoRouter

This final example will be a combination of the first two options. The following code is an example of how to initialize Stream Chat in a part of the widget tree using the [GoRouter package](https://pub.dev/packages/go_router). This solution will change depending on how you do routing in your Flutter application and which package (if any) you use.

### Application Routes and Conditional Stream Initialization

For this example we have the following routes and nested routes:

```dart
|_ '/' -> home page
	|_ 'settings/' - settings page
	|_ 'chat/' - chat home, shows the channels list page
		|_ 'channel/' - specific channel page
```

In the **MyApp** widget, we’ll initialize **GoRouter** and the **StreamChatClient**.
However, we will only expose the **StreamChat** widget for certain routes.
Additionally, we’ll create a **ChatSetup** widget that connects and disconnects the user.
This widget will also **only** be injected for the `/chat` routes.

```dart
class MyApp extends StatefulWidget {
  const MyApp({
    Key? key,
  }) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _client = StreamChatClient(
    'q29npdvqjr99',
    logLevel: Level.OFF,
  );
  bool wasPreviousRouteChat = false;

  late final _router = GoRouter(
    initialLocation: '/',
    navigatorBuilder: (context, state, child) {
      if (state.location.startsWith('/chat')) {
        wasPreviousRouteChat = true;
        return StreamChat(
          client: _client,
          child: ChatSetup(client: _client, child: child),
        );
      } else {
        if (wasPreviousRouteChat) {
          wasPreviousRouteChat = false;
          return StreamChat(
            client: _client,
            child: child,
          );
        } else {
          return child;
        }
      }
    },
    routes: [
      GoRoute(
        path: '/',
        builder: (context, state) => const HomeScreen(),
        routes: [
          GoRoute(
            path: 'settings',
            builder: (context, state) => const SettingsPage(),
          ),
          GoRoute(
            path: 'chat',
            builder: (context, state) => const ChannelListPage(),
            routes: [
              GoRoute(
                path: 'channel',
                builder: (context, state) {
                  final channel = state.extra as Channel;
                  return StreamChannel(
                    channel: channel,
                    child: const ChannelPage(),
                  );
                },
              ),
            ],
          ),
        ],
      ),
    ],
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routeInformationParser: _router.routeInformationParser,
      routeInformationProvider: _router.routeInformationProvider,
      routerDelegate: _router.routerDelegate,
    );
  }
}
```

If you’re unfamiliar with GoRouter it will help to first read the documentation and then come back to this guide.

There are a few important things to note in the above code:

- Define our routes and nested routes
- Specify the initial route with `initialLocation`
- Use the `navigatorBuilder` to wrap certain routes with **StreamChat** and **ChatSetup**.
  - The `wasPreviousRouteChat` \***\*boolean is used to determine if the previous route was a chat route. This is important because when you press the back button from the `/chat` route and navigate to the `/` route, the **StreamChat** widget still needs to be accessible while the navigation transition occurs. However, if you then navigate to the `/setting` route, you no longer need the **StreamChat\*\* widget and can safely remove it.

### Navigating With GoRouter

Within the **HomeScreen** you can create a button that on press navigates to the `/chat` route:

```dart
GoRouter.of(context).go('/chat');
```

### Connecting and Disconnecting Users

Same as before, we’ll use the **ChatSetup** widget to connect and disconnect a user.
This time the widget also takes in a **child** widget to display once the connection is finished.
The child widget is dependent on the route we’re navigating to.

```dart
class ChatSetup extends StatefulWidget {
  const ChatSetup({
    Key? key,
    required this.client,
    required this.child,
  }) : super(key: key);

  final StreamChatClient client;
  final Widget child;

  @override
  State<ChatSetup> createState() => _ChatSetupState();
}

class _ChatSetupState extends State<ChatSetup> {
  late final Future<OwnUser> connectionFuture;

  @override
  void initState() {
    super.initState();
    connectionFuture = widget.client.connectUser(
      User(id: 'USER_ID'),
      'TOKEN',
    );
  }

  @override
  void dispose() {
    widget.client.disconnectUser();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      child: FutureBuilder(
        future: connectionFuture,
        builder: (BuildContext context, AsyncSnapshot snapshot) {
          switch (snapshot.connectionState) {
            case ConnectionState.waiting:
              return const Center(
                child: CircularProgressIndicator(),
              );
            default:
              if (snapshot.hasError) {
                return Text('Error: ${snapshot.error}');
              } else {
                return widget.child;
              }
          }
        },
      ),
    );
  }
}
```

This will connect the Stream chat user within `initState` and disconnect the user on `dispose`. This is similar to the previous examples we explored.

### Displaying the Stream UI Widgets

The **ChannelListPage** can look something like the following:

```dart
class ChannelListPage extends StatefulWidget {
  const ChannelListPage({
    super.key,
  });

  @override
  State<ChannelListPage> createState() => _ChannelListPageState();
}

class _ChannelListPageState extends State<ChannelListPage> {
  late final client = StreamChat.of(context).client;
  late final _controller = StreamChannelListController(
    client: client,
    filter: Filter.in_(
      'members',
      [StreamChat.of(context).currentUser!.id],
    ),
    channelStateSort: const [SortOption('last_message_at')],
  );

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: RefreshIndicator(
        onRefresh: _controller.refresh,
        child: StreamChannelListView(
          controller: _controller,
          onChannelTap: (channel) {
            GoRouter.of(context).go('/chat/channel', extra: channel);
          },
        ),
      ),
    );
  }
}
```

This is the same as before, the only difference is that the navigation is slightly different; now we’re navigating to the `/chat/channel` route for individual channel pages.

## Conclusion

This guide demonstrated three different ways to initialize Stream Chat in a part of the Flutter widget tree.

There are two key takeaways

1. Accessing **StreamChat** depends on your location in the widget tree
2. How you ultimately decide to expose **StreamChat** and connect users will be up to your application architecture and how you manage routing.

The above are only examples that can be refined and tweaked to suit your needs.
