- 28th Mar, 2024
- Dhyey B.
28th Jan, 2021 | Farhan S.
You must have heard about the word “Redux” while working with frontend developers or somewhere else, No ? Let me explain.
Redux is a pattern and library for managing and updating application state, using events called "actions". It serves as a centralized store for state that needs to be used across your entire application, with rules ensuring that the state can only be updated in a predictable fashion.
This article delves into the integration of the Redux approach in Android development, particularly leveraging the power of MutableLiveData.
Redux is a state management pattern and library that originated in the JavaScript and web development ecosystem but has found its way into various other platforms, including Android.
Redux provides a predictable and centralized way to manage the state of an application, making it easier to develop and maintain complex user interfaces.
In the context of Android, Redux is not a specific library or framework but rather a set of principles and patterns that can be implemented using existing tools and libraries.
It was designed and developed for JavaScript but we can follow the Redux pattern in Android as well, Redux can be described in three fundamental principles.
According to the Redux principles we will create three components but with slight variation. For state update callback we will use MutableLiveData an AndroidX lifecycle component which allows us to observe the change in the data.
Observers are the functions which observe the changes in the LiveData and get automatically called when it receives any updates.
LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state.
We are using Kotlin to show code examples.
State
A state contains data that is accessible throughout the App. Here we are creating a To Do App for our sample project, so we will be storing the list of tasks in the App State.
To make our todo list observable we will wrap it with LiveData, similarly you can wrap any type of class with LiveData and make it observable. Moreover you can add as many as variables using LiveData.
data class AppState(
val tasks: LiveData<ArrayList<ToDoTask>>,
var isLoading: LiveData<Boolean>
)
Reducer
Reducer contains the logical part, in this class we will create functions to apply CRUD operations. Here we are using a different approach with respect to the Redux principles, we are creating separate functions for different actions instead of action-dispatch functions.
You can not modify LiveData directly, so first we will cast it to MutableLiveData and then apply changes. For more details about MutableLiveData read official documentation here.
class Reducer(private val state: AppState) {
fun addTask(task: ToDoTask) {
val taskList = state.tasks.value!!
taskList.add(task)
// If you are working on Main thread you can use #setValue(),
// for background thread use #postValue()
(state.tasks as MutableLiveData).postValue(taskList)
}
fun deleteTask(task: ToDoTask): Unit {
val taskList = state.tasks.value!!
taskList.remove(task)
(state.tasks as MutableLiveData).postValue(taskList)
}
fun updateTask(index: Int, isChecked: Boolean): Unit {
val taskList = state.tasks.value!!
taskList[index].isCompleted = isChecked
(state.tasks as MutableLiveData).postValue(taskList)
}
}
Store
Store is a global object which contains the AppState and the Reducer. We will create our Store class with AppState and Reducer, Initialise AppState with initial state and pass the initial state to the reducer.
class Store {
var state: AppState
var reducer: Reducer
init {
// Initial state
state = AppState(
MutableLiveData(ArrayList()),
MutableLiveData(false)
)
reducer = Reducer(state)
}
}
Ok! But, How to Combine these Things Together?
The Store should be accessible throughout the application, so we will need to create a global instance of store. Consequently we will extend the Application class and create a companion object, which will allow us to access the store object App wide.
class ThisApp : Application() {
companion object {
val store = Store()
}
}
Don’t forget to add ThisApp to AndroidManifest.xml
To modify the App State we need to call reducer functions which we created earlier. Below is the code which shows how to call reducer functions.
...
// Add
ThisApp.store.reducer.addTask(ToDoTask(1,"New Task","Description",false))
// Update
itemView.taskCheck.setOnClickListener {
ThisApp.store.reducer.updateTask(position,!task.isCompleted)
}
// Delete
itemView.deleteTaskBtn.setOnClickListener {
ThisApp.store.reducer.deleteTask(task)
}
...
For our To Do App, we require to list the tasks in the recycler view, so we will create an Adapter for it. We will use our tasks array list as a data source for TaskAdapter.
class TaskAdapter : RecyclerView.Adapter<TaskViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder {
return TaskViewHolder(View.inflate(parent.context, R.layout.task_item_layout, null))
}
override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
val task = ThisApp.store.state.tasks.value!![position]
holder.bind(task,position)
}
override fun getItemCount(): Int {
return ThisApp.store.state.tasks.value!!.size
}
}
...
But there is a problem, the TaskAdapter doesn’t know that the data has been changed, so we need to notify it about that. For this purpose we will add an observer for the tasks list in our activity as shown below
...
override fun onCreate(savedInstanceState: Bundle?) {
// Add observer to specific live data
ThisApp.store.state.tasks.observe(this) {
taskAdapter.notifyDataSetChanged()
if (it.size == 0) emptyView.visibility = View.VISIBLE
else emptyView.visibility = View.GONE
}
}
...
override fun onDestroy() {
super.onDestroy()
// Remove all observers
ThisApp.store.state.tasks.removeObservers(this)
}
......
override fun onCreate(savedInstanceState: Bundle?) {
// Add observer to specific live data
ThisApp.store.state.tasks.observe(this) {
taskAdapter.notifyDataSetChanged()
if (it.size == 0) emptyView.visibility = View.VISIBLE
else emptyView.visibility = View.GONE
}
}
...
override fun onDestroy() {
super.onDestroy()
// Remove all observers
ThisApp.store.state.tasks.removeObservers(this)
}
...
Whenever the list gets updated, it will call taskAdapter.notifyDataSetChanged() which will update the recycler view. Yesss ! we made it. That's how we can apply the Redux approach in Android using LiveData.
You can clone the sample project from github.
Integrating the Redux approach with MutableLiveData in Android provides a robust and scalable architecture for state management.
The unidirectional data flow, immutability, and predictability offered by Redux contribute to a more maintainable and debuggable codebase.
By leveraging MutableLiveData for reactive updates, developers can seamlessly connect the Redux architecture with the Android UI, creating a powerful combination for building modern and responsive applications.
As Android development continues to evolve, embracing such architectural patterns becomes increasingly essential for crafting high-quality, scalable, and maintainable apps.
Note: This approach might not suit all types of projects, in the end it depends on you to choose appropriate architecture as per your project requirements. The LiveData which we used is a part of MVVM architecture recommended by the Android officials.
References:
Tags: Android,Kotlin,Redux,Javascript,Design Pattern,Technology
Get insights on the latest trends in technology and industry, delivered straight to your inbox.