Practice of Flux architecture in Toka App

Posted by bidnshop on Thu, 28 Oct 2021 03:54:27 +0200

Introduction: in order to cope with the complex interaction of video editing tools, Toka iOS draws lessons from the design idea of Flux architecture mode and the topology concept of directed acyclic graph, centralizes the event management, and realizes a comfortable, refreshing and easy to control "one-way flow" mode from the development experience; In this scheduling mode, the change and tracking of events become clear and predictable, and significantly increase the scalability of the business.

The full text is 6882 words and the expected reading time is 18 minutes.

1, Architecture background

Video editing tool applications often have complex interactions. Most operations are carried out on the same main interface, and there are many view areas (preview area, axis area, undo redo, operation panel, etc.) in this interface. Each area should not only receive user gestures, but also update the status with user operations. At the same time, in addition to the main scene editing function, it also supports other characteristic functions, such as general editing, quick editing, theme template, etc. all need to use the preview and editing functions; Therefore, there are high requirements for the scalability and reusability of the architecture.

After investigation, Toka iOS finally learned from the design idea of Flux architecture mode and the topology concept of directed acyclic graph to centrally manage events, and realized a comfortable, refreshing and easy to control "one-way flow" mode from the development experience; In this scheduling mode, the change and tracking of events become clear and predictable, and significantly increase the scalability of the business.

2, Playblast multiplexing

The general editing of DKA and many derivative tools and functions need to rely on basic capabilities such as preview and material editing.

For example, the following functions depend on the same set of preview and playback logic. These basic capabilities need to be abstracted as a base controller.

The baseVC structure is:

3, Functional module reuse

The problem of preview playback reuse has been solved. How to add various material editing functions to this set of logic, such as stickers, text, filters and so on, decouple these functions from VC, and finally achieve the purpose of reuse?

Finally, we use the plug-in design concept to abstract each sub function into a plugin, and directly call the dependency layer to write the attributes of controller, view, timeline, streamingContext and liveWindow, which will be used in 90% of the scenarios, and directly assign them to the plugin through weak.

protocol BDTZEditPlugin: NSObjectProtocol {
   // Tissue controller
    var editViewController: BDTZEditViewController? { get set }
   // All controls added to the controller View are added to this View to solve the hierarchy problem
    var mainView: BDTZEditLevelView? { get set }
   // The timeline entity of the editing scene is composed of tracks. There can be multiple video tracks and audio tracks, and the length is determined by the video track
    var timeline: Timeline? { get set }
   // The streaming media context contains the objects of the timeline, preview window, collection, resource package management and other related information collection
    var streamingContext: StreamingContext? { get set }
   // Video preview window control
    var liveWindow: LiveWindow? { get set }

    ///Plug in initialization
    func pluginDidLoad()

    ///Plug in uninstall
    func pluginDidUnload()
}

As long as the protocol is implemented and the plugin is added by calling the add: method of baseVC, the corresponding plugin will get the corresponding attribute to call, avoiding the use of singleton or callback to VC through layers.

 func addPlugin(_ plugin: BDTZEditPlugin) {

        plugin.pluginWillLoad()

        plugin.editViewController = self

        plugin.mainView = self.view

        plugin.liveWindow = liveWindow

        plugin.streamingContext = streamingContext

        plugin.timeline = timeline

        if plugin.conforms(to: BDTZEditViewControllerDelegate.self) {
            pluginDispatcher.add(subscriber: plugin as! BDTZEditViewControllerDelegate)
        }
        plugin.pluginDidLoad()
    }

    func removePugin(_ plugin: BDTZEditPlugin) {

        plugin.pluginWillUnload()

        plugin.editViewController = nil

        plugin.mainView = nil

        plugin.liveWindow = nil

        plugin.streamingContext = nil

        plugin.timeline = nil

        if plugin.conforms(to: BDTZEditViewControllerDelegate.self) {
            pluginDispatcher.remove(subscriber: plugin as! BDTZEditViewControllerDelegate)
        }
        plugin.pluginDidUnload()
    }

plugin is an intermediate layer between specific functions and VC. It can accept VC life cycle events, preview playback events, get key objects in VC, and call all internal public interfaces of VC. As an independent sub function unit inserted in VC, it has the ability of editing, material, network UI interaction and so on.

The plugin is divided into service layer and UI layer. At the beginning of design, the plugin based on this architecture can not only be used in Duca app, but also other apps in the plant can access the plugin immediately with little workload.

All functions can be distributed into plug-ins, assembled and reused on demand.

At the same time, not only a single plugin, but also a combination of multiple plugins can be output. Taking the cover function as an example, cover editing is a controller organized by coverVC, which contains multiple plugins, such as existing text plugins and sticker plugins; In addition to being an independent functional application, coverVC is packaged into a cover plugin, which can be integrated into the general clip VC with only a small amount of data docking code (the general clip data docking plugin in the above figure) and assembled like Lego blocks.

4, Event status management

Due to the complexity of interaction, the editing tool app is very dependent on state update. Generally speaking, the following methods are used to notify object state changes in iOS development:

  • Delegate

  • KVO

  • NotificationCenter

  • Block

These four methods can manage state changes, but there are some problems. Delegate and Block often create strong dependencies between components; KVO and Notifications will create invisible dependencies. If some important messages are removed or changed, they are difficult to find, thus reducing the stability of the application.

Even Apple's MVC model only advocates the separation of the data layer and its presentation layer, without providing any tool code and guiding architecture.

4.1 why choose Flux architecture mode

So we learn from the idea of flux architecture mode. Flux is a very lightweight architecture mode. Facebook uses it for client Web applications to avoid MVC and support one-way data flow (the MVC data flow diagram of the front end listed later). The core idea is centralized control, which allows all requests and changes to be issued only through action s and uniformly distributed by the dispatcher. The advantage is that the View can be kept highly concise. It does not need to care about too much logic, but only about the incoming data. Centralization also controls all data, which can facilitate query and positioning in case of problems.

  • Dispatcher: handles event distribution and maintains dependencies between stores

  • Store: responsible for storing data and processing data related logic

  • Action: trigger Dispatcher

  • View: view, which is responsible for displaying the user interface

As can be seen from the above figure, Flux is characterized by one-way data flow:

  1. The user initiates an Action object to the D dispatcher in the View layer

  2. The Dispatcher receives the Action and asks the Store to make corresponding changes

  3. Store makes corresponding updates, and then issues a changeEvent

  4. View received changeEvent   Update page after event

  • Basic MVC data flow

  • Complex MVC data

  • Simple Flux data flow

  • Complex Flux data flow

Compared with MVC mode, Flux has more arrows and icons, but there is a key difference: all arrows point to one direction and form an event transmission chain in the whole system.

4.2 apply Flux thought to realize state management

There are two states:

  • State changes are generated by events sent by the organization controller, such as the life cycle of the controller, ViewDidLoad(), and callbacks of basic editing preview capabilities, such as seek, progress, playState changes, and so on

  • For the state changes caused by the event transmission between components, the plugin protocol in the figure below abstracts to describe the role of the Store in the figure above

The controller holds the object dispatcher capable of EventDispatch and passes events through this dispatcher.

Dispatcher

class WeakProxy: Equatable {

    weak var value: AnyObject?
    init(value: AnyObject) {
        self.value = value
    }

    static func == (lhs: WeakProxy, rhs: WeakProxy) -> Bool {
        return lhs.value === rhs.value
    }
}

open class BDTZActionDispatcher<T>: NSObject {

    fileprivate var subscribers = [WeakProxy]()

    public func add(subscriber: T) {
        guard !subscribers.contains(WeakProxy(value: subscriber as AnyObject)) else {
            return
        }
        subscribers.append(WeakProxy(value: subscriber as AnyObject))
    }

    public func remove(subscriber: T) {
        let weak = WeakProxy(value: subscriber as AnyObject)
        if let index = subscribers.firstIndex(of: weak) {
            subscribers.remove(at: index)
        }
    }

    public func contains(subscriber: T) -> Bool {
        var res: Bool = false
        res = subscribers.contains(WeakProxy(value: subscriber as AnyObject))
        return res
    }

    public func dispatch(_ invocation: @escaping(T) -> ()) {
        clearNil()
        subscribers.forEach {
            if let subscriber = $0.value as? T {
                invocation(subscriber)
            }
        }
    }


    private func clearNil() {
        subscribers = subscribers.filter({ $0.value != nil})
    }
}

The event is distributed to the internal objects of subscribers through generic multi proxy (addPlugin: add subscribers internally in the above code Block). Of course, it can also be implemented by registering Block.

Dispatcher instance

Declare that a protocol inherits the capabilities to be distributed

@objc protocol BDTZEditViewControllerDelegate: BDTZEditViewLifeCycleDelegate, StreamingContextDelegate, BDTZEditActionSubscriber {
// BDTZEditViewLifeCycleDelegate controller declaration cycle
// StreamingContextDelegate preview editing capability callback
// Communication protocol between BDTZEditActionSubscriber plugin
}

Controller event distribution

public class BDTZEditViewController: UIViewController {
// Instantiated BDTZEditViewControllerDelegate
var pluginDispatcher = BDTZEditViewControllerDelegateImp()
  public override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        pluginDispatcher.dispatch { subscriber in
            subscriber.editViewControllerViewDidAppear?()
        }
    }

    public override func viewDidLoad() {
        super.viewDidLoad()
       /***Omit some codes**/
        setupPlugins()
        //Last call
        pluginDispatcher.dispatch { subscriber in
            subscriber.editViewControllerViewDidLoad?()
        }
    }
    /***...**/
    ///seek progress callback
    func didSeekingTimelinePosition(_ timeline: Timeline!, position: Int64) {
        pluginDispatcher.dispatch { subscriber in
            subscriber.didSeekingTimelinePosition?(timeline, position: position)
        }
    }
   /***...**/
}

Event passing between plugin s

The above BDTZEditActionSubscriber protocol is used for event transmission between plugin s.

@objc protocol BDTZEditAction {
}
@objc protocol BDTZEditActionSubscriber {
    @objc optional func update(action: BDTZEditAction)
}

BDTZEditAction is an empty protocol, which can be inherited by any class to describe any information you want to pass. Combined with the characteristics of editing tools (although the interaction is complex, but the material types and operations are limited), all States can be described with only a small number of actions. At present, we use these events to describe the operations of adding, deleting, moving, clipping and saving some columns of the draft. Let's take the selected action as an example:

When APlugin sends a selected event, BPlugin and CPlugin will receive this event and make corresponding state changes.

//APlugin
func sendAction(model: Any?) { 
       let action = BDTZClipSeleteAction.init(event: .selected, type: .sticker, actionTarget: model)
       editViewController?.pluginDispatcher.dispatch({ subscriber in
            subscriber.update?(action: action)
        })
}
//BPlugin
extension BDTZTrackPlugin: BDTZEditActionSubscriber {
    func update(action: BDTZEditAction) {
        if let action = action as? BDTZClipSeleteAction {
            handleSelectActionDoSomething()
        }
    }
}

When the sticker in the preview area is selected, the axis area will also be selected, and the bottom area will be switched to a three-level menu** After an action is dispatched, all plugins will receive it, and the plugins interested in this action will make corresponding state changes.
**

5, Summary

iOS also has a ReSwift framework designed according to the idea of flux, but if it is developed using pure flux mode, the disadvantages are also very obvious:

  1. With too many levels, it is easy to produce a lot of redundant code.

  2. The workload of old code migration is huge.

For us, adopting the design concept of Flux mode is more important than a specific implementation framework. According to the characteristics of Toka business, we just take its idea and use a single-layer structure to manage the relationship and event transmission between ViewController and plugin abstraction, without adding View to the hierarchy. Any architecture such as MVC and MVVM can be used in the plugin, Just unify the communication mode.

The above is just a simple example to introduce the application of editing tools in Flux thought. However, in practical use, it should also consider:

  1. UI level masking problem: a View in the plug-in needs to be added to the controller View, which will cause the control level masking problem. BDTZEditLevelView in the above code is to solve this problem.

  2. Multithreading problem: in development, it is inevitable that a large number of threads process tasks asynchronously. We must specify the threads between plug-in communication, and the Dispatcher should also have thread management code.

  3. plugin dependency problem: the Dispatcher also needs to maintain the dependency between plugins. For example, an action needs to be processed by APlugin first, and then processed by BPlugin after modifying some data or status. It can be solved by labeling.

  4. action inflation: compared with the method of direct API call, listening for actions may write less code, but it is easy to cause an unlimited increase in actions. Therefore, extensibility and structure should be considered in defining actions.

Reference link:

[1]http://reswift.github.io/ReSwift/master/getting-started-guide.html

[2]https://facebook.github.io/flux/

[3]https://redux.js.org

[4]http://blog.benjamin-encz.de/post/real-world-flux-ios/?utm_source=swifting.io&utm_medium=web&utm_campaign=blog%20post

Recommended reading:

Practice of online symbolization of iOS crash log

| high availability construction method and practice of Baidu Commercial Hosting page system

Application of AI in video field - Bullet screen piercing people

---------- END ----------

Baidu Geek said

The official account of Baidu technology is on the line.

Technology dry goods, industry information, online salon, industry conference

Recruitment Information · internal push information · technical books · Baidu peripheral

Welcome to pay attention

Topics: Swift iOS Back-end architecture