/artist

An artist creates views. Artist is a Gradle plugin that codegens a base set of Android Views.

Primary LanguageKotlinApache License 2.0Apache-2.0

Artist Build Status

As Android apps grow, providing common features and consistent functionality across Views becomes challenging. Typically, this results in copy-pasting features across views, monolithic classes, or complicated inheritance trees. Artist is a highly-extensible platform for creating and maintaining an app’s base set of Android views.

Overview

Artist is a Gradle plugin written in Kotlin that generates a base set of Android Views. Artist-generated views are created using a stencil and trait system. Each view type is declared with a single stencil, which is comprised of a set of traits. All of this comes together to create an easily maintainable system of stencils and traits.

Stencils: A Stencil defines a View class to be generated. Each Stencil has some properties that can be configured and declares a set of traits they exhibit.

Traits: A Trait defines the new functionality that should be added to a view. It is a hook into the Stencil’s codegen process that is called during each Stencil’s generation. It is responsible for generating the code that implements Trait's functionality. This could be used to do things like add automatic view analytics to every view or add first-party support for RxBinding APIs (clicks, attach events, visibility changes, etc.) on all your views.

A simple Trait that adds visibility helper methods would look like:

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

  private 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()
  }
}

A simple ViewStencil to generate a Switch with visibility helper methods would look like:

class SwitchStencil : JavaViewStencil(
    extendedType = "android.support.v7.widget.SwitchCompat",
    constructorCount = 3,
    defaultAttrRes = "switchStyle",
    addedTraits = VisibilityTrait::class.java) {
  
  override fun name() = "MySwitch"
}

Finally leaving you with a generated view like this:

public class MySwitch extends SwitchCompat {
  // Constructors
  
  // 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;
  }
}

This may look like a lot of boilerplate for simple helpers, but it scales quite well when you want to have these methods on all your base views.

Motivation

Common Façade

Everything is behind the façade of commonly named classes, basically "[YOUR_PREFIX]ViewName". This allows you to push as much functionality as you want behind them whilst not changing the front facing entry point. Things we can push behind them include new functionality, other base classes, framework bug fixes, etc.

Sane, simple maintainability

The stencil and trait system ensures that base views are defined in one place and that extra functionality is divided up into single-focus traits.

Reactive Semantics

Artist-generated views can have RxBinding APIs as first class citizens in their public APIs. In a increasingly reactive world, this gracefully bridges common UI listener interactions to RxJava streams. This can optionally be brought in via the artist-traits-rx module.

Intelligence

Artist-generated views have deep internal knowledge of their internal state and interactions. This gives you flexibility to do a number of interesting, contextual actions under the hood.

Automatic Instrumentation: Artist-generated views know when they're being attached, changed to visible, clicked, etc. This allows you to do automatic instrumentation of impressions and taps in views when they occur, provided the developer has provided an ID. You can also detect and signal a developer if an ID is missing where there should be one.

Accessibility: This intelligence gives you enough insight into the state of the view hierarchy to make accessibility a first class citizen in the daily development cycle. Artist-generated views can intelligently infer if there are content description errors associated with them, and signal them to developers in the apps.

For more examples of things you can do with Artist, check out the Recipes wiki page.

Usage

Create the Provider module

  • Create a new plain Java/Kotlin module (non-Android)
  • Add Artist dependencies (API, Traits, Traits-Rx)

Implement the Stencil Provider

  • Create a class that implements JavaViewStencilProvider
  • Annotate your class with @AutoService(JavaViewStencilProvider::class)

Implement Custom Traits (Optional)

  • If you have custom traits, then create classes that implement JavaTrait
  • Annotate those classes with @AutoService(JavaTrait::class)

Add Provider module to Plugin Classpath

Option #1

If your provider module is in it's own project, then you can add the JAR to the buildscript classpath in your main project's root build.gradle like:

buildscript {
  dependencies {
    classpath <include for your jar>
  }
}

Option #2

Otherwise, if your provider module is in your primary project, then in order for Artist to find the classes on the plugin classpath during code generation, we must leverage Gradle's buildSrc. We use this project within your project to build the classes that will be added to the plugin classpath. This will run before your primary project is built.

  • Create a dir at root of project named buildSrc
  • Navigate to buildSrc and add a relative symlink to the provider module cd $PROJECT_ROOT/buildSrc; ln -s ../path/to/provider/module/root custom-artist-providers
  • Create a settings.gradle in buildSrc and add include :custom-artist-providers
  • Update the build.gradle for the buildSrc project to ensure that the custom-artist-providers module is added the buildScript classpath so it is available to the Artist plugin:
subprojects { subproject ->
    if (subproject.buildFile.exists()) {
        repositories {
            jcenter()
            google()
        }

        rootProject.dependencies {
            runtime project(path)
        }
    }
    subproject.afterEvaluate {
        // Disable useless tasks in buildSrc
        if (subproject.plugins.hasPlugin("kotlin")) {
            subproject.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
                kotlinOptions.suppressWarnings = true
            }
        }

        subproject.tasks.findAll {
            it.name.toLowerCase().contains("test") ||
                it.name.toLowerCase().contains("lint") ||
                it.name.toLowerCase().contains("checkstyle") }.each {
            it.enabled = false
        }
    }
}

Use the Generated Views

The generated views will be added to the library's source files. They can then be consumed as regular views. To add even more consistency, you can write a lint rule or ErrorProne check to ensure that all View subclasses use your Artist-generated views.

Further examples

The set of JavaViewStencils that Artist should process are provided via the JavaViewStencilProvider. The sample's ViewStencilProvider would configure Artist to generate these Views.

Download

Artist Plugin Maven Central

classpath 'com.uber.artist:artist:0.4.6'

Artist API Maven Central

classpath 'com.uber.artist:artist-api:0.4.6'

Artist Traits Maven Central

classpath 'com.uber.artist:artist-traits:0.4.6'

Artist Rx Traits Maven Central

classpath 'com.uber.artist:artist-traits-rx:0.4.6'

License

Copyright (C) 2017 Uber Technologies

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.