Skip to content

astrotars/the-stream-android

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Building a Custom Chat Application with Android – Direct Messaging and Group Chat

In this post, we'll create an Android application that allows a user to chat 1-on-1 or chat in groups. Stream's Chat API, combined with Android, make it straightforward to build this sort of complex interaction. All source code for this application is available on GitHub. This application is fully functional on Android.

Prerequisites

Basic knowledge of Node.js (JavaScript), Kotlin, and Android is required to follow this tutorial. This code intended to run locally.

If you'd like to follow along, you'll need a free account with Stream and have the latest Android Studio installed with a relatively recent Android SDK version. If you're having a hard time, feel free to open a GitHub issue.

You also need to have the backend running. Please follow the instructions in the backend README.md to set it up.

Getting Started

To keep things focused, we'll be showing only the essential code. Each snippet has a comment that references the corresponding file and line location in the source code, where you will find the necessary context for the snippets, such as layout or navigation. Please refer to the full source code for an explanation of everything not covered here and to resolve any questions. Also, please refer to the build.gradle file for a full list of libraries required.

To build our social network, we'll need both a backend and a mobile application. Most of the work is done in the mobile app, but we need the backend to create frontend tokens to interact securely with the Stream API.

The backend relies on Express (Node.js) and leverages Stream's JavaScript library.

The mobile application is built in Kotlin wrapping Stream's Android library. The basic flow of the application is as follows.

The app goes through these steps to allow a user to chat with another:

  1. A user navigates to the user list and clicks on their name or chat icon. The mobile application joins a 1-on-1 chat channel between the two users.
  2. The app queries the channel for previous messages and indicates to Stream that we'd like to watch this channel for new messages. The mobile app listens for new messages.
  3. The user creates a new message and sends it to the Stream API.
  4. When the message is created, or a message is received from the other user, the mobile application consumes the event and displays the message.

For group chat, the app goes through these steps:

  1. A user navigates to a list of channels. They can choose to enter a previously made group or start a new group.
  2. If a user chooses to create a new group, the mobile application creates a new Stream channel.
  3. When a user enters a room, it performs the above steps 2-4 from above.

We'll rely on the Stream mobile libraries to do the heavy lifting. The code is split between the Android mobile application contained in the android directory, and the Express backend is in the backend directory. See the README.md in the backend folder for installation and running instructions. If you'd like to follow along with running code, make sure you get both the backend and mobile app running before continuing.

Configuring Stream Chat

First, we need to log in and initialize Stream.

To communicate with Stream, we need a secure frontend token. This allows our mobile application to communicate with Stream's API directly. To do this, we'll need a backend endpoint that uses our Stream account secrets to generate this token. Once we have this token, we don't need the backend to do anything else, since the mobile app has access to the full Stream API, scoped to the authenticated user.

First, we'll be building the login screen which looks like this:

To start, let's lay our form out in Android. In our activity_main.xml layout we have a simple ConstraintLayout with an EditText and Button:

<!-- android/app/src/main/res/layout/activity_main.xml:1 -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="io.getstream.thestream.MainActivity">

    <EditText
        android:id="@+id/user"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:autofillHints="Username"
        android:ems="10"
        android:hint="Username"
        android:inputType="textPersonName"
        app:layout_constraintEnd_toStartOf="@+id/submit"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="HardcodedText" />

    <Button
        android:id="@+id/submit"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:text="Login"
        app:layout_constraintBaseline_toBaselineOf="@+id/user"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/user"
        tools:ignore="HardcodedText" />

</androidx.constraintlayout.widget.ConstraintLayout>

Let's bind to this layout and respond in MainActivity:

// android/app/src/main/java/io/getstream/thestream/MainActivity.kt:16
class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val submit: Button = findViewById(R.id.submit)
        val userView: EditText = findViewById(R.id.user)

        submit.setOnClickListener {
            val user: String = userView.text.toString()

            launch(Dispatchers.IO) {
                BackendService.signIn(user)

                val chatCredentials = BackendService.getChatCredentials()

                launch(Dispatchers.Main) {
                    ChatService.init(applicationContext, user, chatCredentials)

                    startActivity(
                        Intent(applicationContext, AuthedMainActivity::class.java)
                    )
                }
            }
        }
    }
}

Note: The asynchronous approach in this tutorial is not necessarily the best or most robust approach. It's simply a straightforward way to show async interactions without cluttering the code too much. Please research and pick the best asynchronous solution for your application.

Here we bind to our button and user input. We listen to the submit button and sign in to our backend. Since this work is making network calls, we need to do this asynchronously. We use Kotlin coroutines to accomplish this by binding to the MainScope. We dispatch our sign-in code which tells our BackendService to perform two tasks, sign in to the backend and get the chat frontend credentials. We'll look at how the BackendService accomplishes this later in the post.

Once we have our token, we initialize our ChatService which allows us to talk to Stream's API (we'll see this in a second as well). When the user is fully authed and we have our credentials, we start a new activity called AuthedMainActivity which is the rest of the application.

Before seeing how we start a chat, let's see how we auth and initialize Stream Chat. First we sign in to the backend via BackendService.signIn:

// android/app/src/main/java/io/getstream/thestream/services/BackendService.kt:18
fun signIn(user: String) {
    authToken = post(
        "/v1/users",
        mapOf("user" to user)
    )
        .getString("authToken")
    this.user = user
}

// ...

private fun post(path: String, body: Map<String, Any>, authToken: String? = null): JSONObject {
    val request = Request.Builder()
        .url("$apiRoot${path}")
        .post(JSONObject(body).toString().toRequestBody(JSON))

    if (authToken != null) {
        request.addHeader("Authorization", "Bearer $authToken")
    }

    http.newCall(request.build()).execute().use {
        return JSONObject(it.body!!.string())
    }
}

We do a simple HTTP POST request to our backend endpoint /v1/users, which returns a backend authToken which allows the mobile application to make further requests against the backend. Since this is not a real implementation of auth, we'll skip the backend code. Please refer to the source if you're curious. Also, keep in mind, this token is not the Stream token. We need to make another call for that.

Once the user is signed in with our backend we can get our chat credentials via BackendService.getChatCredentials():

// android/app/src/main/java/io/getstream/thestream/services/BackendService.kt:27
data class StreamCredentials(val token: String, val apiKey: String)

fun getChatCredentials(): StreamCredentials {
    val response = post(
        "/v1/stream-chat-credentials",
        mapOf(),
        authToken
    )

    return StreamCredentials(
        response.getString("token"),
        response.getString("apiKey")
    )
}

Similar to before, we POST to our backend to get our chat credentials. The one difference being we use our authToken to authenticate against our backend.

// backend/src/controllers/v1/stream-feed-credentials/stream-feed-credentials.action.js:1
import dotenv from 'dotenv';
import stream from "getstream";

dotenv.config();

exports.streamFeedCredentials = async (req, res) => {
  try {
    const apiKey = process.env.STREAM_API_KEY;
    const apiSecret = process.env.STREAM_API_SECRET;
    const appId = process.env.STREAM_APP_ID;

    const client = stream.connect(apiKey, apiSecret, appId);

    await client.user(req.user).getOrCreate({ name: req.user });
    const token = client.createUserToken(req.user);

    res.status(200).json({ token, apiKey, appId });
  } catch (error) {
    console.log(error);
    res.status(500).json({ error: error.message });
  }
};

We use the Stream JavaScript library to create a user (if they don't exist) and generate a Stream frontend token. We return this token, alongside some API information, back to the Android app.

In the mobile app, we use the returned credentials to intialize our ChatService by calling ChatService.init in MainActivity. Here's the init:

// android/app/src/main/java/io/getstream/thestream/services/ChatService.kt:14
object ChatService {
    private lateinit var client: ChatClient
    private lateinit var user: User

    fun init(context: Context, user: String, credentials: BackendService.StreamCredentials) {
        val chat = Chat
            .Builder(credentials.apiKey, context)
            .logLevel(ChatLogLevel.ALL)
            .build()

        this.user = User(user)

        this.user.extraData["name"] = user
        ChatDomain
            .Builder(context.applicationContext, chat.client, this.user)
            .build()

        this.client = chat.client
        this.client.setUser(this.user, credentials.token)
    }
    // ...
}

The ChatService is a singleton (by using Kotlin's object) which stores a ChatClient instance. ChatClient is a class provided by Stream's library. This class is specifically used to provide the functionality to client applications via frontend tokens. After we initialize our chat instance, we configure our ChatDomain by setting the user that's authenticated. We then set the user on the ChatClient and we're good to go.

Now that we're authenticated with Stream, we're ready to start our first chat!

Creating a 1-on-1 Chat

First, we need to select a user to chat with. We'll build a simple list of users with a prompt to start a chat. Our home screen will look like this:

Let's lay out a simple navigation in AuthedMainActivity and show a list of people by default. First, our layout:

<!-- android/app/src/main/res/layout/activity_authed_main.xml:1 -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <FrameLayout
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

    </FrameLayout>

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="?android:attr/windowBackground"
        app:menu="@menu/navigation" />

</LinearLayout>

And here's our AuthedMainActivity:

// android/app/src/main/java/io/getstream/thestream/AuthedMainActivity.kt:8
class AuthedMainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_authed_main)

        val navigation = findViewById<BottomNavigationView>(R.id.navigation)
        navigation.setOnNavigationItemSelectedListener(navListener)

        addFragment(PeopleFragment())
    }

    private fun addFragment(fragment: Fragment) {
        supportFragmentManager
            .beginTransaction()
            .setCustomAnimations(
                R.anim.design_bottom_sheet_slide_in,
                R.anim.design_bottom_sheet_slide_out
            )
            .replace(R.id.content, fragment, fragment.javaClass.simpleName)
            .commit()
    }

    private val navListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
        when (item.itemId) {
            R.id.navigation_people -> {
                addFragment(PeopleFragment())

                return@OnNavigationItemSelectedListener true
            }
            R.id.navigation_channels -> {
                addFragment(ChannelsFragment())

                return@OnNavigationItemSelectedListener true
            }
        }
        false
    }
}

This is a simple tabbed BottomNavigationView. We have two views, People and Channels. First let's dig into the People view which is backed by PeopleFragment. We also boot the PeopleFragment by default which contains our list of people:

// android/app/src/main/java/io/getstream/thestream/PeopleFragment.kt:20
class PeopleFragment : Fragment(), CoroutineScope by MainScope() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val rootView: View = inflater.inflate(R.layout.fragment_people, container, false)
        val list: ListView = rootView.findViewById(R.id.list_people)

        val adapter = ArrayAdapter(
            rootView.context,
            android.R.layout.simple_list_item_1,
            mutableListOf<String>()
        )
        list.adapter = adapter

        list.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ ->
            val alertDialogBuilder: AlertDialog.Builder = AlertDialog.Builder(rootView.context)

            alertDialogBuilder.setTitle("Pick an action")
            alertDialogBuilder.setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }

            alertDialogBuilder.setPositiveButton("Chat") { dialog, _ ->
                val otherUser = adapter.getItem(position).toString()

                launch(Dispatchers.IO) {
                    val channel = ChatService.createPrivateChannel(otherUser)

                    launch(Dispatchers.Main) {
                        dialog.dismiss()
                        val intent = ChannelActivity.newIntent(rootView.context, channel)
                        startActivity(intent)
                    }
                }
            }

            alertDialogBuilder.show()
        }

        launch(Dispatchers.IO) {
            val users = BackendService.getUsers()

            launch(Dispatchers.Main) { adapter.addAll(users) }
        }

        return rootView
    }

}

This is backed by a simple list layout:

<!-- android/app/src/main/res/layout/fragment_people.xml:1 -->
<?xml version="1.0" encoding="utf-8"?>

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".PeopleFragment">

    <ListView
        android:id="@+id/list_people"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</FrameLayout>

With the layout and navigation out of the way, we can focus on starting our chat. The PeopleFragment populates the list of user's via a BackendService.getUsers call:

// android/app/src/main/java/io/getstream/thestream/services/BackendService.kt:42
fun getUsers(): List<String> {
    val request = Request.Builder()
        .url("$apiRoot/v1/users")
        .addHeader("Authorization", "Bearer $authToken")
        .get()

    http.newCall(request.build()).execute().use { response ->
        val jsonArray = JSONObject(response.body!!.string()).getJSONArray("users")

        return List(jsonArray.length()) { i ->
            jsonArray.get(i).toString()
        }.filterNot { it == user }
    }
}

This is a simple call to the backend to get our list of users. Since the backend is a mock implementation, we won't dig into the /v1/users endpoint here, please refer to the source.

Once this list is returned, we populate the PeopleFragment's ListView with a simple ArrayAdapter. On each item, we bind via onItemClickListener. We pop up an alert and upon a user clicking Chat we create a chat room with ChatService.createPrivateChannel:

// android/app/src/main/java/io/getstream/thestream/services/ChatService.kt:35
fun createPrivateChannel(otherUser: String): Channel {
    val users = listOf(user.id, otherUser)

    val result = client
        .createChannel(ModelType.channel_messaging, users)
        .execute()
    if (result.isSuccess) {
        return result.data()
    } else {
        throw result.error()
    }
}

This calls to the Stream client to create a channel with the type channel_messaging. Since this a private chat, we tell Stream to restrict the channel to those two users.

Viewing A 1-on-1 Chat

Next, we'll create our chat view:

Once the createPrivateChannel method returns, PeopleFragment launches ChannelActivity with the newly created channel. Since this activity wraps Stream's UI components, let's first see the layout definition:

<!-- android/app/src/main/res/layout/activity_channel.xml:1 -->
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="viewModel"
            type="com.getstream.sdk.chat.viewmodel.ChannelViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.example.chattutorial.ChannelActivity">

        <com.getstream.sdk.chat.view.ChannelHeaderView
            android:id="@+id/channelHeader"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#FFF"
            app:layout_constraintEnd_toStartOf="@+id/messageList"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:streamChannelHeaderBackButtonShow="true" />

        <com.getstream.sdk.chat.view.MessageListView
            android:id="@+id/messageList"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_marginBottom="10dp"
            android:background="#FFF"
            app:layout_constraintBottom_toTopOf="@+id/message_input"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/channelHeader" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:padding="6dp"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <ProgressBar
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:isGone="@{!safeUnbox(viewModel.loading)}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <ProgressBar
            android:layout_width="25dp"
            android:layout_height="25dp"
            app:isGone="@{!safeUnbox(viewModel.loadingMore)}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="parent" />

        <com.getstream.sdk.chat.view.MessageInputView
            android:id="@+id/message_input"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="32dp"
            android:layout_marginBottom="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintStart_toEndOf="@+id/messageList" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Here we use a few built in views, ChannelHeaderView, MessageListView, and MessageInputView. In order to use these built-in Stream Chat UI views, we need to back them with a ChannelViewModel. We build this in our ChannelActivity:

// android/app/src/main/java/io/getstream/thestream/ChannelActivity.kt:17
class ChannelActivity : AppCompatActivity(), MessageInputView.PermissionRequestListener {
    private lateinit var binding: ActivityChannelBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val channelType = intent.getStringExtra(EXTRA_CHANNEL_TYPE)!!
        val channelId = intent.getStringExtra(EXTRA_CHANNEL_ID)!!

        binding = DataBindingUtil.setContentView(this, R.layout.activity_channel)
        binding.lifecycleOwner = this

        initViewModel(channelType, channelId)
    }

    override fun onActivityResult(
        requestCode: Int,
        resultCode: Int,
        data: Intent?
    ) {
        super.onActivityResult(requestCode, resultCode, data)
        binding.messageInput.captureMedia(requestCode, resultCode, data)
    }

    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String?>,
        grantResults: IntArray
    ) {
        binding.messageInput.permissionResult(requestCode, permissions, grantResults)
    }

    override fun openPermissionRequest() {
        PermissionChecker.permissionCheck(this, null)
    }

    private fun initViewModel(
        channelType: String,
        channelId: String
    ) {
        val viewModelFactory = ChannelViewModelFactory(application, channelType, channelId)
        val viewModel = ViewModelProvider(this, viewModelFactory).get(ChannelViewModel::class.java)

        viewModel.initialized.observe(this, Observer {
            binding.viewModel = viewModel
            binding.messageList.setViewModel(viewModel, this)
            binding.messageInput.setViewModel(viewModel, this)
            binding.channelHeader.setViewModel(viewModel, this)
            binding.messageInput.setPermissionRequestListener(this)
        })
    }
    // ...
}

In the initViewModel method we use a few built-in Stream classes. First, we create a ChannelViewModelFactory by passing in our channel type and channelId. We then use that factory by passing it into ViewModelProvider which will create an instance of the appropriate ChannelViewModel. Upon initializing, we bind this viewModel to our view and the necessary components. The rest of ChannelActivity is boilerplate that handles permissions and capturing images in the message input.

We now have 1-on-1 chat. Next, let's see how to incorporate group chat.

Listing and Joining Group Chats

Next, we'll list our group chats. Here's what the screen will look like when a user clicks on "Channels" in the bottom navigation bar:

Recall from above we show a ChannelsFragment when the user clicks on the second tab in the navigation. This fragment is a view that shows a list of channels via Stream's ChannelListView. Here's the layout:

<!-- android/app/src/main/res/layout/fragment_channels.xml:1 -->
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">


    <data>

        <variable
            name="viewModel"
            type="com.getstream.sdk.chat.viewmodel.ChannelListViewModel" />
    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ChannelsFragment">


        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context="com.example.chattutorial.MainActivity">


            <com.getstream.sdk.chat.view.ChannelListView
                android:id="@+id/channelList"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_marginBottom="10dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:streamReadStateAvatarHeight="15dp"
                app:streamReadStateAvatarWidth="15dp"
                app:streamReadStateTextSize="9sp"
                app:streamShowReadState="true" />

            <ProgressBar
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:isGone="@{!safeUnbox(viewModel.loading)}"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <ProgressBar
                android:layout_width="25dp"
                android:layout_height="25dp"
                android:layout_marginBottom="16dp"
                app:isGone="@{!safeUnbox(viewModel.loadingMore)}"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/new_channel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end|bottom"
            android:layout_margin="16dp"
            android:src="@drawable/ic_add_white_24dp" />
    </FrameLayout>
</layout>

This layout uses a FrameLayout to float a channel create button above the ChannelListView. Like before, we have another view model, ChannelListViewModel, that the stream channel list view component expects. We initialize this in the ChannelsFragment:

// android/app/src/main/java/io/getstream/thestream/ChannelsFragment.kt:19
class ChannelsFragment : Fragment(), CoroutineScope by MainScope() {
    private lateinit var viewModel: ChannelListViewModel

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val binding = FragmentChannelsBinding.inflate(layoutInflater)
        binding.lifecycleOwner = this
        viewModel = ViewModelProvider(this).get(ChannelListViewModel::class.java)
        viewModel.setQuery(
            eq("type", ModelType.channel_livestream),
            QuerySort()
        )

        binding.viewModel = viewModel
        binding.channelList.setViewModel(viewModel, this)

        binding.newChannel.setOnClickListener {
            startActivityForResult(
                Intent(context, CreateChannelActivity::class.java),
                CHANNEL_CREATE_SUCCESS
            )
        }

        binding.channelList.setOnChannelClickListener { channel ->
            startActivity(
                ChannelActivity.newIntent(context!!, channel)
            )
        }

        return binding.root
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (resultCode == CHANNEL_CREATE_SUCCESS) {
            Toast.makeText(context, "Created Channel!", Toast.LENGTH_LONG).show()
        }
    }

}

In onCreateView uses the Stream ViewModelProvider to give us an instance of ChannelListViewModel. We set the query of this to look for channels of the type channel_livestream. We then bind this viewModel. Note: channel_livestream is a built-in Stream channel type that's convenient for this tutorial. Please read up on different channel types to use one appropriate to your use case.

When a user clicks on a channel, we start our ChannelActivity. Since that activity is generic, there's nothing more for us to do! A user will join the channel and be able to chat in the group.

Creating a New Group Chat

To create a new channel, set a click listener, via setOnClickListener, on our floating action button and start the CreateChannelActivity. This activity is a simple form that takes a new channel name. Here's the layout:

<!-- android/app/src/main/res/layout/activity_create_channel.xml:1 -->
<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="io.getstream.thestream.MainActivity">

    <EditText
        android:id="@+id/channel_name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:autofillHints="Enter Channel Name..."
        android:ems="10"
        android:hint="Channel Name"
        android:inputType="text"
        app:layout_constraintEnd_toStartOf="@+id/submit"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/submit"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:text="Create"
        app:layout_constraintBaseline_toBaselineOf="@+id/channel_name"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/channel_name" />

</androidx.constraintlayout.widget.ConstraintLayout>

And our CreateChannelActivity:

// android/app/src/main/java/io/getstream/thestream/CreateChannelActivity.kt:13
const val CHANNEL_CREATE_SUCCESS = 99

class CreateChannelActivity : AppCompatActivity(), CoroutineScope by MainScope() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_create_channel)

        val submit: Button = findViewById(R.id.submit)
        val channelName: EditText = findViewById(R.id.channel_name)

        submit.setOnClickListener {
            launch(Dispatchers.IO) {
                ChatService.createGroupChannel(
                    channelName.text.toString()
                )

                launch(Dispatchers.Main) {
                    setResult(CHANNEL_CREATE_SUCCESS)
                    finish()
                }
            }
        }
    }
}

This activity takes the name and passes it to ChatService.createGroupChannel, which in turn creates a new group channel in Stream:

// android/app/src/main/java/io/getstream/thestream/services/ChatService.kt:48
fun createGroupChannel(channelName: String) {
    val channelId = channelName
        .toLowerCase(Locale.getDefault())
        .replace("\\s".toRegex(), "-")

    val result = client
        .createChannel(
            ModelType.channel_livestream,
            channelId,
            mapOf(
                "name" to channelName,
                "image" to "https://robohash.org/${channelId}.png"
            )
        )
        .execute()

    if (result.isError) {
        throw result.error()
    }
}

This method sanitizes the incoming chat name, and creates it via the Stream client. We set the type of channel (channel_livestream), channel name, and image.

And that's it! We now have a fully functioning small social application that allows direct and group chat.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published