Враг — это я: вскрытие

Вы помните время, когда вы пытались что-то исправить, но все пошло не так, и после того, как вам, наконец, удалось остановить кровотечение и перевести дух, вы понимаете, что на 98% это было сделано вами самими? Добро пожаловать в пост-мортем обновления, который я сделал для своего производственного приложения.

Мне пришлось сделать перерыв в разработке моего регулярно запланированного приложения Checklist, когда я получил отчет об ошибке для TimeGiver, моего опубликованного приложения. К счастью, мне удалось быстро определить причину проблемы в одной строке кода. Я был немного разочарован простой природой проблемы, потому что я разработал бизнес-логику с помощью довольно обширного раунда разработки через тестирование (TDD). К сожалению, это был крайний случай, который я не предвидел и не тестировал. С тех пор я исправил свои тесты с этим пограничным случаем, но я знаю, что существуют другие пограничные случаи, которые я не рассматриваю, поэтому все еще есть некоторый риск, который мне нужно устранить.

Но я ухожу от темы здесь; вернуться к обновлению. TimeGiver — первое приложение, которое я разработал с помощью Flutter, еще когда Flutter был в альфа-версии. Flutter в настоящее время находится в стадии бета-тестирования, но я использовал основную ветку Flutter для своего приложения Checklist, а не более стабильную бета-ветку. Кроме того, язык Dart также обновляется до версии 2 с некоторыми критическими изменениями, в первую очередь переходом к обязательному вводу текста. В текущей бета-версии Flutter Dart 2 является необязательным, но я использую его, потому что мне очень нравится обязательный ввод. Короче говоря, несмотря на изменения, которые мне, возможно, придется внести, я полностью одобряю Dart 2 и с нетерпением жду его появления.

Все это привело к слишком большому количеству изменений в базовой структуре. Я потратил несколько часов, пытаясь заставить обновление работать правильно, прежде чем, наконец, решил отключить флаг --preview-dart-2 во Flutter и вернуться к бета-ветке. В конце концов, проблемой был не мой код, а множество пакетов, которые были ограничены Dart 1. Как только флаг предварительного просмотра был отключен, и я вернулся к бета-ветке, все снова заработало. Урок 1. Не кодируйте рабочие приложения или обновления с помощью передовых версий бета-фреймворка.

TimeGiver — первое приложение, которое я когда-либо создавал во Flutter, когда я еще только изучал основы фреймворка. В результате часть кода — это меньше Микеланджело и больше каракулей 10-месячного ребенка. Я подумал, что если мне все равно придется прикасаться к коду, чтобы исправить ошибку, то я мог бы заняться домашним хозяйством. По мере того, как я углублялся в код, мне хотелось проводить рефакторинг все больше и больше, что занимало все больше и больше времени. Это прямо противоречило моему желанию очень быстро выпустить обновление для исправления ошибки. После часа или двух игр и рефакторинга я не приблизился к решению проблемы. Поэтому я сдался, начал заново и внес минимальное изменение, чтобы решить проблему. В конце концов, я действительно ничего не добился, кроме повышения уровня стресса. Урок 2: Сделайте минимум, необходимый для исправления ошибки. Как только ошибка будет исправлена, очистите и проведите рефакторинг, не теряя времени.

Итак, ошибка была исправлена, и приложение на моем симуляторе iOS заработало, как и ожидалось. Упакуем и выпустим! Что это, маленький голос? Я должен проверить это на Android? Но код Android я не трогал, так что в этом нет необходимости. Уже поздно, я устала и напряжена, и я просто хочу, чтобы это закончилось. Вы ни о чем не беспокоитесь; Оставь меня в покое.

Я лег спать... и проснулся утром с этим:

Хотите угадать, какая ОС была ответственна за все сбои? Оно начинается на «А», заканчивается на «дроид», это моя любимая мобильная ОС как потребитель, которую я больше всего ненавижу как разработчик, и которую я не тестировал накануне вечером.

так в чем была проблема? На самом деле это было не обновление; это была ошибка, которая присутствовала с первого выпуска, но проявилась только после обновления приложения. Прежде чем мы двинемся дальше, я открою вам секрет: единственный код Android, который я написал для приложения, — это отправка уведомлений. Все остальное закодировано с помощью Dart/Flutter, и ни один из кодов Android не тестируется.

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

Немного предыстории: TimeGiver планирует уведомления, чтобы сообщить вам, когда есть, спать и употреблять кофеин во время путешествия. Идея состоит в том, что выполнение этого в соответствии с планом значительно уменьшит последствия смены часовых поясов. Я попробовал это, и я твердо верю, что это работает. План несколько сложен, поэтому я создал это приложение, чтобы за ним было легко следить.

Я планирую эти уведомления, создавая план в Dart и сохраняя его в файле JSON. Затем Dart вызывает некоторый код Java, используя канал платформы. Код Java считывает этот файл JSON, создает объекты уведомления, а затем встраивает каждый объект уведомления в свой собственный PendingIntent, который планируется с использованием AlarmManager. Это выглядело так:

Сами уведомления строятся в методе getNotifications() с помощью такого компоновщика:

Видите проблему? Мне потребовалось некоторое время, чтобы найти его. Я вызываю builder.setSmallIcon() с целым числом ресурсов. Это целое число ресурса используется для поиска значка уведомления когда уведомление передается, а не когда оно создается. Что происходит, когда пользователь устанавливает обновление, которое изменяет идентификаторы ресурсов и идентификатор, сохраненный в уведомлении уже не действует? Приложение вылетает. Поток выглядит так:

Исправление было довольно тривиальным. Вместо того, чтобы сохранять Notification в PendingIntent, я сохраняю Bundle, который содержит заголовок и текст уведомления. Затем NotificationPublisher извлекает эту информацию, создает уведомление и передает его. Я по-прежнему использую тот же метод builder.setSmallIcon(), но поскольку он вызывается при передаче уведомления, а не при создании плана, потенциальной проблемы с идентификатором ресурса нет; он всегда должен возвращать «текущий» идентификатор. Фиксированный код выглядит примерно так:

Урок 3: Всегда тестируйте на всех ОС. При тестировании обновления сосредоточьте ручные тесты на обеспечении правильной работы приложения после применения обновления.

Я думаю, что последний, всеобъемлющий вывод здесь заключается в том, что мне нужно проявлять такой же уровень заботы о незначительных обновлениях, как и о крупных обновлениях. Человеку свойственно относиться к небольшим обновлениям как к менее важным и менее заслуживающим нашего внимания, и этому мышлению нужно активно сопротивляться. Я не видел никаких дальнейших сбоев приложения, но, возможно, это потому, что я оттолкнул всю свою пользовательскую базу. Мне нужно убедиться, что мои пользователи всегда получают лучшее, что я могу предложить. Основной способ добиться этого — убедиться, что все правильно протестировано, сначала с помощью TDD, а затем с помощью ручного тестирования на симуляторе или физическом устройстве. И если я пока не могу найти способ использовать TDD для данного фрагмента кода, значит, этот код требует особого внимания во время ручных тестов.

Огонь уже потушен, и все начинает возвращаться к нормальной жизни. Я возобновляю работу над своим приложением контрольного списка, а также найду время, чтобы очистить TimeGiver и протестировать больше крайних случаев. Этот эпизод, хотя и болезненный, стал хорошим тревожным звонком и преподнес несколько ценных уроков. Теперь пришло время двигаться вперед и становиться лучше.