GITHUB

Use annotations to bind data to views without any view lookup. Sync all view data in one go and keep view states up to date with application state at all times. View holders are bulit in.

Why Witch?

Witch is a light weight and performant view-data binding framework that helps building predictable UI:s.

Unlike the Data binding library that is part of jetpack, there are no bindings declared in the layout xml and there are no binding adapters. Witch has less ceremony, is simpler to use and dictates no underlying architecture.

Binder

A binder (or whatever you may call it) is the core class you need to define in order to bind data to views. It's just a plain class with annotations declaring data and bindings. Below is a small example of a binder binding a user model to a text view:

1
2
3
4
5
6
7
8
9
10
class MyBinder {
  
  @Data
  lateinit var user: User

  @Bind(id = R.id.name)
  fun bindName(name: TextView, user: User) {
    name.text = user.name
  }
}
1
2
3
4
5
6
7
8
9
10
class MyBinder {
  
  @Data
  User user;

  @Bind(id = R.id.name)
  void bindName(TextView name, User user) {
    name.setText(user.getName());
  }
}

Execute binding

Data is bound when a binder and a view (or an activity) is passed to Witch.bind().

1
2
binder.user = User(name: "Harry Potter")
Witch.bind(binder, activity)
1
2
binder.user = new User("Harry Potter");
Witch.bind(binder, activity);

@Data

The @Data-annotation is used to declare data properties. Only data properties can be used in bind methods. Each time a binder is passed to Witch.bind() the data properties will evaluated. Only properties that has changed since last bind will be bound.

In the above example, the entire user model is declared a data property despite only the user name is bound to a view. This might be OK in some cases, but if the user model is frequently updated, the name will be repeatedly bound without being changed. Since the @Data-annotation can be used on methods, we can create a data property just for the user name. This way the name is only bound when actually changed.

1
2
3
4
@Data
fun userName(): String {
  if(user != null) { user.name } else { "Guest user" }
}
1
2
3
4
5
6
7
@Data
String userName() {
  if (user != null) {
    return user.getName()
  }
  return "Guest user";
}

@Bind

The @Bind-annotation is used to declare bind methods. This is how data gets bound to a view. The view is looked up (once) with the id defined in the annotation. Data passed to the bind method is mapped using type and name of any data property in the same class.

1
2
3
4
@Bind(id = R.id.name) 
fun bindUserName(name: TextView , userName: String) {
  name.text = userName
}
1
2
3
4
@Bind(id = R.id.name)
void bindUserName(TextView name, String userName) {
  name.setText(userName);
}

@BindData

If a binding is simply setting data on a view the @Data and @Bind-annotation can be merged to a @BindData-annotation. The annotation must define a view type and what property to set.

1
2
3
4
@BindData(id = R.id.name, view = TextView::class.java, set = "text")
fun userName(): String {
    return user.name
}
1
2
3
4
5
// The set-perfix can be stripped off, i.e. setText will be defined as set="text"
@BindData(id = R.id.name, view = TextView.class, set = "text")
String userName() {
    return user.getName();
}

@Setup

The @Setup-annotation is used to declare bind methods that should only run once. Setup methods will always run before any bind methods.

1
2
3
4
@Setup(id = R.id.button)
fun setupButton(button: Button) {
  button.setOnClickListener { /* Ohoy! */ }
}
1
2
3
4
5
6
7
8
@Setup(id = R.id.button)
void setupButton(Button button) {
  button.setOnClickListener(new OnClickListener() {
    void onClick(View view) {
      // Ohoy!
    }
  });
}

@BindNull

The @BindNull-annotation enables null values to be used in bind methods. By default, null values are ignored.

1
2
3
4
5
6
7
8
9
@Bind(id = R.id.image)
@BindNull
fun bindImageUrl(image: ImageView, url: String) {
  if(url == null) {
    image.imageDrawable = null
  } else {
    // Load image from url
  }
}
1
2
3
4
5
6
7
8
9
@Bind(id = R.id.image)
@BindNull
void bindImageUrl(ImageView image, String url) {
  if(url == null) {
    image.setImageDrawable(null);
  } else {
    // Load image from url
  }
}

Adapter views

Witch will create view holders for all annotated views. This eliminates the need for creating additional view holders for adapter views.

WitchRecyclerViewAdapter makes it easy implement data bindings for a recycler view.

1
2
3
WitchRecyclerViewAdapter.Builder()
        .binder(PostBinder())
        .build()
1
2
3
new WitchRecyclerViewAdapter.Builder()
        .binder(new PostBinder())
        .build();

Binders must extend WitchRecyclerViewAdapter.Binder. In this case, the PostBinder will handle binding of all items of type Post in the adapters data set. For each position, the binder will be updated with the corresponding data item that will be accassible through the magical property item and should only be reference from within a bind method.

1
2
3
4
5
6
7
class PostBinder() : WitchRecyclerViewAdapter.Binder<Post>(R.layout.post, Post::class.java) {

    @Bind(id = R.id.title)
    fun bindText(title: TextView) {
        title.text = item.title
    }
}
1
2
3
4
5
6
7
8
9
class PostBinder extends WitchRecyclerViewAdapter.Binder<Post> {

    private PostBinder() { super(R.layout.post, Post.class); }

    @Bind(id = R.id.title)
    void bindText(TextView title) {
      title.setText(item.title);
    }
}

Multiple data types are supported by simply providing multiple binders to the adapter.

1
2
3
4
RecyclerViewBinderAdapter.Builder()
        .binder(PostBinder())
        .binder(HeaderBinder())
        .build()
1
2
3
4
new RecyclerViewBinderAdapter.Builder()
        .binder(new PostBinder())
        .binder(new HeaderBinder())
        .build();

Bind order

Data is bound in same order as the bind methods are defined.

1
2
3
4
5
6
7
8
9
@Bind(id = R.id.list) 
fun bindPosts(view: RecyclerView, posts: List<Post>) {
    (view.adapter as MyAdapter).posts = posts
}

@Bind(id = R.id.list) 
fun bindCurrentItem(view: RecyclerView, int: currentItem) {
    view.currentItem = currentItem
}
1
2
3
4
5
6
7
8
9
@Bind(id = R.id.list) 
void bindPosts(RecyclerView view, List<Post> posts) {
    ((MyAdapter)view.getAdapter()).setPosts(posts);
}

@Bind(id = R.id.list) 
void bindCurrentItem(RecyclerView view, int currentItem) {
    view.setCurrentItem(currentItem);
}

Bind control

From a performance perspective, it is important that views are not updated unnecessarily. Updating views can cause chains of measure and layout passes and should be avoided if possible. Witch is designed to only bind data that has changed since last bind pass. Besides positive effect on performance, this alows for things like animations to be executed from bind methods with no risk of running when data has not changed.

This behaviour can be altered by using the @BindWhen annotation. The default strategy for all values is BindWhen.NOT_EQUALS described below.

1
2
3
4
5
6
7
8
9
10
11
@Bind(id = R.id.progress)
@BindWhen(BindWhen.ALWAYS) // Will be part of every bind pass
fun bindProgress() ( ... )

@Bind(id = R.id.items)
@BindWhen(BindWhen.NOT_SAME) // Will run when data is not same instance as last bind pass
fun bindItems( ... )

@Bind(id = R.id.message)
@BindWhen(BindWhen.NOT_EQUALS) // Will run when data is not equal to data from last bind pass 
fun bindMessage( ... )
1
2
3
4
5
6
7
8
9
10
11
@Bind(id = R.id.progress)
@BindWhen(BindWhen.ALWAYS) // Will be part of every bind pass
void bindProgress() ( ... )

@Bind(id = R.id.items)
@BindWhen(BindWhen.NOT_SAME) // Will run when data is not same instance as last bind pass
void bindItems( ... )

@Bind(id = R.id.message)
@BindWhen(BindWhen.NOT_EQUALS) // Will run when data is not equal to data from last bind pass 
void bindMessage( ... ) 

Debugging

Enable logging to get readable representations of all targets when bound.

1
2
3
if(BuildConfig.DEBUG) {
  Witch.setLoggingEnabled(true)
}
1
2
3
if(BuildConfig.DEBUG) {
  Witch.setLoggingEnabled(true);
}

Gradle

1
2
compile 'se.snylt:witch:1.0.0-SNAPSHOT'
annotationProcessor 'se.snylt:witch-processor:1.0.0-SNAPSHOT'
1
2
compile 'se.snylt:witch:1.0.0-SNAPSHOT'
kapt 'se.snylt:witch:1.0.0-SNAPSHOT'
  • Witch is an open source project developed by Simon Edström - software developer and designer at DARESAY - Stockholm, Sweden. Contributions and overall feedback is very welcome.