background

As the complexity of the whole Kuaikan business is getting higher and higher, the code volume is increasing, resulting in higher and higher code complexity. The code volume of an Activity and Fragment is often thousands of lines. Although module division and interface isolation exist, it is difficult to completely avoid the increase of code volume, resulting in the code of the whole page is still bloated. It was against this background that we began to explore a page development framework for the Quick Look business.

The evolution process

MVP

Most of you are familiar with MVP, and Android officially recommends that we use MVP as our development architecture. The official sample address is github.com/android/arc… M (Model) : mainly responsible for data-related operations, V (View) : mainly responsible for UI processing, mainly in the form of Fragment and Activity, P (Presenter) : as the middleware of View and Model, responsible for logic processing and interaction bridge

Advantages:

  • In View layer, only UI-related operations are processed. In View layer, business logic is put in Presenter, which achieves the decoupling effect of UI and logic. It is convenient to manage and beneficial to code reuse

Disadvantages:

  • Through interface interaction, code volume increases, and once the business logic is complex enough, interface explosion and Presenter code volume becomes too large.

MVVM

Advantages:

  • When the Model changes, the View-Model updates automatically, and the View changes automatically

Disadvantages:

  • Because of two-way data binding, each View is bound to a Model. Different module models are basically different, which is not conducive to View reuse

Both MVP and MVVM were more or less inadequate and could not fully meet the current situation of our complex business. We wanted a common framework that could provide the following capabilities:

  • Module decoupling, clear stratification
  • Convenient development and use
  • Performance is good
  • Strong expansibility and reusability
  • Components are automatically aware of the lifecycle
  • Easy communication between components

However, there is no suitable architecture on the market at present, so based on the existing complex business, we sorted out and finally precipitated a set of general framework Arch to meet the needs of quick look complex business.

The Arch frame

The overall page hierarchical structure is shown as follows:

The overall architecture is shown in the figure below:

The core design

BaseArchView

Is a View layer abstraction that represents a life-cycle View, such as our View, Activity, and Fragment, as an entry point to one of our business modules.

BaseDataProvider

For business development in the whole page, there is always some Shared data is passed to the various business modules, such as comic details page, look at various business modules such as pay, advertising, community, etc., Adapter, the ViewHolder, buried point to deal with class requires corresponding cartoon ID, ubiquitous everywhere public parameters. To solve this problem, during the BaseArchView life cycle, the BaseDataProvider will act as the data provider for the BaseArchView. The main purpose is to provide data for any submodule to access during the BaseArchView life cycle. All submodules default to directly holding a reference to the DataProvider, and can access the DataProvodier directly to fetch data and set data to the DataProvider.

BaseMainController

During the life of a BaseArchView, the BaseMainController acts as the manager of the BaseModule and the hub through which the BaseModule accesses each other. All Module bindings are implemented through annotations, which means that when a Module becomes complex enough or needs to be aggregated internally, the Module can be upgraded to become a SubMainController, which then aggregates other modules internally.

BaseModule

During the life cycle of a BaseArchView, Module is a sub-module managed by MainController. For each sub-business, we can unify it into a Module. For example, in the daily fragment of the homepage, We can divide it into advertising Module, dynamic recommendation Module, operation Module and so on

BaseMvpPresent

During the life of a BaseArchView, if a Module uses MVP mode, you can bind BaseMvpPresent in the BaseModule to handle some of the business logic associated with that Module.

BaseMvpView

During the lifetime of a BaseArchView, if a Module uses MVP mode, you can bind the BaseMvpView to the Present to handle the UI logic associated with the Present

BaseDataRepository

It mainly provides data loading logic, network request, database request, file operation, etc

The dynamic extension

As the smallest unit of a business Module, Module has strong operability.

  • When business is simple, we can perform business operations directly within the Module
  • When the business is complex, we can use the MVP pattern internally with BindPresent annotations, and of course, the MVVM pattern
  • When the business reaches a very complex level, this Module can be upgraded to SubMainController, and the internal complex business can be split into modules

In this way, under Arch, the page framework, no matter how complex the business is and how deep the business hierarchy is, the whole framework can disassemble and bind the business modules layer by layer, just like a tree, through Controller as a link.

Application scenarios

So in the quick look page development framework, how do we develop a page? Take look at the popular Daily TAB in the App for example.

Take a look at the directory structure after development with the new architecture:

Sample code:

Bind the Controller and DataProvider in the Fragment

class RecommendByDayFragment : BaseArchFragment() {

    @BindController
    lateinit var recommendByDayController: RecommendByDayController

    @BindDataProvider
    lateinit var recommendByDayDataProvider: RecommendByDayDataProvider
}
Copy the code

Define the business module in Controller

class RecommendByDayController: BaseMainController<Unit>() {

    @BindModule(moduleClass = CacheRecommendModule::class)
    lateinit var cacheRecommendModule: ICacheRecommendComic

    @BindModule(moduleClass = AdModule::class)
    lateinit var adModule: IHomeRecommendAd

    @BindModule(moduleClass = CommonModule::class)
    lateinit var commonModule: IRecommendCommon

    @BindModule(moduleClass = MainModule::class)
    lateinit var mainModule: IRecommendModuleMain
}
Copy the code

Note the BindModule annotation above. The parameter must be the actual Class type and the annotated member variable type must be an interface.

Here are two questions you can think about ahead of time:

  1. MainModule::class (APT) : MainModule::class (APT) : MainModule::class (APT)
  2. Why should annotated member variable types be forced to be interfaces?

These two questions will be answered when explaining APT and data communication.

Ok, let’s look at the implementation of one of the submodules:

class CacheRecommendModule : BaseModule<RecommendByDayController, RecommendByDayDataProvider>(), ICacheRecommendComic {

    @BindRepository(repository = CacheTodayRecommendRepository::class)
    lateinit var cacheRepo: IRecommendCacheRepo

   override fun onInit(view: View) {
        super.onInit(view)
    }

    override fun onStartCall() {
        super.onStartCall()
        loadCacheData()
    }
}
Copy the code

When the Fragment is created, it calls the onStartCall method of the CacheRecommendModule, and the module’s logic is ready to execute.

All parameters are implemented based on annotations. With annotations, we can mask the implementation into the annotation processor, which is very convenient to use. With just one annotation, we can bind a module and automatically inject all the necessary parameters.

For annotations, there are three types of annotations in Java, defined by scope:

  1. Yuan comments:

Information about annotations remains in the source code only, and will be lost in Class files after compilation. Compile-time annotations: Annotations are preserved in the Class file, but are not loaded into the VIRTUAL machine at runtime. In Android, compile-time annotations can be used to automate code generation. 3. Runtime annotations: Annotations are also retained in the VIRTUAL machine and can be reflected to obtain the corresponding annotation information.

There are many ways to implement automatic binding for the entire framework. The automatic binding of the whole framework has also been changed in several versions, from the initial full use of reflection to APT+ reflection, and then from APT+ reflection to APT+ASM peg, and finally, APT+ reflection and APT+ASM coexist. The advantages and disadvantages of automatic binding through different ways are introduced below.

Runtime annotations

All annotations use runtime annotations that are parsed within the Activity/Fragment’s onCreate life cycle

open class BaseArchFragment : BaseFragment(), BaseArchView {
      @CallSuper
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ParseBindAnnotation.parse(this, BaseArchFragment::class.java)
        delegate.onCreated()
    }
}

class ParseBindAnnotation {
    companion object {
        fun parse(baseArchView: BaseArchView, endType: Class<*>) {
            val time = System.currentTimeMillis()
            try {
                createEventBusInstance(baseArchView)
                ReflectUtil.traversalClass(baseArchView, endType.superclass) {
                    parseDataProvider(baseArchView, it)
                    parseMainController(baseArchView, it)
                }
                ReflectRelationHelper.clearReflectData(baseArchView)
            } catch (e: Exception) {
                ErrorReporter.get().reportAndThrowIfDebug(e)
            }


            Logger.loggerReflectTime(baseArchView.javaClass.name, System.currentTimeMillis() - time)
        }
}
Copy the code

The parsing of run-time annotations is divided into the following processes

  • Get all the Field annotations of the current class. If we recognize the annotations provided by BaseDataProvider, BaseController, BaseModule, etc., we will create an instance through reflection and inject the necessary parameters.
  • To create an instance of reflection, you need to recursively call the internal member, get the presence of annotations such as BaseModule, create the instance through reflection again, and inject parameters.

Such as the MainController instance created by the current reflection

class BindMainControllerProcessor { companion object { fun create(field: Field, owner: Any, archView: BaseArchView) { val fieldInstance = field.type.newInstance() ReflectUtil.innerSetField(field, owner, fieldInstance) val controller: BaseMainController<*> = fieldInstance as BaseMainController<*> ReflectRelationHelper.registerController(archView, controller) val dataItem = ReflectRelationHelper.findFromController(controller) innerBind(controller, dataItem, owner) } private fun innerBind(controller: BaseMainController<*>, dataItem: ArchReflectDataItem? , owner: Any) { dataItem? :return controller.javaClass.declaredFields.forEach { when { it.isAnnotationPresent(BindModule::class.java) -> { BindModuleProcessor.create(it, controller, dataItem) } it.isAnnotationPresent(BindController::class.java) -> { create(it, controller, dataItem.archViewRef) } } } } }Copy the code
  • Because of the inheritance issues involved, you need to recursively invoke reflection to create the instance and perform the second step again to parse the runtime annotations.

There are significant performance issues with using reflection in full, basically a page with 30+ annotations would take more than 30ms to parse and bind all the members. This performance loss also basically means that this approach will not go live.

APT + reflection

Due to the large impact on performance of binding in the way of reflection, optimization is needed. By referring to the implementation of ButterKnife and EventBus, runtime annotations are changed to compile-time annotations, and the code that needs to perform binding is automatically generated. Finally, reflection calls are made within the parse method. See the autogeneration of the shameshamebydayFragment code above:

class RecommendByDayFragment_arch_binding(
  recommendbydayfragment: RecommendByDayFragment
) {
  init {
    ReflectRelationHelper.registerEventBus(recommendbydayfragment,
        recommendbydayfragment.eventProcessor)
    val recommendByDayController = RecommendByDayController()
    ReflectRelationHelper.registerController(recommendbydayfragment, recommendByDayController)
    recommendbydayfragment.recommendByDayController=recommendByDayController
    ReflectRelationHelper.registerEventBus(recommendbydayfragment,
        recommendbydayfragment.eventProcessor)
    val recommendByDayDataProvider = RecommendByDayDataProvider()
    recommendByDayDataProvider.eventProcessor = recommendbydayfragment.eventProcessor
    ReflectRelationHelper.registerDataProvider(recommendbydayfragment, recommendByDayDataProvider)
    recommendByDayDataProvider.ownerView = recommendbydayfragment
    recommendbydayfragment.registerArchLifeCycle(recommendByDayDataProvider)
    recommendbydayfragment.recommendByDayDataProvider = recommendByDayDataProvider
    recommendByDayDataProvider.parse()
    recommendByDayController.parse()
  }
}

class RecommendByDayController_arch_binding(
  recommendbydaycontroller: RecommendByDayController
) {
  init {
    val archItem = ReflectRelationHelper.findFromController(recommendbydaycontroller)!!
    val cacheRecommendModule = CacheRecommendModule()
    recommendbydaycontroller.cacheRecommendModule = cacheRecommendModule
    cacheRecommendModule.ownerController = recommendbydaycontroller
    cacheRecommendModule.updateDataProvider(archItem.dataProviderRef)
    cacheRecommendModule.ownerView = archItem.archViewRef
    cacheRecommendModule.eventProcessor = archItem.eventBusRef
    archItem.archViewRef.registerArchLifeCycle(cacheRecommendModule)
    cacheRecommendModule.parse()
    ....
  }
Copy the code

In much the same way as Butterknife, in calling the butterknife. bind method, reflection creates an instance of the current class name + the view_binding suffix. When binding Arch members, shameshamebydayFragment only need to create a shameshamebydayFragment_ARCH_binding instance for shameshamebydayFragment_ARCH_binding via reflection to execute the corresponding logical binding.

object InitArchUtils { fun init(className: String, clazzs: Class<*>, objectClass: Any) { try { ReflectUtil.traversalClassByType(clazzs, IArchBind::class.java) { val clazz = Class.forName(it? .canonicalName + "_arch_binding") clazz.getDeclaredConstructor(clazzs).newInstance(objectClass) } } catch (e: Exception) { //ignore } } }Copy the code

Because of the need to recurse all the parent classes, the first build time for a page with a deep hierarchy of inheritance is about 3-5ms.

Remember the problem we mentioned above, APT does not allow to get annotations of non-basic types of parameters, so we need to use unconventional means to get.

val realType: TypeMirror? = AnnotationValueTypeGetUtil.getAnnotationValueType {
                        it.classElement.getAnnotation(BindModule::class.java).moduleClass
                    }
Copy the code

When you look at MirroredTypeException on the moduleClass attribute that got annotations directly, what did you do? We actually use this exception to get TypeMirror rather than the KClass type directly, see the implementation

 fun getAnnotationValueType(accessAnnotationAction: () -> Unit): TypeMirror? {
        var realType: TypeMirror? = null
        try {
            accessAnnotationAction.invoke()
        } catch (e: MirroredTypeException) {
            realType = e.typeMirror
        }
        return realType
    }
Copy the code

Once we get the TypeMirror we can do the following to generate the code

 methodBuilder.addStatement("val $realName = %T()", realType.asTypeName())
Copy the code

The corresponding generated code is val cacheRecommendModule = cacheRecommendModule () above

So how do you get to the point where the parameters have to be defined as interfaces rather than ordinary classes? Only parameter verification is required in the process of APT.

APT+ASM

For the 3-5ms of APT+ reflection, performance still needs to be optimized. Although a layer of cache can be added to reduce the time of the second reflection search, the time of the first call is still quite long, which needs to be reduced by technical means. After many explorations, the final scheme was determined, using APT+AOP technology. This is done by automating code at compile time and then by ASM inserting method calls during the transfrom phase of compile time, replacing the time spent on reflection calls with the time spent on method calls.

Each arch-related subclass inherits the interface IArchBind

interface IArchBind {
    fun parse()
}
Copy the code

During the TranForm phase during compilation, parse inserts are performed as follows:

  • Scan all APT generated code helper classes, all classes ending in ARCH_binding, recorded in memory
  • If yes, execute the parse method to insert a method call. The ASM code is as follows
@Override
public void visitEnd() {
    String archBindingClassName = className + CollectArchBindingDataContainer.ARCH_END_POSIX;
    MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC, "parse", "()V", null, null);
    mv.visitCode();
    mv.visitVarInsn(ALOAD, 0);
    mv.visitMethodInsn(INVOKESPECIAL, superClassName, "parse", "()V");
    mv.visitTypeInsn(NEW, archBindingClassName);
    mv.visitInsn(DUP);
    mv.visitVarInsn(ALOAD, 0);
    mv.visitMethodInsn(INVOKESPECIAL, archBindingClassName, "<init>", "(" + "L" + className + ";" + ")V");
    mv.visitVarInsn(ASTORE, 1);
    mv.visitInsn(Opcodes.RETURN);
    mv.visitMaxs(2, 2);
    mv.visitEnd();
}
Copy the code

After the packaging of APK is completed, decompilation of APK is carried out through JADX, and the code after piling can be viewed as follows:

public final class RecommendByDayFragment {
    public void parse() {
        super.parse();
        RecommendByDayFragment_arch_binding recommendByDayFragment_arch_binding = new RecommendByDayFragment_arch_binding(this);
    }
}
Copy the code

The time statistics of the above three automatic page binding modes are as follows:

After several iterations and optimization of the automatic binding scheme, the automatic binding time of the whole page creation was finally optimized to 0.2ms, achieving the optimal effect.

The life cycle

Generally speaking, Android has a life cycle of activities and fragments. We do some sub-module initialization during page initialization and sub-module destruction when the page is about to be destroyed. The awareness of the life cycle of modules in the framework is very important. Once the awareness of the life cycle is realized, modules in the framework can be self-initialized and self-destroyed. In the Arch framework, all the members DataProvider, BaseModule, etc. can sense the life cycle of the corresponding ArchView.

Life cycle details


interface IArchLifecycle {
    fun onHandleCreate()
    fun onInit(view: View)
    fun onStartCall()
    fun onStart()
    fun onResumed()
    fun onPaused()
    fun onStop()
    fun onViewDestroy()
    fun onHandleDestroy()
}
Copy the code

The Activity life cycle corresponds to the ArchView life cycle


Activity                            BaseArchView

onCreate()              ===>      onHandleCreate()
setContentView()        ===>      onInit()
                                  onStartCall()
onStart()               ===>      onStart()
onNewIntnt()            ===>      onNewIntnt()
onResume()              ===>      onResumed()
onPaused()              ===>      onPaused()
onStop()                ===>      onStop()
onDestroy()             ===>      onViewDestroy()
                                  onHandleDestroy()
Copy the code

The Fragment life cycle corresponds to the ArchView life cycle

Fragment                          BaseArchView
onCreate()              ===>      onHandleCreate()
onCreateView()          ===>      onInit()
                                  onStartCall()
onStart()               ===>      onStart()
onResume()              ===>      onResumed()
onPaused()              ===>      onPaused()
onStop()                ===>      onStop()
onViewDestroy()         ===>      onViewDestroy()
onDestroy()             ===>      onHandleDestroy()
Copy the code

The above life cycle has two more life cycles than common activities and fragments: onInit() and onStartCall().

What are the implications of these two life cycles? Why do we need these two more life cycles?

  • meaning

OnInit: indicates the initialization method of the submodule

OnStartCall: Indicates that the onInit method of all submodules has been executed and the logical calls can begin.

  • Why do you need these two more life cycles

The main reason is to solve the initialization timing and call dependency problems of each submodule.

In the case of complex business, there are too many modules. If the Create method alone cannot guarantee who completes initialization first and who completes initialization last, once one module calls another uninitialized module, some unknown problems may occur. It is common practice to give priority to module initialization, but this approach is difficult to maintain and extend. So we add two new life cycles, and in the onInit method we only perform in-module initialization, and we can’t call other submodules. After all the onInit methods have been executed, the onStartCall callback can start doing the access logic for the other modules, ensuring that whatever needs to be initialized has been initialized.

In BaseArchView, we also provide a state that will capture the current Lifecycle of the BaseArchView.

enum class LifecycleState {
    Created,
    Init,
    StartCall,
    Start,
    Resume,
    Pause,
    Stop,
    ViewDestroy,
    Destroyed
}
fun getCurrentLifeCycleState(): LifecycleState
fun isState(lifecycleState: LifecycleState): Boolean
Copy the code

Message communication

After a page is created, the message communication mechanism is indispensable, and we have the following communication methods for the entire page.

Event bus

EventBus, an EventBus commonly used in Android, has the following advantages:

  • Data transfer is simple
  • The receiving thread can be set dynamically
  • The data passed can be customized,
  • You can decouple

When the entire page is set up, a local EventBus of EventBus is set up at the same time. The so-called local EventBus is the event sent in the current page, which can only be received in the current page. Scope is only within the current page. For the types of events sent by EventBus, we make two distinctions:

  • Iactionevents are represented as notification events and do not require stateful caches, such as ON_LOAD_MORE (drop refresh) and RELOAD_DATA(reload data).
  • IChangeEvent: indicates the event of data modification, which needs to have the persistence of data status, such as the danmaku switch, and data modification of comic details page

Events can be sent through the eventProcessor bound to all submodules

In order to standardize and simplify the complexity of sending and receiving events, each member in each page can easily send and receive events.

Override fun handleActionEvent(type: IActionEvent, data: Any?) {} override fun handleDataChangeEvent(type: IChangeEvent, data: Any?) {} / / events send eventProcessor. PostLocalActionEvent (RecommendActionEvent CACHE_DATA_LOAD_SUCCEED, null)Copy the code

Explicitly call

The message communication approach of the event bus addresses communication decoupling without an explicit target to be invoked. If two modules are explicitly dependent, they can be called explicitly. By default, each Module has a reference to the MainController, which can be used to explicitly call interfaces provided by other modules. Such as:

recommendByDayController.mainModule.getRecommendMainFilterView().dismissFilterList()
Copy the code

The remaining question is: should annotated member variable types be forced to be interfaces? The Arch framework does not want all the functions of a Module to be exposed to other modules because of explicit calls, so by defining the functions allowed to be exposed in the interface, other modules are only allowed to access the functions defined in the interface, which not only increases Module security, but also increases reusability.

RecyclerView

In many businesses, the complexity of activities and fragments is also the complexity of list data. When the list data is complex enough, there may be dozens of different viewtypes in a recyclerView, and there may be mutual influence between holders of multiple viewtypes. The Arch framework has been optimized for list operations.

Unify the data type of the Adapter

The Adapter itself is data-driven, creating and binding different Viewholders based on different sets of data. Data definitions are necessarily inconsistent among different Adapters for different services, so the first step is to unify the data model and isolate data differences.

class ViewItemData<T>(var viewType: Int, var data: T?) {
    companion object {
        const val TYPE_UNKNOWN = -1
    }
}
Copy the code

ViewType: indicates the type of the ViewHolder to be created. Data: indicates the data object received by the ViewHolder. After the data is unified, the common logic of all service Adapters can be abstracts out the same logic.

Unify adapter-related operations

For different business scenarios, each Adapter provides different capabilities, but there are many common methods. How do we abstract out common methods and provide a common API?

interface IAdapter { fun refreshDataList(dataList: List<ViewItemData<out Any? > >? fun addDataList(dataList: List<ViewItemData<out Any? > >? fun addItemData(data: ViewItemData<out Any? >? fun removeData(position: Int) fun insertData(data: ViewItemData<out Any? >? , dataIndex: Int): Boolean fun insertListData(startIndex: Int, data: List<ViewItemData<out Any? > >? : Boolean fun replaceData(data: ViewItemData<out Any? >? , dataIndex: Int): Boolean fun clear() fun isEmpty(): Boolean fun getAllData(): MutableList<ViewItemData<out Any? >> fun getDataFromIndex(position: Int): ViewItemData<out Any? >? fun getAllListViewType(): List<Int> fun getDataListByViewTypes(viewTypes: List<Int>): List<ViewItemData<out Any? >> fun getRealPosition(position: Int): Int fun registerCreateFactory(factory: CreateFactory): IAdapter fun registerCreateFactory(factory: CreateFactory, viewTypes: ArrayList<Int>): IAdapter fun unRegisterCreateFactory(factory: CreateFactory): IAdapter fun registerBindFactory(factory: BindFactory): IAdapter fun registerBindFactory(factory: BindFactory, viewTypes: ArrayList<Int>): IAdapter fun unRegisterBindFactory(factory: BindFactory): IAdapter fun registerScrollListener(callBack: IScrollListener) fun unRegisterScrollListener(callBack: IScrollListener) }Copy the code

In our page development framework, if a module has a list element, this set of interfaces can be used for list element manipulation.

Adapter logical decoupling

In the old business, the logical coupling of the Adapter was heavy and responsible for the creation and binding of all viewholders. For example, the following code is sure to exist in your project

@Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { switch (viewType) { case 1: return 1; case 2: return 2; case 3: return 3; . } } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) { final int noHeaderPosition = getNoHeaderPosition(position); switch (getItemViewType(position)) { case 1: xxxxx break; case 2: xxxxxx break; case 3: xxxxxx break; . }}Copy the code

We expect this business to be broken up into business modules that are responsible for data insertion, ViewHolder creation, ViewHolder binding, and data deletion. So how do you implement this ViewHolder distribution?

  • The ViewHolder creation factory is first set up in the Adapter to support adding a registration to the Module
Interface CreateFactory {fun createHolder(parent: ViewGroup, viewType: Int): recyclerView. ViewHolder? }Copy the code
  • Distribution is made in the Adapter’s careteViewHolder and bindViewHolder.
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val holder = createHolder(parent, viewType) if (holder ! = null) { return holder } val factoryHolder = holderFactoryContainer.dispatchCreateHolder(parent, viewType) if (factoryHolder ! = null) {return factoryHolder} if (logutils.sdebugbuild) {throw IllegalArgumentException(" Data exception ~ viewType is $viewType  ~") } else { return BaseEmptyViewHolder<Any>(parent, R.layout.empty_holder) } }Copy the code

Create a ViewHolder based on the ViewType in the onCreateViewHolder of the Adapter.

  • The first step is to see if the subclass intercepts the type, and if the subclass has already returned a ViewHolder of that type, it returns it directly

  • See if there is a registered HolderFactory based on ViewType. If there is, call HolderFactory createHolder and return

  • If there is no place to process the ViewType, then the current data is abnormal. The Debug environment throws an exception, and the online environment defaults to using a placeholder holder with width and height 0.

  • Register the HolderFactory and create ViewHolder in each Module.

override fun onStartCall() {
        super.onStartCall()
        getAdapter().registerCreateFactory(this, arrayListOf(TYPE_DYNAMIC_RECOMMEND_TOPICS))
                .registerScrollListener(this)
    }

    override fun createHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder? {
        return RecommendHomeDayDynamicViewHolder(parent, R.layout.layout_home_day_dynamic_topics_vh)
    }
Copy the code

The creation logic in the ViewHolder of the page development framework is shown below:

ViewHolder logical decoupling

The complexity of a ViewHolder is often overlooked, but when a ViewHolder’s business is complex enough, it can reach thousands of lines of code. For this scenario, we also support Mvp mode for ViewHolder for minimal granularity of splitting and reuse. The logic of each Viewholder can also be detached.

BaseArchViewHolder

The ViewHolder base class in the Arch framework, which supports MVP automatic binding capabilities, is mainly used for UI-related operations.

BaseArchViewHolderPresent

Presenter base class in the Arch framework for handling business logic. All of you are probably going to see it more or less because of ViewHolder reuse, because in RecyclerView, you’re going to reuse ViewHolder for performance, to reduce the repetition of UI creation. To solve this problem, the Arch framework ensures that the Presenter will not be reused. Each time the ViewHolder executes onBind, the Presenter will be recreated to ensure that the Presenter will not have a logical exception because the Presenter has some dirty data.

In the Arch framework, the development logic for a ViewHolder is shown below:

Also, we provide a very convenient set of functions for Presenters

  • The life cycle
  • Automatic data injection
  • Visibility callback
  • Event communication

ViewHolder life cycle

For common uses of the ViewHolder, we usually use AttachWindow listening based on its itemView to register and de-register functions.

itemView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
    override fun onViewDetachedFromWindow(v: View?) {}
    override fun onViewAttachedToWindow(v: View?) {}
Copy the code

In BaseArchViewHolderPresent, we redefined the life cycle

fun onStartCall() {}
fun onShown(firstShow: Boolean) {}
fun onHide() {}
fun onRecycled() {
Copy the code
  • OnStartCall: The ViewHolder UI is initialized, the data binding is complete, and the operation logic can begin
  • OnShown: ViewHolder The view is displayed
  • OnHide: ViewHolder is not visible
  • OnRecycled: ViewHolder is recycled

Automatic injection of data corresponding to ViewHolder

A ViewHolder corresponds to a ViewType, which also corresponds to a Data, so we can easily retrieve the Data from the ViewHolder and the corresponding Present by declaring the Data type when BaseArchHolderPresent is defined. You can get the actual data type in the onBind method.

class RecommendComicPresent : BaseArchHolderPresent<Comic, IRecommendByDayAdapter, RecommendByDayDataProvider>() { override fun onStartCall() { data ? : return recommendComicView? .refreshView(data!!) adjustViewState() } }Copy the code

ViewHolder visibility callback

In many businesses, there are logic changes that depend on exposure and disappearance timing, so we’ve incorporated in the present the ability to call back visibility, the logic calculation of internal opportunity View exposure, to provide visibility callbacks.

interface IHolderStatus: IHolderLifecycle {
    fun onShown(firstShow: Boolean)
    fun onHide()
}
Copy the code

If you have to do some logic depending on the visible or invisible time, you can directly copy these two methods. For example, you want to do exposure reporting while visible.

override fun onShown(firstShow: Boolean) { data? .let { trackHomepageComicExposure(it, realPosition) } }Copy the code

Data communication

In complex operations, there is always a ViewHolder that needs to retrieve some data from an Activity or Fragment, and some Fragment or Activity data changes that need to respond to the ViewHolder’s UI changes. There are countless levels of data pass through and callback pass through. Therefore, data communication between ViewHolder and Fragment and Activity needs to be opened.

  • Presenter supports retrieving global data for a page from the dataProvider by default
  • Presenter supports local event sending and event receiving by default

After using the Arch framework, ViewHolder uses the following simple example, such as the comic card Holder from the daycheck page above:

class RecommendComicHolder(parent: ViewGroup, id: Int): BaseArchViewHolder<Comic>(parent, id), IRecommendComicView, IRecommendComicLikeView {

    @BindPresent(present = RecommendComicPresent::class)
    lateinit var comicPresent: IRecommendComicP
    @BindPresent(present = RecommendComicCommentPresent::class)
    lateinit var commentPresent: IRecommendComicCommentPresent
    @BindPresent(present = RecommendComicLikePresent::class)
    lateinit var likePresent: IRecommendComicLikePresent
}
Copy the code

To split the different business logic into different presenters, ViewHolder is only responsible for UI-related operations.

conclusion

Arch framework is precipitated in the quick look complex business development, greatly improved the project of chaotic code. At present, existing new business modules are developed based on this framework, which greatly improves the overall research and development efficiency.

Arch framework not only ensures the decoupling of modules, improves the reuse of the whole module, but also ensures the scalability through the Controller layer of the framework layer. Besides, APT automatically generates code to ensure the convenience of use, and then ASM inserts method calls to improve the running performance to the best. It also provides automatic awareness of life cycle and convenient communication between components.

Of course, no architecture is perfect; it’s best for your business.