Drawing a checkers board (data binding and StateFlow)
In this article I explain how to draw men on a checkers board.
Project
The project I use for the demonstation is Checkers.
Credit
In this article I rely heavily on the examples given in the article StateFlow with One- and TwoWay-DataBinding on Android by Anastasia Finogenova.
New versions
According to doco, StateFlow
can be only combined with data binding starting from Gradle plugin 7.0.0-alpha04.
I will describe now my particular setup of Android Studio Arctic Fox | 2020.3.1 Canary 7.
Here is the relevant section of project-level build.gradle:
dependencies {
classpath 'com.android.tools.build:gradle:7.0.0-alpha07'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.21'
}
It is combined with a new Gradle version in gradle-wrapper.properties:
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-all.zip
In Project Structure I use the following JDK path:
/usr/lib/jvm/java-11-openjdk-amd64
Layout file
This is the layout file:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:custom="http://schemas.android.com/apk/res-auto"
>
<data>
<variable
name="viewModel"
type="pl.org.seva.checkers.game.GameVM"
/>
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<pl.org.seva.checkers.game.view.Board
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<pl.org.seva.checkers.game.view.Pieces
android:layout_width="match_parent"
android:layout_height="match_parent"
data="@{viewModel.dataFlow}"/>
</RelativeLayout>
</layout>
It draws the board and the pieces separately.
Please note the line data="@{viewModel.dataFlow}"
. This is what combines StateFlow
with the view.
The fragment
LifecycleOwner
must be set to the binding. Notice the line binding.lifecycleOwner = this
in the code below:
class GameFragment : Fragment(R.layout.fr_game) {
private lateinit var binding: FrGameBinding
private val viewModel by viewModels<GameVM>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FrGameBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
binding.viewModel = viewModel
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
}
}
The board
Drawing the board is straightforward and will not be explained here:
class Board(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val fill = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLACK
style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
with(canvas) {
val dx = right / 8f
val dy = bottom / 8f
repeat(8) { x ->
repeat(8) { y ->
if (x % 2 != y % 2) {
drawRect(x * dx, y * dy, (x + 1) * dx, (y + 1) * dy, fill)
}
}
}
}
}
}
The ViewModel
This is the ViewModel:
class GameVM : ViewModel() {
private val _dataFlow = MutableStateFlow(GameData(whiteStartPosition, blackStartPosition))
val dataFlow: StateFlow<GameData> = _dataFlow
companion object {
val whiteStartPosition = arrayListOf(0 to 7, 1 to 6, 2 to 7, 3 to 6, 4 to 7, 5 to 6, 6 to 7, 7 to 6, 0 to 5, 2 to 5, 4 to 5, 6 to 5)
val blackStartPosition = arrayListOf(0 to 1, 1 to 0, 2 to 1, 3 to 0, 4 to 1, 5 to 0, 6 to 1, 7 to 0, 1 to 2, 3 to 2, 5 to 2, 7 to 2)
}
}
This is the data class:
data class GameData(
val whiteMen: ArrayList<Pair<Int, Int>>,
val blackMen: ArrayList<Pair<Int, Int>>,
)
The ViewModel contains coordinates of white and black men, wrapped in a StateFlow
.
The BindingAdapter
This is the BindingAdapter
. It can be placed in any file, but it must be a global function:
@BindingAdapter("data")
fun setData(pieces: Pieces, data: GameData) {
pieces.setGameData(data)
}
It works with StateFlow
out of the box.
The pieces
This is the view of the pieces. The current version doesn’t support kings:
class Pieces(context: Context, attrs: AttributeSet) : View(context, attrs) {
private var whites = arrayListOf<Pair<Int, Int>>()
private var blacks = arrayListOf<Pair<Int, Int>>()
private val whiteFill = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.GREEN
style = Paint.Style.FILL
}
private val blackFill = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.RED
style = Paint.Style.FILL
}
fun setGameData(data: GameData) {
whites = data.whiteMen
blacks = data.blackMen
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
with (canvas) {
val dx = right / 8f
val dy = bottom / 8f
val radius = min(dx, dy) / 2 * 0.85f
for (piece in whites) {
drawCircle(piece.first * dx + dx / 2, piece.second * dy + dy / 2, radius, whiteFill)
}
for (piece in blacks) {
drawCircle(piece.first * dx + dx / 2, piece.second * dy + dy / 2, radius, blackFill)
}
}
}
}
The function setGameData(data: GameData)
is important, as this is what is called when the value of StateFlow
changes.
Please don’t forget to call invalidate()
.
Example coroutine
This is the example coroutine you might use to test the behavior of the checkers board. (It is not on GitHub):
class GameVM : ViewModel() {
private val _dataFlow = MutableStateFlow(GameData(whiteStartPosition, blackStartPosition))
val dataFlow: StateFlow<GameData> = _dataFlow
init {
viewModelScope.launch {
delay(1000L)
_dataFlow.value = GameData(
arrayListOf(3 to 3, 3 to 4, 4 to 3, 4 to 4),
ArrayList(),
)
}
}
companion object {
val whiteStartPosition = arrayListOf(0 to 7, 1 to 6, 2 to 7, 3 to 6, 4 to 7, 5 to 6, 6 to 7, 7 to 6, 0 to 5, 2 to 5, 4 to 5, 6 to 5)
val blackStartPosition = arrayListOf(0 to 1, 1 to 0, 2 to 1, 3 to 0, 4 to 1, 5 to 0, 6 to 1, 7 to 0, 1 to 2, 3 to 2, 5 to 2, 7 to 2)
}
}
The above code first draws a checkers board with men in their usual positions, and after one second just four white men in the middle.
Conclusion
The article demonstrates how to draw a checkers board with men using data binding and StateFlow
in the new version of Android Studio.
Step by step it introduces the layout file, ViewModel and two custom views, only one of which reacts to changes in the StateFlow
.
At the end it demonstrates how to write a coroutine that, after a delay, moves the men to a new (though invalid) position.