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);
}
}),
);
},
),
),
],
),
);
}
}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
Channel types
| Type | Use case |
|---|---|
messaging | Direct messages and group chats |
livestream | Livestream chat with many viewers |
team | Team collaboration channels |
commerce | Customer 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.