Skip to content

Instantly share code, notes, and snippets.

@CyxouD
Last active April 8, 2019 11:58
Show Gist options
  • Save CyxouD/f85dd37f2ad5fa45c01e24d29a482726 to your computer and use it in GitHub Desktop.
Save CyxouD/f85dd37f2ad5fa45c01e24d29a482726 to your computer and use it in GitHub Desktop.
Model-View-Presenter+Repository architecture example
//contract which declare action of Presenter and MVP View
interface AddEditCardContract {
interface Presenter : BasePresenter {
fun getCreditCardLogo(creditCardNumber: String)
fun validateCreditCardNumber(creditCardNumber: String)
fun validateCreditCardHolder(creditCardHolder: String)
fun validateCreditCardExpiryDate(creditExpiryDate: String)
fun validateCreditCardCVV(creditCVV: String)
fun validateCreditCardTypeAndPriority(creditCardType: String, creditCardPriority: String)
fun saveCreditCard(number: String,
holderName: String,
expiryDate: String,
cvv: String,
type: CardType,
isAirplus: Boolean,
isPrimary: Boolean)
}
interface View : BaseView<Presenter> {
fun showCreditCardLogo(creditCardEnum: CreditCardEnum)
fun showNoCreditCardLogo()
fun showCreditCardNumberValidatedSuccessfully()
fun showCreditCardNumberFailedToValidate()
fun showCreditCardHolderValidatedSuccessfully()
fun showCreditCardHolderFailedToValidate()
fun showCreditCardExpiryDateValidatedSuccessfully()
fun showCreditCardExpiryDateFailedToValidate()
fun showCreditCardCvvValidatedSuccessfully()
fun showCreditCardCvvFailedToValidate()
fun showCreditCardPriorityAndTypeValidatedSuccessfully()
fun showCreditCardPriorityIsEmpty()
fun showCreditCardTypeIsEmpty()
fun showCreditCardSavedSuccessfully()
fun showCreditCardFailedToSave()
}
enum class CardType {
PERSONAL, BUSINESS
}
}
//MVP View
class AddEditCardFragment : BaseFragment<AddEditCardContract.Presenter>(), AddEditCardContract.View {
// ...Android lifecycle methods
override fun setPresenter(presenter: AddEditCardContract.Presenter) {
mPresenter = presenter
}
override fun showCreditCardLogo(creditCardEnum: CreditCardEnum) {
val logoDrawable = ContextCompat.getDrawable(context, creditCardEnum.cardDrawable!!)
val gradientDrawable = ContextCompat.getDrawable(context, creditCardEnum.gradientDrawable!!)
flipBetweenActiveFrontAndInactiveAppearance(logoDrawable,
gradientDrawable,
toActive = true)
}
override fun showNoCreditCardLogo() {
flipBetweenActiveFrontAndInactiveAppearance(null,
null,
toActive = false)
}
override fun showCreditCardNumberValidatedSuccessfully() {
stateMachine.nextState()
}
override fun showCreditCardNumberFailedToValidate() {
context.toast(R.string.credit_card_number_is_not_valid).show()
}
override fun showCreditCardHolderValidatedSuccessfully() {
stateMachine.nextState()
}
override fun showCreditCardHolderFailedToValidate() {
context.toast(R.string.credit_card_holder_is_not_valid).show()
}
override fun showCreditCardExpiryDateValidatedSuccessfully() {
stateMachine.nextState()
}
override fun showCreditCardExpiryDateFailedToValidate() {
context.toast(R.string.credit_card_expiry_date_is_not_valid).show()
}
override fun showCreditCardCvvValidatedSuccessfully() {
stateMachine.nextState()
}
override fun showCreditCardCvvFailedToValidate() {
context.toast(R.string.credit_card_cvv_is_not_valid).show()
}
override fun showCreditCardSavedSuccessfully() {
stateMachine.nextState()
(activity as AppCompatActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false)
}
override fun showCreditCardFailedToSave() {
save_card_container.showNext()
context.toast(R.string.credit_card_failed_to_save).show()
}
override fun showCreditCardPriorityAndTypeValidatedSuccessfully() {
save()
}
override fun showCreditCardPriorityIsEmpty() {
context.toast(R.string.credit_card_priority_is_empty).show()
}
override fun showCreditCardTypeIsEmpty() {
context.toast(R.string.credit_card_type_is_empty).show()
}
}
class AddEditCardPresenter(override val repository: DataSource,
val view: AddEditCardContract.View,
override val schedulerProvider: BaseSchedulerProvider,
override val mCompositeDisposable: CompositeDisposable = CompositeDisposable()) : AddEditCardContract.Presenter {
init {
view.setPresenter(this)
}
override fun subscribe() {
}
override fun getCreditCardLogo(creditCardNumber: String) {
if (creditCardNumber.contains("*")) return
val creditCardEnum = CreditCardEnum.getCreditCardByNumber(creditCardNumber)
if (creditCardEnum != CreditCardEnum.UNKNOWN) {
view.showCreditCardLogo(creditCardEnum)
} else {
view.showNoCreditCardLogo()
}
}
override fun validateCreditCardNumber(creditCardNumber: String) {
val creditCardEnum = CreditCardEnum.getCreditCardByNumber(creditCardNumber)
if (creditCardEnum != CreditCardEnum.UNKNOWN) {
view.showCreditCardNumberValidatedSuccessfully()
} else {
view.showCreditCardNumberFailedToValidate()
}
}
override fun validateCreditCardHolder(creditCardHolder: String) {
val validCardHolder = Pattern.compile("^((?:[A-Za-z]+ ?){1,3})$").matcher(creditCardHolder).matches()
if (validCardHolder) {
view.showCreditCardHolderValidatedSuccessfully()
} else {
view.showCreditCardHolderFailedToValidate()
}
}
override fun validateCreditCardExpiryDate(creditExpiryDate: String) {
//TODO also validate if year and month is not more than current date
val validExpiryDate = Pattern.compile("^(0[1-9]|1[0-2])/[0-9]{2}$").matcher(creditExpiryDate).matches()
if (validExpiryDate) {
view.showCreditCardExpiryDateValidatedSuccessfully()
} else {
view.showCreditCardExpiryDateFailedToValidate()
}
}
override fun validateCreditCardCVV(creditCVV: String) {
val validCVV = Pattern.compile("^[0-9]{3,4}$").matcher(creditCVV).matches()
if (validCVV) {
view.showCreditCardCvvValidatedSuccessfully()
} else {
view.showCreditCardCvvFailedToValidate()
}
}
override fun validateCreditCardTypeAndPriority(creditCardType: String, creditCardPriority: String) {
val creditCardTypeValid = creditCardType.isNotEmpty()
val creditCardPriorityValid = creditCardPriority.isNotEmpty()
if (!creditCardPriorityValid) {
view.showCreditCardPriorityIsEmpty()
}
if (!creditCardTypeValid) {
view.showCreditCardTypeIsEmpty()
}
if (creditCardPriorityValid && creditCardTypeValid) {
view.showCreditCardPriorityAndTypeValidatedSuccessfully()
}
}
override fun saveCreditCard(number: String,
holderName: String,
expiryDate: String,
cvv: String,
type: AddEditCardContract.CardType,
isAirplus: Boolean,
isPrimary: Boolean) {
val creditCard = CreditCard(number = number.filter { !it.isWhitespace() },
holderName = holderName,
company = if (isAirplus) CreditCardEnum.AIRPLUS.naming else null,
cvc = cvv,
expiryDate = expiryDate.replace("/", "-"),//stripe format
type = when (type) {
PERSONAL -> "Personal"
BUSINESS -> "Business"
},
isPrimary = isPrimary
)
repository.saveCreditCards(listOf(creditCard))
.subscribeOn(schedulerProvider.io())
.observeOn(schedulerProvider.ui())
.subscribeWith(object : CompletableObserverWrapper<AddEditCardPresenter>(view) {
override fun handleError(e: Throwable): Boolean {
this@AddEditCardPresenter.view.showCreditCardFailedToSave()
return false
}
override fun onComplete() {
this@AddEditCardPresenter.view.showCreditCardSavedSuccessfully()
}
}).let { mCompositeDisposable.add(it) }
}
override fun removeAll() {
Utils.removeAll(repository, view, schedulerProvider, mCompositeDisposable)
}
}
open class LocalDataSource private constructor() : DataSource, AnkoLogger {
private val authorizeTokenAlias = "authorizeTokenAlias"
private val conversationIdAlias = "conversationIdAlias"
companion object {
private var INSTANCE: LocalDataSource? = null
fun getInstance(): LocalDataSource {
if (INSTANCE == null) {
INSTANCE = LocalDataSource()
}
return INSTANCE!!
}
}
//... some other methods
override fun getToken(): Flowable<Optional<String>> {
val secureStorage = RxSecureStorage.create(App.context, authorizeTokenAlias)
return secureStorage
.getString(authorizeTokenAlias)
.toFlowable(BackpressureStrategy.BUFFER)
}
override fun getConversationId(): Flowable<Optional<String>> {
val secureStorage = RxSecureStorage.create(App.context, conversationIdAlias)
return secureStorage.getString(conversationIdAlias)
.toFlowable(BackpressureStrategy.BUFFER)
}
override fun saveToken(token: String): Single<Boolean> {
val secureStorage = RxSecureStorage.create(App.context, authorizeTokenAlias)
return secureStorage
.putString(authorizeTokenAlias, token)
}
override fun removeAll(): Single<Boolean> {
val deleteRealmSingle = Single.create<Boolean> { emitter ->
try {
Realm.getDefaultInstance().use { realm ->
realm.executeTransaction(Realm::deleteAll)
emitter.onSuccess(true)
}
} catch (e: Exception) {
emitter.onSuccess(false)
}
}
return Single.merge(removeToken(), removeConversationId(), deleteRealmSingle)
.all { deleted -> deleted }
}
override fun removeToken(): Single<Boolean> {
return removeFromRxSecureStorage(authorizeTokenAlias)
}
override fun removeConversationId(): Single<Boolean> {
return removeFromRxSecureStorage(conversationIdAlias)
}
override fun saveConversationId(conversationId: String): Single<Boolean> {
val secureStorage = RxSecureStorage.create(App.context, conversationIdAlias)
return secureStorage
.putString(conversationIdAlias, conversationId)
}
private fun removeFromRxSecureStorage(alias: String): Single<Boolean> {
val secureStorage = RxSecureStorage.create(App.context, alias)
return secureStorage.putString(alias, null)
.map { notPut -> !notPut } //reverse boolean to show true like it successfully removed
}
//... some other methods
}
open class RemoteDataSource private constructor() : DataSource {
private val service by lazy {
RetrofitService.create()
}
companion object {
private var INSTANCE: RemoteDataSource? = null
fun getInstance(): RemoteDataSource {
if (INSTANCE == null) {
INSTANCE = RemoteDataSource()
}
return INSTANCE!!
}
}
//... some other methods
override fun authorize(credentials: Credentials): Flowable<String> {
return service.authenticate(credentials.email!!, credentials.password!!).map { it.token }
}
override fun getLayerIdentityToken(identityTokenBody: RetrofitService.IdentityTokenBody): Flowable<String> {
return service.getIdentityToken(identityTokenBody)
.map { identityToken -> identityToken.identityToken }
}
override fun getConversationId(): Flowable<Optional<String>> {
return service.getConversationId()
.map { conversationId -> conversationId.conversationId.toOptional() }
}
override fun selectWidget(model: Model, id: Int): Completable {
return when (model) {
Model.FLIGHT -> service.selectFlightWidget(id)
Model.HOTEL -> service.selectHotelWidget(id)
Model.CAR -> service.selectCarWidget(id)
Model.TRAIN -> service.selectTrainWidget(id)
}
}
override fun selectWidgetWithCF(model: Model, optionId: Int, taskId: Int, state: String): Completable {
val selectCfBody = SelectCFBody(optionId, taskId, state)
return when (model) {
Model.FLIGHT -> service.selectFlightWidgetWithCF(selectCfBody)
Model.HOTEL -> service.selectHotelWidgetWithCF(selectCfBody)
Model.CAR -> service.selectCarWidgetWithCF(selectCfBody)
Model.TRAIN -> service.selectTrainWidgetWithCF(selectCfBody)
}
}
override fun resetPassword(email: String): Completable {
return service.resetPassword(RetrofitService.Email(email))
}
override fun saveProfile(profile: Profile): Completable {
return service.saveProfile(profile)
}
override fun getSettings(): Single<Optional<UserSettings>> {
return service.getSettings().map { it.toOptional() }
}
override fun saveAccount(account: UserAccount): Completable {
return service.saveAccount(account)
}
override fun changePassword(newPassword: RetrofitService.NewPassword): Completable {
return service.changePassword(newPassword)
}
override fun saveBusinessAddress(address: Address): Completable {
return service.saveBusinessAddress(address)
}
//... some other methods
}
open class Repository private constructor(
remoteDataSource: DataSource,
localDataSource: DataSource)
: DataSource {
private val mRemoteDataSource = remoteDataSource
private val mLocalDataSource = localDataSource
companion object {
private var INSTANCE: Repository? = null
fun getInstance(remoteDataSource: DataSource, localDataSource: DataSource): Repository {
if (INSTANCE == null) {
INSTANCE = Repository(remoteDataSource, localDataSource)
}
return INSTANCE!!
}
/**
* Used to force {@link #getInstance(TasksDataSource, TasksDataSource)} to create a new instance
* next time it's called. IMPORTANT FOR TESTS
*/
@VisibleForTesting
fun destroyInstance() {
INSTANCE = null
}
}
//... some other methods
override fun getToken(): Flowable<Optional<String>> {
return mLocalDataSource.getToken()
}
override fun authorize(credentials: Credentials): Flowable<String> {
return mRemoteDataSource.authorize(credentials)
.doOnNext { token ->
saveToken(token).subscribe()
}
}
override fun getConversationId(): Flowable<Optional<String>> {
//TODO rewrite to pass exception, @see getCreditCards()
val localConversationId = mLocalDataSource.getConversationId()
val remoteConversationId = mRemoteDataSource.getConversationId()
.doOnNext { conversationId ->
mLocalDataSource.saveConversationId(conversationId.toNullable()!!).subscribe()
}.onErrorReturn { null.toOptional() }
return Flowable.zip(localConversationId, remoteConversationId,
BiFunction<Optional<String>, Optional<String>, Optional<String>> { local, remote ->
remote.toNullable()?.let { remote } ?: local
})
}
override fun getLayerIdentityToken(identityTokenBody: DevGetVoilaService.IdentityTokenBody): Flowable<String> {
return mRemoteDataSource.getLayerIdentityToken(identityTokenBody)
}
override fun saveToken(token: String): Single<Boolean> {
return mLocalDataSource.saveToken(token)
}
override fun removeToken(): Single<Boolean> {
return mLocalDataSource.removeToken()
}
override fun selectWidget(model: Model, id: Int): Completable {
return mRemoteDataSource.selectWidget(model, id)
}
override fun selectWidgetWithCF(model: Model, optionId: Int, taskId: Int, state: String): Completable {
return mRemoteDataSource.selectWidgetWithCF(model, optionId, taskId, state)
}
override fun removeAll(): Single<Boolean> {
return mLocalDataSource.removeAll()
}
override fun removeConversationId(): Single<Boolean> {
return mLocalDataSource.removeConversationId()
}
override fun saveConversationId(conversationId: String): Single<Boolean> {
return mLocalDataSource.saveConversationId(conversationId)
}
override fun resetPassword(email: String): Completable {
return mRemoteDataSource.resetPassword(email)
}
override fun saveProfile(profile: Profile): Completable {
return mRemoteDataSource.saveProfile(profile)
.andThen(mLocalDataSource.saveProfile(profile))
}
override fun getProfile(): Single<Profile> {
return getSettings().map { ObjectMapper.mapToProfile(it.toNullable()) }
}
override fun saveSettings(settings: UserSettings): Completable {
return mLocalDataSource.saveSettings(settings)
}
override fun saveAccount(account: UserAccount): Completable {
return mRemoteDataSource.saveAccount(account)
.andThen(mLocalDataSource.saveAccount(account))
}
//... some other methods
}
//Retrofit service
interface RetrofitService {
companion object {
fun create(): DevGetVoilaService {
val clientBuilder = OkHttpClient.Builder().apply {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
addInterceptor(loggingInterceptor)
addInterceptor(AuthenticationInterceptor())
addInterceptor(AuditInterceptor())
}
val retrofit = Retrofit.Builder()
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(clientBuilder.build())
.baseUrl(BuildConfig.API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(DevGetVoilaService::class.java)
}
private class AuthenticationInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
var response: Response = chain.proceed(originalRequest)
LocalDataSource.getInstance().getToken()
.subscribe(
{ token ->
val newRequest = originalRequest.newBuilder()
.header(Header.TOKEN.headerName, token.toNullable())
.build()
response = chain.proceed(newRequest)
},
{ response = response } //if error, then there is no token, so use the original response
)
return response
}
}
private class AuditInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val newRequest = originalRequest.newBuilder()
.header(Header.APP_PLATFORM.headerName, "android")
.header(Header.APP_VERSION.headerName, BuildConfig.VERSION_CODE.toString())
.build()
return chain.proceed(newRequest)
}
}
}
//... some other methods
@FormUrlEncoded
@POST("authenticate/")
fun authenticate(@Field("username") username: String,
@Field("password") password: String):
Flowable<AuthenticateResult>
@POST("sessions/conversation/v2/")
fun getConversationId(): Flowable<ConversationId>
@POST("layer/identity/")
fun getIdentityToken(@Body identityTokenBody: IdentityTokenBody):
Flowable<IdentityToken>
@POST("flight/task/select/{flightId}")
fun selectFlightWidget(@Path("flightId") flightId: Int): Completable
@POST("hotel/task/select/{hotelId}")
fun selectHotelWidget(@Path("hotelId") hotelId: Int): Completable
@POST("car/task/select/{carId}")
fun selectCarWidget(@Path("carId") carId: Int): Completable
@POST("train/task/select/{trainId}")
fun selectTrainWidget(@Path("trainId") trainId: Int): Completable
@POST("flight/task/select")
fun selectFlightWidgetWithCF(@Body selectCFBody: SelectCFBody): Completable
@POST("hotel/task/select")
fun selectHotelWidgetWithCF(@Body selectCFBody: SelectCFBody): Completable
@POST("car/task/select")
fun selectCarWidgetWithCF(@Body selectCFBody: SelectCFBody): Completable
//... some other methods
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment