Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

B-Wom Case Study: Pure MVP Application

DZone's Guide to

B-Wom Case Study: Pure MVP Application

In this post, we look at how a development team built an MVP application using both Swift and Java for mobile web applications.

· Web Dev Zone ·
Free Resource

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

One of the most important tasks that we carry out at Apiumhub is to collaborate with our clients for the implementation of Agile development methodologies and the introduction of good software development practices (software architecture, testing, etc.). Today I would like to focus on the Pure MVP architecture that we implemented in the B-wom app.

Working With Legacy Code

Many times, the projects that we usually get at Apiumhub contain a great amount of legacy code and our primary task is to help our clients with the improvement of the code, the implementation of new functionalities and the improvement of the packaging and distribution system.

In the specific case of B-Wom, we had two large parts of the system well-differentiated: the first one was the part of backend, where a lot of code was written in PHP with coupling and the second part was two mobile applications: one for Android written in Java and another one for iOS written in Swift (both are the native languages of the platforms).

In the backend part, our main task was to give advice rather than doing development itself, since the great part of the refactoring was done internally by B-Wom and we were involved more in the packaging and continuous integration implementation part.

As for the mobile applications, we were fully involved in taking care of the refactoring and improvements for the packaging of the applications. Normally, working with legacy code is quite delicate because one of the first tasks that you have to take into account is that you will need to modify code without breaking the functionalities of the app.

In this case, to avoid facing the problem of a “Refactoring Nightmare” and having an intractable project, we opted to try to be as unobtrusive as possible and follow the technique of “small steps” (baby steps), which allowed us to make small changes in the code and avoid as much as possible breaking the functionality.

What We Did and How

First, we identified the parts of the code that corresponded to certain functionality and we analyzed how much coupling with other parts of the code we found. Once we identified the dependencies of the functionality, we applied dependency investment techniques to decouple the code and divide it into smaller functionalities.

When we detected code that repeated, what we did was to create private methods that centralized calls to that code. If these methods were shared by different classes to which we were adding functionality, what we did was create separate classes with a functional sense (typing the classes) to expose a unique and common functionality shared with other parts of the application. If possible, the new functionality was covered with tests to ensure it functioned correctly.

Legacy Code Architecture

The structure of the legacy code of mobile applications was reasonably well-separated since there was a series of components with a specific task. We call this architecture "MVC custom."

iOS Architecture

In the part of iOS architecture, the legacy code consisted of a Model – View – Controller with certain peculiarities. The essential difference with a traditional MVC lies in the part of the Model, specifically in the “Services” that were implemented by classes called “Controllers” that contain static methods that call an ApiManager which is a wrapper of “Alamofire” and which is responsible for making backend calls. The part of the model is managed through “Realm” and stored locally to add some “reactive” capacity to the application. The rest of the architecture relies on protocols and class extensions to decouple and provide the necessary functionality to the application.

Android Architecture

The Android part the architecture was very similar to the iOS base in the basic fundamentals, but instead of using an MVC, they used a slightly modified MVP to improve the decoupling of the functionalities. Following the bases of the architecture proposed by Google for the creation of MVP, the presenter communicated with a “Controller” that worked in a way almost identical to the “Service,” which we explained in the iOS architecture, using static methods and using an API Manager which is a Retrofit wrapper and which managed the backend calls that persisted in Realm.

New Pure MVP Architecture

In those functionalities where we started the development from scratch, as in the case of the new Coach functionality, we took advantage of the opportunity to add the new architectural proposal of Apiumhub, supervised by our architect Christian Ciceri. We implemented Pure MVP (MVPP) architecture. The idea is putting into practice an example of architecture that is transversal to the platform (because we use the same idea in Android and iOS) and that allows us to separate in a fairly easy and clear way the responsibilities of the different components of the Applications.
The Pure MVP architecture is basically what we explained in another article a Model – View – Presenter, but here we add a “Pure” aspect, and with the particularity that the presenter only serves as a connector between the view and the service.

The basic architecture that we have implemented in B-Wom is based on Rx, so we have used RxSwift and RxJava to add the reactive part of the application. In this article, we will explain the bottom-up architecture, starting by describing the API connector that executes calls to the backend and ends in the view.

API Manager

Our API Manager is a wrapper that relies on the different frameworks of the two platforms for using Rx to make backend calls and communicate through an API Rest. It is usually a static class that has backend calls defined in a configuration. In the case of Android, we use the wrapper over retrofit and, in the case of iOS over Moya, it is a framework that makes the use of Alamofire with Rx a lot easier. As for the API Manager, we have not done unit tests.

iOS Example:

enum ApiClient {
    case getSurveys
}

extension ApiClient:TargetType, AccessTokenAuthorizable {

    var baseURL:URL {
    return URL(string: API_URL)!
    }

    var path: String {
    switch self {
    case .getSurveys:
        return "".getApiEndpointPath(type: .kAPI_ENDPOINT_SURVEYS)
    }

    var method: Moya.Method {
    switch self {
    case .getSurveys:
        return .get
    }

    var task: Task {
    switch self {
    case .getSurveys:
        return .requestPlain
    }


    var sampleData: Data {
    switch self {
    case .getSurveys:
        return stubbedResponse("surveyList")
    }

    func stubbedResponse(_ filename: String) -> Data! {
    @objc class TestClass: NSObject { }

    let bundle = Bundle(for: TestClass.self)
    let path = bundle.path(forResource: filename, ofType: "json")
    return try? Data(contentsOf: URL(fileURLWithPath: path!))
    }
}

Android Example:

interface NetworkService {

    @GET("$VERSION3$PATH_LANG$SURVEYS")
    fun getSurveyHistory(@Path("$LANG_PARAM") lang:String): Observable<Result<BaseResponse<List>>>

    companion object Factory {

    private val gson: Gson = GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss") .create()


    fun create(): NetworkService {
        return createWithParams(BuildConfig.HOST)
    }

    fun createWithParams(baseUrl:String): NetworkService {
        val retrofit = Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(initOkHTTP())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build()
        return retrofit.create(NetworkService::class.java)
    }
    }
}

The Repository

In the MVPP architecture that we designed in Apiumhub, the repository is in charge of connecting with the ApiManager or connecting with the local storage system, depending on whether the call is online or offline. The only thing that the repository does is expose the concrete methods that the application will have independent of the persistence system, in our case, it would have the input parameters and it would return an observable with the type that it touches. In the case of the repository, an integration test is done with the ApiManager to guarantee that these two components work in a correct and coordinated way. As they do not contain business logic, we do not consider the use of unit tests to validate the code since we believe that the integration tests cover those cases of use.

iOS example:

protocol SurveyRepositoryInterface {
    func getSurveyList() -> Observable<[String]>
    func sendSurvey(surveyId:String, surveyRequest:SurveyRequest) -> Observable
    func getSurveyHistory(surveyId:String) -> Observable<[Survey]>
    func getAllSurveyHistory() -> Observable<[Survey]>
}

class SurveyRepository: SurveyRepositoryInterface {

    let defaultKeyPath = "payload"

    private let apiClient:MoyaProvider

    init(apiClient:MoyaProvider = ApiClientFactory.createProvider()) {
    self.apiClient = apiClient
    }

    func getSurveyList() -> Observable<[String]> {
    return apiClient.rx.request(.getSurveys)
        .asObservable()
        .filterSuccess()
        .map([String].self, atKeyPath: defaultKeyPath)
        .asObservable()
    }
}

Android Example:

interface SurveyRepository:INetworkBaseRepository {

    fun getSurveyHistory(surveyId:String): Observable<List>

    companion object {
    fun create(): SurveyRepository {
        return SurveyRepositoryImpl(NetworkService.create())
    }
    }
}


class SurveyRepositoryImpl(apiService: NetworkService) : BaseRepository(apiService), SurveyRepository {


    override fun getSurveyHistory(surveyId: String): Observable<List> {
    return executeRequest(apiService.getSurveyHistory(LocaleHelper.getAppLanguage(), surveyId), listOf())
    }
}

The Service

The service is the class that is in charge of calling the repositories, executing the business logic and the necessary state changes in the data and returning the result through a callback to the view using the presenter as a “connector.” In the case of the functionality that we are dealing with, the service has very little logic and mostly the state changes are solved by using Rx. The service executes a method that is called from the view through the presenter, using the repository to obtain the data. The repository returns an observable and this observable have subscribed to different publishers depending on the type of information that you want to return or if there has been an error or not.

Having different publishers for the same subscription allows us to filter the data and deal with changes in status, naming method differently than the view depending on the status of them. For example, if we recover the list of ‘Surveys’ and there is an error the service will call the method onError() and if everything is correct it will call the method onSuccess(). In the services we use unit tests to check the application.

iOS Example:

protocol SurveyServiceInterface {

    func getSurveyList()
    func onSurveyList(onSuccess: @escaping (([String]) -> Void))
    func onErrorSurvey(onError : @escaping (Error) -> ())
}


class SurveyService: SurveyServiceInterface {

    private let disposeBag = DisposeBag()
    private let repository:SurveyRepositoryInterface
    private let surveyListStream = PublishSubject<[String]>()
    private let errorStream = PublishSubject<[Error]>()

    init(repository:SurveyRepositoryInterface) {
    self.repository = repository
    }

    func getSurveyList() {
    repository.getSurveyList().subscribe(onNext: { surveyList in
        self.surveyListStream.onNext(surveyList)
    }, onError: { error in
        self.errorStream.onNext([error])
    }).disposed(by: disposeBag)
    }

    func onSurveyList(onSuccess: @escaping (([String]) -> Void)) {
    self.surveyListStream.subscribe(onNext: { surveyList in
        onSuccess(surveyList)
    }).disposed(by: disposeBag)
    }

    func onErrorSurvey(onError: @escaping (Error) -> ()) {
    self.errorStream.subscribe(onNext: { (errors) in
        onError(errors.first!)
    }).disposed(by: disposeBag)
    }
}

Android Example:

interface SurveyService:BaseService {

    fun getSurveyHistory(surveyId:String)
    fun onSurveyHistorySuccess(onSuccess: (surveyList: List) -> Unit)
    fun onErrorSurvey(onError:(exception:Throwable) -> Unit)

    companion object {

    fun create(sequenceNumberProvider: SequenceNumberProvider): SurveyService {
        return SurveyServiceImpl(SurveyRepository.create(), sequenceNumberProvider)
    }
    }
}

class SurveyServiceImpl(private val repository:SurveyRepository): BaseServiceImpl(), SurveyService {

    private val surveyHistoryObservable:PublishSubject<List> = PublishSubject.create()
    private val errorObservable:PublishSubject = PublishSubject.create()

    override fun getSurveyHistory(surveyId: String) {

    val demoSurvey:Observable<List> = repository.getSurveyHistory(DEMO_SURVEY_ID)
    val screeningSurvey:Observable<List> = repository.getSurveyHistory(SCREENING_SURVEY_ID)

    Observables.combineLatest(demoSurvey,screeningSurvey){t1, t2 -> t1 + t2}
            .subscribeOn(Schedulers.newThread())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeUntil(disposableObservable,{
                Log.d("aquitamosServicio", "getSurveyHistory")
                surveyHistoryObservable.onNext(it)
            }, {errorObservable.onError(it)})
    }

    override fun onSurveyHistorySuccess(onSuccess: (surveyList: List) -> Unit) {
    surveyHistoryObservable
            .filter {
                isHistorySurveySuccess(it)
            }
            .subscribeOn(Schedulers.newThread())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeUntil(disposableObservable, {
                Log.d("aquitamosServicio", "onSurveyHistorySuccess")
                onSuccess(it)
            })
    }

    private fun isHistorySurveySuccess(surveyList:List):Boolean {
    return !surveyList.isEmpty() && !isNextAction(surveyList.last())
    }

    override fun onErrorSurvey(onError: (exception: Throwable) -> Unit) {
    INetworkBaseRepository.errorsStream
           .subscribeOn(Schedulers.newThread())
           .observeOn(AndroidSchedulers.mainThread())
           .subscribeUntil(disposableObservable, { onError(it) })
    }
}

The Presenter

In our architecture, the presenter is the most important piece and the one that allows us to build more modular applications and with a very low coupling because the only thing it does is connect the methods of the view and the service. This allows us to have a highly reusable piece of software and have several services per view, although it seems to be a contradiction, it is the advantage of a modular system, since what we intend to have is unique binomial, view – presenter. And we do not add tests to this pieces of software lacking functionality.

iOS Example:

protocol SurveyViewInterface:class {    
    func getAllSurveyHistory(action:@escaping () -> ())
    func onSurveyHistorySuccess(surveyList:[Survey])
    func onSurveyError(error:Error)
}

protocol SurveyServiceInterface {

    func getAllSurveyHistory()
    func onSurveyHistorySuccess(onSuccess: @escaping(([Survey]) -> Void))
    func onErrorSurvey(onError : @escaping (Error) -> ())
}

struct SurveyPresenter:LifeCycleAwareComponent {

    var view:SurveyViewInterface
    var service:SurveyServiceInterface

    func onLifeCycleEvent(event: LifeCycleEvent) {
    if (event == .viewDidLoad) {  
        view.getAllSurveyHistory(action: service.getAllSurveyHistory)
        service.onSurveyHistorySuccess(onSuccess: { self.view.onSurveyHistorySuccess(surveyList: $0)})
    } else if (event == .viewWillAppear) {
        service.onErrorSurvey(onError: {self.view.onSurveyError(error: $0)})
    }
    }
}

To be able to maintain more than one presenter per view, what we have is a base class of the view, which we call LifeCycleAware. This class is the one that manages the lifecycle of the view and is responsible for registering the presenters as components in a publisher that will be responsible for managing the events. This is the example of its implementation in iOS:

enum LifeCycleEvent {
    case viewDidLoad
    case viewWillAppear
    case viewDidAppear
    case viewDidDissapear
}

protocol LifeCycleAwareComponent {
    func onLifeCycleEvent(event:LifeCycleEvent)
}

class LifeCycleAwareViewController: UIViewController {

    private let lifeCicleOwner:PublishSubject = PublishSubject()
    private var subscriptions:[Disposable] = [Disposable]()
    let disposeBag:DisposeBag = DisposeBag()


    func subscribeToLifeCycle(component:LifeCycleAwareComponent) {
    subscriptions.append(lifeCicleOwner.subscribe(onNext: { event in
        component.onLifeCycleEvent(event: event)
      }
    ))
    }

    override func viewDidLoad() {
    super.viewDidLoad()
    lifeCicleOwner.onNext(LifeCycleEvent.viewDidLoad)
    }

    override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    lifeCicleOwner.onNext(LifeCycleEvent.viewWillAppear)
    }

    override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    lifeCicleOwner.onNext(LifeCycleEvent.viewDidAppear)
    }

    override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    lifeCicleOwner.onNext(LifeCycleEvent.viewDidDissapear)
    subscriptions.forEach { $0.dispose()  }
    }

    override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
    }

}

This lifecycle system defines the methods of the lifecycle of the view through an Enum. There will be as many lifecycle methods as needed to emit events that we have to manage to communicate a view with a service. In iOS, if we want a method of the view to be executed in viewDidLoad, we would create a method that will emit an event for that method of the lifecycle and all the presenters subscribed to that event will receive that answer or execute the call, depending on the logic that is needed.

The View

It is responsible for the user to interact with the application and defines the methods of input and output that cause the actions of users. Normally, we define an interface (a contract) that will be implemented by the corresponding class depending on the platform (in iOS it is usually implemented by a ViewController as an extension of the ViewController and in Android it is implemented in a fragment).

This would be like part of an iOS example:

class SurveyViewController: LifeCycleAwareViewController, SurveyViewInterface {

    let dataStream:PublishSubject = PublishSubject()

    func getAllSurveyHistory(action: @escaping() -> ()) {
    dataStream.subscribe(onNext: { (surveyId) in
        action()
    }, onError: { [weak self] (error) in
        self?.showErrorProgress(message: error.localizedDescription)
    }).disposed(by: disposeBag)
    }

    func onSurveyHistorySuccess(surveyList:[Survey]) {
    self.hideLoadingProgress()
    surveyList.forEach { (survey) in
        self.createDataSourceWithQuestion(surveyQuestion: survey)
        if let response = survey.textResponseQuestion() {
            let testAnswer = SurveyTestStruct(question: survey, text: response, answers:nil, haveAnswer:true, surveyTestType: .kSurveyTestTypeAnswer)
            self.dataSource.append(testAnswer)
        }
    }
    self.collectionView.reloadData()
    if self.dataSource.count > 0 {
        self.collectionView.scrollToItem(at: IndexPath(row: self.dataSource.count-1, section: 0), at: .bottom, animated: false)
    }
    }

    func onSurveyError(error: Error) {

    guard let bwomError = error as? BWAPIError, let errorMessage = bwomError.message else {
        self.showErrorProgress(message: "initialtest_api_get_error".localized())
        return
    }
    self.showErrorProgress(message: errorMessage)
    DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), execute: {
        self.loadInitialSurvey()
    })

    }
}

At Apiumhub, we know from our experience that there is not a definitive and final solution to solve an architectural problem and that each client has its own particularities and use cases, so the Pure MVP architecture is a first approach that we have validated that works with real applications that are in production and thanks to the feedback of our customers and the accumulated experience, we are improving and adapting day-by-day.

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

Topics:
mvp app ,software architecture ,web dev ,ios ,mobile application development

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}