Русский
Русский
English
Статистика
Реклама

Surf

Как спроектировать пошаговое заполнение данных в мобильном приложении

01.10.2020 14:20:35 | Автор: admin
Привет! Меня зовут Вита Соколова, я Android Team Lead в Surf.

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

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



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

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

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

Представим, что пользователь откликается на вакансию и заполняет анкету. Если он прервётся на середине, введённые данные сохранятся в черновике. Когда пользователь вернётся к заполнению, информация из черновика автоматически подставится в поля анкеты заполнять всё с нуля ему не нужно.

Когда пользователь заполнит анкету целиком, его отклик отправится на сервер.

Анкета состоит из:
  • Шаг 1 ФИО, типа образования, наличия опыта работы,
  • Шаг 2 место учёбы,
  • Шаг 3 место работы или эссе о себе,
  • Шаг 4 причины, почему заинтересовала вакансия.




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



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

В результате хотим получить такую функциональность:



Примером целиком в моём репозитории на GitHub

Очевидное решение


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

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



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

class Application(    val name: String?,    val surname: String?,    val educationType : EducationType?,    val workingExperience: Boolean?    val education: Education?,    val experience: Experience?,    val motivation: List<Motivation>?)


НО!
Работая с таким объектом, мы обрекаем код наш код покрыться лишним ненужным количеством проверок null. Например, такая структура данных никак не гарантирует, что поле educationType уже будет заполнено на экране Образование.

Как сделать лучше


Рекомендую вынести управление данными в отдельный объект, который обеспечит на вход каждому шагу необходимые non-nullable данные и сохранит результат каждого шага в черновик. Этот объект мы назовём интерактор. Он соответствует слою Use Case из чистой архитектуры Роберта Мартина и для всех экранов отвечает за предоставление данных, собранных из различных источников (сеть, БД, данные с предыдущих шагов, данные из черновика заявки...).

На своих проектах мы в Surf используем Dagger. По ряду причин интеракторы принято делать скоупом @PerApplication: это делает наш интерактор синглтоном в рамках приложения. На самом деле интерактор может быть синглтоном в рамках фичи или даже активити если все ваши шаги представляют собой фрагменты. Всё зависит от общей архитектуры вашего приложения.

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



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

Организация пошаговой фичи с помощью сценариев и интерактора позволяет:
  • Безболезненно менять шаги в сценарии: например, перекрывать дальнейшую работу, если в ходе выполнения выясняется, что пользователю нельзя подавать заявки или добавлять шаги, если информации нужно больше.
  • Задавать контракты: какие данные обязаны быть на входе и на выходе каждого шага.
  • Организовать сохранение заявки в черновик, если пользователь не прошёл все экраны.

Предзаполнять экраны с помощью данных, сохранённых в черновик.

Основные сущности


Механизм работы фичи будет состоять из:
  • Набора моделей для описания шага, входных и выходных данных.
  • Сценария (Scenario) сущности, описывающей, какие шаги (экраны) нужно пройти пользователю.
  • Интерактора (ProgressInteractor) класса, отвечающего за хранение информации о текущем активном шаге, агрегирование заполненной информации после завершения каждого шага и выдачу входных данных для старта нового шага.
  • Черновика (ApplicationDraft) класса, отвечающего за хранение заполненной информации.

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



Для сущности Scenario зададим интерфейс, в котором опишем какую логику мы ждем для любого сценария в приложении (содержать список необходимых шагов и перестраивать его после завершения предыдущего шага, если необходимо.

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

ApplicationDraft не представлен в базовых классах, так как сохранение данных, которые заполнил пользователь, в черновик может не требоваться. Поэтому работать с черновиком будет конкретная реализация ProgressInteractor. С ней же будут взаимодействовать презентеры экранов.

Диаграмма классов для конкретных реализации базовых классов:



Все эти сущности буду взаимодействовать между собой и с презентерами экранов следующим образом:



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

Описание шагов


Начнём с первого пункта. Нам понадобятся сущности для описания шагов:

//Маркерный интерфейс, чтобы обозначить классы, являющиеся шагами в сценарииinterface Step


Для фичи из нашего примера с откликом на вакансию шаги будут следующими:

/** * Шаги в фиче заполнения заявки */enum class ApplicationSteps : Step {    PERSONAL_INFO,  // персональные данные    EDUCATION,      // образование    EXPERIENCE,     // опыт работы    ABOUT_ME,       // эссе "о себе"    MOTIVATION      // что интересно в данной вакансии}


Также нам понадобится описать входные данные для каждого шага. Для этого мы будем по прямому назначению использовать sealed class-ы чтобы создать ограниченную иерархию классов.



Как это будет выглядеть в коде
//Входные данные для шагаinterface StepInData


Для нашего примера это:

//Класс, описывающий входные данные для работы шаговsealed class ApplicationStepInData : StepInData//Входные данные для шага об образованииclass EducationStepInData(val educationType: EducationType) : ApplicationStepInData()//Входные данные для шага о причинах выбора этой вакансииclass MotivationStepInData(val values: List<Motivation>) : ApplicationStepInData()



Аналогично описываем выходные данные:



Как это будет выглядеть в коде
//Маркерный интерфейс, помечающий результат шагаinterface StepOutData//Класс, описывающий результат прохождения шагаsealed class ApplicationStepOutData : StepOutData//Результат прохождения  шага "Персональная информация"class PersonalInfoStepOutData(    val info: PersonalInfo) : ApplicationStepOutData()//Результат прохождения шага "Образование"class EducationStepOutData(    val education: Education) : ApplicationStepOutData()//Результат прохождения  шага "Места работы"class ExperienceStepOutData(    val experience: WorkingExperience) : ApplicationStepOutData()//Результат прохождения шага "Обо мне"class AboutMeStepOutData(    val info: AboutMe) : ApplicationStepOutData()//Результат прохождения шага "Выбор причин"class MotivationStepOutData(    val motivation: List<Motivation>) : ApplicationStepOutData()



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

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

Как это будет выглядеть в коде
/** * Входные данные для шага + данные из черновика, если они есть */interface StepData<I : StepInData, O : StepOutData>sealed class ApplicationStepData : StepData<ApplicationStepInData,  ApplicationStepOutData> {    class PersonalInfoStepData(        val outData: PersonalInfoStepOutData?    ) : ApplicationStepData()    class EducationStepData(        val inData: EducationStepInData,        val outData: EducationStepOutData?    ) : ApplicationStepData()    class ExperienceStepData(        val outData: ExperienceStepOutData?    ) : ApplicationStepData()    class AboutMeStepData(        val outData: AboutMeStepOutData?    ) : ApplicationStepData()    class MotivationStepData(        val inData: MotivationStepInData,        val outData: MotivationStepOutData?    ) : ApplicationStepData()}



Действуем по сценарию


С описанием шагов и входных/выходных данных разобрались. Теперь закрепим в коде порядок этих шагов в сценарии фичи. За управление текущим порядком шагов отвечает сущность Scenario. Сценарий будет выглядеть следующим образом:

/** * Интерфейс, которому должны удовлетворять все классы, описывающие порядок шагов в фиче */interface Scenario<S : Step, O : StepOutData> {        // список шагов    val steps: List<S>    /**     * Внесение изменений в сценарий      * в зависимости от выходной информации при завершении шага     */    fun reactOnStepCompletion(stepOut: O)}


В имплементации для нашего примера сценарий будет таким:

class ApplicationScenario : Scenario<ApplicationStep, ApplicationStepOutData> {    override val steps: MutableList<ApplicationStep> = mutableListOf(        PERSONAL_INFO,        EDUCATION,        EXPERIENCE,        MOTIVATION    )    override fun reactOnStepCompletion(stepOut: ApplicationStepOutData) {        when (stepOut) {            is PersonalInfoStepOutData -> {                changeScenarioAfterPersonalStep(stepOut.info)            }        }    }    private fun changeScenarioAfterPersonalStep(personalInfo: PersonalInfo) {        applyExperienceToScenario(personalInfo.hasWorkingExperience)        applyEducationToScenario(personalInfo.education)    }    /**     * Если нет образования - шаг с заполнением места учёбы будет исключен     */    private fun applyEducationToScenario(education: EducationType) {...}    /**     * Если у пользователя нет опыта работы,     * шаг заполнения мест работы будет заменён на шаг рассказа о себе     */    private fun applyExperienceToScenario(hasWorkingExperience: Boolean) {...}}


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

Как, например, выглядит в коде реакция на наличие или отсутствие опыта работы
/** * Если у пользователя нет опыта работы, * шаг заполнения мест работы будет заменён на шаг рассказа о себе */private fun applyExperienceToScenario(hasWorkingExperience: Boolean) {    if (hasWorkingExperience) {        steps.replaceWith(            condition = { it == ABOUT_ME },            newElem = EXPERIENCE        )    } else {        steps.replaceWith(            condition = { it == EXPERIENCE },            newElem = ABOUT_ME        )    }}



Как устроен Interactor


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

Создадим базовый класс для нашего интерактора и заложим в него общее для всех пошаговых фич поведение.

/** * Базовый класс для интеракторов пошаговых фич * S - входной шаг * I - входные данные для шагов * O - выходные данные для шагов */abstract class ProgressInteractor<S : Step, I : StepInData, O : StepOutData> 


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

// сущность, отвечающая за состав и порядок шаговprotected abstract val scenario: Scenario<S, O>


Также интерактор отвечает за хранение состояния, какой шаг сейчас активен, и переключение на следующий или предыдущий. Он должен вовремя оповещать корневой экран о смене шага, чтобы тот мог переключиться на нужный фрагмент. Всё это легко организовать с помощью рассылки событий, т. е. реактивного подхода. Также методы нашего интерактора часто будут выполнять асинхронные операции (загрузка данных из сети или БД), поэтому для связи интерактора с презентерами мы будем использовать RxJava. Если вы ещё не знакомы с этим инструментом, прочитайте цикл вводных статей.

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

/** * Модель для описания шага и его позиции в сценарии */class StepWithPosition<S : Step>(    val step: S,    val position: Int,    val allStepsCount: Int)


Заведём в интеракторе BehaviorSubject, чтобы свободно эмитить в него информацию о новом активном шаге.

private val stepChangeSubject = BehaviorSubject.create<StepWithPosition<S>>()


Чтобы экраны могли подписаться на этот поток событий заведём публичную переменную stepChangeObservable, являющуюся обёрткой над нашим stepChangeSubject.

val stepChangeObservable: Observable<StepWithPosition<S>> = stepChangeSubject.hide()


В ходе работы интерактора часто нужно знать позицию текущего активного шага. Рекомендую завести в интеракторе отдельное свойство currentStepIndex и переопределить методы get() и set(). Так мы получаем удобный доступ к этой информации из subject.

Как это выглядит в коде
// текущий активный шагprivate var currentStepIndex: Int    get() = stepChangeSubject.value?.position ?: 0    set(value) {        stepChangeSubject.onNext(            StepWithPosition(                step = scenario.steps[value],                position = value,                allStepsCount = scenario.steps.count()            )        )    }



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

Добавим методы для инициализации и завершения работы интерактора, сделаем их открытыми для расширения в наследниках:

Методы для инициализации и завершения работы
/** * Инициализация работы интерактора */@CallSuperopen fun initProgressFeature() {    currentStepIndex = 0}/** * Завершение работы интерактора */@CallSuperopen fun closeProgressFeature() {    currentStepIndex = 0}



Добавим функции, которые должен выполнять любой интерактор пошаговой фичи:
  • getDataForStep(step: S) предоставлять данные на вход шагу S;
  • completeStep(stepOut: O) сохранять выходные данные O и переводить сценарий на следующий шаг;
  • toPreviousStep() - переводить сценарий на предыдущий шаг.


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

/** * Метод получения входной информации для шага */protected abstract fun resolveStepInData(step: S): Single<out StepData<I, O>>


Для презентеров конкретных экранов добавим публичный метод, который будет вызывать resolveStepInData() :

/** * Предоставление входных параметров для шага */fun getDataForStep(step: S): Single<out StepData<I, O>> = resolveStepInData(step)


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

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

/** * Метод обработки выходной информации для шага */protected abstract fun saveStepOutData(stepData: O): Completable


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

/** * Завершение текущего шага и переход к следующему */fun completeStep(stepOut: O): Completable {    return saveStepOutData(stepOut).doOnComplete {        scenario.reactOnStepCompletion(stepOut)        if (currentStepIndex != scenario.steps.lastIndex) {            currentStepIndex += 1        }    }}


И в завершении реализуем метод для возврата к предыдущему шагу.

/** * Переход на предыдущий шаг */fun toPreviousStep() {    if (currentStepIndex != 0) {        currentStepIndex -= 1    }}


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

/** * Интерактор фичи подачи заявления */@PerApplicationclass ApplicationProgressInteractor @Inject constructor(    private val dataRepository: ApplicationDataRepository) : ProgressInteractor<ApplicationSteps, ApplicationStepInData, ApplicationStepOutData>() {    // сценарий оформления    override val scenario = ApplicationScenario()    // черновик заявки    private val draft: ApplicationDraft = ApplicationDraft()    // установка черновика    fun applyDraft(draft: ApplicationDraft) {        this.draft.apply {            clear()            outDataMap.putAll(draft.outDataMap)        }    }    ...}


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

/** * Черновик заявки */class ApplicationDraft(    val outDataMap: MutableMap<ApplicationSteps, ApplicationStepOutData> = mutableMapOf()) : Serializable {    fun getPersonalInfoOutData() = outDataMap[PERSONAL_INFO] as? PersonalInfoStepOutData    fun getEducationStepOutData() = outDataMap[EDUCATION] as? EducationStepOutData    fun getExperienceStepOutData() = outDataMap[EXPERIENCE] as? ExperienceStepOutData    fun getAboutMeStepOutData() = outDataMap[ABOUT_ME] as? AboutMeStepOutData    fun getMotivationStepOutData() = outDataMap[MOTIVATION] as? MotivationStepOutData    fun clear() {        outDataMap.clear()    }}



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

/** * Сохранение выходных данных шага в черновик */override fun saveStepOutData(stepData: ApplicationStepOutData): Completable {    return Completable.fromAction {        when (stepData) {            is PersonalInfoStepOutData -> {                draft.outDataMap[PERSONAL_INFO] = stepData            }            is EducationStepOutData -> {                draft.outDataMap[EDUCATION] = stepData            }            is ExperienceStepOutData -> {                draft.outDataMap[EXPERIENCE] = stepData            }            is AboutMeStepOutData -> {                draft.outDataMap[ABOUT_ME] = stepData            }            is MotivationStepOutData -> {                draft.outDataMap[MOTIVATION] = stepData            }        }    }}


Теперь посмотрим на метод получения входных данных для шага:

/** * Получение входной информации для шага */override fun resolveStepInData(step: ApplicationStep): Single<ApplicationStepData> {    return when (step) {        PERSONAL_INFO -> ...        EXPERIENCE -> ...        EDUCATION -> Single.just(            EducationStepData(                inData = EducationStepInData(                    draft.getPersonalInfoOutData()?.info?.educationType                    ?: error("Not enough data for EDUCATION step")                ),                outData = draft.getEducationStepOutData()            )        )        ABOUT_ME -> Single.just(            AboutMeStepData(                outData = draft.getAboutMeStepOutData()            )        )        MOTIVATION -> dataRepository.loadMotivationVariants().map { reasonsList ->            MotivationStepData(                inData = MotivationStepInData(reasonsList),                outData = draft.getMotivationStepOutData()            )        }    }}


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

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

 ABOUT_ME -> Single.just(            AboutMeStepData(                stepOutData = draft.getAboutMeStepOutData()            )        )


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

EDUCATION -> Single.just(    EducationStepData(        inData = EducationStepInData(            draft.getPersonalInfoOutData()?.info?.educationType            ?: error("Not enough data for EDUCATION step")        ),        outData = draft.getEducationStepOutData()    ))


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

MOTIVATION -> {    dataRepository.loadMotivationVariants().map { reasonsList ->        MotivationStepData(            inData = MotivationStepInData(reasonsList),            outData = draft.getMotivationStepOutData()        )    }}


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

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

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

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

Класс для представления финальной заявки
/** * Модель заявления */class Application(    val personal: PersonalInfo,    val education: Education?,    val experience: Experience,    val motivation: List<Motivation>) {    class Builder {        private var personal: Optional<PersonalInfo> = Optional.empty()        private var education: Optional<Education?> = Optional.empty()        private var experience: Optional<Experience> = Optional.empty()        private var motivation: Optional<List<Motivation>> = Optional.empty()        fun personalInfo(value: PersonalInfo) = apply { personal = Optional.of(value) }        fun education(value: Education) = apply { education = Optional.of(value) }        fun experience(value: Experience) = apply { experience = Optional.of(value) }        fun motivation(value: List<Motivation>) = apply { motivation = Optional.of(value) }        fun build(): Application {            return try {                Application(                    personal.get(),                    education.getOrNull(),                    experience.get(),                    motivation.get()                )            } catch (e: NoSuchElementException) {                throw ApplicationIsNotFilledException(                    """Some fields aren't filled in application                        personal = {${personal.getOrNull()}}                        experience = {${experience.getOrNull()}}                        motivation = {${motivation.getOrNull()}}                    """.trimMargin()                )            }        }    }}



Сам метод отправки заявки:

/** * Отправка заявки */fun sendApplication(): Completable {    val builder = Application.Builder().apply {        draft.outDataMap.values.forEach { data ->            when (data) {                is PersonalInfoStepOutData -> personalInfo(data.info)                is EducationStepOutData -> education(data.education)                is ExperienceStepOutData -> experience(data.experience)                is AboutMeStepOutData -> experience(data.info)                is MotivationStepOutData -> motivation(data.motivation)            }        }    }    return dataRepository.loadApplication(builder.build())}


Как всем этим пользоваться на экранах


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

Наша фича представляет собой активити со стеком фрагментов внутри.

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

progressInteractor.stepChangeObservable.subscribe { stepData ->    if (stepData.position > currentPosition) {        // добавляем шаг в стек через FragmentManager    } else {        // убираем из стека    }    // отображение нужного кол-ва закрашенных шагов в тулбаре}


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

Для примера возьмём экран заполнения информации об образовании.



progressInteractor.getDataForStep(EducationStep)    .filter<ApplicationStepData.EducationStepData>()    .subscribeOn(Schedulers.io())    .subscribe {         val educationType = it.stepInData.educationType // todo: вносим изменения в модель в зависимости от типа образования it.stepOutData?.education?.let {       // todo: применяем к экрану данные из черновика  }    }


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

progressInteractor.completeStep(EducationStepOutData(education)).subscribe {                   // обработка успешного сохранения данных (если нужно)               }


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

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

progressInteractor.sendApplication()            .subscribeOn(Schedulers.io())            .observeOn(AndroidSchedulers.mainThread())            .subscribe(                {                    // реакция на успешную отправку                    activityNavigator.start(ThankYouRoute())                },                {                    // обработка ошибок                }            )


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

progressInteractor.closeProgressFeature()


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

Особая благодарность Васе Беглянину @icebail автору первой реализации этого подхода в проекте. А также Мише Зинченко @midery за помощь в доведении чернового варианта архитектуры до финальной версии, которая и описана в этой статье.
Подробнее..

Сервис на языке Dart каркас серверного приложения

18.08.2020 12:17:00 | Автор: admin
Оглавление


Подготовка


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

В этой статье мы напишем приложение с использованием фреймворка Aqueduct, оценим его производительность и потребление ресурсов в разных режимах, напишем инструментарий для компиляции в нативное приложение для Windows и Linux, разберемся с миграциями схемы базы данных для доменных классов приложения и даже опубликуем наш инструментальный docker образ в публичный регистр DockerHub.



полезности
  • все скриншоты с кодом кликабельные
  • полный код здесь
  • консольные команды собраны в README.md проекта
  • комментарии, вопросы, советы приветствуются
  • пообщаться с автором можно в Телеграмм канале


Установка Aqueduct


Начнем с установки dart-sdk набора средств разработки на языке Dart. Установить его можно с использованием пакетного менеджера вашей операционной системы как предложено здесь. Однако, в случае Windows никакого пакетного менеджера в вашей системе по умолчанию не установлено. Поэтому просто:
  • Скачаем архив и распакуем его на диск C:
  • Теперь, чтобы наша операционная система знала, где искать исполняемые файлы, добавим необходимые пути. Откроем переменные ОС. Для этого начнем вводить изменение переменных среды текущего пользователя в строке поиска
  • В открывшемся окне выберем переменную Path и нажмем Изменить. В открывшемся списке создадим новую строку с адресом до исполняемых файлов dart в файловой системе, например, C:\dart-sdk\bin
  • Проверим, что dart и pub (пакетный менеджер dart) доступны
    dart --version
    



    pub -v
    


  • Возможно, чтобы новые пути стали доступны, придется перезагрузиться
  • Установим утилиту командной строки aqueduct CLI (command line interface)
    pub global activate aqueduct
    
    Проверим доступность
    aqueduct
    



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

Генерация приложения


Итак, откроем папку нашего сервера в VsCode
code c:/docs/dart_server

Для тех, кто не видел первую и вторую статьи, исходный код можно склонировать из guthub репозитория:
git clone https://github.com/AndX2/dart_server.git
Создадим шаблон приложения:
aqueduct create data_app



Ознакомимся с содержимым шаблона проекта:
  • README.md заметка с описанием, как работать с aqueduct проектом, запускать тесты, генерировать API документацию и пр. Вспомогательный файл.
  • pubspec.yaml спецификция для пакетного менеджера pub. Здесь находятся сведения об используемых пакетах, названии, описании, версии проекта и пр.
  • config.yaml и config.src.yaml конфигурация для отладки и тестирования проекта соответственно. Мы не будем использовать этот способ конфигурирования.
  • analysis_options.yaml правила для линтера (утилиты подсветки ошибок в исходном коде). Вспомогательный файл.
  • .travis.yml конфигурация для системы автоматической сборки и тестирования (continuous Integration). Вспомогательный файл.
  • pubspec.lock и .packages автоматически сгенерированные файлы пакетного менеджера pub. Первый список всех зависимостей проекта, включая транзитивные и их конкретные версии, второй расположение скачанных пакетов зависимостей в файловой системе (кэше).
  • .dart_tool/package_config.json файл отчета о генерации кода нашего проекта, созданный aqueduct CLI. Вспомогательный файл.
  • bin/main.dart точка входа в приложение при локальном запуске (например, для отладки). Мы не будем использовать такой способ запуска (исключая тесты).
  • lib/channel.dart Фактически ApplicationChannel это и есть экземпляр нашего приложения. Aqueduct умеет запускать несколько таких экземпляров для более эффективной утилизации ресурсов CPU и RAM. Такие экземпляры работают в изолированных потоках (в Dart их называют isolate) и никак (почти) не могут взаимодействовать друг с другом.
  • lib/data_app.dart файл инкапсуляции импортов зависимостей. Позволяет объединить необходимые пакеты в условную (library) библиотеку dart_app
  • test/ автотесты. Здесь можно разместить юнит-тесты, поскольку механизм тестирования сетевого слоя рассчитан на локальный запуск приложения и не будет использоваться при разработке. Для тестов будем использовать Postman.


Конфигурация


Первая задача, которую предстоит решить, настройка приложения при запуске. Aqueduct имеет встроенный механизм извлечения параметров из конфигурационных файлов, но этот механизм не очень удобен при запуске в Docker контейнере. Мы поступим иначе:
  • Передадим список переменных в операционную систему контейнера.
  • При запуске приложения внутри контейнера прочитаем переменные окружения операционной системы и используем их для первоначальной настройки.
  • Создадим маршрут для просмотра по сети всех переменных окружения запущенного приложения (это будет полезно при просмотре состояния приложения из панели администратора).

В папке /lib создадим несколько папок и первый репозиторий для доступа к переменным окружения:


EnvironmentRepository в конструкторе считывает переменные окружения из операционной системы в виде словаря Map<String, String> и сохраняет в приватной переменной _env. Добавим метод для получения всех параметров в виде словаря:


lib/service/EnvironmentService логический компонент доступа к данным EnvironmentRepository:


Инъекция зависимостей


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

Эти задачи решим с помощью библиотеки GetIt. Подключим необходимый пакет в pubspec.yaml:


Создадим экземпляр контейнера инжектора lib/di/di_container.dart и напишем метод с регистрацией репозитория и сервиса:


Метод инициализации контейнера DI вызовем в методе подготовки приложения:


Cетевой слой


lib/controller/ActuatorController сетевой http компонент. Он содержит методы доступа к служебным данным приложения:


Задекларируем обработчики маршрутов для контроллеров в lib/controller/Routes:


Первый запуск


Для запуска необходимо:
  • приложение упаковать в Docker образ,
  • добавить контейнер в сценарий docker-compose,
  • настроить NGINX для проксирования запросов.

В папке приложения создадим Dockerfile. Это скрипт сборки и запуска образа для Docker:


Добавим контейнер приложения в сценарий docker-compose.yaml:


Создадим файл data_app.env с переменными конфигурации для приложения:


Добавим новый location в отладочный конфиг NGINX conf.dev.d/default.conf:


Запускаем отладочный сценарий с флагом предварительной сборки образов:
docker-compose -f docker-compose.yaml -f docker-compose.dev.yaml up --build



Сценарий успешно запустился, но настораживают несколько моментов:
  • официальный образ со средой dart от google занимает 290MБ в виде архива. В распакованном виде он займет кратно больше места 754МБ. Посмотреть список образов и их размер:
    docker images
    

  • Время сборки и JIT-компиляции составило 100+ сек. Многовато для запуска приложения на проде
  • Потребление памяти в docker dashboard 300 МБ сразу после запуска
  • В нагрузочном тесте (только сетевые запросы GET /api/actuator/) потребление памяти находится в диапазоне 350390 МБ для приложения, запущенного в одном изоляте

Предположительно ресурсов нашего бюджетного VPS не хватит для работы такого ресурсоемкого приложения. Давайте проверим:
  • Создадим на сервере папку для новой версии приложения и скопируем содержимое проекта
    ssh root@dartservice.ru "mkdir -p /opt/srv_2" && scp -r ./* root@91.230.60.120:/opt/srv_2/
    
  • Теперь необходимо перенести в эту папку проект web-страницы из /opt/srv_1/public/ и все содержимое папки /opt/srv_1/sertbot/ (в ней находятся SSL сертификаты для NGINX и логи Lets encrypt бота), также скопируем ключ из /opt/srv_1/dhparam/
  • Запустим в отдельной консоли монитор ресурсов сервера
    htop
    

  • Выполним docker-compose сценарий в папке /opt/srv_2/
    docker-compose up --build -d
    

  • Так выглядит сборка приложения перед запуском:
  • А так в работе:

    Из доступного 1ГБ оперативной памяти наше приложение потребляет 1,5ГБ заняв недостающее в файле подкачки. Да, приложение запустилось, но ни о какой нагрузочной способности речь не идет.
  • Остановим сценарий
    docker-compose down
    


AOT


Нам предстоит решить три задачи:
  • снизить потребление оперативной памяти dart приложением,
  • уменьшить время запуска,
  • снизить размер docker-контейнера приложения.

Решением станет отказ от dart в рантайме. Начиная с версии 2.6, dart-приложения поддерживают компиляцию в нативный исполняемый код. Aqueduct поддерживает компиляцию начиная с версии 4.0.0-b1.
Начнем с локального удаления aqueduct CLI
pub global deactivate aqueduct

Установим новую версию
pub global activate aqueduct 4.0.0-b1

Поднимем зависимости в pubspec.yaml:


Соберем нативное приложение
aqueduct build

Результатом будет однофайловая сборка data_app.aot размером около 6 МБ. Можно сразу запустить это приложение с параметрами, например,
data_app.aot --port 8080 --isolates 2
Потребление памяти сразу после запуска менее 10 МБ.
Посмотрим под нагрузкой. Параметры теста: сетевые запросы GET /actuator, 100 потоков с максимальной доступной скоростью, 10 минут. Результат:


Итого: средняя скорость 13к запросов в сек для тела ответа JSON 1,4кВ, среднее время ответа 7 мсек, потребление оперативной памяти (на два инстанса) 42 MB. Ошибок нет.
При повторном тесте с шестью инстансами приложения средняя скорость, конечно, повышается до 19к/сек, но и утилизация процессора достигает 45% при потреблении памяти 64 МБ.
Это превосходный результат.

Упаковка в контейнер


Здесь мы столкнемся еще с одной сложностью: скомпилировать dart-приложение в натив мы можем только под текущую ОС. В моем случае это Windows10 x64. В docker-контейнере я, конечно, предпочел бы один из дистрибутивов Linux например, Ubuntu20_10.

Решением здесь станет промежуточный docker-стенд, используемый только для сборки нативных приложений под Ubuntu. Напишем его /dart2native/Dockerfile:


Теперь соберем его в docker-образ с именем aqueduct_builder:4.0.0-b1, перезаписав, если есть, старые версии:
docker build --pull --rm -f "dart2native\Dockerfile" -t aqueduct_builder:4.0.0-b1 "dart2native"


Проверим
docker images



Напишем сценарий сборки нативного приложения docker-compose.dev.build.yaml:


Запустим сценарий сборки
docker-compose -f docker-compose.dev.build.yaml up



Файл скомпилированного под Ubuntu приложения data_app.aot занимает уже 9 МБ. При запуске утилизирует 19 МБ оперативной памяти (для двух инстансов). Проведем локальное нагрузочное тестирование с теми же условиями, но в контейнере с проксированием NGINX (GET, 100 потоков):


В среднем 5,3к запросов в секунду. При этом потребление оперативной памяти не превысило 55 МБ. Размер образа уменьшился по сравнению с установленным dart и aqueduct c 840 МБ до 74 МБ на диске.

Напишем новый сценарий docker-compose.aot.yaml запуска приложения. Для этого заменим блок описания data_app, установив базовым образ пустой Ubuntu:20_10. Смонтируем файл сборки и изменим команду запуска:


Решим еще одну сервисную задачу: фактически сборочный docker-образ c установленными dart и aqueduct вполне себе переиспользуемый инструмент. Имеет смысл выгрузить его в общедоступный регистр и подключать как готовый скачиваемый образ. Для этого необходимо:
  • зарегистрироваться в публичном регистре, например, DockerHub,
  • авторизоваться локально с тем же логином
    docker login 
    
  • переименовать выгружаемый образ по схеме login/title:tag
    docker image tag a365ac7f5bbb andx2/aqueduct:4.0.0-b1
    
  • выгрузить образ в регистр
    docker push andx2/aqueduct:4.0.0-b1
    

    https://hub.docker.com/repository/docker/andx2/aqueduct/general

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

Подключение базы данных


В aqueduct уже встроен ORM для работы с базой данных PostgreSQL. Для его использования необходимо:
  • Создать доменные объекты, описывающие записи в базе данных.
  • На основе доменных объектов и связей между ними сгенерировать файл миграции. Заметка: для записи в базу данных необходимо, чтобы в БД были подготовлены таблицы, чьи схемы подходят для хранения доменных объектов. Aqueduct предоставляет инструмент миграции, который фактически обходит все классы проекта, являющиеся расширением ManagedObject (управляемые объекты), прочитывает типы их полей и связей с другими управляемыми объектами и создает специальный файл, в котором описано изменение схемы таблиц и связей в базе данных по сравнению со схемой предыдущего файла миграции. Файлы миграции добавляются при каждой перегенерации схемы.
  • Применить файлы миграции к базе данных. Применение происходит последовательно, начиная с версии, которая записана в БД текущей.
  • В файлы миграции, сгенерированные aqueduct, можно вносить свои изменения, например определить реализацию метода seed() для добавления в БД каких-либо начальных данных.
  • Генерация и применение миграций производится aqueduct CLI.

Начнем с подключения нового docker-контейнера с БД PostgreSQL в сценарии docker-compose.aot.yaml. Готовый образ на основе Linux Alpine (компактная версия Linux для встраиваемых применений):


Здесь необходимо обратить внимание на файл переменных окружения data_db.env. Дело в том, что образ заранее настроен на использование этих переменных в качестве имени пользователя, хоста, порта и пароля доступа. Добавим эти переменные в файл:


Значения приведены условно.
Также смонтируем папку хоста ./data_db/ в контейнер для хранения данных БД.
Далее в приложении data_app добавим класс /service/DbHelper для подключения к базе данных, используя переменные окружения:


Создадим доменный объект, управляемый ORM, для получения настроек клиентского приложения:


Добавим репозиторий и сервис для добавления настроек и получения актуальной версии:




Сетевой контроллер:


Зарегистрируем новые компоненты в DI контейнере:


Добавим новый контроллер и эндпойнт в маршрутизатор:


Теперь сгенерируем файл миграции базы данных. Выполним
aqueduct db generate
Результатом будет создание файлов миграции в папке проекта:




Теперь нужно решить сервисную задачу: миграции нужно применять из системы с установленным aqueduct (и dart) к базе данных, работающей в контейнере, и это нужно выполнять как при локальной разработке, так и на сервере. Используем для этого кейса ранее собранный и опубликованный образ для AOT-сборки. Напишем соответствующий docker-compose сценарий миграции БД:


Интересная деталь строка подключения к БД. При запуске сценария можно передать в качестве аргумента файл с переменными окружения, а затем использовать эти переменные для подстановки в сценарии:
docker-compose -f docker-compose.migrations.yaml --env-file=./data_app.env --compatibility up --abort-on-container-exit

Также обратим внимание на флаги запуска:
  • --compatibility совместимость с версиями docker-compose сценариев 2.х. Это позволит использовать параметры deploy для ограничения использования ресурсов контейнером, которые игнорируются в версиях 3.х. Мы ограничили потребление оперативной памяти до 200МБ и использование процессорного времени до 50%
  • --abort-on-container-exit этот флаг устанавливает режим выполнения сценария таким образом, что при остановке одного из контейнеров сценария будут завершены все остальные. Поэтому, когда выполнится команда миграции схемы базы данных и контейнер с aqueduct остановится, docker-compose завершит также и работу контейнера базы данных


Публикация


Для подготовки к публикации приложения необходимо:
  • Изменить переменные окружения в data_app.env и data_db.env. Напомню, что сейчас у нас POSTGRES_PASSWORD=postgres_password
  • Переименовать сценарий запуска нативного приложения docker-compose.aot.yaml в docker-compose.yaml. Команда запуска приложения на сервере не должна иметь аргументов
  • Временно заблокировать маршрут просмотра переменных окружения запущенного приложения /api/actuator. В следующей статье мы реализуем механизм авторизации по ролям и откроем доступ к этому маршруту только для администратора.

Скопируем на сервер папку приложения ./data_app/. Важным моментом здесь будет ключ -p (копировать с сохранением атрибутов файлов) в команде копирования. Напомню, что при сборке нативного приложения мы установили права на исполнение файлу data_app.aot:
scp -rp ./data_app root@dartservice.ru:/opt/srv_1

Скопируем также:
  • Измененную конфигурацию NGINX ./conf.d/default.conf
  • Сценарии запуска и миграции docker-compose.yaml, docker-compose.migrations.yaml
  • Файлы с переменными окружения data_app.env и data_db.env

Добавим на сервере папку /opt/srv_1/data_db. Это том файловой системы хоста для монтирования в контейнер базы данных. Здесь будут сохраняться все данные PostgreSQL.
mkdir /opt/srv_2/data_db

Выполним сценарий миграции схемы базы данных:
docker-compose -f docker-compose.migrations.yaml --env-file=./data_app.env up --abort-on-container-exit

Запустим сценарий приложения
docker-compose up -d


Исходный код github.

Вместо заключения


Каркас для бэкенд приложений готов. В следующей статье на его основе мы напишем новое приложение для авторизации пользователей сервиса. Для этого мы воспользуемся спецификацией oAuth2 и интегрируемся с VK и Github.
Подробнее..

Формальные грамматики на службе мобильного клиента

17.09.2020 12:16:31 | Автор: admin
В повседневной жизни мы пользуемся готовыми интерпретаторами и компиляторами и редко кому придёт в голову написать их самостоятельно. Во-первых, это же сложно, во-вторых зачем.

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

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



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

В конце концов, заполненную форму мобильное приложение отправляет на сервер. В этот момент важно, чтобы данные были правильно отформатированы. Например, API интернет-банка будет требовать, чтобы номер выглядел вот так: 9161234567 без 8, скобочек и минусов.

Если форма пассивно обрабатывает пользовательские данные, то номер телефона она принимает в любом формате, но возникают проблемы:

  • Мы вынуждаем пользователя разбираться в том, как нужно правильно отформатировать номер телефона, чтобы система его приняла.
  • Мы вынуждаем пользователя набирать на клавиатуре труднодоступные символы: переключать раскладку клавиатуры, чтобы набрать цифры или, например, знак плюса.
  • Нам нужно точно объяснить пользователю, в чем он ошибся.



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

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

Если продолжать пример с номером телефона, использование маски даст следующие преимущества:

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



Как видим, маски помогают пользователю легко и безошибочно вводить сложные данные, а программисту упрощают жизнь: всю тяжелую работу по форматированию пользовательского ввода берёт на себя библиотека по работе с масками на стороне клиентского приложения. Программисту всего лишь нужно описать саму маску.
Маски это по сути UX-сахар


Почему нельзя просто взять и описать маску


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

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

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

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

  1. понять правила построения исходной грамматики,
  2. понять правила построения целевой грамматики,
  3. написать правила перевода из исходной грамматики в целевую,
  4. реализовать всё это в коде.

Это то, для чего и пишутся компиляторы и трансляторы.

Теперь подробно рассмотрим наше решение на основе формальных грамматик.

Предыстория


В нашем приложении довольно много разнообразных экранов, которые формируются по backend driven принципу: полное описание экрана вместе с данными приходит с сервера.


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



Давайте посмотрим, как устроены маски.

Примеры масок в разных форматах


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



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

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



Дальше идёт динамическая часть она всегда выделена угловыми скобками:


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


Здесь записано выражение, по которому мы будем форматировать наш ввод:



Красным выделены кусочки, отвечающие за содержимое динамической части.

\\d любая цифра.

+ обычный репитер: повторить минимум один раз.

${3} символ метаинформации, который уточняет количество повторений. В данном случае должно быть три символа.

Тогда выражение \\d+${3} означает, что должно быть три цифры.

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



Это ограничение появилось не просто так сейчас объясню почему.
Допустим, у нас есть ДВ, в котором жёстко указан размер: 4 элемента. И мы задаём ему 2 элемента с репитером: `<!^\\d+\\v+${4}>`. Под такое ДВ попадают следующие сочетания:

  • 1abc
  • 12ab
  • 123a

Получается, что такое ДВ не даёт нам однозначного ответа, чего ожидать на месте второго символа: цифру или букву.

Берём маску, складываем её с пользовательским вводом. Получаем отформатированный номер телефона:



На клиенте формат у масок может выглядеть по-другому. Например, в библиотеке Input Mask от Redmadrobot маска для номера телефона имеет следующий вид:



Выглядит она симпатичнее и понимать её проще.

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



Переформулируем задачу: как совместить маски разных форматов


Нам нужно эти маски друг с другом совместить или как-то из одной получить вторую.



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

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

Так как мы дошли до интерпретатора, давайте поговорим про грамматики.

Как проводится синтаксический анализ




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

Этот процесс называется токенизация: поток символов превращается в поток токенов. Количество токенов ограничено, они формализованы, следовательно, можно их анализировать.

Далее, исходя из правил грамматики, по потоку токенов строим абстрактное синтаксическое дерево. Из дерева получаем поток символов в нужной нам грамматике.

Есть выражение. Смотрим на него и видим, что у нас есть константа, про которую я говорил выше:



Все константы представим как токен CS, у которого аргумент сама константа:


Следующий вид токенов это начало ДВ:


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



Затем у нас идёт репитер.



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



Конец ДВ. Таким образом, мы разложили всё по токенам.



Пример токенизации маски для номера телефона


Чтобы посмотреть, как в принципе происходит процесс токенизации и как будет работать интерпретатор, возьмём маску для номера телефона и преобразуем её в поток токенов.



Сначала символ +. Преобразуем в константный символ +. Далее то же самое мы делаем для семёрки и для всех остальных символов. Получаем массив из токенов. Это ещё не структура далее будем этот массив анализировать.

Лексер и построение АСД


Теперь более сложная часть это лексер.



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

Правило symbolRule описывает какой-то символ. Если это правило применимо, если оно верно, это значит что мы встретили либо специальный символ, либо константный символ. Можно сказать, что это функция.

Дальше repeaterRule. Это правило описывает ситуацию, когда встречается какой-то символ, а за ним токен репитера.

Дальше всё похожим образом выглядит. Если это ДВ, то это либо symbol, либо repeater. В нашем случае это правило шире. И в конце обязательно должен быть токен с метаданными.
Последнее правило maskRule. Это последовательность из символов и ДВ.

Теперь построим абстрактное синтаксическое дерево (АСД) из массива токенов.

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



У нас есть первый токен +, значит, мы просто добавляем дочерний узел, и все.



То же самое делаем со всеми остальными константными символами, но дальше сложнее. Мы наткнулись на токен ДВ.



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



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



На самом деле, в данном случае, нет. У нас дочерним узлом будет репитер.



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

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

Особенно это было бы заметно на группах символов: например, [abcde]. В том случае, очевидно, должен быть какой-то родительский узел GROUP, у которого будет список дочерних узлов CS(a)CS(b) и т.д.

Возвращаемся к токену с метаданными. Он не включается в контент, он находится сбоку.



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

Закончилось ДВ, и мы не считаем это каким-то узлом: это был токен, который теперь можно выкинуть. Мы не будем превращать его в узел дерева.



У нас уже есть поддерево, корнем которого является узел SR то есть та самая динамическая часть. Токен конца ДВ очень нам помогает в процессе построения дерева мы можем понять, когда закончено построение поддерева для ДВ. Но никакой ценности для логики этот токен не несёт: глядя на построчное дерево, мы и так понимаем, когда ДВ закончится, потому что оно как бы закрыто узлом SR.

Дальше просто обычные константные символы.



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

Синтаксис библиотеки InputMask от Redmadrobot


Рассмотрим синтаксис библиотеки Redmadrobot.



Здесь то же самое выражение. +7 константа, которая добавится автоматически. Внутри фигурных скобок описано ДВ динамическая часть. Внутри ДВ специальный символ d. У Redmadrobot это дефолтная нотация, которая обозначает цифру.

Так выглядит нотация:



Нотация состоит из трёх частей:

  • character символ, который мы будем использовать, чтобы записать маску. То, из чего состоит алфавит маски. Например, d.
  • characterSet какие набранные пользователем символы матчатся этой нотацией. Например, 0, 1, 2, 3, 4 и так далее.
  • isOptional обязательно ли пользователь должен ввести один из символов characterSet или можно ничего не вводить.

Смотрим, у нас сейчас будет такая маска.



  • У символа b специальная нотация цифры и он не опциональный.
  • У символа c другая нотация CharacterSet другой. Он тоже не опциональный.
  • И символ C это то же самое что c, только он опциональный. Это нужно для того, чтобы в маске мы посмотрели на метаданные и увидели, что там не жёсткое ограничение, а слабое.

Если нужно записать правило, когда символов может быть от одного до десяти, то один символ будет не опциональный. А девять символов будут опциональными. То есть в нотации из примера они будут записаны большими буквами. В итоге это правило будет выглядеть так: [cCCCCCCCCC]

Пример: перевод маски номера телефона из формата бекэнда в формат InputMask


Вот дерево, которое мы получили на прошлом этапе. Нам нужно по нему пройтись. Первое, куда мы попадаем, это корень.



Дальше от корня мы попадаем в константный символ + генерируем сразу +. Справа записывается маска в формате InputMask.



Следующий символ понятный просто 7, а за ним открывающая скобка.

Дальше генерируется кусок динамической части, но он пока не заполнен.



Идём внутрь, у нас контент, это технический узел. Ничего не пишем никуда.



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



Доходим, наконец, до какого-то контентного символа.



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

Вот мы его написали, возвращаемся и идём как раз за метаинформацией.



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



В результате мы получаем маску, которая похожа на маску в формате роботов.

На практике мы взяли одну грамматику и сгенерировали из неё другую грамматику.

Правила генерации клиентской грамматики из серверной


Теперь немного про правила генерации. Это важно.

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



Сначала идёт набор символов, и мы должны конвертировать его в какую-то нотацию с точки зрения InputMask. Почему? Потому что это какой-то ограниченный набор символов, которые нам нужно матчить. Нам нужно совместить пользовательский ввод и символ, и поэтому здесь у нас будет записана какая-то определённая нотация.

Дальше у нас идёт символ \\d.

Дальше ДВ с опциональным размером.



Первый, получается, какой-то символ b. У него будет Character Set, содержащий abcd.
Далее понятно, что будет другой уже символ, потому что не сматчишь иначе, или сматчишь неправильно. И дальше у нас это выражение превращается вот в такое.

В последней части обязательно должен быть хотя бы один символ. Обозначим это требование как d. Но также пользователь может ввести два дополнительных символа, и тогда они обозначаются как DD.

Соберём всё вместе.



Здесь приведён пример Character Set, которые генерируются. Видно, что b соответствует Character Set abcd, для цифр соответствующий предустановленный Character Set. Для d и D соответствующий Character Set содержит 12vf.

Итоги


Мы научились автоматически конвертировать одну грамматику в другую: теперь в нашем приложении работают маски по спецификации сервера.

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



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

Работающая библиотека по переводу масок


Можете посмотреть на то, как мы реализовали вышеописанный подход. Библиотека лежит на Гитхабе.

Примеры перевода разных масок


Это первая маска, которую мы смотрели в самом начале. Она интерпретируется в такое RedMadRobot представление.



А это вторая маска просто маска ввода чего-то. Она конвертируется в такое представление.

Подробнее..

Перевод Flutter результаты опроса разработчиков за Q3 2020

24.10.2020 12:06:33 | Автор: admin
Привет! На связи Евгений Сатуров из Surf.

Команда Google опубликовала результаты опроса Flutter-разработчиков за третий квартал 2020 года. Публикую перевод материала с моими комментариями.




Статью подготовила команда Flutter по изучению UX (Flutter UXR) (ДжаЁн Ли, Йоян Хоу, Джек Ким, Тао Дон)

В августе 2020 года команда Flutter запустила 10-й ежеквартальный опрос разработчиков. За 10 дней его прошли 7 668 Flutter-разработчиков по всему миру. Каждый из них потратил на ответы около 7,4 минут суммарно это 39,4 дней. Мы высоко ценим время, которое вы потратили, чтобы оставить свой отзыв, и хотим поделиться с вами результатами.

Как обычно мы спрашивали о степени вашей удовлетворённости различными компонентами Flutter. Кроме того, в этом квартале нас интересовали отзывы по новым темам, таким как Flutter for web, сливеры (виджеты для создания сложных эффектов скроллинга) и отладка проблем, характерных для конкретных платформ. В этой статье мы подробнее рассмотрим каждую из тем.

Сводные данные


  • 94% опрошенных были довольны фреймворком Flutter в целом (PSAT positively satisfied), а 58% были очень им довольны (VSAT very satisfied). Доля PSAT не изменилась, однако процент VSAT неуклонно растёт.
  • Доля профессиональных разработчиков выросла с 26% до 31%. Увеличивается и доля продвинутых пользователей.
  • Из тех, кто за последние 3 месяца использовал Flutter for web, 59% были довольны его работой. 71% довольны способностью Flutter реализовать пользовательский интерфейс, который ведёт себя как нативный веб-UI.
  • Из пользователей, которым хотелось реализовать сложные эффекты скроллинга с помощью сливеров, 79% пробовали их использовать. Основная проблема (36%) состояла в том, что подходящий виджет было трудно найти.
  • 71% пользователей выполняли отладку проблем, характерных для конкретных платформ. Часто проблемы возникали с инструментами (32%), визуальными различиями (28%) и управлением зависимостями (28%).

Степень удовлетворённости и изменения пользовательской базы


В этом квартале процент пользователей, очень довольных Flutter, достиг рекордных 58%. В целом пользователи Flutter всё так же довольны продуктом (94%), несмотря на экспоненциальный рост сообщества. На следующем графике показано, как степень удовлетворенности Flutter менялась с течением времени.


58% пользователей ответили Очень доволен на вопрос: Насколько вы довольны Flutter в целом?

Наша пользовательская база претерпела несколько существенных изменений. Во-первых, процент корпоративных пользователей Flutter-фреймворка значительно вырос с 26% в первом и втором кварталах до 31%, в то время как процент разработчиков из стартапов по-прежнему составляет около 35%.


Несмотря на то, что большинство пользователей Flutter работают на стартапы, процент корпоративных разработчиков значительно вырос с 26% до 31%

Комментарий
Удивляет то, насколько медленно Flutter покоряет агентства разработки. Мы в Surf проверили и убедились: качественная кроссплатформа решает большое количество проблем сервисной разработки.

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

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

Кроме того, в пользовательской базе существенно изменилась субъективная оценка опыта работы с Flutter. Как видно на следующем графике, доля начинающих пользователей постепенно уменьшалась, в то время как доля продвинутых пользователей постепенно увеличивалась. Это значит, что в нашем сообществе есть более опытные пользователи, которые могут поделиться своими знаниями с начинающими. Если вам интересно чему-то научиться или обменяться знаниями, можете поучаствовать в онлайн-обсуждениях с другими разработчиками Flutter. Заходите во вкладку Community на flutter.dev.


Доля продвинутых пользователей неуклонно растёт

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

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

Совет тем, кто столкнулся с проблемой дефицита кадров. Попробуйте практиковать западную HR-политику. Нанимайте не Flutter-разработчиков, а универсальных software-инженеров. Хороший специалист быстро овладеет фреймворком, а изучение нового языка точно не обернётся крахом.

Flutter for Web


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

Участники опроса активно использовали Flutter для создания веб-приложений. Согласно третьей строке следующего графика, около 33% сообщили, что оценивали возможность применения Flutter в коммерческих веб-приложениях (15%), собрали с его помощью демо-версию приложения (11%) или выпустили коммерческое приложение (7%).


33% пользователей (1468 из 4449) сообщили, что оценивали возможность применения Flutter в коммерческих веб-приложениях (15%), собрали с его помощью демо-версию приложения (11%), или выпустили коммерческое приложение (7%)

В частности, 29,5% пользователей с опытом как в мобильной, так и в веб-разработке, за последние 3 месяца более серьёзно использовали Flutter for Web (в потенциально коммерческих целях). Процент варьировался в зависимости от предыдущего опыта взаимодействия разработчиков с платформами. По всей видимости, респонденты, ранее занимавшиеся только веб-разработкой, использовали Flutter в качестве альтернативного веб-фреймворка (22% использовали Flutter for Web в потенциально коммерческих целях), а респонденты, ранее занимавшиеся только мобильной разработкой, активно использовали Flutter for Web для интеграции с веб-разработкой (16% использовали Flutter for Web).


Процент пользователей, применявших Flutter в веб в коммерческих целях за последние 3 месяца, варьировался в зависимости от их предыдущего опыта работы с мобильными и веб-платформами

Также веб-команда Flutter собрала отзывы о различных проблемах, связанных с внешним видом и функциями веб-приложения. Во-первых, мы узнали, что наиболее важными участники опроса считают навигацию в браузере и историю переходов по страницам (55%), копирование/вставку выделенного текста (34%), физику скроллинга (33%) и выделение текста (32%). Респонденты также попросили улучшить документацию по переходам по страницам и адаптации мобильных макетов к веб.

Комментарий
Flutter for Web интересует многих. Кажется, что прямо сейчас основные силы Flutter Team брошены на доработку фреймворка и адаптацию его под web. В недавнем выпуске FlutterDevPodcast мы обсудили текущее состояние Flutter for Web с гостями, которые уже могут похвастаться продуктом, запущенном на этой технологии в продакшне. Мы обсудили производительность, нативный UX, особенности вёрстки и реализации асинхронного кода. Затронули также темы CEO и хостинга для проекта.



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

Команда Flutter учитывает отзывы разработчиков и активно работает над улучшениями. Для решения основных проблем пользователей, связанных с навигацией и переходами по страницам, команда недавно выпустила Navigator 2. Мы также добавили поддержку копирования/вставки выделенного текста и планируем усовершенствовать функциональность выделенного текста (особенно в форматированном тексте). Мы продолжаем работать над проблемами с физикой скроллинга и производительностью, учитывая issues, полученные от сообщества.

В заключение наша группа получила отзывы о рабочих процессах, производительности и сторонних API. Среди всех основных рабочих процессов самым сложным, по мнению респондентов, оказался дебаггинг. Скорость загрузки страниц и скроллинг аспекты, в которых респонденты чаще всего сталкивались с проблемами производительности. Участники опроса попросили улучшить поддержку локального хранилища (например, SQLite), хранилища Firebase и Google карт для Flutter for Web. Наша команда будет работать над улучшением этих аспектов по мере развития Flutter for Web.

Комментарий
Безусловно, стремление довести Flutter for Web до идеала похвально. Готов ли фреймворк стать полноценной заменой своим более традиционным конкурентам? Моё мнение однозначно: нет. Впереди ещё долгий путь улучшений и доработок.

Сливеры


Sliver-виджеты (виджеты, названия которых начинаются с Sliver, например SliverAppBar и SliverList) используются для создания сложных эффектов скроллинга. Многие эффекты скроллинга можно реализовать с помощью таких виджетов, как ListView, GridView, PageView или AnimatedList, однако sliver-виджеты помогают кастомизировать scroll view и сделать UI красивее.


Такие сложные эффекты скроллинга можно реализовать с помощью sliver-виджетов

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

Первым делом мы узнали, что большинство участников опроса (49%) разрабатывает UI с простыми эффектами скроллинга. UI со сложными эффектами разрабатывают 39%. (В опросе были представлены примеры простого и сложного эффектов скроллинга.) Из тех, кому требуются сложные эффекты, 78% сказали, что для реализации необходимых эффектов им нужны сливеры.


UI с простыми эффектами скроллинга разрабатывает больше пользователей (49%), чем UI со сложными эффектами (39%)

20% пользователей, которым нужны сливеры, сообщили, что не пробовали их использовать. А что ещё интереснее, 35% пользователей, воспользовавшихся сливерами, сообщили, что у них возникли проблемы. Когда мы спросили, с чем возникли наибольшие сложности, первое место занял поиск (36%), затем изучение (30%) и, наконец, удобство использования (19%).


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

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

А если вы ещё не знакомы со сливерами и хотите больше о них узнать, предлагаем вам следующие ресурсы:

Slivers, demystified (Небольшая статья)

Slivers explained making dynamic layouts (The Boring Flutter Development Show на YouTube, эпизод 12)

Комментарий
Сливеры камень преткновения для многих Flutter-разработчиков. Их боятся и не понимают. Может быть, виной тому не самый прозрачный API для работы с компонентами, а также хитрая вложенность виджетов. CustomScrollView, SliverList, SliverChildBuilderDelegate пока не разберёшься, какую роль играет каждый из этих компонентов в построении общей картины, кажется, что пытаешься разгадать фокус опытного иллюзиониста. На самом деле сливеры и им подобные механизмы одна из основных причин, почему я люблю Flutter. Возможность просто делать сложные вещи дорогого стоит.

Отладка проблем, характерных для конкретных платформ


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

Во-первых, мы спросили пользователей, какие платформо-специфичные проблемы они дебажили. В результате мы обнаружили, что наиболее часто возникают проблемы с тулингом (32%), визуальными различиями на разных платформах (28%), управлением зависимостями (28%), различиями в функционировании на разных платформах (27%), различиями в функционировании плагинов на разных платформах (26%), а также часто отсутствуют нативные фичи (25%).

Комментарий
Совсем недавно на просторах GitHub появилась кастомная сборка Flutter Engine с отключённым Metal. Оказалось, что без него приложение, собранное под iOS, работает в разы плавнее! С такими по-настоящему неприятными платформенными проблемами мы сталкиваемся не часто. Тем не менее, к ним всегда надо быть готовым. Чтобы быть во всеоружии, поможет только глубокое погружение в фундаментальные основы поддерживаемых платформ.



Наиболее часто проблемы, связанные с конкретной платформой, подразумевают проблемы с инструментами, визуальные различия и проблемы с управлением зависимостями

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


Примечание к рисунку (слева-направо): Difficulty сложность, Test issues проблемы с тестированием, Visual differences визуальные различия, Importance важность, Device-specific issues проблемы, характерные для конкретных устройств, Dependency management issues проблемы с управлением зависимостями, Release issues проблемы с релизом, Tooling issues проблемы с инструментами, Behavioral differences различия в функционировании, Missing native features отсутствие нативных средств, Plugin behavior differences различия в функционировании плагинов, Performance discrepancies различия в производительности.

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


Что касается тестирования приложений на нескольких платформах, 85% респондентов заявили, что это очень или крайне важно. Тем не менее, это оказалось очень или крайне сложно для 27% опрошенных. Следовательно, тестирование приложений на нескольких платформах важно, но для большинства разработчиков оно не представляет большой сложности. Из развёрнутых ответов на вопросы мы узнали, что наиболее часто проблемы с тестированием возникают при тестировании для iOS (особенно у разработчиков под Windows), тестировании для нескольких размеров экрана и тестировании на нескольких физических устройствах.

В следующем квартале наша команда потратит больше времени, основательно сократит число ошибок и решит основные проблемы, связанные с инструментами, управлением зависимостями и релизом. Также мы продолжим совершенствовать документацию на эти темы. Например, мы улучшим пользовательскую документацию для таких инструментов CI, как например, GitHub Actions и Bitrise, которые помогают пользователям тестировать приложения на iOS.

Комментарий
GitHub Actions прекрасен до тех пор, пока ваш репозиторий не становится приватным. С этого момента у вас будет всего лишь 200 бесплатных минут билд-тайма в месяц для сборки на виртуалке под macOS. Прочитайте мою статью про настройку идеального workflow для Flutter-проекта.

Что дальше


Ваш вклад каждый день влияет на наши решения. Мы сохраним верность этой традиции и продолжим принимать решения, исходя из ваших ответов на наши опросы. Поэтому просим вас и дальше участвовать и делиться своими мыслями. Также исследовательская группа Flutter UX проводит широкий спектр исследований пользовательского опыта (UX), чтобы улучшить ваш опыт работы с Flutter. Если вы хотите принять участие в исследовании, зарегистрируйтесь, и вашу кандидатуру рассмотрят в будущих исследованиях.
Подробнее..

SPM модуляризация проекта для увеличения скорости сборки

11.11.2020 12:15:25 | Автор: admin
Привет, Хабр! Меня зовут Эрик Басаргин, я iOS-разработчик в Surf.

На одном большом проекте мы столкнулись с низкой скоростью сборки от трёх минут и более. Обычно в таких случаях студии практикуют модуляризацию проектов, чтобы не работать с огромными монолитами. Мы в Surf решили поэкспериментировать и модуляризовать проект с помощью Swift Package Manager менеджера зависимостей от Apple.

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



Почему именно SPM


Ответ прост это нативно и ново. Он не создает overhead в виде xcworkspace, как Cocoapods, к примеру. К тому же SPM open-source проект, который активно развивается. Apple и сообщество исправляют в нем баги, устраняют уязвимости, обновляют вслед за Swift.

Делает ли это сборку проекта быстрее


Теоретически сборка ускорится из-за самого факта разделения приложения на модули framework'и. Это значит, что каждый модуль будет собираться только в том случае, когда в него внесли изменения. Но утверждать точно можно будет только по окончанию эксперимента.

Note: Эффективность модуляризации напрямую зависит от правильного разбиения проекта на модули.

Как сделать эффективное разбиение


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

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

  • CommonAssets набор ваших Assets'ов и public интерфейс для доступа к ним. Обычно он генерируется с помощью SwiftGen.
  • CommonExtensions набор расширений, к примеру Foundation, UIKit, дополнительные зависимости.

Разделять flow'ы приложения. Рассмотрим древовидную структуру, где MainFlow главное flow приложения. Представим, что у нас новостное приложение.

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

Выносить reusable компоненты в отдельные модули:

  • CommonUIComponents модуль, который содержит в себе любые небольшие UI-компоненты. Обычно они влезают в один файл.
  • Далее список бесконечен и зависит от количества внутренних кастомных UI компонентов. К примеру, нужно создать модули для генерации экранов результата, кастомной коллекции, билдера кастомных алертов и т. д.

Когда нужно выносить компонент в отдельный модуль


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

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

Создаём проект с использованием SPM


Рассмотрим создание тривиального тестового проекта. Я использую Multiplatform App project на SwiftUI. Платформа и интерфейс тут не имеют значения.

Note: Чтобы быстро создать Multiplatform App, нужен XCode 12.2 beta.

Создаём проект и видим следующее:



Теперь создадим первый модуль Common:

  • добавляем папку Frameworks без создания директории;
  • создаём SPM-пакет Common.



  • Добавляем поддерживаемые платформы в файл Package.swift. У нас это platforms: [.iOS(.v14), .macOS(.v10_15)]



  • Теперь добавляем наш модуль в каждый таргет. У нас это SPMExampleProject для iOS и SPMExampleProject для macOS.



Note: Достаточно подключать к таргетам только корневые модули. Они не добавлены как подмодули.

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

Как подключить зависимость у локального SPM-пакета


Добавим пакет AdditionalInfo как Common, но без добавления к таргетам. Теперь изменим Package.swift у Common пакета.



Добавлять больше ничего не нужно. Можно использовать.

Пример, приближенный к реальности


Подключим к нашему тестовому проекту SwiftGen и добавим модуль Palette он будет отвечать за доступ к палитре цветов, утвержденной дизайнером.

  1. Создаем новый корневой модуль по инструкции выше.
  2. Добавляем к нему корневые каталоги Scripts и Templates.
  3. Добавляем в корень модуля файл Palette.xcassets и пропишем какие-либо color set'ы.
  4. Добавляем пустой файл Palette.swift в Sources/Palette.
  5. Добавим в папку Templates шаблон palette.stencil.
  6. Теперь нужно прописать конфигурационный файл для SwiftGen. Для этого добавим файл swiftgen.yml в папку Scripts и пропишем в нем следующее:

xcassets:  inputs:    - ${SRCROOT}/Palette/Sources/Palette/Palette.xcassets  outputs:    - templatePath: ${SRCROOT}/Palette/Templates/palette.stencil      params:        bundle: .module        publicAccess: true      output: ${SRCROOT}/Palette/Sources/Palette/Palette.swift


Итоговый внешний вид модуля Palette

Модуль Palette мы с вами настроили. Теперь надо настроить запуск SwiftGen, чтобы палитра генерировалась при старте сборки. Для этого заходим в конфигурацию каждого таргета и создаем новую Build Phase назовём ее Palette generator. Не забудьте перенести эту Build Phase на максимально высокую позицию.

Теперь прописываем вызов для SwiftGen:

cd ${SRCROOT}/Palette/usr/bin/xcrun --sdk macosx swift run -c release swiftgen config run --config ./Scripts/swiftgen.yml

Note: /usr/bin/xcrun --sdk macosx очень важный префикс. Без него при сборке вылетит ошибка: unable to load standard library for target 'x86_64-apple-macosx10.15.


Пример вызова для SwiftGen

Готово доступ к цветам можно получить следующим образом:
Palette.myGreen (Color type in SwiftUI) и PaletteCore.myGreen (UIColor/NSColor).

Подводные камни


Перечислю то, с чем мы успели столкнуться.

  • Всплывают ошибки архитектуры и портят всю логику разбиения на модули.
  • SwiftLint & SwiftGen не уживаются вместе при подгрузке их через SPM. Причина в разных версиях yml.
  • В крупных проектах не получится сразу избавиться от Cocoapods. А разбивать уже созданный проект с закреплёнными версиями подов настоящее испытание, потому что SPM только развивается и не везде поддерживается. Но SPM и Cocoapods более-менее работают параллельно: разве что поды могут кидать ошибку MergeSwiftModule failed with a nonzero exit code. Это происходит довольно редко, а решается очисткой и пересборкой проекта.
  • На данный момент SPM не позволяет прописать пути поиска библиотек. Приходится явно указывать их с завязкой на -L$(BUILD_DIR).

SPM замена Bundler?


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

SPM дает возможность вызывать swift run, если добавить Package.swift в корень вашего проекта. Что это нам дает? К примеру, можно вызвать fastlane или swiftlint. Пример вызова:

swift run swiftlint --autocorrect.
Подробнее..

Flutter. Слушатель клавиатуры без платформенного кода

19.11.2020 14:19:12 | Автор: admin
Всем привет! Меня зовут Дмитрий Андриянов, я Flutter-разработчик в Surf.

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



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

  • Пишете на Flutter и хотите узнать, что находится у него под капотом.
  • Интересуетесь, как MediaQuery предоставляет данные о UI.
  • Хотите реализовывать интересные штуки на Flutter, покопавшись в нём на более глубоком уровне.

Зачем нам понадобилось написать слушатель без натива


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

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

Исследуем MediaQuery и копаем вглубь


Из MediaQuery мы можем получить данные о размерах системных UI-элементов, которые перекрывают дисплей:

// Поле с данными элементов перекрывающих дисплейMediaQuery.of(context).viewInsets// Отвечает за данные клавиатурыMediaQuery.of(context).viewInsets.bottom

Пример:

class KeyboardScreen extends StatefulWidget { @override _KeyboardScreenState createState() => _KeyboardScreenState();}class _KeyboardScreenState extends State<KeyboardScreen> { @override Widget build(BuildContext context) {   return Scaffold(     body: Column(       mainAxisAlignment: MainAxisAlignment.center,       children: [         Text('Keyboard: ${MediaQuery.of(context).viewInsets.bottom}'),         const SizedBox(height: 20),         TextField(),       ],     ),   ); }}

image

Первая мысль использовать MediaQuery.of(context).viewInsets при изменениях значения: 0 клавиатура скрыта, иначе видна. Но в момент обращения к MediaQueryData мы получим значение, а не Stream, который нужно слушать.

У этого решения две проблемы:
  1. Для использования требуется контекст, что накладывает дополнительные ограничения. Например когда у вас есть модель данных связанная с UI, реагирующая на появление клавиатуры.
  2. viewInsets не дает возможности подписаться на изменения значения.

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

Переходим в исходный код метода MediaQueryData of и видим:

static MediaQueryData of(BuildContext context, { bool nullOk = false }) { assert(context != null); assert(nullOk != null); final MediaQuery query = context.dependOnInheritedWidgetOfExactType<MediaQuery>(); if (query != null)   return query.data; if (nullOk)   return null; throw FlutterError.fromParts(<DiagnosticsNode>[   ErrorSummary('MediaQuery.of() called with a context that does not contain a MediaQuery.'),   ErrorDescription(   ),   context.describeElement('The context used was') ]);}

final MediaQuery query = context.dependOnInheritedWidgetOfExactType<MediaQuery>();

В этой строке по дереву родителей ищется класс MediaQuery. У полученного виджета берутся и возвращаются данные в виде экземпляра MediaQueryData.

Смотрим в MediaQuery: оказывается, это наследник InheritedWidget, и он создаётся в разных виджетах:

image

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

Например, файл dialog:

MediaQuery( data: MediaQuery.of(context).copyWith(   // iOS does not shrink dialog content below a 1.0 scale factor   textScaleFactor: math.max(textScaleFactor, 1.0), ),

Самый верхний MediaQuery создаётся в файле widgets/app.dart.
Класс _MediaQueryFromWindow:

class _MediaQueryFromWindow extends StatefulWidget { const _MediaQueryFromWindow({Key key, this.child}) : super(key: key); final Widget child; @override _MediaQueryFromWindowsState createState() => _MediaQueryFromWindowsState();}class _MediaQueryFromWindowsState extends State<_MediaQueryFromWindow> with WidgetsBindingObserver { @override void initState() {   super.initState();   WidgetsBinding.instance.addObserver(this); }// ACCESSIBILITY@overridevoid didChangeAccessibilityFeatures() { setState(() {   // The properties of window have changed. We use them in our build   // function, so we need setState(), but we don't cache anything locally. });}// METRICS@overridevoid didChangeMetrics() { setState(() {   // The properties of window have changed. We use them in our build   // function, so we need setState(), but we don't cache anything locally. });}@overridevoid didChangeTextScaleFactor() { setState(() {   // The textScaleFactor property of window has changed. We reference   // window in our build function, so we need to call setState(), but   // we don't need to cache anything locally. });}// RENDERING@overridevoid didChangePlatformBrightness() { setState(() {   // The platformBrightness property of window has changed. We reference   // window in our build function, so we need to call setState(), but   // we don't need to cache anything locally. });} @override Widget build(BuildContext context) {   MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window);   if (!kReleaseMode) {     data = data.copyWith(platformBrightness: debugBrightnessOverride);   }   return MediaQuery(     data: data,     child: widget.child,   ); } @override void dispose() {   WidgetsBinding.instance.removeObserver(this);   super.dispose(); }}

Что здесь происходит:


1. Класс _MediaQueryFromWindowsState замешивает миксин WidgetsBindingObserver, чтобы использоваться в качестве наблюдателя за изменениями системного UI из Flutter.

2. В initState вызываем WidgetsBinding.instance.addObserver(this); addObserver принимает на вход экземпляр наблюдателя. В данном случае this, так как текущий класс замешивает WidgetsBindingObserver.

3. WidgetsBindingObserver предоставляет методы, которые вызываются при изменении соответствующих метрик:
didChangeAccessibilityFeatures вызывается при изменении набора активных на данный момент специальных возможностей в системе.
didChangeMetrics вызывается при изменении размеров приложения из-за системы. Например, при повороте телефона или влиянии системного UI (появлении клавиатуры).
didChangeTextScaleFactor вызывается при изменении коэффициента масштабирования текста на платформе.
didChangePlatformBrightness вызывается при изменении яркости.

4. Самое главное, что объединяет эти методы, в каждом из них вызывается setState. Это запускает метод build, заново строит объект MediaQueryData

Widget build(BuildContext context) { MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window);

и передает его вниз по дереву до места вызова MediaQuery.of(context).ИмяПоля:

Подробнее про биндинг можно прочесть в статье моего коллеги Миши Зотьева.

Вывод: мы можем получать изменения системного UI, используя WidgetsBinding и WidgetsBindingObserver.

Реализация слушателя клавиатуры


Начнём реализовывать слушатель клавиатуры на основе этих данных. Для начала создадим класс:

class KeyboardListener with WidgetsBindingObserver {}

Добавим геттер bool чтобы знать, видна ли клавиатура.

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

double get currentKeyboardHeight => _currentKeyboardHeight;double _currentKeyboardHeight = 0;bool get _isVisibleKeyboard => _currentKeyboardHeight > 0;Future(() { final double newKeyboardHeight =     WidgetsBinding.instance.window.viewInsets.bottom; if (newKeyboardHeight > _currentKeyboardHeight) {   /// Новая высота больше предыдущей  клавиатура открылась   _onShow();   _onChange(true); } else if (newKeyboardHeight < _currentKeyboardHeight) {   /// Новая высота меньше предыдущей  клавиатура закрылась   _onHide();   _onChange(false); } _currentKeyboardHeight = newKeyboardHeight;});

Мы знаем, что при видимой клавиатуре в viewInsets.bottom значение больше 0, при скрытой 0.

bool get _isVisibleKeyboard => _currentKeyboardHeight > 0; выполняет проверку: если высота клавиатуры больше нуля, то она видна.

Но на некоторых устройствах с Android 9 при закрытии клавиатуры высота не всегда становилась 0. Открытая клавиатура могла передать значение 400, а закрытая 150. А в следующий раз она передавала уже 0. Нестабильный и сложно уловимый баг.

Поэтому я решил отказаться от возможности получать размер клавиатуры из экземпляра слушателя и стал проверять:

WidgetsBinding.instance.window.viewInsets.bottom > 0;

Это решило проблему.

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

@overridevoid didChangeMetrics() { _listener();}void _listener() { if (isVisibleKeyboard) {   _onChange(true); } else {   _onChange(false); }}void _onChange(bool isOpen) { /// Тут вызываются внешние слушатели}

Как и говорилось выше, благодаря didChangeMetrics мы знаем, что изменился системный UI. И проверяя, видна ли клавиатура, вызываем колбеки появления/сокрытия клавиатуры.

Как использовать


class _KeyboardScreenState extends State<KeyboardScreen> { bool _isShowKeyboard = false; KeyboardListener _keyboardListener = KeyboardListener(); @override void initState() {   super.initState();   _keyboardListener.addListener(onChange: (bool isVisible) {     setState(() {       _isShowKeyboard = isVisible;     });   }); } @override void dispose() {   _keyboardListener.dispose();   super.dispose(); } @override Widget build(BuildContext context) {   return Scaffold(     body: Column(       mainAxisAlignment: MainAxisAlignment.center,       children: [         Text('Keyboard: $_isShowKeyboard'),         const SizedBox(height: 20),         TextField(),       ],     ),   ); }}

image

Полный код


import 'dart:math';import 'dart:ui';import 'package:flutter/widgets.dart';typedef KeyboardChangeListener = Function(bool isVisible);class KeyboardListener with WidgetsBindingObserver { static final Random _random = Random(); /// Колбэки, вызывающиеся при появлении и сокрытии клавиатуры final Map<String, KeyboardChangeListener> _changeListeners = {}; /// Колбэки, вызывающиеся при появлении клавиатуры final Map<String, VoidCallback> _showListeners = {}; /// Колбэки, вызывающиеся при сокрытии клавиатуры final Map<String, VoidCallback> _hideListeners = {}; bool get isVisibleKeyboard =>     WidgetsBinding.instance.window.viewInsets.bottom > 0; KeyboardListener() {   _init(); } void dispose() {   // Удаляем текущий класс из списка наблюдателей   WidgetsBinding.instance.removeObserver(this);    // Очищаем списки колбэков   _changeListeners.clear();   _showListeners.clear();   _hideListeners.clear(); } /// При изменениях системного UI вызываем слушателей @override void didChangeMetrics() {   _listener(); } /// Метод добавления слушателей String addListener({   String id,   KeyboardChangeListener onChange,   VoidCallback onShow,   VoidCallback onHide, }) {   assert(onChange != null || onShow != null || onHide != null);   /// Для более удобного доступа к слушателям используются идентификаторы   id ??= _generateId();   if (onChange != null) _changeListeners[id] = onChange;   if (onShow != null) _showListeners[id] = onShow;   if (onHide != null) _hideListeners[id] = onHide;   return id; } /// Методы удаления слушателей void removeChangeListener(KeyboardChangeListener listener) {   _removeListener(_changeListeners, listener); } void removeShowListener(VoidCallback listener) {   _removeListener(_showListeners, listener); } void removeHideListener(VoidCallback listener) {   _removeListener(_hideListeners, listener); } void removeAtChangeListener(String id) {   _removeAtListener(_changeListeners, id); } void removeAtShowListener(String id) {   _removeAtListener(_changeListeners, id); } void removeAtHideListener(String id) {   _removeAtListener(_changeListeners, id); } void _removeAtListener(Map<String, Function> listeners, String id) {   listeners.remove(id); } void _removeListener(Map<String, Function> listeners, Function listener) {   listeners.removeWhere((key, value) => value == listener); } String _generateId() {   return _random.nextDouble().toString(); } void _init() {   WidgetsBinding.instance.addObserver(this); // Регистрируем наблюдателя } void _listener() {   if (isVisibleKeyboard) {     _onShow();     _onChange(true);   } else {     _onHide();     _onChange(false);   } } void _onChange(bool isOpen) {   for (KeyboardChangeListener listener in _changeListeners.values) {     listener(isOpen);   } } void _onShow() {   for (VoidCallback listener in _showListeners.values) {     listener();   } } void _onHide() {   for (VoidCallback listener in _hideListeners.values) {     listener();   } }}

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

Итог


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

Это решение находится в SurfGear, пакет keyboard_listener.
Подробнее..

Работа с асинхронностью в Dart

27.01.2021 14:17:26 | Автор: admin

Всем привет! Меня зовут Дмитрий Репин, я Flutter-разработчик в Surf.

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

Эта статья написана по материалам моего ролика на YouTube. Посмотрите видео, если больше любите слушать, чем читать.

Dart однопоточный язык программирования, который выполняется в одном процессе. Что это значит по факту: если мы будем в одном потоке выполнять любую операцию, требующую времени, то приложение просто подвиснет. И всё время, пока мы, например, ждём ответа от сервера или выполнения запроса в БД, пользователь будет страдать и смотреть на лагающий интерфейс.

К счастью, Dart хитрый однопоточный язык, который предоставляет механизм Event Loop он даёт возможность откладывать какие-то операции на потом, когда поток будет посвободнее.Такие отложенные операции мы будем называть асинхронными.

Все операции в Dart можно разделить на два типа:

  1. Синхронные те, что блокируют другие операции до своего выполнения.

  2. Асинхронные, которые позволяют другим операциям выполняться, пока текущая не закончится.

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

Создание асинхронной функции с помощью класса Future

Чтобы было нагляднее, давайте напишем простую асинхронную функцию. Пусть она будет принимать строку и смотреть на её длину: если она больше 10 символов, функция вернёт число 42, в ином случае ошибку. Представим, что вычисление длины строки достаточно тяжеловесная операция чтобы сделать её асинхронной.

Чтобы сделать код асинхронным, нам понадобится класс Future. В конструктор он принимает функцию, которую необходимо выполнить асинхронно, а также предоставляет два обработчика: then и catchError. Первый обрабатывает успешное выполнение функции, а второй выполнение функции с ошибкой. В итоге у нас должен получиться такой код:

runSimpleFutureExample({    String question = 'В чем смысл жизни и всего такого?',  }) {    print('Start of future example');    Future(() {      print('Вопрос: $question');      if (question.length > 10) {         return 42;      } else {        throw Exception('Вы задали недостаточно сложный вопрос.');      }    }).then((result) {      print('Ответ: $result');    }).catchError((error) {      print('Ошибка. $error');    });    print('Finish of future example');  }

В начале и конце метода выводятся строки о том, что он начал и закончил работу, а между ними создаётся сам future. В конструктор он принимает метод, который вычисляет длину строки, а затем к Future добавляются обработчики then и catchError, которые обрабатывают выполнение Future. Попробуем запустить этот пример и посмотрим, в каком порядке выводится на консоль результат:

Start of future example

Finish of future example

Вопрос: В чем смысл жизни и всего такого?

Ответ: 42

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

Работать с Future через обработчики then и catchError не самая хорошая идея, особенно если нужно передать результат одного Future во второй, а второго в третий и т. д., порождая callback hell.

Для таких случаев Dart предоставляет ключевые слова async и await. Словом async помечается функция, исполняющая асинхронные операции через await. Словом await помечаются сами асинхронные операции. К сожалению, при таком подходе мы теряем обработчик catchError, однако никто не мешает нам обрабатывать ошибки через стандартный try/catch. Чтобы понять, как это работает, перепишем наш предыдущий пример с использованием async/await и для удобства вынесем Future в отдельный метод:

 runFutureWithAwaitExample({    String question = 'В чем смысл жизни и всего такого?',  }) async {    print('Start of future example');    try {      final result = await _getAnswerForQuestion(        'В чем смысл жизни и всего такого?',      );      print('Ответ: $result');    } catch (error) {      print('Ошибка. $error');    }    print('Finish of future example');  }   Future<int> _getAnswerForQuestion(String question) => Future(() {        print('Вопрос: $question');        if (question.length > 10) {          return 42;        } else {          throw Exception('Вы задали недостаточно сложный вопрос.');        }      });

Здесь мы объявили метод, возвращающий Future. Вызываем его с использованием слова await, передавая в переменную result, с которой мы можем работать дальше, как будто в синхронном коде. Выведем на консоль результат работы метода:

Start of future example

Вопрос: В чем смысл жизни и всего такого?

Ответ: 42

Finish of future example

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

Обработка последовательности асинхронных событий с помощью Stream

Помимо Future, есть ещё один важный класс для работы с асинхронностью Stream. В отличие от Future, Stream может обрабатывать целый набор асинхронных событий. Давайте сразу разберём на примере:

void runStreamSimpleExample() {    print('Simple stream example started');    final stream = Stream.fromIterable([1, 2, 3, 4, 5]);    stream.listen((number) {      print('listener: $number');    });    print('Simple stream example finished');  }

У стрима много разных конструкторов, посмотрите их в документации. Здесь используем fromIterable. Создаём в массиве пять чисел, подписываемся на стрим через метод listen и выводим на консоль:

Simple stream example started

Simple stream example finished

listener: 1

listener: 2

listener: 3

listener: 4

listener: 5

Как и в случае с Future, сначала выполняется синхронный код: сообщения о старте и завершения примера. Только потом обрабатывается стрим и выводятся на консоль числа. В этом примере подписка на стрим обрабатывается асинхронно через listen, однако есть способ обрабатывать стрим через знакомые нам ключевые слова async и await. Перепишем пример с их использованием:

void runStreamAwaitedSimpleExample() async {    print('Simple stream example with await started');    final stream = Stream.fromIterable([1, 2, 3, 4, 5]);    await for (final number in stream) {      print('Number: $number');    }    print('Simple stream example with await finished');  }

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

Simple stream example started

listener: 1

listener: 2

listener: 3

listener: 4

listener: 5

Simple stream example finished

Тут всё так же, как и с async/await во future. Сама функция стала асинхронной, поэтому теперь обработка Stream происходит до завершения метода.

Single-subscription и broadcast стримы. Основы StreamController

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

print('Simple stream example started');    final stream = Stream.fromIterable([1, 2, 3, 4, 5]);    stream.listen((number) {      print('listener 1: $number');    });    stream.listen((number) {      print('listener 2: $number');    });    print('Simple stream example finished');

И результат:

The following StateError was thrown while handling a gesture.

Bad state: Stream has already been listened to.

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

Здесь стоит рассказать о том, что существует два типа стримов: single-subscription и broadcast стримы. Мы создали single-subscription стрим. Такие стримы поставляют все данные подписчику разом и только после самой подписки. Так и происходит в нашем примере.

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

Чтобы увидеть разницу, создадим broadcast стрим. В этом примере для создания стрима мы не будем использовать непосредственно сам Stream. Мы будем работать с классом StreamController. Он предоставляет доступ к самому стриму, даёт возможность управлять им и добавлять в него события.

///пример broadcast стрима  void runBroadcastStreamExample() {    print('Broadcast stream example started');    final streamController = StreamController.broadcast();    streamController.stream.listen((number) {      print('Listener 1: $number');    });    streamController.stream.listen((number) {      print('Listener 2: $number');    });    streamController.sink.add(1);    streamController.sink.add(2);    streamController.sink.add(3);    streamController.sink.add(4);    streamController.sink.add(5);    streamController.close();    print('Broadcast stream example finished');  }

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

Broadcast stream example started

Broadcast stream example finished

Listener 1: 1

Listener 2: 1

Listener 1: 2

Listener 2: 2

Listener 1: 3

Listener 2: 3

Listener 1: 4

Listener 2: 4

Listener 1: 5

Listener 2: 5

Проблем не возникло: два подписчика отработали, как и ожидалось. На самом деле у StreamController достаточно много функций, рекомендую почитать документацию к нему.

Отписка от стрима с помощью StreamSubscription

Помимо управления самим стримом часто возникает потребность управлять подпиской на него для этого в dart:async добавлен StreamSubscription. Кстати, мы уже неявно работали с ним в наших предыдущих примерах, когда подписывались на стрим: на самом деле метод listen возвращает его, и таким образом мы можем выделить подписку в переменную. Модифицируем предыдущий пример так, чтобы вторая подписка отменялась после числа 3:

///пример broadcast стрима  void runBroadcastStreamExample() {    print('Broadcast stream example started');    final streamController = StreamController.broadcast();    streamController.stream.listen((number) {      print('Listener 1: $number');    });    StreamSubscription sub2;    sub2 = streamController.stream.listen((number) {      print('Listener 2: $number');      if (number == 3) {        sub2.cancel();      }    });    streamController.sink.add(1);    streamController.sink.add(2);    streamController.sink.add(3);    streamController.sink.add(4);    streamController.sink.add(5);    streamController.close();    print('Broadcast stream example finished');  }

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

Broadcast stream example started

Broadcast stream example finished

Listener 1: 1

Listener 2: 1

Listener 1: 2

Listener 2: 2

Listener 1: 3

Listener 2: 3

Listener 1: 4

Listener 1: 5

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

Как упростить работу с асинхронностью

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

Completer

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

Разберём на примере. Пусть у нас будет completer, который складывает два числа с трёхсекундной задержкой. Это очень простой пример, но важно понимать, что тут можно сделать большую вложенность из Future и компактно уместить в один класс.

class CompleterTester {  void runCompleterInitTest() async {    print('Completer example started');    var sumCompleter = SumCompleter();    var sum = await sumCompleter.sum(20, 22);    print('Completer result: ' + sum.toString());    print('Completer example finished');  }} class SumCompleter {  Completer<int> completer = Completer();   Future<int> sum(int a, int b) {    _sumAsync(a, b);    return completer.future;  }   void _sumAsync(int a, int b) {    Future.delayed(Duration(seconds: 3), () {      return a + b;    }).then((value) {      completer.complete(value);    });  }}

Разберём, что тут произошло. Мы создали класс SumCompleter и создали в нём completer. Затем объявили метод sum, который вызывает приватный метод на сложение и возвращает Future этого completer. Затем этот приватный метод выполняет операцию и вызывает метод complete у комплитера. После этого пользователь метода получает результат.

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

StreamIterator

Представьте, что вам нужно управлять переходом к следующему айтему стрима и делать это именно тогда, когда вам нужно. Именно это и делает StreamIterator. Он предоставляет метод moveNext для перехода к следующему элементу стрима. moveNext вернет true, если элемент пришел, и false, если стрим был закрыт. Также StreamIterator предоставляет свойство current для получения текущего элемента. Ну и конечно, метод cancel для отмены подписки. Небольшой пример по работе класса:

void runStreamIteratorExample() async {    print('StreamIteratorExample started');    var stream = Stream.fromIterable([1, 2, 3]);    var iterator = StreamIterator(stream);    bool moveResult;    do {      moveResult = await iterator.moveNext();      print('number: ${iterator.current}');    } while (moveResult);    print('StreamIteratorExample finished');  }

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

StreamTransformer

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

void runStreamTransformerExample() async {    print('StreamTransformer example started');    StreamTransformer doubleTransformer =        new StreamTransformer.fromHandlers(handleData: (data, EventSink sink) {      sink.add(data * 2);    });     StreamController controller = StreamController();    controller.stream.transform(doubleTransformer).listen((data) {      print('data: $data');    });     controller.add(1);    controller.add(2);    controller.add(3);    print('StreamTransformer example finished');  }

В нашем случае он будет принимать на вход данные, которые нужно трансформировать, и sink, в который мы должны передать трансформированные данные. В теле метода просто удваиваем приходящее значение. Затем остаётся только трансформировать стрим с помощью метода transform. И вуаля: в методе listen значение удваивается. Обратите внимание: трансформер может и не отдать данные в синк, а может отдать два раза. То есть он работает не только как map или where, а наделён гораздо большей функциональностью.


Рекомендую поиграться с этими примерами они есть на нашем GitHub. Так вы точно поймете, как устроена асинхронность в Dart. Если что-то непонятно, задавайте вопросы я отвечу в комментариях.

Подробнее..

Анонс вебинара Почему компании всё чаще выбирают Flutter и что это значит для разработчиков

28.01.2021 12:17:06 | Автор: admin

Привет!

Уже прошли времена, когда Flutter был просто хайповой технологией, а разработчики при упоминании его закатывали глаза и прогнозировали скорую гибель. Но гибели не случилось: он стабильно развивается уже три года, привлекая на свою сторону всё больше людей. Спрос на Flutter растёт и со стороны бизнеса.

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

Регистрация

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

Что обсудим:

  • Где искать работу Flutter-разработчику. Как меняется спрос и предложение на рынке. Поговорим о цифрах.

  • Почему компании всё чаще выбирают Flutter. Как выглядит рынок мобильной разработки сейчас и какое место в нём занимает Flutter.

  • Наш опыт: как избежать распространённых ошибок, как лучше начать проект, какие задачи решает Flutter.

Мероприятие пройдёт в формате живого обсуждения между участниками и ведущими разработчиками компании Surf, и займёт это примерно 1,5 часа.

Спикеры

Евгений Сатуров, Flutter TeamLead Surf. Основатель Flutter Dev Podcast. Соорганизатор крупнейшей российской конференции Mobius. Лидер IT-комьюнити Google Developer Group Voronezh. За плечами 14 проектов в Surf среди них Росбанк Бизнес (Flutter), SBI Bank (Android), MDK (Android).

Артём Зайцев, руководитель Flutter-отдела Surf. В прошлом Android-разработчик и тимлид. Стоял у истоков Flutter-отдела в Surf. Сегодня Артём руководит этим отделом и активно продвигает Flutter в российском комьюнити. Приложил руку ко многим проектам, включая Магнит, MDK и KFC.

Михаил Зотьев, Flutter-разработчик Surf.Flutter-разработчик, тимлид. Активный спикер Surf, попал ТОП-3 всех докладчиков на Mobius 2020 и DartUp 2020. Ещё Михаил ведёт телеграм-канал о Flutter-разработке Oh, my Flutter.

Андрей Савостьянов, Flutter-разработчик Surf. В прошлом Андрей работал с Java/Spring/Android создавал железные решения и протоколы, прикладные системы по автоматизации и мониторингу технологических комплексов. Почти 2 года назад сменил специализацию и ни разу не пожалел.

Регистрация

Вебинар начнётся 4 февраля в 18:00 МСК на YouTube-канале Surf.

Чтобы поучаствовать в нём, заполните форму регистрации. Мы отправим напоминание и ссылку на трансляцию за день и за час до начала вебинара.

До встречи!

Подробнее..

Анонс вебинара Создаём мультиплатформенное Flutter приложение в интерфейсе Cyberpunk 2077

24.02.2021 20:16:16 | Автор: admin

На живом примере приложения в визуальном стиле игры Cyberpunk 2077 покажем всю мощь фреймворка Flutter и возможность сборки единого кода под разные платформы.

Вместе с вами в прямом эфире соберём по-настоящему мультиплатформенное приложение для веба, iOS, Android и desktop.

РЕГИСТРАЦИЯ

Что будет

  • Разработаем вместе аутентичный дизайн, кастомные виджеты и опубликуем.

  • В UI ките игры Cyberpunk 2077 рисуем кнопку, панельку, градиентный фон, скролл, вращающийся логотип. Билдим.

  • Адаптируем верстку для разных экранов и платформ.

  • Реализуем навигацию между экранами.

  • Открытый исходный код.

Примеры нескольких UI элементов, которые будем создавать в ходе вебинараПримеры нескольких UI элементов, которые будем создавать в ходе вебинара

Вебинар проводят разработчики компании Surf

Андрей Савостьянов, Flutter-разработчик Surf. В прошлом Андрей работал с Java/Spring/Android создавал железные решения и протоколы, прикладные системы по автоматизации и мониторингу технологических комплексов. Почти 2 года назад сменил специализацию и ни разу не пожалел. Андрей большой энтузиаст dart/flutter и даже делает fullstack приложения на дарте.

Михаил Зотьев, Flutter-разработчик Surf.Flutter-разработчик, тимлид. Активный спикер Surf, попал ТОП-3 всех докладчиков на Mobius 2020 и DartUp 2020. Ещё Михаил ведёт телеграм-канал о Flutter-разработке Oh, my Flutter

Регистрация

Вебинар начнётся 25 февраля в 18:00 МСК на YouTube-канале Surf.

Чтобы поучаствовать в нём, заполните форму регистрации. Мы отправим напоминание и ссылку на трансляцию за день и за час до начала вебинара.

До встречи!

РЕГИСТРАЦИЯ

Подробнее..

Какие ошибки совершает аналитик в первые полгода работы и как их избежать

19.05.2021 14:18:09 | Автор: admin

Хайди хо, Кайл!

Меня зовут Диана и я бизнес-аналитик в компании Surf. В прошлом году я закончила бакалавриат факультета компьютерных наук в ВГУ: это дало мне базовые теоретические знания. Однако теория мало применима без практики: теперь набиваю шишки в настоящих проектах.

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

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

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

Не бойтесь задавать больше вопросов и просить уточнений

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

Лучше задать больше вопросов, чем что-то упустить. Например, когда работаешь над проектом, важно уточнить:

  • На чьей стороне будет реализована логика: фронта, middleware, бэка?

  • Как определять нужное состояние элемента? Как и где получать и обрабатывать нужные параметры?

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

  • Касаются ли вашей стороны эти требования или от вас никаких доработок и не потребуется?

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

Проверяйте и перепроверяйте выполнение договорённостей

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

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

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

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

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

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

Пример из жизни

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

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

Из-за этого процесс задержался ещё на неделю, потому что пришлось потратить много времени на повторное обсуждение. В итоге решили кнопку отображать всегда:)

Не будьте, как я, обращайте внимание коллег на ключевые моменты, необходимые для выполнения.

Уточняйте, как сделать проще

Бывает, что бизнес-аналитику реализация фичи кажется удобной. Он предлагает её разработчику, а в ответ получает: Чё-то сложно, давай по-другому?. Это нормально: взгляды аналитика и разработчика на реализацию фичи могут разительно отличаться: разработчик понимает, что ему хотят втюхать что-то сложное и заморочное, и идёт в отказ.

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

Френдли ремайндер

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

Будьте внимательным к потенциально проблемным требованиям

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

Уточните (если нужно, несколько раз), правильно ли вы поняли, что ваша задача просто добавить картинку котика на экран. Может оказаться, что добавить нужно обязательно чеширского кота, а ещё он должен улыбаться в момент, когда вы моргаете. Я специально привела утрированный пример.

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

Важно быть начеку и проверять, не противоречат ли новые требования уже реализованной функциональности.

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

Как определить, что перед вами задача-оборотень, которая может превратиться из человечной в дикую? По количеству непоняток. Берегитесь, если:

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

  • Не очевидна инициализация флоу и переход между экранами.

  • Нет четкого понимания логики.

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

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

Пример

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

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

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

_______

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

Вот и всё, мои маленькие хоббиты. Я поделилась с вами своей мудростью, теперь вы сами по себе. Не дрейфте, не теряйтесь, будьте настойчивы и внимательны. В добрый путь!

Подробнее..

Особенности тестирования Android без Google-сервисов

26.05.2021 16:17:30 | Автор: admin

Привет! Меня зовут Мария Лещинская, я QA-специалист в Surf. Наша компания разрабатывает мобильные приложения с 2011 года. В этом материале поговорим о тестировании устройств Android, на которых нет поддержки Google Services.

Huawei без Google-сервисов начали массово выпускаться в 2019 году. Мы в Surf, разумеется, задумались о будущем: как сильно пострадают наши процессы и что нужно незамедлительно осваивать.

Я поделюсь впечатлениями от работы с Android без Google-сервисов и расскажу, какие возможности имеют такие мобильные устройства при тестировании.

В начале статьи общая информация про AppGallery и AppGallery Connect. Если вы всё это уже знаете, переходите сразу к сути к особенностям тестирования Android-платформы c поддержкой Huawei без Google-сервисов.

Что такое AppGallery, AppGallery Connect и почему Huawei без поддержки Google

Приложения под iOS- и Android-платформы можно встретить в официальных магазинах AppStore и Google Play. Туда мы идём в первую очередь, когда хотим установить новое мобильное приложение на телефон.

С 2018 года во всем мире (а в Китае ещё раньше) появился другой магазин мобильных приложений AppGallery, а с ним и AppGallery Connect.

AppGallery это менеджер пакетов и платформа распространения приложений, разработанная Huawei для операционной системы Android. AppGallery Connect универсальная платформа для поддержки всего жизненного цикла приложения: разработки, распространения, управления, тестирования и анализа.

За AppGallery последовал и выпуск устройств на базе Huawei: с 2019 года для нихв принципе отсутствует возможность работать с Google-сервисами, поэтому работа с Android стала сложнее. Нужно было оперативно включиться в работу и придумать, какизменить процессы тестирования платформы.

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

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

Во-вторых, у AppGallery солидное количество пользователей. Магазин появился в 2018 году, к октябрю 2020 года приложение доступно в 170 странах мира, число уникальных пользователей 700 миллионов человек. Как говорит статистика, ежемесячная аудитория составляет 490 миллионов активных пользователей.

При этом для Huawei написали всего 96 000 приложений. Для сравнения в Play Store 2.9 миллиона приложений: это значит что более двух с половиной миллионов приложений отсутствуют в AppGallery.

В AppGallery нет, например, Instagram, Facebook и WhatsАpp. Их, конечно, можно скачать и установить вручную без ограничений: найти по отдельности в браузере или через какой-нибудь агрегатор. Также в сети появились сервисы, с помощью которых можно скачать самые популярные приложения. Но не каждый пользователь захочет выполнять дополнительные манипуляции.

Три вида Android-устройств

Как только кAndroidс Google-сервисами прибавились Huawei с сервисами HMS, некоторые устройства стали автоматически поддерживать оба вида сервисов (например, как Huawei до 2020 года выпуска).

Ниже представлено сравнение трёх типов устройств Android: с Google-cервисами, без них и с поддержкой обоих.

Android без Google-сервисов

Android только с Google-сервисами

Android с поддержкой Google-сервисов и App Gallery

.apk/.aab

Установочный файл может быть один на все виды устройств Android, или их может быть два: отдельно с сервисами Huawei и отдельно с сервисами Google.

Мы в Surf обычно делаем один .apk/.aab для обоих видов устройств Android. Логика работы приложений на разных устройствах определена внутри сборки. Но также могут быть два установочных файла одного приложения: один идёт в Google play, другой в AppGallery.

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

Проще и быстрее сделать одну сборку, чем две, но как только подключается CI, разница минимизируется. Использование двух сборок теоретически может уменьшить вес приложения особенно если в нем используются разные фреймворки для реализации фич на Android с Google Services и без.

инструменты

AppGallery Connect и сервисы, не использующие Google.

Сервисы, использующие google, в том числе Firebase.

AppGallery Connect, сервисы, не использующие Google, и сервисы, использующие Google в том числе Firebase.

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

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

баги

Баги по общей логике приложения, конечно, будут распространяться на оба вида устройств. Существенные отличия могут появиться при работе сервисов типа Google Pay, push-уведомлений, deep links и dynamic links и так далее.

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

Возможности тестирования через AppGallery Connect

Проблемы начинаются при использовании разных библиотек на двух видах устройств Android. Мы в Surf пользуемся различными сервисами для работы с push-уведомлениями, аналитикой, dynamic или deep links, performance-мониторингом. Поэтому когда стали брать на вооружение работу с Huawei без Google-сервисов, волновались, насколько сильно изменится работа QA: получится ли тестировать push-уведомления и dynamic links в привычном ритме или придётся адаптироваться к абсолютно новому процессу? К счастью, сам процесс меняется несильно. Но есть вещи, о которых необходимо знать, прежде чем браться за работу с устройствами без поддержки Google.

Huawei без Google-сервисов не имеет доступа к инструментам, которые работают с Google, например, Firebase. Сервисы для тестирования и работы мобильного приложения нужно настраивать через AppGallery (к счастью, AppGallery Connect имеет базовые возможности из коробки) или другие доступные инструменты. А возможно, придумывать и свои решения.

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

Аналитика

При тестировании аналитики полезно просматривать события в реальном времени это помогает обеспечить качество реализации отправки событий в мобильном приложении. Проверять аналитику в AppGallery Connect в реальном времени можно, например, с помощью Отладки приложения (аналогично DebugView в Firebase).

Отладка приложения (App Debugging) позволяет смотреть события, приходящие от МП, и их параметры в реальном времени. Чтобы подключить устройство к Отладке приложения в AppGallery Connect, нужно подключить устройство к компьютеру и в терминале выполнить команду:

adb shell setprop debug.huawei.hms.analytics.app <package_name>

Чтобы отключить устройство от отладки, выполнить команду:

adb shell setprop debug.huawei.hms.analytics.app .none.

Чтобы быстрее найти <package_name>, можно воспользоваться командой adb:

adb shell pm list packages

Общий сбор аналитики в AppGallery Connect тоже доступен. Он называется Просмотр в реальном времени (аналогично Events в Firebase)

Просмотр в реальном времени (Real-Time Overview) собирает все события с МП в одном месте. Можно строить графики по выбранным критериям, активировать фильтры и в целом проводить анализ по мобильному приложению.

Удалённая настройка и параметры

Удалённая настройка (Remote Configuration) позволяет управлять различными параметрами для приложения, и при необходимости обращаться к AppGallery Connect прямо из МП для работы с ними (аналогично RemoteConfig в Firebase).

Push-уведомления

При работе с push-уведомлениями Surf использует разные инструменты: Flocktory, Mindbox, Firebase и другие. Не все инструменты пока ещё могут работать с Android без поддержки Google, но базовая возможность подключить push-уведомления для Huawei есть: это их фирменная реализация через AppGallery Connect. Настройка пyшей происходит в PushKit.

Важно отметить, что пушер бэкэнда обязательно должен уметь взаимодействовать с AppGallery PushKit. Иначе push-уведомления придётся отправлять вручную из AppGallery Connect.

App Linking

App Linking сервис для работы с dynamic links. На основании deep links App Linking предоставляет пользователям доступ к нужному контенту непосредственно на веб-страницах и в мобильных приложениях: это повышает конверсию пользователей (аналогично Dynamic Links в Firebase).

Dynamic Links vs Deep Links

Dynamic links это интеллектуальные URL-адреса, которые позволяют отправлять существующих и потенциальных пользователей в любое место в приложении iOS или Android. Dynamic links легко переводят пользователей с любого URL на контент в приложении. Если пользователь не установил приложение на устройство, он увидит контент, когда установит его.

Deep Links это тип локальных ссылок: они направляют пользователей непосредственно в приложение и только. Соответственно, если приложение не установлено, работа с deep links невозможна.

Простыми словами, dynamic links ссылки, которые могут редиректить пользователя откуда угодно прямо в приложение или магазин, если оно не установлено (а после установки и в приложение). Deep links ссылки, которые привязаны к конкретному экрану внутри приложения и работают локально внутри МП.

На данный момент при работе с AppGallery Connect нет возможности создавать кастомные dynamic links: например, которые были бы одинаковыми и для обоих видов устройств Android (с поддержкой Google и без). Но c deep links всё в порядке.

Crash

Чтобы ловить незаметные с первого взгляда баги, стоит мониторить crash-аналитику даже на debug-версиях. Это необходимо, когда приложение потенциально разрабатывается на большую аудиторию и релиз близко не говоря уже о ситуации, когда МП уже доступно магазине и им пользуется много людей.

Нам было важно, чтобы такой инструмент был доступен и для Huawei без Google-сервисов. Crash плагин, позволяющий отслеживать и анализировать баги, краши и ошибки в приложении (аналогично Crashlytics в Firebase).

APM

Чтобы обеспечить качество клиент-серверного взаимодействия, удобно использовать инструмент, который бы помогал анализировать ответы от сервера и отрисовку экранов и элементов в приложении. В AppGallery Connect такой инструмент APM. Это сервис, который помогает искать и устранять проблемы производительности приложения и улучшать таким образом пользовательский интерфейс (аналогично Performance в Firebase).

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

Особенности тестирования Android-платформы c поддержкой Huawei без Google-сервисов

В первое время при работе с устройствами Huawei без Google-сервисов мы тратили много времени на анализ и выстраивание процессов. Сейчас всё наладилось.

В целом можно выделить следующие проблемы и решения.

Шаринг сборок

На проектах мы часто шарим сборки через Firebase, или напрямую скачиваем .apk из Jenkins, или собираем вручную из Android Studio. Проблем со скачиванием или ручной установкой .apk для Huawei без Google-сервисов нет. Проблем с App Tester приложением Firebase для шаринга сборок тоже нет. Использовать непосредственно приложение не получится, но пройти по invite из почты в браузер для скачивания сборки удастся.

Лайфхак: сохраняйте страницу из браузера на рабочий стол телефона и не знайте горя.

Устройства

Конечно, для тестирования необходимы устройства и эмуляторы без Google-сервисов.

  • Если на проекте планируется адаптация под AppGallery, можно отправить заявку Huawei. Они пришлют девайсы для тестирования.Правда, финальное слово всегда за самим Huawei: отправка запроса ничего не гарантирует. Но опция приятная.

  • Неплохой вариант использовать эмуляторы. Конечно, они не восполнят полную работу с устройством, но могут помочь в отдельных случаях.

На что обратить внимание

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

1. Push-уведомления. На Huawei без Google-сервисов не будут работать push-уведомления, реализованные на backend через Firebase (а такое встречается сейчас часто). Такие устройства имеют свой hms-токен, и для работы с ними нужна специальная реализация.

2. Dynamic links. Инструмент AppGallery Connect не поддерживает кастомный формат dynamic links, поэтому нельзя настроить унифицированную ссылку на оба вида конфигураций устройств Android. Решение: использовать deep links или другой инструмент по работе с ссылками, работающий без Google Services.

3. Библиотеки с Google-сервисами. Различия в реализации и потенциальное скопление багов в логике будут, если в проекте используются библиотеки с Google-сервисами. Для Huawei их придётся заменить на другие фреймворки или if-ответвления. Тогда понадобится более тщательно тестировать оба вида устройств.

  • Google Pay. На Android без Google-сервисов можно столкнуться с окном Оплата недоступна, так как для нее нужен доступ к Google-сервисам, которые ваше устройство не поддерживает. Аналогичную ошибку можно встретить при запуске других приложений, не предназначенных для Huawei без Google.

  • Google-карты. Работа с Google-картами может содержать проблемы с кластеризацией, поиском, отрисовкой текущего местоположения и так далее.

  • Google-аккаунт. Авторизация через Google-аккаунт на Huawei без поддержки Google недоступна. Но реализация авторизации-регистрации через Huawei-аккаунт была бы кстати.

  • Магазины. Если мобильное приложение может отправить пользователя в магазин (для оценки, например), то необходимо проверить, что Android без Google Services отправляет в App Gallery, а Android с поддержкой Google в Google Play. Если устройство поддерживает обе конфигурации, было бы здорово, если бы пользователь мог выбрать между магазинами.

4. Сервисы с поддержкой Google. Для Huawei без Google придётся найти аналоги или разработать их самостоятельно. Хорошо, что важные базовые инструменты, как упоминалось выше, доступны в AppGallery Connect из коробки.

Например, на Android-устройствах можно открывать ссылки из приложения тремя разными способами:

  • WebView,

  • CustomTabs (разработка Google),

  • браузер.

Для Huawei без поддержки Google, по умолчанию доступны только два способа или дополнительная разработка вручную.

Android с Google и без: сколько времени понадобится на тестирование

Базовые активности QA:

  • планирование,

  • ревью ТЗ и дизайна,

  • написание проверок,

  • прогоны по фиче, итоговые, регрессионные,

  • написание отчётности

Это пример активностей QA в среднем по больнице. Мы исключаем особенности компании и проектов и говорим немного в вакууме.

При работе с Huawei без Google-сервисов точно добавляется время к каждой из активностей:

  • Ревью ТЗ и дизайна, написание проверок. Будут дополнительные кейсы, отражающие особенности работы с такими устройствами. Можно смело увеличивать оценку временных затрат на это в 1,41,6 раза. Здесь время уйдёт либо на обработку дополнительных сценариев в ТЗ и дизайне, либо на анализ и подтверждение, что никакой особенной реализации для Android без Google-сервисов нет.

  • Прогоны. Во время прогонов (по фиче, итоговых, регрессионных) рекомендуется проводить тестирования как на Android с Google-сервисами, так и без. Особое внимание устройствам, где доступны оба вида сервисов. Здесь сокращение количества устройств может когда-нибудь неприятно выстрелить. Время может увеличиться в 1,82 раза и уйдёт на осуществление прогона на всех трёх видах устройств.

  • Обратная связь. Под обратной связью мы в Surf подразумеваем просмотр маленьких задач (которые не требует прогона по фиче например, Смену статичного текста) и исправленных багов. При работе с обратной связью, а также при анализе и просмотре импакта от багов и прочих задач, снова не стоит забывать про тот же список устройств (без и с Google-сервисами, а также с двумя видами сервисов) для тестирования. Время увеличивается примерно в 1,3 раза снова для того, чтобы осуществить ретест или проверку задачи на этих видах устройств.

  • Послерелизные активности. При релизе приложения в AppGallery необходимо продолжать мониторить работу МП как минимум по crash-сервису, чтобы поддерживать качество и исправлять ошибки вовремя. Если в проекте не используется один инструмент мониторинга обоих видов устройств, то времени на работу с двумя инструментами и анализом багов будет уходить больше. Пожалуй, тут лучше увеличить время в 2 раза.

  • Тестируемые устройства. И, конечно, на время может повлиять количество выбранных устройств для тестирования (автоматизированного или ручного). Подходить к выбору устройств стоит ответственно, проанализировав множественные факторы проекта и аудитории.

При тестировании на Android с Google Services хочется покрыть наибольшее количество устройств: разные операционные системы, оболочки, разрешения экранов, внутренние особенности и возможности. Устройств становится ещё больше, когда добавляются девайсы Huawei с HMS-сервисами.

Таким образом, необходимо покрыть бОльшее количество устройств: не забывая про Android с Google-сервисами, Android без их поддержки, и Android с поддержкой HMS-сервисов помимо Google.

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

Время общего тестирования фичи увеличится:

  • в 1,82 раза в случае разных инструментов для реализации фичи;

  • в 1,31,5 раза в случае одного инструмента для реализации фичи (в том числе при отсутствии отличий на первый взгляд);

  • в 1,41,6 раза в случае дополнительных требований и отличительной части реализации.

Таблица-сравнение по тестированию фичи для устройств с Google и без

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

Фича

Поддержка Android только с сервисами Google

Поддержка Android с Huawei и Google Services

Авторизация по логину и паролю, а также через соц.сети

X

1,5X

Push-уведомления (реализация через Firebase и AppGallery Connect)

X

1,8X

Аналитика, около 15 событий (реализация через Firebase и AppGallery Connect)

X

2X

Аналитика, около 15 событий (реализация через один сервис, например, Amplitude)

X

1,4X

Значение Х время на тестирование Android с Google-сервисами. Оценка Y*X время на тестирование Android с двумя видами сервисов, где Y коэффициент увеличения времени на работу с Huawei с HMS-сервисами

Все временные оценки исходя из нашего опыта. Приводим их для примерного понимания.

Что хотелось бы сказать в конце...

Мы в Surf поддерживаем устройства Huawei без Google-сервисов на некоторых проектах и довольны процессами. Конечно, поработать головой иногда приходится чуть дольше: разработчикам чтобы найти универсальное решение. QA чтобы найти максимальное количество дефектов, широко покрыть фичи проверками и обеспечить качественное тестирование. И на мой взгляд, оно того стоит.

Подробнее..

Перевод Flutter 2.2 что нового

09.06.2021 12:21:23 | Автор: admin

Представляем свежий релиз Flutter 2.2, анонсированный на Google I/O. Да, оригинальная статья вышла ещё в мае, но мы считаем, что лучше поздно, чем никогда. Публикуем перевод статьи с комментариями Евгения Сатурова ex-Flutter TeamLead Surf, а ныне DevRel Surf.

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

Основа Flutter 2

За основу Flutter 2.2 взят Flutter 2, который работает не только с мобильными устройствами, но и с вебом, ПК и встраиваемые системами. Он создан специально для мира, где нас окружают компьютеры: множество разных устройств и форм-факторов создают потребность в единообразии интерфейсов.

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

Flutter стал самым популярным фреймворком для кроссплатформенной разработки.

Рост Flutter отметили в недавнем исследовании мобильной разработки. Компания SlashData провела анализ и выпустила статью Mobile Developer Population Forecast 2021, согласно которой Flutter стал самым популярным фреймворком в кроссплатформенной разработке: его выбирают 45% разработчиков. Рост между Q1 2020 и Q1 2021 составил 47%. Интерес к Flutter продолжает расти: за последние30 дней одно из восьми приложений, загруженных в Play Store, написано на Flutter.

Комментарий Жени Сатурова

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

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

Сегодня клиенты приходят за Flutter целенаправленно и часто даже не рассматривают нативный подход. В наших кейсах можно найти несколько хороших примеров.

На I/O мы рассказали вам, что только в Play Store загружено более 200 тысяч приложений, написанных на Flutter. Эти приложения пишут очень серьёзные компании. Например:

  • Tencent их мессенджером WeChat пользуется более 1,2 миллиарда пользователей iOS и Android.

  • ByteDance создатели TikTok, которые уже написали 70 разных приложений на Flutter.

  • Другие компании, в том числе BMW, SHEIN, Grab и DiDi.

Конечно, Flutter используют не только крупные корпорации. Некоторые новаторские приложения делают компании, названия которых вы, возможно, и не слышали раньше. Например:

  • Wombo вирусное приложение с поющими селфи.

  • Fastly приложение для интервального голодания.

  • Kite красивое приложение для инвестиций и трейдинга.

Flutter 2.2

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

Sound null safety теперь по умолчанию работает для новых проектов. Null safety защищает от ошибок типа null reference exception, так как даёт разработчикам возможность указать non-nullable типы в своём коде. А раз Dart абсолютно непротиворечив (sound), компилятор может не проверять на null во время выполнения. В результате производительность приложения повышается. Наша экосистема отреагировала быстро: около 5 000 пакетов уже обновлены и поддерживают null safety.

Комментарий Жени Сатурова

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

Хорошая новость заключается в том, что столь масштабных изменений во фреймворке и языке Dart в ближайшие годы не предвидится. Шутка ли к внедрению null safety в прод шли больше двух лет. Чтобы облегчить переезд коллегам из других компаний, мы оперативно обновили все наши пакеты, входящие в состав репозитория SurfGear.

Кроме того, в этом релизе есть множество возможностей повысить производительность:

  • Для веб-приложений фоновое кэширование с помощью сервисных работников.

  • Для приложений на Android поддержка отсроченного запуска загружаемых компонентов.

  • Для iOS мы разработали инструменты предварительной компиляции шейдеров.

Благодаря им удастся устранить или сократить лаги при первом запуске.

Комментарий Жени Сатурова

Многие ждали, что возможность прекомпиляции шейдеров для Metal поступит в stable уже в этом релизе, но увы. В утешение могу посоветовать попробовать собрать проект из dev-канала. Обещаю, вы непременно заметите разницу в FPS на iOS в анимациях и переходах.

Ещё мы добавили поддержку сторонних инструментов и расширений и несколько фич в DevTools, чтобы вам стало понятнее, как распределяется память в вашем приложении.

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

Мы не только совершенствуем сам Flutter. Вместе с другими командами Google мы работаем над интеграцией Flutter в более масштабный стек технологий компании. В частности мы продолжаем разработку надёжных сервисов, с помощью которых разработчики смогут с умом монетизировать приложения. В этом релизе мы дополнили новый ads SDK: добавили в него null safety и поддержку адаптивных баннеров. Ещё мы добавили новый плагин для оплаты, который написали в сотрудничестве с командой Google Pay. С его помощью можно провести оплату за материальный товар как на iOS, так и на Android. Также мы обновили свой плагин для оплаты из приложения и соответствующую статью на codelab.

Комментарий Жени Сатурова

Радует, что экосистема вокруг Flutter развивается в правильном направлении. Не так много осталось незакрытых официальными плагинами вопросов. Возможность процессинга платежей через Google Pay существовала уже давно, но официальная реализация это гарантия качества решения. В нашем последнем релизе The Hole мы внедрили in_app_purchase ещё до выхода пакета в стабильный релиз. Надеемся, что теперь работать с ним станет ещё приятнее.

Так как Dart секретный ингредиент в составе Flutter, в этом релизе мы обновили и его. В Dart 2.13 мы расширили возможность интеграции нативного кода, добавив в FFI поддержку массивов и упакованных структур. В том числе появилась поддержка псевдонимов типов, с которыми код станет читабельнее, а некоторые сценарии рефакторинга получится осуществить менее болезненно. Мы продолжаем добавлять новые возможности интеграции с более широкой экосистемой через GitHub Action для Dart и Docker Official Image. Последний проходит тщательную проверку и оптимально подходит для внедрения бизнес-логики в облачную среду.

Не просто проект Google

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

За последние месяцы больше всего роста было связано с тем, что всё больше платформ и операционных систем стали доступны для Flutter. На Flutter Engage мы объявили, что Toyota собирается использовать Flutter в следующем поколении информационно-развлекательных систем своих автомобилей. А месяц назад Canonical отправили в свободное плаванье первый релиз Ubuntu с поддержкой Flutter, в том числе интеграцией со Snap и поддержкой Wayland.

Рост экосистемы отлично демонстрируют два наших новых партнёра. Samsung портирует Flutter на Tizen и оставляет репозиторий опенсорсным, чтобы остальные тоже могли в него контрибьютить. А Sony руководит разработкой решения для Linux на встраиваемых системах.

Дизайнеры тоже выигрывают от того, что проект опенсорсный: так, Adobe анонсировали, что обновили плагин XD to Flutter. Adobe XD даёт дизайнерам отличную возможность экспериментировать и копировать свои предыдущие работы. С улучшенной поддержкой Flutter дизайнеры и разработчики могут вместе работать над материалами а значит, ещё быстрее выводить классные идеи в продакшн.

И наконец, с нами продолжает сотрудничать Microsoft. Команда Surface разрабатывает с помощью Flutter решения для складных устройств. А на этой неделе в альфа канале появится поддержка Flutter для приложений на UWP, разработанных для Windows 10. Мы очень рады, что всё больше мобильных, десктопных, веб- и других приложений используют способность Flutter подстраиваться под разные платформы.

Комментарий Жени Сатурова

Дух open-source разработки захватывает кроссконтинентальные корпорации. Очень радует тенденция открытости и поддержки, даже когда речь идёт о взаимодействиях таких монстров, как Samsung, Google и Microsoft. Пока разработки ведутся в независимых репозиториях и никакого официального отношения к Flutter не имеют, но существует надежда, что скоро список поддерживаемых платформ расширится в полтора раза.

Создаём отличный пользовательский опыт

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

Нам нравится смотреть, какое применение вы находите для Flutter. Один из примеров проект от US Veterans Administration. В видео о том, как приложение на Flutter помогаетсолдатам с реабилитацией от посттравматического стрессового расстройства.

Мы проведём множество разных воркшопов, презентаций, посвящённых Flutter и ответим на возникшие у вас вопросы на Google I/O. А пока не забудьте попробовать нашу веб-фотобудку, написанную во Flutter: в ней можно сделать селфи с нашим талисманом Dash и её друзьями!

Подробнее..

Анонс вебинара Flutter vs технология, на которой пишете вы за чем будущее?

16.10.2020 16:14:58 | Автор: admin
Привет!

20 октября в 18:00 мск проводим вебинар Flutter vs технология, на которой пишете вы: за чем будущее?

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

Регистрация




Flutter кроссплатформенная технология от Google и причина частых холиваров: одни ей восторгаются, другие критикуют.

Мы в Surf одними из первых в России начали использовать в разработке эту технологию и реализовали на ней большие e-commerce и банковские проекты. Среди них приложения для Росбанка, сети аптек Ригла и ресторанов KFC.

Расскажем, каков Flutter в деле и стоит ли обратить на него внимание.

О чём поговорим:

  • Что такое Flutter и какие у него перспективы.
  • Какие проекты можно делать на Flutter.
  • Как сменить привычный стек и перейти во Flutter.


Спикеры



Из Android во Flutter. Как распознать технологию, которая выстрелит


Евгений Сатуров, Flutter TeamLead Surf
Flutter/Android-разработчик, тимлид, активный спикер и технический евангелист в Surf. Основатель Flutter Dev Podcast. Один из организаторов крупнейшей российской мобильной конференции Mobius, а также лидер регионального IT-коммьюнити GDG Voronezh. За плечами 14 проектов, включая Росбанк Бизнес (Flutter), SBI bank (Android), MDK (Android).



Уйти во Flutter, чтобы не быть привязанным к ОС


Артём Зайцев, руководитель Flutter-отдела Surf
В прошлом Android-разработчик и тимлид. Прошёл путь от стажёра до head of. Загорелся кроссплатформой из желания, наконец, не быть привязанным к конкретной ОС. Cо-ведущий Flutter Dev Podcast, выступает на конфах. Приложил руку ко многим проектам, включая Магнит, МДК, KFC.


Путь из сурового энтерпрайза во Flutter


Алексей Радионов, Flutter-разработчик Surf
Занимался автоматизацией торговли нефтепродуктами. Это были серверные решения на C#, позже на Java Spring. Десктопные интерфейсы на Delphi и С++/Qt. Встраиваемые решения на C/С++ с использованием микропроцессоров Atmel или аппаратных платформ с embedded-linux.

Позже увлекся мобильной разработкой. Когда закончил проект под Android, задумался о разработке такого же приложения под iOS. В процессе поиска материалов узнал, что недавно вышел Flutter. Cосредоточился на его изучении. Через какое-то время попал на работу в Surf и уже поучаствовал в разработке приложения Росбанк Бизнес на Flutter.



Из геймдева во Flutter


Михаил Зотьев, Flutter-разработчик Surf
До мобильной разработки работал 5 лет в геймдеве. За это время успел попробовать множество технологий и клиентских, и серверных. Увлекся кроссплатформенной мобильной разработкой и понял, что это именно то, что искал всё время. Сейчас активно работает над проектом КFC. Периодически пишет статьи по разработке на Flutter, делится опытом и знаниями с комьюнити.



Чем Dart очаровал фулл-стек разработчика


Андрей Савостьянов, Flutter-разработчик Surf
В прошлом full-stack Java/Spring/Android, железные решения и протоколы, прикладные системы по автоматизации и мониторингу технологических комплексов. Придерживается широких взглядов на разработку. Language agnostic (Java, TypeScript, Dart). Dart 2.х и Flutter очаровали возможностями: мультиплатформа, производительность, нативная компиляция. Почти 2 года назад сменил специализацию и ни разу не пожалел. Работает на e-commerce проекте.


Регистрация


Вебинар начнётся 20 октября в 18:00. Место встречи YouTube-канал Surf.

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

До встречи!
Подробнее..

Surf на DartUP 2020

24.11.2020 16:19:42 | Автор: admin
4 5 декабря пройдёт единственная в России конференция о Flutter и Dart на русском языке DartUp 2020. Это главное Flutter-событие года. Вас ждут доклады известных российских и мировых спикеров, которые расскажут об экосистеме Dart и дадут практические советы и лайфхаки, а также партнёрские мероприятия.

Мы в Surf тоже участвуем в DartUp в качестве партнёров и подготовили несколько движух. Участвуйте в них, чтобы прокачать себя и сделать мир Flutter-разработки лучше вот так, без регистрации (почти) и смс.

Что будет? Рассказываем.



Вас ждёт:

  • Код-ревью ваших репозиториев в прямом эфире. Его проведёт Артём Зайцев, руководитель Flutter-отдела в Surf.
  • Возможность поучаствовать в развитии Flutter-комьюнити и сделать мир Flutter-разработки ещё круче: предлагайте любые идеи, как улучшить работу с фреймворком.
  • Доклады от Flutter Teamlead Surf Жени Сатурова и ведущего Flutter-разработчика Миши Зотьева.
  • Много подарков и много общения.

А теперь подробнее

Сбор идей для Open Source


Мы в Surf участвуем в развитии Flutter-сообщества и приглашаем вас присоединиться к этому. Как?

Давайте вместе подумаем, что мы можем сделать для мира Flutter-разработки. Как улучшить работу с фрейморком? Чего не хватает? Что сделать, чтобы стало проще, лучше и удобнее? Какие боли есть у Flutter-разработчиков? Что-то ещё, что вы хотите предложить? Абсолютно любые идеи приветствуются. Присылайте!

Пять лучших предложений реализуем в рамках проекта SurfGear. SurfGear это open source набор библиотек, стандартов, инструментов для разработки на Flutter, который мы ведём уже больше года.

Среди всех участников рандомно разыграем мерч Surf.

Код-ревью ваших репозиториев в прямом эфире


Для молодых и для опытных разработчиков руководитель отдела Flutter-разработки в Surf Артём Зайцев вместе с приглашёнными экспертами проведёт код-ревью ваших репозиториев.

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

Спикеры



Gear up, ускоряем Flutter-разработку!


Евгений Сатуров, Flutter TeamLead Surf, активный спикер и технический евангелист в Surf
Два года назад Flutter был всего лишь хайповой технологией с неясными перспективами. Ставки сделаны, и теперь имеется несколько крупных проектов, а также большой репозиторий с опенсорс наработками SurfGear. Главная миссия SurfGear ускорять разработку и упрощать реализацию типовых задач. Доклад представляет собой обзор решений, которые помогут оптимизировать процессы вашего Flutter-проекта.



Flutter под капотом


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


Участие и регистрация


Когда: 4 и 5 декабря
Где: онлайн

Прислать свои предложения, поучаствовать в код-ревью

Программа и регистрация на официальном сайте мероприятия
Подробнее..

Flutter клёвенький у меня только такое объяснение. Обзор лучших выпусков Flutter Dev Podcast

28.09.2020 14:11:21 | Автор: admin
Привет! На связи Flutter Dev Podcast и его создатель и ведущий Евгений Сатуров.

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

Как и зачем мы делаем подкаст, я подробно рассказал на vc.ru. В этой статье я сделал обзор самых интересных и популярных эпизодов Flutter Dev Podcast.




Во Flutter я пришел из Android: узнал о новой технологии в кулуарах конференции от Звиада Кардавы, Developer Relations из Google Russia, который потом стал первым гостем нашего подкаста. Идея создать СМИ про Flutter возникла потому, что мы одни из первых в стране начали что-то делать на этом фреймворке: ниша была свободна.

Flutter это технология кроссплатформенной разработки приложений под iOS, Android, веб и десктоп от Google.

Flutter Dev Podcast я запустил с коллегой Артёмом Зайцевым мы вместе работаем в Surf. На тот момент мы практически ничего не знали про Flutter, и, можно сказать, росли вместе с подкастом. Параллельно с подкастом развивали Flutter в Surf. Теперь у нас целый Flutter-отдел в нём работает 13 человек.Мы с ребятами ведём публичный репозиторий SurfGear на GitHub, где выкладываем всякие полезности для разработки на Flutter: набор библиотек, стандартов, инструментов.

Спасибо Google и лично Екатерине Винниченко и Звиаду Кардаве за поддержку нашего подкаста и за приглашение сделать обзор выпусков в блоге Google.

Выпуски Flutter Dev Podcast: от свежих к ранним




Целая платформа для заработка всевозможных людей


#19 Яндекс.Про

Геннадий Евстратов: Глобальная цель сделать громадного франкенштейна, в котором процент Flutter будет нарастать, нарастать, нарастать, и можно будет просто взять маленькой лопаточкой Android-код, вышвырнуть его и оставить просто Flutter-приложение.

Команда Flutter-разработчиков из Яндекса постоянные гости подкаста. В ранних выпусках они рассказывали о Яндекс.Таксометре это, кстати, тоже попало в наш обзор. Теперь Яндекс.Таксометр переродился в Яндекс.Про. iOS версия написана чисто на Flutter, а Android версия это гибрид: чисто Flutter версия ещё не догнала функциональность Android версии, поэтому выкидывать её пока нельзя.

Гости из Яндекса рассказывают, почему и в каком виде проникает Flutter в проекты компании: фреймворк уже используют для отдельных модулей в Яндекс.Лавке, Яндекс.Такси, Яндекс.Go. Говорят о том, что мешает перейти целиком на Flutter прямо сейчас, какие проблемы вскрылись при работе с Fish Redux из-за масштабирования и через что нужно будет пройти разработчику, чтобы интегрировать Flutter в нативное приложение.





Я много чем занимаюсь во Flutter-команде, но в первую очередь забочусь, чтобы внешние разработчики получали хороший опыт


#17 Flutter Day 2020

Chris Sells: Вы всегда можете написать нативный код в своём приложении или даже создать собственный плагин. Если вы напишете собственный плагин, и у нас такого нет, пожалуйста, поделитесь им с миром. Именно так растёт экосистема Flutter.

Это во многом необычный выпуск: впервые часть подкаста мы вели на английском и впервые сделали онлайн-трансляцию на Youtube.

В гости к Flutter Dev Podcast пришёл Chris Sells продакт менеджер из Google: вместе с Flutter-командой он занимается развитием Flutter. Обсудили разное: возможность одновременно дебажить на большом количестве устройств и эмуляторов, откуда появился Dash символ Flutter, какие проблемы у Flutter-команды есть в режиме удалённой работы.

Chris рассказал, как будет внедряться Null safety и что сильнее всего влияет на архитектуру приложения (и это не выбор State management). Вы узнаете, будет ли во Flutter свой Jetpack, почему из ранних версий Dart убрали Reflection API и добавят ли его обратно, сделают ли поддержку data value объектов. Eщё ведущие обсудили с Chris компиляцию Flutter-приложений на процессоры Arm в новых устройствах от Apple, что мешает выпустить альфа-версию Flutter с поддержкой сборки приложений для Windows и Linux и будет ли во Flutter динамическая загрузка кода.





Медиа это тот тип активности человечества, где все постоянно идёт не так, как ты хочешь


#16 Meduza

Борис Горячев: Я встретил сопротивление, которое я всегда встречаю у нативных разработчиков. Когда они слышат что-то про кроссплатформу, они сразу встают в позу, говорят, что это отстой полный, что всё плохо работает, всё медленно и вообще отстой. У них аргументы примерно такие:
А что если тебе надо будет показать вот это, вот это, вот это?
Но нам не надо будет это показать.
Нет, а что если надо будет?
Очень вряд ли.
Ну плохой перформанс!
Ну вроде как нет.
Нет, ну плохой, на нативе будет быстрее.

Новое приложение Meduza написано на Flutter с нуля. В 16 выпуске Flutter Dev Podcast CTO Meduza Борис Горячев рассказывает, зачем Meduza это надо. Начинаем с истоков: обсуждаем, почему в 2014 провалилась концепция mobile first, говорим про непростые отношения с нативными разработчиками, удивительный мир медиа-разработки, игры со шрифтами, тяготы работы с WebView и Backend Driven UI. А ещё Борис отвечает на претензии Артемия Лебедева.

Подробный пересказ выпуска Flutter Dev Podcast с Борисом Горячевым





Изначально я хотел сделать что-то похожее на VS Code, но лучше


#15 Flide IDE на Flutter

Андрей Лесницкий: Я начал продукт для себя, чтобы повысить свою продуктивность. Если это повышает мою продуктивность, это может повысить продуктивность и всех разработчиков в экосистеме Flutter.

Андрей Лесницкий из Минска пишет среду разработки на Flutter. Он вдохновился Android Studio и VS Code, и старается взять из них всё лучшее но хочет сделать IDE по-своему. Почему для проекта он выбрал Flutter: это челлендж или специальная задумка? Каким продукт задумывался и каким получился?

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





Мне очень понравилось быть водителем такси. Если бы тарифы были повыше, я бы переквалифицировался


#11 Яндекс.Такси

Геннадий Евстратов: Служба безопасности сказала: React Native только через их труп.

Команда Яндекс.Такси делает на Flutter программный комплекс для службы такси приложение Яндекс.Таксометр. В выпуске они рассказывают, почему сначала пилили проект на React Native, но потом перешли на Flutter, зачем сотрудники Яндекса таксуют по ночам и за счёт чего приложение на Flutter делается в два с половиной раза быстрее, чем на Android. А ещё про иероглифы в документации Fish redux, интеграцию Яндекс Map Kit и собственный виджет пак.





CI/CD сделает всё за вас, даже если у вас нету техники от Apple


#9 CI/CD Jenkins, Bitrise, Codemagic

Михаил Токарев: Когда мы разговаривали с Flutter Team по поводу CI/CD, они очень чётко сказали: Мы хотим видеть CI, которым могла бы пользоваться даже моя бабушка. С этой установкой мы и начали делать Codemagic.

Вместе с CTO Codemagic разбирались, зачем нужен CI/CD, в какой момент становится понятно, что без него не обойтись, и чем чреваты локальные сборки. Сравнивали Jenkins, Bitrise и Codemagic по всем параметрам, до которых смогли дотянуться: возможности, ограничения, стабильность, кастомизация, цены. Выясняли, откуда появился Codemagic и почему он позиционируется именно как CI/CD для Flutter, а не для всего подряд, в чем его отличие от остальных инструментов и какая компенсация положена пользователю, если сборка упала по вине инструмента.





Работает на всём, на чём есть экран


#7 Всё про кроссплатформу

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

Максим Биянов: Xamarin подошёл к максимальной матюрности. К состоянию, когда все основные проблемы решены и началось экстенсивное развитие. Есть фишки, которые малозаметны. Сейчас внимание уделяется нативному iOS.

Александр Фёдоров: React Native это промежуточное решение между нативным и вебом. Нативное получается быстрее, веб медленнее. Что-то посередине это React Native. Наверно, самый большой плюс что он пишется на JS. JS-разработчиков много, вход в разработку быстрый. React тоже не очень сложный.

Артём Зайцев: Ключевое отличие Flutter от того же React или Xamarin то, что он имеет собственный движок под капотом. И не использует нативные элементы, а просто рисует такие же.

Роман Яцына: Кotlin Native в целом тот же самый Kotlin, просто зарестрикченный. Сейчас очень сложно найти человека, который согласился бы писать на Java. Многие прям уходят из своих компаний, потому что там нет Kotlin.

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

React Native, Xamarin, PWA, QT, C++, Kotlin Native, Flutter Кроссплатформа достаточно общее слово, за которым кроется много разных технологий.

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





Мобильные разработчики пришли во Flutter, посмотрели на async/await и ужаснулись


#6 Асинхронность

Евгений Кот: Чтобы понимать стримы, нужно понимать, как сантехника работает.

Есть async/await, Future API, Stream API, есть метод Compute, есть даже RXDart. Как из этого многообразия понять, что из этого надо использовать, а что нет. Что делать со всей этой асинхронщиной, если вы пришли из мира iOS или Android. Почему изолят как пирожок с полки, и как Flutter обрабатывает асинхронные операции если Dart однопоточен.





Все виртуальные машины действительно на одно лицо


Слушать выпуск #5 Dart VM

Вячеслав Егоров: Начать можно с того, Dart VM название оно немножко неправильное. Правильно его называть Dart Runtime, потому что он не всегда из себя представляет виртуальную машину. Люди, которые представляют себе виртуальную машину, они представляют себе, что внутри какой-то байт-код исполняется. А более правильно, наверно, это все-таки называть Dart Runtime.

Вячеслав Егоров, разработчик Dart VM, рассказывает, почему Flutter написан на Dart, какую чёрную магию применяет Hot reload, в чем особенности Garbage collector. Про компиляцию из 90-х, прогретые функции и Flutter-веб. Как соотносятся изоляты в Dart и многопоточность, во что компилируется Flutter-приложение в релизной сборке и что у Flutter с реверс-инжинирингом.

Все выпуски Flutter Dev Podcast на Soundcloud
Подробнее..

Перевод Анонс Flutter 1.22

06.10.2020 14:22:26 | Автор: admin
Всем привет! Я Евгений Сатуров, Flutter TeamLead в Surf. Представляю перевод официальной статьи про свежий апдейт Flutter 1.22 как обычно, дополненный моими комментариями.

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

Погнали ближе знакомиться с новой версией Flutter 1.22.




Мы рады представить последний релиз Flutter с обширной поддержкой iOS 14 и Android 11. Flutter 1.22 основан на фундаменте, заложенном предыдущими версиями, и позволяет разработчикам создавать быстрые и красивые пользовательские интерфейсы для нескольких платформ из единой кодовой базы. В наших ежеквартальных stable релизах новые функции, улучшенная производительность и bug fixы.

Недавно вышли новые версии мобильных ОС. Мы сфокусировались на том, чтобы Android 11 и iOS 14 превосходно работали на этом релизе Flutter.

Обновление Flutter для iOS 14: поддержка нового Xcode 12, новых иконок и поддержка App Clips запуска iOS-приложения без установки. App Clips полный аналог Instant Apps for Android.

Обновление Flutter для Android 11: добавили поддержку разных типов чёлок у дисплеев, а также более плавную анимацию при вызове soft keyboard.

Этот релиз выходит всего через два месяца после выхода версии 1.20, но даже за это небольшое время мы закрыли 3024 issues и замёрджили 1944 PR от 197 контрибьюторов.

Комментарий

Кстати, если вы давно хотите стать контрибьютором Flutter, но не знаете с чего начать, вот вам пара советов. Фильтруйте issues по тэгу good first contribution: время от времени там появляются интересные задачи, с которых можно начать свой путь в опенсорс. Также вы можете реализовать фичу чьей-то мечты, отфильтровав issues по тэгу new feature. Там есть задачки даже для начинающих. Вы сможете увековечить своё имя в истории фреймворка конечно, в том случае, если разработчики Flutter-team согласны с тем, что такая фича должна быть реализована.


Вдобавок к поддержке новых версий мобильных ОС, у нас есть и другие новости, включая предварительную версию одной из наиболее востребованных функций для Android:
  • восстановление состояния,
  • новая вселенная Material кнопок,
  • новая библиотека для поддержки локализации и интернационализации, работающая с Hot Reload,
  • новый Navigator,
  • стабильная версия для Platform Views (основа для плагинов Google Maps и WebView),
  • переключатель, который вы можете добавьте свой код, чтобы улучшить прокрутку на устройствах с высокочастотными дисплеями.

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

Целевая платформа iOS 14


Каждый раз, когда анонсируется новая версия мобильной ОС, мы тщательно её тестируем: ищем несовместимости или изменения, которые могут повлиять на работу Flutter и его инструментов.

В случае с iOS 14 мы сделали некоторые изменения во Flutter, чтобы убедиться, что он будет работать ровно так, как того хотят разработчики:

  • Xcode 12 требует iOS 9.0 или выше, поэтому в нашем шаблон по умолчанию теперь вместо версии платформы 8.0 указана версия 9.0.
  • В Flutter 1.22 были исправлены специфические сбои iOS 14 и проблемы с отрисовкой шрифтов.
  • Проблемы с развёртыванием на физические устройства (приложение не запускалось на устройстве только на эмуляторе) были исправлены в Flutter 1.20.4.
  • Новая политика, которая показывает уведомления, когда приложения обращаются к своему буферу обмена, вызвала ложные уведомления в приложениях Flutter и была исправлена в Flutter 1.20.4.
  • Новая политика сетевой безопасности для локальной отладки приложений Flutter заставляет iOS 14 показывать одноразовое диалоговое окно подтверждения (только во время разработки, а не для выпущенных Flutter-приложений).

Итог: если iOS 14 целевая платформа для вашего Flutter-приложения, мы настоятельно рекомендуем вам пересобрать его с помощью Flutter 1.22 и задеплоить его в App Store прямо сейчас, чтобы обеспечить наилучший пользовательский опыт для тех, у кого iOS 14.

Дополнительную информацию, включая некоторые соображения по поводу Add-to-App, дип линкам и уведомлениям, смотрите в документации iOS 14 на flutter.dev.

Отдельно хотим сказать про обновлённую поддержку нового шрифта iOS SF Symbols, который вдохновил нас потратить некоторое время на обновление пакета cupertino_icon. Существующие варианты использования CupertinoIcons будут автоматически отображаться в новом стиле, как только вы обновите зависимость cupertino_icons до новой основной версии 1.0. Если вы используете cupertino_icons 1.0 вместе с Flutter 1.22, у вас будет доступ к ~ 900 новым иконкам через API CupertinoIcons.



Полный список иконок вы найдёте на странице предварительного просмотра cupertino_icons и на странице сведений о миграции на flutter.dev.

Еще одна функция, которую вы можете попробовать с Flutter на iOS 14, это App Clips. App Clips новая фича iOS 14: приложения быстро запускаются без установки на телефон, если весят меньше 10 мб. Flutter версии 1.22 умеет работать с App Clips в предварительном варианте реализации.


App Clips на Flutter

Подробнее о том, как создавать App Clips приложений с помощью Flutter, в документации на flutter.dev. Посмотрите также образец простого проекта.

Комментарий

Помните Instant Apps for Android? Похоже, Apple решили вернуть наш 2017 год и представили свой полный аналог этой фишки App Clips. Теперь вы можете запускать приложения без установки и на iOS. Более того, вы не теряете эту возможность даже в том случае, если вы пишете не нативное приложение, а кроссплатформенное.

Не забывайте про ограничения: если ваша сборка весит более 10 Мб, то запустить её в формате App Clips не получится. В случае с Flutter-приложением это может быть действительно актуальной проблемой. Но об этом вы узнаете во второй половине статьи: там мы расскажем, как вы сможете мониторить размер сборки вашего приложения при помощи удобного инструмента.


Android 11


Этот релиз Flutter вышел практически одновременно с релизом Android 11. Фреймворк и движок Flutter обновились, чтобы поддерживать две новые фичи, представленные в последней версии Android.

Во-первых, Flutter теперь учитывает расположение чёлок и вырезов на экране Android-телефона, а также закруглённые края экрана.



Используя API MediaQuery и SafeArea, теперь легко написать UI, в котором интерактивные области не будут попадать на вырезы и закруглённые края экрана.

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


Обратите внимание на FAB

Issue 19279 давняя проблема, когда анимация отображения / скрытия экранной клавиатуры не синхронизируется с Flutter inset. Для Android 11 это исправлено.

Раньше из-за плагинов Flutter были проблемы, когда в нативное Android-приложение вставляли Flutter-код. Мы переписали API для интеграции Flutter c Android и выпустили API v2, которая лишена этих недостатков. Начиная с версии 1.22, мы прекращаем поддержку старых API v1.

Если вы продолжаете использовать Android v1 API, вот что это для вас значит:
  • Новые плагины создаются без поддержки v1 API.
  • Флаг конфигурации инструмента Flutter no-enable-android-embedding-v2 был удалён. Теперь это поведение по умолчанию.
  • В более старых приложениях, всё ещё использующих API v1, во время сборки будет отображаться предупреждение об устаревании, которое ссылается на документацию по поддержке новых API-интерфейсов плагинов Android.

Между тем, если у вас всё ещё есть приложение Flutter на основе API версии 1 для Android, оно продолжит работать. Однако вы можете начать встречать новые плагины, у которых целевая API v2. Они не могут использоваться API v1 для Android. Дополнительные сведения см. В документации по критическим изменениям.

Комментарий

Пожалуй, основным страхом разработчиков относительно Flutter была неуверенность в оперативной поддержке новых фич операционных систем, которые могут быть использованы в нативных решениях уже с первого дня. На примере релиза 1.22 мы видим, что поддержка всех новых возможностей платформ приоритет для команды. Даже App Clips, который (я уверен) не найдёт широкого применения, уже доступен к реализации. А что касается Android, куда более важное изменение в новом релизе, касающееся его, это сохранение состояния экрана после смерти процесса. О нём ниже.


Расширение вселенной кнопок


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

Чтобы идти в ногу с Material гайдлайнами, мы представляем новую вселенную кнопок во Flutter 1.22.


Новая вселенная Material design кнопок

У новых виджетов и тем новые имена классов. Мы переименовали классы во Flutter, чтобы они соответствовали спецификации Material Design.



Новые темы следуют нормализованному шаблону, который Flutter недавно принял для новых виджетов Material. Демо-версия на DartPad

Это не критическое изменение, поскольку семантика FlatButton, OutlineButton, RaisedButton, ButtonBar, ButtonBarTheme и ButtonTheme не изменится. Вы можете смешивать и сочетать старые кнопки с новыми.

Новая поддержка интернационализации и локализации


Основные функции, необходимые для интернационализации (i18n) и локализации (l10n) ваших приложений, доступны во Flutter с самого начала. В этом релизе мы сделали поддержку Hot reload для обновления вашего приложения по мере добавления новой l10n информации.



Если вам нужна дополнительная информация о поддержке Flutterом l10n, включая локализованные сообщения, сообщения с параметрами, датами, числами и валютами, прочтите Flutter Internationalization User Guide.

Кроме того, если вас интересуют i18n и l10n, вам, возможно, потребуется использовать строки с символами, которые не вписываются в простой старый ASCII например, Unicode и эмодзи. Недавно команда Dart выпустила characters package, который помогает разработчикам работать с кластерами графем Unicode (расширенным). Этот пакет помогает решить проблемы, например, как правильно сократить строку типа A [эмодзи британского флага] text in English до первых 15 символов. Используя класс String, это сокращение будет A [эмодзи британского флага] text in, что составляет всего 12 символов, воспринимаемых пользователем. С другой стороны, использование пакета characters даёт правильное сокращение A [эмодзи британского флага] text in Eng.

С этим PR Flutter использует пакет символов для правильной обработки этих сложных символов. Например, при использовании TextField с ограничением maxLength такие символы, как [эмодзи семьи из трёх человек], теперь правильно считаются как один символ. Кроме того, с этим PR пакет символов доступен в Flutter-проектах автоматически, без необходимости добавлять его вручную. Надеюсь, это упростит работу со строками всех типов из всех локалов. Чтобы узнать больше о пакете символов, ознакомьтесь с отличной статьёй Правильные манипуляции со строками в Dart.

Плагины Google Maps и WebView можно использовать в продакшн-приложении


В команде Flutter мы часто опасаемся маркировать что-либо как готовое к использованию в продакшене, пока не проверим это самостоятельно. В случае плагинов google_maps_flutter и webview_flutter основная причина задержки была в базовой реализации Platform Views, которая позволяет размещать собственные компоненты пользовательского интерфейса как Android, так и iOS в приложении Flutter. В этом выпуске Flutter мы рады сообщить, что мы достаточно укрепили инфраструктуру, чтобы объявить оба этих плагина готовыми к использованию.


webview_flutter plugin, отображающий flutter.dev

Во Flutter 1.22 мы добавили альтернативную реализацию Platform Views, которая устраняет все известные проблемы с клавиатурой и проблемы доступности на Android Views. Это работает с Android API уровня 19 и выше (раньше требовался уровень 20). Мы также внесли улучшения в потоки в iOS, которые делают platform views более эффективными и надёжными (и больше не требует добавления флага io.flutter.embedded_views_preview в ваш iOS Info.plist).

Плагин webview_flutter поддерживает новый режим Android Platform Views, но в настоящее время его необходимо включить вручную. Когда он получит большее распространение в сообществе, мы включим его по умолчанию.

Плагины Google Maps и WebView уже выиграли от улучшений в Platform Views.

Если хотите использовать Platform Views, чтобы размещать собственные UI-элементы в iOS и Android, прочитайте подробнее про нативные Android и iOS views в приложении Flutter.

Комментарий

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


Навигатор 2.0


Если вы раньше использовали навигацию в своих приложениях Flutter, возможно, вы заметили, что основная структура данных, стек страниц, по которым перемещается пользователь, скрыта от вас. Вместо этого, чтобы управлять ею, вы вызываете Navigator.pop() или Navigator.push().

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



Два экрана можно реализовать так:

class ColorListScreen extends StatelessWidget { final List<Color> colors; final void Function(Color color) onTapped; ColorListScreen({this.colors, this.onTapped});  @override Widget build(BuildContext context) => Scaffold(       appBar: AppBar(title: Text('Colors')),       body: Column(         children: [           // you can see and decide on every color in this list           for (final color in colors)             Expanded(               child: GestureDetector(                 child: Container(color: color),                 onTap: () => onTapped(color),               ),             )         ],       ),     );} class ColorScreen extends StatelessWidget { final Color color; const ColorScreen({this.color});  @override Widget build(BuildContext context) => Scaffold(       appBar: AppBar(title: Text('Color')),       body: Container(color: color),     );}


Использование простейшего стиля Navigator 1.0 позволяет вам перемещаться между этими двумя экранами. Это выглядит довольно легко:

class _ColorAppState extends State<ColorApp> { List<Color> _colors = [Colors.red, Colors.green, Colors.blue];  @override Widget build(BuildContext context) => MaterialApp(       title: 'Color App',       home: Builder(         builder: (context) => ColorListScreen(           colors: _colors,           // the Navigator manages the list of pages itself; you can only push and pop           onTapped: (color) => Navigator.push(             context,             MaterialPageRoute(builder: (context) => ColorScreen(color: color)),           ),         ),       ),     );}


Вызов Navigator.push() это всё что нужно, чтобы поместить другую страницу поверх первой, создав стек из двух страниц. Однако, в отличие от списка контейнеров, созданного в методе сборки ColorListScreen, этот стек скрыт от вас. А поскольку он скрыт, им трудно управлять в других сценариях, таких как обработка диплинков initial route, предоставленным, например, нативным встраиванием, или URL-адресом из интернета или намерением из Android. Также чрезвычайно сложно управлять вложенной маршрутизацией между различными структурами одной и той же страницы.

Navigator 2.0 решает эти и другие проблемы, делая стек страниц видимым. Вот обновленный пример перехода между одним и тем же ColorListScreen и ColorScreen:

class _ColorAppState extends State<ColorApp> { Color _selectedColor; List<Color> _colors = [Colors.red, Colors.green, Colors.blue];  @override Widget build(BuildContext context) => MaterialApp(       title: 'Color App',       home: Navigator(           // you can see and decide on every page in this list         pages: [           MaterialPage(             child: ColorListScreen(               colors: _colors,               onTapped: (color) => setState(() => _selectedColor = color),             ),           ),           if (_selectedColor != null) MaterialPage(child: ColorScreen(color: _selectedColor)),         ],         onPopPage: (route, result) {           if (!route.didPop(result)) return false;           setState(() => _selectedColor = null);           return true;         },       ),     );}


Приложение явно создаёт Navigator и предоставляет ему список страниц, представляющий полный стек. Мы создаём пустой _selectedColor, чтобы указать, что цвет еще не выбран, поэтому изначально мы не показываем ColorScreen. Когда пользователь выбирает цвет, мы вызываем setState() как обычно, чтобы указать Flutter, что вы хотите снова вызвать метод build(), который теперь создаёт стек с ColorScreen наверху.

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

Если Navigator 2.0 выглядит как остальная часть Flutter, это нарочно: он декларативен, в отличие от императивного Navigator 1.0. Идея заключалась в том, чтобы объединить модели навигации и остальной части Flutter, одновременно исправляя множество багов и добавляя фичи. Фактически, этот небольшой пример лишь поверхностно описывает то, что есть в Navigator 2.0. Для подробностей я настоятельно рекомендую статью о декларативной навигации и маршрутизации во Flutter.

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

Комментарий

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

С другой стороны, сообщество ждало от нового навигатора гораздо большего. Реализовать вложенную навигацию всё ещё крайне нетривиальная задача для Flutter. И Navigator 2.0 не привнёс никаких улучшений на этом поприще. Известный разработчик Remi Rousselet даже заявил в Twitter, что напишет свой навигатор. Будет любопытно посмотреть, что у него получится. А пока рекомендую опробовать наше собственное решение для реализации вложенной навигации мы разработали его в Surf.



Предварительная версия: Восстановление состояния для Android


Новая функция, с которой вы можете поэкспериментировать в этом выпуске, это поддержка восстановления состояния на Android. Это одна из наших самых желанных функций, получившая 217 отзывов!

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

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

Вот очень простой пример восстановления состояния дефолтного приложения Flutter Counter:

class CounterState extends State<RestorableCounter> with RestorationMixin {  @override  String get restorationId => widget.restorationId;  RestorableInt _counter = RestorableInt(0);  @override  void restoreState(RestorationBucket oldBucket) => registerForRestoration(_counter, 'count');  void _incrementCounter() => setState(() => _counter.value++);  @override  Widget build(BuildContext context) => Scaffold(      body: Center(child: Text('${_counter.value}')),      floatingActionButton: FloatingActionButton(onPressed: _incrementCounter),    );}


Вкратце, каждый виджет получает сегмент хранилища, который регистрируется в RestorationMixin с использованием уникального идентификатора. Используя тип RestorableProperty (например, RestorableInt, как здесь) для хранения UI-специфичных данных и регистрируя эти данные с помощью функции восстановления состояния, данные автоматически сохраняются до того, как Android завершает работу приложения, и восстанавливаются, когда оно возвращается к жизни. И это всё. Все данные, которые хранятся в типе Restoration*, например RestorableInt, RestorableString и RestorableTextEditingController (у нас их несколько), будут восстановлены. И если мы не охватыватили все типы, которые вы хотели бы восстановить, вы можете создать свой собственный, расширив RestorableProperty.

Для автоматического тестирования восстановления состояния мы добавили в WidgetTester новый API restartAndRestore
. А для тестирования вручную проще всего:
  • запустить приложение Flutter с включенным восстановлением состояния на устройстве Android,
  • включить параметр Не сохранять действия в настройках разработчика Android,
  • запустить приложение Flutter,
  • поместить его в фоновый режим,
  • вернуться к нему.

Android убьет и восстановит ваше приложение, и вы увидите, всё ли работает так, как вы ожидаете.



Эта версия восстановления состояния предварительная: предстоит ещё кое-что доделать. Например, восстановление состояния фича не только для Android, приложения для iOS тоже смогут от этого выиграть. Кроме того, мы заняты обновлением собственных виджетов, чтобы сохранять их состояние во время восстановления. Мы сделали поддержку классов Scrollable, таких как ListView и SingleChildScrollView (для запоминания позиции прокрутки пользователя) и TextFields (для восстановления введённого текста), и планируем распространить её на другие виджеты.

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

Комментарий

С одной стороны прорыв. Flutter-приложение всё меньше отличается от нативного в плане UX. С другой реализация довольно тяжеловесная, особенно если вы хотите сохранять не только состояние отдельных виджетов (например, текстовых полей они будут сохраняться по-умолчанию), но и любого State вашего кастомного виджета. Все сохраняемые свойства вам предстоит оборачивать в специальные Restorable-контейнеры, вручную регистрировать их в RestorationMixin.

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


Предварительная версия: плавная прокрутка для непревзойдённых частот ввода и отображения


Работая с нашими внутренними партнёрами из Google, команда Flutter значительно улучшила производительность прокрутки, когда частота ввода и отображения не совпадают. Например, вход Pixel 4 работает с частотой 120 Гц, а дисплей работает с частотой 90 Гц. Это несоответствие может привести к снижению производительности при прокрутке. С новым флагом resamplingEnabled вы можете решить эту проблему:

void main() {  GestureBinding.instance.resamplingEnabled = true;  run(MyApp());}


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

Новый унифицированный инструмент разработчика Dart


Как всегда, обновление Flutter касается не только движка и фреймворка, но и инструментов. Flutter 1.22 включает новую версию Dart (2.10), а также новый инструмент командной строки dart, который также может оказаться полезным.

Исторически у Dart было много небольших инструментов для разработчиков (таких как dartfmt для форматирования и dartanalyzer для анализа кода). В Dart 2.10 есть унифицированный инструмент разработки dart, очень похожий на инструмент flutter.



Начиная с Flutter 1.22 SDK, в папке <flutter-sdk>/bin (которая, вероятно, у вас есть в yourPATH) содержатся команды flutter и dart. Для получения дополнительной информации см. Сообщение в блоге Dart 2.10.

Инструмент анализа размера приложения


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

Вы можете использовать этот инструмент, чтобы собирать данные для анализа. Передайте флаг --analysis-size любой из следующих команд:
  • flutter build apk
  • flutter build appbundle
  • flutter build ios
  • flutter build linux
  • flutter build macos
  • flutter build windows


Использование этого флага при построении выходного артефакта Flutter выводит сводку о размере и составе артефакта. Она включает нативный код, ресурсы и даже разбивку на уровне пакета скомпилированного кода Dart.


Пример разбивки релизного APK Flutter Gallery

Эта сводка полезна, чтобы быстро проанализировать размер сборки и понять, из-за чего она слишком большая. Кроме того, собранные данные также доступны в виде файла JSON, который можно посмотреть в Dart DevTools, что позволяет дополнительно исследовать содержимое приложения, выявлять проблемы с размером и видеть изменения между двумя разными файлами JSON, следуя инструкциям на flutter.dev. После загрузки файла JSON у вас будет интерфейс, в котором отображается древовидная карта размера вашего приложения.


Пример разбивки APK в Dart DevTools

Документации по инструменту анализа размера приложения на сайте flutter.dev

Комментарий

То, что давно умеет Android Studio сама по себе, теперь можно анализировать и через Dev Tools. Реализовано это даже более гибко. Теперь вы можете деплоить отчёты в Json прям на CI и мониторить изменения размера сборки в динамике.


Предварительная версия: обновленная страница Network в DevTools


Еще одна предварительная функция DevTools в этом выпуске: теперь во вкладке Network отображается body запроса.



Чтобы включить эту функцию, убедитесь, что вы находитесь на dev-канале Flutter через flutter channel dev и flutter channel upgrade.

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



Документацию по вкладке Network ищите в разделе Использование Network View на flutter.dev.

Вкладка Hosted DevTools Inspector в IntelliJ


Уже некоторое время мы поддерживаем две копии некоторых наших инструментов Flutter: например, панель Inspector в IntelliJ и вкладку Inspector в Dart DevTools. Это замедляет нашу работу: нам нужно поддерживать две кодовые базы. Кроме того, некоторые функции еще не вошли в плагин IntelliJ например, Layout Explorer. Чтобы решить эти проблемы, мы включили возможность размещения вкладки Inspector из Dart DevTools непосредственно внутри IntelliJ.



Чтобы включить эту опцию, перейдите в Preferences > Languages & Frameworks > Flutter > Enable embedded DevTools inspector.

Комментарий

Ещё одно маленькое, но очень важное изменение. В одном из выпусков Flutter Dev Podcast мы даже посвятили несколько минут обсуждению разницы в инструментах разработчика в Android Studio и VS Code.

Как оказалось, это действительно два разных проекта с независимыми кодовыми базами и это странно. Со временем каждая из этих разработок обрастала своими фичами, всё больше и больше отличаясь от своего аналога для друго IDE. Теперь этому будет положен конец. Предсказываю, что встроенные инструменты скоро исчезнут из плагина для Android Studio. А пока привыкайте к новой интеграции.


Улучшенный вывод логов в Visual Studio Code


Обычное действие, с которым сталкиваются все разработчики Flutter, переход из стектрейсов ошибок в терминале к месту в исходном коде, где произошла ошибка. В самом последнем выпуске расширения Flutter для Visual Studio Code эти ссылки теперь парсятся правильно: по ним можно кликнуть и перейти в нужное место кода, в котором произошло падение.



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

Как всегда, здесь слишком много списка изменений инструментов, но я рекомендую изучить следующие анонсы:


В центре внимания клиентов: EasyA


EasyA это приложение по подписке: школьники занимаются с блестящим репетиторам через обмен мгновенными сообщениями. Приложение написано на Flutter. Недавно Apple отметила его как приложение дня.



Когда в начале этого года школы стали переходить на дистанционку, у нас была задача быстро запустить приложение, чтобы помочь ученикам. Благодаря высокой скорости разработки на Flutter мы смогли сделать приложение как для iOS, так и для Android. И успели их опубликовать в сторах как раз к началу локдауна! В обычных условиях это было бы практически невозможно. Но поскольку Flutter позволяет нам работать со всеми тремя платформами одновременно, мы смогли эффективно переиспользовать код. Наша небольшая команда разработчиков работала на полную.

Фил Квок, соучредитель EasyA


Критические изменения


Как всегда, мы стараемся свести количество критических изменений к минимуму. Вот список из версии Flutter 1.22:


Резюме


Стабильная версия Flutter 1.22, возможно, вышла очень быстро после релиза 1.20, но в ней так много хорошего, что здесь мы не смогли упомянуть всё. Надеемся, что этот релиз поможет вам создавать потрясающие приложения для iOS и Android, и нам не терпится увидеть, что появится в сторах! Спасибо за вашу поддержку: мы создаём Flutter для вас.
Подробнее..

Курс Flutter от Surf успеть за технологиями будущего

29.10.2020 16:15:50 | Автор: admin
Нет времени ждать, пока все поймут, что за Flutter будущее, и начнут где-то учиться. Надеяться, что кто-то сделает крутые курсы, и после них мы сможем находить разработчиков не наш путь. Поэтому мы в Surf решили взять процесс в свои руки и сделать курс по Flutter.




Мы в Surf одни из первых в России начали использовать для разработки мобильных приложений кроссплатформенную технологию Flutter. Flutter очень быстро развивается по данным StackOverflow эта технология уже занимает девятое место по популярности, хотя ей всего два года. Разработка на Flutter быстрее и дешевле, а приложения уже почти не отличаются от нативных. Спрос от клиентов растт, а количество разработчиков на Flutter в России всё ещё ничтожно мало.

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

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

В статье мы подробно рассказываем про курс, а вот краткая выжимка:
  • Flutter это полезно и открывает двери в новую технологию.
  • Основы можно изучить за 4 месяца или быстрее. Курс состоит из 9 блоков, 59 уроков и мастер-классов по сложным темам. Для 48 уроков мы записали качественные лекции примерно по часу каждая.
  • Мы сделали акцент на практике, потому что мы разработчики и знаем, что нужно, чтобы быстро изучить новую платформу.

Почему Flutter


Flutter всего два года, а на него не боятся переходить крупные компании. Мы в Surf уже сделали большие проекты. Например, приложения для Росбанка, сети аптек Ригла и KFC. Flutter активно развивает создатель Google и процесс не замедлила даже пандемия коронавируса. Количество вакансий на бирже фрилансеров Upwork тоже растт: спрос удвоился за полгода. На HeadHunter спрос почти утроился по сравнению с прошлым годом, но кажется, что вакансий пока мало: они часто скрыты среди предложений для нативщиков вроде ищем разработчика для iOS/Android с опытом работы на Flutter.

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

Какие знания пригодятся для прохождения курса


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

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

В ходе курса мы будем пользоваться инструментами, которые входят в привычный набор любого современного разработчика. Большое подспорье умение работать в IDE на базе IntelliJ IDEA или VS Code, системе контроля версий Git. Не помешает и понимание принципов написания чистого кода, SOLID.

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

Кто преподаватели


Наши преподаватели практики, а не теоретики. Это люди, которые работают в Surf и каждый день разрабатывают приложения на Flutter. Они сами учились с нуля и знают подводные камни и сложности, с которыми можно столкнуться. Мы вложили в курс весь практический опыт Surf, а также свои боли и проблемы, которые мы уже преодолели. Помогать осваивать Flutter будут:

Евгений Сатуров тимлид разработчиков в Surf и технический евангелист Flutter. Основатель и ведущий подкаста Flutter Dev Podcast.

Артм Зайцев руководитель Flutter-отдела в Surf. Ведт подкаст вместе с Евгением Сатуровым.

Михаил Зотьев ведущий Flutter-разработчик в Surf. Пишет статьи и делится наработками с сообществом Flutter.


Бэкстейдж со съемок видеоуроков: в кадре Михаил Зотьев, за кадром Артём Зайцев

Как, зачем и откуда ребята пришли во Flutter, они рассказали на вебинаре Flutter vs технология, на которой пишете вы: за чем будущее?

Как проходит обучение


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


Темы уроков из блока про язык Dart

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

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


Вариант домашнего задания

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

Сколько времени занимает учба


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

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

Мы рассчитываем, что на изучение курса у студентов будет уходить в среднем 35 часов в неделю. Так весь курс можно будет закончить за 4 месяца. Всего в курсе 48 уроков и 57 домашних заданий. Плюс вебинары и мастер-классы обсуждение теории и вопросов студентов в режиме онлайн. Вебинары будут проходить вечером в пятницу, чтобы рабочие вопросы не мешали учбе.

Что на выходе


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

Помимо новых знаний и навыков по окончанию курса у вас будут:
  • электронный сертификат;
  • безлимитный доступ ко всем материалам курса и обновлениям по нему;
  • нетворк: чат с преподавателями и другими студентами.

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

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

Сейчас курс доступен по спецпредложению. Узнать цену, прочитать подробности и записаться можно на нашем сайте. А посмотреть тизер к курсам на Youtube.
Подробнее..

Что вам даст учеба у практиков, или почему наш курс по Flutter это про реальные проекты и навыки для работы

19.02.2021 18:05:42 | Автор: admin

Вот уже третий год мы разрабатываем на Flutter. Сделали на нём кроссплатформенные приложения для Росбанка, сети аптек Ригла, ресторанов KFC, в разработке ещё много проектов. Буквально на наших глазах Flutter из нишевой технологии стал мощным игроком, который теснит не только React Native, но и нативную разработку.

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

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

Команда flutter в Surf собрала серьёзную базу знаний:

  • опыт реальных проектов;

  • наши статьи о Flutter, его особенностях и проектах на нем;

  • методология обучения стажёров;

  • open source библиотеки и наработки, которые мы выкладываем в публичном репозитории на Github.

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

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


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

Подробно о курсе можно почитать в статье.


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

И за это, ребята, большое вам спасибо!

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

Но давайте обо всем по порядку.

Можно ли изучить новую технологию самому, или почему практика важна?

Предположим, есть условный Егор. Он разработчик. Видит будущее за мобильными приложениями и хочет освоить новый стек. Ага, Flutter удобный и современный фреймворк для разработки приложений как под IOs, так и под Android, надо попробовать, - думает Егор.

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

И зачем тогда Егору платный курс? Тут все давно сказано за нас. Вспомним правило 10 000 часов, которое сформулировал психолог Андерс Эриксон чтобы добиться высокого уровня мастерства в своём деле, нужно посвятить практике не менее 10 000 часов. А ещё есть модель 70:20:10 Чарльза Дженнингса, в которой говорится о 70% практики, 20% - работы с наставником и 10% теории, которые необходимы для успешного освоения материала.

Документация и бесплатные курсы дадут Егору те самые 10%. Но одной теории мало для освоения технологии. Рынку нужны опытные разработчики, а не теоретики. И тут перед Егором встает резонный вопрос, как и где получить этот опыт. Отработать на практике под руководством ментора один из наиболее продуктивных вариантов. Именно такой формат мы предлагаем в своем курсе по Flutter.

Егор получит знания из первых рук. И это будут не просто лекции, а экспертиза и лучшие практики разработки из реальных проектов Surf на Flutter, которые дают опытные наставники. Обратная связь, код-ревью, рекомендации по конкретному проекту, как сделать лучше и исправить ошибку этого Егор не найдёт в документации. Но это точно пригодится ему в дальнейших проектах.

Вот что пишут студенты первого потока:

Основная ценность курса взаимодействие с наставниками. Тут дело не в сухой информации, которая и так есть в прекрасной документации flutter.

Этот курс отличная возможность глубоко разобраться в теме и понять, как устроена профессиональная разработка на flutter.

Домашние задания сдаются пулл-реквестами. Проверяют их по-взрослому от соответствия макету в figma до стиля кода. Смотрят код внимательно, замечают разные сомнительные архитектурные решения, проблемы с производительностью, подсказывают, как сделать лучше. Причем проверяют разные специалисты из команды surf.

Кирилл

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

Влас

Программа и для джуна, и для тимлида а так бывает?

На первый поток курса пришли ребята с очень разным уровнем подготовки в сфере разработки. Это были:

  • начинающие разработчики;

  • тимлиды;

  • senior-разработчики крупных команд;

  • архитектор Frontend-разработки крупного банка.

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

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

Решить эту проблему нам помогли 2 механизма. Первый индивидуальный выбор уровня нагрузки. Студент может проходить курс с любой скоростью, делать объём, который ему комфортно по уровню знаний или ориентируясь на уровень занятости на работе.

А еще студенты отметили, что гораздо удобней, когда загрузка на курсе равномерная, практические задания примерно одного объема и уровня сложности. Поэтому по обратной связи от ребят за 3 месяца мы переработали более 20% курса. Простые задания дополнили, а слишком сложные переформулировали или разделили на части.

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

Второй механизм это много разных мастер-классов с элементами практики по темам, которые были актуальны ребятам. После первого мастер-класса по Figma и Swagger студенты предложили переделать программу мастер-классов так, чтобы каждый смог подобрать темы, которые актуальны для его уровня подготовки.

В итоге, больше половины тем мы переработали. И теперь их список выглядит так:

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

  • Тестирование Flutter приложений (unit-тестирование, автотесты)

  • Взаимодействие с нативным приложением

  • Обзор возможностей Flutter Web (чем отличается от нативных приложений, JS/HTML под капотом, безопасность веб-приложений, какие есть возможности и производительность, как работать с поисковой оптимизацией и индексацией, как подготовить к использованию в e-commerce)

  • Основы языка Kotlin

  • Основы языка Swift

  • Обзор возможностей Navigator 2.0

  • Обзор возможностей Flutter Desktop

  • Взаимодействие с платформой (Advanced)

  • DevTools Profiling (Advanced)

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

Спикеры мастер-классов это опытные практики, эксперты в области разработки. Например, одним из выступающих стал Михаил Зотьев, который рассказал про внутреннее устройство и архитектурные особенности Flutter. А на мастер-классе по Flutter Web студенты разбирали и задавали вопросы про тонкости и ограничения применения Flutter в вебе, которые, в принципе, известны мало кому в индустрии.

Главный вопрос

Обычно со стороны студентов он звучит так а с трудоустройством поможете?

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

Наполните портфолио

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

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

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

Примеры экранов из практического кейса курса Flutter от Surf мобильное приложение со списком интересных мест и достопримечательностей для путешествий по мируПримеры экранов из практического кейса курса Flutter от Surf мобильное приложение со списком интересных мест и достопримечательностей для путешествий по миру

Разработку приложения вы ведёте в собственном репозитории. По мере продвижения по курсу проводите итерационный рефакторинг кода. Преподаватель делает ревью кода, контролирует, как вы используете EffectiveDart и best practice.

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

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

Виктор

Второе направление карьерные консультации

Вы составите резюме под руководством опытного HR, который специализируется на поиске и найме разработчиков, знает, на что обращают внимание работодатели. Получите рекомендации по поиску работы и консультацию от HR.

Лучшие студенты курса могут пойти на оплачиваемую стажировку в Surf. При е успешном прохождении вы сможете стать частью Surf Flutter team.

На языке цифр

По данным, собранным Кейт Джордан, исследовательницей в сфере образования и технологий, в среднем массовые открытые онлайн-курсы (MOOC) завершают около 15% поступивших.

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


Завершим отзывом студентки:

Про флаттер я узнала случайно, просматривая статьи Хабра и vc. Заинтересовалась, начала искать другие материалы и видео. Данная технология мне показалась очень привлекательной, поэтому следующим шагом была покупка курсов по Flutter и Dart на udemy.

Как только стартовал набор на курс у Surf, записалась сразу не раздумывая, так как компания является экспертом в этой области.

На мой взгляд, курс рассчитан не для новичков в программировании. Хорошо, что у меня есть небольшой бэкграунд + прошла предварительно купленные курсы. Отличием курса Surf от других является то, что у тебя постоянно работает мозг, как решить задачу, и никто, кроме тебя, её не решит и не покажут, как. Могут дать наводку, в какую сторону смотреть.

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

Курс для тех, кто не ленится и не опускает руки, когда что-то не получается. А опытные наставники поправят и направят в нужном направлении.

Татьяна


5 марта стартует новый поток курса по Flutter от команды Surf. Если хотите присоединиться и освоить разработку на Flutter на практике.

Регистрируйтесь на сайте

Подробнее..

За что App Store может отклонить приложение чек-лист

18.06.2021 14:19:30 | Автор: admin

App Store самая строгая площадка для размещения приложений. Ревью проходит дольше и строже, чем у Google Play и Huawei App Gallery. В 2020 году AppStore отклонил миллион приложений, которые публиковались впервые, и миллион апдейтов.

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

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

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

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

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

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

Нарушение функциональных требований

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

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

У нас в Surf, например, однажды отклонили приложение, потому что в нём была подключена библиотека для Apple Pay, но она нигде не использовалась.

UI, не соответствующий Human Interface Guideline. Сложное для использования приложение, имеющее нелогичное поведение и расположение элементов, отклонят.

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

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

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

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

Для распознавания по FaceID используются сторонние технологии. Идентификация пользователя по FaceID должна происходить только с помощью библиотеки LocalAuthentication.

Нет входа по AppleID, если есть возможность входа через соцсети. Это обязательно для iOS 13 и новее.

Нарушения в оформлении приложения

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

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

Есть слова Beta, Demo или Debug в названии приложения. Такие приложения запрещены к публикации в магазине App Store. Для бета-версий есть Test Flight.

Нет описания новой функциональности у обновлённого приложения. Если в приложении появилась новая функциональность, необходимо описать её в поле в App Store Connect. Без чёткого описания приложение ревью не пройдёт.

Скриншоты приложения, иконка и другой контент на странице магазина не подходят для аудитории 4+. И не важно, что приложение может быть не предназначено для такого возраста: аудитория App Store дети от четырёх лет.

Контент не соответствует возрастной метке. При публикации нужно пройти опрос, чтобы установить возрастную метку приложения. Если контент ей не соответствует, приложение или обновление отклонят при публикации.

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

Файл с расширением .ipa превышает размер 50 мб в момент публикации.

Нет демо-пользователя для ревью, если приложение требует обязательной авторизации.

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

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

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

В период пандемии этот пункт стал особенно важен: Apple строго следит за распространением информации, связанной с COVID-19.

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

Контент оскорбляет людей разных рас, вероисповеданий, сексуальной ориентации, национальности.

Приложение поощряет незаконное использование оружия или позволяет его купить.

Есть откровенно сексуальный или порнографический контент.

Приложение разрешает анонимную отправку смс/ммс, анонимные звонки, розыгрыши.

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

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

Покупки в приложении

Цифровой контент продаётся не через in-app purchase.К цифровому контенту относятся подписки, музыка в приложении, видео, расширенный доступ к функциям.

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

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

Приложение не предоставляет пользователю всю необходимую информацию о покупке до момента продажи. Это важно, если приложение использует Apple Pay. Также недопустима кастомизация окна оплаты.

Категория Kids

Kids в App Store отдельный вид приложений. К ним Apple относится максимально строго. Категория Kids делится на три подкатегории: до 5 лет, 68 лет, 911 лет.

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

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

Есть сторонняя аналитика и реклама. Имеется в виду любая система аналитики, передающая информацию на другой сервер. Разрешены лишь те службы, которые не передают никакую идентифицирующую информацию о детях: местоположение, имя, дату рождения, адрес имейла.

Составили гугл-док с чек-листом требований для ревью App Store.

А по каким причинам App Store отклонял ваши приложения? Расскажите в комментариях.

Подробнее..

Тестирование Flutter-приложений инструменты, преимущества, проблемы

03.09.2020 12:19:11 | Автор: admin
Привет! Меня зовут Мария Лещинская, я QA-специалист в Surf. Наша компания разрабатывает нативные приложения с 2011 года, а с 2018-го мы занимаемся ещё и разработкой под Flutter.

В этом материале сравним возможности тестирования нативных и кроссплатформенных приложений. Я поделюсь впечатлениями от работы с Flutter и расскажу, какие инструменты мы в Surf используем при тестировании, чем Flutter удобен и с какими проблемами мы столкнулись.



Возможности тестирования во Flutter не уступают нативным


Когда в компании меняется подход к разработке или появляется новая технология, важно, чтобы не сильно страдали возможности тестирования. Идеально, если при работе с новым языком или фреймворком QA-специалисты продолжают пользоваться привычным стэком инструментов и технологий, зарекомендовавших себя наилучшим образом.

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

Нам было важно, чтобы привычные возможности оставались и при тестировании Flutter-приложений.

Автотестирование

При автотестировании нативных приложений Surf работает с фреймворком Calabash и Ruby. Когда появился Flutter, нам первым делом стало интересно: можно ли не использовать Calabash, но при этом в полной мере работать с автотестами так, как мы привыкли, или даже круче.

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

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

Flutter также поддерживает Behavior Driven Development BDD подход. Мы используем его для UI-тестов. В качестве языка выбрали Gherkin: в нём можно использовать feature-файлы, писать сценарии на английском и русском языках. Он понятный, в нём есть реализация шагов сценариев без дополнительных аргументов внутри или с ними, возможность кастомизации запуска автотестов: например, прогон некоторых сценариев по тегам, а не всех написанных тестов целиком.

Чтобы использовать Gherkin при тестировании Flutter-приложений, подключили opensource-фреймворк flutter_gherkin.

Когда мы поняли, что автотесты на Flutter есть, нам стало интересно, какие отличия между технологиями Calabash и Dart+Gherkin, какой подход лучше. Давайте вместе сравним их.

1. Feature-файлы абсолютно идентичны при обоих подходах.

Например, сценарий авторизации по пин-коду будет корректно восприниматься и на Dart, и на Ruby с использованием Calabash:
Сценарий: Корректная авторизация в приложении с первого раза (короткий код)Когда Я запускаю приложениеИ Я вижу экран ввода короткого кодаТогда Я ввожу корректный короткий кодИ Я вижу главный экран

Обе технологии поддерживают русский, английский и другие языки.

2. Steps отличаются в реализации.
Dart + flutter_gherkin
Calabash
class TapAnErrorButtonOnPinCodeScreen extends ThenWithWorld<FlutterWorld> {@overrideFuture<void> executeStep() async {final elemFromReportAnErrorScreen = find.byValueKey('reportAnErrorButton');await FlutterDriverUtils.tap(world.driver, elemFromReportAnErrorScreen);}@overrideRegExp get pattern => RegExp(r"Я нажимаю Сообщить об ошибке на Экране пин-кода");}

When(/^Я нажимаю Сообщить об ошибке на Экране пин-кода$/) dowait_element("* id:'reportAnErrorButton'")tap_on("* id:'reportAnErrorButton'")end

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

3. Для конфигурации тестов и работы с ними Flutter использует дополнительный файл .dart. При работе с Calabash такого единственного файла не существует.

Сказать, что это недостаток внутри Flutter или Calabash, на наш взгляд, нельзя это всего лишь специфика работы с конкретными инструментами и технологиями.

4. Для удобства работы с элементами в приложении необходимо, чтобы у каждого элемента был свой id. При работе с автотестами с Calabash нужно заранее позаботиться о том, чтобы на обеих платформах в тестируемых приложениях были id. На Dart можно добавлять id в процессе написания автотестов, не перезаливая отдельно файлы iOS и Android приложений это удобно и экономит время.

Наш вывод: автотесты на Dart совсем не уступают автотестированию с помощью фреймворка Calabash.

Прокси

Для повышения покрытия приложения кейсами в Surf используют программы для чтения и подмены трафика например, Charles. Анализ клиент-серверного взаимодействия позволяет:

  1. Определять, есть ли реальное взаимодействие с бэкэндом.
  2. Находить, на чьей стороне проблема: на клиенте или на сервере.
  3. Упрощать и ускорять проверки на готовых тестовых данных, не привлекая разработчиков сервера.
  4. Анализировать поведение мобильного приложения в различных сетевых условиях: отвала запросов, задержки, больших данных. Charles позволяет определить неверно сформированные запросы от клиента к серверу и избежать зацикленности при обращении к серверу, и, как следствие, нагревания устройства и быстрого разряда.

У Dart VM свой клиент для работы с сетью. Так как все запросы идут через него, то необходимые настройки для работы с прокси нужно вводить внутри приложения. Для удобства тестировщиков все нужные настройки вынесены на отдельный экран: мы в Surf используем Debug Screen, который разработали сами.

Debug Screen это дополнительный экран настроек, который доступен только из debug-сборки и помогает в тестировании. В нём можно выбрать необходимый сервер, включить использование чтения и сохранения http-запросов в приложении, просмотреть fcm-токен устройства и не только возможностей для тестирования предусмотрено много.

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

Как видим, возможности при тестировании Flutter-приложений не ограничены. Всё, с чем мы привыкли работать при нативе, есть и удобно в использовании. Это не может не радовать.

Проблемы: баги фреймворка, недоработки в сторонних библиотеках, ожидаемое нативное поведение


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

Расскажем, на что обращать внимание, тестируя Flutter-приложения. Предупреждён значит вооружён.

Баги Flutter-фреймворка

При тестировании столкнулись с проблемой отображения и форматирования шрифтов на iOS: межбуквенный интервал на iOS-платформе был заметно шире, чем на Android. Это вызывало большое количество визуальных багов.

Выяснилось, что проблема в самом фреймворке. Когда наши разработчики мобильных приложений обратились к ребятам из сообщества разработчиков Flutter-фреймворка с просьбой пофиксить такой неприятный баг, вскоре вышло обновление фреймворка и отображение текста на iOS было исправлено.

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

Недоработки в сторонних библиотеках

На iOS версий 10 и 11 встречались недоработки реализации в библиотеках сторонних разработчиков. Например, мы исправляли баг, когда пермишен на доступ к уведомлениям всплывал сразу при запуске приложения, а не по кнопке, как планировалось по ТЗ и дизайну.

Такие проблемы могут встретиться и в кроссплатформе, и в нативе. Решаются фиксами внутри проекта, либо совместно с разработчиками библиотек.

Работа с ожидаемым нативным поведением

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

Системные диалоги на обеих платформах отличаются: в iOS необходимо запросить пермишен на доступ к уведомлениям, а на Android такой доступ дан по умолчанию.

Именно такие части специфики ОС зачастую приходится допиливать вручную. А иногда выпиливать, если вдруг ожидаемое поведение для iOS платформы перекочевало в Android, как было с бэксвайпом.

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

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

Еще один баг встретился нам на Android версии 6: при сворачивании приложение полностью выгружалось из памяти.

Такие баги были пофикшены нашими разработчиками внутри проекта.


iOS прекрасно знают свои устройства и системы, какие фишки они выпускают с новой версией ОС и какие теперь не будут работать в предыдущих версиях, на что делать акцент при обновлении того же Swift. Android осознают, что должны ориентироваться на большое количество устройств и абсолютно разные размеры экранов, и тоже знают свою специфику.

Хочется чтобы кроссплатформа содержала все тонкости реализации от нативной разработки. Конечно, недочёты при работе с Flutter ощущаются, но это не проблема: просто к кроссплатформе нужен свой подход.

Преимущества: единая кодовая база, одна команда разработки


Единая кодовая база сокращает время тестирования

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

Искать баги и проверять фичи можно на одной платформе: с высокой долей вероятности они повторяются на обеих платформах ведь реализация одна.

Логика новых фич на обеих платформах тоже одинакова, так как написан один код: тестирование сложных процессов внутри приложения сводится к тестированию их на одной платформе и подтверждению на другой. Мы проводим полный цикл активностей на одной платформе: делаем исследовательское тестирование, прогоны по фичам, смоук/санити/фулл, анализируем обратную связь. После этого остаётся только подтвердить качество исследовательским тестированием на другой платформе. Такой подход экономит время тестирования логики примерно в 1,3 раза.
Пример
Предположим, что аналитики, тестируя события, заметили ситуацию: по ТЗ событие должно отправиться в соответствующую систему аналитики, но оно не трекается. Такой пробел в логике однозначно говорит о том, что событие не будет отправляться на обеих платформах.

Исправив баги и убедившись на различных сценариях, что сбор аналитики в приложении работает корректно на одной из платформ (например, iOS), с большей уверенностью можем считать, что не встретим баг на другой платформе (Android), связанный с треком лишнего события или отсутствия нужного event в системе аналитики.


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

Мы попробовали примерно оценить тестирование двух похожих проектов один на нативных Android и iOS, второй на Flutter и сравнили их пофично.

Анализ и тестирование проводились на одном устройстве iOS и на одном устройстве Android-платформы. Как видно на практике, Flutter действительно даёт выигрыш во времени, хоть и не в два раза. Это объяснимо: полностью убрать тестирование на одной из двух платформ нельзя. Как ни крути, у них разная специфика и ориентированность на пользователя.

Native iOS
Native Android
Flutter Android+iOS
Восстановление пароля
2h
2h
3h 20m
Авторизация
1h 30m
1h 30m
2h 20m
Пуш-уведомления
2h
2h
4h

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

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

Организовать коммуникацию внутри команды проще

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

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

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

Нам нравится тестировать Flutter-приложения


Быть частью крутого сообщества

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

При этом разработчики из коммьюнити Flutter-фреймворка быстро дают обратную связь по выявленным проблемам, улучшают библиотеку и позволяют нам контрибьютить в проект: мы идём вперёд вместе, и это приятно.

Быть специалистом

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

При разработке и тестировании нативных приложений невозможно собрать iOS приложение из, например, Android Studio или Visual Studio Code. При работе с Flutter IDE едина и для Android, и для iOS. Это круто.

Быть самостоятельным

При работе с Flutter мы в Surf сталкиваемся с очень разными проектами: от e-commerce до банкинга. Практика показала, что QA-инженер может в одиночку справляться с тестированием обеих платформ. Подключать ещё одного специалиста необходимо разве что ближе к релизу, когда темп работы увеличивается, а времени на выполнение задач остаётся всё меньше.

Flutter шаг вперёд


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

Опыт разработки и тестирования Flutter-приложений показал Surf, что этот фреймворк большой шаг вперёд.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru