Парамонов 5. Что такое механизм настроек Для чего он предназначен Как его применять
Скачать 121.97 Kb.
|
3.3. Вопросы для самопроверки:
3.3. Пример приложения, использующего БД для хранения данных Описание приложения. Данный раздел содержит пример приложения, использующего базу данных для хранения списка задачи пользователя (to-do list). Пример призван продемонстрировать, как может использоваться БД в приложении, включая выполнение таких действий, как управление жизненным циклом БД, добавление, изменение и получение записей из БД, отображение данных в списке и их редактирование в отдельной активности. Приложение содержит две активности. Главная активность предназначена для отображения списка дел. Вторая активность позволяет редактировать атрибуты пользовательской задачи, включающие название, описание и дату выполнения. Внешний вид приложения представлен на рис. 44. Класс управления жизненным циклом БД. Для управления жизненным циклом определим в приложении класс DBHelper, унаследованный от класса SQLiteOpenHelper: @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } Рис.44. Главная активность и активность редактора в To-do List Данный класс создаёт схему данных при первой попытке обращения к БД. Поскольку приложение имеет единственную версию, номер версии в конструкторе класса SQLiteOpenHelper установлен равным 1, а метод onUpgrade() оставлен пустым. Каждая задача имеет название (title), описание (description) и дату выполнения (dueDate). Поскольку в SQLite нет специального типа данных для хранения дат, используется строковое представление в формате ISO 8601 (например, 2013-01-21). Для первичного ключа создано поле с названием _ id. Такое название необходимо для правильной работы адаптера списка, связанного с таблицей БД. Пользовательский интерфейс главной активности. Описание интерфейса главной активности размещается в файле res/layout/main.xml: android:layout_width="fill_parent" android:layout_height="fill_parent" xmlns:android="http://schemas.android.com/apk/res/android"> и содержит единственный элемент — список с идентификатором todoList. Инициализация главной активности. Главная активность размещается в классе MainActivity. Будем разбирать содержимое этого класса отдельными фрагментами по мере добавления функциональности в приложение. Начнём с кода инициализации главной активности: public class MainActivity extends Activity { private DBHelper dbHelper; private Cursor cursor; @Override public void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState); setContentView(R.layout.main); ListView todoListView = (ListView) findViewByld(R.id.todoList); todoListView.setOnItemClickListener(new ListView.OnItemClickListener() { @Override public void onItemClick(AdapterView> parent, View view, int position, long id) { onToDoListltemClick(id); } }); dbHelper = new DBHelper(this); cursor = dbHelper.getWritableDatabase().query("todos", null, null, null, null, null, "dueDate"); String[] from = new String[] { "title", "description" }; int[] to = new int[] { R.id.titleText, R.id.descriptionText }; SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.todo_item, cursor, from, to, CursorAdapter.FLAG_AUTO_REQUERY); todoListView.setAdapter(adapter); } При инициализации активности происходит загрузка описания интерфейса из XML-файла, к списку прикрепляется обработчик события выбора элемента, создаётся экземпляр класса DBHelper и помещается в поле класса. Далее осуществляется выборка всех задач из таблицы todos базы данных с сортировкой по сроку завершения в хронологическом порядке. В результате выполнения команды на выборку возвращается курсор, который также сохраняется в поле класса MainActivity. Далее создаётся экземпляр класса SimpleCursorAdapter, который осуществляет отображение записей полученного набора данных на текстовые поля элемента списка. Внешний вид элемента списка описывается файлом res/layout/todo_item.xml. Каждый элемент содержит поле для названия задачи и поле для её описания. < LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> Меню приложения и обработка добавления записи. Меню приложения (в формате панели действий) описывается в файле res/menu/main.xml и содержит кнопку добавления задачи: Инициализация меню и обработка касания кнопки добавления записи осуществляются стандартным образом в классе MainActivity: //... @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { Intent intent = new Intent(this, ToDoEditorActivity.class); startActivityForResult(intent, 1); return true; } //... Касание кнопки приводит к вызову активности ToDoEditorActivity, отвечающей за редактирование записи. Поскольку кнопка в панели действий является единственной, метод onOptionsItemSelected() не содержит кода проверки выбранного элемента меню. Пользовательский интерфейс активности редактора. Интерфейс активности, предназначенной для редактирования атрибутов задач, включает в себя текстовые поля для ввода названия задачи и её описания, компонент DatePicker для ввода даты завершения задачи, две кнопки «OK» и «Cancel», а также метки (TextView) для описания назначения всех элементов интерфейса. Для описания пользовательского интерфейса используется файл res/layout/todo_editor.xml. Он выглядит следующим образом: < LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" > android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="left|center_verticary> android:layout_height="wrap_content" android:layout_gravity="left|center_vertical"/> android:layout_gravity="center_horizontal|bottom" android:onClick="onCancelButtonClick" /> android:layout_gravity="center_horizontal|bottom" android:onClick="onOkButtonClick" /> Интерфейс взаимодействия активностей. Перед тем как перейти к рассмотрению реализации активности редактора, необходимо определить интерфейс взаимодействия этой активности с главной активностью приложения. Возможны два случая использования активности редактора: для создания новой записи или редактирования существующей. Договоримся, что при создании новой записи никаких данных из главной активности не передаётся, тогда как при редактировании через дополнительные поля (extras) интента в активность редактора передаётся следующая информация: int id — идентификатор редактируемой записи в БД; String title — название редактируемой задачи; String description — описание редактируемой задачи; String dueDate — дата завершения редактируемой задачи в формате ISO 8601. Активность редактора возвращает RESULT_OK, если пользователь принял внесённые изменения нажатием кнопки «OK», и RESULT_CANCELED, если пользователь отказался от изменений с помощью кнопки «Cancel» или аппаратной кнопки «Back» устройства. Если пользователь принял изменения, то через механизм дополнительных полей интента возвращаются следующие значения: int id — идентификатор редактируемой записи в БД (только в том случае, если активность была вызвана для редактирования задачи; в противном случае данное значение не передаётся); String title — название добавляемой/отредактированной задачи; String description — описание добавляемой/отредактированной задачи; String dueDate — дата завершения добавляемой/отредактированной задачи в формате ISO 8601. Отметим, что документирование способа взаимодействия чрезвычайно важно для разработки приложений. При отсутствии стандартизованного механизма описания интерфейсов в Android текстовое описание, подобное приведённому выше, может вполне успешно решать задачу передачи информации о правильном использовании активностей между разработчиками. Реализация активности редактора задач. Реализация активности редактора размещается в классе ToDoEditorActivity: public class ToDoEditorActivity extends Activity { private EditText titleText, descriptionText; private DatePicker dueDatePicker; private Intent resultIntent = new Intent(); private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy—MM— dd"); public void onCreate(Bundle savedlnstanceState) { super.onCreate(savedlnstanceState); setContentView(R.layout.todo_editor); titleText = (EditText) findViewByld(R.id.titleText); descriptionText = (EditText) findViewByld(R.id.descriptionText); dueDatePicker = (DatePicker) findViewByld(R.id.dueDatePicker); if (getIntent().hasExtra("id")) { resultIntent.putExtra("id", getIntent().getIntExtra("id", 0)); titleText.setText(getIntent().getStringExtra("title")); descriptionText.setText(getIntent().getStringExtra("description")); GregorianCalendar calendar = stringToDate( getIntent().getStringExtra("dueDate")); dueDatePicker.init(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF JMONTH), null); } } private static String dateToString(int year, int month, int day) { GregorianCalendar calendar = new GregorianCalendar(year, month, day); return dateFormat.format(calendar.getTime()); } private static GregorianCalendar stringToDate(String dateString) { try { Date date = dateFormat.parse(dateString); GregorianCalendar calendar = new GregorianCalendar(); calendar.setTime(date); return calendar; } catch (ParseException e) { return null; } } public void onOkButtonClick(View v) { resultIntent.putExtra("title", titleText.getText().toStringO); resultIntent.putExtra("description", descriptionText.getText().toString()); resultIntent.putExtra("dueDate", dateToString(dueDatePicker.getYear(), dueDatePicker.getMonth(), dueDatePicker.getDayOfMonth())); setResult(RESULT_OK, resultlntent); finish(); } public void onCancelButtonClick(View v) { setResult(RESULT_CANCELED); finish(); } } Метод onCreate() инициализирует интерфейс активности редактора, сохраняет ссылки на необходимые компоненты интерфейса в полях класса активности, а также переносит значения из интента в соответствующие виджеты, если активность была открыта в режиме редактирования существующей записи (это определяется по наличию параметра id в дополнительных параметрах интента). Обработчик кнопки «OK» выполняет обратную передачу данных из виджетов в возвращаемый интент. Кроме того, этот обработчик, как и обработчик кнопки «Cancel», устанавливает возвращаемое значение и завершает выполнение активности с помощью вызова finish(). Для выделения отдельных компонентов даты из строк в формате ISO 8601 и формирования таких строк из отдельных компонентов определены два вспомогательных метода: dateToString() и stringToDate() соответственно. Вызов активности редактора для изменения существующей задачи. Вызов активности редактора в режиме добавления задачи уже рассмотрен ранее. Второй случай вызова осуществляется в том случае, когда пользователь касается одного из пунктов списка задач, вызывая соответствующую задачу на редактирование. Обработка данного действия осуществляется в методе onToDoListItemClick() главной активности: //... public void onToDoListItemClick(long id) { Cursor todoCursor = dbHelper.getReadableDatabase().query("todos", null, "_id = ?", new String[] { String.valueOf(id) }, null, null, null); todoCursor.moveToNext(); Intent intent = new Intent(this, ToDoEditorActivity.class); intent.putExtra("id", todoCursor.getInt( todoCursor.getColumnIndex("_id"))); intent.putExtra("title", todoCursor.getString( todoCursor.getColumnIndex("title"))); intent.putExtra("description", todoCursor.getString( todoCursor.getColumnIndex("description"))); intent.putExtra("dueDate", todoCursor.getString( todoCursor.getColumnIndex("dueDate"))); startActivityForResult(intent, 1); } //... Данный обработчик получает содержимое записи из БД по идентификатору, а затем заполняет дополнительные параметры интента значениями полей полученной записи, после чего вызывает активность редактора. Обработка результата вызова активности редактора в главной активности. Наконец рассмотрим обработку возвращённого значения из активности редактора. Эта обработка осуществляется в методе onActivityResult() класса главной активности: //... @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode != RESULTOK) return; ContentValues cv = new ContentValues(); cv.put("title", data.getStringExtra("title")); cv.put("description", data.getStringExtra("description")); cv.put("dueDate", data.getStringExtra("dueDate")); if (data.hasExtra("id")) { dbHelper.getWritableDatabase().update("todos", cv," Jd = ?", new String[] { String.valueOf(data.getIntExtra("id", 0)) }); } else { dbHelper.getWritableDatabase().insert("todos", null, cv); } cursor.requery(); } } // class MainActivity В том случае, когда пользователь не принял внесённых изменений, происходит возврат из обработчика. В противном случае атрибуты модифицированной задачи переносятся из возвращённого интента в объект класса ContentValues. Далее по наличию значения id в дополнительных параметрах интента выясняется действие, которое необходимо выполнить: добавление новой записи или изменение существу- ющей. В результате вызывается метод insert() или метод update() на объекте базы данных. В последней строке метода вызывается метод requery() на курсоре, что вызывает обновление содержимого списка задач на экране. 3.4. Вопросы и упражнения для самопроверки:
Асинхронное выполнение подразумевает использование отдельных потоков для выполнения некоторых действий. Необходимость в асинхронном выполнении чаще всего вызвана наличием в приложении некоторых процессов (вычисления, обращение к сетевым ресурсам, чтение из базы данных), требующих достаточно высоких временных затрат. Если не переносить выполнение данных процессов на отдельные потоки, это неминуемо скажется на отзывчивости интерфейса пользователя, снижая удобство приложения. Организация асинхронного выполнения может опираться на стандартный механизм потоков Java (класс ^read и интерфейс Runnable) либо использовать Android-специфичные API, являющиеся высокоуровневыми обёртками стандартных потоков. Важной проблемой асинхронного выполнения является синхронизация потоков. Необходимость её связана с тем фактом, что средства Android API, как и любой библиотеки графического интерфейса, являются поточно небезопасными. Последнее означает, что обращение к виджетам с потоков, отличных от главного потока выполнения приложения, может приводить к непредсказуемым последствиям. Для решения проблемы синхронизации Android API предоставляет различные средства, включающие очереди сообщений, класс для организации выполнения асинхронных задач AsyncTask, а также более специализированные механизмы, ориентированные на решение отдельных задач, таких как асинхронная загрузка данных из БД. Класс Handler и очередь сообщений. Класс Handler предназначен для управления очередями сообщений, связанными с потоками выполнения. Сообщения могут отправляться в очередь с любого потока выполнения, но обрабатываются они всегда на главном (связанном с пользовательским интерфейсом) потоке. Таким образом класс Handler обеспечивает синхронизацию потоков. Для отправки сообщения в очередь используются методы | public boolean sendMessage(Message msg); public boolean sendMessageDelayed(Message msg, long delayMillis); public boolean sendMessageAtTime(Message msg, long uptimeMillis); Сообщение представляет собой объект класса Message. Для хранения деталей передаваемого сообщения могут использоваться целочисленные свойства с именами what, argl и arg2, а также свойство obj типа Object. Android API не регламентирует способ применения данных свойств, поэтому разработчик может использовать их по собственному усмотрению. Для оптимизации использования оперативной памяти рекомендуется не создавать объекты класса Message с помощью операции new, а вызывать один из статических методов класса Handler: public Message obtainMessage(int what, int argl, int arg2, Object obj); public Message obtainMessage(int what); public Message obtainMessage(int what, Object obj); Данные методы обеспечивают повторное использование объектов сообщений, организуя пул. В случае запроса объекта с повторяющимися значениями параметров эти методы просто возвращают его из пула, а не создают новый объект. Для обработки сообщений необходимо унаследовать собственный класс от класса Handler, переопределить метод | public void handleMessage(Message msg); и в нём разместить код обработки сообщения, которое передаётся в метод в качестве аргумента. Обработка сообщения осуществляется на главном потоке выполнения приложения, поэтому из данного обработчика можно обращаться к элементам пользовательского интерфейса. Помимо отправки сообщений в очередь можно добавлять объекты классов, реализующих интерфейс Runnable. Это осуществляется с помощью методов public boolean post(Runnable r); public boolean postDelayed(Runnable r, long delayMillis); public boolean postAtTime (Runnable r, long uptimeMillis); При использовании данного подхода в роли обработчика выступает метод run() передаваемого объекта. Пример использования класса Handler. В качестве примера использования класса Handler рассмотрим приложение, определяющее внешний IP-адрес устройства с помощью онлайн-сервиса Google. Поскольку выполнение сетевого запроса требует времени, крайне нежелательно выполнять подобные действия на основном потоке выполнения. Для решения данной проблемы создадим отдельный поток, который будет выполнять запрос, а затем передавать полученный в результате запроса к сервису IP-адрес на главный поток приложения. Интерфейс приложения содержит кнопку для инициирования запроса, индикатор прогресса и поле для вывода результата. Файл res/layout/main.xml, описывающий пользовательский интерфейс главной активности, выглядит следующим образом: < LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> Поскольку для правильного функционирования приложению требуется доступ в Интернет, необходимо добавить соответствующее разрешение в файл манифеста: I Инициализация класса главной активности стандартна, в методе onCreate() происходит заполнение полей класса активности, хранящих ссылки на виджеты пользовательского интерфейса: public class MainActivity extends Activity { private Handler handler; private TextView ipTextView; private ProgressBar progressBar; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); ipTextView = (TextView) findViewById(R.id.ipTextView); progressBar = (ProgressBar) findViewById(R.id.progressBar); handler = new Handler() { @Override public void handleMessage(Message msg) { handleIPDeterminationMessage(msg); В поле handler записывается объект анонимного класса, унаследованного от Handler, который передаёт обработку сообщения методу handleIPDeterminationMessage() класса главной активности. Обработчик кнопки «Determine IP address» выглядит следующим образом: //... public void onDetermineIPAddressClick(View v) { ipTextView.setText(""); progressBar.setVisibility(View.VISIBLE); "bread determinationbread = new ""read() { @Override public void run() { determineIPAddress(); } }; determination"read.start(); } Данный обработчик очищает поле вывода результата, делает видимым индикатор прогресса, а затем инициирует выполнение метода determineIPAddress() класса главной активности на отдельном потоке выполнения. Указанный метод осуществляет запрос к web-сервису, обрабатывает возвращённое значение в формате JSON и отправляет полученный IP-адрес на главный поток выполнения в виде сообщения: //... private void determineIPAddress() { try { URL url = new URL( "http://ip2country.sourceforge.net/ip2c.php?format=JSON"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.connect(); BufferedReader reader = new BufferedReader( new InputStreamReader(conn.getInputStream())); String ip = (String) new JSONObject(reader.readLine()).get("ip"); reader.close(); handler.sendMessage(handler.obtainMessage(0, 0, 0, ip)); } catch (Exception e) { handler.sendMessage(handler.obtainMessage(0, 0, 0, e.getMessage())); } } //... Класс Handler обрабатывает сообщение, вызывая метод handleIPDeterminationMessage() и передавая ему сообщение в качестве аргумента: //... private void handleIPDeterminationMessage(Message msg) { progressBar.setVisibility(View.INVISIBLE); ipTextView.setText(msg.obj.toString()); } } // class MainActivity Данный метод скрывает индикатор прогресса, извлекает определённый IP-адрес из сообщения и помещает его в текстовое поле. Класс AsyncTask. Помимо использования класса Handler, Android API предоставляет более простой подход для организации асинхронного выполнения. Этот способ основан на применении клас- са AsyncTask, инкапсулирующего некоторую задачу, имеющую входные и выходные параметры и требующую выполнения на отдельном потоке. При использовании данного класса нет необходимости создавать поток явно, достаточно лишь унаследовать от AsyncTask собственный класс и переопределить несколько методов. Отметим, что AsyncTask — это параметризованный класс. Его параметры обозначаются protected void onPreExecute(); protected Result doInBackground(Params... params); protected void onProgressUpdate(Progress... values); protected void onPostExecute(Result result); Главный из перечисленных методов — doInBackground(). Он содержит собственно код задачи, выполняемой на отдельном потоке. Остальные приведённые методы вызываются на главном потоке выполнения и, следовательно, могут обращаться к элементам пользовательского интерфейса. Синхронизация между потоками осуществляется классом AsyncTask автоматически. Методы onPreExecute() и onPostExecute() выполняются соответственно перед запуском и после завершения асинхронной задачи. Выполнение метода onProgressUpdate() инициируется вызовом protected void publishProgress(Progress... values); из кода метода doInBackground(). Данная возможность используется для вывода пользователю промежуточных результатов или статуса во время выполнения задачи. Проиллюстрируем приведённую в предыдущем пункте информацию, заменив класс Handler классом AsyncTask. Сначала удалим поле типа Handler и его инициализацию из класса MainActivity: public class MainActivity extends Activity { private TextView ipTextView; private ProgressBar progressBar; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); ipTextView = (TextView) findViewById(R.id.ipTextView); progressBar = (ProgressBar) findViewById(R.id.progressBar); } //... Класс, унаследованный от AsyncTask, назовём IPDeterminationTask и разместим внутри класса активности. Это необходимо для упрощения обновления содержимого виджетов, являющихся полями класса Activity. Код определения IP-адреса будет размещаться в методе doInBackground(): //... private class IPDeterminationTask extends AsyncTask protected String doInBackground(Void... params) { try { URL url = new URL( "http://ip2country.sourceforge.net/ip2c.php?format=JSON"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.connect(); BufferedReader reader = new BufferedReader( new InputStreamReader(conn.getInputStream())); String ip = (String) new JSONObject(reader.readLine()).get("ip"); reader.close(); return ip; } catch (Exception e) { return e.getMessage(); } } Возвращаемое значение имеет тип String и по окончании выполнения задачи содержит результат определения IP-адреса. Методы onPreExecute() и onPostExecute() используются для обновления значения текстового поля результата, а также для отображения и сокрытия индикатора прогресса. Заметим, что вывод результата упрощается, т. к. возвращаемое значение метода doInBackground() передаётся в качестве параметра в метод onPostExecute(): @Override protected void onPreExecute() { ipTextView.setText(""); progressBar.setVisibility(View.VISIBLE); } @Override protected void onPostExecute(String ip) { progressBar.setVisibility(View.INVISIBLE); ipTextView.setText(ip); } } // class IPDeterminationTask //... Наконец, функции обработчика кнопки «Determine IP address» сводятся к созданию экземпляра класса IPDeterminationTask и запуску выполнения задачи: //... public void onDetermineIPAddressClick(View v) { new IPDeterminationTask().execute(); } } // class MainActivity 4.2. Вопросы и упражнения для самопроверки:
4.3. Провайдеры контента Провайдеры контента входят в число основных компонентов Android-приложений, наряду с активностями и сервисами. Их задача состоит в предоставлении данных множеству приложений без привязки к конкретному способу хранения этих данных. Операции, предоставляемые провайдером контента, аналогичны типичным операциям работы с базой данных и включают в себя добавление, обновление и удаление записей, а также выполнение запросов на выборку данных. Следует отметить, что природа этих данных может быть произвольной, поскольку и их структура, и способ хранения всецело находятся в зоне ответственности провайдера контента. Для взаимодействия с провайдером контента клиенты специфицируют URI данных, над которыми выполняется действие, в виде: content: //имя_провайдера_контента/спецификация_данных. Имя провайдера контента должно совпадать со значением, которое указывается при регистрации провайдера контента в файле манифеста. Спецификация данных может иметь любой формат, однако общепринятой является REST-подобная спецификация. Пример стандартного провайдера контента. В качестве примера рассмотрим стандартный провайдер контента, предоставляющий доступ к контактам пользователя мобильного устройства. Данный провайдер использует URI вида content://com.android.contacts/ contacts для получения всех доступных контактов и URI content://com. android.contacts/contacts/1 для доступа к данным контакта под номером 1. Причём последний URI можно использовать не только для получения данных контакта, но и для их изменения или удаления. Провайдер контента для списка задач. Остаток главы по- свящён рассмотрению механизма провайдеров контента на примере, в качестве которого будет выступать модифицированное приложение из главы 7. Здесь мы заменим непосредственное обращение к базе данных на взаимодействие с ней через провайдер контента. Код данного провайдера будет выглядеть следующим образом: public class ToDoContentProvider extends ContentProvider { private DBHelper dbHelper; @Override public boolean onCreate() { dbHelper = new DBHelper(getContext()); return false; } //... В методе onCreate() создаётся экземпляр класса DBHelper. Он используется для связи с хранилищем данных провайдера контента, в роли которого выступает база данных SQLite. //... private static final String AUTHORITY = "ru.ac.uniyar.todoslist.contentprovider"; private static final String BASE_PATH = "todos"; private static final int TODOS = 10; private static final int TODO_ID = 20; private static final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); static { matcher.addURI(AUTHORITY, BASE_PATH, TODOS); matcher.addURI(AUTHORITY, BASE_PATH + "/#", TODOJD); } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLite^eryBuilder queryBuilder = new SQLite^eryBuilder(); queryBuilder.setTables("todos"); int uriType = matcher.match(uri); switch (uriType) { case TODOS: break; case TODOJD: queryBuilder.appendWhere(" Jd = " + uri.getLastPathSegment()); break; default: throw new IllegalArgumentException("Unknown URI:" + uri); } SQLiteDatabase db = dbHelper.getWritableDatabase(); Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; } @Override public String getType(Uri uri) { return null; } @Override public Uri insert(Uri uri, ContentValues values) { int uriType = matcher.match(uri); SQLiteDatabase sqlDB = dbHelper.getWritableDatabase(); long id = 0; switch (uriType) { case TODOS: id = sqlDB.insert("todos", null, values); break; default: throw new IllegalArgumentException("Unknown URI:" + uri); } getContext().getContentResolver().notifyChange(uri, null); return Uri.parse(BASE_PATH + "/" + id); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { int uriType = matcher.match(uri); SQLiteDatabase sqlDB = dbHelper.getWritableDatabase(); int rowsUpdated; switch (uriType) { case TODOS: rowsUpdated = sqlDB.update("todos", values, selection, selectionArgs); break; case TODOJD: String id = uri.getLastPathSegment(); if (TextUtils.isEmpty(selection)) { rowsUpdated = sqlDB.update("todos", values, " Jd =" + id, null); } else { rowsUpdated = sqlDB.update("todos", values," Jd =" + id + " and " + selection, selectionArgs); } break; default: throw new IllegalArgumentException("Unknown URI:" + uri); } getContext().getContentResolver().notifyChange(uri, null); return rowsUpdated; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { throw new UnsupportedOperationException(); } } // class ToDoContentProvider Методы query(), insert(), update() и delete() являются переопределёнными методами класса ContentProvider. Каждый из этих методов принимает URI, специфицирующий данные, над которыми производится соответствующее действие. Методы query() и update() поддерживают URI вида content://ru.ac.uniyar.todoslist.contentprovider/todos и content: //ru.ac.uniyar.todoslist.contentprovider/todos/1 аналогично примеру с контактами пользователя, рассмотренному в п. 9.2. Метод insert() поддерживает лишь URI вида content://ru.ac.uniyar.todoslist.contentprovider/ todos, поскольку в момент добавления никакого идентификатора записи ещё не присвоено. Метод delete() для данного провайдера контента не реализован (выбрасывает исключение), поскольку в рассматриваемом примере необходимости в нём нет. Рассматривая реализацию перечисленных методов, можно отметить, что основная функция провайдера контента в данном случае сводится к преобразованию запросов к провайдеру в запросы к БД, хранящей данные о задачах. Регистрация провайдера контента в файле манифеста. Каждый провайдер контента должен быть зарегистрирован в файле манифеста. В нашем примере регистрация выглядит следующим образом: android:authorities="ru.ac.uniyar.todoslist.contentprovider" > После установки приложения доступ к провайдеру контента можно будет получить из любых приложений, используя соответствующие URI. Асинхронная загрузка данных. При использовании провайдеров контента существует очень полезная возможность асинхронной загрузки данных с помощью класса CursorLoader. Этот класс осуществляет запрос к провайдеру контента на отдельном потоке выполнения и вызывает callback- метод, когда загрузка завершается и можно использовать полученные данные. Для использования класса CursorLoader необходим класс, реализующий интерфейс LoaderManager.LoaderCallbacks, который определяет все необходимые callback-методы. По соглашению таким классом обычно является класс активности: public class MainActivity extends Activity implements LoaderManager.LoaderCallbacks @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); ListView todoListView = (ListView) findViewById(R.id.todoList); todoListView.setOnItemClickListener( new ListView.OnItemClickListener() { @Override public void onItemClick(AdapterView> parent, View view, int position, long id) { onToDoListItemClick(id); } }); getLoaderManager().initLoader(0, null, this); String[] from = new String[] { "title", "description" }; int[] to = new int[] { R.id.titleText, R.id.descriptionText }; adapter = new SimpleCursorAdapter(this, R.layout.todo_item, null, from, to, 0); todoListView.setAdapter(adapter); } //... Заметим, что, в отличие от метода onCreate() из п. 7.4, в данном примере отсутствует запрос к БД, а объект класса SimpleCursorAdapter создаётся не связанным с конкретным курсором (третий параметр конструктора равен null). Это делается потому, что курсор будет создан динамически классом CursorLoader по окончании загрузки данных. Метод initLoader() инициирует запуск метода onCreateLoader() интерфейса LoaderManager.LoaderCallbacks: //... @Override public Loader Uri.parse("content://ru.ac.uniyar.todoslist.contentprovider/todos"), null, null, null, null); } //... По окончании загрузки данных вызывается callback-метод onLoadFinished(), которому передаётся курсор, связанный с полученными данными: //... @Override public void onLoadFinished(Loader } //... Метод swapCursor() приводит к отображению полученных данных в списке главной активности. Когда данные становятся недоступными, вызывается метод onLoaderReset: //... @Override public void onLoaderReset(Loader } //... При использовании провайдера контента вставка и обновление данных происходят практически так же, как и в случае непосредственного выполнения данных операций с базой данных. Единственное отличие заключается в том, что методы получения и изменения данных вызываются не на объекте класса SQLiteDatabase, а на объекте класса ContentResolver, предоставляющего доступ к данным через провайдер контента, идентифицированного по URI: //... @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode != RESULT_OK) return; ContentValues cv = new ContentValues(); cv.put("title", data.getStringExtra("title")); cv.put("description", data.getStringExtra("description")); cv.put("dueDate", data.getStringExtra("dueDate")); if (data.hasExtra("id")) { getContentResolver().update( Uri.parse("content://ru.ac.uniyar.todoslist.contentprovider/todos/" + data.getIntExtra("id", 0)), cv, null, null); } else { getContentResolver().insert( Uri.parse("content://ru.ac.uniyar.todoslist.contentprovider/todos/"), cv); } } public void onToDoListItemClick(long id) { Cursor todoCursor = getContentResolver().query( Uri.parse("content://ru.ac.uniyar.todoslist.contentprovider/todos/" + id), null, null, null, null); todoCursor.moveToNext(); Intent intent = new Intent(this, ToDoEditorActivity.class); intent.putExtra("id", todoCursor.getInt( todoCursor.getColumnIndex("_id"))); intent.putExtra("title", todoCursor.getString( todoCursor.getColumnIndex("title"))); intent.putExtra("description", todoCursor.getString( todoCursor.getColumnIndex("description"))); intent.putExtra("dueDate", todoCursor.getString( todoCursor.getColumnIndex("dueDate"))); startActivityForResult(intent, 1); } } // class MainActivity Важная особенность класса CursorLoader заключается в том, что он автоматически отслеживает изменения в данных, получение которых он осуществляет. При изменении этих данных процесс загрузки производится заново без дополнительных усилий со стороны программиста. Поэтому при изменении данных нет необходимости в вызове метода requery(), который использовался в п. 7.10. Заметим, что код методов работы с меню onCreateOptionsMenu() и onOptionsItemSelected() не меняется по сравнению с приведённым в п. 7.5. После сборки и запуска приложения легко убедиться, что оно функционирует точно так же, как и приложение из главы 7. 4.4. Вопросы и упражнения для самопроверки:
Добавьте возможность удаления записей в приложении- примере. Указание. Начните решение задачи с реализации метода delete() провайдера контента. |