7 |
8 | Chat conversation using Legacy XML layout
9 |
10 |
11 |
12 | Chat conversation using Compose
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | This is an Android chat application built using Kotlin and Kotlin Flow. The application allows users to send and receive messages in a chat room.
21 |
22 | ## Getting Started
23 | [](https://www.repostatus.org/#active)
24 | [](https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.4.0)
25 | [](https://developer.android.com/jetpack/androidx/releases/compose-material3#1.0.1)
26 |
27 | ## Features
28 | - Real-time messaging using Kotlin Flow.
29 | - Users can send and receive messages in a chat room.
30 | - Simple user interface with a clean and modern design.
31 |
32 | This project contains four modules
33 | - `:ui:legacy`, demonstrates of the Android chat using legacy android XML layouts
34 | - `:ui:compose`, demonstrates of the Android chat using Jetpack Compose layouts
35 | - `:ui:core`, share module for both legacy and compose modules with sharing resources
36 | - `:data`, data modules for providing chat messages, and submitting outgoing messages using a repository
37 |
38 | ## Dependencies
39 | - Android Architecture Components: ViewModel
40 | - Kotlin Coroutines.
41 | - Kotlin Flow.
42 | - Material Design Components.
43 |
44 | ## Installation
45 | 1- Clone the repository:
46 | ```bash
47 | git clone https://github.com/mo0rti/android-blueprint-kotlin-flow-chat.git
48 | ```
49 | - Open the project in Android Studio.
50 | - Build and run the application on an emulator or physical device.
51 |
52 | ## Usage
53 | - Launch the application.
54 | - Enter a username and click on the login button to enter the chat room.
55 | - Type a message in the text field at the bottom of the screen and click on the send button to send the message.
56 | - Incoming messages will appear in the chat room in real-time.
57 |
58 | ## Contributing
59 | Contributions to this project are welcome. To contribute, please follow these steps:
60 |
61 | - Fork the repository.
62 | - Create a new branch.
63 | - Make your changes and commit them.
64 | - Push your changes to your fork.
65 | - Create a pull request.
66 | - Please ensure that your pull request follows the project's coding style and that it includes appropriate tests.
67 |
68 | The Kotlin Flow Chat Application is a great project to learn Kotlin SharedFlow because it demonstrates how to use SharedFlow to build a chat application that supports real-time messaging.
69 | Here are brief explanations of the three main classes in the project:
70 | ## ChatRepository
71 | The `ChatRepository` class is responsible for managing the chat messages. It provides methods to send and receive chat messages and exposes a `SharedFlow` of incoming messages. The `incomingMessages` flow emits incoming chat messages as they arrive and can be observed by clients to display the chat history. The `sendMessage` method sends a chat message to the server. It takes a `username` and `content` as input parameters and emits a new `ChatMessage` with `MessageType.OUTGOING`. The `simulateIncomingMessages` method simulates incoming chat messages for testing purposes. It emits a random chat message every 500-2000 milliseconds until a fixed number of messages is reached.
72 |
73 | ## ChatViewModel
74 | The `ChatViewModel` class is responsible for managing the chat messages and providing an interface for the UI to interact with the chat. It provides a `sendMessage` method to send chat messages and exposes a `SharedFlow` of incoming messages. The `incomingMessages` flow emits incoming chat messages as they arrive and can be observed by clients to display the chat history. The `sendMessage` method delegates to the `ChatRepository` to send the message to the server. The method takes a `username` and `content` as input parameters and forwards them to the repository for processing.
75 |
76 | ## ChatAdapter (Legacy Android UI XML format)
77 | The `ChatAdapter` class is responsible for displaying the chat messages in the `RecyclerView` in the `ChatActivity`. It extends the `ListAdapter` class and implements the `DiffUtil.ItemCallback` interface to efficiently handle changes in the list of chat messages. The `addMessage` method is used to add a new chat message to the list of messages and update the UI.
78 |
79 | Overall, this project is a great example of how to use Kotlin SharedFlow to build a real-time messaging feature in an Android application. It demonstrates how to use the `MutableSharedFlow` and `SharedFlow` classes to emit and observe messages, and how to use coroutines to manage concurrency and perform asynchronous operations. It also shows how to use the `ViewModel` architecture component to separate the UI and data layers of the application and how to use the `ListAdapter` class to efficiently handle changes in the list of chat messages.
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | compose_ui_version = '1.4.0'
4 | }
5 | }// Top-level build file where you can add configuration options common to all sub-projects/modules.
6 | plugins {
7 | id 'com.android.application' version '7.4.1' apply false
8 | id 'com.android.library' version '7.4.1' apply false
9 | id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
10 | }
--------------------------------------------------------------------------------
/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/data/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | namespace 'mortitech.blueprint.chat.data'
8 | compileSdk 33
9 |
10 | defaultConfig {
11 | minSdk 29
12 | targetSdk 33
13 |
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | consumerProguardFiles "consumer-rules.pro"
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_1_8
26 | targetCompatibility JavaVersion.VERSION_1_8
27 | }
28 | kotlinOptions {
29 | jvmTarget = '1.8'
30 | }
31 | }
32 |
33 | dependencies {
34 |
35 | implementation 'androidx.core:core-ktx:1.9.0'
36 | implementation 'androidx.appcompat:appcompat:1.6.1'
37 | implementation 'com.google.android.material:material:1.8.0'
38 | testImplementation 'junit:junit:4.13.2'
39 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
40 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
41 | }
--------------------------------------------------------------------------------
/data/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/data/consumer-rules.pro
--------------------------------------------------------------------------------
/data/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/data/src/androidTest/java/mortitech/blueprint/chat/data/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.data
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("mortitech.blueprint.chat.data.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/data/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/data/src/main/java/mortitech/blueprint/chat/data/ChatMessage.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.data
2 |
3 | data class ChatMessage(val sender: String, val content: String, val time: String, val messageType: MessageType) {
4 | enum class MessageType { INCOMING, OUTGOING }
5 |
6 | fun isOutgoing(): Boolean {
7 | return messageType == MessageType.OUTGOING
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/data/src/main/java/mortitech/blueprint/chat/data/ChatRepository.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.data
2 |
3 | import android.util.Log
4 | import kotlinx.coroutines.*
5 | import kotlinx.coroutines.flow.*
6 | import mortitech.blueprint.chat.data.Constants.TAG
7 | import java.text.SimpleDateFormat
8 | import java.util.*
9 | import kotlin.random.Random
10 |
11 | /**
12 | * The [ChatRepository] class is responsible for managing the chat messages.
13 | *
14 | * It provides methods to send and receive chat messages, and exposes a [SharedFlow] of incoming messages.
15 | * The [incomingMessages] flow emits incoming chat messages as they arrive, and can be observed by clients to display the chat history.
16 | *
17 | * The [sendMessage] method sends a chat message to the server. It takes a [username] and [content] as input parameters, and emits a new [ChatMessage] with [MessageType.OUTGOING].
18 | * The message is sent on the IO coroutine dispatcher to avoid blocking the main thread.
19 | *
20 | * The [cancel] method cancels all coroutines launched by the [ChatRepository] instance.
21 | *
22 | * The [simulateIncomingMessages] method simulates incoming chat messages for testing purposes. It emits a random chat message every 500-2000 milliseconds until a fixed number of messages is reached.
23 | * The method is launched on the IO coroutine dispatcher to avoid blocking the main thread.
24 | */
25 | class ChatRepository {
26 | private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
27 |
28 | /**
29 | * A [SharedFlow] of incoming chat messages.
30 | *
31 | * The flow emits incoming chat messages as they arrive, and can be observed by clients to display the chat history.
32 | */
33 | private val _incomingMessages = MutableSharedFlow(replay = 1)
34 | val incomingMessages: SharedFlow = _incomingMessages
35 | .onEach { Log.d(TAG, "Received incoming message: $it") }
36 | .catch { e -> Log.e(TAG, "Error collecting incoming messages", e) }
37 | .flowOn(Dispatchers.Default)
38 | .shareIn(coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0), 1)
39 |
40 | /**
41 | * Sends a chat message to the server.
42 | *
43 | * @param username the username of the sender.
44 | * @param content the content of the message.
45 | */
46 | fun sendMessage(username: String, content: String) {
47 | coroutineScope.launch {
48 | val currentTime = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
49 | _incomingMessages.emit(ChatMessage(username, content, currentTime, ChatMessage.MessageType.OUTGOING))
50 | }
51 | }
52 |
53 | /**
54 | * Cancels all coroutines launched by the [ChatRepository] instance.
55 | */
56 | fun cancel() {
57 | coroutineScope.cancel()
58 | }
59 |
60 | /**
61 | * Initializes the [ChatRepository] by simulating incoming chat messages for testing purposes.
62 | *
63 | * The method emits a random chat message every 500-2000 milliseconds until a fixed number of messages is reached.
64 | * The messages are generated using a predefined list of senders and phrases, and a random selection of each for each message.
65 | */
66 | init {
67 | simulateIncomingMessages()
68 | }
69 |
70 | /**
71 | * Simulates incoming chat messages for testing purposes.
72 | *
73 | * The method emits a random chat message every 500-2000 milliseconds until a fixed number of messages is reached.
74 | * The messages are generated using a predefined list of senders and phrases, and a random selection of each for each message.
75 | *
76 | * This method is launched on the IO coroutine dispatcher to avoid blocking the main thread.
77 | */
78 | private fun simulateIncomingMessages() {
79 | coroutineScope.launch {
80 | val senders = listOf("Alice", "Bob", "Charlie", "David", "Eve")
81 | val phrases = listOf(
82 | "Hey, how are you doing?",
83 | "Did you hear about the new restaurant downtown?",
84 | "I'm running late for our meeting, sorry!",
85 | "What do you think about the new project?",
86 | "Can you send me the report by EOD?",
87 | "Did you see the game last night?",
88 | "I'm so excited for the concert tomorrow!",
89 | "Let's grab lunch sometime this week."
90 | )
91 | repeat(1000) {
92 | delay(Random.nextLong(500, 2000))
93 | val sender = senders.random()
94 | val content = phrases.shuffled().take(Random.nextInt(1, 3)).joinToString(separator = " ")
95 | val currentTime = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
96 | val message = ChatMessage(sender, content, currentTime, ChatMessage.MessageType.INCOMING)
97 | _incomingMessages.emit(message)
98 | }
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/data/src/main/java/mortitech/blueprint/chat/data/Constants.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.data
2 |
3 | object Constants {
4 | const val TAG = "Blueprint-Chat"
5 | }
--------------------------------------------------------------------------------
/data/src/test/java/mortitech/blueprint/chat/data/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.data
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Mar 24 12:29:25 CET 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/media/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/media/.DS_Store
--------------------------------------------------------------------------------
/media/compose-chat-list-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/media/compose-chat-list-screenshot.png
--------------------------------------------------------------------------------
/media/legacy-chat-list-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/media/legacy-chat-list-screenshot.png
--------------------------------------------------------------------------------
/media/legacy-chat-recording.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/media/legacy-chat-recording.gif
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "BlueprintChat"
16 | include ':ui:legacy'
17 | include ':ui:compose'
18 | include ':ui:core'
19 | include ':data'
20 |
--------------------------------------------------------------------------------
/ui/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/ui/.DS_Store
--------------------------------------------------------------------------------
/ui/compose/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/ui/compose/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | namespace 'mortitech.blueprint.chat.compose'
8 | compileSdk 33
9 |
10 | defaultConfig {
11 | applicationId "mortitech.blueprint.chat.compose"
12 | minSdk 29
13 | targetSdk 33
14 | versionCode 1
15 | versionName "1.0"
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | vectorDrawables {
19 | useSupportLibrary true
20 | }
21 | }
22 |
23 | buildTypes {
24 | release {
25 | minifyEnabled false
26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
27 | }
28 | }
29 | compileOptions {
30 | sourceCompatibility JavaVersion.VERSION_1_8
31 | targetCompatibility JavaVersion.VERSION_1_8
32 | }
33 | kotlinOptions {
34 | jvmTarget = '1.8'
35 | }
36 | buildFeatures {
37 | compose true
38 | }
39 | composeOptions {
40 | kotlinCompilerExtensionVersion '1.4.4'
41 | }
42 | packagingOptions {
43 | resources {
44 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
45 | }
46 | }
47 | }
48 |
49 | dependencies {
50 |
51 | implementation 'androidx.core:core-ktx:1.9.0'
52 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
53 | implementation 'androidx.activity:activity-compose:1.7.0'
54 | implementation "androidx.compose.ui:ui:$compose_ui_version"
55 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
56 | implementation "androidx.compose.material3:material3:1.0.1"
57 | implementation "androidx.compose.material3:material3-window-size-class:1.0.1"
58 | implementation 'com.jakewharton.timber:timber:5.0.1'
59 |
60 | implementation project(":ui:core")
61 | implementation project(":data")
62 |
63 | testImplementation 'junit:junit:4.13.2'
64 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
65 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
66 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
67 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
68 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"
69 | }
--------------------------------------------------------------------------------
/ui/compose/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/ui/compose/src/androidTest/java/mortitech/blueprint/chat/compose/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.compose
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import org.junit.Assert.*
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | /**
10 | * Instrumented test, which will execute on an Android device.
11 | *
12 | * See [testing documentation](http://d.android.com/tools/testing).
13 | */
14 | @RunWith(AndroidJUnit4::class)
15 | class ExampleInstrumentedTest {
16 | @Test
17 | fun useAppContext() {
18 | // Context of the app under test.
19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
20 | assertEquals("mortitech.blueprint.chat.compose", appContext.packageName)
21 | }
22 | }
--------------------------------------------------------------------------------
/ui/compose/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/ui/compose/src/main/java/mortitech/blueprint/chat/compose/ChatComposeApplication.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.compose
2 |
3 | import android.app.Application
4 | import timber.log.Timber
5 |
6 | class ChatComposeApplication : Application() {
7 |
8 | override fun onCreate() {
9 | super.onCreate()
10 |
11 | // Initialize Timber for logging
12 | if (BuildConfig.DEBUG) {
13 | Timber.plant(Timber.DebugTree())
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/ui/compose/src/main/java/mortitech/blueprint/chat/compose/conversation/ChatMessageIncoming.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.compose.conversation
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.border
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.foundation.shape.CircleShape
8 | import androidx.compose.foundation.shape.RoundedCornerShape
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.draw.clip
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.graphics.painter.Painter
16 | import androidx.compose.ui.res.painterResource
17 | import androidx.compose.ui.text.font.FontWeight
18 | import androidx.compose.ui.tooling.preview.Preview
19 | import androidx.compose.ui.unit.dp
20 | import androidx.compose.ui.unit.sp
21 | import mortitech.blueprint.chat.core.R
22 | import mortitech.blueprint.chat.data.ChatMessage
23 |
24 | @Composable
25 | fun ChatMessageIncoming(message: ChatMessage) {
26 | Row(
27 | modifier = Modifier
28 | .fillMaxWidth()
29 | .padding(top = 8.dp, bottom = 8.dp, start = 8.dp, end = 32.dp),
30 | verticalAlignment = Alignment.Top
31 | ) {
32 | CircleImage(
33 | painter = painterResource(id = R.drawable.profile_placeholder),
34 | contentDescription = null,
35 | modifier = Modifier
36 | .size(40.dp)
37 | .border(2.dp, Color.Gray, CircleShape)
38 | )
39 | Column(
40 | modifier = Modifier
41 | .weight(1f)
42 | .padding(start = 16.dp)
43 | .background(
44 | color = Color(0xFFECEFF1),
45 | shape = RoundedCornerShape(8.dp)
46 | )
47 | .padding(8.dp)
48 | ) {
49 | Text(
50 | text = message.sender,
51 | fontWeight = FontWeight.Bold,
52 | fontSize = 14.sp,
53 | color = Color.Black,
54 | )
55 | Text(
56 | text = message.content,
57 | fontSize = 16.sp,
58 | color = Color.Black,
59 | modifier = Modifier.padding(top = 8.dp)
60 | )
61 | Text(
62 | text = message.time,
63 | fontSize = 12.sp,
64 | color = Color.Gray,
65 | modifier = Modifier
66 | .padding(top = 8.dp)
67 | .align(Alignment.End)
68 | )
69 | }
70 | }
71 | }
72 |
73 | @Composable
74 | fun CircleImage(
75 | painter: Painter,
76 | contentDescription: String?,
77 | modifier: Modifier = Modifier,
78 | ) {
79 | Image(
80 | painter = painter,
81 | contentDescription = contentDescription,
82 | modifier = modifier
83 | .clip(CircleShape)
84 | )
85 | }
86 |
87 |
88 | @Preview(showBackground = true)
89 | @Composable
90 | private fun IncomingChatMessagePreview() {
91 | ChatMessageIncoming(
92 | message = ChatMessage(
93 | sender = "John Doe",
94 | content = "Hello John Doe, how are you?",
95 | time = "12:00 PM",
96 | messageType = ChatMessage.MessageType.INCOMING
97 | )
98 | )
99 | }
--------------------------------------------------------------------------------
/ui/compose/src/main/java/mortitech/blueprint/chat/compose/conversation/ChatMessageInput.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.compose.conversation
2 |
3 | import androidx.compose.foundation.layout.Row
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.text.KeyboardOptions
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.filled.Send
9 | import androidx.compose.material3.*
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.text.input.ImeAction
15 | import androidx.compose.ui.text.input.KeyboardType
16 | import androidx.compose.ui.tooling.preview.Preview
17 | import androidx.compose.ui.unit.dp
18 |
19 | @OptIn(ExperimentalMaterial3Api::class)
20 | @Composable
21 | fun ChatMessageInput(
22 | modifier: Modifier = Modifier,
23 | value: String,
24 | onValueChange: (String) -> Unit,
25 | onSend: () -> Unit
26 | ) {
27 | Row(
28 | modifier = modifier.fillMaxWidth(),
29 | verticalAlignment = Alignment.CenterVertically
30 | ) {
31 | OutlinedTextField(
32 | value = value,
33 | onValueChange = onValueChange,
34 | label = { Text(text = "Enter your message") },
35 | singleLine = true,
36 | keyboardOptions = KeyboardOptions(
37 | imeAction = ImeAction.Send,
38 | keyboardType = KeyboardType.Text
39 | ),
40 | modifier = Modifier.weight(1f)
41 | )
42 |
43 | IconButton(
44 | onClick = onSend,
45 | modifier = Modifier.padding(start = 8.dp)
46 | ) {
47 | Icon(
48 | imageVector = Icons.Default.Send,
49 | contentDescription = "Send",
50 | tint = Color.Blue
51 | )
52 | }
53 | }
54 | }
55 |
56 | @Preview(showBackground = true)
57 | @Composable
58 | private fun ChatInputMessagePreview() {
59 | ChatMessageInput(
60 | value = "",
61 | onValueChange = {},
62 | onSend = {}
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/ui/compose/src/main/java/mortitech/blueprint/chat/compose/conversation/ChatMessageOutgoing.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.compose.conversation
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.foundation.shape.RoundedCornerShape
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.tooling.preview.Preview
12 | import androidx.compose.ui.unit.dp
13 | import androidx.compose.ui.unit.sp
14 | import mortitech.blueprint.chat.data.ChatMessage
15 |
16 | @Composable
17 | fun ChatMessageOutgoing(message: ChatMessage) {
18 | Row(
19 | modifier = Modifier
20 | .fillMaxWidth()
21 | .padding(top = 8.dp, bottom = 8.dp, start = 32.dp, end = 8.dp),
22 | horizontalArrangement = Arrangement.End,
23 | verticalAlignment = Alignment.Top
24 | ) {
25 | Column(
26 | modifier = Modifier
27 | .weight(1f)
28 | .padding(end = 16.dp)
29 | .background(
30 | color = Color(0xFF2F80ED),
31 | shape = RoundedCornerShape(8.dp)
32 | )
33 | .padding(12.dp)
34 | ) {
35 | Text(
36 | text = message.content,
37 | fontSize = 16.sp,
38 | color = Color.White,
39 | modifier = Modifier.align(Alignment.End)
40 | )
41 | Text(
42 | text = message.time,
43 | fontSize = 12.sp,
44 | color = Color.White,
45 | modifier = Modifier.padding(top = 8.dp)
46 | )
47 | }
48 | }
49 | }
50 |
51 | @Preview(showBackground = true)
52 | @Composable
53 | private fun OutgoingChatMessagePreview() {
54 | ChatMessageOutgoing(
55 | message = ChatMessage(
56 | sender = "John Doe",
57 | content = "Hello John Doe, how are you?",
58 | time = "12:00 PM",
59 | messageType = ChatMessage.MessageType.INCOMING
60 | )
61 | )
62 | }
--------------------------------------------------------------------------------
/ui/compose/src/main/java/mortitech/blueprint/chat/compose/conversation/ConversationActivity.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.compose.conversation
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Surface
9 | import androidx.compose.ui.Modifier
10 | import mortitech.blueprint.chat.compose.ui.theme.BlueprintChatTheme
11 |
12 | class ConversationActivity : ComponentActivity() {
13 |
14 | companion object {
15 | const val EXTRA_USERNAME = "mortitech.blueprint.chat.EXTRA_USERNAME"
16 | }
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 |
20 | val username = intent.getStringExtra(EXTRA_USERNAME)
21 | if (username.isNullOrBlank()) {
22 | finish()
23 | return
24 | }
25 |
26 | setContent {
27 | BlueprintChatTheme {
28 | // A surface container using the 'background' color from the theme
29 | Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
30 | ConversationScreen(
31 | viewModel = ConversationViewModel(),
32 | userName = username
33 | )
34 | }
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ui/compose/src/main/java/mortitech/blueprint/chat/compose/conversation/ConversationScreen.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.compose.conversation
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.lazy.LazyColumn
5 | import androidx.compose.foundation.lazy.LazyListState
6 | import androidx.compose.foundation.lazy.items
7 | import androidx.compose.foundation.lazy.rememberLazyListState
8 | import androidx.compose.runtime.*
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.tooling.preview.Preview
12 | import androidx.compose.ui.unit.dp
13 | import mortitech.blueprint.chat.data.ChatMessage
14 |
15 | @Composable
16 | fun ConversationScreen(
17 | userName: String,
18 | viewModel: ConversationViewModel,
19 | ) {
20 | val messages = remember { mutableStateListOf() }
21 | val message = remember { mutableStateOf("") }
22 | val scrollState = rememberLazyListState()
23 |
24 | LaunchedEffect("Key") {
25 | viewModel.incomingMessages.collect {
26 | messages.add(it)
27 | scrollState.scrollToItem(messages.size - 1)
28 | }
29 | }
30 |
31 | ConversationContent(
32 | messages = messages,
33 | message = message.value,
34 | updateMessage = { message.value = it },
35 | onSendClick = {
36 | if (message.value.isNotEmpty()) {
37 | viewModel.sendMessage(userName, message.value)
38 | message.value = ""
39 | }
40 | },
41 | scrollState = scrollState,
42 | )
43 | }
44 |
45 | @Composable
46 | fun ConversationContent(
47 | messages: List,
48 | message: String,
49 | updateMessage: (String) -> Unit,
50 | onSendClick: () -> Unit,
51 | scrollState: LazyListState,
52 | ) {
53 | Column(
54 | modifier = Modifier.fillMaxSize()
55 | ) {
56 | ConversationMessages(
57 | modifier = Modifier.weight(1f),
58 | messages = messages,
59 | scrollState = scrollState
60 | )
61 |
62 | ChatMessageInput(
63 | modifier = Modifier.padding(start = 8.dp, bottom = 8.dp),
64 | value = message,
65 | onValueChange = { updateMessage(it) },
66 | onSend = onSendClick
67 | )
68 | }
69 | }
70 |
71 | @Composable
72 | private fun ConversationMessages(
73 | modifier: Modifier = Modifier,
74 | scrollState: LazyListState,
75 | messages: List,
76 | ) {
77 | LazyColumn(
78 | modifier = modifier.fillMaxSize(),
79 | contentPadding = PaddingValues(8.dp),
80 | reverseLayout = true,
81 | state = scrollState,
82 | verticalArrangement = Arrangement.spacedBy(8.dp),
83 | horizontalAlignment = Alignment.Start
84 | ) {
85 | items(messages) { message ->
86 | if (message.isOutgoing())
87 | ChatMessageOutgoing(message = message)
88 | else
89 | ChatMessageIncoming(message = message)
90 | }
91 | }
92 | }
93 |
94 | @Preview(showBackground = true)
95 | @Composable
96 | private fun ConversationContentPreview() {
97 | val messages = listOf(
98 | ChatMessage(
99 | sender = "John Doe",
100 | content = "Hello, World!",
101 | time = "12:00:00",
102 | ChatMessage.MessageType.INCOMING
103 | ),
104 | ChatMessage(
105 | sender = "Me",
106 | content = "Hello, John!",
107 | time = "12:01:00",
108 | ChatMessage.MessageType.OUTGOING
109 | ),
110 | )
111 | ConversationContent(
112 | messages = messages,
113 | message = "",
114 | updateMessage = {},
115 | onSendClick = {},
116 | scrollState = rememberLazyListState()
117 | )
118 | }
119 |
--------------------------------------------------------------------------------
/ui/compose/src/main/java/mortitech/blueprint/chat/compose/conversation/ConversationViewModel.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.compose.conversation
2 |
3 | import androidx.lifecycle.ViewModel
4 | import kotlinx.coroutines.flow.SharedFlow
5 | import mortitech.blueprint.chat.data.ChatMessage
6 | import mortitech.blueprint.chat.data.ChatRepository
7 |
8 | class ConversationViewModel : ViewModel() {
9 | private val chatRepository = ChatRepository()
10 |
11 | /**
12 | * A [SharedFlow] of incoming chat messages.
13 | *
14 | * The flow emits incoming chat messages as they arrive, and can be observed by clients to display the chat history.
15 | */
16 | val incomingMessages: SharedFlow = chatRepository.incomingMessages
17 |
18 | /**
19 | * Sends a chat message to the server.
20 | *
21 | * @param username the username of the sender.
22 | * @param content the content of the message.
23 | */
24 | fun sendMessage(username: String, content: String) {
25 | chatRepository.sendMessage(username, content)
26 | }
27 |
28 | /**
29 | * Cancels all coroutines launched by the [ConversationViewModel] instance.
30 | */
31 | override fun onCleared() {
32 | super.onCleared()
33 | chatRepository.cancel()
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/ui/compose/src/main/java/mortitech/blueprint/chat/compose/login/LoginActivity.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.compose.login
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.compose.setContent
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.Surface
10 | import androidx.compose.ui.Modifier
11 | import mortitech.blueprint.chat.compose.conversation.ConversationActivity
12 | import mortitech.blueprint.chat.compose.ui.theme.BlueprintChatTheme
13 |
14 | class LoginActivity : ComponentActivity() {
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | setContent {
18 | BlueprintChatTheme {
19 | Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
20 | LoginScreen { username ->
21 | val intent = Intent(this, ConversationActivity::class.java)
22 | intent.putExtra(ConversationActivity.EXTRA_USERNAME, username)
23 | startActivity(intent)
24 | finish()
25 | }
26 | }
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/ui/compose/src/main/java/mortitech/blueprint/chat/compose/login/LoginScreen.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.compose.login
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.foundation.text.KeyboardOptions
6 | import androidx.compose.material3.Button
7 | import androidx.compose.material3.ExperimentalMaterial3Api
8 | import androidx.compose.material3.OutlinedTextField
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.*
11 | import androidx.compose.runtime.saveable.rememberSaveable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.res.painterResource
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.compose.ui.text.input.ImeAction
17 | import androidx.compose.ui.text.input.KeyboardType
18 | import androidx.compose.ui.tooling.preview.Preview
19 | import androidx.compose.ui.unit.dp
20 | import mortitech.blueprint.chat.core.R
21 |
22 |
23 | @Composable
24 | fun LoginScreen(
25 | onLogin: (userName: String) -> Unit,
26 | ) {
27 | val usernameError = stringResource(R.string.login_username_error)
28 |
29 | var username by rememberSaveable { mutableStateOf("") }
30 | var errorMessage by rememberSaveable { mutableStateOf("") }
31 |
32 | LoginContent(
33 | username = username,
34 | errorMessage = errorMessage,
35 | updateUsername = { username = it },
36 | onLogin = {
37 | if (username.isNotEmpty()) {
38 | onLogin(username)
39 | } else {
40 | errorMessage = usernameError
41 | }
42 | }
43 | )
44 | }
45 |
46 | @OptIn(ExperimentalMaterial3Api::class)
47 | @Composable
48 | fun LoginContent(
49 | username: String,
50 | errorMessage: String,
51 | updateUsername: (String) -> Unit,
52 | onLogin: (username: String) -> Unit
53 | ) {
54 | Column(
55 | modifier = Modifier
56 | .fillMaxSize()
57 | .padding(16.dp),
58 | verticalArrangement = Arrangement.Center
59 | ) {
60 | Spacer(modifier = Modifier.weight(1f))
61 |
62 | OutlinedTextField(
63 | value = username,
64 | isError = errorMessage.isNotEmpty(),
65 | supportingText = {
66 | if (errorMessage.isNotEmpty()) {
67 | Text(text = errorMessage)
68 | }
69 | },
70 | onValueChange = { updateUsername(it) },
71 | label = { Text(text = stringResource(R.string.login_username_hint)) },
72 | singleLine = true,
73 | keyboardOptions = KeyboardOptions(
74 | imeAction = ImeAction.Done,
75 | keyboardType = KeyboardType.Text
76 | ),
77 | modifier = Modifier.fillMaxWidth()
78 | )
79 |
80 | Spacer(modifier = Modifier.height(16.dp))
81 |
82 | Button(
83 | onClick = { onLogin(username) },
84 | modifier = Modifier.fillMaxWidth()
85 | ) {
86 | Text(text = stringResource(R.string.login_button_text))
87 | }
88 |
89 | Spacer(modifier = Modifier.weight(1f))
90 |
91 | Image(
92 | painter = painterResource(R.mipmap.watermark),
93 | contentDescription = null,
94 | modifier = Modifier
95 | .fillMaxWidth()
96 | .padding(bottom = 16.dp)
97 | .wrapContentHeight()
98 | .align(Alignment.CenterHorizontally)
99 | )
100 | }
101 | }
102 |
103 | @Preview(name = "Normal State", showBackground = true)
104 | @Composable
105 | fun LoginContentPreview() {
106 | LoginContent(
107 | username = "johndoe",
108 | errorMessage = "",
109 | updateUsername = {},
110 | onLogin = {}
111 | )
112 | }
113 |
114 | @Preview(name = "Error State", showBackground = true)
115 | @Composable
116 | fun LoginContentErrorPreview() {
117 | LoginContent(
118 | username = "",
119 | errorMessage = stringResource(R.string.login_username_error),
120 | updateUsername = {},
121 | onLogin = {}
122 | )
123 | }
--------------------------------------------------------------------------------
/ui/compose/src/main/java/mortitech/blueprint/chat/compose/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.compose.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple200 = Color(0xFFBB86FC)
6 | val Purple500 = Color(0xFF6200EE)
7 | val Purple700 = Color(0xFF3700B3)
8 | val Teal200 = Color(0xFF03DAC5)
--------------------------------------------------------------------------------
/ui/compose/src/main/java/mortitech/blueprint/chat/compose/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.compose.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.*
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.platform.LocalContext
8 |
9 | private val DarkColors = darkColorScheme(
10 | primary = Purple200,
11 | onPrimary = Purple700,
12 | secondary = Teal200
13 | )
14 |
15 | private val LightColors = lightColorScheme(
16 | primary = Purple500,
17 | onPrimary = Purple700,
18 | secondary = Teal200
19 | )
20 |
21 | @Composable
22 | fun BlueprintChatTheme(
23 | darkTheme: Boolean = isSystemInDarkTheme(),
24 | content: @Composable () -> Unit
25 | ) {
26 | val colorScheme =
27 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
28 | val context = LocalContext.current
29 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
30 | } else {
31 | if (darkTheme) DarkColors else LightColors
32 | }
33 |
34 | MaterialTheme(
35 | colorScheme = colorScheme,
36 | content = content
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/ui/compose/src/main/res/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/ui/compose/src/main/res/.DS_Store
--------------------------------------------------------------------------------
/ui/compose/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BluePrint Chat Compose
3 |
--------------------------------------------------------------------------------
/ui/compose/src/test/java/mortitech/blueprint/chat/compose/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.compose
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, 2 + 2)
15 | }
16 | }
--------------------------------------------------------------------------------
/ui/core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/ui/core/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | namespace 'mortitech.blueprint.chat.core'
8 | compileSdk 33
9 |
10 | defaultConfig {
11 | minSdk 29
12 | targetSdk 33
13 |
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | consumerProguardFiles "consumer-rules.pro"
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_1_8
26 | targetCompatibility JavaVersion.VERSION_1_8
27 | }
28 | kotlinOptions {
29 | jvmTarget = '1.8'
30 | }
31 | }
32 |
33 | dependencies {
34 |
35 | implementation 'androidx.core:core-ktx:1.9.0'
36 | implementation 'androidx.appcompat:appcompat:1.6.1'
37 | implementation 'com.google.android.material:material:1.8.0'
38 | testImplementation 'junit:junit:4.13.2'
39 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
40 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
41 | }
--------------------------------------------------------------------------------
/ui/core/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/ui/core/consumer-rules.pro
--------------------------------------------------------------------------------
/ui/core/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/ui/core/src/androidTest/java/mortitech/blueprint/chat/core/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.core
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import org.junit.Assert.*
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | /**
10 | * Instrumented test, which will execute on an Android device.
11 | *
12 | * See [testing documentation](http://d.android.com/tools/testing).
13 | */
14 | @RunWith(AndroidJUnit4::class)
15 | class ExampleInstrumentedTest {
16 | @Test
17 | fun useAppContext() {
18 | // Context of the app under test.
19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
20 | assertEquals("mortitech.blueprint.chat.core.test", appContext.packageName)
21 | }
22 | }
--------------------------------------------------------------------------------
/ui/core/src/main/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/ui/core/src/main/.DS_Store
--------------------------------------------------------------------------------
/ui/core/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ui/core/src/main/res/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/ui/core/src/main/res/.DS_Store
--------------------------------------------------------------------------------
/ui/core/src/main/res/drawable/image_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/ui/core/src/main/res/drawable/image_placeholder.png
--------------------------------------------------------------------------------
/ui/core/src/main/res/drawable/profile_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/ui/core/src/main/res/drawable/profile_placeholder.png
--------------------------------------------------------------------------------
/ui/core/src/main/res/font/chat_fonts.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
14 |
--------------------------------------------------------------------------------
/ui/core/src/main/res/font/roboto_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/ui/core/src/main/res/font/roboto_bold.ttf
--------------------------------------------------------------------------------
/ui/core/src/main/res/font/roboto_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/ui/core/src/main/res/font/roboto_regular.ttf
--------------------------------------------------------------------------------
/ui/core/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/ui/core/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ui/core/src/main/res/mipmap-xxxhdpi/watermark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/ui/core/src/main/res/mipmap-xxxhdpi/watermark.png
--------------------------------------------------------------------------------
/ui/core/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
--------------------------------------------------------------------------------
/ui/core/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
11 | #ECEFF1
12 | #6B7B88
13 | #FF4081
14 | #808080
15 |
16 |
--------------------------------------------------------------------------------
/ui/core/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Enter username
3 | Please enter a valid username
4 | Login
5 | Chat
6 | Send message
7 | Enter message
8 |
--------------------------------------------------------------------------------
/ui/core/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
19 |
--------------------------------------------------------------------------------
/ui/core/src/test/java/mortitech/blueprint/chat/core/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.core
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, 2 + 2)
15 | }
16 | }
--------------------------------------------------------------------------------
/ui/legacy/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/ui/legacy/.DS_Store
--------------------------------------------------------------------------------
/ui/legacy/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/ui/legacy/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | namespace 'mortitech.blueprint.chat.legacy'
8 | compileSdk 33
9 |
10 | defaultConfig {
11 | applicationId "mortitech.blueprint.chat.legacy"
12 | minSdk 29
13 | targetSdk 33
14 | versionCode 1
15 | versionName "1.0"
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | }
19 |
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 | compileOptions {
27 | sourceCompatibility JavaVersion.VERSION_1_8
28 | targetCompatibility JavaVersion.VERSION_1_8
29 | }
30 | kotlinOptions {
31 | jvmTarget = '1.8'
32 | }
33 | buildFeatures {
34 | viewBinding = true
35 | dataBinding = true
36 | }
37 | }
38 |
39 | dependencies {
40 |
41 | implementation 'androidx.core:core-ktx:1.9.0'
42 | implementation 'androidx.activity:activity-ktx:1.7.0'
43 | implementation 'androidx.appcompat:appcompat:1.6.1'
44 | implementation 'com.google.android.material:material:1.8.0'
45 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
46 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
47 | implementation 'com.jakewharton.timber:timber:5.0.1'
48 | implementation 'de.hdodenhof:circleimageview:3.1.0'
49 |
50 | implementation project(":ui:core")
51 | implementation project(":data")
52 |
53 | testImplementation 'junit:junit:4.13.2'
54 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
55 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
56 | }
--------------------------------------------------------------------------------
/ui/legacy/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/ui/legacy/src/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/ui/legacy/src/.DS_Store
--------------------------------------------------------------------------------
/ui/legacy/src/androidTest/java/mortitech/blueprint/chat/legacy/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.legacy
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import androidx.test.platform.app.InstrumentationRegistry
5 | import org.junit.Assert.*
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | /**
10 | * Instrumented test, which will execute on an Android device.
11 | *
12 | * See [testing documentation](http://d.android.com/tools/testing).
13 | */
14 | @RunWith(AndroidJUnit4::class)
15 | class ExampleInstrumentedTest {
16 | @Test
17 | fun useAppContext() {
18 | // Context of the app under test.
19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
20 | assertEquals("mortitech.blueprint.chat", appContext.packageName)
21 | }
22 | }
--------------------------------------------------------------------------------
/ui/legacy/src/main/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/ui/legacy/src/main/.DS_Store
--------------------------------------------------------------------------------
/ui/legacy/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/ui/legacy/src/main/java/mortitech/blueprint/chat/legacy/ChatLegacyApplication.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.legacy
2 |
3 | import android.app.Application
4 | import androidx.appcompat.app.AppCompatDelegate
5 | import timber.log.Timber
6 |
7 | class ChatLegacyApplication : Application() {
8 |
9 | override fun onCreate() {
10 | super.onCreate()
11 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
12 |
13 | // Initialize Timber for logging
14 | if (BuildConfig.DEBUG) {
15 | Timber.plant(Timber.DebugTree())
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/ui/legacy/src/main/java/mortitech/blueprint/chat/legacy/conversation/ConversationActivity.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.legacy.conversation
2 |
3 | import android.os.Bundle
4 | import android.view.inputmethod.EditorInfo
5 | import androidx.activity.viewModels
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.lifecycle.lifecycleScope
8 | import androidx.recyclerview.widget.LinearLayoutManager
9 | import kotlinx.coroutines.launch
10 | import mortitech.blueprint.chat.legacy.databinding.ActivityConversationBinding
11 |
12 | class ConversationActivity : AppCompatActivity() {
13 | companion object {
14 | const val EXTRA_USERNAME = "mortitech.blueprint.chat.EXTRA_USERNAME"
15 | }
16 |
17 | private val viewModel: ConversationViewModel by viewModels()
18 | private lateinit var binding: ActivityConversationBinding
19 | private val conversationAdapter = ConversationAdapter()
20 |
21 | override fun onCreate(savedInstanceState: Bundle?) {
22 | super.onCreate(savedInstanceState)
23 | binding = ActivityConversationBinding.inflate(layoutInflater)
24 | setContentView(binding.root)
25 |
26 | val username = intent.getStringExtra(EXTRA_USERNAME)
27 | if (username.isNullOrBlank()) {
28 | finish()
29 | return
30 | }
31 |
32 | setupRecyclerView()
33 | setupSendButton(username)
34 | observeIncomingMessages()
35 | }
36 |
37 | private fun setupRecyclerView() {
38 | binding.recyclerView.apply {
39 | layoutManager = LinearLayoutManager(this@ConversationActivity)
40 | adapter = conversationAdapter
41 | }
42 | }
43 |
44 | private fun setupSendButton(username: String) {
45 | binding.messageEditText.setOnEditorActionListener { _, actionId, _ ->
46 | if (actionId == EditorInfo.IME_ACTION_SEND) {
47 | val content = binding.messageEditText.text.toString()
48 | sendMessage(username, content)
49 | true
50 | } else {
51 | false
52 | }
53 | }
54 |
55 | binding.messageInputLayout.setEndIconOnClickListener {
56 | val content = binding.messageEditText.text.toString()
57 | sendMessage(username, content)
58 | }
59 | }
60 |
61 | private fun sendMessage(username: String, content: String) {
62 | if (content.isNotBlank() && username.isNotBlank()) {
63 | viewModel.sendMessage(username, content)
64 | binding.messageEditText.text?.clear()
65 | }
66 | }
67 |
68 | private fun observeIncomingMessages() {
69 | lifecycleScope.launch {
70 | viewModel.incomingMessages.collect { message ->
71 | conversationAdapter.addMessage(message)
72 | binding.recyclerView.scrollToPosition(conversationAdapter.itemCount - 1)
73 | }
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/ui/legacy/src/main/java/mortitech/blueprint/chat/legacy/conversation/ConversationAdapter.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.legacy.conversation
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.DiffUtil
6 | import androidx.recyclerview.widget.ListAdapter
7 | import androidx.recyclerview.widget.RecyclerView
8 | import mortitech.blueprint.chat.data.ChatMessage
9 | import mortitech.blueprint.chat.legacy.databinding.ChatMessageIncomingItemBinding
10 | import mortitech.blueprint.chat.legacy.databinding.ChatMessageOutgoingItemBinding
11 |
12 | class ConversationAdapter : ListAdapter(DiffCallback) {
13 |
14 | companion object {
15 | private const val VIEW_TYPE_INCOMING = 0
16 | private const val VIEW_TYPE_OUTGOING = 1
17 | }
18 |
19 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
20 | return when (viewType) {
21 | VIEW_TYPE_INCOMING -> IncomingMessageViewHolder(
22 | ChatMessageIncomingItemBinding.inflate(
23 | LayoutInflater.from(parent.context),
24 | parent,
25 | false
26 | )
27 | )
28 | VIEW_TYPE_OUTGOING -> OutgoingMessageViewHolder(
29 | ChatMessageOutgoingItemBinding.inflate(
30 | LayoutInflater.from(parent.context),
31 | parent,
32 | false
33 | )
34 | )
35 | else -> throw IllegalArgumentException("Invalid view type")
36 | }
37 | }
38 |
39 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
40 | val message = getItem(position)
41 | when (holder.itemViewType) {
42 | VIEW_TYPE_INCOMING -> (holder as IncomingMessageViewHolder).bind(message)
43 | VIEW_TYPE_OUTGOING -> (holder as OutgoingMessageViewHolder).bind(message)
44 | else -> throw IllegalArgumentException("Invalid view type")
45 | }
46 | }
47 |
48 | override fun getItemViewType(position: Int): Int {
49 | val message = getItem(position)
50 | return if (message.messageType == ChatMessage.MessageType.INCOMING) {
51 | VIEW_TYPE_INCOMING
52 | } else {
53 | VIEW_TYPE_OUTGOING
54 | }
55 | }
56 |
57 | fun addMessage(message: ChatMessage) {
58 | submitList(currentList + message)
59 | }
60 |
61 | inner class IncomingMessageViewHolder(private val binding: ChatMessageIncomingItemBinding) :
62 | RecyclerView.ViewHolder(binding.root) {
63 |
64 | fun bind(message: ChatMessage) {
65 | binding.message = message
66 | binding.executePendingBindings()
67 | }
68 | }
69 |
70 | inner class OutgoingMessageViewHolder(private val binding: ChatMessageOutgoingItemBinding) :
71 | RecyclerView.ViewHolder(binding.root) {
72 |
73 | fun bind(message: ChatMessage) {
74 | binding.message = message
75 | binding.executePendingBindings()
76 | }
77 | }
78 |
79 | object DiffCallback : DiffUtil.ItemCallback() {
80 | override fun areItemsTheSame(oldItem: ChatMessage, newItem: ChatMessage): Boolean {
81 | return oldItem === newItem
82 | }
83 |
84 | override fun areContentsTheSame(oldItem: ChatMessage, newItem: ChatMessage): Boolean {
85 | return oldItem == newItem
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/ui/legacy/src/main/java/mortitech/blueprint/chat/legacy/conversation/ConversationViewModel.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.legacy.conversation
2 |
3 | import androidx.lifecycle.ViewModel
4 | import kotlinx.coroutines.flow.SharedFlow
5 | import mortitech.blueprint.chat.data.ChatMessage
6 | import mortitech.blueprint.chat.data.ChatRepository
7 |
8 | /**
9 | * The [ConversationViewModel] class is responsible for managing the chat messages and providing an interface for the UI to interact with the chat.
10 | *
11 | * It provides a [sendMessage] method to send chat messages, and exposes a [SharedFlow] of incoming messages.
12 | * The [incomingMessages] flow emits incoming chat messages as they arrive, and can be observed by clients to display the chat history.
13 | *
14 | * The [sendMessage] method delegates to the [ChatRepository] to send the message to the server.
15 | * The method takes a [username] and [content] as input parameters, and forwards them to the repository for processing.
16 | *
17 | * The [onCleared] method cancels all coroutines launched by the [ConversationViewModel] instance.
18 | */
19 | class ConversationViewModel : ViewModel() {
20 | private val chatRepository = ChatRepository()
21 |
22 | /**
23 | * A [SharedFlow] of incoming chat messages.
24 | *
25 | * The flow emits incoming chat messages as they arrive, and can be observed by clients to display the chat history.
26 | */
27 | val incomingMessages: SharedFlow = chatRepository.incomingMessages
28 |
29 | /**
30 | * Sends a chat message to the server.
31 | *
32 | * @param username the username of the sender.
33 | * @param content the content of the message.
34 | */
35 | fun sendMessage(username: String, content: String) {
36 | chatRepository.sendMessage(username, content)
37 | }
38 |
39 | /**
40 | * Cancels all coroutines launched by the [ConversationViewModel] instance.
41 | */
42 | override fun onCleared() {
43 | super.onCleared()
44 | chatRepository.cancel()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/ui/legacy/src/main/java/mortitech/blueprint/chat/legacy/login/LoginActivity.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.legacy.login
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.inputmethod.EditorInfo
6 | import androidx.appcompat.app.AppCompatActivity
7 | import mortitech.blueprint.chat.core.R
8 | import mortitech.blueprint.chat.legacy.databinding.ActivityLoginBinding
9 | import mortitech.blueprint.chat.legacy.conversation.ConversationActivity
10 |
11 | class LoginActivity : AppCompatActivity() {
12 |
13 | private lateinit var binding: ActivityLoginBinding
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | binding = ActivityLoginBinding.inflate(layoutInflater)
18 | setContentView(binding.root)
19 |
20 | binding.loginButton.setOnClickListener {
21 | val username = binding.usernameEditText.text.toString().trim()
22 | if (username.isNotEmpty()) {
23 | val intent = Intent(this, ConversationActivity::class.java)
24 | intent.putExtra(ConversationActivity.EXTRA_USERNAME, username)
25 | startActivity(intent)
26 | finish()
27 | } else {
28 | binding.usernameTextInputLayout.error = getString(R.string.login_username_error)
29 | }
30 | }
31 |
32 | binding.usernameEditText.setOnEditorActionListener { _, actionId, _ ->
33 | if (actionId == EditorInfo.IME_ACTION_DONE) {
34 | binding.loginButton.performClick()
35 | true
36 | } else {
37 | false
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/ui/legacy/src/main/res/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/ui/legacy/src/main/res/.DS_Store
--------------------------------------------------------------------------------
/ui/legacy/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/ui/legacy/src/main/res/drawable/chat_message_incoming_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/ui/legacy/src/main/res/drawable/chat_message_outgoing_background.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ui/legacy/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/ui/legacy/src/main/res/drawable/ic_send.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/ui/legacy/src/main/res/layout/activity_conversation.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
30 |
31 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/ui/legacy/src/main/res/layout/activity_login.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
22 |
23 |
31 |
32 |
33 |
34 |
43 |
44 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/ui/legacy/src/main/res/layout/chat_message_incoming_item.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
9 |
10 |
11 |
12 |
20 |
21 |
28 |
29 |
37 |
38 |
46 |
47 |
55 |
56 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/ui/legacy/src/main/res/layout/chat_message_outgoing_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
19 |
20 |
28 |
29 |
37 |
38 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/ui/legacy/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BluePrint Chat Legacy
3 |
--------------------------------------------------------------------------------
/ui/legacy/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/ui/legacy/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/ui/legacy/src/test/java/mortitech/blueprint/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/ui/legacy/src/test/java/mortitech/blueprint/.DS_Store
--------------------------------------------------------------------------------
/ui/legacy/src/test/java/mortitech/blueprint/chat/legacy/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package mortitech.blueprint.chat.legacy
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, 2 + 2)
15 | }
16 | }
--------------------------------------------------------------------------------