Pluggable Domain Component in iOS

Ali Akhtar
HungerStation
Published in
6 min readJan 17, 2023

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
Gif 1

Classes Structure

In order to demonstrate the concept of building a pluggable component, let’s keep the structure as simple as possible:

Figure 1

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 our OrderTrackingView to show a skeleton view until we get the response from the server. The response looks like as shown in gist 2:
gist 1
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:

Gist 3

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:

Figure 1

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 and Location modules. In Core the module we will put shared/common things which can be used by all these modules.
Figure 2

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.

Gist 5

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 calls renderView and provided only the componentId and superView 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.
Gist 6

Step 4 → Integration Part

In the integration part, the client has to do the following things:

  • Inject PaymentDetailsViewLoadable input interface with a concrete class PaymentDetailsViewLoader
  • Create container view paymentDetailsViewContainer and add constraints where the client wants to load our component.
  • Call renderView and provide componentId which will use to get the component data plus paymentDetailsViewContainer 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.
Gist 7

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.
Gif 2

--

--

Ali Akhtar
HungerStation

Senior iOS Engineer | HungerStation | Delivery Hero