Slide 1

Slide 1 text

Scaling UI James Barr Uber

Slide 2

Slide 2 text

Platform UI

Slide 3

Slide 3 text

UI Issues & Considerations

Slide 4

Slide 4 text

Design System

Slide 5

Slide 5 text

Before & After #000000 #09091A #151525 #222231 #3A3A48 #6B6B76 #9D9DA3 #FFFFFF #B2B2BA #C0C0C8 #CDCDD3 #D9D9DE #E6E6E9 #F2F2F4 #F9F9F9 #1EACC7 #1FBAD6 #661FBAD6 #4CC8DE #79D6E6 #A5E3EF #D2F1F7 ...

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

Design System Guardrails

Slide 8

Slide 8 text

Lint

Slide 9

Slide 9 text

Lint class ColorResourceUsageDetector : ResourceXmlDetector() { override fun visitAttribute(context: XmlContext, attr: Attr) { if (attr.name in COLOR_ATTRIBUTES { if (attr.value.startsWith(PREFIX_COLOR_RES) && attr.value !in WHITELISTED_COLORS) { when { isInResFolder(LAYOUT) && !isColorSelector(attr.value) -> reportError() isInVectorDrawable(attribute) -> reportError() isInResFolder(context, DRAWABLE) && !hasQualifiedVersion(QUALIFIER_V21) && !isColorSelector(context, attributeVal) -> reportError() } } else if (attr.value.startsWith(PREFIX_THEME_ATTR) && isInResFolder(DRAWABLE) && !isInVectorDrawable(attribute) && !isQualifiedVersion(context)) { reportError() } } } }

Slide 10

Slide 10 text

Custom Fonts

Slide 11

Slide 11 text

Font Updates 1. Add new font asset 2. Update font name -- fonts/Roboto.ttf ++ fonts/GoogleSans.ttf 3. ✅ ? src/main/res/value/strings-fonts.xml

Slide 12

Slide 12 text

Calligraphy

Slide 13

Slide 13 text

Calligraphy Wrapped

Slide 14

Slide 14 text

ViewPump View inflation with a pre/post- processing API github.com/InflationX/ViewPump

Slide 15

Slide 15 text

ViewPump override fun onCreateView( name: String, context: Context, attrs: AttributeSet): View { return ViewPump.get().inflate(InflateRequest.builder() .name(name) .context(context) .attrs(attrs) .fallbackViewCreator(mViewCreator) .build()).view() } ViewPumpLayoutInflater.java

Slide 16

Slide 16 text

ViewPump override fun onCreateView( name: String, context: Context, attrs: AttributeSet): View { return ViewPump.get().inflate(InflateRequest.builder() .name(name) .context(context) .attrs(attrs) .fallbackViewCreator(mViewCreator) .build()).view() } ViewPumpLayoutInflater.java

Slide 17

Slide 17 text

ViewPump Interceptors class TextUpdatingInterceptor() : Interceptor { override fun intercept(chain: Chain): InflateResult { val result: InflateResult = chain.proceed(chain.request()) val view: View = result.view() if (view is TextView) { // You also have access to result.context and result.attrs view.text = "[Prefix] ${view.text}" } return result } }

Slide 18

Slide 18 text

Calligraphy class CalligraphyInterceptor( val calligraphy: Calligraphy) : Interceptor { override fun intercept(chain: Chain): InflateResult { val result: InflateResult = chain.proceed(chain.request()) return result.toBuilder() .view( calligraphy.onViewCreated( result.view(), result.context(), result.attrs())) .build() } }

Slide 19

Slide 19 text

Configuring ViewPump ViewPump.init(ViewPump.builder() .addInterceptor(CalligraphyInterceptor(CalligraphyConfig.Builder().build())) .addInterceptor(AccessibilityEncouragingInterceptor()) .addInterceptor(TextUpdatingInterceptor()) .addInterceptor(AppCompatInterceptor()) .build())

Slide 20

Slide 20 text

Reimplement AppCompat class AppCompatInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): InflateResult { val request = chain.request() val viewNameToInflate = viewNameToInflate(request.name()) return chain.proceed(request.toBuilder().name(viewNameToInflate).build()) } fun viewNameToInflate(requestedViewName: String) = when (requestedViewName) { "Button" -> AppCompatButton::class.java.name "EditText" -> AppCompatEditText::class.java.name "ImageView" -> AppCompatImageView::class.java.name "TextView" -> AppCompatTextView::class.java.name else -> requestedViewName } }

Slide 21

Slide 21 text

Reimplement AppCompat class AppCompatInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): InflateResult { val request = chain.request() val viewNameToInflate = viewNameToInflate(request.name()) return chain.proceed(request.toBuilder().name(viewNameToInflate).build()) } fun viewNameToInflate(requestedViewName: String) = when (requestedViewName) { "Button" -> AppCompatButton::class.java.name "EditText" -> AppCompatEditText::class.java.name "ImageView" -> AppCompatImageView::class.java.name "TextView" -> AppCompatTextView::class.java.name else -> requestedViewName } }

Slide 22

Slide 22 text

Hide Unaccessible Views class AccessibilityEncouragingInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): InflateResult { val result = chain.proceed(chain.request()) result.view()?.let { if (it.isClickable && it.contentDescription.isNullOrEmpty()) { it.visibility = View.INVISIBLE } } return result } }

Slide 23

Slide 23 text

Other Example Usages • Geocities-ify an app • Draw over views under some conditions • Backport functionality for new attrs • Dynamic string reloading • Track recently inflated views & add data to crash reports

Slide 24

Slide 24 text

Platform UI Widgets

Slide 25

Slide 25 text

Artist A build tool that codegens a base set of Android Views github.com/uber/artist

Slide 26

Slide 26 text

Artist Traits (functionality) + Stencils (definitions) ↓ Views

Slide 27

Slide 27 text

Artist Trait @AutoService(Trait::class) class VisibilityTrait : Trait { override fun generateFor( type: TypeSpec.Builder, initMethod: MethodSpec.Builder, rClass: ClassName, baseType: String) { arrayOf("visible", "invisible", "gone").forEach { type.addMethod(createVisibilityConvenienceMethod(it)) } } ... }

Slide 28

Slide 28 text

Artist Trait @AutoService(Trait::class) class VisibilityTrait : Trait { ... fun createVisibilityConvenienceMethod(type: String): MethodSpec { return MethodSpec.methodBuilder("is${type.capitalize()}") .addModifiers(Modifier.PUBLIC) .returns(TypeName.BOOLEAN) .addStatement( "return getVisibility() == \$T.${type.toUpperCase()}", TypeNames.Android.View) .build() } }

Slide 29

Slide 29 text

Artist Stencil class TextViewStencil : ViewStencil( extendedType = "android.support.v7.widget.AppCompatTextView", constructorCount = 3, defaultAttrRes = "textViewStyle", addedTraits = VisibilityTrait::class.java )

Slide 30

Slide 30 text

Artist Generated View public class MyTextView extends AppCompatTextView { public MyTextView(Context context) { this(context, null); } public MyTextView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.textViewStyle); } public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr, 0); } // protected init method - provided in every stencil public boolean isVisible() { return getVisibility() == View.VISIBLE; } public boolean isGone() { return getVisibility() == View.GONE; } public boolean isInvisible() { return getVisibility() == View.INVISIBLE; } }

Slide 31

Slide 31 text

@AutoService(ViewStencilProvider.class) class SampleViewStencilProvider : ViewStencilProvider { override fun stencils() = setOf( ViewStencil("android.support.v7.widget.AppCompatButton", 3, "buttonStyle"), ViewStencil("android.support.v7.widget.AppCompatEditText", 3, "android.R.attr.editTextStyle", TextInputTrait.class), ViewStencil("android.widget.LinearLayout", 3), ViewStencil("android.support.v7.widget.AppCompatImageView", 3), ViewStencil("android.support.v4.widget.NestedScrollView", 3, ScrollableTrait.class), ViewStencil("android.support.v7.widget.AppCompatTextView", 3, "android.R.attr.textViewStyle") ) override fun globalTraits() = setOf( VisibilityTrait.class::java, ForegroundTrait.class::java, ViewTrait.class::java ) }

Slide 32

Slide 32 text

Artist Use Cases • Make Rx APIs 1st class citizens: myView.clicks() • Add built-in analytics to taps and impressions • Make a bugfix to MyTextView and all other usages will receive it • Easily change all of your parent classes from AppCompatWidget to MaterialWidget

Slide 33

Slide 33 text

Wrapping Up • Collaborate with Design to create a system that works • Use semantic names for colors & styles flexibility across apps • Enforce your design system with Lint & ErrorProne • Use ViewPump to post-process inflated views • Use Artist to easily maintain your base set of views

Slide 34

Slide 34 text

Scaling UI James Barr • uber.github.io • We're Hiring @jbarr21