Todo App With Settings Example - Android

In this example, let's see how we can build an application with multiple screens. Ideally, we would have a single state for related screens, but for didactical reasons, they are different in this example. (Check the Todo app with settings application source code on GitHub).

The first screen of our app has a to-do list and a button to go to the settings screen, which has options for theming the list.

These are the states for both screens:

// State for the list
public class TodoList {
    private final List<TodoItem> items;

    TodoList(List<TodoItem> items) {
        this.items = new ArrayList<>(items);
    }

    List<TodoItem> getItems() {
        return new ArrayList<>(items);
    }
}

// State for the settings
public class TodoSettings {

    private final int backgroundColor;
    private final int textColor;

    public TodoSettings(int backgroundColor, int textColor) {
        this.backgroundColor = backgroundColor;
        this.textColor = textColor;
    }

    public int getBackgroundColor() {
        return backgroundColor;
    }

    public int getTextColor() {
        return textColor;
    }
}

Based on the states, these are the Actions available in the app:

//Actions for the list
static final String ADD_ITEM = "ADD_ITEM";
static final String DELETE_ITEM = "DELETE_ITEM";
static final String MOVE_ITEM = "MOVE_ITEM";
static final String TOGGLE_ITEM = "TOGGLE_ITEM";

//Dispatched to add a new todo item
static Action addAction(String itemTitle) {
    return new Action<>(ADD_ITEM, itemTitle);
}

//Dispateched to delete a todo item
static Action deleteAction(int itemIndex) {
    return new Action<>(DELETE_ITEM, itemIndex);
}

//Dispateched to change the position of a todo item in the list
static Action moveAction(Pair<Integer, Integer> indexesToMove) {
    return new Action<>(MOVE_ITEM, indexesToMove);
}

//Dispatched to toggle the todo item as completed
static Action toggleAction(int itemIndex) {
    return new Action<>(TOGGLE_ITEM, itemIndex);
}

//Actions for the settings
static final String CHANGE_BACKGROUND_COLOR = "CHANGE_BACKGROUND_COLOR";
static final String CHANGE_TEXT_COLOR = "CHANGE_TEXT_COLOR";

//Dispatched to change the background color of the list
static Action changeBackgroundColorAction(int backgroundColor) {
    return new Action<>(CHANGE_BACKGROUND_COLOR, backgroundColor);
}

//Dispatched to change the text color of the list
static Action changeTextColorAction(int textColor) {
    return new Action<>(CHANGE_TEXT_COLOR, textColor);
}

Since we have two separate states we also have two separate reducers:

  • TodoReducer that reduces the todo list action.
  • SettingsReducer that reduces the settings screen actions.
// Reduces todos actions and state only
public class TodoReducer extends Reducer<TodoList> {

    @NonNull
    @Override
    public TodoList reduce(@NonNull TodoList oldState, @NonNull Action<?> action) {

        switch (action.getActionType()) {
            case TodoActionFactory.ADD_ITEM: {
                String title = action.getData();
                TodoItem newItem = new TodoItem(title, false);
                List<TodoItem> todoItems = oldState.getItems();
                todoItems.add(newItem);
                return new TodoList(todoItems);
            }

            case TodoActionFactory.DELETE_ITEM: {
                int index = action.getData();
                List<TodoItem> todoItems = oldState.getItems();
                todoItems.remove(index);
                return new TodoList(todoItems);
            }

            case TodoActionFactory.MOVE_ITEM: {
                final Pair<Integer, Integer> data = action.getData();
                int from = data.first;
                int to = data.second;
                List<TodoItem> todoItems = oldState.getItems();
                TodoItem itemToMove = todoItems.remove(from);
                todoItems.add(to, itemToMove);
                return new TodoList(todoItems);
            }

            case TodoActionFactory.TOGGLE_ITEM: {
                int index = action.getData();
                List<TodoItem> todoItems = oldState.getItems();
                TodoItem itemToToggle = todoItems.remove(index);
                TodoItem toggledItem =
                        new TodoItem(itemToToggle.getTitle(), !itemToToggle.isCompleted());
                todoItems.add(index,toggledItem);
                return new TodoList(todoItems);
            }

            default: {
                return oldState;
            }
        }
    }

    @NonNull
    @Override
    public TodoList getInitialState() {
        return new TodoList(new ArrayList<TodoItem>());
    }
}

// Reduces settings actions and state only
public class SettingsReducer extends Reducer<TodoSettings> {
  
    @Nullable
    @Override
    public TodoSettings reduce(@NonNull TodoSettings oldState, @NonNull Action<?> action) {
        switch (action.getActionType()) {
            case SettingsActionFactory.CHANGE_BACKGROUND_COLOR: {
                int backgroundColor = action.getData();
                int textColor = oldState.getTextColor();

                return new TodoSettings(backgroundColor, textColor);
            }

            case SettingsActionFactory.CHANGE_TEXT_COLOR: {
                int backgroundColor = oldState.getBackgroundColor();
                int textColor = action.getData();

                return new TodoSettings(backgroundColor, textColor);
            }

            default: {
                return oldState;
            }
        }
    }

    @NonNull
    @Override
    public TodoSettings getInitialState() {
        return new TodoSettings(TodoColors.WHITE, TodoColors.BLACK);
    }
}

Now that we defined the states, actions, and reducers, it's time to create the store:

public class TodoApplication extends Application {

    private Store store;

    @Override
    public void onCreate() {
        super.onCreate();

        store = Suas.createStore(new TodoReducer(), new SettingsReducer())
                .withDefaultFilter(Filters.EQUALS)
                .build();
    }

    public Store getStore() {
        return store;
    }
}

Building the UI

We have two screens in our application; the todo screen and the settings screen.

Settings Screen

The settings screen looks like the following:

That's the code for SettingsActivity:

public class SettingsActivity extends AppCompatActivity implements Listener<TodoSettings> {

    private Store store;
    private View backgroundColorPreview;
    private View textColorPreview;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_settings);

        backgroundColorPreview = findViewById(R.id.background_color_preview);
        textColorPreview = findViewById(R.id.text_color_preview);

        store = ((TodoApplication) getApplication()).getStore();
    }

    @Override
    protected void onStart() {
        super.onStart();
        store.addListener(TodoSettings.class, this).informWithCurrentState();
    }

    @Override
    protected void onStop() {
        super.onStop();
        store.removeListener(this);
    }

    @Override
    public void update(@NonNull TodoSettings settings) {
        backgroundColorPreview.setBackgroundColor(settings.getBackgroundColor());
        textColorPreview.setBackgroundColor(settings.getTextColor());
    }

    public void selectColor(View colorOption) {
        switch (colorOption.getId()) {
            case R.id.background_black: {
                store.dispatch(SettingsActionFactory.changeBackgroundColorAction(TodoColors.BLACK));
                return;
            }

            case R.id.background_white: {
                store.dispatch(SettingsActionFactory.changeBackgroundColorAction(TodoColors.WHITE));
                return;
            }

            case R.id.background_red: {
                store.dispatch(SettingsActionFactory.changeBackgroundColorAction(TodoColors.RED));
                return;
            }

            case R.id.background_blue: {
                store.dispatch(SettingsActionFactory.changeBackgroundColorAction(TodoColors.BLUE));
                return;
            }

            case R.id.text_black: {
                store.dispatch(SettingsActionFactory.changeTextColorAction(TodoColors.BLACK));
                return;
            }

            case R.id.text_white: {
                store.dispatch(SettingsActionFactory.changeTextColorAction(TodoColors.WHITE));
                return;
            }

            case R.id.text_red: {
                store.dispatch(SettingsActionFactory.changeTextColorAction(TodoColors.RED));
                return;
            }

            case R.id.text_blue: {
                store.dispatch(SettingsActionFactory.changeTextColorAction(TodoColors.BLUE));
                return;
            }

            default: {
                throw new IllegalArgumentException("Invalid option");
            }
        }
    }
}

Let's break the SettingsActivity down:

  • SettingsActivity is a very simple color picker. It offers color options for background and text and shows what's selected.
  • First let's get a reference to the Store in the TodoApplication during onCreate().
  • Then map the selected color button to an Action and dispatch it to the store on selectColor(View colorOption).
  • Finally, because of SettingsActivity implements Listener<TodoSettings>, it's time to show the select color on the screen by handling the current state in update(@NonNull TodoSettings settings).

Todo List Screen

The list screen looks like the following:

Notice that this screen has both states: a list of todo items and settings for them. For this reason, we should subscribe to the whole state of the Store and combine the TodoList and TodoSettings in a single state that can be used by the screen. To achieve this let's create a view model based on both called TodoListViewModel and then implement StateSelector<TodoListViewModel> to generate it.

//View model using both states
class TodoListViewModel {

    private final TodoList todoList;
    private final TodoSettings todoSettings;

    TodoListViewModel(TodoList todoList, TodoSettings todoSettings) {
        this.todoList = todoList;
        this.todoSettings = todoSettings;
    }

    TodoList getTodoList() {
        return todoList;
    }

    TodoSettings getTodoSettings() {
        return todoSettings;
    }
}

//Implementation of StateSelector<TodoListViewModel> on the MainActivity
public class MainActivity extends AppCompatActivity implements StateSelector<TodoListViewModel>,
        Listener<TodoListViewModel> {
    
    //...
    @Nullable
    @Override
    public TodoListViewModel selectData(@NonNull State state) {
        TodoList todoList = state.getState(TodoList.class);
        TodoSettings todoSettings = state.getState(TodoSettings.class);
        return new TodoListViewModel(todoList, todoSettings);
    }
    
}

We then register the MainActivity as both: Listener<TodoListViewModel> and StateSelector<TodoListViewModel>:

public class MainActivity extends AppCompatActivity implements StateSelector<TodoListViewModel>,
        Listener<TodoListViewModel> {

    //...
    @Override
    protected void onStart() {
        super.onStart();
        store.addListener(this, this).informWithCurrentState();
    }

    @Override
    protected void onStop() {
        super.onStop();
        store.removeListener(this);
    }
}

And finally, we handle the TodoListViewModel in the update() method:

@Override
public void update(@NonNull TodoListViewModel todoListViewModel) {
    todoListAdapter.update(todoListViewModel);
}

What's Next

Todo app with settings application source code on GitHub

Learn more about Suas listeners

Suas listeners
Using the StateSelector
Adding a listener with a filter

Other sample apps

List of sample applications
Counter App Example
Todo App Example
Search Cities Example