Button's' evolutionary journey '| how do we design the compose API

Android Developers 2021-10-14 06:23:48

This paper is written by Jetpack Compose Team Louis Pullen-Freilich ( Software engineer )、Matvei Malkov ( Software engineer ) and Preethi Srinivas (UX researcher ) Write together .

In the near future Jetpack Compose Released 1.0 edition , Brings a series of tools for building UI The stability of the API. Earlier this year , We released API guide , This paper introduces the compilation of the Jetpack Compose API Best practices and API Design patterns . After many iterations API Interface (API surface) Guidelines formed later , In fact, it does not show the formation process of these design patterns and the story behind our decisions in the iterative process .

This article will take you through a " Simple " Of Button Of " Evolutionary journey ", To learn more about how we iterate design API, Make it easy to use without failure . This process needs to be based on developer feedback , Yes API Several adaptations and improvements have been made to the availability of .

Draw a clickable rectangle

Google Of Android Toolkit One of the team teased : What we do is draw a rectangle with color on the screen , And make it clickable . The fact proved that , This is a UI toolkit One of the most difficult things to achieve in .

One might think that , A button is a simple component : Just a colored rectangle , With a click listener . cause Button API There are many reasons for complex design : discoverability 、 The order and naming of parameters, etc . Another constraint is flexibility : Button Many parameters are provided , Developers can customize various elements at will . Some of these parameters use the configuration of the theme by default , Some parameters can be based on the values of other parameters . This combination makes Button API The design of has become an interesting challenge .

We aim at Button API The first iteration of , By a two-year-old public commit Start . At that time API It looks like this :

@Composable
fun Button( text: String, onClick: (() -> Unit)? = null, enabled: Boolean = true, shape: ShapeBorder? = null, color: Color? = null, elevation: Dp = 0.dp ) {
// Here is the implementation 
}

△ The original Button API

Besides the name , The original Button API Far from the final version of the code . It has gone through many iterations , We will show you this process :

@Composable
fun Button(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
content: @Composable RowScope.() -> Unit
) {
// Here is the implementation 
}

△ 1.0 Version of Button API

Get developer feedback

stay Compose In the early stages of research and experiment , our Button The component can receive a ButtonStyle Parameters of type .ButtonStyle by Button Defines visually relevant configurations , Such as color and shape . This allows us to show three different Material Button type : Introversion (Contained)、 Contour type (Outlined) And plain text (Text); We directly expose the top-level build function , It will return one ButtonStyle example , This instance corresponds to Material The corresponding button type in the specification . Developers can copy these built-in button styles and fine tune them , Or create a new... From scratch ButtonStyle, To completely redesign the custom Button. We are for the initial Button API I was quite satisfied , This API It's reusable , It also includes easy-to-use styles .

To test our assumptions and design methods , We invite developers to participate in programming activities , And use Button API Complete simple programming exercises . Programming exercises include implementing the interface shown in the figure below :

△ What developers need to develop Rally Material Study The interface of

Observations on the development of these codes use Cognitive dimension framework (Cognitive Dimensions Framework) Redo , To evaluate Button API Of Usability .

Soon , We observed an interesting phenomenon : Some developers started using it this way Button API:

Button(text = "Refresh"){
}

△ Use Button API

There are also developers trying to create one Text Components , Then surround the text with rounded rectangles :

// Here we have Padding Composable function , But there are no modifiers 
Padding(padding = 12.dp) {
Column {
Text(text = "Refresh", style = +themeTextStyle { body1 })
}
}

△ stay Text Add Padding To simulate a Button

Style used at that time API, such as themeShape or themeTextStyle, Need to add + Operator prefix . That's because at the time Compose Runtime Caused by certain restrictions . The developer survey shows that : Developers find it difficult to understand how this operator works . The Enlightenment from this phenomenon is , Not under the direct control of the designer API Style will affect developers' understanding of API The cognitive . such as , We know that a developer's comment on the operators here is :

As far as I know , It is reusing an existing style , Or extend based on this style .

Most developers think Compose API There is an inconsistency between —— such as , Yes Button Add styles in the same way Text Components add styles in different ways *.

* Most developers want to add... Before the style " plus ", Use +themeButtonStyle perhaps +buttonStyle, Similar to their understanding of Text Components use +themeTextStyle In the same way .

Besides , We found that most developers are Button When fillet edges are implemented on , Have gone through a painful process , But the original expectation was very simple . Usually , They need to explore multiple levels of implementation code , To understand the API Structure .

I feel like I just stacked some things here at random , No confidence can make it work .

Button{
text = "Refresh",
textStyle = +themeStyle {caption},
color = rallyGreen,
shape = RoundedRectangleBorder(borderRadius = BorderRadius.circular(5.dp.value))
}

△ Correctly customize Button Text style 、 Color and shape

This affects developers' understanding of Button How to set the style . such as , When it comes to Android Application add Button when ,ContainedButtonStyle It can't correspond to the style known by the developer . Click here See early insight videos from developer research .

Through these programming activities , We realize the need to simplify Button API, So that it can implement simple custom operations , At the same time, it supports complex application scenarios . We started working on discoverability and personalization , These two points bring us a series of challenges : Style and naming .

keep API The consistency of

In our programming activities , Style brings a lot of problems to developers . To see why , Let's go back to why the concept of style exists in Android Framework and other toolkits .

" style " Is essentially related to UI A collection of related attributes , Can be applied to components ( Such as Button). Style has two main advantages :

1. take UI Configuration is separated from business logic

In the command toolkit , Defining styles independently helps to separate concerns and make the code easier to read : UI Can be defined in one place , such as XML In file ; Callbacks and business logic can be defined and associated elsewhere .

Similar to Compose In the declarative toolkit , Will reduce business logic and... Through design UI The coupling of . image Button Such a component , Mostly stateless , It just displays the data you pass . When the data is updated , You don't need to update its internal status . Because components are also functions , Can be passed to Button Function parameters are passed to realize customization , Just like other functions . But this will increase UI The difficulty of separating configuration from function configuration . such as , Set up Button Of enabled = false , Not only control Button The function of , And control Button Whether or not shown .

This raises a question : enabled It should be a top-level parameter , Or should it be passed as an attribute in the style ? And for what can be used for Button What about other styles of , such as elevation, Or when Button Be ordered on time , Its color changes ? Design available API One of the core principles of is consistency . We found that in different UI In the component , Guarantee API Consistency is very important .

2. Customize multiple instances of a component

In a typical Android View In the system , The style has great advantages , Because the cost of creating a new component is high : You need to create a subclass , Implement the construction method , And enable custom properties . Styles allow for a more concise way , To express a series of shared properties . such as , Create a LoginButtonStyle, To define the appearance of all login buttons in the application . stay Compose in , The implementation is as follows :

val LoginButtonStyle = ButtonStyle(
backgroundColor = Color.Blue,
contentColor = Color.White,
elevation = 5.dp,
shape = RectangleShape
)
Button(style = LoginButtonStyle) {
Text(text = "LOGIN")
}

△ Define styles for login buttons

Now you can UI All kinds of Button Upper use LoginButtonStyle, Without having to be in every Button Set these parameters explicitly on the . However , If you also want to extract text , Let all login buttons display the same text : "LOGIN", What to do ?

stay Compose in , Each component is a function , So the conventional solution is to define a function , One call Button, And for Button Provide correct text :

@Composable
fun LoginButton( onClick: () -> Unit, modifier: Modifier = Modifier ) {
Button(onClick = onClick, modifier = modifier, style = LoginButtonStyle) {
Text(text = "LOGIN")
}
}

△ Create a that expresses its meaning semantically LoginButton function

Due to the inherent statelessness of components , The cost of refining functions in this way is very low : Parameters can be directly from the encapsulated function , The button passed to the inside . Because you don't inherit a class , So just expose the required parameters ; The rest can stay in LoginButton In the internal implementation of , So as to avoid color and text being overwritten . This method is applicable to many custom scenarios , Beyond the scope of the style .

Besides , Compared to the Button Set up LoginButtonStyle, Create a LoginButton function , Can have more semantic meaning . We also found that : Compared to the style , Independent functions are more discoverable .

No style ,LoginButton It can now be refactored directly into Button The ginseng , Without using style objects , This will be consistent with other custom operations :

@Composable
fun LoginButton( onClick: () -> Unit, modifier: Modifier = Modifier ) {
Button(
onClick = onClick,
modifier = modifier,
shape = RectangleShape,
elevation = ButtonDefaults.elevation(defaultElevation = 5.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Blue, contentColor = Color.White)
) {
Text(text = "LOGIN")
}
}

△ The final LoginButton Realization

In the end, we Get rid of the pattern , And flatten the parameters into the component —— On the one hand, for the sake of the whole Compose Consistency of design , The other is to encourage developers to create more semantic features " encapsulation " function :

@Composable
inline fun OutlinedButton(
modifier: Modifier = Modifier.None,
noinline onClick: (() -> Unit)? = null,
backgroundColor: Color = MaterialTheme.colors().surface,
contentColor: Color = MaterialTheme.colors().primary,
shape: Shape = MaterialTheme.shapes().button,
border: Border? =
Border(1.dp, MaterialTheme.colors().onSurface.copy(alpha = OutlinedStrokeOpacity)),
elevation: Dp = 0.dp,
paddings: EdgeInsets = ButtonPaddings,
noinline children: @Composable() () -> Unit
) = Button(
modifier = modifier,
onClick = onClick,
backgroundColor = backgroundColor,
contentColor = contentColor,
shape = shape,
border = border,
elevation = elevation,
paddings = paddings,
children = children
)

△ 1.0 In version OutlinedButton

Improve API Discoverability or visibility of

We also found that , There is a major flaw in how to set the button shape . To customize Button The shape of the , Developers can use shape Parameters , It can accept a Shape object . When developers need to create a new button with chamfer , This can usually be achieved by :

  1. Create a simple... With default values Button
  2. from MaterialTheme.kt Refer to the content related to the theme setting of shapes in the source file
  3. Look back MaterialButtonShapeTheme function
  4. find RoundedCornerShape, And use a similar method to create a corner with a corner shape

Most developers will be confused here , Browsing a lot of API And source code , Often at a loss . We find it hard for developers to find CutCornerShape, This is because it is from with others shape API Exposed in different bags .

Visibility is used to measure when developers achieve their goals , The difficulty of locating a function or parameter . It is directly related to the cognitive process and effort required to write code ; The deeper the path used to explore, discover and use a method ,API The worse the visibility . Final , This leads to lower efficiency and poor developer experience . Based on this recognition , We take CutCornerShape transfer To other shape API In the same package , To support easy discoverability .

Map the developer's working framework

Then there's more feedback —— We are in a series of further programming activities , Reassessed Button API The usability of . In these activities , We use Material Design The button is named according to the definition of the button in : Button Turn into ContainedButton To match it in Material Design The characteristics of . then , We tested the new naming , And the whole Button API, And evaluated two main developer goals :

  • establish Button And handle click events
  • Use predefined Material The theme is Button Add the style

△ material.io Medium Material Button

△ material.io Medium Material Button

We got a key lesson from the developer activities —— Most developers are not familiar with Material Button Naming conventions in . such as , Many developers can't distinguish ContainedButton and OutlinedButton:

ContainedButton What does that mean ?

We found that when entering Button, And see three of the automatic completion suggestions Button When the component , Developers spend a lot of energy guessing which is what they need . Most developers want the default button to be ContainedButton, Because this is the most commonly used one , And most like " Button " One of the . So it's clear that we need a default setting , So that developers can use it directly without reading Material Design Guide to . Besides , View based MDC-Android Button The default is the fill button , This is also a precedent for using it as the default button .

Describe the role more clearly

The study found that , Another puzzling point is that there are two existing Button Version of : One Button One... Is acceptable String Type as text , And one Button A modifiable... Is acceptable lambda Parameters , Represents common content . The intention of this design is to provide... From two different levels API:

  • With text Button Simpler , Easier to implement
  • More advanced Button, Its content is more open

We found that when developers choose between the two , There will be some difficulties : But from String Overload transferred to lambda Heavy load , Customize " Steep cliff " The existence of , Make incremental customization Button Become challenging . We often hear developers ask for in String Overload is Button increase TextStyle Parameters .

It allows you to customize the internal TextStyle Without having to use lambda Overloaded version .

We provide String The intention is to simplify the implementation of the simplest use cases , But this prevents developers from using with composable lambda overloaded , Instead, ask String Overloading adds extra functionality . These two separate API The existence of , It not only causes confusion for developers , It also shows that overloading with primitive types does have some fundamental problems : They accepted the original type , such as String, Not composable lambda type .

One step code

Original type Button Overloading takes text directly as an argument , Reduces the need for developers to create textual Button The code you need to write . We initially used a simple String Type as a text parameter , But it turns out String Types make it difficult to add styles to some of the text .

For such needs ,Compose Provides AnnotatedString API, To add custom styles to different parts of the text . However , It adds a certain cost to simple application scenarios , Because developers first need to String Convert to AnnotatedString. This also makes us consider whether we should provide new Button heavy load , It can be accepted that String As a parameter , acceptable AnnotatedString As a parameter , To support simple and more advanced requirements .

our API Design discussions are more complex in terms of pictures and icons , For example, when FloatingActionButton When you need pictures or icons .icon The type of the parameter should be Vector still Bitmap? How to support icons with animation ? Even if we do our best , Finally found that we can only support Compose The types available in —— Any third-party image type requires developers to implement their own overloads to provide support .

Side effects of tight coupling

Compose One of the biggest advantages is composability . Create composable functions to separate concerns at less cost , Build reusable and relatively independent components . Through composable lambda heavy load , You can see this idea intuitively : Button Is a container for clickable content , But it doesn't care what it's about .

But for overloads of primitive types , The situation becomes complicated : Accept text parameters directly Button, Now you need to be responsible for being a clickable container , You need to Text Components are passed to the inside . This means that it now needs to manage the public API Interface , This also raises another important question : Button What text related parameters should be exposed ? It will also Button and Text Public API Interfaces are bound together : If the future Text Added new parameters and functions , Does that mean Button There is also a need to increase support for these new content ? Tight coupling is Compose One of the problems you're trying to avoid , And it is difficult to answer this question on all components in a unified way , This also leads to public API Interface inconsistency .

Support work framework

Overloading primitive types allows developers to avoid using composable lambda heavy load , At the cost of less custom space . But when developers need to overload the original type , Realize customization that cannot be realized originally ? The only choice , Is to use composable lambda heavy load , then , Copy the internal implementation code from the original type overload , And make corresponding modifications . We found in our research that , Custom action " Steep cliff " It prevents developers from using more flexible 、 Combinable API, Because the operation between levels is more challenging than before .

Use "slot API" solve the problem

After listing the above problems , We decided to get rid of Button The original type overload of , For each Button Leave only composable for content lambda Parametric API. We began to put this general API The form is called "slot API", It has been widely used in various components .

Button(backgroundColor = Color.Purple) {
// Any composable content can be written here 
}

△ With blank "slot" Of Button

Button(backgroundColor = Color.Purple) {
Row {
MyImage()
Spacer(4.dp)
Text("Button")
}
}

△ With horizontally arranged pictures and text Button

One "slot" Represents a composable lambda Parameters , It represents anything in the component , such as Text perhaps Icon.Slot API Increased composability , Make components simpler , Reduce the number of independent concepts between components , So that developers can quickly start to create a new component , Or switch between different components .

△ Remove the overloaded of the original type CL

Looking forward to the future

We are right. Button API The number of changes made , In the discussion Button The amount of time spent in the meeting , And the energy invested in collecting feedback from developers , It's amazing . That being the case , We are right. API The overall effect is very satisfactory . In hindsight , We see in the Compose in Button Become more discoverable 、 Customizability , Most importantly, it promotes combinatorial thinking .

@Composable
fun Button(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
content: @Composable RowScope.() -> Unit
) {
// Implementation body code 
}

1.0 Button AP

It is important to recognize , Our design decisions are based on the following slogan :

Make simple development simple , Make difficult development possible .*

* It comes from famous technical books : English version :《Learning Perl: Making Easy Things Easy and Hard Things Possible》(Randal L. Schwartz、Brian D Foy and Tom Phoenix Writing ), Chinese version :《Perl Introduction to language 》( Translated by Sheng Chun )

We try to reduce overloading , And will " style " Flattening , Make development easier . meanwhile , We improved Android Studio Automatic completion function of , To help developers improve efficiency .

Here we would like to make a special mention of the whole API Two key points in the design process :

  1. API The design of is an iterative process . stay API It is almost impossible to reach perfect state in the initial iteration . There are some needs that are easy to ignore . As a API The author of , You need to make some assumptions . This includes different developer backgrounds , Different ways of thinking ¹ , Finally, it affects developers to explore and use API The way . Adaptation adjustment is inevitable , It's a good thing , Continuous iteration can lead to higher availability and more intuitive API.
  2. In iterating over a API When the design , One of your most valuable tools is for developers to use API Feedback loop of experience . For our team , The key is to understand what developers say " This API It's too complicated " What does it mean . When an error calls API when , It usually reduces the success rate and efficiency of developers , The insights gained from it , Will help us better understand " complex API" It means . The key driver of our continuous iteration is that we want to design easy-to-use and excellent API. So , Create a developer feedback loop , We used a variety of research paths —— Field programming activities ² , And need developers to provide experience diary ³ Remote access to . We can already understand how developers deal with API, And the functions they intend to implement , Find the path taken by the right method . Such as the engineer's way of thinking (Programmer Thinking Styles) And cognitive latitude (Cognitive Dimensions) Pillars in such frames , It helps our cross functional team to maintain consistency in language and thinking , Not only in the audit 、 Communicating developer feedback , It also involves API Design discussion . In especial , When evaluating the relationship between user experience and functionality , This framework helps us shape the discussion of choices and trade-offs .
  1. come from Android Developer UX Team Meital Tagor Sbero suffer Role models and ways of thinking (personas & Thinking Styles) Design and Cognitive dimension framework (Cognitive Dimensions Framework) Inspired by the , The framework of engineer's thinking mode is developed (Programmer Thinking Styles Framework). The framework uses what developers need in a limited time " The type of solution " Your motivation and attitude , Help developers determine API Design idea of usability . It takes into account the way ordinary engineers work , And optimized availability for high-intensity development tasks .

  2. We usually use this method to evaluate API Availability of specific aspects . such as , Each event invites a group of developers to use Button API To complete a series of development tasks , These tasks will deliberately expose some API Characteristics of , And these characteristics are the goals we want to collect feedback . We think aloud , To get more information about what developers pursue and what developers envision . These activities also include some follow-up questions , To further understand the needs of developers . We will review these activities , So as to determine the behavior patterns that developers contribute to success or failure in programming tasks .

  3. We usually use this method to evaluate API Availability and ease of learning over time . This can be done by listening to the feedback of developers in their regular work , To capture the moments of difficulty and inspiration . In the process , We will have a group of developers to develop specific projects of their own choice , Also make sure they use what we want to evaluate API. We will combine the diaries submitted by developers , And by researchers based on the cognitive dimension framework (Cognitive Dimensions Framework) ( Example ) The in-depth investigation organized , And interviews to help us determine API The usability of .

We acknowledge that although we are interested in the existing version Button API Very satisfied , But we also know that it is not perfect . Developers have many ways of thinking , Add different application scenarios , And an endless stream of needs , It requires us to constantly meet new challenges . That's not a problem !Button The whole evolutionary process of , It means a lot to us and the developer community . All this is for Compose Designed and shaped a usable Button API —— A simple rectangle that can be clicked on the screen .

I hope this article can help you clearly understand how your feedback can help us improve Compose in Button API. If you are using Compose If you have any problems , Or to the new API There is nothing to improve your experience Suggestions and ideas , Please tell us . Welcome developers to participate in our next User research activities in , Looking forward to your registration .

Welcome Click here Submit feedback to us , Or share your favorite content 、 Problems found . Your feedback is very important to us , Thank you for your support !

Please bring the original link to reprint ,thank
Similar articles

2021-10-14

2021-10-14

2021-10-14

2021-10-14

2021-10-14

2021-10-14

2021-10-14

2021-10-14

2021-10-14

2021-10-14

2021-10-14