Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building UI Consistent Android Apps - Yelp Engi...

Building UI Consistent Android Apps - Yelp Engineering

Slides for the talk "Building UI Consistent Android Apps" by Nicola Corti, Android Engineer @ Yelp (Hamburg).

Abstract
=====
Consistency is probably one of the best-known design principles. Consistent UIs are easy to use, easy to learn and frustration free. Nonetheless, they are also extremely easy to break! Just a few development iterations are enough to totally mess up your color palette or your icon sets.

Consistency gets even trickier if you have to ensure it across different Android apps. In such a scenario, defining style guides becomes crucial. Assets, icons, colors, metrics, and components should be easily accessible by developers/designers across your teams. Furthermore, you must ensure that every style guide change will not break your app.

Yelp ships its experience across Android, iOS and Web apps used by millions of users. In this talk, you will get an insight into the challenges we face on a daily basis ensuring our visual consistency, and the solutions we adopted.

Nicola Corti

June 21, 2017
Tweet

More Decks by Nicola Corti

Other Decks in Technology

Transcript

  1. Does it fit? • Can be reused? • Is it

    visible to the user? • Development plan?
  2. Attributes <resources>
 <declare-styleable name="UserPassport">
 <!-- Determines user's name -->
 <attr

    name="userPassportName" format="string"/>
 </declare-styleable>
 </resources>
  3. Attributes <resources>
 <declare-styleable name="UserPassport">
 <!-- Determines user's name -->
 <attr

    name="userPassportName" format="string"/>
 
 <!-- Determines user's description/role -->
 <attr name="userPassportDescription" format="string"/>
 </declare-styleable>
 </resources>
  4. Attributes <resources>
 <declare-styleable name="UserPassport">
 <!-- Determines user's name -->
 <attr

    name="userPassportName" format="string"/>
 
 <!-- Determines user's description/role -->
 <attr name="userPassportDescription" format="string"/>
 </declare-styleable>
 <attr name="userPassportStyle" format="reference"/>
 </resources>
  5. Theme <resources>
 <!—- This theme is the parent of all

    themes of Yelp's android apps. —->
 <style name="YelpStyleguideTheme"/>
 </resources>
  6. Theme <resources>
 <!—- This theme is the parent of all

    themes of Yelp's android apps. —->
 <style name="YelpStyleguideTheme" parent=“Theme.AppCompat.Light.DarkActionBar"/>
 </resources>
  7. Theme <resources>
 <!—- This theme is the parent of all

    themes of Yelp's android apps. —->
 <style name="YelpStyleguideTheme" parent=“Theme.AppCompat.Light.DarkActionBar">
 <item name="userPassportStyle">@style/UserPassport</item>
 </style>
 </resources>
  8. public class UserPassport extends RelativeLayout {
 
 private TextView mUserName;


    private TextView mDescription;
 
 public void setName(String name) {
 mUserName.setText(name);
 }
 
 

  9. public class UserPassport extends RelativeLayout {
 
 private TextView mUserName;


    private TextView mDescription;
 
 public void setName(String name) {
 mUserName.setText(name);
 }
 
 public void setDescription(String description) {
 if (TextUtils.isEmpty(description)) {
 mDescription.setVisibility(GONE);
 } else {
 mDescription.setVisibility(VISIBLE);
 mDescription.setText(description);
 }
 }
 

  10. public class UserPassport extends RelativeLayout {
 
 private TextView mUserName;


    private TextView mDescription;
 
 public UserPassport(final Context context) {
 super(context);
 init(context, null, 0);
 }
 
 public UserPassport(final Context context, final AttributeSet attrs) {
 super(context, attrs);
 init(context, attrs, R.attr.userPassportStyle);
 }
 
 public UserPassport(final Context context, final AttributeSet attrs, final int defStyleAttr) {
 super(context, attrs, defStyleAttr);
 init(context, attrs, defStyleAttr);
 }
  11. public class UserPassport extends RelativeLayout {
 
 private TextView mUserName;


    private TextView mDescription;
 
 public UserPassport(final Context context) {
 super(context);
 init(context, null, 0);
 }
 
 public UserPassport(final Context context, final AttributeSet attrs) {
 super(context, attrs);
 init(context, attrs, R.attr.userPassportStyle);
 }
 
 public UserPassport(final Context context, final AttributeSet attrs, final int defStyleAttr) {
 super(context, attrs, defStyleAttr);
 init(context, attrs, defStyleAttr);
 }
  12. public class UserPassport extends RelativeLayout {
 
 private TextView mUserName;


    private TextView mDescription;
 
 private void init(
 final Context context, final AttributeSet attrs, final int defStyleAttr) { 
 }
 

  13. public class UserPassport extends RelativeLayout {
 
 private TextView mUserName;


    private TextView mDescription;
 
 private void init(
 final Context context, final AttributeSet attrs, final int defStyleAttr) { 
 LayoutInflater.from(context).inflate(R.layout.user_passport, this, true); 
 }
 

  14. public class UserPassport extends RelativeLayout {
 
 private TextView mUserName;


    private TextView mDescription;
 
 private void init(
 final Context context, final AttributeSet attrs, final int defStyleAttr) { 
 LayoutInflater.from(context).inflate(R.layout.user_passport, this, true); 
 mUserName = (TextView) findViewById(R.id.user_name);
 mDescription = (TextView) findViewById(R.id.description);
 
 }
 

  15. public class UserPassport extends RelativeLayout {
 
 private TextView mUserName;


    private TextView mDescription;
 
 private void init(
 final Context context, final AttributeSet attrs, final int defStyleAttr) { 
 LayoutInflater.from(context).inflate(R.layout.user_passport, this, true); 
 mUserName = (TextView) findViewById(R.id.user_name);
 mDescription = (TextView) findViewById(R.id.description);
 
 final TypedArray styles = context.obtainStyledAttributes(attrs, R.styleable.UserPassport, defStyleAttr, 0);
 
 }

  16. public class UserPassport extends RelativeLayout {
 
 private TextView mUserName;


    private TextView mDescription;
 
 private void init(
 final Context context, final AttributeSet attrs, final int defStyleAttr) { 
 LayoutInflater.from(context).inflate(R.layout.user_passport, this, true); 
 mUserName = (TextView) findViewById(R.id.user_name);
 mDescription = (TextView) findViewById(R.id.description);
 
 final TypedArray styles = context.obtainStyledAttributes(attrs, R.styleable.UserPassport, defStyleAttr, 0);
 
 setName(styles.getString(R.styleable.UserPassport_userPassportName)); setDescription(styles.getString( R.styleable.UserPassport_userPassportDescription));
 
 }
 

  17. public class UserPassport extends RelativeLayout {
 
 private TextView mUserName;


    private TextView mDescription;
 
 private void init(
 final Context context, final AttributeSet attrs, final int defStyleAttr) { 
 LayoutInflater.from(context).inflate(R.layout.user_passport, this, true); 
 mUserName = (TextView) findViewById(R.id.user_name);
 mDescription = (TextView) findViewById(R.id.description);
 
 final TypedArray styles = context.obtainStyledAttributes(attrs, R.styleable.UserPassport, defStyleAttr, 0);
 
 setName(styles.getString(R.styleable.UserPassport_userPassportName)); setDescription(styles.getString( R.styleable.UserPassport_userPassportDescription));
 
 styles.recycle();
 }
 

  18. <style name="UserPassport">
 <item name="userPassportName">Joe Smith</item>
 <item name="userPassportDescription">@null</item>
 <item name="userPassportTint">@color/orange_dark_interface</item>
 <item

    name="userPassportNameColor">@color/black_regular_interface</item>
 <item name="userPassportSize">Regular</item>
 <item name="userPassportShowName">true</item>
 <item name="userPassportShowIcons">true</item>
 <item name="userPassportEliteYear">-1</item>
 <item name="userPassportFriends">0</item>
 <item name="userPassportReviews">0</item>
 <item name="userPassportPhotos">0</item>
 <item name="userPassportCheckIns">0</item>
 <item name="userPassportShowCheckIn">false</item>
 </style>
  19. <style name="UserPassport.White">
 <item name="userPassportName">Joe Smith</item>
 <item name="userPassportDescription">@null</item>
 <item name="userPassportTint">@color/orange_dark_interface</item>
 <item

    name="userPassportNameColor">@color/black_regular_interface</item>
 <item name="userPassportSize">Regular</item>
 <item name="userPassportShowName">true</item>
 <item name="userPassportShowIcons">true</item>
 <item name="userPassportEliteYear">-1</item>
 <item name="userPassportFriends">0</item>
 <item name="userPassportReviews">0</item>
 <item name="userPassportPhotos">0</item>
 <item name="userPassportCheckIns">0</item>
 <item name="userPassportShowCheckIn">false</item>
 </style>
  20. <style name="UserPassport.White">
 <item name="userPassportName">Joe Smith</item>
 <item name="userPassportDescription">@null</item>
 <item name="userPassportTint">@color/white_interface</item>
 <item

    name="userPassportNameColor">@color/white_interface</item>
 <item name="userPassportSize">Regular</item>
 <item name="userPassportShowName">true</item>
 <item name="userPassportShowIcons">true</item>
 <item name="userPassportEliteYear">-1</item>
 <item name="userPassportFriends">0</item>
 <item name="userPassportReviews">0</item>
 <item name="userPassportPhotos">0</item>
 <item name="userPassportCheckIns">0</item>
 <item name="userPassportShowCheckIn">false</item>
 </style>
  21. isInEditMode() public void setMediaCount(int photo, int video, int media) {


    mMediaCount.setText(String.valueOf(media));
 Resources resources = getResources();
 String contentDesc;
 }
  22. isInEditMode() public void setMediaCount(int photo, int video, int media) {


    mMediaCount.setText(String.valueOf(media));
 Resources resources = getResources();
 String contentDesc;
 if (video == 0) {
 contentDesc = resources.getString(R.string.photo_count, photo);
 } else if (photo == 0 && video > 0) {
 contentDesc = resources.getString(R.string.video_count, video);
 } else {
 contentDesc = resources.getString(R.string.photo_and_video_count, media);
 }
 }
  23. isInEditMode() public void setMediaCount(int photo, int video, int media) {


    mMediaCount.setText(String.valueOf(media));
 Resources resources = getResources();
 String contentDesc;
 if (video == 0) {
 contentDesc = resources.getString(R.string.photo_count, photo);
 } else if (photo == 0 && video > 0) {
 contentDesc = resources.getString(R.string.video_count, video);
 } else {
 contentDesc = resources.getString(R.string.photo_and_video_count, media);
 }
 mMediaCount.setContentDescription(contentDesc);
 }
  24. isInEditMode() public void setMediaCount(int photo, int video, int media) {


    mMediaCount.setText(String.valueOf(media));
 if (!isInEditMode()) {
 Resources resources = getResources();
 String contentDesc;
 if (video == 0) {
 contentDesc = resources.getString(R.string.photo_count, photo);
 } else if (photo == 0 && video > 0) {
 contentDesc = resources.getString(R.string.video_count, video);
 } else {
 contentDesc = resources.getString(R.string.photo_and_video_count, media);
 }
 mMediaCount.setContentDescription(contentDesc);
 }
 }
  25. Color <color name="black_extra_light_interface">#666666</color>
 <color name="black_regular_interface">#333333</color>
 <color name="blue_dark_interface">#0073bb</color>
 <color name="blue_extra_light_interface">#d0ecfb</color>
 <color

    name="blue_regular_interface">#0097ec</color>
 <color name="gray_dark_interface">#999999</color>
 <color name="gray_extra_light_interface">#f5f5f5</color>
 <color name="gray_light_interface">#e6e6e6</color>
 <color name="gray_regular_interface">#cccccc</color>
 <color name="green_extra_light_interface">#daecd2</color>
 <color name="green_regular_interface">#41a700</color>
 <color name="mocha_extra_light_interface">#f8e3c7</color>
 <color name="mocha_light_interface">#f1bd79</color>
 <color name="orange_dark_interface">#f15c00</color>
 <color name="orange_extra_light_interface">#ffebcf</color>
 <color name="purple_extra_light_interface">#dad1e4</color>
 <color name="red_dark_interface">#d32323</color>
 <color name="red_extra_light_interface">#fcd6d3</color>
 <color name="slate_extra_light_interface">#cddae2</color>
 <color name="white_interface">#ffffff</color>
 <color name="yellow_dark_interface">#fec011</color>
 <color name="yellow_extra_light_interface">#fff7cc</color>
  26. Color <color name="black_extra_light_interface">#666666</color>
 <color name="black_regular_interface">#333333</color>
 <color name="blue_dark_interface">#0073bb</color>
 <color name="blue_extra_light_interface">#d0ecfb</color>
 <color

    name="blue_regular_interface">#0097ec</color>
 <color name="gray_dark_interface">#999999</color>
 <color name="gray_extra_light_interface">#f5f5f5</color>
 <color name="gray_light_interface">#e6e6e6</color>
 <color name="gray_regular_interface">#cccccc</color>
 <color name="green_extra_light_interface">#daecd2</color>
 <color name="green_regular_interface">#41a700</color>
 <color name="mocha_extra_light_interface">#f8e3c7</color>
 <color name="mocha_light_interface">#f1bd79</color>
 <color name="orange_dark_interface">#f15c00</color>
 <color name="orange_extra_light_interface">#ffebcf</color>
 <color name="purple_extra_light_interface">#dad1e4</color>
 <color name="red_dark_interface">#d32323</color>
 <color name="red_extra_light_interface">#fcd6d3</color>
 <color name="slate_extra_light_interface">#cddae2</color>
 <color name="white_interface">#ffffff</color>
 <color name="yellow_dark_interface">#fec011</color>
 <color name="yellow_extra_light_interface">#fff7cc</color>
  27. Color <color name="black_extra_light_interface">#666666</color>
 <color name="black_regular_interface">#333333</color>
 <color name="blue_dark_interface">#0073bb</color>
 <color name="blue_extra_light_interface">#d0ecfb</color>
 <color

    name="blue_regular_interface">#0097ec</color>
 <color name="gray_dark_interface">#999999</color>
 <color name="gray_extra_light_interface">#f5f5f5</color>
 <color name="gray_light_interface">#e6e6e6</color>
 <color name="gray_regular_interface">#cccccc</color>
 <color name="green_extra_light_interface">#daecd2</color>
 <color name="green_regular_interface">#41a700</color>
 <color name="mocha_extra_light_interface">#f8e3c7</color>
 <color name="mocha_light_interface">#f1bd79</color>
 <color name="orange_dark_interface">#f15c00</color>
 <color name="orange_extra_light_interface">#ffebcf</color>
 <color name="purple_extra_light_interface">#dad1e4</color>
 <color name="red_dark_interface">#d32323</color>
 <color name="red_extra_light_interface">#fcd6d3</color>
 <color name="slate_extra_light_interface">#cddae2</color>
 <color name="white_interface">#ffffff</color>
 <color name="yellow_dark_interface">#fec011</color>
 <color name="yellow_extra_light_interface">#fff7cc</color>
  28. VCS & CI • git submodule • Run the Build

    for • submodule • consumer app • business app
  29. Custom Lint Checks 
 Button b = new Button(context);
 


    SwitchCompat switchCompat = new SwitchCompat(context);
 
 Snackbar.make(getRootView(), “Test”, LENGTH_SHORT).show();
  30. Custom Lint Checks @SuppressLint("")
 Button b = new Button(context);
 


    @SuppressLint("")
 SwitchCompat switchCompat = new SwitchCompat(context);
 
 @SuppressLint("")
 Snackbar.make(getRootView(), “Test”, LENGTH_SHORT).show();
  31. Custom Lint Checks @SuppressLint("NonStyleguideButtonInstance")
 Button b = new Button(context);
 


    @SuppressLint("")
 SwitchCompat switchCompat = new SwitchCompat(context);
 
 @SuppressLint("")
 Snackbar.make(getRootView(), “Test”, LENGTH_SHORT).show();
  32. Custom Lint Checks @SuppressLint("NonStyleguideButtonInstance")
 Button b = new Button(context);
 


    @SuppressLint("NonStyleguideToggleInstance")
 SwitchCompat switchCompat = new SwitchCompat(context);
 
 @SuppressLint("")
 Snackbar.make(getRootView(), “Test”, LENGTH_SHORT).show();
  33. Custom Lint Checks @SuppressLint("NonStyleguideButtonInstance")
 Button b = new Button(context);
 


    @SuppressLint("NonStyleguideToggleInstance")
 SwitchCompat switchCompat = new SwitchCompat(context);
 
 @SuppressLint("NonStyleguideSnackbarInstance")
 Snackbar.make(getRootView(), “Test”, LENGTH_SHORT).show();
  34. Custom Lint Checks // Using stock Button because […] +

    Ticket number. @SuppressLint("NonStyleguideButtonInstance")
 Button b = new Button(context);
 
 @SuppressLint("NonStyleguideToggleInstance")
 SwitchCompat switchCompat = new SwitchCompat(context);
 
 @SuppressLint("NonStyleguideSnackbarInstance")
 Snackbar.make(getRootView(), “Test”, LENGTH_SHORT).show();
  35. Custom Lint Checks <Button
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 tools:ignore="NonStyleguideButtonTag" />
 
 <Switch


    android:layout_width="match_parent"
 android:layout_height="match_parent"
 tools:ignore="NonStyleguideToggleTag" />
  36. build.gradle android {
 lintOptions {
 abortOnError true
 warningsAsErrors true
 


    lintConfig file("lint.xml")
 baseline file("lint-baseline.xml")
 }
 }
  37. Test your component • Your component ❤ Espresso? • Do

    you handle state changes? • contentDescription ?
  38. Taking Screenshots with Espresso public class ScreenshotViewActions {
 
 public

    static ViewAction screenshot(final String folderName, final String fileName) {
 return new ViewAction() {
 
 };
 }
 }
  39. Taking Screenshots with Espresso public class ScreenshotViewActions {
 
 public

    static ViewAction screenshot(final String folderName, final String fileName) {
 return new ViewAction() {
 // Other methods omitted.
 
 @Override
 public void perform(UiController uiController, View view) {
 ScreenshotsUtil.takeScreenshot(folderName, fileName, view);
 }
 };
 }
 }
  40. Sample Espresso Test public class StarsViewActivityTests {
 
 @Test
 public

    void takeScreenshot() throws InterruptedException {
 onView(withId(R.id.stars_view_4)).perform(setStarsNumber(4));
 
 }
 }
  41. Sample Espresso Test public class StarsViewActivityTests {
 
 @Test
 public

    void takeScreenshot() throws InterruptedException {
 onView(withId(R.id.stars_view_4)).perform(setStarsNumber(4));
 onView(withId(R.id.stars_view_5)).perform(setStarsNumber(5),
 screenshot(FOLDER_NAME, "stars_with_text"));
 
 }
 }
  42. Sample Espresso Test public class StarsViewActivityTests {
 
 @Test
 public

    void takeScreenshot() throws InterruptedException {
 onView(withId(R.id.stars_view_4)).perform(setStarsNumber(4));
 onView(withId(R.id.stars_view_5)).perform(setStarsNumber(5),
 screenshot(FOLDER_NAME, "stars_with_text"));
 
 ScreenshotUtil.fullScreenshot(FOLDER_NAME, "stars_fullscreen");
 }
 }