Stevia.kt
Stevia.kt copied to clipboard
:leaves: Healthy ConstraintLayout Sugar
⚠️ For all new projects we strongly encourage you to use Compose, the new official way to build views in Kotlin. Stevia.kt will no longer be updated since Compose is now the way forward :)
Stevia.kt
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 🎉
⚠️ Please be aware that this is an early version, the api is subject to change so use at your own risk :👨🔬👩🔬💥
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 🚀
You + Stevia = 🦄
- [x] 💡 Write concise, readable layouts
- [x] 🏖 Reduce your maintenance time
- [x] 🎨 Compose your styles, CSS-like
Reason - Installation - Documentation - Example
💡 Reason
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
How
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 ❤️
⚙️ Installation
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 :)
📖 Documentation
For reference you can also find the full iOS documentation here, the concepts and naming are very similar.
The 3 pillars of Layout: Hierarchy, Layout, Styling
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!
1 - View Hierarchy
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)
2 - Layout
Sizing
view.width(100)
view.height(50)
view.percentWidth(0.3F)
view.percentHeight(0.5F)
view.size(80)
Centering
view.centerHorizontally()
view.centerVertically(20) // offset
view.centerInParent()
Filling
view.fillHorizontally()
view.fillVertically(20) // padding
view.fillContainer()
Absolute Positioning
view.top(100).left(30)
view.bottom(20).right(40)
Relative Positioning
view.constrainTopToBottomOf(button)
view.constrainCenterXToCenterXOf(button)
view.constrainCenterYToBottomOf(button)
view.followEdgesOf(button)
These are all chainable :rocket:
view.size(60).top(80).centerHorizontally()
Alignment
alignHorizontally(viewA, viewB, viewC) // aligns B & C with A.
alignLefts(viewA, viewB, viewC)
Horizontal layout
// 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
Vertical Layout
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
)
3 - Styling
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!
Content & event binding.
There is no specific api for Content
& Event binding
so you just write it natively :)
About dimensions
All Stevia margin and sizes use dp
sizes to you don't have to explicitly specify it.
🖼 Example
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
}
}
Noteworthy
ConstraintLayout
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 :)
Reflexivity
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)
Why not anko?
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.
Not clear
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.
Not solving the layout part
Another issue I found was that although the view was written in code, nothing was actually improving the readability of the layout code itself.