Deck building
Topics: reducers, actions as data
This section dives a bit more in how we can see a view model from the lenses of an important topic in functional programming: reducers (also known as folds, or if you prefer a Greeker word, catamorphisms).
Let's look at the components of the DeckViewModel
:
- An initial state, comprised of
Awesome Deck
as title and an empty list of cards, - A series of operations (
changeTitle
,clear
,add
) which transform this state.
Using these two elements, we can understand the whole lifetime of the application as consecutive steps, each transforming the previous state.
graph LR; State1[State 1]; State2[State 2]; State3[State 3]; State4[...]; Initial -->|add| State1; State1 -->|changeTitle| State2; State2 -->|add| State3; State3 -->|add| State4;
To understand why we call this the reducer model, let's take a small leap of faith, and assume we somehow represent the sequence of transformations as a list (no worries, we are making this real in a few paragraphs). Then the current state of the system is define using the fold
operation over the list of transformations until that point.
val currentState =
transformationsUntilNow.fold(initialState) { state, transformation ->
transformation.apply(state)
}
In Kotlin, the difference between fold
and reduce
is that in the former you provide an initial state, whereas in the latter the initial state is the first element in the list. But this is not as clear cut in other programming languages. For example, Redux is one of the libraries that popularized this concept in JavaScript.
Instead of using methods, we can express each of the available operations as data. We call this technique actions as data, or with a fancier term, reification. The description of each action should contain enough information to apply the operation to the current state. For our view model, we obtain:
sealed interface DeckOperation {
data class ChangeTitle(val newTitle: String): DeckOperation
data class AddCard(val card: Card): DeckOperation
data object Clear: DeckOperation
}
Now our view model can use a single function, and dispatch based on the operation.
class DeckViewModel: ViewModel() {
fun apply(operation: DeckOperation) = when (operation) {
is DeckOperation.ChangeTitle -> { ... }
is DeckOperation.AddCard -> { ... }
DeckOperation.Clear -> { ... }
}
}
In the tasks below we use this idea to improve the implementation, and introduce new functionality.
Actions as data is the beginning of a journey to domain specific languages (DSLs), the idea of introducing a small language to describe your specific domain. In particular, actions as data relate to a particular technique to implement DSLs, called initial style. Inikio is a compiler plug-in to facilitate the development of such DSLs in Kotlin.
Move to actions as data
Your task is to finish the conversion of the given code into an actions-as-data-based approach. That is, copy (and extend if necessary) the DeckOperation
type given above, and change the view model to use a single point of entry apply
to every transformation.
Even though you can keep just the list of actions that were performed, and apply them whenever the current state is required, this choice usually leads to bad performance. We strongly recommend that you keep the same MutableState
as you have now.
Remove a card
Right now the only option the users of Poké-Fun have if they have added a card they do not like is to clear the entire deck 🫠 Your task is to implement functionality to remove a card from the deck: this involes changes in both view model and view.
Undo and redo
One functionality which becomes much easier to implement when operations are reified as data is undo and redo, since you can very easily keep track of what the user has done.
Your task is to finish the implementation: the given view contains buttons for the actions, but they do nothing and are never enabled. At the end, the corresponding buttons in the view should only be enabled when there are operations to undo (or redo, respectively).