Store: a brief history
One of the first libraries I open sourced was Store which was an implementation of the Repository pattern wrapped in RxJava. Store abstracted away the common pattern of load from memory, if not present load from disk parse the data, if not present load from network. This was all done with minimal setup by a user:
Initially Store was something I developed based on my use cases. While everything was interface based, I shipped a few implementations of parsers for Gson, Moshi, Jackson) and a FilePersister which eased in streaming data from a network to disk.
Fast forward 18 months and Store is up to 3000+ stars & 37 contributors mostly from folks I’ve never met (Thank you all!). In that time the community has helped implement:
- Memory & disk caching policies
- Wrappers for fetchers that return full objects rather than streams
- Result abstractions to determine which source data is coming from
- New API for refreshing get calls on clearing data
- A fluent Kotlin DSL
- Documentation including translations to Russian & Spanish
- An update to RxJava2
- Many other PRs full of performance improvements and bug fixes.
Store as it stands today is a full fledged data solution that both helps prevent extraneous data calls to your network and helps write apps that exhibit proper separation of concerns. Since moving on from the New York Times, I’ve had a chance to rethink how Store can better suit the needs of users in the modern Android ecosystem. I’m not just the primary maintainer of Store, I also use it in every project I have built since.
Time to get a Room
One of the most popular open source requests Store had was integration with Room. I was hesitant to integrate Room because I had never used it before and I wasn’t really sure how the integration would work. Now that I’m working at Nike’s s23NYC studio, we explored what data solution best fits into our re-architecting of the SNKRS Android app that we maintain. After exploring various solutions we decided that Room was by far the most robust and easy to use persistence solution. While Room was great at working with a database, it was still missing key features like request de-duping, memory caching, stale/refresh policies and all the other great features mentioned above. We still needed a repository abstraction that would wrap Room to implement these features. So, my team set out to fuse all the great features of Room with all the things we loved from Store.
Since most of Store was based on interfaces, we were able to integrate Room into Store by building a RoomPersister and StoreRoom, which is a specialized type of store. The StoreRoom includes the familiar features from Store while taking advantage of the reactive nature of Room & Flowables.
Room: Remind me what that is?
Room is a persistence library that is part of Google’s Architecture Components. Room is an ORM and does a fantastic job of giving you all the power of working with a database without any of the messiness of cursors, database connectors or having to map data yourself. Room is built on top of SQLite and takes care of a lot of the boilerplate associated with saving to or reading from a database.
Additionally, Room’s reactive API through its first class RxJava support transforms simple SQL queries into auto updating Flowables. Room queries exposed as RxJava Flowables is what I’ve always imagined as the perfect persistence layer to Store. It helped me get rid of messy code like the old stream & getRefreshing functions we previously used to fake endless streams. Using a Room backed persister allows Store subscriptions to update anytime the underlying data changes.
Store+Room a match made in Android heaven
Using the new StoreRoom is every bit as easy as using a regular Store.
First, create a Room DAO as you always do with Room. Within your DAO, expose an
Insert & a
Query function. The types do not need to be the same.
Next, implement Store’s
Fetcher to tell StoreRoom how to get new network data. Below is an example that would use an API (Retrofit is recommended) to return a
Single<User> based on a passed in user id.
Since both Retrofit & Room work with objects, there’s no reason to implement a parser anymore. We will let Retrofit take care of the parsing when a new network request comes in.
Next, we need to make an implementation of our RoomPersister. Notice how the read function returns a
List<User> while the write function is only writing a single user each time.
You can of course change what the read and write functions return. As another example, you might want to write a collection or return a single item — the choice is up to you.
Now that we have our
Persister, we can create an implementation of a StoreRoom:
Voila! All the functionality we always had with Stores but rather than being backed by a file persister we are now backed by a Room DAO.
Want to get data but don’t care about the source? Call
store.get(). Need to bust through your cache and call network only? Call
So far this feels just like a normal Store. What do we gain by using Room besides ability to query & have different read/write types? Reactive reads, that’s what!
Since Room can expose data as Flowables, StoreRoom will expose endless reactive streams from your get and fetch calls. For example, if you call store.get() and subscribe to the results, anytime another call is made to your stores’
fetch method, your subscriber receive an updated value.
With StoreRoom we not only have disk and memory caching but we now have endless observables that will auto update subscribers if the underlying data we are querying for changes. Particularly in the above example every new user that gets fetched from the network and saved to the DB will cause an emission of all the users to any current subscribers.
Store and Room are a match made in heaven, Room is inherently reactive as the use case above demonstrates while Store gives you memory that sits on top of those DB requests and missing features like request throttling when you request the same data in parallel from various parts of your app (Think of reading the same config value N times on app start without having to hit your network or db more than once). As always with Store, new subscribers to
Store.get() don’t need to go all the way to your DB if your memory cache has not expired. This leads to my ideal data workflow of fast reads from memory and reactive reads from disk all backed by a network source.
I hope you enjoy using our new
RoomPersisterthat our s23NYC team is contributing back to Store. Next, we hope to work towards an annotation processor that would auto generate the above Store code from a Room DAO. As always community help is appreciated!