Browse Source

Merge pull request #6 from MrOzOn/refactoring_feature_auth

Refactoring feature auth
MrOzOn 5 years ago
parent
commit
3a4517f808

+ 1 - 1
app/src/main/java/com/mrozon/healthdiary/presentation/main/MainActivity.kt

@@ -89,7 +89,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(){
     override fun subscribeUi() {
 
         viewModel.cleared.observe(this, Observer { cleared ->
-            if (cleared) {
+            cleared.getContentIfNotHandled()?.let {
                 navController.navigate(R.id.action_global_loginFragment)
             }
         })

+ 4 - 3
app/src/main/java/com/mrozon/healthdiary/presentation/main/MainActivityViewModel.kt

@@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
 import com.mrozon.core_api.entity.User
 import com.mrozon.healthdiary.data.UserRepository
+import com.mrozon.utils.Event
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -19,15 +20,15 @@ class MainActivityViewModel @Inject constructor(
 
     val currentUser = userRepository.getLocalUser()
 
-    private val _cleared = MutableLiveData<Boolean>(false)
-    val cleared: LiveData<Boolean>
+    private val _cleared = MutableLiveData<Event<Unit>>()
+    val cleared: LiveData<Event<Unit>>
         get() = _cleared
 
     fun logoutUser(user: User) {
         CoroutineScope(Dispatchers.IO).launch(getJobErrorHandler()) {
             userRepository.clearLocalUser(user)
             withContext(Dispatchers.Main){
-                _cleared.value = true
+                _cleared.value = Event(Unit)
             }
         }
     }

+ 9 - 5
app/src/main/res/navigation/nav_graph.xml

@@ -15,9 +15,8 @@
             app:destination="@id/loginFragment"
             app:enterAnim="@anim/slide_in_right"
             app:exitAnim="@anim/slide_out_left"
-            app:launchSingleTop="true"
             app:popEnterAnim="@anim/slide_in_left"
-            app:popExitAnim="@anim/slide_out_right" />
+            app:popExitAnim="@anim/slide_out_right"/>
         <action
             android:id="@+id/action_splashFragment_to_listPersonFragment"
             app:destination="@id/listPersonFragment"
@@ -64,15 +63,20 @@
             app:enterAnim="@anim/slide_in_right"
             app:exitAnim="@anim/slide_out_left"
             app:popEnterAnim="@anim/slide_in_left"
-            app:popExitAnim="@anim/slide_out_right"/>
+            app:popExitAnim="@anim/slide_out_right"
+            app:popUpTo="@id/loginFragment"
+            app:popUpToInclusive="true" />
     </fragment>
 
-    <action android:id="@+id/action_global_loginFragment"
+    <action
+        android:id="@+id/action_global_loginFragment"
         app:destination="@id/loginFragment"
         app:enterAnim="@anim/slide_in_right"
         app:exitAnim="@anim/slide_out_left"
         app:popEnterAnim="@anim/slide_in_left"
-        app:popExitAnim="@anim/slide_out_right"/>
+        app:popExitAnim="@anim/slide_out_right"
+        app:popUpTo="@id/loginFragment"
+        app:popUpToInclusive="true" />
 
     <fragment
         android:id="@+id/listPersonFragment"

+ 19 - 14
feature_auth/src/main/java/com/mrozon/feature_auth/presentation/LoginFragment.kt

@@ -20,6 +20,7 @@ import com.mrozon.utils.extension.visible
 import com.mrozon.utils.network.Result
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.FlowPreview
+import timber.log.Timber
 import javax.inject.Inject
 
 class LoginFragment : BaseFragment<FragmentLoginBinding>() {
@@ -37,6 +38,7 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
     override fun onAttach(context: Context) {
         super.onAttach(context)
         LoginFragmentComponent.injectFragment(this)
+        Timber.d("onAttach")
     }
 
     @ExperimentalCoroutinesApi
@@ -70,23 +72,26 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
             }
         })
 
-        viewModel.loggedUser.observe(viewLifecycleOwner, Observer { result ->
-            if(result!=null){
-                when (result.status) {
-                    Result.Status.LOADING -> {
-                        binding?.progressBar?.visible(true)
-                    }
-                    Result.Status.SUCCESS -> {
-                        binding?.progressBar?.visible(false)
-                        navigator.navigateToListPerson(findNavController())
-                    }
-                    Result.Status.ERROR -> {
-                        binding?.progressBar?.visible(false)
-                        binding?.btnLogin?.shake()
-                        showError(result.message!!)
+        viewModel.loggedUser.observe(viewLifecycleOwner, Observer { event ->
+            event.getContentIfNotHandled().let { result ->
+                if(result!=null){
+                    when (result.status) {
+                        Result.Status.LOADING -> {
+                            binding?.progressBar?.visible(true)
+                        }
+                        Result.Status.SUCCESS -> {
+                            binding?.progressBar?.visible(false)
+                            navigator.navigateToListPerson(findNavController())
+                        }
+                        Result.Status.ERROR -> {
+                            binding?.progressBar?.visible(false)
+                            binding?.btnLogin?.shake()
+                            showError(result.message!!)
+                        }
                     }
                 }
             }
+
         })
 
     }

+ 4 - 3
feature_auth/src/main/java/com/mrozon/feature_auth/presentation/LoginFragmentViewModel.kt

@@ -8,6 +8,7 @@ import com.mrozon.core_api.entity.User
 import com.mrozon.core_api.providers.CoroutineContextProvider
 import com.mrozon.feature_auth.data.UserAuthRepository
 import com.mrozon.feature_auth.data.UserAuthRepositoryImpl
+import com.mrozon.utils.Event
 import com.mrozon.utils.base.BaseViewModel
 import com.mrozon.utils.network.Result
 //import com.mrozon.utils.extension.asFlow
@@ -23,8 +24,8 @@ class LoginFragmentViewModel @Inject constructor(
     private val coroutineContextProvider: CoroutineContextProvider
 ): BaseViewModel() {
 
-    private val _loggedUser = MutableLiveData<Result<User>?>(null)
-    val loggedUser: LiveData<Result<User>?>
+    private val _loggedUser = MutableLiveData<Event<Result<User>>>()
+    val loggedUser: LiveData<Event<Result<User>>>
         get() = _loggedUser
 
     @ExperimentalCoroutinesApi
@@ -66,7 +67,7 @@ class LoginFragmentViewModel @Inject constructor(
         viewModelScope.launch(coroutineContextProvider.IO){
             repository.loginUser(userName,psw).collect {
                 withContext(coroutineContextProvider.Main) {
-                    _loggedUser.value = it
+                    _loggedUser.value = Event(it)
                 }
             }
         }

+ 8 - 6
feature_auth/src/main/java/com/mrozon/feature_auth/presentation/RegistrationFragment.kt

@@ -39,6 +39,7 @@ class RegistrationFragment: BaseFragment<FragmentRegistrationBinding>() {
     override fun onAttach(context: Context) {
         super.onAttach(context)
         RegistrationFragmentComponent.injectFragment(this)
+        Timber.d("onAttach")
     }
 
     @ExperimentalCoroutinesApi
@@ -66,13 +67,14 @@ class RegistrationFragment: BaseFragment<FragmentRegistrationBinding>() {
     @FlowPreview
     override fun subscribeUi() {
 
-        viewModel.error.observe(viewLifecycleOwner, Observer {error ->
-            if(error!=null)
+        viewModel.error.observe(viewLifecycleOwner, Observer {event ->
+            event.getContentIfNotHandled()?.let { error ->
                 showError(error) {}
+            }
         })
 
-        viewModel.registeredUser.observe(viewLifecycleOwner, Observer { result ->
-            if(result!=null){
+        viewModel.registeredUser.observe(viewLifecycleOwner, Observer { event ->
+            event.getContentIfNotHandled()?.let { result ->
                 when (result.status) {
                     Result.Status.LOADING -> {
                         binding?.progressBar?.visible(true)
@@ -80,8 +82,8 @@ class RegistrationFragment: BaseFragment<FragmentRegistrationBinding>() {
                     Result.Status.SUCCESS -> {
                         binding?.progressBar?.visible(false)
                         showInfo(getString(R.string.userRegistered)) {
-                                navigator.navigateToLoginUser(findNavController(), result.data?.email?:"")
-                            }
+                            navigator.navigateToLoginUser(findNavController(), result.data?.email?:"")
+                        }
                     }
                     Result.Status.ERROR -> {
                         binding?.progressBar?.visible(false)

+ 7 - 5
feature_auth/src/main/java/com/mrozon/feature_auth/presentation/RegistrationFragmentViewModel.kt

@@ -1,5 +1,6 @@
 package com.mrozon.feature_auth.presentation
 
+import android.util.EventLog
 import android.util.Patterns.EMAIL_ADDRESS
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
@@ -8,6 +9,7 @@ import com.mrozon.core_api.entity.User
 import com.mrozon.core_api.providers.CoroutineContextProvider
 import com.mrozon.feature_auth.data.UserAuthRepository
 import com.mrozon.feature_auth.data.UserAuthRepositoryImpl
+import com.mrozon.utils.Event
 import com.mrozon.utils.base.BaseViewModel
 import com.mrozon.utils.network.Result
 //import com.mrozon.utils.extension.asFlow
@@ -24,12 +26,12 @@ class RegistrationFragmentViewModel @Inject constructor(
     private val coroutineContextProvider: CoroutineContextProvider
 ): BaseViewModel() {
 
-    private val _error = MutableLiveData<String?>()
-    val error: LiveData<String?>
+    private val _error = MutableLiveData<Event<String>>()
+    val error: LiveData<Event<String>>
         get() = _error
 
-    private val _registeredUser = MutableLiveData<Result<User>?>(null)
-    val registeredUser: LiveData<Result<User>?>
+    private val _registeredUser = MutableLiveData<Event<Result<User>>>()
+    val registeredUser: LiveData<Event<Result<User>>>
         get() = _registeredUser
 
     @ExperimentalCoroutinesApi
@@ -94,7 +96,7 @@ class RegistrationFragmentViewModel @Inject constructor(
         viewModelScope.launch(coroutineContextProvider.IO) {
             repository.registerUser(user,psw).collect {
                 withContext(coroutineContextProvider.Main) {
-                    _registeredUser.value = it
+                    _registeredUser.value = Event(it)
                 }
             }
         }

+ 5 - 1
feature_auth/src/test/java/com/mrozon/feature_auth/data/UserAuthRepositoryImplTest.kt

@@ -3,6 +3,7 @@ package com.mrozon.feature_auth.data
 import com.mrozon.core_api.db.HealthDiaryDao
 import com.mrozon.core_api.network.HealthDiaryService
 import com.mrozon.core_api.network.model.*
+import com.mrozon.core_api.security.SecurityTokenService
 import com.mrozon.utils.network.Result.Companion.loading
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -37,6 +38,9 @@ class UserAuthRepositoryImplTest {
     @Mock
     lateinit var dao: HealthDiaryDao
 
+    @Mock
+    lateinit var securityTokenService: SecurityTokenService
+
     lateinit var repository: UserAuthRepository
 
     @get:Rule
@@ -45,7 +49,7 @@ class UserAuthRepositoryImplTest {
     @Before
     fun setUp() {
         val source = UserAuthRemoteDataSource(apiService)
-        repository = UserAuthRepositoryImpl(source, dao)
+        repository = UserAuthRepositoryImpl(source, dao, securityTokenService)
     }
 
     @Test

+ 6 - 5
feature_auth/src/test/java/com/mrozon/feature_auth/presentation/LoginFragmentViewModelTest.kt

@@ -5,6 +5,7 @@ import androidx.lifecycle.Observer
 import com.mrozon.core_api.entity.User
 import com.mrozon.feature_auth.data.CoroutineTestRule
 import com.mrozon.feature_auth.data.UserAuthRepository
+import com.mrozon.utils.Event
 import com.mrozon.utils.network.Result
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.FlowPreview
@@ -40,7 +41,7 @@ class LoginFragmentViewModelTest {
     lateinit var repository: UserAuthRepository
 
     @Mock
-    lateinit var observer: Observer<Result<User>?>
+    lateinit var observer: Observer<Event<Result<User>>>
 
     @Mock
     lateinit var validateObserver: Observer<Boolean>
@@ -72,8 +73,8 @@ class LoginFragmentViewModelTest {
 
         viewModel.loginUser()
 
-        Mockito.verify(observer).onChanged(Result.loading())
-        Mockito.verify(observer).onChanged(Result.success(user))
+        Mockito.verify(observer).onChanged(Event(Result.loading()))
+        Mockito.verify(observer).onChanged(Event(Result.success(user)))
     }
 
     @Test
@@ -89,8 +90,8 @@ class LoginFragmentViewModelTest {
 
         viewModel.loginUser()
 
-        Mockito.verify(observer).onChanged(Result.loading())
-        Mockito.verify(observer).onChanged(Result.error(error))
+        Mockito.verify(observer).onChanged(Event(Result.loading()))
+        Mockito.verify(observer).onChanged(Event(Result.error(error)))
     }
 
     @FlowPreview

+ 6 - 5
feature_auth/src/test/java/com/mrozon/feature_auth/presentation/RegistrationFragmentViewModelTest.kt

@@ -6,6 +6,7 @@ import com.mrozon.core_api.entity.User
 import com.mrozon.core_api.providers.CoroutineContextProvider
 import com.mrozon.feature_auth.data.CoroutineTestRule
 import com.mrozon.feature_auth.data.UserAuthRepository
+import com.mrozon.utils.Event
 import com.mrozon.utils.network.Result
 import kotlinx.coroutines.*
 import kotlinx.coroutines.flow.flowOf
@@ -38,7 +39,7 @@ class RegistrationFragmentViewModelTest {
     lateinit var repository: UserAuthRepository
 
     @Mock
-    lateinit var observer: Observer<Result<User>?>
+    lateinit var observer: Observer<Event<Result<User>>>
 
     @Mock
     lateinit var validateObserver: Observer<ValidateDataError>
@@ -75,8 +76,8 @@ class RegistrationFragmentViewModelTest {
 
         viewModel.registerUser()
 
-        Mockito.verify(observer).onChanged(Result.loading())
-        Mockito.verify(observer).onChanged(Result.success(user))
+        Mockito.verify(observer).onChanged(Event(Result.loading()))
+        Mockito.verify(observer).onChanged(Event(Result.success(user)))
     }
 
     @Test
@@ -98,8 +99,8 @@ class RegistrationFragmentViewModelTest {
 
         viewModel.registerUser()
 
-        Mockito.verify(observer).onChanged(Result.loading())
-        Mockito.verify(observer).onChanged(Result.error(error))
+        Mockito.verify(observer).onChanged(Event(Result.loading()))
+        Mockito.verify(observer).onChanged(Event(Result.error(error)))
     }
 
     @FlowPreview

+ 25 - 0
feature_person/src/main/java/com/mrozon/feature_person/presentation/ListPersonFragment.kt

@@ -2,7 +2,9 @@ package com.mrozon.feature_person.presentation
 
 import android.content.Context
 import android.os.Bundle
+import android.os.Handler
 import android.view.View
+import androidx.activity.addCallback
 import androidx.fragment.app.viewModels
 import androidx.lifecycle.Observer
 import androidx.lifecycle.ViewModelProvider
@@ -21,6 +23,12 @@ import javax.inject.Inject
 
 class ListPersonFragment : BaseFragment<FragmentListPersonBinding>() {
 
+    companion object {
+        const val DELAY_EXIT_CONFIRM = 2000L
+    }
+
+    private var doubleBackToExitPressedOnce = false
+
     override fun getLayoutId(): Int = R.layout.fragment_list_person
 
     @Inject
@@ -36,6 +44,7 @@ class ListPersonFragment : BaseFragment<FragmentListPersonBinding>() {
     override fun onAttach(context: Context) {
         super.onAttach(context)
         ListPersonFragmentComponent.injectFragment(this)
+        Timber.d("onAttach")
     }
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -79,4 +88,20 @@ class ListPersonFragment : BaseFragment<FragmentListPersonBinding>() {
         })
     }
 
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        requireActivity().onBackPressedDispatcher.addCallback(this) {
+            finishIfConfirm()
+        }
+    }
+
+    private fun finishIfConfirm() {
+        if (doubleBackToExitPressedOnce)
+            requireActivity().finish()
+        doubleBackToExitPressedOnce = true
+        show(getString(R.string.confirm_exit))
+        Handler().postDelayed({
+            doubleBackToExitPressedOnce = false
+        }, DELAY_EXIT_CONFIRM)
+    }
 }

+ 1 - 0
feature_person/src/main/res/values/strings.xml

@@ -15,4 +15,5 @@
     <string name="error_empty_string">Empty field</string>
     <string name="error_invalid_email">Invalid format username (e-mail)</string>
     <string name="share_done">Done</string>
+    <string name="confirm_exit">Please click BACK again to exit</string>
 </resources>

+ 2 - 0
feature_splash/src/main/java/com/mrozon/feature_splash/presentation/SplashFragment.kt

@@ -16,6 +16,7 @@ import com.mrozon.feature_splash.R
 import com.mrozon.feature_splash.databinding.FragmentSplashBinding
 import com.mrozon.feature_splash.di.SplashFragmentComponent
 import com.mrozon.utils.base.BaseFragment
+import timber.log.Timber
 import javax.inject.Inject
 
 
@@ -34,6 +35,7 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
     override fun onAttach(context: Context) {
         super.onAttach(context)
         SplashFragmentComponent.injectFragment(this)
+        Timber.d("onAttach")
     }
 
     override fun subscribeUi() {

+ 44 - 0
utils/src/main/java/com/mrozon/utils/Event.kt

@@ -0,0 +1,44 @@
+package com.mrozon.utils
+
+import java.util.Objects.toString
+
+/**
+ * Used as a wrapper for data that is exposed via a LiveData that represents an event.
+ */
+open class Event<out T> (private val content: T) {
+
+    var hasBeenHandled = false
+        private set
+
+    /**
+     * Returns the content and prevents its use again.
+     */
+    fun getContentIfNotHandled(): T? {
+        return if (hasBeenHandled) {
+            null
+        } else {
+            hasBeenHandled = true
+            content
+        }
+    }
+
+    /**
+     * Returns the content, even if it's already been handled.
+     */
+    fun peekContent(): T = content
+
+    override fun toString(): String {
+        return "Event --> $content"
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other?.javaClass != javaClass) return false
+        val event = other as Event<*>
+        return content==event.content
+    }
+
+    override fun hashCode(): Int {
+        return content.hashCode()
+    }
+}

+ 3 - 1
utils/src/main/java/com/mrozon/utils/base/BaseFragment.kt

@@ -8,7 +8,9 @@ import androidx.annotation.LayoutRes
 import androidx.databinding.DataBindingUtil
 import androidx.databinding.ViewDataBinding
 import androidx.fragment.app.Fragment
+import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG
 import com.google.android.material.snackbar.Snackbar
+import timber.log.Timber
 
 abstract class BaseFragment<T : ViewDataBinding>: Fragment()//,
 {
@@ -67,7 +69,7 @@ abstract class BaseFragment<T : ViewDataBinding>: Fragment()//,
     }
 
     fun show(message: String) {
-        val snackbar = Snackbar.make(binding?.root!!,message,Snackbar.LENGTH_LONG)
+        val snackbar = Snackbar.make(binding?.root!!,message,LENGTH_LONG)
         snackbar.show()
     }
 }