В последнее время на Хабре стали все чаще появляться статьи связанные с разработкой для Android. Дабы не оставаться в стороне и внести свой небольшой вклад в помощь подрастающему поколению Android разработчиков, решил написать статью, в которой мы разработаем полноценное приложение-шпаргалку с использованием ряда наиболее востребованных компонентов Android SDK. Данное руководство рассчитано на разработчиков начального уровня имеющих общее представление касательно основных компонентов Android приложений таких как: Activity, Service, Intent, Broadcast Receiver.
В процессе разработки мы познакомимся с такими вещами как:
- Activity
- Service
- Broadcast Receiver
- SQLite
- Asynk Tasks
- XML parsing
- ActionBar Sherlock
И так, в качестве задания примем такое: «Необходимо сделать приложение отображающее погоду в Москве, данные нужно брать с сервера Yahoo».
Для того чтоб охватить больше разнообразных компонентов Android SDK и сторонних библиотек, предлагаю пойти длинным и с токи зрения проектирования не самым верным путем.
В начале, нам необходимо продумать общую структуру приложения:
- Прежде всего, нам необходимо Activity, в котором мы будем отображать информацию.
- Для получения данных и их обработки нам необходим Service. Кроме того, что работать с сервисами очень удобно, это позволит инкапсулировать все запросы из сети в одном месте.
- Последним кирпичиком основных компонентов нам необходим Broadcast Receiver он будет обеспечивать получения уведомления о завершении работы сервиса.
- Последним крупным элементом является база данных, в которой будет храниться полученная информация.
Определив основные аспекты нашего приложения, приступим к разработке нашей Activity. Для этого создайте в папке /src своего проекта файл MainActivity.java.
Для использования Action Bar в более ранних версиях чем API 11 нам необходимо использовать библиотеку Action Bar Sherlock, в связи с этим в отличии от обычной Activity мы должны наследоваться от SherlockActivity таким вот образом:
public class MainActivity extends SherlockActivity {
}
vЕсли вы еще не подключили библиотечный проект ActionBarSherlock – то самое время это сделать, найти его можно по адресу http://actionbarsherlock.com/. Библиотека поставляется с большим количеством примеров, потому дальнейшее ее исследование – выходящее за рамки этой статьи не должно вызвать у вас никаких трудностей.
Чаще всего, первым делом при создании Activity нам необходимо переопределить в ней метод
onCreate(Bundle savedInstanceState). За всю свою практику разработки под Android, я всего 1 раз сталкивался с ситуацией, когда это не нужно.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(R.style.Theme_Sherlock);
setContentView(R.layout.main);
}
Код очень простой, состоит из 3х строек: вызова конструктора суперкласса, установки темы для Activity – необходимо для работы Action Bar и последняя строка отвечает за установку layout для нашей Activity.
Теперь давайте настроим наш Action Bar и переопределим еще 2 метода. Для начала необходимо добавить задействованные переменные в нашу Activity:
public final static String NEW_WEATHER_EVENT = "com.andrewkravets.weather.NEW_WEATHER_ADDED";
private final static String REFRESH_MENU_ITEM = "Refresh";
private UpdateService mService;
private boolean mBound;
И непосредственно сами методы:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(REFRESH_MENU_ITEM).setIcon(android.R.drawable.ic_dialog_info)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
return true;
}
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
if (item.getTitle().equals(REFRESH_MENU_ITEM)) {
if (mBound) {
mService.loadWeatherData();
}
return true;
}
return false;
}
На этом этапе нам стоит оставить работу с Activity и перейти к созданию Service. Пусть вас не смущает то, что я вызываю на сервисе метод, или ввел кучу непонятных переменных, мы как раз сейчас к этому перейдем.
В нашем сервисе мы будем использовать несколько констант, поэтому предлагаю вынести их все в отдельный интерфейс.
public interface Consts {
public static final String API_URL = "http://weather.yahooapis.com/forecastrss?w=2122265&u=c";
public static final String ROOT = "yweather:condition";
public static final String CONDITION_TEXT = "text";
public static final String CONDITION_TEMP = "temp";
public static final String CONDITION_DATE = "date";
}
Теперь перейдем конкретно к сервису, создадим класс UpdateService который будет унаследован от Service.
public class UpdateService extends Service implements Consts {
}
Далее делаем все стандартно по примеру из статьи о сервисах и добавляем следующий код:
private final IBinder mBinder = new LocalBinder();
private boolean mIsLoading;
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public class LocalBinder extends Binder {
public UpdateService getService() {
return UpdateService.this;
}
}
Переменная mIsLoading нам необходима, для ограничения количества одновременно скачивающих потоков.
Следующим шагом нам необходимо написать AsynkTask для того, чтоб получать информацию, парсить ее и заносить в базу.
Для этого нам необходим класс модель, создадим простой POJO класс, WeatherObject, который содержит три текстовых поля: состояние, температуру и дату. При помощи IDE сгенерируем для него конструктор и get- и set- методы.
private class ManageWeatherData extends AsyncTask<Void, Void, Void> {
@Override
protected void onPreExecute() {
super.onPreExecute();
mIsLoading = true;
}
@Override
protected Void doInBackground(Void... params) {
getWeather();
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
Intent intent = new Intent(MainActivity.NEW_WEATHER_EVENT);
sendBroadcast(intent);
mIsLoading = false;
}
}
private void getWeather() {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder db = dbf.newDocumentBuilder();
Document dom = db.parse(API_URL);
parseDocument(dom);
} catch (ParserConfigurationException pce) {
pce.printStackTrace();
} catch (SAXException se) {
se.printStackTrace();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
private void parseDocument(Document dom) {
Element docEle = dom.getDocumentElement();
NodeList nl = docEle.getElementsByTagName(ROOT);
if (nl != null && nl.getLength() > 0) {
for (int i = 0; i < nl.getLength(); i++) {
Element el = (Element) nl.item(i);
WeatherObject weatherObject = parseWeather(el);
writeToDB(weatherObject);
}
}
}
private WeatherObject parseWeather(Element el) {
String condition = el.getAttribute(CONDITION_TEXT);
String temp = el.getAttribute(CONDITION_TEMP);
String date = el.getAttribute(CONDITION_DATE);
return new WeatherObject(condition, temp, date);
}
private void writeToDB(WeatherObject weatherObject) {
DatabaseHandler.getInstance(getApplicationContext()).addWeather(weatherObject);
}
Для завершения кода нашего Service осталось только добавить недостающий метод, при помощи которого ему отдается команда начать загрузку.
public void loadWeatherData() {
if (!mIsLoading) {
new ManageWeatherData().execute();
}
}
На этом наш Service полностью завершен и нам необходимо написать класс для работы с базой данных.
Ниже я приведу полный текст класса, с комментариями к методам, которые могут вызвать затруднения.
public class DatabaseHandler extends SQLiteOpenHelper {
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "weather";
private static final String TABLE_CURRENT_WEATHER = "currentWeather";
private static final String KEY_ID = "id";
private static final String KEY_CONDITION = "condition";
private static final String KEY_TEMP = "temperature";
private static final String KEY_DATE = "date";
private static final String KEY_SIZE = "count(*)";
private static final String SIZE_QUERY = "SELECT "+ KEY_SIZE +" FROM " + TABLE_CURRENT_WEATHER;
private static final String SELECT_QUERY = "SELECT * FROM " + TABLE_CURRENT_WEATHER;
private static final String DROP_QUERY = "DROP TABLE IF EXISTS " + TABLE_CURRENT_WEATHER;
private static final String CREATE_WEATHER_TABLE_QUERY = "CREATE TABLE " + TABLE_CURRENT_WEATHER + "("
+ KEY_ID + " INTEGER PRIMARY KEY," + KEY_CONDITION + " TEXT," + KEY_TEMP + " TEXT," + KEY_DATE + " TEXT" + ")";
private static DatabaseHandler mInstance = null;
//Я предпочитаю делать этот класс в виде Singleton хотя это не обязательно
private DatabaseHandler(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
public static DatabaseHandler getInstance(Context context) {
if (mInstance == null) {
synchronized (DatabaseHandler.class) {
if (mInstance == null) {
mInstance = new DatabaseHandler(context);
}
}
}
return mInstance;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_WEATHER_TABLE_QUERY);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(DROP_QUERY);
onCreate(db);
}
//Метод добавляющий объект в базу
public void addWeather(WeatherObject object) {
SQLiteDatabase db = mInstance.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(KEY_CONDITION, object.getCondition());
values.put(KEY_TEMP, object.getTemp());
values.put(KEY_DATE, object.getDate());
//Проверяем наличие записей в базе, если их нет - создаем первую, в противном случае перезаписываем ее.
if (getDBSize() == 0) {
db.insert(TABLE_CURRENT_WEATHER, null, values);
} else {
db.update(TABLE_CURRENT_WEATHER, values, KEY_ID + "=1", null);
}
db.close();
}
//Очень простой метод для получения количества записей в таблице
private int getDBSize() {
SQLiteDatabase db = mInstance.getReadableDatabase();
Cursor cursor = db.rawQuery(SIZE_QUERY, null);
if (cursor.moveToFirst()) {
return Integer.parseInt(cursor.getString(cursor.getColumnIndex(KEY_SIZE)));
}
cursor.close();
return 0;
}
//Метод возвращающий объект из базы
public WeatherObject getWeather() {
WeatherObject weatherObject = null;
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.rawQuery(SELECT_QUERY, null);
if (cursor.moveToFirst()) {
int condition = cursor.getColumnIndex(KEY_CONDITION);
int temp = cursor.getColumnIndex(KEY_TEMP);
int date = cursor.getColumnIndex(KEY_DATE);
weatherObject = new WeatherObject(cursor.getString(condition), cursor.getString(temp), cursor.getString(date));
}
cursor.close();
return weatherObject;
}
}
Как мы видим, в работе с базой данных в Android, нет ничего сложного, только необходимо не забывать, о том, что в младших версиях обязательно нужно закрывать класс Cursor по окончании работы с ним.
Итак, мы вышли на финишную прямую, осталось лишь описать внешний вид нашего приложения, добавить механизм отображения информации и немного поправить AndroidManifest.xml
Начнем с наброска мнималистичного интерфейса нашего приложения:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:id="@+id/main_date_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/date"/>
<TextView
android:id="@+id/main_date_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/main_date_label"
android:layout_alignLeft="@+id/main_temperature_value"/>
<TextView
android:id="@+id/main_temperature_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/main_date_label"
android:text="@string/temperature"/>
<TextView
android:id="@+id/main_temperature_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_below="@+id/main_date_label"
android:layout_toRightOf="@+id/main_temperature_label"/>
<TextView
android:id="@+id/main_condition_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/main_temperature_label"
android:text="@string/condition"/>
<TextView
android:id="@+id/main_condition_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/main_temperature_label"
android:layout_toRightOf="@+id/main_condition_label"
android:layout_alignLeft="@+id/main_temperature_value"/>
</RelativeLayout>
В коде мы использовали несколько текстовых надписей при помощи ключа @string поэтому в файл strings.xml необходимо добавить несколько строчек:
<string name="temperature">Temperature:</string>
<string name="date">Date:</string>
<string name="condition">Condition:</string>
Вернемся к нашей Activity и напишем в ней Broadcast Receiver который будет получать сообщение об окончании загрузки и провоцировать обновление интерфейса.
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
new UpdateUI().execute();
}
};
private class UpdateUI extends AsyncTask<Void, Void, WeatherObject> {
@Override
protected WeatherObject doInBackground(Void... params) {
return DatabaseHandler.getInstance(MainActivity.this).getWeather();
}
@Override
protected void onPostExecute(WeatherObject object) {
super.onPostExecute(object);
if (object != null) {
((TextView) findViewById(R.id.main_date_value)).setText(object.getDate());
((TextView) findViewById(R.id.main_temperature_value)).setText(object.getTemp());
((TextView) findViewById(R.id.main_condition_value)).setText(object.getCondition());
}
}
}
В данном случае мы использовали еще один AsynkTask для получения данных из базы и внесения изменений в интерфейс.
Последним штрихом будет внести несколько корректив в наш Android Manifest, задекларируем работу сервиса:
<service android:enabled="true" android:name=".tools.UpdateService"/>
И добавим разрешение на работу с сетью интернет:
<uses-permission android:name="android.permission.INTERNET"/>
Вот и все наше приложение готово, кроме того, что вы потренировались, вы теперь всегда сможете заглянуть в него и найти кусочек кода, который может вам понадобиться в повседневной работе.
Версию которую я писал параллельно статье вы можете найти на Bitbucket
P.S.
Буду раз конструктивной критике, спасибо за внимание.
Автор: Olkorns