Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The project is pre-configured with a shared [Stream](https://getstream.io) accou
## Quick start

1. Clone the repository
2. Open the project in Android Studio (Arctic Fox or later)
2. Open the project in the latest stable version of Android Studio
3. Run the _app_
4. Make sure to check the [Details](#details) section below for the different steps

Expand All @@ -18,19 +18,18 @@ The project is pre-configured with a shared [Stream](https://getstream.io) accou
The tutorial app consists of two screens:

* `MainActivity`: Shows the list of available channels.
* `MessagesActivity`: Shows the selected channel view, which includes the header, message list, and message input view.
* `ChannelActivity`: Shows the selected channel view, which includes the header, message list, and message input view.

There are a handful of `MessagesActivity` implementations, which correspond to the steps of the tutorial. You can easily swap them by changing the `onChannelClick` handler located in `MainActivity`:
`ChannelActivity` follows the published tutorial step-by-step and includes a colors customization on `ChatTheme`. Three additional `ChannelActivity*` implementations show alternative customization techniques and screen compositions. To try one, point `MainActivity`'s `onChannelClick` at it:

```kotlin
onChannelClick = { channel ->
startActivity(MessagesActivity4.getIntent(this, channel.cid))
startActivity(ChannelActivity4.getIntent(this, channel.cid))
},
```

You can choose from four different `MessagesActivity` implementations:
You can choose from three alternative `ChannelActivity` implementations:

* `MessagesActivity` - a basic _Message Screen_ implementation
* `MessagesActivity2` - includes customization of the screen by using `ChatTheme`
* `MessagesActivity3` - uses bound and stateless components to build the chat screen, with further customization
* `MessagesActivity4` - uses a custom message composer component for extended customization
* `ChannelActivity2` - customizes typography via `ChatTheme`
* `ChannelActivity3` - uses bound and stateless components to build the chat screen
* `ChannelActivity4` - uses a custom message composer component
16 changes: 7 additions & 9 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ plugins {
}

android {
compileSdk 35
compileSdk 36
namespace "com.example.chattutorial"

defaultConfig {
applicationId "com.example.chattutorial"
minSdk 21
targetSdk 34
minSdk 24
targetSdk 36
versionCode 1
versionName "1.0"

Expand Down Expand Up @@ -45,15 +45,13 @@ android {
}

dependencies {
implementation "io.getstream:stream-chat-android-compose:6.12.1"
implementation "io.getstream:stream-chat-android-offline:6.12.1"
implementation "io.getstream:stream-chat-android-compose:7.3.0"

implementation(platform("androidx.compose:compose-bom:2024.06.00"))
implementation("androidx.activity:activity-compose:1.10.1")
implementation(platform("androidx.compose:compose-bom:2025.08.01"))
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling")
implementation("androidx.compose.runtime:runtime")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.material:material")
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.material3:material3")
}
10 changes: 5 additions & 5 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@
android:supportsRtl="true"
android:theme="@style/Theme.ChatTutorial">
<activity
android:name=".MessagesActivity"
android:name=".ChannelActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".MessagesActivity2"
android:name=".ChannelActivity2"
android:exported="true"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".MessagesActivity3"
android:name=".ChannelActivity3"
android:exported="true"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".MessagesActivity4"
android:name=".ChannelActivity4"
android:exported="true"
android:windowSoftInputMode="adjustResize" />
<activity
Expand All @@ -52,4 +52,4 @@
</intent>
</queries>

</manifest>
</manifest>
62 changes: 62 additions & 0 deletions app/src/main/java/com/example/chattutorial/ChannelActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.example.chattutorial

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.ui.graphics.Color
import io.getstream.chat.android.compose.ui.messages.ChannelScreen
import io.getstream.chat.android.compose.ui.theme.ChatTheme
import io.getstream.chat.android.compose.ui.theme.StreamDesign
import io.getstream.chat.android.compose.viewmodel.messages.ChannelViewModelFactory
import io.getstream.chat.android.compose.viewmodel.messages.MessageListOptions

class ChannelActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Load the ID of the selected channel from Intent Extras.
// If there is no channelId, then we finish the Activity and return.
val channelId = intent.getStringExtra(KEY_CHANNEL_ID)
if (channelId == null) {
finish()
return
}

// Set the UI
setContent {
val baseColors = if (isSystemInDarkTheme()) {
StreamDesign.Colors.defaultDark()
} else {
StreamDesign.Colors.default()
}
ChatTheme(
colors = baseColors.copy(
accentPrimary = Color(0xFF005FFF),
)
) {
ChannelScreen(
viewModelFactory = ChannelViewModelFactory(
context = this,
channelId = channelId,
messageListOptions = MessageListOptions(messageLimit = 30),
),
onBackPressed = { finish() }
)
}
}
}

// Define a helper function to build an Intent for this Activity.
companion object {
private const val KEY_CHANNEL_ID = "channelId"

fun getIntent(context: Context, channelId: String): Intent {
return Intent(context, ChannelActivity::class.java).apply {
putExtra(KEY_CHANNEL_ID, channelId)
}
}
}
}
60 changes: 60 additions & 0 deletions app/src/main/java/com/example/chattutorial/ChannelActivity2.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.example.chattutorial

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import io.getstream.chat.android.compose.ui.messages.ChannelScreen
import io.getstream.chat.android.compose.ui.theme.ChatTheme
import io.getstream.chat.android.compose.ui.theme.StreamDesign
import io.getstream.chat.android.compose.viewmodel.messages.ChannelViewModelFactory
import io.getstream.chat.android.compose.viewmodel.messages.MessageListOptions

/**
* Demonstrates customizing the [ChatTheme] typography. The same pattern works for any
* [StreamDesign.Typography] field; here we swap the font family and weights on a few text styles
* via [copy].
*/
class ChannelActivity2 : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val channelId = intent.getStringExtra(KEY_CHANNEL_ID)
if (channelId == null) {
finish()
return
}

setContent {
val baseTypography = StreamDesign.Typography.default(fontFamily = FontFamily.Serif)
ChatTheme(
typography = baseTypography.copy(
bodyEmphasis = baseTypography.bodyEmphasis.copy(fontWeight = FontWeight.Bold),
headingMedium = baseTypography.headingMedium.copy(fontWeight = FontWeight.Black),
)
) {
ChannelScreen(
viewModelFactory = ChannelViewModelFactory(
context = this,
channelId = channelId,
messageListOptions = MessageListOptions(messageLimit = 30),
),
onBackPressed = { finish() }
)
}
}
}

companion object {
private const val KEY_CHANNEL_ID = "channelId"

fun getIntent(context: Context, channelId: String): Intent {
return Intent(context, ChannelActivity2::class.java).apply {
putExtra(KEY_CHANNEL_ID, channelId)
}
}
}
}
165 changes: 165 additions & 0 deletions app/src/main/java/com/example/chattutorial/ChannelActivity3.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package com.example.chattutorial

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import io.getstream.chat.android.compose.ui.components.messageactions.MessageActions
import io.getstream.chat.android.compose.ui.components.messageactions.ReactionsMenu
import io.getstream.chat.android.compose.ui.components.messageoptions.defaultMessageOptionsState
import io.getstream.chat.android.compose.ui.messages.attachments.AttachmentPickerMenu
import io.getstream.chat.android.compose.ui.messages.composer.MessageComposer
import io.getstream.chat.android.compose.ui.messages.list.MessageList
import io.getstream.chat.android.compose.ui.theme.ChatTheme
import io.getstream.chat.android.compose.viewmodel.messages.AttachmentsPickerViewModel
import io.getstream.chat.android.compose.viewmodel.messages.ChannelViewModelFactory
import io.getstream.chat.android.compose.viewmodel.messages.MessageComposerViewModel
import io.getstream.chat.android.compose.viewmodel.messages.MessageListViewModel
import io.getstream.chat.android.compose.viewmodel.messages.MessageListOptions
import io.getstream.chat.android.ui.common.state.messages.MessageMode
import io.getstream.chat.android.ui.common.state.messages.list.SelectedMessageOptionsState
import io.getstream.chat.android.ui.common.state.messages.list.SelectedMessageReactionsState

/**
* Demonstrates building the chat screen from low-level, bound + stateless components instead of
* using [io.getstream.chat.android.compose.ui.messages.ChannelScreen]. The selected-message
* overlay is rendered with [MessageActions] / [ReactionsMenu].
*/
class ChannelActivity3 : ComponentActivity() {

private val factory by lazy {
ChannelViewModelFactory(
context = this,
channelId = intent.getStringExtra(KEY_CHANNEL_ID) ?: "",
messageListOptions = MessageListOptions(messageLimit = 30),
)
}

private val listViewModel: MessageListViewModel by viewModels { factory }
private val attachmentsPickerViewModel: AttachmentsPickerViewModel by viewModels { factory }
private val composerViewModel: MessageComposerViewModel by viewModels { factory }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val channelId = intent.getStringExtra(KEY_CHANNEL_ID)
if (channelId == null) {
finish()
return
}

setContent {
ChatTheme {
MyCustomUi()
}
}
}

@Composable
private fun MyCustomUi() {
val messagesState by listViewModel.currentMessagesState
val selectedMessageState = messagesState.selectedMessageState
val user by listViewModel.user.collectAsState()

Box(
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding()
.consumeWindowInsets(WindowInsets.ime),
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
containerColor = ChatTheme.colors.backgroundCoreApp,
bottomBar = {
Column {
MessageComposer(
viewModel = composerViewModel,
onAttachmentsClick = {
attachmentsPickerViewModel.setPickerVisible(true)
}
)
AttachmentPickerMenu(
attachmentsPickerViewModel = attachmentsPickerViewModel,
composerViewModel = composerViewModel,
)
}
}
) { contentPadding ->
MessageList(
modifier = Modifier
.background(ChatTheme.colors.backgroundCoreApp)
.padding(contentPadding)
.fillMaxSize(),
viewModel = listViewModel,
onThreadClick = { message ->
composerViewModel.setMessageMode(MessageMode.MessageThread(message))
listViewModel.openMessageThread(message)
}
)
}

if (selectedMessageState is SelectedMessageOptionsState) {
val selectedMessage = selectedMessageState.message
MessageActions(
message = selectedMessage,
messageOptions = defaultMessageOptionsState(
selectedMessage = selectedMessage,
currentUser = user,
isInThread = listViewModel.isInThread,
channel = selectedMessageState.channel,
),
ownCapabilities = selectedMessageState.ownCapabilities,
onMessageAction = { action ->
composerViewModel.performMessageAction(action)
listViewModel.performMessageAction(action)
},
onShowMoreReactionsSelected = {
listViewModel.selectExtendedReactions(selectedMessage)
},
onDismiss = { listViewModel.removeOverlay() },
currentUser = user,
)
} else if (selectedMessageState is SelectedMessageReactionsState) {
val selectedMessage = selectedMessageState.message
ReactionsMenu(
message = selectedMessage,
currentUser = user,
ownCapabilities = selectedMessageState.ownCapabilities,
onMessageAction = { action ->
composerViewModel.performMessageAction(action)
listViewModel.performMessageAction(action)
},
onShowMoreReactionsSelected = {
listViewModel.selectExtendedReactions(selectedMessage)
},
onDismiss = { listViewModel.removeOverlay() },
)
}
}
}

companion object {
private const val KEY_CHANNEL_ID = "channelId"

fun getIntent(context: Context, channelId: String): Intent {
return Intent(context, ChannelActivity3::class.java).apply {
putExtra(KEY_CHANNEL_ID, channelId)
}
}
}
}
Loading
Loading