image/svg+xml $ $ ing$ ing$ ces$ ces$ Res Res ea ea Res->ea ou ou Res->ou r r ea->r ch ch ea->ch r->ces$ r->ch ch->$ ch->ing$ T T T->ea ou->r

Some different approaches

Several approaches and related APIs are proposed to develop GUI for Android:

  1. The standard API handling classes inheriting from View (graphical components, layouts...). This API exists from the first version of Android and consists in creating trees of graphical components. These trees can be created:
    1. Programmatically but the code is very verbose
    2. Using a XML layout file to describe with XML tags the trees of components with their properties
    3. One can add listeners to components to react to events (like click, touch, key press...). One can also programmatically change the properties of the components (like setting the text displayed by a TextView with setText(...)) when events arise (clicks, lifecycle events of the activity...).
  2. The MVVM (Model-View-ViewModel) approach using the Jetpack Data Binding Library. This approach is similar to the approach by some JavaScript web frameworks like Angular. It was originally proposed in the Windows Presentation Foundation (WPF), a GUI API for the .Net platform. With this approach, the view is defined with a template (layout XML file) with embedded evaluable expressions and code for event handling. When observable objects are updated, the view is automatically refreshed with the new values. Therefore we don't have to call setters anymore on views to change their properties.
  3. The functional composing approach with Jetpack compose. This API is dedicated for the Kotlin language: it proposes a kind of DSL to create GUI by declaring composable functions. Defining the layout in XML file is not required anymore with this approach; all the work is done programmatically in functions. This approach is inspired by the frameworks React (JavaScript) and Flutter (Dart language). Like React and Flutter, Jetpack compose can also be used to create native desktop applications (for Windows, Linux or MacOS).

An example implemented using different approaches

We want to implement a small game with a screen containing one button and a label displaying the number of times the button is clicked. Click after click the background color of the label changes. After N clicks, the game is ended with a congratulation popup message (toast). The activity restarts itself with a goal of N+k clicks.

The examples are implemented using the Kotlin language.

Classical implementation

The classical implementation relies on the use of a declarative layout for the tree of components and the activity where actions to manage lifecycle and events are implemented.

<?xml version="1.0" encoding="utf-8"?>
<!-- Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License -->

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".clickstyle.classical.ClassicalClickGame">

    <TextView
        android:id="@+id/clickTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#000000"
        android:textColor="#FFFFFF"
        android:textSize="40sp"
        app:layout_constraintBottom_toTopOf="@+id/clickButton"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/clickButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Click here"
        android:textSize="40sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.clickstyle.classical

import android.content.Intent
import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import fr.upem.coursand.R
import kotlinx.android.synthetic.main.activity_classical_click_game.*

class ClassicalClickGame : AppCompatActivity() {
    companion object {
        const val DEFAULT_CLICK_GOAL = 10
        const val DEFAULT_CLICK_GOAL_INCREMENT = 10
    }

    private var clickGoal: Int = DEFAULT_CLICK_GOAL
    private var clickNumber: Int = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        clickGoal = intent.getIntExtra("clickGoal", DEFAULT_CLICK_GOAL)
        setContentView(R.layout.activity_classical_click_game)
        clickButton.setOnClickListener {
            clickNumber++
            clickTextView.text = "$clickNumber clicks"
            clickTextView.setBackgroundColor(Color.rgb(0, ((clickNumber.toFloat() / clickGoal) * 255).toInt(), 0))
            if (clickNumber == clickGoal) {
                Toast.makeText(this, "Congratulations, $clickNumber clicks made!", Toast.LENGTH_LONG).show()
                finish()
                Intent(this, ClassicalClickGame::class.java).apply {
                    putExtra("clickGoal", clickGoal + DEFAULT_CLICK_GOAL_INCREMENT)
                    startActivity(this)
                }
            }
        }
    }
}

Data-binding implementation

For the data-binding implementation, we declare a class containing all the data required by the activity (the model):

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.clickstyle.databinding

import androidx.databinding.BaseObservable
import androidx.databinding.Bindable
import fr.upem.coursand.BR

class GameData(_clickGoal: Int = DEFAULT_CLICK_GOAL): BaseObservable() {
    companion object {
        const val DEFAULT_CLICK_GOAL = 10
        const val DEFAULT_CLICK_GOAL_INCREMENT = 10
    }

    @Bindable
    var clickGoal: Int = _clickGoal


    @Bindable
    var clickNumber: Int = 0
        set(value) {
            field = value
            notifyPropertyChanged(BR.clickNumber)
        }

    @get:Bindable
    val isFinished: Boolean get() = clickNumber >= clickGoal


    fun incrementClick() {
        clickNumber += 1
    }
}

We have also a class to implement the actions to do:

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.clickstyle.databinding

import android.app.Activity
import android.content.Intent
import android.graphics.Color
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.databinding.BindingAdapter

object GameAction {

    /** Finish the current activity and start a new one with a new goal */
    private fun restartActivity(v: View, newGoal: Int) {
        val activity = v.context as Activity
        activity.finish()
        activity.startActivity(Intent(activity, DataBindingClickGame::class.java).putExtra("clickGoal", newGoal))
    }

    fun addClick(v: View, data: GameData) {
        data.incrementClick()
        Log.i(javaClass.name, "Clicks made: ${data.clickNumber}/${data.clickGoal}")
        if (data.isFinished) {
            Toast.makeText(v.context, "Congratulations, ${data.clickNumber} clicks made!", Toast.LENGTH_LONG).show()
            restartActivity(v, data.clickGoal + GameData.DEFAULT_CLICK_GOAL_INCREMENT)
        }
    }

    @BindingAdapter("greeness")
    @JvmStatic fun setGreeness(view: View, greeness: Float) {
        view.setBackgroundColor(Color.rgb(0, (greeness * 255).toInt(), 0))
    }
}

Then we create a layout XML file like we have done for the classical implementation but we add the declaration of the required variables gameData and gameAction and we embed declarations of expressions for the values of properties and call to a method for onclick:

<?xml version="1.0" encoding="utf-8"?>
<!-- Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License -->

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="gameData"
            type="fr.upem.coursand.clickstyle.databinding.GameData" />
        <variable
            name="gameAction"
            type="fr.upem.coursand.clickstyle.databinding.GameAction" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".clickstyle.databinding.DataBindingClickGame">

        <TextView
            android:id="@+id/clickTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:greeness="@{(float)gameData.clickNumber / gameData.clickGoal}"
            android:textColor="#ffffffff"
            android:textSize="40sp"
            app:layout_constraintBottom_toTopOf="@+id/clickButton"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:text='@{"" + gameData.clickNumber}' />

        <Button
            android:id="@+id/clickButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Click here"
            android:textSize="40sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:onClick="@{(view) -> gameAction.addClick(view, gameData)}" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Finally we create the activity that binds the model and the template layout together:

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.clickstyle.databinding


import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import fr.upem.coursand.R
import fr.upem.coursand.databinding.ActivityDataBindingClickGameBinding

class DataBindingClickGame : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: ActivityDataBindingClickGameBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding_click_game)

        // create the gameData object containing the data displayed by the activity
        binding.gameData = GameData(intent.getIntExtra("clickGoal", GameData.DEFAULT_CLICK_GOAL))
        binding.gameAction = GameAction
    }

}

Compose implementation

The compose implementation relies on the use of the latest Android Studio canary version.

We create a class to represent the model of the activity (like we have done for the data-binding version) with the annotation @Model:

package fr.upem.coursand.clickstyle.compose

import androidx.compose.Model

@Model
class GameState(val clickGoal: Int = 10, var clickNumber: Int = 0)

And we create the activity that embeds functions to compose the user interface. The user interface is automatically rebuild when fields of the model are updated:

package fr.upem.coursand.clickstyle.compose

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.compose.Composable
import androidx.compose.Model
import androidx.ui.core.*
import androidx.ui.graphics.Color
import androidx.ui.layout.*
import androidx.ui.material.Button
import androidx.ui.material.MaterialTheme
import androidx.ui.material.OutlinedButtonStyle
import androidx.ui.text.TextStyle
import androidx.ui.tooling.preview.Preview

class ComposeClickGame : AppCompatActivity() {
    companion object {
        const val DEFAULT_CLICK_GOAL = 10
        const val DEFAULT_CLICK_INCREMENT = 10
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val gameState = GameState(clickGoal = intent.getIntExtra("clickGoal", DEFAULT_CLICK_GOAL))
        setContent {
            mainView(gameState = gameState)
        }
    }

    @Composable
    fun mainView(gameState: GameState) {
        MaterialTheme() {
            Column(mainAxisAlignment = MainAxisAlignment.SpaceBetween, crossAxisAlignment = CrossAxisAlignment.Center) {
                textView(clickNumber = gameState.clickNumber, clickGoal = gameState.clickGoal)
                HeightSpacer(height = 24.dp)
                Button(text="Let's click here", onClick = {
                    gameState.clickNumber += 1
                    Log.v(javaClass.name, "Clicks: ${gameState.clickNumber}/${gameState.clickGoal}")
                    if (gameState.clickNumber >= gameState.clickGoal) {
                        finish()
                        startActivity(Intent(this@ComposeClickGame, ComposeClickGame::class.java)
                            .putExtra("clickGoal", gameState.clickGoal + DEFAULT_CLICK_INCREMENT))
                    }
                })
            }
        }
    }

    @Composable
    fun textView(clickNumber: Int, clickGoal: Int) {
        Text(text="Clicks: $clickNumber", style = TextStyle(fontSize = 40.sp,
            color = Color.White,
            background = Color(0.0f, clickNumber.toFloat() / clickGoal.toFloat(), 0.0f)
        ))
    }

    @Preview
    @Composable
    fun mainViewPreview() {
        mainView(GameState(10, 1))
    }
}

We notice that we don't need a XML layout anymore; the code to write (in Kotlin) is more concise.