├── .DS_Store ├── .gitignore ├── .idea ├── .name ├── compiler.xml └── misc.xml ├── README.md ├── build.gradle ├── data ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── mortitech │ │ └── blueprint │ │ └── chat │ │ └── data │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── mortitech │ │ └── blueprint │ │ └── chat │ │ └── data │ │ ├── ChatMessage.kt │ │ ├── ChatRepository.kt │ │ └── Constants.kt │ └── test │ └── java │ └── mortitech │ └── blueprint │ └── chat │ └── data │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── media ├── .DS_Store ├── compose-chat-list-screenshot.png ├── legacy-chat-list-screenshot.png └── legacy-chat-recording.gif ├── settings.gradle └── ui ├── .DS_Store ├── compose ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── mortitech │ │ └── blueprint │ │ └── chat │ │ └── compose │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── mortitech │ │ │ └── blueprint │ │ │ └── chat │ │ │ └── compose │ │ │ ├── ChatComposeApplication.kt │ │ │ ├── conversation │ │ │ ├── ChatMessageIncoming.kt │ │ │ ├── ChatMessageInput.kt │ │ │ ├── ChatMessageOutgoing.kt │ │ │ ├── ConversationActivity.kt │ │ │ ├── ConversationScreen.kt │ │ │ └── ConversationViewModel.kt │ │ │ ├── login │ │ │ ├── LoginActivity.kt │ │ │ └── LoginScreen.kt │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ └── Theme.kt │ └── res │ │ ├── .DS_Store │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── mortitech │ └── blueprint │ └── chat │ └── compose │ └── ExampleUnitTest.kt ├── core ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── mortitech │ │ └── blueprint │ │ └── chat │ │ └── core │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── .DS_Store │ ├── AndroidManifest.xml │ └── res │ │ ├── .DS_Store │ │ ├── drawable │ │ ├── image_placeholder.png │ │ └── profile_placeholder.png │ │ ├── font │ │ ├── chat_fonts.xml │ │ ├── roboto_bold.ttf │ │ └── roboto_regular.ttf │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── watermark.png │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── mortitech │ └── blueprint │ └── chat │ └── core │ └── ExampleUnitTest.kt └── legacy ├── .DS_Store ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── .DS_Store ├── androidTest └── java │ └── mortitech │ └── blueprint │ └── chat │ └── legacy │ └── ExampleInstrumentedTest.kt ├── main ├── .DS_Store ├── AndroidManifest.xml ├── java │ └── mortitech │ │ └── blueprint │ │ └── chat │ │ └── legacy │ │ ├── ChatLegacyApplication.kt │ │ ├── conversation │ │ ├── ConversationActivity.kt │ │ ├── ConversationAdapter.kt │ │ └── ConversationViewModel.kt │ │ └── login │ │ └── LoginActivity.kt └── res │ ├── .DS_Store │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── chat_message_incoming_background.xml │ ├── chat_message_outgoing_background.xml │ ├── ic_launcher_background.xml │ └── ic_send.xml │ ├── layout │ ├── activity_conversation.xml │ ├── activity_login.xml │ ├── chat_message_incoming_item.xml │ └── chat_message_outgoing_item.xml │ ├── values │ └── strings.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml └── test └── java └── mortitech └── blueprint ├── .DS_Store └── chat └── legacy └── ExampleUnitTest.kt /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mo0rti/android-blueprint-kotlin-flow-chat/5dd1e1ae1d7ca3f3a8f5ae059c68809f20c980cf/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/deploymentTargetDropDown.xml 45 | .idea/assetWizardSettings.xml 46 | .idea/dictionaries 47 | .idea/libraries 48 | # Android Studio 3 in .gitignore file. 49 | .idea/caches 50 | .idea/modules.xml 51 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 52 | .idea/navEditor.xml 53 | 54 | # Keystore files 55 | # Uncomment the following lines if you do not want to check your keystore files in. 56 | #*.jks 57 | #*.keystore 58 | 59 | # External native build folder generated in Android Studio 2.2 and later 60 | .externalNativeBuild 61 | .cxx/ 62 | 63 | # Google Services (e.g. APIs or Firebase) 64 | # google-services.json 65 | 66 | # Freeline 67 | freeline.py 68 | freeline/ 69 | freeline_project_description.json 70 | 71 | # fastlane 72 | fastlane/report.xml 73 | fastlane/Preview.html 74 | fastlane/screenshots 75 | fastlane/test_output 76 | fastlane/readme.md 77 | 78 | # Version control 79 | vcs.xml 80 | 81 | # lint 82 | lint/intermediates/ 83 | lint/generated/ 84 | lint/outputs/ 85 | lint/tmp/ 86 | # lint/reports/ -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | BlueprintChat -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin Flow Chat Application 2 | 3 |
4 | 5 | 6 | 10 | 14 | 15 |
7 | Kotlin chat legacy 8 |
Chat conversation using Legacy XML layout 9 |
11 | Kotlin chat compose 12 |
Chat conversation using Compose 13 |
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 | [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) 24 | [![Compatible with Compose — 1.4.0](https://img.shields.io/badge/Compatible%20with%20Compose-1.4.0-brightgreen)](https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.4.0) 25 | [![Material design 3](https://img.shields.io/badge/Material%20Design%203-1.0.1-brightgreen)](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 | Kotlin chat flow 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 |