Skip to content

Yummypets/Stevia.kt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

35 Commits
 
 
 
 
 
 
 
 

Repository files navigation

⚠️ 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

Language: Kotlin Platform: Android 8+ codebeat badge License: MIT

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 = 🦄

  • 💡 Write concise, readable layouts
  • 🏖 Reduce your maintenance time
  • 🎨 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 🚀

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.