Skip to main content

Introduction

This is the second part of our two-part guide on handling CRUD (Create, Read, Update, Delete) operations in Store. In the first part, we focused on defining a flexible data model and operations to accommodate complex queries and mutations. This part will focus on implementing these CRUD operations in our Store.
Prerequisites:
  • Complete Part 1 of this guide.
  • Have an understanding of the core Store concepts.
The code for this example is available in the Trails repository.

Implementing the Fetcher

We’ll begin by extracting the Fetcher creation logic into a separate class called PostFetcherFactory.
We only need to handle Query operations in the Fetcher, because we only invoke Fetcher on reads.
Using Fetcher.ofFlow allows us to support operations that observe changes over time, such as ObserveOne and ObserveMany.

Implementing the Source of Truth

Next, we’ll extract the Source of Truth creation logic into separate classes for better readability. We will create a Reader to handle read operations and a Writer to handle create, update, and delete operations.

Defining the Reader

The Reader is responsible for reading data from the local database based on the operation requested.
Similar to Fetcher, our Reader only needs to handle Query operations.

Defining the Writer

The Writer handles writing data to the local database after fetches or mutations.
However, our Writer needs to handle all Query and Mutation operations because we write to the Source of Truth on both reads and writes.

Setting Up the Source of Truth Factory

Implementing the Reader

When emitting data, if there is no value to emit (e.g., no entities found), we return null instead of an empty list. This prevents the Store from considering the operation fulfilled and triggers a fetch from the network.

Implementing the Writer

Ensure that your Writer handles all operation cases, including both mutations and queries, to maintain consistency between the local data and remote sources.

Implementing the Updater

Next, we’ll extract the Updater creation logic into a separate class called PostUpdaterFactory. The Updater is responsible for synchronizing local mutations with the remote data source.
We invoke Updater on reads if conflicts might exist. This means we need to handle query operations too. If we do hit code for handling a query operation, it means we are fetching a Post but the Bookkeeper has an unresolved sync failure for that Post. So, before we can pull the latest value, we need to push our latest local value to the network.

Implementing the Bookkeeper

We only check for failed syncs on reads. We only set failed syncs on writes.

Conclusion

Finally, we can use these components to create a Store for our Post use case.
We use the same type for all three parameters in the Converter because we are not transforming the data anymore. The Converter simply passes the data through as-is.
Organizing our code into separate classes and factories makes it more maintainable and easier to understand. Each component has a clear responsibility.
By implementing these components, we’ve built a Store that supports complex CRUD operations, enabling efficient data synchronization between local storage and remote sources. This modular approach allows for easier testing, maintenance, and potential reuse in other parts of your app.