Как рассказать об основных компонентах Android за 15 минут

Введение


В этой статье пойдет речь о том, как рассказать человеку, ранее не знакомому с программированием под Android, об основных его компонентах. Заинтересовать и показать, что все не так сложно, как многие думают. При этом сделать это за 15 минут и не уходя в объяснение какой-то базовой теории, которую каждый может прочитать сам и вернуться уже с уточняющими вопросами.


Когда я попробовал сделать это первый раз, был неприятно удивлен собой. Мое "простое и понятное" объяснение превратилось в занудство, в рамках которого четко прослеживалась отчаянная попытка объять необъятное и рассказать в двух словах обо всем понемногу. Нужно ли говорить, что такой рассказ скорее не заинтересует, а напугает Вашего собеседника, попутно уменьшив желание сделать что-то свое, даже если раньше в планах был небольшой калькулятор.


Не секрет, что в Интернете размещено огромное количество статей на эту тему, но в моем случае повествование будет немного отличаться: здесь будет только наглядная практика, без определений и прочих деталей. То есть смотрим — видим — комментируем происходящее. Смотрится, на мой взгляд, все достаточно просто и наглядно, куски кода получились тоже небольшие и очень простые, готовые к быстрому использованию в собственном проекте. Мне кажется, такой подход дает достаточно широкую обзорную картину классических инструментов Android, и при написании первого приложения вместо вопросов "что мне использовать" будут более конкретные вопросы "как именно мне использовать компонент Х". А уже все подробности об этом человек сможет узнать сам — если захочет.


Итак, поехали!


Изучаем компоненты


Устанавливаем приложение, запускаем его, и… пока достаточно того, что перед нами открылось MainActivity. На вопрос "почему именно оно" ответ будет дан позднее.


Первым делом рассмотрим, откуда оно берется — из main_activity.xml, где объявлены все элементы интерфейса. Размещены они в LinearLayout, поэтому вопросы здесь вряд ли возникнут.


Посмотреть код
<LinearLayout
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
    android:id="@+id/textView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Android Demo Application" />

<TextView
    android:id="@+id/textView3"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Часть 1" />

<Button
    android:id="@+id/buttonShowToast"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Показать Toast" />
...
</LinearLayout>

Простые компоненты


Toast


Посмотреть картинку


Теперь перейдем в MainActivity.java и первой кнопке его интерфейса — "Показать Toast" (всплывающее уведомление).
Находим идентификатор кнопки в main_activity.xml и переходим к ее OnClickListener в MainActivity.java.


Посмотреть код
Button btn = findViewById(R.id.buttonShowToast);
btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(getApplicationContext(), "This is a Toast", Toast.LENGTH_LONG).show();
        }
});

Оказывается, чтобы вывести всплывающее уведомление, достаточно одной строки кода! Здорово, не правда ли?


Взаимодействие с другим Activity


Посмотреть картинку

Первая Activity

Вторая Activity


Теперь попробуем перейти куда-нибудь за пределы главной страницы приложения. Например, на другую такую страницу! Переходим во "Взаимодействие с другим Activity" — и мы попадаем в другую активность с другими элементами управления. Как разные активности в одном приложении общаются между собой? Здесь самое время рассказать про постоянное хранилище значений — shared_prefs, а также показать механизм startActivityForResult / onActivityResult (кратко — если из открытой активности запустить новую активность при помощи startActivityForResult, то по завершении второй активности будет вызван onActivityResult в первой активности. Не пугайтесь, если это пока не понятно).
И, конечно же, продемонстрировать на практике!


Запись в shared_prefs:


Посмотреть код
String valueToSave = "test";
SharedPreferences.Editor editor = getSharedPreferences("demoapp", MODE_PRIVATE).edit();
editor.putString("myValue", valueToSave);
editor.apply();

Чтение из shared_prefs:


Посмотреть код
SharedPreferences prefs = getSharedPreferences("demoapp", MODE_PRIVATE);
String storedValue = prefs.getString("myValue", "NOT_FOUND");

Пример с onActivityResult — см. в исходниках приложения.


Сохранение и восстановление настроек


Посмотреть картинку


Раз мы упомянули про shared_prefs, давайте с ними и закончим. Переходим в "Сохранение и восстановление настроек", где перед нами открывается типичная карточка типичного аккаунта с различными типами полей (отметим, что их тип задается всего лишь одной переменной — переключателем). Содержимое этих полей мы и будем сохранять в shared_prefs, а затем восстанавливать их. Пока отдельной кнопкой, никаких onResume — до них мы еще не добрались!
Сохраняем:


Посмотреть код
SharedPreferences.Editor editor = getSharedPreferences(MY_PREFS_NAME, MODE_PRIVATE).edit();
EditText et = findViewById(R.id.editTextName);
String name = et.getText().toString();
editor.putString("name", name);
editor.apply();

Восстанавливаем:


Посмотреть код
SharedPreferences prefs = getSharedPreferences(MY_PREFS_NAME, MODE_PRIVATE);
EditText et = findViewById(R.id.editTextName);
et.setText(prefs.getString("name", ""));
...

Простое меню


Посмотреть картинку


Следующий раздел — простое меню. Из него мы узнаем, что ничего сложного в нем нет — достаточно задать onCreateOptionsMenu и наполнить его структурой из my_menu.xml.


Посмотреть код
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.my_menu, menu);
    return true;
}

Всплывающее меню


Сразу за ним — всплывающее по кнопке меню. Оно уже интереснее предыдущего хотя бы наличием памяти настроек и наличием подменю.
Код всплывающего меню можно посмотреть в исходниках приложения.


Аудиоплеер


Посмотреть картинку


На примере Аудиоплеера, помимо того, что ничего сложного в проигрывании музыки нет, можно продемонстрировать привязку seekBar к чему-либо, в данном случае — к текущей позиции медиаплеера. В регулировке громкости — тоже ничего сверхъестественного. Бонусом показана утечка ресурсов. Откроем активити с медиаплеером, запустим воспроизведение и нажмем кнопку "Назад". Музыка продолжает играть, и ее уже не остановить… Проблема! Как ее решить — узнаем чуть позже.
Код аудиоплеера можно посмотреть в исходниках приложения.


Веб-браузер


Ну и в заключение первой части покажем, что в веб-браузере тоже ничего божественного нет.


Посмотреть картинку


Посмотреть код
WebView view = findViewById(R.id.webView);
view.setWebViewClient(new WebViewClient());
view.getSettings().setJavaScriptEnabled(true);
view.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
view.loadUrl("https://google.com");

Следующая часть уже сложнее.


Сервисы и уведомления


BroadcastReceiver


Посмотреть картинку


Вспомним, как мы передавали результат из одной активности в другую. Там каким-то образом (мы пока не знаем, каким) получилось, что при закрытии второго активити результат прилетел в onActivityResult первого. А можем ли мы так передать результат куда угодно в пределах нашего приложения? Да, можем объявить и зарегистрировать слушателя, который услышит нас из любой точки программы. Такой слушатель называется BroadcastReceiver'ом.


Посмотреть код
public BroadcastReceiver MyReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
            Toast.makeText(getApplicationContext(), "Broadcast receiver: received!", Toast.LENGTH_LONG).show();
    }
};

Здесь уже не обойтись без интентов, но на самом примитивном уровне: пока нам достаточно факта, что они есть, отправляются в некую общую шину, и что по заранее заданному action'y BroadcastReceiver услышит нас, где бы мы ни находились.


Посмотреть код
IntentFilter filter = new IntentFilter();
filter.addAction("MyCustomActionName");
registerReceiver(MyReceiver, filter);

sendBroadcast(new Intent("MyCustomActionName"));

На данный момент имеем базовое представление, что такое Receiver, но зачем он нужен, не понятно: это нормально, и сейчас станет легче.


Простой сервис


Посмотреть картинку


Плавно переходим к сервису, где BroadcastReceiver и найдет свое полноценное применение. И вместо попытки в двух словах рассказать, что такое Android-сервис, я предлагаю сразу приступить к демонстрации. Запустим сервис, который начинает свою работу в фоновом режиме. Вместе с этим зарегистрируем два Receiver'a: один в Activity и один в Service.
Receiver в Service нужен для демонстрации того, что, несмотря на фоновое выполнение, мы всегда сможем достучаться до него из Activity.


Посмотреть код
public BroadcastReceiver MyServiceReceiver = new BroadcastReceiver() {
@Override
 public void onReceive(Context context, Intent intent) {
        Toast.makeText(getApplicationContext(), "Toast from Service: I hear you!", Toast.LENGTH_LONG).show();
    }
};

Receiver в Activity нужен для вывода результатов работы сервиса в TextView.


Посмотреть код
public BroadcastReceiver MyPrintReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
        Toast.makeText(getApplicationContext(), "pong", Toast.LENGTH_SHORT).show();

        TextView tv = findViewById(R.id.textViewSimpleServiceStatus);
        String msg = intent.getStringExtra("msg");
        tv.setText(msg);
    }
};

И, для полного понимания, при отправке сообщения из сервиса в Activity будем выводить Toast("ping"). А при получении сообщения в Activity и фактической отрисовке значения в TextView будем выводить Toast("pong").
Пусть основная задача сервиса — отправлять такие "пинги" в активность, а задача активности — просто их отображать в своем интерфейсе.


Посмотреть код
Handler handler = new Handler();
runnable = new Runnable() {
    public void run() {
        if (running) {
            printMsg("Service is still running " + running);
            handler.postDelayed(runnable, 5000);  // создает цикл
        } else {
            printMsg("Service exited");
        }
    }
};
handler.postDelayed(runnable, 5000);

Пример работы handler мы детально рассмотрим позже, на данный момент это лишь средство отправки ping каждые 5 секунд.


И вот теперь, после запуска сервиса, мы видим Toast("Service created!") и пошли ping-pong уведомления. А в TextView начали поступать сообщения из сервиса.


Отлично, фоновое выполнение мы увидели. А теперь закроем приложение! На последних версиях Андроид мы увидим следующее: сервис перезапустился (появится Toast("Service created!")) и пошли "пинги". При этом "понгов" нет — ведь больше нет активити, которое их обрабатывает! Спустя несколько секунд на моем смартфоне прекращаются и пинги. Сервис уничтожается. Открываем настройки энергопотребления, отключаем оптимизацию для нашего приложения и проделываем процедуру заново. Теперь сервис не уничтожается, и даже после закрытия программы мы видим стабильно поступающие "пинги". Но, понятное дело, здесь никакой гарантии нет, и такой сервис проживет очень не долго. С точки зрения разработчика это может быть ужасно, но давайте посмотрим на это глазами простого пользователя: хотим ли мы, чтобы любое приложение могло вот так свободно и безнаказанно работать в фоне, поедая аккумулятор? Вряд ли. Как же тогда полноценно работать в фоне?


Для этого нужно лишь уведомлять об этом пользователя. Это обязательное требование Android, который, при наличии канала уведомлений, позволяет запустить уже не обычный, а Foreground-сервис, который будет полноценно работать в фоне без риска быть убитым на ровном месте.
Добавим в onCreate нашего сервиса:


Посмотреть код
String CHANNEL_ID = "my_channel_01";
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
        "Channel human readable title",
        NotificationManager.IMPORTANCE_DEFAULT);
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);

Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
        .setContentTitle("")
        .setContentText("").build();
startForeground(1, notification);

И будем запускать его командой:


Посмотреть код
startForegroundService(new Intent(getApplicationContext(), TestServiceForeground.class));

Вернемся в наше демо-приложение, вернем настройки энергосбережения в исходное состояние. И запустим теперь уже Foreground сервис. Закрывая приложение при запущенном сервисе, обратим внимание, что сервис уже не перезапускается и не уничтожается, а просто стабильно продолжает работать в фоне, отправляя каждые 5 секунд свои "пинги". При этом в уведомлениях будет висеть его значок.


Посмотреть картинку


Пользователь сможет скрыть эти уведомления, если захочет, но скрыть их программно в момент создания канала уведомлений нельзя.


Плавающая кнопка (Overlay)


Посмотреть картинку


Зная, что такое сервис, можно перейти к наиболее простому и очевидному его применению — работе с плавающими кнопками. На актуальных Андроидах нельзя просто так объявить права на отрисовку Overlays — нужно явно запросить у пользователя разрешение "Поверх всех окон". После чего нажимаем Draw Overlay и видим плавающую иконку, за которой на самом деле стоит сервис, слушающий нажатия на нее.


Отправка уведомлений


Посмотреть картинку


Еще одна важная возможность, которая наверняка пригодится в большинстве Андроид-приложений, это отправка уведомлений пользователю. Рассмотрим кратко, как это работает. Для начала (на актуальных Андроидах) нам требуется создать канал уведомлений — да-да, один из тех, которые имеются как правило в большом количестве у современных приложений.


Посмотреть код
private void createNotificationChannel() {
try {
        CharSequence channelName = CHANNEL_ID;
        String channelDesc = "channelDesc";
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            int importance = NotificationManager.IMPORTANCE_LOW;
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID, channelName, importance);
            channel.setDescription(channelDesc);
            NotificationManager notificationManager = getSystemService(NotificationManager.class);
            assert notificationManager != null;
            NotificationChannel currChannel = notificationManager.getNotificationChannel(CHANNEL_ID);
            if (currChannel == null) {
                notificationManager.createNotificationChannel(channel);
                Toast.makeText(getApplicationContext(), "channel created", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(getApplicationContext(), "channel exists", Toast.LENGTH_SHORT).show();
            }
        }
    } catch (Exception e) {
    }
}

Ну а затем можно без ограничений отправлять в него уведомления.


Посмотреть код
public void setNotify() {
    try {
        Intent snoozeIntent = new Intent("ActionFromNotify");
        PendingIntent snoozePendingIntent =
                PendingIntent.getBroadcast(this, 0, snoozeIntent, 0);

        String title = "Start";
        Intent intent = new Intent(this, MainActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);

        NotificationCompat.Action action = new NotificationCompat.Action.Builder(R.drawable.ic_launcher_background, title, snoozePendingIntent).build();
        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_launcher_background)
                .setContentTitle("MyNotification")
                .setContentText("Text here")
                .setPriority(NotificationCompat.PRIORITY_LOW)
                .setContentIntent(null)
                .setOngoing(true)  // нельзя смахнуть
                .setSound(null)
                .addAction(action);
        NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
        int notificationId = (int) (System.currentTimeMillis() / 4);
        notificationManager.notify(notificationId, mBuilder.build());

    } catch (Exception e) {
        Log.e("error", e.toString());
    }
}

Сервис с отправкой уведомлений


Посмотреть картинку


Отправка уведомлений прямо из активити, согласитесь, не очень впечатляет. Мы все же привыкли к другому. Но что мы пока знаем про фоновое выполнение? Только то, что там есть сервисы. Так не будем же спешить и сделаем простой сервис (даже не Foreground — для простоты), отправляющий новое уведомление каждые 5 секунд. Согласитесь, это уже симпатичнее и больше впечатляет, чем просто отправка уведомлений по кнопке.


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


Дополнительные компоненты


Таблица с данными


Посмотреть картинку


Начнем нашу паузу с таблицы с данными. Изучение исходного кода оставим на усмотрение читателю.


Окно с вкладками


Посмотреть картинку


Следующая остановка — окно с вкладками. При помощи нехитрого TabView можно разместить несколько активити на одном экране.


Посмотреть код
public class TabsActivity extends TabActivity {

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

    // получаем TabHost
    TabHost tabHost = getTabHost();

    TabHost.TabSpec tabSpec;

    tabSpec = tabHost.newTabSpec("tag1");
    tabSpec.setIndicator("Один");
    tabSpec.setContent(new Intent(this, SaveRestorePrefsActivity.class));
    tabHost.addTab(tabSpec);

    tabSpec = tabHost.newTabSpec("tag2");
    tabSpec.setIndicator("Два");
    tabSpec.setContent(new Intent(this, FloatingMenuActivity.class));
    tabHost.addTab(tabSpec);
...
    }
}

Вывод объектов-структур: Fragment и таблица


Вывод объектов-структур можно сделать фрагментами и таблицей.
Вот так выглядит заполнение фрагментами


Посмотреть картинку


А вот так таблицей


Посмотреть картинку


Жизненный цикл Activity


Посмотреть картинку


Плавно завершая нашу паузу, переходим к жизненному циклу активити. Здесь самое время узнать, что кроме onCreate существует ряд других методов. Небольшой кусок кода и всплывающие уведомления поначалу помогут понять их лучше каких-либо объяснений.


Посмотреть код
@Override
protected void onResume() {
    super.onResume();
    Toast.makeText(getApplicationContext(), "onResume - активити на переднем плане", Toast.LENGTH_SHORT).show();
}

@Override
protected void onDestroy() {
    super.onDestroy();
    Toast.makeText(getApplicationContext(), "onDestroy - уничтожение активити", Toast.LENGTH_SHORT).show();
}

@Override
protected void onPause() {
    super.onPause();
    Toast.makeText(getApplicationContext(), "onPause - активити больше не на переднем плане", Toast.LENGTH_SHORT).show();
}

Это же касается и OnTouchListener'ов, и onTextChanged.


Посмотреть код
CheckBox cb = findViewById(R.id.checkBoxChangeExample);
cb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            printMsg("CheckBox - OnCheckedChangeListener: new value is checked = " + isChecked);
        }
    });

    SeekBar seekBar = (SeekBar) findViewById(R.id.seekBarChangeExample);
    seekBar.setMax(100);
    seekBar.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            Integer progress = ((SeekBar)v).getProgress();
           printMsg("SeekBar - OnTouchListener: new value is = " + progress.toString());
            return false;
        }
    });

Отложенное, параллельное и регулярное выполнение


Переходим к наиболее сложной части повествования — отложенному и параллельному выполнению.


Отложенное выполнение: Handler


Посмотреть картинку


Начнем погружение с Handler. Что такое handler во всех деталях — человек позже прочитает сам, не будем лишать его этого удовольствия. Но, так как мы его рассматриваем, важно знать основное — что он позволяет выполнить отложенную задачу, причем делает это не параллельно.
Покажем это. Создадим handler, добавим в него задачу "вывести Toast через 5 секунд". Видим, что Toast был выведен и никаких sleep'ов (приостановок выполнения всей программы) не потребовалось.


Посмотреть код
Handler handler = new Handler();
Runnable r = new Runnable() {
public void run() {
    Toast.makeText(getApplicationContext(), "Delayed task executed", Toast.LENGTH_SHORT).show();
                }
            };
handler.postDelayed(r, 5000);

Теперь добавим в handler циклическую задачу:


Посмотреть код
Runnable r = new Runnable() {
     public void run() {
         Toast.makeText(getApplicationContext(), "Delayed task executed", Toast.LENGTH_SHORT).show();
          handler.postDelayed(this, 5000);  // создает цикл
            }
        };
handler.postDelayed(r, 5000);

Убедившись, что она выполняется каждые 5 секунд, очищаем Handler


Посмотреть код
handler.removeCallbacksAndMessages(null);

Осталось продемонстрировать, что выполнение задач из Handler происходит не параллельно. Проще всего показать это, нагрузив его чем-то тяжелым и одновременно простым. Таким, как… while(true) без sleep! Секунд на десять, чтобы не убивать совсем приложение.


Посмотреть код
Runnable r = new Runnable() {
public void run() {
    long initTime = System.currentTimeMillis();
    boolean timeElapsed = false;
    while(!timeElapsed){
        if(System.currentTimeMillis() - initTime > 10000 ){
            timeElapsed = true;  // очень ресурсоемкая функция, так делать нельзя! (тут сделано специально, чтобы съесть все ресурсы). В обычных приложениях в бесконечных циклах обязательно добавляют sleep
                    }
                }
            }
        };
Toast.makeText(getApplicationContext(), "Hard Delayed task started", Toast.LENGTH_SHORT).show();
handler.postDelayed(r, 100);

Запуская такую задачу, видим, что приложение эти 10 секунд не реагирует на наши нажатия — оно целиком и полностью занято обработкой нашего сложного цикла. Второй вывод, который следует из этого примера — нельзя запускать ресурсоемкие задачи в одном потоке с интерфейсом. Поток UI всегда должен быть свободен, а его функции отрабатывать максимально быстро. На часть операций в UI-потоке наложен явный запрет: например, Андроид аварийно завершит приложение, если оно в UI-потоке попробует обратиться к Интернет.


Параллельное выполнение: поток


Посмотреть картинку


Логичный теперь вопрос — а как создавать новые потоки и работать параллельно?
Покажем и создание потока, и факт параллельности его работы. Создадим поток и нагрузим его той же задачей, из-за которой в случае с habdler'ом мы остались с неработающим интерфейсом на 10 секунд.


Посмотреть код
Thread thread = new Thread()
    {
        @Override
        public void run() {
            try {
                sendMsgUsingBroadcast("Thread started");
                long initTime = System.currentTimeMillis();
                boolean timeElapsed = false;
                while(!timeElapsed){
                    if(System.currentTimeMillis() - initTime > 10000 ){
                        timeElapsed = true;
                    }
                }
                sendMsgUsingBroadcast("Thread stopped"); 
            } catch (Exception e) {
                sendMsgUsingBroadcast("Thread error " + e.toString()); 
            }
        }
    };
    thread.start();

Создали, нагрузили — а интерфейс работает! Выполнение потока действительно происходит параллельно.
Следующий вопрос — как управлять потоком. Важно понимать, что поток будет завершен, только когда весь его исходный код выполнится. Нельзя просто так взять и убить поток. Самое время вспомнить про shared_prefs и применить переменную для сихронизации: пусть вместе со стартом потока будет задана переменная running=true. В каждой своей итерации поток проверяет, выполняется ли running==true, и если нет, завершает свое выполнение.


Посмотреть код
Thread thread = new Thread()
    {
        @Override
        public void run() {
            try {
                sendMsgUsingBroadcast("Thread started");  
                SharedPreferences prefs = getSharedPreferences(MY_PREFS_NAME, MODE_PRIVATE);
                while (true) {
                    if (!prefs.getBoolean("running", false)) {
                        break;
                    } else {
                        try {
                            Thread.sleep(100);
                        } catch (Exception e) {}
                    }
                }
                sendMsgUsingBroadcast("Thread stopped"); 

            } catch (Exception e) {
                sendMsgUsingBroadcast("Thread error " + e.toString()); 
            }
        }
    };
    thread.start();

Итак, мы продвинулись еще на несколько шагов дальше и теперь знаем не только про сервис, но и про Handler и поток. Казалось бы, этих инструментов достаточно, чтобы начать писать приложение, имеющее одной из функций регулярные нотификации пользователя. Но у них есть одна особенность: всех их объединяет тот факт, что мы ни на секунду не теряем управление, и всегда вынуждены находиться в каком-то бесконечном цикле, который большую часть времени спит и изредка просыпается проверить выполнение пары условий, чтобы понять — вывести пользователю уведомление или снова идти спать на длительный срок. Сюда добавляется головная боль: а что, если наш сервис или приложение убьет оптимизатор батареи, а зачем мы отъедаем столько ресурсов смартфона ради маленькой не особо-то и нужной свистелки? Согласитесь, было бы гораздо удобнее, если бы сама система раз в указанное время вызывала наш обработчик и передавала ему управление, мы по-быстрому проводили пару своих проверок, отправляли при необходимости уведомление пользователю и завершались до следующего вызова по системному таймеру?


Повторяющееся выполнение: AlarmManager


Такая возможность есть и называется она AlarmManager.


Посмотреть картинку


И все, что он делает — вызывает sendBroadcast с Action, который мы слушаем в своем заранее объявленном BroadcastReceiver!


Напишем BroadcastReceiver, который, при получении управления, будет выводить уведомление пользователю:


Посмотреть код
public class MyAlarmServiceReceiver extends BroadcastReceiver {
private String CHANNEL_ID = "MyNotificationsChannel";

@Override
public void onReceive(Context context, Intent intent) {
    Toast.makeText(context, "MyAlarmServiceReceiver onReceive", Toast.LENGTH_SHORT).show();
    setNotify("Notify from AlarmManager", context);
    }
}

Попросим AlarmManager вызывать этот Receiver каждые 15 минут:


Посмотреть код
AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
Intent intent = new Intent(AlarmActivity.this, MyAlarmServiceReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(),
                    0,
                    intent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, 1);
calendar.set(Calendar.MINUTE, 10);
//alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, 1 * 60 * 1000, pendingIntent);  // not repeating - just one run, if needed
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),     AlarmManager.INTERVAL_FIFTEEN_MINUTES, pendingIntent);

Результат не заставит себя долго ждать: каждые 15 минут мы будет получать соответствующие уведомления.


Последним пунктом (просто чтобы не заканчивать на сложном) я вынес логирование.


Логирование


Посмотреть картинку


Открываем Logcat Reader и видим логи, отправляемые в него нашим приложением.
А при необработанном исключении (здесь для примера приводится деление на ноль) видим причину, по которой произошло падение приложения.


Посмотреть код
String LOG_TAG = "MYDEMOAPP";
Log.i(LOG_TAG, "Test info log");
Log.d(LOG_TAG, "Test debug log");
Log.e(LOG_TAG, "Test error log");

Самое время закончить рассказ манифестом приложения, откуда становится понятно, почему при запуске программы открывается именно MainActivity, а также посмотреть список разрешений, использованных в приложении.


Исходный код


Исходный код доступен по ссылке


Заключение


На этом завершается первая часть повествования, в рамках которой мы приходим к интуитивному пониманию основных компонентов Android. Дальнейший путь к написанию своего первого приложения уже гораздо более долгий, но такая наглядная демонстрация, на мой взгляд, значительно повышает веру в собственные силы: ничего сложного здесь не написано, но при этом все работает. Ну и, разумеется, из исходного кода теперь уже знакомой демонстрационной программы можно забирать какие-то кусочки и пробовать собрать свой первый Android-проект.

Источник: habr.ru