In this series I want to implement a simple shopping app using the Redux pattern in Xamarin Forms.
First we need to create the nessesary folders and files.
I like to seperate app features in own feature folders. So my Order feature folder structure look like this:
The domain folder contains all domain specific parts. In this examples the Models file which contains all nessesary Models as Records.
Contains the XAML-Page and the ViewModel
The state contains all Redux related parts:
In the next parts i implement one use-case after another to fill this whole files with content.
The user should see all available products to buy.
public record ProductId(int Value);
public record ShoppingCartItemId(int Value);
public record Product(ProductId Id,decimal Price,string Description);
public record ShoppingCartItem(ShoppingCartItemId Id,Product Product,int Amount);
public record InitializeOrderAction();
public record InitializeOrderActionSuccess(List<ShoppingCartItem> ShoppingCartItems);
public record InitializeOrderActionFailure(Exception Exception);
When dealing with collections, you often repeat the same process to add, update and remove entity from your collection state. Entities in ReduxSimple simplify the managing of such cases in the reducer.
public class ShoppingCartItemEntityState : EntityState<ShoppingCartItemId,ShoppingCartItem> { }
public static class Entities
{
public static EntityAdapter<ShoppingCartItemId, ShoppingCartItem> ShoppingCartItemAdapter { get; set; }
= EntityAdapter<ShoppingCartItemId, ShoppingCartItem>.Create(item => item.Id);
}
There is a little boilerplate code involved but you will see the benefits later.
The state represents the UI-State of the order
public record OrderState
{
public decimal Sum { get; set; }
public ShoppingCartItemEntityState ShoppingCartItems { get; set;}
public bool IsLoading { get; set; }
public static OrderState InitialState => new OrderState()
{
ShoppingCartItems = new ShoppingCartItemEntityState(),
Sum = 0m,
IsLoading = false
};
}
We define ShoppingCartItems which uses the ShoppingCartItemEntityState-helper for representing the ShoppingCartItems list.
The Sum property represent the sum in € of all products.
The IsLoading property indicates the the view is busy.
After OrderState is created we can add it to our RootState:
public record RootState
{
public OrderState Order { get; set; }
public static RootState InitialState => new RootState()
{
Order = OrderState.InitialState
};
}
Here all reducers from all subreducers are combined. We have only one reducer.
public static class Reducers
{
public static IEnumerable<On<RootState>> CreateReducers()
{
return CombineReducers(Order.State.Reducers.GetReducers());
}
}
Based on what you need, you can observe the entire state or just a part of it.
Selector allows you to select parts of the State.
public static class Selectors
{
public static ISelectorWithoutProps<RootState, OrderState> SelectOrderState = CreateSelector(
(RootState state) => state.Order
);
public static ISelectorWithoutProps<RootState, decimal> SelectSum = CreateSelector(
SelectOrderState,
(state) => state.Sum
);
public static ISelectorWithoutProps<RootState, bool> SelectIsLoading = CreateSelector(
SelectOrderState,
(state) => state.IsLoading
);
// Selectors for ShoppingCartItem-List
private static readonly ISelectorWithoutProps<RootState, ShoppingCartItemEntityState> SelectShoppingCartItemsEntityState = CreateSelector(
SelectOrderState,
state => state.ShoppingCartItems
);
private static readonly EntitySelectors<RootState, ShoppingCartItemId, ShoppingCartItem> ShoppingCartItemsSelector = ShoppingCartItemAdapter.GetSelectors(SelectShoppingCartItemsEntityState);
public static ISelectorWithoutProps<RootState, List<ShoppingCartItem>> SelectShoppingCartItems = ShoppingCartItemsSelector.SelectEntities;
}
First we create a selector for the OrderState. This selector selects only the OrderState from the RootState and can be reused to select deeper parts of OrderState.
The second selector selects to the Sum changes.
The third selector selects to the IsLoading changes.
The fourth selector selects with a little magic from ReduxSimple to the ShoppingCartItem-List changes from OrderState.
public static IEnumerable<On<RootState>> GetReducers()
{
return CreateSubReducers(SelectOrderState)
.On<InitializeOrderAction>(state => {
var mockShoppingCartItems = new List<ShoppingCartItem>()
{
new ShoppingCartItem(new ShoppingCartItemId(1),
new Product(new ProductId(1),109.95m,"Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops"),
0),
new ShoppingCartItem(new ShoppingCartItemId(2),
new Product(new ProductId(2),22.3m,"Mens Casual Premium Slim Fit T-Shirts"),
0)
};
return state with
{
ShoppingCartItems = ShoppingCartItemAdapter.AddAll(mockShoppingCartItems, state.ShoppingCartItems),
};
})
.ToList();
}
This reducer reacts on InitializeOrderAction and fills the ShoppingCartItem list with mock data.
You can see how i’am using the previosily created ShoppingCartItemAdapter to populate the property.
I’am also using C#9 with-Expressions to create a new state without mutating it 🤩. It creates a new state where only ShoppingCartItems has a new value, the rest of the properties remain unchanged.
In the next part, when we use effects, we will get real data from an api and create a new InitializeOrderActionSuccess from the effect.
The last part is the linking of all parts in the ViewModel and the view.
using System;
using System.Collections.ObjectModel;
using static ShoppingApp.App;
using static ShoppingApp.Order.State.Selectors;
namespace ShoppingApp.Order.Presentation
{
public class OrderViewModel : BaseViewModel
{
public OrderViewModel()
{
Title = "Order";
var shoppingCartDisposable = Store.Select(SelectShoppingCartItems)
.Subscribe(x => CartItems = new ObservableCollection<ShoppingCartItem>(x));
Disposables.Add(shoppingCartDisposable);
Store.Dispatch(new InitializeOrderAction());
}
public ObservableCollection<ShoppingCartItem> CartItems { get; set; }
}
}
The ViewModel selects ShoppingCartItems from the store and fills the CartItems ObservableCollection when something changes.
To trigger the loading of the list the InitializeOrderAction must be dispatched to the store.
In the first iteration of OrderPage we can see the loaded mock data.
We have implemented the store parts:
We also used this parts in the ViewModel and View.
You can find the current state of the app’s code here: