Database caching: Advanced

This section shows you how to build your own local cache using a database. This has several advantages like storing raw data in a file and enabling queries on stored channels and messageso particularly. Our examples are written based on the SQLite, it's not difficult to follow these steps with any database of your choice, such as Realm.


Serialize and deserialize SendBird objects

To store SendBird objects such as messages, channels, and users in local storage, we provide serialization and deserialization methods through our SDK. Use serialize() to convert a SendBird object to binary data, which can then be natively stored in your persistent database.

byte[] baseMessage.serialize()
BaseMessage BaseMessage.buildFromSerializedData(byte[] data)

Save and load messages with serialization and deserialization

Design a message table

A basic table to store messages contains the following columns:

message_id channel_url message_ts payload
123123 sendbird_channel_234802384 1432039402823 Serialized data
234234 sendbird_channel_234802384 1432039403417 Serialized data

Caching procedures

  1. After fetching new messages using a MessageListQuery, serialize and insert each message into your database. However, we recommend storing the message ID, timestamp, and channel URL in separate columns by using message.getMessageId(), message.getCreatedAt(), and message.getChannelUrl(). This allows you to query the dataset later on.

  2. Before loading messages within a channel, order rows chronologically by message_ts. Then, deserialize each message and display them in your UI.

  3. When loading previous messages that are not currently stored in the local database, obtain the timestamp of the earliest stored message. Then, query for messages created before that value.

  4. Likewise, when loading new messages, query for messages with a later timestamp than the most recent message.

Example 1: When entering a channel
// Get messages from local database
final SQLiteDatabase database = dbHelper.getWritableDatabase();

String selection = COLUMN_NAME_CHANNEL_URL + " = ?";
String[] selectionArgs = { CURRENT_CHANNEL_URL };

String sortOrder = COLUMN_NAME_TIMESTAMP + " DESC";

Cursor cursor = database.query(
        TABLE_NAME,
        null, // The columns to return; all if null.
        selection, // The columns for the WHERE clause
        selectionArgs, // The values for the WHERE clause
        null, // Don't group the rows
        null, // Don't filter by row groups
        sortOrder, // The sort order
        "30" // The limit
);

// Create a List of BaseMessages by deserializing each item.
List<BaseMessage> prevMessageList = new ArrayList<>();

while (cursor.moveToNext()) {
    byte[] data = cursor.getBlob(cursor.getColumnIndex(COLUMN_NAME_PAYLOAD));
    BaseMessage message = BaseMessage.buildFromSerializedData(data);
    prevMessageList.add(message);
}

cursor.close();

// Pass messages to adapter for displaying them in a RecyclerView, ListView, and so on.
mMessageListAdapter.addMessages(prevMessageList);

// Get new messages from the SendBird servers
long latestStoredTs = prevMessageList.get(0).getCreatedAt(); // Get the timestamp of the last stored message.
MessageListQuery query = mChannel.createMessageListQuery();
query.next(latestStoredTs, 30, false, new MessageListQuery.MessageListQueryResult() {
    @Override
    public void onResult(List<BaseMessage> list, SendBirdException e) {
        if (e != null) {
            // Error!
            return;
        }
        // New messages successfully fetched.
        mMessageListAdapter.addMessages(list);

        // Insert each new message in your local database
        for (BaseMessage message : list) {
            // Store each new message into the local database
            ContentValues values = new ContentValues();
            values.put(COLUMN_NAME_ID, message.getMessageId());
            values.put(COLUMN_NAME_CHANNEL_URL, message.getChannelUrl());
            values.put(COLUMN_NAME_TIMESTAMP, message.getCreatedAt());
            values.put(COLUMN_NAME_PAYLOAD, message.serialize());

            database.insert(TABLE_NAME, null, values);
        }
    }
});

database.close();
Example 2: When receiving new messages
SendBird.addChannelHandler(CHANNEL_HANDLER_ID, new SendBird.ChannelHandler() {
    @Override
    public void onMessageReceived(BaseChannel baseChannel, BaseMessage baseMessage) {
        if (baseChannel.getUrl().equals(mChannelUrl)) {
            // Pass the message to your adapter.
            mMessageListAdapter.addMessage(baseMessage);

            // Store the message in your local database.
            // It is a good idea to have a helper class or method for database transactions.
            final SQLiteDatabase database = dbHelper.getWritableDatabase();

            ContentValues values = new ContentValues();
            values.put(COLUMN_NAME_ID, baseMessage.getMessageId());
            values.put(COLUMN_NAME_CHANNEL_URL, baseMessage.getChannelUrl());
            values.put(COLUMN_NAME_TIMESTAMP, baseMessage.getCreatedAt());
            values.put(COLUMN_NAME_PAYLOAD, baseMessage.serialize());

            database.insert(TABLE_NAME, null, values);

            database.close();
        }
    }
});
Example 3: When sending a message
mChannel.sendUserMessage(messageBody, new BaseChannel.SendUserMessageHandler() {
    @Override
    public void onSent(UserMessage userMessage, SendBirdException e) {
        if (e != null) {
            // Error!
            return;
        }

        // Display sent message to RecyclerView
        mMessageListAdapter.addMessage(userMessage);

        // Store the message in your local database.
        // It is a good idea to have a helper class or method for database transactions.
        final SQLiteDatabase database = dbHelper.getWritableDatabase();

        ContentValues values = new ContentValues();
        values.put(COLUMN_NAME_ID, userMessage.getMessageId());
        values.put(COLUMN_NAME_CHANNEL_URL, userMessage.getChannelUrl());
        values.put(COLUMN_NAME_TIMESTAMP, userMessage.getCreatedAt());
        values.put(COLUMN_NAME_PAYLOAD, userMessage.serialize());

        database.insert(TABLE_NAME, null, values);

        database.close();
    }
});

Caveats

Currently, it is difficult to sync deleted or edited messages. We are working to provide this feature in both our SDKs and APIs, and hope to release it soon.


Save and load channels with serialization and deserialization

Note: The examples in this section are based on Group Channel. To cache Open Channel, slightly improvise from the directions below (such as changing last_message_ts to channel_created_at).

Design a channel table

A basic table to store channels contains the following columns:

channel_url last_message_ts payload
sendbird_channel_234802384 1432039402416 Serialized data
sendbird_channel_234802384 1432039403610 Serialized data

Caching procedures

  1. After fetching new channels using a OpenChannelListQuery or GroupChannelListQuery, serialize and insert each channel into your database. As with messages, we recommend storing the channel URL and timestamp of the last message in separate columns by using channel.getUrl() and channel.getLastMessage().getCreatedAt(). This allows you to query the dataset later on.

  2. Before loading a list of channels, order rows chronologically by last_message_ts. Then, deserialize each channel and display them in your UI.

  3. Unlike messages, channels are relatively few in number and go through frequent property changes, such as cover URL changes, name changes, or deletions. Therefore, we recommend updating your cache by completely replacing the dataset when possible.

  4. When real-time changes are made to a channel list, update your cache.

Example 1: When entering the channel list screen
// Load channels from local database
final SQLiteDatabase database = dbHelper.getWritableDatabase();

String sortOrder = COLUMN_NAME_LAST_MESSAGE_TIMESTAMP + " DESC";

Cursor cursor = database.query(
        TABLE_NAME,
        null, // The columns to return; all if null.
        null, // The columns for the WHERE clause
        null, // The values for the WHERE clause
        null, // Don't group the rows
        null, // Don't filter by row groups
        sortOrder, // The sort order
        "30" // The limit
);

// Create a List of BaseChannels by deserializing each item.
final List<BaseChannel> prevChannelList = new ArrayList<>();

while (cursor.moveToNext()) {
    byte[] data = cursor.getBlob(cursor.getColumnIndex(COLUMN_NAME_PAYLOAD));
    BaseChannel channel = BaseChannel.buildFromSerializedData(data);
    prevChannelList.add(channel);
}

cursor.close();

// Pass channels to adapter to display them in a RecyclerView, ListView, and so on.
mChannelListAdapter.addChannels(prevChannelList);

// Get new channels from the SendBird servers
GroupChannelListQuery query = GroupChannel.createMyGroupChannelListQuery();
query.next(new GroupChannelListQuery.GroupChannelListQueryResultHandler() {
    @Override
    public void onResult(List<GroupChannel> list, SendBirdException e) {
        if (e != null) {
            // Error!
            return;
        }

        // Replace the current (cached) dataset
        mChannelListAdapter.clear();
        mChannelListAdapter.addChannels(prevChannelList);

        // Clear the current cache
        database.delete(TABLE_NAME, null, null);

        // Insert each new channel in your local database
        for (GroupChannel channel : list) {
            // Store each new channel into the local database
            ContentValues values = new ContentValues();
            values.put(COLUMN_NAME_CHANNEL_URL, channel.getUrl());
            values.put(COLUMN_NAME_LAST_MESSAGE_TIMESTAMP, channel.getLastMessage().getCreatedAt());
            values.put(COLUMN_NAME_PAYLOAD, channel.serialize());

            database.insert(TABLE_NAME, null, values);
        }
    }
});

database.close();
}
Example 2: On real-time events such as additions or updates
SendBird.addChannelHandler(CHANNEL_HANDLER_ID, new SendBird.ChannelHandler() {
    ...
    @Override
    public void onChannelChanged(BaseChannel channel) {
        final SQLiteDatabase database = dbHelper.getWritableDatabase();

        String selection = COLUMN_NAME_CHANNEL_URL + " = ?";
        String[] selectionArgs = { CURRENT_CHANNEL_URL };

        // Get the changed channel from the local database using its URL.
        Cursor cursor = database.query(
                TABLE_NAME,
                null, // The columns to return; all if null.
                selection, // The columns for the WHERE clause
                selectionArgs, // The values for the WHERE clause
                null, // Don't group the rows
                null, // Don't filter by row groups
                null, // The sort order
                "1" // The limit
        );

        byte[] data = cursor.getBlob(cursor.getColumnIndex(COLUMN_NAME_PAYLOAD));

        ContentValues values = new ContentValues();
        values.put(COLUMN_NAME_CHANNEL_URL, channel.getUrl());
        long lastMessageTs = ((GroupChannel) channel).getLastMessage().getCreatedAt();
        values.put(COLUMN_NAME_LAST_MESSAGE_TIMESTAMP, lastMessageTs);
        values.put(COLUMN_NAME_PAYLOAD, channel.serialize());

        if (data != null) {
            // If the channel is in the current cache, update it.
            database.update(TABLE_NAME, values, selection, selectionArgs);
        } else {
            // If the channel is not currently cached, add it.
            database.insert(TABLE_NAME, null, values);
        }

        database.close();
    }
});

Note: A similar process can be followed for onChannelDeleted(), onUserJoined(), and onUserLeft().