diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 674414f..41871c2 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -15,6 +15,7 @@ diff --git a/.idea/misc.xml b/.idea/misc.xml index 703e5d4..3378229 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -5,7 +5,7 @@ - + diff --git a/app/build.gradle b/app/build.gradle index df2d3b4..88a2a22 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -83,6 +83,10 @@ android { ktlint } + kotlinOptions { + freeCompilerArgs = ["-Xallow-result-return-type"] + } + task ktlint(type: JavaExec, group: "verification") { description = "Check Kotlin code style." classpath = configurations.ktlint @@ -117,21 +121,21 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0' - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.2.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation "androidx.preference:preference-ktx:1.1.0" - implementation 'com.google.android.material:material:1.1.0' + implementation "androidx.preference:preference-ktx:1.1.1" + implementation 'com.google.android.material:material:1.2.1' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.28.2' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test:runner:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' - androidTestImplementation 'androidx.test:rules:1.2.0' + androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.3.1' //---- ANDROID ARCH ROOM ---- implementation 'android.arch.persistence.room:runtime:1.1.1' @@ -139,15 +143,18 @@ dependencies { //---- ANDROID ARCH LIFECYCLE ---- implementation 'android.arch.lifecycle:common-java8:1.1.1' kapt "android.arch.lifecycle:compiler:1.1.1" - implementation 'android.arch.lifecycle:runtime:1.1.1' - implementation 'android.arch.lifecycle:extensions:1.1.1' + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" + //---- GOOGLE JSON SERIALIZER/DESERIALIZER ---- implementation 'com.google.code.gson:gson:2.8.5' //---- MixPanel ---- implementation 'com.mixpanel.android:mixpanel-android:5.6.1' //---- Firebase ---- - implementation 'com.google.firebase:firebase-core:17.2.2' - implementation 'com.google.firebase:firebase-analytics:17.2.1' + implementation 'com.google.firebase:firebase-core:18.0.0' + implementation 'com.google.firebase:firebase-analytics:18.0.0' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'org.jetbrains.kotlin:kotlin-reflect:1.3.61' //---- Image ---- diff --git a/app/src/main/java/com/rootstrap/android/network/managers/IUserManager.kt b/app/src/main/java/com/rootstrap/android/network/managers/IUserManager.kt new file mode 100644 index 0000000..d30f5a6 --- /dev/null +++ b/app/src/main/java/com/rootstrap/android/network/managers/IUserManager.kt @@ -0,0 +1,11 @@ +package com.rootstrap.android.network.managers + +import com.rootstrap.android.network.models.User +import com.rootstrap.android.network.models.UserSerializer +import com.rootstrap.android.util.extensions.Data + +interface IUserManager { + suspend fun signUp(user: User): Result> + suspend fun signIn(user: User): Result> + suspend fun signOut(): Result> +} diff --git a/app/src/main/java/com/rootstrap/android/network/managers/UserManager.kt b/app/src/main/java/com/rootstrap/android/network/managers/UserManager.kt index f324299..bb9475d 100644 --- a/app/src/main/java/com/rootstrap/android/network/managers/UserManager.kt +++ b/app/src/main/java/com/rootstrap/android/network/managers/UserManager.kt @@ -1,75 +1,31 @@ package com.rootstrap.android.network.managers import androidx.annotation.RestrictTo -import com.rootstrap.android.bus import com.rootstrap.android.network.models.User import com.rootstrap.android.network.models.UserSerializer import com.rootstrap.android.network.providers.ServiceProvider import com.rootstrap.android.network.services.ApiService import com.rootstrap.android.util.extensions.ActionCallback -import retrofit2.Response +import com.rootstrap.android.util.extensions.Data /** * Singleton Object * */ -object UserManager { +class UserManager : IUserManager { private var service = ServiceProvider.create(ApiService::class.java) - fun signUp(user: User) { - val userSerializer = UserSerializer(user) - val signUp = service.signUp(userSerializer) - signUp.enqueue(UserCallback()) - } + override suspend fun signUp(user: User): Result> = + ActionCallback.call(service.signUp(UserSerializer(user))) - fun signIn(user: User) { - val userSerializer = UserSerializer(user) - val signIn = service.signIn(userSerializer) - signIn.enqueue(LogInCallback()) - } + override suspend fun signIn(user: User): Result> = + ActionCallback.call(service.signIn(UserSerializer(user))) - fun signOut() { - val signOut = service.signOut() - signOut.enqueue(LogOutCallback()) - } - - private class UserCallback : ActionCallback() { - override fun responseAction(response: Response) { - super.responseAction(response) - response.body()?.let { - SessionManager.user = it.user - } - bus.post(UserCreatedSuccessfullyEvent()) - } - } - - private class LogInCallback : ActionCallback() { - - override fun responseAction(response: Response) { - super.responseAction(response) - response.body()?.let { - SessionManager.signIn(it.user) - } - bus.post(SignInSuccessfullyEvent()) - } - } - - private class LogOutCallback : ActionCallback() { - - override fun responseAction(response: Response) { - super.responseAction(response) - - SessionManager.signOut() - bus.post(SignedOutSuccessfullyEvent()) - } - } + override suspend fun signOut(): Result> = + ActionCallback.call(service.signOut()) @RestrictTo(RestrictTo.Scope.TESTS) - open fun reloadService(url: String) { + fun reloadService(url: String) { service = ServiceProvider.create(ApiService::class.java, url) } - - class UserCreatedSuccessfullyEvent - class SignInSuccessfullyEvent - class SignedOutSuccessfullyEvent } diff --git a/app/src/main/java/com/rootstrap/android/network/models/User.kt b/app/src/main/java/com/rootstrap/android/network/models/User.kt index 2f490d3..99185e8 100644 --- a/app/src/main/java/com/rootstrap/android/network/models/User.kt +++ b/app/src/main/java/com/rootstrap/android/network/models/User.kt @@ -12,4 +12,4 @@ data class User( @Json(name = "username") val username: String = "" ) -data class UserSerializer(val user: User) +data class UserSerializer(@Json(name = "user") val user: User) diff --git a/app/src/main/java/com/rootstrap/android/ui/activity/main/ProfileActivity.kt b/app/src/main/java/com/rootstrap/android/ui/activity/main/ProfileActivity.kt index 0f8403f..7b24c22 100644 --- a/app/src/main/java/com/rootstrap/android/ui/activity/main/ProfileActivity.kt +++ b/app/src/main/java/com/rootstrap/android/ui/activity/main/ProfileActivity.kt @@ -42,7 +42,7 @@ class ProfileActivity : BaseActivity(), ProfileView { override fun updateState() { when (viewModel.state) { ProfileState.signOutFailure -> showError(viewModel.error) - ProfileState.signedOutSuccessfully -> goToFirstScreen() + ProfileState.signOutSuccessfully -> goToFirstScreen() else -> { } } diff --git a/app/src/main/java/com/rootstrap/android/ui/activity/main/ProfileActivityViewModel.kt b/app/src/main/java/com/rootstrap/android/ui/activity/main/ProfileActivityViewModel.kt index d2a8995..8e38de0 100644 --- a/app/src/main/java/com/rootstrap/android/ui/activity/main/ProfileActivityViewModel.kt +++ b/app/src/main/java/com/rootstrap/android/ui/activity/main/ProfileActivityViewModel.kt @@ -2,53 +2,53 @@ package com.rootstrap.android.ui.activity.main import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.rootstrap.android.network.managers.IUserManager import com.rootstrap.android.network.managers.UserManager import com.rootstrap.android.ui.base.BaseViewModel import com.rootstrap.android.util.NetworkState import com.rootstrap.android.util.ViewModelListener -import com.rootstrap.android.util.extensions.ErrorEvent -import com.rootstrap.android.util.extensions.FailureEvent -import com.squareup.otto.Subscribe +import com.rootstrap.android.util.extensions.ApiErrorType +import com.rootstrap.android.util.extensions.ApiException +import kotlinx.coroutines.launch open class ProfileActivityViewModel(listener: ViewModelListener?) : BaseViewModel(listener) { - private val manager = UserManager + private val manager: IUserManager = UserManager() fun signOut() { networkState = NetworkState.loading - manager.signOut() - } - - var state: ProfileState = ProfileState.none - set(value) { - field = value - listener?.updateState() + viewModelScope.launch { + val result = manager.signOut() + if (result.isSuccess) { + networkState = NetworkState.idle + state = ProfileState.signOutSuccessfully + } else { + manageError(result.exceptionOrNull()) + } } - - @Subscribe - fun signedOutSuccessfully(event: UserManager.SignedOutSuccessfullyEvent) { - networkState = NetworkState.idle - state = ProfileState.signedOutSuccessfully } - @Subscribe - fun signOutError(event: ErrorEvent) { - error = event.error - networkState = NetworkState.idle - networkState = NetworkState.error - } + private fun manageError(exception: Throwable?) { + error = if (exception is ApiException && exception.errorType == ApiErrorType.apiError) { + exception.message + } else null - @Subscribe - fun signOutFailure(event: FailureEvent) { - error = null networkState = NetworkState.idle + networkState = NetworkState.error state = ProfileState.signOutFailure } + + var state: ProfileState = ProfileState.none + set(value) { + field = value + listener?.updateState() + } } enum class ProfileState { signOutFailure, - signedOutSuccessfully, + signOutSuccessfully, none, } diff --git a/app/src/main/java/com/rootstrap/android/ui/activity/main/SignInActivity.kt b/app/src/main/java/com/rootstrap/android/ui/activity/main/SignInActivity.kt index be29db0..2ea67eb 100644 --- a/app/src/main/java/com/rootstrap/android/ui/activity/main/SignInActivity.kt +++ b/app/src/main/java/com/rootstrap/android/ui/activity/main/SignInActivity.kt @@ -52,8 +52,8 @@ class SignInActivity : PermissionActivity(), AuthView { private val viewModelListener = object : ViewModelListener { override fun updateState() { when (viewModel.state) { - SignInState.signedInFailure -> showError(viewModel.error) - SignInState.signedInSuccess -> showProfile() + SignInState.signInFailure -> showError(viewModel.error) + SignInState.signInSuccessfully -> showProfile() else -> { } } diff --git a/app/src/main/java/com/rootstrap/android/ui/activity/main/SignInActivityViewModel.kt b/app/src/main/java/com/rootstrap/android/ui/activity/main/SignInActivityViewModel.kt index 3ea6f64..df138aa 100644 --- a/app/src/main/java/com/rootstrap/android/ui/activity/main/SignInActivityViewModel.kt +++ b/app/src/main/java/com/rootstrap/android/ui/activity/main/SignInActivityViewModel.kt @@ -2,18 +2,21 @@ package com.rootstrap.android.ui.activity.main import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.rootstrap.android.network.managers.IUserManager +import com.rootstrap.android.network.managers.SessionManager import com.rootstrap.android.network.managers.UserManager import com.rootstrap.android.network.models.User import com.rootstrap.android.ui.base.BaseViewModel import com.rootstrap.android.util.NetworkState import com.rootstrap.android.util.ViewModelListener -import com.rootstrap.android.util.extensions.ErrorEvent -import com.rootstrap.android.util.extensions.FailureEvent -import com.squareup.otto.Subscribe +import com.rootstrap.android.util.extensions.ApiErrorType +import com.rootstrap.android.util.extensions.ApiException +import kotlinx.coroutines.launch open class SignInActivityViewModel(listener: ViewModelListener?) : BaseViewModel(listener) { - private val manager = UserManager + private val manager: IUserManager = UserManager() var state: SignInState = SignInState.none set(value) { @@ -23,33 +26,35 @@ open class SignInActivityViewModel(listener: ViewModelListener?) : BaseViewModel fun signIn(user: User) { networkState = NetworkState.loading - manager.signIn(user) - } + viewModelScope.launch { + val result = manager.signIn(user = user) + if (result.isSuccess) { + result.getOrNull()?.value?.user?.let { user -> + SessionManager.signIn(user) + } - @Subscribe - fun signedInSuccessfully(event: UserManager.SignInSuccessfullyEvent) { - networkState = NetworkState.idle - state = SignInState.signedInSuccess + networkState = NetworkState.idle + state = SignInState.signInSuccessfully + } else { + manageError(result.exceptionOrNull()) + } + } } - @Subscribe - fun signedInError(event: ErrorEvent) { - error = event.error - networkState = NetworkState.idle - networkState = NetworkState.error - } + private fun manageError(exception: Throwable?) { + error = if (exception is ApiException && exception.errorType == ApiErrorType.apiError) { + exception.message + } else null - @Subscribe - fun signedInFailure(event: FailureEvent) { - error = null networkState = NetworkState.idle - state = SignInState.signedInFailure + networkState = NetworkState.error + state = SignInState.signInFailure } } enum class SignInState { - signedInFailure, - signedInSuccess, + signInFailure, + signInSuccessfully, none, } diff --git a/app/src/main/java/com/rootstrap/android/ui/activity/main/SignUpActivity.kt b/app/src/main/java/com/rootstrap/android/ui/activity/main/SignUpActivity.kt index 627bec9..ad1dabc 100644 --- a/app/src/main/java/com/rootstrap/android/ui/activity/main/SignUpActivity.kt +++ b/app/src/main/java/com/rootstrap/android/ui/activity/main/SignUpActivity.kt @@ -52,8 +52,8 @@ class SignUpActivity : BaseActivity(), AuthView { private val viewModelListener = object : ViewModelListener { override fun updateState() { when (viewModel.state) { - SignUpState.signedUpFailure -> showError(viewModel.error) - SignUpState.signedUpSuccess -> showProfile() + SignUpState.signUpFailure -> showError(viewModel.error) + SignUpState.signUpSuccessfully -> showProfile() else -> { } } diff --git a/app/src/main/java/com/rootstrap/android/ui/activity/main/SignUpActivityViewModel.kt b/app/src/main/java/com/rootstrap/android/ui/activity/main/SignUpActivityViewModel.kt index 9545e63..ebe198e 100644 --- a/app/src/main/java/com/rootstrap/android/ui/activity/main/SignUpActivityViewModel.kt +++ b/app/src/main/java/com/rootstrap/android/ui/activity/main/SignUpActivityViewModel.kt @@ -2,18 +2,21 @@ package com.rootstrap.android.ui.activity.main import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.rootstrap.android.network.managers.IUserManager +import com.rootstrap.android.network.managers.SessionManager import com.rootstrap.android.network.managers.UserManager import com.rootstrap.android.network.models.User import com.rootstrap.android.ui.base.BaseViewModel import com.rootstrap.android.util.NetworkState import com.rootstrap.android.util.ViewModelListener -import com.rootstrap.android.util.extensions.ErrorEvent -import com.rootstrap.android.util.extensions.FailureEvent -import com.squareup.otto.Subscribe +import com.rootstrap.android.util.extensions.ApiErrorType +import com.rootstrap.android.util.extensions.ApiException +import kotlinx.coroutines.launch open class SignUpActivityViewModel(listener: ViewModelListener?) : BaseViewModel(listener) { - private val manager = UserManager + private val manager: IUserManager = UserManager() var state: SignUpState = SignUpState.none set(value) { @@ -23,33 +26,36 @@ open class SignUpActivityViewModel(listener: ViewModelListener?) : BaseViewModel fun signUp(user: User) { networkState = NetworkState.loading - manager.signUp(user) - } + viewModelScope.launch { + val result = manager.signUp(user = user) - @Subscribe - fun signedUpSuccessfully(event: UserManager.UserCreatedSuccessfullyEvent) { - networkState = NetworkState.idle - state = SignUpState.signedUpSuccess - } + if (result.isSuccess) { + result.getOrNull()?.value?.user?.let { user -> + SessionManager.signIn(user) + } - @Subscribe - fun signedUpError(event: ErrorEvent) { - error = event.error - networkState = NetworkState.idle - networkState = NetworkState.error + networkState = NetworkState.idle + state = SignUpState.signUpSuccessfully + } else { + manageError(result.exceptionOrNull()) + } + } } - @Subscribe - fun signedUpFailure(event: FailureEvent) { - error = null + private fun manageError(exception: Throwable?) { + error = if (exception is ApiException && exception.errorType == ApiErrorType.apiError) { + exception.message + } else null + networkState = NetworkState.idle - state = SignUpState.signedUpFailure + networkState = NetworkState.error + state = SignUpState.signUpFailure } } enum class SignUpState { - signedUpFailure, - signedUpSuccess, + signUpFailure, + signUpSuccessfully, none, } diff --git a/app/src/main/java/com/rootstrap/android/util/extensions/ActionCallback.kt b/app/src/main/java/com/rootstrap/android/util/extensions/ActionCallback.kt index 175ee6a..ef22a27 100644 --- a/app/src/main/java/com/rootstrap/android/util/extensions/ActionCallback.kt +++ b/app/src/main/java/com/rootstrap/android/util/extensions/ActionCallback.kt @@ -1,44 +1,57 @@ package com.rootstrap.android.util.extensions import com.google.gson.Gson -import com.rootstrap.android.bus import com.rootstrap.android.network.models.ErrorModel -import com.rootstrap.android.util.ErrorUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import retrofit2.Call -import retrofit2.Callback import retrofit2.Response -abstract class ActionCallback : Callback { +class ActionCallback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - responseAction(response) - } else { - errorAction(response) - } - } + companion object { - override fun onFailure(call: Call, t: Throwable) { - failureAction(t) - } - - open fun responseAction(response: Response) {} + suspend fun call(apiCall: Call): Result> = + withContext(Dispatchers.IO) { + val response = apiCall.execute() + handleResponse(response) + } - open fun errorAction(response: Response) { - try { - response.errorBody()?.let { - val error = Gson().fromJson(it.charStream(), ErrorModel::class.java) - bus.post(ErrorEvent(ErrorUtil.handleCustomError(error))) + private fun handleResponse(response: Response): Result> { + if (response.isSuccessful) { + return Result.success( + Data(response.body()) + ) + } else { + try { + response.errorBody()?.let { + val apiError = Gson().fromJson(it.charStream(), ErrorModel::class.java) + return Result.failure( + ApiException( + errorMessage = apiError.error + ) + ) + } + } catch (ignore: Exception) { + } } - } catch (e: Exception) { - bus.post(FailureEvent()) + + return Result.failure(ApiException(errorType = ApiErrorType.unknownError)) } } +} - open fun failureAction(throwable: Throwable?) { - bus.post(FailureEvent()) - } +class Data(val value: T?) + +class ApiException( + private val errorMessage: String? = null, + val errorType: ApiErrorType = ApiErrorType.apiError +) : java.lang.Exception() { + override val message: String? + get() = errorMessage } -class ErrorEvent(val error: String) -class FailureEvent +enum class ApiErrorType { + apiError, + unknownError +}