Almost every application has data that is shown in a list. A user is able to interact with this data in various ways, such as liking, favoriting, and removing. We also usually give the user an ability to refresh the list data, load data via pages, or only display new data.
All of these requirements make it difficult to implement a view properly. If we don't synchronize requests we may have strange behaviors due to unpredictable reasons: user interaction event and order, network latency, and even service availability. All of these cases should be covered if we want to have a good working product, and this is where implementing a Command Based Architecture helps us.
Let’s build a sample Android application...
User use-cases we want to cover
AAU I want to see an indicator when loading the initial list
AAU I want to see an error message and retry button if initial loading fails
AAU I want to see a special empty message if loading was successful but no data was passed
AAU I want to see a list of data if initial loading was successful and at least one item was returned from the API
AAU I want to pull-to-refresh (p2r) to refresh page data at any time
AAU I want to see additional data as I scroll down the page
AAU I want to change item favorite status
AAU I want to see immediate favorite status change after the click
Let’s start from the beginning and create additional developer use-cases that we want to cover in addition to our user stories.
Handling pagination:
AAD I want to have only one initial loading request at a time
AAD I want to have only one p2r loading request at a time
AAD I do NOT want to have any p2r loading request if initial is running
AAD I want to only have one next page loading request at a time
These requirements are not too difficult. There are several libraries that are able to solve the above use-cases, including custom state machines or libraries. These are out of the scope of this sample but other possible solutions can be found online.
Handling favorite status change:
AAD I want to be able to execute favorite change request asynchronously for each item (based on uid)
AAD I want to have only one favorite change request at a time for each uid
AAD I want to have as few favorite change requests as possible
AAD I want to have good favorite error handling logic with UI state changes if needed
The above use-cases are a bit more difficult because we have to save different states now: what tasks are running and what was the latest server synced value? There are much fewer libraries that are able to handle all above cases, which causes most of this code to fall on the shoulders of the developer. But nothing is impossible!
Also, we forgot a couple large use-cases that are not handled above:
AAU I want to have a correct UI for the case: change favorite state and perform p2r when the change favorite state was delayed longer than the p2r request
In the above case, p2r can return to the old server value if the change favorite status request is still in progress. However, the user would like to see their change locally and track the favorite request status. Because if it fails, it will be nice to reverse the change and perhaps warn the user.
We additionally have a bit more developer use-cases:
AAD I want to sync p2r and favorite requests to understand which data and changes are synced and which are not
AAD I want to have immediate favorite status update represented on the UI
These new cases are much more complicated because we have to know pagination and favorite requests states and we are not able to encapsulate them. This becomes much more interesting if we have favorites, likes, remove, or any additional functionality. Without an appropriate way of syncing, our system can become unpredictable. Like in the gif below —the user can change favorite status from unfavorite to favorite and back. Two items were updated correctly, but one was updated from the server incorrectly.
Raise condition when adding/removing favorites
Possible solution that can fit ANY logic
The above cases can be rewritten as a single command that contains all dependencies and solves all above requirements. Let’s make it happen!
FavoriteChangeCommand
Action:
Change favorite status if it was updated
Rules:
Only one command can be executed at a time for each item uid
Commands can be executed asynchronously if they are for different item uids
Commands should be executed only if there are no other running commands for this item uid; otherwise, it should be added to the execution queue and remove all other pending commands for this item uid
After successful execution, we should set the new server synced state and execute any other pending command for this item uid
After failed execution, we should revert status and warn the user only if there is no other command in the queue; otherwise, we should start the other command immediately
RefreshCommand
Action:
Control initial loading and p2r
Rules:
Only one command of this type can be executed at a time
Should postpone any other command (favorite or next page load) if running
NextPageLoadCommand
Action:
Load all next pages (except initial one)
Rules:
Only one command of this type can be executed at a time
Should not be executed or added to a queue if RefreshCommand is in the queue or already running (it doesn’t make sense to load page number 5 when there is an active request to load the initial one)
If we’re able to encapsulate this information to create commands with the ability to sync with the above rules, then that will solve all our other use-cases!
ActionCommandis the base class for each command. There are two sets of methods in that class.
The first set of methods controls the execution lifecycle and supports data state change during execution:
Called when command was added to execution queue (may be called multiple times)
Called right before command starts execution
Main execution method — should return command result or fail with exception
Called if command executed normally
Called if command executed with error
Always called after success or fail (like final block in Java)
Any lifecycle method that gets current data state and can update by copying the existing one with appropriate new values. If the new dataState will be changed, the CommandManager will push the new state to the UI (via LiveData).
open fun onCommandWasAdded(dataState: DataState): DataState = dataState
open fun onExecuteStarting(dataState: DataState): DataState = dataState
abstract suspend fun executeCommand(dataState: DataState): CommandResult
open fun onExecuteSuccess(dataState: DataState, result: CommandResult): DataState = dataState
open fun onExecuteFail(dataState: DataState, error: Throwable): DataState = dataState
open fun onExecuteFinished(dataState: DataState): DataState = dataState
The second set of methods controls the execution strategy:
Control if the command should ever be executed or skipped based on the current data, pending commands, or already running commands
Should block other commands from execution while it is running
Control if the command can be executed immediately or should delay
ExecutionStrategy A helper class for common execution strategies that can be shared across different commands.
ConcurrentStrategy
Allows the user to execute each command asynchronous without blocking any other command from execution.
open class ConcurrentStrategy : ExecutionStrategy {
override fun shouldAddToPendingActions(
pendingActionCommands: RemoveOnlyList<ActionCommand<*, *>>,
runningActionCommands: List<ActionCommand<*, *>>
): Boolean =
true
override fun shouldBlockOtherTask(pendingActionCommand: ActionCommand<*, *>): Boolean =
false
override fun shouldExecuteAction(
pendingActionCommands: RemoveOnlyList<ActionCommand<*, *>>,
runningActionCommands: List<ActionCommand<*, *>>
): Boolean =
true
}
ConcurrentStrategyWithTag
Same as ConcurrentStrategy, but will remove any other pending commands with the same tag and wait for previous command execution with the same uid.
open class ConcurrentStrategyWithTag(private val tag: Any) : ConcurrentStrategy() {
override fun shouldAddToPendingActions(
pendingActionCommands: RemoveOnlyList<ActionCommand<*, *>>,
runningActionCommands: List<ActionCommand<*, *>>
): Boolean {
pendingActionCommands.removeAll { command ->
command.strategy.let { it is ConcurrentStrategyWithTag && it.tag == tag }
}
return true
}
override fun shouldExecuteAction(
pendingActionCommands: RemoveOnlyList<ActionCommand<*, *>>,
runningActionCommands: List<ActionCommand<*, *>>
): Boolean =
!runningActionCommands.any { command ->
command.strategy.let { it is ConcurrentStrategyWithTag && it.tag == tag }
}
}
SingleStrategy
Will be executed only in a single action and any other commands will be postponed from execution. It only allows one command with this strategy to exist in the execution queue.
open class SingleStrategy : ExecutionStrategy {
override fun shouldAddToPendingActions(
pendingActionCommands: RemoveOnlyList<ActionCommand<*, *>>,
runningActionCommands: List<ActionCommand<*, *>>
): Boolean =
!pendingActionCommands.any { it.strategy is SingleStrategy }
&& !runningActionCommands.any { it.strategy is SingleStrategy }
override fun shouldBlockOtherTask(pendingActionCommand: ActionCommand<*, *>): Boolean =
true
override fun shouldExecuteAction(
pendingActionCommands: RemoveOnlyList<ActionCommand<*, *>>,
runningActionCommands: List<ActionCommand<*, *>>
): Boolean =
runningActionCommands.isEmpty()
}
SingleStrategyWithTag
It is similar to the SingleStrategy, but will allow only commands with different tags to be added to the execution queue.
open class SingleWithTagStrategy(private val tag: Any) : SingleStrategy() {
override fun shouldAddToPendingActions(
pendingActionCommands: RemoveOnlyList<ActionCommand<*, *>>,
runningActionCommands: List<ActionCommand<*, *>>
): Boolean =
!pendingActionCommands.any { command ->
command.strategy.let { it is SingleWithTagStrategy && it.tag == tag }
}
&&
!runningActionCommands.any { command ->
command.strategy.let { it is SingleWithTagStrategy && it.tag == tag }
}
}
ActionCommandWithStrategy
It will route execution strategy methods to the past ExecutionStrategy class and leave for subclass-only lifecycle methods.
Lets code our sample application
RefreshCommand
Should add refresh indicator state in onCommandWasAdded
Should perform request on background thread in executeCommand
Should remove refresh indicator state in onExecuteSuccess
Should swap old list with a new one in onExecuteSuccess
Should save next page information in onExecuteSuccess (page number, latest loaded item. or whatever is required) if any or remove it if there is no next page
Should remove refresh indicator state in onExecuteFail
Should have its own RefreshStrategy that extends SingleWithTagStrategy (“RefreshStrategy”)
LoadNextCommand
Should add next page loading indicator state in onCommandWasAdded
Should perform request on background thread in executeCommand
Should remove next page loading indicator state in onExecuteSuccess
Should append new data to the existing list in onExecuteSuccess
Should save next page information in onExecuteSuccess (page number, or latest loaded item or whatever is needed) if any or remove it if there is no next page
Should remove next page loading indicator state in onExecuteFail
Should have its own LoadNextStrategy that extends SingleWithTagStrategy(“LoadNext”) and skip this command if any RefreshStrategy is already in the queue or running
ChangeFavoriteStatusCommand
Should save previous server state and new pending state in onCommandWasAdded
Should perform request if pending state is still the one that was requested in this command on background thread in executeCommand
Should change server state to a new one and keep pending state if it is newer than current in onExecuteSuccess
Should revert data to server state if pending state is still the same as requested in this command in onExecuteFail
Should have SingleWithTagStrategy(itemUid) strategy
As you can see, the rules above will completely cover all the necessary use-cases that we described earlier. The only thing that we need to do is implement appropriate data structure and missed logic.
Since pagination is a common task, we created base classes
Page data information:
PageData: for page data that has list of items
PageDataWithNextPageNumber: If you also need page data with next page information as a number
PageDataWithLatestItem: if you also need page data with next page information as latest item
Pagination loading state — PaginationState:
Has PageData object
RefreshState for p2r action states
NextPageLoadingState for next page action states
Commands:
RefreshCommand: contains everything except real request and basic UI items for each state
LoadNextCommand: base class that has everything except real request and basic UI items for each state
LoadNextWithPageNumberCommand: an implementation for PageDataWithNextPageNumber page data
LoadNextWithLatestItemCommand: an implementation for PageDataWithLatestItem page data
Result implementation
We used already-implemented commands for pagination and custom for favorite status change:
internal class ChangeFavoriteStatusCommand(
private val mainItemUid: String,
private val changeToFavorite: Boolean,
private val changeFavoriteAction: suspend () -> Unit,
private val onFavoriteChangeFailed: (Throwable) -> Unit
) : ActionCommandWithStrategy<Unit, ListScreenState>(ConcurrentStrategyWithTag(mainItemUid)) {
override fun onCommandWasAdded(dataState: ListScreenState): ListScreenState =
// we update item state only if the list contains this item
if (dataState.pageData?.itemsList?.any { it.key == mainItemUid } == true) {
dataState.copy(
pageData = dataState.pageData?.updateItemFavoriteState {
it.newSelection(changeToFavorite)
}
)
} else {
dataState
}
override suspend fun executeCommand(dataState: ListScreenState) {
val uiMainItem = dataState.pageData?.itemsList?.firstOrNull { it.key == mainItemUid }
if (uiMainItem?.favoriteState is FavoriteState.PreSelectProgress && uiMainItem.favoriteState.hasChange()) {
// we should run this command only if there was a change. otherwise we should skip it
changeFavoriteAction()
}
}
override fun onExecuteSuccess(dataState: ListScreenState, result: Unit): ListScreenState =
if (dataState.pageData?.itemsList?.any { it.key == mainItemUid } == true) {
dataState.copy(
pageData = dataState.pageData?.updateItemFavoriteState {
it.newFinalState(changeToFavorite)
}
)
} else {
dataState
}
override fun onExecuteFail(dataState: ListScreenState, error: Throwable): ListScreenState =
if (dataState.pageData?.itemsList?.any { it.key == mainItemUid } == true) {
var hasChange = false
val newData = dataState.copy(
pageData = dataState.pageData?.updateItemFavoriteState {
val newFavoriteState = it.revertState(!changeToFavorite)
if (it.favorite != newFavoriteState.favorite) {
hasChange = true
}
newFavoriteState
}
)
if (hasChange) onFavoriteChangeFailed(error)
newData
} else {
dataState
}
private fun PageDataWithNextPageNumber<UIMainItem>.updateItemFavoriteState(
newStateGenerator: (FavoriteState) -> FavoriteState
): PageDataWithNextPageNumber<UIMainItem> =
PageDataWithNextPageNumber(
itemsList.map {
if (it.key == mainItemUid)
it.copy(favoriteState = newStateGenerator(it.favoriteState))
else
it
},
nextPageNumber
)
}
Favorite change command implementation:
internal class ChangeFavoriteStatusCommand(
private val mainItemUid: String,
private val changeToFavorite: Boolean,
private val changeFavoriteAction: suspend () -> Unit,
private val onFavoriteChangeFailed: (Throwable) -> Unit
) : ActionCommandWithStrategy<Unit, ListScreenState>(ConcurrentStrategyWithTag(mainItemUid)) {
override fun onCommandWasAdded(dataState: ListScreenState): ListScreenState =
// we update item state only if the list contains this item
if (dataState.pageData?.itemsList?.any { it.key == mainItemUid } == true) {
dataState.copy(
pageData = dataState.pageData?.updateItemFavoriteState {
it.newSelection(changeToFavorite)
}
)
} else {
dataState
}
override suspend fun executeCommand(dataState: ListScreenState) {
val uiMainItem = dataState.pageData?.itemsList?.firstOrNull { it.key == mainItemUid }
if (uiMainItem?.favoriteState is FavoriteState.PreSelectProgress && uiMainItem.favoriteState.hasChange()) {
// we should run this command only if there was a change. otherwise we should skip it
changeFavoriteAction()
}
}
override fun onExecuteSuccess(dataState: ListScreenState, result: Unit): ListScreenState =
if (dataState.pageData?.itemsList?.any { it.key == mainItemUid } == true) {
dataState.copy(
pageData = dataState.pageData?.updateItemFavoriteState {
it.newFinalState(changeToFavorite)
}
)
} else {
dataState
}
override fun onExecuteFail(dataState: ListScreenState, error: Throwable): ListScreenState =
if (dataState.pageData?.itemsList?.any { it.key == mainItemUid } == true) {
var hasChange = false
val newData = dataState.copy(
pageData = dataState.pageData?.updateItemFavoriteState {
val newFavoriteState = it.revertState(!changeToFavorite)
if (it.favorite != newFavoriteState.favorite) {
hasChange = true
}
newFavoriteState
}
)
if (hasChange) onFavoriteChangeFailed(error)
newData
} else {
dataState
}
private fun PageDataWithNextPageNumber<UIMainItem>.updateItemFavoriteState(
newStateGenerator: (FavoriteState) -> FavoriteState
): PageDataWithNextPageNumber<UIMainItem> =
PageDataWithNextPageNumber(
itemsList.map {
if (it.key == mainItemUid)
it.copy(favoriteState = newStateGenerator(it.favoriteState))
else
it
},
nextPageNumber
)
}
This approach gives you the ability to sync any command and use-case between each other, even if you have split their logic to different classes.
The best thing that you can do is to have a root command manager that will be able to broadcast any update from any child command manager to others, so you will be able to update data after changing in a command manager. This and the current approach can be found in our public Scalio Github repository.
Please feel free to ask any questions here, on github, or via my private email.