Основные компоненты приложения.

Context

  • Context – это объект, который предоставляет доступ к базовым функциям приложения
  • доступ к ресурсам, к файловой системе, вызов активности и т.д.
  • Activity является подклассом Context
  • в коде мы можем использовать её как ИмяАктивности.this (напр. MainActivity.this)
  • Доступ к контексту можно получить разными способами: getApplicationContext(), getContext(), getBaseContext()

Context

  • Context имеет свои методы, позволяющие получать доступ к ресурсам и другим объектам: getAssets(), getResources(), getPackageManager(), getString(), getSharedPrefsFile().
  • getAssets(): приложение может иметь ресурсы в папке assets вашего проекта
  • getResources: получить доступ к ресурсу цвета используется конструкция getResources().getColor(), которая может получить доступ к данным из файла res/colors.xml.

Context

  • Ключевое слово this не работает в анонимных классах
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Toast.makeText(MainActivity.this, "Мяу", Toast.LENGTH_SHORT).show();
    }
});
				    

ContextCompact

  • В библиотеки совместимости появился свой класс для контекста ContextCompat.
  • Может вам пригодиться, когда студия вдруг подчеркнёт метод в старом проекте и объявит его устаревшим.
// Старый метод
context.getResources().getColor(R.color.some_color_resource_id);

// Студия ругается, что нужно использовать новый вариант getColor(int, Theme)
public void onClick(View view) {
    int color = getResources().getColor(R.color.colorPrimary); // ругается
    Button button = (Button) findViewById(R.id.button);
    button.setTextColor(color);
}

// Теперь не ругается
int color = ContextCompat.getColor(this, R.color.colorPrimary);

				    

Activity

Объявление activity в AndroidManifest

<activity
    android:name="com.example.app.ChildActivity"
    android:label="@string/title_child_activity"
    <!-- Parent activity meta-data to support API level 4+ -->
    <meta-data
        android:name="android.support.PARENT_ACTIVITY"
	android:value="com.example.app.MainActivity" />
    <intent-filter>
                
                
    </intent-filter>
</activity>
				

Объявление activity в AndroidManifest

  • name - java или kotlin класс, который описывает activity
  • label - название на верхней панели.
  • intent-filter - события, на которые будет реагировать экран.
  • множество других параметров (подробнее).

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

Жизненный цикл activity.

  • Набор методов управленияе activity
  • жёстко контролируется системой и зависит от нужд пользователя, доступных ресурсов и т. д.
  • Решение о запуске приложения принимает система.
  • Хотя последнее слово и остаётся за системой, она подчиняется определённым заданным и логическим правилам, позволяющим определить, можно ли загрузить, приостановить приложение или прекратить его работу.
  • Если в данный момент пользователь работает с определённым окном, система даёт приоритет соответствующему приложению.
  • И наоборот, если окно невидимо и система решает, что работу приложения необходимо остановить, чтобы мобилизовать дополнительные ресурсы, будет прекращена работа приложения, имеющего более низкий приоритет.

Жизненный цикл activity. onCreate

  • вызывается при создании экземпляра класса activity.
  • устанавливается разметка activity setContentView()
  • Внутри данного метода настраивают статический интерфейс активности.
  • после вызова метода activity переходит в состояние Created.
  • Инициализирует статические данные активности, связывают данные со списками и т.д.
  • Связывает с необходимыми данными и ресурсами. Задаёт внешний вид через метод setContentView().

Жизненный цикл activity. onCreate

  • принимает объект Bundle, содержащий состояние пользовательского интерфейса, сохранённое в последнем вызове обработчика onSaveInstanceState.
  • Операции по инициализации, занимающие много времени, следует выполнять в фоновом процессе, а не с помощью метода onCreate().
  • В противном случае можно получить диалоговое окно ANR (Application Not Responding, приложение не отвечает).
  • В методе можно сделать проверку, запущено ли приложение впервые или восстановлено из памяти.
  • Если значение переменной savedInstanceState будет null, приложение запускается первый раз:

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

if ( savedInstanceState == null )   // приложение запущено впервые
{
   currentBillTotal = 0.0;    // инициализация суммы счета нулем
   // другой код
} 
else // приложение восстановлено из памяти
{
    // инициализация суммы счета сохранённой в памяти суммой
    currentBillTotal = savedInstanceState.getDouble(BILL_TOTAL);
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    outState.putDouble(BILL_TOTAL, currentBillTotal);
}

				    

Жизненный цикл activity. onStart

  • За onCreate() всегда следует вызов onStart()
  • перед onStart() не обязательно должен идти onCreate(), так как onStart() может вызываться и для возобновления работы приостановленного приложения (приложение останавливается методом onStop())
  • При вызове onStart() окно ещё не видно пользователю, но вскоре будет видно.
  • Вызывается непосредственно перед тем, как активность становится видимой пользователю.
  • Сопровождается вызовом метода onResume(), если активность получает передний план, или вызовом метода onStop(), если становится скрытой.

Жизненный цикл activity. onResume

  • Метод onResume() вызывается после onStart(), даже когда окно работает в приоритетном режиме и пользователь может его наблюдать.
  • В этот момент пользователь взаимодействует с созданным вами окном.
  • Приложение получает монопольные ресурсы.
  • Запускает воспроизведение анимации, аудио и видео. Также может вызываться после onPause().
  • система вызывает этот метод каждый раз, когда ваша активность идёт на переднем плане, в том числе, при первом создании.
  • Таким образом, вы должны реализовать onResume() для инициализации компонентов, регистрации любых широковещательных приёмников или других процессов, которые вы освободили/приостановили в onPause()
  • выполнять любые другие инициализации, которые должны происходить, когда активность вновь активна.

Жизненный цикл activity. onResume

  • не нужно перезагружать состояние пользовательского интерфейса внутри него, так как эти функции возложены на обработчики onCreate() и onRestoreInstanceState.
  • Пытайтесь размещать относительно быстрый и легковесный код, чтобы ваше приложение оставалось отзывчивым при скрытии с экрана или выходе на передний план.
  • Например, после метода onPause(), в котором мы приостановили работу камеры (см. ниже) снова запускаем камеру:
@Override
public void onResume() {
    super.onResume();

    // Get the Camera instance as the activity achieves full user focus
    if (mCamera == null) {
        initializeCamera(); // Local method to handle camera init
    }
}
				    

Жизненный цикл activity. onPause

  • Когда пользователь решает перейти к работе с новым окном, система вызовет для прерываемого окна метод onPause().
  • По сути происходит свёртывание активности.
  • Деактивирует и выпускает монопольные ресурсы.
  • Останавливает воспроизведение видео, аудио и анимацию.
  • В этом методе необходимо остановить анимацию и другие действия, которые загружают процессор. Зафиксировать несохранённые данные, например, черновик письма, потому как после его выполнения работа активности может прерваться без предупреждения. Освободить системные ресурсы, например, обработку данных от GPS.
  • Исходя из архитектуры своего приложения, вы также можете приостановить выполнение потоков, процессов или широковещательных приёмников, пока активность не появится на переднем плане.

Жизненный цикл activity. onPause

  • Когда активность приостановлена, то все компоненты сохраняются в памяти и при возобновления нет необходимости повторно инициализировать их.
  • Например, при работе с камерой метод используется следующим образом:
@Override
public void onPause() {
    super.onPause();

    // Release the Camera because we don't need it when paused
    // and other activities might need to use it.
    if (mCamera != null) {
        mCamera.release()
        mCamera = null;
    }
}
				    

Жизненный цикл activity. onStop

  • Метод onStop() вызывается, когда окно становится невидимым для пользователя.
  • Это может произойти при её уничтожении, или если была запущена другая активность (существующая или новая), перекрывшая окно текущей активности.
  • Когда ваша активность останавливается, объекты активности хранятся в памяти и восстанавливаются, когда активность возобновляет свою работу.
  • Даже если система закрыла вашу активность, когда она была остановлена, она по-прежнему сохраняет состояние объектов, таких как текст в EditText в специальном объекте Bundle (в виде ключ-значение) и восстанавливает их, если пользователь переходит обратно к тому же экземпляру активности.
  • При нехватке памяти система может уничтожить скрытую активность, минуя метод onStop() с вызовом метода onDestroy().

Жизненный цикл activity. onRestart

  • Если окно возвращается в приоритетный режим после вызова onStop(), то в этом случае вызывается метод onRestart().
  • Т.е. вызывается после того, как активность была остановлена и снова была запущена пользователем.
  • Всегда сопровождается вызовом метода onStart().
  • onRestart предшествует вызовам метода onStart() (кроме самого первого).
  • Используйте его для специальных действий, которые должны выполняться только при повторном запуске активности в рамках «полноценного» состояния.

Жизненный цикл activity. onDestroy

  • Метод вызывается по окончании работы активности, при вызове метода finish()
  • Или в случае, когда система уничтожает этот экземпляр активности для освобождения ресурсов.
  • Эти два сценария уничтожения можно определить вызовом метода isFinishing().
  • Вызывается перед уничтожением активности.
  • Это последний запрос, который получает активность от системы.
  • Если определённое окно находится в верхней позиции в стеке, но невидимо пользователю и система решает завершить это окно, вызывается метод onDestroy().

Последовательности вызовов

  • Запуск приложения: onCreate() → onStart() → onResume()
  • Нажимаем кнопку Назад для выхода из приложения: onPause() → onStop() → onDestroy()
  • Нажата кнопка Домой: onPause() → onStop()
  • После нажатия кнопки Домой, когда приложение запущено из списка недавно открытых приложений или через значок onRestart() → onStart() → onResume()
  • Когда запускается другое приложение из области уведомлений или открывается приложение Настройки onPause() → onStop()
  • Нажата кнопка Назад в другом приложении или в Настройках и ваше приложение стало снова видимым. onRestart() → onStart() → onResume()
  • Открывается диалоговое окно onPause()

Последовательности вызовов

  • Диалоговое окно закрывается onResume()
  • Кто-то звонит на телефон onPause() → onResume()
  • Пользователь отвечает на звонок onPause()
  • Разговор окончен onResume()
  • Экран телефона гаснет: onPause() → onStop()
  • Экран снова включён onRestart() → onStart() → onResume()

Порядок вызовов

  • После onCreate() - onStart()
  • После onRestart() - onStart()
  • После onStart() - onResume() или onStop()
  • После onResume() - onPause()
  • После onPause() - onResume() или onStop()
  • После onStop() - onRestart() или onDestroy()
  • После onDestroy() - ничего

Настройки, диалоговые окна.

SharedPreferences

  • SharedPreferences - компонент хранения настроек приложения
  • Каталог хранения: /data/data/имя_пакета/shared_prefs/имя_файла_настроек.xml
  • getPreferences() — внутри Activity, чтобы обратиться к определённому для Activity предпочтению;
  • getSharedPreferences() — внутри Activity, чтобы обратиться к предпочтению на уровне приложения;
  • getDefaultSharedPreferences() — из объекта PreferencesManager, чтобы получить общедоступную настройку, предоставляемую Android.

Получение настроек

  • getBoolean(String key, boolean defValue)
  • getFloat(String key, float defValue)
  • getInt(String key, int defValue)
  • getLong(String key, long defValue)
  • getString(String key, String defValue)
  • тип double не поддерживается.

SharedPreferences: получение объекта настроек

// это будет именем файла настроек
public static final String APP_PREFERENCES = "mysettings"; 

SharedPreferences mySharedPreferences = 
	getSharedPreferences(APP_PREFERENCES, Context.MODE_PRIVATE);
				    

SharedPreferences: флаги

  • MODE_PRIVATE - только приложение имеет доступ к настройкам.
  • MODE_APPEND - присоединяет новые настройки к существующим
  • MODE_ENABLE_WRITE_AHEAD_LOGGING - включена запись с опережением записи.
  • MODE_MULTI_PROCESS - проверяет изменение настроек, даже если экземпляр sharedpreference уже был загружен
  • MODE_WORLD_READABLE - позволяет другим приложениям читать настройки
  • MODE_WORLD_WRITEABLE - позволяет другим приложениям записывать новые настройки

SharedPreferences: файл

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="0005">|000000|000000|000000|0</string>
<string name="0004">|000000|000000|000000|1</string>
<string name="0006">|000000|000000|000000|0</string>
</map>				    
				    

SharedPreferences: пример

// это будет именем файла настроек
public static final String APP_PREFERENCES = "mysettings"; 

public static final String APP_PREFERENCES_NAME = "Nickname";
public static final String APP_PREFERENCES_AGE = "Age";

SharedPreferences mSettings;
mSettings = getSharedPreferences(APP_PREFERENCES, Context.MODE_PRIVATE);

EditText editNickname = findViewById(R.id.editNickname);
String strNickName = editNickname.getText().toString(); // здесь содержится текст, введенный в текстовом поле
Editor editor = mSettings.edit();
editor.putString(APP_PREFERENCES_NAME, strNickName);
editor.apply();
				    

SharedPreferences: сохранение

  • Чтобы внести изменения в настройки (редактировать), нужно использовать класс SharedPreferences.Editor.
  • Получить объект Editor можно через вызов метода edit объекта SharedPreferences, который вы хотите изменить.
  • После того, как вы внесли все необходимые изменения, вызовите метод commit() или apply() объекта Editor
  • Метод apply() появился в API 9 и работает в асинхронном режиме, что является более предпочтительным вариантом.
  • Метод commit() приходится использовать для старых версий и кроме того, он возвращает значение true в успешном случае и false в случае ошибки.

SharedPreferences: дополнительные методы

  • Для очистки значений используйте методы SharedPreferences.Editor.remove(String key)
  • Очистить все значения: SharedPreferences.Editor.clear().
  • Можно получить ассоциативный массив со всеми ключами и значениями через метод getAll()
  • Проверить наличие конкретного ключа с помощью метода contains().
  • Начиная с API 11, у класса SharedPreferences появился новый метод getStringSet()
  • У класса SharedPreferences.Editor родственный ему метод putStringSet().

SharedPreferences: Методы getStringSet() и putStringSet()

SharedPreferences sp;
String catnames;

// записываем имена котов в файл настроек
public void onPutSettings(View v){
	Set catnames = new HashSet();
	catnames.add("Мурзик");
	catnames.add("Барсик");
	catnames.add("Рыжик");
	Editor e = sp.edit();
	e.putStringSet("strSetKey", catnames);
	e.apply();
}

// считываем имена котов обратно
public void onShowSettings(View v)
{
 	Set ret = sp.getStringSet("strSetKey", new HashSet());
	for(String r : ret) {
	    Log.i("Share", "Имя кота: " + r);
	}
}				    
				    

getDefaultSharedPreferences()

  • getSharedPreferences() - необходимо придумывание имени файла для хранения настроек.
  • Этот способ придаёт гибкости в том случае, когда вам нужно создать несколько отдельных файлов.
  • Но если вам нужен один файл, то можно ничего не придумывать, а использовать метод getDefaultSharedPreferences() из объекта PreferencesManager.
  • Система сама сгенерирует имя файла из имени вашего пакета с добавлением слова _preferences.

getDefaultSharedPreferences()

public static SharedPreferences getDefaultSharedPreferences(Context context) {
    return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
        getDefaultSharedPreferencesMode());
}

private static String getDefaultSharedPreferencesName(Context context) {
    return context.getPackageName() + "_preferences";
}

private static int getDefaultSharedPreferencesMode() {
    return Context.MODE_PRIVATE;
}				    
				    

Диалоговые окна

  • Dialog - базовый класс для всех типов диалоговых окон;
  • AlertDialog — диалоговое окно с кнопками, списком, флажками или переключателями;
  • CharacterPickerDialog - диалоговое окно, позволяющее выбрать символ с ударением, связанный с базовым символом;
  • ProgressDiaiog — диалоговое окно с индикатором прогресса при помощи компонента ProgressBar. В API 26 признан устаревшим.
  • DatePickerDialog — диалоговое окно выбора даты с элементом DatePicker
  • TimePickerDialog — диалоговое окно выбора времени с элементом TimePicker

Класс Dialog

  • Класс Dialog является базовым для всех классов диалоговых окон.
  • ProgressDialog, TimePickerDialog И DatePickerDialog — расширение класса AlertDialog, они также могут иметь командные кнопки.
  • Каждое диалоговое окно должно быть определено внутри активности или фрагмента
  • Диалоговое окно можно открыть один раз или несколько раз.
  • Для отображения диалогового окна необходимо вызвать метод showDialog()

Класс Dialog

  • Метод dismissDialog() прячет диалоговое окно (но не удаляет), не отображая его на экране.
  • Окно остаётся в пуле диалоговых окон данной активности.
  • При повторном отображении при помощи метода showDialog() будет использована кэшированная версия окна.
  • Метод removeDialog() удаляет диалоговое окно из пула окон данной активности.
  • При повторном вызове метода showDialog() диалоговое окно придётся создавать снова.

Dialog: пример

Dialog dialog;

@Override
public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);

	dialog = new Dialog(MainActivity.this);

	// Установите заголовок
	dialog.setTitle("Заголовок диалога");
	// Передайте ссылку на разметку
	dialog.setContentView(R.layout.dialog_view);
	// Найдите элемент TextView внутри вашей разметки
	// и установите ему соответствующий текст
	TextView text = (TextView) dialog.findViewById(R.id.dialogTextView);
	text.setText("Текст в диалоговом окне. Вы любите котов?");
}

public void onClick(View v)
{
    // Выводим диалоговое окно на экран
    dialog.show();
}				    
				    

Методы onCreateDialog() и onPrepareDialog()

  • Метод onCreateDialog() вызывается один раз при создании окна.
  • После начального создания при каждом вызове метода showDialog() будет срабатывать обработчик onPrepareDialog().
  • Переопределив этот метод, вы можете изменять диалоговое окно при каждом его выводе на экран.
  • Это позволит привнести контекст в любое из отображаемых значений.
  • Если требуется перед каждым вызовом диалогового окна изменять его свойства (например, текстовое сообщение или количество кнопок), то можно реализовать внутри этого метода.

Методы onCreateDialog() и onPrepareDialog()

@Override
public void onPrepareDialog(int id, Dialog dialog) {
  switch(id) {
    case (TIME_DIALOG) :
	SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        Date currentTime = new Date(java.lang.System.currentTimeMillis());
        String dateString = sdf.format(currentTime);
        AlertDialog timeDialog = (AlertDialog)dialog;
        timeDialog.setMessage(dateString);
        break;
    }
}				    
				    

Фрагменты

  • Два способа использования фрагментов
  • Первый способ основан на замещении родительского контейнера.
  • Создаётся стандартная разметка и в том месте, где будут использоваться фрагменты, размещается контейнер, например, FrameLayout.
  • В коде контейнер замещается фрагментом.
  • При использовании подобного сценария в разметке не используется тег fragment, так как его нельзя менять динамически.
  • Также придётся обновлять ActionBar, если он зависит от фрагмента.

Фрагменты

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

Фрагменты: классы

  • Сами фрагменты наследуются от androidx.fragment.app.Fragment.
  • Существует подклассы фрагментов: ListFragment, DialogFragment, PreferenceFragment, WebViewFragment и др.
  • Для взаимодействия между фрагментами используется класс android.app.FragmentManager - специальный менеджер по фрагментам.
  • Для транзакций (добавление, удаление, замена) используется класс-помощник android.app.FragmentTransaction.
  • В 2018 году Гугл объявила фрагменты из пакета androd.app устаревшими.
  • Заменяйте везде на версию из библиотеки совместимости. В 2020 году уже используют пакет androidx.fragment.app.

Фрагменты

  • У каждого фрагмента должен быть свой класс.
  • Класс наследуется от класса Fragment или схожих классов
  • Создаются различные методы типа onCreate() и др.
  • Если фрагмент имеет разметку, то используется метод onCreateView() - считайте его аналогом метода setContentView()
  • При этом метод onCreateView() возвращает объект View, который является корневым элементом разметки фрагмента.

Фрагменты

public class FirstFragment extends Fragment implements OnClickListener {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.first_fragment,
                container, false);

        Button nextButton = (Button) view.findViewById(R.id.button_first);
        nextButton.setOnClickListener(this);

        return view;
    }

    // ...
}				    
				    

Фрагменты

  • Перед методом findViewById() используется view
  • Этот метод относится к компоненту, а не к активности, как обычно делается в программах, когда просто опускали имя активности.
  • ищем ссылку на кнопку не среди разметки активности, а внутри разметки самого фрагмента.
  • в методе inflate() последний параметр должен иметь значение false в большинстве случаев.

FragmentManager

  • Класс FragmentManager имеет два метода, позволяющих найти фрагмент, который связан с активностью:
  • findFragmentById(int id) - Находит фрагмент по идентификатору
  • findFragmentByTag(String tag) - Находит фрагмент по заданному тегу

Методы транзакции (FragmentTransaction)

  • add() - Добавляет фрагмент к активности
  • remove() - Удаляет фрагмент из активности
  • replace() - Заменяет один фрагмент на другой
  • hide() - Прячет фрагмент (делает невидимым на экране)
  • show() - Выводит скрытый фрагмент на экран
  • detach() (API 13) - Отсоединяет фрагмент от графического интерфейса, но экземпляр класса сохраняется
  • attach() (API 13) - Присоединяет фрагмент, который был отсоединён методом detach()

Методы транзакции (FragmentTransaction)

  • Методы remove(), replace(), detach(), attach() не применимы к статичным фрагментам.
  • Перед началом транзакции нужно получить экземпляр FragmentTransaction через метод FragmentManager.beginTransaction().
  • В конце любой транзакции, которая может состоять из цепочки вышеперечисленных методов, следует вызвать метод commit().
FragmentManager fragmentManager = getFragmentManager()
fragmentManager.beginTransaction()
    .remove(fragment1)
    .add(R.id.fragment_container, fragment2)
    .show(fragment3)
    .hide(fragment4)
    .commit();				    
				    

Адаптеры

  • Адаптеры упрощают связывание данных с элементом управления.
  • Адаптеры используются при работе с виджетами, которые дополняют android.widget.AdapterView: ListView, ExpandableListView, GridView, Spinner, Gallery
  • AdapterView дополняет android.widget.ViewGroup
  • Назначение адаптера заключается в том, чтобы предоставлять дочерние view для контейнера.
  • Адаптер берет данные и метаданные определенного контейнера и строит каждый дочерний вид.
  • Например, мы формируем пункты списка (массив строк) и передаём его списку ListView.

Готовые адаптеры

  • ArrayAdapter - предназначен для работы с ListView. Данные представлены в виде массива, которые размещаются в отдельных элементах TextView
  • ListAdapter - адаптер между ListView и данными. Строго говоря, это класс-интерфейс, который можно использовать и в ArrayAdapter и в SimpleAdapter и т.д.
  • SpinnerAdapter - адаптер для связки данных с элементом Spinner. Это тоже интерфейс, как ListAdapter и работает по схожему принципу
  • SimpleAdapter - адаптер, позволяющий заполнить данными список более сложной структуры, например, два текста в одной строке списка.

Пример ArrayAdapter

String[] countries = {"Россия", "США", "Германия", "Бразилия"};

ArrayAdapter adapter = new ArrayAdapter<>(
    this,
    android.R.layout.simple_list_item_1,
    countries
);

ListView listView = findViewById(R.id.listView);
listView.setAdapter(adapter);				    
				    

Готовые адаптеры

  • SimpleCursorAdapter - дополняет ResourceCursorAdapter и создаёт компоненты TextView/ImageView из столбцов, содержащихся в курсоре. Компоненты определяются в ресурсах
  • CursorAdapter - предназначен для работы с ListView, предоставляет данные для списка через курсор, который должен иметь колонку с именем "_id"
  • ResourceCursorAdapter - этот адаптер дополняет CursorAdapter и может создавать виды из ресурсов
  • HeaderViewListAdapter - расширенный вариант ListAdapter, когда ListView имеет заголовки.
  • WrapperListAdapter - еще один адаптер для списков.

BaseAdapter

  • Если вам нужен свой собственный адаптер, то в Android есть абстрактный класс BaseAdapter, который можно расширить.
  • Собственный адаптер необходим в тех случаях, когда требуется специальное управление данными или дополнительный контроль над отображением дочерних представлений.
  • Кроме того, вы можете предусмотреть в своём адаптере элементы кэширования для повышения производительности работы.
  • У BaseAdapter есть несколько методов, которые следует переопределить. Например, метод getCount() позволяет узнать количество выводимых объектов.

Пример BaseAdapter

public class MyAdapter extends BaseAdapter {
    private List items;
    private LayoutInflater inflater;
    
    public MyAdapter(Context context, List items) {
        this.items = items;
        this.inflater = LayoutInflater.from(context);
    }
    
    @Override
    public int getCount() {
        return items.size();
    }
    
    @Override
    public Object getItem(int position) {
        return items.get(position);
    }
    
    @Override
    public long getItemId(int position) {
        return position;
    }
    
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null) {
            convertView = inflater.inflate(R.layout.my_item_layout, parent, false);
            holder = new ViewHolder();
            holder.title = convertView.findViewById(R.id.title);
            holder.icon = convertView.findViewById(R.id.icon);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        
        MyItem item = items.get(position);
        holder.title.setText(item.getTitle());
        holder.icon.setImageResource(item.getIconRes());
        
        return convertView;
    }
    
    private static class ViewHolder {
        TextView title;
        ImageView icon;
    }
}
				    

Ключевые методы адаптеров

  • getCount() - возвращает общее количество элементов
  • getItem(int position) - возвращает объект данных для позиции
  • getItemId(int position) - возвращает уникальный ID для позиции
  • getView(int position, View convertView, ViewGroup parent) - создаёт/переиспользует view для элемента
  • getViewTypeCount() и getItemViewType(int position) - для разных типов элементов в одном списке

ViewHolder

  • ViewHolder — это паттерн оптимизации, который значительно ускоряет работу адаптеров в Android, особенно в ListView, GridView и RecyclerView.
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View view = convertView;
    if (view == null) {
        view = LayoutInflater.from(context).inflate(R.layout.list_item, parent, false);
    }
    
    TextView title = view.findViewById(R.id.title);
    TextView subtitle = view.findViewById(R.id.subtitle);
    ImageView icon = view.findViewById(R.id.icon);
    
    MyItem item = items.get(position);
    title.setText(item.getTitle());
    subtitle.setText(item.getSubtitle());
    icon.setImageResource(item.getIconRes());
    
    return view;
}				    
				    

ViewHolder

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;
    
    if (convertView == null) {
        convertView = LayoutInflater.from(context).inflate(R.layout.list_item, parent, false);
        holder = new ViewHolder();
        
        holder.title = convertView.findViewById(R.id.title);
        holder.subtitle = convertView.findViewById(R.id.subtitle);
        holder.icon = convertView.findViewById(R.id.icon);
        
        convertView.setTag(holder);
    } else {
        holder = (ViewHolder) convertView.getTag();
    }
    
    MyItem item = items.get(position);
    holder.title.setText(item.getTitle());
    holder.subtitle.setText(item.getSubtitle());
    holder.icon.setImageResource(item.getIconRes());
    
    return convertView;
}
				    

Адаптер. Работа с данными и уведомлениями

  • После изменения данных нужно уведомить адаптер
  • adapter.notifyDataSetChanged(); // Полное обновление (медленно)
  • adapter.notifyItemChanged(position); // Точечное обновление (быстро)
  • adapter.notifyItemInserted(position);
  • adapter.notifyItemRemoved(position);

Разметка

  • Компоновка (также используются термины разметка или макет) хранится в виде XML-файла в папке /res/layout.
  • компоновка – это некий визуальный шаблон для пользовательского интерфейса вашего приложения, который позволяет управлять элементами управления, их свойствами и расположением.
  • Каждый файл разметки должен содержать только один корневой элемент компоновки, который должен быть объектом View или ViewGroup.
  • Внутри корневого элемента вы можете добавлять дополнительные объекты разметки или виджеты как дочерние элементы.

Виды разметок

  • FrameLayout
  • LinearLayout
  • TableLayout
  • RelativeLayout
  • GridLayout
  • SwipeRefreshLayout
  • ConstraintLayout
  • CoordinatorLayout

FrameLayout

  • Самая простая разметка.
  • Принцип работы: Все дочерние элементы размещаются в левом верхнем углу, друг на друге. Порядок отрисовки определяется порядком в XML: последний элемент в коде будет отображаться поверх предыдущих.
  • Простота и эффективность. Подходит для отображения одного элемента или для наложения элементов (например, иконка уведомления на кнопке, прогресс-бар поверх контента).
  • Для отображения одного виджета (например, TextView или ImageView).
  • Когда вам нужно наложить один элемент на другой.
  • Как контейнер для фрагментов (часто используется в FragmentTransaction).

LinearLayout

  • Разметка, выстраивающая элементы в линию.
  • Элементы располагаются последовательно, в одну строку или в один столбец.
  • Направление задается атрибутом android:orientation (horizontal или vertical).Простота и линейность. Легко управлять весом (android:layout_weight) элементов, чтобы они занимали определенную долю доступного пространства.
  • Для простых форм, где поля ввода идут друг за другом вертикально.
  • Для создания строки или колонки с кнопками.
  • Когда нужно равномерно распределить пространство между несколькими элементами с помощью layout_weight.
  • Неэффективность при вложенности нескольких LinearLayout для создания сложных интерфейсов (возникает проблема избыточной меры - "measure pass").

TableLayout

  • Организует дочерние элементы в строки и столбцы. Каждая строка определяется тегом .
  • Количество столбцов определяется строкой с наибольшим количеством ячеек.
  • Семантическое представление табличных данных.
  • Для отображения данных в табличной форме (например, расписание, список свойств "ключ-значение").
  • Негибкость для создания сложных, адаптивных интерфейсов. В современной разработке используется редко, чаще заменяется на GridView или RecyclerView для сложных таблиц или на ConstraintLayout для выравнивания.

RelativeLayout

  • Разметка, позиционирующая элементы относительно друг друга или родителя.
  • Позиция каждого дочернего элемента задается правилами (rules), например, layout_toLeftOf, layout_alignParentTop, layout_centerInParent.
  • Гибкость без глубокой вложенности. Позволяет создавать более сложные интерфейсы, чем LinearLayout, избегая избыточного количества ViewGroup.
  • Для интерфейсов средней сложности, где элементы логически связаны друг с другом (например, метка слева от поля ввода, кнопка "Отправить" выровнена по правому краю родителя).
  • Производительность может страдать при очень большом количестве правил и зависимостей, такой макет сложнее в отладке и поддержке. Во многом вытеснен более производительным ConstraintLayout.

GridLayout

  • Разметка, организующая элементы в сетку.
  • Позволяет размещать элементы в ячейках заранее определенной сетки. Вы задаете количество строк и столбцов. Элемент может занимать несколько ячеек (объединять их), используя layout_rowSpan и layout_columnSpan.
  • Более мощная и гибкая альтернатива TableLayout. Не требует использования .
  • Для создания интерфейсов, которые по своей сути являются сеткой (например, калькулятор, клавиатура, главный экран с иконками приложений).
  • Менее распространен и интуитивно понятен, чем LinearLayout или RelativeLayout.

ConstraintLayout

  • Современная, гибкая и высокопроизводительная разметка. Рекомендуемая к использованию.
  • Позиционирует элементы на основе ограничений (constraints). Каждая сторона элемента (верх, низ, лево, право) может быть "привязана" к другой стороне другого элемента или к границе родителя. Также поддерживает направляющие (Guidelines) и барьеры (Barriers).
  • Плоская иерархия (минимальная вложенность), что положительно сказывается на производительности. Огромная гибкость для создания адаптивных интерфейсов, которые хорошо выглядят на экранах разных размеров.
  • Практически для любого макета. Это текущий стандарт де-факто в разработке под Android.
  • Когда нужно избежать глубокой вложенности LinearLayout и RelativeLayout.

ConstraintLayout

  • Каждая сторона элемента (верх, низ, лево, право) и базовые линии могут быть привязаны к:
    • Другим элементам
    • Границам родителя
    • Направляющим (Guidelines)
    • Барьерам (Barriers)
<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintEnd_toStartOf="@+id/other_button"
    app:layout_constraintBottom_toBottomOf="parent" />
				    

ConstraintLayout

  • Типы размеров
    • Fixed: конкретный размер в dp
    • Wrap Content: под размер содержимого
    • Match Constraints (0dp): растягивается согласно ограничениям
<View
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent" />
				    

ConstraintLayout. Цепи (Chains)

  • Группируют элементы горизонтально или вертикально, позволяя контролировать как они распределяют пространство.
  • Spread - элементы равномерно распределены (по умолчанию)
  • Spread Inside - первый и последний элемент прижаты к краям, остальные распределены
  • Packed - элементы сгруппированы вместе
  • Weighted - с весовыми коэффициентами

ConstraintLayout. Направляющие (Guidelines)

  • Вертикальные или горизонтальные линии, невидимые в runtime, к которым можно привязывать элементы.
<androidx.constraintlayout.widget.Guideline
    android:id="@+id/guideline"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    app:layout_constraintGuide_percent="0.3" />

<Button
    app:layout_constraintStart_toStartOf="@+id/guideline"
    ... />
				    

ConstraintLayout. Барьеры (Barriers)

  • "Умные" направляющие, которые перемещаются в зависимости от размеров нескольких элементов.
<Button android:id="@+id/button1" ... />
<Button android:id="@+id/button2" ... />

<androidx.constraintlayout.widget.Barrier
    android:id="@+id/barrier"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:barrierDirection="end"
    app:constraint_referenced_ids="button1,button2" />

<TextView
    app:layout_constraintStart_toEndOf="@+id/barrier"
    ... />				    
				    

ConstraintLayout

  • Используйте 0dp (match constraints) вместо match_parent
  • Избегайте глубокой вложенности - старайтесь укладываться в 2-3 уровня
  • Используйте Guidelines для повторяющихся отступов и выравнивания
  • Группируйте связанные элементы с помощью Chains
  • Тестируйте на разных размерах экранов