With Stevia.kt
, you build concise views in code using ConstraintLayout
.
detail.top(35).left(12)
nameLabel.centerInParent()
backgroundImage.fillParent()
I-20-firstname-20-latname-20-I
This is all native ConstraintLayout
under the hood 🎉
This is a kotlin port of the popular Stevia iOS layout library. The new ConstraintLayout
in android is very similar to the iOS AutoLayout so we figured, why not apply what we learnt on iOS to android?
Stevia.kt
was born 🚀
- 💡 Write concise, readable layouts
- 🏖 Reduce your maintenance time
- 🎨 Compose your styles, CSS-like
Reason - Installation - Documentation - Example
Because nothing holds more truth than pure code 🤓
XML files are heavy, hard to maintain, hard to merge.
They split the view concept into 2 separate files making debugging a nightmare
There must be a better way
By creating a tool that makes layout code finally readable by a human being.
View layout becomes fun, concise, maintainable and dare I say, beautiful ❤️
Gradle
In your top project .gradle
file add maven { url 'https://dl.bintray.com/s4cha/Stevia' }
like so
...
allprojects {
repositories {
google()
jcenter()
maven { url 'https://dl.bintray.com/s4cha/Stevia' }
}
}
...
then in your app .gradle
file add implementation 'yummypets.stevia.android:stevia:1.0.3'
like so
dependencies {
...
implementation 'yummypets.stevia.android:stevia:1.0.3' // server
}
- Manually Copy and paste the source folder for now :)
For reference you can also find the full iOS documentation here, the concepts and naming are very similar.
Stevia chose the path of clearly separating the different layout steps.
It has a neat effect: you know exactly where to look for when coming back for modifying the code.
Remember, code is read way more often than it is written!
subviews(
name,
detail
)
subviews()
is essentially a shortcut that calls addView()
and
makes sure the view you are using has a unique id
setting id = View.generateViewId()
on both subviews and the container itself.
It also has the benefit of being very visual so that your can actually see what the view hierarchy is. This is especially true for nested hierarchies :
subviews(
subview1,
subview2.subviews(
nestedView1,
nestedView2̨
),
subview3
)
Which is the equivalent of the native code below :
id = View.generateViewId()
subview1.id = View.generateViewId()
subview2.id = View.generateViewId()
subview3.id = View.generateViewId()
nestedView1.id = View.generateViewId()
nestedView2.id = View.generateViewId()
addView(subview1)
addView(subview2)
addView(subview3)
subview2.addView(nestedView1)
subview2.addView(nestedView2)
view.width(100)
view.height(50)
view.percentWidth(0.3F)
view.percentHeight(0.5F)
view.size(80)
view.centerHorizontally()
view.centerVertically(20) // offset
view.centerInParent()
view.fillHorizontally()
view.fillVertically(20) // padding
view.fillContainer()
view.top(100).left(30)
view.bottom(20).right(40)
view.constrainTopToBottomOf(button)
view.constrainCenterXToCenterXOf(button)
view.constrainCenterYToBottomOf(button)
view.followEdgesOf(button)
These are all chainable 🚀
view.size(60).top(80).centerHorizontally()
alignHorizontally(viewA, viewB, viewC) // aligns B & C with A.
alignLefts(viewA, viewB, viewC)
// Stick a label to the left of the screen
I-label
// With a custom margin
I-42-label
// Combine all at once \o/
I-avatar-15-name-20-followButton-I
layout(
50,
avatar
)
// This is the equivalent of avatar.top(50)
While using layout
for a single element might seem a bit overkill, it really shines when combined with horizontal layout.
Then we have the full layout in one place (hence the name).
layout(
50,
I-15-avatar.size(60)
)
The avatar is 50px from the top with a left margin of 15px and a size of 60px
Another great example is a login view, representable in one single statement !
layout(
100,
I-email-I,
8,
I-password-forgot-I,
"",
I-login-I,
0
)
Well, just call style
on a View subclass :
In-line for small or unique styles
textView.style {
textSize = 4F.dp
textAlignment = TEXT_ALIGNMENT_CENTER
}
Or in a separate function to make them reusable
// My style method, kinda like CSS
fun textStyle(t: TextView) {
t.textSize = 4F.dp
t.textAlignment = TEXT_ALIGNMENT_CENTER
}
// Later
{
// Set my style
textView.style(::textStyle)
}
This way the styles become reusable and composable: you can chain them! You can even create a Style File grouping high level functions for common styles. Usage then becomes very similar to CSS!
There is no specific api for Content
& Event binding
so you just write it natively :)
All Stevia margin and sizes use dp
sizes to you don't have to explicitly specify it.
Because an example is worth a thousand words :) This is taken from a view we use in production. Spoiler alert, write Half the code that is actually 10X more expressive and maintainable 🤓
package com.octopepper.yummypets.component.food.home
import ...
import com.octopepper.yummypets.common.stevia.*
class FoodTypeFilter(context: Context, wording: String, imageResource: Int) : CardView(context) {
var button = Button(context)
private val constraintLayout = ConstraintLayout(context)
private val imageView = ImageView(context)
private val textView = TextView(context)
init {
// View Hierarchy
subviews(
constraintLayout.subviews(
imageView,
textView,
button
)
)
// Layout
constraintLayout.size(103)
imageView.fillParent()
textView.bottom(10).fillHorizontally()
button.fillParent()
// Style
style {
radius = 0F.dp
cardElevation = 4F.dp
useCompatPadding = true
isClickable = true
isFocusable = true
}
button.style {
setBackgroundResource(selectableItemBackground(context))
setBackgroundColor(Color.TRANSPARENT)
}
textView.style {
textSize = 4F.dp
textAlignment = TEXT_ALIGNMENT_CENTER
}
// Content
imageView.setImageResource(imageResource)
textView.text = wording
}
}
ConstraintLayout
children are supposed to have their width
and height
set to MATCH_CONSTRAINT
.
This can be problematic with TextView
or Button
for example, since they are set to WRAP_CONTENT
by default.
This means you have to explicitly opt-out from wrap content
mode when you want to stretch them out.
For example
label.width(matchConstraints) // This is needed, and is the same as == label.layoutParams.width = ConstraintSet.MATCH_CONSTRAINT
label.left(20).right(20)
Thankfully Stevia does this automatically for you when it's obvious that you don't want to have wrapContent
.
For example if you write
label.fillHorizontally()
Stevia automatically sets label.layoutParams.width = ConstraintSet.MATCH_CONSTRAINT
.
Please be aware that this has nothing to do with Stevia itself, this just standard ConstraintLayout
machinery :)
Note that the way ConstraintLayout
works is that constraints are not reflexive.
This can be quite misleading at first, especially for those of you coming from iOS with Autolayout, where this is the case.
This is very important to keep in mind, this means that
viewA.constrainLeftToRightOf(viewB) != viewB.constrainRightToLeftOf(viewA)
alignHorizontally(viewA, viewB) != alignHorizontally(viewB, viewA)
Actually, coming from using Stevia extensively on iOS, first thing we did coming to android was looking for a similar approach: building views in code that don't suck. Naturally we first tried anko which is very well known in the community. After a while using it, I personally was still frustrated, yes the views were in code but I didn't feel like the code was any clearer.
Indeed anko's classic approach is to do everything at once, View hierarchy, layout, styling. While is seems tempting at first, the resulting code is compact and is quite hard to maintain.
Another issue I found was that although the view was written in code, nothing was actually improving the readability of the layout code itself.