📜 ⬆️ ⬇️

Responsive Android application or 1001 ways to upload a picture

Much has already been written about the implementation of multithreading in the development of Android applications. In the same article, I would like to make a comparison of several ways to download / read / save / calculate common today, while not giving the user a reason for irritation. Try to understand when this or that decision will be appropriate, and what better not to do at all. Let's try to show why the usual things, such as the Thread class and the java.util.concurrent package, are not enough when it comes to an Android application.

The article does not have a task to highlight all the details of the implementation of each approach, but it is impossible to compare them without telling the basics. And therefore ...

Thread

The class Thread migrated from Java to the Android API is probably the easiest way to launch a new thread. Here are a couple of examples of how this is done: you can create a heir from Thread or pass into an instance of the Thread class an object that implements the Runnable interface.
')
Example 1. Expansion Thread.
class WorkingThread extends Thread{ @Override public void run() { //  } } 





Example 2. Runnable.
 class WorkingClass implements Runnable{ @Override public void run() { //  } } WorkingClass workingClass = new WorkingClass(); Thread thread = new Thread(workingClass); thread.start(); 


As a rule, after performing the required operations there is a need to provide the result to the user. But you can't just take and access UI elements from another thread. By virtue of the Android multithreading model, changing the state of interface elements is allowed only from the stream in which these elements were created, otherwise a CalledFromWrongThreadException will be thrown. In this case, the Android API provides several solutions at once.

Example 1. View # post (Runnable action).
 public class MainActivity extends Activity { TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView)findViewById(R.id.hello); WorkingClass workingClass = new WorkingClass(); Thread thread = new Thread(workingClass); thread.start(); } class WorkingClass implements Runnable{ @Override public void run() { //  //  UI   Runnable textView.post(new Runnable() { @Override public void run() { textView.setText("The job is done!"); } }); } } } 



Example 2. Activity # runOnUiThread (Runnable action).
 public class MainActivity extends Activity { TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView)findViewById(R.id.hello); WorkingClass workingClass = new WorkingClass(); Thread thread = new Thread(workingClass); thread.start(); } class WorkingClass implements Runnable{ @Override public void run() { //  //  UI   Runnable MainActivity.this.runOnUiThread(new Runnable() { @Override public void run() { textView.setText("The job is done!"); } }); } } } 



Example 3. Handler.
 public class MainActivity extends Activity { TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView)findViewById(R.id.hello); WorkingClass workingClass = new WorkingClass(true); Thread thread = new Thread(workingClass); thread.start(); } class WorkingClass implements Runnable{ public static final int SUCCESS = 1; public static final int FAIL = 2; private boolean dummyResult; public WorkingClass(boolean dummyResult){ this.dummyResult = dummyResult; } @Override public void run() { //  //    if (dummyResult){ //      uiHandler.sendEmptyMessage(SUCCESS); } else { //       Message msg = Message.obtain(); msg.what = FAIL; msg.obj = "An error occurred"; uiHandler.sendMessage(msg); } } } Handler uiHandler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { switch (msg.what) { case WorkingClass.SUCCESS: textView.setText("Success"); return true; case WorkingClass.FAIL: textView.setText((String)msg.obj); return true; } return false; } }); } 


In general, simple, but when it comes to active interaction with interface elements, the code can turn into a jumble of Runnable interfaces or a considerable size Handler class. To simplify the work of synchronizing the main and background threads, the AsyncTask class was already proposed in Android 1.5

AsyncTask

To use AsyncTask, you need to create its descendant class with parameterized types and override the necessary methods. After launch, AsyncTask will call its methods in the following order: onPreExecute (), doInBackground (Params ...), onPostExecute (Result), the first and the last of them will be called in the UI thread, and the second one, as is easy to guess, in a separate one. Moreover, the AsyncTask class allows the background process to inform the UI thread about its progress using the publishProgress (Progress ...) method, which in turn will call onProgressUpdate (Progress ...) in the UI thread.

AsyncTask example
 public class MainActivity extends Activity { private static final String IMAGE_URL = "http://eastbancgroup.com/images/ebtLogo.gif"; TextView textView; ImageView imageView; ProgressDialog progressDialog; DownloadTask downloadTask; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView)findViewById(R.id.hello); imageView = (ImageView)findViewById(R.id.imageView); downloadTask = new DownloadTask(); // ,      downloadTask.execute(IMAGE_URL); } @Override protected void onStop() { //   , //  Activity downloadTask.cancel(true); super.onStop(); } /* *    AsyncTask<Params, Progress, Result> *  ,     generic-. * Params -   .     String, .. *   url  * Progress -  ,      . *    Integer. * Result -  .    Drawable. */ class DownloadTask extends AsyncTask<String, Integer, Drawable>{ @Override protected void onPreExecute() { //    progressDialog = new ProgressDialog(MainActivity.this); progressDialog.setIndeterminate(false); progressDialog.setMax(100); progressDialog.setProgress(0); progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); progressDialog.setMessage("Downloading Image"); progressDialog.show(); } @Override protected Drawable doInBackground(String... params) { //       //  URLConnection int count; try { URL url = new URL(params[0]); URLConnection conection = url.openConnection(); conection.connect(); int lenghtOfFile = conection.getContentLength(); InputStream input = new BufferedInputStream(url.openStream(), 8192); OutputStream output = new FileOutputStream("/sdcard/downloadedfile.jpg"); byte data[] = new byte[256]; long total = 0; while ((count = input.read(data)) != -1) { //,     if (isCancelled()){ return null; } total += count; output.write(data, 0, count); //  . // ,     //       //onProgressUpdate    publishProgress((int)((total*100)/lenghtOfFile)); } output.flush(); output.close(); input.close(); } catch (Exception e) { Log.e("Error: ", e.getMessage()); } String imagePath = Environment.getExternalStorageDirectory().toString() + "/downloadedfile.jpg"; return Drawable.createFromPath(imagePath); } @Override protected void onProgressUpdate(Integer... progress) { progressDialog.setProgress(progress[0]); } //     @Override protected void onPostExecute(Drawable result) { imageView.setImageDrawable(result); progressDialog.dismiss(); } //     onPostExecute, //      //AsyncTask#cancel(boolean mayInterruptIfRunning) @Override protected void onCancelled() { } } } 



This example of loading a picture shows all the possibilities provided by the AsyncTask class: preparation, background operations, progress update, final actions, and work stoppage. And at each of these stages, the developer does not need to worry about synchronization of the background thread and the main one.

Although AsyncTask is often easier to create threads with the Thread class, there are cases in which the first way to implement multithreading will be more advantageous. Here are important, in our opinion, differences, taking into account which can help when choosing how to implement multithreading:

The second point is especially important. The fact is that because of this model of work of tasks on their basis, it is impossible to make a long-lived background process (such as a timer). Such an AsyncTask instance will block the queue and prevent any subsequent tasks from running. But using a bunch of Thread and Handler, on the contrary, it is enough just to achieve the execution of the code at intervals.

Timer example.
 public class MainActivity extends Activity { TextView textView; private int counter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); counter = 0; textView = (TextView)findViewById(R.id.hello); //  new Thread(new WorkingClass()).start(); } class WorkingClass implements Runnable{ public static final int RELAUNCH = 1; private static final int DELAY = 1000; @Override public void run() { //  //      1000ms uiHandler.sendEmptyMessageDelayed(RELAUNCH, DELAY); } } Handler uiHandler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { //  if (msg.what == WorkingClass.RELAUNCH){ textView.setText("Times: "+counter); counter++; new Thread(new WorkingClass()).start(); return true; } return false; } }); } 


Note: in fact, you can run several AsyncTask in parallel, using the AsyncTask # executeOnExecutor method (Executor exec, Params ... params) https://developer.android.com/reference/android/os/AsyncTask.html if you really need it .

In previous examples, both Thread and AsyncTask are used in the context of some Activity. In most cases, this is normal, but such a model can bring certain problems. It is necessary to understand that those working, although in the background, AsyncTask or Thread will not allow the garbage collector to delete an instance of our Activity when it is no longer needed. And it can happen very simply, for example, when turning the screen of the device. Each time the screen orientation changes, a new Activity will be created, and each time AsyncTask will be called. The larger the size of the loaded image, the faster the application closes with an OutOfMemoryError error. Worse than such an example can be, perhaps, the use of anonymous classes, as it is shown in many educational articles. By not keeping a link to a new task or flow, you are depriving yourself of the ability to control the progress of the process, for example, to stop it when you close the same Activity.

Total:

From the comparison of the Thread and AsyncTask classes, several conclusions can be drawn.
Tasks for which the use of Thread is justified:

Tasks for which the use of AsyncTask is justified:

The main condition imposed on work with Thread and AsyncTask: if the work was launched in the context of Activity / Fragment, then it should end as soon as possible, after stopping Activity / Fragment.

Loaders

There are types of data operations that, although it is possible to execute in the main application thread, can significantly slow down the interface or even trigger an ANR message . An illustrative example of such an operation is reading from a database / files. Until recently, a good practice of working with the database was to use the already reviewed Thread and AsyncTask, but in Android 3.0 classes such as Loader and LoaderManager were added, the purpose of which is to simplify asynchronous data loading into Activity or Fragment. For older versions of platforms, the same classes are available in the android support library .

The principle of working with Loaders is:

1. You need to create your own class that extends the Loader class or one of its standard heirs.
2. Implement loading generic-type D data in it
3. In Activity, get a link to LoaderManager and initialize your Loader, passing it and callback LoaderManager.LoaderCallbacks to the manager.

We give an example of how using the standard CursorLoader class you can display a list of phone contacts.

Loader example.
 public class MainActivity extends ListActivity implements LoaderCallbacks<Cursor> { //     static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] { Contacts._ID, Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS, }; private static final int LOADER_ID = 1; private SimpleCursorAdapter adapter; TextView textview; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //,      textview = (TextView)findViewById(R.id.loading); //  ,     getListView().setVisibility(View.GONE); //  ListView adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_2, null, new String[] { Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS }, new int[] { android.R.id.text1, android.R.id.text2 }, 0); setListAdapter(adapter); // Loader' //  id Loader'  callback LoaderManager lm = getLoaderManager(); lm.initLoader(LOADER_ID, null, this); } //    Loader,   //      @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { Uri baseUri = Contacts.CONTENT_URI; String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND (" + Contacts.HAS_PHONE_NUMBER + "=1) AND (" + Contacts.DISPLAY_NAME + " != '' ))"; return new CursorLoader(this, baseUri, CONTACTS_SUMMARY_PROJECTION, select, null, Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"); } //  ,     //  ,      @Override public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { switch (loader.getId()) { case LOADER_ID: adapter.swapCursor(cursor); textview.setVisibility(View.GONE); getListView().setVisibility(View.VISIBLE); break; } } @Override public void onLoaderReset(Loader<Cursor> loader) { adapter.swapCursor(null); } } 


Do not forget to specify the appropriate permission to read contacts in the application manifest.

Total:

The use of the Loaders template is closely related to the application components responsible for the mapping (Activity, Fragment) and therefore the execution time for data loading operations must be comparable to the lifetime of these components.

Service and IntentService

Service is one of the components of the Android application. The service itself is not a separate process or a separate thread. However, the service has its own life cycle, and it is just suitable for performing time-consuming operations in it. Additional threads running in the service context can run without interfering with user navigation through the application. For communication between the service and other components of the application, there are usually two methods used: ServiceConnection / IBinder interfaces or broadcast messages. The essence of the first method is to get a link to the running instance of the service. This is not to say that such a method somehow solves the problems of multitasking, it is rather suitable for managing the service. Communication with broadcast messages is thread-safe and will therefore be considered in the example.

Sample service.
 public class BackgroundService extends Service { public static final String CHANNEL = BackgroundService.class.getSimpleName()+".broadcast"; //     , //     Intent @Override public int onStartCommand(Intent intent, int flags, int startId) { //        sendResult(); return Service.START_NOT_STICKY; } //     , // Broadcast private void sendResult() { Intent intent = new Intent(CHANNEL); sendBroadcast(intent); } @Override public IBinder onBind(Intent intent) { return null; } } 


 public class MainActivity extends Activity { TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView)findViewById(R.id.hello); //     registerReceiver(receiver, new IntentFilter(BackgroundService.CHANNEL)); // ,    Intent Intent intent = new Intent(this, BackgroundService.class); startService(intent); } @Override protected void onStop() { //    unregisterReceiver(receiver); super.onStop(); } private BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { textView.setText("Message from Service"); } }; } 


Do not forget that the service, as well as Activity, must be declared in the project manifest.

In addition, the Android API provides the IntentService class, which extends the standard Service, but performs the processing of the data passed to it in a separate stream. When a new request is received, IntentService will create a new stream and call the IntentService # onHandleIntent (Intent intent) method https://developer.android.com/reference/android/app/IntentService.html#onHandleIntent(android.content.Intent) in it , which you just have to override. If upon receipt of a new request, the processing of the previous one has not yet ended, it will be queued.

Example IntentService.
 public class DownloadService extends IntentService { public DownloadService() { super("DownloadService"); } public static final String CHANNEL = DownloadService.class.getSimpleName()+".broadcast"; private void sendResult() { Intent intent = new Intent(CHANNEL); sendBroadcast(intent); } @Override public IBinder onBind(Intent intent) { return null; } //        @Override protected void onHandleIntent(Intent intent) { //  //     sendResult(); } } 

 public class MainActivity extends Activity { TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView)findViewById(R.id.hello); //     registerReceiver(receiver, new IntentFilter(DownloadService.CHANNEL)); // ,    Intent Intent intent = new Intent(this, DownloadService.class); startService(intent); } @Override protected void onStop() { //    unregisterReceiver(receiver); super.onStop(); } private BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { textView.setText("Message from Service"); } }; } 



Total:

The life cycle of services is usually longer than an Activity. Having started once, the service will be alive until it has completed its work, after which it will stop on its own. The developer basically needs only to organize the desired processing of incoming messages (intents): compare, construct a queue, etc., and send messages about the completion of each operation.
As you can see from the examples, it does not matter where the broadcast message will be sent from, the main thing is that it will be received in the main stream.

DownloadManager
Starting with the Android API version 9, the task of downloading and saving files over the network becomes even easier, thanks to the DowloadManager system service. All that remains to be done is to transfer this service to Uri, if you wish, specify the text that will be shown in the notification area during and after the download and subscribe to events that DownloadManager can send. This service will be set up to connect, respond to errors, resuming downloads, creating notifications in the Notification bar and, of course, downloading files in the background stream.

DownloadManager example.

DownloadManager Example
 public class MainActivity extends Activity { private static final String IMAGE_URL = "http://eastbancgroup.com/images/ebtLogo.gif"; ImageView imageView; DownloadManager downloadManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); imageView = (ImageView)findViewById(R.id.imageView); //   DownloadManager  downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE); //   Request request = new Request(Uri.parse(IMAGE_URL)); request.setTitle("Title"); //   request.setDescription("My description"); //   request.setMimeType("application/my-mime"); //mine type   //  ,  , //-      request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); //    downloadManager.enqueue(request); } @Override protected void onResume() { super.onResume(); //     registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_NOTIFICATION_CLICKED)); }; @Override protected void onPause() { super.onPause(); //    unregisterReceiver(receiver); }; BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); //  ,    if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)){ long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); DownloadManager.Query query = new DownloadManager.Query(); query.setFilterById(downloadId); Cursor cursor = dm.query(query); if (cursor.moveToFirst()){ int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS); if (DownloadManager.STATUS_SUCCESSFUL == cursor.getInt(columnIndex)) { String uriString = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)); imageView.setImageURI(Uri.parse(uriString)); } } //     } else if (DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(action)){ DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); //        , //       ,   //  long[] ids = intent.getLongArrayExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS); DownloadManager.Query query = new DownloadManager.Query(); query.setFilterById(ids); Cursor cursor = dm.query(query); int idIndex = cursor.getColumnIndex(DownloadManager.COLUMN_ID); if (cursor.moveToFirst()){ do { //    id   //   long downloadId = cursor.getLong(idIndex); } while (cursor.moveToNext()); } } } }; } 


DownloadManager has one feature. The fact is that when you click on a notification that a file has been successfully downloaded, contrary to expectations, a broadcast message like DownloadManager.ACTION_NOTIFICATION_CLICKED will not be sent. But instead, the service will try to find an Activity that can handle this click. So, if you want to react to this event, then add a new intent filter of the following content to the desired activity in the project manifest:

 <intent-filter> <action android:name="android.intent.action.VIEW" /> <data android:mimeType="application/my-mime" /> (mime type,   ) <category android:name="android.intent.category.DEFAULT" /> </intent-filter> 


In this case, when you click on the notification, your activity will be launched, to which the Intent with the download identifier will already be transferred. You can get it, for example, like this:

Intent intent = getIntent ();
String data = intent.getDataString ();

Total:

DownloadManager service is convenient to use for downloading large files that may be of interest to the user separately from your application, for example, images, media files, archives and much more. Keep in mind that other applications can also access files uploaded by you.

It’s not to say that we covered all the patterns of the implementation of the background work of the android-application, but with a high degree of confidence we can say that the considered methods are widely distributed. We hope this article will help you design the background-work most correctly and conveniently.

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


All Articles