Как было сказано ранее, во второй части статьи речь пойдет о типовых приемах написания приложений для android. Однако, вначале несколько слов о Java API, имеющимся в распоряжении разработчика.

Разработчики платформы реализовали значительное число классов из J2SE. Их список расширяется и обновляется с выходом каждого нового релиза. Так, для последней на момент написания этой статьи версии платформы 2.3, в распоряжении программистов имеются классы из пакетов (перечислены не все пакеты):

java.io
java.lang
java.math
java.net
java.nio
java.security
java.sql
java.text
java.util (включая concurrent, atomic, locks, regex, zip)
javax.crypto
javax.net
javax.net.ssl
javax.security.auth
javax.security.cert
javax.sql
javax.xml (включая поддержку для SAX, DOM, XSLT, XPATH)
org.w3c.dom
org.xml.sax

Имеется поддержка для работы с форматом JSON и для создания JUNIT-тестов.

Особо следует отметить реализацию пакета org.apache.http и его подпакетов, что позволяет писать клиентскую часть http приложений с минимальными трудозатратами на обработку особенностей http протокола.

Пакеты с именем android.* предоставляют программисту доступ к различному оборудованию и содержат классы, отражающие особенности программирования для этой платформы. Кроме этого, ряд этих пакетов дублирует функциональность уже упомянутых пакетов, но, с некоторой адаптацией для платформы android (например, android.net.http). Другие добавляют классы и методы, которые, отсутствуют в J2SE (см. android.sax , android.text.* , android.util).

Теперь — о разработке приложений под android. Я попытаюсь рассказать, какие приложения можно написать под android и как это сделать, не перегружая текст блога примерами законченных приложений, достаточное число которых имеется на сайте google и множестве около-google сайтов.

Приложение 1

Приложение, вся работа которого состоит во взаимодействии с пользователем и которое не имеет долгих фоновых задач, является наиболее простым типом приложения. В этом случае приложение представляет собой набор Activity, одни из которых вызывают другие. Поскольку вызов — это обработка некоторого Intent (созданного другой Activity) и происходит в главной нити, которая в том числе обрабатывает и события, генерируемые пользовательским интерфейсом, обработка должна происходить гарантированно быстро. Так, будет неправильно в обработчике пытаться получить информацию с сетевого сервера или обратиться к сетевой файловой системе, используя блокирующий вызов — если сервер окажется недоступным, пользовательский интерфейс зависнет. В качестве примера вызова одной Activity из другой с передачей вызываемой Activity некоторой информации и получением от нее ответа рассмотрим CallerActivity, которая имеет параметры, устанавливаемые в CalledActivity.

public class CallerActivity extends Activity {
    // ....
    // этот метод может, например, вызываться из обработчика нажатия кнопки
    // "Изменить параметры"
    void someMethod(int requestCode) {
        // Создаем объект для передачи вызываемой Activity. Нужно указать
        // экземпляр(!) вызывающей и класс(!) вызываемой Activities
        Intent intent = new Intent(this, CalledActivity.class);
        // Формируем набор параметров для передачи. Это могут быть не только
        // строки. Подробности смотри в javadoc.
        String[] params = { "Edit ", "parameters please." };
        // "цепляем" параметры к передаваемому объекту
        intent.putExtra(INPUT_PARAMS, params);
        // вызываем CalledActivity. После того, как главная нить получит
        // обратно управление, будет создан экземпляр класса CalledActivity и
        // вызван его метод onCreate(), который сформирует новый экран
        startActivityForResult(intent, requestCode);
    }

    // Этот метод будет вызываться, когда CalledActivity возвратит управление.
    // Важно, что "data" это не тот Intent объект, который передавался при ее
    // вызове, но requestCode будет тот же.
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == Activity.RESULT_OK) {
            if (data != null) {
                // получить данные, возвращенные CalledActivity
                String[] params = (String[]) data.getExtras()
                        .get(OUTPUT_PARAMS);
                // сохранить параметры в приватный файл приложения
                // ....
            }
        }
    }
    // ....
}

public class CalledActivity extends Activity {
    // ....
    // Этот метод вызывается при создании данной активити
    public void onCreate(Bundle savedInstanceState) {
        // нужно обязательно вызвать конструктор суперкласса
        super.onCreate(savedInstanceState);
        // ....
        // получить переданный из caller-а объект
        Intent data = getIntent();
        // получить переданные параметры
        String[] params = (String[]) data.getExtras().get(INPUT_PARAMS);
        // ....
    }

    // этот метод может, например, вызываться из обработчика нажатия кнопки
    // "Сохранить параметры"
    private void returnResult() {
        // создаем носитель для возвращаемых значений
        Intent intent = new Intent();
        // Формируем набор возвращаемых значений
        String[] params = { "Done" };
        // "цепляем" параметры к передаваемому объекту
        intent.putExtra(OUTPUT_PARAMS, params);
        // устанавливаем статус, который потом будет анализироваться в
        // CallerActivity
        setResult(Activity.RESULT_OK, intent);
        // возвращаем управление. После того, как главная нить получит
        // обратно управление, экземпляр класса CallerActivity будет извлечен из
        // стека. Затем будет вызван его метод onActivityResult, который
        // обработает переданную из CalledActivity информацию
        finish();
    }
    // ....
}

INPUT_PARAMS и OUTPUT_PARAMS — некоторые константы, определенные в приложении.

Весь пользовательский интерфейс создается в методах onCreate каждой Activity, при этом рекомендуется использование разметок (layout). Разметка подключается в этом методе при вызове метода setContentView(R.layout.ZZZ); Ожидается, что файл ZZZ.xml содержится в каталоге res/layout проекта. Важно то, что интерфейс R — это генерируемый интерфейс, который размещается в каталоге gen проекта, а не одноименный класс R из пакета android. Поэтому при использовании конструкций вида R.id.* до перекомпиляции проекта они могут рассматриваться редактором IDE как ошибочные.

Каждый раз, когда пользователь нажимает кнопку «Назад», текущая Activity завершается и управление передается вызвавшей ее Activity (при условии, что при вызове не была порождена новая задача — task). Таким образом вызвавшая ее Activity может и не получить ожидаемых данных и должна уметь обрабатывать эту ситуацию. Если данная Activity находится на дне стека, то приложение завершается.

Activity может быть завершена или перезапущена системой. Наиболее простой пример — изменение ориентации экрана. В этом случае текущая Activity завершается с вызовом ее метода onDestroy() и рестартует (но уже с новыми параметрами экрана) с вызовом метода onCreate(). Замечу, что вызов onDestroy не гарантируется системой, если приложение выгружается из-за нехватки памяти.

Приложение 2

Теперь рассмотрим архитектуру приложения, которое должно выполнять некоторую фоновую задачу, даже, если на экране выведена Activity другого приложения. Это может быть, например, периодическое получение с сервера некоторой информации и ее обработка. Главная особенность этой архитектуры состоит в том, что фоновая задача выполняется в нити, созданной сервисом. Итак, приложение состоит из:

  • Activity, задача которой состоит в том, чтобы позволить пользователю управлять процессом. Она запускается пользователем при необходимости и может быть завершена пользователем или системой. Эта компонента получает доступ к сервису (запуская его, если он еще не запущен) с помощью метода bindService и передает ему команды пользователя.
  • сервиса, который запускает и останавливает фоновую нить.

Как уже отмечалось, система может в любой момент остановить и выгрузить из памяти любую компоненту, но любой сервис будет выгружен только после выгрузки всех неактивных (не видимых на экране) Activity. То что нить запускается из сервиса, гарантирует ей существенно меньшую вероятность случайной выгрузки, чем когда бы она запускалась из Activity.

Далее приведено словесное описание и код одного из возможных сценариев. Чтобы избежать слишком длинных трудновоспринимаемых фраз, там, где это возможно, некоторые слова пропущены. Надеюсь обращение к коду позволит устранить все двусмысленности.

Для работы с сервисом Activity нужно:

  1. создать экземпляр наследника класса Handler, в котором будет обрабатываться ответ сервиса (конечно, следовало бы сказать в «методе … класса» и т.п., но, как я уже отметил, для краткости подобные уточнения будут опускаться).
  2. создать экземпляр класса Messenger и передать при создании его конструктору созданный экземпляр Handler. Этот объект будет передаваться сервису, чтобы сервис мог вернуть данные.
  3. создать экземпляр класса, реализующего интерфейс ServiceConnection. Методы этого интерфейса будут вызываться в момент установки и разрыва соединения с сервисом. В методе установки соединения можно получить и запомнить объект Messenger, для посылки команд сервису.
  4. в методе onCreate, например, следует вызвать метод bindService и передать ему в качестве параметра созданный экземпляр класса ServiceConnection
  5. когда требуется послать команду сервису следует использовать метод send объекта Messenger из п.3. Для передачи данных сервису нужно создать и заполнить экземпляр объекта Bundle, поместить его объект Message и передать этот Message в качестве параметра вышеупомянутому методу send.

Сервис при этом может использовать следующий шаблон:

  1. создать экземпляр наследника класса Handler, в котором будут обрабатываться команды и возвращаться ответ. Соответствующий метод этого класса будет получать объект Message, сформированный Activity.
  2. создать экземпляр класса Messenger и передать при создании его конструктору созданный экземпляр Handler.
  3. реализовать метод onBind, возвращающий созданный объект Messenger
  4. для возврата данных Activity, сервису следует вызвать метод send объекта Messenger. Этот Messenger содержится в поле replyTo объекта Message, полученного экземпляром Handler из п.1.

Вот пример кода сервиса, в котором реализованы все указанные шаги (обратите внимание на обработку сообщения TEST_CODE):

public class TestService extends Service {
    public static final int TEST_CODE = 1;
    public static final int START_THREAD = 2;
    public static final int STOP_THREAD = 3;
    // объект для обработки команд от Activity и посылки ответа
    private Messenger mMessenger = new Messenger(new Handler() {
        public void handleMessage(Message msg) {
            try {
                switch (msg.what) {
                case START_THREAD:
                    // start background thread
                    break;
                case STOP_THREAD:
                    // stop background thread
                    break;
                case TEST_CODE:
                    Bundle bundle = new Bundle();
                    // извлекаем принятые от Activity данные и формируем тестовые данные
                    bundle.putSerializable("date",
                        msg.getData().getSerializable("pref") + " : " + new Date());
                    // создаем экземпляр класса Message
                    Message m = Message.obtain(null, TEST_CODE);
                    // помещаем тестовые данные в message
                    m.setData(bundle);
                    // используя содержимое поля replyTo посылаем данные обратно Activity
                    msg.replyTo.send(m);
                    break;
                default:
                    super.handleMessage(msg);
                }
            } catch (Exception e) {
            }
        }
    });
    public IBinder onBind(Intent intent) {
        return mMessenger.getBinder();
    }
}

А вот соответствующая Activity (не относящиеся к делу части кода опущены для краткости):

public class TestActivity extends Activity {
    // объект для посылки команд сервису
    private Messenger mService = null;
    // объект для обработки ответов сервиса
    private Messenger mMessenger = new Messenger(new Handler() {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case TestService.TEST_CODE:
                    // получить данные, возвращенные сервисом и что-то с ними сделать
                    Object obj = msg.getData().getSerializable("date");
                    // ...
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    });
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName name, IBinder service) {
            mService = new Messenger(service);
        }
        public void onServiceDisconnected(ComponentName name) {
            mService = null;
        }
    };
    // посылаем команду сервису
    // подробности смотри в аналогичном коде сервиса
    private void testService() {
        Bundle bundle = new Bundle();
        bundle.putSerializable("pref", "Date");
        Message msg = Message.obtain(null, TestService.TEST_CODE);
        msg.setData(bundle);
        // заполняем поле объектом, который сервис будет
        // использовать для посылки ответа
        msg.replyTo = mMessenger;
        try {
            mService.send(msg);
        } catch (RemoteException e) {
        }
    }
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // ...
        // устанавливаем соединение с сервисом
        boolean res = bindService(new Intent(this, TestService.class),
                mConnection, Context.BIND_AUTO_CREATE);
        // анализ значения res - успешности соединения с сервисом
        // ...
    }
}

Важно знать, что вызов send является синхронным и выполняется в главной нити. Поэтому он должен как можно быстрее возвратить управление. Нить, запускаемая сервисом для выполнения фоновой задачи, может быть как обычной нитью, так и неявной нитью, создаваемой при инициализации задачи таймера (Timer, TimerTask классы), неблокируемого nio вызова, классом HandlerThread и т.п..

Возможно использование и других подходов для выполнения поставленной задачи.

Приложение 3

Устройства имеют т.н. спящий режим, в который они входят после некоторого периода неактивности пользователя. В этом режиме все приложения, включая фоновые, останавливаются. Для приложений, которые должны выполняться, несмотря на спящий режим, в android имеется специальное API.

Предположим, что приложение должно посылать некоторую информацию на сервер через определенные промежутки времени, независимо от того «спит» устройство или нет. Здесь не будет подробно рассматриваться данный тип приложений, однако рекомендации по их реализации будут приведены.

Нужно решить 2 задачи. Первая — это выйти из спящего режима в определенный момент времени. Вторая — не допустить переход в спящий режим пока не будет выполнена задача (послана информация на сервер).

Для выполнения первой задачи следует использовать класс android.app.AlarmManager . Этот класс позволяет пробуждаться приложению вне зависимости от состояния устройства. При этом могут использоваться различные стратегии повторных пробуждений.

Для решения второй задачи имеется класс android.os.PowerManager, который позволяет «захватить» WakeLock объект, что не позволит устройству перейти в спящий режим, пока он не будет «освобожден».

Подробности смотри в javadoc для этих классов.

Приложение могло бы содержать:

  1. Activity, с помощью которой пользователь управляет приложением и которая взаимодействует с сервисом и устанавливает расписание пробуждения для Broadcast receiver-а.
  2. Broadcast receiver, который будет пробуждаться AlarmManager-ом в указанное время и при выборе соответствующей стратегии инициировать повторное пробуждение через определенное время.
  3. Сервис, который будет вызываться из Broadcast receiver-а и запускать фоновую нить для выполнения нужной задачи. Нить при запуске должна захватывать WakeLock, чтобы предотвратить «засыпание» устройства во время своей работы. По окончании работы нить должна передать результат работы сервису или записать в файл и освободить WakeLock.

Здесь нужно разъяснить следующую особенность. Хотя на период работы метода обработки события пробуждения Broadcast receiver-а и автоматически захватывается WakeLock, он освобождается сразу после завершения этого метода. Вся обработка проходит в главной нити и должна быть завершена как можно быстрее. Именно поэтому запускаемая сервисом нить должна захватить свой WakeLock, поскольку во время ее выполнения первый лок будет уже освобожден.

В заключение будет кратко рассмотрен механизм логирования в android. Для вывода сообщений в лог в android используется объект android.util.Log . Он имеет метод i(), соответствующий уровню INFO, e() — уровню ERROR и т.д. Методы принимают в качестве первого параметра идентифицирующую строку (это может быть имя класса, метода или что-то еще), а в качестве второго строку сообщения. Возможно задание и третьего аргумента типа Throwable. Чтобы увидеть лог в IDE Eclipse нужно открыть окно LogCat из меню Window -> Show View -> Other -> Android. Для production версии рекомендуется удалить все логирование. Одним из способов сделать это является добавление перед каждым оператором логирования условного if от глобальной константы. Например:

if (LOG_ENABLED) { Log.e(TAG, e.getMessage()); }

Для тестовой версии эта константа устанавливается в true, а для production — в false. «Правильный» компилятор должен удалить неиспользуемый код.

  • Sharing

    Facebooktwittergoogle_plusredditpinterestlinkedinmailby feather