DSL custom bullet box for Android development

Now most of the projects will have a pop-up component, the purpose of which is to manage and maintain the logic or ui involved in the pop-up scene in a unified way. Define one, resulting in a flood of bullet frames, but call the api provided by the component to render a bullet frame that meets the design specification. If the design specification is updated, just update the component, then all the bullet boxes can be updated together, saving one by one Modified time. On the other hand, because the pop-up component is used by almost everyone in the entire team, its advantages and disadvantages will be exposed, so how to design a pop-up component is a problem that every developer must consider. At present, there are two common design methods for bullet frame components:

common design

One-click generation using constructor

This is a design method that will pass the bullet box title, bullet box content, bullet box button copy, bullet box button click event to the constructor, and then reload several functions to support some specific scenarios such as no title, single Buttons, color of copywriting, etc., if I usually take over a project, if this project is developed by multiple people, I will take the initiative to take over the task of developing the pop-up box component, not because writing pop-up boxes is addictive, but mainly because I am worried that others will use this method Writing a frame is difficult to say, and it is really a nightmare to do. The advantages and disadvantages of this method are summarized as follows

  • Pros: Unknown
  • shortcoming:
    • From the perspective of code, the readability is relatively poor, and a large number of input parameters will confuse the caller when filling in the parameters, and do not know what function a specific parameter corresponds to.
    • For maintainers, every time a component needs to change an element, it is necessary to modify the logic of each constructor, which is a heavy workload and error-prone.
    • For the caller, a large number of parameters need to be written each time, and the declaration order of the parameters needs to be strictly followed. If the component updates the function signature, a compilation error will be generated at the call site

Chain calls using the builder pattern

Another design method is to use the builder mode, which is also my usual method. All the elements in the bullet box are exposed to a method one by one, so that the caller can set it, and set which element needs to be used. Element, a set of styles is implemented by default inside the component. If some elements are not set by the caller, the implementation method that comes with the component will be used by default. However, this method also has advantages and disadvantages, summarized as follows

  • Advantages: Separate functions with functions, clear functions, and the caller can selectively call the corresponding function to render the bullet box according to his own needs
  • Disadvantage: The maintainer needs to constantly add new methods to the component for the caller to use according to new requirements. For example, if you want to make the title bold, if the component does not provide the corresponding setTitleBold method, the caller will not be able to realize this function. After multiple rounds of iterations, various methods may have been accumulated in the components. If they are not properly classified and managed, it will be a headache to read

The third design method

In view of the two design methods mentioned above and the advantages and disadvantages summarized above, we can't help but have a question, this method is not good, and that method is not very good, so is there no better design method for such commonly used components? , can be designed to meet the following requirements

  • Components are extremely extensible, and callers can freely define the functions they need
  • The maintainer does not need to frequently add functions to the component to maintain the stability of the component
  • Clear structure, each code block is responsible for the functionality of a component element

Definition of DSL

To achieve the above points, we will use the key DSL of this article. What is a DSL? It is a domain-specific language: a computer language that specifically solves a specific problem. For example, our commonly used regular expressions are a A DSL, which is different from our commonly used api, has its own unique structure, also called grammar. In Kotlin, we use lambda expressions to complete this structure

lambda with receiver

Before using DSL to customize the pop-up box, let's look at an example. When we first came into contact with kotlin, we must have come into contact with the let and apply functions in its standard library, and also learned the difference between these two functions by rote. It has also been used in actual development. For example, if there is a button, we need to set its copy, font size and click event. Generally, we will do this

We see that every time we access an attribute of the button, we need to write the button repeatedly. If there are more attributes to access, the code will be very verbose, so at this time, the let and apply functions come in handy

We can see that the difference between the two is reflected in the lambda expression behind let. Use it to display instead of button. If button needs to change the variable name, we only need to change the button on the left of let, and after apply In the expression, it is completely omitted. The scope of the entire expression is button, which can directly access the properties of button. While we keep this difference in mind, should we also think about why there is such a difference between these two functions? Woolen cloth? The answer lies in the source code of these two functions, let's take a look

We see that the biggest difference between the two function source codes is that the input parameter of let is a parameter of function type whose parameter is T, so in the lambda expression we can use it to replace T, while the input parameter of apply is slightly different. Its input parameter is also a function type, but T is moved to the front of the parentheses, as a receiver to accept the result returned in the lambda expression, so only its attributes and values, structure and its Streamlined, and the main grammatical point of the DSL in kotlin is the lambda with receiver, now we take this grammatical point and start to customize our bullet box step by step

start development

First of all, let's start with a simple implementation of an AlertDialog box

One of the characteristics of AlertDialog is the use of the builder mode. After each setting function is completed, it will be returned to AlertDialog.Builder. From this point, we can convert the process of generating Dialog into one with a receiver, just like the apply function. lambda expression, the first thing to do is to add an extension function to AlertDialog.Builder, which internally receives a parameter of a lambda expression with a receiver

Now we can use the new createDialog function to change the code that just generated AlertDialog

The function of createDialog is similar to the function apply. The scope of the lambda code block is AlertDialog.Builder, which can access any function in AlertDialog.Builder. We can simplify the above code, and use createDialog as a top-level function to generate AlertDialog.Builder inside the function For example, the top-level function is as follows

And the code where the pop-up box is called is also changed to

Run the code and we will get a bullet box that comes with the system

But such a pop-up frame, I think few domestic designers would like it, so according to the visual diagram given by the designer, customizing the pop-up frame on the existing basis is what we will do next, leaving aside some specific In business scenarios, a pop-up component needs to have the following functions

  1. The layout of the bullet box can be customized in style, such as rounded corners and background color
  2. The title of the bullet box can be customized, such as copywriting, font color, size
  3. The content of the bullet box can be customized, such as copywriting, font color, size
  4. The number of popup buttons can be configured as one or two

Popup Layout

The first step is to make the layout of the pop-up frame. For a pop-up frame component, the designer will design all the pop-up frame styles in advance, so the general style of the overall layout is fixed. We use a simple dialog_layout layout file Style as popup

The entire layout structure is very simple, from top to bottom are the title, content, and button area. Next, we set the layout in the bullet box in the lambda expression of the top-level function createDialog, and make the width of the bullet box equal to the screen width Proportionally adaptive, after all, the width of the bullet box in different apps is not necessarily the same

The effect is as follows

A pure white bullet box comes out. Next, let’s simplify the code. Since every time the bullet box is called, dialog.show and the following codes for setting the width and the position of the bullet box will be called, so in order to avoid repetition, the wheel is repeatedly built. We can add an extension function to AlertDialog, and put all these codes in the extension function. The upper layer only needs to call this extension function. We will name the extension function showDialog, and the code is as follows

The place where the upper layer calls the pop-up box becomes

Isn’t it a lot more streamlined? The effect of the code running is the same, so we won’t show it, but at present our frame is just a normal style. If we want to set a rounded corner for it, and then add some backgrounds with gradient effects ,how should I do it? The first thing we thought of was to make a drawable file, write these styles in it, and then set it to the background of the root view of the layout. This is indeed a way, but if one day the designer has a whim and thinks In some scenarios, the bullet box uses style A, and in some scenarios, style B is used. Is it possible to generate a new drawable file? In this way, a single bullet box component needs to maintain two style files, which brings more troubles to project maintenance. There is a certain cost, so we have to think of a better way, which is to use GradientDrawable to dynamically style the layout. The method is as follows

Seeing that the red frame and the green frame are used to distinguish the two parts of the code in the code, let’s look inside the red frame first, and we can see that it is mainly for rendering. A GradientDrawable instance is generated, and then the background color is set for it respectively. Gradient direction, rounded corner size, and we can replace this with a lambda expression with a receiver. GradientDrawable is the receiver. Looking at the green box, although there are not many codes now, but before setView, you must check the elements in the view Do a series of operations such as initialization, so view is also a receiver, and operations such as initialization can be performed in lambda expressions. After clarifying these, we add an extension function rootLayout of AlertDialog.Builder

The rootLayout function receives three parameters in total, root is our pop-up view, render is the rendering operation, and job is the operation of initializing the view. For the rendering operation, rootLayout has implemented a set of default styles. If the caller does not use The render function, the pop-up box uses the default style. If the render function is used, then the same attribute in the render will be overwritten, and the new attribute will be added. At this time, the upper-level caller code will be changed to

Let's run it to see the effect

It is exactly the same as the effect we want to set. Now let’s try not to use the default style. We want the rounded corners on the top of the bullet box to be 12dp, and there are no rounded corners on the bottom. The background gradient changes from gray to white from left to right. , we add these settings in the render function

After running, the effect becomes

popup title

With the development experience of the bullet box layout, the title is much easier. Since the receiver of the job function is the View, then we first define an extension function title for the View

This function is specially used to operate on the relevant parts of the title, and the parameter of title is a lambda expression whose receiver is TextView, which is used to add additional settings to the title on the caller. Now we can add a title to the bullet box By the way, turn the four corners of the frame into rounded corners to look better

Added a dark bold title, where the textColor attribute is an extended attribute I added to make the code look neater, and the effect is equivalent to setTextColor(getColor(R.color.color_303F9F))

Run it again, the title will come out

It seems that the title is a bit too high, let's add a 10dp inner margin to the bullet box as a whole to see the effect

The effect is out, let's go to the next step

popup content

With the example of the title, the content of the pop-up box is basically the same, so don’t say much and directly upload the code

Then add a piece of text to the bullet box

The effect is as follows

popup button

Usually, the bullet box component has two types: a single button bullet box (prompt type) and two button bullet boxes (interactive type). In our dialog_layout layout, there are two TextViews used as buttons respectively. The negativeBtn on the left is hidden by default. , the positiveBtn on the right is displayed. Here I follow the logic of setting buttons in AlertDialog. When only calling setPositiveButton, it means that it is a single button popup box. When calling setNegativeButton at the same time, it means two The pop-up frame of a button, we also borrow this idea here, define two functions to control these two buttons

The code is very simple. Of course, you can also add some default styles to the function. For example, positiveBtn is generally a highlight color value, and negativeBtn is a gray color value. Now let’s call these two functions and first display a bullet box with only one button

Just call the positiveBtn function like Alertdialog, and the rendering is as follows

When we want to display two buttons on the bullet box, we only need to add another negativeBtn, like this

The next step is to set the listening event for the button, which is very easy, just call setOnClickListener

In this way, the work can actually be done. After clicking the pop-up box, you can do some business logic and make the pop-up box disappear, but only in this way, there are still some unreasonable design in our code.

  • After each createDialog, the pop-up box must be shown after the Dialog. This can be done by the component itself without the caller going to showDialog every time.
  • rootLayout returns the AlertDialog.Builder object, you must call create to get the AlertDialog object to operate the display and hide of the bullet box, these should also be placed in the component
  • The default action of clicking the button of the bullet box is basically to close the bullet box, so there is no need to call the dismiss function every time it is displayed in the click event, or you can put the closing action in the component

Then we need to change the rootLayout function so that its return value changes from AlertDialog.Builder to Unit, and the above-mentioned create and showDialog operations must be performed in rootLayout. The changed code is as follows

mDialog is a top-level attribute maintained in the component. This is also to close the pop-up box inside the component when the pop-up button is clicked. Next, we start to process the click event of the pop-up button. Since the click event is applied to the TextView, so First add an extension function clickEvent to TextView to handle the logic of closing the bullet box and other click events

Now we can go back to the caller, update the code of the bullet box, and add the new clickEvent function to positiveBtn and negativeBtn respectively as a click event, and a Toast will pop up as a response event after positiveBtn is clicked

createDialog(this) {
    rootLayout(
        root = layoutInflater.inflate(R.layout.dialog_layout, null),
        render = {
            orientation = GradientDrawable.Orientation.LEFT_RIGHT
            colors = intArrayOf(
                getColor(R.color.color_BBBBBB),
                getColor(R.color.white)
            )
            cornerRadius = DensityUtil.dp2px(12f).toFloat()
        }
    ) {
        title {
            text = "DSL弹框"
            typeface = Typeface.DEFAULT_BOLD
            textColor = getColor(R.color.color_303F9F)
        }
        message {
            text = "用DSL方式自定义的弹框用DSL方式自定义的弹框用DSL方式自定义的弹框用DSL方式自定义的弹框"
            gravity = Gravity.CENTER
            textColor = getColor(R.color.black)
        }
        positiveBtn {
            text = "知道了"
            textColor = getColor(R.color.color_FF4081)
            clickEvent {
                Toast.makeText(this@MainActivity, "开始处理响应事件", Toast.LENGTH_SHORT).show()
            }
        }
        negativeBtn {
            text = "取消"
            textColor = getColor(R.color.color_303F9F)
            clickEvent { }
        }
    }
}

At this point, our bullet box component is done, and the source code of AlertDialog.kt is pasted by the way

The source code of the bullet box component

lateinit var mDialog: AlertDialog
var TextView.textColor: Int
    get() {
        return this.textColors.defaultColor
    }
    set(value) {
        this.setTextColor(value)
    }

fun createDialog(ctx: Context, body: AlertDialog.Builder.() -> Unit) {
    val dialog = AlertDialog.Builder(ctx)
    dialog.body()
}

@RequiresApi(Build.VERSION_CODES.M)
inline fun AlertDialog.Builder.rootLayout(
    root: View,
    render: GradientDrawable.() -> Unit = {},
    job: View.() -> Unit
) {
    with(GradientDrawable()){
        //默认样式
        render()
        root.background = this
    }
    root.setPadding(DensityUtil.dp2px(10f))
    root.job()
    mDialog = setView(root).create()
    mDialog.showDialog()
}

inline fun View.title(titleJob: TextView.() -> Unit) {
    val title = findViewById<TextView>(R.id.dialog_title)
    //可以加一些标题的默认操作,比如字体颜色,字体大小
    title.titleJob()
}

inline fun View.message(messageJob: TextView.() -> Unit) {
    val message = findViewById<TextView>(R.id.dialog_message)
    //可以加一些内容的默认操作,比如字体颜色,字体大小,居左对齐还是居中对齐
    message.messageJob()
}

inline fun View.negativeBtn(negativeJob: TextView.() -> Unit) {
    val negativeBtn = findViewById<TextView>(R.id.dialog_negative_btn_text)
    negativeBtn.visibility = View.VISIBLE
    negativeBtn.negativeJob()
}

inline fun View.positiveBtn(positiveJob: TextView.() -> Unit) {
    val positiveBtn = findViewById<TextView>(R.id.dialog_positive_btn_text)
    positiveBtn.positiveJob()
}

inline fun TextView.clickEvent(crossinline event: () -> Unit) {
    setOnClickListener {
        mDialog.dismiss()
        event()
    }
}

fun AlertDialog.showDialog() {
    show()
    val mWindow = window
    mWindow?.setBackgroundDrawableResource(R.color.transparent)
    val group: ViewGroup = mWindow?.decorView as ViewGroup
    val child: ViewGroup = group.getChildAt(0) as ViewGroup
    child.post {
        val param: WindowManager.LayoutParams? = mWindow.attributes
        param?.width = (DensityUtil.getScreenWidth() * 0.8).toInt()
        param?.gravity = Gravity.CENTER
        mWindow.setGravity(Gravity.CENTER)
        mWindow.attributes = param
    }
}

Summarize

Some people may have already discovered that the calling method of our bullet box is very similar to Compose and React, which is the recently popular declarative UI. Why is it popular and easier to use than our traditional imperative UI? The main difference The reason is that the declarative UI caller only needs to care about the description of the view, but the caller does not need to care about how the real view is rendered or measured. In our example of the pop-up box, all the caller needs to do is to face the visual For the manuscript, it is enough to write the elements in the bullet box and the required attribute styles one by one. Even if the requirements of the bullet box change frequently in the later stage, it is only a matter of adding or subtracting a few element attributes for the caller, and what about the bullet box? Setting a custom view, how to measure the width ratio between the screen and so on, does not need the caller to care, so this method can be gradually learned, adapted, and used in our future development, not only in writing This declarative UI is only used in projects such as React, Flutter or Compose

Author: Coffeeee
Link: https://juejin.cn/post/7204601386607706172

at last

If you want to become an architect or want to break through the 20-30K salary range, then don't be limited to coding and business, but you must be able to select models, expand, and improve programming thinking. In addition, a good career plan is also very important, and the habit of learning is very important, but the most important thing is to be able to persevere. Any plan that cannot be implemented consistently is empty talk.

If you have no direction, here I would like to share with you a set of "Advanced Notes on the Eight Major Modules of Android" written by the senior architect of Ali, to help you organize the messy, scattered and fragmented knowledge systematically, so that you can systematically and efficiently Master the various knowledge points of Android development.

insert image description here
Compared with the fragmented content we usually read, the knowledge points of this note are more systematic, easier to understand and remember, and are arranged strictly according to the knowledge system.

Full set of video materials:

1. Interview collection
insert image description here
2. Source code analysis collection

insert image description here
3. Collection of open source frameworks

insert image description here
Welcome everyone to support with one click and three links. If you need the information in the article, just click on the CSDN official certification WeChat card at the end of the article to get it for free↓↓↓

Guess you like

Origin blog.csdn.net/Eqiqi/article/details/129300239