Случайны выбор дневника Раскрыть/свернуть полный список возможностей


Найдено 1793 сообщений
Cообщения с меткой

разработка мобильных приложений - Самое интересное в блогах

Следующие 30  »
rss_rss_hh_new

Простые сладкие приложения с Kivy

Воскресенье, 24 Июля 2016 г. 18:59 (ссылка)



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



Возможно, для вас будет новостью, но разрабатывать мобильные приложения с функционалом, который доступен Java разработчикам, под Android с помощью фреймворка Kivy не просто просто, а очень просто! Именно этого правила я придерживаюсь, создавая свои проекты с Python + Kivy — разработка должна быть максимально простой и быстрой. Как щелчок пальцами.



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



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





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



А мы умеем и нам понадобятся: кофе-сигареты, террариум с третьим Python-ом, то ли птица, то ли фрукт — Kivy и немного мозгов. Наличие последних приветствуется! Заходим на github и качаем Мастер создания нового проекта для фреймворка Kivy + Python3 (да, я полностью отказался от использования Python2, что и вам советую). Распаковываем, переходим в папку с мастером и запускаем:



python3 main.py name_project path_to_project -repo repo_project_on_github


если у проекта имеется репозиторий на github.



Или



python3 main.py name_project path_to_project


если репозитория не github не имеется.



В этом случае после создания откройте файл проекта main.py и отредактируйте, функцию отправки баг репорта вручную.





Итак, в результате мы получаем дефолтный Kivy проект со следующей структурой каталогов:





Отдельно следует рассмотреть каталог Libs:





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





Все что нам нужно, это использовать свой главный экран, то есть заменить файл startscreen.py в директории Libs/uix, создать новый файл разметки экрана startscreen.kv в папке Libs/uix/kv, отредактировать базовый класс program.py, ну, и добавить новые импорты и сопутствующие классы, если таковые имеются.



Давайте начнем с кастомных кнопкок, которые используются в нашем главном экране:





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



Создадим в директории Libs/uix файл custombutton.py и определим в нем класс нашей кнопки:



custombutton.py
import os

from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.lang import Builder
from kivy.properties import StringProperty, ObjectProperty, ListProperty

from . imagebutton import ImageButton

root = os.path.split(__file__)[0]
Builder.load_file('{}/kv/custombutton.kv'.format(
root if root != '' else os.getcwd())
)

class CustomButton(BoxLayout, Button):
icon = StringProperty('')
icon_map = StringProperty('')
icon_people = StringProperty('')
text = StringProperty('')
button_color = ListProperty([0, 0, 0, .2])
text_color = ListProperty([0, 0, 0, .1])
events_callback = ObjectProperty(None)


Разметка кнопки в директории Libs/uix/kv — файл custombutton.kv:



custombutton.kv
#:kivy 1.9.1 

:
id: root.text
padding: 5
size_hint_y: None
height: 60
on_release: root.events_callback(root.text)

canvas:
# Цвет кнопки
Color:
rgba: root.button_color
Rectangle:
pos: self.x + 2.5, self.y - 3
size: self.size
# Тень кнопки
Color:
rgba: [1, 1, 1, 1]
Rectangle:
pos: self.pos
size: self.size

Image:
source: root.icon
size_hint: .2, 1
Label:
markup: True
text: root.text
text_size: self.width - 70, None
color: root.text_color

BoxLayout:
orientation: 'vertical'
size_hint: .1, 1
spacing: 6
ImageButton:
source: root.icon_people
on_release: root.events_callback({'people': root.text})
ImageButton:
source: root.icon_map
on_release: root.events_callback({'map': root.text})


Кастомная кнопка для экрана локаций:





custommenu.py
import os 

from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
from kivy.properties import ListProperty, StringProperty, ObjectProperty

root = os.path.split(__file__)[0]
Builder.load_file('{}/kv/custommenu.kv'.format(
root if root != '' else os.getcwd())
)

class CustomMenuItem(BoxLayout):
background_item = ListProperty([.1, .1, .1, 1])
text_color = ListProperty([.1, .1, .1, 1])
icon_item = StringProperty('')
text_item = StringProperty('')
id_item = StringProperty('')
events_callback = ObjectProperty(None)


custommenu.kv
#:kivy 1.9.1 
#:import ImageButton Libs.uix.imagebutton.ImageButton

:
orientation: 'vertical'
canvas:
Color:
rgba: root.background_item
Rectangle:
pos: self.pos
size: self.size

ImageButton:
source: root.icon_item
on_release: root.events_callback(root.id_item.split('.')[0])
Label:
text: root.text_item
color: root.text_color
font_size: '19sp'


Также мы будем использовать класс ImageButton — кнопку с изображением — для баннеров. Поскольку класс относится к UI, я поместил файл imagebutton.py в каталог Libs/uix:



imagebutton.py
from kivy.uix.image import Image
from kivy.uix.behaviors import ButtonBehavior

class ImageButton(ButtonBehavior, Image):
pass


Далее нам потребуется класс для смены рекламных баннеров на главном экране программы. Для данного тестого примера приложения плакаты с рекламными баннерами помещены в локальную папку проекта. Создадим файл show_banners.py в директории классов приложения Libs/programclass:



show_banners.py
import os 

from kivy.uix.boxlayout import BoxLayout

from Libs.uix.imagebutton import ImageButton

class ShowBanners(object):
'''Меняет и выводит на главном экране рекламные баннеры.'''

def __init__(self):
self.banner_list = os.listdir(
'{}/Data/Images/banners'.format(self.directory)
)
# Направление смены слайдов баннеров.
self.directions = ('up', 'down', 'left', 'right')

def show_banners(self, interval):
if self.screen.ids.screen_manager.current == '':
name_banner = self.choice(self.banner_list)

box_banner = BoxLayout()
new_banner = ImageButton(
id=name_banner.split('.')[0],
source='Data/Images/banners/{}'.format(name_banner),
on_release=self.press_banner
)
box_banner.add_widget(new_banner)

name_screen = name_banner
banner = self.Screen(name=name_screen)
banner.add_widget(box_banner)
self.screen.ids.banner_manager.add_widget(banner)
effect = self.choice(self.effects_transition)
direction = self.choice(self.directions)
if effect != self.SwapTransition:
self.screen.ids.banner_manager.transition = effect(
direction=direction
)
else:
self.screen.ids.banner_manager.transition = effect()
self.screen.ids.banner_manager.current = name_screen
self.screen.ids.banner_manager.screens.pop()

def press_banner(self, instance_banner):
if isinstance(instance_banner, str):
print(instance_banner)
else:
print(instance_banner.id)


Данный класс просто меняет экраны с баннерами, устанавливая их в менеджере экранов banner_manager:





Не забываем добавить импорт созданного класса в библиотеку классов programclass в файле инициализации:





Набор шейдеров для анимаций смены афиш мы импортируем в базовом файле program.py:





Реализация главного экрана приложения:



startscreen.py
import os 

from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
from kivy.properties import ObjectProperty, ListProperty, StringProperty

from Libs.uix.custombutton import CustomButton

root = os.path.split(__file__)[0]
root = root if root != '' else os.getcwd()

class StartScreen(BoxLayout):
events_callback = ObjectProperty(None)
'''Функция обработки сигналов экрана.'''

core = ObjectProperty(None)
'''module 'Libs.programdata' '''

color_action_bar = ListProperty(
[0.4, 0.11764705882352941, 0.2901960784313726, 0.5607843137254902]
)
'''Цвет ActionBar.'''

color_body_program = ListProperty(
[0.15294117647058825, 0.0392156862745098, 0.11764705882352941, 1]
)
'''Цвет фона экранов программы.'''

color_tabbed_panel = ListProperty(
[0.15294117647058825, 0.0392156862745098, 0.11764705882352941, 1]
)
'''Цвет фона tabbed panel.'''

title_previous = StringProperty('')
'''Заголовок ActionBar.'''

tabbed_text = StringProperty('')
'''Текст пунктов кастомной tabbed panel.'''

Builder.load_file('{}/kv/startscreen.kv'.format(root))

def __init__(self, **kvargs):
super(StartScreen, self).__init__(**kvargs)
self.ids.custom_tabbed.bind(on_ref_press=self.events_callback)

# Cписок магазинов.
for name_shop in self.core.dict_shops.keys():
self.ids.shops_list.add_widget(
CustomButton(
text=self.core.dict_shops[name_shop],
icon='Data/Images/shops/{}.png'.format(name_shop),
icon_people='Data/Images/people.png',
icon_map='Data/Images/mapmarker.png',
events_callback=self.events_callback,
)
)


startscreen.kv
#: kivy 1.9.1 
#: import StiffScrollEffect Libs.uix.garden.stiffscroll.StiffScrollEffect


orientation: 'vertical'
canvas:
Color:
rgb: root.color_body_program
Rectangle:
pos: self.pos
size: self.size

ActionBar:
id: action_bar
canvas:
Color:
rgb: root.color_action_bar
Rectangle:
pos: self.pos
size: self.size
ActionView:
id: action_view
ActionPrevious:
id: action_previous
app_icon: 'Data/Images/logo.png'
title: root.title_previous
with_previous: False
on_release: root.events_callback('navigation_drawer')
ActionButton:
icon: 'Data/Images/trash_empty.png'
ActionButton:
icon: 'Data/Images/search.png'

Label:
id: custom_tabbed
text: root.tabbed_text
bold: True
markup: True
size_hint: 1, .35
text_size: self.width - 40, None
canvas.before:
Color:
rgb: root.color_tabbed_panel
Rectangle:
pos: self.pos
size: self.size

ScreenManager:
id: screen_manager
size_hint: 1, 8
Screen:
ScreenManager:
id: banner_manager
size_hint: 1, .38
pos_hint: {'top': 1}

ScrollView:
effect_cls: StiffScrollEffect
size_hint_y: None
height: root.height // 1.8
pos_hint: {'top': .62}
GridLayout:
id: shops_list
cols: 1
spacing: 5
padding: 5
size_hint_y: None
height: self.minimum_height


Как вы могли заметить, я не использую TabbedPanel, так как считаю ее стандартную реализацию в Android не слишком красивой. Она была заменена на Label + ref:







Базовый класс Program:



program.py
import os 
import sys

from random import choice

from kivy.app import App
from kivy.uix.screenmanager import Screen, SlideTransition, SwapTransition
from kivy.core.window import Window
from kivy.config import ConfigParser
from kivy.clock import Clock
from kivy.utils import get_hex_from_color, get_color_from_hex
from kivy.properties import ObjectProperty, NumericProperty

from Libs.uix.kdialog import KDialog, BDialog, Dialog
from Libs.uix.startscreen import StartScreen
from Libs.uix.custommenu import CustomMenuItem
from Libs.uix.navigationmenu import NavigationMenu

from Libs.uix.garden.navigationdrawer import NavigationDrawer

# Классы программы.
from Libs import programclass as prog_class

from Libs import programdata as core
from Libs.manifest import Manifest

# Графика для диалоговых окон.
Dialog.background_image_buttons = core.image_buttons
Dialog.background_image_shadows = core.image_shadows
Dialog.background = core.decorator

class Program(App, prog_class.ShowPlugin, prog_class.ShowBanners,
prog_class.SearchShop, prog_class.ShowLicense,
prog_class.ShowLocations):
'''Функционал программы.'''

start_screen = ObjectProperty(None)
''':attr:`start_screen` is a :class:`~Libs.uix.startscreen.StartScreen`'''

screen = ObjectProperty(None)
''':attr:`screen` is a :class:`~Libs.uix.startscreen.StartScreen`'''

window_text_size = NumericProperty(15)

def __init__(self, **kvargs):
super(Program, self).__init__(**kvargs)
Window.bind(on_keyboard=self.events_program)

# Для области видимомти в programclass.
self.Screen = Screen
self.Clock = Clock
self.CustomMenuItem = CustomMenuItem
self.KDialog = KDialog
self.BDialog = BDialog
self.Manifest = Manifest
self.SwapTransition = SwapTransition
self.choice = choice
self.get_color_from_hex = get_color_from_hex
self.get_hex_from_color = get_hex_from_color
self.core = core
self.name_program = core.string_lang_title
self.navigation_drawer = NavigationDrawer(side_panel_width=230)
self.current_open_tab = core.string_lang_tabbed_menu_shops
self.shop = False # выбранный магазин
self.open_dialog = False # открыто диалоговое окно

self.effects_transition = (SlideTransition, SwapTransition)
# Список магазинов.
self.shops = core.dict_shops.keys()
# Список локаций.
self.locations = [
location.split('.')[0].lower() for location in os.listdir(
'{}/Data/Images/locations'.format(core.prog_path))]

def build_config(self, config):
config.adddefaultsection('General')
config.setdefault('General', 'language', 'Русский')
config.setdefault('General', 'theme', 'default')

def build(self):
self.title = self.name_program # заголовок окна программы
self.icon = 'Data/Images/logo.png' # иконка окна программы
self.use_kivy_settings = False

self.config = ConfigParser()
self.config.read('{}/program.ini'.format(core.prog_path))
self.set_var_from_file_settings()

# Главный экран программы.
self.start_screen = StartScreen(
color_action_bar=core.color_action_bar,
color_body_program=core.color_body_program,
color_tabbed_panel=core.color_tabbed_panel,
tabbed_text=core.string_lang_tabbed_menu.format(
TEXT_SHOPS=core.string_lang_tabbed_menu_shops,
TEXT_LOCATIONS=core.string_lang_tabbed_menu_locations,
COLOR_TEXT_SHOPS=get_hex_from_color(core.color_action_bar),
COLOR_TEXT_LOCATIONS=core.theme_text_color),
title_previous=self.name_program[1:],
events_callback=self.events_program, core=core
)

self.screen = self.start_screen
navigation_panel = NavigationMenu(
events_callback=self.events_program,
items=core.dict_navigation_items
)

Clock.schedule_interval(self.show_banners, 4)

self.navigation_drawer.add_widget(navigation_panel)
self.navigation_drawer.anim_type = 'slide_above_anim'
self.navigation_drawer.add_widget(self.start_screen)

return self.navigation_drawer

def set_var_from_file_settings(self):
'''Установка значений переменных из файла настроек program.ini.'''

self.language = core.select_locale[
self.config.get('General', 'language')
]

def set_current_item_tabbed_panel(self, color_current_tab, color_tab):
self.screen.ids.custom_tabbed.text = \
core.string_lang_tabbed_menu.format(
TEXT_SHOPS=core.string_lang_tabbed_menu_shops,
TEXT_LOCATIONS=core.string_lang_tabbed_menu_locations,
COLOR_TEXT_SHOPS=color_tab,
COLOR_TEXT_LOCATIONS=color_current_tab
)

def events_program(self, *args):
'''Обработка событий программы.'''

if self.navigation_drawer.state == 'open':
self.navigation_drawer.anim_to_state('closed')

if len(args) == 2: # нажата ссылка
event = args[1]
else: # нажата кнопка программы
try:
_args = args[0]
event = _args if isinstance(_args, str) else _args.id
except AttributeError: # нажата кнопка девайса
event = args[1]

if core.PY2:
if isinstance(event, unicode):
event = event.encode('utf-8')

if event == core.string_lang_settings:
pass
elif event == core.string_lang_exit_key:
self.exit_program()
elif event == core.string_lang_license:
self.show_license()
elif event == core.string_lang_plugin:
self.show_plugins()
elif event in self.locations:
print(event)
elif event == 'search_shop':
self.search_shop()
elif event == 'navigation_drawer':
self.navigation_drawer.toggle_state()
elif event == core.string_lang_tabbed_menu_locations:
self.show_locations()
elif event == core.string_lang_tabbed_menu_shops:
self.back_screen(event)
elif event == 'obi_banner':
self.press_banner(event)
elif event in (1001, 27):
self.back_screen(event)
elif event in self.shops:
print(event)
return True

def back_screen(self, event):
'''Менеджер экранов.'''

# Нажата BackKey на главном экране.
if self.screen.ids.screen_manager.current == '':
if event in (1001, 27):
self.exit_program()
return
if len(self.screen.ids.screen_manager.screens) != 1:
self.screen.ids.screen_manager.screens.pop()
self.screen.ids.screen_manager.current = \
self.screen.ids.screen_manager.screen_names[-1]
# Устанавливаем имя предыдущего экрана.
#self.screen.ids.action_previous.title = self.screen.ids.screen_manager.current
# Устанавливаем активный пункт в item_tabbed_panel.
self.set_current_item_tabbed_panel(
core.theme_text_color, get_hex_from_color(core.color_action_bar)
)

def exit_program(self, *args):
def dismiss(*args):
self.open_dialog = False

def answer_callback(answer):
if answer == core.string_lang_yes:
sys.exit(0)
dismiss()

if not self.open_dialog:
KDialog(answer_callback=answer_callback, on_dismiss=dismiss,
separator_color=core.separator_color,
title_color=get_color_from_hex(core.theme_text_black_color),
title=self.name_program).show(
text=core.string_lang_exit.format(core.theme_text_black_color),
text_button_ok=core.string_lang_yes,
text_button_no=core.string_lang_no, param='query',
auto_dismiss=True
)
self.open_dialog = True

def on_pause(self):
'''Ставит приложение на 'паузу' при выхоже из него.
В противном случае запускает программу заново'''

return True

def on_resume(self):
print('on_resume')

def on_stop(self):
print('on_stop')


Оставляя за бортом (не рассмотренными) файл локализации — Data/Language/russian.txt, расфасовку графических ресурсов, цветовую схему приложения в файле Data/Themes/default/default.ini, создание данных в programdata.py, меню Navigation Drawer — все это несложные мелочи, запускаем тестовый пример:



python3 main.py


… и получаем вот такую картинку:





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



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



Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/306300/

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

Дайджест интересных материалов для мобильного разработчика #163 (18-24 июля)

Воскресенье, 24 Июля 2016 г. 16:00 (ссылка)

В новом дайджесте тестирование в Mail.Ru, онбординг с помощью видео, сертификация разработчиков от Google, Agile API от Badoo и мобильный программатик на пальцах. На прошлой неделе было относительно мало материалов, на этой наверстываем!












Как настроить расширяемую систему для регрессионного тестирования на телефонах: опыт мобильной Почты Mail.Ru

Сегодня я хочу рассказать, как мы построили с нуля гибкую и расширяемую систему для выполнения автотестов на Android-смартфонах. Сейчас у нас используется около 60 устройств для регрессионного тестирования мобильного приложения Почты Mail.Ru. В среднем они тестируют около 20 сборок приложения ежедневно. Для каждой сборки выполняется около 600 UI-тестов и более 3500 unit-тестов.




Как мы обновили и переписали iOS-приложение банка «Открытие»

В жизненном цикле мобильного продукта рано или поздно наступает момент, когда нужно радикально обновиться. Потому что за время, прошедшее с запуска, выросли требования бизнеса и ожидания клиентов, изменились возможности платформы и средства разработки — и обновления становится невозможно реализовать путем “косметического ремонта”. В мире мобильных приложений жизненный цикл ПО составляет 2-3 года против 10-15 лет в обычном Enterprise-сегменте. Для нас с командой “Открытие Digital” момент радикального обновления мобильного банка настал в конце прошлого года.



iOS



Android



Windows



Разработка



Аналитика, маркетинг и монетизация



Устройства и IoT





<- Предыдущий дайджест. Если у вас есть другие интересные материалы или вы нашли ошибку — пришлите, пожалуйста, в почту.
Original source: habrahabr.ru.

https://habrahabr.ru/post/306296/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best

Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

В поисках чистой архитектуры (1-я часть) — Swift 3.0

Пятница, 22 Июля 2016 г. 17:32 (ссылка)

image



Приветствую уважаемых жителей Хабрахабра!



Не так давно я стал замечать, что мой код становится громоздким и даже в рамках одного контроллера мне все сложней удержать в голове то, что в нем происходит. Как следствие, на выходе не всегда ожидаемый результат, что я хотел реализовать, так как мозг “замылился” и я легко могу упустить существенную деталь. А после, ручной анализ кода, работа с отладчиком и так далее… Да что уж говорить, доходило до абсурда, при сборке приложения xcode падал замертво и я даже не успевал понять, что случилось в приложении! Нужно было что то менять и думать над архитектурой, так как я не хочу всю свою карьеру писать плохоподдерживаемый код…



Кому интересен вопрос архитектуры приложения, добро пожаловать под кат!



На Swift я перешел не так давно, потому казалось, что в данном языке, априори невозможны подобные реализации. А оказалось, что поговорка про плохого танцора все же имеет ко мне непосредственное отношение. Кстати, за переход на Swift я благодарен лично Ивану Акулову со swiftbook.ru, так как у меня был какой-то психологический барьер на изучение Swift, до версии 2.0 даже не пытался ковырять его, слишком все сырым казалось, да и Objective-C казался вполне легким и логичным. Так было до первой реализации моего первого свифт-приложения, теперь мне сложно при необходимости настроиться писать на “старичке”.



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



Итак, не будем сильно отвлекаться и вернемся к нашим баранам. Когда я решил всерьез улучшить качество кода, то первым делом подумал за VIPER, так как регулярно посещал все тусовки разработчиков в Рамблер и теории нахватался достаточно. Стоит заметить, что в Рамблер поощряют использование их наработок и охотно консультируют по всем сложным вопросам. Они в реальности фанатеют от таких вещей как: VIPER, TDD, Typhoon и слушать их доклады сплошное удовольствие, но остается маленькое но… Это все теория для слушателей! Нужно брать и писать код в реальности, а не виртуально обсуждать все сильные и слабые стороны паттерна. Особенно смущал тот факт, что Рамблер использует свиззлинг в роутинге, это как то размывало классическое определение паттерна. Что не так еще с VIPER? Существует множество модификаций паттерна, нет единого толкования, определения и практик использования. Каждая команда разработчиков понимает его на свой лад и проповедует свою реализацию, как единственно правильную и заслуживающую право на жизнь. В то время, когда я пытался понять, как же правильно использовать VIPER, мне довелось увидеть столько различных его модификаций, что это еще больше запутало меня и все усложнило!



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



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



Так мы искали VIPER, а что нашли?!



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



Итак, кто владеет английским, могут почитать в оригинале: clean-swift.com. Зовут разработчика Рэймонд и он активно поддерживает связь со своими читателями через email и комментарии на страницах своего блога.



Коротко о паттерне



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



Как он видит чистую архитектуру?



Изображение под спойлером
image





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



Изначально я прошел мимо, не вникая в подробности, просто машинально отправил в закладки браузера, чтобы после почитать для саморазвития, не используя на практике. Но помог случай его попробовать. Откопал старый недоделанный проект для Apple TV, нужно было определить простую реализацию, чтобы код получился не громоздким и “читабельным”. Вот тут и вспомнил за блог Рэймонда, решил все же попробовать его подход в реализации.



Так это VIPER?



Однозначно, нет! Но он имеет право на жизнь, причем и в сложных проектах! Код достаточно просто покрывается тестами (одна из основных фишек VIPER!), причем покрывается как угодно. Это и TAD, TDD, BDD, простые юнит-тесты. Есть четкое понимание, что стоит покрыть тестами, а что можно пропустить. Такая динамичная штука на практике получилась! Почему на практике? Да просто достаточно легко этот пресловутый V-I-P лег на проект!



Итак, позвольте еще раз представить: Clean Swift! Первоисточник: clean-swift.com



Я пообщался с Рэймондом и попросил разрешения использовать его наработки в своих публикациях.



Что мы попробуем сделать?



Понятно, что для примера подойдет максимально простой проект, так же максимально приближенный к боевой реализации. Никаких «Hello World», только реальный код! В этом плане мне на Хабре понравился недавний цикл статей по CoreData, где angryscorp показал работу с CoreData как если бы писался реальный проект.



Значит и мы попробуем повторить его опыт.



ПЛАН ПУБЛИКАЦИЙ:



1-я часть) Будем делать приложение, которое разберет плейлист YouTube, подгрузит в таблицу и через сегвей мы будем использовать передачу кастомной сущности для просмотра в другом контроллере/сцене. Да, роутинг может быть легким и ненавязчивым!



2-я часть) Разберемся с TAD (Test After Development).



3-я часть) Реализуем правильный TDD с аналогичным проектом. Правда для этого я постараюсь добавить материал из еще одного крутого источника: Dr. Dominik Hauser — Test-Driven iOS Development with Swift (снова на версии языка 3.0).



4-я часть) Создадим свои шаблоны для быстрой реализации сцены/контроллеров (Generamba — Rambler&Co нам будет в помощь).



5-я часть) Разберем классические MVC, но только от слова Massive и разложим реально-массивный код через V-I-P.



Приступим?



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



Перейдем по ссылке: developers.google.com/youtube/v3, нам потребуется аккаунт Google, у кого его нет, в конфиге тестового проекта оставлю рабочий ключ, специально созданный для этих целей.



Последовательность действия проста и понятна. На вкладке “GUIDES” перейдем по ссылке “Open the API Library”. Нам потребуется категория YouTube API, в ней нужно перейти по ссылке “YouTube Data API” и следовать указаниям мастера активации API и создания ключа.



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



Получение ключа для сервиса YouTube Data API
image

image

image





После того, как у нас есть ключ, заимплементим его в файле Config.swift. Я вообще предпочитаю конфиги приложения и хелперы хранить в отдельной области проекта, но решение остается за вами. Самое главное — это ключ! При регистрации ключа, ни в коем случае не указываем бандл приложения, иначе YouTube начнет фильтровать запросы не в вашу пользу, почему то у Google это криво реализовано.



Что дальше?



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



Архив с шаблонами, доступен для загрузки на странице проекта GitHub, стоит посмотреть папку “Extended”. Скачиваем шаблоны и через терминал переходим в папку с распакованными шаблонами. Далее простая команда “make install_templates” и шаблоны установлены. Их можно использовать для работы.



Как Рэймонд использует шаблоны можно посмотреть в этом небольшом видео








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



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



Структура проекта
image



TableSceneConfigurator.swift
import UIKit

// MARK: Connect View, Interactor, and Presenter

extension TableSceneViewController: TableScenePresenterOutput {
override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) {
router.passDataToNextScene(segue: segue)
}
}

extension TableSceneInteractor: TableSceneViewControllerOutput {}
extension TableScenePresenter: TableSceneInteractorOutput {}

class TableSceneConfigurator {
// MARK: Object lifecycle
class var sharedInstance: TableSceneConfigurator {
return TableSceneConfigurator()
}

// MARK: Configuration
func configure(viewController: TableSceneViewController) {

let router = TableSceneRouter()
router.viewController = viewController

let presenter = TableScenePresenter()
presenter.output = viewController

let interactor = TableSceneInteractor()
interactor.output = presenter

viewController.output = interactor
viewController.router = router
}

}




TableSceneInteractor.swift
import UIKit

protocol TableSceneInteractorInput {
func doSomething(request: TableSceneRequest)
}

protocol TableSceneInteractorOutput {
func presentSomething(response: TableSceneResponse)
}

class TableSceneInteractor: TableSceneInteractorInput {

var output: TableSceneInteractorOutput!
var worker: TableSceneWorker!
// MARK: Business logic
func doSomething(request: TableSceneRequest) {
// NOTE: Create some Worker to do the work
worker = TableSceneWorker()
worker.doSomeWork()
// NOTE: Pass the result to the Presenter
let response = TableSceneResponse()
output.presentSomething(response: response)
}

}




TableSceneModels.swift
import UIKit

struct TableSceneRequest {

}

struct TableSceneResponse {

}

struct TableSceneViewModel {

}




TableScenePresenter.swift
import UIKit

protocol TableScenePresenterInput {
func presentSomething(response: TableSceneResponse)
}

protocol TableScenePresenterOutput: class {
func displaySomething(viewModel: TableSceneViewModel)
}

class TableScenePresenter: TableScenePresenterInput {

weak var output: TableScenePresenterOutput!
// MARK: Presentation logic
func presentSomething(response: TableSceneResponse) {
// NOTE: Format the response from the Interactor and pass the result back to the View Controller
let viewModel = TableSceneViewModel()
output.displaySomething(viewModel: viewModel)
}

}




TableSceneRouter.swift
import UIKit

protocol TableSceneRouterInput {
func navigateToSomewhere()
}

class TableSceneRouter: TableSceneRouterInput {

weak var viewController: TableSceneViewController!
// MARK: Navigation
func navigateToSomewhere() {
// NOTE: Teach the router how to navigate to another scene. Some examples follow:

// 1. Trigger a storyboard segue
// viewController.performSegueWithIdentifier("ShowSomewhereScene", sender: nil)

// 2. Present another view controller programmatically
// viewController.presentViewController(someWhereViewController, animated: true, completion: nil)

// 3. Ask the navigation controller to push another view controller onto the stack
// viewController.navigationController?.pushViewController(someWhereViewController, animated: true)

// 4. Present a view controller from a different storyboard
// let storyboard = UIStoryboard(name: "OtherThanMain", bundle: nil)
// let someWhereViewController = storyboard.instantiateInitialViewController() as! SomeWhereViewController
// viewController.navigationController?.pushViewController(someWhereViewController, animated: true)
}

// MARK: Communication
func passDataToNextScene(segue: UIStoryboardSegue) {
// NOTE: Teach the router which scenes it can communicate with
if segue.identifier == "ShowSomewhereScene" {
passDataToSomewhereScene(segue: segue)
}
}

func passDataToSomewhereScene(segue: UIStoryboardSegue) {
// NOTE: Teach the router how to pass data to the next scene
// let someWhereViewController = segue.destinationViewController as! SomeWhereViewController
// someWhereViewController.output.name = viewController.output.name
}
}




TableSceneViewController.swift
import UIKit

protocol TableSceneViewControllerInput {
func displaySomething(viewModel: TableSceneViewModel)
}

protocol TableSceneViewControllerOutput {
func doSomething(request: TableSceneRequest)
}

class TableSceneViewController: UITableViewController, TableSceneViewControllerInput {
var output: TableSceneViewControllerOutput!
var router: TableSceneRouter!
// MARK: Object lifecycle
override func awakeFromNib() {
super.awakeFromNib()
TableSceneConfigurator.sharedInstance.configure(viewController: self)
}

// MARK: View lifecycle
override func viewDidLoad() {
super.viewDidLoad()
doSomethingOnLoad()
}

// MARK: Event handling
func doSomethingOnLoad() {
// NOTE: Ask the Interactor to do some work
let request = TableSceneRequest()
output.doSomething(request: request)
}

// MARK: Display logic
func displaySomething(viewModel: TableSceneViewModel) {
// NOTE: Display the result from the Presenter
// nameTextField.text = viewModel.name
}

}




TableSceneWorker.swiftt
import UIKit

class TableSceneWorker {
// MARK: Business Logic
func doSomeWork() {
// NOTE: Do the work
}
}






Теперь самое время позаботиться о сервисе, который возьмет на себя работу с YouTube Data API и вернет нам требуемый результат. Подробно на создании сервиса останавливаться не будем, так как это стандартная реализация в коде, к паттерну он практически не имеет отношения, он будет вызываться в интеракторе путем передачи управления в отдельный менеджер. Я под спойлером просто приведу готовую реализацию класса, которую мы и будем использовать. В любом случае, в конце статьи будет ссылка на GitHub с готовым проектов в этой части статьи, можно просто скачать или форкнуть проект, чтобы подробно во всем разобраться!



YoutubeManager.swift
import Foundation
import UIKit

class YoutubeManager {

/// Синглтон для YoutubeManager
static let sharedInstance = YoutubeManager()

/**
Получение массива с сущностями "Видео"
- parameter playlistID: ID нашего плейлиста (не опциональная строка)
*/
func getVideosForChannelWithPlaylistID(playlistID: String!, completion: (array: Array) -> Void) {
let urlString = "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails&maxResults=50&playlistId=\(playlistID!)&key=\(Config.GoogleDataKey)"
let targetURL = URL(string: urlString)
performGetRequest(targetURL: targetURL) { data, HTTPStatusCode, error -> Void in
if HTTPStatusCode == 200 && error == nil {
do {
let resultsDict = try JSONSerialization.jsonObject(with: data! as Data, options: []) as! Dictionary
let items: Array> = resultsDict["items"] as! Array>
var array = Array()
for i in 0 ..< items.count {
let playlistSnippetDict = (items[i] as Dictionary)["snippet"] as! Dictionary
if (playlistSnippetDict["thumbnails"] as? Dictionary) != nil {
let publishedAt = playlistSnippetDict["publishedAt"] as! String!
let title = playlistSnippetDict["title"] as! String!
let description = playlistSnippetDict["description"] as! String!
let videoID = (playlistSnippetDict["resourceId"] as! Dictionary)["videoId"] as! String!
let thumbnail = ((playlistSnippetDict["thumbnails"] as! Dictionary)["default"] as! Dictionary)["url"] as! String!
let videoItem = VideoEntity(publishedAt: publishedAt, title: title, description: description, videoID: videoID, thumbnail: thumbnail)
array.append(videoItem)
} else {
continue
}
}
completion(array: array)
} catch {
completion(array: [])
}
} else {completion(array: [])}
}
} // getVideosForChannelWithPlaylistID

} // class DataAPI

/// Helper for perform data request
extension YoutubeManager {
/**
Подготавливаем "GET" запрос к нашему YouTube сервису
- parameter targetURL: ссылка для запроса (NSURL!)
- parameter completion: комплишен результата отработанного запроса
*/
private func performGetRequest(targetURL: NSURL!, completion: (data: NSData?, HTTPStatusCode: Int, error: NSError?) -> Void) {
let request = NSMutableURLRequest(url: targetURL! as URL)
request.httpMethod = "GET"
let sessionConfiguration = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfiguration)
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error -> Void in
DispatchQueue.main.async(execute: { () -> Void in
completion(data: data, HTTPStatusCode: (response as! HTTPURLResponse).statusCode, error: error)
})
})
task.resume()
} // performGetRequest

} // extension YoutubeManager




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





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



На этот раз мы унаследуемся от простого UIViewController и назовем: DetailedScene. Она будет использоваться для отображения и проигрывания видео из основного списка видео файлов.



Если все сделано правильно, структура проекта выглядит приблизительно так:



Структура проекта
image





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



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



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



TableScene



TableSceneConfigurator.swift
import UIKit

extension TableSceneViewController: TableScenePresenterOutput {
/// Переопределяем сегвей для контроллера
override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) {
router.passDataToNextScene(segue: segue)
}
}

extension TableSceneInteractor: TableSceneViewControllerOutput {}
extension TableScenePresenter: TableSceneInteractorOutput {}

class TableSceneConfigurator {
/// Настройка производится лишь один раз
class var sharedInstance: TableSceneConfigurator {
return TableSceneConfigurator()
}

/// Настройка и конфигурация контроллера
func configure(viewController: TableSceneViewController) {
/// Создаем роутер
let router = TableSceneRouter()
router.viewController = viewController
/// Создаем презентер
let presenter = TableScenePresenter()
presenter.output = viewController
/// Создаем интерактор
let interactor = TableSceneInteractor()
interactor.output = presenter
/// Связываем контроллер с иницированными зависимостями
viewController.output = interactor
viewController.router = router
}

}





TableSceneInteractor.swift
import UIKit

protocol TableSceneInteractorInput {
func doRequest(request: TableSceneRequest)
var videos: [VideoEntity]? { get }
}

protocol TableSceneInteractorOutput {
func presentData(response: TableSceneResponse)
}

class TableSceneInteractor: TableSceneInteractorInput {

var output: TableSceneInteractorOutput!
var worker: TableSceneWorker!
var videos: [VideoEntity]?

/// Показали, что был послан запрос, запускаем сервис и обрабатываем результат
func doRequest(request: TableSceneRequest) {
worker = TableSceneWorker()
worker.loadList { videos -> Void in
self.videos = videos
let response = TableSceneResponse(array: self.videos!)
self.output.presentData(response: response)
}
}

}




TableSceneModels.swift
import UIKit

/// Модель данных, специфичная для данного контроллера

/// Общий запрос
struct TableSceneRequest {}
/// Формат ответа
struct TableSceneResponse {
var array = Array()
}
/// Модель представления
struct TableSceneViewModel {
var array = Array()
}




TableScenePresenter.swift
import UIKit

protocol TableScenePresenterInput {
func presentData(response: TableSceneResponse)
}

protocol TableScenePresenterOutput: class {
func displayData(viewModel: TableSceneViewModel)
}

class TableScenePresenter: TableScenePresenterInput {

weak var output: TableScenePresenterOutput!

/// Возвращаем полученные данные для отображения в контроллере
func presentData(response: TableSceneResponse) {
let viewModel = TableSceneViewModel(array: response.array)
output.displayData(viewModel: viewModel)
}

}




TableSceneRouter.swift
import UIKit

protocol TableSceneRouterInput {
func navigateToNextController()
}

class TableSceneRouter: TableSceneRouterInput {

weak var viewController: TableSceneViewController!

/// Здесь можно произвести переход без использования сегвея
func navigateToNextController() {

}

/// Передача данных в следующий контроллер через сегвей
func passDataToNextScene(segue: UIStoryboardSegue) {
/// Проверили сегвей, так как в одном роутере мы можем использовать несколько контроллеров для перехода и передачи данных
if segue.identifier == "ShowDetailedScene" {
if let selectedIndexPath = viewController.tableView.indexPathForSelectedRow {
if let selectedVideo = viewController.output.videos?[(selectedIndexPath as NSIndexPath).row] {
let detailedViewController = segue.destinationViewController as! DetailedSceneViewController
detailedViewController.output.video = selectedVideo
}
}
}
}

}




TableSceneViewController.swift
import UIKit

protocol TableSceneViewControllerInput {
func displayData(viewModel: TableSceneViewModel)
}

protocol TableSceneViewControllerOutput {
func doRequest(request: TableSceneRequest)
var videos: [VideoEntity]? { get }
}

class TableSceneViewController: UITableViewController, TableSceneViewControllerInput {

var output: TableSceneViewControllerOutput!
var router: TableSceneRouter!
var videoArray: Array! = []

/// Нстройка контроллера при старте
override func awakeFromNib() {
super.awakeFromNib()
TableSceneConfigurator.sharedInstance.configure(viewController: self)
}

/// При полной загрузке делаем запрос данных
override func viewDidLoad() {
super.viewDidLoad()
loadData()
}

/// Передаем запрос далее в интерактор
func loadData() {
let request = TableSceneRequest()
output.doRequest(request: request)
}

/// Данные вернулись, их можно показать в контроллере
func displayData(viewModel: TableSceneViewModel) {
self.videoArray = viewModel.array
tableView.reloadData()
}

// MARK: TableView DataSource

override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return videoArray.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let videoitem = videoArray[(indexPath as IndexPath).row]
var cell = tableView.dequeueReusableCell(withIdentifier: "cell")
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
}
cell?.textLabel?.text = videoitem.title
cell?.detailTextLabel?.text = videoitem.description
return cell!
}

}




TableSceneWorker.swift
import UIKit

class TableSceneWorker {

/// Наш сервис обращается к менеджеру и позвращает результат в интерактор
func loadList(callback: (videos: Array) -> Void) {
let playlistID = VideoPlaylist()
YoutubeManager.sharedInstance.getVideosForChannelWithPlaylistID(playlistID: playlistID) { array -> Void in
callback(videos: array)
}
}

}






DetailedScene



DetailedSceneConfigurator.swift
import UIKit
import XCDYouTubeKit

extension DetailedSceneViewController: DetailedScenePresenterOutput {
/// Переопределяем сегвей для контроллера
override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) {
router.passDataToNextScene(segue: segue)
}
}

extension DetailedSceneInteractor: DetailedSceneViewControllerOutput {}
extension DetailedScenePresenter: DetailedSceneInteractorOutput {}

class DetailedSceneConfigurator {
/// Настройка производится лишь один раз
class var sharedInstance: DetailedSceneConfigurator {
return DetailedSceneConfigurator()
}

/// Настройка и конфигурация контроллера
func configure(viewController: DetailedSceneViewController) {
/// Создаем роутер
let router = DetailedSceneRouter()
router.viewController = viewController
/// Создаем презентер
let presenter = DetailedScenePresenter()
presenter.output = viewController
/// Создаем интерактор
let interactor = DetailedSceneInteractor()
interactor.output = presenter
/// Связываем контроллер с иницированными зависимостями
viewController.output = interactor
viewController.router = router
/// Создаем плеер для последующей работы с ним
viewController.videoPlayerViewController = XCDYouTubeVideoPlayerViewController()
}

}




DetailedSceneInteractor.swift
import UIKit

protocol DetailedSceneInteractorInput {
var video: VideoEntity! { get set }
func getVideoID(request: DetailedSceneRequest)
}

protocol DetailedSceneInteractorOutput {
func presentVideo(response: DetailedSceneResponse)
}

class DetailedSceneInteractor: DetailedSceneInteractorInput {

var output: DetailedSceneInteractorOutput!
var video: VideoEntity!

/// Показали, что был послан запрос и обрабатываем результат
func getVideoID(request: DetailedSceneRequest) {
let response = DetailedSceneResponse(video: video)
output.presentVideo(response: response)
}

}




DetailedSceneModels.swift
import UIKit

/// Модель данных, специфичная для данного контроллера

/// Общий запрос
struct DetailedSceneRequest {}
/// Формат ответа
struct DetailedSceneResponse {
var video: VideoEntity
}
/// Модель представления
struct DetailedSceneViewModel {
var videoID: String!
}




DetailedScenePresenter.swift
import UIKit

protocol DetailedScenePresenterInput {
func presentVideo(response: DetailedSceneResponse)
}

protocol DetailedScenePresenterOutput: class {
func displayVideo(viewModel: DetailedSceneViewModel)
}

class DetailedScenePresenter: DetailedScenePresenterInput {

weak var output: DetailedScenePresenterOutput!

/// Возвращаем полученные данные для отображения в контроллере
func presentVideo(response: DetailedSceneResponse) {
let viewModel = DetailedSceneViewModel(videoID: response.video.videoID)
output.displayVideo(viewModel: viewModel)
}

}




DetailedSceneRouter.swift
import UIKit

protocol DetailedSceneRouterInput {
func navigateToNextController()
}

class DetailedSceneRouter: DetailedSceneRouterInput {

weak var viewController: DetailedSceneViewController!

/// Здесь можно произвести переход без использования сегвея
func navigateToNextController() {

}

/// Передача данных в следующий контроллер через сегвей
func passDataToNextScene(segue: UIStoryboardSegue) {
if segue.identifier == "OtherScene" {

}
}

}




DetailedSceneViewController.swift
import UIKit
import XCDYouTubeKit

protocol DetailedSceneViewControllerInput {
func displayVideo(viewModel: DetailedSceneViewModel)
}

protocol DetailedSceneViewControllerOutput {
var video: VideoEntity! { get set }
func getVideoID(request: DetailedSceneRequest)
}

class DetailedSceneViewController: UIViewController, DetailedSceneViewControllerInput {

@IBOutlet weak var videoContainerView: UIView!
var videoPlayerViewController: XCDYouTubeVideoPlayerViewController!

var output: DetailedSceneViewControllerOutput!
var router: DetailedSceneRouter!

/// Нстройка контроллера при старте
override func awakeFromNib() {
super.awakeFromNib()
DetailedSceneConfigurator.sharedInstance.configure(viewController: self)
}

/// При полной загрузке делаем запрос данных
override func viewDidLoad() {
super.viewDidLoad()
getVideoID()
}

/// Передаем запрос далее в интерактор
func getVideoID() {
let request = DetailedSceneRequest()
output.getVideoID(request: request)
}

/// Данные вернулись, их можно показать в контроллере
func displayVideo(viewModel: DetailedSceneViewModel) {
videoPlayerViewController.present(in: videoContainerView)
videoPlayerViewController.videoIdentifier = viewModel.videoID
videoPlayerViewController.moviePlayer.prepareToPlay()
}

}




DetailedSceneWorker.swift
import UIKit

class DetailedSceneWorker {

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

}






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



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



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



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



P.S.: Ссылка на проект в GitHub: github.com/InstaRobot/CleanApp-Swift3



Какие паттерны Вы используете в разработке?


























































Проголосовал 1 человек. Воздержавшихся нет.





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


Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/306206/

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

Кулуары VR. Библиотеки для отображения панорам 360*

Пятница, 22 Июля 2016 г. 16:20 (ссылка)





Всем привет! VR нынче в тренде, тема злободневная, различных проектов и способов реализовывать отображение трёхмерной картинки, кажется, скоро будет миллион. Сегодня у нас в гостях Михаил Вайсман, основатель компании по разработке мобильных приложений Trinity Digital. Он вызвался рассказать про работу с библиотеками для отображения 360-градусных панорам, за что мы выражаем ему признательность. Ладно, вы все пришли сюда за интересным опытом, а не за контентом до ката. ;)





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



Привет, Хабр! Представить меня уже успели, но правила есть правила. Я — Михаил Вайсман. Опыт работы с VR я приобрел, работая в Trinity Digital над проектом Airpano Virtual Travel вместе с командой Doubble. Airpano — приложение с возможностью просмотра VR фото- и видеопанорам. В процессе работы я столкнулся с проблемой: тема VR уже не первый год в тренде, но готовых библиотек, позволяющих качественно отобразить VR контент, нет. Пришлось выбирать между написанием с нуля собственного решения и применением того, что уже есть на рынке. Выбрали компромиссный вариант — взять имеющееся на рынке open source решение и дорабатывать под наши цели. У последного варианта есть свои плюсы, но и о минусах забывать не стоит. Мы попробовали три библиотеки, и вот что из этого вышло:



Krpano — библиотека на основе WebView



Мы начали работу с варианта, реализованного на основе WebView — с библиотеки krpano. Причиной выбора стали:




  • Быстрая возможность интеграции в проект;


  • Декларация поддержки всех Android устройства с версией не ниже 4.2;


  • Универсальность решения, возможность переиспользования для разных платформ.






В реальности с библиотекой krpano все оказалось не столь радужно, потому что у krpano браузерный движок. Начать можно хотя бы с того, что не на всех устройствах есть современные движки хрома, которые могут максимально эффективно реализовывать возможности данной библиотеки. Большинство не самых топовых устройств работали медленно с панорамами исключительно из-за использования webview, а на части из них библиотека и вовсе не работала. В качестве потенциального решения мы попробовали интегрировать в приложение движки от Chrome и Firefox (сначала один, потом другой — для тестов). В какой-то степени это помогло. Работала данная штука везде, вот только…



В итоге приложение занимало более 100 мегабайт — и это не наши файлы или код, а библиотека, плюс браузерный движок в проекте. Самое обидное, что даже эти извращения не помогли значительно оптимизировать качество и скорость работы — они оставались на непримлемо низком уровне. Решающим минусом krpano стала необходимость программировать на уровне xml-конфигураций (подобное конфигурирование проходит долго и увеличивает цену разработки для каждой панорамы).

https://habrahabr.ru/post/306246/

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

Разработка для SailfishOS: основы

Пятница, 22 Июля 2016 г. 15:56 (ссылка)

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



В состав SailfishOS SDK (работа с которым была описана в прошлой статье) входит Sailfish Silica — QML модуль, использующийся для создания Sailfish приложений. Данный модуль содержит QML компоненты, которые выглядят и управляются в соответствии со стандартами приложений для Sailfish. Помимо прочего, Sailfish Silica так же содержит инструменты для создания специфических элементов Sailfish приложений, таких как, например, Cover Page, которые были немного затронуты в прошлой статье. Для того, чтобы воспользоваться модулем Sailfish Silica и его инструментами и компонентами, необходимо просто импортировать данный модуль в QML файлы кода приложения. Это будет показано в примере чуть позже.



ApplicationWindow



QML основной любого Sailfish приложения является компонент ApplicationWindow, который описывает окно приложения и содержит пользовательский интерфейс приложения и вообще является основной и обязательной входной точкой загрузки Sailfish приложения. Экраны же приложения реализуются при помощи компонента Page. При этом ApplicationWindow содержит свойство initialPage, позволяющее установить начальный экран приложения. Таким образом минимальное приложение для платформы Sailfish будет выглядеть следующим образом:

import QtQuick 2.2
import Sailfish.Silica 1.0

ApplicationWindow {
initialPage: Component {
Page {
Label {
text: "Привет, Хабр!"
anchors.centerIn: parent
}
}
}
}


Данное приложение будет отображать одну простую страницу с надписью Привет, Хабр! посередине. Не обязательно описывать саму страницу прямо в описании свойства, можно просто передать туда id страницы или URL файла, где описана страница.



Page Stack



Помимо начальной страницы, ApplicationWindow так же содержит свойство pageStack — содержащее компонент Page Stack, позволяющий управлять стеком экранов (или страниц) приложения. В примере выше, Page Stack состоит всего из одной страницы, которая была положена на этот стек с помощью свойства initialPage. Добавить страницу на верх стека страниц (и, соответственно, отобразить ее на экране) можно с помощью метода push(), передавая ему в качестве аргумента путь до QML файла со страницей либо id этой страницы. Расширим наш пример, добавив под надпись кнопку, при нажатии на которую будет происходить переход на следующую страницу (будем предполагать, что код этой страницы содержится в файле SecondPage.qml):

ApplicationWindow {
initialPage: initialPage
Page {
id: initialPage
Label {
id: helloLabel
text: "Привет, Хабр!"
anchors.centerIn: parent
}
Button {
text: "Следующий"
anchors.top: helloLabel.bottom
anchors.horizontalCenter: parent.horizontalCenter
onClicked: pageStack.push(Qt.resolvedUrl("SecondPage.qml"))
}
}
}


В метод push() так же, вместо одной страницы, можно передать массив, содержащий несколько страниц. В таком случае все эти страницы будут добавлены на стек. У данного метода есть еще два опциональных аргумента. Первый — это параметры страницы, а второй — тип операции, определяющий нужно ли использовать анимацию при переходе на заданную страницу. Он может быть одним из двух значений: PageStackAction.Animated или PageStackAction.Immediate. Данный аргумент так же присутствует и во всех других методах Page Stack, которые отвечают за смену текущей страницы (например, метод pop, который будет рассмотрен далее). По умолчанию все переходы осуществляются с анимацией, что удобно. Если по какой-то причине анимация при переходе не нужна, можно вызвать метод следующим образом:

pageStack.push(Qt.resolvedUrl("SecondPage.qml"), { }, PageStackAction.Immediate)


Для того, чтобы вернуться на предыдущую страницу нужно на компоненте Page Stack вызвать метод pop(). Этот метод уберет со стека самую верхнюю страницу и, соответственно, перейдет на страницу назад. Опционально методу можно так же указать некоторую страницу, которая уже есть на стеке. В этом случае, метод уберет со стека все страницы, расположенные на стеке выше указанной. Здесь так же стоит отметить, что переход назад в платформе Sailfish OS реализован с помощью горизонтального свайпа от левого края экрана, однако в Sailfish Silica данный функционал уже реализован и при данном жесте автоматически вызывается метод pop(), что удобно, поскольку разработчику не нужно прилагать усилий для реализации стандартного функционала.



Кроме вышеописанных методов, Page Stack так же предоставляет такие свойства как depth (количество страниц в стеке) и currentPage (текущая страница), а также такие методы, как:


  • replace() — заменяет текущую верхнюю страницу на стеке,

  • pushAttached() — добавляет указанную страницу на верх стека, но не осуществляет переход к ней (пользователь может перейти к данной странице с помощью горизонтального свайпа от правого края экрана),

  • navigateForward() и navigateBack() — осуществляют переход на, соответственно, следующую или предыдущую страницу в стеке относительно текущей, при этом не изменяя сам стек.



Конечно, выше описаны не все методы и свойства компонента Page Stack. Однако, основные, которые могут пригодиться в первую очередь, я попытался описать. Подробнее про компонент можно прочитать в официальной документации.



Dialog



Диалоги в Sailfish OS представляют собой те же самые страницы. Однако, предназначены они для того, чтобы отобразить пользователю некоторые данные, с которыми он может согласиться или не согласиться. Причем сделать он может это как нажатием на кнопки, так и свайпом влево (согласиться) или вправо (отказаться). Поскольку, диалог представляет собой особую страницу, то и в Sailfish Silica компонент Dialog «наследуется» от компонента Page. Заменим в предыдущем примере переход на следующую страницу на показ минимального диалога. Для этого опишем диалог в нашем ApplicationWindow:

ApplicationWindow {
initialPage: initialPage
Page {
id: initialPage
Label {
id: helloLabel
text: "Привет, Хабр!"
anchors.centerIn: parent
}
Button {
text: "Следующий"
anchors.top: helloLabel.bottom
anchors.horizontalCenter: parent.horizontalCenter
onClicked: pageStack.push(dialog)
}
}
Dialog {
id: dialog
Label {
text: "Я - диалог"
anchors.centerIn: parent
}
}
}


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



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

onAccepted: helloLabel.text = "Согласился"
onRejected: helloLabel.text = "Отказался"


Так же на диалоге можно с помощью компонента DialogHeader добавить стандартные кнопки «Cancel» и «Accept» вверху диалога. При этом для отображения данных кнопок достаточно просто добавить пустой компонент. Опционально можно так же указать свойство title, которое определит текст, который будет расположен под кнопками. Данный текст обычно используется для отображения вопроса к пользователю. Добавим DialogHeader в диалог из примера выше:

Dialog {
id: dialog
DialogHeader {
title: "Простой диалог"
}
Label {
text: "Я - диалог"
anchors.centerIn: parent
}
onAccepted: helloLabel.text = "Согласился"
onRejected: helloLabel.text = "Отказался"
}


Теперь он выглядит так:





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

[W] unknown:189 - file:///usr/lib/qt5/qml/Sailfish/Silica/DialogHeader.qml:189: TypeError: Cannot read property 'backIndicatorDown' of null
[W] unknown:194 - file:///usr/lib/qt5/qml/Sailfish/Silica/DialogHeader.qml:194: TypeError: Cannot read property 'backIndicatorDown' of null
[W] unknown:247 - file:///usr/lib/qt5/qml/Sailfish/Silica/DialogHeader.qml:247: TypeError: Cannot read property 'forwardIndicatorDown' of null
[W] unknown:242 - file:///usr/lib/qt5/qml/Sailfish/Silica/DialogHeader.qml:242: TypeError: Cannot read property 'forwardIndicatorDown' of null


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



Подробнее о Dialog можно почитать в официальной документации.



Жизненный цикл приложений и Cover



Жизненный цикл приложений для Sailfish OS весьма прост. Поскольку в платформе реализована полноценная многозадачность, приложение может находится в одном из трех состояний: либо оно не запущено вовсе, либо оно работает в фоне (background), либо работает в активном режиме (foreground). При этом в активном режиме приложение развернуто во весь экран, тогда как в фоновом режиме приложение представлено своей миниатюрой (называемой cover) на главном экране системы (об этом было немного написано в предыдущей статье). Определить в каком именно состоянии находится приложение можно с помощью свойства Qt.application.state. Если приложение находится в фоне, данное свойство принимает значение Qt.ApplicationInactive. В противном случае — Qt.ApplicationАctive. Состояние приложения необходимо знать и использовать, например, для остановки тяжелых вычислительных задач или анимаций, когда приложение находится в фоне, чтобы не тратить ресурсы системы.



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



Установить свой cover можно с помощью свойства cover компонента ApplicationWindow. Переделаем пример выше так, чтобы при нажатии на кнопки в диалоге менялся текст не на главном экране приложения, а на его cover:

ApplicationWindow {
initialPage: initialPage
cover: cover
Page {
id: initialPage
// Описание главной страницы приложения...
}
Cover {
id: cover
transparent: true
Label {
id: coverLabel
text: "Привет, Хабр!"
anchors.centerIn: parent
}
}
Dialog {
id: dialog
DialogHeader {
title: "Простой диалог"
}
Label {
text: "Я - диалог"
anchors.centerIn: parent
}
onAccepted: coverLabel.text = "Согласился"
onRejected: coverLabel.text = "Отказался"
}
}


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



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


  • Cover.Inactive — миниатюра не видна и пользователь не может с ней взаимодействовать,

  • Cover.Active — миниатюра видна и пользователь может с ней взаимодействовать,

  • Cover.Activating — миниатюра переходит в статус Cover.Active,

  • Cover.Deactivating — миниатюра переходит в статус Cover.Inactive.





Помимо этого cover так же может предоставлять пользователю возможность управления приложением непосредственно с самой миниатюры. Для этого с помощью компонента CoverActionList, внутри которого определяются компоненты CoverAction, можно добавить на миниатюру кнопки. Например, для музыкального плеера это могут быть кнопки остановки и воспроизведения композиции, а также кнопки перехода на следующий или предыдущий трек. Добавим кнопку управления на миниатюру из нашего примера. Эта кнопка будет менять надпись на нашей миниатюре:

Cover {
id: cover
transparent: true
Label {
id: coverLabel
text: "Привет, Хабр!"
anchors.centerIn: parent
}
CoverActionList {
CoverAction {
iconSource: "image://theme/icon-cover-next"
onTriggered: coverLabel.text = "Следующий!"
}
}
}


Подробнее про Cover можно прочитать в официальной документации.



Ориентация устройства



Как и другие мобильные устройства, устройства на платформе Sailfish OS поддерживают две возможных ориентации экрана: портретную и ландшафтную. Для того, чтобы узнать текущую ориентацию устройства можно воспользоваться свойствами isPortrait и isLandscape компонента Page, либо свойством orientation, которое принимает одно из следующих значений: Orientation.Portrait, Orientation.Landscape, Orientation.PortraitInverted или Orientation.LandscapeInverted. Так же, если важно проверить например, что устройство находится в портретной ориентации, а инвертировано оно или нет — не важно, то можно сравнить значение свойства orientation с маской Orientation.PortraitMask. Для ландшафтного режима существует аналогичная маска Orientation.LandscapeMask.



Если необходимо, чтобы приложение работало только в определенных ориентациях, то можно воспользоваться свойством allowedOrientations компонента ApplicationWindow, которому можно указать, в каких ориентациях должно работать приложение. В качестве значений можно указывать те же значения, что возвращает свойство orientation, а так же маски Orientation.LandscapeMask, Orientation.PortraitMask или Orientation.All. Значение по умолчанию свойства allowedOrientations зависит от конкретного устройства, поэтому если для приложения важно, что оно должно работать в определенных ориентациях (или в любых), то лучше указать это явно. Кроме того, это свойство можно указать и у компонента Page, тогда правило разрешенных ориентаций будет применяться только к конкретной странице.



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



В следующей статье я расскажу о других компонентах, входящих в состав Sailfish Silica.



Автор статьи: Денис Лаурэ
Original source: habrahabr.ru.

https://habrahabr.ru/post/306188/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

Любовь, похожая на код

Пятница, 22 Июля 2016 г. 14:15 (ссылка)

Женские вкусы — штука изменчивая. Судить можно хотя бы по популярным песням. Вспомните: «Парней так много холостых, а я люблю женатого», потом «Любите, девушки, простых романтиков, отважных лётчиков и моряков», в 90-е — «А я люблю военных, красивых, здоровенных...крутых и всяких деловых», в начале 2000-х — «Робот, робот, робот, я тебя люблю, мы так хотели…» Хотя, погодите, всё началось ещё в далёком 1965 году — тогда Алла Пугачёва пела «Неужели в две тысячи первом году нам заменят сердца на транзисторы?… Робот - это выдумка века. Я прошу, ну попробуй, стань опять человеком…»

И вот настала очередь создателей роботов и приложений — программистов и инженеров. «Мой парень-программист» из мемов и забавных записок в ЖЖ превратился в социальное явление. Попробуем разобраться, почему это произошло, на что способны программисты и какой он, идеальный мужчина XXI века? Читать далее

https://habrahabr.ru/post/306212/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

Монетизация по рекламной модели через Appodeal

Четверг, 21 Июля 2016 г. 19:31 (ссылка)

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

https://habrahabr.ru/post/306042/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best

Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

App Intro c использованием видео из YouTube

Четверг, 21 Июля 2016 г. 17:14 (ссылка)

Привет, друзья! Некоторое время назад я писал статью про App Intro (Onboarding Experience), с помощью которого мы можем дать юзеру нашего приложения ознакомительный материал при первом запуске.



Теперь же я хочу рассказать и показать, как можно реализовать App Intro c помощью видео из YouTube.

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













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



Итак приступим к реалзации:



1) Для начала нам нужно создать проект и получить API Key в Google Developer Console:



a) создаем проект и активируем YouTube Data API:







b) Создаем API Key для Android приложения: даем название (произвольное), пакет своего проекта и генерируем SHA-1







После этих шагов вы получите API Key, такого плана: AIzaSyCKQSxрJFAHFHj9r2wfwfqkagkhFFa



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



2) Создаем новый проект в Android Studio:



a) добавляем в strings.xml:



    Introduction
Skip Intro
Error initializing Youtube player: %1$s




b) в colors.xml:




#9e9e9e
#616161




c) в dimens.xml:




8dp
40dp




d) создадим директорию color и в ней файл intro_text_link.xml (будем применять этот стиль в layouts):













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

Скачиваем архив библиотеки YouTube по этой ссылке.



Далее, нужно скопировать и вставить файл YouTubeAndroidPlayerApi.jar в app/libs (в студии переключиться на Project), и нажать кнопку Sync на панеле инструментов (или в меню: Tools > Android > Sync Project with Gradle Files)



3) Теперь создадим Java класс Config, в котором будем хранить Google API Key и id для видео, которое мы будем использовать в нашем Intro (в URL следует после "?v=", например, для видео www.youtube.com/watch?v=aScEqSidNf8 id будет aScEqSidNf8):



public class Config {

//Google API Key
static final String API_KEY = "AIzaSyCKQSxрJFAHFHj9r2wfwfqkagkhFFa";

//YouTube video id for intro
static final String YOUTUBE_INTRO_ID = "aScEqSidNf8";
}




4) Создадим теперь xml файлы внутри папке drawable, которые будут отвечать за background нашего Intro и за скругление углов:



a) intro_gradient.xml:











b) intro_rounded_corners.xml:
















5) Теперь создадим layout файл activity_intro.xml для нашего Intro (вертикальная ориентация):
























И для горизонтальной ориентации (создаем директорию layout-land, в которую помещаем идентичный файл activity_intro.xml):
























6) Создаем класс YouTubeFailureRecoveryActivity. Выносим его в отдельный класс, поскольку в случае наличия YouTube-функционала в других частях приложение, используется этот же класс:



public abstract class YouTubeFailureRecoveryActivity extends YouTubeBaseActivity implements
YouTubePlayer.OnInitializedListener {

private static final int RECOVERY_DIALOG_REQUEST = 1;

@Override
public void onInitializationFailure(YouTubePlayer.Provider provider,
YouTubeInitializationResult errorReason) {
if (errorReason.isUserRecoverableError()) {
errorReason.getErrorDialog(this, RECOVERY_DIALOG_REQUEST).show();
} else {
String errorMessage = String.format(getString(R.string.error_player), errorReason.toString());
Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show();
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == RECOVERY_DIALOG_REQUEST) {
// Retry initialization if user performed a recovery action
getYouTubePlayerProvider().initialize(Config.API_KEY, this);
}
}

protected abstract YouTubePlayer.Provider getYouTubePlayerProvider();

}




7) Создаем IntroActivity.java:



public class IntroActivity extends YouTubeFailureRecoveryActivity {

YouTubePlayerView youTubeView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_intro);

youTubeView = (YouTubePlayerView) findViewById(R.id.youtube_view);
youTubeView.initialize(Config.API_KEY, this);

// Закрываем Intro при клике на ссылку Skip Intro
TextView skipIntro = (TextView) findViewById(R.id.skip_intro);
skipIntro.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
/* Чтобы Intro можно было посмотреть позже, например, из главного активити
* и чтобы это активити не открывалось повторно при закрытии Intro, добавим
* экстра к интенту вызывающего активити: intent.putExtra(EXTRA_TEXT, "MainActivity").
* А здесь проверяем: если есть экстра - значит Intro было запущено из
* активити - тогда при клике на Skip Intro просто закрываем Intro.
* А если нет экстра - значит Intro было запущено при первом открытии аппа -
* сначала открываем главное активити, и затем закрываем Intro. */
Intent intent = getIntent();
String extra = intent.getStringExtra(MainActivity.EXTRA_TEXT);
if (extra == null) {
intent.setClass(getApplicationContext(), MainActivity.class);
startActivity(intent);
}
finish();
}
});
}


@Override
protected YouTubePlayer.Provider getYouTubePlayerProvider() {
return youTubeView;
}

@Override
public void onInitializationSuccess(YouTubePlayer.Provider provider, YouTubePlayer youTubePlayer, boolean b) {
if (!b) {
youTubePlayer.cueVideo(Config.YOUTUBE_INTRO_ID);
}
}
}





Последним шагом в реализации данного функционала остается добавление permissions в Manifest и декларация IntroActivity:




...





Так выглядит результат проделанной работы в вертикальной и горизонтальной ориентации:







Я постарался в этой статье рассказать полезную вещь, которая надеюсь пригодится вам для создания интересных проектов и будет полезна. Если у вас остались вопросы, то попробую ответить на них.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/306164/

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

[Перевод] Метрики против Опыта

Четверг, 21 Июля 2016 г. 16:18 (ссылка)

image



Данная публикация — местами вольный перевод статьи за авторством Julie Zhuo, продукт-дизайнера в Facebook. Приятного чтения.



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



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



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



«Делаем ли мы это просто для получения метрики?»

«Как мы можем сбалансировать полученные цифры и сделать при этом что-то достойное?»

И мой фаворит: «Вы, те, кто управляет данными, на самом деле заботитесь о пользователях и UX?»



Ох! Сильные слова и жгучие обвинения!



Может, хотите продуктивно поговорить о метриках и позитивном опыте? Вот что знаю я.



Не создавайте рамок в стиле «Метрики против Опыта»



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



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



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



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



И наконец, третья причина по которой метрики столь ценны в том, что они помогают сплотить команду вокруг чего-то ясного и ощутимого, благодаря чему люди могут видеть собственные результаты. С чисто материально-технической точки зрения трудно управлять 50 людьми с установкой «мы должны создать что-то крутое!». Конечно, каждый может махать кулаками после драки и кричать «Да! Мы сделали что-то удивительное! Это то, чего мы хотели добиться!», но когда это утро понедельника и команда А появляется в офисе и начинает расхваливать себя, команда Б может отреагировать примерно в таком стиле: «Гм, нет, это на самом деле кусок дерьма». Что вообще происходит? Как четко определить, что есть это самое «что-то крутое?».



Одним из способов решения этой проблемы является построение иерархической лестницы внутри коллектива. Вы можете обозначить конкретное лицо (или перечень лиц) в компании, чтобы те выступали «судьями», что есть «крутое», а что — нет. Если же вы предпочитаете работать без иерархии, то другой доступный вам метод — определение измеримой цели. «Сверхвысокое качество продукта означает, что 50% пользователей обратится к данной „фиче“ еще раз в течении недели». Теперь команда А и команда Б точно знают, к чему они идут изо дня в день и как близко они находятся к этой цели.



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



Плохие решения принимаются во имя «улучшения показателей»



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



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



а) «Изначально рейтинг кликов по данной функции составлял 2%. Когда мы внесли изменения, то рейтинг поднялся до 5%! Ура!».



В чем тут проблема? Увеличение кликов по функционалу не говорит нам о том, что UX был на самом деле улучшен. А что если я поменял все ссылки на сайте на «нажмите здесь, чтобы заработать 250 баксов»? Черт подери, клики реально будут ползти вверх! Но в конце концов люди поймут, что я на самом деле не дам им эти 250 баксов и обозлятся на меня. Они перестанут переходить по моим ссылкам, удалят приложение и влепят мне 1 звезду в магазине с припиской «FUUUUUU». И да, именно заглавными буквами. Мой бизнес будет разрушен, моя жизнь превратится в дерьмо. Конец.



б) Люди привыкли тратить на мое приложение 5 минут. Теперь же, после запуска последней введенной функции, они стали тратить всего 3 минуты. Э-э-э, вы чего?



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



с) «Изначально наше приложение с фотками котиков и мемами больше использовали в штате Иллинойс, но теперь у нас больше пользователей в штате Огайо».



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



Конечно, есть важные вещи, которые нельзя легко и точно измерить



Если бы мы могли читать мысли пользователя, то можно было бы теоретически спроектировать идеальный UX. К сожалению, все мы не Джина Грей (телепат из вселенной X-Men, прим.), поэтому нам приходится измерять все, что можно, делать все, чтобы построить обоснованные предположения на тему того, что же нужно людям. Но объем измеряемых данных имеет свои пределы и нам важно помнить об этом. Просто статистика того, что люди делают в вашем продукте не может ничего сказать вам о таких вещах как:



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

Увеличивает ли или уменьшает сбор статистических данных доверие людей к вашему продукту с течением времени;

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

Как люди видят ваш продукт в сравнении с аналогичными предложениями на рынке;

Какие вещи пользователям хотелось бы изменить, добавить или оставить в этом виде, как есть;

Каков будет уровень интереса к вашему продукту с течением времени.



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



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



Понимание цены сложности (Understanding the cost of complexity): каждый раз, когда вы добавляете в свое приложение новую функцию, вполне вероятно, что отслеживаемые вами метрики показывают положительную динамику (в конце концов, до этого момента никто не использовал функционал Х, а теперь все больше и больше людей обращаются к нему. И, кажется, что это не приводит к уменьшению использования функционала Y или Z, так что в целом это похоже на победу). Тем не менее, если вы продолжите добавлять новые функции, в какой то момент ваш продукт станет раздутым. Затем, внезапно, ваш конкурент пойдет вертикально вверх из-за того, что у него будет просто реализован функционал Q, который нравится части аудитории. Это парадокс выбора и размера затрат когнитивных усилий. И мы просто еще не поняли, как это точно измерить.



Понимание силы бренда (Understanding the power of brand): когда компании Apple или Nike выпускают на рынок свой новый продукт, многие склонны покупать его, даже не проводя какого-либо анализа, просто потому, что в прошлом у них был позитивный опыт, связанный с данным брендом. Но тоже самое не сработает, если на рынке появится какой-нибудь выскочка под брендом «Pear» (груша) или «Sike» с эквивалентным продуктом. На общем уровне мы все знаем и понимаем это. Тем не менее, нам трудно оценить силу бренда и превратить ее в число, которое мы сможем ежедневно отслеживать. Трудно понять, какое именно из тысячи действий повлияет на имидж компании, каковы будут выгоды и потери от от принятия того или иного решения.



Сила игры по-крупному (The power of big bets): нет, метрики не могут сказать вам, какое смелое действие нужно совершить, чтобы в будущем выиграть. Представьте себе 2008 год, когда смартфоны только начали появляться. Глядя на метрики своего сайта вы бы увидели крошечный кусочек трафика, приходящий с мобильных устройств. Вы бы, возможно, пришли к выводу, что в оптимизацию под мобильные устройства не стоит вкладывать сил, потому что это слишком небольшая часть вашей аудитории. Сегодня же мы пониманием дальновидность тех, кто сделал ставку именно на мобильный сегмент и кто сейчас пожинает лавры. Ни одна экспертиза текущего поведения пользователей не может точно сказать, в какую сторону вам нужно «прыгнуть». Стратегическое, долгосрочное планирование по-прежнему требует того же, чего и всегда: доверять своему нутру.



Некоторые правила чистоплотных метрик



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



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



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



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



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



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



Используйте качественные исследования, чтобы получить ответы. Используйте количественные данные, которые говорят вам, что люди делали в приложении, в паре с качественными исследованиями, которые дают вам представление о людских эмоциях и чувствах. Проведите юзабилити-тесты, используйте фокус-группы, запустите опросы и узнайте, что стоит за поведением, которое вы наблюдаете.
Original source: habrahabr.ru (comments, light).

https://habrahabr.ru/post/306154/

Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

Как мы обновили и переписали iOS-приложение банка «Открытие»: кейс

Четверг, 21 Июля 2016 г. 14:35 (ссылка)

В жизненном цикле мобильного продукта рано или поздно наступает момент, когда нужно радикально обновиться. Потому что за время, прошедшее с запуска, выросли требования бизнеса и ожидания клиентов, изменились возможности платформы и средства разработки — и обновления становится невозможно реализовать путем “косметического ремонта”. В мире мобильных приложений жизненный цикл ПО составляет 2-3 года против 10-15 лет в обычном Enterprise-сегменте. Для нас с командой “Открытие Digital” момент радикального обновления мобильного банка настал в конце прошлого года.







От mobile first к mobile only



Мобильное приложение банка «Открытие» появилось в AppStore осенью 2014 года, когда пользователи имели айфоны 4-й и 5-й моделей с iOS 7 и 8, и была востребована довольно простая функциональность — перевод между счетами, платежи, конвертация, просмотр выписки, информация о картах и вкладах, поиск отделений и банкоматов. Считалось, что для более сложных операций клиентам удобно обратиться к интернет-банку или зайти в отделение.







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



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



В 2010 году известный веб-дизайнер Люк Вроблевски выдвинул концепцию разработки “mobile first”. Для того времени это была революционная идея — сначала разрабатывать UX и UI для мобильных устройств, а потом адаптировать проект для веба и десктопных приложений. Но это уже вчерашний день: появилась категория пользователей, для которых мобильное устройство является единственным каналом коммуникации, в том числе и в общении с банком.



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



Agile, кайдзен и кайкаку



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



image

Александр Нестеров, product owner, “Открытие Digital”

«Еще на стадии проектирования нашего первого приложения мы думали о том, как сделать его максимально удобным для пользователя и поэтому выбрали путь постоянных улучшений. Сначала мы шли достаточно длинными итерациями, первое большое обновление случилось спустя несколько месяцев, когда добавили возможность открытия вкладов. В банке тогда жили по стандартному waterfall’у и еще не умели дробить крупные фичи и вводить их по частям, но постепенно длина итераций начала сокращаться — мы перешли на Agile и сейчас релизимся каждые две-три недели. Это сильно помогает нам нащупывать правильный вектор развития и при этом страхует от серьезных ошибок».



Разработкой приложения мы, Redmadrobot, занимаемся в суперплотном взаимодействии с клиентом, “Открытие Digital”, на всех уровнях: от серверной команды и службы безопасности до продуктовиков и топ-менеджмента (о рабочем процессе мы расскажем подробнее в одном из будущих постов). Последние два месяца с нашей стороны на проекте работало две группы разработчиков. Одна занималась развитием и поддержкой текущего приложения, а другая — разработкой нового (фултайм два iOS-разработчика, аналитик, дизайнер и администратор проекта). А core team в лице тимлида, дизайн-команды и менеджеров проекта была занята сразу по обоим направлениям. Чтобы уложиться в запланированный срок релиза, задачи каждого члена команды были распланированы на каждый день на два месяца вперед и постоянно корректировались по ходу работы.







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



Это кайдзен — путь постоянных небольших улучшений. Но в определенный момент наступает необходимость провести радикальное изменение системы, чтобы выйти на новый уровень — в японской бизнес-терминологии его называют кайкаку. В нашем случае этот момент наступил в конце прошлого года, когда совпали два фактора: со стороны бизнеса и пользователей назрела потребность в серьезном расширении функционала, которому стало тесно в прежнем дизайне, а с технической стороны нужно было «обрубить хвосты» — перестать поддерживать старые версии iOS и перейти на Swift, чтобы создать платформу для будущего развития. Задачи на старте работы над проектом были следующими.



Со стороны бизнеса:


  • Персонализация сервиса;

  • Повышение удобства пользования;

  • Увеличение транзакционного дохода;

  • Наращивание клиентской базы;



С технической стороны:


  • Прекращение поддержки старых версий iOS (только 9 и выше);

  • Перевод разработки с Objective C на Swift;

  • Максимальное увеличение комфортного использования приложения за счёт средств платформы (быстрый доступ к функциям через 3D Touch, Spotlight Search и т.д.)





Таймлайн проекта, организация работы и юзабилити-тестирование





В ноябре 2015 года мы начали обсуждать логику нового приложения «Открытия»: составили свое видение развития продукта, получили от банка road map, а дальше началась работа по приведению их в соответствие друг другу. К январю мы представили клиенту дизайн-концепцию — свое видение идеального мобильного банка — каким должно быть банковское приложение, если представить, что нет ограничений по ресурсам и интеграции с системами заказчика. Но тут, конечно, пришлось приоритизировать.



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







А в середине апреля мы приступили к разработке, которая заняла 2,5 месяца.



Проектирование, дизайн и юзабилити



image

Леонид Борисов, арт-директор Redmadrobot



«Перед нами стояла задача оптимизировать пользовательский workflow по наиболее частым действиям — сократить количество шагов в сценариях, сделать подачу контента более удобной и начать движение в сторону персонализации сервиса».



Платежный функционал



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


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

  • Оплата услуг и перевод средств теперь осуществляется в пару касаний

  • В поле оплаты появились быстрые кнопки пополнения с суммой

  • Быстрый доступ к обмену валют

  • Переработанный каталог услуг

  • Быстрый перевод средств по номеру телефона для клиентов «Открытия»









Расширение возможностей прелогин-зоны



Мы переработали прелогин-зону — неавторизованный режим перестал быть историей исключительно для клиентов «Открытия». Посмотреть информацию о банкоматах и офисах можно без авторизации, а кроме того, теперь в прелогин-зоне есть функционал P2P-переводов с карты на карту (раньше, чтобы сделать перевод, пользователь перенаправлялся в Safari на сайт банка).







Персонализация и “умный банк”




  • Новый раздел «Профиль» мы задумали как центр управления настройками аккаунта и коммуникации с пользователем

  • Персональные контекстные предложения от банка в “Профиле”

  • Блок коммуникации с пользователем на главном экране с персональными предложениями

  • Контекстные сall-to-action buttons по всем ключевым продуктам банка

  • Персонализация для банковских продуктов — например, если у пользователя карта «Открытия» с «Игрой престолов», то рисунок с ней будет на главном экране, а на странице самого продукта — стилизованное фоновое изображение





image

Павел Усачев, бизнес-аналитик Redmadrobot

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

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








Разработка



Проект полностью на Swift



image

Егор Тафланиди ( BepTep ), архитектор Redmadrobot

«Задача архитектора — сделать так, чтобы из проекта в проект использовались одни и те же решения — с такими проектами проще работать, и их проще поддерживать. В случае «Открытия», естественно, исключений не было, дизайн кода и идеология RESTful-взаимодействия с сервером остались неизменными, но проект был переписан с использованием нового языка программирования, что повлекло за собой обновление (зачастую, переписывание с нуля) старых библиотек, написанных на Objective-C. Фактически, слой обработки данных и взаимодействия с сервером был полностью переосмыслен и собран заново».



Мобильное приложение спроектировано с оглядкой на принципы SOA (service-oriented architecture), с разделением логики на слои Presentation / Business Logic / Persistency). Подробнее про этот подход вы можете прочитать в статьях (раз, два) нашего архитектора Егора Тафланиди. В зависимости от сложности сценариев и количества типов данных часть пользовательских историй написано в стиле MVVM в связке с RxSwift и нашей собственной библиотекой для биндинга (спасибо Роме Чуркину). А в части сценариев применяется «любимый» MVC с облегченными (lighter) view контроллерами и выделенными делегатами, data source и прочими вспомогательными объектами (любим принцип S из SOLID). Мы по-прежнему с большой аккуратностью применяем реактивное программирование и используем его только для простых подписок и преобразований потоков данных. Прежде всего это связано с легкостью поддержки, дебага и развития такого кода.



Так как это приложение — мобильный банк, мы уделяли первостепенное внимание соблюдению лучших практик и стандартов безопасности. Код регулярно проверяется лично ответственными специалистами по безопасности из банка, статическими анализаторами, периодически проводится внешний аудит и исследования. 0 (ноль) критических уязвимостей безопасности — это про приложение мобильного банка “Открытие”. В свое время данный проект послужил пионером и драйвером для развития практик по безопасности в Redmadrobot. И сейчас ни один проект не обходится без таких базовых вещей, как SSL-pinning, защита от подключений отладчика, от запуска на jailbreak-устройствах, отключение кэширования web-запросов, очень внимательное отношение к тем данным, которые сохраняются в БД, и многое-многое другое.



Мы продолжаем активно применять практику повторного использования кода, что позволяет придерживаться единой архитектуры и сократить время разработки на нескольких проектах, а также повысить надежность. Мы реиспользуем единый каркас бизнес-логики: сервисный слой, транспорт, ДАО и сейчас работаем над кодогенерацией парсеров, трансляторов и моделей БД. Вовсю используем общие наработки по цветам и шрифтам, при этом перечисления (enum) из Swift позволяют в полной мере развивать применение стилей. Расширения и кастомные элементы UI часто используются повторно на разных проектах.



Проект “Открытия” преимущественно написан на Swift 2.2.1. Представленный два года назад на WWDC язык Swift хоть и молод по традиционным меркам, уже пережил несколько релизов и из проприетарного перешел в open source. Несмотря на то, что еще не вся экосистема iOS перестроилась на новые рельсы, рискованным решение портировать разработку мобильного банка на Swift назвать нельзя. Главное — мы увидели, что на Swift можно соблюсти все требования по безопасности, и это стало отправной точкой. Инициатива шла от разработчиков. Потихоньку мы начали применять этого язык на проектах уже довольно давно (мы рассказывали об этом в кейсе про разработку “АльфаСтрахования Мобайл”). Поначалу стали делать один-два экрана на Swift, оставляя бизнес-логику на Objective-C, а затем внутри компании собралась команда энтузиастов, которая за несколько недель переписала на Swift весь наш шаблонный каркас для iOS-приложений, включая широкий пласт логики взаимодействия с сервером.



Это был не «перевод» с одного языка на другой, это было «изложение». Как в школе, когда текст нужно сначала запомнить, а потом написать заново. Возможно, другими словами. С точки зрения методологии разработки вообще нет особой разницы, на каком языке писать. В проекте “Открытия” осталось всего несколько модулей, реализацию которых не затронул переход на новый язык программирования. Например, внутри приложения в поле суммы, когда делаешь перевод, есть калькулятор, чтобы посчитать, сколько каждый должен, допустим, заплатить за ресторан — можно записать общую сумму и поделить на троих. Скорее всего, мы возьмем и просто встроим его в новое приложение. В целом на сегодня мы почти отказались от Objective-C и все новые проекты пишем на новом языке.



image

Антон Подеречин, iOS-разработчик Redmadrobot

«В Swift больше синтаксического сахара, на нем приятнее пишется, его легче форматировать и код в итоге выходит более читаемым. Да, еще не все идеально – дебаггер немного хромает, рефакторинг пока не работает, бывает в IDE подсветка синтаксиса отваливается. По большому счету если сравнивать с Objective-C, то у Swift в каком-то смысле даже меньше возможностей, например, в нем нет динамической типизации, поэтому нет каких-то вещей, которые можно сделать на Swift, но нельзя было сделать на ObjC. Разумеется, статическая типизация это с одной стороны плюс, а с другой даже немного минус потому что компилятор везде подсвечивает ошибки несовпадения типов и не дает вольничать как прежде. А динамизм Objective-C иногда нам очень помогал — например, так парсить JSON было удобнее. Но нельзя сказать, что это такая ужасная боль, чтобы нам захотелось вернуться на Objective-C».



Было понятно, что релиз приложения состоится летом, а уже в сентябре появится iOS 10, и нам удалось убедить банк ограничиться поддержкой только iOS 9 — в ней многие вещи удобнее, чем в более ранних версиях: работа с анимациями таблиц, Stack View, WebKit, Layout Guides, Layout Anchors, Touch ID Reuse Duration, Contacts Framework и так далее. Но это, конечно, не было просто нашей прихотью — решение было продиктовано аналитикой: версия iOS ниже 8 или 9 установлена у 13% пользователей, а все устройства, поддерживающие iOS 8, вполне могут обновиться до 9-й версии. Исключение составляют только iPhone 4 c iOS 7, но их в клиентской базе осталось совсем немного (и все они смогут пользоваться предыдущей версией мобильного банка).



Кэширование данных



image

Григорий Матвиевич ( fountainhead ), тимлид iOS-команды Redmadrobot

«Раньше мы хранили все данные только в рамках текущей сессии, и не сохраняли ничего в БД, поскольку со стороны банковских безопасников были возражения. При подготовке редизайна мы проработали этот вопрос, и всю информацию, не относящуюся к персональным банковским данным, сохраняем в БД. Благодаря нашей архитектуре кэширование взлетело по щелчку пальцев — нам потребовалось только написать модели данных для БД и трансляторы. Сейчас мы используем Realm, но не зависим от реализации и при желании сможем быстро перейти на CoreData или любую другую технологию».



Интеграция с банковскими системами



Мы работаем с серверной командой мобильного банка на стороне “Открытия”. Все данные клиентов, их аккаунты и заявки хранятся в многочисленных внутренних системах банка. Мы встречались с аналитиками “Открытия”, чтобы понять, какие у нас есть возможности, и помогали команде сервера мобильного банка с интеграцией в части модели данных. Были сложности, когда данные, которые представлялись нам частью одной сущности, хранились в разных системах. По каждому такому случаю приходилось тестировать и принимать отдельное решение, что лучше: делать несколько запросов из приложения или один запрос и объединять данные на сервере».



Так, данные по тарифам и лояльности хранятся в очень разных местах. Части данных по вкладам, например, вообще нет, но их можно вычислить на основе других данных. Всё это нам приходилось исследовать, а затем транслировать на обе части команды — нашу в Redmadrobot и серверную в “Открытии”.



Повторные операции, шаблоны и возможности платформы



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



Из несложного с точки зрения реализации, но приятного для пользователя — мы активно использовали возможности платформы, добавив в приложение 3D Touch и Spotlight Search — в iOS 9 он позволяет искать не только по базе контактов и заметкам, но и по объектам приложения, если они опубликованы. Таким образом, даже не заходя в приложение, через Spotlight человек может найти нужный шаблон и, нажав на него, сразу попасть на экран соответствующего платежа в мобильном банке.







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



Схема платежа и мгновенный расчет комиссии



Провайдеров платежей десятки или даже сотни, типов переводов много, поэтому делать уникальный экран для каждого из них невозможно. Еще давно в приложении был придуман специальный протокол: схемы платежа, в соответствии с которым сервер присылает набор полей, необходимых для проведения платежа. У этих полей могут быть ограничения: маска ввода, набор допустимых значений, регулярные выражения для проверки, зависимости от других полей и прочие. Помимо этого у каждого поля есть тип, который определяет интерфейс в приложении. Данный протокол был значительно доработан с учетом новых потребностей: дизайн банковских карт других банков, определение банка и корр счета по БИК, определение клиента банка по номеру телефона. Другой пример — раньше комиссия показывалась только в алерте после нажатия кнопки “Оплатить”. В момент оплаты пользователь не знал размер комиссии, теперь по протоколу сразу приходят поля, от которых зависит комиссия, что позволяет вычислять её налету.



Прочие улучшения



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



Мы начали работу по актуализации JSON банкоматов и офисов, это позволило убрать отображавшиеся раньше на карте дубликаты. Спасибо команде интернет-банка! И тут у нас большие планы: добавить метро, описания, где их еще нет, кнопку «пожаловаться на банкомат».



Чат



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







Что у нас в бэклоге



Как обычно, все классные идеи не уместились в свежий релиз :) Что же мы хотим добавить?




  • Более плотную интеграцию с CRM-системами банка — то, что мы сейчас сделали в рамках персональных предложений, — только первый шаг.

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

  • Дальнейшее наращивание платежного функционала: например, оплата штрафов ГИБДД. Довольно сложно сделать этот функционал действительно удобным образом, нужно уметь искать по гибкому набору реквизитов — номеру водительских прав, номеру и марке автомобиля, ПТС. С учетом всех нюансов нужно сделать такой поиск, мимо которого не пройдет ни один штраф.

  • Сервис оплаты по номеру мобильного телефона с автоматическим определением оператора. Для клиентов было бы удобно не заполнять лишнее поле, да и вообще, когда нас просят кому-то кинуть денег на телефон, оператора мы не всегда помним. Но банку обязательно надо знать поставщика услуг связи, чтобы провести платеж. Существует простая база кодов мобильных операторов, но после вступления в силу закона об отмене «мобильного рабства» появилось 4 миллиона людей, оператора которых по коду определить нельзя, нужна дополнительная интеграция со сторонним сервисом.

  • Информативные графики по вкладам и состоянию счетов.





Также в планах добавление для переводов карт других банков, оплата ЖКХ по штрих-коду, оплата парковок и многое другое. На последней WWDC был представлен Siri Kit, Apple открыла API для общения со своим умным помощником, и это предоставляет огромные перспективы для новых пользовательских сценариев, в том числе и для мобильного банка «Открытие».



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



Пользователи, чьи устройства работают под управлением iOS 9 и выше, могут скачать обновленное приложение. Для тех, у кого iOS 8, доступна последняя версия в старом дизайне, которую мы также максимально обновили — там появилось переименование продуктов, создание шаблонов, досрочное погашение кредитов. Старое приложение продолжит работать и поддерживаться, но перестанет развиваться.



Так мы делали новое «Открытие». Как в приложение не уместились все классные идеи, так и в эту статью не вошло все, что мы хотели рассказать о проекте, поэтому мы запланировали еще несколько постов — о том, как работали над MVP и делали чат в приложении. Stay tuned :)







Запилили!
Original source: habrahabr.ru.

https://habrahabr.ru/post/306116/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best

Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

[Перевод] Agile API — возможно ли?

Вторник, 19 Июля 2016 г. 14:25 (ссылка)

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



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





Наше внутреннее API и его использование



Наш Badoo API (протокол обмена сообщениями) представляет собой набор структур данных (сообщений) и значений (enums), которыми обмениваются клиенты и сервер. Структура протокола у нас задаётся на основе определений Google protobuf и хранится в отдельном репозитории git. На основе этих определений генерируются классы моделей для различных платформ.



Он используется для шести платформ — сервера и пяти клиентов: Android, iOS, Windows Phone, Mobile Web и Desktop Web. Кроме того, тот же самый API используется в нескольких самостоятельных приложениях на каждой платформе. Чтобы все эти приложения и сервер могли обмениваться требуемой информацией, наш API “вырос” достаточно большим. Немного цифр:




  • 450 сообщений, 2665 полей.

  • 135 enum’ов, 2096 значений.

  • 125 флагов фич, которыми можно управлять на стороне сервера.

  • 165 флагов клиентского поведения, которые сообщают серверу особенности поведения клиента и поддерживаемого клиентом протокола.





Когда это нужно сделать? Вчера!



В Badoo мы стараемся реализовывать новые функции как можно быстрее. Логика проста: чем скорее появится версия с новыми возможностями, тем быстрее пользователи смогут ими воспользоваться. Кроме того, параллельно мы проводим A/B-тестирование, но в этой статье мы не станем подробно останавливаться на нем.



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



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



С какой стороны подойти?



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

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




  • Product Owner;

  • дизайнеры;

  • разработчики серверных решений;

  • разработчики API

  • разработчики клиентских приложений для разных платформ;

  • QA;

  • аналитики данных.





Как можно гарантировать, что все они понимают друг друга и говорят на одном языке? Нам нужен документ с описанием требований к функциональности (PRD).Обычно такой документ готовит Product Owner. Он создает вики-страницу с перечнем требований, вариантами использования, описанием флоу, эскизами дизайна и т. д.



На основе PRD мы можем приступать к планированию и реализации необходимых изменений в API.



Тут опять не всё так просто.



Подходы к проектированию протокола



Существует много подходов к «распределению обязанностей» между сервером и клиентом. Подходы варьируются от «вся логика реализована на сервере» до «вся логика реализована в клиенте». Обсудим «за» и «против» каждого подхода:



Вариант 1. Вся логика реализована на сервере (клиент работает как View из шаблона MVC).



Плюсы:


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

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





Минусы:


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

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

  • Может негативно повлиять на удобство использования приложения через медленное или нестабильное соединение.

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





Вариант 2. Вся логика реализована в клиенте — клиентское приложение содержит всю логику и использует сервер как источник данных (характерно для большинства публичных API).



Плюсы:


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

  • Лучше работает в автономном режиме и через медленную или нестабильную сеть.

  • Значительно упрощается кэширование.

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

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





Минусы:


  • Занимает больше времени. Всю логику необходимо реализовать на каждом из клиентов, а не один раз на сервере.

  • Для реализации даже самых незначительных изменений необходимо релизить каждое клиентское приложение.

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





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



Техническая документация



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



Документация в спецификации протокола



Затем появились еще три клиентские платформы: Windows Phone, Mobile Web и Desktop Web, да количество разработчиков в Android и iOS командах выросло в три раза. Устное обсуждение стало обходиться все дороже, и мы решили, что настало время тщательно все документировать. Теперь эта документация включает намного больше, чем просто комментарии о полях. Например, мы добавляем краткое описание фичи, sequence-диаграммы, скриншоты и примеры сообщений. В простейшем случае документация может выглядеть так:



подробная документация



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



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



Теперь мы можем с легкостью сказать разработчикам «RTFM», что экономит нам массу времени. Кроме того, такой подход помогает новичкам понять, как все устроено, и быстро включиться в процесс разработки.



Сервируем документацию



Мы готовим техническую документацию в формате reStructuredText и храним ее в репозитории git вместе с определениями протокола, а с помощью Sphinx мы генерируем HTML-версию, которая доступна во внутренней сети работникам компании.



Документация разделена на ряд основных разделов, посвященных различным аспектам реализации протокола и другим вопросам:




  • Протокол – документация, созданная на основе комментариев для определений protobuf. Функции продукта – техническая документация по вопросам реализации функций. (диаграмма конвейера и т. д.).

  • Общее — документы о протоколе и флоу, не относящаяся к конкретным фичам продукта.

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

  • Статистика — общее описание протокола и процессов, связанных со сбором статистики и информации о производительности приложений.

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

  • Архитектура и инфраструктура — структура нижнего уровня для протокола, двоичные форматы протокола, фреймворк для A/B-тестирования и т. д.





Итак, мы внесли изменения в API. Что дальше?



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



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




  • Достаточность — достаточно ли данных изменений API для реализации фичи на каждой платформе.

  • Совместимость — совместимы ли изменения с кодом приложений на платформах. Может быть можно немного подправить протокол и сэкономить одной-двум платформам кучу времени и ресурсов?

  • Понятность





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



Закончить на данном этапе было бы слишком хорошо. Часто у ProductOwner’а уже есть дальнейшие планы по улучшению данной фичи, но при этом он все же хочет выпустить приложение сейчас в текущем виде. Или еще интереснее. Иногда нас просят внести изменения для одной платформы, а для остальных оставить все как есть.



Фича как ребенок, она растет и развивается



Фичи развиваются. Мы проводим A/B-тесты и (или) анализируем обратную связь после выпуска новых фич. Иногда анализ показывает, что фича нуждается в доработке. Тогда Product Owner'ы вносят изменения в PRD. И тут возникает «проблема». Теперь PRD не соответствует формату протокола и документации. Более того, может получиться так, что для одной платформы изменения уже внесены, а другая команда только приступает к работе. Чтобы предотвратить возможные противоречия, мы используем версионирование PRD. К примеру, для одной платформы фича может быть реализована в соответствии с версией R3. Через некоторое время Product Owner решает доработать новый функционал и обновляет PRD до версии R5. И мы должны обновить протокол и техническую документацию с учетом новой версии PRD.



Для мониторинга обновлений PRD мы используем историю изменений в Confluence (вики от компании Atlassian). В технической документации на протокол мы добавляем ссылки на конкретную версию PRD, просто указывая ?pageVersion=3 в адресе вики-страницы, или берем ссылку из истории изменений. Благодаря этому каждый разработчик всегда знает, на основе какой версии или части PRD реализована та или иная функция.



Все изменения в PRD рассматриваются как новый функционал. Product Owner’ы накапливают изменения (R1, R2…) до тех пор, пока не решат, что настала пора отправить их в разработку. Они готовят задание для разработчиков API с описанием требуемых изменений в протоколе, а затем такие же задания получают все команды разработчиков, отвечающие за разные платформы. Когда для фичи готов следующий набор изменений, создается еще один тикет на доработку API, затем аналогичным образом реализуются изменения для всех платформ:



Шаги по изменению API



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



Управление изменениями в протоколе



В предыдущем разделе мы обсудили контроль версий PRD. Чтобы реализовать эти изменения в API, мы должны рассмотреть варианты управления версиями протокола. Для простоты можно сказать, что существует три варианта (уровня) со своими преимуществами и недостатками.



Уровень протокола



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




  • V1. Поддерживает функции A, B, C.

  • V2. Поддерживает функции B’, C и D, где B’ — это обновленная функция B (требующая другой последовательности команд).





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



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



Контроль версий на основе сообщений



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



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



AddPhotoToAlbumV1 {

required string album_id = 1;

required string photo_id = 2;

}





Затем Product Owner решил, что достаточно будет трех заранее определенных типов альбомов: my photos (мои фотографии), other photos (прочие фотографии) и private photos (личные фотографии). Чтобы клиенты могли различать эти три типа, нужно было добавить enum; соответственно, следующая версия сообщения будет выглядеть следующим образом:



AddPhotoToAlbumV2 {

required AlbumType album_type = 1;

required string photo_id = 2;

}





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



Уровень полей/значений



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



Пример:



AddPhotoToAlbum {

optional string album_id = 1 [deprecated=true];

optional string photo_id = 2;

optional AlbumType album_type = 3;

}





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



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



Поддержка изменений протокола



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



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



Например, недавно мы реализовали функцию «What’s New» (Что нового). Таким образом мы информируем наших пользователей о новых функциях в приложении. Приложения, поддерживающие эту функцию, отправляют серверу флаг SUPPORTS_WHATS_NEW. В результате сервер знает, что клиенту можно отправлять сообщения о новых функциях и они будут нормально отображаться.



image



Как поддерживать порядок в API?



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



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



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



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



Общение



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



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



Мы поняли, что хорошо документированный API также помогает людям, которые не являются разработчиками, понять сам API и процесс его разработки. Тестировщики обращаются к документации в ходе тестирования, а Product Owner’ы используют ее для того, чтобы продумать варианты решения поставленных задач с минимальными изменениями в протоколе (да, у нас такие крутые Product Owner’ы!).



Заключение



При разработке гибких API и связанных с ним процессов необходимо быть терпеливыми и прагматичными. Протокол и процесс должны работать с различными комбинациями команд, версий ПО и платформ, устаревшими версиями приложения, а также учитывать многие другие факторы. Тем не менее, это очень интересная задача, по которой на данный момент опубликовано крайне мало информации. Поэтому мы были очень рады поделиться нашими наработками в данном направлении. Спасибо за внимание!
Original source: habrahabr.ru.

https://habrahabr.ru/post/305888/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

Дайджест интересных материалов для мобильного разработчика #162 (11-17 июля)

Воскресенье, 17 Июля 2016 г. 15:46 (ссылка)

Лето, и новостей не так много. Немного про Sailfish, немного про Qt и Unity, тестирование в Facebook и аутентификацию. А, много про Pokemon Go, но от этого уже никуда не деться.












Начало разработки для Sailfish OS

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



iOS



Android



Разработка



Аналитика, маркетинг и монетизация



Устройства и IoT





<- Предыдущий дайджест. Если у вас есть другие интересные материалы или вы нашли ошибку — пришлите, пожалуйста, в почту.
Original source: habrahabr.ru.

https://habrahabr.ru/post/305792/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best

Комментарии (0)КомментироватьВ цитатник или сообщество

Следующие 30  »

<разработка мобильных приложений - Самое интересное в блогах

Страницы: [1] 2 3 ..
.. 10

LiveInternet.Ru Ссылки: на главную|почта|знакомства|одноклассники|фото|открытки|тесты|чат
О проекте: помощь|контакты|разместить рекламу|версия для pda