📜 ⬆️ ⬇️

Social network on Android for a few days off - part I (client)

Introduction


Despite the abundance of social networks, over the past few years, a number of new, original and unusual social applications have appeared, such as just yo , snapchat , secret , etc. The success of the just yo application, limited to a single function - sending a fixed content message My friends and I also decided to try to write another social network on Android. Our goal was to outline the range of tasks common to most similar applications, to propose solutions and prepare a skeleton from which everyone can make something original and original without wasting time on resolving routine issues. The results of the work can be immediately found on github - android client and server on ruby ​​on rails .

Content


Concept and functionality
Interface
Accelerate Photo Upload
Count of friends
Network requests
Authorization
SQLite for database contacts, images
To be continued


Concept and functionality


To begin with, we decided on the concept of a new service and chose a model - an instagram and whatsapp hybrid . From instagram we took the main usage scenario - uploading photos to the friends feed with comments and likes. And from the principle of organizing the graph of friends, through a notebook on the phone numbers of friends in automatic mode.
Features of the selected model
The selected graph model implies an average network growth rate, since Each new user can involve a maximum of a few dozen / hundreds of people from the address book. On the other hand, this approach eliminates the need for moderation and content filtering at the initial stage of development, since availability of content is determined by the user's personal contacts and prevents mass spamming and other harmful phenomena. As a result, we have a simple service that is ready to be used among friends and does not require large expenses for operational support from a technical and organizational point of view.

Next, we outlined the functional minimum of our application:


Interface


Having outlined a range of basic functions, we moved on to the interface design. Each function fits well on a separate fragment and the equiprobability of their use prompts the use of a horizontal swipe transition using the ViewPager (Tutorials on swipe and ViewPager tutorial 1 tutorial 2 ). At the first stage, we got the following transition diagram:
Fig. 1. Transition Chart
image

Consider each of the fragments in more detail.

Contact sheet

Fig. 2. Contact list - wireframe and screenshot
image

The contact list displays a list of friends in the address book registered in the service, allows you to subscribe, and also opens a detailed user profile when clicked. It is implemented by the usual ListView ( tutorial ).
')
Gallery

Fig. 3. Gallery - wireframe and screenshot
image

The gallery displays local and uploaded photos of the user with a brief description in the title.
Since the size of the photos, taking into account the title and the aspect ratio may vary, we decided to use the asymmetric GridView from Etsy AndroidStaggeredGrid . The algorithm for positioning mappings in this case requires a special approach, in particular, in AndroidStaggeredGrid , a DynamicHeightImageView with a predetermined ratio is used instead of an ImageView . The result is a rather beautiful and smoothly scrolling tiled gallery.

Tape

The feed displays photos uploaded by friends that the user has subscribed to. Here we also applied the usual ListView , since each photo can occupy a large part of the screen for easy viewing and zooming can take place across the width of the screen. Clicking on the image opens a detailed description of the photo with comments.

Fig. 4. Ribbon - wireframe and screenshot
image

Detailed description of the photo

A detailed description of the photo contains the metadata of the selected image (author's name, description, number of likes, etc.), as well as a list of comments.
Fig. 5. Photo description - wireframe and screenshot
image

User Profile

A user profile contains user metadata and a gallery of user uploaded photos.
Fig. 6. User profile - wireframe and screenshot
image


Accelerate Photo Upload


L1 / L2 cache

Uploading photos is a key and fairly resource-intensive process. Photos are downloaded both from the local gallery on the device and from the remote storage. The loading speed affects the smoothness of the gallery scrolling and the overall convenience of the interface, so we decided to use a two-level cache - L1 cache in RAM and L2 cache on the disk storage device.
We chose the popular Jake Wharton plugin as a disk L2 cache, it supports logging and provides a convenient wrapper over the standard DiskLruCache from the Android SDK . L1 cache is implemented by the standard Androids LruCache (see com.freecoders.photobook.utils.DiskLruBitmapCache and com.freecoders.photobook.utils.MemoryLruCache ).

Proactive scaling

In the case of news feeds, there is an option when the feed is already loaded and the photos continue to be downloaded from the remote storage. Then, while scrolling, a hopping effect is possible at the completion of loading, if the user has already scrolled the list down. To avoid it, we applied proactive scaling of mappings in the tape — i.e. The size of the frame for the photo is calculated based on the aspect ratio and is set before the photo is downloaded from the server. Thus, the position of the items in the ListView does not change after loading new photos.
Code 1. An example of proactive scaling in DynamicHeightImageView
class FeedAdapter
public View getView(int position, View convertView, ViewGroup parent) { ... holder.imgView.setHeightRatio(feedEntry.image.ratio); holder.imgView.setTag(pos); mImageLoader.get(feedEntry.image.url_medium, new ImageListener(pos, holder.imgView, null)); ... } 


Downsampling

In addition, in Android there are restrictions on the maximum size and resolution of the photo displayed on the screen. This is designed to prevent memory overflow. Therefore, before downloading a bitmap, you must perform downsampling .
Code 2. Example of downsampling images
class ImageUtils
 public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { final int halfHeight = height / 2; final int halfWidth = width / 2; while (((halfHeight / inSampleSize) > reqHeight) || ((halfWidth / inSampleSize) > reqWidth)) { inSampleSize *= 2; } } return inSampleSize; } public static Bitmap decodeSampledBitmap(String imgPath, int reqWidth, int reqHeight) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(imgPath, options); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(imgPath, options); } 


Thumbnails

Also, if the media scanner managed to process all the photos, then thumbnails in the device’s memory can already be created for them. This rule is not always fulfilled, but if there is a thumbnail, it greatly speeds up the loading process and avoids downsampling.
Code 3. Example of downloading thumbnails from MediaStore
class ImagesDataSource
  public String getThumbURI(String strMediaStoreID) { ContentResolver cr = mContext.getContentResolver(); String strThumbUri = ""; Cursor cursorThumb = cr.query(MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Images.Thumbnails.DATA}, MediaStore.Images.Thumbnails.IMAGE_ID + "= ?", new String[]{strMediaStoreID}, null); if( cursorThumb != null && cursorThumb.getCount() > 0 ) { cursorThumb.moveToFirst(); strThumbUri = cursorThumb.getString( cursorThumb.getColumnIndex( MediaStore.Images.Thumbnails.DATA )); } cursorThumb.close(); return strThumbUri; } 



Count of friends


The next step, defining the interface and the logic of the service, was the specification of the friends graph. We considered two approaches - the principle of friends (like VKontakte) and the principle of subscribers (like on Twitter). The principle of friends implies mutual consent to access to the gallery and personal profile, and also requires confirmation of acquaintance from the opposite side. In the case of subscribers, it is not necessary to request confirmation from the opposite side and allows each of the parties to independently determine the sources of filling their photo tape.
This choice implies a directed graph, which will be implemented as strings (subscriber ID, author ID) of the relational database on the server.
image


Network requests


All network requests are executed asynchronously, and the result of the operation using the callback interface is transmitted to the requesting module. In 2013, Google introduced its own Volley plugin as a replacement for Apache HTTPClient . Its advantages are support for the request queue , prioritization , standard wrappers for string and json requests, keepalive , resubmit on failure, etc. We decided to use it as the basis for most network requests.
What did not like in Volley
Looking ahead, Volley does simplify the development of network interfaces compared to HTTPClient, but at the time of development, standard wrappers for String and Json requests from Volley were still quite raw, for example, they did not allow to configure ContentType or HttpHeaders, there was no support for MultiPart requests, so we had to rewrite them a bit (see com.freecoders.photobook.network.MultiPartRequest and com.freecoders.photobook.network.StringRequest)

Code 4. Sample network request (User profile request)
class ServerInterface
  public static final void getUserProfileRequest (Context context, String[] userIds, final Response.Listener<HashMap<String, UserProfile>> responseListener, final Response.ErrorListener errorListener) { HashMap<String, String> headers = makeHTTPHeaders(); String strIdHeader = userIds.length > 0 ? userIds[0] : ""; for (int i = 1; i < userIds.length; i++) strIdHeader = strIdHeader + "," + userIds[i]; headers.put(Constants.KEY_ID, strIdHeader); Log.d(LOG_TAG, "Get user profile request"); StringRequest request = new StringRequest(Request.Method.GET, Constants.SERVER_URL + Constants.SERVER_PATH_USER, "", headers, new Response.Listener<String>() { @Override public void onResponse(String response) { Log.d(LOG_TAG, response); Type type = new TypeToken<ServerResponse <HashMap<String, UserProfile>>>(){}.getType(); try { ServerResponse<HashMap<String, UserProfile>> res = gson.fromJson(response, type); if (res != null && res.isSuccess() && res.data != null && responseListener != null) responseListener.onResponse(res.data); else if (responseListener != null) responseListener.onResponse(new HashMap<String, UserProfile>()); } catch (Exception e) { if (responseListener != null) responseListener.onResponse(new HashMap<String, UserProfile>()); } } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { if ((error != null) && (error.networkResponse != null) && (error.networkResponse.data != null)) Log.d(LOG_TAG, "Error: " + new String(error.networkResponse.data)); if (errorListener != null) errorListener.onErrorResponse(error); } } ); VolleySingleton.getInstance(context).addToRequestQueue(request); } 


Code 5. VolleySingleton
class VolleySingleton
 public class VolleySingleton { ... public <T> void addToRequestQueue(Request<T> req) { int socketTimeout = 90000; RetryPolicy policy = new DefaultRetryPolicy(socketTimeout, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT); req.setRetryPolicy(policy); getRequestQueue().add(req); } ... } 



Authorization


For authorization, we decided to use a pair of public / private id . Upon registration, this pair is sent to the client, and the private id is available only to this user and is sent in the HTTP header with each request to the server. A public id is available to all users and is used by other clients when they request to add friends or view a profile.


SQLite for database contacts, images


The local client database contains the necessary minimum information to ease the load on the server:

The list of friends from the address book registered in the service, their metadata (avatar, name, etc.) is requested on the server each time the application is launched and stored in the local database to fill the first fragment (contact list). The list of downloaded photos contains metadata of photos uploaded to the server. Both lists are synchronized with the server in case of reinstalling the application.
Code 6. Example of working in SQLite
class FriendsDataSource
 public class FriendsDataSource { private SQLiteDatabase database; private SQLiteHelper dbHelper; private String[] allColumns = { SQLiteHelper.COLUMN_ID,SQLiteHelper.COLUMN_NAME, SQLiteHelper.COLUMN_CONTACT_KEY, SQLiteHelper.COLUMN_USER_ID, SQLiteHelper.COLUMN_AVATAR, SQLiteHelper.COLUMN_STATUS}; ... public FriendEntry createFriend(String Name, String ContactKey, String UserId, String Avatar, int Status) { //Add new FriendEntry ContentValues cv = new ContentValues(); cv.put(dbHelper.COLUMN_CONTACT_KEY,ContactKey); cv.put(dbHelper.COLUMN_NAME,Name); cv.put(dbHelper.COLUMN_USER_ID,UserId); cv.put(dbHelper.COLUMN_AVATAR,Avatar); cv.put(dbHelper.COLUMN_STATUS,Status); cv.put(dbHelper.COLUMN_TYPE,FriendEntry.INT_TYPE_PERSON); database.insert(dbHelper.TABLE_FRIENDS, null, cv); return null; } public ArrayList<FriendEntry> getFriendsByStatus(int StatusSet[]) { String selection = dbHelper.COLUMN_STATUS + " IN (?"; String values[] = new String[StatusSet.length]; values[0] = String.valueOf(StatusSet[0]); for (int i = 1; i < StatusSet.length; i++) { selection = selection + ",?"; values[i] = String.valueOf(StatusSet[i]); } selection = selection + ") "; selection = selection + " AND " + SQLiteHelper.COLUMN_TYPE + " = " + FriendEntry.INT_TYPE_PERSON; String orderBy = SQLiteHelper.COLUMN_NAME + " ASC"; Cursor cursor = database.query(dbHelper.TABLE_FRIENDS, null, selection, values, null, null, orderBy); ArrayList<FriendEntry> listFriends = new ArrayList<FriendEntry>(); if (cursor == null) { return listFriends; } else if (!cursor.moveToFirst()) { cursor.close(); return listFriends; } do{ listFriends.add(cursorToFriendEntry(cursor)); }while (cursor.moveToNext()); cursor.close(); return listFriends; } private FriendEntry cursorToFriendEntry(Cursor cursor) { FriendEntry friend = new FriendEntry(); friend.setId(cursor.getInt(idColIndex)); friend.setName(cursor.getString(nameColIndex)); friend.setUserId(cursor.getString(userIdColIndex)); friend.setAvatar(cursor.getString(avatarColIndex)); friend.setStatus(cursor.getInt(statusColIndex)); friend.setType(cursor.getInt(typeColIndex)); friend.setContactKey(cursor.getString(ContactKeyColIndex)); return friend; } ... } 



To be continued


In this part of the article, we tried to consider only the main issues and problems that we encountered when developing an Android client. The project was developed in the style of the weekend hackathon and without any commercial purposes, so we do not pretend to the originality of the approaches, we can not boast of a coherent style of the code. If you have other tips, solutions or ideas for developing mobile social applications, then we will be glad to hear them in the comments. Also, if you liked or benefited our manual, you can freely use it in your projects, improve or even send pull-requests, for which we will be especially grateful.
In the second part, we will take a closer look at the server part of the service, features of uploading images to the AWS S3 cloud storage, post-processing of images, delivery of push notifications, etc.

Have a great weekend and see you soon!

Continued - Part II (Server)

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


All Articles