Core module

Nabla Android Messaging core module

📘

This guide is about the data/domain layer

This page describes how to query the patient data, allowing you to build your own UI. If you'd rather use our built-in customisable UI components, check out the Messaging UI Components page.

Watchers and cached data

Our core SDK has a cache layer that persists the latest fetched data from the network to a disk cache, allowing to retrieve those data without network at any time.

Every watcher returns a Response object that contains 3 things:

  • A isDataFresh boolean property that indicates if the emitted data is fresh or not.
  • A refreshingState property that has 3 different states:
    • Refreshing: the data emitted comes from the cache, we are refreshing it in background. isDataFresh should be false when that happens, and you can expect another emit following with the result of the background refresh.
    • Refreshed: the data emitted comes from the network or is local only and no follow-up attempt to refresh it will be made. isDataFresh should be true when that happens.
    • ErrorWhileRefreshing: the background refresh of the data has failed and no follow-up attempt to refresh will be made. isDataFresh should be false when that happens.
  • A data property that contains the data itself

🚧

If no cached data are available and the network fetch fails, the Flow will throw an error, so you need to implement catch even for cached flows.

For the vast majority of apps, showing cached data is a good user experience, especially if you're showing a hint to the user when it's not fresh. If your app needs to show only fresh data, you can filter items that aren't fresh and you can relaunch the watcher to trigger a new network fetch if needed.

Watch a paginated list

Either for the list of conversations or the list of a conversation’s content, the functions watchConversations() and watchConversationItems(conversationId: ConversationId) will return a Flow of WatchPaginatedResult<T> where T is the content to be loaded gradually, e.g. conversations or messages.

The returned flow will emit a new value whenever the concerned data changes. Change can be of any type, for instance in the case of a conversation’s content it can be:

  • New messages arriving in the conversation either from the current user or distant users.
  • Loaded messages changing in any way, for instance:
    • their status changing from “Sending” to “Sent”;
    • or their content being deleted;
    • or their author changing their avatar, etc.
  • New pages loaded using the loadMore trigger.

WatchPaginatedResult also provides a loadMore callback to load more content, precisely an additional page. This callback is suspend and its Result<Unit> return type informs whether the operation was successful or not.

Watch patient conversations list

You can watch the list of conversations a user has access to. This watcher will be called every-time there's a change in those conversations and will always return all the data (You don't need to store them to perform a diff):

coroutineScope.launch {
  NablaClient.getInstance().messagingClient.watchConversations()
    .catch { error ->
      // TODO: handle error
    }
    .collect { response ->
      // You can update your UI with the list of conversations:
      val conversations = result.data.content

      // To load more conversation, you can use the loadMore callback
      // It will be null if there are no more elements to load
      val loadMoreCallback = result.data.loadMore
    }
}

The loadMoreCallback is simply a nullable suspend function you can call to load more elements.

coroutineScope.launch {
  if (loadMoreCallback != null) {
    loadMoreCallback()
      .onFailure { error -> /* TODO: handle error */ }
      .onSuccess {
        /* More conversations have been loaded,
        the watchConversations callback will be called
        with those new conversations */
      }
  }
}

🚧

Don't forget to update your loadMoreCallback reference every-time you get a new result.loadMore value.

Create a new conversation

If you want your Patient to be able to start a new conversation, you can allow to create it into your app directly:

NablaClient.getInstance().messagingClient.startConversation()

Note that this conversation is created locally on the device first and will be created server side once the Patient sends a first message in it.

Watch a conversation

To watch the items (message and conversation activity) of a conversation, the same pagination principles applies:

coroutineScope.launch {
  NablaClient.getInstance().messagingClient.watchConversationItems(conversationId)
    .catch { error ->
      // TODO: handle error
    }
    .collect { response ->
      // You can update your UI with the new data
      val items = response.data.content.items

      // To load more items, you can use the loadMore callback
      val loadMoreCallback = response.data.loadMore
    }
}

You can also watch for a conversation details update (like a Provider typing status):

coroutineScope.launch {
  NablaClient.getInstance().messagingClient.watchConversation(conversationId)
    .catch { error ->
      // TODO: handle error
    }
    .collect { response ->
      // You can update your UI with the new data
      val conversation = response.data
    }
}

Send a new message

Creating and sending your message

The user can send a message in an existing conversation:

coroutineScope.launch {
  NablaClient.getInstance().messagingClient
    .sendMessage(
      input = MessageInput.Text(text = "Text of the message"),
      conversationId = conversationId,
    )
    .onFailure { /* handle error */ }
    .onSuccess { /* message sent successfully 🙌 */ }
}

📘

Message sending is asynchronous

Note that here you don't need to handle failure or success directly: As soon as you call sendMessage(..) the new message will be locally added to the conversation: its status will be SendStatus.Sending and its id of type MessageId.Local.

It means that watchConversationItems will be called immediately after calling this method and the message will be included in the appropriate state, the SDK will then take care of sending it automatically.

Handling failure

If sending the message fails, the message will still be included in watchConversationItems but its status will
be SendStatus.ErrorSending. You can then retry sending it:

val erredMessage: Message = yourMessageInErrorSendingStatus

coroutineScope.launch {
  NablaMessagingClient.getInstance().retrySendingMessage(erredMessage.id, conversationId)
    .onFailure { /* No-op */ }
    .onSuccess { /* No-op */ }
}

Different types of messages

You can send following types of messages:

  • Text
  • Image
  • Video
  • Document
  • Audio

Here is how to create each of them:

val newTextMessage = MessageInput.Text(text = "Hello world!")

val newImageMessage = MessageInput.Media.Image(
  mediaSource = FileSource.Local(
    FileLocal.Image(
      Uri("file:///uri/from/android/img.jpg"),
      fileName = "My image",
      mimeType = MimeType.Image.Jpeg,
    ),
  ),
)

val newVideoMessage = MessageInput.Media.Video(
  mediaSource = FileSource.Local(
    FileLocal.Video(
      Uri("file:///uri/from/android/video.mp4"),
      fileName = "My video",
      mimeType = MimeType.Video.Mp4,
    ),
  ),
)

val newDocumentMessage = MessageInput.Media.Document(
  mediaSource = FileSource.Local(
    FileLocal.Document(
      Uri("file:///uri/from/android/file.pdf"),
      fileName = "My prescription",
      mimeType = MimeType.Application.Pdf,
    )
  ),
)

val newVoiceMessage = MessageInput.Media.Audio(
  mediaSource = FileSource.Local(
    FileLocal.Audio(
      Uri("file:///uri/from/android/audio.mp3"),
      fileName = "My voice message",
      mimeType = MimeType.Audio.Mp3,
      estimatedDurationMs = 42_000L,
    )
  ),
)