📜 ⬆️ ⬇️

RESTful API for Android: pattern B

More recently, at an interview in Yandex, I happened to discuss the organization of Rest-interaction in Android applications. During the discussion, the question came up - why of the three patterns proposed on Google IO 2010 Virgil Dobjanschi, the first one is used significantly more often than the other two. The question interested me.

Since the topic of discussion is rather narrowly specialized, I’ll let the reader skip the words about how important the Rest-interaction architecture is important in Android applications and how often Android developers are faced with similar tasks.

Brief description of patterns and overview

( more info )
Pattern APattern BPattern C
Service API used: Activity -> Service -> Content Provider. In this version, Activity works with the Android Servcie API. If necessary, send a REST request. Activity creates a Service, Service asynchronously sends requests to a REST server, and saves the results to Content Provider (sqlite). The activity receives a data readiness notification and reads the results from the Content Provider (sqlite).Used ContentProvider API: Activity -> Content Provider -> Service. In this case, the Activity works with the API Content Provider, which acts as a facade for the service. This approach is based on the similarity of the Content Provider API and the REST API: GET REST is equivalent to a select query to the database, POST REST is equivalent to insert, PUT REST ~ update, DELETE REST ~ delete. Activity results are also loaded from sqlite.The Content Provider API + SyncAdapter is used: Activity -> Content Provider -> Sync Adapter. A variation of the “B” approach, which uses its own Sync Adapter instead of the service. Activity gives the command Content Provider, which redirects it to the Sync Adapter. The Sync Adapter is called from the Sync Manager, but not immediately, but at a “convenient” moment for the system. Those. delays in the execution of commands are possible.
A quick review confirmed that Pattern A is really much more widely used. Dv in his wonderful article “REST for Android. Part 1: Virgil Dobjanschi patterns ” mentions a number of libraries and examples of Pattern A implementation ( there is one in Habré), and just one example of Pattern B is described in the book Programming Android, 2nd Edition by Zigurd Mednieks, Laird Dornin, G. Blake Meike and Masumi Nakamura [1], in Chapter 13, “A Content Provider for a RESTful Web Service”, is available on github.com . I could not find others.
Reading the original report of Virgil Dobjanschi only added intrigue.
Content a little bit. ... Again, we're not forcing you to adopt these particular design patterns.
In general, do not want - do not use. It is encouraging.
I propose to briefly review the existing implementation of Pattern B and try to understand what its features are.

FinchVideo application


Immediately, I note that this code was written by a respected G. Blake Meike in 2012, and has not been significantly modified since then, so we will treat with understanding the use of any deprecated managedQuery constructions, disuse of such wonderful things as Loader, synchronized (HashMap) instead of ConcurrentHashMap and other things - they have no effect on the architecture of the application.
')
So let's start with the user interface. In FinchVideoActivity, everything is completely transparent - a Cursor is attached to the ListView via SimpleCursorAdapter, into which the results of the managedQuery queries to FinchVideoContentProvider merge.
Further - more interesting.

FinchVideoContentProvider
public class FinchVideoContentProvider extends RESTfulContentProvider { public static final String VIDEO = "video"; public static final String DATABASE_NAME = VIDEO + ".db"; static int DATABASE_VERSION = 2; public static final String VIDEOS_TABLE_NAME = "video"; private static final String FINCH_VIDEO_FILE_CACHE = "finch_video_file_cache"; private static final int VIDEOS = 1; private static final int VIDEO_ID = 2; private static final int THUMB_VIDEO_ID = 3; private static final int THUMB_ID = 4; private static UriMatcher sUriMatcher; // Statically construct a uri matcher that can detect URIs referencing // more than 1 video, a single video, or a single thumb nail image. static { sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); sUriMatcher.addURI(FinchVideo.AUTHORITY, FinchVideo.Videos.VIDEO, VIDEOS); // use of the hash character indicates matching of an id sUriMatcher.addURI(FinchVideo.AUTHORITY, FinchVideo.Videos.VIDEO + "/#", VIDEO_ID); sUriMatcher.addURI(FinchVideo.AUTHORITY, FinchVideo.Videos.THUMB + "/#", THUMB_VIDEO_ID); sUriMatcher.addURI(FinchVideo.AUTHORITY, FinchVideo.Videos.THUMB + "/*", THUMB_ID); } /** uri for querying video, expects appending keywords. */ private static final String QUERY_URI = "http://gdata.youtube.com/feeds/api/videos?" + "max-results=15&format=1&q="; private DatabaseHelper mOpenHelper; private SQLiteDatabase mDb; private static class DatabaseHelper extends SQLiteOpenHelper { private DatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) { super(context, name, factory, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { createTable(sqLiteDatabase); } private void createTable(SQLiteDatabase sqLiteDatabase) { String createvideoTable = "CREATE TABLE " + VIDEOS_TABLE_NAME + " (" + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + FinchVideo.Videos.TITLE + " TEXT, " + FinchVideo.Videos.DESCRIPTION + " TEXT, " + FinchVideo.Videos.THUMB_URI_NAME + " TEXT," + FinchVideo.Videos.THUMB_WIDTH_NAME + " TEXT," + FinchVideo.Videos.THUMB_HEIGHT_NAME + " TEXT," + FinchVideo.Videos.TIMESTAMP + " TEXT, " + FinchVideo.Videos.QUERY_TEXT_NAME + " TEXT, " + FinchVideo.Videos.MEDIA_ID_NAME + " TEXT UNIQUE," + FinchVideo.Videos.THUMB_CONTENT_URI_NAME + " TEXT UNIQUE," + FinchVideo.Videos._DATA + " TEXT UNIQUE" + ");"; sqLiteDatabase.execSQL(createvideoTable); } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldv, int newv) { sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + VIDEOS_TABLE_NAME + ";"); createTable(sqLiteDatabase); } } public FinchVideoContentProvider() { } public FinchVideoContentProvider(Context context) { } @Override public boolean onCreate() { FileHandlerFactory fileHandlerFactory = new FileHandlerFactory(new File(getContext().getFilesDir(), FINCH_VIDEO_FILE_CACHE)); setFileHandlerFactory(fileHandlerFactory); mOpenHelper = new DatabaseHelper(getContext(), DATABASE_NAME, null); mDb = mOpenHelper.getWritableDatabase(); return true; } @Override public SQLiteDatabase getDatabase() { return mDb; } /** * Content provider query method that converts its parameters into a YouTube * RESTful search query. * * @param uri a reference to the query for videos, the query string can * contain, "q='key_words'". The keywords are sent to the google YouTube * API where they are used to search the YouTube video database. * @param projection * @param where not used in this provider. * @param whereArgs not used in this provider. * @param sortOrder not used in this provider. * @return a cursor containing the results of a YouTube search query. */ @Override public Cursor query(Uri uri, String[] projection, String where, String[] whereArgs, String sortOrder) { Cursor queryCursor; int match = sUriMatcher.match(uri); switch (match) { case VIDEOS: // the query is passed out of band of other information passed // to this method -- its not an argument. String queryText = uri. getQueryParameter(FinchVideo.Videos.QUERY_PARAM_NAME); if (queryText == null) { // A null cursor is an acceptable argument to the method, // CursorAdapter.changeCursor(Cursor c), which interprets // the value by canceling all adapter state so that the // component for which the cursor is adapting data will // display no content. return null; } String select = FinchVideo.Videos.QUERY_TEXT_NAME + " = '" + queryText + "'"; // quickly return already matching data queryCursor = mDb.query(VIDEOS_TABLE_NAME, projection, select, whereArgs, null, null, sortOrder); // make the cursor observe the requested query queryCursor.setNotificationUri( getContext().getContentResolver(), uri); /** * Always try to update results with the latest data from the * network. * * Spawning an asynchronous load task thread, guarantees that * the load has no chance to block any content provider method, * and therefore no chance to block the UI thread. * * While the request loads, we return the cursor with existing * data to the client. * * If the existing cursor is empty, the UI will render no * content until it receives URI notification. * * Content updates that arrive when the asynchronous network * request completes will appear in the already returned cursor, * since that cursor query will match that of * newly arrived items. */ if (!"".equals(queryText)) { asyncQueryRequest(queryText, QUERY_URI + encode(queryText)); } break; case VIDEO_ID: case THUMB_VIDEO_ID: long videoID = ContentUris.parseId(uri); queryCursor = mDb.query(VIDEOS_TABLE_NAME, projection, BaseColumns._ID + " = " + videoID, whereArgs, null, null, null); queryCursor.setNotificationUri( getContext().getContentResolver(), uri); break; case THUMB_ID: String uriString = uri.toString(); int lastSlash = uriString.lastIndexOf("/"); String mediaID = uriString.substring(lastSlash + 1); queryCursor = mDb.query(VIDEOS_TABLE_NAME, projection, FinchVideo.Videos.MEDIA_ID_NAME + " = " + mediaID, whereArgs, null, null, null); queryCursor.setNotificationUri( getContext().getContentResolver(), uri); break; default: throw new IllegalArgumentException("unsupported uri: " + QUERY_URI); } return queryCursor; } /** * Provides a handler that can parse YouTube gData RSS content. * * @param requestTag unique tag identifying this request. * @return a YouTubeHandler object. */ @Override protected ResponseHandler newResponseHandler(String requestTag) { return new YouTubeHandler(this, requestTag); } /** * Provides read only access to files that have been downloaded and stored * in the provider cache. Specifically, in this provider, clients can * access the files of downloaded thumbnail images. */ @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { // only support read only files if (!"r".equals(mode.toLowerCase())) { throw new FileNotFoundException("Unsupported mode, " + mode + ", for uri: " + uri); } return openFileHelper(uri, mode); } @Override public String getType(Uri uri) { switch (sUriMatcher.match(uri)) { case VIDEOS: return FinchVideo.Videos.CONTENT_TYPE; case VIDEO_ID: return FinchVideo.Videos.CONTENT_VIDEO_TYPE; case THUMB_ID: return FinchVideo.Videos.CONTENT_THUMB_TYPE; default: throw new IllegalArgumentException("Unknown video type: " + uri); } } @Override public Uri insert(Uri uri, ContentValues initialValues) { // Validate the requested uri if (sUriMatcher.match(uri) != VIDEOS) { throw new IllegalArgumentException("Unknown URI " + uri); } ContentValues values; if (initialValues != null) { values = new ContentValues(initialValues); } else { values = new ContentValues(); } SQLiteDatabase db = getDatabase(); return insert(uri, initialValues, db); } private void verifyValues(ContentValues values) { if (!values.containsKey(FinchVideo.Videos.TITLE)) { Resources r = Resources.getSystem(); values.put(FinchVideo.Videos.TITLE, r.getString(android.R.string.untitled)); } if (!values.containsKey(FinchVideo.Videos.DESCRIPTION)) { Resources r = Resources.getSystem(); values.put(FinchVideo.Videos.DESCRIPTION, r.getString(android.R.string.untitled)); } if (!values.containsKey(FinchVideo.Videos.THUMB_URI_NAME)) { throw new IllegalArgumentException("Thumb uri not specified: " + values); } if (!values.containsKey(FinchVideo.Videos.THUMB_WIDTH_NAME)) { throw new IllegalArgumentException("Thumb width not specified: " + values); } if (!values.containsKey(FinchVideo.Videos.THUMB_HEIGHT_NAME)) { throw new IllegalArgumentException("Thumb height not specified: " + values); } // Make sure that the fields are all set if (!values.containsKey(FinchVideo.Videos.TIMESTAMP)) { Long now = System.currentTimeMillis(); values.put(FinchVideo.Videos.TIMESTAMP, now); } if (!values.containsKey(FinchVideo.Videos.QUERY_TEXT_NAME)) { throw new IllegalArgumentException("Query Text not specified: " + values); } if (!values.containsKey(FinchVideo.Videos.MEDIA_ID_NAME)) { throw new IllegalArgumentException("Media ID not specified: " + values); } } /** * The delegate insert method, which also takes a database parameter. Note * that this method is a direct implementation of a content provider method. */ @Override public Uri insert(Uri uri, ContentValues values, SQLiteDatabase db) { verifyValues(values); // Validate the requested uri int m = sUriMatcher.match(uri); if (m != VIDEOS) { throw new IllegalArgumentException("Unknown URI " + uri); } // insert the values into a new database row String mediaID = (String) values.get(FinchVideo.Videos.MEDIA_ID_NAME); Long rowID = mediaExists(db, mediaID); if (rowID == null) { long time = System.currentTimeMillis(); values.put(FinchVideo.Videos.TIMESTAMP, time); long rowId = db.insert(VIDEOS_TABLE_NAME, FinchVideo.Videos.VIDEO, values); if (rowId >= 0) { Uri insertUri = ContentUris.withAppendedId( FinchVideo.Videos.CONTENT_URI, rowId); getContext().getContentResolver().notifyChange(insertUri, null); return insertUri; } throw new IllegalStateException("could not insert " + "content values: " + values); } return ContentUris.withAppendedId(FinchVideo.Videos.CONTENT_URI, rowID); } private Long mediaExists(SQLiteDatabase db, String mediaID) { Cursor cursor = null; Long rowID = null; try { cursor = db.query(VIDEOS_TABLE_NAME, null, FinchVideo.Videos.MEDIA_ID_NAME + " = '" + mediaID + "'", null, null, null, null); if (cursor.moveToFirst()) { rowID = cursor.getLong(FinchVideo.ID_COLUMN); } } finally { if (cursor != null) { cursor.close(); } } return rowID; } @Override public int delete(Uri uri, String where, String[] whereArgs) { int match = sUriMatcher.match(uri); int affected; SQLiteDatabase db = mOpenHelper.getWritableDatabase(); switch (match) { case VIDEOS: affected = db.delete(VIDEOS_TABLE_NAME, (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs); break; case VIDEO_ID: long videoId = ContentUris.parseId(uri); affected = db.delete(VIDEOS_TABLE_NAME, BaseColumns._ID + "=" + videoId + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs); getContext().getContentResolver().notifyChange(uri, null); break; default: throw new IllegalArgumentException("unknown video element: " + uri); } return affected; } @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { getContext().getContentResolver().notifyChange(uri, null); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count; switch (sUriMatcher.match(uri)) { case VIDEOS: count = db.update(VIDEOS_TABLE_NAME, values, where, whereArgs); break; case VIDEO_ID: String videoId = uri.getPathSegments().get(1); count = db.update(VIDEOS_TABLE_NAME, values, BaseColumns._ID + "=" + videoId + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } getContext().getContentResolver().notifyChange(uri, null); return count; } } 
FinchVideoContentProvider, in addition to implementing the base for ContentProvider (query, insert, etc.) operations, to SQLiteDatabase inherits from RESTfulContentProvider the mechanism for launching http requests in separate streams
asyncQueryRequest
public void asyncQueryRequest (String queryTag, String queryUri) {
synchronized (mRequestsInProgress) {
UriRequestTask requestTask = getRequestTask (queryTag);
if (requestTask == null) {
requestTask = newQueryTask (queryTag, queryUri);
Thread t = new Thread (requestTask);
// allows other requests to run in parallel.
t.start ();
}
}
}
and HashMap <String, UriRequestTask> - mRequestsInProgress (respectively, a set of executed queries). The results of the requests are processed in the YouTubeHandler implements ResponseHandler , which is passed to the UriRequestTask task when it is created.

Let's compare existing classes with Pattern B.
With Activity and Content Provider everything is quite clear. The Service object is not used explicitly in the example, its functions and partly the ServiceHelper functions on structuring and launching requests are performed by the FinchVideoContentProvider. He also performs the functions of the Processor, about the Rest method is written above. Such is the simplified implementation.

findings


Based on the analysis of the existing implementation of Pattern B and its descriptions, I made the following conclusions for myself
  1. The biggest plus of Pattern B, as the author of the example describes in the section “Summary of Benefits” [1 - p. 369], is the increased query performance, since they are primarily implemented to the local database (Content Provider);
  2. The reverse side of this plus is the mismatch between the local and server database and the complicated logic of data acquisition.
    It is not surprising that the author of the example used only a query (GET) query - this is the easiest option. Did not receive the new data - we will take old from a cache. And if to implement insert (PUT)? It will be necessary to first make changes to the local database, set the “not synchronized” flag to them (changes), then if the GET request is unsuccessful, retry this attempt, for example with an exponentially increasing pause (as suggested by the author of the pattern) ... All this time the user will see the added data that is not on the server. Moreover, that they are not on the server, he will not be able to find out either (see clause 3);
  3. And the unpleasant side effect associated with the limited interaction of the Activity with REST (only through the Content Provider mechanisms) - in the GUI we cannot get anything but data.
    For example, we will never know the reasons for the lack of data. Error in parsing? The server did not return anything? No network at all? The result is one - no data. In the implementation of Pattern A for this purpose we could send a RequestListener from the Activity to the ServiceHelper. C Content Provider this number will not work.
    Of course, we can get data, for example through Broadcast Receiver, and bypassing the Content Provider, but this will no longer be related to Pattern B.

Thus, when using Pattern B, the above points must be considered.

Maybe someone used this pattern in work projects or knows more successful examples of implementation? Does it make any sense to implement it more qualitatively (there was such an idea), if in 4 years nobody bothered with this? I will be glad to see the answers in the comments.

Source: https://habr.com/ru/post/240543/


All Articles