Как стать автором
Обновить
68.07
Surf
Создаём веб- и мобильные приложения

Как мы сделали миграцию пользовательских данных с нативного приложения на Flutter

Время на прочтение 6 мин
Количество просмотров 4.4K
Всем привет! Меня зовут Дмитрий Андриянов, я Flutter-разработчик в Surf.

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



Статья будет полезна, если вы:

  1. Пишете на Flutter и хотите увидеть пример работы с платформенным кодом.
  2. Пишете для Android/iOS и хотите перенести свой проект на Flutter.
  3. Переживаете, что оценки в сторах могут просесть, потому что пользователи будут недовольны барьерами из-за обновления приложения: например, из-за того, что придётся заново регистрироваться или авторизоваться.

Задача: обновить приложение банка с переносом пользовательских данных


В 2019 году к нам пришёл заказчик с запросом на разработку мобильного приложения для банка для B2B. Прежняя версия была написана на нативных Android и iOS технологиях. Заказчик решил отказаться от них в пользу Flutter, чтобы сократить затраты на разработку и поддержку.
Flutter — это не React Native где «7 раз отмерь и в итоге откажись», а эффективный инструмент, способный конкурировать с нативной разработкой.

При разработке приложения пришлось столкнуться с рядом нетривиальных задач. Одна из них — реализовать автоматический перенос пользовательских данных при установке новой версии на Flutter поверх нативной существующей.

Перенести имеющиеся данные было критически важно: существующие пользователи не должны входить заново в, по сути, новое приложение. Если у вас возник вопрос: «Разве повторный вход в приложение — проблема?», опишу этот занимательный процесс:

  1. Установить специальное расширение для браузера, отвечающее за безопасность. Его поставляет сам банк.
  2. Зарегистрироваться или войти на сайт.
  3. Сгенерировать ключ для электронной подписи и дождаться его активации. Он используется при каждом входе на сайт.
  4. Добавить через сайт мобильное устройство, с которого пользователь будет работать с приложением.
  5. Получить сгенерированный временный логин и пароль, ввести их и подтвердить добавление устройства.

image
Сразу замечу, что в приложении нет всем знакомого «пользователя». Его роль выполняет сущность «компания» — холдинг, способный объединять несколько разных организаций. Их количество не ограничено, и каждую нужно регистрировать. Также есть режим «мультиаккаунта» со множеством несвязанных организаций — им пользуются, например, бухгалтеры, которые обслуживают на аутсорсе разные фирмы.

Чтобы пользователю понравилась новая версия, и он в порыве гнева не пошёл ставить малоприятные отзывы, требовалось сделать перенос данных тихо и быстро.

Решение


Решение задачи мы видели таким:

  • Определить, где в нативном приложении хранятся данные для каждой из платформ.
  • На сплэше проверять наличие пин-кода в новом хранилище со стороны обновлённой версии приложения на Flutter.
  • Если данные есть, направлять на авторизацию.
  • Если данных нет, проверять хранилище в старом нативном коде через MethodChannel. При их наличии также отправлять на авторизацию и запускать миграцию в случае успеха. Миграция здесь — это извлечение и перенос данных в другое хранилище с более простым доступом со стороны Flutter, а также очистка старого места хранения.
  • В случае отсутствия данных — регистрация.

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

image

Проблемы при решении


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

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

Пройдя стадию принятия, мы начали реализовывать задачу.

На Android в исходниках приложения оказалось много абстракций, поэтому мы решили выделить необходимые части нативного кода в отдельный модуль с реализацией аналогичной логики извлечения данных пользователя. Этот модуль и подключили к нашему Flutter-приложению через MethodChannel.

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

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

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

Использование ProGuard вызывало несоответствие классов, и приложение крашилось. Для решения проблемы сравнивали каждый падающий класс в apk-файле старого и нового приложения и приводили их к общему виду. Это тоже замедляло разработку.

В iOS, в отличие от Android, всё оказалось просто: нашли нужные сертификаты для доступа к Keychain — специализированной базе данных Apple, где в защищённом виде хранятся метаданные и конфиденциальная информация пользователя. Из Keychain достали новые данные.

Так мы побороли нативного зверя. Дальше нужно было интегрировать его в наш Flutter-проект.

Интеграция


Интегрировали, интегрировали, да заинтегрировали

На данном этапе у нас уже имелся сплэш, экран регистрации и экран входа по пин-коду/биометрии.

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

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

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

Свидетельством того, что пользователь есть хоть где-то, является сохранённый пин-код. Точнее, его зашифрованный хэш: данные можно достать только с его помощью. Всё ради безопасности.

Запускаем и смотрим, есть ли пин-код на Flutter или в нативном уровне. Решаем, куда пустить: на регистрацию или авторизацию.

Future<bool> IsPinCorrect(String pin) async {
 if (pin == null || pin.isEmpty) return false;

 String pinHash = CryptoUtils.getHash(pin);

 if (await _migrator.needLoadAuthDataFromPlatform) {
   return _platformAuthDataProvider.isPinCorrect(pin);
 } else {
   return _dataProvider.isPinCorrect(pinHash);
 }
}

image
В качестве архитектуры выбрали, как и во всех других наших проектах, собственное проверенное решение mwwm из пакета SurfGear и пакет relation для более эффективного управления состоянием.

Подробнее о mwwm можно посмотреть в презентации на YouTube.

Дополнительно используем Clean architecture. Входной точкой в логику авторизации является AuthInteractor.

Работу с данными поделили на классы:

  • DataProvider для работы с данными на уровне Flutter.
  • PlatformAuthDataProvider для работы с данными в прежнем хранилище на уровне платформы.

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

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

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

Сначала просто копируем компании из старого нативного хранилища в новое на Flutter. Эти компании не имеют подтверждённых сертификатов и пользователь не сможет полноценно работать с ними, но они уже хранятся в нужном нам месте.

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

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

image
Future<void> migrate(String pinHash, NavigatorState navigator) async {
 final deviceInfo = await _deviceInfoInteractor.getDeviceInfo();
 final List<Company> companies = await _dataProvider.getCompanies(pinHash);

 for (Company company in companies) {
   try {
/// Сетевой запрос на начало миграции выбранной компании
     final startMigration = await _migrationRepository.migrationStart(
       deviceInfo,
     );

/// На этом месте в реальном коде
/// локальная логика работы с сертификатами компании

/// Сетевой запрос на окончание миграции выбранной компании
         await _migrationRepository.migrationFinish(
       startMigration.migrationId,
       … передача параметров шифрования
     );

     await _confirmMigrate(
         startMigration,
         company,
         publicPrivateKeys,
       );
     company.needMigrate = false;
   } on Exception catch (e) {
     Logger.e(e.toString());
   }
 }

/// Удаление данных из старого  хранилища
 await _platformDataProvider.clearData();
 await _dataProvider.saveCompanies(pinHash, companies);
 await _dataProvider.setPin(pinHash);
}

Итог


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

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

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

Больше полезного про Flutter — в телеграм-канале Surf Flutter Team. Публикуем кейсы, лучшие практики, новости и вакансии Surf, а также проводим прямые эфиры. Присоединяйтесь!
Теги:
Хабы:
+11
Комментарии 0
Комментарии Комментировать

Публикации

Информация

Сайт
surf.ru
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия