Освой Android играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Базовый пример
Загружаем картинку
Загружаем серию картинок
Загружаем картинку с индикатором загрузки
Загружаем текстовый файл из сети
Когда деревья были большими, а коты были котятами, т.е. до версии Android 2.3.3 включительно, разработчик мог не прислушиваться к подобным рекомендациям и загружать картинки из сети в основном потоке. Но если проделать такую же операцию в проекте, рассчитанном на Android 4.0, то код не сработает и приложение будет вылетать с ошибкой. Поэтому придётся играть по новым правилам и использовать класс AsyncTask.
Стоит отметить, что в Android 11 класс признан устаревшим. Но прослужил он много лет верой и правдой. Современный подход предполагает использование корутин в Kotlin.
Теоретическую часть описания класса можно найти в отдельной статье. Коротко напомним алгоритм:
Все примеры подходят только небольших картинок в ограниченном количестве. Если ваше приложение активно загружает изображения по сети, то используйте готовые специальные библиотеки для загрузки картинок из интернета, которые содержат удобные методы для управления размерами изображения, кэширования и обработки ошибок, например, 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, который отвечает за изменение данных. Изменяющиеся данные мы используем для заполнения индикатора.
Запустив проект, вы увидите работающий индикатор прогресса, который заполнится за несколько секунд, и по завершении вы увидите сообщение об окончании загрузки.
Используя тот же механизм, мы можем загрузить картинку, которая находится на каком-нибудь сайте. Добавим в разметку элемент 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()));
}
}
}
Во время загрузки.
После окончания загрузки
Не только картинки с котиками, но и текст нельзя загружать из интернета в основном потоке. Поэтому напишем пример загрузки текстового файла с сайта в 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);