Handler. Трохи теорії. Наочний приклад використання

В Android до потоку (thread) може бути прив'язана черга повідомлень. Ми можемо поміщати туди повідомлення, а система буде за чергою стежити і відправляти повідомлення на обробку. При цьому ми можемо вказати, щоб повідомлення пішло на обробку не відразу, а через певну кількість часу.

Handler - це механізм, який дає змогу працювати з чергою повідомлень. Він прив'язаний до конкретного потоку (thread) і працює з його чергою. Handler вміє поміщати повідомлення в чергу. При цьому він ставить самого себе як одержувача цього повідомлення. І коли настає час, система дістає повідомлення з черги і відправляє його адресату (тобто в Handler) на обробку.

Handler дає нам дві цікаві та корисні можливості:

  1. реалізувати відкладене за часом виконання коду
  2. виконання коду не у своєму потоці

У цьому уроці зробимо невеликий додаток. Він буде емулювати будь-яку довгу дію, наприклад закачування файлів і в TextView виводитиме кількість закачаних файлів. За допомогою цього прикладу ми побачимо, навіщо може бути потрібен Handler.

strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">`Handler`</string>
    <string name="start">Start</string>
    <string name="test">Test</string>
</resources>

main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical">
    <ProgressBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true">
    </ProgressBar>
    <TextView
        android:id="@+id/tvInfo"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="">
    </TextView>
    <Button
        android:id="@+id/btnStart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onclick"
        android:text="@string/start">
    </Button>
    <Button
        android:id="@+id/btnTest"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onclick"
        android:text="@string/test">
    </Button>
</LinearLayout>

ProgressBar у нас буде крутитися завжди. Пізніше стане зрозуміло, навіщо. TextView - для виведення інформації про закачування файлів. Кнопка Start стартуватиме закачування. Кнопка Test буде просто виводити в лог слово test.

Кодим MainActivity.java:

public class MainActivity extends Activity {
 
  final String LOG_TAG = "myLogs";
 
  Handler h;
  TextView tvInfo;
  Button btnStart;
 
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    tvInfo = (TextView) findViewById(R.id.tvInfo);
  }
 
  public void onclick(View v) {
    switch (v.getId()) {
    case R.id.btnStart:
      for (int i = 1; i <= 10; i++) {
        // довгий процес
        downloadFile();
        // оновлюємо TextView
        tvInfo.setText("Закачано файлов: " + i);
        // пишемо лог
        Log.d(LOG_TAG, "Закачано файлов: " + i);
      }
      break;
    case R.id.btnTest:
      Log.d(LOG_TAG, "test");
      break;
    default:
      break;
    }
  }
 
  void downloadFile() {
    // пауза - 1 секунда
    try {
      TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

В обробнику кнопки Start ми організуємо цикл для закачування файлів. У кожній ітерації циклу виконуємо метод downloadFile (який емулює закачування файлу), оновлюємо TextView і пишемо в лог інформацію про те, що кількість закачаних файлів змінилася. Разом у нас мають закачатися 10 файлів і після закачування кожного з них лог і екран мають показувати, скільки файлів уже закачано.

Після натискання кнопки Test - просто виводимо в лог повідомлення.

downloadFile - емулює закачування файлу, це просто пауза в одну секунду.

Ми бачимо, що ProgressBar крутиться. Натискаємо на кнопку Test, у логах з'являється test. Усе гаразд, додаток відгукується на наші дії.

Якщо ми натиснемо кнопку Start, то маємо спостерігати, як оновлюється TextView і пишеться лог після закачування чергового файлу. Але на ділі буде трохи не так. Наш застосунок просто "зависне" і перестане реагувати на натискання. Зупиниться ProgressBar, не буде оновлюватися TextView, і не буде натискатися кнопка Test. Тобто UI (екран) для нас стане недоступним. І тільки за логами буде зрозуміло, що застосунок насправді працює і файли закачуються. Натисніть Start і переконайтеся.

Екран "висить", а логи йдуть. Щойно всі 10 файлів буде закачано, застосунок оживе і знову почне реагувати на ваші натискання.

А все чому? Тому що робота екрана забезпечується основним потоком програми. А ми зайняли весь цей основний потік під свої потреби. У нашому випадку, начебто під закачування файлів. І щойно ми закінчили закачувати файли - потік звільнився, і екран став знову оновлюватися і реагувати на натискання.

Тут треба зрозуміти одну річ - основний потік програми відповідає за екран. Цей потік у жодному разі не можна вантажити чимось важким - екран просто перестає оновлюватися і реагувати на натискання. Якщо у вас є довгограючі завдання - їх треба винести в окремий потік. Спробуємо це зробити.

Перепишемо onclick:

public void onclick(View v) {
  switch (v.getId()) {
  case R.id.btnStart:
    Thread t = new Thread(new Runnable() {
      public void run() {
        for (int i = 1; i <= 10; i++) {
          // довгий процес
          downloadFile();
          // оновлюємо TextView
          tvInfo.setText("Закачано файлов: " + i);
          // пишемо лог
          Log.d(LOG_TAG, "i = " + i);
        }
      }
    });
    t.start();
    break;
  case R.id.btnTest:
    Log.d(LOG_TAG, "test");
    break;
  default:
    break;
  }
}

Тобто ми просто поміщаємо весь цикл у новий потік і запускаємо його. Тепер закачування файлів піде в цьому новому потоці. А основний потік буде не зайнятий і зможе без проблем промальовувати екран і реагувати на натискання. А отже, ми бачитимемо зміну TextView після кожного закачаного файлу і ProgressBar, що крутиться. І, взагалі, зможемо повноцінно взаємодіяти з додатком. Здавалося б, ось воно щастя :)

Все збережемо і запустимо додаток. Тиснемо Start.

Додаток вилетів із помилкою. Дивимося лог помилок у LogCat. Там є рядки:

android.view.ViewRoot$CalledFromWrongThreadException: Лише оригінальний потік, який створив ієрархію подання, може торкатися її подань.

і

at com.arakviel.develop.p0801Handler.MainActivity$1.run(MainActivity.java:37)

Дивимося, що за код у нас у MainActivity.java в 37-му рядку:

tvInfo.setText("Закачано файлів: " + i);

При спробі виконати цей код (не в основному потоці) ми отримали помилку "Only the original thread that created a view hierarchy can touch its views" ("Тільки оригінальний потік, який створив view-компоненти, може взаємодіяти з ними"). Тобто робота з view-компонентами доступна тільки з основного потоку. А нові потоки, які ми створюємо, не мають доступу до елементів екрана.

Тобто з одного боку, не можна завантажувати основний потік важкими завданнями, щоб не "вішався" екран. З іншого боку - нові потоки, створені для виконання важких завдань, не мають доступу до екрана, і ми не зможемо з них показати користувачеві, що наше важке завдання якось рухається.

Тут нам допоможе Handler. План такий:

  • ми створюємо в основному потоці Handler
  • у потоці закачування файлів звертаємося до Handler і з його допомогою поміщаємо в чергу повідомлення для нього ж самого
  • система бере це повідомлення, бачить, що адресат - Handler, і відправляє повідомлення на опрацювання в Handler
  • Handler, отримавши повідомлення, оновить TextView

Чим це відрізняється від нашої попередньої спроби оновити TextView з іншого потоку? Тим, що Handler був створений в основному потоці, і обробляти повідомлення, що надходять до нього, він буде в основному потоці, а отже, матиме доступ до екранних компонентів і зможе змінити текст у TextView. Отримати доступ до Handler з будь-якого іншого потоку ми зможемо без проблем, оскільки основний потік монополізує тільки доступ до UI. А елементи класів (у нашому випадку це Handler у MainActivity.java) доступні в будь-яких потоках. Таким чином Handler виступить як «міст» між потоками.

public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  tvInfo = (TextView) findViewById(R.id.tvInfo);
  btnStart = (Button) findViewById(R.id.btnStart);
  h = new Handler() {
    public void handleMessage(android.os.Message msg) {
      // оновлюємо TextView
      tvInfo.setText("Закачано файлов: " + msg.what);
      if (msg.what == 10) btnStart.setEnabled(true);
    };
  };
}

Тут ми створюємо Handler і в ньому реалізуємо метод обробки повідомлень handleMessage. Ми витягуємо з повідомлення атрибут what - це кількість закачаних файлів. Якщо вона дорівнює 10, тобто всі файли закачані, ми активуємо кнопку Start. (кількість закачаних файлів ми самі кладемо в повідомлення - зараз побачите, як)

Метод onclick перепишемо так:

public void onclick(View v) {
  switch (v.getId()) {
  case R.id.btnStart:
    btnStart.setEnabled(false);
    Thread t = new Thread(new Runnable() {
      public void run() {
        for (int i = 1; i <= 10; i++) {
          // довгий процес
          downloadFile();
          h.sendEmptyMessage(i);
          // пишемо лог
          Log.d(LOG_TAG, "i = " + i);
        }
      }
    });
    t.start();
    break;
  case R.id.btnTest:
    Log.d(LOG_TAG, "test");
    break;
  default:
    break;
  }
}

Ми деактивуємо кнопку Start перед запуском закачування файлів. Це просто захист, щоб не можна було запустити кілька закачувань одночасно. А в процесі закачування, після кожного закачаного файлу, відправляємо (sendEmptyMessage) для Handler повідомлення з кількістю вже закачаних файлів. Handler це повідомлення прийме, витягне з нього кількість файлів і оновить TextView.

Усе зберігаємо і запускаємо додаток. Тиснемо кнопку Start.

Кнопка Start стала неактивною, оскільки ми її самі вимкнули. А TextView оновлюється, ProgressBar крутиться і кнопка Test натискається. Тобто і закачування файлів триває, і застосунок продовжує працювати без проблем, відображаючи статус закачування.

Коли всі файли закачаються, кнопка Start знову стане активною.

Підсумуємо все вищесказане.

  1. Спочатку ми спробували вантажити додаток важким завданням в основному потоці. Це призвело до того, що ми втратили екран - він перестав оновлюватися і відповідати на натискання. Сталося це тому, що за екран відповідає основний потік програми, а він був сильно завантажений.
  2. Ми створили окремий потік і виконали весь важкий код там. І це б спрацювало, але нам треба було оновлювати екран у процесі роботи. А з не основного потоку доступу до екрану немає. Екран доступний тільки з основного потоку.
  3. Ми створили Handler в основному потоці. А з нового потоку відправляли для Handler повідомлення, щоб він нам оновлював екран. У підсумку Handler допоміг нам оновлювати екран не з основного потоку.

Простий приклад

Як ми пам'ятаємо, Handler дає змогу класти в чергу повідомлення і сам же вміє їх обробляти. Фішка тут у тому, що покласти повідомлення він може з одного потоку, а прочитати з іншого.

Повідомлення може містити в собі атрибути. Розглянемо найпростіший варіант, атрибут what.

Напишемо простий додаток-клієнт. Він, начебто, буде підключатися до сервера, виконувати якусь роботу і відключатися. На екрані ми спостерігатимемо, як змінюється статус підключення і як крутиться ProgressBar під час підключення.

При змінах стану підключення ми будемо відправляти повідомлення для Handler. А в атрибут what будемо класти поточний статус. Handler під час обробки повідомлення прочитає з нього what і виконає будь-які дії.

strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">HandlerSimpleMessage</string>
    <string name="connect">Connect</string>
</resources>

main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical">
    <Button
        android:id="@+id/btnConnect"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onclick"
        android:text="@string/connect">
    </Button>
    <TextView
        android:id="@+id/tvStatus"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="">
    </TextView>
    <ProgressBar
        android:id="@+id/pbConnect"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        android:visibility="gone">
    </ProgressBar>
</LinearLayout>

Кнопка для старту підключення, TextView для виведення інформації про статус підключення і ProgressBar, що працює в процесі підключення.

MainActivity.java:

public class MainActivity extends Activity {
 
  final String LOG_TAG = "myLogs";
 
  final int STATUS_NONE = 0;
  final int STATUS_CONNECTING = 1;
  final int STATUS_CONNECTED = 2;
 
  Handler h;
 
  TextView tvStatus;
  ProgressBar pbConnect;
  Button btnConnect;

  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
 
    tvStatus = (TextView) findViewById(R.id.tvStatus);
    pbConnect = (ProgressBar) findViewById(R.id.pbConnect);
    btnConnect = (Button) findViewById(R.id.btnConnect);
 
    h = new Handler() {
      public void handleMessage(android.os.Message msg) {
        switch (msg.what) {
        case STATUS_NONE:
          btnConnect.setEnabled(true);
          tvStatus.setText("Not connected");
          break;
        case STATUS_CONNECTING:
          btnConnect.setEnabled(false);
          pbConnect.setVisibility(View.VISIBLE);
          tvStatus.setText("Connecting");
          break;
        case STATUS_CONNECTED:
          pbConnect.setVisibility(View.GONE);
          tvStatus.setText("Connected");
          break;
        }
      };
    };
    h.sendEmptyMessage(STATUS_NONE);
  }
 
  public void onclick(View v) {
    Thread t = new Thread(new Runnable() {
      public void run() {
        try {
          // встановлюємо підключення
          h.sendEmptyMessage(STATUS_CONNECTING);
          TimeUnit.SECONDS.sleep(2);
 
          // встановлено
          h.sendEmptyMessage(STATUS_CONNECTED);
 
          // виконується якась робота
          TimeUnit.SECONDS.sleep(3);
 
          // розриваємо підключення
          h.sendEmptyMessage(STATUS_NONE);
 
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    });
    t.start();
  }
 
}

STATUS_NONE, STATUS_CONNECTING, STATUS_CONNECTED - це константи статусу. Їх будемо передавати в повідомленні, в атрибуті what. Зрозуміло, назви і значення цих констант довільні і взяті з голови. Ви можете придумати і використовувати свої.

В onCreate ми створюємо Handler і реалізуємо його метод handleMessage. Цей метод відповідає за обробку повідомлень, які призначені для цього Handler. Відповідно на вхід методу йде повідомлення - Message. Ми читаємо атрибут what і залежно від статусу підключення змінюємо екран:

  • STATUS_NONE - немає підключення. Кнопка підключення активна, TextView відображає статус підключення.
  • STATUS_CONNECTING - у процесі підключення. Кнопка підключення неактивна, показуємо ProgressBar, TextView відображає статус підключення.
  • STATUS_CONNECTED - підключено. Приховуємо ProgressBar, TextView відображає статус підключення.

В onCreate після створення Handler ми відразу відправляємо йому повідомлення зі статусом STATUS_NONE. Для цього ми використовуємо метод sendEmptyMessage. У цьому методі створюється повідомлення, заповнюється його атрибут what (значенням, яке ми передаємо в sendEmptyMessage), встановлюється Handler як адресат і повідомлення надсилається в чергу.

У методі onclick ми створюємо і запускаємо новий потік. У ньому ми, за допомогою sleep, емулюємо процес під'єднання до сервера, виконання роботи і вимкнення. І, в міру виконання дій, відправляємо повідомлення зі статусами для Handler. Тобто виходить, що після натискання на кнопку Connect статус змінюється на STATUS_CONNECTING, дві секунди йде підключення, статус змінюється на STATUS_CONNECTED, 3 секунди виконуються дії і статус змінюється на STATUS_NONE. Давайте перевіримо.

Усе збережемо і запустимо застосунок.

Тобто для простого оновлення статусу з нового потоку нам вистачило атрибута what. Але крім what повідомлення може мати ще кілька атрибутів.

Приклад з більш змістовними повідомленнями

Ми використовували метод sendEmptyMessage. Цей метод сам створював повідомлення Message, заповнював його атрибут what і відправляв у чергу. Крім what у повідомлення є ще атрибути arg1 і arg2 типу int, і obj типу Object. У цьому уроці ми самі створюватимемо повідомлення, заповнюватимемо атрибути та відправлятимемо.

Створимо застосунок, який буде підключатися до сервера, запитувати кількість файлів, готових для завантаження, емулювати завантаження і відображати на екрані перебіг дій, використовуючи горизонтальний ProgressBar і TextView.

strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">HandlerAdvMessage</string>
    <string name="connect">Connect</string>
</resources>

main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical">
    <Button
        android:id="@+id/btnConnect"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onclick"
        android:text="@string/connect">
    </Button>
    <TextView
        android:id="@+id/tvStatus"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="">
    </TextView>
    <ProgressBar
        android:id="@+id/pbDownload"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="gone">
    </ProgressBar>
</LinearLayout>

MainActivity.java:

public class MainActivity extends Activity {
 
  final String LOG_TAG = "myLogs";
 
  final int STATUS_NONE = 0; // нет подключения
  final int STATUS_CONNECTING = 1; // подключаемся
  final int STATUS_CONNECTED = 2; // подключено
  final int STATUS_DOWNLOAD_START = 3; // загрузка началась
  final int STATUS_DOWNLOAD_FILE = 4; // файл загружен
  final int STATUS_DOWNLOAD_END = 5; // загрузка закончена
  final int STATUS_DOWNLOAD_NONE = 6; // нет файлов для загрузки
 
  Handler h;
 
  TextView tvStatus;
  ProgressBar pbDownload;
  Button btnConnect;
 
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
 
    tvStatus = (TextView) findViewById(R.id.tvStatus);
    pbDownload = (ProgressBar) findViewById(R.id.pbDownload);
    btnConnect = (Button) findViewById(R.id.btnConnect);
 
    h = new Handler() {
      public void handleMessage(android.os.Message msg) {
        switch (msg.what) {
        case STATUS_NONE:
          btnConnect.setEnabled(true);
          tvStatus.setText("Not connected");
          pbDownload.setVisibility(View.GONE);
          break;
        case STATUS_CONNECTING:
          btnConnect.setEnabled(false);
          tvStatus.setText("Connecting");
          break;
        case STATUS_CONNECTED:
          tvStatus.setText("Connected");
          break;
        case STATUS_DOWNLOAD_START:
          tvStatus.setText("Start download " + msg.arg1 + " files");
          pbDownload.setMax(msg.arg1);
          pbDownload.setProgress(0);
          pbDownload.setVisibility(View.VISIBLE);
          break;
        case STATUS_DOWNLOAD_FILE:
          tvStatus.setText("Downloading. Left " + msg.arg2 + " files");
          pbDownload.setProgress(msg.arg1);
          saveFile((byte[]) msg.obj);
          break;
        case STATUS_DOWNLOAD_END:
          tvStatus.setText("Download complete!");
          break;
        case STATUS_DOWNLOAD_NONE:
          tvStatus.setText("No files for download");
          break;
        }
      };
    };
    h.sendEmptyMessage(STATUS_NONE);
  }
 
  public void onclick(View v) {
 
    Thread t = new Thread(new Runnable() {
      Message msg;
      byte[] file;
      Random rand = new Random();
 
      public void run() {
        try {
          // встановлюємо підключення
          h.sendEmptyMessage(STATUS_CONNECTING);
          TimeUnit.SECONDS.sleep(1);
 
          // підключення встановлено
          h.sendEmptyMessage(STATUS_CONNECTED);
 
          // визначаємо кількість файлів
          TimeUnit.SECONDS.sleep(1);
          int filesCount = rand.nextInt(5);
 
          if (filesCount == 0) {
            // повідомляємо, що файлів для завантаження немає
            h.sendEmptyMessage(STATUS_DOWNLOAD_NONE);
            // і відключаємося
            TimeUnit.MILLISECONDS.sleep(1500);
            h.sendEmptyMessage(STATUS_NONE);
            return;
          }
 
          // завантаження починається
          // створюємо повідомлення, з інформацією про кількість файлів
          msg = h.obtainMessage(STATUS_DOWNLOAD_START, filesCount, 0);
          // відправляємо
          h.sendMessage(msg);
 
          for (int i = 1; i <= filesCount; i++) {
            // завантажується файл
            file = downloadFile();
            // створюємо повідомлення з інформацією про порядковий номер
            // файла,
            // кількістю тих, що залишилися, і самим файлом
            msg = h.obtainMessage(STATUS_DOWNLOAD_FILE, i,
                filesCount - i, file);
            // відправляємо
            h.sendMessage(msg);
          }
 
          // завантаження завершено
          h.sendEmptyMessage(STATUS_DOWNLOAD_END);
 
          // відключаємося
          TimeUnit.MILLISECONDS.sleep(1500);
          h.sendEmptyMessage(STATUS_NONE);
 
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    });
    t.start();
  }
 
  byte[] downloadFile() throws InterruptedException {
    TimeUnit.SECONDS.sleep(2);
    return new byte[1024];
  }
 
  void saveFile(byte[] file) {
 
  }
 
}

В onCreate ми створюємо Handler і в його методі обробки (handleMessage) прописуємо всю логіку зміни екрана залежно від повідомлень, які надходять. Не буду детально це розписувати, там усе просто - змінюємо текст, вмикаємо/вимикаємо кнопку, показуємо/прикриваємо ProgressBar, змінюємо значення ProgressBar. З цікавого тут варто зазначити, що читаємо ми цього разу не тільки what, а й інші атрибути повідомлення - arg1, arg2, obj. А як вони заповнюються, побачимо далі.

В onclick створюємо новий потік для завантаження файлів. Встановлюємо підключення, отримуємо кількість готових для завантаження файлів. Якщо файлів для завантаження немає, надсилаємо відповідне повідомлення в Handler і відключаємося. Якщо ж файли є, ми створюємо повідомлення Message за допомогою методу getMessage(int what, int arg1, int arg2). Він приймає на вхід атрибути what, arg1 і arg2. У what ми кладемо статус, в arg1 - кількість файлів, arg2 - не потрібен, там просто нуль.

Далі починаємо завантаження. Після завантаження кожного файлу ми створюємо повідомлення Message за допомогою методу getMessage(int what, int arg1, int arg2, Object obj), заповнюємо його атрибути: what - статус, arg1 - порядковий номер файлу, arg2 - к-ть файлів, що залишилися, obj - файл. І відправляємо.

По завершенню завантаження відправляємо відповідне повідомлення і відключаємося.

  • downloadFile - емулює завантаження файлу. чекає дві секунди і повертає масив із 1024 байтів.
  • saveFile - метод збереження файлу на диск. Просто заглушка. Нічого не робить.

Все зберігаємо і запускаємо. Тиснемо Connect.

Використовуючи різні атрибути крім what, ми змогли передати в основний потік і використовувати там більш різноманітні дані.

Ми створюємо повідомлення за допомогою різних реалізацій методу getMessage. А чому б не створювати безпосередньо об'єкт Message за допомогою його конструкторів? У принципі можна, але офіційний хелп рекомендує користуватися методами getMessage, тому що це ефективніше і швидше. У цьому випадку повідомлення дістається з глобального пулу повідомлень, а не створюється з нуля.

Тут ви можете подивитися всі реалізації методу getMessage для формування повідомлень і використовувати той, який підходить для ситуації. Вони різняться різними комбінаціями вхідних параметрів.