Какова цель Looper и как его использовать?
Я новичок в Android. Я хочу знать, что Looper
класс делает, а также как его использовать. Я прочитал документацию по классу Android Looper, но не могу понять ее полностью. Я видел это во многих местах, но не мог понять его цель. Может ли кто-нибудь помочь мне, определив цель Looper
а также, приведя простой пример, если это возможно?
14 ответов
Что такое Лупер?
Looper - это класс, который используется для выполнения сообщений (Runnables) в очереди. Обычные потоки не имеют такой очереди, например, простой поток не имеет никакой очереди. Он выполняется один раз, и после завершения выполнения метода поток не запускает другое сообщение (Runnable).
Где мы можем использовать класс Looper?
Если кто-то хочет выполнить несколько сообщений (Runnables), он должен использовать класс Looper, который отвечает за создание очереди в потоке. Например, при написании приложения, которое загружает файлы из Интернета, мы можем использовать класс Looper для помещения файлов для загрузки в очередь.
Как это устроено?
Есть prepare()
способ приготовления лупера. Тогда вы можете использовать loop()
метод для создания цикла сообщений в текущем потоке, и теперь ваш Looper готов выполнять запросы в очереди, пока вы не выйдете из цикла.
Вот код, по которому вы можете подготовить Looper.
class LooperThread extends Thread {
public Handler mHandler;
@Override
public void run() {
Looper.prepare();
mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// process incoming messages here
}
};
Looper.loop();
}
}
Вы можете лучше понять, что такое Looper в контексте инфраструктуры GUI. Looper сделан, чтобы сделать 2 вещи.
1) Looper преобразует обычный поток, который завершается при возврате его метода run(), во что-то, работающее непрерывно, пока не запускается приложение Android, что необходимо в графическом интерфейсе (технически, он все еще завершается, когда возвращается метод run(). Но позвольте мне уточнить, что я имею в виду ниже).
2) Looper предоставляет очередь, в которой выполняются задания, которые также необходимы в среде GUI.
Как вы, возможно, знаете, когда приложение запускается, система создает поток выполнения для приложения, называемый "основным", и приложения Android обычно запускаются полностью в одном потоке, по умолчанию "основной поток". Но главная нить не какая-то секретная, особая нить. Это просто нормальная нить, похожая на нити, которые вы создаете new Thread()
код, что означает, что он завершается, когда его метод run() возвращается! Подумайте о приведенном ниже примере.
public class HelloRunnable implements Runnable {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
(new Thread(new HelloRunnable())).start();
}
}
Теперь давайте применим этот простой принцип к приложениям для Android. Что произойдет, если приложение Android будет работать в обычном потоке? Поток с именем "main" или "UI" или чем-то еще, запускает ваше приложение и рисует весь UI. Итак, первый экран отображается для пользователей. И что теперь? Основной поток заканчивается? Нет, не должно. Следует подождать, пока пользователи что-то сделают, верно? Но как мы можем достичь этого поведения? Ну, мы можем попробовать с Object.wait()
или же Thread.sleep()
, Например, основной поток завершает свою начальную работу для отображения первого экрана и спит. Он пробуждается, что означает прерванный, когда выбирается новая работа. Пока все хорошо, но в данный момент нам нужна структура данных в виде очереди для хранения нескольких заданий. Подумайте о случае, когда пользователь касается экрана последовательно, и выполнение задачи занимает больше времени. Таким образом, нам нужна структура данных для хранения заданий, выполняемых в порядке "первым пришел - первым вышел". Кроме того, вы можете себе представить, что реализовать поток, который постоянно выполняется и обрабатывает задание, когда поступает, используя прерывание, нелегко и приводит к сложному и часто не поддерживаемому коду. Мы бы предпочли создать новый механизм для этой цели, и это то, что Лупер это все. Официальный документ класса Looper гласит: "С потоками по умолчанию не связан цикл сообщений", а Looper - это класс, "используемый для запуска цикла сообщений для потока". Теперь вы можете понять, что это значит.
Чтобы сделать вещи более понятными, давайте проверим код, в котором преобразован основной поток. Все это происходит в классе ActivityThread. В его методе main() вы можете найти приведенный ниже код, который превращает обычный основной поток в то, что нам нужно.
public final class ActivityThread {
...
public static void main(String[] args) {
...
Looper.prepareMainLooper();
Looper.loop();
...
}
}
а также Looper.loop()
Метод зацикливается бесконечно и удаляет сообщение из очереди и обрабатывает по одному:
public static void loop() {
...
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
...
msg.target.dispatchMessage(msg);
...
}
}
Итак, в основном, Looper - это класс, созданный для решения проблемы, возникающей в среде GUI. Но такого рода потребности могут возникать и в других ситуациях. На самом деле это довольно известный шаблон для многопоточного приложения, и вы можете узнать о нем больше в "Параллельном программировании на Java" Дуга Ли (особенно было бы полезно узнать главу 4.1.4 "Рабочие потоки"). Кроме того, вы можете себе представить, что этот вид механизма не уникален в платформе Android, но все графические интерфейсы могут нуждаться в некоторой аналогии с этим. Вы можете найти почти такой же механизм в среде Java Swing.
Looper позволяет последовательно выполнять задачи в одном потоке. И обработчик определяет те задачи, которые нам нужно выполнить. Это типичный сценарий, который я пытаюсь проиллюстрировать в этом примере:
class SampleLooper extends Thread {
@Override
public void run() {
try {
// preparing a looper on current thread
// the current thread is being detected implicitly
Looper.prepare();
// now, the handler will automatically bind to the
// Looper that is attached to the current thread
// You don't need to specify the Looper explicitly
handler = new Handler();
// After the following line the thread will start
// running the message loop and will not normally
// exit the loop unless a problem happens or you
// quit() the looper (see below)
Looper.loop();
} catch (Throwable t) {
Log.e(TAG, "halted due to an error", t);
}
}
}
Теперь мы можем использовать обработчик в некоторых других потоках (скажем, в потоке пользовательского интерфейса), чтобы опубликовать задачу в Looper для выполнения.
handler.post(new Runnable()
{
public void run() {
//This will be executed on thread using Looper.
}
});
В потоке пользовательского интерфейса у нас есть неявный Looper, который позволяет нам обрабатывать сообщения в потоке пользовательского интерфейса.
Android Looper
это обертка для прикрепления MessageQueue
в Thread
и он управляет обработкой очереди. Это выглядит очень загадочно в документации Android, и много раз мы можем столкнуться Looper
связанные проблемы доступа к пользовательскому интерфейсу. Если мы не понимаем основ, с этим становится очень трудно справиться.
Вот статья, которая объясняет Looper
жизненный цикл, как его использовать и использование Looper
в Handler
Looper = Thread + MessageQueue
Looper
это класс, который превращает поток в поток конвейера и Handler
дает вам механизм для вставки задач в него из любых других потоков.
Так что PipeLine Thread
это поток, который может принимать больше задач из других потоков через Handler
,
Looper назван так, потому что он реализует цикл - берет следующую задачу, выполняет ее, затем берет следующую и так далее. Обработчик называется обработчиком, потому что он используется для обработки или принятия следующей задачи каждый раз из любого другого потока и передачи в Looper (Thread или PipeLine Thread).
Отличным примером Looper and Handler или PipeLine Thread является загрузка нескольких изображений или загрузка их на сервер (Http) по одному в одном потоке вместо запуска нового потока для каждого сетевого вызова в фоновом режиме.
Узнайте больше здесь о Looper и Handler и определении Threading Thread:
Понимание нитей Looper
Поток Java - это единица выполнения, которая была разработана для выполнения задачи в своем методе run() и завершается после этого:
Но в Android есть много случаев, когда нам нужно поддерживать активность потока и ждать, например, пользовательских вводов / событий. Пользовательский интерфейс акаMain Thread
,
Основной поток в Android - это поток Java, который сначала запускается JVM при запуске приложения и продолжает работать до тех пор, пока пользователь не решит закрыть его или не встретит необработанное исключение.
Когда приложение запускается, система создает поток выполнения для приложения, называемый "основным". Этот поток очень важен, потому что он отвечает за отправку событий в соответствующие виджеты пользовательского интерфейса, включая события рисования.
Теперь обратите внимание на то, что хотя основной поток является потоком Java, он продолжает прослушивать пользовательские события и рисует кадры со скоростью 60 кадров в секунду на экране, и все же он не умирает после каждого цикла.как это так?
Ответ: Looper Class: Looper - это класс, который используется для поддержания потока в рабочем состоянии и управления очередью сообщений для выполнения задач в этом потоке.
По умолчанию потоки не имеют связанного с ними цикла сообщений, но вы можете назначить его, вызвав Looper.prepare () в методе run, а затем вызвать Looper.loop ().
Цель Looper - сохранить поток и дождаться следующего цикла ввода
Message
объект для выполнения вычислений, которые в противном случае будут уничтожены после первого цикла выполнения.
Если вы хотите копнуть глубже, как управлять Looper Message
очередь объектов, то вы можете взглянуть на исходный код Looperclass
:
https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/os/Looper.java
Ниже приведен пример того, как вы можете создать Looper Thread
и общаться с Activity
класс с использованием LocalBroadcast
class LooperThread : Thread() {
// sendMessage success result on UI
private fun sendServerResult(result: String) {
val resultIntent = Intent(ServerService.ACTION)
resultIntent.putExtra(ServerService.RESULT_CODE, Activity.RESULT_OK)
resultIntent.putExtra(ServerService.RESULT_VALUE, result)
LocalBroadcastManager.getInstance(AppController.getAppController()).sendBroadcast(resultIntent)
}
override fun run() {
val looperIsNotPreparedInCurrentThread = Looper.myLooper() == null
// Prepare Looper if not already prepared
if (looperIsNotPreparedInCurrentThread) {
Looper.prepare()
}
// Create a handler to handle messaged from Activity
handler = Handler(Handler.Callback { message ->
// Messages sent to Looper thread will be visible here
Log.e(TAG, "Received Message" + message.data.toString())
//message from Activity
val result = message.data.getString(MainActivity.BUNDLE_KEY)
// Send Result Back to activity
sendServerResult(result)
true
})
// Keep on looping till new messages arrive
if (looperIsNotPreparedInCurrentThread) {
Looper.loop()
}
}
//Create and send a new message to looper
fun sendMessage(messageToSend: String) {
//Create and post a new message to handler
handler!!.sendMessage(createMessage(messageToSend))
}
// Bundle Data in message object
private fun createMessage(messageToSend: String): Message {
val message = Message()
val bundle = Bundle()
bundle.putString(MainActivity.BUNDLE_KEY, messageToSend)
message.data = bundle
return message
}
companion object {
var handler: Handler? = null // in Android Handler should be static or leaks might occur
private val TAG = javaClass.simpleName
}
}
Использование:
class MainActivity : AppCompatActivity() {
private var looperThread: LooperThread? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// start looper thread
startLooperThread()
// Send messages to Looper Thread
sendMessage.setOnClickListener {
// send random messages to looper thread
val messageToSend = "" + Math.random()
// post message
looperThread!!.sendMessage(messageToSend)
}
}
override fun onResume() {
super.onResume()
//Register to Server Service callback
val filterServer = IntentFilter(ServerService.ACTION)
LocalBroadcastManager.getInstance(this).registerReceiver(serverReceiver, filterServer)
}
override fun onPause() {
super.onPause()
//Stop Server service callbacks
LocalBroadcastManager.getInstance(this).unregisterReceiver(serverReceiver)
}
// Define the callback for what to do when data is received
private val serverReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val resultCode = intent.getIntExtra(ServerService.RESULT_CODE, Activity.RESULT_CANCELED)
if (resultCode == Activity.RESULT_OK) {
val resultValue = intent.getStringExtra(ServerService.RESULT_VALUE)
Log.e(MainActivity.TAG, "Server result : $resultValue")
serverOutput.text =
(serverOutput.text.toString()
+ "\n"
+ "Received : " + resultValue)
serverScrollView.post( { serverScrollView.fullScroll(View.FOCUS_DOWN) })
}
}
}
private fun startLooperThread() {
// create and start a new LooperThread
looperThread = LooperThread()
looperThread!!.name = "Main Looper Thread"
looperThread!!.start()
}
companion object {
val BUNDLE_KEY = "handlerMsgBundle"
private val TAG = javaClass.simpleName
}
}
Можем ли мы вместо этого использовать задачу Async или Intent Services?
Асинхронные задачи предназначены для выполнения коротких операций в фоновом режиме и обеспечения прогресса и результатов в потоке пользовательского интерфейса. Асинхронные задачи имеют ограничения, например, вы не можете создать более 128 асинхронных задач и
ThreadPoolExecutor
позволит только до 5 асинхронных задач.IntentServices
также предназначены для выполнения фоновой задачи на более длительный срок, и вы можете использоватьLocalBroadcast
общаться сActivity
, Но сервисы разрушаются после выполнения задачи. Если вы хотите, чтобы он работал долго, вам нужно делать такие хаки, какwhile(true){...}
,
Другие значимые варианты использования для Looper Thread:
Используется для двусторонней связи через сокет, когда сервер продолжает прослушивать сокет клиента и записывает подтверждение
Обработка растровых изображений в фоновом режиме. Передайте URL-адрес изображения в поток Looper, и он применит эффекты фильтра и сохранит его в определенном месте, а затем передаст временный путь изображения.
Продолжительность жизни java Thread заканчивается после завершения run()
метод. Та же тема не может быть запущена снова.
Лупер превращается нормально Thread
в цикл сообщений. Ключевые методы Looper
являются:
void prepare ()
Инициализируйте текущий поток как петлитель. Это дает вам возможность создать обработчики, которые затем ссылаются на этот петлитель, прежде чем фактически запустить цикл. Обязательно вызовите loop() после вызова этого метода и завершите его, вызвав quit().
void loop ()
Запустите очередь сообщений в этой теме. Обязательно вызовите quit (), чтобы завершить цикл.
void quit()
Выходит из петлителя.
Заставляет метод loop() завершаться, не обрабатывая больше сообщений в очереди сообщений.
В этой статье от Janishar мы расскажем об основных понятиях.
Looper
связан с темой. Если тебе надо Looper
в потоке пользовательского интерфейса, Looper.getMainLooper()
вернет связанный поток.
Тебе нужно Looper
быть связанным с обработчиком.
Looper
, Handler
, а также HandlerThread
Android-способ решения проблем асинхронного программирования.
Когда у вас есть Handler
Вы можете позвонить ниже API.
post (Runnable r)
Заставляет Runnable r быть добавленным в очередь сообщений. Runnable будет запущен в потоке, к которому прикреплен этот обработчик.
boolean sendMessage (Message msg)
Помещает сообщение в конец очереди сообщений после всех ожидающих сообщений до текущего времени. Он будет получен в handleMessage(Message) в потоке, прикрепленном к этому обработчику.
HandlerThread - удобный класс для запуска нового потока, в котором есть петлитель. Цикл затем может быть использован для создания классов обработчиков
В некоторых случаях вы не можете запустить Runnable
задачи в потоке пользовательского интерфейса. например, сетевые операции: отправьте сообщение в сокет, откройте URL и получите контент, прочитав InputStream
В этих случаях, HandlerThread
Полезно. Ты можешь получить Looper
объект из HandlerThread
и создать Handler
на HandlerThread
вместо основного потока.
Код HandlerThread будет выглядеть так:
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
Обратитесь к сообщению ниже для примера кода:
Лупер имеет synchronized
MessageQueue
используется для обработки сообщений, помещенных в очередь.
Он реализует Thread
Конкретный шаблон хранения.
Только один Looper
в Thread
, Ключевые методы включают prepare()
,loop()
а также quit()
,
prepare()
инициализирует текущий Thread
как Looper
, prepare()
является static
метод, который использует ThreadLocal
класс, как показано ниже.
public static void prepare(){
...
sThreadLocal.set
(new Looper());
}
prepare()
должен быть вызван явно перед запуском цикла обработки событий.loop()
запускает цикл обработки событий, который ожидает поступления сообщений в очередь сообщений определенного потока. После получения следующего сообщенияloop()
метод отправляет сообщение своему целевому обработчикуquit()
выключает цикл событий. Это не завершает цикл, но вместо этого ставит в очередь специальное сообщение
Looper
может быть запрограммирован в Thread
через несколько шагов
простираться
Thread
Вызов
Looper.prepare()
инициализировать поток какLooper
Создать один или несколько
Handler
(ы) для обработки входящих сообщений- Вызов
Looper.loop()
обрабатывать сообщения, пока цикл не скажетquit()
,
Этот ответ не имеет ничего общего с вопросом, но использование "петлителя" и способа, которым люди создали обработчик и петлитель во ВСЕХ ответах здесь, - это просто плохая практика (хотя некоторые объяснения верны), я должен опубликовать это:
HandlerThread thread = new HandlerThread(threadName);
thread.start();
Looper looper = thread.getLooper();
Handler myHandler = new Handler(looper);
и для полной реализации
Что такое Лупер?
ОТ ДОКУМЕНТОВ
Looper
Looper
Класс, используемый для запуска цикла сообщений для thread
, По умолчанию потоки не имеют петли сообщений, связанной с ними; чтобы создать его, позвоните prepare()
в потоке, который должен запустить цикл, а затем loop()
чтобы он обрабатывал сообщения до тех пор, пока цикл не будет остановлен.
-
Looper
цикл обработки сообщений: - Важным символом Looper является то, что он связан с потоком, в котором создается Looper
- Класс Looper поддерживает
MessageQueue
, который содержит список сообщений. Важным символом Looper является то, что он связан с потоком, в котором создается Looper. -
Looper
назван так, потому что он реализует цикл - берет следующую задачу, выполняет ее, затем берет следующую и так далее.Handler
называется обработчиком, потому что кто-то не мог придумать лучшего имени - Android
Looper
это класс Java в пользовательском интерфейсе Android, который вместе с классом Handler обрабатывает события пользовательского интерфейса, такие как нажатия кнопок, перерисовка экрана и переключение ориентации.
Как это устроено?
Создание Looper
Нить получает Looper
а также MessageQueue
позвонив Looper.prepare()
после его запуска. Looper.prepare()
идентифицирует вызывающий поток, создает Looper и MessageQueue
объект и связать поток
ОБРАЗЕЦ КОДА
class MyLooperThread extends Thread {
public Handler mHandler;
public void run() {
// preparing a looper on current thread
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
// process incoming messages here
// this will run in non-ui/background thread
}
};
Looper.loop();
}
}
Для получения дополнительной информации проверьте ниже пост
Лучшим примером является обработка нескольких загруженных или загруженных элементов в Сервисе.
Handler
а также AsnycTask
часто используются для распространения событий / сообщений между пользовательским интерфейсом (потоком) и рабочим потоком или для задержки действий. Таким образом, они больше связаны с пользовательским интерфейсом.
Looper
обрабатывает задачи (Runnables, Futures) в очереди, связанной с потоком, в фоновом режиме - даже без взаимодействия с пользователем или отображаемого пользовательского интерфейса (приложение загружает файл в фоновом режиме во время вызова).
Я постараюсь как можно проще объяснить назначение класса лупера. В обычном потоке Java, когда метод run завершает выполнение, мы говорим, что поток выполнил свою работу, и после этого поток больше не живет. что, если мы хотим выполнять больше задач в нашей программе с тем же потоком, который больше не живет? О, теперь проблема, не так ли? Да, потому что мы хотим выполнять больше задач, но поток больше не работает. Здесь нам на помощь приходит Looper.Looper, как следует из названия, предполагает петли.Looper - это не что иное, как бесконечный цикл внутри вашего потока. Таким образом, он поддерживает поток в течение бесконечного времени, пока мы явно не вызовем метод quit(). Вызов метода quit() в бесконечно живом потоке сделает условие ложным в бесконечном цикле внутри потока, таким образом, бесконечный цикл завершится. так,нить умрет или больше не будет в живых. И очень важно вызвать метод quit() в нашем потоке, к которому подключен лупер, иначе они будут в вашей системе, как зомби. Так, например, если мы хотим создать фоновый поток, чтобы выполнять над ним несколько задач. мы создадим простой поток Java и будем использовать класс Looper для подготовки цикла и присоединения подготовленного цикла к этому потоку, чтобы наш поток мог жить так долго, как мы хотим, потому что мы всегда можем вызвать quit() в любое время, когда мы хотим завершить наша ветка. Таким образом, наш петлитель будет поддерживать наш поток в рабочем состоянии, поэтому мы сможем выполнять несколько задач с одним и тем же потоком, и когда мы закончим, мы вызовем quit() для завершения потока.Что, если мы хотим, чтобы наш основной поток или поток пользовательского интерфейса отображал результаты, вычисленные фоновым потоком или потоком, отличным от пользовательского интерфейса, для некоторых элементов пользовательского интерфейса? для этой цели используется концепция обработчиков; через обработчики мы можем осуществлять межпроцессное взаимодействие или, скажем, через обработчики два потока могут взаимодействовать друг с другом. Таким образом, основной поток будет иметь связанный обработчик, а фоновый поток будет связываться с основным потоком через этот обработчик, чтобы выполнить задачу по отображению результатов, вычисленных им в некоторых элементах пользовательского интерфейса в основном потоке. Я знаю, что объясняю здесь только теорию, но постараюсь понять концепцию, потому что глубокое понимание концепции очень важно. И я публикую ссылку ниже, которая приведет вас к небольшой серии видео о Looper,Handler и HandlerThread, и я настоятельно рекомендую посмотреть его, и все эти концепции будут прояснены там с примерами.
Официальный способ использования Looper описан в документации и не использует устаревший Handler CTOR, не имеющий параметров:
https://developer.android.com/reference/android/os/Looper
class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler(Looper.myLooper()) {
public void handleMessage(Message msg) {
// process incoming messages here
}
};
Looper.loop();
}
}
Вот улучшенная версия этого в Kotlin, которую я сделал:
open class LooperThread : Thread() {
@Volatile
var handler: Handler? = null
private set
@WorkerThread
override fun run() {
Looper.prepare()
handler = object : Handler(Looper.myLooper()!!) {
fun handleMessage(msg: Message?) {
this@LooperThread.handleMessage(msg!!)
}
}
Looper.loop()
}
@WorkerThread
open fun handleMessage(msg: Message) {
}
/**stops handling messages for the looper, so it can be released
* @param quitSafely whether to use Looper.quitSafely (wait for normal messages) or Looper.quit (ditch all pending messages):
* https://developer.android.com/reference/android/os/Looper#quitSafely()
* https://developer.android.com/reference/android/os/Looper#quit()*/
fun quit(quitSafely: Boolean) {
handler?.let { handler ->
if (quitSafely)
handler.looper.quitSafely()
else handler.looper.quit()
this@LooperThread.handler = null
}
}
}
Я пытаюсь привести пример на Котлине. Вот пример кода.
Во-первых, нам нужно создать экземпляр обработчика переменных из Handler(предоставленный цикл вместо стандартного), который запрашивает основной поток (Looper.getMainLooper()).
Функция getAllCourses() должна возвращать LiveData, поэтому мы используем handler.postDelayed() для добавления в очередь сообщений и запуска через x количество миллисекунд, указанное в константе SERVICE_LATENCY_IN_MILLIS.
Пожалуйста, не стесняйтесь уточнить мои пояснения, чтобы было больше ясности.
class RemoteDataSource private constructor(private val jsonHelper: JsonHelper) {
private val handler = Handler(Looper.getMainLooper())
companion object {
private const val SERVICE_LATENCY_IN_MILLIS: Long = 2000
@Volatile
private var instance: RemoteDataSource? = null
fun getInstance(helper: JsonHelper): RemoteDataSource =
instance ?: synchronized(this) {
RemoteDataSource(helper).apply { instance = this }
}
}
fun getAllCourses(): LiveData<ApiResponse<List<CourseResponse>>> {
EspressoIdlingResource.increment()
val resultCourse = MutableLiveData<ApiResponse<List<CourseResponse>>>()
handler.postDelayed({
resultCourse.value = ApiResponse.success(jsonHelper.loadCourses())
EspressoIdlingResource.decrement()
}, SERVICE_LATENCY_IN_MILLIS)
return resultCourse
}