recommended: To see so muchMVP+Dagger2+Retrofit+RxjavaProject, easy to takestarAre you tempted? Are you frustrated that your friends are already using these technologies early in their projects and you’re not?

MVPArmsIs aMVP+Dagger2+Retrofit+RxjavaConfigurable fast integration framework (currently the most complex and highly configurable integration framework for Dagger application), with tens of thousands of charactersThe documentAs well asA key generation MVPDagger2File and other functions, mature and stable, with access to thousands of commercial projects, 5K + STAR (# 1 MVP framework in the worldFor now, just focus on logic and do the restMVPArmsCome and build your ownMVP+Dagger2+Retrofit+RxjavaProject!!!!

The original address: http://www.jianshu.com/p/b58ef6b0624b

preface

Retrofit is one of the most popular web request libraries out there right now. It’s a standard part of every project to use Retrofit with Okhttp. Because Okhttp comes with a cache, many people don’t care about other cache libraries, but those of you who have used Okhttp caches know that Okhttp caches must be used with headers “, which is cumbersome and not flexible enough, so I now recommend a special Retrifit cache library RxCache

Project address: RxCache Demo address: RxCacheSample

Introduction to the

RxCache use annotations to Retrofit the configuration cache information, internal use dynamic proxy and Dagger, the library information is relatively small, the official tutorial is all in English, it is harder to use for developers, but my English is not good, but the source code is general, so I from the Angle of the source code for everyone to explain this library The difficulty of the source code of this library actually lie in the Dagger injection. I will explain the usage of the Dagger first and I will write an article later to explain the source code. For those of you who are learning Dagger, besides checking my MVPArms, you can also check the source code of RxCache, you can learn a lot Fresh, please look forward to my later source code analysis

use

1. Define the interface, which is similar to Retrofit. Each method in the interface corresponds to the method in Retrofit interface one by one AmicKeyGroup and EvictProvider are not required, but if passed, only one object can be passed in each. Otherwise, an error will be reported

/** * public interface CacheProviders {@lifecache (duration = 2, duration = 2); timeUnit = TimeUnit.MINUTES) Observable<Reply<List<Repo>>> getRepos(Observable<List<Repo>> oRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey); @LifeCache(duration = 2, timeUnit = TimeUnit.MINUTES) Observable<Reply<List<User>>> getUsers(Observable<List<User>> oUsers, DynamicKey idLastUserQueried, EvictProvider evictProvider); Observable<Reply<User>> getCurrentUser(Observable<User> oUser, EvictProvider evictProvider); }Copy the code

2. Instantiate the interface, which is similar to Retrofit construction. Pass in the using method of the interface, and return a dynamic proxy object of the interface

CacheProviders cacheProviders = new RxCache.Builder()
                .persistence(cacheDir, new GsonSpeaker())
                .using(CacheProviders.class);
Copy the code

Break down

RxCache is easy to use, the above two steps can easily implement the cache, this library features mainly on the cache custom configuration, so I will focus on the parameters and annotations.

parameter

Observable

Observable means that you pass in the Retrofit interface that you want to cache as a parameter (the return value must be Observable). RxCache will be reinvoked through the Retrofit interface if there is no cache, the cache has expired, or the EvictProvider is true Get the latest data, wrap the result as Reply and return it to the in-memory cache and disk cache before returning

Observable

>> if you want to know where the returned result comes from (local, in-memory, or network) and whether it’s encrypted or not, you can use Observable

> as the return value of the method. RxCache wraps the result in Reply Observable > Declares the resulting data type.

exception

If the build RxCache will useExpiredDataIfLoaderNotAvailable when set to true, the data is empty or when an error occurs, ignore EvictProvider to true or cache expiration, continue to use the cache (premise is the request before the cache)

DynamicKey & DynamicKeyGroup

What many developers are most confused about is the meaning of these two parameters. Does it matter if they are passed together or not? How does RxCache store caches? RxCache does not store and retrieve caches using urls as identifiers

What is that?

That’s right, RxCache is an identifier that stores and fetches a cache from these two objects plus the method names declared in the CacheProviders interface above

The identifier rule is: method name +$d$d$d$" + dynamicKey.dynamicKey + "$g$g$g$"+ DynamicKeyGroup. Group dynamicKey or DynamicKeyGroup is null, then an empty string is returned.The method name$d$d$d$$g$g$g$"
Copy the code

What does that mean?

RxCache, for example, uses Map as its in-memory cache, and uses this identifier as its Key, PUT, and GET data. (Local caches use this identifier as the file name, and stream the file to or from the file to store or fetch the cache.) If the stored and retrieved identifiers are inconsistent, the cache cannot be retrieved

What does it have to do with us?

For example, we an interface with functions of paging, we use RxCache give him 3 minutes to set up the cache, if the two objects are not in the incoming parameters, it will default to the interface name of the method to store and retrieve the cache, before we use this interface to obtain the first page of the data, multiple calls within three minutes this interface, request it So if we want to have paging now, we have to pass in DynamicKey. DynamicKey has a key stored inside it. We pass in the number of pages when we build All it does is change the method name +DynamicKey to a String identifier to fetch and store the cache

What’s the relationship between DynamicKey and DynamicKeyGroup

DynamicKey stores a Key. The application scenario of DynamicKey is as follows: When requesting the same interface, different data needs to be returned based on different variables. For example, when constructing pages, the number of pages can be passed in

DynamicKeyGroup stores two keys. DynamicKeyGroup is an enhanced version based on DynamicKeys. Application scenarios: When a DynamicKeyGroup is created, the same interface requests need to be paging and each page needs to return different data based on different logins In the constructor, pass the page number as the first argument and the user id as the second

DynamicKeyGroup = DynamicKeyGroup = DynamicKeyGroup = DynamicKeyGroup = DynamicKeyGroup = DynamicKeyGroup Two keys (user identifiers) plus the interface name form the identifier to retrieve and store data, ignoring the first Key(page number) of the DynamicKeyGroup

EvictProvider & EvictDynamicKey & EvictDynamicKeyGroup

All three objects have a Boolean field inside them, which means whether to expel (use or delete) the cache. RxCache considers whether to use the cache based on this Boolean field when it fetches an unexpired cache, and if true, refetches new data through Retrofit False will use this cache

What’s the relationship between these three objects?

EvictProvider < EvictDynamicKey < EvictDynamicKeyGroup: you can only pass one of these three objects. If you pass more than one object, you will get an error. If you pass more than one object, it will not matter

What’s the difference?

If there is an unexpired cache and the Boolean in it is false, it doesn’t matter which of the three you pass, but if the Boolean is true, it makes a difference. RxCache will delete the cache when the Boolean is true after Retrofit requests new data

What are the deletion rules?

Again, take asking for an interface whose data will return different data for different pages and display different data for different users on the same page

New EvictProvider(false); That defaults to false and does not delete any cache

EvictDynamicKeyGroup will only delete the cache for the corresponding user under the corresponding page

EvictDynamicKey will delete all caches under that page. For example, if you are requesting data from user1 under the first page, it will delete not only user1’s data but also other user2,user3 under the current page. The data of

EvictProvider will delete all caches under the current interface. For example, if you request the first page of data, it will not only delete the first page of data, but also delete all other pages under the interface

So you can choose which object to pass according to your own logic. If the requested interface doesn’t have pagination, and you don’t want to use caching, you should pass EvictProvider and pass true when you build it, but if you pass EvictDynamicKey and EvictDynamicKeyGro Up achieves the same effect

annotations

@LifeCache

@lifecache, as its name implies, is used to define the life cycle of the cache. When Retrofit gets the latest data, it encapsulates the data and the configuration information of the data into a Record, which saves one copy locally and one copy in memory. The Record stores the value of ** @lifecache ** (ms) and the current data The time to achieve success (milliseconds)timeAtWhichWasPersisted

Each time the cache is fetched later, timeatwhichwhichwaspersisted + @lifecache will be determined if the value is less than the current time (milliseconds), which will expire, and the current cache will be cleaned immediately and Retrofit will be used to re-request the latest data, if EvictProvider is true The cache is not used regardless of whether the cache is expired

@EncryptKey & @Encrypt

The purpose of these two annotations is to encrypt the cache, but the scope is different

EncryptKey is used on interfaces

@EncryptKey("123")
public interface CacheProviders {

}
Copy the code

Whereas ** @encrypt ** is used for methods

@EncryptKey("123") public interface CacheProviders { @Encrypt Observable<Reply<User>> getCurrentUser(Observable<User> oUser, EvictProvider evictProvider); }}Copy the code

If you need to Encrypt the cache of a request interface, add ** @encrypt ** to the corresponding method. RxCache uses the ** @encryptKey value as the Key to Encrypt and decrypt cached data when storing and obtaining cache data. Therefore, all methods in each provider can only be enabled Use the same Key** for encryption and decryption

Note that RxCache encrypts only the local cache and does not encrypt the in-memory cache. CipherInputStream is used to encrypt local data and CipherOutputStream is used to decrypt data

@Expirable

Remember that when we built RxCache, we had a setMaxMBPersistenceCache method. This can be set to the maximum size of the local cache, in MB, or default to 100MB if not set

@expirable = Expirable = @expirable = @expirable = @expirable = @expirable

B: of course! Remember I said that every time Retrofit retrieves the latest data, it stores a copy of the latest data in the in-memory cache and a copy in the local cache before returning it

Once stored, the current local cache size is checked, and if the total cache size currently stored in the local cache is greater than or equal to 95 percent of the size set in setMaxMBPersistenceCache (default: 100MB),RxCache does something to keep the total cache size at 100 percent 70 the following

What do you do?

RxCache will iterate over all data stored in a cacheDirectory until it is less than 70 percent of the size of the data stored in RxCache Using it on the method and setting it to false**(default to true if you don’t use this annotation) ensures that the interface’s cached data survives every time it needs to be cleaned up

   @Expirable(false)
	Observable<Reply<User>> getCurrentUser(Observable<User> oUser, EvictProvider evictProvider);
Copy the code

It is worth noting: The cacheDirectory passed in by the persistence method when building RxCache is the directory that holds the local RxCache. It is best not to have any data other than RxCache in this folder. This will save unnecessary overhead every time the cache needs to be traversed and cleaned because of RxCa Che doesn’t check the file name, whether it’s in its cache or not, it’s going to go through and get it

@SchemeMigration & @Migration

These two annotations are used for data migration.

@SchemeMigration({
            @Migration(version = 1, evictClasses = {Mock.class}),
            @Migration(version = 2, evictClasses = {Mock2.class})
    })
interface Providers {}

Copy the code

What is data migration?

Simple said is an interface in the latest version of the return value type internal changed, thus the way to get the data changed, but the data is stored locally, is not to change version, which may occur when the deserialization errors, in order to avoid the risk, the author joined the function of data migration

What are the application scenarios?

Maybe the above words are not easy to understand, but here’s a very simple example:


public class Mock{
	private int id;
}

Copy the code

The Mock has a field ID in it, which is now an integer int, which meets our current requirements, but as the product iterates, we find that int is not enough


public class Mock{
	private long id;
}

Copy the code

To meet our current requirements, we use long instead of int. Since the Mock in the cache is the same as before and has not expired, the problem arises when the local cache is used to deserialize the data and change int to long

How does data migration solve this problem?

RxCache removes all cached classes that have been previously cached and modified internally

How does RxCache work?

It is worth noting that every time a dynamic proxy for the interface is created, that is, every time rxCache.using (CacheProviders. Class) is called, two operations are performed to clean the cache containing evictClasses** declared in ** @migration and to iterate over the local cache The folder clears all caches that have expired

Each time the cache that requires data Migration is cleaned, the version** @migration value with the largest version value is saved locally

@SchemeMigration({
            @Migration(version = 1, evictClasses = {Mock.class}),
            @Migration(version = 3, evictClasses = {Mock3.class}),
            @Migration(version = 2, evictClasses = {Mock2.class})
    })
interface Providers {}

Copy the code

Each time using() is called, the last saved version value will be retrieved from the local data Migration. It will look for @migration ** whose version value is greater than this one and retrieve the evictCl The asses will run through all the local caches and wipe out any cached data that contains the class you declared

Observable< List< Mock >>,Observable< Map< String,Mock >,Observable< Mock[] All interface caches that are returned by the > or Observable< Mock > are cleared, and the maximum version value is logged locally

Therefore, every time there is a class that requires data Migration, a new @migration ** must be added to ** @schememigration, and the version value in the annotation must be **+1**

@SchemeMigration({
            @Migration(version = 1, evictClasses = {Mock.class}),
            @Migration(version = 3, evictClasses = {Mock3.class}),
            @Migration(version = 2, evictClasses = {Mock2.class}),
            @Migration(version = 4, evictClasses = {Mock2.class})
            
    })
interface Providers {}

Copy the code

If you add a ** @migration **,version = 4(3+1). Using () will only change version = The @migration class declared by evictClasses performs data Migration (i.e. cleaning the cache containing this class)

@Actionable

The annotation handler will be used to give the Interface that used the annotation, automatically generate a class file with the same class name and an Actionable ending, and use the APi of this class to perform write operations more easily

conclusion

This is the end of the RxCache introduction, I believe that after reading this article, the basic use must be no problem

However, a problem was found in use. If BaseResponse< T > was used, there would be errors when wrapping data, such as issue#41 and issue#73

To analyze problems

RxCache encapsulates the data returned from Retrofit into a Record object. The Record decides what type the data is, whether it is a Collection(the parent of a List), an array, or a Map

If the data type is List< String >, the container class name is List, the value class name is String, and the Key class name is empty. If the data type is Map< String >, the container class name is List, and the value class name is String. String,Integer >, the container class name is Map, the value class name is Integer, and the key class name is String

The purpose of these three fields is that you can use Gson to recover the type of real data based on the field type when retrieving the local cache, and that’s the problem because BaseResponse< T is used > parcel data, in the judgment, he ruled out the data as a List, array, or it will only be determined after the Map this data is a common object, then he will only keep the median class name in the three fields of BaseResponse others is empty, the type of paradigm it wasn’t through field records, so it at the time of taking natural won’t return T correctly type

To solve the problem

Now that we know what the problem is, we are going to solve the problem. There are two ways to solve the problem, one is internal and the other is external. The external solution can be solved by the way mentioned above issue#73

The so-called internal solution is to change the internal code of this framework, the problem is in the Record when the data is a common object, he will not use the field to save the type name of the model, so when taking the local cache can not recover the data type correctly

The idea is that we have to do something special for common objects. The simplest way is to check instanceof BaseResponse if the data is an object, and repeat the above check if it is true

Is T of type List, array,Map, or object in BaseResponse?

Then in the corresponding field holds the corresponding type name, when I was in the local cache can use Gson by these fields to restore the correct data type, but such compulsory judgment instanceof flexibility and extensibility for a framework will be discounted, so I’m back to write the source code analysis will seriously consider this problem, if you can I’ll P Ull Rxcache Request

The public,

Scan the code to follow my official account JessYan to learn and make progress together. If there is any update to the framework, I will inform you immediately on my official account


Hello, my name is JessYan. If you like my articles, you can follow me on the following platforms

  • Personal homepage: Jessyan.me
  • Making: github.com/JessYanCodi…
  • The Denver nuggets: juejin. Cn/user / 976022…
  • Jane: www.jianshu.com/u/1d0c0bc63…
  • Weibo: weibo.com/u/178626251…

— The end