State Layer
Every object the SDK hands you that holds data (a feed, an activity, a list) owns its state and keeps it current. You read the state, and the SDK updates it as responses arrive and WebSocket events come in. You do not merge responses yourself.
Reading state
Each of these objects exposes the same three accessors:
| Accessor | Type | Use |
|---|---|---|
state |
the state object | The current value, read synchronously |
stream |
Stream<T> |
Every subsequent value |
notifier |
StateNotifier<T> |
For the Flutter builders below |
final feed = client.feed(group: 'user', id: 'john');
await feed.getOrCreate();
// The current value, read synchronously
final activities = feed.state.activities;
// A stream of every subsequent value
final subscription = feed.stream.listen((state) {
print('${state.activities.length} activities');
});
// Or the StateNotifier itself, for the Flutter builders below
final notifier = feed.notifier;
await subscription.cancel();
feed.dispose();Call dispose() on a feed, activity or list when you are done with it. This is separate from StreamFeedsClient.dispose(), which is terminal and releases the whole client. Do not call that from a widget's dispose.
FeedState
Populated by getOrCreate() and kept current from then on.
| Field | Type | Description |
|---|---|---|
fid |
FeedId |
The feed this state belongs to |
feedQuery |
FeedQuery |
The query the feed was created from |
feed |
FeedData? |
The feed's own data: name, description, counts, location |
activities |
List<ActivityData> |
The activities, in the order the feed returned them |
aggregatedActivities |
List<AggregatedActivityData> |
The groups, for an aggregated feed |
followers |
List<FollowData> |
Follows where this feed is the target |
following |
List<FollowData> |
Follows where this feed is the source |
followRequests |
List<FollowData> |
Pending requests to follow this feed |
members |
List<FeedMemberData> |
The feed's members |
pinnedActivities |
List<ActivityPinData> |
The pinned activities |
notificationStatus |
NotificationStatusResponse? |
Unread and unseen counts, for a notification feed |
activitiesPagination |
PaginationData? |
The cursors for the activity list |
membersPagination |
PaginationData? |
The cursors for the member list |
canLoadMoreActivities |
bool |
Derived from activitiesPagination |
canLoadMoreMembers |
bool |
Derived from membersPagination |
final feed = client.feed(group: 'user', id: 'john');
await feed.getOrCreate();
final state = feed.state;
final title = state.feed?.name ?? state.fid.rawValue;
for (final activity in state.activities) {
print('$title: ${activity.text}');
}
// `canLoadMoreActivities` and `canLoadMoreMembers` are derived from the
// pagination cursors, so you never have to read those yourself
if (state.canLoadMoreActivities) {
await feed.queryMoreActivities(limit: 10);
}ActivityState
| Field | Type | Description |
|---|---|---|
activity |
ActivityData? |
The activity itself, null until get() returns |
comments |
List<CommentData> |
Its comments |
commentsPagination |
PaginationData? |
The cursors for the comment list |
canLoadMoreComments |
bool |
Derived from commentsPagination |
final activity = client.activity(
activityId: 'activity_123',
fid: const FeedId.user('john'),
);
await activity.get();
// `activity` is null until `get()` returns
final state = activity.state;
print(state.activity?.text);
for (final comment in state.comments) {
print('${comment.user.id}: ${comment.text}');
}
if (state.canLoadMoreComments) {
await activity.queryMoreComments(limit: 10);
}
activity.dispose();There is no standalone poll state in the Flutter SDK. A poll travels on ActivityData.poll, and PollList covers querying polls across activities.
Enum-like fields are Strings
The models you read off state use extension types over String for their enum-like fields: ActivityData.visibility, FeedData.visibility, CommentData.status, FeedMemberData.status, FollowData.status and pushPreference, ActivityData.restrictReplies.
They are not Dart enums, so there is no .values, no .name, no .index, and no catch-all member. A value the server adds later arrives verbatim rather than collapsing to something unknown, which means a switch over one is never exhaustive and always needs a default arm.
await feed.getOrCreate();
for (final activity in feed.state.activities) {
// `visibility` and `visibilityTag` are two separate fields: a `tag:premium`
// activity has `visibility` of `tag` and `visibilityTag` of `premium`.
print('${activity.visibility} / ${activity.visibilityTag}');
// The visibility values are extension types over String, not a Dart enum, so
// an unrecognized value the server adds later arrives verbatim rather than
// collapsing to a catch-all. A switch over one always needs a default arm.
final label = switch (activity.visibility) {
ActivityDataVisibility.public => 'Everyone',
ActivityDataVisibility.private => 'Only me',
ActivityDataVisibility.tag => 'Tagged: ${activity.visibilityTag}',
_ => 'Unrecognized: ${activity.visibility}',
};
print(label);
}Because they implement String, comparing one against a plain string works, and so does using one anywhere a String is expected.
List state
The paginated queries all share one shape: build one from a query, get() the first page, queryMore…() for the next, and read the results off state.
// Every list API has the same shape: create it from a query, call `get()` for
// the first page, then `queryMore…` for the next.
final feedList = client.feedList(
FeedsQuery(
filter: Filter.equal(FeedsFilterField.createdById, 'john'),
limit: 10,
),
);
await feedList.get();
for (final feed in feedList.state.feeds) {
print(feed.name);
}
if (feedList.state.canLoadMore) {
await feedList.queryMoreFeeds(limit: 10);
}
feedList.dispose();These are the list objects, each created from the client:
| Object | Created with | Holds |
|---|---|---|
ActivityList |
client.activityList |
activities |
ActivityCommentList |
client.activityCommentList |
comments, threaded |
ActivityReactionList |
client.activityReactionList |
reactions |
BookmarkList |
client.bookmarkList |
bookmarks |
BookmarkFolderList |
client.bookmarkFolderList |
bookmarkFolders |
CommentList |
client.commentList |
comments |
CommentReactionList |
client.commentReactionList |
reactions |
CommentReplyList |
client.commentReplyList |
replies |
FeedList |
client.feedList |
feeds |
FollowList |
client.followList |
follows |
MemberList |
client.memberList |
members |
ModerationConfigList |
client.moderationConfigList |
configs |
PollList |
client.pollList |
polls |
PollVoteList |
client.pollVoteList |
votes |
UserList |
client.userList |
users |
Querying users
UserList is the query over users. Unlike the others it paginates with limit and offset rather than cursors.
// Users are paginated with limit/offset rather than cursors
final userList = client.userList(
UsersQuery(
filter: Filter.autoComplete(UsersFilterField.name, 'Al'),
sort: [UsersSort.asc(UsersSortField.name)],
limit: 25,
),
);
await userList.get();
while (userList.state.canLoadMore) {
final result = await userList.queryMoreUsers();
if (result.isFailure) break;
}
for (final user in userList.state.users) {
print('${user.name ?? user.id} is ${user.online ? 'online' : 'offline'}');
}
userList.dispose();Using state in Flutter
notifier is a StateNotifier, so it works with flutter_state_notifier's builders and providers.
// Wiring a feed's state into the widget tree with `flutter_state_notifier`,
// which is a separate dependency:
//
// dependencies:
// flutter_state_notifier: ^1.0.0
class FeedActivities extends StatelessWidget {
const FeedActivities({super.key, required this.feed});
final Feed feed;
@override
Widget build(BuildContext context) {
return StateNotifierBuilder<FeedState>(
stateNotifier: feed.notifier,
builder: (context, state, child) {
return ListView.builder(
itemCount: state.activities.length,
itemBuilder: (context, index) {
final activity = state.activities[index];
return Text(activity.text ?? '');
},
);
},
);
}
}stream works with a plain StreamBuilder if you would rather not add the dependency.