World Flux Architecture in iOS (part 1)
Half a year ago, we started accepting Flux architecture in PlanGrid iOS application. This article will analyze the motivation that makes us convert from traditional MVC to Flux and share more experiences gained through the project.
By analyzing code in production, I am trying to describe the important parts of Flux implementation. If you are interested in the issue at a higher level, you can skip the middle part of this article.
Reason for our conversion
To make our decision consistent with the context, I want to describe some of the challenges that PlanGrid application faces. Some challenges are only for business software, while others are applicable for most iOS applications.
We have all State
PlanGrid is a complex iOS application. It allows users to view blueprints and coordination on those plans using a variety of annotations, problems and attachments (and many other things that require specialized knowledge).
An important first aspect of the application is offline calculation. Users can interact with all the features in the application whether or not the Internet. So we need to store a lot of data and customer status. We also need to implement a set of internal business rules (eg, which annotation does a user delete?)
The PlanGrid application runs on both iPad and iPhone, but its UI is optimized to take advantage of larger available memory on tablets. Unlike many iPhone applications, we often display multiple view controllers at the same time. These view controllers tend to share some amount of state.
State management status
In general, your application must manage the state well. Any changes in the application, more or less will lead to the following steps:
- Update status in internal object
- Update UI
- Update the database.
- Sorting changes will be sent to the server according to the available network connection
- Notify other objects about status changes
I will solve all aspects of the new structure in other articles, this article only focuses on the 5th step. How to fill in the status updates in your application?
This is worth a billion dollar question in application programming.
Most iOS engineers, including the early programmers of PlanGrid application, proposed the following answers:
- KVO
- NSNotificationCenter
- Callback Blocks
- Use DB as truth data (source of truth)
All of these methods will work in different situations. However, these different options have become a source of instability in a large codebase that has evolved over the years.
The danger of freedom
Classic MVC only supports the separation of data and display part of that data. Without other structural instructions, every dev must solve all the rest.
The PlanGrid application (like most iOS applications) did not have a pattern defined to manage state.
Many current state management tools such as delegation and blocks tend to create strong dependencies between components as desired – 2 quick view controllers are tightly coupled to share state updates with each other.
Other tools, like KVO and Notifications, create invisible dependencies. When you use them in a large codebase, you may experience code changes and unexpected side effects. A controller can observe the model layer details that should not be interesting.
Code reviews and style guides can support quite a bit, but many structural problems stem from inconsistent chores and they also take some time to become serious problems. With the well-defined patterns, you can easily detect standard deviations early.
A architectural pattern for State administration
One of our most important goals during the reconstruction of the PlanGrid application is to clarify the best appropriate patterns and practices. This allows you to write future features more consistently and integrate new engineers more effectively.
State management is one of the biggest causes of complexity in our application, so we decided to define a pattern that all features can use in the future.
The issues in the current codebase are very similar to the problems Facebook has encountered when they present the Flux pattern for the first time:
- State-level updates are not as expected
- It is difficult to understand the dependencies between components
- Disturb the information flow
- Source of truth is not clear
It seems Flux will be best suited to solve many of the problems we face.
Short introduction about Flux
Flux is a lightweight structure pattern that Facebook uses for cliend-side web applications. Despite a reference implementation , Facebook emphasized the idea that the Flux pattern is more suitable for this specific implementation.
This chart clarifies the Flux pattern with different flux components:
In the Flux structure, 1 store is single truth of truth for a certain part of the application. Whenever the state in the store is updated, it will send an event change to all registered subscriptions for the store. View receive changes only through this interface called by store.
The updated state can only occur through the ac tions.
An action represents a predetermined state change, but it does not execute the state change itself. All components that want to change any state send an action to the global dispatcher . The stores register with the dispatcher and tell it which actions they care about. Whenever an action is submitted, all related stores will receive it.
In response to actions, some stores will update their state and announce new state views.
The Flux structure implements a one-way data stream as shown on the diagram above. It also performs analysis of concerns:
- Views will only receive data from the stores. Whenever a store updates, the handler method in view will be called
- Views can only change the state by sending actions. Because actions are the only description of intents, the business logic is hidden from the view
- A store only updates its state when it receives an action
These constraints support the design, development and debugging of new features more easily.
Flux in PlanGrid for iOS
With the iOS PlanGrid application, we have deviated a bit from the Flux reference implementation. We ensure that each store will have a visible property state
. Unlike the original Flux implementation, we didn't export an event change when a store is updated. Instead, views view the property state
of the store. Whenever views see a state change, they will update the response as follows:
This is a very small standard deviation from the Flux reference implementation, but resolving it will greatly support the next part.
With an understanding of the foundation of the Flux architecture, let's analyze some more details of implementaton and the questions we need to answer when implementing Flux in PlanGrid application.
What is the scope of 1 Store?
The scope of each single store is a very interesting question that often appears when you use Flux pattern.
Because Facebook launched the Flux pattern, different variations have been developed by the community. One of them is Redux, repeating in the Flux pattern by authenticating each application should only have 1 single store. This store stores the state of the entire application (there are many other small, sophisticated differences outside the scope of this post).
Thanks to the idea of another single store simplifying the structure of many applications Redux became famous for. In traditional Flux, thanks to a lot of stores, applications can run instances where applications to combine state are stored in separate stores to render a certain view. That approach can quickly reintroduce the problems that Flux patterns have tried to solve, as the dependencies are complex among different components in an application.
For PlanGrid application, we still decided to work with traditional Flux, instead of using Redux. We are not sure how a single-store approach saves the entire state of the application to scale with such a large application. Moreover, we have determined that there are very few inter-store dependencies, so considering the choice of Redux is not important anymore.
We have not yet defined a tough rule for the scope of each single store.
Until now, I can identify two patterns in our codebase:
- Feature / View Specific Stores: Each view controller (or each view controllers relies on each other) will receive its own store. This store template mimics the view specific state pattern.
- Shared State Stores: We have shared storage and state management stores between multiple views. We try to keep these stores at a minimum. An example of such a store is the
IssueStore
. This store is responsible for managing the status of all issues found in the recently selected blueprint. Many views display and interact with issues that get information from this store. These stores will now act as a direct update database query.
We are currently implementing our first shared state stores and are still thinking about choosing the best way to mimic many of the dependencies of different views in these stores.
Source: IDE Academy via Blog.Benjamin (continued)