source link: https://blog.stylingandroid.com/datastore-1-0-0-alpha08/?utm_campaign=Feed%3A+StylingAndroid+%28Styling+Android%29
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
DataStore: 1.0.0-alpha08Skip to content
Back in September 2020, I published a series of articles about the shiny new DataStore Jetpack library. DataStore impressed me at the time. However, there have been some changes since the
1.0.0-alpha01 release that we looked at back then. At the time of writing the latest version is
1.0.0-alpha08 and there have been some breaking changes since
alpha01. In this article, we’ll look at those changes and bring the sample code up to date.
I have generally updated the project to use the latest versions of all the dependent libraries. Most of the updates were simply a case of updating the library version, but there were a couple of other dependencies which broke things.
Firstly there are some breaking changes to Hilt. Previously, the
DataStoreModule was installed into the
ApplicationComponent has been deprecated in favour of
SingletonComponent. This is more consistent with Dagger’s scope names.
The other breaking change was in Wire. The
ComplexData protobuf had members named
external. These were instances of the enums
External respectively. The newer implementation mapped these field names to
external_ to avoid potential clashes with the type itself. So I renamed these to
externalEnum instead of using the training underscores.
Before moving on to the changes required for DataStore, I must confess to a bug in the original sample code. When adding the DataStore encryption, I inadvertently included a bug that limits the encrypted data to 255 bytes. As part of the encrypted stream, I included the size of the encrypted data. This was written as an
Integer. So it would overflow if the length was greater than 255 bytes. This would result in crashes when reading the data back as the size was incorrect.
To fix this I removed the size of the encrypted data altogether, and when reading it back we just read all of the remaining data:
The size of the Initialisation Vector will typically be small – 16 bytes – so we don’t need to worry about storing its size as an
There have been a number of changes to DataStore which broke the code. Jetpack libraries may not have stable APIs at alpha release. So one of the problems of using them is that you may have to update your code if the APIs change.
The first change which affected the sample code from the previous series is that the core DataStore library has been split in to two separate libraries.
datastore-core contains the pure Kotlin components that have no dependencies on the Android Framework.
datastore is much smaller but provides integrations with the Android Framework. So it was necessary to change the library dependency to use
datastore instead of
This refactoring into separate libraries, also included some package changes for some of the DataStore components. These needed changing accordingly throughout the sample code.
The most significant change is how we construct instances of our DataStore. Previously there was an extension function of
createDataStore(). A property delegate that returns a global data store instance has replaced this. The idea here is that we use the delegate to obtain an instance of our global DataStore instance in the consumer:
The intention here is good, but I’m not sure I agree with the implementation. The intent is that by using this pattern, we’ll use a singleton throughout the app. However, this becomes problematic when we’re doing things that are a little more complex than basic use-cases.
The problem with the property delegate
Hilt injects a Singleton DataStore instance wherever it is needed in the sample code. The module which provides it looks like this in the old sample code:
We cannot use the property delegate inside the provides function. It is not a property if it is declared inside a function. So it needs to be declared as a property of the
DataStoreModule object. However, the
DataStoreModule cannot be injected with the required
Crypto instance. It is part of the DI framework, not part of our application code.
This rules out using the property delegate in our DI modules if we have other dependencies which are required for the DataStore. In this case the
Serializer instance needs a
One option would be to inject the
Serializer instance in to the consumer, and use the property delegate there:
However, as the comment suggests, this will not work because
serializer will not have been initialised by the time the property delegate is invoked to obtain the data store instance.
The solution that I decided upon was to avoid using the property delegate altogether, and use
This works, but it is now the responsibility of the DI components to ensure that a singleton is used. That is achieved by the
The property delegate ensures the use of a singleton instance. But the choice of implementing this as a property delegate means that it can be impossible to use it in more complex scenarios which benefit most from Inversion Of Control provided by Dependency Injection.
The use of a property delegate also makes testing much harder. Mocking things that we don’t own is generally considered bad practice for testing. While we don’t own the
DataStore instance, we do own the data that it produces and consumes. (It’s actually a bit more subtle than that. We sort of own the DataStore because it is generated code based off of our protobuf definition). So being able to substitute in an alternative
DataStore instance can make testing much easier. However, the use of a property delegate makes this impossible. So for testability, we need to avoid using the property delegate. Once again, creating the
DataStore instance in the Hilt module makes testing much easier.
viewModels() property delegate
Those familiar with the KTX
viewModels() implementation will know that this is actually a property delegate. While I think that the choice of a property delegate for
DataStore has been inspired by this, there are some significant differences in implementation.
ViewModel is a first class citizen when it comes to Hilt. The
@HiltViewModel annotation adds the
ViewModel to the dependency graph. The
viewModels() property delegate will provide injected instances from the Hilt dependency graph. By contrast,
DataStore currently has no integration whatsoever with Hilt.
viewModels() property delegate has an optional
factoryProducer argument. The
factoryProducer is responsible for creating
ViewModel instances. So we can substitute in alternative
ViewModel implementations using this mechanism. This is used as part of the Hilt integration, but also allows us to use fakes or mocks for testing. Once again, the
DataStore property delegate is currently hard-coded to create the
DataStore instance so doesn’t offer the same flexibility.
viewModels() property delegate is also a
Lazy delegate. This means that it will not attempt to instantiate the
ViewModel instance until it is accessed. This plays much nicer within Android lifecycles because creation is performed much later on. Conversely, the
DataStore property delegate is invoked when its parent class is created. This will be very early on in components with a lifecycle, and may be before injection has occurred.
On the whole,
DataStore is improving. However, I feel that the current implementation of the property delegate is flawed because it simply will not play nicely with DI nor allow for easy testing. Having to manually enforce a singleton in the DI modules can easily be missed.
The source code for this article is available here.
Many thanks to Sebastiano Poggi for proofreading and technical feedback.
© 2021, Mark Allison. All rights reserved.
Copyright © 2021 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.
Aggregate valuable and interesting links.
Joyk means Joy of geeK