Работа с данными: адаптеры и отображение. Плюс работа с обработчиками событий виджетов.
Начиная с этой статьи, я больше не буду придерживаться строгого порядка нумерации, а также не буду вдаваться в откровенно простые вещи, которые можно прочитать в официальных руководствах.
В официальном руководстве есть обучающая статья про данные, называется она Saving Data. К сожалению, статья не очень понятная, а ещё перегруженная совершенно не нужными для обучающего текста данными (паттернами и прочим энтерпрайзом). Я постараюсь в моей статье раскрыть тему хранения и отображения данных более понятно (и по-русски).
Модель отображения данных и обработчик события
В андроиде активно используется популярная и логичная концепция отделения данных от их представления. Типичным примером представления данных является список, он реализуется в классе android.widget.ListView
. По сути это просто список элементов с вертикальной прокруткой, по одному элементу на строчку, во всех элементах используется одинаковый макет1 для отображения содержимого элемента. Именно на примере ListView мы и рассмотрим работу с данными.
Для разминки сделаем простейший проект: окно с кнопкой «Add item» и виджет ListView, при клике на кнопку в виджете появляется новый элемент. Исходные коды традиционно доступны на гитхабе: android-tutorials/listview-simple-demo (самая актуальная версия, с дополнительными комментариями).
Для начала добавим кнопку и виджет ListView в макет операции2:
<Button android:id="@+id/button" android:text="@string/add_item" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <ListView android:id="@+id/listview" android:layout_width="fill_parent" android:layout_height="fill_parent" />
Кнопке назначается автоматический ресурсный идентификатор button
, виджету списка — listview
. Геометрию элементов задаём так, чтобы кнопка занимала ровно необходимое ей место сверху, а оставшуюся часть занимал список.
Автоматический ресурсный идентификатор внутри XML-файла макета выглядит как
@+id/something
, такая запись (т.е. плюс после символа@
) заставляет сборщик сгенерить идентификатор для указанного элемента интерфейса, и этот идентификатор далее в коде будет доступен через конструкцию-константу видаR.id.something
.
В принципе, на этом этапе приложение вполне годится для запуска, только оно ничего пока не умеет: кнопка не реагирует, список пустой. Займёмся сначала списком, данные для отображения он берёт из так называемого адаптера — экземпляра специального класса, главная цель которого — поставлять данные для виджета списка. Поэтому сделаем сначала адаптер:
-
Добавим в класс
MainActivity
полеmAdapter
типаArrayAdapter<String>
(для простоты будем работать со строками):protected ArrayAdapter<String> mAdapter;
-
Внутри реализации метода
onCreate()
добавим его инициализацию:mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
-
И, наконец, назначаем адаптер виджету Listview:
mListView = (ListView)findViewById(R.id.listview); mListView.setAdapter(mAdapter);
В конструктор мы передаём контекст (операция является наследником класса Context
, поэтому можно передать this), а также идентификатор макета, который будет использоваться для отрисовки одного элемента списка. Здесь я использую константу android.R.layout.simple_list_item_1
— это один из стандартных андроидных макетов, как раз для отображения одного элемента списка. Но вы можете сами сделать свой собственный макет (на основе виджета TextView) и указать его идентификатор.
Дальше добавим обработчик нажатия на кнопку, я приведу весь кусок кода, и пояснения к нему:
Button button = (Button)findViewById(R.id.button); button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mAdapter.add("test"); } });
Находим кнопку через метод findViewById()
и назначаем ей обработчик нажатия. Обработчик реализован через создание анонимного экземпляра анонимного класса, реализующего интерфейс android.view.View.OnClickListener
. У этого интерфейса один метод — onClick
— его и реализуем, в нём пишем обработчик нажатия: при каждом клике добавляем в адаптер строку "test", и эта строка появляется отдельно строчкой в виджете списка.
Вы можете внести больше разнообразия в этот код, например, добавлять не фиксированную строку, а случайный набор символов или же строку, состояющую из текущего времени, ну и так далее. А можно добавить ещё один виджет — ввода текста, android.widget.EditText
, например и брать добавляемый в список текст, введённый пользователем.
Если мы теперь выйдем из приложения, все добавленные в список элементы пропадут, поскольку они не сохранялись ни в каком постоянном хранилище. Следующий раздел как раз про сохранение, не закрывайте проект, продолжим в нём же.
Простое сохранение и восстановление
Самая очевидная идея — при выходе из приложения запомнить содержимое списка во каком-нибудь хранилище (например, файле), а при восстановлении или запуске считать их оттуда и заполнить список заново.
Сначала напишем метод в классе нашей операции для сохранения содержимого списка. Сохранять будем в файл во внутреннем файловом хранилище андроида, в приватной части приложения:
protected void saveListViewLines() { FileOutputStream fos; try { // открываем файл в приватном пространстве приложения fos = openFileOutput("listview-lines.txt", Context.MODE_PRIVATE); } catch (FileNotFoundException e) { e.printStackTrace(); return; } int cnt = mAdapter.getCount(); String item; // считываем все элементы из адаптера и сохраняем их в файл for (int i=0; i<cnt; ++i) { try { item = mAdapter.getItem(i) + "\n"; fos.write( item.getBytes("UTF-8") ); } catch (IOException e) { e.printStackTrace(); return; } } try { fos.close(); } catch (IOException e) { e.printStackTrace(); } }
И будем этот метод вызывать при скрывании операции, для этого переопределяем метод onStop()
и вставляем в него вызов saveListViewLines
:
@Override protected void onStop() { super.onStop(); // save saveListViewLines(); }
Теперь, если выйти из приложения, содержимое списка будет записано в файл в приватном хранилище приложения, в файловой системе это будет каталог /data/data/com.regolit.android.listview_simple_demo/files
, однако нам этим можно не заморачиваться, вызов метода openFileOutput()
сам об этом позаботится.
Замечание Вы можете удостовериться, что программа записала данные из списка в файл, для этого можно воспользоваться командой
adb shell
и просто посмотреть, что же в файле:% adb shell # cat /data/data/com.regolit.android.listview_simple_demo/files/listview-lines.txt test test test
Теперь нужно восстановить содержимое списка при старте приложения. Традиционно сделаем это в виде отдельного метода:
protected void restoreListViewLines() { FileInputStream fis; try { // открываем файл из приватного хранилища fis = openFileInput("listview-lines.txt"); } catch (FileNotFoundException e) { e.printStackTrace(); return; } // создаём «читатель» данных из файла, чтобы прочитать // их построчно. InputStreamReader isr = new InputStreamReader(fis); BufferedReader br = new BufferedReader(isr); String line; try { while (true) { line = br.readLine(); if (line == null) { break; } // и каждую успешно считанную строчку добавляем // в список через адаптер mAdapter.add(line); } } catch (IOException e) { } }
Теперь вставим вызов этого метода в метод onCreate()
:
// восстановим содержимое списка при запуске restoreListViewLines();
И можно проверять: добавим несколько элементов, выйдем из приложения, снова его запустим — элементы на месте. Если вы теперь закроете приложение и отредактируете файл /data/data/com.regolit.android.listview_simple_demo/files/listview-lines.txt
, то при следующем запуске там будут строчки из него.
И напомню, что все исхоники доступны в моём репозитории на гитхабе: https://github.com/sigsergv/android-tutorials/tree/master/listview-simple-demo.
Надеюсь, после этой статьи вам стало понятнее, как именно связаны адаптер и виджеты, его использующие. Помимо простейшего адаптера ArrayAdapter
, существуют более мощные, позволяющие работать с данными из СУБД, например. О них мы поговорим в следующих статьях.
Ссылки по теме
- android.widget.ListView — документация к классу ListView с офсайта
- android.widget.ListAdapter — там же, про адаптеры
- Using the Internal Storage — об использовании внутреннего файлового хранилища
fdsfsf
fsdf