Last active
April 8, 2019 11:58
-
-
Save CyxouD/f85dd37f2ad5fa45c01e24d29a482726 to your computer and use it in GitHub Desktop.
Model-View-Presenter+Repository architecture example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | |
[email protected]() | |
return false | |
} | |
override fun onComplete() { | |
[email protected]() | |
} | |
}).let { mCompositeDisposable.add(it) } | |
} | |
override fun removeAll() { | |
Utils.removeAll(repository, view, schedulerProvider, mCompositeDisposable) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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