Освой Android играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Используем AsyncTask для загрузки изображений и текста из сети (Java)

Базовый пример
Загружаем картинку
Загружаем серию картинок
Загружаем картинку с индикатором загрузки
Загружаем текстовый файл из сети

Когда деревья были большими, а коты были котятами, т.е. до версии Android 2.3.3 включительно, разработчик мог не прислушиваться к подобным рекомендациям и загружать картинки из сети в основном потоке. Но если проделать такую же операцию в проекте, рассчитанном на Android 4.0, то код не сработает и приложение будет вылетать с ошибкой. Поэтому придётся играть по новым правилам и использовать класс AsyncTask.

Стоит отметить, что в Android 11 класс признан устаревшим. Но прослужил он много лет верой и правдой. Современный подход предполагает использование корутин в Kotlin.

Теоретическую часть описания класса можно найти в отдельной статье. Коротко напомним алгоритм:

  • Наследуемся от AsyncTask
  • Переопределяем некоторые методы класса
  • Создаём экземпляр созданного класса и вызываем метод execute()

Все примеры подходят только небольших картинок в ограниченном количестве. Если ваше приложение активно загружает изображения по сети, то используйте готовые специальные библиотеки для загрузки картинок из интернета, которые содержат удобные методы для управления размерами изображения, кэширования и обработки ошибок, например, Picasso.

Базовый пример

Рассмотрим сначала базовый пример для первого знакомства. AsyncTask предоставляет метод, который содержит все действия для выполнения в отдельном потоке – doInBackground() (делаем в фоне). Также используются методы onPreExecute() (предварительные операции), onProgressUpdate() (обновление данных) и onPostExecute() (после всего).

В качестве параметров метод doInBackground() может получать массив аргументов первого типа – это может быть массив веб-адресов, по которым следует загрузить картинки. Данный метод по окончании должен будет вернуть результат третьего типа.

Метод onPreExecute() вызывается в UI-потоке до вызова метода doInBackground(). Здесь можно инициализировать индикатор прогресса, который будет отображать процесс выполнения задачи.

Метод onPostExecute() также выполнится в UI-потоке по окончании выполнения doInBackground(). Тут можно выдать пользователю результат, например, что загрузка данных окончена.

Метод onProgressUpdate(), выполняемый в UI-потоке, позволяет отображать промежуточные результаты. Если в doInBackground() вызвать метод publishProgress() – объекты второго типа, переданные ему в качестве аргументов, попадут в onProgressUpdate().

Подготовим простейшую разметку для примера:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <ProgressBar
        android:id="@+id/progressbar"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:indeterminate="false" />

</LinearLayout>

Переходим к основной активности и пишем следующий код:


package ru.alexanderklimov.asynctaskdemo;

import android.os.AsyncTask;
import android.os.Bundle;
import android.os.SystemClock;
import android.widget.ProgressBar;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
        
        new MyAsyncTask().execute();
    }

    private class MyAsyncTask extends AsyncTask<Void, Integer, Void> {

        private ProgressBar mProgressBar;

        @Override
        protected void onPreExecute() {
            super.onPreExecute();

            mProgressBar = (ProgressBar) findViewById(R.id.progressbar);
        }

        @Override
        protected Void doInBackground(Void... params) {
            // имитируем длительную операцию
            for (int i = 1; i <= 100; i++) {
                SystemClock.sleep(100);
                publishProgress(i);
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);

            mProgressBar.setProgress(values[0]);
        }

        @Override
        protected void onPostExecute(Void result) {
            super.onPostExecute(result);

            Toast.makeText(MainActivity.this, "Загрузка закончена!",
                    Toast.LENGTH_LONG).show();
        }
    }
}

Мы определили внутренний класс MyAsyncTask, который расширяет AsyncTask. В нём переопределили вышеописанные методы. В методе onPreExecute() получаем ссылку на элемент ProgressBar. В методе doInBackground(Void… params) имитируем длительные действия. Далее вызываем метод publishProgress(), которому в качестве параметра передаём текущий прогресс выполнения. Когда закончится выполнение действий в методе doInBackground(), то в UI-потоке вызовется метод onPostExecute() в котором покажем пользователю всплывающее сообщение. Осталось только вызвать метод execute().

Обратите внимание, что в параметрах для AsyncTask<Void, Integer, Void> нас интересует второй параметр типа Integer, который отвечает за изменение данных. Изменяющиеся данные мы используем для заполнения индикатора.

Запустив проект, вы увидите работающий индикатор прогресса, который заполнится за несколько секунд, и по завершении вы увидите сообщение об окончании загрузки.

ProgressBar через AsyncTask

Загружаем картинку

Используя тот же механизм, мы можем загрузить картинку, которая находится на каком-нибудь сайте. Добавим в разметку элемент ImageView:


<ImageView
    android:id="@+id/web_image"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

Так как мы будем пользоваться интернетом, то не забываем прописать разрешение android.permission.INTERNET в манифесте и включить интернет на устройстве.

На этот раз обойдёмся без индикатора (хотя можно было и оставить), а задействуем первый и третий параметр для AsyncTask - AsyncTask<URL, Void, Bitmap>. Модифицируем код для дочернего класса:


package ru.alexanderklimov.asynctaskdemo;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle;
import android.widget.ImageView;

import androidx.appcompat.app.AppCompatActivity;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

public class MainActivity extends AppCompatActivity {

    private ImageView webImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        webImageView = findViewById(R.id.web_image);

        // Загружаем картинку из интернета
        String imageUrl = "http://developer.alexanderklimov.ru/android/images/android_cat.jpg";
        URL url;

        try {
            url = new URL(imageUrl);
            new MyAsyncTask().execute(url);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }

    private class MyAsyncTask extends AsyncTask<URL, Void, Bitmap> {

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
        }

        @Override
        protected Bitmap doInBackground(URL... urls) {
            Bitmap networkBitmap = null;

            URL networkUrl = urls[0]; // загружаем первый элемент
            try {
                networkBitmap = BitmapFactory.decodeStream(networkUrl
                        .openConnection().getInputStream());
            } catch (IOException e) {
                e.printStackTrace();
            }
            return networkBitmap;
        }

        protected void onPostExecute(Bitmap result) {
            webImageView.setImageBitmap(result);
        }
    }
}

Запускаем проект и через несколько мгновений (при хорошем соединении) в приложении загрузится картинка с сайта.

Загружаем кота через интернет

Загружаем серию картинок

Если вам нужно загрузить несколько картинок, то сделать это очень просто в рамках нашего примера. Метод doInBackground(URL... urls) позволяет использовать несколько параметров. Поэтому можно модифицировать программу для загрузки сразу трёх изображений с использованием индикатора прогресса.

Внесём изменения в разметку:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ProgressBar
        android:id="@+id/progressbar"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:indeterminate="false" />

    <ImageView
        android:id="@+id/first_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <ImageView
        android:id="@+id/second_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <ImageView
        android:id="@+id/third_image"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

Изменим код для загрузки трёх изображений с использованием индикатора прогресса.


package ru.alexanderklimov.asynctaskdemo;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

public class MainActivity extends AppCompatActivity {

    private ImageView[] mWebImageViews = new ImageView[3]; // массив для трёх картинок

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mWebImageViews[0] = findViewById(R.id.first_image);
        mWebImageViews[1] = findViewById(R.id.second_image);
        mWebImageViews[2] = findViewById(R.id.third_image);

        ProgressBar progressBar = findViewById(R.id.progressbar);

        // Адреса картинок из интернета
        String firstImageUrl = "http://developer.alexanderklimov.ru/android/images/pinkhellokitty.jpg";
        String secondImageUrl = "http://developer.alexanderklimov.ru/android/images/keyboard-cat.jpg";
        String thirdImageUrl = "http://developer.alexanderklimov.ru/android/images/cat-tips.jpg";

        URL firstUrl, secondUrl, thirdUrl;

        try {
            firstUrl = new URL(firstImageUrl);
            secondUrl = new URL(secondImageUrl);
            thirdUrl = new URL(thirdImageUrl);

            new MyAsyncTask(3, mWebImageViews, progressBar).execute(firstUrl, secondUrl,
                    thirdUrl);

        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }

    private class MyAsyncTask extends AsyncTask<URL, Integer, Void> {

        private ImageView[] mImageViews;
        private Bitmap[] mBitmaps;
        private ProgressBar mProgressBar;

        public MyAsyncTask(int numberOfImage, ImageView[] imageViews, ProgressBar progressBar) {
            mBitmaps = new Bitmap[numberOfImage];

            mImageViews = new ImageView[numberOfImage];

            for (int i = 0; i < numberOfImage; i++) {
                mImageViews[i] = imageViews[i];
            }

            // Упрощенный вариант копирования массива
            //  System.arraycopy(imageViews, 0, mImageViews, 0, numberOfImage);

            mProgressBar = progressBar;
        }

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
        }

        @Override
        protected Void doInBackground(URL... urls) {
            if (urls.length > 0) {
                for (int i = 0; i < urls.length; i++) {
                    URL networkUrl = urls[i];

                    try {
                        mBitmaps[i] = BitmapFactory.decodeStream(networkUrl
                                .openConnection().getInputStream());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                    publishProgress(i);

                    // делаем искусственную задержку (необязательно)
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            return null;
        }

        protected void onPostExecute(Void result) {
            Toast.makeText(getBaseContext(), "Загрузка завершена",
                    Toast.LENGTH_LONG).show();
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            if (values.length > 0) {
                for (Integer value : values) {
                    mImageViews[value].setImageBitmap(mBitmaps[value]);
                    mProgressBar.setProgress(value * 50);
                }
            }
        }
    }
}

Запускаем проект. Приложение при старте сразу загружает три картинки с сайта в ImageView.

Загружаем котов через интернет

Загружаем картинку с индикатором загрузки

Будем загружать картинку не сразу при запуске приложения, а по щелчку кнопки. Также будем выводить индикатор загрузки, чтобы было понятно, сколько ещё ждать. В этом случае вы должны рассматривать загрузку картинки, как загрузку файла. Тогда вы можете определить размер файла, чтобы управлять состоянием индикатора, а также сохранить сохранить картинку на диск устройства, например, в созданную приватную папку приложения.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/info_textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"/>

    <Button
        android:id="@+id/download_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="Загрузить"
        android:onClick="onClick"/>

    <ProgressBar
        android:id="@+id/progressbar"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp">
    </ProgressBar>

    <ImageView
        android:id="@+id/web_imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"/>

</LinearLayout>

Код для программы.


package ru.alexanderklimov.asynctaskdemo;

import android.content.Context;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;

public class MainActivity extends AppCompatActivity {

    private TextView mInfoTextView;
    private ProgressBar mProgressBar;
    private ImageView mImageView;

    private File directory;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        String filePath = "MyFileStorage";
        directory = this.getDir(filePath, Context.MODE_PRIVATE);

        mImageView = findViewById(R.id.web_imageView);
        mInfoTextView = findViewById(R.id.info_textView);
        mProgressBar = findViewById(R.id.progressbar);

    }

    public void onClick(View view) {
        String imageUrl = "http://developer.alexanderklimov.ru/android/images/webview3.png";
        new GetImageTask().execute(imageUrl);
    }

    private class GetImageTask extends AsyncTask<String, Integer, String> {

        protected void onPreExecute() {
            mProgressBar.setProgress(0);
        }

        protected String doInBackground(String... urls) {

            String filename = "android_cat.png";
            File myFile = new File(directory, filename);

            try {
                URL url = new URL(urls[0]);
                URLConnection connection = url.openConnection();
                connection.connect();
                int fileSize = connection.getContentLength();

                InputStream is = new BufferedInputStream(url.openStream());
                OutputStream os = new FileOutputStream(myFile);

                byte data[] = new byte[1024];
                long total = 0;
                int count;
                while ((count = is.read(data)) != -1) {
                    total += count;
                    publishProgress((int) (total * 100 / fileSize));
                    os.write(data, 0, count);
                }

                os.flush();
                os.close();
                is.close();

            } catch (Exception e) {
                e.printStackTrace();
            }
            return filename;
        }

        protected void onProgressUpdate(Integer... progress) {
            mInfoTextView.setText(String.valueOf(progress[0]) + "%");
            mProgressBar.setProgress(progress[0]);
        }

        protected void onCancelled() {
            Toast toast = Toast.makeText(getBaseContext(),
                    "Error connecting to Server", Toast.LENGTH_LONG);
            toast.show();
        }

        protected void onPostExecute(String filename) {
            mProgressBar.setProgress(100);
            mInfoTextView.setText("Загрузка завершена...");
            File myFile = new File(directory, filename);
            mImageView.setImageBitmap(BitmapFactory.decodeFile(myFile
                    .getAbsolutePath()));
        }
    }
}

Во время загрузки.

AsyncTask

После окончания загрузки

AsyncTask

Загружаем текстовый файл из сети

Не только картинки с котиками, но и текст нельзя загружать из интернета в основном потоке. Поэтому напишем пример загрузки текстового файла с сайта в TextView при помощи AsyncTask.

Создадим простейшую разметку из двух текстовых меток. В первой будем выводить информацию о текущей операции, во вторую метку загрузим текст, загруженный с сайта по указанному адресу. Не забудьте прописать разрешение на интернет.


<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/LinearLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/operation_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=""
        android:textAppearance="?android:attr/textAppearanceMedium" />

    <TextView
        android:id="@+id/message_text"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbarAlwaysDrawVerticalTrack="true"
        android:scrollbars="vertical"
        android:text=""
        android:textAppearance="?android:attr/textAppearanceMedium" />
</LinearLayout>

Далее пишем код.


package ru.alexanderklimov.asynctask;

import android.os.AsyncTask;
import android.os.Bundle;
import android.text.method.ScrollingMovementMethod;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;

public class MainActivity extends AppCompatActivity {

    private TextView mInfoTextView, mResultTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mInfoTextView = findViewById(R.id.operation_text);
        mResultTextView = findViewById(R.id.message_text);
        mResultTextView.setMovementMethod(new ScrollingMovementMethod());

        mInfoTextView.setText("Подождите...");

        new FileReadTask().execute();
    }

    private class FileReadTask extends AsyncTask<Void, Void, Void> {

        private String mResultText;

        @Override
        protected Void doInBackground(Void... params) {

            URL url;
            String source = "http://developer.alexanderklimov.ru/android/apk/realcat.txt";

            try {
                url = new URL(source);
                String buffer;
                BufferedReader bufferReader = new BufferedReader(
                        new InputStreamReader(url.openStream()));

                while ((buffer = bufferReader.readLine()) != null) {
                    mResultText += buffer;
                }
                bufferReader.close();
            } catch (IOException e) {
                e.printStackTrace();
                mResultText = e.toString();
            }

            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            mResultTextView.setText(mResultText);
            mInfoTextView.setText("Готово!");

            super.onPostExecute(result);
        }
    }
}

В фоновом потоке запускаем входной поток для считывания символов, используя буфер. Чтение происходит по строкам, добавляя считанные символы в переменную. Окончательный вариант выводится на экран.

Другой способ считывания входного потока через URLConnection. Создадим другую задачу.


class UrlConnectionTask extends AsyncTask<String, Void, String> {

    @Override
    protected String doInBackground(String... params) {

        String result = "";

        try {
            URL url = new URL(params[0]);
            URLConnection urlConnection = url.openConnection();
            InputStream inputStream = urlConnection.getInputStream();
            InputStreamReader inputStreamReader =
                    new InputStreamReader(inputStream);
            BufferedReader bufferedReader = 
                    new BufferedReader(inputStreamReader);

            String line;
            while ((line = bufferedReader.readLine()) != null) {
                result += line;
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

    @Override
    protected void onPostExecute(String result) {
        mResultTextView.setText(result);

        mInfoTextView.setText("Готово!");

        super.onPostExecute(result);
    }
}

Запускаем задачу.


UrlConnectionTask urlConnectionTask = new UrlConnectionTask();
String source = "http://developer.alexanderklimov.ru/android/apk/realcat.txt";
urlConnectionTask.execute(source);
Реклама