Keeping Styles in Scope

Styles and Themes are a great thing. They allow us to abstract common view properties into a single location, making the look and feel of our application UI more consistent and easier to maintain. If you look at Google’s UI guide for Styles and Themes, it mentions the motivation behind this as a separation between design logic and content.

However, as with any abstraction API, it can be easy to take things a bit too far. Android does not restrict which attributes one can abstract into a style. Any attribute you can put on a view element, you can place in a style. But just because you can put something in a style doesn’t mean you should.

What I hope to communicate here is that specifically adding LayoutParams to your style definitions is not a great idea, and complicates your code instead of simplifying it.

An Example

Let’s start off with an example to illustrate the point. Imagine you are asked to review code written by someone else, or perhaps take over where they left off. You run the application for the first time and you see the following form layout:

layout
Figure 1. Example Layout

You then jump into the activity layout and find this:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        style="@style/FormButton.Cancel"
        android:text="@android:string/cancel"/>
    <Button
        style="@style/FormButton.Ok"
        android:text="@android:string/ok"/>
    <TextView
        style="@style/LabelText.Username"
        android:text="@string/label_username"/>
    <TextView
        style="@style/LabelText.Password"
        android:text="@string/label_password"/>
    <EditText
        style="@style/FormText.Username"/>
    <EditText
        style="@style/FormText.Password"/>
</RelativeLayout>

Can you tell by analyzing the layout file whether the rendered view is correct? Are the text labels aligned properly? Is the button width appropriate? In order to answer questions like these we will have to examine res/values/styles.xml since they aren’t apparent here.

<resources>
    ...

    <style name="LabelText" parent="android:TextAppearance.Medium">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">@dimen/label_height</item>
        <item name="android:paddingRight">@dimen/label_padding</item>
        <item name="android:textStyle">bold</item>
        <item name="android:gravity">bottom</item>
    </style>
    <style name="LabelText.Username">
        <item name="android:id">@id/label_username</item>
        <item name="android:layout_above">@id/label_password</item>
    </style>
    <style name="LabelText.Password">
        <item name="android:id">@id/label_password</item>
        <item name="android:layout_above">@id/button_ok</item>
    </style>

    <style name="FormButton" parent="android:Widget.Holo.Light.Button">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_marginLeft">@dimen/button_margin</item>
        <item name="android:layout_marginRight">@dimen/button_margin</item>
        <item name="android:layout_alignParentBottom">true</item>
        <item name="android:background">@drawable/button_background</item>
        <item name="android:minWidth">96dp</item>
    </style>
    <style name="FormButton.Ok">
        <item name="android:id">@id/button_ok</item>
        <item name="android:layout_toLeftOf">@id/button_cancel</item>

    </style>
    <style name="FormButton.Cancel">
        <item name="android:id">@id/button_cancel</item>
        <item name="android:layout_alignParentRight">true</item>
    </style>

    <style name="FormText" parent="android:Widget.Holo.Light.EditText">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
    </style>
    <style name="FormText.Username">
        <item name="android:layout_toRightOf">@id/label_username</item>
        <item name="android:layout_alignBottom">@id/label_username</item>
        <item name="android:inputType">textEmailAddress</item>
    </style>
    <style name="FormText.Password">
        <item name="android:layout_toRightOf">@id/label_password</item>
        <item name="android:layout_alignBottom">@id/label_password</item>
        <item name="android:inputType">textPassword</item>
    </style>
</resources>

The above is completely valid code; as I said, styles will accept any attribute. This includes LayoutParams and even ids.

Now, as you are working, the question in your mind becomes “Is this style used anywhere else? It must have been abstracted for a reason.” If so, making a change to adjust the layout here will affect those other layouts. If that effect is undesired, you’ll end up having to spin off another style to accommodate the change.

The Issue is Context

…​no, not that context. LayoutParams on a view by themselves have no meaning without the parent container. Any attribute that is prefixed with android:layout_ is meant to describe the relationship between the view itself and its parent. Therefore, every layout attribute only makes sense in the context of those two elements, and it follows that those attributes ought to be present in the layout files that define that relationship.

Following the same principle, it is not necessarily safe to abstract attributes from a view that are specific to a particular layout manager. This places an implicit constraint that the style that it will only be applied in certain containers. For example, placing android:layout_alignParentRight or android:layout_gravity in a style is dangerous because those attributes are not globally supported by all container views. The former is only allowed in RelativeLayout, which doesn’t support the latter attribute at all.

Sometimes I’ll get pushback on this that only one container type is ever expected in the application. If that is the case, why go through the trouble of abstracting into a style at all for use in one place? Or if in multiple places, why not use a layout include or a custom view as a cleaner approach to code reuse?

In my opinion, this takes typing abstraction to the level where it actually negatively affects the maintainability of the code. It forces you to inspect the applied styles in concert with the layout to diagnose any possible issues related to the placement and sizing of views. The scope of a layout attribute should be just to that layout, not a style that will likely be applied across multiple layouts.

What About Just Height & Width?

I often get pushback on this from folks indicating that they only abstract android:layout_width and android:layout_height in styles since they are global to all layout managers. If any additional LayoutParams are necessary, those can be placed directly in the layouts themselves. This solves the problem of accidentally using attributes from the wrong layout manager, right?

Yes, it does…​but it does nothing for the readability. In fact, I believe it makes it slightly worse. Let’s rewrite the above example using this as a guiding principle.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/button_cancel"
        style="@style/FormButton"
        android:layout_marginLeft="@dimen/button_margin"
        android:layout_marginRight="@dimen/button_margin"
        android:layout_alignParentRight="true"
        android:layout_alignParentBottom="true"
        android:text="@android:string/cancel"/>
    <Button
        android:id="@+id/button_ok"
        style="@style/FormButton"
        android:layout_marginLeft="@dimen/button_margin"
        android:layout_marginRight="@dimen/button_margin"
        android:layout_toLeftOf="@id/button_cancel"
        android:layout_alignParentBottom="true"
        android:text="@android:string/ok"/>
    <TextView
        android:id="@+id/label_username"
        style="@style/LabelText"
        android:layout_above="@id/label_password"
        android:text="@string/label_username"/>
    <TextView
        android:id="@+id/label_password"
        style="@style/LabelText"
        android:layout_above="@id/button_ok"
        android:text="@string/label_password"/>
    <EditText
        android:id="@+id/text_username"
        style="@style/FormText.Username"
        android:layout_toRightOf="@id/label_username"
        android:layout_alignBottom="@id/label_username"/>
    <EditText
        android:id="@+id/text_password"
        style="@style/FormText.Password"
        android:layout_toRightOf="@id/label_password"
        android:layout_alignBottom="@id/label_password"/>
</RelativeLayout>

The problem you might already see is that, at first glance, the LayoutParams in this file look complete until you stare at it for a moment. So as the outside observer, you might expect everything you need to be here in front of you. Only to be confused shortly thereafter when you realize android:layout_width and android:layout_height are missing. So we still have to open styles.xml to get the full picture of what is going on with the full layout:

<resources>
    ...

    <style name="LabelText" parent="android:TextAppearance.Medium">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">@dimen/label_height</item>
        <item name="android:paddingRight">@dimen/label_padding</item>
        <item name="android:textStyle">bold</item>
        <item name="android:gravity">bottom</item>
    </style>

    <style name="FormButton" parent="android:Widget.Holo.Light.Button">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:background">@drawable/button_background</item>
        <item name="android:minWidth">96dp</item>
    </style>

    <style name="FormText" parent="android:Widget.Holo.Light.EditText">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
    </style>
    <style name="FormText.Username">
        <item name="android:inputType">textEmailAddress</item>
    </style>
    <style name="FormText.Password">
        <item name="android:inputType">textPassword</item>
    </style>
</resources>

Bottom line: You shouldn’t have to open styles.xml to diagnose an issue with the layout XML. You also shouldn’t have to worry about whether a change made in styles.xml to fix a specific layout problem is going to propagate to other layouts, possibly breaking something elsewhere, since “there must have been a reason to abstract it to a common location”.

This still forces an implicit constraint that you always want the size of the widget to be consistent along with the styling. Layouts should be free to define size and placement, styles should be free to govern the look and feel regardless of what size the layout requests.

Common Arguments

To conclude, listed here are other miscellaneous thoughts I’ve heard on the subject that I felt were worth addressing.

Hey dummy, the UI guide you linked does this exact same thing. It’s obviously a sanctioned method.
…​don’t blindly trust DEBB. Just because you saw it in an example, even one from Google, doesn’t make it a good idea. If you look at Google’s code (i.e. the core styles.xml file in AOSP) you will notice that this is a practice done rarely, and they are scoped to very specific situations (such as the widgets that make up the action bar).

I am the only developer on this project, and I’ll remember how I’ve structured my own code. Spending time making my code more readable to others is a waste.
…​today, perhaps; but tomorrow? Who knows? Besides, even if you are a 100% solo operation, realize that we are all morons. Your memory isn’t as good as you think it is; let’s try to do things that won’t piss off our future selves.

What about margins? Those are tied to the spacing we always want applied to the widget.
…​that’s a valid argument, but not all layout containers support margins and you may find yourself in a situation where margins aren’t respected. This is why the framework bakes most widget spacings into the asset backgrounds (Button, EditText, etc. all do this).

That’s so much extra typing!
…​code completion was invented to solve this problem, not style abstraction.

 

Dave

Dave Smith is an embedded software developer based in Denver, CO and head geek Wireless Designs, LLC. He has been focused on the Android platform since 2009. If you would like to hear more from Dave, you can follow him on Twitter @devunwired. You can also find him on Google+.

 
  • Great article. I couldn’t agree more.
    XML layouts and styles are not like HTML & CSS.
    Readability is much more important than abstraction in this regard.

    FWIW it would be great though if IDEs were able to automagically toggle style attributes on/off similar to the string resources.

  • Jens –

    Agreed. I think there are a lot of cool things the tools team could do inside of Studio to reveal the applied style logic in the layout view in one form or another.

  • Nigel Heylen

    Great article. But what when you are using different flavors and you only need to change the style of each xml file? Do you overwrite every xml file with another?

  • I’m not sure I understand the question. When separating product flavors, duplicate files must exist whether they be a layout, style, dimen, etc.

  • Daniel Lew

    I’m actually working on an article on how to properly setup styles and this is part of it. 🙂

    I’m even more radical: I think one should only use styles because one has multiple widgets that should look and feel the same. It is not about code reduction via common attributes. If one treats styles as just a collection of common attributes then they will run into sadness down the line when one of the many Views using a style suddenly wants to be different.

  • I agree. As far as I’m concerned, you and I are saying exactly the same thing.

  • amor enew

    i agree with all points except gravity because i have to support rtl layouts “arabic language” so i need gravity in styles and align parent too.

  • If you use the proper start|end Gravity/Algnment constants instead of left|right to the let framework handle RTL, why would that be any different?

  • amor enew

    you are right but start and end in api 16 4.2 and i support from api 14 4.0

  • Sagar

    I had used in a situation where layout was same for both mobile and tablet, but for tablet I have to place views in center. So I created a style for layout that set layout_width and layout_height to match_parent for mobile and layout_width to 600dp and layout_height to match_parent. Is this the right approach to did this work. I did this because layout was same in both mobile and tablet. Please reply?