# 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        |

```dart label="Dart"
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();
```

>
> **Note:** 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`                           |

```dart label="Dart"
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`               |

```dart label="Dart"
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();
```

>
> **Info:** 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 type`s 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 `enum`s, 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.

```dart label="Dart"
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`.

```dart label="Dart"
// 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.

```dart label="Dart"
// 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`](https://pub.dev/packages/flutter_state_notifier)'s builders and providers.

```dart label="Dart"
// 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.

---

For the most recent version of this documentation, visit [https://getstream.io/activity-feeds/docs/flutter/state/](https://getstream.io/activity-feeds/docs/flutter/state/).