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

Sep 09, 2014 11:22

Оригинал взят у _adept_ в Снятся ли андроидам электро-грабли?
Я тут потратил несколько вечеров, чтобы написать програмку под андроид. Очень простую (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 у меня сложилось далеко не самое благоприятное. Рассказывайте теперь, как надо было делать правильно :)
Previous post
Up