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

Мобильное приложение

Автоматизация тестирования продуктовой аналитики в мобильных приложениях

12.10.2020 12:10:07 | Автор: admin

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

Вы когда-нибудь выпускали релиз, в котором случайно удалили код отправляющий некоторые важные события аналитики? Или забывали покрыть событиями новую фичу? А сколько времени ваши аналитики или тестировщики тратят на ручное тестирование перед каждым релизом? А если это приложение с тысячей событий?

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

Тестирование аналитики вручную

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

  1. Можно отправить локальное уведомление (типа Push) с названием и параметрами события. Это неудобно, так как перекрывает интерфейс приложения, а также сложно тестировать цепочку событий из-за того, что каждое новое уведомление перекрывает старые.

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

  3. Либо события аналитики можно логировать и сразу отслеживать в консоли.

События аналитики в Console.appСобытия аналитики в Console.app

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

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

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

Тестирование аналитики UI-тестами

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

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

На практике есть два способа передачи данных из приложения в UI-тесты:

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

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

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

Так в итоге будет выглядеть UI-тест, проверяющий события аналитики на экране авторизации:

func testLoginSuccess() {    // Запустить приложение    launchApp()        // Проверить что отправилось событие показа экрана авторизации    analytics.assertContains(name: "open_login_screen")        // Успешно залогиниться    loginScreen.login(success: true)        // Проверить что отправилось событие успешной авторизации    analytics.assertContains("authorization", ["success": true])}

Доработки со стороны приложения

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

Базовые сущности

Представим событие аналитики в виде следующей структуры:

public struct MetricEvent: Equatable {     public let name: String        public let values: [String: AnyHashable]?     public init(name: String, values: [String: AnyHashable]? = nil) {        self.name = name        self.values = values    }}

Структура MetricEvent будет использоваться и в коде приложения, и в коде UI-тестов. Поэтому вынесем её в отдельный модуль MetricExampleCore. Для этого нужно создать новый Target типа Framework.

Событие что-то должно отправлять, поэтому объявим соответствующий протокол:

import MetricExampleCore /// Сервис отправки событий в аналитикуpublic protocol MetricService {        func send(event: MetricEvent)    }

В первой строчке импортируем модуль, в котором объявили структуру MetricEvent.

Сервисы отправки событий

Этому протоколу будут соответствовать классы, отправляющие события куда-либо. К примеру, класс для отправки событий в AppMetrica:

import Foundationimport MetricExampleCoreimport YandexMobileMetrica open class AppMetricaService: MetricService {     public init(configuration: YMMYandexMetricaConfiguration) {        YMMYandexMetrica.activate(with: configuration)    }     open func send(event: MetricEvent) {        YMMYandexMetrica.reportEvent(event.name, parameters: event.values, onFailure: nil)    }}

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

import Foundationimport MetricExampleCoreimport UIKit final class MetricServiceForUITests: MetricService {     // Массив всех отправленных событий аналитики    private var metricEvents: [MetricEvent] = []     func send(event: MetricEvent) {        guard ProcessInfo.processInfo.isUITesting,              ProcessInfo.processInfo.sendMetricsToPasteboard else {            return        }                if UIPasteboard.general.string == nil ||           UIPasteboard.general.string?.isEmpty == true {            metricEvents = []        }         metricEvents.append(event)         if let metricsString = try? encodeMetricEvents(metricEvents) {            UIPasteboard.general.string = metricsString        }    }     private func encodeMetricEvents(_ events: [MetricEvent]) throws -> String {        let arrayOfEvents: [NSDictionary] = events.map { $0.asJSONObject }        let data = try JSONSerialization.data(withJSONObject: arrayOfEvents)        return String(decoding: data, as: UTF8.self)    }}

В методе send можно проверить, что приложение запущено в режиме UI-тестирования и разрешена отправка событий в буфер обмена. Затем в массив всех отправленных событий добавляется новое.

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

// MetricEvent.swift...    /// Представляет событие в виде словаря для передачи в JSONSerialization.data(withJSONObject:)    public var asJSONObject: NSDictionary {        return [            "name": name,            "values": values ?? [:]        ]    }...

Каждый UIViewController, который будет отправлять события, получит в инициализатор зависимость MetricService.

final class LoginViewController: UIViewController {        private let metricService: MetricService        init(metricService: MetricService = ServiceLayer.shared.metricService) {        self.metricService = metricService        super.init(nibName: nil, bundle: nil)    }    ...

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

import Foundationimport YandexMobileMetrica final class ServiceLayer {        static let shared = ServiceLayer()        private(set) lazy var metricService: MetricService = {        if ProcessInfo.processInfo.isUITesting {            return MetricServiceForUITests()        } else {            let config = YMMYandexMetricaConfiguration(apiKey: "APP_METRICA_API_KEY")            return AppMetricaService(configuration: config)        }    }()}

Если приложение запущено в режиме UI-тестирования, то для отправки событий используется MetricServiceForUITests. В ином случае AppMetricaService.

Отправка событий

Осталось объявить все события, которые будут отправляться. Для этого нужно написать расширение MetricEvent:

import Foundationimport MetricExampleCore extension MetricEvent {        /// Пользователь перешел на экран авторизации    static var openLogin: MetricEvent {        MetricEvent(name: "open_login_screen")    }     /// Пользователь ввел логин и пароль и инициировал авторизацию.    ///    /// - Parameter success: Успешность запроса.    /// - Returns: Событие метрики.    static func authorization(success: Bool) -> MetricEvent {        MetricEvent(            name: "authorization",            values: ["success": success]        )    }}

Теперь события можно отправлять:

metricService.send(event: .openLogin)metricService.send(event: .authorization(success: true))metricService.send(event: .authorization(success: false))

Аргументы запуска

Я уже упоминал такие вещи, как:

ProcessInfo.processInfo.isUITestingProcessInfo.processInfo.sendMetricsToPasteboard

При запуске UI-тестов на аналитику будут передаваться два аргумента: --UI-TESTING и --SEND-METRICS-TO-PASTEBOARD. Первый показывает, что приложение запущено в режиме UI-тестирования. Второй что приложению разрешено отправлять события аналитики в буфер обмена. Чтобы получить доступ к этим аргументам, нужно написать расширение для ProcessInfo:

import Foundation extension ProcessInfo {    var isUITesting: Bool { arguments.contains("--UI-TESTING") }    var sendMetricsToPasteboard: Bool { arguments.contains("--SEND-METRICS-TO-PASTEBOARD") }}

Доработки со стороны UI-тестов

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

Получение списка отправленных событий

Чтобы получить текстовые данные из буфера, используем UIPasteboard.general.string. Затем строку нужно преобразовать в массив событий (MetricEvent). В методе decodeMetricEvents строка преобразуется в объект Data и десериализуется в массив с помощью JSONSerialization:

/// Возвращает список всех событий аналитики произошедших с момента запуска приложенияfunc extractAnalytics() -> [MetricEvent] {    let string = UIPasteboard.general.string!    if let events = try? decodeMetricEvents(from: string) {        return events    } else {        return []    }} /// Преобразует строку с массивом событий в массив объектов [MetricEvent]private func decodeMetricEvents(from string: String) throws -> [MetricEvent] {    guard !string.isEmpty else { return [] }    let data = Data(string.utf8)     guard let arrayOfEvents: [NSDictionary] = try JSONSerialization.jsonObject(with: data) as? [NSDictionary] else {        return []    }     return arrayOfEvents.compactMap { MetricEvent(from: $0) }}

Далее массив словарей преобразуется в массив MetricEvent. Для этого у MetricEvent нужно добавить инициализатор из словаря:

/// Пытается создать объект MetricEvent из словаряpublic init?(from dict: NSDictionary) {    guard let eventName = dict["name"] as? String else { return nil }    self = MetricEvent(        name: eventName,        values: dict["values"] as? [String: AnyHashable])}

Теперь можно получить массив событий [MetricEvent] и проанализировать его.

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

UIPasteboard.general.string = ""

Проверки списка событий

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

/// Проверяет наличие события с указанным именем/// - Parameters:///   - name: Название события///   - count: Количество событий с указанным именем. По умолчанию равно 1.func assertContains(    name: String,    count: Int = 1) {     let records = extractAnalytics()     XCTAssertEqual(        records.filter { $0.name == name }.count,        count,        "Событие с именем \(name) не найдено.")}

В итоге получился класс AnalyticsTestBase. Посмотреть его можно на GitHub AnalyticsTestBase.swift

Создадим класс, наследника XCTestCase, от которого будут наследоваться классы, тестирующие аналитику. Он создает класс AnalyticsTestBase для тестирования аналитики и метод launchApp, запускающий приложение.

import XCTestclass TestCaseBase: XCTestCase {        var app: XCUIApplication!    var analytics: AnalyticsTestBase!        override func setUp() {        super.setUp()                app = XCUIApplication()        analytics = AnalyticsTestBase(app: app)    }        /// Запускает приложение для UI-тестирования с указанными параметрами.    func launchApp(with parameters: AppLaunchParameters = AppLaunchParameters()) {        app.launchArguments = parameters.launchArguments        app.launch()    }}

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

struct AppLaunchParameters {        /// Отправлять аналитику в UIPasteboard    private let sendMetricsToPasteboard: Bool        init(sendMetricsToPasteboard: Bool = false) {        self.sendMetricsToPasteboard = sendMetricsToPasteboard    }        var launchArguments: [String] {        var arguments = ["--UI-TESTING"]        if sendMetricsToPasteboard {            arguments.append("--SEND-METRICS-TO-PASTEBOARD")        }        return arguments    }}

В обычных UI-тестах приложение будет запускаться с параметрами:

AppLaunchParameters(sendMetricsToPasteboard: false)

А в UI-тестах на аналитику:

AppLaunchParameters(sendMetricsToPasteboard: true)

Теперь можно писать тесты на аналитику. Например, это тест на экран входа:

final class LoginAnalyticsTests: TestCaseBase {        private let loginScreen = LoginScreen()        func testLoginSuccess() {        launchApp(with: AppLaunchParameters(sendMetricsToPasteboard: true))                // Проверить что отправилось событие показа экрана входа        analytics.assertContains(name: "open_login_screen")                // Успешно залогинится        loginScreen.login(success: true)                // Проверить что отправилось событие успешной авторизации        analytics.assertContains("authorization", ["success": true])    }}

LoginScreen это Page Object, описывающий экран авторизации. Посмотреть его можно на GitHub LoginScreen.swift

Примеры

Example проект

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

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

Тест, покрывающий все эти события:

import XCTest final class AnalyticsTests: TestCaseBase {        private let loginScreen = LoginScreen()    private let menuScreen = MenuScreen()        // MARK: - Login        func testLoginSuccess() {        launchApp(with: AppLaunchParameters(sendMetricsToPasteboard: true))                analytics.assertContains(name: "open_login_screen")        loginScreen.login(success: true)         analytics.assertContains("authorization", ["success": true])    }        func testLoginFailed() {        launchApp(with: AppLaunchParameters(sendMetricsToPasteboard: true))                analytics.assertContains(name: "open_login_screen")        loginScreen.login(success: false)        analytics.assertContains("authorization", ["success": false])    }        // MARK: - Menu        func testOpenMenu() {        launchApp(with: AppLaunchParameters(sendMetricsToPasteboard: true))         loginScreen.login(success: true)        waitForElement(menuScreen.title)        analytics.assertContains(name: "open_menu_screen")    }        func testMenuSelection() {        launchApp(with: AppLaunchParameters(sendMetricsToPasteboard: true))                loginScreen.login(success: true)        waitForElement(menuScreen.title)         menuScreen.profileCell.tap()                analytics.assertContains("menu_item_selected", ["name": "Профиль"])                menuScreen.messagesCell.tap()        analytics.assertContains("menu_item_selected", ["name": "Сообщения"])    }}

Реальный проект

Пример UI-тестов на аналитику экрана авторизации из реального проекта LoginAnalyticsTests.swift

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

Итоги

Плюсы подхода:

  1. Продуктовому аналитику или тестировщику не нужно проверять все события аналитики вручную. А это экономия времени и, соответственно, денег.

  2. Если у вас настроен CI, то UI-тесты на аналитику можно запускать по расписанию, например, раз в неделю или по команде из Slack.

Есть и минусы:

  1. UI-тесты выполняются относительно долго. Имеет смысл запускать их только в процессе регрессионного тестирования перед каждым релизом.

  2. UI-тесты на аналитику смогут написать только те тестировщики, которые имеют опыт написания нативных UI-тестов.

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

Поэтому в случае большого проекта есть смысл автоматизировать проверку событий аналитики.

Подробнее..

Как исправить баг с Drawable.setTint в API 21 Android SDK

09.11.2020 18:08:51 | Автор: admin

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

В 21 версии API Android SDK появился универсальный инструмент изменения цвета для всех Drawable - Drawable.setTint(int color). Но как раз-таки в этой самой версии он не работает у некоторых наследников Drawable, а именно GradientDrawable, InsetDrawable, RippleDrawable и всех наследников DrawableContainer.

Если посмотреть в исходники API 21, скажем, GradientDrawable (прямого наследника Drawable), мы не найдем переопределенного метода setTint и его вариаций. А это значит, что в данной реализации разработчики попросту не поддержали эту функцию.

Проблему условно решили в библиотеке обратной совместимости. Сейчас ее можно найти по артефакту androidx.core:core. Чтобы поддержать tinting на версиях 14-22, были созданы обертки WrappedDrawableApi14 и WrappedDrawableApi21. Последняя является наследницей первой и, по сути, не несет логики по поддержке окрашивания.

Чтобы обернуть оригинальный Drawable, нужно всего лишь подать его в метод DrawableCompat.wrap(Drawable). Основная идея состоит в том, что сам ColorStateList тинта хранится в обертках, а у оригинального Drawable изменяется цветовой фильтр при изменении состояния Drawable.

final ColorStateList tintList = mState.mTint;final PorterDuff.Mode tintMode = mState.mTintMode;if (tintList != null && tintMode != null) {   final int color = tintList.getColorForState(state, tintList.getDefaultColor());   if (!mColorFilterSet || color != mCurrentColor || tintMode != mCurrentMode) {       setColorFilter(color, tintMode);       mCurrentColor = color;       mCurrentMode = tintMode;       mColorFilterSet = true;       return true;   }} else {   mColorFilterSet = false;   clearColorFilter();}

Данный кусок кода будет вызываться каждый раз при вызове Drawable.setState(int[] stateSet).

При использовании этих оберток вы теряете возможность вызывать специфические методы для конкретных Drawable. Так, например, при оборачивании GradientDrawable вы не сможете управлять градиентом, так как обертка в своем интерфейсе не имеет методов таких, как setShape, setGradientType и.т.п. Чтобы получить доступ к данным методам, обернутый Drawable придется развернуть (DrawableCompat.unwrap(Drawable)). Но в таком случае вы теряете тинт. Если он у вас состоял только из одного цвета, ничего страшного, ведь этот цвет сохранится как цветовой фильтр в оригинальном Drawable. Но если тинт был stateful, цвета для стейтов, отличных от текущего, будут потеряны.

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

Если ваш тинт состоит лишь из одного цвета, вы можете в любой момент выполнить следующие действия:

val wrapped = DrawableCompat.wrap(drawable)wrapped.setTint(...)drawable = DrawableCompat.unwrap(wrapped)

После чего смело делать дальше свои дела.

В ином случае есть смысл воспользоваться следующим решением:

class GradientDrawableWrapper(    val original: GradientDrawable,     var ColorStateList tint) {    fun get(): Drawable {        return wrap()    }    fun setShape(@Shape shape: Int) {        original.setShape(shape)    }    // other specific method proxies...    private fun wrap(): Drawable {        val wrapped = DrawableCompat.wrap(original)        wrapped.setTint(tint)        return wrapped    }}

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

Подробнее..

Метод Любищева и учет времени по мотивам книги Даниила Гранина Эта странная жизнь

23.11.2020 12:04:01 | Автор: admin

Каждый из нас хотя бы раз в жизни задумывался о времени, его неумолимости и быстротечности.Вроде бы вчера ты окончил школу и поступил в университет, а уже сегодня тебе 36 лет, ты женат и у тебя двое детей, а по ощущениям тапропасть времени, которая прошла, слилась в один миг. Все очень стремительноНе так давно, в наше не простое время я был на онлайн-конференции слушателем и один из спикеров, известный предприниматель и основатель крупной ИТ-фирмы с множеством филиалов по всей России, говорил о своем личном и рабочем времени. Все как у всех:времени мало, то, что есть трудно распределить между важным и важным, плюс семья, одно накладывается на другое и в результате нет ни на что времени. Конференция была онлайн, докладчик был дома, выступая в череде таких же докладчиков, доклады которых слились в одно и слушались мною фоном.Но этот харизматичный человек как-то по-домашнему все рассказывал,даже показал нам свою собаку-пекинеса (чтобы вы понимали про атмосферу доклада). Столь необычная подача материала естественно приковала к себе мое внимание, и я начал внимательно его слушать. Речь была о книге Даниила Гранина "Эта странная жизнь" и методе Любищева, который практиковал автор доклада. Докладчик честно сказал, что эта вторая его попытка использовать этот метод в своей работе и личной жизни. На самом деле не понятно на сколько его хватит (намеренно не рассказываю о сути, т.к. позже постараюсь сделать это сам). А в конце доклада был совет прочесть эту книгу

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

Про книгу "Эта странная жизнь" Даниила Гранина

Мне стало интересно, и я ее прочел. Странное ощущение от ее чтения, скажу откровенно. Как будто сел ты на лавочку в парке, а на ней уже сидит пожилой человек (Гранин). Он начинает с тобой говорить и в какой-то момент ты уже сам не понимаешь, как так получилось, что ты очень внимательно его слушаешь и ловишь каждое слово. Это совсем не похоже на книжный рассказ, больше на настоящий монолог. А рассказывает он о своем знакомом - профессоре Любищеве Александре Александровиче, который занимался наукой и добился в своей научной работе огромных результатов. Причем не только в энтомологии (раздел зоологии, изучающий насекомых), основной своей специальности, но и куче других наук: философии, зоологии, генетике, истории науки, математике. Был достаточно уважаемым в абсолютно разных кругах ученным. Когда Гранин рассказывал про профессора, у меняскладывалось впечатление, что он очень сильно переживал. Причем в самом начале я не совсем понимал причину почему, столь умудренный годами старик так переживает свой рассказ, но чем больше он говорил, тем больше становилось понятно, что он проецировал жизнь своего товарища по науке на себя. Его серьезно что-то беспокоило Любищев был примером для Гранина и Гранин, глядя на то, как организовал свою научную и личную жизнь его знакомый, как мне показалось, корил себя. Корил за то, что в отличииот своего коллеги, он не знает сколько он читает книг, как с годами меняется его работоспособность, как много он проводит времени с семьей и т.д. Это был разбор и переосмысление подхода одного умного человека к работе и жизни как жил другой выдающийся человек. Любищев был олицетворением устремленности. Например, Любищев, будучи энтомологом классифицировал блошек и, в то время как он шел на работу, ловил этих блошек на улице. Его коллекция этих блошек была больше, чем в музеях того времени. При всем при этом он не тратил на это свое рабочее время! Фи Скажет читатель. "Так что в этом выдающегося?".Мужик собирал блох, по дороге на работу. Это не достижение

На самом деле это пример того, о чем мы бы и не подумали бы никогда. Человек не тратил на коллекционирование своего РАБОЧЕГО времени, но имел сумасшедшую коллекцию.

А как вам следующийпример? Любищеввыучил в дороге в "отбросах времени" английский язык. Скажи читатель: ты каждый рабочий день едешь в метро, ты выучил хоть что-нибудь? :) А ведь то время, которое ты едешь и ничем не занимаешь это же минус время из твоей жизни. А время, как известно, это самый ценный и не восстановимый ресурс. Вчера вернуть будет уже нельзя.

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

Про Любищева и его систему

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

  1. Насколько хорошо он отработал в этот период по сравнению с прошлым периодом?

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

  3. Сколькоон написал статей, рукописей, прочел книг, посетил выставок и т.д.?

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

Приведу очень хороший пример из нашей жизни.Навернякалюбой из вассталкивался с ситуацией, когда открываешь Ютубчик в обед в выходной день, начинаешь смотреть ролик, а потом еще, а потом еще Иглядь, через достаточно небольшой промежуток времени, а прошло уже часов 6 (!) При этом лично для вас время пролетело очень быстро. Любищев рассматривал время как что-то осязаемое и старался исключитьвот такие "быстрые" временные промежутки. Этого позволяет добиться как раз фиксирование времени, а фиксация порождает осознанность.

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

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

Про целесообразность

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

  1. А сколько времени ты потратил за последний год на свое обучение?

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

  3. И наоборот, сколько тратишь личного времени на рабочие вопросы?

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

Проблема в том, что ты этого не знаешь и вряд ли об этом даже задумывался Зачем вроде как это все тебе нужно? Я во всяком случае точно не задумывался.

А теперь возьмем твою личную жизнь.

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

  2. Есть хобби? Если оно есть, много посвятил ему времени за последний год?

  3. Как часто виделся со своими друзьями в этом году?

  4. Как известно, чтобы хорошо работать, необходим хороший отдых. А как ты отдыхал и сколько времени ушло на отдых?

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

И лично я, когда задавал себе эти вопросы - понял, что я ни черта не знаю о своем времени!

НИ-ЧЕ-ГО!

Про инструменты

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

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

Завел вот такую форму:
Форма Google FormsФорма Google Forms

В ней ввел категории (которые потом и оставил):

  1. Работа (все рабочие моменты)

  2. Простой (все что относится к отбросам времени - например, дорога на работу)

  3. Поддержка семьи (покупка в магазине, что-то домой)

  4. Семья (время, которое я провожу с женой и детьми)

  5. Еда (без комментариев)

  6. Сон (без комментариев)

  7. Обучение (курсы, чтение литературы и повышение квалификации)

  8. Спорт (тренажерка, занятия с тренером)

  9. Хобби (есть свой YouTube канал, где я занимаюсь поделками и DIY)

  10. Здоровье (врачи, посещение больниц)

  11. Яркость жизни (путешествия, какие-то яркие события и действия)

  12. Друзья (общение и встречи с друзьями)

  13. Прочее (все что не отнеслось к категориям выше)

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

Выглядит это вот так:
Ответы при заполнении формыОтветы при заполнении формыА это лист с результатами ответов в виде таблицыА это лист с результатами ответов в виде таблицы

Дальше можно строить по этой таблицы разные графики.

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

Про мобильное приложение и трудности

Я решил поискать что-то готовое на эту тему и ничего не нашел, чтобы мне понравилось и подошло под мои запросы. Решил зайти со своей стороны Компания, в которой я работаю занимается разработкой ПО. У нас есть десктопная и мобильная версию программы, которая предназначена для ИТ-компаний (складской учет, комплекты/комплектующие,учет заявок пользователей, инвентаризации, учет ремонтов и прочее). Не буду спойлерить, не об этом речь. В общем приняли решение реализовать блок учета времени по Любищевув своем ПО.

Наше мобильное приложение работает как в связке с десктопом, так и без него. Причем, когда без него это вполне самостоятельное приложение, которое может использовать любой на Android и iOS, причемабсолютно бесплатно.

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

  • Создать документ "Ежедневныйотчет".Он должен иметь возможность ввода данных и хранить как за день, так и за период и "собирать" все работы в себе.

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

  • Надо что-то придумать с переходящими датами. Я ложусь спать в 22:00, проснусь в 7:00, приложение должно разбить на два временных участка одно и то же действия с 22:00 до 23:59:59 - сон, с 00:00 до 7:00 - сон.

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

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

  • Должны быть графики и обычные отчеты причем с разной степенью детальности и с разными аналитическими разрезами.

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

Что получилось?

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

Для тех, кому не терпится посмотреть:

Ссылка в Google Play

Ссылка в AppStore

Как выглядит мобильное приложение
Главный экран мобильного приложенияГлавный экран мобильного приложенияДобавление работ с главного экрана. Дата/время с/по устанавливаются автоматически, как и длительностьДобавление работ с главного экрана. Дата/время с/по устанавливаются автоматически, как и длительностьЕжедневные отчеты. Все работы и дела идут последовательноЕжедневные отчеты. Все работы и дела идут последовательноА вот и отчеты по учету времени. Внизу разные варианты. Вверху сам отчет с настройкамиА вот и отчеты по учету времени. Внизу разные варианты. Вверху сам отчет с настройкамиГрафикиГрафики

Выводы

По личным ощущениям блок учета рабочего времени не идеальный, но в целом, я получил то, что хотел. Можно вводить текст с описанием работы с клавиатуры, а можно надиктовать, если писать слишком долго. В этом месяце я переболел Covid-19 и по графику видно (видно ли?) проседание рабочей производительности и в каком периоде это произошло. Я руководитель проектов и даже болея вынужден кому-то помогать, с кем-то говорить, а где-то написать письмо. Поэтому на графике практически нет не рабочих периодов.

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

Из того, что сейчас можно сказать точно это интересно, даже если и не взлетит в отдаленной перспективе. Так же это весьма познавательно, когда открываешь случайный день и воспроизводишь его заново. Вдруг всплывают какие-то детали, дела, которые прошли, и ты про них "почти забыл". Не в плане что-то не сделал, а в плане каких-то мелочей, на которые не обратил внимание, а надо было бы. С удивлением понял, что рабочий день проходит хорошо, если действительно на рабочее время будет выделено в районе 5-6 часов. Обычно гораздо меньше получается. Даже элементарное попил кофе это минус 10 минут от рабочего, а в сумме таких кусочков набирается прям прилично. Об этом буду серьезно думать чуть позже. Осознавать это все весьма неожиданно.

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

Подробнее..

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

18.12.2020 20:18:20 | Автор: admin

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

Задачи: почему мы вообще в это ввязались

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

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

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

Поиск решения: почему выбрали Flutter

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

По большому счёту, выбирали между нативом и Flutter. React Native и Xamarin отбросили почти сразу, с ними уже имелся опыт не очень удачный. Также рассматривали PWA, но этот вариант тоже пришлось забраковать: на момент выбора там было не всё хорошо с поддержкой нативных функций системы, особенно в iOS. К тому же для хорошей поддержки требовались свежие версии OS, что тоже могло стать проблемой. В общем, изучив вопрос поглубже, решили не собирать грабли.

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

  • достаточно быстрый, за счет компиляции, сравним с нативом

  • виджетный (компонентный) подход, сродни React

  • кроссплатформенный меньше кода, потенциально упрощает тестирование

  • богатый инструментарий разработчика IDE и прочее

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

  • продукт, который развивает и продвигает Google и большое Open-source community

  • крупные компании Google, Groupon, Alibaba разрабатывают на нём свои приложения

Были и минусы:

  • мало разработчиков всего 121 резюме, 11 лидов/сеньоров в Москве

  • дорогие лиды/сеньоры

  • относительно мало технической информации stackoverflow и т.п.

  • пока мало готовых (сложных) компонентов стандартные/системные тут не используются

  • язык новичкам придётся учить Dart (но легко переходить, если есть опыт разработки на Java или JavaScript)

  • сложно найти подрядчиков с опытом

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

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

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

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

Основная масса специалистов по Flutter это младшие разработчики, которые только начинают свой путь, а также старшие или ведущие, у которых есть время изучать современные технологии и пробовать их в виде proof of concept. Проще всего адаптироваться к Flutter тем, кто имеет опыт Android-разработки. На втором месте web-разработчики: react, vue.js здесь близкий по духу компонентный подход.

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

Однако с синьорами и мидлами дело было совсем плохо, обычные каналы поиска не работали. Пришлось фантазировать: искали людей в тематических telegram-каналах и через Хабр, среди авторов статей по Flutter. С подходящими кандидатами списывались, общались, давали тестовые задания. Это вынудило нас расширить географию на всю Россию, что стало новым опытом: до этого мы рассматривали только кандидатов из городов, в которых есть наши офисы но с Flutter это не работало.

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

Изначально работу выстраивали по принципу feature-driven development (FDD, разработка, управляемая функциональностью). Но впоследствии решили перейти на унифицированный процесс разработки на основе OpenUP: такой способ лучше подходит для управления разработкой всего продукта и координации нескольких команд архитекторов, аналитиков, разработчиков, тестировщиков. Внутри команд разработки используем Scrum.

Особенности разработки на Flutter

Когда говорят о кроссплатформенной мобильной разработке, часто сравнивают React Native и Flutter. Однако нужно понимать, что React Native это не история про один код на все платформы. Недаром официальный слоган проекта Learn once, write anywhere. Дело в том, что в React Native пишут код на JavaScript, который использует нативные для каждой платформы компоненты и они довольно сильно отличаются. Да, есть общие компоненты, вроде Text и View: под обе платформы, хотя и с нюансами. Но много и таких, что работают только на одной ОС. Поэтому в React Native нередко можно встретить выражения:

if (Platform.OS == 'ios') ...`

При использовании Flutter разработка ведётся на языке Dart, который компилируется в нативный для платформы код. При этом вместо нативных компонентов используется собственная библиотека виджетов, которые выглядят как нативные Material и Cupertino для Android и iOS соответственно. Вы можете использовать виджеты из обеих библиотек в одном приложении, кроме того, их очень легко кастомизировать.

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

Бывает и так, что есть уникальная для платформы функциональность. Подстановка СМС-кодов верификации на iOS работает по умолчанию, от программиста не требуется никаких действий. А на Android нужно использовать системные методы, чтобы получить этот код программно и подставить в нужное поле. Хотя даже для этой задачи уже существуют Flutter-библиотеки, например, sms_user_consent. Весь платформенный код в ней уже создан, остаётся только добавить свой listener для получения сообщений.

if (Platform.isAndroid) {  listenForCode();}void listenForCode() {  smsUserConsent = SmsUserConsent(      phoneNumberListener: () {},      // Действия при получении смс-сообщения      smsListener: () {        Log('code is updated to ${smsUserConsent.receivedSms}');        final sms = smsUserConsent.receivedSms;        _smsCodeController.text = sms.substring(sms.lastIndexOf(' ') + 1);      });  smsUserConsent.requestSms();}

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

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

Разработка на Flutter значительно быстрее, нежели нативная. Если говорить о кодовой базе, то нативный мобильный код по объёму для каждой платформы по отдельности был бы примерно аналогичен Dart-коду, но за счет того, что платформ две, получаем существенное увеличение трудозатрат.

Результаты и выводы: стоит ли разрабатывать на Flutter

Новый фреймворк не подвёл: после полугода разработки MVP доступно в магазинах приложений для Android и iOS. Проект по-прежнему развивается, и в этом очень помогает фидбэк пользователей: мы активно взаимодействуем с фермерами, которые делятся своими впечатлениями от использования платформы. В ближайшем будущем хотим подключить к нашему маркетплейсу сторонние сервисы доставки, внедрить онлайн-платежи, организовать процесс совместных покупок. Для реализации этого нам понадобятся не только мидлы и сеньоры, специализирующиеся на Flutter, но и PHP-разработчики (Senior и Team Lead с опытом Magento 2), а также опытные специалисты по Vue.js. Если видите себя членом нашей новой команды обязательно пишите!

Нам очень понравилось работать с Flutter. Тем не менее это решение пока нельзя считать универсальным: для нашего проекта оно подошло на 100%, но при разработке какой-нибудь игры или продукта, использующего более сложные и передовые технологии VR, AR, ML и т.п., всё могло бы сложиться не так гладко. Впрочем, со временем эта функциональность наверняка подтянется.

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

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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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