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 into
Component
- each
Component
Views are private and cannot be accessed externally. You can only modify the state of the Component, but not the Component view -
Component
Internal 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?
-
Component
How cent? -
Component
What do I need to pass in? -
Component
Where to put it? -
Component
How to write the internal data stream? -
Component
Exposed to what? How? -
Component
How is the internal state managed?
Let’s look at a graph to illustrate
Recyclerview is used as a Recyclerview. Each Item in the Recyclerview can be treated as a Component
Furthermore, the book details items in this Component can be treated as child Components
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
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)
-
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
-
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
-
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
-
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
-
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
-
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
-
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