In the past days, I taked a coding challenge, that, in summary, required making a Tic Tac Toe game.
The requirements involves, optionally, target mobile platforms, using Kotlin, Swift or Flutter, but it can be maded using a minimal TUI.
My main language is Java, most of my projects and my study targets Java, and profissionally I worked most with React and Typescript. The logical choice here could be going for TUI with Java or Typescript, but I opt in target mobile platform, and learn Jetpack Compose and Android Development in the proccess.
Here is what I've learned:
The Programming Model
The modern Android development uses Jetpack Compose library for developing UI's. How is it?
It's mainly similar to React model of making UI.
The main artifact that you produce to compose a UI is a Composable. Composables are functions marked with @Composable
annotation, that can return an Unit
(that's somewhat similar to void) or can return some value, but the majority of composables returns just Unit
.
But how is it similar to React if React return objects (behind JSX)?
The DSL is really clear in this aspect. Composables are functions that paint the UI, where you use composition, crafting the UI with other Composables.
Here's an example of a dummy Composable:
@Composable
fun FormSection(
@StringRes title: Int,
modifier: Modifier = Modifier,
children: @Composable (() -> Unit) = {},
) {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = stringResource(title),
fontWeight = FontWeight.Bold,
fontSize = TextUnit(20.0F, TextUnitType.Sp),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(5.dp))
children()
}
}
This composable is dummy because it don't holds any state or effect in it. Here, it accepts another function, similar to the children
declared in React to accept another composable.
"Hooks"
In Jetpack Compose, we don't have any type of hooks, but we have constructs that acts similarly to hooks.
Explicitly, the programming model is builded around observables. Observables are based on the Observer pattern, were we have a set of subscribers, that consists of callbacks. When the Observable receives some update (or some value), it gonna notify every subscriber, by invoking the stored callbacks.
The Jetpack Compose's useState behaves exactly like that. We use delegation to remember
function, and the callback function it receives returns a mutableStateOf<T>
.
var someState by remember { mutableStateOf(0) }
But, an important thing to see here is that, the main subscriber of this observer is the Composable. When the observer receives some value, the Composable is invoked again in a process called recomposition. Just like React's reconciliation.
Another interesting thing are LaunchedEffect
s. They are really the useEffect
of Jetpack Compose. They are really fit to run some asynchronous rotine, like a Coroutine Timeout to close some type of dialog:
LaunchedEffect(key1 = "Popup") {
// this is a suspend function call
viewModel.makeRobotPlay()
}
The best part of this is, unlike React, LaunchedEffect
s can run in any part of Composable lifecycle. So we can wrap this in some conditional to run only in a certain condition:
if (openRobotPlayingTime) {
LaunchedEffect(key1 = "Popup") {
viewModel.makeRobotPlay()
}
AlertDialog(
onDismissRequest = { /*TODO*/ },
confirmButton = { /*TODO*/ },
text = {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(
text = stringResource(R.string.robot_time_playing_popup_title),
style = MaterialTheme.typography.titleLarge
)
Text(
text = stringResource(R.string.robot_time_playing_popup_content))
}
})
}
Every composable here can be invoked in any order.
Coroutines
Coroutines are core part of Kotlin programming, so here it haves a central utility.
Normally you can use coroutines for every async need, but remember that coroutines are colored functions (just like Promises). So, remember that coroutine only runs in:
- A
CoroutineScope
launch callback - A LaunchedEffect
- Another suspend function
To tackle this in a functional and synchronous programming model, Jetpack Compose give us Reactive Programming based upon observables.
I learned the hard way that some observables should not be used on some type of coroutine scope (generally associated on data persistence).
These observables can be used to manage state accross application.
View Model
The view model is where we centralize the application state to manage more complex states in simple data objects. Then, we gonna wrap these objects in StateFlow
and make progressive updates on these objects. Every function and composable listening on the StateFlow
gonna be notified and updated.
Is really that simple on three steps:
- Create a data class that will represent the state of application
data class GameState(
val player1Name: String? = null,
val player2Name: String? = null,
val isRobotEnabled: Boolean = false,
val tableSize: Int = 3,
val gameTable: Array<Array<CellStates>>? = null,
val playerTime: PlayerTime = PlayerTime.Player1,
val winner: Winner = Winner.NoWinner
)
- Create your view model, based on this data class. The object should be wrapped on a
StateFlow
observable and should be exposed with.asStateFlow()
function.
class GlobalStateViewModel(db: AppDatabase) : ViewModel() {
private val _uiState = MutableStateFlow(GameState())
val uiState = _uiState.asStateFlow()
}
- Subscribe to the view model in Composable using delegation:
@Composable
fun GameScreen(
viewModel: GlobalStateViewModel, modifier: Modifier = Modifier,
onNewGame: () -> Unit
) {
val globalUiState by viewModel.uiState.collectAsState()
}
To update the data on this viewModel, just call the method update on the _uiState
. The update should be exposed through a viewModel method:
fun makeUpdate{
_uiState.update {
it.copy(
// update the components of the data class here
)
}
}
Data Access
Data access can be maded with Room Library.
I describe this library as Spring Data without steroids.
We use annotated classes and interfaces to model the data access and data entities of app persistence level.
@Entity
data class User(@PrimaryKey(autoGenerated = true) val id: Long?, @ColumnInfo(name="name) val name: String?)
@Dao
interface UserDao {
@Insert
fun create(user: User): Long?
}
Finally, we then define our database with an abstract class that gonna expose the creation of our Dao.
@Database(version = 1, entities=[User::class])
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
And that's it.
Just instantiate the database in your code and you are ready to use it.
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "tic-tac-toe"
).build()
Initially, when using database, I go through some crashes. It's because the database cannot be accessed on the main thread, to prevent blockings on the UI.
Behind the scenes, Room is just a wrapper around SQLite. So it's just a file, and the interaction with the file is maded with some C functions.
How to access the database without blocking the UI?
You can use a corroutine with Dispatchers.IO
dispatcher, like that:
viewModelScope.launch(Dispatchers.IO) {
dao.create(user)
}
I find that a good way to abstract this is using in viewModel.
Conclusion
That was a good experience, and I learned a lot of things!
The final code for this challenge is here:
https://github.com/EronAlves1996/TicTacToe
pt-BR version: https://github.com/EronAlves1996/artigos-ptbr/blob/main/Coisas-que-aprendi-fazendo-um-desafio-de-codigo-em-kotlin-(android).md
Top comments (0)