Android
Caching Data Advanced

Database caching: Advanced

This page shows you how to build a local cache using a database. This has several advantages like storing raw data in a file and enabling paricular queries on stored channels and messages. The content is provided on the assumption that you use SQLite database as a local storage, you can easily follow these steps with any other database like Realm.


Serialize and deserialize SendBird objects

In order to enable you to store SendBird objects such as messages, channels, and users in a local storage, we provide serialization and deserialization methods through our SDK.

By using the serialize() method to convert a SendBird object to binary data like the following, you can store the data natively in your persistent database.

Light Color Skin
Copy
byte[] data = baseMessage.serialize();
BaseMessage deserializedMessage = BaseMessage.buildFromSerializedData(data);

Save and load messages with serialization and deserialization

Your database should have a table to save and load messages, which has columns corresponding to the properties of a message object.

Design a message table

A basic table to store messages should contain the following columns:

message_idchannel_urlmessage_tspayload

22312323345

sendbird_channel_2348023

1432039402823

Serialized data

23423445562

sendbird_channel_2348023

1432039403417

Serialized data

Caching procedures

  1. After fetching new messages by using a MessageListQuery, serialize and insert each message into your database. However, we recommend that you store 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 data on a row-by-row basis later on.
  2. Before loading messages in a channel, sort rows in the chronological order 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.

Note: Because messages in a channel can be updated or deleted, to keep your local database synced with data in SendBird server, your client app should check changes to the messages regularly and apply the changes to the database. You can retrieve change logs of messages in a channel by using the getMessageChangeLogsByToken() or getMessageChangeLogsByTimestamp() method, with which you can manage your local database updates.

Example 1: When joining a channel
Light Color Skin
Copy
// Load messages from your 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 maximum number of returned results 
);

// Create a list of `BaseMessage` objects by deserializing each.
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 SendBird server.
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 into 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 a new message
Light Color Skin
Copy
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
Light Color Skin
Copy
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

Your database should have a table to save and load channels, which has columns corresponding to the properties of a channel object.

Note: The following examples are based on a group channel. To cache an 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_urllast_message_tspayload

sendbird_channel_2348023

1432039402416

Serialized data

sendbird_channel_2348023

1432039403610

Serialized data

Caching procedures

  1. After fetching new channels by 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 the channel.getUrl() and channel.getLastMessage().getCreatedAt(). This allows you to query the data on a row-by-row basis later on.
  2. Before loading a list of channels, sort rows in the chronological order 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 and name changes, their deletions. We recommend updating a cache by completely replacing with new data when possible.
  4. When real-time changes are made to a channel list, update your cache.

Note: In group channels, information associated with them can be updated or the current user might leave them anytime. To keep your local database synced with data in SendBird server, your client app should check changes to the channels regularly and apply them to the database. You can retrieve change logs of the current user's group channels using getMyGroupChannelChangeLogsByToken() or getMyGroupChannelChangeLogsByTimestamp() method, with which you can manage your local database updates.

Example 1: When displaying a list of channels
Light Color Skin
Copy
// Load channels from your 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 maximum number of returned results
);

// Create a list of `BaseChannel` objects by deserializing each.
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 SendBird server.
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 into 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 adding or updating a channel
Light Color Skin
Copy
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 maximum number of returned resulting a channels
        );

        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 applied to the onChannelDeleted(), onUserJoined(), and onUserLeft().