The Meltwater Mobile application development team was formed in 2014 and has grown from 3 to 18 members. We build native apps for both iOS and Android platforms. Usage of the native apps has grown in sync with the growth of the team size.
In part 1 of this series we discussed what went into my team’s decision to move to VIPER as our app development architecture. In the second part of this series we will share our experience and provide some lessons learned. Hopefully reading about our experience will make your move a little bit easier.
Time to Move
Not all the snakes are poisonous and not all the poisons are deadly! Keep this in mind when bitten. ~ Mehmet Murat ildan
“VIPER is an application of Clean Architecture to iOS apps”. Uh Oh! We do native iOS and native Android. VIPER was designed with iOS in mind and information on using VIPER with Android is scarce. We immediately needed to determine if this was going to hold us back from using VIPER since we definitely wanted to have the same architecture across the different platforms. We found out that the biggest deterrent to using VIPER in Android was the rule that required the Router to bootstrap a module.
In iOS the module can be instantiated through a static method. That method can instantiate all of the components for a module and use global functions to get the module (specifically the ViewController) loaded into the viewport of the app. In Android this bootstrapping requires that a context exists to create new intents from the “previous” intent. To overcome this difference in the platforms we devised a rule that says when a Module is going to be created the calling module must pass it’s own context into the create function. This gives the new Router its own context to create the new View (activity).
Explanations of the illustration above:
- The app instantiates our FirstModule via the MainActivity (passing itself in to the FirstModule).
- The FirstModule uses the MainActivity context to bootstrap itself and holds on to the new View (FirstActivity with a weak reference) that it creates as its own context.
- From the FirstModule we can now navigate to SecondRouter.createModule(this)
- Inside SecondRouter.createModule the context passed in is used to create the new View (SecondActivity with a weak reference).
- This pattern is repeated for subsequent navigations (say for example from SecondRouter to the ThirdRouter)
OK, we can do this!
Your life works to the degree you keep your agreements. ~ Werner Erhard
As we moved ahead with our transition to VIPER we decided that we would not attempt to refactor everything. We needed to have some agreed upon path forward so that we could take a methodical approach to bringing our code into the new world. We decided to take an iterative approach towards getting things in order by first agreeing that any new code we write would be using the VIPER architecture. As time allows we will go back and transition features one by one from our old codebase to the new structure. As we started designing components we quickly realized that VIPER didn’t make every decision for us. We were going to have to augment the rules with our own set of agreements. What follows is a list of the most important agreements that we made to enable us to move forward.
Xib vs storyboard - As you are likely aware, in iOS development, storyboards can get very complex and can create a lot of artificial dependencies across different features of an application. Additionally, using segues (and prepareForSegue) can make the information being passed around during navigation…obscure. We agreed to remove these issues by simply using xib files and instantiating ViewController inside our Router when needed.
Routers - Does VIPER mean one router per screen (a screen is a conceptual View plus supporting files) or can you have many screens per router? Is a screen a Module or can you have many screens per Module? These questions are not explicitly dictated by VIPER. We decided that a Module means a “feature”; and a feature may consist of multiple screens that work together. A Router exists to navigate between screens in a Module as well as navigate to a different Module. Thus we may have many VIP files that are part of a Module that has a single Router.
Presenter is the decision maker - I know… this is already a VIPER rule. But it is worth reiterating. The Presenter is THE place for decisions to be made. We need to keep reminding ourselves of this. Additionally this is all business in here… no iOS API calls, no Android SDK calls.
Alerts stem from the View - Obviously the View is going to display an Alert, so why is this an agreement? This is a reminder that the interfaces between the components should not make any assumptions about what each end of the interface call are doing. For example, when the acceptance criteria for a feature calls for an Alert to be displayed by the UI because some error was returned from a backend service it is important to NOT make assumptions all the way through the VIP components.
- In this example the Interactor gets a result that indicates an error.
It will call to the Presenter with the Error result without any care for what the presenter will do with the information it receives. Such a call might be something inside the Interactor code like:
The presenter should then make a decision regarding that error such as reformatting & handing the information to the View with code such as:
- The View can then make the decision to display an alert while none of the other components through the stack had any idea that would ultimately be what happens.
No “code chaining” through the Presenter - When you first get started coding in VIPER components you might write a piece of code inside of your View that looks like this:
Don’t do it! This will compile because the getters are public so that the pieces can communicate with each other. VIPER requires that the interfaces/protocols between the components are well defined. However this form of chaining calls through the presenter breaks all of the benefits that have been provided by the VIPER rules. Chaining through the Presenter creates a new relationship. In this example, the View now knows about something inside the Interactor. This sort of code is absolutely forbidden (insert whatever form of punishment your team sees fit)!
Just a Beginning
Coming together is a beginning; keeping together is progress; working together is success. ~ Henry Ford
With agreements in place, well-defined rules, and a clear goal for developing in a platform agnostic way we have tools that will allow us to build simplified components in a predictable fashion at a greater velocity with less debate among ourselves. We also have a very specific way to describe how we build our software to other members of our organization or to potential new hires which enables us to bring more resources onto our projects and scale our efforts to meet higher demands from management. Still, this is just a beginning. As you embark on your journey to VIPER keep these critical reminders on your mind:
Communication is critical in the early stages. Everyone needs to buy into what you are doing. Without cooperation VIPER can be very intimidating if not confusing.
Iterate and refactor continuously. The first pass at VIPER components almost never seemed right early on. We have to constantly go back and look at the decisions we made and the interfaces we created. Ask if we are sticking to the rules and change them if we are not.
Factor out platform specific code from the unit testable code by creating classes that are internal (abstract the platform details). If you have to write code that is specific to a platform (and it is not in the View) hide the details in base classes, helper functions, or some other component that is not directly inside the VIPER components on a per module basis. This abstraction will allow for code that is reusable across the board.
We can seriously do unit tests now? If we follow the rules, guidelines, and reminders we should be able write unit tests for a vast majority of our codebase. In my opinion, in the short history of Mobile App development, unit testing mobile client software has been an exercise in futility. VIPER creates such a great separation of concerns in your code that your business logic can now really be tested without having all of the system running in place like is needed for integration testing.
Going through a transformation from a rather loose architecture to something as rigid as VIPER requires a lot of forethought, a good amount of introspection, a careful eye for detail, and a willingness to try something a bit more challenging than you wanted. But the result is a development platform that provides for stability and predictability for new and existing developers. Ultimately we have the ability to develop software that is focused on the business as opposed to the platform.
I hope that reading our journey to VIPER will help you in your architectural decisions. VIPER can be intimidating, but I am here to tell you that once you start to take advantage of the structure, things get a lot simpler. Code is more reusable, more testable, easier to debug, and easier to understand. Developers are more efficient with predictable code output per feature leading to less debate and more functionality in less time.
And who knows, maybe someday a lot of the code that we write in these VIPER components will be able to live natively on both iOS and Android. Kotlin is emerging on Android. Swift is the future for iOS (it is open source). Doesn’t every native app developer have this dream?
So what are you waiting for? VIPER, Get Bit!