Пара новостей о Тяжеловато

Пользователи просили нас добавить в Тяжеловато доллары и евро. Мы знали, что если будем добавлять эти валюты, нам придётся добавить запятую на клавиатуру, чтобы можно было вводить дробные суммы.

Это было технически дорого, поэтому мы долго тянули с обновлением. Но вот наконец‑то оно пришло!

Обновление доступно:

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

В будущем мы хотим внедрить в приложение несколько новых классных штук. Но перед этим хочется привести нынешнюю версию в порядок, чтобы работать было приятнее.

Если вам интересно поучаствовать или вы знаете кого‑то, кто сможет нам помочь, напишите, пожалуйста, на почту bespoyasov@me.com.

Что я понял за полтора года преподавания

Полтора года назад я провёл своё первое занятие в Нетологии. С тех пор я читал лекции, проверял домашние задания, менторил дипломные проекты и даже сам был студентом на одном из курсов. За это время я смог осознать для себя некоторые штуки о процессе обучения, которыми решил поделиться.

Будьте аккуратнее с новыми терминами

Если вы объясняете новичкам незнакомые термины, то это стоит делать понятиями, которые им уже известны. Следовать этому правилу труднее, чем кажется.

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

Но как только вы начнёте объяснять это другим людям, у них вполне оправданно появится куча вопросов. Что за маржины, что значит «схлопываются», как сбрасывать флоты, зачем они нужны, откуда отступы, зачем они там?

Чтобы объяснить эти тонкости, надо использовать уже знакомые понятия. Потому что иначе вы не только не объясните, что хотели, но и отобьёте интерес к теме. Никому не хочется разбираться в непонятной фигне, по крайней мере — в чересчур непонятной.

Поэтому объяснение нового термина должно:

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

Избегайте путаницы

Если у термина есть синонимы (флот ⇔ плавающий элемент), надо это пояснить на старте.

Можно также объяснить, почему эти термины взаимозаменяемы. (Флот переводится, как плавающий, а используется такой элемент, чтобы его «обтекал» текст. Поэтому он как бы плавающий, это такой грубый перевод.)

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

Автоматизируйте всё, что можно

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

Часть ошибок — заблуждения, которые возникают при погружении в тему. На такие ошибки стоит заранее приготовить развёрнутое объяснение, почему заблуждение — это именно заблуждение, и как решить задачу было бы правильнее.

Нетипичные ошибки более редкие. Они особенно важны, потому что как бы ставят под вопрос рациональность технологии. На них отвечать надо ещё более полно.

Грамотно объяснить сложные вещи, будучи уставшим, трудно. Именно чтобы не уставать от ответов на типичные ошибки, лучше на них заранее готовить развёрнутый ответ.

Объясняйте на примерах

Объяснение с примерами понятнее. Я пробовал объяснять и абстрактно, и на примерах — на примерах лучше.

Абстракция — ресурсозатратная штука. Даже чтобы представить что‑то уже знакомое, требуется внимание и сосредоточенность. Чтобы представить какое‑то новое понятие, ресурсов потребуется больше.

Примеры будут экономить ученикам внимание и силы. Кроме этого они сильнее связывают новое понятие с каким‑то процессом или образом. И, возможно, как мне кажется, я не уверен, но так ученики лучше запомнят то, о чём вы говорили.

Делайте перерывы во время занятия

Во время объяснений стоит делать небольшие перерывы.

У меня не всегда получается вовремя вспомнить о них, потому что есть ограничение по времени на лекцию. Иногда я просто забываю о них. Но в последнее время стараюсь делать перерывы чаще и спрашиваю, какие есть вопросы.

Ученикам перерыв даёт время передохнуть и восстановить внимание. Вам — перевести дыхание.

Стимулируйте обратную связь

Задавать вопросы страшно и неловко. Но вопросы надо поощрять и стимулировать. Часто перед ответом на вопрос я говорю, что вопрос хороший, логичный или правильный. Не знаю, насколько это снижает страх задавать вопросы, надеюсь, помогает.

Если вопрос задан расплывчато, и вам непонятно, как ответить, просто уточните, что имелось в виду. И, если в чём‑то сомневаетесь, не стесняйтесь гуглить прямо во время занятия. Я так и говорю: «не уверен, давайте прямо сейчас и загуглим».

Пейте воду во время занятия

Мне трудно долго говорить, особенно если приходится следить за таймингом и проговаривать слова быстро и громко. Поэтому я во время лекции иногда отвлекаюсь на глоток воды. Голос не садится к концу, а ещё это помогает сделать вынужденный перерыв.

Пить воду ещё можно, если чувствуете, что плывёте или сильно волнуетесь. Это работает как микротаймаут: пока пьёте воду, можно перевести дух, сформулировать мысль и продолжить.

Дозируйте время на ревью

Любую работу стоит дозировать и разбивать на кусочки. Если два часа подряд ревьюить домашки, то к концу вы будете чувствовать большую усталость, чем если бы чередовали ревью с задачами по другим проектам.

Дело в монотонности, от неё надо избавляться. Чередовать темы, тасовать задачи, менять вид деятельности. Короче, делать всё, чтобы ревью не приедалось. Иначе отвечать свежо не получится и просто пропустить вопрос, на который стоит дать более развёрнутый ответ, или ошибку в решении.

Получайте пользу

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

Если есть возможность заниматься менторством, то пробуйте. Вы глубже поймёте то, что знаете, научитесь объяснять сложные понятия и сможете помочь кому‑то разобраться с новыми для них вещами.

Е2Е тестирование Койна

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

Сейчас в нём три важных сценария: вход в приложение, создание бюджета и запись траты или дохода. Логика покрыта юнит‑тестами, но этого мало. Хочется быть уверенным, что если сценарии где‑то сломаются, то я об этом узнаю сразу. Поэтому для Койна я пишу ещё и Е2Е тесты.

Инструменты

End‑to‑end (Е2Е) тесты — это интеграционные тесты, которые взаимодействуют с интерфейсом так, как это делал бы пользователь. Для них я попробовал несколько инструментов, но больше всего мне понравился Сайпрес.

После его установки и запуска в корне проекта появляется папка cypress/. Внутри неё: integration/ — там находятся сами тесты, и support/ — там вспомогательные функции (об этом подробнее дальше).

Вход в приложение

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

Исхода у сценария два: успешный и неуспешный вход. Пишем тест на первый случай.

describe('Login window', () => {
  it('Valid code passes login', () => {
    // здесь будет логика теста
  })
})

Нам надо зайти в приложение и попасть на страницу логина. На страницу мы зайдём с помощью команды visit, передав аргументом адрес:

// к примеру проверяем приложение локально
cy.visit('localhost:8081')

Проверим, существует ли форма логина, и пустое ли поле ввода. Проверяем наличие нужных блоков и то, что поле пустое:

cy.get('.login').should('have.length', 1)
cy.get('.login-code').should('be.empty')

Выборка элементов в Сайпресе работает похоже на Джейквери. Например, здесь мы выбираем элементы по классам. Метод should проверит, что на странице только 1 элемент с классом login, а элемент с классом login‑code пустой.

Набор текста в настоящих полях ввода в Сайпресе делается через метод type. Но в Койне клавиатура ненативная и настоящих полей ввода там тоже нет. Вместо них — блоки, в которых отображается «набранная» последовательность. Чтобы набрать какой‑то код на нашей клавиатуре, надо «нажать» клавишу с нужной цифрой. Мы будем разбивать код на символы и нажимать на клавиши с указанными символами.

Метод contains ищет элемент, который содержит переданный в аргументе текст, в нашем случае — символ. Метод closest находит ближайшего родителя с указанным селектором, в нашем случае — классом button.

const chars = code.toString().split('')

chars.forEach(char => {
  cy.get('.keyboard')
    .contains(char)
    .closest('.button')
    .click()
})

Когда код набран, можно нажать на красную кнопку, чтобы «отправить» код.

cy.get('.button.is-enter').click()

Код теста целиком будет выглядеть так:

describe('Login window', () => {
  it('Valid code passes login', () => {
    // зайти в приложение
    cy.visit('localhost:8081')

    // проверить форму
    cy.get('.login').should('have.length', 1)
    cy.get('.login-code').should('be.empty')

    // ввести код
    const chars = validCode.toString().split('')

    chars.forEach(char => {
      cy.get('.keyboard')
        .contains(char)
        .closest('.button')
        .click()
    })

    // нажать энтер
    cy.get('.button.is-enter').click()
  })
})

После запуска Сайпрес запустит браузер, прогонит сценарий и покажет, прошёл тест или нет. Выглядит это так: Результат выполнения теста на вход в приложение

Рефакторинг и второй сценарий

Теперь перейдём к тесту с неправильным кодом. Он будет таким же, только код будет другой, и результат будет отличаться. Чтобы не дублировать код, мы можем вынести повторяющиеся действия в функции. Но у Сайпреса есть более изящное решение — команды.

Команды похожи на плагины. Вы описываете функцию‑команду, и она становится доступной глобально через cy. Команды хранятся в папке support/, их можно как угодно разделять по файлам. Главное — импортировать их в support/index.js, чтобы Сайпрес их увидел.

Адрес страницы‑приложения меняться не будет, поэтому вход в приложение вынесем в команду enterApp, а сам адрес запишем в fixtures/common.json:

import {baseUrl} from '../fixtures/common.json'

Cypress.Commands.add('enterApp', () =>
  cy.visit(baseUrl))

Проверка формы тоже будет повторяться, поэтому вынесем её в команду appContainsEmptyLoginForm.

Cypress.Commands.add('appContainsEmptyLoginForm', () => {
  cy.get('.login').should('have.length', 1)
  cy.get('.login-code').should('be.empty')
})

Я предпочитаю называть команды либо:

  • глаголом с действием, которое надо выполнить: enterApp;
  • предикатом для проверок: appContainsEmptyLoginForm.

Первые ничего не проверяют, а лишь выполняют какое‑то побочное действие. Вторые проверяют то, что описано в названии.

Ввод чисел на клавиатуре нам тоже понадобится в других тестах приложения. Поэтому его мы превратим в команду keyboardType.

Cypress.Commands.add('keyboardType', (str) => {
  const chars = str.toString().split('')

  chars.forEach(char => {
    cy.get('.keyboard')
      .contains(char)
      .closest('.button')
      .click()
  })
})

Нажатие на «энтер» нам тоже пригодится в других местах:

Cypress.Commands.add('pressEnter', () => {
  cy.get('.button.is-enter').click()
})

В итоге код теста станет таким:

describe('Login', () => {
  it('Valid code passes login', () => {
    cy.enterApp()
    cy.appContainsEmptyLoginForm()
    cy.enterLoginCode(validCode)
    cy.get('.login').should('have.length', 0)
  })
})

Теперь напишем тест на неправильный код:

it('Invalid codes dont pass login', () => {
  cy.enterApp()
  cy.appContainsEmptyLoginForm()
  cy.enterLoginCode(invalidCode)
  cy.get('.login').should('have.length', 1)
})

Как мы видим, первые две строки повторяются, поэтому их можно вынести к сетап теста:

describe('Login', () => {
  beforeEach(() => {
    cy.enterApp()
    cy.appContainsEmptyLoginForm()
  })

  it('Valid code passes login', () => {
    cy.enterLoginCode(validCode)
    cy.get('.login').should('have.length', 0)
  })

  it('Invalid codes dont pass login', () => {
    cy.enterLoginCode(invalidCode)
    cy.get('.login').should('have.length', 1)
  })
})

После запуска увидим такую картину: Результат выполнения отказного теста на вход в приложение

Если какой‑то тест свалится, то в логах будет описано, что не совпало. Сайпрес офигительно описывает ошибку: показывает, какой узел её вызвал, что не совпало, и сохраняет снапшот состояния. То есть можно ткнуть мышью в этот баг, и справа отобразится состояние страницы в это время! Описание ошибки в тестировании

Сценарий создания бюджета

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

Заведём бюджет из 10000 попугаев на 10 дней. Приложение запишет в бюджет только 95% от той суммы, которую вводим, чтобы план не оказался впритык. Значит, после сохранения бюджет будет содержать 9500 попугаев.

describe('Budget creation', () => {
  before(() => {
    // команда для быстрого логина в приложение, минуя форму
    cy.login()
    cy.enterApp()    
    // открывает настройки бюджета
    cy.openBudgetSettings()
  })

  it('Inputs the budget sum and saves it', () => {
    cy.keyboardType('10000')
    cy.pressEnter()

    cy.get('.budget')
      .contains('9500')
  })
})

Дальше выбираем срок. Мы выберем 10 дней, поэтому нам надо выделить 10‑й пункт в крутилке с датами. Проверяем, что даты до выбранной включительно стали красными и что в бюджете появилась строка с суммой на день.

it('Inputs the budget time and saves it', () => {
  cy.get('.datepicker-item')
    // индексы начинаются с нуля, 10-й элемент — eq(9)
    .eq(9)
    .click()

  cy.get('.datepicker-item.has-red-color')
    .should('have.length', 10)

  cy.get('.dialogue-secondary')
    .contains('на 10 дней. 950 в день')

  cy.get('.button.is-fixed-rb')
    .click()
})

После сохранения проверяем, правильно ли посчиталась сумма на день. Проверять содержимое счётчиков нам понадобится и в других тестах, поэтому создадим команду:

Cypress.Commands.add('counterContains', (content) => {
  cy.get('.mainContent .dialogue .counter')
    .contains(content)
})

it('Tests todays limit', () => {
  cy.counterContains(950)
})

И что сохранилась запись в истории о создании бюджета:

Cypress.Commands.add('budgetRecordContains', (sum, days) => {
  const $lastRecord = (selector) =>
    cy.get('.timeline')
      .find(selector)
      .last()

  $lastRecord('.record--budget').contains(sum)
  $lastRecord('.record--budget').contains(days)
})

it('Tests history record', () => {
  cy.budgetRecordContains(9500, 10)
})

После запуска увидим такую картину: Результат выполнения теста на создание бюджета

Основной сценарий трат

Теперь проверим сценарий пользовательских трат. Есть два варианта трат: когда бюджета ещё нет, и когда он задан. Чтобы отделить набор тестов для первого случая от набора для второго, будем использовать context.

describe('Tests spendings', () => {
  context('When budget is not set', () => {
    beforeEach(() => {
      cy.login()
      cy.enterApp()
    })

    it('Spends 400 parrots for helpful stuff', () => {
      // ...
    })
  })
})

Каждую трату мы можем сделать полезной или вредной. Чтобы проверить все случаи и не повторять код, напишем функцию spendMoneyOnce, которая будет заниматься тратами.

// команда для включения и выключения категорий
Cypress.Commands.add('toggleCategory', (type='helpful') => {
  cy.get(`.category.is-${type}`).click()
})

const spendMoneyOnce = (amount, category='unknown') => {
  amount = `${amount}`
  cy.keyboardType(amount)
  cy.get('.numberDisplay-value')
      .contains(amount)

  if (category !== 'unknown') {
    cy.toggleCategory(category)
  }

  cy.pressEnter()
}

И функцию, которая будет проверять, сохранилась ли трата:

const spendSaved = (amount, category) => {
  // команда для проверки последней записи в истории
  cy.lastRecordContains(amount, category)
}

Тогда тестирование трат в категориях будет выглядеть следующим образом:

it('Spends 400 parrots for helpful stuff', () => {
  const [amount, category] = [400, 'helpful']
  spendMoneyOnce(amount, category)
  spendSaved(amount, category)
})

it('Spends 400 parrots for harmful stuff', () => {
  const [amount, category] = [400, 'harmful']
  spendMoneyOnce(amount, category)
  spendSaved(amount, category)
})

Траты из заполненного бюджета

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

context('When budget is set, 950 for today', () => {
  beforeEach(() => {
    cy.login()
    cy.enterApp()
    // команда для быстрого создания бюджета с указанными параметрами
    cy.createBudgetWith(10000, 10)
  })

  it('Spends amount smaller than the limit for today', () => {
    testSpendWithActiveBudget({
      amount: 100,
      forToday: 850
    })
  })

  it('Spends amount bigger than the limit for today', () => {
    testSpendWithActiveBudget({
      amount: 1000,
      forToday: -50,
      newDayLimit: '944,44'
    })
  })
})

Функция testSpendWithActiveBudget берёт на себя алгоритм проверки. Она совершает трату, проверяет, что трата записалась, затем проверяет, должна ли была сумма на день пересчитаться. Если да, то проверяет новую сумму на день. Если нет, то проверяет остаток на сегодня — разницу между дневным лимитом и суммой трат за сегодня.

const testSpendWithActiveBudget = ({
  amount,      // число
  forToday,    // число
  newDayLimit, // форматированная строка
}) => {
  spendMoneyOnce(amount, 'unknown')
  cy.lastRecordContains(amount, 'unknown')

  // трата меньше, чем лимит на сегодня
  if (forToday > 0) {
    cy.counterContains(forToday)
  }
  // трата больше, приложение пересчитает сумму на день
  else {
    cy.counterRowContains('Новая сумма на день', 0)
    cy.counterRowContains(newDayLimit, 0)

    cy.counterRowContains('На сегодня дно пробито', 1)
    cy.counterRowContains(forToday, 1)
  }
}

Меняем даты в браузере

Осталось протестировать, как себя ведёт бюджет и история, если приложение запускают через день или несколько дней.

context('Tests next day settings', () => {
  beforeEach(() => {
    cy.login()
    cy.enterApp()
    cy.createBudgetWith(10000, 10)
  })
})

Сперва проверим, что непотраченные деньги попадают в копилку:

it('Tests next day safe record', () => {
  spendMoneyOnce(400)
  cy.skipDay()
  cy.safeRecordContains(550)
})

Затем, что сумма на день осталась той же, если пользователь не вышел за вчерашний лимит

it('Tests next day limit after spend less than prev limit', () => {
  spendMoneyOnce(400)
  cy.skipDay()
  cy.counterContains(950)
})

И что сумма уменьшится, если пользователь вышел за лимит:

it('Tests next day limit after spending more than prev limit', () => {
  spendMoneyOnce(1000)
  cy.skipDay()
  cy.counterContains('944,44')
})

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

Вначале я создаю базовую команду skipDays, которая будет принимать количество дней для пропуска и момент времени, от которого отсчитывать. Внутри команды работает cy.clock, который описывает изменение времени.

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

Между тестами сбрасывать настройки времени через restore необязательно, потому что Сайпрес делает это сам. Но если мы запускаем skipDays внутри одного теста несколько раз, то чтобы перетереть предыдущие настройки, надо вызвать restore.

Метод reload перезагружает страницу — будто пользователь заходит в приложение спустя указанное время.

Cypress.Commands.add('skipDays', (count=1, from=Date.now()) => {
  cy.clock().then(clock => clock.restore())
  cy.clock(from + (count * MSECONDS_IN_DAY), ['Date'])
  cy.reload()
})

// синоним для cy.skipDays(1)
Cypress.Commands.add('skipDay', () => {
  cy.skipDays(1)
})

Результат

Самое кайфовое в таких тестах — смотреть, как они работают. Вот ускоренный видосик со всеми тестами, которые есть в проекте. Самые глазастые смогут там найти настоящий рабочий пароль для входа в приложение :–)

Видео с работой всех тестов в проекте

Ссылки по теме

Как мы делали «Фронтенд — это не больно!»

«Фронтенд — это не больно!» — длинная статья, цель которой помочь разработчикам научиться справляться с рутиной и получать удовольствие от работы. В этой заметке я расскажу, как появилась идея и почему этот проект для меня был важен.

Предпосылки

Когда‑то описанные в нём проблемы были у меня. Мне тоже казалось, что найти с дизайнерами общий язык очень трудно, объяснять что‑то менеджерам бесполезно, и проще переписать фронтенд, чем говорить с бекендом. Но со временем я стал умнее и понял, что дело не в команде, а в моём отношении к ситуации и проблемам.

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

Этого казалось мало, потому что теперь я очутился на другой стороне: видел других фронтендеров с теми же проблемами. У меня был опыт того, как начать работать без напряга. Я хотел им поделиться, но не знал как.

Идея

В феврале 2017 мне написал Вадим с идеей сделать доклад или лекцию для разработчиков о понимании дизайнеров. Мы хотели показать, что разработчики часто упарываются по технологиям, не замечают важного и злятся на дизайнеров. А было бы круто, если все работали сообща и понимали друг друга.

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

Казалось, что материала слишком много, было трудно сформулировать идеи. Непонятно было даже, с чего начинать доклад. Решили сделать вместо него почтовую рассылку. Её можно править на ходу по отзывам, и прямо в письмах спрашивать, о чём рассказывать дальше. Я начал работать над рассылкой, сделал первую половину первого письма.

После этого решил ещё раз подумать, в чём цель идеи — достучаться до большего количества разработчиков. Рассылки, лекции, доклады — это слишком сложно. На рассылку надо подписываться, на лекцию или доклад идти. А вот у статей охват потенциально больше, потому что их проще читать и распространять. А чем больше охват, тем большему количеству людей они могут помочь.

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

Запуск, результаты и планы

Статья разлетелась по соцсетям, о ней написали Форвеб и Вебстандарты, обсудили в паре подскастов. Судя по всему, разработчикам она тоже понравилась. Мы даже получили несколько писем с благодарностями, надеемся кому‑то действительно помогло.

Сейчас у нас в планах сделать более весёлое дополнение к проекту. Если вам интересно поучаствовать, то заходите в чат в Телеграме, там уже есть наработки :–)

Айбайк Уфа, как это было

В этой заметке я расскажу, с какими трудностями столкнулась наша команда, и какие мы принимали решения во время работы над приложением для фестиваля Айбайк Уфа.

Сроки, модульность и АПИ

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

Фронт я писал сразу так, будто у меня уже есть АПИ для общения с сервером. Описал, какие данные буду отправлять на сервер, и что буду ждать в ответ. Расписал, по каким ссылкам будет удобнее общаться и какого формата сообщения мы будем использовать.

Плюс такого подхода в том, что сложность прикрутки фронтенда к бекенду выносится в отдельную плоскость. Бек и фронт ничего не знают друг о друге. Им важно лишь, какого формата сообщения используются для общения между ними (по‑умному это ещё называется слабая связность модулей).

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

Редизайн в середине разработки

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

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

Для сравнения — первая версия главного экрана выглядела так:

Первая версия приложения

Финальная — так:

Финальная версия приложения

Подстройка под реалии

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

Решение с крутящимися цифрами для ввода кода оказалось непонятным

Вместо барабанчиков мы поставили поле:

Вместо барабанчиков мы поставили поле

Пришлось подстраивать и карточку площадки — на некоторых площадках заданий не было, на других заданием был репост в соцсети. Для этого мы изменили блоки с заданием и кнопку под расписанием событий на площадке.

Людям пофиг, что это веб. Требования — как от натива

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

К примеру, анимация перехода между экранами по задумке должна была быть, как будто меняются карточки на экране. Но из‑за проблем с прокруткой на странице и особенностями Реакта пришлось сделать «псевдонаплыв» одной карточки на другую.

В вебе трудно справляться с асинхронными запросами, как принято в нативных приложениях; карты не работают в офлайне; механика экранов с картами проще, чем в нативе. Из‑за этого мы словили несколько негативных отзывов.

Вывод — люди не видят разницы, между нативным, веб‑приложением или гибридным, им всё равно, сколько по срокам заняла разработка. Требования к приложению всегда будут жёсткими.

Фестиваль отменили

Самая большая проблема появилась за день до запуска — фестиваль отменили. Из программы осталось несколько площадок и рекорд Гиннеса.

Как так получилось, и почему — в обращении организаторов.

Итог

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

Раньше ↓