├── .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 |
109 |
110 |
125 |
126 |
139 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_messages.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
20 |
21 |
29 |
30 |
44 |
45 |
54 |
55 |
71 |
72 |
89 |
90 |
99 |
100 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_sign_up.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
20 |
21 |
28 |
29 |
45 |
46 |
60 |
61 |
77 |
78 |
92 |
93 |
109 |
110 |
125 |
126 |
139 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_welcome.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
27 |
28 |
44 |
45 |
56 |
57 |
71 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_message_received.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
22 |
23 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_message_sent.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
22 |
23 |
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_bk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/mipmap-hdpi/ic_launcher_bk.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/mipmap-hdpi/logo.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_bk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/mipmap-mdpi/ic_launcher_bk.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_bk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/mipmap-xhdpi/ic_launcher_bk.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_bk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/mipmap-xxhdpi/ic_launcher_bk.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/mipmap-xxxhdpi/ic_launcher_bk.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #75d810
6 | #b42127
7 | #75d810
8 | #98e747
9 | #4e900d
10 | #7aa94c
11 | #FFFFFF
12 | #000000
13 | #4f5f6d
14 | #505152
15 | #b6c4cc
16 | #e5ebef
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ChatAppKotlin
3 | Chat app
4 | Sign up
5 | Login
6 | User name
7 | password
8 | By signing up, you agree to the Terms of Service and Privacy Policy, including Cookie Use. Others will be able to find you by searching for your email address or phone number when provided.
9 | Value is incorrect
10 | Log out
11 | send
12 | Start a new message
13 | You
14 | Please check your internet connection
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/imakeanapp/chatappkotlin/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.chatappkotlin
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 | }
18 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.2.71'
5 | repositories {
6 | google()
7 | jcenter()
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:3.2.0'
11 | classpath 'com.google.gms:google-services:4.0.2'
12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13 |
14 | // NOTE: Do not place your application dependencies here; they belong
15 | // in the individual module build.gradle files
16 | }
17 | }
18 |
19 | allprojects {
20 | repositories {
21 | google()
22 | jcenter()
23 | }
24 | }
25 |
26 | ext {
27 | minSdkVersion = 21
28 | compileSdkVersion = 28
29 | targetSdkVersion = 28
30 |
31 | // Support
32 | app_compat_version = '1.0.0'
33 | support_v4_version = '1.0.0'
34 | support_media_version = '1.0.0'
35 | constraint_layout_version = '1.1.3'
36 | material_version = '1.0.0'
37 | recyclerview_version = '1.0.0'
38 | lifecycle_extensions_version = '2.0.0'
39 |
40 | // RxJava
41 | rxjava_version = '2.2.2'
42 |
43 | // RxAndroid
44 | rxandroid_version = '2.1.0'
45 |
46 | // Dagger
47 | dagger_version = '2.17'
48 |
49 | // Firebase
50 | firebase_firestore_version = '17.1.0'
51 | firebase_core_version = '16.0.3'
52 |
53 | // Test
54 | junit_version = '4.12'
55 | runner_version = '1.1.0-alpha4'
56 | espress_core_version = '3.1.0-alpha4'
57 | }
58 |
59 | task clean(type: Delete) {
60 | delete rootProject.buildDir
61 | }
62 |
--------------------------------------------------------------------------------
/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/data/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-kapt'
6 |
7 | android {
8 | compileSdkVersion rootProject.ext.compileSdkVersion
9 |
10 | defaultConfig {
11 | minSdkVersion rootProject.ext.minSdkVersion
12 | targetSdkVersion rootProject.ext.targetSdkVersion
13 | versionCode 1
14 | versionName "1.0"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 |
18 | }
19 |
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 |
27 | }
28 |
29 | dependencies {
30 | implementation project(':domain')
31 |
32 | implementation fileTree(dir: 'libs', include: ['*.jar'])
33 |
34 | // Kotlin
35 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
36 |
37 | // Support
38 | implementation "androidx.appcompat:appcompat:$app_compat_version"
39 | // Override firebase libraries third-party dependencies to fix incompatible versions
40 | // implementation "androidx.legacy:legacy-support-v4:$support_v4_version"
41 | // implementation "androidx.media:media:$support_media_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 | // Dagger
51 | implementation "com.google.dagger:dagger:$dagger_version"
52 | kapt "com.google.dagger:dagger-compiler:$dagger_version"
53 |
54 | // Test
55 | testImplementation "junit:junit:$junit_version"
56 | androidTestImplementation "androidx.test:runner:$runner_version"
57 | androidTestImplementation "androidx.test.espresso:espresso-core:$espress_core_version"
58 | }
59 |
60 | apply plugin: 'com.google.gms.google-services'
--------------------------------------------------------------------------------
/data/google-services.json:
--------------------------------------------------------------------------------
1 | {
2 | "project_info": {
3 | "project_number": "117552302803",
4 | "firebase_url": "https://chatapp-be1de.firebaseio.com",
5 | "project_id": "chatapp-be1de",
6 | "storage_bucket": "chatapp-be1de.appspot.com"
7 | },
8 | "client": [
9 | {
10 | "client_info": {
11 | "mobilesdk_app_id": "1:117552302803:android:2d583c8506a09da3",
12 | "android_client_info": {
13 | "package_name": "com.imakeanapp.data"
14 | }
15 | },
16 | "oauth_client": [
17 | {
18 | "client_id": "117552302803-sthbmnt0epmknvmekpt5h1a2c6gfjita.apps.googleusercontent.com",
19 | "client_type": 3
20 | }
21 | ],
22 | "api_key": [
23 | {
24 | "current_key": "AIzaSyAfFCsKdfLKuMmINA8Yy5Y8FtET9nK_VKc"
25 | }
26 | ],
27 | "services": {
28 | "analytics_service": {
29 | "status": 1
30 | },
31 | "appinvite_service": {
32 | "status": 1,
33 | "other_platform_oauth_client": []
34 | },
35 | "ads_service": {
36 | "status": 2
37 | }
38 | }
39 | }
40 | ],
41 | "configuration_version": "1"
42 | }
--------------------------------------------------------------------------------
/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
22 |
--------------------------------------------------------------------------------
/data/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/data/src/main/java/com/imakeanapp/data/core/DatabaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.data.core
2 |
3 | import com.google.firebase.firestore.FirebaseFirestore
4 | import dagger.Module
5 | import dagger.Provides
6 | import javax.inject.Singleton
7 |
8 | @Module
9 | class DatabaseModule {
10 |
11 | @Provides
12 | @Singleton
13 | fun providesFirebaseFirestore() = FirebaseFirestore.getInstance()
14 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/imakeanapp/data/core/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.data.core
2 |
3 | import com.imakeanapp.data.messages.MessagesRepositoryImpl
4 | import com.imakeanapp.data.user.AuthRepositoryImpl
5 | import com.imakeanapp.domain.messages.repository.MessagesRepository
6 | import com.imakeanapp.domain.user.repository.AuthRepository
7 | import dagger.Module
8 | import dagger.Provides
9 |
10 | @Module
11 | class RepositoryModule {
12 |
13 | @Provides
14 | fun providesAuthRepository(repository: AuthRepositoryImpl): AuthRepository {
15 | return repository
16 | }
17 |
18 | @Provides
19 | fun providesMessagesRepository(repository: MessagesRepositoryImpl): MessagesRepository {
20 | return repository
21 | }
22 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/imakeanapp/data/messages/MessagesRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.data.messages
2 |
3 | import com.google.firebase.firestore.FirebaseFirestore
4 | import com.google.firebase.firestore.Query
5 | import com.imakeanapp.domain.messages.model.Message
6 | import com.imakeanapp.domain.messages.repository.MessagesRepository
7 | import io.reactivex.Completable
8 | import io.reactivex.Observable
9 | import io.reactivex.ObservableOnSubscribe
10 | import io.reactivex.schedulers.Schedulers
11 | import javax.inject.Inject
12 | import javax.inject.Singleton
13 |
14 | @Singleton
15 | class MessagesRepositoryImpl @Inject constructor(private val db: FirebaseFirestore) : MessagesRepository {
16 |
17 | override fun sendMessage(message: Message): Completable {
18 | return Completable.create { emitter ->
19 | db.collection("messages")
20 | .add(message)
21 | .addOnSuccessListener { emitter.onComplete() }
22 | .addOnFailureListener { emitter.onError(it) }
23 | }.subscribeOn(Schedulers.io())
24 | }
25 |
26 | override fun getMessages(): Observable> {
27 | return Observable.create(ObservableOnSubscribe> { emitter ->
28 | db.collection("messages")
29 | .orderBy("sent", Query.Direction.DESCENDING)
30 | .addSnapshotListener { snapshots, e ->
31 | if (e != null) {
32 | emitter.onError(e)
33 | return@addSnapshotListener
34 | }
35 |
36 | val messages = arrayListOf()
37 | snapshots?.let {
38 | for (doc in snapshots) {
39 | messages.add(
40 | Message(
41 | doc.getString("message")!!,
42 | doc.getString("sender")!!,
43 | doc.getLong("sent")!!
44 | )
45 | )
46 | }
47 | }
48 |
49 | emitter.onNext(messages)
50 | }
51 | }).subscribeOn(Schedulers.io())
52 | }
53 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/imakeanapp/data/user/AuthRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.data.user
2 |
3 | import com.google.firebase.firestore.FirebaseFirestore
4 | import com.imakeanapp.domain.user.model.User
5 | import com.imakeanapp.domain.user.repository.AuthRepository
6 | import io.reactivex.Single
7 | import io.reactivex.SingleOnSubscribe
8 | import io.reactivex.schedulers.Schedulers
9 | import javax.inject.Inject
10 | import javax.inject.Singleton
11 |
12 | @Singleton
13 | class AuthRepositoryImpl @Inject constructor(private val db: FirebaseFirestore) : AuthRepository {
14 |
15 | override fun signup(username: String, password: String): Single {
16 | return Single.create(SingleOnSubscribe { emitter ->
17 | val ref = db.collection("users").document(username)
18 | ref.get().addOnCompleteListener { task ->
19 | if (task.isSuccessful) {
20 | val document = task.result
21 | if (document.exists()) {
22 | emitter.onError(Exception("User exists"))
23 | } else {
24 | val user = User(username, password)
25 | db.collection("users")
26 | .document(username)
27 | .set(user)
28 | .addOnSuccessListener { emitter.onSuccess(user) }
29 | .addOnFailureListener { emitter.onError(it) }
30 | }
31 | } else {
32 | emitter.onError(task.exception!!)
33 | }
34 | }
35 | }).subscribeOn(Schedulers.io())
36 | }
37 |
38 | override fun login(username: String, password: String): Single {
39 | return Single.create(SingleOnSubscribe { emitter ->
40 | val ref = db.collection("users").document(username)
41 | ref.get().addOnCompleteListener {
42 | if (it.isSuccessful) {
43 | val document = it.result
44 | if (document.exists()) {
45 | val user = document.toObject(User::class.java)
46 | if (user != null && user.password.contentEquals(password)) {
47 | emitter.onSuccess(user)
48 | } else {
49 | emitter.onError(Exception("Password incorrect"))
50 | }
51 | } else {
52 | emitter.onError(Exception("User doesn't exist"))
53 | }
54 | } else {
55 | emitter.onError(it.exception!!)
56 | }
57 | }
58 | }).subscribeOn(Schedulers.io())
59 | }
60 | }
--------------------------------------------------------------------------------
/data/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | data
3 |
4 |
--------------------------------------------------------------------------------
/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/domain/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 |
3 | dependencies {
4 | implementation fileTree(dir: 'libs', include: ['*.jar'])
5 |
6 | // Kotlin
7 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
8 |
9 | // RxJava
10 | implementation "io.reactivex.rxjava2:rxjava:$rxjava_version"
11 | }
12 |
13 | sourceCompatibility = "1.7"
14 | targetCompatibility = "1.7"
15 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/imakeanapp/domain/core/CompletableUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.domain.core
2 |
3 | import io.reactivex.Completable
4 |
5 |
6 | interface CompletableUseCase {
7 |
8 | fun execute(): Completable
9 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/imakeanapp/domain/core/CompletableWithParamUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.domain.core
2 |
3 | import io.reactivex.Completable
4 |
5 |
6 | interface CompletableWithParamUseCase {
7 |
8 | fun execute(t: T): Completable
9 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/imakeanapp/domain/core/ObservableUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.domain.core
2 |
3 | import io.reactivex.Observable
4 |
5 |
6 | interface ObservableUseCase {
7 |
8 | fun execute(): Observable
9 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/imakeanapp/domain/core/SingleUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.domain.core
2 |
3 | import io.reactivex.Single
4 |
5 |
6 | interface SingleUseCase {
7 |
8 | fun execute(): Single
9 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/imakeanapp/domain/core/SingleWithParamUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.domain.core
2 |
3 | import io.reactivex.Single
4 |
5 |
6 | interface SingleWithParamUseCase {
7 |
8 | fun execute(t: T): Single
9 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/imakeanapp/domain/messages/model/Message.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.domain.messages.model
2 |
3 | data class Message(
4 | var message: String = "",
5 | var sender: String = "",
6 | var sent: Long = 0
7 | )
--------------------------------------------------------------------------------
/domain/src/main/java/com/imakeanapp/domain/messages/repository/MessagesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.domain.messages.repository
2 |
3 | import com.imakeanapp.domain.messages.model.Message
4 | import io.reactivex.Completable
5 | import io.reactivex.Observable
6 |
7 |
8 | interface MessagesRepository {
9 |
10 | fun sendMessage(message: Message): Completable
11 |
12 | fun getMessages(): Observable>
13 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/imakeanapp/domain/messages/usecase/GetMessagesUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.domain.messages.usecase
2 |
3 | import com.imakeanapp.domain.core.ObservableUseCase
4 | import com.imakeanapp.domain.messages.model.Message
5 | import com.imakeanapp.domain.messages.repository.MessagesRepository
6 | import io.reactivex.Observable
7 |
8 | class GetMessagesUseCase(private val repository: MessagesRepository) : ObservableUseCase> {
9 |
10 | override fun execute() = repository.getMessages()
11 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/imakeanapp/domain/messages/usecase/SendMessageUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.domain.messages.usecase
2 |
3 | import com.imakeanapp.domain.core.CompletableWithParamUseCase
4 | import com.imakeanapp.domain.messages.model.Message
5 | import com.imakeanapp.domain.messages.repository.MessagesRepository
6 | import io.reactivex.Completable
7 |
8 | class SendMessageUseCase(private val repository: MessagesRepository) : CompletableWithParamUseCase {
9 |
10 | override fun execute(t: Message) = repository.sendMessage(t)
11 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/imakeanapp/domain/user/model/User.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.domain.user.model
2 |
3 | data class User(var username: String = "", var password: String = "")
--------------------------------------------------------------------------------
/domain/src/main/java/com/imakeanapp/domain/user/repository/AuthRepository.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.domain.user.repository
2 |
3 | import com.imakeanapp.domain.user.model.User
4 | import io.reactivex.Single
5 |
6 |
7 | interface AuthRepository {
8 |
9 | fun signup(username: String, password: String): Single
10 |
11 | fun login(username: String, password: String): Single
12 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/imakeanapp/domain/user/usecase/LoginUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.domain.user.usecase
2 |
3 | import com.imakeanapp.domain.core.SingleWithParamUseCase
4 | import com.imakeanapp.domain.user.model.User
5 | import com.imakeanapp.domain.user.repository.AuthRepository
6 | import io.reactivex.Single
7 |
8 | class LoginUseCase(private val repository: AuthRepository) : SingleWithParamUseCase {
9 |
10 | override fun execute(t: User) = repository.login(t.username, t.password)
11 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/imakeanapp/domain/user/usecase/SignUpUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.imakeanapp.domain.user.usecase
2 |
3 | import com.imakeanapp.domain.core.SingleWithParamUseCase
4 | import com.imakeanapp.domain.user.model.User
5 | import com.imakeanapp.domain.user.repository.AuthRepository
6 | import io.reactivex.Single
7 |
8 | class SignUpUseCase(private val repository: AuthRepository) : SingleWithParamUseCase {
9 |
10 | override fun execute(t: User) = repository.signup(t.username, t.password)
11 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arthlimchiu/ChatApp-Clean-Kotlin/33172ca3312caef1ce334e1baee4368904b53377/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Sep 26 20:59:05 PHT 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':data', ':domain'
2 |
--------------------------------------------------------------------------------