📜 ⬆️ ⬇️

Lists with different item types and different data providers

Foreword


Once it took me to display in one ListView cards of different types, and even received from the server using different APIs. Like, let the user be happy and see in one news feed:

It is obvious that tinkering with one large layout, in which taking into account all possible variants of cards is bad, and it will be expanding so-so.



The second difficulty was that the data sources for the cards could be completely different server resources, the list had to be collected using simultaneous requests to several different APIs that give different types of data.
')


Well, so that life does not seem like honey, the server API cannot be changed.

From API to ListView


Virgil Dobjanschi on Google I / O 2010 perfectly laid out how to implement interaction with the REST API. The very first pattern reads:
  1. Activity creates a Service that makes a request to the REST API;
  2. Service parses the answer and stores the data in the database through ContentProvider;
  3. The activity receives a data change notification and updates the view.


UPD Here a small holivar on the subject of using the service has arisen, so it is better to replace this word with “a library that implements HTTP requests” - no matter which way.

So in the end everything works: we make a packet of requests to the API, insert the data using the ContentProvider into separate tables related to the types of REST resources, notify with the help of notifyChange about the availability of new data in the tape. But, as usual, there are two problems:



Display different types of cards


First, let's deal with the fact that simpler. The solution is easily found in Google, so I quote it briefly.
In the adapter of the list of cards we override the methods:

@Override int getViewTypeCount() { //   ,       return VIEW_TYPE_COUNT; } @Override int getItemViewType(int position) { //          Cursor c = (Cursor)getItem(position); int columnIndex = c.getColumnIndex(VIEW_TYPE_COLUMN); return c.getInt(columnIndex); } @Override void bindView(View view, Context context, Cursor c) { //           int columnIndex = c.getColumnIndex(VIEW_TYPE_COLUMN); int viewType = c.getInt(columnIndex); switch(viewType) { case VIEW_TYPE_VIDEO: bindVideoView(view); break; case VIEW_TYPE_SUBSCRIPTION: //    } } @Override View newView(Context context, Cursor cursor, ViewGroup parent) { //        int columnIndex = c.getColumnIndex(VIEW_TYPE_COLUMN); int viewType = c.getInt(columnIndex); switch(viewType) { case VIEW_TYPE_VIDEO: return newVideoView(cursor); case VIEW_TYPE_SUBSCRIPTION: //    } } 


Next, the wonderful CursorAdapter class will do everything itself: it initializes individual view caches for different types of representations, it’s up to itself whether to create new or reuse old views ... in general everything’s great, you just need to get VIEW_TYPE_COLUMN in the cursor.

We collect the SQL query for the tape


Let for definiteness in the database there are tables:



So, you need to construct a query that returns the following columns:

columnvideoauthortagcomment
idvideo_idauthor_idtag_idprimary key in the corresponding table
view_typeVIDEOSUBSCRIPTIONSUBSCRIPTIONtype of card to display
content_typevideosauthorstagscontent type - or table name, if it is more convenient
titlevideo_titleNullNullvideo title
nameNullauthor_nametag_nameauthor name or tag name
picturelinklinklinkpicture link
updatedtimestamptimestamptimestampobject update time on server


Let me explain a little more.


The sqlite query is quite simple:

 SELECT 0 as view_type, 'videos' as content_type, title, NULL as name, picture, updated FROM videos UNION ALL SELECT 1 as view_type, 'authors' as content_type, NULL as title, name, picture, updated FROM authors UNION ALL SELECT 1 as view_type, 'tags' as content_type, NULL as title, name, picture, updated FROM tags ORDER BY updated 


Of course, you can build such a query with your own hands, but in SQLiteQueryBuilder there are some slightly buggy, but working methods for constructing such a query.

So, the Activity requests a tape from our ContentProvider:

 Cursor c = getContext().getContentResolver().query(Uri.parse("content://MyProvider/feed/")); 


In this case, in the MyProvider.query method, MyProvider.query necessary to determine that the request is being MyProvider.query to the Uri tape, and switch to the “intelligent” query building mode.

 Cursor query(Uri contentUri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if (isFeedUri(contentUri)) return buildFeedUri(); //       // ... } Cursor buildFeedUri() { //   "-"      HashSet<String> unionColumnsSet = new HashSet<String>(); //  Uri  ,    (videos, authors  tags) List<Uri>contentUriList = getSubqueryContentUriList(); //       viewType String[] viewTypeColumns = new String[contentUriList.size()]; //      contentType String[] contentTypeColumns = new String[contentUriList.size()]; for (int i=0; i<contentUriList.size(); i++) { Uri contentUri = contentUriList.get(i); //       viewTypeColumns[i] = getViewTypeExpr(contentUri); // "0 as view_type" //   content_type contentTypeColumns[i] = getContentTypeExpr(contentUri); // "'videos' as content_type" //      List<String> projection = getProjection(contentUri); //       unionColumnsSet.addAll(projection); } // ,       ,  :  , //  content-type    ,    . String[] subqueries = new String[contentUriList.size()]; for (int i=0; i<contentUriList.size(); i++) { Uri contentUri = contentUriList.get(i); SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); builder.setTables(getTable(contentUri)); //         "1 as content_type" //     ,  builder   //  "SELECT X as Y"   String[] unionColumns = prependContentTypeExpr(contentTypeColumns[i], unionColumnSet); //    ""     "0 as view_type" //  ,       Set<String> projection = prependViewTypeExpr(viewTypeColumns[i], getProjection(contentUri)); //  ,   String selection = computeWhere(contentUri); subqueries[i] = builder.buildUnionSubQuery( "content_type", // typeDiscriminatorColumn -   , //        unionColumns, projection, 0, getTable(contentUri), //    content_type // (      ) selection, null, // selectionArgs -   buildUnionSubQuery    // (   API level 1,  API level 11 -   ) null, // groupBy null // having ); } //   ,        . SQLiteQueryBuilder builder = new SQLiteQueryBuilder() String orderBy = "updated DESC"; String query = builder.buildUnionQuery( subqueries, orderBy, null // limit -   ,  . ); return getDBHelper().getReadableDatabase().rawQuery( query, null // selectionArgs -    ); } 


In general, if the example is written correctly, when accessing the content://MyProvider/feed/ our ContentProvider will generate the UNION request we need and give the necessary data to the adapter.

We receive updates from the server



But what is it? We request the second page of the API video, data, judging by the logs, are stored in the database, but the ListView is not updated ...
The point is in the implementation of LoaderCallbacks

 @Override public Loader<Cursor> onCreateLoader(int loaderId, Bundle params) { return new CursorLoader( getContext(), Uri.parse("content://MyContentProvider/feed/"), ... ); } 


When an Activity requests ContentProvider, the CursorLoader creates a ContentObserver that watches for Uri content://MyProvider/feed/ ; when our service saves the results of a request to the server API, ContentProvider automatically notifies about data changes via another Uri, content://MyProvider/videos/ .

How to correctly and finally solve this problem, I do not know. In my application, it was enough in the code that stores the results of the query in the database, to explicitly notify about changes in the tape data (notification of changes in a specific table falls on the shoulders of the provider):

 getContext.getContentResolver().notifyChange(Uri.parse("content://MyProvider/feed/", null)); 


Alternative solutions


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


All Articles