Tablespoon
Tablespoon (creatively named after Dagger and Butterknife) helps you bind attributes easily in your custom views using annotations to generate boilerplate code.
class CustomView (...) : View(...) {
@ColorAttr(R.styleable.CustomView_bgColor)
var bgColor: Int = Color.RED
@DimensionAttr(R.styleable.CustomView_radius)
var radius: Float = 0f
var text: String by dynamicAttr("") // Auto updates View when being set
init {
TableSpoon.init(this, attrs, R.styleable.CustomView)
}
}
Features
- Avoid boilerplate code such as:
val typedArray = view.context.theme.obtainStyledAttributes(...)
try {
radius = typedArray.getDimension(...)
// do more with typedArray...
} finally {
a.recycle()
}
- Make your properties dynamic by using the
dynamicAttr
extension. When these properties are updated, the view is automatically updated by callingrequestLayout()
andinvalidate()
on the View.
Install
You can install Tablespoon by adding this to your build.gradle file:
dependencies {
implementation 'com.nikhilpanju:tablespoon:1.0.2'
kapt 'com.nikhilpanju:tablespoon-processor:1.0.2'
}
Limitation
From Android Gradle Plugin 5.0 onwards (yet to be released), resource IDs (any R.xyz
value) will be non final. To circumvent this, add Butterknife's gradle plugin to your buildscript
.
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.jakewharton:butterknife-gradle-plugin:10.1.0'
}
}
and then apply it in your module:
apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'
Now make sure you use R2 instead of R inside all Tablespoon annotations:
@ColorAttr(R2.styleable.CustomView_bgColor)
var bgColor: Int = Color.RED
Usage
First, all the attributes must be declared in res/values/attrs.xml
. For example:
<resources>
<declare-styleable name="CustomView">
<attr name="bgColor" format="color" />
<attr name="radius" format="dimension" />
<attr name="icon" format="reference" />
</declare-styleable>
</resources>
Each of these attributes can then be used with their respective annotation.
- All annotations must be defined with the corresponding attribute ID as defined above.
Tablespoon.init()
must be called in the view constructor with the parent styleable ID as defined above (R.styleable.CustomView
)- Default values can directly be defined on the property
- Properties cannot be
private
orprotected
.
Example
class CustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs, defStyleAttr) {
@ColorAttr(R.styleable.CustomView_bgColor)
var bgColor: Int = Color.RED // RED is default value
@DimensionAttr(R.styleable.CustomView_radius)
var radius: Float = 0f // 0f is default value
// dynamic attributes auto update your view when they are updated
@delegate:DrawableAttr(R.styleable.CustomView_icon)
var icon: Drawable? by dynamicDrawableAttr(null)
init {
TableSpoon.init(this, attrs, R.styleable.CustomView)
}
}
Initializing
Tablespoon.init()
can be called using all or some of the view constructor parameters. Additionaly, you can also supply a lambda to it which will be called on the TypedArray
before recycling it in case some custom operations need to be performed.
class CustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0,
) : View(context, attrs, defStyleAttr, defStyleRes) {
var fullName: String? = null
init {
TableSpoon.init(this, attrs, R.styleable.CustomView, defStyleAttr, defStyleRes) {
// this block is called using: TypedArray.() -> Unit
fullName = getString(R.styleable.CustomView_firstName) +
getString(R.styleable.CustomView_lastName)
// ... any other operation on TypedArray before it's recycled
}
}
}
Dynamic Properties
These are properties that will automatically redraw the view when updated.
@delegate:ColorAttr(R.styleable.CustomView_bgColor)
var bgColor: Int by dynamicIntAttr(Color.RED) // RED is default value
fun makeBgGreen() {
// updating bgColor here will automatically update the view by
// calling the view's requestLayout and invalidate methods
bgColor = Color.GREEN
}
Note: When using dynamic properties with annotations, the prefix
@delegate:
must be used before the annotation so that the delegate can be targeted.
- For any property that is not annotated, you can simply use
dynamicAttr()
.
Additional parameters
- If you don't want both
invalidate()
andrequestLayout()
to be called, you can set either of them to false. - You can also supply a lambda which will be called before updating the view (acts like an
Observable
delegate).
var stringAttr: String by dynamicStringAttr(
initialValue = "default string",
invalidate = true, // default is true
requestLayout = false // default is true
) { newString ->
// this block is called before the view is updated
// in case some pre-operations need to be performed
}
Annotations and Dynamic Properties
Here is a list of all the possible annotations and dynamic property delegates that can be used for each attribute and field type
XML Attr Type | Field Type | Annotation | Dynamic propery delegate |
---|---|---|---|
boolean | Boolean | @BooleanAttr | dynamicBooleanAttr() |
color | Int | @ColorAttr | dynamicIntAttr() |
color | ColorStateList | @ColorAttr | dynamicColorStateAttr() |
dimension | Int | @DimensionAttr | dynamicIntAttr() |
dimension | Float | @DimensionAttr | dynamicFloatAttr() |
enum | Int | @IntAttr | dynamicIntAttr() |
flags | Int | @IntAttr | dynamicIntAttr() |
float | Float | @FloatAttr | dynamicFloatAttr() |
integer | Int | @IntAttr | dynamicIntAttr() |
reference | Int | @ResourceIdAttr | dynamicIntAttr() |
reference | Drawable | @DrawableAttr | dynamicDrawableAttr() |
string | String | @StringAttr | dynamicStringAttr() |
DrawableAttr
@DrawableAttr
is a convenience annotation for directly getting a Drawable
from a reference/resource ID. dynamicDrawableAttr()
is the corresponding dynamic property delegate.
Contributing
Pull requests are welcome! Feel free to browse through open issues to look for things that need work. If you have a feature request or bug, please open a new issue so we can track it.
License
Copyright 2020 Nikhil Panju.
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.