The second lecture on the architecture of client-server android-applications, in which we will consider such concepts as REST-architecture, ContentProvider, Patterns A, B and C and different approaches to implement interaction with the network.
Plan:
- REST
- ContentProvider
- Patterns A / B / C
- Pattern A
- Pattern B
- Pattern С
- Practical assignment
- Links and useful resources
Introduction
At the last lecture, we looked at ways to handle the re-creation of the Activity and the problem of saving data during re-creation. But there is another factor here, because the Activity can not be recreated when the query is executed, but in general it is destroyed. And it threatens with some problems. First of all, you may have an out-of-sync on the server and the client. The client will execute the request, the server will accept it and return the result with new data, which the client can no longer receive (due to the fact that the application was closed). When the client opens the application the next time, for example, offline, he will see the old information that he does not expect to see. Secondly, it is possible that to complete the operation you need to receive one response, process it and execute a new request. Undoubtedly, this is a terrible situation and it’s never necessary to do that, but backends are different, and they are not always ideal. In this case, the situation is even worse, since the data of your application may hang in the middle of the process.
We must say at once that the problems described above do not apply to all applications. Moreover, they do not apply to most applications. But at the same time to know how to solve such problems is necessary.
There is an obvious, but quite correct solution to such problems — to use for queries a component that is not destroyed by the system, even if the user closes the application. Fortunately, there is such a component, and this is Service.
Service is a component that is designed to perform certain operations in the background and that does not contain any UI elements. A huge plus of services is that they will continue to work even when the user closes all Activity applications.
The simplest implementation of the service is as follows:
public class SimpleService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { // TODO : do you work here // This code runs in the UI thread return super.onStartCommand(intent, flags, startId); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } }
After that, the service needs to be registered in the manifest:
<service android:name=".network.SimpleService"/>
And you can run it like this:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_weather); Intent intent = new Intent(this, SimpleService.class); startService(intent); }
Intent, which is passed as a parameter to the startService method, eventually gets a parameter to the onStartCommand method, where you can get all the required parameters passed.
The onBind method serves to create a different type of services, which we are not interested in in the context of the topic under consideration. One can only say that this allows you to tie the service to another component of the application (for example, to Activity) and perform various background work, including between processes. But since the tied service dies along with the component to which it is attached, it will not help us to solve the current problem. More details about its applications can be found in the documentation.
Despite the fact that the service is designed to work in the background, the Service class itself does not contain the tools to ensure asynchronous execution of tasks, and the onStartCommand method works in the main application thread. Asynchrony should be provided by the developer, using any means known to him. There is also an alternative — IntentService, which is a simple single-threaded implementation of the service. For example, to load data and execute network requests, IntentService can be used as follows:
public class NetworkService extends IntentService { public NetworkService() { super(NetworkService.class.getName()); } @Override protected void onHandleIntent(Intent intent) { try { City city = ApiFactory.getWeatherService() .getWeather(getString(R.string.default_city)) .execute() .body(); } catch (IOException ignored) { } } }
Here you also need to make a small note — IntentService is single-threaded and processes all incoming requests sequentially.
It would seem that once the service is such a powerful component that is not destroyed when the configuration is changed or even when the application is closed, why is it not used for network requests always? And here we are faced with the main problem, which answers this question. Pay attention again to the onHandleIntent method:
@Override protected void onHandleIntent(Intent intent) { try { City city = ApiFactory.getWeatherService() .getWeather(getString(R.string.default_city)) .execute() .body(); } catch (IOException ignored) { } }
Namely, the fact that this method is of type void. That is, we uploaded weather data, but how to convey this information is not entirely clear. This is the main problem of services — since they are not related to UI classes, they can not transfer data to them.
One of the standard solutions in this case is the use of various Bus, for example, EventBus, Otto or even a bus on RxJava. But such methods are unreliable, since working with Bus requires subscription and unsubscription in UI classes according to the methods of the life cycle. In addition, their use makes the code unobvious.
Unfortunately, there is no simple and good solution for this problem. And that’s why we’ll look at the patterns that were presented at the Google I / O conference in 2010. [wpanchor id = «1»]
In order to understand these patterns, we will first need to become more familiar with the concepts of REST, as well as explore the ContentProvider API, because these patterns are completely based on them.
REST
Probably everyone has heard such words as REST API, RESTful services and other related concepts. What is meant by these concepts? First of all, REST is not some hard-coded format for working with web services, but rather a set of principles that defines how Web standards / components such as HTTP, URI, and how they should be used should interact . These principles are designed to build the architecture of Web services.
From this set of principles, there are several key ones:
- Each entity must have a unique identifier – URI.
- Entities must be related.
- Standard methods should be used to read and modify data.
- There should be support for several types of resources.
- Interaction should be carried out without a state.
Most of these principles are needed only when implementing web services, of course. For the development of the client part, in particular for the mobile application, we need the 1 st and 3 rd principle.
Principle 1 says that each object must have its own unique identifier, moreover, the URI address where it can be accessed. URI (from Uniform Resources Identifier) is a unique resource identifier. As we will see later, this term is widely used in Android. A unique identifier is needed to access this object and to associate it with other objects.
Principle 3 reports the use of standard methods for calling remote procedures (and changing data). These methods are well known to all:
- GET – Receiving data without changing it. This is the most popular and easy method. It only returns data, and does not change it, so on the client you do not need to worry about that you can damage the data.
- POST is a method that imply the insertion of new records.
- PUT is a method that involves changing existing records.
- PATCH — a method that involves changing the identifier of existing records.
- DELETE — a method that involves deleting records.[wpanchor id=»2″]
Knowledge of these methods is very important for our further study. And for us enough information about REST, let’s just simplify his final understanding and give his presentation. The REST API is a set of remote calls to standard methods that return data in a specific format. With this definition, we will continue to work.
ContentProvider
In this lecture we will also consider a very convenient way to work with data in Android, namely ContentProvider. ContentProvider is a class that provides a unified interface for accessing application data. This class allows you to use a single data source in your application.
ContentProvider also allows you to transfer data between applications. For example, this way you work with phone contacts, sms and other system data. Unfortunately, there is a serious error in this regard. The documentation for ContentProvider says that «we do not need to develop your own provider if you do not intend to share your data with other applications.», That is, we should not implement our ContentProvider if we are going to use it only inside Your application. Such an error is expensive, many developers after reading believe that the ContentProvider will never need them, and this is not so. So what are the advantages of this class, and why should we need it?
First, as already mentioned, ContentProvider provides a unified interface for accessing data. The unified here means both independent of the implementation (and this will help you very much in cases where you decide to change the implementation of your storage), and covering all the necessary cases.
Secondly, you do not need to manage the object lifecycle to access the data (for example, the SQLiteDatabase instance). After all, in the case of direct use of such objects, there are many questions: where to store this object? When to close the database? When to destroy this object? ContentProvider allows you not to worry about such things. In addition, it allows you to access data from any location where the context of the application is available (Context instance).
And thirdly, ContentProvider fully complies with the concepts of REST, which we examined. This is good because it allows us to use it for our purposes (the spoiler — ContentProvider can act as a front for requests to the server), and this will be discussed in more detail later. In the meantime, let’s move on to working with ContentProvider.
Because ContentProvider conforms to the principles of REST, for each entity it has its own URI. ContentProvider has a base URI, which is defined by the application creator and registered in the manifest. It looks something like this:
<provider android:name=".data.sqlite.WeatherContentProvider" android:authorities="ru.gdgkazan.simpleweather" android:exported="false"/>
To access data through the ContentProvider, we need a URI for this data (by the URI, you can access both the data group and the individual object). The application’s ContentProvider always defines a base URI that is formed from authorities in the manifest and the content: // prefix. Therefore, in our case, the base URI will be:
content://ru.gdgkazan.simpleweather
Next, suppose we want to create a group in which weather information will be stored (within the database it will be a table). Then the URI for this group should look like this:
content://ru.gdgkazan.simpleweather/weather
If we access data in the ContentProvider using this URI, we get all the instances stored in this group.
Finally, if we need a URI for an individual object, it will look like this:
content://ru.gdgkazan.simpleweather/weather/4
Where 4 is the number of the added instance.
Working with the URI is very important. The ContentProvider API allows you to monitor changes in data for a specific URI, which is extremely convenient, since it makes it easy to organize automatic updating of data in UI classes.
But in general, there is nothing complicated and new here, these are the standard means of forming a URI (and in fact a URL is a special case of a URI, so this principle is perfectly familiar to everyone). Therefore, we end with the URI and proceed to the implementation of our ContentProvider.
Note: In the future, within the framework of this lecture, we will work with a special library that is based on ContentProvider, supports a table model and allows you to perform all the necessary functions, including subscribing to notifications of changes in the table. This hides some unnecessary details of implementation, allowing you to concentrate entirely on the essence of the lecture. The description and code of the library are available at the link above. All examples will be based on this library.
To implement your ContentProvider, you need to create a class that will inherit from the ContentProvider and override all required methods. Consider these methods in order. First, it’s the onCreate method, in which you need to initialize all the fields that you need to work:
@Override public final boolean onCreate() { SQLiteConfig config = new SQLiteConfig(getContext()); prepareConfig(config); sContentAuthority = config.getAuthority(); sBaseUri = Uri.parse("content://" + sContentAuthority); mSchema = new SQLiteSchema(); prepareSchema(mSchema); mSQLiteHelper = new SQLiteHelper(getContext(), config, mSchema); return true; }
What does the code do in this method? First, it creates a basic URI for further access to the data in the ContentProvider. Secondly, it creates a data schema in the ContentProvider (in the simplest case, it adds tables to the database). Finally, in this method, an instance of SQLiteOpenHelper is created, through which access to the data store will be organized.
The next method is the getType method. This method must return the data type that is contained in the URI passed as the parameter. Typically, this method is used to map the table name and URI, so that you can then execute the query to the database. In our case, the implementation looks simple, but we will not go deeper (this can always be done by looking at the library code):
@Nullable @Override public final String getType(@NonNull Uri uri) { return mSchema.findTable(uri); }
And then the methods that are directly intended for working with data begin. These are methods that completely correspond to HTTP methods, which we will use in the future. In the meantime, let’s see the implementation of these methods. In most cases, these methods only redirect the call to the SQLite database. First of all, this is the query method (which corresponds to the HTTP GET method):
@Nullable @Override public final Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteDatabase database = mSQLiteHelper.getWritableDatabase(); String table = getType(uri); if (TextUtils.isEmpty(table)) { throw new IllegalArgumentException("No such table to query"); } else { return database.query(table, projection, selection, selectionArgs, null, null, sortOrder); } }
This method first checks if the table is called, and if so, calls the query method of the SQLiteDatabase object, which, based on all parameters, builds an SQL query and returns the data.
Next is the insert method, which is used to add an element to the ContentProvider and the corresponding HTTP method to POST:
@NonNull @Override public final Uri insert(@NonNull Uri uri, ContentValues values) { SQLiteDatabase database = mSQLiteHelper.getWritableDatabase(); String table = getType(uri); if (TextUtils.isEmpty(table)) { throw new IllegalArgumentException("No such table to insert"); } else { long id = database.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE); return ContentUris.withAppendedId(uri, id); } }
Pay attention to the following call:
return ContentUris.withAppendedId(uri, id);
The SQLite database does not know anything about such things as URI, it’s used to working with identifiers in a different form, for example, as an id. Fortunately, the built-in Android tools allow you to easily combine the use of ContentProvider and SQLite.
The next necessary method for overriding is the delete method and is used to delete data from the ContentProvider. And by the way, this is the only method of ContentProvider, HTTP analog has the same name. The implementation of this method is analogous to all the others:
@Override public final int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { SQLiteDatabase database = mSQLiteHelper.getWritableDatabase(); String table = getType(uri); if (TextUtils.isEmpty(table)) { throw new IllegalArgumentException("No such table to delete"); } else { return database.delete(table, selection, selectionArgs); } }
And the last method that you need to override for implementing ContentProvider is the update method (in the terminology of the HTTP methods — PUT or PATCH):
@Override public final int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { SQLiteDatabase database = mSQLiteHelper.getWritableDatabase(); String table = getType(uri); if (TextUtils.isEmpty(table)) { throw new IllegalArgumentException("No such table to update"); } else { return database.update(table, values, selection, selectionArgs); } }
In addition to these methods, the method of bulkInsert (which unlike previous methods is not mandatory for implementation) is often redefined. This method is for inserting an array of elements, and its default implementation calls the insert method for each element, which is inefficient. Therefore, this method is usually redefined to perform all inserts within a single transaction:
@Override public final int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) { SQLiteDatabase database = mSQLiteHelper.getWritableDatabase(); String table = getType(uri); if (TextUtils.isEmpty(table)) { throw new IllegalArgumentException("No such table to insert"); } else { int numInserted = 0; database.beginTransaction(); try { for (ContentValues contentValues : values) { long id = database.insertWithOnConflict(table, null, contentValues, SQLiteDatabase.CONFLICT_REPLACE); if (id > 0) { numInserted++; } } database.setTransactionSuccessful(); } finally { database.endTransaction(); } return numInserted; } }
That’s all! We created our ContentProvider, added it to the manifest, and now we can access it as an interface for working with data. There is a small subtlety here — we created a ContentProvider object, but we need to access the data through the ContentResolver object, which can be obtained via the getContentResolver method in the Context class:
Cursor cursor = mContext.getContentResolver().query(table.getUri(), null, where.where(), where.whereArgs(), where.limit());
[wpanchor id=»3″]
It is impossible not to say the following remark. Most modern libraries for working with a database provide you with a much more convenient interface to work than SQL queries to the database. At the same time, they either hide their work with ContentProvider under their implementation, or work directly with database instances (and in that case they certainly do not support notifications about changing the tables or even more specific records). Therefore, we do not need to deeply study the principles of ContentProvider work, but there is enough understanding of the relationship between ContentProvider and REST architecture and the principles of operation of the main methods.
Patterns A / B / C
After the previous section, it became clear how the principles of REST and ContentProvider are related to each other, it remains only to understand how they apply to our task.
The idea is very simple — ContentProvider will act as the intermediary between the service and UI-classes, as its ideology and methods completely coincide with the REST architecture. Access to the data in the ContentProvider can be obtained from any location where the context of the application is available, that is, from the service and from the UI classes. And, along with the ability to track changes to specific entities or tables through the URI ContentProvider, you can provide an incredibly high-quality and convenient (in comparison with other ways of interaction between services and UI-classes) the level of interaction of these components.
Паттерн A
In general, the architecture of pattern A can be described by the following sequence of actions:
- The UI class starts the service to execute the query and subscribes to the data change in the table.
- The service executes the query, saves the data to the database, and notifies the table change.
- The UI class receives a notification about changing the data in the table, reads the new data and displays it to the user.
This sequence of actions can be represented in the form of the following diagram:
In this case, there are many intermediate actions, which will be discussed later. In the meantime, let’s implement such a simple approach. First of all, we modify the service for loading data, so that it stores data in the database and notifies subscribers about this:
@Override protected void onHandleIntent(Intent intent) { SQLite.get().delete(CityTable.TABLE); try { City city = ApiFactory.getWeatherService() .getWeather(getString(R.string.default_city)) .execute() .body(); SQLite.get().insert(CityTable.TABLE, city); } catch (IOException ignored) { } finally { SQLite.get().notifyTableChanged(CityTable.TABLE); } } Теперь остается запустить этот сервис в любом UI-классе и подписаться на изменения в таблице: private void loadWeather() { mLoadingView.showLoadingIndicator(); SQLite.get().registerObserver(CityTable.TABLE, this); startService(new Intent(this, NetworkService.class)); }
When the service loads the data, the subscribed class will receive a notification about it that it can process:
@Override public void onTableChanged(@NonNull List<City> cities) { if (cities.isEmpty()) { mCity = null; showError(); } else { mCity = cities.get(0); showWeather(); } mLoadingView.hideLoadingIndicator(); SQLite.get().unregisterObserver(this); }
Such a decision does not look particularly complicated, but in fact here it is necessary to solve a lot of questions and problems. First, we must correctly handle various life-cycle events and unsubscribe from notifications. That is, we did not leave here the problems that were considered in the framework of the previous lecture. Of course, these problems can be solved by the same means, but I would like that these problems were not at all.
Secondly, you need to be able to handle errors. In the example above, we looked, if there are no records in the database, then an error occurred. Of course, in any more serious example, this option does not work.
And thirdly, more precisely, developing the second point, we need to be able to track the status of each request to show the download process and hide it, and also to not re-run an already running query.
That is why the example above is naive and the simplest implementation of pattern A. In fact, in the original presentation of the pattern, everything is more complicated, but this example was needed to show why these difficulties are needed.
First of all, we need to create a class that will be responsible for the status and information about each request. This will allow us, and track status, and handle errors. This class must contain the request ID, request status and error body (which will be empty if the request is successful):
public class Request { @NetworkRequest private final String mRequest; private RequestStatus mStatus; private String mError; public Request(@NonNull @NetworkRequest String request, @NonNull RequestStatus status, @NonNull String error) { mRequest = request; mStatus = status; mError = error; } }
Create a table for this class. After that, in UI-classes we will always subscribe to changes of this table with the status (ideally, of course, it should be subscription to a separate element, but it is more difficult to implement).
After that we rework the service for queries as follows (due to the fact that the code has significantly increased, we will quote it in parts):
@Override protected void onHandleIntent(Intent intent) { Request request = SQLite.get().querySingle(RequestTable.TABLE); if (request != null && request.getStatus() == RequestStatus.IN_PROGRESS) { return; } if (request == null) { request = new Request(NetworkRequest.CITY_WEATHER, RequestStatus.IN_PROGRESS, ""); } else { request.setStatus(RequestStatus.IN_PROGRESS); } SQLite.get().insert(RequestTable.TABLE, request); SQLite.get().notifyTableChanged(RequestTable.TABLE); //... }
First, we get the current record of requesting weather information. If this request is IN_PROGRESS, that is, already in the process of execution, then we ignore this new call.
After that we translate the request into the IN_PROGRESS status and notify the subscribers (this is necessary for the UI classes to display the download process).
Let’s continue the consideration of the method:
//... try { City city = ApiFactory.getWeatherService() .getWeather(getString(R.string.default_city)) .execute() .body(); SQLite.get().delete(CityTable.TABLE); SQLite.get().insert(CityTable.TABLE, city); request.setStatus(RequestStatus.SUCCESS); } catch (IOException e) { request.setStatus(RequestStatus.ERROR); request.setError(e.getMessage()); } finally { SQLite.get().insert(RequestTable.TABLE, request); SQLite.get().notifyTableChanged(RequestTable.TABLE); }
Here everything is simple. We get the result from the server, store them in the database and depending on the success / failure when retrieving the data we translate the status either in SUCCESS or in ERROR. And notify subscribers about the completion of the request.
The UI part can process these notifications as follows (here RxJava tools are used to provide asynchrony when working with the database.) RxJava will be considered in the next lecture, but in the meantime we will comment on the code in detail)
When we receive a notification about changing the data in the table, we read the status of the request from the database. After that, we choose the actions depending on this status:
- If the status is IN_PROGRESS, then we show the boot process
- If the status is ERROR, then the received error is rolled up and displayed.
- If the status is SUCCESS, then we read the information about the city and send it to the subscriber, who will show this information.
Thus, we can track the changes in the status of the request and correctly respond to these changes. Of course, as already mentioned, it will be ideal to track not all the table with queries, but each request separately.
But we still need to try to handle life cycle events so that we do not restart the service every time we recreate Activity. To do this, you can use the different approaches discussed in the first lecture, for example, loaders.
Summarizing the subtotal, we can say that the pattern A has 2 main advantages:
- Guaranteed execution of all requests regardless of the life cycle of UI classes. [Wpanchor id = «4»]
- Support for a single data source in the application.
But he, of course, has problems, but all of them can be solved. True, this can greatly complicate the architecture of the application.
Паттерн B
Pattern A, which we examined, of course, has its drawbacks. This is a lot of code, and the need to use data-based work on the basis of ContentProvider, and the task of processing the life cycle, and others. Therefore, other variants of client-server interaction are offered, which lack some of these drawbacks (although frankly, they have other drawbacks).
Pattern B in terms of ideological blocks is very similar to pattern A, it also uses Service and ContentProvider, but the order of their use is diametrically opposed. If the A ContentProvider pattern was used as an auxiliary layer for interaction between the service performing network requests and UI classes, then in the B pattern, the UI classes work exclusively and always with the ContentProvider API. And already other classes synchronize the data from the ContentProvider with the data from the server. Therefore, for a given pattern, the ContentProvider API is generally a local version of the server part with all the ensuing consequences.
Pattern B pattern is as follows:
In this pattern there is an incredibly significant plus — all work with the «network» is carried out locally, and this gives us the confidence that any query will be executed if not instantly, then very quickly. Therefore, we get rid of long operations and waiting, as well as many problems in the processing of the life cycle. In addition, our application will work without any delays, which will create a very good impression for the user.
But you have to pay for everything. And it is clear that for such a serious plus to pay will also be very serious. What is it? First, you will always risk getting a situation where your local data is not synchronized with the data on the server, which means that the operations that the user has performed can be canceled and the user does not even understand the reasons.
Secondly, in order to somehow protect yourself from too frequent errors on the server side, you need to transfer at least part of the server logic to the application. And this at least means duplication of code and logic (the double possibility to be mistaken), and in most cases the server part is a huge system that is extremely irrationally transferred to the client side. [Wpanchor id = «5»]
Unfortunately, we must admit that such a pattern, in connection with its drawbacks, has too great disadvantages to apply it in practice. Exceptions may be situations where you have a small and simple application without complex server logic and when you can be sure that all the logic on the client side will be enough. Such cases are rare, but in them the pattern B will give a tangible gain in the speed of the application, so it must be borne in mind.
Паттерн С
And the last pattern under consideration is pattern C, which in fact is a modification of the previous pattern and very similar to it. The difference is that the SyncAdapter class is not used for synchronization. As you can guess from the name, SyncAdapter is designed to synchronize data. It has a number of advantages over services:
- Saves battery power. When the user starts the application, you start pulling requests to the network, the system is forced to use the radio module at full power, which increases the battery consumption. When using the SyncAdapter, the system starts the synchronization itself at a convenient time. Briefly, the system periodically switches on the radio module and synchronizes the data in all applications that use the SyncAdapter. This is much more efficient in terms of battery power consumption than if each application itself decided when to use the network.
- Control over the state. This implies both monitoring the network status and automatic synchronization when the Internet appears, and re-synchronization, if it was not possible to synchronize the data last time.
- Scheduling synchronization. You can configure the parameters and the schedule for synchronization, which will be taken into account by the system scheduler.
- Your account for the application and the ability for the user to synchronize data manually in the system settings of the application.
SyncAdapter is a convenient way to synchronize data. But it is not often used for several reasons:
- Not all applications need periodic data synchronization. Moreover, it is not needed by the vast majority of applications. Most applications work, so to speak, in a session mode, that is, they update the data and send requests to the server only during active work. And along with the push messages, the data model without synchronization covers the requirements of a very large number of applications. Exceptions can be, for example, applications for working with finances or weather applications.
- Inconvenient interaction with data. Unfortunately this is the case. Updating data via SyncAdapter and then reading it from ContentProvider requires much more effort than directly updating data in UI classes.[wpanchor id=»6″]
Implementing your SyncAdapter is not difficult, so we will omit the consideration of this topic. Detailed information can be found on the links at the end of the lecture.
The concrete implementation of each of the patterns examined depends very much on the application that you are developing. The main thing is that you understand the main idea when organizing such a method of client-server interaction, because in this case it will be easy for you to refine these patterns for your application.
Practice
- Download the SimpleWeatherPatternA Project. Description of the task in the file ru.gdgkazan.simpleweather.screen.weatherlist.WeatherListActivity
- You need to get a list of cities and save it to the base http://openweathermap.org/help/city_list.txt
- Get information about the weather in all cities by one request http://openweathermap.org/current#severalid
- All work must be performed in the service (it needs to be finalized)
Implement update of data via SwipeRefreshLayout - Implement re-create Activity
Links to the source code of your solutions you can leave in the comments. Share your decisions with the community, get feedback and constructive criticism. The best solutions will be published on our channel and the website fandroid.info with the authorship of the winners!
Links and useful resources
- Applications from the repository:
— SimpleWeatherPatternA is a realization of the pattern A for downloading weather and a practical task.
— WeatherSyncAdapter — a template for a practical task on the SyncAdapter. - Documentation for services, including for bound services.
- Article about REST.
- Documentation for ContentProvider.
- Article to create your ContentProvider on the basis of SQLite.
- An example library that uses the ContentProvider API with tests.
- Directly outline the A / B / C patterns in the original.
- A good article with a detailed analysis of the patterns and libraries that implement them.
- Article about the pattern B.
- Implementing your SyncAdapter.
- Article about using SyncAdapter for updating data.