Skip to content

Instantly share code, notes, and snippets.

@IlyaGulya
Created December 29, 2018 04:18
Show Gist options
  • Save IlyaGulya/ec294786c6286f2f153170626b35d651 to your computer and use it in GitHub Desktop.
Save IlyaGulya/ec294786c6286f2f153170626b35d651 to your computer and use it in GitHub Desktop.
Android code sample
package com.app.feature.editprofile.di
import com.app.feature.editprofile.presenter.EditProfilePresenter
import com.app.feature.editprofile.di.EditProfileScope
@EditProfileScope
interface EditProfileComponent {
val editProfilePresenter: EditProfilePresenter
}
package com.app.feature.editprofile.interactor
import android.net.Uri
import com.app.manager.usermanager.AuthStatus
import com.app.network.model.Country
import com.app.network.response.auth.AuthInfo
import io.reactivex.Single
interface EditProfileInteractor {
fun onCountriesAvailable(): Single<List<Country>>
fun getCurrentAuthStatus(): AuthStatus
fun modifyUserInfo(modifier: (AuthInfo) -> Unit)
fun saveUserData(): Single<UserEditResult>
fun setCurrentPassword(password: String)
fun setNewPassword(password: String)
fun setNewPasswordAgain(password: String)
fun onAvatarPicked(image: Uri)
}
package com.app.feature.editprofile.interactor
import android.net.Uri
import com.b.basetools.util.Utils
import com.app.exception.NothingChangedException
import com.app.exception.PasswordNotGivenException
import com.app.exception.PasswordsAreNotEqual
import com.app.manager.usermanager.AuthStatus
import com.app.manager.usermanager.LoggedInStatus
import com.app.manager.usermanager.UserManager
import com.app.network.AppApi
import com.app.network.model.Country
import com.app.network.request.edit.EditUserRequest
import com.app.network.request.edit.User
import com.app.network.response.auth.AuthInfo
import com.app.source.country.CountrySource
import com.app.utils.appContext
import com.app.feature.editprofile.interactor.UserEditResult
import com.app.network.request.edit.EditUserRequest
import com.book2read.network.request.edit.EditUserRequest
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
class EditProfileInteractorImpl constructor(private val api: AppApi,
private val countrySource: CountrySource,
private val userManager: UserManager) : EditProfileInteractor {
private var curUserInfo: AuthInfo = (getCurrentAuthStatus() as LoggedInStatus).auth
private var editableUserInfo: AuthInfo = curUserInfo.copy()
private var newAvatar: Uri? = null
private var curPassword: String? = null
private var newPassword: String? = null
private var newPasswordAgain: String = ""
override fun onCountriesAvailable(): Single<List<Country>> = countrySource.getDataCached()
override fun getCurrentAuthStatus(): AuthStatus = userManager.getCurrentAuthStatus()
override fun modifyUserInfo(modifier: (AuthInfo) -> Unit) {
modifier(editableUserInfo)
}
override fun saveUserData(): Single<UserEditResult> {
val userDataToSave = editableUserInfo
val isNeedToPresentPassword = curUserInfo.showPassField && (!newPassword.isNullOrBlank() || curUserInfo.mail != editableUserInfo.mail)
val passwordIsPresented = !curPassword.isNullOrBlank()
return if (isNeedToPresentPassword && !passwordIsPresented) {
Single.error(PasswordNotGivenException())
} else {
val userWantToChangePassword = !newPassword.isNullOrBlank()
if (userWantToChangePassword) {
val passwordsAreEqual = newPassword == newPasswordAgain
if (passwordsAreEqual) {
editValidData(userDataToSave)
} else {
Single.error(PasswordsAreNotEqual())
}
} else {
val isSomethingChanged = isSomethingChanged()
return when {
isSomethingChanged -> editValidData(userDataToSave)
else -> Single.error(NothingChangedException())
}
}
}
}
private fun isSomethingChanged(): Boolean {
return curUserInfo != editableUserInfo || newAvatar != null
}
private fun editValidData(userInfo: AuthInfo): Single<UserEditResult> {
return prepareEditRequest(userInfo)
.flatMap { api.editUserData(it) }
.flatMap { api.userInfo() }
.map { UserEditResult(it.authInfo, !newPassword.isNullOrBlank()) }
.doOnSuccess { editResult ->
val updated = editResult.userInfo
curUserInfo = updated
editableUserInfo = curUserInfo.copy()
if (editResult.passwordWasChanged) {
dispose()
}
userManager.notifyUserChanged(updated)
}
}
private fun prepareEditRequest(userInfo: AuthInfo): Single<EditUserRequest> {
return if (newAvatar == null) {
Single.just(User(userInfo.mail, curPassword, newPassword, userInfo.country))
} else {
Single.just(newAvatar)
.observeOn(Schedulers.computation())
.map {
val avatar = Utils.createFileBody(appContext(), it)
User(userInfo.mail, curPassword, newPassword, userInfo.country, avatar)
}
}.map(::EditUserRequest)
}
override fun setCurrentPassword(password: String) {
curPassword = password
}
override fun setNewPassword(password: String) {
newPassword = password
}
override fun setNewPasswordAgain(password: String) {
newPasswordAgain = password
}
override fun onAvatarPicked(image: Uri) {
newAvatar = image
}
}
package com.app.feature.editprofile.di
import com.app.feature.editprofile.interactor.EditProfileInteractor
import com.app.feature.editprofile.interactor.EditProfileInteractorImpl
import com.app.manager.usermanager.UserManager
import com.app.network.AppApi
import com.app.source.country.CountrySource
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class EditProfileModule {
@Provides
@Singleton
fun provideEditProfileInteractor(apiManager: AppApi, countrySource: CountrySource, userManager: UserManager): EditProfileInteractor {
return EditProfileInteractorImpl(apiManager, countrySource, userManager)
}
}
package com.app.feature.editprofile.presenter
import android.net.Uri
import com.arellomobile.mvp.InjectViewState
import com.b.basetools.component.BasePresenter
import com.app.R
import com.app.feature.editprofile.interactor.EditProfileInteractor
import com.app.feature.editprofile.interactor.UserEditResult
import com.app.feature.editprofile.view.EditProfileView
import com.app.manager.usermanager.LoggedInStatus
import com.app.network.model.Country
import com.app.network.response.auth.AuthInfo
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import timber.log.Timber
import javax.inject.Inject
@InjectViewState
class EditProfilePresenter @Inject constructor(private val interactor: EditProfileInteractor) : BasePresenter<EditProfileView>() {
private var userData: AuthInfo = (interactor.getCurrentAuthStatus() as LoggedInStatus).auth
override fun onFirstViewAttach() {
super.onFirstViewAttach()
subscribeToScreenData().track()
viewState.setProgressVisible(true)
viewState.setInterfaceEnabled(false)
}
private fun subscribeToScreenData(): Disposable {
return interactor.onCountriesAvailable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(::onScreenDataReady, Timber::e)
}
private fun onScreenDataReady(countries: List<Country>) {
val selectedCountry = countries.firstOrNull { it.isoCode.toLowerCase() == userData.country.toLowerCase() } ?: countries.first()
with(viewState) {
setCountries(countries, selectedCountry)
setUserInfo(userData)
setProgressVisible(false)
setInterfaceEnabled(true)
}
}
fun onEditPhotoClick() {
viewState.goToPickImage()
}
fun onImagePicked(image: Uri) {
interactor.onAvatarPicked(image)
viewState.setPortrait(image)
}
fun onImagePickFailed(err: Throwable) {
viewState.showError(err)
}
fun onSaveClick() {
save()
}
fun onPasswordForgot() {
viewState.goRestorePassword()
}
private fun save() {
viewState.setInterfaceEnabled(false)
viewState.setProgressVisible(true)
interactor.saveUserData()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(::onSaveDataSuccess, ::onSaveDataFailed).track()
}
private fun onSaveDataSuccess(userEditResult: UserEditResult) {
viewState.notify(R.string.saved)
if (userEditResult.passwordWasChanged) {
viewState.clearPasswordFields()
}
onSaveDataComplete()
}
private fun onSaveDataFailed(err: Throwable) {
viewState.notify(err.toString())
onSaveDataComplete()
}
private fun onSaveDataComplete() {
viewState.setInterfaceEnabled(true)
viewState.setProgressVisible(false)
}
fun onCountrySelected(country: Country) {
interactor.modifyUserInfo { userInfo -> userInfo.country = country.isoCode }
}
fun onEmailChanged(email: CharSequence) {
interactor.modifyUserInfo { userInfo -> userInfo.mail = email.toString().trim() }
}
fun onCurrentPasswordChanged(password: CharSequence) {
interactor.setCurrentPassword(password.toString().trim())
}
fun onNewPasswordChanged(password: CharSequence) {
interactor.setNewPassword(password.toString().trim())
}
fun onNewPasswordRepeatChanged(password: CharSequence) {
interactor.setNewPasswordAgain(password.toString().trim())
}
}
package com.app.feature.editprofile.view
import android.net.Uri
import android.support.annotation.StringRes
import com.arellomobile.mvp.MvpView
import com.arellomobile.mvp.viewstate.strategy.AddToEndSingleStrategy
import com.arellomobile.mvp.viewstate.strategy.SkipStrategy
import com.arellomobile.mvp.viewstate.strategy.StateStrategyType
import com.app.network.model.Country
import com.app.network.response.auth.AuthInfo
@StateStrategyType(AddToEndSingleStrategy::class)
interface EditProfileView : MvpView {
@StateStrategyType(SkipStrategy::class)
fun goToPickImage()
fun setPortrait(image: Uri)
@StateStrategyType(SkipStrategy::class)
fun showError(err: Throwable)
fun setCountries(countries: List<Country>, selectedCountry: Country)
fun setUserInfo(authInfo: AuthInfo)
fun notify(@StringRes messageResource: Int)
fun notify(message: String)
fun setProgressVisible(visible: Boolean)
fun setInterfaceEnabled(enabled: Boolean)
fun clearPasswordFields()
@StateStrategyType(SkipStrategy::class)
fun goRestorePassword(email: String = "")
}
package com.app.feature.editprofile.view
import android.content.Context
import android.content.Intent
import android.databinding.ObservableBoolean
import android.net.Uri
import android.os.Bundle
import android.support.v4.app.ActivityCompat.invalidateOptionsMenu
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import com.arellomobile.mvp.presenter.InjectPresenter
import com.arellomobile.mvp.presenter.ProvidePresenter
import com.b.basetools.binding.ObservableString
import com.b.basetools.util.Utils
import com.app.AppApplication
import com.app.R
import com.app.component.AppBaseActivity
import com.app.databinding.ViewEditProfileBinding
import com.app.feature.editprofile.presenter.EditProfilePresenter
import com.app.feature.registration.requestnewpassword.view.RequestNewPasswordVm
import com.app.network.model.Country
import com.app.network.response.auth.AuthInfo
import com.app.utils.AppUtils
import com.app.utils.dialog
import com.app.utils.portraitloader.PortraitLoader
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CircleCrop
import com.bumptech.glide.request.RequestOptions
class EditProfileVm : AppBaseActivity<ViewEditProfileBinding>(), EditProfileView {
override val layoutId: Int = R.layout.view_edit_profile
val oProgressVisible = ObservableBoolean()
val oInterfaceEnabled = ObservableBoolean()
val oName = ObservableString()
val oEmail = ObservableString()
val oPassFieldVisible = ObservableBoolean()
@InjectPresenter
lateinit var presenter: EditProfilePresenter
@ProvidePresenter
fun providePresenter(): EditProfilePresenter {
return AppApplication.mainComponent.editProfileComponent.editProfilePresenter
}
override fun init(savedInstanceState: Bundle?) {
binding.vm = this
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.view_edit_profile, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.save -> presenter.onSaveClick()
}
return true
}
override fun goToPickImage() {
PortraitLoader.goPickImage(this)
.subscribe(presenter::onImagePicked, presenter::onImagePickFailed).track()
}
override fun setPortrait(image: Uri) {
Glide.with(this)
.load(image)
.apply(RequestOptions().transforms(CircleCrop()))
.into(binding.portrait)
}
override fun setCountries(countries: List<Country>, selectedCountry: Country) {
val selectionIndex = countries.indexOf(selectedCountry)
val countryNames = countries.map { it.displayName }
binding.countrySelector.adapter = ArrayAdapter<String>(this, R.layout.spinner_list_item, countryNames)
binding.countrySelector.setSelection(selectionIndex)
binding.countrySelector.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(adapter: AdapterView<*>, view: View, position: Int, id: Long) {
presenter.onCountrySelected(countries[position])
}
override fun onNothingSelected(adapter: AdapterView<*>) {}
}
}
override fun setUserInfo(authInfo: AuthInfo) {
oName.set(authInfo.name)
oEmail.set(authInfo.validEmail)
oPassFieldVisible.set(authInfo.showPassField)
Glide.with(this)
.load(authInfo.image)
.apply(AppUtils.getAvatarRequestOptions())
.into(binding.portrait)
}
override fun notify(messageResource: Int) {
notify(getString(messageResource))
}
override fun notify(message: String) {
Utils.dialog(this, message)
}
override fun setProgressVisible(visible: Boolean) {
oProgressVisible.set(visible)
}
override fun setInterfaceEnabled(enabled: Boolean) {
oInterfaceEnabled.set(enabled)
invalidateOptionsMenu()
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
menu.findItem(R.id.save).isEnabled = oInterfaceEnabled.get()
return true
}
override fun clearPasswordFields() {
binding.currentPassword.setText("")
binding.newPassword.setText("")
binding.confirmNewPassword.setText("")
}
override fun goRestorePassword(email: String) {
RequestNewPasswordVm.newInstance(email)
.show(this)
}
override fun showError(err: Throwable) {
dialog(this, err)
}
companion object {
fun with(ctx: Context) = Intent(ctx, EditProfileVm::class.java)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment