Mobile: Tackling Complex Screens

Jay Chang
GOGOX Technology
Published in
8 min readSep 7, 2020
Design iterations of the package information screen

In the GOGOX consumer app, one of the rapidly evolving screens is the delivery package information screen. The complexity grows in the following ways:

  • Rich experiments. We have done a lot of experiments to improve the UI/UX design. The screen is really dynamic and controlled by feature flags. There are 17 feature flags for this single screen. Some experiments are minor changes such as adding a merchant order number component, and some of them are major changes such as replacing the size and weight component with brand new design and rearrange the components layout of the screen.
  • Multiple countries support. The user interface and behavior may be different for each country to cater to country-specific needs.
  • Multiple account types support. The user interface and behavior may be different for personal and business users. For example, we present a set of predefined size options for personal users but allow custom sizing for business users.

Without a proper design, the screen complexity will grow out of control as we have to take into account different combinations of scenarios. It is tempting to embed if-else conditions inline for different scenarios. However, the logic becomes tightly coupled and hard to understand, which makes it easy to break the working code unintentionally. For instance, updating the feature for country A may break the feature for country B. As a result, the developer productivity will be slowed down as part of the time is wasted to fix bugs and puzzle over workarounds. And product manager also has to compromise the pace of feature rollout.

What are our goals?

🎯 Easy to adapt to changing requirements.

The meaning of the term easy may be different for different people. To define the definition in our case:

  • Easy to add a feature — We can add a new component in isolation without changing the existing working code.
  • Easy to remove a feature — We can remove a component by just deleting the linkages.
  • Easy to update a feature — We can update a component in isolation without creating side effects breaking other components.

How to accomplish our goals?

One of the key techniques to tackle software complexity is the separation of concerns. A concern is the details of a specific building block. For example, in mobile app development, a screen is composed of smaller components. The presentation and behavior of a single component is a concern that we want to separate from other components; the composition of all the components in a screen and interaction between them are other concerns.

When requirements change, you extend the behaviour of such modules by adding new code, not by changing old code that already works. — Robert C. Martin

Unsurprisingly, a well-known approach can help accomplish our goals — Open Closed Principle. Let’s see how to apply this approach to make the package information module open for extension, but close for modification.

1. Decomposing screen

The term “component” is abstract and the implementation may differ in backend and frontend. Basically, it is a building block encompassing cohesive pieces of functions and data in order to encapsulate reusable application functionality. In mobile app development, it is a molecule or an organism in terms of atomic design.

Breaking the screen into components

Breaking a big screen into small independent components may roughly reduce its complexity by N times with N being the number of components, so we can focus on one component at a time. If you have no idea of how to break down the screen, the designer can help you with this because they are the authors of the screen. Cooperating with them can lead us to produce a set of reasonable small components. Furthermore, the article Package by Feature by Philipp Hauer has explained why this approach is better than package by technical concerns.

Component structure

In the GOGOX consumer app, we have adapted RIBs as a high-level architectural pattern to encapsulate the app state as a tree, and we will use components as the internal pattern of the node if the node is complex enough.

State

A Presentation Model encapsulates the state and behavior of the component. This is the model shared in the component as the single source of truth. The formula UI = f(state) denotes that the UI of the component can be derived from the state. The state model represents a UI state to provide an interface to the view. Minimizing the decision making in the view helps identify the culprit of UI bugs. For example, the description text is computed from the user property. If the final result of the description is not expected, it is most likely caused by either the wrong user state or wrong computation logic.

To test whether the transformations of the state are expected, it is simple to feed different inputs and verify the outputs using parameterized tests, no mocking framework is required; it is also fast, no UI environment is required.

View

A platform UI renders the component with state and emits events. This is the place to interact with the platform such as accessing system photo library. The application level UI logic are already pulled to the state model and been well tested, so the main responsibility of View is making the UI beautiful and pixel perfect.

The view is composed using the UI components from the GOGOX mobile design system, in which we use screenshot tests to guard against broken UI.

Presenter

A handler handles the application logic which coordinates the data layer and UI layer. It observes the input events (from view or other external systems such as Pusher) and processes them. For example, PhotoPresenter observes the photo picked event and save the photo to the order stream for later processing.

To test whether the presenter works as intended, we stub the collaborators (e.g. input event stream and state model) and verify the interaction logic using behavior verification.

Component

An aggregate object groups Presenter and View as a single unit. It plays the facade role to communicate with others within the host module. The other components or the host module should not be aware of the internal details of the component.

To test whether the component is integrated correctly as a whole unit, we use real or fake collaborators for integration tests.

Builder

A factory encapsulates the component creation logic. We can config the component with different State/Presenter/View combinations based on the application requirements (e.g. country, account type). At GOGOX, we use split.io feature management platform to manage the requirements. Moving the component configuration to a remote service enables dynamic and targeted feature rollout. The target rule system controls the enablement of the component, we can easily enable/disable the component or update the rules remotely without any changes in the app.

To test whether the component configuration logic is correct, we can stub the application requirements and verify if the correct State/Presenter/View combinations are provided using state verification. The remote target rules will be tested by our QAs manually because those are external dependencies that the app doesn’t own directly, mocking what you don’t own makes no sense. The tests passed, but that is fake news, the actual things won’t work.

RIB or component?

The major factors to determine the choice is whether the module is intended for reuse and the underlying context. A RIB module is self-contained and able to be attached as a child node to any parent node conforming to its contract, which makes it reusable. For example, the payment footer is actually a RIB and be attached to the package info RIB as a child node. The footer is shared between the transport service and delivery service and perhaps other future services, so it is a good candidate to make it as a RIB. On the other hand, in the case of the package info node, we found that making each component as a RIB makes not much sense as the Router doesn’t fit into any role and will make the structure more complex. Also, the components are not intended for reuse, it’s unlikely to be shared because they are designed for delivery package information context in mind.

2. Connecting components

We have a set of isolated pluggable components at the current stage. The next step is straightforward. The PackageInfoBuilder installs the component builders and provides an organised list of components based on the application requirements. Then we connect these components in the PackageInfoInteractor and PackageInfoScreen.

Is it easy enough?

To add a feature,

  1. Create a component in isolation.
  2. Register the component to the host module.
    2.1. Install the builder.
    2.2. Add it to the component list.

To remove a feature,

  1. Delete the component package.
  2. Unregister the component from the host module.
    2.1. Uninstall its builder.
    2.2. Remove it from the component list.

To update a feature,

  1. Find the component by the package name.
  2. Update it based on the requirement changes.

How about Server-Driven UI?

The idea of Server-Driven UI (SDUI) is simple: given the app has defined a set of components, the server returns instructions to tell the app what to render and how to interact with the server. SDUI has different maturity levels:

  • Level 1 — Server returns content and visual structure to instruct the client what components should be rendered, and most of the logic still lies on the client-side. This is more like a CMS approach that provides a way to config the server response. It is a good fit for a campaign driven app so that the marketing and product teams have a portal to control how the app changes. One of the good examples is the real-time survey feature.
  • Level 2 — Server returns content, visual structure, and actions to instruct the client what components should be rendered and how the components should perform actions such as navigation, asynchronous request. Most of the logic lies on the server-side. Tom Lokhorst had written a great article about this topic in detail.

Do we need SDUI in our app? Unsurprisingly, the answer is “it depends”. Given we have implemented a set of reusable native components, the effort to update the screen by either backend or frontend is not much different. The question should be changed to “Should we trade simplicity for speed of iteration?”. Inevitably, adopting SDUI requires upfront investment in time and engineering resources, and on top of the complexity of the backend system, it also adds an additional level of complexity to the system.

Key Points

  • Componentization results in high cohesive and loosely coupled module. The module is open for extension but closed for modification, it can behave in new and different ways by plugging in different components based on the application requirements. This enables our rapid product iteration, and it will improve our developer productivity as the complexity is under control.
  • There’s no silver bullet, it is not possible that this solution will work for all the apps magically as the context are different, even the complexity of each screen within the same app is also different. Therefore, it makes more sense to use polyglot patterns in an application to tackle different problems.
  • When we attempt to design a solution to attack a problem, “How to reduce the cost of change” will be a crucial question that the solution needs to answer. Making the solution to adhere OCP is one of the approaches.

--

--

Jay Chang
GOGOX Technology

A software engineer building apps to make your&my life better:)