Pluggable Domain Component in iOS
In mobile apps, sometimes screens are considered the real estate of the software we are developing, where each screen or group of screens belongs to a single domain/team. However it can sometimes happen that this real estate (screen) is too big to be handled by one domain. In this article we will look at how to create domain components that has embedded business logic, data preparation, rendering logic, tracking, and many more so that it can easily be pluggable into any screen.
In the HungerStation app, we have the Order Tracking screen, where the user can see the placed order information. Gif 1 represents a simplified version of the screen and contains the following components:
- Payment Information
- Restaurant Information
- Delivery Information
- Estimated Time the order delivery is expected
Classes Structure
In order to demonstrate the concept of building a pluggable component, let’s keep the structure as simple as possible:
Data Flow
- The data flow starts with
viewDidLoad
where we did two things called our endpoint which returns the whole data of the order tracking screen response and the second thing we called ourOrderTrackingView
to show a skeleton view until we get the response from the server. The response looks like as shown in gist 2:
Once we got the response we called configureContainerView
the method that renders data in UI as shown above in Gif 3. Furthermore, you will see View / Components only hold the rendering logic:
Problems with this Approach
To give a little bit more context; the teams here at HungerStation are divided into squads, where each squad is cross-functional, autonomous teams (typically 6–12 individuals) and are focused on part of the business domain. These business domains can span anywhere from one screen to multiple screens. Those screens are not necessarily mutually exclusive, and so it can happen that multiple squads are contributing to the same real estate/screen, each with his own domain. Here is the list of problems we can identify by looking at our current approach to building this screen:
Problem 1 → Poor horizontal scalability
This is the main engineering problem since there is a lot of shared code between multiple squads which includes API calls, parsing, and data preparation for UI components that lead to multiple bugs, conflicts, etc. directly impacting delivery and business goals.
Problem 2 → Single Point of failure
As you can see in Figure 1, due to a bug introduced by the payments squad backend (logo_s
instead of logo
) in response data that failed decoding ultimately fails the whole parsing logic and nothing will render in the Order Tracking screen:
Problem 3 → Monolith or bad modularisation
While the single monolithic application approach may work well for simple mobile applications, surely there is a better way to build large complex mobile applications that have several different functional elements, in different modules. In our case, if every squad will create a separate module that can run independently it will create huge benefits that include reusability, module/feature isolation, etc.
Brainstorming
Our Order tracking screen should treat components as a black box which means it doesn’t know how to create data for this component, how to render it, and how to apply the business logic of that component. To accomplish this, we should create some contract allowing external modules/components to receive the state of the Order tracking screen and report back to it. Let me use the PaymentDetails
section as an example to show you how we transformed it as a black box and how we transformed our Order Tracking screen from a monolith into a modular which can plug black box components into it. In the next section, we will step by step convert our idea into a prototype with some code snippets.
Step 1 → Create Container View With State
In this step, we will do the following things:
- Create a
PaymentDetailsContainerView.
- In the container view, we will put this component skeleton/loading view and the actual view inside vertically
stackView
when we need to show the loading view we will set the state to.loading
, and once we get data we will set the state to.info
with data. - Last thing we do we will create a separate module for each squad. This is very important since now we fix the issue of the Monolith application as shown in Figure 2 we create
Payments
,Order
andLocation
modules. InCore
the module we will put shared/common things which can be used by all these modules.
Step 2 → API / Response Break Down
This step is crucial to make this component pluggable and it requires some backend changes as well. As shown in Gist 5 you can clearly see now this component is hitting its own endpoint that returns data relevant to that component only. In addition, it is also preparing data for its UI, applying business logic, and rendering UI as usual.
With this step, we eliminated the single point of failure problem. In addition to this, we also moved this component business logic from the main component which makes the main component lightweight that indirectly reduces the code conflict and code duplication.
Step 3 → View Loadable Class
In this step, we will create an interface for our pluggable component, the name of this class is debatable. Any module or screen wants to load Payment Details
should talk to this class. The responsibilities of this class include:
- Load
Payment Details View
black box component into a parent view provided by the client, it can load either skeleton or actual view as per internal logic, the client doesn’t care about anything, the client only callsrenderView
and provided only thecomponentId
andsuperView
and our pluggable component handle all tasks related to it as we saw in Gist 5. - Output some events to the client to react in our case we are outputting only hide events, so the hiding logic will implement by the client.
- The Input communication to the component is
PaymentDetailsViewLoadable
- The output communication of the component is
PaymentDetailLoadableOutput
- We created bidirectional communication between the client and component by using the above two classes.
Step 4 → Integration Part
In the integration part, the client has to do the following things:
- Inject
PaymentDetailsViewLoadable
input interface with a concrete classPaymentDetailsViewLoader
- Create container view
paymentDetailsViewContainer
and add constraints where the client wants to load our component. - Call
renderView
and providecomponentId
which will use to get the component data pluspaymentDetailsViewContainer
where our component will add as a subview. - Listen to components events
paymentDetailsInjector.output
, and react accordingly in the case we only have to hide. Hiding the logic of the component is the responsibility of the client, we need to do this for all our components.
Conclusion
As shown in Gif2
we implement the Payment Summary
component using modular/pluggable technique. If we compare this technique with the one where the main container was interfering with every components matter we see clear benefits. Here is the list of advantages we can observe:
- The component can easily plug into any screen.
- Component business logic, UI data preparation, API, and rendering logic are embedded in their own space which reduces code conflict, and code duplication and it’s easy to scale new components into that screen.
- No more single point of failure.
- Lightweight container code or less shared code between multiple squads.
- Fewer bugs.
- Every squad can work independently.
- Code changes can be easily automated and assigned to code owners.