company_banner

Советы по созданию приложений к окончанию набора в Школу мобильной разработки Яндекса

  • Tutorial
Уже очень скоро завершится набор в Школу мобильной разработки, которая традиционно пройдет в Москве. Упор в ней будет сделан на практические занятия — командные мини-хакатоны, в которых помимо написания кода нужно будет принимать решения, разбираться с возникшими спорными вопросами и заниматься долгосрочным планированием. Помогать студентам — каждой команде индивидуально — будут ребята из Яндекса. Более подробно о предстоящей школе можно почитать здесь. Мы закончим принимать заявки 6 мая в 23:59 по московскому времени, а пока ещё есть время на выполнение заданий, мы решили разобрать прошлогодний вариант. Вы узнаете, какие ошибки часто допускают начинающие разработчики и чему следует уделить внимание при написании кода вашего первого приложения.



Традиционно задание построено так, чтобы мы могли обратить внимание на разные аспекты разработки. К ним относится архитектура приложения, стабильность, производительность, верстка, удобство использования. Все составляющие одинаково важны: даже идеально причесанный и разложенный на слои код с большой вероятностью не пройдет отбор, если возникнут проблемы в интерфейсе или падения в процессе выполнения базовых пользовательских сценариев. Универсального рецепта приготовления идеального приложения, которое гарантированно пройдёт отбор, нет. Есть множество подходов к разработке и разные варианты построения архитектуры, но одна из составляющих успеха — позитивные пользовательские ощущения. Продукт должен создавать впечатление законченности, независимо от того, сколько в нем полезной функциональности, экранов или элементов.

Содержание



Репозиторий и coding style


Театр начинается с вешалки, а проект начинается с репозитория.

Перед началом разработки добавьте соответствующий .gitignore-файл, чтобы лишние файлы точно не попали в ваш репозиторий. Так, не рекомендуется хранить в репозитории файл настроек вашей IDE, файлы с ключами для подписи приложения и любые логины и пароли.

Во время разработки следуйте золотой середине — не стоит коммитить полпроекта за раз, но и множества мелких коммитов по одной-двум строкам стоит избегать. Пишите понятные сообщения к каждому коммиту. Для отдельных фич стоит использовать ветки, чтобы разрабатывать фичи независимо и вcегда иметь актуальную (и работающую) версию приложения в master-ветке. Старайтесь сначала реализовать минимально необходимые фичи или лишь затем все «красивости» и улучшения. Если вам нужно переключиться на другую ветку и вы не хотите коммитить незавершенный код (но и терять его не хотите), удобно использовать заначки.

Код пишется один раз, а читается многократно, поэтому важно, чтобы coding style и наименование переменных были адекватными. Оставляйте осмысленные комментарии, если они нужны, не добавляйте магические числа и не перемешивайте код из различных библиотек, которые делают одно и то же. Если код пишется одним разработчиком, то поддерживать общий стиль и подход легко, а в большой команде можно использовать различные анализаторы или сформировать свой стиль и закоммитить его в систему контроля версий, чтобы он подхватывался у каждого разработчика.

Вот на что еще стоит обратить внимание:

  • хардкод, бесполезный код, закомментированный код,
  • лишние вызовы super,
  • лишние аннотации,
  • отсутствующие аннотации,
  • попадание в Release кода, необходимого только в Debug-сборке
  • отсутствующий модификатор final,
  • длинные методы,
  • одинаковые методы,
  • unused imports,
  • new Something() без присваивания,
  • magic numbers,
  • копипаста, использование кода, которого вы не понимаете,
  • и еще — юзайте lint.

SDK и библиотеки


Использование Android SDK и библиотек часто вызывает вопросы у начинающих разработчиков. Ниже мы перечислим некоторые типичные проблемы и их решения.

Неправильная работа со списками
Использовать LinearLayout + ScrollView для отображения множества однотипных вью — значит совершить ошибку. Такой способ создает нагрузку на память, все вью находятся в ней одновременно. Скролл начинает тормозить. Правильно в таком случае использовать как минимум ListView, а лучше — более продвинутый RecyclerView. Эти контейнеры обеспечивают переиспользование вью (с помощью механизма адаптера). Вью, которые в данный момент не видны пользователю, наполняются свежими данными.

Но и тут можно совершить ошибку. RecyclerView, в отличие от ListView, заставляет разработчика пользоваться паттерном ViewHolder. Как и следует из его названия, этот паттерн состоит в создании класса, объекты которого хранят ссылки на уже найденые в иерархии вью. Не следует вызывать findViewById каждый раз, когда нужно проставить некоторое значение. Нужно сделать это один раз, во время создания холдера.

Сохранение состояния
Ещё одна большая проблема начинающих — сохранение состояния приложения при смене конфигурации (например, когда меняется ориентация, язык, размер экрана и т. д.). Часто бывает, что новички не тестируют такие случаи. Это приводит к недовольству пользователя или даже крешам. Иногда разработчики останавливаются на фиксации ориентации — что, конечно, не спасает. Существует несколько способов поддержать смену конфигурации «из коробки». Не будем углубляться в это, всегда можно почитать документацию. Главное помнить, что такая проблема существует, тестировать, обращать внимание на диалоги (лучше использовать DialogFragment, а не AlertDialog), поскольку они тоже должны восстанавливать состояние. Нужно помнить, что хранить состояние экрана в персистентом хранилище (например, SharedPreferences) не рекомендуется. В итоге «чистый» запуск может привести к уже имеющемуся состоянию, а это не особо идиоматично.

Загрузка данных и кеширование
Ни для кого не секрет, что хождение в сеть на UI-потоке в Android запрещено. Для сети есть множество библиотек, например OkHttp, если у вас REST — добавьте Retrofit, а для загрузки картинок — Glide или Picasso. Таким образом, давно уже не нужно использовать HttpUrlConnection внутри AsyncTask (особенно для загрузки картинок — что на самом деле непросто). Но многие начинающие не задумываются, что, например, чтение и запись в БД — это тоже I/O-операция, которая может сказаться на UX из-за фризов главного потока. То же самое относится к чтению файлов, доступу к ContentProviders. Все подобные операции должны происходить в отдельном потоке. Что для этого использовать — каждый решает сам, описать всё разнообразие решений в этом формате не представляется возможным.

Изменение системного поведения кнопки Back
Часто возникает соблазн повесить туда нестандартную для системы реакцию, в том числе полностью проглотить это событие (обработать, но ничего при этом не сделать). Так вот — лучше не надо. У пользователя этой ОС есть привычки, и Back должна вести на предыдущий экран или приводить к выходу из приложения.

Архитектура


Архитектура Android-приложения часто бывает больным местом даже для опытных разработчиков. В этом разделе мы дадим несколько общих советов. Следовать ли им — решаете вы.

Архитектура и декомпозиция
Отсутствие декомпозиции — признак плохой архитектуры. Платформа не дает четких указаний, как правильно писать приложения, поэтому есть большой соблазн писать весь в код в Activity или Fragment. В итоге код становится нетестируемым, тяжело вносить любые изменения. Чтобы избежать такой ситуации, можно применять современные архитектурные практики и шаблоны проектирования. Почитайте про MVP, MVVM, MVI. Напишите первые три класса и покройте их тестами. Вы наверняка заметите, что писать тесты сложно и нужно думать над архитектурой.

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

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

UI (ресурсы, графика, верстка)


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

Качество верстки


Быстродействие в Android довольно сильно зависит от качества верстки. Сложные иерархии контейнеров дольше обсчитываются. Кроме того, при большой вложенности возможны падения во время обсчета и отрисовки (из-за переполнения стека). С появлением ConstraintLayout (и особенно c установкой его корневым элементом xml при создании из визарда) у новичков значительно усложнились иерархии. Чаще используются вложенные Relative/ConstraintLayout, что в корне неверно. ConstraintLayout предназначен как раз для того, чтобы делать иерархию плоской. Рекомендуем прочитать хотя бы введение в документацию и стараться применять этот класс правильно. Также избегайте излишней вложенности ViewGroup.

Дизайн


Не все разработчики имеют навыки дизайнера. Часто в приложении бывает неконсистентная цветовая палитра, которая нравится разработчику, но большинству пользователей — нет. Бывает, что надписи не могут прочесть не только люди с ограниченными возможностями (например, дальтоники), но и остальные пользователи. Стандартная проверка: если надписи видны в grayscale, скорее всего, большинство пользователей тоже их разглядят. Для выбора палитры цветов и общих принципов дизайна можно посмотреть две ссылки: material.io и www.materialpalette.com.

Что еще стоит сделать


  • Проработайте аспекты, которые дают ощущение законченности приложения: название, иконку, экран about.
  • Адаптируйте внешний вид приложения под различную плотность точек на экране.
  • Уберите неиспользуемые строковые, графические и другие ресурсы.
  • Выполните локализацию на другие языки.
  • Если выполнен предыдущий пункт, убедитесь, что локализовали всё, что нужно.

Обработка ошибок


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

При обработке ошибок можно впасть в две крайности — обрабатывать всё и вся не задумываясь или не обрабатывать ничего. Второй вариант, очевидно, всплывает довольно быстро, а вот первый более коварен и приводит к непредсказуемым последствиям.

Остановимся немного подробнее на некоторых примерах таких ошибок.

try..catch...everywhere


Такая обработка, как следует из названия, состоит в заключении любого подозрительного (с точки зрения автора) кода в блок try..catch. Под раздачу попадают NPE и IndexOutOfBoundsException, IllegalArgumentException и даже OutOfMemoryError, то есть исключения, которые обычно говорят о логических ошибках в приложении, о состояниях приложения, из которых нельзя адекватно восстановить его работу. Конечно, правильным решением будет исправление логических ошибок. Кроме того, при написании кода на Java можно воспользоваться статическим анализом и проставить, как минимум, аннотации @NonNull и @Nullable везде, где нужно. Это поможет отловить NPE.

WeakReference<Everything>


Часто новички, узнав об утечках памяти, начинают бояться их как огня. При недостатке опыта это может обернуться оборачиванием любых объектов в WeakReference. Обычно такой прием говорит о плохом понимании жизненного цикла объектов и связей между ними. Вот пример:

public class MyAdapter extends RecyclerView.Adapter<ViewHolder> {

    private final WeakReference<Context> contextRef;

    public MyAdapter (@NonNull Context context) {
        this.contextRef = new WeakReference < > (context);
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        final Context context = contextRef.get();
        return context == null ? null : LayoutInflater.from(context).inflate(R.layout.item, parent, false);
    }
    ...
} 

Здесь в конструктор адаптера приходит ссылка на Context. Но адаптер — это часть UI, который показан в некой Activity. Следовательно, он не должен жить дольше, чем Context, а из этого следует, что WeakReference здесь не нужна.

Другой пример исопльзования WeakReference:

public class MyAsyncTask extends AsyncTask<String, String, String> {

    private final WeakReference<Context> contextRef;

    public MyAsyncTask(@NonNull Context context) {
        this.contextRef = new WeakReference<>(context);
    }

    ...

    @Override
    protected void onPostExecute(@Nullable String result) {
        final Context context = contextRef.get();
        if (context != null && result != null) {
            Toast.makeText(context, result, Toast.LENGTH_SHORT).show();
        }
    }
} 

С виду ничего не изменилось, но AsyncTask способна пережить Activity, в которой она была создана. Следовательно, использование WeakReference здесь обосновано.

Чтобы избежать утечек памяти, в первую очередь следует понимать, каков жизненный цикл объектов, которые вы используете. Для поиска утечек памяти в существующем большом проекте можно использовать Memory Profiler и LeakCanary.

Отсутствие отмены запросов


Иногда причина ошибок кроется в отсутствии отмены асинхронных операций, сетевых запросов или таймеров. Учитывайте долговременность и периодичность этих операций, обращайте внимание на парные методы API вроде subscribe/unsubscribe, create/destroy, start/cancel.

Тесты


Отсутствие юнит-тестов


Мы любим юнит-тесты за то, что при создании продукта они помогают писать более качественный и поддерживаемый код, а также служат своего рода документацией, которая не устаревает со временем. Некоторые разработчики пренебрегают юнит-тестами — говорят, что их сложно и долго писать, их запуск занимает много времени.

Каждый из перечисленных пунктов можно оспорить. Начнем с пункта про временные затраты. Возможно, в самом начале написание юнит-тестов будет отнимать много времени, но с опытом скорость будет расти. Кроме того, юнит-тесты — инвестиция в будущее, которая в дальнейшем позволит писать код быстрее. Проблема со скоростью запуска решается еще проще: достаточно один раз настроить Continuous Integration и прогонять тесты автоматически. Если юнит-тесты сложно писать, значит, вы пишете не самый качественный код. Хорошая архитектура облегчает задачу.

Бесполезные тесты


Бесполезные тесты не только затрудняют понимание исходного кода, но и влияют на время сборки приложения, так что это даже хуже, чем бесполезный код. Можно выделить несколько случаев, когда тесты бесполезны:

Тестирование чужого кода
Предполагается, что чужой код уже поставляется с тестами, поэтому такое тестирование только отнимает время.

Тесты зависят друг от друга или от внешних факторов
Зависящие друг от друга тесты сложно поддерживать, так как изменение одного теста влияет на множество других. Кроме того, тестирование становится непредсказуемым, поскольку тесты могут исполняться в разном порядке.

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

Заключение


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

  • Внесение исправлений в последнюю минуту без проверки работоспособности.
  • Отсутствие или некорректность обработки ошибок в коде.
  • Неверная трактовка описания метода API, упущение из виду каких-нибудь особенностей работы или ограничений компонентов платформы.
  • Недостаточная внимательность при организации связей между компонентами, при работе со ссылками и при учете жизненного цикла объектов.
  • Использование новых непроверенных технологий.

Всем поступающим в школу в этом году и тем, кто разрабатывает свое первое приложение, мы рекомендуем:

  • Быть внимательнее и проверять работоспособность приложения даже после внесения безобидных с виду изменений.
  • Изучать тонкости языка: принципы работы с объектами в памяти, виды ссылок.
  • Использовать статический анализатор кода и дополнительные инструменты вроде LeakCanary.
  • Изучать ограничения и особенности работы используемых компонентов платформы и библиотек.
  • С осторожностью использовать новые технологии, не успевшие себя зарекомендовать на известных проектах. API или код может быть нестабильным, в сети может не найтись документации или разборов типовых ошибок, ответы на вопросы придется искать самостоятельно.

Полезные ссылки


Cовсем начинающему разработчику, который не написал ни единой строчки для Android, полезно начать свой путь с курсов от Google на Udacity (на английском с русскими субтитрами). Если не бросите курс после первых несколько видео и пройдёте его до конца, то получите не только хорошие теоретические знания, но и первое приложение, которые вы сделали сами!

YouTube-канал Android Developers — просто кладезь коротких обучающих видео и новостей из мира Android-разработки. Советуем посмотреть Android Perfomance Patterns, ведь производительность важна, не так ли?

Чтобы не отставать от трендов и всегда знать, куда развиваться дальше, можно каждую неделю по чуть-чуть учиться новому с помощью рассылки Android Weekly (на английском) или слушать по дороге домой подкасты, например Fragmented и AndroidDevPodcast.

Знакомство с Kotlin лучше всего начать с Kotlin koans — можно получить представления о базовом синтаксисе языка. А видеокурс от Computer Science Center стоит пройти, чтобы подробнее изучить язык, узнать о его плюсах и минусах, понять, почему он реализован именно так. В случае с Java также есть фундаментальный курс от Computer Science Center — теоретические знания подкрепляются с помощью большого количества практических заданий.

Посмотреть на реализации распространнёных архитектур можно в репозитории Android Architecture Blueprints.

Для изучения архитектурных паттернов часто рекомендуют книги, которые новичку сложно осилить. Для первого знакомства идеально подходит SourceMaking — материал хорошо структурирован, а к большинству паттернов есть понятные метафоры и примеры реализации на Java.

Завершив онлайн-курс от Computer Science Center по алгоритмам, вы познакомитесь с основными алгоритмическими методами и набьёте руку, реализовывая классические алгоритмы. Также в курсе можно найти хороший список литературы. Если практики окажется недостаточно, всегда можно потренироваться на www.hackerrank.com. Только не стоит тратить время на задачи с уровнем сложности Easy — они слишком простые.

Изучить основы git позволит learngitbranching.js.org, а более продвинутые возможности можно освоить в мобильном приложении Enki.

Тем, кто решит пользоваться git исключительно из консоли, лучше изучить базовые команды Vim, например с помощью игры vim-adventures.com.
Яндекс 284,95
Как мы делаем Яндекс
Поделиться публикацией
Комментарии 10
    0
    Спасибо за инфу!
      0
      Что вы думаете по поводу использования Kotlin Coroutines для I/O операций в место потоков?
        0

        Почему бы и нет, когда они выйдут из экспериментального статуса. Сейчас тащить в прод как минимум опрометчиво.

          0

          Пока что rx в среднем сильно понятнее как концепция и проще в миграции, если только мы не говорим о каких-то совсем простых вещах без переключения потоков.

          0
          Вопросы в коде:
          public class MyAsyncTask extends AsyncTask<String, String, String> {
          
              private final WeakReference<Context> contextRef;
          
              public MyAsyncTask(@NonNull Context context) {
                  // Разве здесь должно быть не так?
                  // this.contextRef = new WeakReference<>(context);
                  this.contextRef = new WeakReference(context);
                  // И в чём разница?
              }
          
              ...
          
              @Override
              protected void onPostExecute(@Nullable String result) {
                  final Context context = contextRef.get();
                  if (context != null && result != null) {
                      Toast.makeText(context, result, Toast.LENGTH_SHORT).show();
                  }
              }
          }


          Спасибо.
            0

            Всё верно, должно быть с <>. Это т.н. diamond operator, позволяет опускать полный тип. В процессе редактирования потерял скобки, уже поправил, спасибо.

            0
            А что насчет следующего задания в системе Яндекс.Контест, как подготовиться к нему? И почему там нет языков типа Swift/Kotlin?
              0

              Позволю себе процитировать окончание статьи


              Завершив онлайн-курс от Computer Science Center по алгоритмам, вы познакомитесь с основными алгоритмическими методами и набьёте руку, реализовывая классические алгоритмы. Также в курсе можно найти хороший список литературы. Если практики окажется недостаточно, всегда можно потренироваться на www.hackerrank.com. Только не стоит тратить время на задачи с уровнем сложности Easy — они слишком простые.

              Что касается языков, ограничение было введено намеренно, для прохождения курса желательно знание Java.

                0
                Я далёк от мира андроид-разработки, но краем уха слышал что Kotlin заменяет Java как официальный поддерживаемый язык, вся новая документация пишется Kotlin-first. Можете прокомментировать почему для новичков желательно начинать с Java? Только ли старые проекты поддерживать, как это происходит в iOS-мире с Objective-C, или же что-то большее?
                  0

                  Лично я считаю, что при разработке на Kotlin/JVM под Android нужно знать Java. Несмотря на то, что Kotlin стал официальным языком разработки, это произошло относительно недавно. Старые, как вы говорите, проекты, не такие уж старые.
                  Кроме того, существуют документация, примеры, и исходники, которые всё ещё не переведены.
                  Ну м понимание, что там, под капотом тоже лишним не будет. Смотреть декомпилированный в джаву байт-код приходится иногда.

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

            Самое читаемое