Dmitry Astapov (_adept_) wrote,
Dmitry Astapov
_adept_

Category:

Снятся ли андроидам электро-грабли?

Я тут потратил несколько вечеров, чтобы написать програмку под андроид. Очень простую (3 активности, пять кнопочек, два calendar view, одна база sqlite). Остаток поста - о том, какие грабли мне попались по пути, и кто, интересно, их там разложил?

Кто пишет под андроид - может почитать и поржать. Кто не пишет, но собирается - может почитать и подумать, так ли оно ему надо? Кто пишет под iOS - может почитать и позлорадствовать, что с Apple все по-другому.

Full disclosure: последний раз я что-то делал на Java четыре года тому назад, и каждый раз, когда у меня возникали какие-то проблемы, я шел прямиком в google и искал по характерным ключевым словам. Я намерено не собирался читать толстых фолиантов типа "The Absolute Definite Android Programming Guide and Reference", т.к. книг много, а хороших книг - мало, да и те быстро устаревают. Так что только гугл + stackoverflow + developer.android.com - такой себе "тяп-ляп и в продакшн".

Первый звоночек был сразу после установки Android Studio. Оказывается, сразу после установки надо сделать определенные действия - открыть SDK manager и скачать все, что оно посчитает нужным. Сама студия об этом молчит - нет, чтобы показать что-то такое при первом запуске. Информация об этом показывается на странице, с которой ты скачиваешь студию, но появляется она там через сколько-то секунд после начала скачивания. Я к этому времени эту страницу уже благополучно закрыл :)

В результате ты рисуешь интерфейс своего первого приложения в графическом дизайнере гуя, и все прекрасно и замечательно, пока ты не добавляешь туда тривиальное поле для ввода текста. И тут твой красивый гуй пропадает, а вместо него появляется загадочная надпись "java.lang.system.arraycopy(ci cii)v". Можно легко проверить, что интернет полон страдальцев, бьющихся головой об эту надпись, а ларчик открывается просто - пока вы не запустите SDK manager, и не накачаете себе всякого разного добра, которое вам предложат по умолчанию, у вашей студии будет только один вариант SDK, который она и будет использовать. Это SDK для Android Wear, то бишь для часов. И куча элементов интерфейса для них просто "не бывает", и вот это самый exception - это способ сообщить пользователю об этом. Я было думал, что это в новой студии такие косяки, а в старом добром эклипсе все ок, но разведчики доносят, что в эклипсе - точно такая же фигня. Ладно, запустил SDK Manager, скачал все что надо, поехали дальше. (ссылка на StackOverflow)

Пару простеньких примеров с developer.android.com собрались, но при попытке запустить их в эмуляторе я обнаружил, что эмулятор запускается через раз. Опытным путем выяснилось, что если попросить "use snapshot", то эмулятор работает, а без этой опции - нет. При помощи strace и такой-то матери было выяснено, что опция "use snapshot" несовместимо с использованием опции "use opengl rendering", и включая-выключая "snapshot" я фактически включал-выключал opengl. А с ним проблемы, если у тебя JDK 7 или выше, linux, используется emulator-arm и луна - в первой четверти. У меня был именно такой JDK, linux, и луной судя по всему тоже повезло, т.к. эмулятор с opengl у меня так и не завелся. Ладно, буду запускать с отключенным, поехали дальше (ссылка на отдаленно имеющих отношение к делу баг, который помог понять, что это в принципе может быть).

Для разминки я решил написать приложение, которое трекает даты. Знаете такие таблички типа "Уже X дней работаем без происшествий"?. Ну вот, чтобы можно было туда вводить даты, оно показывало, сколько дней прошло с последней введенной даты, и можно было посмотреть историю - какие даты вводились раньше и сколько дней между ними прошло. Как раз для разминки - не очень просто, но и не очень сложно.

И вот я делаю в своем приложении MainActivity, у которого в layout есть кнопка, давишь на нее - открывается CalendarActivity, в котором CalendarView, чтобы можно было выбрать дату. И тут у меня начинаются открытия - одно за другим, только успевай записывать.

Во-первых, у CalendarView рекомендуется повесить обработчик на событие onSelectedDateChange, но вот незадача - текущий день уже selected, и сделать так, чтобы никакой день не был selected - нельзя. Но чтобы разработчик не скучал, сделано вот что - если взять и поскроллить календарик (не меняя выбранную дату), то ВНЕЗАПНО выстрелит событие selectedDateChange, возможно даже несколько раз. Интернеты предлагают в обработчике события "изменилась дата" проверять, РЕАЛЬНО ли изменилась дата, и таким образом понимать, изменил пользователь дату или просто поскроллил календарь. Я спасовал против этой логики, выкинул обработчик onSelectedDateChange, и добавил позорную кнопку "Ok", на которую пользователь должен нажать, чтобы подтвердить выбор даты (ссылка на StackOverflow). Тут активность с календарем стала открываться по 10 секунд, но добрая студия подсказала мне, что не надо делать календарю layoutHeight=wrap_content, если другие атрибуты говорят "отдай календарю все, что осталось от других элементов интерфейса".

Далее выяснилось, что внешний вид календаря более-менее прибит гвоздями - текущая неделя всегда выделяется другим цветом фона, а у текущей даты слева-справа от числа будут две "палочки", но это и все. Как сделать у выбранной даты другой цвет фона я так и не нашел, и таких страдальцев, опять же, полон интернет, и всем им советуют - "просто возьмите другой third-party календарь". Теперь я по крайней мере понял, почему во всех приложениях с календарями эти календари разные.

Ладно, я решил, что буду жить со стандартным CalendarView - по крайней мере, пока. Скомпилировал свой пример, поставил на свой телефон, открыл и увидел календарик с мааааахонькими циферками - намного меньшими, чем остальной текст. Как оказалось, у меня на телефоне Android 4.1, а в нем CalendarView поломали - отрисовка дат происходит без использования задаваемого пользователем (или темой) размера шрифтам. В 4.2 уже починили, в 4.0 еще не поломали, а у меня - вот так. Стало еще более понятно, почему все любят кастомные календари. (ссылка на StackOverflow - там видно, как это выглядит).

Ура, теперь у меня работает выбор даты. Дальше я добавил класс для работы с базой SQLite, создал там табличку для хранения дат - все "по учебникам". Даты выбираются, даты сохраняются - красота.

Пришло время считать интервалы между датами. Тут у меня был хитрый план - есть база, sqlite умеет нормально исполнять достаточно сложные запросы, поэтому почему бы не посчитать почти все, что нужно, силами SQL-запроса:
select _id, the_date, prev_date, julianday(the_date)-julianday(prev_date) as duration 
from (select _id,the_date,
             coalesce((select max(the_date) 
                       from dates 
                       where the_date < d.the_date),
                      the_date) as prev_date 
       from dates d) foo;
1|2014-09-01|2014-09-01|0.0
2|2014-09-10|2014-09-01|9.0
3|2014-09-20|2014-09-10|10.0


Засовываю я этот запрос в db.rawQuery("..."), и получают ошибку во время исполнения - "column not found: the_date". Как же как unknown, вот же она! Неа, говорит мне какая-то библиотека из дебрей андроидного SDK, ты мужик меня не обманешь - раз я сказала "нету", значит нету. Как назло, текст ошибки такой, что в гугле находится куча всего постороннего, и 100500 несчастных, которые реально указали не то имя колонки. Но у меня-то в sqlite3 все работает, дело точно в чем-то другом. Попробовав и так и сяк я плюнул и решил, что просто сделаю view, и буду запрашивать данные уже оттуда.

Добавил в метод создания базы вызов db.executeSql("CREATE VIEW AS ..."), и ... получил ту же самую ошибку! Оказалось, что библиотека для работы с sqlite в Android SDK пытается парсить все(!) запросы, которые ты собираешься выполнять. И когда запрос слишком сложный для нее, она валится с вот такой вот диагностикой. Я как-то могу понять, зачем это делать в rawQuery (чтобы получить имена колонок для Cursor-а), но зачем это делать в executeSql - я понять не могу.

Ладно, фиг с ним, посчитаем разницу вручную. Для работы с датами предлагается java.util.Calendar - что в нем есть для подсчета количества дней между датами? Быстрый просмотр доступных методов ничего не дал, и я пошел в гугл. И нашел вот такой ответ на StackOverflow. Настоятельно рекомендую сходить и почитать, памятуя о том, что на дворе у нас 21 век, эра победившего tzinfo и все такое прочее. Вот вам для затравки один из ответов оттуда:
int difference= 
((int)((startDate.getTime()/(24*60*60*1000))
-(int)(endDate.getTime()/(24*60*60*1000))));


Я, каюсь, был насколько впечатлен увиденным, что в результате тоже использовал позорное:
TimeUnit.MILLISECONDS.toDays(curr.getTimeInMillis()-prev.getTimeInMillis());


Короче говоря, приложение я написал, но первое впечатление об андроиде и его SDK у меня сложилось далеко не самое благоприятное. Рассказывайте теперь, как надо было делать правильно :)
Tags: android
Subscribe

  • Post a new comment

    Error

    default userpic

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 69 comments