Android View Component architecture design thinking

Refactoring can remember

Why refactor?

  • The DataBinding framework currently used in the project severely limits the compilation speed, and the DataBinding framework has the problem of confusing error prompts, which greatly reduces the development efficiency when errors occur (which is fast even when they are correct).
  • When I tried to adapt Freeline to the latest DataBinding, I encountered great resistance. The possibility of implementation was very low, and only partial compatibility was achieved. Therefore, it required long full compilation, and the development efficiency was very low
  • The Freeline adaptation of the Kotlin delta was successful, so we started to develop in Kotlin (except kapt), preparing for a mass migration to kotlin
  • Some of the previous logic has chaos problems, and the coupling relationship between modules needs to be further sorted out

What to do?

  • Use your own observer framework instead of Google’s own DataBinding for data flow
  • Use Kotlin to write refactoring code and partially replace Java code
  • Remove some of the painless annotation processing frameworks and remove the AROUTER and Butterknife before extensive application

Think first => what architecture

What architecture should I use MVP MVVM?

  • MVP is a popular architecture for Android applications, because full decoupling is widely used in the industry, and the pain is that it requires a number of interfaces to regulate the behavior of each layer to further decouple. Interfaces can also be used for unit testing. There is not enough effort in the current project to write unit tests, and there is no need to replace the Model or some other layer, so use the MVP architecture (if you have an MVP scenario) that abstractions only the View interface.
  • The MVVM architecture is gradually popularized in android with the introduction of DataBinding architecture. ViewModel, as the data rendering layer, takes on the responsibility of rendering model to view, and uses DataBinding to associate it with view. The design principle of MVVM is that the ViewModel layer doesn’t hold a View reference, plus the DataBinding has limited functionality and some parts are extremely painful, you can do efficient development but sometimes they are extremely painful. Of course, I personally like the MVVM architecture and DataBinding thinking very much, Therefore, it is also the architecture of reconstructing the main module of the former Micro Beiyang

So both architectures have their own pain points, can there be a new way of architecture design

There are!

The React framework calls each UI Component a Component. Each Component maintains its own view and some logic inside, and exposes the state instead of the view. The Component UI and other details can be changed by changing the Component state. The Component exposed state is the smallest set of states that can determine the global state. The rest of the Component states can be pushed out by changing the exposed state. Of course this should be responsive, automatic.

Of course, you can’t write anything like JSX on Android, but if it’s anything like it, it’s Anko’s DSL layout framework, which allows you to write views inside components.

But it’s ok to write views in Xml and then find those views during Component initialization. (Because Anko’s DSL is still a controversial layout, although my custom Freeline does kotlin’s incremental compilation within 10 seconds, DSL still has a lot of holes)

Having said that, what exactly does this architecture look like?

  • All view components are abstracted intoComponent
  • eachComponentViews are private and cannot be accessed externally. You can only modify the state of the Component, but not the Component view
  • ComponentInternal maintenance of the relationship between the view and the state, the recommended use of responsive data flow for binding, some data changes when the corresponding view also changes

As you can see, Components are highly cohesive and expose minimal state, so external changes to Component behavior /view can be made with minimal state (set) modifications, making external calls extremely convenient and without logic interference

How to do?

  • ComponentHow cent?
  • ComponentWhat do I need to pass in?
  • ComponentWhere to put it?
  • ComponentHow to write the internal data stream?
  • ComponentExposed to what? How?
  • ComponentHow is the internal state managed?

Let’s look at a graph to illustrate





PNG Screen Shot 2017-10-30 at 11.32.46am.png

Recyclerview is used as a Recyclerview. Each Item in the Recyclerview can be treated as a Component





PNG Screen Shot 2017-10-30 at 11.34.29am


Furthermore, the book details items in this Component can be treated as child Components





PNG Screen Shot 2017-10-30 at 11.36.31am.png

Their XML layout is so simple that we can skip the Component design and start with the smallest item

Because it’s not in Recyclerview, it’s optional to inherit the ViewHolder or not, but for uniformity, it’s optional to inherit the recyclerView.viewholder.

Let’s look at the data Model part of this Component

public class Book {

    Barcode: TD002424561 * Title: Design Psychology. 3, Living with complexity * Author: (美 国) Donald A. Norman * Callno: TB47/N4(5) v.2 * Local: 北 京 园工 程 学 报 * Type: LoanTime: 2017-01-09 * returnTime: 2017-03-23 */

    public String barcode;
    public String title;
    public String author;
    public String callno;
    public String local;
    public String type;
    public String loanTime;
    public String returnTime;

    /** ** How many days to return the book *@return* /
    public int timeLeft(a){
        return BookTimeHelper.getBetweenDays(returnTime);
// return 20;
    }

    /** * to see if the book return date is overdue *@return* /
    public boolean isOverTime(a){
        return this.timeLeft() < 0;
    }

    public boolean willBeOver(a){
        return this.timeLeft() < 7&&! isOverTime(); }}Copy the code

Our requirement is: in this view, there is the name of the book, the return date, and the coloring scheme of the book icon changes color with the distance from the return date

First declare the view and Context that you’re using, okay

class BookItemComponent(lifecycleOwner: LifecycleOwner,itemView: View) : RecyclerView.ViewHolder(itemView) {
    private val mContext = itemView.context
    private val cover: ImageView = itemView.findViewById(R.id.ic_item_book)
    private val name: TextView = itemView.findViewById(R.id.tv_item_book_name)
    private val returntimeText: TextView = itemView.findViewById(R.id.tv_item_book_return)
}
Copy the code

LifecycleOwner is a component from Android Architecture Components that manages the Android lifecycle to avoid component memory leaks

The next step is to declare observable data (also known as states)

    private val bookData = MutableLiveData<Book>()
Copy the code

Because the Component is logically simple and can infer its state by observing the Book class, it is also the smallest set of states for the Component

Here’s a little extra information:

LiveData

and MutableLiveData

also come from the Components of Android Architecture Components, which are life-cycle aware observable dynamic data Components

Sample:

LiveData<BigDecimal> myPriceListener = ... ; myPriceListener.observe(this, price -> {
            // Update the UI. 
        });
Copy the code

And of course kotlin wrote a simple functional extension of it

/** * The kotlin extension for automatic LiveData binding no longer specifies manual overloading of HHH */
fun <T> LiveData<T>.bind(lifecycleOwner: LifecycleOwner, block : (T?). -> Unit) {
    this.observe(lifecycleOwner,android.arch.lifecycle.Observer<T>{
        block(it)
    })
}
Copy the code

Okay, back to business, then we should bind the observable data/state of the View to the Component

init { bookData.bind(lifecycleOwner) { it? .apply { name.text =this.title
                setBookCoverDrawable(book = this)
                returntimeText.text = "Due date:${this.returnTime}"}}}Copy the code
// Here is the function just called to write the details of the dynamic coloring
private fun setBookCoverDrawable(book: Book) {
        var drawable = ContextCompat.getDrawable(mContext, R.drawable.lib_book)
        val leftDays = book.timeLeft()
        when {
            leftDays > 20 -> DrawableCompat.setTint(drawable, Color.rgb(0.167.224)) //blue
            leftDays > 10 -> DrawableCompat.setTint(drawable, Color.rgb(42.160.74)) //green
            leftDays > 0- > {if (leftDays < 5) {
                    val act = mContext as? Activity act? .apply { Alerter.create(this)
                                .setTitle("Book Return Reminder")
                                .setBackgroundColor(R.color.assist_color_2)
                                .setText(book.title + "Please return the book as soon as you have less than five days left.")
                                .show()
                    }
                }
                DrawableCompat.setTint(drawable, Color.rgb(160.42.42)) //red
            }
            else -> drawable = ContextCompat.getDrawable(mContext, R.drawable.lib_warning)
        }
        cover.setImageDrawable(drawable)
    }
Copy the code

Component state changes are implemented by observing LiveData

, so you only need to modify the Book to implement a series of changes related to that Component

And then we just need to expose the correlation function

    fun render(a): View = itemView

    fun bindBook(book: Book){
        bookData.value = book
    }
Copy the code

Then create and call it as needed

val view = inflater.inflate(R.layout.item_common_book, bookContainer, false)
val bookItem = BookItemComponent(life cycleOwner = lifecycleOwner, itemView = view)
bookItem.bindBook(it)
bookItemViewContainer.add(view)
Copy the code

Something more complicated?

Look at the library module of the home page






PNG Screen Shot 2017-10-30 at 11.34.29am

The library module itself is a Component.

Requirements: The icon in the second row shows the ProgressBar when refreshed, the ImageView when refreshed successfully (check box), and the imageView when refreshed incorrectly (wrong image)

  1. This Item is going to be in Recyclerview, so it’s going to inherit the ViewHolder

    class HomeLibItemComponent(private val lifecycleOwner: LifecycleOwner, itemView: View) : RecyclerView.ViewHolder(itemView) {
    }
    Copy the code
  2. Declare the view used in this Component

    class HomeLibItemComponent(private val lifecycleOwner: LifecycleOwner, itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val context = itemView.context
        private val stateImage: ImageView = itemView.findViewById(R.id.ic_home_lib_state)
        private val stateProgressBar: ProgressBar = itemView.findViewById(R.id.progress_home_lib_state)
        private val stateMessage: TextView = itemView.findViewById(R.id.tv_home_lib_state)
        private val bookContainer: LinearLayout = itemView.findViewById(R.id.ll_home_lib_books)
        private val refreshBtn: Button = itemView.findViewById(R.id.btn_home_lib_refresh)
        private val renewBooksBtn: Button = itemView.findViewById(R.id.btn_home_lib_renew)
        private val loadMoreBooksBtn: Button = itemView.findViewById(R.id.btn_home_lib_more)
    
    Copy the code
  3. Declare observable data flows in Component

    private val loadMoreBtnText = MutableLiveData<String>()
    private val loadingState = MutableLiveData<Int> ()private val message = MutableLiveData<String>()
    private var isExpanded = false
    Copy the code
  4. Declare something else to use

    // Query the barcode and book
    private val bookHashMap = HashMap<String, Book>()
    private val bookItemViewContainer = mutableListOf<View>() // Cache view folding inside the LinearLayout to improve efficiency
    private val libApi = RetrofitProvider.getRetrofit().create(LibApi::class.java)
    Copy the code
  5. Establishing a binding relationship

        init {
            // Bind it for a bit
            message.bind(lifecycleOwner) { message ->
                stateMessage.text = message
            }
    
            loadingState.bind(lifecycleOwner) { state ->
                when (state) {
                    PROGRESSING -> {
                        stateImage.visibility = View.INVISIBLE
                        stateProgressBar.visibility = View.VISIBLE
                        message.value = "Refreshing"
    
                    }
                    OK -> {
                        stateImage.visibility = View.VISIBLE
                        stateProgressBar.visibility = View.INVISIBLE
                        Glide.with(context).load(R.drawable.lib_ok).into(stateImage)
    
                    }
                    WARNING -> {
                        stateImage.visibility = View.VISIBLE
                        stateProgressBar.visibility = View.INVISIBLE
                        Glide.with(context).load(R.drawable.lib_warning).into(stateImage)
                    }
                }
            }
     
            loadMoreBtnText.bind(lifecycleOwner) {
                loadMoreBooksBtn.text = it
                if (it == NO_MORE_BOOKS) {
                    loadMoreBooksBtn.isEnabled = false}}}Copy the code
  6. Write a callback to OnBindViewHolder (this will be done manually, considering the interface specification)

    fun onBind(a) {
            refreshBtn.setOnClickListener {
                refresh(true)
            }
            refresh()
            renewBooksBtn.setOnClickListener {
                renewBooksClick()
            }
            loadMoreBooksBtn.setOnClickListener { view: View ->
                if (isExpanded) {
                    // The LinearLayout remove will be linear, so the LinearLayout will be traversed from back to front
                    (bookContainer.childCount - 1 downTo 0)
                            .filter { it >= 3 }
                            .forEach { bookContainer.removeViewAt(it) }
                    loadMoreBtnText.value = "Display residual (${bookItemViewContainer.size - 3})"
                    isExpanded = false
                } else{(0 until bookItemViewContainer.size)
                            .filter { it >= 3 }
                            .forEach { bookContainer.addView(bookItemViewContainer[it]) }
                    loadMoreBtnText.value = "Fold display"
                    isExpanded = true}}}Copy the code
  7. All that remains is the implementation of the method depending on how you like it to be handled. For example, I like coroutines to handle network requests, and then use LiveData to handle mapping of multiple requests

    For example, a simple network request and cache encapsulation

    object LibRepository {
        private const val USER_INFO = "LIB_USER_INFO"
        private val libApi = RetrofitProvider.getRetrofit().create(LibApi::class.java)
    
        fun getUserInfo(refresh: Boolean = false): LiveData<Info> {
            val livedata = MutableLiveData<Info>()
            async(UI) {
                if(! refresh) {val cacheData: Info? = bg { Hawk.get<Info>(USER_INFO) }.await() cacheData? .let { livedata.value = it } }val networkData: Info? = bg { libApi.libUserInfo.map { it.data}.toBlocking().first() }.await() networkData? .let { livedata.value = it bg { Hawk.put(USER_INFO, networkData) } } }return livedata
        }
    
    }
    Copy the code

8. Compositions with other components can be integrated with each other simply by passing in the synchronized view and the corresponding LifecycleOwener

   data? .books? .forEach { bookHashMap[it.barcode] = itval view = inflater.inflate(R.layout.item_common_book, bookContainer, false)
     val bookItem = BookItemComponent(lifecycleOwner = lifecycleOwner, itemView = view)
     bookItem.bindBook(it)
     bookItemViewContainer.add(view)
 }
Copy the code

Summary: state binding, data observation

In the development of the Component of the library, a series of state changes of the Component can be realized simply by changing the relevant state values and observable data streams when initiating various tasks and processing the information returned by the tasks. So the Component currently does not expose any states or views. Realize data flow and high cohesion within the module. The in-module data flow can greatly simplify the code, avoiding some of the clutter that can be caused by direct operations on the View, such as exception handling

private fun handleException(throwable: Throwable?). {
        // The card display status during error handlingthrowable? .let { Logger.e(throwable,"Home page library module error")
            when (throwable) {
                is HttpException -> {
                    try {
                        valerrorJson = throwable.response().errorBody()!! .string()val errJsonObject = JSONObject(errorJson)
                        val errcode = errJsonObject.getInt("error_code")
                        val errmessage = errJsonObject.getString("message")
                        loadingState.value = WARNING
                        message.value = errmessage
                    } catch (e: IOException) {
                        e.printStackTrace()
                    } catch (e: JSONException) {
                        e.printStackTrace()
                    }

                }
                is SocketTimeoutException -> {
                    loadingState.value = WARNING
                    this.message.value = "Network timeout... Very desperate"
                }
                else -> {
                    loadingState.value = WARNING
                    this.message.value = "Thick thread honey error."}}}}Copy the code

On receiving the relevant error code, modify the observation values of state and message, and the relevant data flow will be automatically notified to the relevant view based on the initial binding relationship, such as the observation of loadingState:

        loadingState.bind(lifecycleOwner) { state ->
            when (state) {
                PROGRESSING -> {
                    stateImage.visibility = View.INVISIBLE
                    stateProgressBar.visibility = View.VISIBLE
                    message.value = "Refreshing"

                }
                OK -> {
                    stateImage.visibility = View.VISIBLE
                    stateProgressBar.visibility = View.INVISIBLE
                    Glide.with(context).load(R.drawable.lib_ok).into(stateImage)

                }
                WARNING -> {
                    stateImage.visibility = View.VISIBLE
                    stateProgressBar.visibility = View.INVISIBLE
                    Glide.with(context).load(R.drawable.lib_warning).into(stateImage)


                }
            }
        }
Copy the code