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

Android

Создаем приложение для ANDROID быстро и просто

26.05.2021 20:08:54 | Автор: admin

Сегодня я хотел бы поделиться с Вами, как быстро и просто можно создать приложение для Android с базовыми знаниями HTML CSS и JS. По данному примеру код на Java для Android будет минимальным. Благодаря платформе XAMARIN приложения для мобильных телефонов можно делать в Visual Studio.

Шаг 1 - Переходим на сайт и Скачиваем бесплатную версию Community.



Шаг 2 - Запускаем установку и выбираем параметры. Нас интересует XAMARIN. Но Вы также можете выбрать другие параметры.



После успешной установки мы можем создать свой первый проект.

Шаг 3 - Запускаем Visual Studio. Создать проект. В фильтре пишем xamarin, платформа Android, язык c# (Если желаете другой язык можете его выбрать)


Шаг 4 - Далее. Указываете имя для своего приложения, выбираете каталог где его сохранить. Создать.


Шаг 5 - Указываем пустое приложение и выбираем минимальную версию андроида для запуска этого приложения.


Шаг 6 - Жмем ок. Visual Studio автоматически создает код для приложения



Мы можем его запустить в эмуляторе, который идет комплекте с Visual Studio нажав клавишу F5.



Шаг 7 - Теперь немного модифицируем код. В данном случае мы вообще не будем использовать Java. Так как мы будем кодить на C#.



Приводим код к такому виду. Здесь мы создаем WebView контейнер который будет грузить локальный HTML файл, который находится в проекте в папке Assets.

public class MainActivity : AppCompatActivity    {        WebView mWebview; //это контейнер для просмотра HTML        protected override void OnCreate(Bundle savedInstanceState)        {            base.OnCreate(savedInstanceState);            Xamarin.Essentials.Platform.Init(this, savedInstanceState);                       mWebview = new WebView(this);            mWebview.Settings.JavaScriptEnabled = true; //это разрешение работа JS скриптов            mWebview.Settings.DomStorageEnabled = true; //это разрешение на запись в память браузера            mWebview.Settings.BuiltInZoomControls = true; //это разрешение на масштабирование пальцами щипком            mWebview.Settings.DisplayZoomControls = false; //это запрет вывода кнопок масштаба            mWebview.Settings.CacheMode = CacheModes.NoCache; //это отключает либо включает кэширование данных             mWebview.LoadUrl($"file:///android_asset/Content/login.html"); //это загрузка локального файла из папки Asset/Content            SetContentView(mWebview);         }        public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)        {            Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);            base.OnRequestPermissionsResult(requestCode, permissions, grantResults);        }    }

Шаг 8 - Создадим там папку Content.



Шаг 9 - Добавим в папку Content файл login.html



Шаг 10 - Далее уже пишем на привычном нам HTML CSS JS. Можем нажать на F5 и увидеть результат нашей работы.



По такому принципу можно создать приложение быстро и просто. Файлы html будут выглядеть одинаково на всех устройствах. То есть, Вы можете сделать приложения для Android и iOS с одинаковым интерфейсом. Не надо изучать сложные языки разметки, не надо изучать сложные макеты (сториборды) на iOS. Все можно сделать на HTML.

В идеале, вместо локальных файлов можно сделать загрузку со стороннего сайта. В этом случае Вы можете менять контент приложения без его обновления в AppStore и Google Play.
Q: Но как быть с функциями самой платформы? Пуш сообщения? Как взаимодействовать с самой платформой?
Все очень просто! JavaScript можно использовать для вызова функций Android:

Шаг 1 - Немного модифицируем наш файл MainActivity



//добавляем интерфейс для javascript            mWebview.AddJavascriptInterface(new JavaScriptInterface(), "interface");              //

Шаг 2 - Далее создаем класс JavaScriptInterface на который будет ругаться Visual Studio



  public class JavaScriptInterface : Java.Lang.Object    {        [JavascriptInterface]        [Export("alert")]  //здесь мы указываем название функции вызываемой из html файла interface.alert('сообщение пользователю');        public void alert(string data)        {            Toast.MakeText(Application.Context, data, ToastLength.Short).Show();//здесь Андроид выведет сообщение посредством Toast        }    }

Мы видим, что теперь программа ругается на Export так как не знает что это такое.

Шаг 3 - Добавим нужную библиотеку



Шаг 4 - В фильтре напишем mono



Шаг 5 - Найдем Export и поставим галочку



Шаг 6 - Жмем ок и видим что ошибка пропала.

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

Toast.MakeText(Application.Context, data, ToastLength.Short).Show();

Данная функция это показ всплывающей информации на экране. Она выполняется именно на платформе Андроида. То есть мы можем написать в HTML файле вызов функции Андроида. Получается полное дружелюбие двух платформ по JavaScript интерфейсу. Данные можно передавать туда сюда. Вызывать переход от одной активити в другую. Все через HTML + JavaScript.

Немного модифицируем файл login.htm:



<html><head>    <style>        h1 {            color: yellowgreen;        }    </style></head><body>    <h1>Привет мир</h1>    <button onclick="sendToAndroid();">Нажми меня</button>    <script>        function sendToAndroid() {            //здесь мы запускаем функцию андроида из HTML файла по javacsript интерфейсу            interface.alert("текст сообщения");        }    </script></body></html>

жмем F5



Теперь при нажатии на кнопку HTML вызывается функция Toast андроида и выводиться сообщение пользователю.

Спасибо за внимание.

P.s. Полный листинг MainActivity

using Android.App;using Android.OS;using Android.Runtime;using Android.Webkit;using Android.Widget;using AndroidX.AppCompat.App;using Java.Interop;namespace MyFirstApp{    [Activity(Label = "@string/app_name", Theme = "@style/AppTheme", MainLauncher = true)]    public class MainActivity : AppCompatActivity    {        WebView mWebview; //это контейнер для просмотра HTML        protected override void OnCreate(Bundle savedInstanceState)        {            base.OnCreate(savedInstanceState);            Xamarin.Essentials.Platform.Init(this, savedInstanceState);            mWebview = new WebView(this);            mWebview.Settings.JavaScriptEnabled = true; //это разрешение работа JS скриптов            mWebview.Settings.DomStorageEnabled = true; //это разрешение на запись в память браузера            mWebview.Settings.BuiltInZoomControls = true; //это разрешение на масштабирование пальцами щипком            mWebview.Settings.DisplayZoomControls = false; //это запрет вывода кнопок масштаба            mWebview.Settings.CacheMode = CacheModes.NoCache; //это отключает либо включает кэширование данных            //добавляем интерфейс для javascript            mWebview.AddJavascriptInterface(new JavaScriptInterface(), "interface");                         //            mWebview.LoadUrl($"file:///android_asset/Content/login.html"); //это загрузка локального файла из папки Asset/Content            SetContentView(mWebview);        }        public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)        {            Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);            base.OnRequestPermissionsResult(requestCode, permissions, grantResults);        }    }    public class JavaScriptInterface : Java.Lang.Object    {        [JavascriptInterface]        [Export("alert")]        public void alert(string data)        {            Toast.MakeText(Application.Context, data, ToastLength.Short).Show();        }    }}



Подробнее..

Обновляемся на новую версию API Android по наставлению Google

27.05.2021 20:23:24 | Автор: admin

Скоро выходит Android 12, но в этом августе уже с 11-й версии разработчикам придётся использовать новые стандарты доступа приложений к внешним файлам. Если раньше можно было просто поставить флаг, что ваше приложение не поддерживает нововведения, то скоро они станут обязательными для всех. Главный фокус повышение безопасности.

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

Что происходит

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

В Android есть внутреннее Internal Storage (IS) и внешнее хранилище External Storage (ES). Исторически это были встроенная память в телефоне и внешняя SD-карта, поэтому ES был больше, но медленнее и дешевле. Отсюда и разделение настройки и критически важное записывали в IS, а в ES хранили данные и большие файлы, например, медиа. Потом ES тоже стал встраиваться в телефон, но разделение, по крайней мере логическое, осталось.

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

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

В Android решили всё это переделать ещё в 10-й версии, а в 11-й это стало обязательным.

Чтобы минимизировать риски для пользователя в Google решили внедрить Scoped Storage (SS) в ES. Возможность проникнуть в папки других приложений убрали, а доступ есть только к своим данным теперь это сугубо личная папка. А IS с 10-й версии ещё и зашифрована по умолчанию.

В Android 11 Google зафорсировала использование SS когда таргет-версия SDK повышается до 30-й версии API, то нужно использовать SS, иначе будут ошибки, связанные с доступом к файлам. Фишка Android в том, что можно заявить совместимость с определённой версией ОС. Те, кто не переходили на 11, просто говорили, что пока не совместимы с этой версий, но теперь нужно начать поддерживать нововведения всем. С осени не получится заливать апдейты, если не поддерживаешь Android 11, а с августа нельзя будет заливать новые приложения.

Если SS не поддерживается (обычно это для девайсов ниже 10-й версии), то для доступа к данным других приложений требуется получить доступ к чтению и записи в память. Иначе придётся получать доступ к файлам через Media Content, Storage Access Framework или новый, появившийся в 11-м Android, фреймворк Datasets в зависимости от типа данных. Здесь тоже придётся получать разрешение доступа к файлу, но по более интересной схеме. Когда расшариваемый файл создаёшь сам, то доступ к нему не нужен. Но если переустановить приложение доступ к нему опять потребуется. К каждому файлу система привязывает приложение, поэтому когда запрашиваешь доступ, его может не оказаться. Особо беспокоиться не нужно, это сложно отследить, поэтому лучше просто сразу запрашивать пермишен.

Media Content, SAF и Datasets относятся к Shared Storage (ShS). При удалении приложения расшаренные данные не удаляются. Это полезно, если не хочется потерять нужный контент.

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

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

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

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

Перейдём к практике.

Переход на новую версию

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

Для этого выделили в общий интерфейс работу с файлами, реализация которого зависела от версии API.

interface FilesManipulator {    fun createVideoFile(fileName: String, copy: Copier): Uri    fun createImageFile(fileName: String, copy: Copier): Uri    fun createFile(fileName: String, copy: Copier): Uri    fun getPath(uri: Uri): String    fun deleteFile(uri: Uri)}

FilesManipulator представляет собой интерфейс, который знает, как работать с файлами и предоставляет разработчику API для записи информации в файл. Copier это интерфейс, который разработчик должен реализовать, и в который передаётся поток вывода. Грубо говоря, мы не заботимся о том, как создаются файлы, мы работаем только с потоком вывода. Под капотом до 10-й версии Android в FilesManipulator происходит работа с File API, после 10-й (и включая её) MediaStore API.

Рассмотрим на примере сохранения картинки.

fun getContentValuesForImageCreating(fileName: String): ContentValues {    return ContentValues().apply {        put(MediaStore.Images.Media.DISPLAY_NAME, fileName)        put(MediaStore.Images.Media.IS_PENDING, FILE_WRITING_IN_PENDING)        put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + File.separator + appFolderName)    }}fun createImageFile(fileName: String, copy: Copier): Uri {    val contentUri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)    val contentValues = getContentValuesForImageCreating(fileName)    val uri = contentResolver.insert(contentUri, contentValues)         ?: throw IllegalStateException("New image file insert error")    downloadContent(uri, copy)    return uri}fun downloadContent(uri: Uri, copy: Copier) {    try {        contentResolver.openFileDescriptor(uri, FILE_WRITE_MODE)                .use { pfd ->                    if (pfd == null) {                        throw IllegalStateException("Got nullable file descriptor")                    }                    copy.copyTo(FileOutputStream(pfd.fileDescriptor))                }        contentResolver.update(uri, getWriteDoneContentValues(), null, null)    } catch (e: Throwable) {        deleteFile(uri)        throw e    }}fun getWriteDoneContentValues(): ContentValues {    return ContentValues().apply {        put(MediaStore.Images.Media.IS_PENDING, FILE_WRITING_DONE)    }}

Так как операция сохранения медиафайлов достаточно длительная, то целесообразно использовать MediaStore.Images.Media.IS_PENDING, которая при установлении значения 0 не дает видеть файл приложениям, отличного от текущего.

По сути, вся работа с файлами реализована через эти классы. Шаринг в другие приложения автоматически сохраняют медиа в память устройства и последующая работа с URI уже происходит по новому пути. Но есть такие SDK, которые ещё не успели перестроиться под новые реалии и до сих пор используют File API для проверки медиа. В этом случае используем кеш из External Storage и при необходимости провайдим доступ к файлу через FileProvider API.

Помимо ограничений с памятью в приложениях, таргетированных на 30-ю версию API, появилось ограничение на видимость приложения. Так как iFunny использует шаринг во множество приложений, то данная функциональность была сломана полностью. К счастью, достаточно добавить в манифест query, открывающую область видимости к приложению, и можно будет также полноценно использовать SDK.

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

<manifest  ><queries><intent>    <action android:name="android.intent.action.SENDTO" />    <data android:scheme="smsto, mailto" /></intent>    <package android:name="com.twitter.android" />    <package android:name="com.snapchat.android" />    <package android:name="com.whatsapp" />    <package android:name="com.facebook.katana" />    <package android:name="com.instagram.android" />    <package android:name="com.facebook.orca" />    <package android:name="com.discord" />    <package android:name="com.linkedin.android" /></queries></manifest>

После проверок запуска UI-тестов на девайсах с версиями API 29-30 было выявлено, что они также перестали корректно отрабатываться.

Первоначально в LogCat обнаружил, что приложение не может приконнектиться к процессу Orchestrator и выдает ошибку java.lang.RuntimeException: Cannot connect to androidx.test.orchestrator.OrchestratorService.

Эта проблема из разряда видимости других приложений, поэтому достаточно было добавить строку <package android:name="androidx.test.orchestrator" /> .

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

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

Так как нам нужно использовать этот пермишен только для тестов, то нам условия подходят. Поэтому я быстренько написал свой ShellCommandExecutor, который выполняет команду adb shell appops set --uid PACKAGE_NAME MANAGE_EXTERNAL_STORAGE allow на создании раннера тестов.

На Android 11 тесты удачно запустились и стали проходить без ошибок.

После попытки запуска на 10-й версии Android обнаружил, что отчет Allure также перестал сохраняться в память девайса. Посмотрев issue Allure, обнаружил, что проблема известная, как и с 11-й версией. Достаточно выполнить команду adb shell appops set --uid PACKAGE_NAME LEGACY_STORAGE allow. Сказано, сделано.

Запустил тесты всё еще не происходит сохранения в память отчёта. Тогда я обнаружил, что в манифесте WRITE_EXTERNAL_STORAGE ограничен верхней планкой до 28 версии API, то есть запрашивая работу памятью мы не предоставили все разрешения. После изменения верхней планки (конечно, для варианта debug) и запроса пермишена на запись тесты удачно запустились и отчёт Allure сохранился в память устройства.

Добавлены следующие определения пермишенов для debug-сборки.

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /><uses-permission    android:name="android.permission.WRITE_EXTERNAL_STORAGE"    android:maxSdkVersion="29"    tools:node="replace" />

После всех вышеописанных манипуляций с приложением, можно спокойно устанавливать targetSdkVersion 30, загружать в Google Play и не беспокоиться про дедлайн, после которого загружать приложения версией ниже станет невозможно.

Подробнее..

Мобильные контейнеры для раздельного хранения данных

07.06.2021 12:06:42 | Автор: admin

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

Но и эта медаль двухсторонняя. Сотрудники требуют гарантий, что работодатель не читает их переписку или не смотрит их фотографии. Работодатели в свою очередь против того, чтобы сотрудники делились корпоративными документами в социальных сетях или передавали их в СМИ.

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

Зачем нужен контейнер работодателям и их сотрудникам?

Работодатели и сотрудники хотят, чтобы контейнер защищал ИХ секреты. Только секреты каждой из сторон находятся по разные стороны контейнера секреты работодателя находятся внутри контейнера, а секреты сотрудника снаружи.

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

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

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

Особенность контейнеров для iOS

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

С точки зрения контейнеризации в iOS есть встроенный механизм разделения корпоративного и личного. Apple называет корпоративное управляемым (managed), а личное неуправляемым (unmanaged).

Управляемыми в iOS могут быть приложения, учётные записи и URL в Safari. С помощью встроенных политик можно запретить передачу данных между управляемым и неуправляемым, но только случайную. Это значит, что сотрудник не может передать вложение из корпоративной почты в личную почту или в WhatsApp, но может скопировать текст вложения и вставить его куда угодно!

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

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

Разнообразие Android - контейнеров

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

Первой компанией, которая сделала возможным создание контейнеров на Android, стала компания Samsung. В 2012 году на смартфоне Samsung Galaxy S3 впервые стали доступны функции корпоративной платформы безопасности Samsung Knox. В частности, Knox контейнеры. Большинство современных устройств Samsung также их поддерживают. Многие функции платформы Samsung Knox бесплатны, но функции Knox контейнеров были платной опцией. Позже Google анонсировал свою бесплатную корпоративную платформу Android for Enterprise c меньшим набором функций.

С годами число функций управления контейнерами, которые были эксклюзивно доступны на устройствах Samsung, сокращалось, потому что они появлялись у Google. В результате, начиная с
1 июля 2021 года, Samsung принял решение о бесплатном предоставлении возможностей Knox-контейнеризации. В составе платформы Samsung Knox ещё остаются платные опции, которых нет у Google. Например, корпоративных сервис обновления прошивок мобильных устройств E-FOTA One, о котором мы рассказывали в одной из прошлых статей.

Но конкретно Knox контейнеры отныне бесплатны.

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

Без явного разрешения администратора пользователь не сможет ничего вынести за пределы контейнера.

Вроде то, что нужно? Да, но не обошлось без важных особенностей.

В составе Samsung Knox есть клиентские библиотеки для Android, с помощью которых можно разработать клиент управления, который сам создаст контейнер, применит в нём необходимые политики безопасности и наполнит его приложениями.

Работает эта схема так:

  1. На мобильное устройство устанавливается клиент управления.

  2. Пользователь регистрирует устройство на сервере управления.

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

  4. Можно доставить обновления настроек быстрее с помощью push-уведомлений от Google, но их использование необязательно.

Преимущество этой схемы в том, что её можно построить в локальной инфраструктуре заказчика (on-premise). Для крупного бизнеса в России это важно.

У платформы Android for Enterprise от Google тоже есть клиентские библиотеки и с их помощью можно управлять устройствами без контейнеров устанавливать приложения, настраивать ограничения и т.д. Даже можно создать контейнер, поместить в него встроенный Gmail или Google Chrome и запретить сотруднику копировать из контейнера файлы. Всё это будет работать on-premise.

Но клиентские библиотеки Google не помогут, если нужно разместить в контейнере приложение собственной разработки или какое-то приложение из Google Play. В этом случае необходимо использовать Android Management API, который работает по несложной схеме:

  1. На мобильное устройство устанавливается клиент управления от Google Android Device Policy.

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

  3. Сервер передаёт настройки контейнера в Google с помощью Android Management API. Дальше Google обеспечивает их применение на устройстве самостоятельно. При этом все команды и все дистрибутивы корпоративных приложений нужно передать в Google.

    Источник: https://developers.google.com/android/management/introductionИсточник: https://developers.google.com/android/management/introduction

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

Корпоративный Android в личном пользовании

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

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

Источник: https://docs.samsungknox.com/admin/knox-platform-for-enterprise/work-profile-on-company-owned-devices.htmИсточник: https://docs.samsungknox.com/admin/knox-platform-for-enterprise/work-profile-on-company-owned-devices.htm

В качестве альтернативы Samsung предложил оригинальную технологию Knox Separated Apps. Технология позволяет создать на корпоративных устройствах контейнер для личных приложений. Работодатель управляет всем устройством и не получает доступа к данным сотрудника в личном контейнере. А приложения в личном контейнере не получают доступа к корпоративным данным, поэтому с их помощью нельзя реализовать утечку информации.

Knox Separated Apps, как и Knox контейнеры, с 1 июля 2021 года также станет бесплатной.

Советы для топ-менеджеров

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

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

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

На каждом новом устройстве корпоративный софт не работает совершенно особенным образом. Каждая мажорная версия Android c 4 по 12 серьёзно отличаются. У каждого производителя своя сборка Android. У каждой сборки Android, особенно китайской, свои тараканы неуправляемые менеджеры памяти и батареи, которые так и норовят поскорее закрыть приложение или не дать ему вовремя получить данные от сервера, дополнительные разрешения, которые пользователь должен вручную дать приложению и которые он может в любой момент отобрать и т.п.

Если планировать мобилизацию компании на 3-5 лет вперёд, дешевле выбрать несколько моделей устройств, и дальше проверять и разрабатывать корпоративный софт (клиент документооборота, приложения с BI-отчётностью, мессенджеры и т.п.) на этих конкретных моделях. Всё-таки корпоративный софт пишут не тысячи разработчиков Facebook по всему миру. Поэтому чем меньше вариативность устройств, тем меньше ошибок, тем меньше число уязвимостей и ниже вероятность их успешной эксплуатации. Короче, всем удобнее и безопаснее.

Если у вас возникли вопросы или сомнения после прочтения статьи, напишите о них в комментариях.

Подробнее..

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

24.05.2021 12:21:25 | Автор: admin

Если вы регулярно читаете Хабр, то вам попадались статьи в духе: бросайте всё и начинайте изучать Swift, Kotlin или Flutter прямо сейчас. Давайте разбираться, правда ли стоит переобуваться в мобильного разработчика. Мы попросили спикеров, программный комитет и разработчиков взглянуть на сферу мобильной разработки с разных ракурсов и приоткрыть завесу тайны грядущей конференции Мир. Труд. Мобайл. В конце приятный бонус для читателей Хабра и подробности программы.

Мобильная разработка актуальна. Это факт

В отчёте State of Mobile 2021 говорится, что рынок мобильных приложении и игр вырос на 30% за 2020 год пользователи потратили на них рекордные $111 млрд. Пандемия и изоляция внесли свой вклад.

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

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

В сторах можно найти приложение на любой случай жизни. Что говорить, когда даже у автомата по розливу воды у дома в спальном районе (за 3 /литр) есть мобильное приложение для онлайн-оплаты. Кажется, мобильщики выбрали как девиз любая идея достойна мобильного приложения! Почему? Отчасти потому что новые технологии упростили и шаблонизировали разработку.

Отчёт State of Mobile 2021 Отчёт State of Mobile 2021

Про изменения в подходах к разработке мы спросили Фёдора Цымбала из Orion Innovations. Он выступит с докладом: Android Automotive. Не путать с Android Auto.

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

В плане UI-фреймворков сейчас очень популярен Flutter. Но Jetpack Compose вполне может его потеснить. Стоит выбрать что-то одно из этих двух опций.

Заметен уход с мобилок на другие устройства: часы, телевизоры, автомобили. Android на этих устройствах сейчас активно развивается. По моему мнению, это тоже очень интересная тема. Про Android Automotive я и буду рассказывать: Google Automotive Services, Driver Distraction Guidelines, Garage Mode и об интеграции Android с подсистемами автомобиля, такими как камера заднего вида, климат контроль или поворотники.

Павел Стрельченко из hh.ru занимается Android-разработкой с 2015 года, поэтому успел застать разработку под Android 4, первую версию Android Studio, жизнь без Jetpack, Architecture Components и Kotlin. Павел выступит с темой: Укрощая фиче-флаги. Разберем проблемы постоянных merge-конфликтов, сбора флагов в один-единственный список.

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

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

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

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

Все спикеры

Evelio Tarazona Cceres
Instagram / Facebook

Server Driven Cross-Platform UI/Features/Apps. At Instagram we leverage Server Driven UI approach to build once/iterate quickly and ship features to billions of users on Android/iOS and the web.

Федор Цымбал
Orion Innovations

Android Automotive. Не путать с Android Auto. Google Automotive Services, Driver Distraction Guidelines, Garage Mode и об интеграции Android с подсистемами автомобиля, такими как камера заднего вида, климат контроль или поворотники.

Ольга Сартакова
Redmadrobot

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

Евгений Ртищев
Sberbank

Оптимизируем процессы разработки и параметры приложения.

Андрей Малеваник
Нетологиия

Красота или функциональность. Должен ли интерфейс быть красивым?

Павел Стрельченко
hh.ru

Укрощая фиче-флаги. Разберем проблемы постоянных merge-конфликтов, сбора флагов в один-единственный список.

Екатерина Петрова
JetBrains

State of Kotlin Multiplatform Mobile. О том, что поменялось в экосистеме KMM с момента большого релиза, о трендах и планах развития.

Александр Аверин
adVentures, Mail.ru

История перезапуска музыкального приложения BOOM глазами дизайнера.

Александр Гращенков
RoadAR

Почему тормозит iPhone. От базового уровня перевода задач в бэкграунд, до ускорения отрисовки с помощью Metal.

Михаил Никипелов
Distillery

Пиктограммы 80-го уровня. Про подбор идей и отсекание лишнего: как при работе с библиотеками, так и при отрисовке своих иконок.

Дмитрий Мельников
EventSheep (ех-Yandex, ех-Mail.ru)

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

Андрей Чевозеров
Банк ВТБ

SwiftUI в production. Как и зачем?

Антон Назаров
Crisalix

RxSwift vc Combine. О личном опыте миграции с RxSwift на Combine, какие подводные камни есть, как облегчить процесс.

Александр Денисов
EPAM

Так ли страшен Null, как его малюют? О том, что такое Null Safety, чем она может помочь в разработке, какие сложности могут ждать при миграции и чем реализация в Dart похожа, а чем отлична от Kotlin и Swift реализации.

Антон Шилов
Badoo

Воркшоп по анимациям на Jetpack Compose. Разберем основные API и инструменты для работы с анимациями от простого к сложному.

Мария Кирдун
EPAM

Искусство коммуникаций, или как творчеству выжить в IT. Игры на коммуникацию на реальных примерах,

Евгений Сатуров
Surf

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

Павел Горшков
экс-Redmadrobot, экс-Яндекс

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

Сергей Акентьев
Кошелёк

CI на Apple M1. Жутко больно и запредельно быстро. Как работать с кластером MacMini на M1 и почему мы оказались в Дата-центре? Проблемы архитектуры arm64, билды под Apple Rosetta 2, как бороться с софтом Apple и стоит ли оно того?

Алексей Бородкин
Магнит

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

Александр Соболь
МегаФон

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

Приложение остаётся способом реализовать большую идею малыми силами

Мобильное приложение по-прежнему отличный вариант для старта своего продукта с небольшой затратой ресурсов. Самый сладкий и большой кусок пирога мобильные игры. Пользователи потратили на них в 2020 году $143 млрд. Доля мобильных игр в магазинах приложений в 2021 году вырастет до 20%.

Про технологии, рынок, зарплаты и перспективы вроде понятно. Вопрос сколько стоит реализовать свою идею.

Приложения комбинируют механики. Афиша DVIZZ реализована в виде свайпов мероприятийПриложения комбинируют механики. Афиша DVIZZ реализована в виде свайпов мероприятий

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

Основатель DVIZZ Михаил Иванов рассказывает, что потратил 500 000 на разработку и 3 млн на зарплаты за полгода после запуска.

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

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

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

Что будет на Мир. Труд. Мобайл

Два формата: бесплатный онлайн и офлайн на цифровой даче в Иннополисе. Всего будет 5 больших хабов:

  • Android;

  • iOS;

  • Кроссплатформа;

  • Дизайн;

  • Софтскилы.

Офлайн

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

Ждём в гости!Ждём в гости!

Онлайн

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

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

Увидимся на Мир. Труд. Мобайл.

27 мая

Скидка на офлайн 25% для читателей Хабра по промокоду habr

Мобайл редьки слаще!

Подробнее..

Получаем результат правильно(Часть2). FragmentResultAPI

25.05.2021 20:20:29 | Автор: admin

Мы продолжаем рассказ о новинках библиотеки Jetpack, призванных упростить обмен данными между компонентами Android приложения. Первая часть была посвящена передаче данных из Activity и новому Api Activity Result.

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

Как передать данные между двумя фрагментами? - частый вопрос на собеседованиях. Ответить на него можно по-разному: создание общей ViewModel, имплементация интерфейса в Activity, использование targetFragment и другие способы.

С появлением Fragment Result Api в этот список добавился простой способ передачи небольшого объема информации из одного фрагмента в другой. Например, возвращение результата какого-либо пользовательского сценария. Мы разберем, как применять новый Api на практике, но сначала немного теории.

Теория

Начиная с версии 1.3.0-alpha04, FragmentManager реализует интерфейс FragmentResultOwner. Это означает, что FragmentManger является диспетчером для результатов, которые отправляют фрагменты. Благодаря этому фрагменты могут обмениваться информацией, не имея прямых ссылок друг на друга.

Таким образом, всё взаимодействие происходит через FragmentManager:

  • Если фрагмент ожидает получить некоторые данные от другого фрагмента, он должен зарегистрировать слушатель во FragmentManger с помощью метода setFragmentResultListener().

  • Если фрагменту необходимо вернуть результат другому фрагменту, он передает FragmentManger объект Bundle, содержащий информацию. Для этого вызывается метод setFragmentResult().

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

Упрощенно данную схему можно представить так:

FragmentB передает данные в FragmentA . FragmentManager выполняет роль диспетчераFragmentB передает данные в FragmentA . FragmentManager выполняет роль диспетчера

Достоинством Fragment Result Api является lifecycle-безопасность - результат передается во фрагмент, только когда тот достиг состояния STARTED, но еще не находится в состоянии DESTROYED.

Под капотом FragmentManger хранит все зарегистрированные слушатели и все отправленные результаты в потокобезопасных реализациях Map:

  • Map<String, Bundle> для результатов, отправленных фрагментами

  • Map<String, LifecycleAwareResultListener> для зарегистрированных слушателей

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

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

А теперь практика.

Практика

В качестве примера возьмем следующий кейс: ProductsFragment содержит список товаров, которые можно сортировать по различным критериям, а SortFragment позволяет указать нужную сортировку. Информация о выбранной сортировке будет передаваться с помощью Fragment Result Api.

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

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

Шаг 1

В ProductsFragment, который ожидает получить результат, мы должны зарегистрировать слушатель с помощью FragmentManager. Для этого воспользуемся экстеншен-функцией setFragmentResultListener из fragment-ktx, которая принимает строковый ключ и слушатель, обрабатывающий результат.

Регистрацию слушателя можно произвести в колбеке onCreate():

override fun onCreate(savedInstanceState: Bundle?) {   super.onCreate(savedInstanceState)   setFragmentResultListener("request_key") { key, bundle ->        val selectedSort = bundle.getParcelable<Sort>("extra_key")        // применение полученной сортировки   }}

Шаг 2

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

applyButton.setOnClickListener {   setFragmentResult(      "request_key",       bundleOf("extra_key" to getSelectedSort())   )}

Вот и всё, что требуется для передачи результата с помощью Fragment Result Api.

Важно

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

Выбор FragmentManager

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

Сначала представим так называемую master-detail конфигурацию. Активити содержит два фрагмента, FragmentA и FragmentB, между которыми требуется передать результат.

Активити является хостом для FragmentA и FragmentBАктивити является хостом для FragmentA и FragmentB

В таком случае передавать результат между фрагментами может FragmentManager активити-хоста, т.к. доступ к нему имеют оба фрагмента. Получить данный FragmentManager можно путем вызова requireActivity().supportFragmentManager либо parentFragmentManager.

Следующая ситуация характерна, например, для открытия DialogFragment или в случае, если FragmentA размещает внутри себя FragmentC.

FragmentA является хостом для FragmentСFragmentA является хостом для FragmentС

При таком сценарии, передать результат из FragmentС в FragmentA можно двумя способами:

  • Через FragmentManager активити с помощью requireActivity().supportFragmentManager

  • Через дочерний FragmentManager у FragmentA. Чтобы получить на него ссылку, FragmentA должен обращаться к childFragmentManager, а FragmentС к parentFragmentManager.

Особенности Lifeсycle

Как уже сказано, Fragment Result Api обеспечивает lifecycle-безопасность - результат доставляется, только если фрагмент находится на экране. Рассмотрим несколько примеров.

Представим стандартный случай - фрагмент подписывается в колбеке onCreate, затем переходит в состояние STARTED, и как только другой фрагмент передает во FragmentManager результат, фрагмент-подписчик его получает.

Фрагмент получит лишь bundle3, так как он был отправлен последнимФрагмент получит лишь bundle3, так как он был отправлен последним

Если еще до перехода фрагмента в состояние STARTED, во FragmentManager было передано несколько результатов, то фрагмент получит лишь последний из них (так как FragmentManager хранит результаты в Map<String, Bundle>, то каждый последующий перезаписывает предыдущий).

Автоматическая отписка фрагментов происходит при достижении состояния DESTROYEDАвтоматическая отписка фрагментов происходит при достижении состояния DESTROYED

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

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

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

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

Все рассмотренные ситуации объединяет то, что фрагмент подписывался по уникальному строковому ключу. Но что если сразу несколько подписчиков будут использовать один и тот же ключ? Напомним, что FragmentManager сохраняет информацию о подписках в Map<String, LifecycleAwareListener>, следовательно не может содержать несколько записей с одним и тем же ключом. Именно поэтому результат будет доставлен в тот фрагмент, который зарегистрировал слушатель последним.

Результат получает только последний подписчикРезультат получает только последний подписчик

Заключение

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

  • Fragment Result Api является стабильным, можно не бояться использовать его в продакшене. Тем, кто использует targetFrament особенно стоит присмотреться, ведь targetFrament стал Deprecated.

  • Api прост в использовании и не требует написания большого количества кода

  • Учитывает жизненный цикл фрагментов - при получении результата, можно сразу работать со view фрагмента

  • Позволяет пережить изменение конфигурации и даже смерть процесса (FragmentManager умеет сохранять данные о переданных результатах в Parcelable)

Но присутствуют и недостатки:

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

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

В целом, Fragment Result Api оставляет положительное впечатление, и точно стоит того, чтобы его опробовать, а наглядный пример можно найти по ссылке.

Подробнее..

Перевод Миграция с LiveData на Kotlins Flow

08.06.2021 16:18:29 | Автор: admin

LiveData была нужна нам еще в 2017 году. Паттерн наблюдателя облегчил нам жизнь, но такие опции, как RxJava, в то время были слишком сложными для новичков. Команда Architecture Components создала LiveData: очень авторитетный класс наблюдаемых хранилищ данных, разработанный для Android. Он был простым, чтобы облегчить начало работы, а для более сложных случаев реактивных потоков рекомендовалось использовать RxJava, используя преимущества интеграции между ними.

DeadData?

LiveData по-прежнему остается нашим решением для Java-разработчиков, новичков и простых ситуаций. В остальном, хорошим вариантом является переход на Kotlin Flows. Flows (потоки) все еще имеют крутую кривую обучения, но они являются частью языка Kotlin, поддерживаемого Jetbrains; кроме того, на подходе Compose, который хорошо сочетается с реактивной моделью.

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

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

Flow: простые вещи труднее, а сложные легче

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

Давайте рассмотрим некоторые паттерны LiveData и их эквиваленты Flow:

#1: Показ результата однократной операции с модифицированным держателем данных

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

Показ результата однократной операции с модифицированным (Mutable) держателем данных (LiveData)Показ результата однократной операции с модифицированным (Mutable) держателем данных (LiveData)
<!-- Copyright 2020 Google LLC.   SPDX-License-Identifier: Apache-2.0 -->class MyViewModel {    private val _myUiState = MutableLiveData<Result<UiState>>(Result.Loading)    val myUiState: LiveData<Result<UiState>> = _myUiState    // Load data from a suspend fun and mutate state    init {        viewModelScope.launch {             val result = ...            _myUiState.value = result        }    }}

Чтобы сделать то же самое с потоками, мы используем модифицированный StateFlow:

Показ результата однократной операции с модифицированным держателем данных (StateFlow)Показ результата однократной операции с модифицированным держателем данных (StateFlow)
class MyViewModel {    private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)    val myUiState: StateFlow<Result<UiState>> = _myUiState    // Load data from a suspend fun and mutate state    init {        viewModelScope.launch {             val result = ...            _myUiState.value = result        }    }}

StateFlow это особый вид SharedFlow (который является особым типом Flow), наиболее близкий к LiveData:

  • У него всегда есть значение.

  • У него только одно значение.

  • Он поддерживает несколько наблюдателей (поэтому поток является общим).

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

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

#2: Показ результата однократной операции

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

В LiveData мы использовали для этого конструктор корутин liveData:

Показ результата однократной операции (LiveData)Показ результата однократной операции (LiveData)
class MyViewModel(...) : ViewModel() {    val result: LiveData<Result<UiState>> = liveData {        emit(Result.Loading)        emit(repository.fetchItem())    }}

Поскольку держатели состояния всегда имеют значение, хорошей идеей будет обернуть наше UI-состояние в какой-нибудь класс Result, который поддерживает такие состояния, как Loading, Success и Error.

Эквивалент Flow немного сложнее, потому что вам придется выполнить некоторую настройку:

Показ результата однократной операции (StateFlow)Показ результата однократной операции (StateFlow)
class MyViewModel(...) : ViewModel() {    val result: StateFlow<Result<UiState>> = flow {        emit(repository.fetchItem())    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000), // Or Lazily because it's a one-shot        initialValue = Result.Loading    )}

stateIn это оператор Flow, который преобразует его в StateFlow. Давайте пока доверимся этим параметрам, так как позже нам понадобится более сложная информация для правильного объяснения.

#3: Однократная загрузка данных с параметрами

Допустим, вы хотите загрузить некоторые данные, которые зависят от ID пользователя, и вы получаете эту информацию от AuthManager, который показывает Flow:

Однократная загрузка данных с параметрами (LiveData)Однократная загрузка данных с параметрами (LiveData)

С помощью LiveData можно сделать примерно следующее:

class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: LiveData<String?> =         authManager.observeUser().map { user -> user.id }.asLiveData()    val result: LiveData<Result<Item>> = userId.switchMap { newUserId ->        liveData { emit(repository.fetchItem(newUserId)) }    }}

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

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

class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }    val result: LiveData<Result<Item>> = userId.mapLatest { newUserId ->       repository.fetchItem(newUserId)    }.asLiveData()}

Выполнение этого действия с помощью Flows выглядит очень похоже:

Однократная загрузка данных с параметрами (StateFlow)Однократная загрузка данных с параметрами (StateFlow)
class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }    val result: StateFlow<Result<Item>> = userId.mapLatest { newUserId ->        repository.fetchItem(newUserId)    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.Loading    )}

Обратите внимание, что если вам нужна большая гибкость, вы также можете использовать transformLatest и emit элементы в явном виде:

val result = userId.transformLatest { newUserId ->        emit(Result.LoadingData)        emit(repository.fetchItem(newUserId))    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.LoadingUser // Note the different Loading states    )

#4: Наблюдение за потоком данных с параметрами

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

Продолжая наш пример: вместо вызова fetchItem на источнике данных мы используем гипотетический оператор observeItem, который возвращает Flow.

С помощью LiveData вы можете преобразовать поток в LiveData и все обновления emitSource:

Наблюдение за потоком с параметрами (LiveData)Наблюдение за потоком с параметрами (LiveData)
class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: LiveData<String?> =         authManager.observeUser().map { user -> user.id }.asLiveData()    val result = userId.switchMap { newUserId ->        repository.observeItem(newUserId).asLiveData()    }}

Или, лучше всего, объединить оба потока с помощью flatMapLatest и преобразовать только выход в LiveData:

class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: Flow<String?> =         authManager.observeUser().map { user -> user?.id }    val result: LiveData<Result<Item>> = userId.flatMapLatest { newUserId ->        repository.observeItem(newUserId)    }.asLiveData()}

Имплементация Flow похожа, но в ней нет преобразований LiveData:

Наблюдение за потоком с параметрами (StateFlow)Наблюдение за потоком с параметрами (StateFlow)
class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: Flow<String?> =         authManager.observeUser().map { user -> user?.id }    val result: StateFlow<Result<Item>> = userId.flatMapLatest { newUserId ->        repository.observeItem(newUserId)    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.LoadingUser    )}

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

#5 Объединение нескольких источников: MediatorLiveData -> Flow.combine

MediatorLiveData позволяет вам наблюдать за одним или несколькими источниками обновлений (наблюдаемыми LiveData) и что-то делать, когда они получают новые данные. Обычно вы обновляете значение MediatorLiveData:

val liveData1: LiveData<Int> = ...val liveData2: LiveData<Int> = ...val result = MediatorLiveData<Int>()result.addSource(liveData1) { value ->    result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))}result.addSource(liveData2) { value ->    result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))}

Эквивалент Flow намного проще:

val flow1: Flow<Int> = ...val flow2: Flow<Int> = ...val result = combine(flow1, flow2) { a, b -> a + b }

Можно также использовать функцию combineTransform или zip.

Настройка открытого StateFlow (оператор stateIn)

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

val result: StateFlow<Result<UiState>> = someFlow    .stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.Loading    )

Однако если вы не уверены в этом, казалось бы, случайном 5-секундном параметре started, читайте дальше.

StateIn имеет 3 параметра (из документации):

@param scope the coroutine scope in which sharing is started.@param started the strategy that controls when sharing is started and stopped.@param initialValue the initial value of the state flow.This value is also used when the state flow is reset using the [SharingStarted.WhileSubscribed] strategy with the `replayExpirationMillis` parameter.

started может принимать 3 значения:

  • Lazily: начать, когда появится первый подписчик, и остановить, когда scope будет отменен.

  • Eagerly: начать немедленно и остановить, когда scope будет отменен.

  • WhileSubscribed: Это сложно.

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

Стратегия WhileSubscribed

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

WhileSubscribed принимает два параметра:

public fun WhileSubscribed(    stopTimeoutMillis: Long = 0,    replayExpirationMillis: Long = Long.MAX_VALUE)

Таймаут остановки

Из документации:

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

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

Решение в конструкторе корутины liveData заключалось в добавлении задержки в 5 секунд, после которой корутина будет остановлена, если нет подписчиков. WhileSubscribed(5000) делает именно это:

class MyViewModel(...) : ViewModel() {    val result = userId.mapLatest { newUserId ->        repository.observeItem(newUserId)    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.Loading    )}

Этот подход отвечает всем требованиям:

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

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

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

Истечение срока воспроизведения

replayExpirationMillis настраивает задержку (в миллисекундах) между завершением работы программы совместного доступа и сбросом кэша воспроизведения (что делает кэш пустым для оператора shareIn и возвращает кэшированное значение к исходному initialValue для stateIn). По умолчанию он равен Long.MAX_VALUE (кэш воспроизведения сохраняется постоянно, буфер никогда не сбрасывается). Используйте нулевое значение для немедленного истечения срока действия кэша.

Наблюдение StateFlow из представления

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

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

  • Activity.lifecycleScope.launch: запускает корутину немедленно и отменяет ее при завершении активности.

  • Fragment.lifecycleScope.launch: немедленно запускает корутину и отменяет ее при завершении фрагмента.

LaunchWhenStarted, launchWhenResumed...

Специализированные версии launch, называемые launchWhenX, будут ждать, пока lifecycleOwner находится в состоянии X, и приостановят выполнение корутины, когда lifecycleOwner упадет ниже состояния X. Важно отметить, что они не отменяют выполнение программы до тех пор, пока жизненный цикл не будет закончен.

Сбор потоков с помощью launch/launchWhenX небезопасенСбор потоков с помощью launch/launchWhenX небезопасен

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

Это означает, что все, что мы делали до сих пор для настройки StateFlow, было бы совершенно бесполезно; однако в нашем распоряжении есть новый API.

lifecycle.repeatOnLifecycle на помощь

Этот новый конструктор корутин (доступный в lifecycle-runtime-ktx 2.4.0-alpha01) делает именно то, что нам нужно: он запускает корутины в определенном состоянии и останавливает их, когда уровень жизненного цикла опускается ниже этого состояния.

Различные методы сбора потокаРазличные методы сбора потока

Например, во Фрагменте:

onCreateView(...) {    viewLifecycleOwner.lifecycleScope.launch {        viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {            myViewModel.myUiState.collect { ... }        }    }}

Сбор начнется, когда представление фрагмента будет STARTED, продолжится до RESUMED и остановится, когда оно вернется к STOPPED. Читайте об этом в статье Более безопасный способ сбора потоков из пользовательских интерфейсов Android.

Сочетание API repeatOnLifecycle с приведенным выше руководством по StateFlow обеспечит вам наилучшую производительность при рациональном использовании ресурсов устройства.

StateFlow выставляется с помощью WhileSubscribed(5000) и собирается с помощью repeatOnLifecycle(STARTED)StateFlow выставляется с помощью WhileSubscribed(5000) и собирается с помощью repeatOnLifecycle(STARTED)

Предупреждение: Поддержка StateFlow, недавно добавленная в Data Binding, использует launchWhenCreated для сбора обновлений, и она начнет использовать repeatOnLifecycle` вместо этого, когда достигнет стабильности.

Для Data Binding вы должны использовать Flows везде и просто добавить asLiveData(), чтобы отобразить их в представлении. Привязка данных будет обновлена, когда lifecycle-runtime-ktx 2.4.0 станет стабильным.

Резюме

Лучшим способом предоставления данных из ViewModel и сбора их из представления является:

  • Выставить StateFlow, используя стратегию WhileSubscribed, с таймаутом. [пример]

  • Собирать с помощью repeatOnLifecycle. [пример].

Любая другая комбинация будет поддерживать восходящие потоки активными, расходуя ресурсы:


Перевод материала подготовлен в рамках курса "Android Developer. Basic". Если вам интересно узнать о курсе больше, приходите на день открытых дверей онлайн. На нем вы сможете узнать подробнее о программе и формате обучения, познакомиться с преподавателем.

Подробнее..

Аналог R.string в android приложении

20.06.2021 12:09:43 | Автор: admin

Всем привет! Меня зовут Владимир, я Android-разработчик в компании Альфа-Капитал. Наверняка любое мобильное приложение в процессе развития нуждается в гибкой настройке текстовой информации за счет серверной части. В этой статье я поделюсь мыслями и решениями нашей команды. Также я покажу пример генерации кода с помощью gradle скрипта, сильно упростивший жизнь android команде.

С чего всё начиналось

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

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

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

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

  1. Удобный и единый источник текстов для всех мобильных платформ (android/ios);

  2. Обновление текстов в рантайме при старте приложения (для обновления важных мест без выпуска фиксов/релизов);

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

  4. Обновление текстов должно быть доступно без вмешательства разработчиков (т.е. чтобы условный аналитик / тестировщик смог спокойно обновить тексты при необходимости);

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

Firebase Remote Config не подошел слишком хороший функционал для простых текстов. У нас быстро получился большой список необходимых лексем, а их добавление / редактирование становилось слишком сложным. Нелегкой задачей была и установка дефолтных значений в приложении. Нам хотелось чего-то попроще.

Мы решили, что самым оптимальным будет объединение необходимых текстов в JSON файл. Почему именно JSON, а не XML, который кажется более нативным для Android? Так показалось удобней для обеих команд (Android и iOS). JSON понятный формат данных, его легко разберет любая платформа. Этот файл можно легко скачать, положить в проект и получить дефолтные данные.Схема работает и в обратную сторону. Пришла задача с новым текстом? Нужно добавить новые строки в проект, закинуть этот же JSON c ключами на сервер.

Пример json файла:

{ "screen1_text1": "Text 1", "screen1_text2": "Text 2 \nnext line", "screen1_text3": "Text 3", "screen1_text4": "Text 4"}

Первая реализация

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

object Lexemator {fun getString(key: String): String}

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

class MainActivity : Activity() {   override fun onCreate(savedInstanceState: Bundle?) { ...       val textView = findViewById<TextView>(R.id.text1)       textView.text = Lexemator.getString("screen1_text1")   }}

По сравнению с Firebase стало лучше, но был один существенный недостаток: достаточно одной ошибки в ключе, и мы получаем не тот текст. Нам хотелось получить статическую поддержку, похожую на R.string, где Android Studio подсказывала бы константы и проект не компилировался бы при ошибке.

Это была предыстория, теперь переходим к коду.

Gradle - наше всё

Раз в проекте имеется JSON файл с текстами, значит, уже на этапе сборки мы понимаем, какие есть ключи для разных текстов. Если каких-то ключей нет, то они либо не нужны, либо их всё равно надо добавить для дефолтных значений. Выходит, на этапе сборки можно сгенерировать код, который будет содержать ключи для текстов. Мы решили сделать это с помощью gradle task.

Ниже представлен получившийся скрипт

import groovy.json.JsonSlurper/*** Таска ищет файл с текстами с названием strings.json и создает объект LL.* Для каждого текста из strings.json создает переменную LL.key внутри объекта** Если файла strings.json не существует - скрипт кинет Exception.** Чтобы сгенерить текста заново, достаточно перебилдить проект, или изменить файл strings.json*/def classFileName = "LL"def stringsFileName = "strings.json"def filePath = project.rootProject.getProjectDir().path + "/app/src/main/assets/json"def outputPath = project.rootProject.getProjectDir().path + "/app/build/generated/strings"def inputFile = new File(filePath + "/${stringsFileName}")def outputFile = new File(outputPath + "/${classFileName}.kt")task createStrings {   /**    * Если что-то изменится в inputFile, то при следующей сборке будет заново сгенерирован    * outputFile.    * Если ничего не изменилось, и outputFile уже есть, таска будет помечена "UP-TO-DATE" и    * не будет выполняться лишний раз.    */   inputs.file(inputFile)   outputs.file(outputFile)   doLast {       if (!inputFile.exists()) {           throw RuntimeException("файл ${inputFile} не найден")       }       println("Начало создания файла ${outputFile.path}")       outputFile.delete()       outputFile.createNewFile()       /**        * Тройные кавычки нужны для того, чтобы перевод строки (\n) в strings.json        * не ломал строки в созданном LL.kt файле.        */       def s1 = """package com.obolonnyy.lexemator//<!--Этот файл создан автоматически gradle скриптом из create_strings.gradle -->object ${classFileName} {"""       def s2 =               """      fun addLexems(map: Map<String, String>) {       map.forEach { k, v -> addLexem(k, v) }   }   fun addLexem(key: String, value: String) {       when(key) {"""       def json = new JsonSlurper().parse(inputFile)       assert json instanceof Map       json.each { entry ->           s1 += "    var ${entry.key} = \"\"\"${entry.value}\"\"\"\n        private set\n"           s2 += "            \"${entry.key}\" -> ${entry.key} = value\n"       }       def result = s1 + "\n\n" + s2 + """        }   }}"""       outputFile.write(result)       println("файл ${outputFile.path} успешно создан.")   }}/*** Показываем, что созданный файл теперь тоже является частью проекта.* Без этого мы не сможем использовать созданный LL.kt класс в своих классах.*/android {   sourceSets {       main {           java {               srcDirs += outputPath           }       }   }}

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

Комментарий про название объекта LL: сначала мы думали назвать L (от слова Lexemator), чтобы было привычно как с R, но мешала константа android.icu.lang.UCharacter.GraphemeClusterBreak.L.Поэтому, мы не придумали ничего лучше, чем назвать класс LL.

Сгенерированный объект LL выглядит следующим образом:

//<!--Этот файл создан автоматически gradle скриптом из create_strings.gradle -->object LL {   var screen1_text1 = """Text 1"""       private set   var screen1_text2 = """Text 2next line"""       private set   var screen1_text3 = """Text 3"""       private set   var screen1_text4 = """Text 4"""       private set     fun addLexems(map: Map<String, String>) {       map.forEach { k, v -> addLexem(k, v) }   }   fun addLexem(key: String, value: String) {       when(key) {           "screen1_text1" -> screen1_text1 = value           "screen1_text2" -> screen1_text2 = value           "screen1_text3" -> screen1_text3 = value           "screen1_text4" -> screen1_text4 = value       }   }}

Пример использования объекта LL в коде выглядит следующим образом:

class MainActivity : Activity() {   override fun onCreate(savedInstanceState: Bundle?) {...       val textView = findViewById<TextView>(R.id.text1)       textView.text = LL.screen1_text1   }}

Получилось довольно просто и привычно.

Итоги

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

Рабочий пример можно найти по ссылке.

Подробнее..

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

.apk/.aab

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

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

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

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

инструменты

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

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

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

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

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

баги

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

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

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

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

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

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

Аналитика

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

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

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

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

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

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

adb shell pm list packages

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

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

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

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

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

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

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

App Linking

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

Dynamic Links vs Deep Links

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

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

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

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

Crash

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

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

APM

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

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

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

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

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

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

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

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

Устройства

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • WebView,

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

  • браузер.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Фича

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

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

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

X

1,5X

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

X

1,8X

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

X

2X

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

X

1,4X

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

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

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

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

Подробнее..

Открылся набор в Indie Games Accelerator и Indie Games Festival от Google Play

14.06.2021 12:13:27 | Автор: admin

Indie Games Accelerator и Indie Games Festival две программы для независимых (инди) разработчиков мобильных игр, организованных командой Google Play. Программы направлены на то, чтобы помочь небольшим игровым студиям и разработчикам стать популярнее в Google Play независимо от того, на какой стадии находятся их проекты.

В этом году обе программы пройдут в онлайн-формате, заявки принимаются до 1 июля подробности под катом.

Для нас важно поддерживать не только крупные международные компании, но и небольшие инди-команды благодаря своей креативности и увлеченности играми, они создают уникальные и интересные проекты. Если вы работаете над уникальным проектом и хотите, чтобы о нем узнал мир, предлагаем вам принять участие в одной (или обоих сразу) из наших программ Indie Games Accelerator и Indie Games Festival.

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

Indie Games Accelerator: обучение и менторская поддержка

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

Проекты, которые пройдут отбор и станут участниками акселератора, смогут присоединиться к 12-недельной образовательной программе, а также получат возможность поработать над своими проектами вместе с экспертами из Google, крупных игровых студий и венчурных фондов. Rovio, Game Insight, Zynga, Play Ventures, Unity Technologies, Belka Games с полным списком менторов и условиями участия можно ознакомиться здесь.

В этом году в акселерационной программе участвуют более 70 стран, заявки на Indie Games Accelerator из России, Украины и Беларуси будут приниматься впервые!

Indie Games Festival: промо-кампании для финалистов

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

Основные критерии отбора: инновационность, увлекательность и дизайн. Среди призов: фичеринг на Google Play и промо-кампании для 3 игр-победителей стоимостью 100 000 евро.

Условия участия: в программе участвуют 29 стран Европы, включая Россию, Украину и Беларусь; максимальное количество человек в команде 50, игра должна быть выпущена на Google Play не ранее 3 марта 2020 г. Подробнее с правилами участия и критериями отбора можно ознакомиться здесь.

В прошлом году в финал конкурса прошло три проекта из России: My Diggy Dog 2 от King Bird Games, Color Spots от UX Apps и Tricky Castle от Team Tricky подать заявку можно до 1 июля.

Подробнее..

То, чего нам так не хватало Render Effect в Android 12

21.05.2021 10:23:30 | Автор: admin

Иногда бывает нужно размыть задний план на экранах мобильного приложения, например в чате. Теперь это можно сделать всего парой строк кода. В Android 12 появился новый API Render Effect, который позволяет накладывать визуальные эффекты на Canvas или View. Этот API радует своей простотой и высокой скоростью отрисовки. Наибольший интерес представляет Render Effect дляразмытия (BlurEffect), но в этой статье мы затронем и остальные виды эффектов. Материал может быть полезен не только андроид-разработчикам, но и дизайнерам мобильных приложений.

Итак, каким образом можно размыть задний план при отображении диалога? Раньше в Андроиде для этого надо было отрисовать все вью с заднего плана на bitmap-е и затем размыть его с помощью RenderScript или OpenGL. Но это означает, что в проекте появится немало запутанного для прочтения кода, либо придется подключить стороннюю библиотеку для размытия. Плюс добавятся обработчики событий отрисовки и код для получения битмапа. К тому же по производительности эти решения могут не давать желаемого результата. При использовании некоторых популярных библиотек для размытия разработчики замечают лаги, например, если есть RecyclerView, который содержит много BlurView.

С помощью Render Effect мы можем всего парой строк кода реализовать блюр без лагов. Render Effect работает эффективно за счет того, что он использует Render Nodes (узлы отрисовки). Они образуют иерархию аппаратно ускоренной отрисовки, и эта отрисовка происходит с помощью GPU (графического процессора). Перерисовываться будут только те части UI, которые оказались в состоянии invalidated. Все вычисления для Render Effect выполняются в Render потоке и не блокируют UI.

У Render Effect очень простой API:

val renderNode = RenderNode("myRenderNode")renderNode.setPosition(0, 0, 50, 50)renderNode.setRenderEffect(renderEffect) canvas.drawRenderNode(renderNode)

Мы добавили эффект для RenderNode методом setRenderEffect(), и его отрисовка происходит в методе Canvas.drawRenderNode().

Можно применить Render Effect и к узлу отрисовки вьюшки:

imageView.setRenderEffect(renderEffect)

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

val blurEffect = RenderEffect.createBlurEffect(       20f, //radiusX       20f, //radiusY              Shader.TileMode.CLAMP)imageView.setRenderEffect(blurEffect)

Здесь мы создаем эффект размытия и применяем его к узлу отрисовки, привязанному к imageView. Задаем интенсивность размытия по оси X и оси Y (Значения 20f) . Последний аргумент Shader.TileMode определяет то, как будет выглядеть эффект на краях отрисовываемой области.

Если применить размытие к корневому layout-у, то будут размыты все вьюшки: и кнопка, и ползунки.

Варианты значений TileMode:

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

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

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

  • DECAL (появился в API 31). Отрисовка shader-а только в пределах границ. Можно заметить, что на границе изображение стало чуть светлее.

Мы также можем применить размытие при создании тени. Конечно, можно просто создать тень с помощью свойства Elevation:

android:elevation="10dp"

Однако, в этом случае мы не сможем задавать направление, в котором должна отбрасываться тень, или ее цвет. Также тень для Elevation по умолчанию работает только с формами скругленного прямоугольника (круг и прямоугольник частные случаи прямоугольника со скругленными углами). Для тени другой формы нужно реализовать ViewOutlineProvider, чтобы он возвращал Outline с setPath(...), а это может быть весьма трудоемко.

Кастомную тень можно создать и с помощью xml, прописав <shape> с градиентом:

<gradient       android:type="radial"       android:centerColor="#90000000"       android:gradientRadius="70dp"       android:startColor="@android:color/white"       android:endColor="@android:color/transparent"/>

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

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

<shape android:shape="oval">   <solid android:color="#CCCCCC" /></shape>

Затем этот drawable устанавливаем в качестве содержимого ImageView и применяем размытие к ImageView:

imageView.setImageResource(R.drawable.gray_circle)val renderEffect = RenderEffect.createBlurEffect(20f, 20f, Shader.TileMode.CLAMP)imageView.setRenderEffect(renderEffect)

А вот как с помощью RenderEffect можно добиться тени произвольной формы и, если понадобится, произвольного цвета:

<ImageView   android:id="@+id/shadow"   android:layout_width="150dp"   android:layout_height="150dp"   android:src="@drawable/ic_baseline_time_to_leave_24"   android:tint="#444444"/>
val effect = RenderEffect.createBlurEffect(10f, 10f, Shader.TileMode.CLAMP)imageViewfindViewById<ImageView>(R.id.shadow).setRenderEffect(effect)

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

Всего есть семь видов RenderEffect:

  • Bitmap отрисовывает содержимое битмапа. Обычно используется в цепочке для наложения последующих эффектов.

  • Blur размытие по оси X и Y.

  • ColorFilter применяется цветной фильтр (см. скриншот ниже).

  • Offset сдвиг по осям X, Y (жаль, что нет поворота).

  • Shader отрисовывает шейдер, переданный в аргументах. С помощью шейдеров можно создавать, например, различные градиенты.

  • Chain комбинация 2 эффектов. Результат отрисовки одного эффекта используется как источник для второго эффекта.

  • BlendMode для объединения 2 эффектов в определенном режиме.

Можно добиться красивого эффекта замерзшего стекла (или запотевшего окна): он достигается сочетанием размытия и наложения прозрачно-белого цвета.

Применим такой эффект к панели внизу экрана. Фактически здесь надо сочетать три эффекта: отрисовка битмапа + размытие + цветной фильтр. К сожалению, нельзя сделать так, чтобы размывалась та часть UI, которая отрисована под вьюшкой. Поэтому RenderEffect нужно применить не к самой панели, а к тому, что находится под ней и содержит фоновое изображение: к imageView или к корневому layout-у. Комбинировать эффекты можно 2 способами: передавать один эффект в параметр create-метода другого эффекта, либо использовать метод createChainEffect():

val bmpEffect = RenderEffect.createBitmapEffect(...)val blurBmpEffect = RenderEffect.createBlurEffect(..., bmpEffect, ..)val finalEffect = RenderEffect.createColorFilterEffect(..., blurBitmapEffect)

или

val finalEffect = RenderEffect.createChainEffect(colorEffect,blurEffect)

Приведем пример наложения цветного фильтра:

val argb = Color.valueOf(red, green, blue, transparency).toArgb()val colorEffect = RenderEffect.createColorFilterEffect(       PorterDuffColorFilter(argb, PorterDuff.Mode.SRC_ATOP))

Результат:

Или можно с помощью цветного фильтра поменять насыщенность изображения:

val matrix = ColorMatrix()matrix.setSaturation(0f)imageView.setRenderEffect(RenderEffect.createColorFilterEffect(ColorMatrixColorFilter(matrix)))

Результат для setSaturation(0f) и setSaturation(100f):

BlendMode

С помощью метода RenderEffect.createBlendModeEffect() можно объединить два эффекта в одном из режимов:

  • CLEAR

  • SRC

  • DST

  • SRC_OVER

  • DST_OVER

  • SRC_IN

  • DST_IN

  • SRC_OUT

  • DST_OUT

  • SRC_ATOP

  • DST_ATOP

  • XOR

  • PLUS

  • MODULATE

  • SCREEN

  • OVERLAY

  • DARKEN

  • LIGHTEN

  • COLOR_DODGE

  • COLOR_BURN

  • HARD_LIGHT

  • SOFT_LIGHT

  • DIFFERENCE

  • EXCLUSION

  • MULTIPLY

  • HUE

  • SATURATION

  • COLOR

  • LUMINOSITY

Подробное описание режимов можно посмотреть здесь. Для примера приведем три режима (здесь источник, то есть первый RenderEffect это синий квадрат, а приемник, то есть второй Render Effect красный круг):

DST_ATOP выбрасывает пиксели, которые не накладываются на источник. DST_ATOP выбрасывает пиксели, которые не накладываются на источник. OVERLAY умножает цвета.OVERLAY умножает цвета.COLOR сохраняет оттенок и насыщенность источника, а яркость делает как у приемника.COLOR сохраняет оттенок и насыщенность источника, а яркость делает как у приемника.

Как уже упоминалось выше, RenderEffect работает крайне эффективно. При скролле RecyclerView с большим количеством элементов, у которых размыто по две ImageView (размытие с параметрами radiusX: 20f, radiusY: 20f, Shader.TileMode.CLAMP), никаких подтормаживаний не наблюдается. Размытие вью с анимацией тоже работает без задержек. В настоящий момент работа RenderEffect была проверена автором на эмуляторе Android 12 (API 31).

Что касается поддержки RenderEffect API, можно надеяться, что она будет добавлена в библиотеке AndroidX для Android 10 и 11, так как Render Nodes, лежащие в основе RenderEffect, были добавлены в Android 10 (API level 29). Однако, как пишут в официальной документации, Render Effect могут поддерживать не все устройства: Different Android devices may or may not support the feature due to limited processing power.

Отметим также, что Render Effect является одним из вариантов замены для RenderScript API, который устарел начиная с Android 12.

Заключение

Итак, в Android 12 мы получили то, чего так долго не хватало многим разработчикам простой API для отрисовки визуальных эффектов. Сочетая простые эффекты (размытие, цветные фильтры, шейдеры), мы можем получать интересные графические результаты. При этом больше не нужно возиться с вытаскиванием bitmap-изображения, эффект можно просто повесить на вьюшку. А благодаря использованию RenderNodes, отрисовка RenderEffect работает очень быстро.

Спасибо за внимание! Надеемся, что наш опыт был вам полезен.

Подробнее..

Рисуем светом длинная выдержка на Android

21.05.2021 22:21:42 | Автор: admin

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

Длинная выдержка

Выдержка - термин из мира фотографии, который определяет время открытия затвора при съемке. Чем дольше открыт затвор, тем дольше свет экспонирует светочувствительную матрицу. Проще говоря, делает фотографию более яркой. Современные фотоаппараты используют выдержки длинной в 1/2000 cекунды, что позволяет получить освещенную, но при этом не пересвеченную фотографию. Длинная выдержка подразумевает открытие затвора на секунду и больше. При верно выбранной сцене это позволяет получать фантастические фотографии, способные запечатлеть движение света в объективе камеры. Причем фотографировать можно что угодно: ночные улицы с мчащимися машинами или маятник, с укрепленным на нем фонариком, позволяющим выписывать фигуры Лиссажу. А можно вообще рисовать светом самому и получать целые картины-фотографии.

Улицы города, сфотографированные с использованием длинной выдержкиУлицы города, сфотографированные с использованием длинной выдержки

Улицы города, сфотографированные с использованием длинной выдержки

Теория

Для создания эффекта длинной выдержки можно использовать два подхода:

  • аппаратный - состоит в управлении физическим открытием и закрытием затвора

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

Главным недостатком аппаратного подхода является отсутствие возможности наблюдать за процессом появления фотографии онлайн - результат будет виден лишь после закрытия затвора и формирования изображения. Нарисовать что-либо светом человеку без опыта в таком режиме вряд ли удастся. Еще одним недостатком становится ограничение смартфонов на максимальное время выдержки - на Android оно составляет 30 секунд.

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

Практика

Для реализации работы с камерой смартфона будем использовать API CameraX. Это обусловлено ее гибкостью и лаконичностью. Также для программного подхода нам потребуется OpenGL ES для работы с изображениями. Данный выбор был сделан так как, это позволит работать напрямую с изображениями в видео памяти и обеспечить минимальную задержку при записи, так как вся обработка изображений происходит в реальном времени.

Аппаратный подход

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

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

val imageCaptureBuilder = ImageCapture.Builder()Camera2Interop.Extender(imageCaptureBuilder).apply {   setCaptureRequestOption(    CaptureRequest.CONTROL_AE_MODE,    CaptureRequest.CONTROL_AE_MODE_OFF  )  setCaptureRequestOption(    CaptureRequest.SENSOR_EXPOSURE_TIME,    EXPOSURE_TIME_SEC * NANO_IN_SEC  )}

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

val manager = getSystemService(CAMERA_SERVICE) as CameraManagerfor (cameraId in manager.cameraIdList) {  val chars = manager.getCameraCharacteristics(cameraId)  val range = chars.get(CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE)    Log.e("CameraCharacteristics", "Camera $cameraId range: ${range.toString()}")}

Программный подход

Для начала определимся с общей идеей нашей реализации.

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

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

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

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

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

#extension GL_OES_EGL_image_external : requireprecision mediump float;uniform mat4 stMatrix;uniform texType0 tex_sampler;uniform texType1 old_tex_sampler;varying vec2 v_texcoord;void main() {        vec4 color = texture2D(tex_sampler, (stMatrix * vec4(v_texcoord.xy, 0, 1)).xy);    vec4 oldColor = texture2D(old_tex_sampler, v_texcoord);      float oldBrightness = oldColor.r * 0.2126 + oldColor.g * 0.7152 + oldColor.b * 0.0722 + oldColor.a;     float newBrightness = color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722 + color.a;  // объединяем пиксели}

Работа шейдера состоит из нескольких этапов:

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

  2. Затем вычисляем яркость пикселя на обоих кадрах

  3. Объединяем пиксели

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

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

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

if (newBrightness > oldBrightness) {  gl_FragColor = color;} else {  gl_FragColor = oldColor;}

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

Длинная выдержкаДлинная выдержка

Длинная выдержка

Однако любая ошибка при рисовании требует перезапуска камеры, т.к. один раз попавший на нее свет уже нельзя стереть! Такое поведение приемлемо при выполнении каких-то заранее спланированных фотографий, но что если мы хотим просто рисовать светом и сохранять изображение, лишь когда нам действительно понравился результат? Перезапускать постоянно камеру совсем неудобно. Значит свет все-таки должен пропадать через какое то время. Этого можно добиться с помощью постепенного затухания ярких пикселей. Чтобы добиться такого эффекта достаточно просто на каждом новом шаге добавлять к каждому пикселю немного черного цвета (чтобы сохранять корректность картинки мы будем добавлять не черный цвет, а просто более темный пиксель из доступных - это позволит и эффект угасания получить и сохранить гамму цветов). Тогда объединение пикселей будет выглядеть следующим образом

if (newBrightness > oldBrightness) {    gl_FragColor = mix(color, oldColor, 0.01);} else {   gl_FragColor = mix(oldColor, color, 0.01);}

Вот несколько примеров с разными коэффициентами и временем затухания света.

Коэффициент 0.001Коэффициент 0.001

Коэффициент 0.001

Коэффициент 0.01Коэффициент 0.01

Коэффициент 0.01

Коэффициент 0.5Коэффициент 0.5

Коэффициент 0.5

Заключение

 Вот несколько примеров, что умеет получившееся приложение. Все таки я не художник :( А что нарисуете вы? Вот несколько примеров, что умеет получившееся приложение. Все таки я не художник :( А что нарисуете вы?

Вот несколько примеров, что умеет получившееся приложение. Все таки я не художник :( А что нарисуете вы?

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

Подробнее..

Какв Ozon пришли к релизам мобильных приложений раз в неделю

23.05.2021 16:04:26 | Автор: admin

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

С чем мы столкнулись, пока выпускали релизы по этой схеме:

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

  2. Долгоправитьбаги.Вкоде они могутбыть исправленыбыстро. А вотдо пользователейфиксдоходит уже вместе с той самой глобальной фичей.

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

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

  5. Невыносимо долго проводить регрессионное тестирование. Всё,что было переделаноза несколько месяцев,мы будем проверять, чинить, перепроверять, чинить, перепроверятьНу, вы поняли

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

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

Мы в шоке, как это реализоватьнепонятно.Мы в шоке, как это реализоватьнепонятно.

Тут у насслучиласьстадия отрицания:Мы не успеем всёпроверить, никаких фичей в релиз не попадет, Apple ревьюит о-го-госколько.Зачем, почему, может,не надо?.В ответ мы услышали: Релизы раз в неделю.

Сокращаем время выпуска релиза: первая попытка

Решили, что надодвигаться к целиитерационно.

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

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

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

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

Вторая попытка: нужно что-то менять

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

Тем временемтестировщикипродолжают писатьавтотесты

Пробуем всёравно каждый раз не успеваем.

Стали анализировать,из-за чегоне выходит уложиться в срок:

  1. Неправильно оцениваем время на разработку.

  2. Блочимсябекендом от этого тормозится и разработка, и тестирование.

  3. Продактыпоздно вносятпоследниеправки в ТЗ.

  4. Не учитываем время на починкубагов.

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

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

Разработчиков много, они разделены по разномуфункционалуи задвенедели успеваютнакомититьпрактически во все компоненты. Стало понятно, что несмотря на то, что мыНИ РАЗУне успели в срок ни при месячном релизе, ни при двухнедельных пора двигаться дальше.

Недельные релизымыидём к вам!

Прежде всего надо было составить календарь наших действий на неделю. По разработке получилось так:

Понедельник

Релизная ветка

Вторник

Фичи

Среда

Фичи

Четверг

Фичи

Пятница

Фиксы багов

Фичиуходятвсвои фича-ветки. И когда она полностью сделана, проверена, принятапродактом, тогда уже попадает вdevelop.

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

Получается, что где-то в среду-четверг должен начаться процентныйроллаутприложения.

Меняемся в тестировании

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

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

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

  1. Дизайн;

  2. Бекенд;

  3. Контракт.

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

Дальше встал вопрос о том, когда же можно включатьфичув релиз. Получились такие правила:

  1. Бекендфичидолжен быть напродакшене.

  2. К началурелизноготестирования нет критичныхбагов(ни на фронте, ни набекенде).

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

Меняемся в разработке

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

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

Тикетавтоматом двигается по статусам. Запушилкоммит перешел вinprogress.Создалmergerequest перешел вcodereview.ПрошелreviewпопалвQA.

Втикетеавтоматически проставляется версия релиза, куда он попал и номербилда.

По каждой сборке запускаютсяUI-автотестыпозатронутомуфункционалу.Это тоже определяется самопо измененным файлам вmergerequest.В результате репорт попадаетвкомментарийтикета вJira.

Дажеmergerequestна влитие эпика вdevсоздаётсяпросто по принятию продактом фичивJira.Если нет конфликтов, то и вливается сам.Релиз сам закрывается, а новый самсоздаётся.

QA Notes

Ещёмы ввели требования к разработчикам писатьQANotes.Там указывается:

  1. Что было сделано.

  2. Что могло быть задето.

  3. Приложены скриншоты или видео.

  4. На какой среде удалось проверить.

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

QANotesпозволили значительно ускоритьтестированиеиревьюкода. А ещёдали нам скрытый бонус:пропалиреопеныотQAиз-закрешейна старте.

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

Автотестыраньше находят баги. Теперь не надо ждатьрелизноготестирования, чтобы увидеть проблемы после интеграции. Прогоныавтотестовпроисходят после каждоговливания эпика вdev,а такжекаждуюночь. Еслинадо, то можно запустить на эпик-веткедо вливания вdev. Раньше нашли баг раньше исправили.

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

А еще добавиласьротируемаяроль QAза релиз.Этоттестировщикгде-то раз вдватримесяцаделаетповторяющиесявещи:

  1. Составляет наборрелизныхтестов.

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

  3. Напоминаеттестировщикампосмотретьотчёты поавтотестамнарелизномбилде.

  4. Пушитпересборкурелиз-кандидатов, если что-то добавилось.

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

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

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

Мы также проверяем, что новыеавтотестыне ломают существующие:

Новая схема релиза мобильных приложений

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

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

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

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

Какиеестьсложности

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

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

Что ещёможно улучшить

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

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

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

  1. Строгие требования к готовностифичи.

  2. Приоритет напереоткрытыхтикетах.

  3. Весь функционалзакрытфичефлагами.

  4. Строгое расписание релизов.

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

Подробнее..

За что банит Apple(и Google)

27.05.2021 02:21:05 | Автор: admin

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

Рассмотрим некоторые из них.

Покупки не через сервисы Google&Apple

Начнем с одной из самых известных сейчас блокировок - удаление игры Fortnite от Epic Games из мобильных сторов. Издатель решил, что отдавать 30 процентов комиссии с каждой покупки слишком много и сделал оплату в обход стандартного механизма In-app payment. Что, конечно, запрещено. И ни Apple, ни Google не захотели терять свой доход(хотя на некоторые послабления уже пошли Apple Google).

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

COVID-19

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

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

В итоге в мае сторы на поисковые запросы давали такие результатыВ итоге в мае сторы на поисковые запросы давали такие результаты

Apple ссылались на пункт 5.2.1 Apps should be submitted by the person or legal entity that owns or has licensed the intellectual property and other relevant rights.

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

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

Хранение данных

Когда-то я работал в компании, которой прилетел реджект за то, что мы храним данные приложения в iCloud'е пользователя. Делали мы это не специально, а по не знанию) При этом, как оказалось, какие-то данные хранить в iCloud можно, но это должен быть сгенерированный пользователем контент.

Это самый первый пункт из iOS Data Storage Guidelines: "Only documents and other data that is user-generated, or that cannot otherwise be recreated by your application, should be stored in the <Application_Home>/Documents directory and will be automatically backed up by iCloud. "

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

Пермишены

В iOS 14.5 стал обязательным запрос нового пермишена - про трекинг данных пользователя. Компании принялись рассказывать юзерам на предварительных экранах почему же надо разрешить трекинг и... некоторые столкнулись с блокировками как раз из-за онбординг экрана для пермишена. Дело было в добавлении двух кнопок на этот экран. По нажатию одной - запрашивалось разрешение, второй - нет. В гайдланах это строчка "If you display a custom screen that precedes a privacy-related permission request, it must offer onlyoneaction, which must display the system alert."

При этом, например, у facebook'а получалось проходить ревью с двумя кнопками При этом, например, у facebook'а получалось проходить ревью с двумя кнопками Но позже и facebook, и instagram заменили эти экраны на однокнопочныеНо Но позже и facebook, и instagram заменили эти экраны на однокнопочныеНо

Ссылки на другие приложения

Когда я начинал разрабатывать свои приложения, то пользовался кросслинками из одного в другое. И так мои новые игры набирали лояльную аудиторию из старых. Довольно неплохо работало. Делал я это максимально примитивно - ставил иконку нового приложения в угол экрана меню старых. А еще на экране подтверждения закрытия приложения третьей кнопкой был переход в новую игру. Но через какое-то время Google начал поочередно блокировать одно приложение за другим, т.к. "Ads must not simulate the user interface of any app". Оказалось, что к подобным переходам надо явно писать, что они являются рекламой.

Слишком взрослый рейтинг

Напоследок совсем забавная для меня формулировка. Первая от Google. Приложения не пропускали в стор из-за того, что google play решил, что поставлен слишком высокий возрастной рейтинг. Сделано это, чтобы не заморачиваться с контентом, который мог где-нибудь(например, в рекламе) появиться, а детям его показывать не стоит. Но google сказал, что "We determine that some elements of your store listing may appeal to children under 13: Animated characters in app icon, young characters". Пришлось понижать возрастной рейтинг и фильтровать "опасные" категории рекламы.

А с какими блокировками приходилось сталкиваться вам?

Подробнее..

Проекты в Gradle 7 как не зависеть от зависимостей

03.06.2021 16:16:28 | Автор: admin

Привет! Меня зовут Ксения Кайшева, я пишу приложения под Android в компании 65apps. Сегодня расскажу о новой возможности, которая позволяет централизованно описывать зависимости на проектах с системой сборки Gradle.

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

В 7й версии Gradle представлена новая функция, позволяющая описывать все зависимости централизованно. Эта функция находится на стадии превью, и чтобы воспользоваться ей в файле settings.gradle(.kts) необходимо добавить строку:

enableFeaturePreview("VERSION_CATALOGS")

Так выглядит использование (описанных в централизованном каталоге) зависимостей в любом build.gradle скрипте:

dependencies {
implementation libs.lifecycle.runtime
implementation libs.lifecycle.viewmodel.ktx
implementation libs.lifecycle.extentions
implementation libs.lifecycle.livedata.ktx
}

Здесь:

libs это сам каталог
lifecycle.runtime это зависимость в этом каталоге.

Каталог описывается в settings.gradle(.kts) файле:

dependencyResolutionManagement {
versionCatalogs {
libs {
alias('lifecycle-runtime')
.to(androidx.lifecycle:lifecycle-runtime:2.2.0')
alias('lifecycle-viewmodel-ktx').to(androidx.lifecycle', 'lifecycle-viewmodel-ktx').version {
strictly '[2.2.0, 2.3.0['
prefer '2.3.1'
}
}
}
}

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

Разделение через тире является рекомендованным.

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

Например,

lifecycle-runtime
lifecycle_runtime
lifecycle.runtime
junit5-test-core
spek-runner-junit5

Недопустимо иметь псевдоним для зависимости, которая также принадлежит вложенной группе. Например, lifecycle-viewmodel и lifecycle-viewmodel-ktx.

Gradle рекомендует в таком случае использовать регистр для различения.
Например, lifecycleViewmodel и lifecycleViewmodelKtx.

Версии можно объявлять отдельно и затем ссылаться на них в описаниях самих зависимостей:

dependencyResolutionManagement {
versionCatalogs {
libs {
version('lifecycle', '2.3.1')
alias('lifecycle-viewmodel-ktx').to('androidx.lifecycle', 'lifecycle-viewmodel-ktx').versionRef('lifecycle')
}
}
}

Объявленные таким образом версии также доступны из любогоbuild.gradle файла:

version = libs.versions.lifecycle.get()

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

dependencyResolutionManagement {
versionCatalogs {
libs {
version('lifecycle', '2.3.1')
alias('lifecycle-runtime').to('androidx.lifecycle, 'lifecycle-runtime').versionRef('lifecycle')
alias('lifecycle-viewmodel-ktx').to('androidx.lifecycle, 'lifecycle-viewmodel-ktx').versionRef('lifecycle')
bundle('lifecycle',
['lifecycle-runtime', 'lifecycle-viewmodel-ktx'])
}
}
}

Подключение пакета зависимостей будет выглядеть так:

dependencies {
implementation libs.bundles.lifecycle
}

Добавление одного пакета эквивалентно добавлению всех зависимостей из пакета по отдельности.

Помимо описания каталога в settings.gradle(.kts) файле, есть более простая возможность собрать все зависимости вместе использовать toml-файл каталоге gradle: libs.versions.toml.

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

По умолчанию libs.versions.toml файл будет входом в libs каталог. Можно также изменить имя каталога по умолчанию, например:

dependencyResolutionManagement {
defaultLibrariesExtensionName.set('deps')
}

Toml-файл состоит из 3 основных разделов:

[versions] - раздел для объявления версий
[libraries] - раздел для объявления зависимостей
[bundles] - раздел для объявления пакетов зависимостей

Например,

[versions]
lifecycle = "2.3.1"

[libraries]
lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycle" }
lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }

[bundles]
dagger = ["lifecycle-runtime", "lifecycle-viewmodel-ktx"]

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

[versions]
any-lib1 = 1.0
any-lib2 = { strictly = "[1.0, 2.0[", prefer = "1.2" }

Более подробно о расширенном варианте версии по ссылке

Семантика объявления номера версии по ссылке

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

[libraries]
any-lib = "com.company:anylib:1.4"
any-other-lib = { module = "com.company:other", version="1.4" }
any-other-lib2 = { group = "com.company", name="alternate", version="1.4" }
anylib-full-format = { group = "com.company", name="alternate", version={ require = "1.4" } }

Если необходимо сослаться на версию, объявленную в [versions] разделе, то следует использовать свойство version.ref:

[versions]
some = "1.4"

[libraries]
any-lib = { group = "com.company", name="anylib", version.ref="some" }

Можно использовать несколько toml-файлов.Для этого нужно указать, как импортировать соответствующий файл:

dependencyResolutionManagement {
versionCatalogs {
testLibs {
from(files('gradle/test-libs.versions.toml'))
}
}
}

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

Подробнее по ссылке

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

При использовании Groovy не работает автоподстановка при указании зависимости в build.gradle файле и, соответственно, нет возможности провалиться в описание зависимости при нажатии на нее. Исправлять это не планируют. Решение для автоподстановки использовать Kotlin DSL.

Подробнее..

Основы Flutter для начинающих (Часть I)

30.05.2021 12:21:38 | Автор: admin

Вступление

Добрый день всем желающим познакомиться с Flutter!

У меня появилось горячее желание поделиться с вам моими знаниями, которые я накопил за несколько месяцев.

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

Результатом нашей работы будет небольшое Flutter приложение, которое будет брать данные из JSONPlaceholder.

Первый шаг - Настройка и установка компонентов

Ну что ж, приступим.

Переходим на страницу установки: Install - Flutter и загружаем Flutter для своей платформы

Затем устанавливаем редактор или IDE по инструкции Set up an editor

Я буду использовать Android Studio IDE от Google.

Для разработки на Android Studio нужно установить Flutter плагин (в инструкции Set up an editor, описано как это сделать).

Второй шаг - Создание проекта

Выбираем Flutter Application

Далее указываем название приложения (имя Flutter приложения должно иметь нижний регистр, отдельные слова могут разделяться нижним подчеркиванием).

Затем указываем package name (используется для того, чтобы идентифицировать наше приложение среди других в Google Play или Apple Store, его впоследствии можно будет изменить, более подробно об Android Application ID или об Apple App ID):

Нажимаем Finish.

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

Очищаем main.dart файл от ненужного кода:

import 'package:flutter/material.dart';// main() является главной функцией с которой начинается // выполнение приложения// возвращает виджет приложенияvoid main() => runApp(MyApp());// В Flutter все является виджетом (кнопки,списки, текст и т.д.)// виджет - это отдельный компонент, который может быть отрисован// на экране (не путать с Android виджетами)// Наиболее простые виджеты наследуются от StatelessWidget класса// и не имеют состоянияclass MyApp extends StatelessWidget {// функция build отвечает за построение иерархии виджетов  @override  Widget build(BuildContext context) {// виджет MaterialApp - главный виджет приложения, который  // позволяет настроить тему и использовать    // Material Design для разработки.    return MaterialApp(    // заголовок приложения      // обычно виден, когда мы сворачиваем приложение      title: 'Json Placeholder App',      // настройка темы, мы ещё вернёмся к этому      theme: ThemeData(        primarySwatch: Colors.blue,      ),      // указываем исходную страницу, которую мы создадим позже      home: HomePage(),    );  }}

Затем создаем пакет (код должен быть всегда огранизован, дабы сделать его понятнее):

Называем его pages:

Затем создаем в пакете файл home_page.dart:

И реализуем нашу первую страницу:

import 'package:flutter/material.dart';// StatefulWidget имеет состояние, с которым// позже мы будем работать через функцию// setState(VoidCallback fn);// обратите внимание setState принимает другую функциюclass HomePage extends StatefulWidget {  // StatefulWidget должен возвращать класс,  // которые наследуется от State  @override  _HomePageState createState() => _HomePageState();}// В треугольных скобках мы указываем наш StatefulWidget // для которого будет создано состояние// нижнее подчеркивание _ используется для того, // чтобы скрыть доступ к _HomePageState  из других файлов// нижнее подчеркивание аналогия private в Java / Kotlinclass _HomePageState extends State<HomePage> {    // функция buil, как мы уже отметили, строит  // иерархию наших любимых виджетов  @override  Widget build(BuildContext context) {    // В большинстве случаев Scaffold используется,    // как корневой виджет для страницы или экрана    // Scaffold позволяет вам указать AppBar, BottomNavigationBar,    // Drawer, FloatingActionButton и другие не менее важные    // компоненты (виджеты).    return Scaffold(      // мы создаем AppBar с текстом "Home Page"      appBar: AppBar(title: Text("Home page")),      // указываем текст в качестве тела Scaffold      // текст предварительно вложен в Center виджет,      // чтобы выровнять его по центру        body: Center(        child: Text(          "Hello, JSON Placeholder!!!",          // Также выравниваем текст внутри самого виджета Text          textAlign: TextAlign.center,          // Theme.of(context) позволяет получить доступ к           // текущему ThemeData, который был указан в MaterialApp          // После получения ThemeData мы можем использовать          // различные его стили (например headline3, как здесь)          style: Theme.of(context).textTheme.headline3,        )      )    );  }  }

Обратите внимание на мощь Flutter - мы можем вкладывать различные виджеты друг в друга, комбинировать их и создавать более сложные структуры

Четвертый шаг - запуск

Ну что ж, пора испытать наше приложение.

Не забудьте импортировать HomePage в main файл:

import 'pages/home_page.dart';

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

import 'package:json_placeholder_app/pages/home_page.dart';

По моему небольшому опыту могут возникнуть проблемы, когда вы захотите поменять имя приложения в pubspec.yaml файле (pubspec.yaml находиться в корневой директории проекта):

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

Переходим к запуску, выбираем устройство на котором будет выполняться приложение (в данном случае я использую реальное устройство, мой Honor 30i), и нажимаем Run:

Та дам!

Если вас раздражает надпись DEBUG в правом верхнем углу, то её можно убрать:

import 'package:flutter/material.dart';// main() является главной функцией с которой начинается // выполнение приложения// возвращает виджет приложенияvoid main() => runApp(MyApp());// В Flutter все является виджетом (кнопки,списки, текст и т.д.)// виджет - это отдельный компонент, который может быть отрисован// на экране (не путайте с Android виджетами)// Наиболее простые виджеты наследуются от StatelessWidget класса// и не имеют состоянияclass MyApp extends StatelessWidget {// функция build отвечает за построение иерархии виджетов  @override  Widget build(BuildContext context) {    // виджет MaterialApp - главный виджет приложения, который    // позволяет настроить тему и использовать    // Material Design для разработки.    return MaterialApp(      // заголовок приложения      // обычно виден, когда мы сворачиваем приложение      title: 'Json Placeholder App',      // убираем баннер      debugShowCheckedModeBanner: false,      // настройка темы, мы ещё вернёмся к этому      theme: ThemeData(        primarySwatch: Colors.blue,      ),      // указываем исходную страницу, которую мы создадим позже      home: HomePage(),    );  }}

Также обратите внимание, когда вы запустите приложение, вы можете использовать hot reload:

Hot Reload позволяет буквально за 2-5 секунд внести изменения, когда ваше приложение выполняется.

Это довольно приятная опция, которая ускорит вашу разработку.

При каждом вызове Hot Reload происходит перезапуск build функции. (вся иерархия виджетов перестраивается)

Будьте внимательны: не во всех ситуциях Hot Reload срабатывает и изменения отражаются в приложении, поэтому в таких ситуациях нужно перезапускать приложение полностью.

Также есть довольно интересный факт: размер отладочного приложения на Flutter с одним экраном, которое мы только что создали:

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

Отладочное приложение содержит много дополнительной информации, а также к этому добавляется поддержка Hot Reload.

Четвертый шаг - использование состояния

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

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

import 'package:flutter/material.dart';// StatefulWidget имеет состояние, с которым// позже мы будем работать через функцию// setState(VoidCallback fn);// обратите внимание setState принимает другую функциюclass HomePage extends StatefulWidget {  // StatefulWidget должен возвращать класс,  // которые наследуется от State  @override  _HomePageState createState() => _HomePageState();}// В треугольных скобках мы указываем наш StatefulWidget// для которого будет создано состояние// нижнее подчеркивание используется для того, чтобы // скрыть доступ к _HomePageState из других файлов// нижнее подчеркивание - аналогия private в Java / Kotlinclass _HomePageState extends State<HomePage> {  // добавим переменную, которая будет нашим состоянием  // т.к. _counter мы будем использовать только внутри нашего  // класса, то сделаем его недоступным для других классов  // _counter будет хранить значение счетчика  var _counter = 0;  // build как мы уже отметили, строит  // иерархию наших любимых виджетов  @override  Widget build(BuildContext context) {    // В большинстве случаев Scaffold используется    // как корневой виджет для страницы или экрана    // Scaffold позволяет вам указать AppBar, BottomNavigationBar,    // Drawer, FloatingActionButton и другие не менее важные    // компноненты (виджеты).    return Scaffold(      // мы создаем AppBar с текстом "Home page"      appBar: AppBar(title: Text("Home page")),      // указываем текст в качестве тела Scaffold      // текст предварительно вложен в Center виджет,      // чтобы выровнять его по центру      body: Center(        // добавляем AnimatedSwitcher, который и будет управлять        // нашей анимацией        child: AnimatedSwitcher(          // обратите внимание: const указывает          // на то, что нам известно значение Duration во время          // компиляции и мы не будем его менять во время выполнения          // класс Duration позволяет указать задержку в разных          // единицах измерения (секунды, миллисекунды и т.д.)          duration: const Duration(milliseconds: 900),          // AnimatedSwitcher создает reverse эффект,          // то  есть эффект возврата анимации к первоначальному          // состоянию, что выглядит не всегда красиво,          // поэтому я указал reverseDuration в 0          // вы можете поэкспериментировать с этим значением          reverseDuration: const Duration(milliseconds: 0),          child: Text(            // вывод значения счетчика            // при каждой перерисовки виджетов _counter             // увеличивается на единицу            "$_counter",            // здесь самое интересное            // когда мы изменяем значение _counter            // и вызываем функцию setState, компоненты            // перерисовываются и AnimatedSwitcher сравнивает            // предыдущий key своего дочернего виджета с текущим,            // если они не совпадают, то вопроизводит анимацию            key: ValueKey<int>(_counter),            // Также выравниваем текст внутри самого виджета Text            textAlign: TextAlign.center,            // Theme.of(context) позволяет получить доступ к            // текущему ThemeData, который мы указали в MaterialApp            // После получения ThemeData мы можем использовать            // различные его стили (например headline3, как здесь)            style: Theme.of(context).textTheme.headline3,          ),        )      ),      // добавляем кнопку      // FloatingActionButton - круглая кнопка в правом нижнем углу      floatingActionButton: FloatingActionButton(        // указываем иконку        // Flutter предлагает нам большой спектр встроенных иконок        child: Icon(Icons.animation),        onPressed: () {          // наконец то мы дошли до функции setState          // которая даст сигнал, что пора перерисовывать           // наши виджеты.           // здесь мы просто увеличиваем наш счетчик          setState(() {            _counter++;          });        },      ),    );  }}

Выполняем приложение:

Та дам! Выглядит здорово!

Заключение

Статья получилась достаточно информативной и по моему мнению полезной для новичков.

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

Примерный план:

1) Часть 1 (текущая статья) - введение в разработку, первое приложение, понятие состояния;

2) Часть 2 - BottomNavigationBar и Navigator;

3) Часть 3 - MVC. Мы будем использовать именно этот паттерн, как один из самый простых;

4) Часть 4 - http пакет. Создание Repository класса, первые запросы, вывод списка постов;

5) Часть 5 - Работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;

6) Часть 6 - Создание своей темы, добавление кастомных шрифтов и анимации;

7) Часть 7 - Немного о тестировании;

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

Все пожелания в комментариях)

До скорой встречи!

Подробнее..

Основы Flutter для начинающих (Часть II)

31.05.2021 18:12:13 | Автор: admin

Вступление

Добрый денек!

Мы продолжаем изучать Flutter.

И в этой статье мы познакомимся с файлом pubspec.yaml, а также поработаем с Flutter в командной строке.

Ну что ж, приступим!

Наш план
  • Часть 1 - введение в разработку, первое приложение, понятие состояния;

  • Часть 2 (текущая статья) - файл pubspec.yaml и использование flutter в командной строке;

  • Часть 3 - BottomNavigationBar и Navigator;

  • Часть 4 - MVC. Мы будем использовать именно этот паттерн, как один из самый простых;

  • Часть 5 - http пакет. Создание Repository класса, первые запросы, вывод списка постов;

  • Часть 6 - Работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;

  • Часть 7 - Создание своей темы, добавление кастомных шрифтов и анимации;

  • Часть 8 - Немного о тестировании;

Файл pubspec.yaml

Расширение .yaml указывает на то, что мы используем YAML формат данных (более подробнее в Википедии).

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

pubspec.yaml находится в корневой директории проекта и служит для общей настройки, добавления зависимостей, шрифтов, картинок в ваш проект.

Немного об организации файлов:

  • .dart-tool содержит информацию для Dart Tools (набор различных утилит для работы с кодом Dart)

  • .idea была создана самой Android Studio и хранит настройки проекта

  • build содержит файлы сборки, в том числе и наш release apk

  • ios папка содержит нативный код iOS и предназначена для отдельной настройки iOS приложения, а также его публикации через XCode

  • android папка содержит нативный код Android и предназначена для отдельной настройки Android приложения

  • lib содержит непосредственно наш код на Dart

  • test предназначена для тестов

    Далее идет несколько файлов:

  • README.md и .gitignore - это файлы Git

  • о pubspec.yaml мы говорили выше, а pubspec.lock содержит информацию о версиях наших pub-пакетов.

  • .metadata содержит необходимую информацию для обновления Flutter

  • .packages дополнительная информация о пакетах

Рассмотрим минимальный pubspec.yaml:

# имя Flutter приложения# обычно данное имя используется в качестве# названия pub-пакета. Это важно лишь в том случае,# если вы разрабатываете свой pub-пакет и собираетесь# выложить его в общий доступ# как я уже отметил имя Android и iOS приложения впоследствии# можно будет изменить отдельно для каждой из платформname: json_placeholder_app# краткое описание на английскомdescription: json_placeholder_app is an demo application# в данном случае мы не собираемся# опубликовывать pub-пакет и поэтому# запрещаем команду flutter publishpublish_to: 'none' # версия Android и iOS приложения# состоит из 2 частей, разделенных знаком плюса# первая часть - это имя версии, которое будет# видно для пользователей, например 1.1.5# вторая часть позволяет Google Play и Apple Store# отличать разные версии нашего приложения (например: 5)version: 1.0.0+1# версия Dart SDKenvironment:  sdk: ">=2.7.0 <3.0.0"# блок зависимостейdependencies:  flutter:    sdk: flutter  # использование иконок для Cupertino компонентов  # Cupertino компоненты - это компоненты в стили iOS  # В данном приложении мы не будем использовать их и поэтому  # удалим ненужный pub-пакет  #cupertino_icons: ^1.0.2# зависимости для разработки# в данном случае подключено тестированиеdev_dependencies:  flutter_test:    sdk: flutter# в данной секции вы можете подключить шрифты и assets файлы# об этом мы поговорим позжеflutter:  # указываем, что мы используем MaterialApp иконки и наше  # приложение соответствует Material Design  uses-material-design: true

Немного о pub-пакетах

Все pub-пакеты расположены на pub.dev. Здесь вы можете найти довольно большое количество интересных и полезных пакетов и плагинов для ваших приложений.

Все pub-пакеты делятся на собственно пакеты и плагины.

В чем же отличие пакета от плагина?

Пакет - это код на Dart с pubspec.yaml файлом, а плагин - подвид пакета, который содержит нативный код какой-либо платформы.

Например плагин camera позволяет получить доступ к камере на Android и iOS устройствах и содержит нативный код отдельно для Android (папка android) и отдельно для iOS (папка ios)

Добавление зависимостей

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

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

# имя Flutter приложения# обычно данное имя используется в качестве# названия pub-пакета. Это важно лишь в том случае,# если вы разрабатываете свой pub-пакет и собираетесь# выложить его в общий доступ# как я уже отметил имя Android и iOS приложения впоследствии# можно будет изменить отдельно для каждой из платформname: json_placeholder_app# краткое описание на английскомdescription: json_placeholder_app is an demo application# в данном случае мы не собираемся# опубликовывать pub-пакет и поэтому# запрещаем команду flutter publishpublish_to: 'none' # версия Android и iOS приложения# состоит из 2 частей, разделенных плюсом# первая часть - это имя версии, которое будет# видно для пользователей, например 1.1.5# вторая часть позволяет Google Play и Apple Store# отличать разные версии нашего приложения (например: 5)version: 1.0.0+1# версия Dart SDKenvironment:  sdk: ">=2.7.0 <3.0.0"  # блок зависимостейdependencies:  flutter:    sdk: flutter      # подключение необходимых pub-пакетов    # используется для произвольного размещения  # компонентов в виде сетки  flutter_staggered_grid_view: ^0.4.0    # мы будем использовать MVC паттерн  mvc_pattern: ^7.0.0    # большая часть данных будет браться из сети,  # поэтому мы будем использовать http для  # осуществления наших запросов  http: ^0.13.3    # зависимости для разработки# в данном случае подключено тестированиеdev_dependencies:  flutter_test:    sdk: flutter# в данной секции вы можете подключить шрифты и assets файлы# об этом мы поговорим позжеflutter:  # указываем, что мы используем MaterialApp иконки и наше  # приложение соответствует Material Design  uses-material-design: true

Пока на этом все!

Flutter в командной строке

Для начала нужно запустить командную строку или терминал.

У меня Debian 10, поэтому я буду использовать терминал.

Теперь надо определить местоположения главного flutter скрипта.

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

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

В директории Flutter есть папка bin, в которой лежит главный скрипт - flutter.

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

Ну что ж давайте пройдемся по основным командам.

Создание проекта

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

# также перед созданием проекта можно отключить web поддержку# с помощью команды: flutter config --no-enable-webflutter create new_flutter_app

Результат:

Установка зависимостей

Для установки зависимостей нужно выполнить:

flutter pub get

Вы также можете использовать встроенный терминал в Android Studio:

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

flutter devices

Результат:

Здесь мы видем мой Honor и Chrome браузер (т.к. включена web поддержка)

Запуск

Для запуска нужно указать устройство через параметр -d

flutter run -d JYXNW20805003141

Результат:

Получение скрина

Теперь когда мы запустили наше приложение, мы можем получить скрин с экрана нашего устройства c помощью команды:

# -d указываем устройство# -o путь в файлу, куда будет сохранен наш скринflutter screenshot -d JYXNW20805003141 -o ~/Downloads/screen_1.png

Результат:

Скрин:

Сборка релиза

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

Данный этап мы рассмотрим в заключительных уроках.

Для создания release apk выполните:

flutter build apk --release

Результат:

В данном случае мы имеем неподписанный apk с набором всех архитектур (armeabi-v7a, arm64-v8a и 86_64).

Лучшим вариантом является использование опции --split-per-abi для разделения архитектур по разным файлам:

flutter build apk --split-per-abi

Результат:

Допольнительные команды

Определение версии Flutter:

flutter --version

Обновление Flutter:

flutter upgrade

Чтобы получить справку по какой-либо команде нужно использовать --help опцию:

flutter create --help

Результат:

Заключение

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

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

Не забывайте оставлять пожелания в комментах)))

Далее переходим к навигации.

Подробнее..

Перевод Всё о PendingIntents

01.06.2021 18:16:36 | Автор: admin

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

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

Что такое PendingIntent?

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

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

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

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

Распространенный случай

Самый распространенный и наиболее простой способ использования PendingIntent - это действие, связанное с уведомлением:

val intent = Intent(applicationContext, MainActivity::class.java).apply {    action = NOTIFICATION_ACTION    data = deepLink}val pendingIntent = PendingIntent.getActivity(    applicationContext,    NOTIFICATION_REQUEST_CODE,    intent,    PendingIntent.FLAG_IMMUTABLE)val notification = NotificationCompat.Builder(        applicationContext,        NOTIFICATION_CHANNEL    ).apply {        // ...        setContentIntent(pendingIntent)        // ...    }.build()notificationManager.notify(    NOTIFICATION_TAG,    NOTIFICATION_ID,    notification)

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

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

После вызова NotificationManagerCompat.notify() все готово. Система отобразит уведомление и, когда пользователь нажмет на него, вызовет PendingIntent.send() для нашего PendingIntent, запуская наше приложение.

Обновление неизменяемого PendingIntent

Вы можете подумать, что если приложению нужно обновить PendingIntent, то он должен быть изменяемым, но это не всегда так! Приложение, создающее PendingIntent, всегда может обновить его, передав флаг FLAG_UPDATE_CURRENT:

val updatedIntent = Intent(applicationContext, MainActivity::class.java).apply {   action = NOTIFICATION_ACTION   data = differentDeepLink}// Because we're passing `FLAG_UPDATE_CURRENT`, this updates// the existing PendingIntent with the changes we made above.val updatedPendingIntent = PendingIntent.getActivity(   applicationContext,   NOTIFICATION_REQUEST_CODE,   updatedIntent,   PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)// The PendingIntent has been updated.

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

Технология Inter-app APIs

Распространенный случай полезен не только для взаимодействия с системой. Хотя для получения обратного вызова после выполнения действия чаще всего используются startActivityForResult() и onActivityResult(), это не единственный способ.

Представьте себе приложение для онлайн-заказа, которое предоставляет API для интеграции с ним приложений. Оно может принять PendingIntent как extra к своему собственному Intent, которое используется для запуска процесса заказа еды. Приложение заказа запускает PendingIntent только после того, как заказ будет доставлен.

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

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

Изменяемые PendingIntents

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

Ответом на этот вопрос является использование изменяемого PendingIntent.

Поскольку PendingIntent это, по сути, обертка вокруг Intent, можно подумать, что существует метод PendingIntent.getIntent(), который можно вызвать для получения и обновления обернутого Intent, но это не так. Так как же это работает?

Помимо метода send() в PendingIntent, который не принимает никаких параметров, есть несколько других версий, включая эту, которая принимает Intent:

fun PendingIntent.send(    context: Context!,     code: Int,     intent: Intent?)

Этот параметр intent не заменяет Intent, содержащийся в PendingIntent, а скорее используется для заполнения параметров из обернутого Intent, которые не были предоставлены при создании PendingIntent.

Давайте рассмотрим пример.

val orderDeliveredIntent = Intent(applicationContext, OrderDeliveredActivity::class.java).apply {   action = ACTION_ORDER_DELIVERED}val mutablePendingIntent = PendingIntent.getActivity(   applicationContext,   NOTIFICATION_REQUEST_CODE,   orderDeliveredIntent,   PendingIntent.FLAG_MUTABLE)

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

val intentWithExtrasToFill = Intent().apply {   putExtra(EXTRA_CUSTOMER_MESSAGE, customerMessage)}mutablePendingIntent.send(   applicationContext,   PENDING_INTENT_CODE,   intentWithExtrasToFill)

Тогда вызывающее приложение увидит дополнительное EXTRA_CUSTOMER_MESSAGE в своем Intent и сможет отобразить сообщение.

Важные соображения при объявлении изменяемости отложенного намерения (pending intent)

  • При создании изменяемого PendingIntent ВСЕГДА явно задавайте компонент, который будет запущен в этом Intent. Это можно реализовать так, как мы сделали выше, явно задав точный класс, который будет его получать, либо с помощью вызова Intent.setComponent().

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

  • Если вы попытаетесь переопределить значения в PendingIntent, который был создан с FLAG_IMMUTABLE, произойдет тихий сбой, и исходный обернутый Intent будет передан без изменений.

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

Подробности о флагах

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

FLAG_IMMUTABLE: Указывает, что Intent внутри PendingIntent не может быть изменен другими приложениями, которые передают Intent в PendingIntent.send(). Приложение всегда может использовать FLAG_UPDATE_CURRENT для изменения своих собственных PendingIntent.

До Android 12 PendingIntent, созданный без этого флага, по умолчанию был изменяемым.

В версиях Android до Android 6 (API 23) PendingIntents всегда изменяемы.

FLAG_MUTABLE: Указывает, что Intent внутри PendingIntent должен позволять приложению обновлять его содержимое путем объединения значений из параметра намерения PendingIntent.send().

Всегда заполняйте ComponentName обернутого Intent любого изменяемого PendingIntent. Невыполнение этого требования может привести к уязвимостям в системе безопасности!

Этот флаг был добавлен в Android 12. До Android 12 любые PendingIntents, созданные без флага FLAG_IMMUTABLE, были неявно изменяемыми.

FLAG_UPDATE_CURRENT: Запрашивает, чтобы система обновила существующий PendingIntent новыми дополнительными данными, а не создавала новый PendingIntent. Если PendingIntent не был зарегистрирован, то регистрируется этот.

FLAG_ONE_SHOT: Позволяет отправить PendingIntent только один раз (через PendingIntent.send()). Это может быть важно при передаче PendingIntent другому приложению, если содержащийся в нем Intent может быть отправлен только один раз. Такое требование обусловлено удобством или необходимостью предотвратить многократное выполнение приложением какого-либо действия.

Использование FLAG_ONE_SHOT предотвращает такие проблемы, как "атаки повторного воспроизведения (replay attacks)".

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

Получение PendingIntents

Иногда система или другие фреймворки предоставляют PendingIntent как ответ на вызов API. Одним из примеров является метод MediaStore.createWriteRequest(), который был добавлен в Android 11.

static fun MediaStore.createWriteRequest(    resolver: ContentResolver,     uris: MutableCollection<Uri>): PendingIntent

Резюме

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

Мы также говорили о том, что PendingIntents обычно должен быть неизменяемым и что это не мешает приложению обновлять свои собственные объекты PendingIntent. Это можно сделать, используя флаг FLAG_UPDATE_CURRENT в дополнение к FLAG_IMMUTABLE.

Мы также говорили о мерах предосторожности, которые необходимо предпринять - заполнить ComponentName обернутого Intent - если PendingIntent должен быть изменяемым.

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

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

Хотите еще больше? Мы призываем вас протестировать свои приложения на новой предварительной версии ОС для разработчиков и поделиться с нами своими впечатлениями!


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

Подробнее..

Основы Flutter для начинающих (Часть IX)

11.06.2021 16:11:37 | Автор: admin

Flutter позволяет вам писать простые и понятные тесты для разных частей приложения.

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

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

Ну что ж, приступаем к тестированию!

Наш план
  • Часть 1- введение в разработку, первое приложение, понятие состояния;

  • Часть 2- файл pubspec.yaml и использование flutter в командной строке;

  • Часть 3- BottomNavigationBar и Navigator;

  • Часть 4- MVC. Мы будем использовать именно этот паттерн, как один из самых простых;

  • Часть 5- http пакет. Создание Repository класса, первые запросы, вывод списка постов;

  • Часть 6- работа с формами, текстовые поля и создание поста.

  • Часть 7- работа с картинками, вывод картинок в виде сетки, получение картинок из сети, добавление своих в приложение;

  • Часть 8 - создание своей темы, добавление кастомных шрифтов и анимации;

  • Часть 9 (текущая статья) - немного о тестировании;

Добавления необходимых зависимостей

Нам понадобиться два дополнительных пакета mockito и build_runner, поэтому добавим их:

# зависимости для разработки# в данном случае подключено тестированиеdev_dependencies:  flutter_test:    sdk: flutter  mockito: ^5.0.10  build_runner: ^2.0.4

Теперь мы можем приступать к тестированию

Пишем первый тест

В качестве объекта тестирования будет небольшой класс Stack:

class Stack<T> {  final stack = <T>[];    void push(T t) {    stack.add(t);  }    T? pop() {    if (isEmpty) {      return null;    }    return stack.removeLast();  }    bool get isEmpty => stack.isEmpty; }

Обратите внимание: класс Stack является обобщенным.

В корневой директории нашего проекта есть папка test, которая предназначена для тестов.

Создадим в ней новый файл stack_test.dart:

import 'package:flutter_test/flutter_test.dart';import 'package:json_placeholder_app/helpers/stack.dart';void main() {  // группа тестов  group("Stack", () {    // первый тест на пустой стек    test("Stack should be empty", () {      // expect принимает текущее значение       // и сравнивает его с правильным      // если значения не совпадают, тест не пройден      expect(Stack().isEmpty, true);    });    test("Stack shouldn't be empty", () {      final stack = Stack<int>();      stack.push(5);      expect(stack.isEmpty, false);    });    test("Stack should be popped", () {      final stack = Stack<int>();      stack.push(5);      expect(stack.pop(), 5);    });    test("Stack should be work correctly", () {      final stack = Stack<int>();      stack.push(1);      stack.push(2);      stack.push(5);      expect(stack.pop(), 5);      expect(stack.pop(), 2);      expect(stack.isEmpty, false);    });  });}

Довольно просто! Не правда ли?

На самом деле, это один из типов тестирования, который называется unit (модульное).

Также Flutter поддерживает:

  • Widget тестирование

  • Интеграционное тестирование

В данной статье мы рассмотрим только unit тестирование.

Давайте выполним наши тесты командой flutter test test/stack_test.dart:

Успешно!

Тестируем получение постов

Сначала видоизменим метод fetchPosts:

Future<PostList> fetchPosts({http.Client? client}) async {  // сначала создаем URL, по которому  // мы будем делать запрос  final url = Uri.parse("$SERVER/posts");  // делаем GET запрос  final response =  (client == null) ? await http.get(url) : await client.get(url);  // проверяем статус ответа  if (response.statusCode == 200) {    // если все ок то возвращаем посты    // json.decode парсит ответ    return PostList.fromJson(json.decode(response.body));  } else {    // в противном случае вызываем исключение    throw Exception("failed request");  }}

Теперь переходим к написанию самого теста.

Мы будем использовать mockito для создания фейкового http.Client'а

Создадим файл post_test.dart в папке tests:

import 'package:flutter_test/flutter_test.dart';import 'package:http/http.dart' as http;import 'package:json_placeholder_app/data/repository.dart';import 'package:json_placeholder_app/models/post.dart';import 'package:mockito/annotations.dart';import 'package:mockito/mockito.dart';// данный файл будет сгенерированimport 'post_test.mocks.dart';// аннотация mockito@GenerateMocks([http.Client])void main() {  // создаем наш репозиторий  final repo = Repository();  group("fetchPosts", () {      test('returns posts if the http call completes successfully', () async {        // создаем фейковый клиент        final client = MockClient();        // ответ на запрос        when(client.get(Uri.parse('https://jsonplaceholder.typicode.com/posts')))            .thenAnswer((_) async => http.Response('[{"userId": 1, "id": 2, "title": "Title", "content": "Content"}]', 200));        // проверяем корректность работы fetchPosts        // при удачном выполнении        final postList = await repo.fetchPosts(client: client);        expect(postList, isA<PostList>());        expect(postList.posts.length, 1);        expect(postList.posts.first.title, "Title");      });      test('throws an exception if the http call completes with an error', () {        final client = MockClient();        // генерация ошибки        when(client.get(Uri.parse('https://jsonplaceholder.typicode.com/posts')))            .thenAnswer((_) async => http.Response('Not Found', 404));        // проверка на исключение        expect(repo.fetchPosts(client: client), throwsException);      });  });}

Перед запуском теста необходимо сгенерировать post_test.mocks.dart файл:

flutter pub run build_runner build

После этого выполняем наши тесты командой flutter test test/post_test.dart:

Вуаля!

Заключение

Мы разобрали один из самых простых и известных типов тестирования - unit (модульное).

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

Полезные ссылки:

Всем хорошего кода!

Подробнее..

Перевод Как использовать Android Data Binding в пользовательских представлениях?

16.06.2021 20:20:54 | Автор: admin

. . .

Как вы знаете, Data Binding Library - это отличная часть библиотеки Android Jetpack, позволяющая сократить количество шаблонного кода и связать представления с данными более эффективным способом, чем это было возможно ранее. В этой статье я собираюсь объяснить, как можно использовать привязку данных в наших пользовательских представлениях.

. . .

Начало работы

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

class MyCustomView @JvmOverloads constructor(    context: Context,    attrs: AttributeSet? = null,    defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) {    init {        attrs?.let {            val typedArray =                context.obtainStyledAttributes(it, R.styleable.MyCustomView)            // some attr handling stuffs...            typedArray.recycle()        }    }}

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

Автоматический выбор метода

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

<?xml version="1.0" encoding="utf-8"?><resources>    <declare-styleable name="MyCustomView">        <attr name="currencyCode" format="string" />    </declare-styleable></resources>

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

Методы привязки

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

class MyCustomView @JvmOverloads constructor(    context: Context,    attrs: AttributeSet? = null,    defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) {    private val currencyFormatter = NumberFormat.getCurrencyInstance(Locale.getDefault())        //..    fun setCurrency(currencyCode: String?) {        if (currencyCode.isNullOrEmpty())            return        currencyFormatter.currency = Currency.getInstance(currencyCode)    }    //..}

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

@BindingMethods(    value = [        BindingMethod(            type = MyCustomView::class,            attribute = "currencyCode",            method = "setCurrency"        )    ])class BindingMethods

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

Адаптеры привязки

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

@BindingAdapter(    value = ["paddingEnd", "paddingTop", "paddingStart", "paddingBottom"],    requireAll = false)fun MyCustomView.setPaddingRelative(    paddingEnd: Int = 0,    paddingTop: Int = 0,    paddingStart: Int = 0,    paddingBottom: Int = 0) {    this.setPaddingRelative(paddingStart, paddingTop, paddingEnd, paddingBottom)}

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

. . .

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

Если у вас есть какие-либо комментарии, не стесняйтесь связаться со мной в Twitter.


Перевод материала подготовлен в рамках запуска курса "Android Developer. Professional".

Всех желающих приглашаем на двухдневный интенсив по теме: "Полный coverage. Покрываем Android приложение юнит/интеграционными/UI тестами"


Подробнее..

Перевод Android 12 лет истории дизайна ОС

20.06.2021 12:09:43 | Автор: admin
Android установлен примерно на 2,5 миллиардах активных устройств. С чего он начинался? Давайте проверим и разберёмся. Мы протестируем все версии Android, с 1.0 по 9.0, и посмотрим, как менялась система.

image

ОС Android имеет довольно долгую историю: о выпуске самого первого Android-телефона HTC Dream объявили в сентябре 2008 года. Найти этот телефон может оказаться сложно, но это нам и не нужно компания Google создала для разработчиков эмулятор каждой из версий Android. SDK для версии 1.0 можно скачать со страницы https://developer.android.com/sdk/older_releases.html, и это единственная версия, не требующая установки. Достаточно просто запустить файл tools\emulator.exe. При первом запуске мы получаем ошибку:


Создание отсутствующей папки AppData\Local\Android\SDK-1.0 позволило решить проблему, после чего мы смогли запустить эмулятор:


Эмулятор Android 1.0

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


Непривычны два аспекта. Во-первых, на телефоне есть около десяти аппаратных кнопок (в том числе курсорных клавиш). Например, кнопка Menu обеспечивает доступ к некоторым функциям:


В целом, все операции можно выполнить, не касаясь экрана, при помощи только аппаратных кнопок.

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


Телефон HTC Dream

Android 1.0 работал на телефоне с 192 МБ ОЗУ, процессором на 528 МГц, аккумулятором на 1150 мАч и экраном с разрешением 320x480.

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

Вызовы и SMS


Очевидно, что я не мог совершить телефонный звонок или отправить SMS через эмулятор, но, по крайней мере, мы видим UI:


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

Контакты



Карты


Google Maps работают на удивление хорошо. Поиск выполняется, режим отображения карты можно менять, и так далее.


Удивительно, что Google Maps API не изменился за более чем 12 лет.

Интернет


Google Maps работают хорошо, но ситуация сильно ухудшается, если протестировать Интернет-браузер. Поиск Google работает:


Но все остальные сервисы недоступны например www.youtube.com показывает, что требуется версия не ниже Android 4.0.


Я попробовал открыть Medium.com, первая страница Get started работала (более-менее), но после нажатия на Get Started отобразилась ошибка:


На самом деле, веб-сайт www.google.com оказался единственным, который я смог открыть. Это неудивительно, ведь Android 1.0 был выпущен больше десяти лет назад, а веб-стандарты сильно изменились.

Android 4.0 (2011 год)


Было бы слишком скучно тестировать все версии Android, поэтому давайте перенесёмся на несколько лет вперёд, к Android 4.0. Типичным телефоном того времени был LG Optimum L5 или HTC Desire C: 4-дюймовый экран с разрешением 320x480, процессор на 600 МГц и аккумулятор на 1230 мАч.


HTC Desire C

Для тестирования этой версии нам понадобится AVD (Android Virtual Device), который является частью Android Studio. Эта версия предназначена для разработчиков, но для запуска эмулятора нам не нужно писать код. Компонент AVD Manager позволяет выбирать разные версии и устройства:



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


Как мы видим, UI и аппаратная раскладка изменились: больше нет отдельной кнопки Menu и клавиш курсора, только три аппаратные кнопки (Home, Back и Apps List), их можно увидеть и в современном Android.

Добавлена новая функция UI Widgets:


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

Settings по сравнению с современными версиями не сильно изменились, однако UI и шрифты, разумеется, другие:


Contacts теперь можно сохранять локально или синхронизировать с аккаунтом Google. Contacts и Dialer (набор номера) теперь стали двумя отдельными приложениями.



Отправка SMS не особо изменилась:


Web Browser работает, но большинство страниц (google play, youtube, даже Wikipedia) не открывается:


Medium.com по-прежнему открыть нельзя, но, по крайней мере, первая страница выглядит лучше, чем на Android 1.0:


Мне удалось открыть страницу MSN (с предупреждениями), страница BBC открылась без ошибок, но UI выглядел странно, а сайт NY Times вообще не открылся:


В картах добавлена новая функция: Google Maps Navigation:


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

Android 6.0 (2015 год)


Четыре года долгий срок для мира технологий, и характеристики смартфонов значительно улучшились. Хорошим примером устройства с Android 6 может служить Samsung Galaxy S6: 5,1 дюймовый AMOLED-экран с разрешением 1440x2560, восьмиядерным процессором и аккумулятором на 2550 мАч:


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

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


Contacts и Dialer по-прежнему остаются двумя отдельными приложениями (и двумя значками на экране), но разница между ними не так очевидна:



Интерфейс Settings тоже не особо изменился:


Web browser работает гораздо лучше, даже видео воспроизводится корректно, однако medium.com снова не прошёл тест отображается только белая страница:



На самом деле, www.medium.com это единственный сайт, который мне не удалось открыть.

Теперь в Android добавлены Gmail и Google Photos:


Google Maps работают хорошо, но, на удивление, спустя пять лет навигация по-прежнему находится в бета-версии.


В целом, интерфейс Android 6.0 выглядит достаточно современно даже по нынешним меркам, а разница между 4.0 и 6.0 гораздо очевиднее, чем между Android 6.0 и 10.

Android 8.0 (2017 год)


Я не собирался тестировать Android 8.0, с точки зрения UI отличий было бы не так много. Но мне стало любопытно, в какой версии Android корректно откроется medium.com. Давайте проверим.

Первое забавное отличие список приложений снова можно перетаскивать снизу вверх, точно так же, как в Android 1.0 (для сравнения см. изображение в начале статьи):


Как мы видим, как отдельные приложения были добавлены Youtube, Google Drive и Google Play Music.

Давайте снова протестируем браузер на medium.com. В целом, всё стало намного лучше мне удалось добраться до первого этапа логина:


Но на этом этапе страница зависает, и постоянно появляется всплывающее окно Sign in.

Android 9.0 (2018 год)


Очевидно, в каждой новой версии Android происходило множество скрытых изменений в безопасности, API и фоновых сервисах, но с точки зрения UI эта версия не сильно изменилась по сравнению с Android 6.0 2015 года. Как мы видим, добавилась левая панель Google. Приложения можно разделить на секции популярные и все приложения:


Напоследок давайте снова проверим страницу medium.com. Вуаля, теперь она работает:


Программирование


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


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


На следующем этапе нужно указать имя приложения, его уникальный идентификатор, язык программирования (Java или Kotlin) и минимальный уровень версии Android.


После нажатия на Finish будут сгенерированы исходный код и ресурсы приложения.


Теперь мы можем запустить своё приложение в эмуляторе или в реальном устройстве:


Очевидно, что это приложение не делает ничего полезного, если вас интересуют последующие шаги, то изучите туториалы на веб-сайте https://developer.android.com.

Заключение


Исследование истории Android оказалось любопытным занятием. Как обычно, я призываю заинтересовавшихся читателей установить эмулятор и самостоятельно увидеть все различия. Один из способов это Android Studio, но она выполняет образ x86 и не может запускать сторонние приложения для Android. Ещё один удобный эмулятор это Genymotion, он основан на VirtualBox и обеспечивает полную эмуляцию ARM. Кроме того, он бесплатен для личного пользования. Я пользовался Genymotion несколько лет назад, но последняя версия по неизвестным причинам не работает. Возможно, кому-то из читателей повезёт. Однако существует множество других способов запуска Android на PC, так что можете выбрать подходящий для вас.

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

Дизайн UI



Совместимость веб-страниц






На правах рекламы


Воплощайте любые идеи и проекты с помощью наших серверов с мгновенной активацией на Linux или Windows, на наших серверах можно установить даже Android!

Подписывайтесь на наш чат в Telegram.

Подробнее..

Категории

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

© 2006-2021, personeltest.ru