Проект — шпаргалка для начинающих Android разработчиков

в 9:38, , рубрики: android, android development, tutorial, метки: ,

В последнее время на Хабре стали все чаще появляться статьи связанные с разработкой для 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js