Blanche
Engineer
Engineer
  • UID619
  • Fans2
  • Follows2
  • Posts59
Reads:878Replies:0

XCoreRedux framework: componentized Android UI and Redux practice

Created#
More Posted time:Oct 28, 2016 9:29 AM
Open this project in Android Studio.
The directory is structured as follows:
• demo
A small demo written based on the XCore framework.
• xcore
The core code base of XCoreRedux.
• pics
The picture resources of the documents.
Preface
• Thoughts on code architecture in Android development
I recently studied many front-end frameworks, such as React, Flux and Redux. React and Redux are both popular front-end frameworks. Android-wise, Google doesn’t seem to care about it very much and there is no universally-recognized outstanding architecture in the industry. Similar to the front-end frameworks, complicated data state management also nags the Android development. Having understood what Store, Reducer and Action mean, and based on the Redux+React idea, I proposed a Redux framework based on the Android platform which I name: XCoreRedux. This warehouse is the implementation of XCoreRedux+UIComponent framework. It expresses an idea and your advice and suggestions are welcomed.
About XCoreRedux framework
Similar to the front-end Redux framework, the XCoreRedux framework is illustrated in the figure below:


Action
Action is a valid carrier to pass the data to the Store. It is the only data source of the Store. We usually pass the action to the store through store.dispatch(). An action generally requires two parameters: the type and the data. In the XCoreRedux framework, we define the action as follows:
public class XCoreAction {

    //The action type
    public final String type;
    //The value carried by the action. The value can be null.
    public final Object value;

    public XCoreAction(String type, Object value) {
        this.type = type;
        this.value = value;
    }

    public XCoreAction(String type) {
        this(type, null);
    }

    @Override
    public boolean equals(Object object) {
       ...
    }

    @Override
    public int hashCode() {
        ...
    }
}


In order to manage actions in a uniform way, you can implement an ActionCreator. For example, a creator of the contact service is created in the demo:
public class ContactsActionCreator {

    public static final String ADD_ITEM = "AddContacts";
    public static final String ADD_TITLE = "addCategory";
    public static final String DELETE_LAST = "deleteLast";
    public static final String CHECK_BOX = "contactsCheck";

    public static XCoreAction addContacts(Contacts contacts) {
        return new XCoreAction(ADD_ITEM, contacts);
    }

    public static XCoreAction addCategory(Title title) {
        return new XCoreAction(ADD_TITLE, title);
    }

    public static XCoreAction deleteLast() {
        return new XCoreAction(DELETE_LAST);
    }

    public static XCoreAction checkBoxClick(ContactsWrapper contactsWrapper) {
        return new XCoreAction(CHECK_BOX, contactsWrapper);
    }
}


The concept of Action is easy to comprehend. Next, let's take a look at the Reducer.
Reducer
Literally, Reducer means a “decelerator”. An action describes the event, and a reducer decides how to update the state based on the action. Reducer interface is defined as follows:
public interface IXCoreReducer<State> {
    State reduce(State state, XCoreAction xcoreAction);
}


A new state will be gained from processing the input action and the current state.
(previousState, action) => newState

To be more straightforward, Reducer is a set of a series of pure functions, as shown in the project in the demo:
public class ContactsReducer implements IXCoreReducer<List<XCoreRecyclerAdapter.IDataWrapper>> {

    /**
     * Add a contact
     *
     * @param contactsWrappers
     * @param contacts
     * @return
     */
    private List<XCoreRecyclerAdapter.IDataWrapper> addOneContacts(List<XCoreRecyclerAdapter.IDataWrapper> contactsWrappers, Contacts contacts) {
        ...
        ...
        return wrappers;
    }

    /**
     * Add a title
     *
     * @param contactsWrappers
     * @param value
     * @return
     */
    private List<XCoreRecyclerAdapter.IDataWrapper> addOneTitle(List<XCoreRecyclerAdapter.IDataWrapper> contactsWrappers, Title value) {
        ...
        ...
        return wrappers;
    }

    /**
     * Delete the last one
     *
     * @param contactsWrappers
     * @return
     */
    private List<XCoreRecyclerAdapter.IDataWrapper> deleteLast(List<XCoreRecyclerAdapter.IDataWrapper> contactsWrappers) {
        List<XCoreRecyclerAdapter.IDataWrapper> wrappers = new ArrayList<>(contactsWrappers);
        if (wrappers.size() > 0) {
            wrappers.remove(wrappers.size() - 1);
        }
        return wrappers;
    }

    /**
     * Set the checkbox status
     *
     * @param contactsWrappers
     * @param value
     * @return
     */
    private List<XCoreRecyclerAdapter.IDataWrapper> changeCheckBoxStatus(List<XCoreRecyclerAdapter.IDataWrapper> contactsWrappers, ContactsWrapper value) {
        value.isChecked = !value.isChecked;
        return contactsWrappers;
    }

    @Override
    public List<XCoreRecyclerAdapter.IDataWrapper> reduce(List<XCoreRecyclerAdapter.IDataWrapper> contactsWrappers, XCoreAction xcoreAction) {
        switch (xcoreAction.type) {
            case ContactsActionCreator.ADD_ITEM:
                return addOneContacts(contactsWrappers, (Contacts) xcoreAction.value);

            case ContactsActionCreator.ADD_TITLE:
                return addOneTitle(contactsWrappers, (Title) xcoreAction.value);

            case ContactsActionCreator.DELETE_LAST:
                return deleteLast(contactsWrappers);

            case ContactsActionCreator.CHECK_BOX:
                return changeCheckBoxStatus(contactsWrappers, (ContactsWrapper) xcoreAction.value);
            ...
        }
        return contactsWrappers;
    }
}


From the implementation of the above reducer, we can discover that the Reducer is a set of a series of functions. A key function among them is the Reduce function which executes different methods to process different types of actions.
Store
Literally, Store means storage. In the Redux framework, Store has no relationships with the database and file. It doesn't mean persistent storage. Store manages the states of data sources and connects the Action with the Reducer. The responsibilities of the Store include:
• 1. Save the current state of the data source.
• 2. Provide the dispatch method to update the state.
• 3. Register listeners through subscribe and notify the observer when the state changes.
• 4. Provide the getState method to get the current state.
Java implementation of the Store:
public class XCoreStore<State> {
    private final IXCoreReducer<State> mIXCoreReducer;//Data processor - Reducer
    private final List<IStateChangeListener<State>> listeners = new ArrayList<>();//Observer
    private volatile State state;// Stored data by the Store

    private XCoreStore(IXCoreReducer<State> mIXCoreReducer, State state) {
        this.mIXCoreReducer = mIXCoreReducer;
        this.state = state;
    }

    /**
     * Internal dispatch
     *
     * @param xCoreAction
     */
    private void dispatchAction(final XCoreAction xCoreAction) throws Throwable {
        synchronized (this) {
            state = mIXCoreReducer.reduce(state, xCoreAction);
        }
        for (IStateChangeListener<State> listener : listeners) {
            listener.onStateChanged(state);
        }
    }


    /**
     * Create a store
     *
     * @param reducer
     * @param initialState
     * @param <S>
     * @return
     */
    public static <S> XCoreStore<S> create(IXCoreReducer<S> reducer, S initialState) {
        return new XCoreStore<>(reducer, initialState);
    }

    public State getState() {
        return state;
    }


    public void dispatch(final XCoreAction action) {
        try {
            dispatchAction(action);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    /**
     * The registration interface. It adds an observer and notifies the observer when the state changes.
     *
     * @param listener
     */
    public void subscribe(final IStateChangeListener<State> listener) {
        listeners.add(listener);
    }

    /**
     * Logout
     *
     * @param listener
     */
    public void unSubscribe(final IStateChangeListener<State> listener) {
        listeners.remove(listener);
    }

    /**
     * The callback interface when the state changes
     *
     * @param <S> State
     */
    public interface IStateChangeListener<S> {
        void onStateChanged(S state);
    }

}


In Android, a Redux page (Fragment or Activity) only has a single store. When the data processing logic needs to be split, you should use the reducer combination instead of creating multiple stores.
Combination with UIComponent
Similar to the combination of front-end Redux and React, XCoreRedux is used in combination with UIComponent.
UIComponent
In the front-end React framework, we often hear the concept of the UIComponent. In the UIComponent, there are two types of components: the general component and the item component (or called the cell component).
General component
• Single components, such as a custom widget, with a View such as the custom CircleImageView.
• Container components, derived from ViewGroup, including FrameLayout, LinearLayout and RelativeLayout. There are some common list components, such as ListView or RecyclerView components.
General components are encapsulated in the form of FrameLayout in XCore. To compile a general component, you only need to implement the following methods:
• 1.public int getLayoutResId()
It returns the layout resource ID of the component.
• 2.public void onViewCreated(View view)
It initializes the View.
• 3. Implement the IStateChangeListener interface in XCoreStore and perform data binding in onStateChanged. To associate the UIComponent with Store, the UIComponent can implement the IStateChangeListener interface, then act as the observer to observe the changes of the store states and then perform data binding in the onStateChanged method.
Item component (cell component)
For the front end, the item components have no difference with the general components. But for Android or iOS, the item components and general components are essentially different. Taking the RecyclerView as an example, the item components will be reused for the same type. In the XCoreRedux framework, the definition of item components needs to inherit from the XCoreItemUIComponent, as the item component is not a View. The required implementation methods include:
• View onCreateView (LayoutInflater inflater, ViewGroup container); Similar to the onCreateView of Fragment, the method is responsible for creating the layout View of the item.
• void onViewCreated (View view); Similar to the onViewCreated of Fragment, the initialization of the View is written here.
• public String getViewType(); The type of data source of the item component.
• public void bindView (IXCoreComponent coreComponent, XCoreRecyclerAdapter coreRecyclerAdapter, XCoreRecyclerAdapter.IDataWrapper data, int pos); Data binding. When the adapter calls the bindViewHolder method, it calls back the bindView method.
The item components need to be connected with the corresponding list components through the adapter. Targeting the common RecyclerViews of Android, XCoreRedux provides universal plug-in XCoreRecyclerAdapter.
Framework of XCoreRedux with list components


Different from the previous practices, here the entire list is encapsulated into a list component to provide registration items to the outside, such as the XCoreRecyclerViewComponent source code.
public class XCoreRecyclerViewComponent extends XCoreUIBaseComponent implements XCoreStore.IStateChangeListener<List<XCoreRecyclerAdapter.IDataWrapper>> {

    private SwipeRefreshLayout mSwipeRefreshLayout;

    private RecyclerView mRecyclerView;
    private RecyclerView.LayoutManager mLayoutManager;
    private XCoreRecyclerAdapter mXCoreRecyclerAdapter;

    public XCoreRecyclerViewComponent(Context context) {
        super(context);
    }

    public XCoreRecyclerViewComponent(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public XCoreRecyclerViewComponent(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public final int getLayoutResId() {
        return R.layout.xcore_recyclerview_component;
    }

    @Override
    public void onViewCreated(View view) {
        //Initialize the View
        mSwipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.xcore_refresh_layout);
        mSwipeRefreshLayout.setEnabled(false);
        mRecyclerView = (RecyclerView) findViewById(R.id.xcore_rv);
        //Initialize the RecyclerView
        mLayoutManager = new LinearLayoutManager(getContext());
        mRecyclerView.setLayoutManager(mLayoutManager);
        mXCoreRecyclerAdapter = new XCoreRecyclerAdapter(this);
        mRecyclerView.setAdapter(mXCoreRecyclerAdapter);
    }

    public SwipeRefreshLayout getSwipeRefreshLayout() {
        return mSwipeRefreshLayout;
    }

    public RecyclerView getRecyclerView() {
        return mRecyclerView;
    }

    public RecyclerView.LayoutManager getLayoutManager() {
        return mLayoutManager;
    }

    public XCoreRecyclerAdapter getXCoreRecyclerAdapter() {
        return mXCoreRecyclerAdapter;
    }

    /**
     * When the state changes, issue a notification automatically
     *
     * @param status
     */
    @Override
    public void onStateChanged(List<XCoreRecyclerAdapter.IDataWrapper> status) {
        mXCoreRecyclerAdapter.setDataSet(status);
        mXCoreRecyclerAdapter.notifyDataSetChanged();
    }

    /**
     * Provide registration of item components to the outside
     *
     * @param xCoreItemUIComponent
     * @return
     */
    public XCoreRecyclerViewComponent registerItemComponent(XCoreItemUIComponent xCoreItemUIComponent) {
        mXCoreRecyclerAdapter.registerItemUIComponent(xCoreItemUIComponent);
        return this;
    }

    public void setRefreshEnable(boolean enable) {
        mSwipeRefreshLayout.setEnabled(enable);
    }
}


When we are using the component, we only need to:


1. Add a component in the XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <!-- Header component-->
    <com.example.haibozheng.myapplication.components.container.HeaderComponent
        android:id="@+id/recycler_view_header_component"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <!-- List component-->
    <com.github.nuptboyzhb.xcore.components.impl.XCoreRecyclerViewComponent
        android:id="@+id/recycler_view_component"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>


2. Initialization
...
        //Create the store of the data source
        mContactsListXCoreStore = XCoreStore.create(new ContactsReducer(), new ArrayList<XCoreRecyclerAdapter.IDataWrapper>());

        //Create the UI component of the RecyclerView
        mXCoreRecyclerViewComponent = (XCoreRecyclerViewComponent) findViewById(R.id.recycler_view_component);
        //Register the item component template
        mXCoreRecyclerViewComponent.registerItemComponent(new TextItemComponent())
                .registerItemComponent(new ImageItemComponent());

        //Create a header component
        mHeaderComponent = (HeaderComponent) findViewById(R.id.recycler_view_header_component);

        //Add an observer
        mContactsListXCoreStore.subscribe(mXCoreRecyclerViewComponent);
        mContactsListXCoreStore.subscribe(mHeaderComponent);
        ...


Communication between components
The communication between components includes the communications between the item components and the list components as well as between the item components and the general components. The EventBus used in this demo is a light-weight Otto. Every component that inherits from the XCoreUIBaseComponent has been registered or unregistered in onCreate and onDestroy respectively. To use the component, you only need to specify the subscription method using the @Subscribe annotation. Therefore, you can call the method below wherever you want:
XCoreBus.getInstance().post(action);

Minor optimization
I implemented two optimizations in data binding:
1.I packaged data through Wrapper
2.I used UIBinderHelper for stream data binding, such as:
public class ImageItemComponent extends XCoreItemUIComponent implements View.OnClickListener {

    private UIBinderHelperImpl mUIBinderHelperImpl;

    ...

    @Override
    public void bindView(IXCoreComponent coreComponent,
                         XCoreRecyclerAdapter coreRecyclerAdapter,
                         XCoreRecyclerAdapter.IDataWrapper data,
                         int pos) {
        mContactsWrapper = (ContactsWrapper) data;
        mUIBinderHelperImpl.from(R.id.item_content_tv).setText(mContactsWrapper.bindContentText())
                .from(R.id.item_pic_iv).setImageUrl(mContactsWrapper.getAvatarUrl())
                .from(R.id.item_title_tv).setText(mContactsWrapper.bindItemTitle())
                .from(R.id.checkbox).setButtonDrawable(mContactsWrapper.isChecked ? R.mipmap.checkbox_checked : R.mipmap.checkbox_normal)
                .setOnClickListener(this);
    }

    ...
Guest