Skip to content

UX SDK 5.0 Overview

Siddharth Utgikar edited this page Dec 22, 2020 · 3 revisions

The UXSDK widgets follow the MVVM pattern. Every widget in the UXSDK has two components: The Widget which represents the View and the WidgetModel which represents the ViewModel. The SDK represents the Model.

Table of Contents

Widget

The Widget handles the UI logic. There are two base widget classes that all other widgets inherit from: FrameLayoutWidget and ConstraintLayoutWidget. These both have the same lifecycle, just a different underlying view structure.

Creating a custom Widget

To create a custom Widget, the class must extend from either FrameLayoutWidget or ConstraintLayoutWidget. These are the methods that need to be overridden:

  • void initView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) - Invoked after the view is created. This is where the views and the WidgetModel should be initialized.
  • void reactToModelChanges() - This is where reactions to the data from the WidgetModel should be initialized by calling addReaction(@NonNull Disposable reaction).
  • String getIdealDimensionRatioString() - This returns the ideal dimension ratio as a String in the format "width:height". This can optionally be used in the containing ConstraintLayout as the layout_constraintDimensionRatio attribute for fixed-ratio widgets.

The WidgetModel's setup() method must be called at the beginning of the Widget's lifecycle in order for the WidgetModel to start processing data. Similarly, the cleanup() method must be called at the end of the Widget's lifecycle in order for the WidgetModel to stop processing data.

public class MyWidget extends FrameLayoutWidget {

    private MyWidgetModel widgetModel;

    @Override
    protected void initView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        widgetModel = new MyWidgetModel(DJISDKModel.getInstance(),
                                        ObservableInMemoryKeyedStore.getInstance());
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        widgetModel.setup();
    }

    @Override
    protected void onDetachedFromWindow() {
        widgetModel.cleanup();
        super.onDetachedFromWindow();
    }
}

APIs

List of the Widget APIs
  • protected void addDisposable(@NonNull Disposable disposable) - Add a disposable which is automatically disposed with the view's lifecycle.
  • protected void addReaction(@NonNull Disposable reaction) - Add a reaction which is automatically disposed with the view's lifecycle.
  • protected Consumer<Throwable> logErrorConsumer(@NonNull String tag, @NonNull String message) - Get a throwable error consumer for the given error.

WidgetModel

The WidgetModel gets the data from the SDK and handles the business logic which compiles the data into Flowables that can be consumed by the Widget.

Creating a custom WidgetModel

To create a custom WidgetModel, the class must extend the base WidgetModel class. These are the methods that need to be overridden:

  • void inSetup() - This is where keys should be initialized and bound to data processors by calling bindDataProcessor(@NonNull DJIKey key, @NonNull DataProcessor<?> dataProcessor)
  • void inCleanup() - If the widget needs to do any cleanup to avoid memory leaks, it should be done here.
  • void updateStates() - This method is called every time a data processor is updated with a new value.
public class MyWidgetModel extends WidgetModel {

    public MyWidgetModel(@NonNull DJISDKModel djiSdkModel,
                         @NonNull ObservableInMemoryKeyedStore keyedStore) {
        super(djiSdkModel, keyedStore);
        // Initialize data processors here.
    }

    @Override
    protected void inSetup() {
        // Initialize keys and bind them to data processors here.
    }

    @Override
    protected void inCleanup() {
        // Handle teardown here if necessary.
    }

    @Override
    protected void updateStates() {
        // Handle state updates here.
    }
}

APIs

List of the WidgetModel APIs
  • public void setup() - Set up the widget model by initializing all the required resources. This must be called by the Widget in the beginning of its lifecycle, for example in onAttachedToWindow, to start processing data in the WidgetModel.
  • public void cleanup() - Clean up the widget model by destroying all the resources used. This must be called by the Widget at the end of its lifecycle, for example in onDetachedFromWindow, to stop processing data in the WidgetModel.
  • public Flowable<Boolean> getProductConnection() - Get the product connection status in the form of a flowable which emits true if connected, and false otherwise.
  • protected void addDisposable(@NonNull Disposable disposable) - Add a disposable which is automatically disposed with the model's lifecycle.
  • protected void bindDataProcessor(@NonNull DJIKey key, @NonNull DataProcessor<?> dataProcessor) - Bind the given DJIKey to the given data processor. This data processor will be invoked with every update to the key.
  • protected void bindDataProcessor(@NonNull DJIKey key, @NonNull DataProcessor<?> dataProcessor, @NonNull Consumer<Object> sideEffectConsumer) - Bind the given DJIKey to the given data processor and attach the given consumer to it. The data processor and side effect consumer will be invoked with every update to the key. The side effect consumer will be called before the data processor is updated.
  • protected void bindDataProcessor(@NonNull UXKey key, @NonNull DataProcessor<?> dataProcessor) - Bind the given UXKey to the given data processor. This data processor will be invoked with every update to the key.
  • protected void bindDataProcessor(@NonNull UXKey key, @NonNull DataProcessor<?> dataProcessor, @NonNull Consumer<Object> sideEffectConsumer) - Bind the given UXKey to the given data processor and attach the given consumer to it. The data processor and side effect consumer will be invoked with every update to the key. The side effect consumer will be called before the data processor is updated.
  • protected Consumer<Throwable> logErrorConsumer(@NonNull String tag, @NonNull String message) - Get a throwable error consumer for the given error.
  • protected void addModule(@NonNull BaseModule baseModule) - Add the module to this WidgetModel's lifecycle.

Sub Module

A Sub Module is an abstraction of data from the SDK which is compiled into common logic that can be used by multiple WidgetModels.

Using a Sub Module in your custom WidgetModel

To use a sub module in your custom WidgetModel, just call addModule(@NonNull BaseModule baseModule) during initialization in order for the sub module to be included in the WidgetModel's lifecycle. For example, if you would like to include the FlatCameraModule in your custom WidgetModel, you can use the following code:

public class MyWidgetModel extends WidgetModel {

    private FlatCameraModule flatCameraModule;

    public MyWidgetModel(@NonNull DJISDKModel djiSdkModel,
                         @NonNull ObservableInMemoryKeyedStore keyedStore) {
        super(djiSdkModel, keyedStore);
        flatCameraModule = new FlatCameraModule();
        addModule(flatCameraModule);
    }
}

Creating a custom Sub Module

To create a custom Sub Module, the class must extend the base BaseModule class. These are the methods that need to be overridden:

  • fun setup(widgetModel: WidgetModel) - This is where keys should be initialized and bound to data processors by calling bindDataProcessor(widgetModel: WidgetModel, key: DJIKey, dataProcessor: DataProcessor<*>, sideEffectConsumer: Consumer<Any> = Consumer {})
  • fun cleanup() - If the sub module needs to do any cleanup to avoid memory leaks, it should be done here.
public class MyModule extends BaseModule {

    public MyModule() {
        // Initialize data processors here.
    }

    @Override
    protected void setup(@NonNull WidgetModel widgetModel) {
        // Initialize keys and bind them to data processors here.
    }

    @Override
    protected void cleanup() {
        // Handle teardown here if necessary.
    }
}

APIs

List of the BaseModule APIs
  • protected fun bindDataProcessor(widgetModel: WidgetModel, key: DJIKey, dataProcessor: DataProcessor<*>, sideEffectConsumer: Consumer<Any> = Consumer {}) - Bind the given DJIKey to the given data processor and attach the given consumer to it. The data processor and side effect consumer will be invoked with every update to the key. The side effect consumer will be called before the data processor is updated.
  • protected fun logErrorConsumer(tag: String, message: String): Consumer<Throwable?>? - Get a throwable error consumer for the given error.

DataProcessors

A DataProcessor emits the most recent item it has observed and all subsequent observed items.

When creating a custom WidgetModel, DataProcessors can be used to create Flowables for each DJIKey or UXKey that is necessary for the widget's logic. When bound to a key using WidgetModel.bindDataProcessor, the processor will receive the value changes of the key and emit them.

public class MyWidgetModel extends WidgetModel {
    private final DataProcessor<FlightMode> flightModeProcessor;

    public MyWidgetModel(@NonNull DJISDKModel djisdkModel, 
                         @NonNull ObservableInMemoryKeyedStore keyedStore) {
        super(djisdkModel, keyedStore);
        flightModeProcessor = DataProcessor.create(FlightMode.ATTI);
    }

    @Override
    protected void inSetup() {
        DJIKey flightModeKey = FlightControllerKey.create(FlightControllerKey.FLIGHT_MODE);
        bindDataProcessor(flightModeKey, flightModeProcessor);
    }

    // ...
}

In addition, DataProcessors can be used to create Flowables for complex logic when the data the Widget needs doesn't come directly from the key. For example, a distance measurement in meters might be converted to feet using UXKeys.UNIT_TYPE before being emitted to the Widget.

private void convertValueByUnit(float altitude) {
    if (unitTypeProcessor.getValue() == UnitConversionUtil.UnitType.IMPERIAL) {
        altitudeProcessor.onNext(UnitConversionUtil.convertMetersToFeet(altitude));
    } else {
        altitudeProcessor.onNext(altitude);
    }
}

Global Preferences

Global Preferences are values stored locally that should persist across app restarts. The UXSDK has an interface to manage all the global preferences called the GlobalPreferencesInterface. This includes all the get and set methods for the functions that need to be stored under the global preferences and be available across app restarts. The UXSDK has a default implementation of the GlobalPreferencesInterface called DefaultGlobalPreferences. The user has the option to implement the interface in a custom way or use the internal DefaultGlobalPreferences implementation to initialize the GlobalPreferencesManager. Once the GlobalPreferencesManager is initialized, the corresponding interface will be used by all the widgets that use the methods in the GlobalPreferencesInterface.

The GlobalPreferencesManager must be initialized in the base Application before initializing the widgets with either the default implementation or a custom implementation. If this is not done, no global preferences will take effect or persist across app restarts and the widgets will only use the default values for all the functionality included under the GlobalPreferencesInterface.

public class SampleApplication extends MultiDexApplication {
    @Override
    public void onCreate() {
        super.onCreate();
        //For the global preferences to take effect, this must be done before the widgets are initialized
        //If this is not done, no global preferences will take effect or persist across app restarts
        GlobalPreferencesManager.initialize(new DefaultGlobalPreferences(this));
    }
}

UXKeys

We have extended the SDK's Keyed Interface into UXSDK. These UXKeys can be used the same way as DJIKeys from the SDK.

GlobalPreferenceKeys

GlobalPreferenceKeys are a subset of UXKeys that correspond to the methods in the GlobalPreferencesInterface. When the value is updated in one widget, any widgets listening to these keys will receive the new value in the data processor. Below is an example of how to listen to the UNIT_TYPE UXKey.

public class MyWidget extends FrameLayoutWidget {

    private MyWidgetModel widgetModel;

    @Override
    protected void initView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        widgetModel = new MyWidgetModel(DJISDKModel.getInstance(),
                                        ObservableInMemoryKeyedStore.getInstance(),
                                        GlobalPreferencesManager.getInstance());
    }
}
public class MyWidgetModel extends WidgetModel {

    private final GlobalPreferencesInterface preferencesManager;
    private final DataProcessor<UnitConversionUtil.UnitType> unitTypeProcessor;
    private UXKey unitKey;

    public MyWidgetModel(@NonNull DJISDKModel djiSdkModel,
                         @NonNull ObservableInMemoryKeyedStore keyedStore,
                         @Nullable GlobalPreferencesInterface preferencesManager) {
        super(djiSdkModel, keyedStore);
        this.preferencesManager = preferencesManager;
        unitTypeProcessor = DataProcessor.create(UnitConversionUtil.UnitType.METRIC);
        if (preferencesManager != null) {
            unitTypeProcessor.onNext(preferencesManager.getUnitType());
        }
    }

    @Override
    protected void inSetup() {
        unitKey = UXKeys.create(GlobalPreferenceKeys.UNIT_TYPE);
        bindDataProcessor(unitKey, unitTypeProcessor);
        if (preferencesManager != null) {
            preferencesManager.setUpListener();
        }
    }

    @Override
    protected void inCleanup() {
        if (preferencesManager != null) {
            preferencesManager.cleanup();
        }
    }

    // ...

    // Set the value of a UXKey to notify other widgets of the updated value.
    private void setUnitType(UnitConversionUtil.UnitType unitType) {
        // Update the value in the preferences manager to persist the change across app restarts
        preferencesManager.setUnitType(unitType);
        // Notify other widgets of the updated value
        addDisposable(keyedStore.setValue(unitType, unitKey).subscribe());
    }
}

Custom UXKeys

To create custom UXKeys, simply extend the UXKeys class and add it using UXKeys.addNewKeyClass.

public final class CameraKeys extends UXKeys {

    @UXParamKey(type = Integer.class)
    public static final String PEAK_THRESHOLD = "PeakThreshold";

}

Panels

Panels are a cluster of widgets designed to contain collections of widgets. The panel types available for use are Bar, Toolbar, and List. See Panel Widget for more information on the Panel architecture.

Modules

UX SDK 5 Beta is split into multiple modules. See UX SDK Modules Wiki.

Hooks

Hooks is a way to observe widget behavior. See UX SDK Hooks Wiki.

Clone this wiki locally