Переглянути джерело

add reactive UI logic for RegisterFragment

MrOzOn 5 роки тому
батько
коміт
265b41c2c7
32 змінених файлів з 766 додано та 8 видалено
  1. 6 1
      app/src/main/AndroidManifest.xml
  2. 5 0
      app/src/main/java/com/mrozon/healthdiary/di/NavigationModule.kt
  3. 17 0
      app/src/main/java/com/mrozon/healthdiary/navigation/LoginNavigatorImpl.kt
  4. 17 1
      app/src/main/res/navigation/nav_graph.xml
  5. 6 0
      app/src/main/res/xml/network_security_config.xml
  6. 1 0
      core_api/build.gradle
  7. 5 0
      core_api/src/main/java/com/mrozon/core_api/db/HealthDiaryDao.kt
  8. 7 0
      core_api/src/main/java/com/mrozon/core_api/navigation/LoginNavigator.kt
  9. 1 0
      core_api/src/main/java/com/mrozon/core_api/navigation/NavigatorProvider.kt
  10. 12 4
      core_api/src/main/java/com/mrozon/core_api/network/HealthDiaryService.kt
  11. 6 0
      core_api/src/main/java/com/mrozon/core_api/network/model/LoginRequest.kt
  12. 21 0
      core_api/src/main/java/com/mrozon/core_api/network/model/LoginResponse.kt
  13. 9 0
      core_api/src/main/java/com/mrozon/core_api/network/model/RegisterRequest.kt
  14. 10 0
      core_api/src/main/java/com/mrozon/core_api/network/model/RegisterResponse.kt
  15. 1 0
      core_impl/build.gradle
  16. 1 0
      feature_auth/build.gradle
  17. 18 0
      feature_auth/src/main/java/com/mrozon/feature_auth/data/UserAuthRemoteDataSource.kt
  18. 36 0
      feature_auth/src/main/java/com/mrozon/feature_auth/di/RegistrationFragmentComponent.kt
  19. 23 0
      feature_auth/src/main/java/com/mrozon/feature_auth/di/RegistrationFragmentModule.kt
  20. 18 1
      feature_auth/src/main/java/com/mrozon/feature_auth/presentation/LoginFragment.kt
  21. 35 0
      feature_auth/src/main/java/com/mrozon/feature_auth/presentation/LoginFragmentViewModel.kt
  22. 110 0
      feature_auth/src/main/java/com/mrozon/feature_auth/presentation/RegistrationFragment.kt
  23. 140 0
      feature_auth/src/main/java/com/mrozon/feature_auth/presentation/RegistrationFragmentViewModel.kt
  24. 146 0
      feature_auth/src/main/res/layout/fragment_registration.xml
  25. 8 0
      feature_auth/src/main/res/values/strings.xml
  26. 1 0
      scripts/deps_versions.gradle
  27. 1 1
      utils/build.gradle
  28. 38 0
      utils/src/main/java/com/mrozon/utils/base/BaseDataSource.kt
  29. 34 0
      utils/src/main/java/com/mrozon/utils/base/BaseFragment.kt
  30. 24 0
      utils/src/main/java/com/mrozon/utils/network/Result.kt
  31. 5 0
      utils/src/main/res/values/colors.xml
  32. 4 0
      utils/src/main/res/values/strings.xml

+ 6 - 1
app/src/main/AndroidManifest.xml

@@ -1,7 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     package="com.mrozon.healthdiary">
 
+    <uses-permission android:name="android.permission.INTERNET" />
+
     <application
         android:allowBackup="true"
         android:icon="@mipmap/ic_launcher"
@@ -9,7 +12,9 @@
         android:roundIcon="@mipmap/ic_launcher_round"
         android:supportsRtl="true"
         android:name=".App"
-        android:theme="@style/AppTheme">
+        android:networkSecurityConfig="@xml/network_security_config"
+        android:theme="@style/AppTheme"
+        tools:ignore="AllowBackup">
         <activity android:name=".presentation.main.MainActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />

+ 5 - 0
app/src/main/java/com/mrozon/healthdiary/di/NavigationModule.kt

@@ -5,9 +5,11 @@ import androidx.navigation.NavController
 import androidx.navigation.fragment.findNavController
 import com.mrozon.core_api.db.HealthDiaryDao
 import com.mrozon.core_api.mapper.UserToUserDbMapper
+import com.mrozon.core_api.navigation.LoginNavigator
 import com.mrozon.core_api.navigation.SplashNavigator
 import com.mrozon.feature_splash.repository.LocalUser
 import com.mrozon.feature_splash.repository.LocalUserImp
+import com.mrozon.healthdiary.navigation.LoginNavigatorImpl
 import com.mrozon.healthdiary.navigation.SplashNavigatorImpl
 import com.mrozon.utils.base.BaseFragment
 import dagger.Binds
@@ -23,4 +25,7 @@ interface NavigationModule {
     @Binds
     fun splashNavigator(navigator: SplashNavigatorImpl): SplashNavigator
 
+    @Reusable
+    @Binds
+    fun loginNavigator(navigator: LoginNavigatorImpl): LoginNavigator
 }

+ 17 - 0
app/src/main/java/com/mrozon/healthdiary/navigation/LoginNavigatorImpl.kt

@@ -0,0 +1,17 @@
+package com.mrozon.healthdiary.navigation
+
+import androidx.navigation.NavController
+import com.mrozon.core_api.navigation.LoginNavigator
+import com.mrozon.core_api.navigation.SplashNavigator
+import com.mrozon.healthdiary.R
+import javax.inject.Inject
+
+class LoginNavigatorImpl @Inject constructor()
+    :LoginNavigator {
+
+    override fun navigateToRegisterUser(navController: NavController) {
+        navController.navigate(R.id.action_loginFragment_to_registrationFragment)
+    }
+
+
+}

+ 17 - 1
app/src/main/res/navigation/nav_graph.xml

@@ -14,9 +14,25 @@
             android:id="@+id/action_splashFragment_to_loginFragment"
             app:destination="@id/loginFragment" />
     </fragment>
+
     <fragment
         android:id="@+id/loginFragment"
         android:name="com.mrozon.feature_auth.presentation.LoginFragment"
         tools:layout="@layout/fragment_login"
-        android:label="LoginFragment" />
+        android:label="LoginFragment" >
+        <action
+            android:id="@+id/action_loginFragment_to_registrationFragment"
+            app:destination="@id/registrationFragment" />
+    </fragment>
+
+    <fragment
+        android:id="@+id/registrationFragment"
+        tools:layout="@layout/fragment_registration"
+        android:name="com.mrozon.feature_auth.presentation.RegistrationFragment"
+        android:label="RegistrationFragment" >
+        <action
+            android:id="@+id/action_registrationFragment_to_loginFragment"
+            app:destination="@id/loginFragment" />
+    </fragment>
+
 </navigation>

+ 6 - 0
app/src/main/res/xml/network_security_config.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+    <domain-config cleartextTrafficPermitted="true">
+        <domain includeSubdomains="true">10.0.2.2</domain>
+    </domain-config>
+</network-security-config>

+ 1 - 0
core_api/build.gradle

@@ -35,6 +35,7 @@ dependencies {
     implementation dagger
     kapt daggerCompiler
     implementation room
+    implementation roomKtx
     implementation okhttp
     implementation converterGson
     implementation navigationFragment

+ 5 - 0
core_api/src/main/java/com/mrozon/core_api/db/HealthDiaryDao.kt

@@ -2,6 +2,8 @@ package com.mrozon.core_api.db
 
 import androidx.lifecycle.LiveData
 import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
 import androidx.room.Query
 import com.mrozon.core_api.db.model.UserDb
 
@@ -10,4 +12,7 @@ interface HealthDiaryDao {
 
     @Query("SELECT * FROM user_table LIMIT 1")
     fun getUser(): LiveData<UserDb>
+
+    @Insert(onConflict = OnConflictStrategy.REPLACE)
+    suspend fun insertUser(userDb: UserDb)
 }

+ 7 - 0
core_api/src/main/java/com/mrozon/core_api/navigation/LoginNavigator.kt

@@ -0,0 +1,7 @@
+package com.mrozon.core_api.navigation
+
+import androidx.navigation.NavController
+
+interface LoginNavigator {
+    fun navigateToRegisterUser(navController: NavController)
+}

+ 1 - 0
core_api/src/main/java/com/mrozon/core_api/navigation/NavigatorProvider.kt

@@ -2,4 +2,5 @@ package com.mrozon.core_api.navigation
 
 interface NavigatorProvider {
     fun provideSplashNavigator(): SplashNavigator
+    fun provideLoginNavigator(): LoginNavigator
 }

+ 12 - 4
core_api/src/main/java/com/mrozon/core_api/network/HealthDiaryService.kt

@@ -1,15 +1,23 @@
 package com.mrozon.core_api.network
 
+import com.mrozon.core_api.network.model.LoginRequest
+import com.mrozon.core_api.network.model.LoginResponse
+import com.mrozon.core_api.network.model.RegisterRequest
+import com.mrozon.core_api.network.model.RegisterResponse
 import retrofit2.Response
-import retrofit2.http.GET
-import retrofit2.http.Path
-import retrofit2.http.Query
+import retrofit2.http.*
 
 interface HealthDiaryService {
     companion object {
-        const val ENDPOINT = "https://rebrickable.com/api/v3/"
+        const val ENDPOINT =  "http://10.0.2.2:8000/"//"https://hdb.mr-ozon-1982.tk/"
     }
 
+    @POST("login/")
+    suspend fun loginUser(@Body body: LoginRequest): Response<LoginResponse>
+
+    @POST("register/")
+    suspend fun registerUser(@Body body: RegisterRequest): Response<RegisterResponse>
+
 //    @GET("lego/themes/")
 //    suspend fun getThemes(@Query("page") page: Int? = null,
 //                          @Query("page_size") pageSize: Int? = null,

+ 6 - 0
core_api/src/main/java/com/mrozon/core_api/network/model/LoginRequest.kt

@@ -0,0 +1,6 @@
+package com.mrozon.core_api.network.model
+
+data class LoginRequest(
+    val username: String,
+    val password: String
+)

+ 21 - 0
core_api/src/main/java/com/mrozon/core_api/network/model/LoginResponse.kt

@@ -0,0 +1,21 @@
+package com.mrozon.core_api.network.model
+
+import com.mrozon.core_api.db.model.UserDb
+
+data class LoginResponse(
+    val email: String,
+    val first_name: String,
+    val last_name: String,
+    val token: String,
+    val user_id: Int
+)
+
+fun LoginResponse.toUserDb(): UserDb {
+    return UserDb(
+        id = user_id.toLong(),
+        email = email,
+        firstname = first_name,
+        lastname = last_name,
+        token = token
+    )
+}

+ 9 - 0
core_api/src/main/java/com/mrozon/core_api/network/model/RegisterRequest.kt

@@ -0,0 +1,9 @@
+package com.mrozon.core_api.network.model
+
+data class RegisterRequest(
+    val email: String,
+    val first_name: String,
+    val last_name: String,
+    val password: String,
+    val username: String
+)

+ 10 - 0
core_api/src/main/java/com/mrozon/core_api/network/model/RegisterResponse.kt

@@ -0,0 +1,10 @@
+package com.mrozon.core_api.network.model
+
+data class RegisterResponse(
+    val email: String,
+    val first_name: String,
+    val id: Int,
+    val last_login: Any,
+    val last_name: String,
+    val username: String
+)

+ 1 - 0
core_impl/build.gradle

@@ -34,6 +34,7 @@ dependencies {
     implementation dagger
     kapt daggerCompiler
     implementation room
+    implementation roomKtx
     kapt roomCompiler
     implementation okhttp
     implementation loggingInterceptor

+ 1 - 0
feature_auth/build.gradle

@@ -49,6 +49,7 @@ dependencies {
     //Timber
     implementation timber
     implementation navigationFragment
+    implementation retrofit
 
     implementation fileTree(dir: "libs", include: ["*.jar"])
     implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

+ 18 - 0
feature_auth/src/main/java/com/mrozon/feature_auth/data/UserAuthRemoteDataSource.kt

@@ -0,0 +1,18 @@
+package com.mrozon.feature_auth.data
+
+import com.mrozon.core_api.network.HealthDiaryService
+import com.mrozon.core_api.network.model.LoginRequest
+import com.mrozon.core_api.network.model.RegisterRequest
+import com.mrozon.utils.base.BaseDataSource
+import javax.inject.Inject
+
+class UserAuthRemoteDataSource @Inject constructor(private val service: HealthDiaryService): BaseDataSource() {
+
+    suspend fun loginUser(userName: String, userPsw: String)
+            = getResult { service.loginUser(LoginRequest(username = userName, password = userPsw)) }
+
+    suspend fun registerUser(email: String, password: String, firstName: String, lastName: String)
+            = getResult { service.registerUser(RegisterRequest(email = email, username = email,
+                password = password, first_name = firstName, last_name = lastName )) }
+
+}

+ 36 - 0
feature_auth/src/main/java/com/mrozon/feature_auth/di/RegistrationFragmentComponent.kt

@@ -0,0 +1,36 @@
+package com.mrozon.feature_auth.di
+
+import com.mrozon.core_api.navigation.SplashNavigator
+import com.mrozon.core_api.providers.AppWithFacade
+import com.mrozon.core_api.providers.ProvidersFacade
+import com.mrozon.core_api.viewmodel.ViewModelsFactoryProvider
+import com.mrozon.feature_auth.presentation.LoginFragment
+import com.mrozon.feature_auth.presentation.RegistrationFragment
+import dagger.Component
+import javax.inject.Singleton
+
+@Singleton
+@Component(
+    modules = [RegistrationFragmentModule::class],
+    dependencies = [ProvidersFacade::class]
+)
+interface RegistrationFragmentComponent: ViewModelsFactoryProvider {
+
+    companion object {
+
+        fun create(providersFacade: ProvidersFacade): RegistrationFragmentComponent {
+            return DaggerRegistrationFragmentComponent.builder()
+                .providersFacade(providersFacade)
+                .build()
+        }
+
+        fun injectFragment(fragment: RegistrationFragment): RegistrationFragmentComponent  {
+            val component = create((fragment.activity?.application
+                    as AppWithFacade).getFacade())
+            component.inject(fragment)
+            return component
+        }
+    }
+
+    fun inject(fragment: RegistrationFragment)
+}

+ 23 - 0
feature_auth/src/main/java/com/mrozon/feature_auth/di/RegistrationFragmentModule.kt

@@ -0,0 +1,23 @@
+package com.mrozon.feature_auth.di
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.mrozon.core_api.viewmodel.ViewModelKey
+import com.mrozon.feature_auth.presentation.LoginFragmentViewModel
+import com.mrozon.feature_auth.presentation.RegistrationFragmentViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+
+@Module
+interface RegistrationFragmentModule {
+
+    @Binds
+    @IntoMap
+    @ViewModelKey(RegistrationFragmentViewModel::class)
+    fun bindViewModel(viewmodel: RegistrationFragmentViewModel): ViewModel
+
+    @Binds
+    fun viewModelFactory(factory: DaggerViewModelFactory): ViewModelProvider.Factory
+
+}

+ 18 - 1
feature_auth/src/main/java/com/mrozon/feature_auth/presentation/LoginFragment.kt

@@ -13,6 +13,9 @@ import androidx.fragment.app.Fragment
 import androidx.fragment.app.viewModels
 import androidx.lifecycle.Observer
 import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import com.mrozon.core_api.navigation.LoginNavigator
+import com.mrozon.core_api.navigation.SplashNavigator
 import com.mrozon.feature_auth.R
 import com.mrozon.feature_auth.databinding.FragmentLoginBinding
 import com.mrozon.feature_auth.di.LoginFragmentComponent
@@ -33,6 +36,9 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
     @Inject
     lateinit var viewModelFactory: ViewModelProvider.Factory
 
+    @Inject
+    lateinit var navigator: LoginNavigator
+
     private val viewModel by viewModels<LoginFragmentViewModel> { viewModelFactory }
 
     override fun onAttach(context: Context) {
@@ -47,10 +53,16 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
         binding?.etUserName?.offer(viewModel.userNameChannel)
         binding?.etUserPassword?.offer(viewModel.userPasswordChannel)
 
-        btnLogin.setOnClickListener {
+        binding?.btnLogin?.setOnClickListener {
             hideKeyboard()
             viewModel.loginUser(etUserName.text.toString().trim(),etUserPassword.text.toString().trim())
         }
+
+        binding?.btnRegistration?.setOnClickListener {
+            hideKeyboard()
+            navigator.navigateToRegisterUser(findNavController())
+        }
+
     }
 
 
@@ -68,6 +80,11 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
             binding?.btnLogin?.isEnabled = !progress && viewModel.enableLogin.value?:false
             binding?.btnRegistration?.isEnabled = !progress
         })
+
+        viewModel.error.observe(viewLifecycleOwner, Observer {error ->
+            if(error!=null)
+                showError(error) {}
+        })
     }
 
 }

+ 35 - 0
feature_auth/src/main/java/com/mrozon/feature_auth/presentation/LoginFragmentViewModel.kt

@@ -4,6 +4,11 @@ import android.util.Patterns.EMAIL_ADDRESS
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.viewModelScope
+import com.mrozon.core_api.db.HealthDiaryDao
+import com.mrozon.core_api.db.model.UserDb
+import com.mrozon.core_api.mapper.UserToUserDbMapper
+import com.mrozon.core_api.network.model.toUserDb
+import com.mrozon.feature_auth.data.UserAuthRemoteDataSource
 import com.mrozon.utils.base.BaseViewModel
 //import com.mrozon.utils.extension.asFlow
 import kotlinx.coroutines.*
@@ -12,15 +17,22 @@ import kotlinx.coroutines.flow.asFlow
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.combineLatest
+import timber.log.Timber
 import javax.inject.Inject
 
 class LoginFragmentViewModel @Inject constructor(
+    private val userAuthRemoteDataSource: UserAuthRemoteDataSource,
+    private val healthDiaryDao: HealthDiaryDao
 ): BaseViewModel() {
 
     private val _progress = MutableLiveData<Boolean>(false)
     val progress: LiveData<Boolean>
         get() = _progress
 
+    private val _error = MutableLiveData<String?>()
+    val error: LiveData<String?>
+        get() = _error
+
     @ExperimentalCoroutinesApi
     val userNameChannel = ConflatedBroadcastChannel<String>()
 
@@ -54,5 +66,28 @@ class LoginFragmentViewModel @Inject constructor(
 
     fun loginUser(userName: String, userPsw: String) {
         _progress.value = true
+        CoroutineScope(Dispatchers.IO).launch(getJobErrorHandler()) {
+            val response = userAuthRemoteDataSource.loginUser(userName, userPsw)
+                if (response.status == com.mrozon.utils.network.Result.Status.SUCCESS) {
+                    val result = response.data
+                    healthDiaryDao.insertUser(result?.toUserDb()!!)
+                    Timber.d(result.token)
+                    withContext(Dispatchers.Main) {
+                        _progress.value = false
+                    }
+                } else if (response.status == com.mrozon.utils.network.Result.Status.ERROR) {
+                    Timber.e(response.message!!)
+                    withContext(Dispatchers.Main) {
+                        _progress.value = false
+                        _error.value = response.message!!
+                    }
+                }
+
+        }
+    }
+
+    private fun getJobErrorHandler() = CoroutineExceptionHandler { _, e ->
+        Timber.e(e)
+        _error.value = e.message
     }
 }

+ 110 - 0
feature_auth/src/main/java/com/mrozon/feature_auth/presentation/RegistrationFragment.kt

@@ -0,0 +1,110 @@
+package com.mrozon.feature_auth.presentation
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import com.mrozon.feature_auth.R
+import com.mrozon.feature_auth.databinding.FragmentRegistrationBinding
+import com.mrozon.feature_auth.di.LoginFragmentComponent
+import com.mrozon.feature_auth.di.RegistrationFragmentComponent
+import com.mrozon.utils.base.BaseFragment
+import com.mrozon.utils.extension.hideKeyboard
+import com.mrozon.utils.extension.offer
+import com.mrozon.utils.extension.visible
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
+import timber.log.Timber
+import javax.inject.Inject
+
+class RegistrationFragment: BaseFragment<FragmentRegistrationBinding>() {
+
+    override fun getLayoutId(): Int = R.layout.fragment_registration
+
+    @Inject
+    lateinit var viewModelFactory: ViewModelProvider.Factory
+
+    private val viewModel by viewModels<RegistrationFragmentViewModel> { viewModelFactory }
+
+    override fun onAttach(context: Context) {
+        super.onAttach(context)
+        RegistrationFragmentComponent.injectFragment(this)
+    }
+
+    @ExperimentalCoroutinesApi
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        binding?.etUserName?.offer(viewModel.emailChannel)
+        binding?.etUserPassword?.offer(viewModel.passwordChannel)
+        binding?.etUserPasswordAgain?.offer(viewModel.passwordConfirmChannel)
+        binding?.etFirstName?.offer(viewModel.firstNameChannel)
+        binding?.etLastName?.offer(viewModel.lastNameChannel)
+
+        binding?.btnRegistration?.setOnClickListener {
+            hideKeyboard()
+            viewModel.registerUser()
+        }
+    }
+
+    @ExperimentalCoroutinesApi
+    @FlowPreview
+    override fun subscribeUi() {
+
+        viewModel.progress.observe(viewLifecycleOwner, Observer { progress ->
+            binding?.progressBar?.visible(progress)
+            binding?.btnRegistrationCancel?.isEnabled = !progress
+            Timber.d("progress=${progress} and valid=${viewModel.validateData.value==ValidateDataError.OK}")
+            binding?.btnRegistration?.isEnabled = (!progress) and (viewModel.validateData.value==ValidateDataError.OK)
+        })
+
+        viewModel.error.observe(viewLifecycleOwner, Observer {error ->
+            if(error!=null)
+                showError(error) {}
+        })
+
+        viewModel.registered.observe(viewLifecycleOwner, Observer { registeredUser ->
+            if(registeredUser) {
+                showInfo(getString(R.string.userRegistered)) {
+
+                }
+            }
+        })
+
+        viewModel.validateData.observe(viewLifecycleOwner, Observer {
+            binding?.btnRegistration?.isEnabled =  false
+            binding?.etUserPassword?.error = null
+            binding?.etUserPasswordAgain?.error = null
+            binding?.etUserName?.error = null
+            binding?.etFirstName?.error = null
+            binding?.etLastName?.error = null
+            when (it) {
+                ValidateDataError.OK -> {
+                    binding?.btnRegistration?.isEnabled = true and !(viewModel.progress.value!!)
+                }
+                ValidateDataError.INCORRECT_EMAIL -> {
+                    binding?.etUserName?.error = getString(R.string.uncorrect_email)
+                }
+                ValidateDataError.PASSWORD_EMPTY -> {
+                    binding?.etUserPassword?.error = getString(R.string.empty_field)
+                }
+                ValidateDataError.PASSWORD_AGAIN_EMPTY -> {
+                    binding?.etUserPasswordAgain?.error = getString(R.string.empty_field)
+                }
+                ValidateDataError.PASSWORD_NOT_EQUAL -> {
+                    binding?.etUserPassword?.error = getString(R.string.equals_psw)
+                }
+                ValidateDataError.FIRST_NAME_EMPTY -> {
+                    binding?.etFirstName?.error = getString(R.string.empty_field)
+                }
+                ValidateDataError.LAST_NAME_EMPTY -> {
+                    binding?.etLastName?.error = getString(R.string.empty_field)
+                }
+                else -> {}
+            }
+        })
+    }
+
+}

+ 140 - 0
feature_auth/src/main/java/com/mrozon/feature_auth/presentation/RegistrationFragmentViewModel.kt

@@ -0,0 +1,140 @@
+package com.mrozon.feature_auth.presentation
+
+import android.util.Patterns.EMAIL_ADDRESS
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Transformations
+import androidx.lifecycle.viewModelScope
+import com.mrozon.core_api.db.HealthDiaryDao
+import com.mrozon.core_api.db.model.UserDb
+import com.mrozon.core_api.mapper.UserToUserDbMapper
+import com.mrozon.core_api.network.model.toUserDb
+import com.mrozon.feature_auth.data.UserAuthRemoteDataSource
+import com.mrozon.utils.base.BaseViewModel
+//import com.mrozon.utils.extension.asFlow
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.ConflatedBroadcastChannel
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.combineLatest
+import timber.log.Timber
+import javax.inject.Inject
+
+class RegistrationFragmentViewModel @Inject constructor(
+    private val userAuthRemoteDataSource: UserAuthRemoteDataSource,
+    private val healthDiaryDao: HealthDiaryDao
+): BaseViewModel() {
+
+    private val _progress = MutableLiveData<Boolean>(false)
+    val progress: LiveData<Boolean>
+        get() = _progress
+
+    private val _error = MutableLiveData<String?>()
+    val error: LiveData<String?>
+        get() = _error
+
+    private val _registered = MutableLiveData<Boolean>(false)
+    val registered: LiveData<Boolean>
+        get() = _registered
+
+    @ExperimentalCoroutinesApi
+    val emailChannel = ConflatedBroadcastChannel<String>()
+
+    @ExperimentalCoroutinesApi
+    val passwordChannel = ConflatedBroadcastChannel<String>()
+
+    @ExperimentalCoroutinesApi
+    val passwordConfirmChannel = ConflatedBroadcastChannel<String>()
+
+    @ExperimentalCoroutinesApi
+    val firstNameChannel = ConflatedBroadcastChannel<String>()
+
+    @ExperimentalCoroutinesApi
+    val lastNameChannel = ConflatedBroadcastChannel<String>()
+
+    @FlowPreview
+    @ExperimentalCoroutinesApi
+    val validateData = object: MutableLiveData<ValidateDataError>() {
+
+        override fun onActive() {
+            value?.let { return }
+            viewModelScope.launch {
+                var job: Deferred<Unit>? = null
+                combine(
+                    emailChannel.asFlow(),
+                    passwordChannel.asFlow(),
+                    passwordConfirmChannel.asFlow(),
+                    firstNameChannel.asFlow(),
+                    lastNameChannel.asFlow()
+                ) { email: String, psw1: String, psw2: String, firstName: String, lastName: String ->
+                    arrayListOf(email, psw1, psw2, firstName, lastName)
+                }
+                    .collect {
+                        job?.cancel()
+                        job = async(Dispatchers.Main) {
+                            value = if (!EMAIL_ADDRESS.matcher(it[0]).matches())
+                                ValidateDataError.INCORRECT_EMAIL
+                            else if (it[1].isEmpty())
+                                ValidateDataError.PASSWORD_EMPTY
+                            else if (it[2].isEmpty())
+                                ValidateDataError.PASSWORD_AGAIN_EMPTY
+                            else if (it[1] != it[2])
+                                ValidateDataError.PASSWORD_NOT_EQUAL
+                            else if (it[3].isEmpty())
+                                ValidateDataError.FIRST_NAME_EMPTY
+                            else if (it[4].isEmpty())
+                                ValidateDataError.LAST_NAME_EMPTY
+                            else
+                                ValidateDataError.OK
+                        }
+                    }
+            }
+        }
+    }
+
+    @ExperimentalCoroutinesApi
+    fun registerUser() {
+        val email = emailChannel.value
+        val psw = passwordChannel.value
+        val firstName = firstNameChannel.value
+        val lastName = lastNameChannel.value
+        _progress.value = true
+        CoroutineScope(Dispatchers.IO).launch(getJobErrorHandler()) {
+            val response = userAuthRemoteDataSource.registerUser(
+                email = email,
+                password = psw,
+                lastName = lastName,
+                firstName = firstName
+            )
+            if (response.status == com.mrozon.utils.network.Result.Status.SUCCESS) {
+                withContext(Dispatchers.Main) {
+                    _progress.value = false
+                    _registered.value = true
+                }
+            } else if (response.status == com.mrozon.utils.network.Result.Status.ERROR) {
+                Timber.e(response.message!!)
+                withContext(Dispatchers.Main) {
+                    _progress.value = false
+                    _error.value = response.message!!
+                }
+            }
+
+        }
+    }
+
+    private fun getJobErrorHandler() = CoroutineExceptionHandler { _, e ->
+        Timber.e(e)
+        _error.value = e.message
+    }
+}
+
+enum class ValidateDataError(val code: Int) {
+    OK(0),
+    INCORRECT_EMAIL(10),
+    PASSWORD_EMPTY(20),
+    PASSWORD_AGAIN_EMPTY(21),
+    PASSWORD_NOT_EQUAL(30),
+    FIRST_NAME_EMPTY(40),
+    LAST_NAME_EMPTY(50),
+}

+ 146 - 0
feature_auth/src/main/res/layout/fragment_registration.xml

@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <data>
+        <variable
+            name="viewModel"
+            type="com.mrozon.feature_auth.presentation.RegistrationFragmentViewModel" />
+    </data>
+
+    <ScrollView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/layoutLogin"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <androidx.constraintlayout.widget.Guideline
+                android:id="@+id/guideline"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                app:layout_constraintGuide_percent="0.2" />
+
+            <EditText
+                android:id="@+id/etUserName"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="32dp"
+                android:layout_marginEnd="32dp"
+                android:ems="10"
+                android:gravity="center"
+                android:hint="@string/etUsername"
+                android:inputType="textEmailAddress"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="@+id/guideline" />
+
+            <EditText
+                android:id="@+id/etUserPassword"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="32dp"
+                android:layout_marginTop="16dp"
+                android:layout_marginEnd="32dp"
+                android:ems="10"
+                android:gravity="center"
+                android:hint="@string/etUserPassword"
+                android:inputType="textPassword"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/etUserName" />
+
+            <EditText
+                android:id="@+id/etUserPasswordAgain"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="32dp"
+                android:layout_marginTop="16dp"
+                android:layout_marginEnd="32dp"
+                android:ems="10"
+                android:gravity="center"
+                android:hint="@string/etUserPasswordAgain"
+                android:inputType="textPassword"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/etUserPassword" />
+
+            <EditText
+                android:id="@+id/etFirstName"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="32dp"
+                android:layout_marginTop="16dp"
+                android:layout_marginEnd="32dp"
+                android:ems="10"
+                android:gravity="center"
+                android:hint="@string/etFirstName"
+                android:inputType="textPersonName|text"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/etUserPasswordAgain" />
+
+            <EditText
+                android:id="@+id/etLastName"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="32dp"
+                android:layout_marginTop="16dp"
+                android:layout_marginEnd="32dp"
+                android:ems="10"
+                android:gravity="center"
+                android:hint="@string/etLastName"
+                android:inputType="textPersonName|text"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/etFirstName" />
+
+            <Button
+                android:id="@+id/btnRegistration"
+                style="@style/Widget.AppCompat.Button.Colored"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="32dp"
+                android:layout_marginTop="64dp"
+                android:layout_marginEnd="32dp"
+                android:enabled="false"
+                android:text="@string/btnRegistration"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/etLastName" />
+
+            <Button
+                android:id="@+id/btnRegistrationCancel"
+                style="@style/Widget.AppCompat.Button.Borderless.Colored"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="32dp"
+                android:layout_marginTop="16dp"
+                android:layout_marginEnd="32dp"
+                android:layout_marginBottom="16dp"
+                android:text="@string/btnRegistrationCancel"
+                android:textSize="12sp"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/btnRegistration" />
+
+            <ProgressBar
+                android:id="@+id/progressBar"
+                style="?android:attr/progressBarStyle"
+                android:layout_width="128dp"
+                android:layout_height="128dp"
+                android:indeterminate="true"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+    </ScrollView>
+</layout>

+ 8 - 0
feature_auth/src/main/res/values/strings.xml

@@ -5,4 +5,12 @@
     <string name="etUserPassword">Type Your password</string>
     <string name="btnLogin">Login</string>
     <string name="btnRegistration">Registration</string>
+    <string name="btnRegistrationCancel">Cancel</string>
+    <string name="etUserPasswordAgain">Repeat Your password</string>
+    <string name="etFirstName">Type Your first name</string>
+    <string name="etLastName">Type Your last name</string>
+    <string name="equals_psw">passwords must match</string>
+    <string name="uncorrect_email">uncorrect e-mail</string>
+    <string name="empty_field">Empty filed</string>
+    <string name="userRegistered">Success! User registered\nNow login</string>
 </resources>

+ 1 - 0
scripts/deps_versions.gradle

@@ -23,6 +23,7 @@ ext {
     daggerCompiler = "com.google.dagger:dagger-compiler:$daggerVersion"
     // Room
     room = "androidx.room:room-runtime:$roomVersion"
+    roomKtx = "androidx.room:room-ktx:$roomVersion"
     roomCompiler = "androidx.room:room-compiler:$roomVersion"
 //    picasso = "com.squareup.picasso:picasso:$picassoVersion"
 //    flexLayout = "com.google.android:flexbox:$flexLayoutVersion"

+ 1 - 1
utils/build.gradle

@@ -38,10 +38,10 @@ android {
 apply from: "$project.rootDir/scripts/deps_versions.gradle"
 
 dependencies {
-
     implementation navigationFragment
     implementation navigationUi
     implementation timber
+    implementation retrofit
     implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
 
 

+ 38 - 0
utils/src/main/java/com/mrozon/utils/base/BaseDataSource.kt

@@ -0,0 +1,38 @@
+package com.mrozon.utils.base
+
+import retrofit2.Response
+import timber.log.Timber
+import com.mrozon.utils.network.Result
+import org.json.JSONObject
+
+abstract class BaseDataSource {
+
+    protected suspend fun <T> getResult(call: suspend () -> Response<T>): Result<T> {
+        try {
+            val response = call()
+            //TODO response.errorBody()
+            if (response.isSuccessful) {
+                val body = response.body()
+                if (body != null) return Result.success(body)
+            }
+            var errMessage = response.message()
+            val errorBody= response.errorBody()
+            val jObjError = JSONObject(errorBody?.string()?:"")
+            if(jObjError.length()>0){
+                val key = jObjError.names().get(0).toString()
+                errMessage = jObjError[key].toString().trim('[',']')
+            }
+//            return error(" ${response.code()} $errMessage")
+            return error(errMessage)
+        } catch (e: Exception) {
+            return error(e.message ?: e.toString())
+        }
+    }
+
+    private fun <T> error(message: String): Result<T> {
+        Timber.e(message)
+        return Result.error("Network error:\n $message")
+//        return Result.error("Network call has failed for a following reason:\n $message")
+    }
+
+}

+ 34 - 0
utils/src/main/java/com/mrozon/utils/base/BaseFragment.kt

@@ -1,5 +1,6 @@
 package com.mrozon.utils.base
 
+import android.graphics.Color
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
@@ -7,11 +8,14 @@ import android.view.ViewGroup
 import android.view.ViewTreeObserver
 import android.view.InflateException
 import androidx.annotation.LayoutRes
+import androidx.core.content.ContextCompat.getColor
 import androidx.databinding.DataBindingUtil
 import androidx.databinding.ViewDataBinding
 import androidx.fragment.app.Fragment
 import androidx.navigation.Navigation.findNavController
 import androidx.navigation.fragment.NavHostFragment
+import com.google.android.material.snackbar.Snackbar
+import com.mrozon.utils.R
 import timber.log.Timber
 
 abstract class BaseFragment<T : ViewDataBinding>: Fragment()//,
@@ -54,4 +58,34 @@ abstract class BaseFragment<T : ViewDataBinding>: Fragment()//,
         subscribeUi()
         return binding!!.root
     }
+
+    fun showError(message: String, action:()->Unit) {
+        val snackbar = Snackbar.make(binding?.root!!,message,Snackbar.LENGTH_INDEFINITE)
+        snackbar.view.setBackgroundColor(getColor(requireContext(),R.color.color_snack_error))
+        snackbar.setActionTextColor(Color.WHITE)
+        snackbar.setAction(R.string.Ok) {
+            snackbar.dismiss()
+            action()
+        }
+        snackbar.show()
+    }
+
+    fun showInfo(message: String, action:()->Unit) {
+        val snackbar = Snackbar.make(binding?.root!!,message,Snackbar.LENGTH_INDEFINITE)
+        snackbar.view.setBackgroundColor(getColor(requireContext(),R.color.color_snack_info))
+        snackbar.setActionTextColor(Color.WHITE)
+        snackbar.setAction(R.string.Ok) {
+            snackbar.dismiss()
+            action()
+        }
+        snackbar.show()
+    }
+
+    fun show(message: String) {
+        val snackbar = Snackbar.make(binding?.root!!,message,Snackbar.LENGTH_LONG)
+        snackbar.setAction(R.string.Ok) {
+            snackbar.dismiss()
+        }
+        snackbar.show()
+    }
 }

+ 24 - 0
utils/src/main/java/com/mrozon/utils/network/Result.kt

@@ -0,0 +1,24 @@
+package com.mrozon.utils.network
+
+data class Result<out T>(val status: Status, val data: T?, val message: String?) {
+
+    enum class Status {
+        SUCCESS,
+        ERROR,
+        LOADING
+    }
+
+    companion object {
+        fun <T> success(data: T): Result<T> {
+            return Result(Status.SUCCESS, data, null)
+        }
+
+        fun <T> error(message: String, data: T? = null): Result<T> {
+            return Result(Status.ERROR, data, message)
+        }
+
+        fun <T> loading(data: T? = null): Result<T> {
+            return Result(Status.LOADING, data, null)
+        }
+    }
+}

+ 5 - 0
utils/src/main/res/values/colors.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="color_snack_error">#F44336</color>
+    <color name="color_snack_info">#00c853</color>
+</resources>

+ 4 - 0
utils/src/main/res/values/strings.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="Ok">Ok</string>
+</resources>