A multi-threaded Activity

The ImageView we saw here displayed an image that was stored in the app’s app/res/drawable directory. The ImageView in this app is downloaded from the web while the app is running.

The UI thread (a.k.a. the main thread) must never try to do anything that would take more than 5 seconds, such as downloading a file from the web. See Keeping Your App Responsive. To do a long job, the main thread must spawn a worker thread. But the user interface (the UI) can be accessed only by the main thread.

Source code in DownloadFile.zip

  1. MainActivity.java. The MainActivity creates a Runnable. When the Runnable is finished, its last action is to create another Runnable. The image is from Wikipedia.
  2. activity_main.xml
  3. strings.xml
  4. AndroidManifest.xml uses-permission android.permission.INTERNET.
  5. build.gradle (Module: app)

Things to try

  1. The URL should be a string resource in strings.xml.
        <string name="url">https://upload.wikimedia.org/wikipedia/commons/thumb/8/8d/President_Barack_Obama.jpg/440px-President_Barack_Obama.jpg</string>
    
                    URL url = new URL(getString(R.string.url));
    

  2. What would happen if we try to download the image in the main thread? Change onCreate to the following.
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            InputStream inputStream;
            try {
                URL url = new URL(getString(R.string.url));
                inputStream = url.openStream();
            } catch (Exception exception) {
                Log.e("myTag", "thread threw exception", exception);
                return;
            }
    
            Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
            if (bitmap == null) {
                Log.e("myTag", "image data in downlaoded file could not be decoded");
                return;
            }
    
            ImageView imageView = (ImageView)findViewById(R.id.imageView);
            imageView.setImageBitmap(bitmap);
        }
    
    The LogCat output shows that openStream would throw an android.os.NetworkOnMainThreadException.

  3. Given the outcome of the above exercise, we are forced to spawn a second thread to download the image. Once the image is downloaded, what would happen if the second thread tries to install the image into the ImageView by calling setImageBitmap? We will discover that the second thread is not allowed to make any change to the UI, not even by displaying a Toast. That’s why we’re reporting errors with the Log.e we saw in Text.
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            Runnable runnable = new Runnable() {
                @Override
                public void run() { //This run method is executed by the second thread.
                    InputStream inputStream;
                    try {
                        URL url = new URL(getString(R.string.url));
                        inputStream = url.openStream();
                    } catch (Exception exception) {
                        Log.e("myTag", "thread threw exception", exception);
                        return;
                    }
    
                    Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                    if (bitmap == null) {
                        Log.e("myTag", "image data in downlaoded file could not be decoded");
                        return;
                    }
    
                    ImageView imageView = (ImageView)findViewById(R.id.imageView);
                    imageView.setImageBitmap(bitmap);
                }
            };
    
            Thread thread = new Thread(runnable);
            thread.start();
        }
    
    Process: edu.nyu.scps.downloadfile, PID: 6709
        android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
    

  4. Here’s the code in the above .zip file with the Thread and the outermost Runnable changed to anonymous temporary objects.
  5.     @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            new Thread(new Runnable() {
                @Override
                public void run() { //This run method is executed by the second thread.
                    InputStream inputStream;
                    try {
                        URL url = new URL(getString(R.string.url));
                        inputStream = url.openStream();
                    } catch (Exception exception) {
                        Log.e("myTag", "thread threw exception", exception);
                        return;
                    }
    
                    final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                    if (bitmap == null) {
                        Log.e("myTag", "image data in downlaoded file could not be decoded");
                        return;
                    }
    
                    final ImageView imageView = (ImageView)findViewById(R.id.imageView);
                    imageView.post(new Runnable() {
                        @Override
                        public void runsetImageBitmap(bitmap);
                        }
                    });
                }
            }).start();
        }