why deepstreamHub? compare us getting started feature roadmap faq
use cases pricing
developers
company
enterprise blog contact

This tutorial will take you through building a realtime chat app for Android. We'll be implementing quite a few features and showing you just how easy it is to do this with deepstreamHub. After this, we'll be able to:

-view a list of users and whether they are online or offline

-chat with users on a one-to-one basis

-be notified when the person you're chatting to is typing

-edit your old messages and have them synced on other devices

This tutorial will be covering a lot of concepts in deepstreamHub and we'd definitely recommend being familiar with Records and Lists before giving this a go, prior Android or Java experience will be helpful as well.

Because Java and Android projects have a lot of boilerplate associated with them, for the sake of brevity we'll be cutting the cruft and including the most important parts for this tutorial. If you have any questions please take a look at the GitHub repository or get in touch.

Create a free account and get your API key

Connect to deepstreamHub and log in

The first thing we'll do is create a new Android application with a LoginActivity template, you can find more information on this here. We can then include the Java client sdk in our build.gradle file as follows.

compile 'io.deepstream:deepstream.io-client-java:2.0.4'

With the Java sdk there are a few different ways of instantiating the client, but because the same client will need to be passed between activities, we'll be using the DeepstreamFactory. To instantiate a client with this it's as simple as:

DeepstreamFactory deepstreamFactory = DeepstreamFactory.getInstance();
DeepstreamClient client = deepstreamFactory.getClient("<Your app url>");

Using the UserLoginTask already included in the LoginActivity, we can try to login a user with the details they provide. If they don't exist, we can use the deepstreamHub HTTP API to create them.

LoginResult result;
DeepstreamClient client;
try {
    client = deepstreamFactory.getClient("<Your app url>");
} catch (URISyntaxException e) {
    return false;
}

result = client.login(credentials);

if (!result.loggedIn()) {
    // either incorrect credentials entered
    if (result.getErrorEvent() == Event.INVALID_AUTH_DATA) {
        Toast.makeText(getApplicationContext(), "Incorrect login details", Toast.LENGTH_LONG).show();
        return false;
    }
    // or the user doesn't exist so we create them and log them in
    createUser(credentials);
    result = client.login(credentials);
    if (!result.loggedIn()) {
        Toast.makeTest(getApplicationContext(), "Error creating user", Toast.LENGTH_LONG).show();
        return false;
    }
}

The createUser method is very simple and just creates an HTTP POST to our deepstreamHub API. You can find out more about our HTTP API here

private void createUser(JsonObject credentials) {
    URL url = null;
    HttpURLConnection conn = null;
    BufferedWriter writer;
    try {
        String endpoint = "https://api.deepstreamhub/api/v1/user-auth/signup/" + "<Your api key>"
        url = new URL(endpoint);

        conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");
        conn.setRequestProperty("Content-Type", "application/json");
        conn.setDoOutput(true);
        conn.setDoInput(true);

        writer = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));
        writer.write(credentials.toString());
        writer.flush();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

When an authenticated user logs into into deepstreamHub, a client data object is returned containing the ID of the user as well as any other data specified while creating them. To access this client data from the LoginResult we can call the getData() method.

After this, we want to add the newly created user into our List of users and add the users email and id to our StateRegistry. The StateRegistry is just a static class that holds some state between Android activities. Similarly the User class is just a simple POJO containing the users ID, email and whether or not they are online.

Gson gson = new Gson();
JsonObject clientData = (JsonObject) gson.toJsonTree(result.getData());
String userId = clientData.get("id").getAsString();
String email = credentials.get("email").getAsString();

stateRegistry.setUserId(userId);
stateRegistry.setEmail(email);

User user = new User(userId, email, true);
Record record = client.record.getRecord("users/" + userId);
record.set(gson.toJsonTree(user));

// if this is the first time the user has logged in, they won't be in
// the list of users, so we need to add them
List users = client.record.getList("users");
if ( !Arrays.asList(users.getEntries()).contains(userId) ) {
    users.addEntry(userId);
}
return true;

In the onPostExecute method of our AsyncTask, we now just need to create an Intent for the ChatOverView activity:

@Override
protected void onPostExecute(final Boolean success) {
    if (success) {
        Intent intent = new Intent(ctx, ChatOverviewActivity.class);
        startActivity(intent);
    } else {
        mPasswordView.setError("An error occurred connecting to deepstreamHub");
        mPasswordView.requestFocus();
    }
}

Viewing the users in our app

At this stage we have a deepstream List called users that contains the user ids of all the users in our application. From here, we need to:

-display all users in our application in a ListView

-start a chat with them whenever we click on a user

-have the list update in realtime whenever someone new joins

-display each users online/offline status and have it update whenever they login or logout

The first thing we need to do is get the list of user ids in our application. We can do this through the getList(String listname) method, which will return all the Record names in the list.

final List userIds = client.record.getList("users");

To populate the ListView with these users, we'll be using a LinkedHashMap with a custom adapter. By using a LinkedHashMap, it'll be much easier for us to toggle the users online/offline state using presence. However we also need to remove the users own ID from this ArrayList, it wouldn't make sense for a user to chat to themselves.

final String[] userIds = userList.getEntries();
users = new LinkedHashMap<>();
for (String userId : userIds) {
    if (!userId.equals(stateRegistry.getUserId())) {
        addUser(userId);
    }
}

Our addUser method looks as follows, all we're doing is getting each users metadata from their Record, and adding it to our LinkedHashMap.

Record userRecord = client.record.getRecord("users/" + id);
String email = userRecord.get("email").getAsString();
boolean online = userRecord.get("online").getAsBoolean();
users.put(id, new User(
        id,
        email,
        online)
);
userRecord.discard();

Now it's a simple matter of creating an Adapter for the list and setting the Adapter on the ListView.

final UserAdapter adapter = new UserAdapter(this, users);
ListView listView = (ListView) findViewById(R.id.user_list);
listView.setAdapter(adapter);

Next, to start a chat with a user, it's as simple as setting an OnItemClickListener on the list and creating an Intent with the ID and email of the user you want to.

listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
       Intent i = new Intent(ctx, ChatActivity.class);
        java.util.List<String> idList = new ArrayList<String>(users.keySet());
        String userId = idList.get(position);
        i.putExtra("userId", userId);
        i.putExtra("userEmail", user.getEmail());
        startActivity(i);
    }
});

To update the list of users in real time, we just need to subscribe to our deepstream List and add a User entry to the Adapter whenever an entry is added.

userList.subscribe(new ListEntryChangedListener() {
    @Override
    public void onEntryAdded(String listName, final String userId, final int position) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                addUser(userId);
                adapter.notifyDataSetChanged();
            }
        });
    }
});

We'll also want the online/offline status of users to change whenever they login or logout. This is as simple as using the deepstream presence API and updating our list of users accordingly.

client.presence.subscribe(new PresenceEventListener() {
    @Override
    public void onClientLogin(String userId) {
        User user = users.get(userId);
        // happens first time a user connects
        if (user == null) {
           return;
        }
        user.setOnline(true);
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                adapter.notifyDataSetChanged();
            }
        });
    }

    @Override
    public void onClientLogout(String userId) {
        User user = users.get(userId);
        user.setOnline(false);
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                adapter.notifyDataSetChanged();
            }
        });
    }
});

At this stage of the tutorial, we should have something like this: Login gif

Sending and editing messages

The final stage of our chat application is being able to actually send messages between users and for this there are a few requirements, we want to:

-see when a user we're talking to is typing

-be able to edit older messages

-receive the messages in realtime

In deepstream, Records are tiny blobs of JSON that we can modify, subscribe to and permission. This makes them perfect to model various things and we'll use them to represent our messages as follows:

{
    "content": "...",
    "email": "...",
    "id": "...",
    "msgId": "..."
}

We'll also need a Record that represents the state of the conversation, we'll just be using this to show whether a user is typing or not, but it could also be used to store additional metadata about the conversation.

{
    "${userOneId}": {
        "isTyping": true
    },
    "${userTwoId}": {
        "isTyping": false
    }
}

When the ChatActivity is started, either the user has talked to the user they clicked on before, or they haven't. If they haven't, we need to initialise the conversation. We do this by creating a List with the name ${userId}::${otherUserId} and adding Records (the messages in our chat) to this List. To ensure the ordering of the user Id's in the chat name, we can sort them in place and create the chat name from them as follows:

String[] tempChatArray = new String[]{ currentUserId, otherUserId };
Arrays.sort(tempChatArray);

chatName = tempChatArray[0] + "::" + tempChatArray[1];
chatList = client.record.getList(chatName);

if (chatList.isEmpty()) {
    initialiseStateRecord(chatName);
}

Our initialiseStateRecord method just creates our state Record to hold various details about the conversation.

private void initialiseStateRecord(String chatName) {
    stateRecord = client.record.getRecord(chatName + "/state");
    JsonObject userMetaData = new JsonObject();
    userMetaData.addProperty("isTyping", false);
    stateRecord.set(currentUserId, userMetaData);
    stateRecord.set(otherUserId, userMetaData);
}

After we've initialised the chat (or maybe it already has messages in it), we can put the messages from the List into our ListView. This is very similar to how we did it earlier in our ChatOverviewActivity, except this time we're not using a LinkedHashMap, just a standard ArrayList.

String[] entries = chatList.getEntries();
messages = new ArrayList<>();

for (String msgId : entries) {
    addMessage(msgId);
}
adapter = new ChatAdapter(this, messages);

Our addMessage method just gets the details of each message in the conversation and subscribes to changes on it.

private void addMessage(String msgId) {
    Record msgRecord = client.record.getRecord(msgId);
    msgRecord.subscribe("content", new ChatItemUpdate(messages.size(), messages, adapter));
    JsonObject msgJson = msgRecord.get().getAsJsonObject();
    Message m = new Message(
            msgJson.get("email").getAsString(),
            msgJson.get("content").getAsString(),
            msgJson.get("id").getAsString(),
            msgJson.get("msgId").getAsString()
    );
    messages.add(m);
}

One thing that might look off here is the line

msgRecord.subscribe("content", new ChatItemUpdate(messages.size(), messages, adapter));`

So lets take a look into it. The subscribe method we're using is defined as:

public Record subscribe(String path, RecordPathChangedCallback recordPathChangedCallback);

And the ChatItemUpdate class is defined as:

private class ChatItemUpdate implements RecordPathChangedCallback {
    private int position;
    private ArrayList<Message> messages;
    private ChatAdapter adapter;
    ChatItemUpdate(int position, ArrayList<Message> messages, ChatAdapter adapter) {
        this.position = position;
        this.messages = messages;
        this.adapter = adapter;
    }
    @Override
    public void onRecordPathChanged(String recordName, String path, JsonElement data) {
        Message msgToEdit = messages.get(position);
        msgToEdit.setContent(data.getAsString());
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                adapter.notifyDataSetChanged();
            }
        });
    }
}

What we're doing here is saying that when the content of a message Record changes, get the message in the position we initialised the wrapper class with earlier, and update it.

Now lets actually send some messages so that we have something to edit. We have a Button that we've called postButton, all we need to do is set an OnClickListener on it. When the button is pressed we get the text from the EditText and we create a new Record representing the message. We then add the name of the new Record to our List.

postButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        String input = textField.getText().toString();
        if (input.isEmpty()) {
            return;
        }
        String msgId = UUID.randomUUID().toString();
        String msgName = chatName + "/" + msgId;
        Record msgRecord = client.record.getRecord(msgName);
        Message message = new Message(
                currentUserEmail,
                input,
                currentUserId,
                msgId
        );
        msgRecord.set(stateRegistry.getGson().toJsonTree(message));
        chatList.addEntry(msgName);
        textField.setText("");
    }
});

As we've seen before, we can just subscribe to the List so that whenever a new entry is added, it can be updated in the Adapter. We're also subscribing to content changes in the newly added message the same way we did before.

ListEntryChangedListener entryChangedListener = new ListEntryChangedListener() {
    @Override
    public void onEntryAdded(String listName, final String msgId, final int position) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                addMessage(msgId);
                adapter.notifyDataSetChanged();
            }
        });
    }
};

chatList.subscribe(entryChangedListener);

Now that we have messages to edit, we can implement a simple AlertDialog to handle editing messages. So we set an OnClickListener on the ListView, and if the user wrote this message, then an AlertDialog pops up and they're able to edit it.

listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        Message currentMsg = messages.get(position);
        final Record msgRecord = client.record.getRecord(stateRegistry.getCurrentChatName() + "/" + currentMsg.getMsgId());
        // don't want to allow editing other peoples messages
        if (!currentMsg.getWriterId().equals(stateRegistry.getUserId())) {
            return;
        }

        final EditText editText = new EditText(getApplicationContext());
        editText.setText(currentMsg.getContent());
        new AlertDialog.Builder(ctx)
                .setView(editText)
                .setPositiveButton("Ok", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                        String newContent = editText.getText().toString();
                        msgRecord.set("content", newContent);
                    }
                })
                .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                    }
                })
                .show();
    }
});

The last thing we need to do here is have an isTyping notification, so that users can see when the user they're talking to is typing. We already have the state Record set up for this, so the last step is to update this Record when a user is typing.

In our XML layout we have a TextView with ID is_typing_field, all we need to do is a setText("${user} is typing...") or setText("") on this TextView whenever the isTyping field in the record changes.

textField = (EditText) findViewById(R.id.input_message);
isTypingField = (TextView) findViewById(R.id.is_typing_field);
textField.addTextChangedListener(new TextWatcher() {
    @Override
    public void afterTextChanged(Editable s) {
        if (s.toString().length() > 0) {
            stateRecord.set(stateRegistry.getUserId() + ".isTyping", true);
        } else {
            stateRecord.set(stateRegistry.getUserId() + ".isTyping", false);
        }
    }
});

And the associated Record code:

pathChangedCallback = new RecordPathChangedCallback() {
    @Override
    public void onRecordPathChanged(String recordName, String path, final JsonElement data) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                boolean isTyping = data.getAsBoolean();
                if (isTyping) {
                    isTypingField.setText(otherUserEmail + " is typing...");
                } else {
                    isTypingField.setText("");
                }
            }
        });
    }
};

stateRecord.subscribe(otherUserId + ".isTyping", pathChangedCallback);

Now that our realtime chat application is finished, it should look as follows.

Chat gif

Thanks for staying with us, to get a deeper look into deepstreamHub, take a look at our other example apps or our various integrations.