Extra Data

Extra Data is additional information that can be added to the default data of Stream. It is a map of key-value pairs that can be attached to messages, users, channels, and most other domain models in the Stream SDK.

In the Flutter SDK, extra data is represented by the following map, Map<String, Object?>. The values are plain JSON-encodable Dart types: a String, a number (int or double), a bool, a List, a Map, or null. Because the values are untyped (Object?), you cast them to the type you expect when reading.

Map<String, Object?> extraData;

Adding Extra Data

Adding extra data can be done through the Server-Side SDKs or through the Client SDKs. In the Flutter Stream Chat SDK, you can add extra data when creating or updating a message, user, channel, or any other model.

As a simple example, let's see how you can add a new email field to the currently logged in user.

final currentUser = client.state.currentUser!;
await client.partialUpdateUser(
  currentUser.id,
  set: {
    'name': 'John Doe',
    'email': 'john.doe@example.com',
  },
);

For a more complete example now, let's imagine you want to add ticket information to a message.

final channel = client.channel('messaging', id: 'your-channel-id');
await channel.watch();

await channel.sendMessage(
  Message(
    text: 'A new message',
    extraData: {
      'ticket': {
        'name': 'Rock Concert',
        'price': 20,
      },
    },
  ),
);

Reading Extra Data

All of the most important domain models in the SDK have an extraData property that you can read the additional information added by your app.

You can read extra data properties very easily. The following code snippet shows how to get an email from a user's extra data.

final email = user.extraData['email'] as String? ?? '';
print(email);
Tip:

In order to access the email even more easily, you can add an extension on our models to provide an extra property, in this case, you can add an email property to the User model like this:

extension UserEmail on User {
  String? get email => extraData['email'] as String?;
}

To see how you can get data with different types from extra data, we can pick the example of the ticket information again and see how you can get it from extra data.

final ticket = message.extraData['ticket'] as Map<String, dynamic>?;
final name = ticket?['name'] as String? ?? '';
final price = (ticket?['price'] as num?)?.toDouble() ?? 0.0;

The Flutter SDK does not provide typed accessors. Values are stored as Object?, so you cast each one to the type you expect. Numbers decoded from JSON can be either int or double, so cast to num and convert when you need a specific numeric type. Below are the common patterns:

final string = extraData['key'] as String?;
final integer = (extraData['key'] as num?)?.toInt();
final number = (extraData['key'] as num?)?.toDouble();
final boolean = extraData['key'] as bool?;
final map = extraData['key'] as Map<String, dynamic>?;
final list = extraData['key'] as List?;
final stringList = (extraData['key'] as List?)?.cast<String>();

Advanced Example

Most likely your app has more complex data structures compared to the ones described above. So, let's see an example of how you could map your domain models to extra data and vice-versa by imagining that a message can have details of a booking flight.

class BookingFlight {
  const BookingFlight({
    required this.flightNumber,
    required this.departureDate,
    required this.arrivalDate,
    required this.price,
    required this.passengers,
    required this.destinations,
  });

  final String flightNumber;
  final DateTime? departureDate;
  final DateTime? arrivalDate;
  final double price;
  final List<Passenger> passengers;
  final List<String> destinations;
}

class Passenger {
  const Passenger({
    required this.name,
    required this.age,
  });

  final String name;
  final int age;
}

Next, let's see how we can provide Extra Data mappings for these models:

extension PassengerMapper on Passenger {
  static Passenger? fromExtraData(Map<String, dynamic> extraData) {
    final name = extraData['name'] as String?;
    final age = extraData['age'] as num?;
    if (name == null || age == null) return null;
    return Passenger(name: name, age: age.toInt());
  }

  Map<String, Object?> toExtraData() => {
        'name': name,
        'age': age,
      };
}

extension BookingFlightMapper on BookingFlight {
  static BookingFlight? fromExtraData(Map<String, dynamic> extraData) {
    final flightNumber = extraData['flightNumber'] as String?;
    final price = extraData['price'] as num?;
    if (flightNumber == null || price == null) return null;

    final destinations =
        (extraData['destinations'] as List?)?.cast<String>() ?? [];
    final passengers = (extraData['passengers'] as List?)
            ?.whereType<Map<String, dynamic>>()
            .map(PassengerMapper.fromExtraData)
            .whereType<Passenger>()
            .toList() ??
        [];

    return BookingFlight(
      flightNumber: flightNumber,
      price: price.toDouble(),
      departureDate:
          DateTime.tryParse(extraData['departureDate'] as String? ?? ''),
      arrivalDate: DateTime.tryParse(extraData['arrivalDate'] as String? ?? ''),
      destinations: destinations,
      passengers: passengers,
    );
  }

  Map<String, Object?> toExtraData() => {
        'flightNumber': flightNumber,
        'departureDate': departureDate?.toIso8601String(),
        'arrivalDate': arrivalDate?.toIso8601String(),
        'price': price,
        'destinations': destinations,
        'passengers': passengers.map((it) => it.toExtraData()).toList(),
      };
}

Then, we can add an extension on the Message model and add a bookingFlight property:

extension BookingFlightMessage on Message {
  BookingFlight? get bookingFlight {
    final data = extraData['flight'] as Map<String, dynamic>?;
    if (data == null) return null;
    return BookingFlightMapper.fromExtraData(data);
  }
}

Finally, if we want to create a message with the booking flight information, we can do it like this:

final BookingFlight bookingFlight = ...;
final extraData = {'flight': bookingFlight.toExtraData()};

final channel = client.channel('messaging', id: 'your-channel-id');
await channel.watch();
await channel.sendMessage(
  Message(text: 'A new message', extraData: extraData),
);