Утонченная Java-технология для Android-приложений Введение В настоящей статье рассматриваются некоторые инструменты SDK Android для работы в сложных ситуациях. Для разработки Android-приложений необходима последняя версия SDK Android, которая требует Java Development Kit (JDK). Я использую Android 2.2 и JDK 1.6.0_17 (см. раздел Ресурсы). Физическое устройство не требуется; весь код из этой статьи будет прекрасно работать на эмуляторе Android, который входит в состав SDK. Нужно быть знакомым с программированием для Android, так как эта статья не содержит основ Android-разработки, но вы все поймете, если знакомы с языком программирования Java. Часто используемые сокращения API: Application Programming Interface – интерфейс программирования приложений SQL: Structured Query Language – язык структурированных запросов SDK: Software Developer Kit – комплект разработчика ПО UI: User Interface – интерфейс пользователя XML: Extensible Markup Language – расширяемый язык разметки Параллелизм и работа в сети Одна из наиболее распространенных задач Android-приложений состоит в обмене данными с удаленным сервером по сети. Часто результатом этой операции становятся некоторые новые данные, которые нужно представить пользователю. Для этого нужно изменить пользовательский интерфейс. Большинство разработчиков знает, что нельзя выполнять потенциально длительные задачи, такие как доступ к данным по сети (особенно на мобильном телефоне, который может иметь очень медленное сетевое соединение), в главном потоке пользовательского интерфейса. Это заморозит приложение до завершения решения длительной задачи. Если задаче требуется больше пяти секунд, операционная система Android наградит ваше приложение пресловутым сообщением Application Not Responding (рисунок 1). Рисунок 1. Печально известное сообщение Application Not Responding Насколько медленным может быть сетевое соединение пользователя, неизвестно. Чтобы не искушать судьбу, такие задачи нужно решать в другом потоке, по крайней мере не в главном потоке пользовательского интерфейса. Многие, если не большинство приложений Android, имеют дело с несколькими потоками и, следовательно, параллелизмом. Приложениям часто бывает нужно хранить данные локально, и для этого хорошо подходит локальная база данных Android. В среде Java для всех этих трех сценариев (разные потоки, параллелизм и локальное хранение данных) существуют стандартные решения. Однако Android, как вы увидите, предлагает несколько других вариантов. Рассмотрим каждый из них, а также их плюсы и минусы. Android в сети В Java-программировании вызов по сети делается легко. Знакомый пакет java.net содержит для этого несколько классов. Большинство из этих классов присутствует и в Android, и как в любом другом Java-приложении, можно использовать такие классы, как java.net.URL и java.net.URLConnection. Однако в Android есть еще библиотека Apache HttpClient. Это предпочтительный способ сетевых операций на платформе Android. Даже при использовании обычных классов Java реализация Android все равно будет обращаться к HttpClient. В листинге 1 показан пример использования этой важной библиотеки. (См. раздел Загрузки.) Листинг 1. Использование библиотеки HttpClient на платформе Android Code: private ArrayList<Stock> fetchStockData(Stock[] oldStocks) throws ClientProtocolException, IOException{ StringBuilder sb = new StringBuilder(); for (Stock stock : oldStocks){ sb.append(stock.getSymbol()); sb.append('+'); } sb.deleteCharAt(sb.length() – 1); String urlStr = "http://finance.yahoo.com/d/quotes.csv?f=sb2n&s=" + sb.toString(); HttpClient client = new DefaultHttpClient(); HttpGet request = new HttpGet(urlStr.toString()); HttpResponse response = client.execute(request); BufferedReader reader = new BufferedReader( new InputStreamReader(response.getEntity().getContent())); String line = reader.readLine(); int i = 0; ArrayList<Stock> newStocks = new ArrayList<Stock>(oldStocks.length); while (line != null){ String[] values = line.split(","); Stock stock = new Stock(oldStocks[i], oldStocks[i].getId()); stock.setCurrentPrice(Double.parseDouble(values[1])); stock.setName(values[2]); newStocks.add(stock); line = reader.readLine(); i++; } return newStocks; } Этот код работает с массивом объектов Stock. Это базовая структура данных объектов, которые содержат информацию о ценных бумагах, принадлежащих пользователю (символ, цена и т.д.) наряду с персональной информацией, такой как цена, уплаченная за ЦБ пользователем. Динамические данные извлекаются из Yahoo Finance (например, текущая цена акции) с использованием класса HttpClient. HttpClient берет HttpUriRequest, а в данном случае используется HttpGet, подкласс класса HttpUriRequest. Кроме того, на случай, если необходимо разместить данные на удаленном сервере, существует класс HttpPost. Получая HttpResponse от клиента, можно извлечь InputStream с ответом, сохранить его в буфере и проанализировать, получив данные об акциях. Теперь, когда мы умеем получать данные по сети, посмотрим, как использовать эти данные для быстрого обновления UI Android с использованием многопоточности. Параллелизм Android на практике Если код из листинга 1 выполнять в главном потоке пользовательского интерфейса приложения, это, в зависимости от быстродействия сети пользователя, может привести к появлению сообщения Application Not Responding. Поэтому для извлечения этих данных создадим новый поток. Один из способов показан в листинге 2. Листинг 2. Наивная многопоточность (не делайте так, это работать не будет!) Code: private void refreshStockData(){ Runnable task = new Runnable(){ public void run() { try { ArrayList<Stock> newStocks = fetchStockData(stocks.toArray( new Stock[stocks.size()])); for (int i=0;i<stocks.size();i++){ Stock s = stocks.get(i); s.setCurrentPrice( newStocks.get(i).getCurrentPrice()); s.setName(newStocks.get(i).getName()); refresh(); } } catch (Exception e) { Log.e("StockPortfolioViewStocks", "Exception getting stock data", e); } } }; Thread t = new Thread(task); t.start(); } Подпись к листингу 2 гласит, что это наивный код, и так оно и есть. В этом простом примере мы вызываем метод fetchStockData (см. листинг 1), поместив его в объект Runnable и выполнив в новом потоке. Затем в этом новом потоке мы обращаемся к stocks, переменной-члену вмещающего класса Activity (который создает UI). Как следует из названия, это структура данных (в данном случае java.util.ArrayList) объектов Stock. Другими словами, это обмен данными между двумя потоками, основным потоком пользовательского интерфейса и порожденным потоком (вызываемым в коде из листинга 2). Изменив общие данные в порожденном потоке, мы обновляем пользовательский интерфейс, вызывая метод refresh объекта Activity. Если бы мы программировали приложения Java Swing, то могли бы пойти по этому пути. Но в Android это работать не будет. Порожденный поток вообще не в состоянии изменить пользовательский интерфейс. Так как же получить данные, не замораживая пользовательский интерфейс, но так, чтобы его можно было изменять сразу по получении данных? Класс android.os.Handler позволяет координировать потоки и устанавливать связь между ними. В листинге 3 показан измененный метод refreshStockData, который использует обработчик (Handler). Листинг 3. Многопоточность с использованием Handler Code: private void refreshStockData(){ final ArrayList<Stock> localStocks = new ArrayList<Stock>(stocks.size()); for (Stock stock : stocks){ localStocks.add(new Stock(stock, stock.getId())); } final Handler handler = new Handler(){ @Override public void handleMessage(Message msg) { for (int i=0;i<stocks.size();i++){ stocks.set(i, localStocks.get(i)); } refresh(); } }; Runnable task = new Runnable(){ public void run() { try { ArrayList<Stock> newStocks = fetchStockData(localStocks.toArray( new Stock[localStocks.size()])); for (int i=0;i<localStocks.size();i++){ Stock ns = newStocks.get(i); Stock ls = localStocks.get(i); ls.setName(ns.getName()); ls.setCurrentPrice(ns.getCurrentPrice()); } handler.sendEmptyMessage(RESULT_OK); } catch (Exception e) { Log.e("StockPortfolioViewStocks", "Exception getting stock data", e); } } }; Thread dataThread = new Thread(task); dataThread.start(); } Между примерами кода из листинга 2 и листинга 3 есть два основных различия. Очевидное различие состоит в присутствии Handler. Второе заключается в том, что в порожденном потоке пользовательский интерфейс не изменяется. Вместо этого мы отправляем сообщение в Handler, а уже Handler изменяет UI. Обратите внимание также на то, что в потоке переменная-член stocks не изменяется, как это делалось раньше. Вместо этого изменяется локальная копия данных. Это не обязательно, но так безопаснее. Листинг 3 демонстрирует очень распространенный в параллельном программировании шаблон: копирование данных с передачей их новому потоку, который выполняет длительную задачу, возвращая полученные данные в основной поток пользовательского интерфейса, с последующим обновлением основного потока пользовательского интерфейса указанными данными. (Обработчики) – основной механизмом связи для этой цели в Android, который упрощает реализацию этой модели. Но в листинге 3 много другого шаблонного кода. К счастью, Android предоставляет способ инкапсулировать и исключить большую часть этого шаблонного кода. Это продемонстрировано в листинге 4. Листинг 4. Упрощение многопоточности с помощью AsyncTask Code: private void refreshStockData() { new AsyncTask<Stock, Void, ArrayList<Stock>>(){ @Override protected void onPostExecute(ArrayList<Stock> result) { ViewStocks.this.stocks = result; refresh(); } @Override protected ArrayList<Stock> doInBackground(Stock... stocks){ try { return fetchStockData(stocks); } catch (Exception e) { Log.e("StockPortfolioViewStocks", "Exception getting stock data", e); } return null; } }.execute(stocks.toArray(new Stock[stocks.size()])); } Как видите, в листинге 4 гораздо меньше стандартного кода, чем в листинге 3. Мы не создаем потоки или обработчики. Мы инкапсулируем все это с помощью AsyncTask. Чтобы создать AsyncTask, нужно выполнить метод doInBackground. Он всегда будет выполняться в отдельном потоке, поэтому можно свободно вызывать медленные задачи. Его входной тип происходит от параметра типа создаваемого AsyncTask. В данном случае первым параметром типа был Stock, так что doInBackground берет переданный ему массив объектов Stock. Аналогично, он возвращает ArrayList<Stock>, потому что это третий параметр типа AsyncTask. В данном примере я решил также переопределить метод onPostExecute. Это дополнительный метод, который можно реализовать, когда с данными, возвращаемыми от doInBackground, нужно что-то делать. Этот метод всегда выполняется в основном потоке пользовательского интерфейса, поэтому он идеально подходит для его изменения. С помощью AsyncTask можно существенно упростить многопоточный код. Он исключает из процесса разработки многие ловушки параллелизма. Тем не менее, в AsyncTask все еще остаются некоторые потенциальные проблемы, например то, что происходит при изменении ориентации устройства во время выполнения метода doInBackground в объекте AsyncTask. Некоторые решения для подобных случаев приведены по ссылкам в разделе Ресурсы. А теперь перейдем к другой общей задаче, при решении которой Android существенно отклоняется от обычного пути Java – работе с базами данных. Связь с базой данных Android Одна чрезвычайно полезная функция Android – наличие локальной реляционной базы данных. Конечно, данные можно хранить и в локальных файлах, но часто полезнее делать это с помощью реляционной системы управления базами данных (СУБД). Android позволяет работать с популярной базой данных SQLite, которая оптимизирована для встраиваемых систем, подобных Android. Она используется многими важными Android-приложениями. Например, адресная книга пользователя хранится в базе данных SQLite. Теперь, когда есть Android-реализация Java, можно получить доступ к этим базам данных с помощью JDBC. В Android входят пакеты java.sql и javax.sql, которые составляют основную часть API JDBC. Однако они бессильны, когда речь идет о работе с локальными базами данных Android. Вместо них нужно брать пакеты android.database и android.database.sqlite. В листинге 5 приведен пример использования этих классов для хранения и извлечения данных. Листинг 5. Доступ к базе данных Android Code: public class StocksDb { private static final String DB_NAME = "stocks.db"; private static final int DB_VERSION = 1; private static final String TABLE_NAME = "stock"; private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (id INTEGER PRIMARY KEY, symbol TEXT, max_price DECIMAL(8,2), " + "min_price DECIMAL(8,2), price_paid DECIMAL(8,2), " + "quantity INTEGER)"; private static final String INSERT_SQL = "INSERT INTO " + TABLE_NAME + " (symbol, max_price, min_price, price_paid, quantity) " + "VALUES (?,?,?,?,?)"; private static final String READ_SQL = "SELECT id, symbol, max_price, " + "min_price, price_paid, quantity FROM " + TABLE_NAME; private final Context context; private final SQLiteOpenHelper helper; private final SQLiteStatement stmt; private final SQLiteDatabase db; public StocksDb(Context context){ this.context = context; helper = new SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION){ @Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_TABLE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { throw new UnsupportedOperationException(); } }; db = helper.getWritableDatabase(); stmt = db.compileStatement(INSERT_SQL); } public Stock addStock(Stock stock){ stmt.bindString(1, stock.getSymbol()); stmt.bindDouble(2, stock.getMaxPrice()); stmt.bindDouble(3, stock.getMinPrice()); stmt.bindDouble(4, stock.getPricePaid()); stmt.bindLong(5, stock.getQuantity()); int id = (int) stmt.executeInsert(); return new Stock (stock, id); } public ArrayList<Stock> getStocks() { Cursor results = db.rawQuery(READ_SQL, null); ArrayList<Stock> stocks = new ArrayList<Stock>(results.getCount()); if (results.moveToFirst()){ int idCol = results.getColumnIndex("id"); int symbolCol = results.getColumnIndex("symbol"); int maxCol = results.getColumnIndex("max_price"); int minCol = results.getColumnIndex("min_price"); int priceCol = results.getColumnIndex("price_paid"); int quanitytCol = results.getColumnIndex("quantity"); do { Stock stock = new Stock(results.getString(symbolCol), results.getDouble(priceCol), results.getInt(quanitytCol), results.getInt(idCol)); stock.setMaxPrice(results.getDouble(maxCol)); stock.setMinPrice(results.getDouble(minCol)); stocks.add(stock); } while (results.moveToNext()); } if (!results.isClosed()){ results.close(); } return stocks; } public void close(){ helper.close(); } } Класс из листинга 5 полностью инкапсулирует базу данных SQLite, используемую для хранения информации об акциях. Поскольку мы работаем со встроенной базой данных, которая не только будет использоваться нашим приложением, но и создается им, нужно написать код создания базы данных. В Android для этого есть полезный абстрактный вспомогательный класс, который называется SQLiteOpenHelper. Расширим этот абстрактный класс и напишем код для создания базы данных в его методе onCreate. Получив экземпляр этого помощника, можно получить экземпляр SQLiteDatabase и использовать его для выполнения произвольных SQL-операторов. Наш класс базы данных содержит пару удобных методов. Во-первых, это метод addStock для записи в базу данных сведений о новых акциях. Заметьте, что вы используете экземпляр SQLiteStatement. Это аналог java.sql.PreparedStatement. Обратите внимание, что он скомпилирован в конструкторе класса, так что его можно повторно использовать каждый раз при вызове addStock. При каждом обращении к addStock переменные SQLiteStatement (вопросительные знаки в строке INSERT_SQL) связываются с данными, передаваемыми в addStock. Опять же, это очень похоже на PreparedStatement, с которым вы должны быть знакомы по JDBC. Другой удобный метод – getStocks. Как следует из названия, он извлекает все акции из базы данных. Обратите внимание, что мы опять используем для этого строку SQL, как в JDBC. Это делается с помощью метода rawQuery класса SQLiteDatabase. В этом классе есть также несколько методов запросов, которые позволяют обращаться к базам данных без прямого использования SQL. Все эти методы возвращают объект Cursor, очень похожий на java.sql.ResultSet. Cursor можно перемещать по строкам данных, возвращаемых запросом. В каждой строке можно использовать getInt, getString и другие методы для получения значений, связанных с различными столбцами таблицы базы данных. Опять же, это очень похоже на ResultSet. На ResultSet похоже и то, что важно закрыть Cursor, как только работа с ним закончена. В противном случае можно быстро исчерпать запас памяти, что приведет к краху приложения. Запросы к локальной базе данных могут быть медленным процессом, особенно если строк данных много или нужно запускать сложные запросы, соединяющие несколько таблиц. Хотя маловероятно, что какие-то запросы к базе данных или вставки займут более пяти секунд и вызовут появление сообщения Application Not Responding, по-прежнему не рекомендуется рисковать замораживанием пользовательского интерфейса на время чтения и записи данных. Естественно, простейший способ избежать этой ситуации – использование AsyncTask. Пример приведен в листинге 6. Листинг 6. Добавление в базу данных отдельного потока Code: Button button = (Button) findViewById(R.id.btn); button.setOnClickListener(new OnClickListener(){ public void onClick(View v) { String symbol = symbolIn.getText().toString(); symbolIn.setText(""); double max = Double.parseDouble(maxIn.getText().toString()); maxIn.setText(""); double min = Double.parseDouble(minIn.getText().toString()); minIn.setText(""); double pricePaid = Double.parseDouble(priceIn.getText().toString()); priceIn.setText(""); int quantity = Integer.parseInt(quantIn.getText().toString()); quantIn.setText(""); Stock stock = new Stock(symbol, pricePaid, quantity); stock.setMaxPrice(max); stock.setMinPrice(min); new AsyncTask<Stock,Void,Stock>(){ @Override protected Stock doInBackground(Stock... newStocks) { // There can be only one! return db.addStock(newStocks[0]); } @Override protected void onPostExecute(Stock s){ addStockAndRefresh(s); } }.execute(stock); } }); Для начала создадим приемник событий для кнопки. Когда пользователь нажимает кнопку, биржевые данные считываются из различных виджетов (точнее, виджетов EditText) и заполняют новый объект Stock. Создадим AsyncTask и вызовем метод addStock из листинга 5 посредством метода doInBackground. Таким образом, addStock будет выполняться в фоновом потоке, а не в основном потоке пользовательского интерфейса. Как только он завершится, передадим новый объект Stock из базы данных в метод addStockAndRefresh, который выполняется в главном потоке пользовательского интерфейса. Заключение В этой статье показано, что несмотря на то, что Android поддерживает лишь подмножество многих API-интерфейсов среды Java, он определенно не уступает ей в плане возможностей. В некоторых случаях, таких, как работа по сети, он полностью реализует знакомые API, но также предоставляет несколько удобных способов работы. В других случаях, как, например, с параллелизмом, Android добавляет дополнительные API и соглашения, которые должны соблюдаться. Наконец, в случае доступа к базе данных Android предлагает совершенно иной способ, но с использованием многих знакомых идей. Эти различия между стандартной технологией Java и Android-Java не случайны: они образуют фундаментальные конструктивные блоки Android-разработки. Загрузка Пример исходного кода StockPortfolio.zip 18 КБ HTTP 05.08.2012 Майкл Галпин, инженер по программному обеспечению, Vitria Technology http://www.ibm.com/developerworks/ru/library/x-gourmetandroid/index.html?ca=drs- http://www.ibm.com/developerworks/java/library/x-gourmetandroid/index.html?S_TACT=105AGX99&S_CMP=CP