Creating Channels

This recipe shows how to build a user search screen that lets the current user select one or more members and create a new channel with them.

Implementation

class NewChannelPage extends StatefulWidget {
  const NewChannelPage({super.key});

  @override
  State<NewChannelPage> createState() => _NewChannelPageState();
}

class _NewChannelPageState extends State<NewChannelPage> {
  final _searchController = TextEditingController();
  List<User> _searchResults = [];
  final List<User> _selectedUsers = [];
  bool _isLoading = false;

  Future<void> _searchUsers(String query) async {
    if (query.isEmpty) {
      setState(() => _searchResults = []);
      return;
    }
    setState(() => _isLoading = true);
    final client = StreamChat.of(context).client;
    final response = await client.queryUsers(
      filter: Filter.and([
        Filter.autoComplete('name', query),
        Filter.notEqual('id', client.state.currentUser!.id),
      ]),
      sort: [SortOption<User>.asc('name')],
    );
    setState(() {
      _searchResults = response.users;
      _isLoading = false;
    });
  }

  Future<void> _createChannel() async {
    if (_selectedUsers.isEmpty) return;
    final client = StreamChat.of(context).client;

    final memberIds = [
      client.state.currentUser!.id,
      ..._selectedUsers.map((u) => u.id),
    ];

    // Use messaging type for direct messages or group chats
    final channel = client.channel(
      'messaging',
      extraData: {
        'members': memberIds,
        if (_selectedUsers.length > 1)
          'name': _selectedUsers.map((u) => u.name).join(', '),
      },
    );

    await channel.watch();

    if (!mounted) return;
    Navigator.pushReplacement(
      context,
      MaterialPageRoute(
        builder: (_) => StreamChannel(
          channel: channel,
          child: const ChannelPage(),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('New Message'),
        actions: [
          TextButton(
            onPressed: _selectedUsers.isNotEmpty ? _createChannel : null,
            child: const Text('Create'),
          ),
        ],
      ),
      body: Column(
        children: [
          if (_selectedUsers.isNotEmpty)
            SizedBox(
              height: 56,
              child: ListView(
                scrollDirection: Axis.horizontal,
                padding: const EdgeInsets.symmetric(horizontal: 8),
                children: _selectedUsers
                    .map(
                      (user) => Padding(
                        padding: const EdgeInsets.only(right: 8),
                        child: Chip(
                          label: Text(user.name ?? user.id),
                          onDeleted: () => setState(
                            () => _selectedUsers.remove(user),
                          ),
                        ),
                      ),
                    )
                    .toList(),
              ),
            ),
          Padding(
            padding: const EdgeInsets.all(8),
            child: TextField(
              controller: _searchController,
              onChanged: _searchUsers,
              decoration: const InputDecoration(
                prefixIcon: Icon(Icons.search),
                hintText: 'Search by name...',
              ),
            ),
          ),
          if (_isLoading)
            const Center(child: CircularProgressIndicator())
          else
            Expanded(
              child: ListView.builder(
                itemCount: _searchResults.length,
                itemBuilder: (context, index) {
                  final user = _searchResults[index];
                  final isSelected = _selectedUsers.contains(user);
                  return ListTile(
                    leading: StreamUserAvatar(user: user),
                    title: Text(user.name ?? user.id),
                    trailing: isSelected
                        ? const Icon(Icons.check_circle, color: Colors.blue)
                        : null,
                    onTap: () => setState(() {
                      if (isSelected) {
                        _selectedUsers.remove(user);
                      } else {
                        _selectedUsers.add(user);
                      }
                    }),
                  );
                },
              ),
            ),
        ],
      ),
    );
  }
}

Channel types

TypeUse case
messagingDirect messages and group chats
livestreamLivestream chat with many viewers
teamTeam collaboration channels
commerceCustomer support

Choose the channel type that matches the built-in permission set closest to your requirements, then fine-tune in the Stream Dashboard.

Watching vs. querying an existing channel

channel.watch() creates the channel if it does not exist, or returns the existing channel. To look up an existing channel without creating it, use client.queryChannels with a filter.