├── .gitignore ├── Jenkinsfile ├── README.md ├── app ├── .gitignore ├── build.gradle ├── lint.xml ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── imakeanapp │ │ └── chatappkotlin │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── imakeanapp │ │ │ └── chatappkotlin │ │ │ ├── MainActivity.kt │ │ │ ├── MainActivityFragmentsListener.kt │ │ │ ├── auth │ │ │ ├── view │ │ │ │ ├── LoginFragment.kt │ │ │ │ ├── SignUpFragment.kt │ │ │ │ └── WelcomeFragment.kt │ │ │ └── viewmodel │ │ │ │ ├── AuthViewModel.kt │ │ │ │ └── AuthViewModelFactory.kt │ │ │ ├── core │ │ │ ├── App.kt │ │ │ ├── AppComponent.kt │ │ │ └── ViewModelModule.kt │ │ │ ├── messages │ │ │ ├── view │ │ │ │ ├── MessagesAdapter.kt │ │ │ │ └── MessagesFragment.kt │ │ │ └── viewmodel │ │ │ │ ├── MessagesViewModel.kt │ │ │ │ └── MessagesViewModelFactory.kt │ │ │ └── util │ │ │ └── InputUtil.kt │ └── res │ │ ├── animator │ │ ├── fade_out.xml │ │ ├── slide_in_from_left.xml │ │ ├── slide_in_from_right.xml │ │ └── slide_out_to_left.xml │ │ ├── drawable-hdpi │ │ ├── speech_bubble.9.png │ │ └── speech_bubble_sent.9.png │ │ ├── drawable-mdpi │ │ ├── speech_bubble.9.png │ │ └── speech_bubble_sent.9.png │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xhdpi │ │ ├── speech_bubble.9.png │ │ └── speech_bubble_sent.9.png │ │ ├── drawable-xxhdpi │ │ ├── speech_bubble.9.png │ │ └── speech_bubble_sent.9.png │ │ ├── drawable-xxxhdpi │ │ ├── speech_bubble.9.png │ │ └── speech_bubble_sent.9.png │ │ ├── drawable │ │ ├── button_background_dark.xml │ │ ├── button_background_positive_1.xml │ │ ├── button_background_positive_1_disabled.xml │ │ ├── button_background_positive_1_enabled.xml │ │ ├── button_background_positive_2.xml │ │ ├── button_background_positive_2_disabled.xml │ │ ├── button_background_positive_2_enabled.xml │ │ ├── ic_launcher_background.xml │ │ └── text_field_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── fragment_login.xml │ │ ├── fragment_messages.xml │ │ ├── fragment_sign_up.xml │ │ ├── fragment_welcome.xml │ │ ├── item_message_received.xml │ │ └── item_message_sent.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher_bk.png │ │ ├── ic_launcher_round.png │ │ └── logo.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher_bk.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher_bk.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher_bk.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher_bk.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── imakeanapp │ └── chatappkotlin │ └── ExampleUnitTest.kt ├── build.gradle ├── data ├── .gitignore ├── build.gradle ├── google-services.json ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── imakeanapp │ │ └── data │ │ ├── core │ │ ├── DatabaseModule.kt │ │ └── RepositoryModule.kt │ │ ├── messages │ │ └── MessagesRepositoryImpl.kt │ │ └── user │ │ └── AuthRepositoryImpl.kt │ └── res │ └── values │ └── strings.xml ├── domain ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── com │ └── imakeanapp │ └── domain │ ├── core │ ├── CompletableUseCase.kt │ ├── CompletableWithParamUseCase.kt │ ├── ObservableUseCase.kt │ ├── SingleUseCase.kt │ └── SingleWithParamUseCase.kt │ ├── messages │ ├── model │ │ └── Message.kt │ ├── repository │ │ └── MessagesRepository.kt │ └── usecase │ │ ├── GetMessagesUseCase.kt │ │ └── SendMessageUseCase.kt │ └── user │ ├── model │ └── User.kt │ ├── repository │ └── AuthRepository.kt │ └── usecase │ ├── LoginUseCase.kt │ └── SignUpUseCase.kt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Generated files 9 | bin/ 10 | gen/ 11 | out/ 12 | 13 | # Gradle files 14 | .gradle 15 | build/ 16 | 17 | # Local configuration file (sdk path, etc) 18 | local.properties 19 | 20 | # gradle properties 21 | gradle.properties 22 | 23 | # IntelliJ 24 | *.iml 25 | .idea/ 26 | 27 | # Keystore files 28 | # Uncomment the following line if you do not want to check your keystore files in. 29 | *.jks 30 | 31 | # Google Services (e.g. APIs or Firebase) 32 | # google-services.json 33 | 34 | # Android Studio captures folder 35 | captures/ 36 | 37 | # External native build folder generated in Android Studio 2.2 and later 38 | .externalNativeBuild 39 | 40 | .DS_Store -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | stages { 4 | stage('Pull Staging') { 5 | when { 6 | anyOf { 7 | branch 'apricot'; 8 | branch 'carrot'; 9 | branch 'banana' 10 | } 11 | } 12 | steps { 13 | sh 'git fetch origin staging:staging' 14 | sh 'git merge staging' 15 | } 16 | } 17 | stage('Setup Gradle') { 18 | steps { 19 | sh 'chmod +x gradlew' 20 | } 21 | } 22 | stage('Build Debug APK') { 23 | when { 24 | not { 25 | branch 'master' 26 | } 27 | } 28 | steps { 29 | script { 30 | if (env.BRANCH_NAME == 'apricot') { 31 | sh './gradlew assembleApricot --configure-on-demand --daemon' 32 | } else if (env.BRANCH_NAME == 'carrot') { 33 | sh './gradlew assembleCarrot --configure-on-demand --daemon' 34 | } else if (env.BRANCH_NAME == 'banana') { 35 | sh './gradlew assembleBanana --configure-on-demand --daemon' 36 | } else { 37 | sh './gradlew assembleDebug --configure-on-demand --daemon' 38 | } 39 | } 40 | 41 | archiveArtifacts '**/*.apk' 42 | } 43 | post { 44 | aborted { 45 | slackSend color: 'warning', message: "DEBUG BUILD ABORTED: ${env.RUN_DISPLAY_URL}" 46 | } 47 | unstable { 48 | slackSend color: 'warning', message: "DEBUG BUILD UNSTABLE: ${env.RUN_DISPLAY_URL}" 49 | } 50 | failure { 51 | slackSend color: 'danger', message: "DEBUG BUILD FAILED: ${env.RUN_DISPLAY_URL}" 52 | } 53 | success { 54 | slackSend color: 'good', message: "DEBUG BUILD SUCCESSFUL: \n `${env.RUN_DISPLAY_URL}` \n DEBUG APK: \n `${env.BUILD_URL}artifact/app/build/outputs/apk/debug/app-debug.apk`" 55 | } 56 | } 57 | } 58 | stage('Build Release APK') { 59 | when { 60 | branch 'master' 61 | } 62 | steps { 63 | sh './gradlew assembleRelease --configure-on-demand --daemon' 64 | 65 | archiveArtifacts '**/*.apk' 66 | 67 | signAndroidApks keyStoreId: 'personal-keystore', keyAlias: 'myplaystorekey', apksToSign: '**/*-unsigned.apk' 68 | } 69 | post { 70 | aborted { 71 | slackSend color: 'warning', message: "RELEASE BUILD ABORTED: ${env.RUN_DISPLAY_URL}" 72 | } 73 | unstable { 74 | slackSend color: 'warning', message: "RELEASE BUILD UNSTABLE: ${env.RUN_DISPLAY_URL}" 75 | } 76 | failure { 77 | slackSend color: 'danger', message: "RELEASE BUILD FAILED: ${env.RUN_DISPLAY_URL}" 78 | } 79 | success { 80 | slackSend color: 'good', message: "RELEASE BUILD SUCCESSFUL: \n `${env.RUN_DISPLAY_URL}` \n RELEASE APK: \n `${env.BUILD_URL}artifact/SignApksBuilder-out/personal-keystore/myplaystorekey/app-release-unsigned.apk/app-release.apk`" 81 | } 82 | } 83 | } 84 | stage('Deploy to Alpha') { 85 | when { 86 | branch 'master' 87 | } 88 | steps { 89 | androidApkUpload googleCredentialsId: 'google-play-publishing', apkFilesPattern: '**/*-release.apk', trackName: 'alpha' 90 | } 91 | post { 92 | aborted { 93 | slackSend color: 'warning', message: "ALPHA RELEASE ABORTED: \n `${env.RUN_DISPLAY_URL}`" 94 | } 95 | unstable { 96 | slackSend color: 'warning', message: "ALPHA RELEASE UNSTABLE: \n `${env.RUN_DISPLAY_URL}`" 97 | } 98 | failure { 99 | slackSend color: 'danger', message: "ALPHA RELEASE FAILED: \n `${env.RUN_DISPLAY_URL}`" 100 | } 101 | success { 102 | slackSend color: 'good', message: "ALPHA RELEASE SUCCESSFUL: \n `${env.RUN_DISPLAY_URL}`" 103 | } 104 | } 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatApp-Clean-Kotlin 2 | Kotlin Version of Firebase Chat application implemented using Clean Architecture + MVVM, RxJava 2, Dagger 2, ViewModel, Cloud Firestore 3 | 4 | Language: Kotlin 5 | 6 | Architecture: Clean Architecture + MVVM 7 | 8 | Libraries: RxJava 2, Dagger 2, Jetpack ViewModel, Cloud Firestore 9 | 10 | Note: Please your own google-services.json in **data module** which you can get when you create a Firebase project. The google-services.json file found in this project will be removed in the future. 11 | 12 | ## Scope 13 | * 1 chat room only 14 | * No encryption for passwords as this is just a sample app. 15 | 16 | ## Screenshots 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-kapt' 6 | 7 | android { 8 | compileSdkVersion rootProject.ext.compileSdkVersion 9 | defaultConfig { 10 | applicationId "com.imakeanapp.chatappkotlin" 11 | minSdkVersion rootProject.ext.minSdkVersion 12 | targetSdkVersion rootProject.ext.targetSdkVersion 13 | versionCode 7 14 | versionName "1.6" 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation project(':domain') 27 | implementation project(':data') 28 | 29 | implementation fileTree(dir: 'libs', include: ['*.jar']) 30 | 31 | // Kotlin 32 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 33 | 34 | // Support 35 | implementation "androidx.appcompat:appcompat:$app_compat_version" 36 | implementation "androidx.recyclerview:recyclerview:$recyclerview_version" 37 | implementation "com.google.android.material:material:$material_version" 38 | implementation "androidx.constraintlayout:constraintlayout:$constraint_layout_version" 39 | 40 | // ViewModel 41 | implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_extensions_version" 42 | 43 | // Firebase 44 | implementation "com.google.firebase:firebase-firestore:$firebase_firestore_version" 45 | implementation "com.google.firebase:firebase-core:$firebase_core_version" 46 | 47 | // RxJava 48 | implementation "io.reactivex.rxjava2:rxjava:$rxjava_version" 49 | 50 | // RxAndroid 51 | implementation "io.reactivex.rxjava2:rxandroid:$rxandroid_version" 52 | 53 | // Dagger 54 | implementation "com.google.dagger:dagger:$dagger_version" 55 | kapt "com.google.dagger:dagger-compiler:$dagger_version" 56 | 57 | // Test 58 | testImplementation "junit:junit:$junit_version" 59 | androidTestImplementation "androidx.test:runner:$runner_version" 60 | androidTestImplementation "androidx.test.espresso:espresso-core:$espress_core_version" 61 | } 62 | -------------------------------------------------------------------------------- /app/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/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 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/imakeanapp/chatappkotlin/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.imakeanapp.chatappkotlin 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.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.getTargetContext() 22 | assertEquals("com.imakeanapp.chatappkotlin", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/imakeanapp/chatappkotlin/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.imakeanapp.chatappkotlin 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.imakeanapp.chatappkotlin.auth.view.LoginFragment 6 | import com.imakeanapp.chatappkotlin.auth.view.SignUpFragment 7 | import com.imakeanapp.chatappkotlin.auth.view.WelcomeFragment 8 | import com.imakeanapp.chatappkotlin.messages.view.MessagesFragment 9 | 10 | class MainActivity : AppCompatActivity(), MainActivityFragmentsListener { 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | setContentView(R.layout.activity_main) 15 | 16 | showWelcomeFragment() 17 | } 18 | 19 | override fun onLoginClick() = showLoginFragment() 20 | 21 | override fun onSignUpClick() = showSignUpFragment() 22 | 23 | override fun onLogoutClick() = showSignUpFragment() 24 | 25 | override fun onLoginSuccess(username: String) = showChatFragment(username) 26 | 27 | override fun onSignUpSuccess(username: String) = showChatFragment(username) 28 | 29 | private fun showWelcomeFragment() { 30 | supportFragmentManager.beginTransaction().apply { 31 | add(R.id.fragment_container, WelcomeFragment()) 32 | addToBackStack(null) 33 | commit() 34 | } 35 | } 36 | 37 | private fun showLoginFragment() { 38 | if (supportFragmentManager.backStackEntryCount > 1) { 39 | supportFragmentManager.popBackStack() 40 | } 41 | 42 | supportFragmentManager.beginTransaction().apply { 43 | setCustomAnimations(R.animator.slide_in_from_right, R.animator.slide_out_to_left, 44 | R.animator.slide_in_from_left, R.animator.fade_out) 45 | replace(R.id.fragment_container, LoginFragment()) 46 | addToBackStack(null) 47 | commit() 48 | } 49 | } 50 | 51 | private fun showSignUpFragment() { 52 | if (supportFragmentManager.backStackEntryCount > 1) { 53 | supportFragmentManager.popBackStack() 54 | } 55 | 56 | supportFragmentManager.beginTransaction().apply { 57 | setCustomAnimations(R.animator.slide_in_from_right, R.animator.slide_out_to_left, 58 | R.animator.slide_in_from_left, R.animator.fade_out) 59 | replace(R.id.fragment_container, SignUpFragment()) 60 | addToBackStack(null) 61 | commit() 62 | } 63 | } 64 | 65 | private fun showChatFragment(username: String) { 66 | if (supportFragmentManager.backStackEntryCount > 1) { 67 | supportFragmentManager.popBackStack() 68 | } 69 | 70 | supportFragmentManager.beginTransaction().apply { 71 | setCustomAnimations(R.animator.slide_in_from_right, R.animator.slide_out_to_left, 72 | R.animator.slide_in_from_left, R.animator.fade_out) 73 | replace(R.id.fragment_container, MessagesFragment.newInstance(username), "MessagesFragment") 74 | addToBackStack(null) 75 | commit() 76 | } 77 | } 78 | 79 | override fun onBackPressed() { 80 | if (supportFragmentManager.findFragmentByTag("MessagesFragment") != null) { 81 | showSignUpFragment() 82 | return 83 | } 84 | 85 | if (supportFragmentManager.backStackEntryCount > 1) { 86 | supportFragmentManager.popBackStack() 87 | } else { 88 | finish() 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/com/imakeanapp/chatappkotlin/MainActivityFragmentsListener.kt: -------------------------------------------------------------------------------- 1 | package com.imakeanapp.chatappkotlin 2 | 3 | 4 | interface MainActivityFragmentsListener { 5 | 6 | fun onLoginClick() 7 | 8 | fun onSignUpClick() 9 | 10 | fun onLogoutClick() 11 | 12 | fun onLoginSuccess(username: String) 13 | 14 | fun onSignUpSuccess(username: String) 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imakeanapp/chatappkotlin/auth/view/LoginFragment.kt: -------------------------------------------------------------------------------- 1 | package com.imakeanapp.chatappkotlin.auth.view 2 | 3 | import android.content.Context 4 | import android.graphics.Paint 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.Button 11 | import android.widget.EditText 12 | import android.widget.TextView 13 | import androidx.fragment.app.Fragment 14 | import androidx.lifecycle.ViewModelProviders 15 | import com.imakeanapp.chatappkotlin.MainActivityFragmentsListener 16 | import com.imakeanapp.chatappkotlin.R 17 | import com.imakeanapp.chatappkotlin.auth.viewmodel.AuthViewModel 18 | import com.imakeanapp.chatappkotlin.core.injector 19 | import com.imakeanapp.chatappkotlin.util.InputUtil 20 | import io.reactivex.android.schedulers.AndroidSchedulers 21 | import io.reactivex.disposables.CompositeDisposable 22 | 23 | class LoginFragment : Fragment() { 24 | 25 | private val factory = injector.authViewModelFactory() 26 | private lateinit var viewModel: AuthViewModel 27 | 28 | private lateinit var callback: MainActivityFragmentsListener 29 | 30 | private lateinit var username: EditText 31 | private lateinit var password: EditText 32 | private lateinit var usernameError: TextView 33 | private lateinit var passwordError: TextView 34 | private lateinit var signUp: TextView 35 | private lateinit var login: Button 36 | 37 | private val disposables = CompositeDisposable() 38 | 39 | override fun onCreate(savedInstanceState: Bundle?) { 40 | super.onCreate(savedInstanceState) 41 | 42 | viewModel = ViewModelProviders.of(requireNotNull(activity), factory).get(AuthViewModel::class.java) 43 | } 44 | 45 | override fun onAttach(context: Context?) { 46 | super.onAttach(context) 47 | 48 | try { 49 | callback = context as MainActivityFragmentsListener 50 | } catch (e: ClassCastException) { 51 | throw ClassCastException("Activity must implement MainActivityFragmentsListener") 52 | } 53 | } 54 | 55 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 56 | val view = inflater.inflate(R.layout.fragment_login, container, false) 57 | 58 | username = view.findViewById(R.id.username) 59 | password = view.findViewById(R.id.password) 60 | usernameError = view.findViewById(R.id.username_error) 61 | passwordError = view.findViewById(R.id.password_error) 62 | signUp = view.findViewById(R.id.sign_up) 63 | signUp.paintFlags = signUp.paintFlags or Paint.UNDERLINE_TEXT_FLAG 64 | login = view.findViewById(R.id.login) 65 | 66 | signUp.setOnClickListener { callback.onSignUpClick() } 67 | 68 | login.setOnClickListener { 69 | if (!hasErrors()) { 70 | InputUtil.hideKeyboard(requireNotNull(context), view) 71 | disableLoginButton() 72 | disposables.add( 73 | viewModel.login(username.text.toString(), password.text.toString()) 74 | .observeOn(AndroidSchedulers.mainThread()) 75 | .subscribe( 76 | { user -> callback.onLoginSuccess(user.username) }, 77 | { e -> 78 | enableLoginButton() 79 | showUsernameError() 80 | showPasswordError() 81 | Log.e("LoginFragment", "Error: ", e) 82 | } 83 | ) 84 | ) 85 | } 86 | } 87 | 88 | return view 89 | } 90 | 91 | override fun onStop() { 92 | super.onStop() 93 | disposables.clear() 94 | } 95 | 96 | private fun hasErrors(): Boolean { 97 | var hasError = false 98 | 99 | val usernameValue = username.text.toString() 100 | if (usernameValue.isEmpty() || usernameValue.length < 8) { 101 | hasError = true 102 | showUsernameError() 103 | } else { 104 | hideUsernameError() 105 | } 106 | 107 | val passwordValue = password.text.toString() 108 | if (passwordValue.isEmpty() || passwordValue.length < 8) { 109 | hasError = true 110 | showPasswordError() 111 | } else { 112 | hidePasswordError() 113 | } 114 | 115 | return hasError 116 | } 117 | 118 | private fun showUsernameError() { 119 | usernameError.visibility = View.VISIBLE 120 | } 121 | 122 | private fun hideUsernameError() { 123 | usernameError.visibility = View.GONE 124 | } 125 | 126 | private fun showPasswordError() { 127 | passwordError.visibility = View.VISIBLE 128 | } 129 | 130 | private fun hidePasswordError() { 131 | passwordError.visibility = View.GONE 132 | } 133 | 134 | private fun enableLoginButton() { 135 | login.isEnabled = true 136 | } 137 | 138 | private fun disableLoginButton() { 139 | login.isEnabled = false 140 | } 141 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imakeanapp/chatappkotlin/auth/view/SignUpFragment.kt: -------------------------------------------------------------------------------- 1 | package com.imakeanapp.chatappkotlin.auth.view 2 | 3 | import android.content.Context 4 | import android.graphics.Paint 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.Button 11 | import android.widget.EditText 12 | import android.widget.TextView 13 | import androidx.fragment.app.Fragment 14 | import androidx.lifecycle.ViewModelProviders 15 | import com.imakeanapp.chatappkotlin.MainActivityFragmentsListener 16 | import com.imakeanapp.chatappkotlin.R 17 | import com.imakeanapp.chatappkotlin.auth.viewmodel.AuthViewModel 18 | import com.imakeanapp.chatappkotlin.core.injector 19 | import com.imakeanapp.chatappkotlin.util.InputUtil 20 | import io.reactivex.android.schedulers.AndroidSchedulers 21 | import io.reactivex.disposables.CompositeDisposable 22 | 23 | class SignUpFragment : Fragment() { 24 | 25 | private val factory = injector.authViewModelFactory() 26 | private lateinit var viewModel: AuthViewModel 27 | 28 | private lateinit var callback: MainActivityFragmentsListener 29 | 30 | private lateinit var username: EditText 31 | private lateinit var password: EditText 32 | private lateinit var usernameError: TextView 33 | private lateinit var passwordError: TextView 34 | private lateinit var login: TextView 35 | private lateinit var signUp: Button 36 | 37 | private val disposables = CompositeDisposable() 38 | 39 | override fun onCreate(savedInstanceState: Bundle?) { 40 | super.onCreate(savedInstanceState) 41 | 42 | viewModel = ViewModelProviders.of(requireNotNull(activity), factory).get(AuthViewModel::class.java) 43 | } 44 | 45 | override fun onAttach(context: Context?) { 46 | super.onAttach(context) 47 | 48 | try { 49 | callback = context as MainActivityFragmentsListener 50 | } catch (e: ClassCastException) { 51 | throw ClassCastException("Activity must implement MainActivityFragmentsListener") 52 | } 53 | } 54 | 55 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 56 | val view = inflater.inflate(R.layout.fragment_sign_up, container, false) 57 | 58 | username = view.findViewById(R.id.username) 59 | password = view.findViewById(R.id.password) 60 | usernameError = view.findViewById(R.id.username_error) 61 | passwordError = view.findViewById(R.id.password_error) 62 | 63 | login = view.findViewById(R.id.login) 64 | login.paintFlags = login.paintFlags or Paint.UNDERLINE_TEXT_FLAG 65 | 66 | signUp = view.findViewById(R.id.sign_up) 67 | 68 | login.setOnClickListener { callback.onLoginClick() } 69 | 70 | signUp.setOnClickListener { 71 | if (!hasErrors()) { 72 | InputUtil.hideKeyboard(requireNotNull(context), view) 73 | disableSignUpButton() 74 | disposables.add( 75 | viewModel.signUp(username.text.toString(), password.text.toString()) 76 | .observeOn(AndroidSchedulers.mainThread()) 77 | .subscribe( 78 | { user -> callback.onSignUpSuccess(user.username) }, 79 | { e -> 80 | enableSignUpButton() 81 | showUsernameError() 82 | showPasswordError() 83 | Log.e("SignUpFragment", "Error: ", e) 84 | } 85 | ) 86 | ) 87 | } 88 | } 89 | 90 | return view 91 | } 92 | 93 | override fun onStop() { 94 | super.onStop() 95 | disposables.clear() 96 | } 97 | 98 | private fun hasErrors(): Boolean { 99 | var hasError = false 100 | 101 | val usernameValue = username.text.toString() 102 | if (usernameValue.isEmpty() || usernameValue.length < 8) { 103 | hasError = true 104 | showUsernameError() 105 | } else { 106 | hideUsernameError() 107 | } 108 | 109 | val passwordValue = password.text.toString() 110 | if (passwordValue.isEmpty() || passwordValue.length < 8) { 111 | hasError = true 112 | showPasswordError() 113 | } else { 114 | hidePasswordError() 115 | } 116 | 117 | return hasError 118 | } 119 | 120 | private fun showUsernameError() { 121 | usernameError.visibility = View.VISIBLE 122 | } 123 | 124 | private fun hideUsernameError() { 125 | usernameError.visibility = View.GONE 126 | } 127 | 128 | private fun showPasswordError() { 129 | passwordError.visibility = View.VISIBLE 130 | } 131 | 132 | private fun hidePasswordError() { 133 | passwordError.visibility = View.GONE 134 | } 135 | 136 | private fun enableSignUpButton() { 137 | signUp.isEnabled = true 138 | } 139 | 140 | private fun disableSignUpButton() { 141 | signUp.isEnabled = false 142 | } 143 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imakeanapp/chatappkotlin/auth/view/WelcomeFragment.kt: -------------------------------------------------------------------------------- 1 | package com.imakeanapp.chatappkotlin.auth.view 2 | 3 | import android.animation.AnimatorSet 4 | import android.animation.ObjectAnimator 5 | import android.content.Context 6 | import android.os.Bundle 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.Button 11 | import android.widget.ImageView 12 | import androidx.fragment.app.Fragment 13 | import com.imakeanapp.chatappkotlin.MainActivityFragmentsListener 14 | import com.imakeanapp.chatappkotlin.R 15 | 16 | class WelcomeFragment : Fragment() { 17 | 18 | private lateinit var callback: MainActivityFragmentsListener 19 | 20 | private lateinit var signUp: Button 21 | private lateinit var login: Button 22 | private lateinit var logo: ImageView 23 | 24 | override fun onAttach(context: Context?) { 25 | super.onAttach(context) 26 | 27 | try { 28 | callback = context as MainActivityFragmentsListener 29 | } catch (e: ClassCastException) { 30 | throw ClassCastException("Activity must implement MainActivityFragmentsListener") 31 | } 32 | } 33 | 34 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 35 | val view = inflater.inflate(R.layout.fragment_welcome, container, false) 36 | 37 | logo = view.findViewById(R.id.logo) 38 | signUp = view.findViewById(R.id.sign_up) 39 | login = view.findViewById(R.id.login) 40 | 41 | signUp.setOnClickListener { callback.onSignUpClick() } 42 | 43 | login.setOnClickListener { callback.onLoginClick() } 44 | 45 | return view 46 | } 47 | 48 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 49 | super.onViewCreated(view, savedInstanceState) 50 | val animationX = ObjectAnimator.ofFloat(logo, "scaleX", 0f, 1f) 51 | val animationY = ObjectAnimator.ofFloat(logo, "scaleY", 0f, 1f) 52 | animationX.duration = 500 53 | animationY.duration = 500 54 | 55 | val scaleUp = AnimatorSet() 56 | scaleUp.playTogether(animationX, animationY) 57 | scaleUp.start() 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imakeanapp/chatappkotlin/auth/viewmodel/AuthViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.imakeanapp.chatappkotlin.auth.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.imakeanapp.domain.user.model.User 5 | import com.imakeanapp.domain.user.usecase.LoginUseCase 6 | import com.imakeanapp.domain.user.usecase.SignUpUseCase 7 | 8 | class AuthViewModel(private val signUp: SignUpUseCase, 9 | private val login: LoginUseCase) : ViewModel() { 10 | 11 | fun signUp(username: String, password: String) = signUp.execute(User(username, password)) 12 | 13 | fun login(username: String, password: String) = login.execute(User(username, password)) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imakeanapp/chatappkotlin/auth/viewmodel/AuthViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.imakeanapp.chatappkotlin.auth.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.imakeanapp.domain.user.usecase.LoginUseCase 6 | import com.imakeanapp.domain.user.usecase.SignUpUseCase 7 | import javax.inject.Inject 8 | import javax.inject.Singleton 9 | 10 | @Suppress("UNCHECKED_CAST") 11 | @Singleton 12 | class AuthViewModelFactory @Inject constructor( 13 | private val signUp: SignUpUseCase, 14 | private val login: LoginUseCase) : ViewModelProvider.Factory { 15 | 16 | override fun create(modelClass: Class): T { 17 | if (modelClass.isAssignableFrom(AuthViewModel::class.java)) { 18 | return AuthViewModel(signUp, login) as T 19 | } 20 | throw IllegalArgumentException("Unknown ViewModel class") 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imakeanapp/chatappkotlin/core/App.kt: -------------------------------------------------------------------------------- 1 | package com.imakeanapp.chatappkotlin.core 2 | 3 | import android.app.Application 4 | import com.imakeanapp.data.core.DatabaseModule 5 | import com.imakeanapp.data.core.RepositoryModule 6 | 7 | class App : Application() { 8 | 9 | override fun onCreate() { 10 | super.onCreate() 11 | injector = DaggerAppComponent.builder() 12 | .databaseModule(DatabaseModule()) 13 | .repositoryModule(RepositoryModule()) 14 | .viewModelModule(ViewModelModule()) 15 | .build() 16 | } 17 | } 18 | 19 | lateinit var injector: AppComponent -------------------------------------------------------------------------------- /app/src/main/java/com/imakeanapp/chatappkotlin/core/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.imakeanapp.chatappkotlin.core 2 | 3 | import com.google.firebase.firestore.FirebaseFirestore 4 | import com.imakeanapp.chatappkotlin.auth.viewmodel.AuthViewModelFactory 5 | import com.imakeanapp.chatappkotlin.messages.viewmodel.MessagesViewModelFactory 6 | import com.imakeanapp.data.core.DatabaseModule 7 | import com.imakeanapp.data.core.RepositoryModule 8 | import com.imakeanapp.domain.messages.repository.MessagesRepository 9 | import com.imakeanapp.domain.user.repository.AuthRepository 10 | import dagger.Component 11 | import javax.inject.Singleton 12 | 13 | @Component(modules = [ 14 | RepositoryModule::class, 15 | DatabaseModule::class, 16 | ViewModelModule::class 17 | ]) 18 | @Singleton 19 | interface AppComponent { 20 | 21 | fun authViewModelFactory(): AuthViewModelFactory 22 | 23 | fun messagesViewModelFactory(): MessagesViewModelFactory 24 | 25 | fun authRepository(): AuthRepository 26 | 27 | fun messagesRepository(): MessagesRepository 28 | 29 | fun firebaseFirestore(): FirebaseFirestore 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imakeanapp/chatappkotlin/core/ViewModelModule.kt: -------------------------------------------------------------------------------- 1 | package com.imakeanapp.chatappkotlin.core 2 | 3 | import com.imakeanapp.chatappkotlin.auth.viewmodel.AuthViewModelFactory 4 | import com.imakeanapp.chatappkotlin.messages.viewmodel.MessagesViewModelFactory 5 | import com.imakeanapp.data.messages.MessagesRepositoryImpl 6 | import com.imakeanapp.data.user.AuthRepositoryImpl 7 | import com.imakeanapp.domain.messages.usecase.GetMessagesUseCase 8 | import com.imakeanapp.domain.messages.usecase.SendMessageUseCase 9 | import com.imakeanapp.domain.user.usecase.LoginUseCase 10 | import com.imakeanapp.domain.user.usecase.SignUpUseCase 11 | import dagger.Module 12 | import dagger.Provides 13 | 14 | @Module 15 | class ViewModelModule { 16 | 17 | @Provides 18 | fun providesAuthViewModelFactory(repository: AuthRepositoryImpl): AuthViewModelFactory { 19 | return AuthViewModelFactory( 20 | SignUpUseCase(repository), 21 | LoginUseCase(repository) 22 | ) 23 | } 24 | 25 | @Provides 26 | fun providesMessagesViewModelFactory(repository: MessagesRepositoryImpl): MessagesViewModelFactory { 27 | return MessagesViewModelFactory( 28 | GetMessagesUseCase(repository), 29 | SendMessageUseCase(repository) 30 | ) 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imakeanapp/chatappkotlin/messages/view/MessagesAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.imakeanapp.chatappkotlin.messages.view 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.TextView 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.imakeanapp.chatappkotlin.R 9 | import com.imakeanapp.domain.messages.model.Message 10 | 11 | class MessagesAdapter(private val username: String, 12 | private var chats: List) : RecyclerView.Adapter() { 13 | 14 | private val SENT = 0 15 | private val RECEIVED = 1 16 | 17 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder { 18 | val view = when (viewType) { 19 | SENT -> { 20 | LayoutInflater.from(parent.context).inflate(R.layout.item_message_sent, parent, false) 21 | } 22 | else -> { 23 | LayoutInflater.from(parent.context).inflate(R.layout.item_message_received, parent, false) 24 | } 25 | } 26 | 27 | return MessageViewHolder(view) 28 | } 29 | 30 | override fun getItemCount() = chats.size 31 | 32 | override fun onBindViewHolder(holder: MessageViewHolder, position: Int) { 33 | holder.bind(chats[position]) 34 | } 35 | 36 | override fun getItemViewType(position: Int): Int { 37 | return if (chats[position].sender.contentEquals(username)) { 38 | SENT 39 | } else { 40 | RECEIVED 41 | } 42 | } 43 | 44 | fun updateData(chats: List) { 45 | this.chats = chats 46 | notifyDataSetChanged() 47 | } 48 | 49 | inner class MessageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 50 | 51 | private val chatMessage: TextView = itemView.findViewById(R.id.chat_message) 52 | private val chatSender: TextView = itemView.findViewById(R.id.chat_sender) 53 | 54 | fun bind(chat: Message) { 55 | chatMessage.text = chat.message 56 | 57 | if (!username.contentEquals(chat.sender)) { 58 | chatSender.text = chat.sender 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imakeanapp/chatappkotlin/messages/view/MessagesFragment.kt: -------------------------------------------------------------------------------- 1 | package com.imakeanapp.chatappkotlin.messages.view 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.text.Editable 6 | import android.text.TextWatcher 7 | import android.util.Log 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import android.widget.Button 12 | import android.widget.EditText 13 | import android.widget.Toast 14 | import androidx.fragment.app.Fragment 15 | import androidx.lifecycle.ViewModelProviders 16 | import androidx.recyclerview.widget.LinearLayoutManager 17 | import androidx.recyclerview.widget.RecyclerView 18 | import com.imakeanapp.chatappkotlin.MainActivityFragmentsListener 19 | import com.imakeanapp.chatappkotlin.R 20 | import com.imakeanapp.chatappkotlin.core.injector 21 | import com.imakeanapp.chatappkotlin.messages.viewmodel.MessagesViewModel 22 | import com.imakeanapp.domain.messages.model.Message 23 | import io.reactivex.android.schedulers.AndroidSchedulers 24 | import io.reactivex.disposables.CompositeDisposable 25 | 26 | class MessagesFragment : Fragment() { 27 | 28 | companion object { 29 | const val ARG_USERNAME = "arg_username" 30 | 31 | fun newInstance(username: String): MessagesFragment { 32 | val args = Bundle() 33 | 34 | args.putString(ARG_USERNAME, username) 35 | 36 | val fragment = MessagesFragment() 37 | fragment.arguments = args 38 | return fragment 39 | } 40 | } 41 | 42 | private val factory = injector.messagesViewModelFactory() 43 | private lateinit var viewModel: MessagesViewModel 44 | 45 | private lateinit var callback: MainActivityFragmentsListener 46 | 47 | private lateinit var messagesList: RecyclerView 48 | private lateinit var adapter: MessagesAdapter 49 | 50 | private lateinit var logOut: Button 51 | private lateinit var sendMessage: Button 52 | private lateinit var message: EditText 53 | 54 | private lateinit var username: String 55 | 56 | private val disposables = CompositeDisposable() 57 | 58 | override fun onCreate(savedInstanceState: Bundle?) { 59 | super.onCreate(savedInstanceState) 60 | 61 | viewModel = ViewModelProviders.of(this, factory).get(MessagesViewModel::class.java) 62 | } 63 | 64 | override fun onAttach(context: Context?) { 65 | super.onAttach(context) 66 | 67 | try { 68 | callback = context as MainActivityFragmentsListener 69 | } catch (e: ClassCastException) { 70 | throw ClassCastException("Activity must implement MainActivityFragmentsListener") 71 | } 72 | } 73 | 74 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 75 | val view = inflater.inflate(R.layout.fragment_messages, container, false) 76 | 77 | username = requireNotNull(arguments).getString(ARG_USERNAME)!! 78 | 79 | sendMessage = view.findViewById(R.id.send) 80 | message = view.findViewById(R.id.message) 81 | logOut = view.findViewById(R.id.log_out) 82 | messagesList = view.findViewById(R.id.message_list) 83 | 84 | val manager = LinearLayoutManager(context) 85 | manager.reverseLayout = true 86 | messagesList.layoutManager = manager 87 | 88 | adapter = MessagesAdapter(username, listOf()) 89 | messagesList.adapter = adapter 90 | 91 | logOut.setOnClickListener { callback.onLogoutClick() } 92 | 93 | sendMessage.setOnClickListener { _ -> 94 | val chatMessage = Message( 95 | message.text.toString(), 96 | username, 97 | System.currentTimeMillis() 98 | ) 99 | message.setText("") 100 | 101 | disposables.add( 102 | viewModel.sendMessage(chatMessage) 103 | .observeOn(AndroidSchedulers.mainThread()) 104 | .subscribe( 105 | { Log.d("MessagesFragment", "Message sent") }, 106 | { showInternetError() } 107 | ) 108 | ) 109 | } 110 | 111 | addMessageBoxTextListener() 112 | 113 | disposables.add( 114 | viewModel.getMessages() 115 | .observeOn(AndroidSchedulers.mainThread()) 116 | .subscribe( 117 | { adapter.updateData(it) }, 118 | { showInternetError() } 119 | ) 120 | ) 121 | 122 | return view 123 | } 124 | 125 | private fun showInternetError() { 126 | Toast.makeText(context, getString(R.string.internet_connection_error), Toast.LENGTH_SHORT).show() 127 | } 128 | 129 | private fun addMessageBoxTextListener() { 130 | message.addTextChangedListener(object : TextWatcher { 131 | override fun afterTextChanged(s: Editable?) { 132 | } 133 | 134 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { 135 | } 136 | 137 | override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { 138 | sendMessage.isEnabled = s.isNotEmpty() 139 | } 140 | }) 141 | } 142 | 143 | override fun onStop() { 144 | super.onStop() 145 | disposables.clear() 146 | } 147 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imakeanapp/chatappkotlin/messages/viewmodel/MessagesViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.imakeanapp.chatappkotlin.messages.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.imakeanapp.domain.messages.model.Message 5 | import com.imakeanapp.domain.messages.usecase.GetMessagesUseCase 6 | import com.imakeanapp.domain.messages.usecase.SendMessageUseCase 7 | 8 | class MessagesViewModel(private val getMessages: GetMessagesUseCase, 9 | private val sendMessage: SendMessageUseCase) : ViewModel() { 10 | 11 | fun sendMessage(message: Message) = sendMessage.execute(message) 12 | 13 | fun getMessages() = getMessages.execute() 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imakeanapp/chatappkotlin/messages/viewmodel/MessagesViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.imakeanapp.chatappkotlin.messages.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import com.imakeanapp.domain.messages.usecase.GetMessagesUseCase 6 | import com.imakeanapp.domain.messages.usecase.SendMessageUseCase 7 | import javax.inject.Inject 8 | import javax.inject.Singleton 9 | 10 | @Suppress("UNCHECKED_CAST") 11 | @Singleton 12 | class MessagesViewModelFactory @Inject constructor( 13 | private val getMessages: GetMessagesUseCase, 14 | private val sendMessage: SendMessageUseCase) : ViewModelProvider.Factory { 15 | 16 | override fun create(modelClass: Class): T { 17 | if (modelClass.isAssignableFrom(MessagesViewModel::class.java)) { 18 | return MessagesViewModel(getMessages, sendMessage) as T 19 | } 20 | throw IllegalArgumentException("Unknown ViewModel class") 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/imakeanapp/chatappkotlin/util/InputUtil.kt: -------------------------------------------------------------------------------- 1 | package com.imakeanapp.chatappkotlin.util 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.view.View 6 | import android.view.inputmethod.InputMethodManager 7 | 8 | class InputUtil { 9 | companion object { 10 | fun hideKeyboard(context: Context, view: View) { 11 | val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager 12 | imm.hideSoftInputFromWindow(view.windowToken, 0) 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/res/animator/fade_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/animator/slide_in_from_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/animator/slide_in_from_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/animator/slide_out_to_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/speech_bubble.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/drawable-hdpi/speech_bubble.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/speech_bubble_sent.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/drawable-hdpi/speech_bubble_sent.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/speech_bubble.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/drawable-mdpi/speech_bubble.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/speech_bubble_sent.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/drawable-mdpi/speech_bubble_sent.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/speech_bubble.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/drawable-xhdpi/speech_bubble.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/speech_bubble_sent.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/drawable-xhdpi/speech_bubble_sent.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/speech_bubble.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/drawable-xxhdpi/speech_bubble.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/speech_bubble_sent.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/drawable-xxhdpi/speech_bubble_sent.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/speech_bubble.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/drawable-xxxhdpi/speech_bubble.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/speech_bubble_sent.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/drawable-xxxhdpi/speech_bubble_sent.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_background_dark.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_background_positive_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_background_positive_1_disabled.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_background_positive_1_enabled.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_background_positive_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_background_positive_2_disabled.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_background_positive_2_enabled.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/text_field_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 20 | 21 | 28 | 29 | 45 | 46 | 60 | 61 | 77 | 78 | 92 | 93 |