В статье собраны несколько заметок и шаблонов кода для разработки приложений на Android с использованием Android SDK.

Статья не претендует на полноценный разжеванный туториал но тем не менее я постарался описать кратко основные понятия.

Осторожно, статья содержит много англоязычного сленга, например Интент (Intent), Вью (View), Лейяут (Layout), Евент (Event), etc. Так удобней писать и надеюсь что читателю будет удобней привыкать к правильным понятиям читая документацию в оригинале.

 

Интенты

Интент - сообщение к системе Андроид на запрос запуска активности.

Активность - класс связанный с шаблоном (.xml), описывающим интерфейс. Обычно активность - это один экран приложения со своими элементами управления (тут каждый из элементов называют вью - View). В приложении может быть несколько активностей.

Интент может быть явный (explicit) - запрос на запуск активности в своем приложении по указанному класу активности, либо неявный (implicit) - на запуск активности из других приложений по указанному Mime-типу.

Создать явный интент и передать через него параметр.

Intent intent = new Intent(this, ReceiveMessageActivity.class);
intent.putExtra("name", переменная с параметром)

Тип параметра может быть разный благодаря перегрузке реализованной putExtra в классе Intent.

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

Intent i = getIntent();
String str = i.getStringExtra("name");

Создать неявный интент, например отправки сообщения комуто:

Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, msgText.getText().toString());
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
msgText.setText("Activities not found");
}

Все экшены смотреть в документации https://developer.android.com/guide/components/intents-common.html.

В случае если есть несколько активностей которые подходят по тайпу андроид предложет диалог выбора и возможность установки дефолтного приложения. Что бы убрать возможность установки дефолтной активити (если активностей нет, то будет выведено сообщение, ловить ActivityNotFoundException как в прошлом случае не надо):

Intent chosenIntent = Intent.createChooser(intent, "Send message...");
startActivity(chosenIntent);

Описание фильтра неявных интентов которые может принимать активность приложения выполняется в манифесте (это файл AndroidManifest.xml в проекте приложения).

<intent-filter>
  <action android:name="android.intent.action.SEND"/>
  <category android:name="android.intent.category.DEFAULT"/>
  <data android:mimeType="text/plain"/>
</intent-filter>

Фильтр типа может использовать *, например text/*.

При запуске интента Андроид проверяет запущен ли процесс приложения если нет то запускает его и активность в нем, иначе он запускает активность в существующем процессе. Когда юзер запускает приложение также выполняется startActivity(intent).

Жизненный цикл активности

Евенты жизненного цикла можно получать переопределив определенные методы активности:

  1. При запуске активности (загрузке класса в файл), выполняется onCreate(). Активность еще не видна. В этом методе обычно обязательно есть вызов назначения лейяута setContentView(R.layout...), по этому onCreate переопределен всегда.
  2. onStart() вызывается непосредственно перед тем как активность собирается стать видимой
  3. Когда активность перестает быть видимой пользователю (сворачивается) вызывается onStop(). После выполнения метода onStop() активность уже не видна на экране.
  4. Если активность снова становится видимой пользователю и она уже запущена, вызывается onRestart(), за которым сразу следует вызов onStart()
  5. Если активность просто теряет фокус - не скрывается полностью а видна на заднем плане то она приостонавливается. Это можно определить переопределив методы методами onPause() и onResume()
  6. При уничтожении вызывается onDestroy(), а перед ним обычно onStop() но программист не должен расчитывать на этот onStop() при нехватке памяти в системе.

Еще раз, обобщим какие события есть:

  • Create - создание активности если она не была еще загружена в память
  • Start -  показывание активности
  • Resume - Получение фокуса
  • Pause - Потеря фокуса
  • Stop - Потеря видимости
  • Restart - показывается но после того как была спрятана. То есть после запуска активности (после onCreate) не выполняется.
  • Destroy - полное завершение активности.

Исходя из такой трактовки понятно что при первоначальном запуске выполняется сразу и Create (Создание) и Start (Показывание) и Resume (Получение фокуса). Если вас интерисует последовательность как эти методы выполнятся друг относительно дурга то она именно такая: Create Start Resume, и такая последовательность логична ведь фокус может получить только уже показанная активность а показаться уже созданная. Когда активность например скрывается то последовательность перевернутая - сначала она теряет фокус Pause, потом скрывается Stop

Важно!

Переопределяя методы нужно вызывать метод супер-класса:

@Override
protected void onStop() {
    super.onStop();
   ...код

Активность перезапускается при повороте?

Поворот устройства для ОС выглядит как изменение конфигурации системы (на ряду, например с локалью системы либо разрешением экрана), а при любом изменении конфигурации андроид перезапускает все активности.

1. Игнор

Чтобы проигнорировать перезапуск своей активности в AndroidManifest.xml можно настроить:

<activity
android:name="..."
android:configChanges="orientation|screenSize" >

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

public void onConfigurationChanged(Configuration config) {
} 

2. Разрешить только одну ориентацию

В манифесте:

<activity android:name=".."
android:screenOrientation="portrait">

3. Сохранение стейта

Самый правильный подход и он же нужен вообще по нормальному что бы обрабатывать любые другие изменения конфигураций устройства - это сохранить стейт вашего приложения переопределив метод:

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
   savedInstanceState.putInt("seconds", seconds);
}

А потом восстановить стейт при создании:

if (savedInstanceState != null) {
    seconds = savedInstanceState.getInt("seconds");
}

Лейяуты

android:layout_width и android:width а также android:...height могут принимать значения:

  1. match_parent (устаревшее fill_parent), вписывается в родителя
  2. wrap_content - минимальное значение что бы завенуть все внутренности
  3. px, pt, dp, mm, etc , см Единици измерения в низу

Также можно сразу указать ширину элемента в символах (Самых широких, например Ш) android:ems="10".

Еще пару свойств: 

  • android:layout_gravity="Right"  // выравнивание вью в нерелетив леяут, fill заполняет (в т.ч. спаненые гриды)
  • android:layout_weight="1" // вес
  • android:hint="Message" // текст подсказки в элементе (плейсхолдер)
  • android:gravity="bottom" // выравнивание текста в элементе

Работа с вью в коде

Получить текст с поля ввода

EditText msgText = (EditText) findViewById(R.id.message);
msgText.getText().toString()

Среагировать на клик по ToggleButton, Switch, CheckBox

public void onClicked(View view) {
// Получить состояние двухпозиционной кнопки.
boolean on = ((ХХХ) view).isChecked();

Где ххх - класс (Switch, ToggleButton, etc)

Выбрать элемент в раскрывающимся списке:

Spinner spinner = (Spinner) findViewById(R.id.spinner);
String string = String.valueOf(spinner.getSelectedItem());

Один обработчик для нескольких элементов

Можно нескольким вью назначить один хендлер, а в нем проверить айди:

public void onCheckboxClicked(View view) {
    switch(view.getId()) {
       case R.id.checkbox_milk:

Аналогично можно искать выделенный RadioButton в RadioGroup:

<RadioGroup android:id="@+id/radio_group">
<RadioButton android:id="@+id/radio_cavemen"
  android:onClick="onRadioButtonClicked" />
<RadioButton android:id="@+id/radio_astronauts"
  android:onClick="onRadioButtonClicked" />

Так:

public void onRadioButtonClicked(View view) {
    RadioGroup radioGroup = findViewById(R.id.radioGroup);
    int id = radioGroup.getCheckedRadioButtonId();
    switch(id) {
        case R.id.radio_cavemen:

Управляемый ListView

Что бы иметь возможность в коде задавать и менять итемы в ListView нужно сделать адаптер который связывает лист с листвью:

List<String> devices;
ArrayAdapter<String> itemsAdapter;

devices = new ArrayList<String>(); itemsAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, devices); ListView dev_list = (ListView) findViewById(R.id.dev_list); dev_list.setAdapter(itemsAdapter);

Обновление:

devices.add("Some Item");
itemsAdapter.notifyDataSetChanged();

ListView с кастомными итемами

Тут сначала нужнос сделать отдельно .xml шаблон для итема (как он будет выглядить). А потом переопределить адаптер, например можно пронаследовать его от ArrayAdapter:

public class DevListAdapter extends ArrayAdapter<BluetoothDevice> {

    private ConnectCallBack cCallBack;
    public DevListAdapter(Context context, ArrayList<BluetoothDevice> devices, ConnectCallBack ccb) {
        super(context, 0, devices);
        cCallBack = ccb;
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // Get the data item for this position
        BluetoothDevice dvc = getItem(position);
            
// Check if an existing view is being reused, otherwise inflate the view if (convertView == null) { convertView = LayoutInflater.from(getContext()).inflate(R.layout.device_list_item, parent, false); }
// Lookup view for data population TextView tvName = (TextView) convertView.findViewById(R.id.tvName); TextView tvMac = (TextView) convertView.findViewById(R.id.tvMac); Button btn = (Button) convertView.findViewById(R.id.btnConn); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { cCallBack.connect(getItem(position)); } });
// Populate the data into the template view using the data object tvName.setText(dvc.getName()); tvMac.setText(dvc.getAddress()); // Return the completed view to render on screen return convertView; } }

Тост

Тост - небольшое сообщение без кнопок которое всплывает поверх активности ненадолго а потом исчезает само.

Toast toast = Toast.makeText(this, text, Toast.LENGTH_LONG);
toast.show();

Там где this - это класс активности (контекст), такчто если вызывается не из метода класса активности то учитывайте это.

Службы в Android

Бывают запускаемые (живут до конца не зависимо от жизни активности) и связанные (живут пока есть связь с активностью).

Запускаемые обычно наследуют IntentService, связанные Service.

public class DelayedMessageService extends IntentService {
  public DelayedMessageService() {
    super("DelayedMessageService");
  }
  
  @Override
  protected void onHandleIntent(Intent intent) {
    //... выполняется в отдельном потоке, пока не выполнится
  }

@Override
onStartCommand() {
// ... выполняется перед запуском в основном потоке
}
}
<service
  android:name=".DelayedMessageService"
  android:exported="false" // use only by this app>
</service>

Запуск (Если запустить несколько раз то служба будет запускаться - одна после завершения другой.)

Intent intent = new Intent(this, DelayedMessageService.class);
startService(intent)

Работа с интерфейсом из службы

С интерфейсом можно работать только из основного потока. Для этого нужно создать Handler в основном потоке.

private Handler handler;
...
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    handler = new Handler();
    return super.onStartCommand(intent, flags, startId);
}

Потом из доп потока (из метода onHandleIntent) можно, например создать Toast: 

handler.post(new Runnable() {
  @Override
  public void run() {
    Toast.makeText(getApplicationContext(), text, Toast.LENGTH_LONG).show(); // на фоне любого! 
// активного сейчас приложения } });

Уведомление и запуск активности

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

Intent intent = new Intent(this, MainActivity.class);  // обычный явный интент

TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); 
stackBuilder.addParentStack(MainActivity.class);  // добавляем стек возврата что бы кнопка Назад работала
stackBuilder.addNextIntent(intent);

PendingIntent pendingIntent =
stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); // затем нужно достать этот интент

и только потом указать в нотификейшен наш pendingIntent через setContentIntent.

Notification notification = new Notification.Builder(this)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(getString(R.string.app_name))
.setContentText(text)
.setAutoCancel(true)  // убрать при щелчке
.setPriority(Notification.PRIORITY_MAX) // показать и на экране и в панеле
.setDefaults(Notification.DEFAULT_VIBRATE)  // вибрация

.setContentIntent(pendingIntent)
.build();

Запуск уведомления (можно из фонового потока)

public static final int NOTIFICATION_ID = 5453; // если айди двух уведомлений одинаковое то второе заменит первое 
...
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFICATION_ID, notification)

Связанная служба

Служба:

public class OdometerService extends Service {
  private final IBinder binder = new OdometerBinder();

public class OdometerBinder extends Binder {
OdometerService getOdometer() {
return OdometerService.this;
}
}

@Override public IBinder onBind(Intent intent) { return binder; } }

Обнаружение связаннсоти:

public class MainActivity extends Activity {
  private OdometerService odometer;
  private boolean bound = false;
  ...
  private ServiceConnection connection = new ServiceConnection() {
  
    @Override
    public void onServiceConnected(ComponentName componentName, IBinder binder) {
      OdometerService.OdometerBinder odometerBinder =
(OdometerService.OdometerBinder) binder;
      odometer = odometerBinder.getOdometer();
      bound = true;
    }

    @Override
    public void onServiceDisconnected(ComponentName componentName) {
      bound = false;
    }
  };
}

Связывание:

Intent intent = new Intent(this, OdometerService.class);
bindService(intent, connection, Context.BIND_AUTO_CREATE);

Отвязывание:

if (bound) {
  unbindService(connection);
  bound = false;
}

 

Единици измерения элеметов GUI в Android

  • px - Pixels - пиксели. Однозначно нельзя сказать сколько физических единиц (мм, дюймов, etc) займет один пиксель потому что разные экраны имеют разную плотность пикселей (dpi, Dots per inch). Только зная ее можно сказать сколько пикселей займут дюйм, либо узнать размер пикселя.
  • in - Inches - Дюймы, физический размер в имперской системе - 1 Inch = 25.4 mm. Физический размер означает то что нарисовав кнопку на экране размером дюйм на дюйм мы можем измерить ее реальной линейкой по верх экрана и она действительно будет дюйм на дюйм. Конечно сам андроид на самом низком уровне может рисовать только пикселями поэтому когда он встречает in или другие физические единици он сначала сам расчитывает число пиксилей умножая на известный ему dpi экрана. 
  • mm - Милиметры, физический размер только уже в метрической системе
  • pt - Points - 1/72 часть дюйма, соответственно также это физический размер.
  • dp (компилятор также понимает dip) Density-independent Pixels - абстрактная единица пикселя который не завист от плотности пикселей на дюйм в физических размерах. 1 dp - это физический размер размер пикселя который был бы нарисован на экране с плотностью 160 dpi, соответственно 160 dp это 1 дюйм (25.4 мм) на экране с 160 dpi.
  • sp - Scale-independent Pixels - так же как и dp сам масштабируется в зависимости от плотности экрана но по мимо этого еще масштабируется исходя из настроек размера шрифта юзера. Если масштаб шрифта нейтральный (1) то 1 sp == 1 dp

Cтоит отметить что андроид каждой реальной плотности сопостовляет значение из дискретного ряда плотностей (называют их Bucket-ами):

Density Bucket Screen Density
ldpi 120 dpi
mdpi 160 dpi
hdpi 240 dpi
xhdpi 320 dpi
xxhdpi 480 dpi
xxxhdpi 640 dpi  

 Это удобно как опреационной системе (легче пересчитывать размеры), так и разработчику приложения. Дело в том что разработчику приходится предоставлять разные изображения для каждой денсити.