[Программирование, Разработка мобильных приложений, Dart, Flutter] Как мы сделали миграцию пользовательских данных с нативного приложения на Flutter
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Всем привет! Меня зовут Дмитрий Андриянов, я 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
===========
Похожие новости:
- [Разработка веб-сайтов, Программирование, Dart, Flutter] DartUP 2020: итоги и видеозаписи докладов
- [PHP, Программирование, Анализ и проектирование систем] PHP коммьюнити в СНГ. Было плохо — стало хуже
- [Программирование, IT-инфраструктура, Виртуализация, Промышленное программирование, Управление разработкой] Цифровая индустрия: непрерывная оптимизация процессов
- [Поисковые технологии, Python, Разработка мобильных приложений, Kotlin] Не баян: ищем дубликаты изображений на основе Milvus с индексом FAISS внутри
- [Разработка веб-сайтов, JavaScript, Программирование, ReactJS] Заметка о том, как React обновляет состояние (перевод)
- [Программирование, Управление разработкой, Развитие стартапа, Карьера в IT-индустрии, Научно-популярное] Путь CTO в небольшом стартапе (Zapier) (перевод)
- [Разработка под iOS, Разработка мобильных приложений] Как создавать гибкие списки: обзор динамического UICollectionView – IGListKit
- [Информационная безопасность, Программирование, Софт] Антирекорд 2020: в ПО выявили уязвимостей больше, чем в любой другой год
- [Программирование, Prolog, Искусственный интеллект, Natural Language Processing] Роль логического программирования, и стоит ли планировать его изучение на 2021-й
- [Программирование, Разработка под Android, Визуализация данных, Криптовалюты] CoinRoad: Как мы сделали приложение на базе кастомных пушей в Android
Теги для поиска: #_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
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 15:43
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Всем привет! Меня зовут Дмитрий Андриянов, я Flutter-разработчик в Surf. В этой статье я расскажу про бесшовную миграцию данных при установке новой версии приложения, написанного на Flutter, поверх предыдущей версии, написанной на нативе. Это решение реализовано моим коллегой по Flutter-отделу в Surf Александром Трущинским. Статья будет полезна, если вы:
Задача: обновить приложение банка с переносом пользовательских данных В 2019 году к нам пришёл заказчик с запросом на обновление банковского B2B приложения. Прежняя версия была написана на нативных Android и iOS технологиях. Заказчик решил отказаться от них в пользу Flutter, чтобы сократить затраты на разработку и поддержку. Flutter — это не React Native где «7 раз отмерь и в итоге откажись», а эффективный инструмент, способный конкурировать с нативной разработкой.
При разработке приложения пришлось столкнуться с рядом нетривиальных задач. Одна из них — реализовать автоматический перенос пользовательских данных при установке новой версии на 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. Работу с данными поделили на классы:
Мы точно знаем, что при первом входе в обновлённое приложение данные существующих компаний хранятся в старом хранилище, ведь мы их ещё не вытягивали. Если данные есть только на нативном уровне, наконец-то начинаем миграцию. Один из нюансов миграции данных — она происходит в тандеме с сервером, а не только локально. Поэтому есть риск, что запрос обвалится. В таком случае не хотелось бы снова лезть в старый код. Чтобы избежать этого, миграцию можно условно разделить на два этапа. Сначала просто копируем компании из старого нативного хранилища в новое на 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 =========== Похожие новости:
Блог компании Surf ), #_programmirovanie ( Программирование ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_dart, #_flutter |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 15:43
Часовой пояс: UTC + 5