Урок 91 последний в группе уроков про AsyncTask и я тут накидал небольшой пример с комментариями в коде в котором находятся все задачи которые мы прошли, может кому пригодится, или кто-то посмотрит как можно реализровать тот или иной функционал. В общем все то, что мы прошли, ничего нового:
1. AsyncTask выполняется в отдельном потоке и эмулирует "тяжелый процесс", объект MyTask сохраняется и передается в новый Актвити при уничтожении старого. То есть отдельный поток MyTask не теряется. Используется новый метод onRetainCustomNonConfigurationInstance() вместо устаревшого onRetainNonConfigurationInstance()
2. MyTask класс наследует AsyncTask и объявлен как вложенный статический, чтобы избежать внутренней ссылки на внешний класс (в отличии от внутреннего).
3. в MyTask есть методы link() & unlink() чтобы обнулять ссылку на объект Актвити перед уничтожением старого активити (в случае смены ориентации экрана) и подключать ее после создания нового активити.
3. класс MyTask выполняет onPreExecute() и onPostExecute(), а так же onProgressUpdate(), который постонно передает в UI обновление хода процесса.*
4. * - из-за того что doInBackground() бежит в отдельном потоке и постоянно обращается к UI с помощью метода publishProgress() то иногда при повороте экрана происходит ситуация когда unlink уже обнулил ссылку на активити, а link еще не присвоил ее и в этот момент publishProgress() обращается к activity который равен null и соответственно падает с NullPointerException. Для этого сделал небольшой "буфер" в который складываются данные пока activity не будет назначен.
5. Реализована кнопка Cancel с досрочной остановкой потока MyTask
6. Кнопка Start неактивна пока процесс работает.
7. Результат который постоянно передается в TextView не обнуляется при поворотах экрана и выглядит все красиво. Используется onSaveInstanceState() и onRestoreInstanceState()
в коде подробные комментарии. За указания на ошибки или предложения лучшего решения буду на самом деле очень благодарен
layout/main.xml
[syntax=xml]
<RelativeLayout xmlns:android="
http://schemas.android.com/apk/res/android"
xmlns:tools="
http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
tools:context=".MainActivity">
<Button
android:id="@+id/btnCancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btnCancel" />
<Button
android:layout_toEndOf="@id/btnCancel"
android:id="@+id/btnStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btnStart" />
<ProgressBar
android:id="@+id/pb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/btnStart"
/>
<TextView
android:id="@+id/tvStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/btnStart"
android:text="Status: " />
<TextView
android:id="@+id/tvResult"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/tvStatus"
android:text="..."
android:textSize="25sp" />
</RelativeLayout>
[/syntax]
MainActivity.java:
[syntax=java5]
package com.mypackage.sa091_asynctask_5_retainactivityandmytask;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.util.concurrent.TimeUnit;
public class MainActivity extends AppCompatActivity {
private static String LOG_TAG = "myLogs";
private TextView tvResult, tvStatus;
private Button btnStart, btnCancel;
private ProgressBar pb;
private MyTask task;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
tvResult = (TextView) findViewById(R.id.tvResult);
tvStatus = (TextView) findViewById(R.id.tvStatus);
btnStart = (Button) findViewById(R.id.btnStart);
btnCancel = (Button) findViewById(R.id.btnCancel);
pb = (ProgressBar) findViewById(R.id.pb);
pb.setVisibility(View.GONE);
/*
Восстанавливаем сохраненный объект MyTask при создании нового Активити:
*/
task = (MyTask) getLastCustomNonConfigurationInstance();
/*
Если Активити создается первый раз значит создадим MyTask конструктором:
*/
if (task == null) {
task = new MyTask();
}
/* Присваиваем текущий объект Активити в поле объекта MyTask чтобы там был доступ к View элементам:
*/
task.link(this);
/*
Определяем с каким статусом отрисовывать кнопку Start, если AsyncTask поток выполняется
тогда она должна быть сразу неактивна при появлении.
*/
btnStart.setEnabled(isStartable());
btnStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
/* Делаем кнопку неактивной */
btnStart.setEnabled(false);
tvResult.setText("");
/* в кнопке Start еще раз создаем объект MyTask и добавляем в него текущий активити.
Делается это чтобы повторно не стартовать уже отработавший объект MyTask (иначе Exception)
*/
task = new MyTask();
task.link(MainActivity.this);
Log.d(LOG_TAG, "---btnStart: MainActivity hash: " + MainActivity.this.hashCode() + " MyTask hash: " + task.hashCode());
task.execute("android developer 80 level ");
}
});
btnCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (task != null) task.cancel(false);
}
});
}
/*
Готов к старту: если объект MyTask не равен null
и при этом он или Canceled или не RUNNING (значит PENDING или FINISHED)
*/
private boolean isStartable() {
if (task != null && (task.isCancelled() || !task.getStatus().equals(AsyncTask.Status.RUNNING))) {
return true;
} else {
return false;
}
}
/*
Сохраняем текст из tvResult и tvStatus для переноса в новое активити
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("tvResult", tvResult.getText().toString());
outState.putString("tvStatus", tvStatus.getText().toString());
}
/*
Вставляем сохраненный текст из tvResult и tvStatus в новое активити из старого
*/
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
tvResult.setText(savedInstanceState.getString("tvResult", ""));
tvStatus.setText(savedInstanceState.getString("tvStatus", ""));
}
/*
Перед уничтожением Активити вызывается этот метод, который сохраняет объект MyTask с уже работающим потоком.
Чтобы он не держал старое Активити - убираем у него ссылку на объект Активити.
*/
@Override
public Object onRetainCustomNonConfigurationInstance() {
if (task != null) {
task.unlink();
}
return task;
}
/*
Вложенный класс MyTask сделали статическим чтобы он не содержал скрытую ссылку на внешний класс Активити
*/
static class MyTask extends AsyncTask<String, Character, String> {
MainActivity activity;
StringBuffer sb = new StringBuffer();
public void link(MainActivity activity) {
this.activity = activity;
}
public void unlink() {
activity = null;
}
@Override
public void onPreExecute() {
activity.tvStatus.setText("Status: Begin");
activity.pb.setVisibility(View.VISIBLE);
}
public String doInBackground(String[] strings) {
int i = 0;
for (char c : strings[0].toCharArray()) {
try {
i++;
if (isCancelled()) return "";
TimeUnit.MILLISECONDS.sleep(200);
publishProgress(c);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return i + " chars successfully printed";
}
/*
Если новый (AsynkTask) поток продолжает работать и в этот момент пересоздастся Активити, то в
какой-то короткий момент объект класса MyTask (поток AsynkTask) не будет иметь ссылку на Активити.
Это происходит в момент когда unlink уже отработал, но link еще не сработал (В Main потоке),
а в AsynkTask потоке произошло обращение к эелменту в активити (например вызовом publishProgress()
который вызывает onProgressUpdate()) - тогда сработает исключение NullPointerException.
Для этого в методе onProgressUpdate() делаем буфер куда будет складываться промежуточная
информация в случае activity == null.
Когда активити вновь будет присвоен - этот буффер обновит элементы в актвити и будет очищен.
*/
@Override
public void onProgressUpdate(Character[] characters) {
if (activity == null) {
sb.append(characters[0]);
} else {
if (sb.length() > 0) {
activity.tvResult.append(sb.toString());
sb.setLength(0);
}
activity.tvResult.append(characters[0].toString());
Log.d(LOG_TAG, "---doInBackground: " + characters[0] + " MainActivity hash: " + activity.hashCode() +
" MyTask hash: " + this.hashCode());
}
}
/*
onPostExecute() выполняется когда doInBackground закончил работу.
Если к этому моменту активити так и не был назначен - вываливаем буфер здесь.
*/
@Override
public void onPostExecute(String result) {
if (activity == null) return;
activity.tvStatus.setText("Status: End. " + result);
activity.pb.setVisibility(View.GONE);
if (sb.length() > 0) {
activity.tvResult.append(sb.toString());
}
/* Делаем кнопку активной */
activity.btnStart.setEnabled(true);
}
/*
если был вызван метод cancel() тогда onCancelled() выполняется вместо onPostExecute()
поэтому здесь делаем то же что и в onPostExecute - вываливаем буфер во View элементы
и делаем кнопку Start вновь активной.
*/
@Override
protected void onCancelled() {
super.onCancelled();
if (activity == null) return;
activity.tvStatus.setText("Status: Canceled");
activity.pb.setVisibility(View.GONE);
if (sb.length() > 0) {
activity.tvResult.append(sb.toString());
}
/* Делаем кнопку активной */
activity.btnStart.setEnabled(true);
}
}
}
[/syntax]