Anko is a library for Android development in Kotlin. The library provides helper methods that take advantage of Kotlin's extension functions as a way to reduce the amount of boilerplate the Android SDK requires. Those extension functions make it possible to accomplish common tasks like starting an activity or displaying a toast in a very succinct way.
A big part of the library though, focuses on creating a type-safe builder for creating view hierarchies, as an alternative to the XML-inflated view approach. Some of the benefits of defining a layout with Anko are type-safety, and efficiency, since it's not necessary to parse the XML.
I decided to take the DSL for a test drive by rewriting the "Navigation Drawer Activity" template from AndroidStudio, replacing some of the XML layouts with the Anko DSL.
We can define an AnkoComponent
to create the UI:
package com.maximomussini.anko
import android.support.design.widget.AppBarLayout
import android.support.design.widget.Snackbar
import android.support.v4.content.ContextCompat
import android.support.v4.view.GravityCompat
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import com.maximomussini.anko.util.snackbar
import org.jetbrains.anko.*
import org.jetbrains.anko.appcompat.v7.toolbar
import org.jetbrains.anko.design.appBarLayout
import org.jetbrains.anko.design.coordinatorLayout
import org.jetbrains.anko.design.floatingActionButton
import org.jetbrains.anko.design.navigationView
import org.jetbrains.anko.support.v4._DrawerLayout
import org.jetbrains.anko.support.v4.drawerLayout
class MainUI : AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>): View = with(ui) {
drawerLayout {
id = R.id.drawer
fitsSystemWindows = true
createAppBar(ui)
createNavigationView(ui)
}
}
fun _DrawerLayout.createAppBar(ui: AnkoContext<MainActivity>) {
coordinatorLayout {
fitsSystemWindows = true
appBarLayout {
toolbar {
id = R.id.toolbar
popupTheme = R.style.AppTheme_PopupOverlay
backgroundResource = R.color.colorPrimary
}.lparams(width = matchParent) {
val tv = TypedValue()
if (ui.owner.theme.resolveAttribute(R.attr.actionBarSize, tv, true)) {
height = TypedValue.complexToDimensionPixelSize(tv.data, resources.displayMetrics);
}
}
}.lparams(width = matchParent)
relativeLayout {
horizontalPadding = resources.getDimensionPixelSize(R.dimen.activity_horizontal_margin)
verticalPadding = resources.getDimensionPixelSize(R.dimen.activity_vertical_margin)
textView("Hello World!")
}.lparams(width = matchParent, height = matchParent) {
behavior = AppBarLayout.ScrollingViewBehavior()
}
floatingActionButton {
imageResource = android.R.drawable.ic_dialog_email
backgroundColor = ContextCompat.getColor(ui.owner, R.color.colorAccent)
onClick {
snackbar("Replace with your own action", Snackbar.LENGTH_LONG) {
setAction("Action") { ui.toast("Clicked Snack") }
}
}
}.lparams {
margin = resources.getDimensionPixelSize(R.dimen.fab_margin)
gravity = Gravity.BOTTOM or GravityCompat.END
}
}.lparams(width = matchParent, height = matchParent)
}
fun _DrawerLayout.createNavigationView(ui: AnkoContext<MainActivity>) {
navigationView {
fitsSystemWindows = true
setNavigationItemSelectedListener(ui.owner)
inflateHeaderView(R.layout.nav_header_main)
inflateMenu(R.menu.activity_main_drawer)
}.lparams(height = matchParent, gravity = GravityCompat.START)
}
}
And then, use the component to set the content view for the activity instead of using an XML layout:
package com.maximomussini.anko
import android.os.Bundle
import android.support.design.widget.NavigationView
import android.support.v4.view.GravityCompat
import android.support.v4.widget.DrawerLayout
import android.support.v7.app.ActionBarDrawerToggle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.Menu
import android.view.MenuItem
import org.jetbrains.anko.find
import org.jetbrains.anko.setContentView
import org.jetbrains.anko.toast
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
lateinit var drawer: DrawerLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
MainUI().setContentView(this)
drawer = find<DrawerLayout>(R.id.drawer)
val toolbar = find<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
val toggle = ActionBarDrawerToggle(this, drawer, toolbar,
R.string.navigation_drawer_open, R.string.navigation_drawer_close)
drawer.setDrawerListener(toggle)
toggle.syncState()
}
override fun onBackPressed() {
if (drawer.isDrawerOpen(GravityCompat.START)) {
drawer.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_settings -> {
toast("Settings")
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.nav_camera -> toast("Camera")
R.id.nav_gallery -> toast("Gallery")
R.id.nav_slideshow -> toast("Slideshow")
R.id.nav_manage -> toast("Manage")
R.id.nav_share -> toast("Share")
R.id.nav_send -> toast("Send")
}
drawer.closeDrawer(GravityCompat.START)
return true
}
}
In contrast with the generated version, the Anko version does require some boilerplate to set dimensions and colors from resources, but has a lot of expressiveness when it comes to bindings. Notice how it's not necessary to create references to most of the components, since the listeners are added to each view when they are instantiated:
floatingActionButton {
onClick { snackbar("FAB", Snackbar.LENGTH_LONG) }
}
Compare this to the usual code, which incurs in the cost of finding the view (even if it's a very low cost) and referencing the view id:
val fab:FloatingActionButton = findViewById(R.id.fab) as FloatingActionButton
fab.setOnClickListener {
Snackbar.make(it, "FAB", Snackbar.LENGTH_LONG).show()
}
The Anko DSL exposes the native API of each View, so it's only possible to do what Android components can do, with the exception of a few synthetic properties to set text or an image from a resource.
Unfortunately, that means things get pretty rough once we dive into styling and theming. The Android SDK and support libraries contain a lot of hacks that rely on the view being created by a LayoutInflater
from the XML, initializing the view with a Context
and an AttributeSet
. There's no first-class support for setting the style or theme programmatically, which means it's not possible to set them when using Anko either.
Anko does provide a way to style a view, but it leaves much to be desired since it requires targeting the different view classes manually, unlike styling in XML where valid attributes are applied automatically.
When it comes to theming, Android uses ContextThemeWrapper
internally to override getTheme
for a view or its children. Since the current Anko version does not allow to override the context used to create a view inside the DSL, using a theme-wrapped context manually is extremely contrived.
No theming support is a serious limitation, since most components in the design library need a theme to be styled properly.
It should be possible to add support for theming in Anko, but unfortunately theming is just one of many XML-based hacks and workarounds in the SDK.
Adding views with Java code is very cumbersome, so most Java developers will stick to XML, and the SDK and support library will continue to do hacks around XML inflation.
The idea behind the Anko DSL is a very interesting one, but it seems like the Android SDK is not polished enough for Anko to reach its full potential.