[Программирование, Разработка мобильных приложений, Dart, Flutter] Как мы сделали миграцию пользовательских данных с нативного приложения на Flutter

Автор Сообщение
news_bot ®

Стаж: 6 лет 4 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
23-Дек-2020 15:32

Всем привет! Меня зовут Дмитрий Андриянов, я Flutter-разработчик в Surf.
В этой статье я расскажу про бесшовную миграцию данных при установке новой версии приложения, написанного на Flutter, поверх предыдущей версии, написанной на нативе. Это решение реализовано моим коллегой по Flutter-отделу в Surf Александром Трущинским.
Статья будет полезна, если вы:
  • Пишете на Flutter и хотите увидеть пример работы с платформенным кодом.
  • Пишете для Android/iOS и хотите перенести свой проект на Flutter.
  • Переживаете, что оценки в сторах могут просесть, потому что пользователи будут недовольны барьерами из-за обновления приложения: например, из-за того, что придётся заново регистрироваться или авторизоваться.


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

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


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

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

Проблемы при решении
Когда пришло время выполнения задачи по миграции, Александр ринулся в бой, а остальная команда занималась UI и прочими делами. Тут-то и началось самое интересное. Мы понятия не имели, как работает под капотом текущая авторизация и как лучше подступиться к этому монолиту. Для понимания нужны были исходники.
Получив желанные файлы, мы не смогли собрать их. Оказалось, что у подрядчика, который разрабатывал предыдущую версию приложения, была своя билд-система и библиотека для авторизации — доступа к ним у нас не оказалось.
Пройдя стадию принятия, мы начали реализовывать задачу.
На Android в исходниках приложения оказалось много абстракций, поэтому мы решили выделить необходимые части нативного кода в отдельный модуль с реализацией аналогичной логики извлечения данных пользователя. Этот модуль и подключили к нашему Flutter-приложению через MethodChannel.
Вменяемой документации не было, и нам пришлось потратить время, чтобы понять механизм работы и определить, какой код брать. Когда разобрались, создали отдельный Android-проект, чтобы отдебажить изъятые куски и привести их к виду удобоваримого модуля.
Целую неделю мы превращали изъятый код в интерфейс, с которым можно продуктивно работать. Но баги всё равно преследовали нас, потому что часто приходилось править и пересобирать локально чужой код на Kotlin и библиотеки, от которых он зависел.
Миграцию разрабатывали в дебажной версии приложения, а в релизной сборке появились новые проблемы. В предыдущей версии приложения использовалась утилита ProGuard — она удаляет неиспользуемый код, изменяет имена переменных и методов для усложнения реверс-инжиниринга приложения, а также позволяет уменьшить размер файлов.
Использование ProGuard вызывало несоответствие классов, и приложение крашилось. Для решения проблемы сравнивали каждый падающий класс в apk-файле старого и нового приложения и приводили их к общему виду. Это тоже замедляло разработку.
В iOS, в отличие от Android, всё оказалось просто: нашли нужные сертификаты для доступа к Keychain — специализированной базе данных Apple, где в защищённом виде хранятся метаданные и конфиденциальная информация пользователя. Из Keychain достали новые данные.
Так мы побороли нативного зверя. Дальше нужно было интегрировать его в наш Flutter-проект.
Интеграция
Интегрировали, интегрировали, да заинтегрировали
На данном этапе у нас уже имелся сплэш, экран регистрации и экран входа по пин-коду/биометрии.
Первое, что нам было необходимо, — понять, какой экран открывать. Для этого нужно знать, существуют ли данные. Если да — экран входа. На нём миграция и запустится. Если данных нет — отправляем на экран регистрации пользователя.
При открытии приложения на сплеше проверяем Flutter-хранилище: вдруг это не первый вход и данные уже перенесены, либо пользователь регистрировался через новую версию приложения. Тогда миграция не нужна.
Если во Flutter-хранилище нас ожидает пустота, идём в наш сервис и ищем данные там. Обнаружились — значит, мы писали код не зря, и теперь их нужно переносить. В противном случае никакой миграции — нужно направлять на регистрацию.

Свидетельством того, что пользователь есть хоть где-то, является сохранённый пин-код. Точнее, его зашифрованный хэш: данные можно достать только с его помощью. Всё ради безопасности.
Запускаем и смотрим, есть ли пин-код на 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);
}
}


В качестве архитектуры выбрали, как и во всех других наших проектах, собственное проверенное решение mwwm из пакета SurfGear и пакет relation для более эффективного управления состоянием.
Подробнее о mwwm можно посмотреть в презентации на YouTube.
Дополнительно используем Clean architecture. Входной точкой в логику авторизации является AuthInteractor.
Работу с данными поделили на классы:
  • DataProvider для работы с данными на уровне Flutter.
  • PlatformAuthDataProvider для работы с данными в прежнем хранилище на уровне платформы.


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

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-приложение поверх существующего нативного. Так мы сократили команду разработки в будущем и избавились от легаси в проекте.
Такая незаурядная задача была очень увлекательным вызовом. Но это было только начало. Проект принес немало интересных задач и сложных кейсов.
Хочется сказать спасибо всем, кто был причастен к нему. И отдельное спасибо Саше Трущинскому за реализацию такого непростого кейса с принятием нативного удара на себя.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_programmirovanie (Программирование), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_dart, #_flutter, #_surf, #_dart, #_dartlang, #_flutter, #_razrabotka_mobilnyh_prilozhenij (разработка мобильных приложений), #_razrabotka_mobilnogo_prilozhenija (разработка мобильного приложения), #_razrabotka_mobilnogo_po (разработка мобильного по), #_flutter_app_development, #_krossplatformennaja_razrabotka (кроссплатформенная разработка), #_kejs_po_proektu (кейс по проекту), #_reshenie_problemy (решение проблемы), #_blog_kompanii_surf (
Блог компании Surf
)
, #_programmirovanie (
Программирование
)
, #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
)
, #_dart, #_flutter
Профиль  ЛС 
Показать сообщения:     

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

Текущее время: 02-Июл 20:12
Часовой пояс: UTC + 5