Anko DSL vs Android XML-First

January 04, 2016 5 min read

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.

Alt

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.

Anko does not have theming support

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.

By @Maximo @MaximoMussini