Android Offline Confused about "Android Offline"?
Let us know how we can improve our documentation:
Confused about "Android Offline"?
Let us know how we can improve our documentation:
The offline library exposes easy to use LiveData/StateFlow objects for messages, reads, typing, members, watchers and more. It also adds support for offline chat. This means you can send messages, reactions and even create channels while you're offline. When the user comes back online, the library will automatically recover lost events and retry sending messages. The offline storage also enables it to implement optimistic UI updates.
InstallCopied!Confused about "Install"?
Let us know how we can improve our documentation:
Confused about "Install"?
Let us know how we can improve our documentation:
Open up your app's module level build.gradle
file and add the following line:
1
2
3
dependencies {
implementation "io.getstream:stream-chat-android-offline:$stream_version"
}
For the latest version, check our GitHub releases page.
Building the ChatDomainCopied!Confused about "Building the ChatDomain"?
Let us know how we can improve our documentation:
Confused about "Building the ChatDomain"?
Let us know how we can improve our documentation:
The offline library builds on top of the client library. You can build your ChatDomain
object like this:
1
2
3
4
5
val chatClient = ChatClient.Builder(apiKey, appContext).build()
val chatDomain = ChatDomain.Builder(appContext, chatClient)
.offlineEnabled()
.userPresenceEnabled()
.build()
1
2
3
4
5
ChatClient chatClient = new ChatClient.Builder(apiKey, appContext).build();
ChatDomain chatDomain = new ChatDomain.Builder(appContext, chatClient)
.offlineEnabled()
.userPresenceEnabled()
.build();
You should only have a single ChatDomain
object in your application. Note that the ChatDomain
is user specific so you'll want to disconnect your ChatDomain
and create a new one when the user in your app changes. You can retrieve the current chatDomain from anywhere in your app:
1
val chatDomain = ChatDomain.instance()
1
ChatDomain chatDomain = ChatDomain.instance();
To disconnect from ChatDomain, just disconnect from ChatClient
and it will trigger a disconnection for ChatDomain
too:
1
2
ChatClient chatClient = ChatClient.instance();
chatClient.disconnect(); //It will disconnect from ChatClient AND ChatDomain
1
2
val chatClient = ChatClient.instance()
chatClient.disconnect() //It will disconnect from ChatClient AND ChatDomain
By default, the retry policy for the ChatDomain is set to retry three times and wait attempt * 1000
milliseconds in between attempts. It will also retry when the connection recovers. You can customize the default behavior like this:
1
2
3
4
5
6
7
8
9
chatDomain.retryPolicy = object : RetryPolicy {
override fun shouldRetry(client: ChatClient, attempt: Int, error: ChatError): Boolean {
return attempt < 3
}
override fun retryTimeout(client: ChatClient, attempt: Int, error: ChatError): Int {
return 1000 * attempt
}
}
1
2
3
4
5
6
7
8
9
10
11
chatDomain.setRetryPolicy(new RetryPolicy() {
@Override
public boolean shouldRetry(@NotNull ChatClient client, int attempt, @NotNull ChatError error) {
return attempt < 3;
}
@Override
public int retryTimeout(@NotNull ChatClient client, int attempt, @NotNull ChatError error) {
return 1000 * attempt;
}
});
The chatDomain exposes the following LiveData objects for the user:
Name | Type | Description |
---|---|---|
initialized | LiveData<Boolean> | If the connection with Stream's chat API has been initialized |
online | LiveData<Boolean> | If you are currently online |
totalUnreadCount | LiveData<Int> | The total count of unread messages for the current user |
channelUnreadCount | LiveData<Int> | The number of unread channels for the current user |
muted | LiveData<List<Mute>> | Who you've muted, automatically updated when new mute events are received. |
banned | LiveData<Boolean> | If you're currently banned or not |
Watching a ChannelCopied!Confused about "Watching a Channel"?
Let us know how we can improve our documentation:
Confused about "Watching a Channel"?
Let us know how we can improve our documentation:
All operations on the ChatDomain are exposed via useCases
. The following example shows how to get the messages, reads and typing information for a channel:
1
2
3
4
5
6
7
8
9
10
11
chatDomain.useCases.watchChannel(cid = "messaging:123", messageLimit = 0)
.enqueue { result ->
if (result.isSuccess) {
val channelController = result.data()
// LiveData objects to observe
channelController.messages
channelController.reads
channelController.typing
}
}
1
2
3
4
5
6
7
8
9
10
11
chatDomain.getUseCases().getWatchChannel().invoke("messaging:123", 0)
.enqueue(result -> {
if (result.isSuccess()) {
ChannelController channelController = result.data();
// LiveData objects to observe
channelController.getMessages();
channelController.getReads();
channelController.getTyping();
}
});
All use cases return a Call object. You can run a Call in one of three ways:
execute
synchronously in the current thread (make sure this is not the UI thread!),enqueue
asynchronously with a callback, which executes the call in the background and then notifies you on the UI thread,await
it inside a coroutine, which is a suspending operation.
All calls return a Result
object, which is either successful or an error, which you should always check this using isSuccess
or isError
, and then access either data()
or error()
. This behavior is identical to the low level client.
As you see in the example above, the channel controller exposes the messages, reads and typing LiveData objects. The full list of LiveData objects available is:
NAME | TYPE | DESCRIPTION |
---|---|---|
messages | LiveData<List<Message>> | list of messages sorted by message.createdAt |
messagesState | LiveData<MessagesState> | sealed class for various message states |
watcherCount | LiveData<Int> | the number of people currently watching the channel |
watchers | LiveData<List<User>> | the list of users currently watching this channel. note that for large channels you will need to paginate to get all watchers |
typing | LiveData<List<User>> | who is currently typing (current user is excluded from this) |
reads | LiveData<List<ChannelUserRead>> | how far every user in this channel has read |
read | LiveData<ChannelUserRead> | read status for the current user |
unreadCount | LiveData<Int> | unread count for this channel, calculated based on read state (this works even if you're offline) |
members | LiveData<List<Member>> | the list of members of this channel |
channelData | LiveData<ChannelData> | updated whenever we receive a channel.updated event |
loading | LiveData<Boolean> | if we are currently loading |
loadingOlderMessages | LiveData<Boolean> | if we are currently loading older messages |
loadingNewerMessages | LiveData<Boolean> | if we are currently loading newer messages |
endOfOlderMessages | LiveData<Boolean> | set to true if there are no more older messages to load |
endOfNewerMessages | LiveData<Boolean> | set to true if there are no more newer messages to load |
You can load more messages for a channel like this:
1
2
3
4
5
6
chatDomain.useCases.loadOlderMessages.invoke("messaging:123", 10)
.enqueue { result ->
if (result.isSuccess) {
val channel = result.data()
}
}
1
2
3
4
5
6
7
8
9
chatDomain.getUseCases().getLoadOlderMessages().invoke("messaging:123", 10)
.enqueue(new Call.Callback<Channel>() {
@Override
public void onResult(@NotNull Result<Channel> result) {
if (result.isSuccess()) {
Channel channel = result.data();
}
}
});
Sending a MessageCopied!Confused about "Sending a Message"?
Let us know how we can improve our documentation:
Confused about "Sending a Message"?
Let us know how we can improve our documentation:
This is how you can send a message
1
2
3
4
5
6
7
val message = Message(text = "Hello world")
chatDomain.useCases.sendMessage.invoke(message)
.enqueue { result ->
if (result.isSuccess) {
val message = result.data()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
Message message = new Message();
message.setText("Hello world");
chatDomain.getUseCases().getSendMessage().invoke(message)
.enqueue(new Call.Callback<Message>() {
@Override
public void onResult(@NotNull Result<Message> result) {
if (result.isSuccess()) {
Message message = result.data();
}
}
});
The following useCases help you update chat data
createChannel
sendMessage
editMessage
deleteMessage
sendReaction
deleteReaction
keystroke (sends a typing event as needed)
stopTyping (sends a stop typing event, only if needed)
markRead (marks the channel as read)
Querying ChannelsCopied!Confused about "Querying Channels"?
Let us know how we can improve our documentation:
Confused about "Querying Channels"?
Let us know how we can improve our documentation:
The other most commonly used interface for chat is showing a list of channels. You can query channels like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
val members = listOf("thierry")
val filter = Filters.and(
Filters.eq("type", "messaging"),
Filters.`in`("members", members),
)
val sort = QuerySort<Channel>()
chatDomain.useCases.queryChannels(filter, sort)
.enqueue { result ->
if (result.isSuccess) {
val queryChannelsController = result.data()
// LiveData objects to observe
queryChannelsController.channels
queryChannelsController.loading
queryChannelsController.endOfChannels
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
List<String> members = new ArrayList<>();
members.add("thierry");
FilterObject filter = Filters.and(
Filters.eq("type", "messaging"),
Filters.in("members", members)
);
QuerySort<Channel> sort = new QuerySort<>();
int limit = 10;
int messageLimit = 1;
chatDomain.getUseCases().getQueryChannels().invoke(filter, sort, limit, messageLimit)
.enqueue(new Call.Callback<QueryChannelsController>() {
@Override
public void onResult(@NotNull Result<QueryChannelsController> result) {
if (result.isSuccess()) {
final QueryChannelsController controller = result.data();
// LiveData objects to observe
controller.getChannels();
controller.getLoading();
controller.getEndOfChannels();
}
}
});
Loading more channels for a particular query can be done like this:
1
2
3
4
5
6
chatDomain.useCases.queryChannelsLoadMore.invoke(filter, sort)
.enqueue { result ->
if (result.isSuccess) {
val channels: List<Channel> = result.data()
}
}
1
2
3
4
5
6
7
8
9
chatDomain.getUseCases().getQueryChannelsLoadMore().invoke(filter, sort, limit, messageLimit)
.enqueue(new Call.Callback<List<Channel>>() {
@Override
public void onResult(@NotNull Result<List<Channel>> result) {
if (result.isSuccess()) {
final List<Channel> channels = result.data();
}
}
});
The following LiveData objects are available on a query channels controller
Name | Type | DESCRIPTION |
---|---|---|
channels | LiveData<List<Channel>> | The list of channels for this query. Will update when the underlying channel data changes or new channels are added. |
channelsState | LiveData<ChannelsState> | Similar to channels but exposes the various loading states via a sealed class |
loading | LiveData<Boolean> | If we are currently loading the first screen of channels |
loadingMore | LiveData<Boolean> | If we are currently loading more channels |
Unread CountsCopied!Confused about "Unread Counts"?
Let us know how we can improve our documentation:
Confused about "Unread Counts"?
Let us know how we can improve our documentation:
Stream provides 2 unread counts for a user. Here's how to retrieve LiveData objects for them:
1
2
3
// LiveData objects to observe
val totalUnreadCount = chatDomain.useCases.getTotalUnreadCount().execute().data()
val unreadChannelCount = chatDomain.useCases.getUnreadChannelCount().execute().data()
1
2
3
// LiveData objects to observe
LiveData<Integer> totalUnreadCount = chatDomain.getUseCases().getGetTotalUnreadCount().invoke().execute().data();
LiveData<Integer> unreadChannelCount = chatDomain.getUseCases().getGetUnreadChannelCount().invoke().execute().data();
ThreadsCopied!Confused about "Threads"?
Let us know how we can improve our documentation:
Confused about "Threads"?
Let us know how we can improve our documentation:
Here's how you can retrieve the messages for a thread:
1
2
3
4
5
6
7
8
9
10
chatDomain.useCases.getThread(cid, parentId).enqueue { result ->
if (result.isSuccess) {
val threadController = result.data()
// LiveData objects to observe
threadController.messages
threadController.loadingOlderMessages
threadController.endOfOlderMessages
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
chatDomain.getUseCases().getGetThread().invoke(cid, parentId)
.enqueue(new Call.Callback<ThreadController>() {
@Override
public void onResult(@NotNull Result<ThreadController> result) {
if (result.isSuccess()) {
final ThreadController threadController = result.data();
// LiveData objects to observe
threadController.getMessages();
threadController.getLoadingOlderMessages();
threadController.getEndOfOlderMessages();
}
}
});
Loading more messages for a thread can be done like this:
1
2
3
4
5
6
chatDomain.useCases.threadLoadMore.invoke(cid, parentId, messageLimit)
.enqueue { result ->
if (result.isSuccess) {
val messages: List<Message> = result.data()
}
}
1
2
3
4
5
6
7
8
9
chatDomain.getUseCases().getThreadLoadMore().invoke(cid, parentId, messageLimit)
.enqueue(new Call.Callback<List<Message>>() {
@Override
public void onResult(@NotNull Result<List<Message>> result) {
if (result.isSuccess()) {
final List<Message> messages = result.data();
}
}
});