Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

/// The entry screen from Step 4 of the
/// [Flutter Chat tutorial](https://getstream.io/chat/sdk/flutter/tutorial/).
///
/// [StreamChannelListController] owns the query, pagination, and live updates;
/// [StreamChannelListView] renders it. Tapping a channel pushes
/// [StreamChannelPage], the SDK's ready-made conversation screen - it wires up
/// the header, message list, composer, and threads for you.
///
/// Shared by all three `main_step*.dart` entry points.
class ChannelListPage extends StatefulWidget {
const ChannelListPage({super.key});

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

class _ChannelListPageState extends State<ChannelListPage> {
/// Queries channels the current user belongs to, newest activity first.
/// The controller owns pagination and live updates.
late final _listController = StreamChannelListController(
client: StreamChat.of(context).client,
filter: Filter.in_('members', [StreamChat.of(context).currentUser!.id]),
channelStateSort: const [SortOption.desc('last_message_at')],
limit: 20,
);

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

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: context.streamColorScheme.backgroundApp,
appBar: const StreamChannelListHeader(),
body: StreamChannelListView(
controller: _listController,
onChannelTap: (channel) => Navigator.of(context).push(
MaterialPageRoute(
/// `StreamChannel` scopes the tapped channel to the subtree and
/// calls `watch()` on it, so `StreamChannelPage` needs no arguments.
builder: (_) => StreamChannel(
channel: channel,
child: const StreamChannelPage(),
),
),
),
),
);
}
}
46 changes: 46 additions & 0 deletions packages/stream_chat_flutter/example/lib/tutorial_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// ignore_for_file: public_member_api_docs
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_persistence/stream_chat_persistence.dart';

/// Credentials and client setup shared by the tutorial entry points.
///
/// This mirrors the top of the tutorial's own `main.dart` in Step 4 of the
/// [Flutter Chat tutorial](https://getstream.io/chat/sdk/flutter/tutorial/).
/// The three `tutorial_main_step*.dart` entry points differ only in `MyApp`,
/// so the setup lives here rather than being repeated in each of them.

/// Credentials from Step 3 of the tutorial.
/// - API key: `getstream env --target flutter` writes it to `dart_defines.json`,
/// passed in with `--dart-define-from-file` and read here. Falls back to the demo key.
/// - User + token: set [userId] to the user you minted a token for and paste that
/// token below - both must match, or the connection is rejected. Or keep the
/// demo pair below as-is.
const _envApiKey = String.fromEnvironment('STREAM_API_KEY');

const apiKey = _envApiKey == '' ? 'b67pax5b2wdq' : _envApiKey;
const userId = 'tutorial-flutter';
const userName = 'Tutorial Flutter';
const userToken =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidHV0b3JpYWwtZmx1dHRlciJ9.S-MJpoSwDiqyXpUURgO5wVqJ4vKlIVFLSEyrFYCOE1c';

/// Builds the client, turns on offline storage, and connects the user.
Future<StreamChatClient> connectTutorialUser() async {
/// Offline support: channels and messages are cached on device, so the app
/// opens with content even without a connection. Attach the persistence
/// client *before* `connectUser` - attaching it afterwards does nothing for
/// the current session.
final client = StreamChatClient(apiKey, logLevel: Level.INFO)
..chatPersistenceClient = StreamChatPersistenceClient(
logLevel: Level.INFO,
connectionMode: ConnectionMode.regular,
);

/// Development token from `getstream token`. In production, fetch the
/// token from your backend after login - never hardcode secrets.
await client.connectUser(
User(id: userId, name: userName),
userToken,
);

return client;
}
40 changes: 40 additions & 0 deletions packages/stream_chat_flutter/example/lib/tutorial_main_step4.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_example/tutorial_channel_list_page.dart';
import 'package:stream_chat_flutter_example/tutorial_client.dart';

/// Step 4 of the
/// [Flutter Chat tutorial](https://getstream.io/chat/sdk/flutter/tutorial/) -
/// a working app on the default theme.
///
/// Run with: `flutter run -t lib/tutorial_main_step4.dart`
///
/// Two screens are all you write: the channel list, and the client setup that
/// puts [StreamChat] above the app. The conversation itself is
/// [StreamChannelPage], which the list navigates to.
///
/// `tutorial_main_step5.dart` and `tutorial_main_step6.dart` differ from this file only in
/// `MyApp` - that is the whole surface theming and component overrides touch.
Future<void> main() async {
final client = await connectTutorialUser();

runApp(MyApp(client: client));
}

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

/// The client created in `main`. Holds the connection and the local cache.
final StreamChatClient client;

@override
Widget build(BuildContext context) {
return MaterialApp(
/// `StreamChat` must be an ancestor of every Stream widget. Putting it in
/// `builder` keeps it above whatever `home` renders.
builder: (context, child) => StreamChat(client: client, child: child),
home: const ChannelListPage(),
);
}
}
85 changes: 85 additions & 0 deletions packages/stream_chat_flutter/example/lib/tutorial_main_step5.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_example/tutorial_channel_list_page.dart';
import 'package:stream_chat_flutter_example/tutorial_client.dart';

/// Step 5 of the
/// [Flutter Chat tutorial](https://getstream.io/chat/sdk/flutter/tutorial/) -
/// the same app, themed.
///
/// Run with: `flutter run -t lib/tutorial_main_step5.dart`
///
/// Theming works in two layers, and you rarely need more than the first:
///
/// 1. Design tokens - a [StreamTheme] registered as a [ThemeData] extension.
/// Give it a brand color and Stream derives its whole semantic palette
/// from that swatch.
/// 2. Per-widget overrides - a [StreamChatThemeData] passed to
/// [StreamChat.themeData], merged on top. Reach for this only when one
/// component needs to differ.
///
/// Both land inside [StreamChannelPage] as well, since it resolves the ambient
/// theme like any other Stream widget. Only `MyApp` changes from
/// `tutorial_main_step4.dart`.
Future<void> main() async {
final client = await connectTutorialUser();

runApp(MyApp(client: client));
}

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

/// The client created in `main`. Holds the connection and the local cache.
final StreamChatClient client;

@override
Widget build(BuildContext context) {
/// One brand color per brightness. Stream derives its whole semantic
/// palette from the swatch, so this single value restyles bubbles,
/// sending indicators, unread badges, and the composer cursor.
final brand = StreamColorSwatch.fromColor(Colors.green);
final brandDark = StreamColorSwatch.fromColor(
Colors.green,
brightness: Brightness.dark,
);

/// Per-widget override, merged on top of the derived palette. Reusing
/// `brand.shade100` is what keeps the tiles and the message bubbles in
/// the same green family.
final customTheme = StreamChatThemeData(
channelListItemTheme: StreamChannelListItemThemeData(
titleStyle: const TextStyle(fontWeight: FontWeight.bold),
backgroundColor: WidgetStateProperty.all(brand.shade100),
),
);

return MaterialApp(
theme: ThemeData(
brightness: Brightness.light,
extensions: [
StreamTheme(
brightness: Brightness.light,
colorScheme: StreamColorScheme.light(brand: brand),
),
],
),
darkTheme: ThemeData(
brightness: Brightness.dark,
extensions: [
StreamTheme(
brightness: Brightness.dark,
colorScheme: StreamColorScheme.dark(brand: brandDark),
),
],
),
builder: (context, child) => StreamChat(
client: client,
themeData: customTheme,
child: child,
),
home: const ChannelListPage(),
);
}
}
89 changes: 89 additions & 0 deletions packages/stream_chat_flutter/example/lib/tutorial_main_step6.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_example/tutorial_channel_list_page.dart';
import 'package:stream_chat_flutter_example/tutorial_client.dart';
import 'package:stream_chat_flutter_example/tutorial_rounded_avatar.dart';

/// Step 6 of the
/// [Flutter Chat tutorial](https://getstream.io/chat/sdk/flutter/tutorial/) -
/// the themed app, with one widget swapped out.
///
/// Run with: `flutter run -t lib/tutorial_main_step6.dart`
///
/// Theming changes tokens (colors, fonts, shapes). To change an actual
/// *widget*, register a component builder and override only the slot you want -
/// every other widget keeps its default. Here [RoundedAvatar] replaces the
/// circular avatar with a rounded square.
///
/// `avatar` is one of the shared slots you pass directly. Chat-specific ones -
/// `messageItem`, `channelListItem`, `messageComposer` - go through
/// `extensions: streamChatComponentBuilders(...)` instead.
///
/// Component builders resolve through the factory, so they reach inside
/// [StreamChannelPage] too even though it owns its own header, list, and
/// composer. Only `MyApp` changes from `tutorial_main_step5.dart`.
Future<void> main() async {
final client = await connectTutorialUser();

runApp(MyApp(client: client));
}

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

/// The client created in `main`. Holds the connection and the local cache.
final StreamChatClient client;

@override
Widget build(BuildContext context) {
/// One brand color per brightness. Stream derives its whole semantic
/// palette from the swatch, so this single value restyles bubbles,
/// sending indicators, unread badges, and the composer cursor.
final brand = StreamColorSwatch.fromColor(Colors.green);
final brandDark = StreamColorSwatch.fromColor(
Colors.green,
brightness: Brightness.dark,
);

/// Per-widget override, merged on top of the derived palette. Reusing
/// `brand.shade100` is what keeps the tiles and the message bubbles in
/// the same green family.
final customTheme = StreamChatThemeData(
channelListItemTheme: StreamChannelListItemThemeData(
titleStyle: const TextStyle(fontWeight: FontWeight.bold),
backgroundColor: WidgetStateProperty.all(brand.shade100),
),
);

return MaterialApp(
theme: ThemeData(
brightness: Brightness.light,
extensions: [
StreamTheme(
brightness: Brightness.light,
colorScheme: StreamColorScheme.light(brand: brand),
),
],
),
darkTheme: ThemeData(
brightness: Brightness.dark,
extensions: [
StreamTheme(
brightness: Brightness.dark,
colorScheme: StreamColorScheme.dark(brand: brandDark),
),
],
),
builder: (context, child) => StreamChat(
client: client,
themeData: customTheme,
componentBuilders: StreamComponentBuilders(
avatar: (context, props) => RoundedAvatar(props: props),
),
child: child,
),
home: const ChannelListPage(),
);
}
}
Loading
Loading