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

Compose

Перевод Compose повсюду композиция функций в JavaScript

25.09.2020 18:15:34 | Автор: admin
Перевод статьи подготовлен специально для студентов курса JavaScript Developer.Basic.





Введение


Кажется, библиотеки Lodash и Underscore теперь используются повсюду, и в них до сих пор есть известный нам суперэффективный метод compose.

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

Основы


Мы рассмотрим много функций Lodash, потому что 1) мы не собираемся писать собственные базовые алгоритмы это отвлечет нас от того, на чем я предлагаю сконцентрироваться; и 2) библиотека Lodash используется многими разработчиками, и ее можно без проблем заменить на Underscore, любую другую библиотеку или ваши собственные алгоритмы.

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

var compose = function(f, g) {    return function(x) {        return f(g(x));    };};


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

А теперь рассмотрим этот код:

function reverseAndUpper(str) {  var reversed = reverse(str);  return upperCase(reversed);}


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

var reverseAndUpper = compose(upperCase, reverse);


Теперь можно использовать функцию reverseAndUpper:

reverseAndUpper('тест'); // ТСЕТ


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

function reverseAndUpper(str) {  return upperCase(reverse(str));}


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

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

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

var compose = function() {  var funcs = Array.prototype.slice.call(аргументы);   return funcs.reduce(function(f,g) {    return function() {      return f(g.apply(this, аргументы));    };  });};


С такой функцией мы можем написать примерно такой код:

Var doSometing = compose(upperCase, reverse, doSomethingInitial); doSomething('foo', 'bar');


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

Примеры


Начнем с простого:

function notEmpty(str) {    return ! _.isEmpty(str);}


Функция notEmpty это отрицание значения, возвращаемого функцией _.isEmpty.

Мы можем добиться такого же результата с использованием функции _.compose из библиотеки Lodash. Напишем функцию not:

function not(x) { return !x; } var notEmpty = _.compose(not, _.isEmpty);


Теперь можно использовать функцию notEmpty с любым аргументом:

notEmpty('foo'); // truenotEmpty(''); // falsenotEmpty(); // falsenotEmpty(null); // false


Это очень простой пример. Давайте рассмотрим что-нибудь посложнее:
функция findMaxForCollection возвращает максимальное значение из коллекции объектов со свойствами id и val (значение).

function findMaxForCollection(data) {    var items = _.pluck(data, 'val');    return Math.max.apply(null, items);} var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}]; findMaxForCollection(data);


Для решения этой задачи можно использовать функцию compose:

var findMaxForCollection = _.compose(function(xs) { return Math.max.apply(null, xs); }, _.pluck); var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}]; findMaxForCollection(data, 'val'); // 6


Здесь есть над чем поработать.

_.pluck ожидает коллекцию в качестве первого аргумента, и функцию обратного вызова в качестве второго. А можно применить функцию _.pluck частично? В этом случае можно использовать каррирование, чтобы изменить порядок следования аргументов.

function pluck(key) {    return function(collection) {        return _.pluck(collection, key);    }}


Функцию findMaxForCollection нужно еще немного подкрутить. Давайте создадим собственную функцию max.

function max(xs) {    return Math.max.apply(null, xs);}


Теперь можно сделать функцию compose более элегантной:

var findMaxForCollection = _.compose(max, pluck('val')); findMaxForCollection(data);


Мы написали собственную функцию pluck и можем использовать ее только со свойством 'val'. Возможно, вам непонятно, зачем писать собственный метод выборки, если в Lodash уже есть готовая и удобная функция _.pluck. Проблема в том, что _.pluck ожидает коллекцию в качестве первого аргумента, а мы хотим сделать по-другому. Изменив порядок следования аргументов, мы можем применить функцию частично, передав ключ в качестве первого аргумента; возвращаемая функция будет принимать данные (data).
Можно еще немного подшлифовать наш метод выборки. В Lodash есть удобный метод _.curry, который позволяет записать нашу функцию так:

function plucked(key, collection) {    return _.pluck(collection, key);} var pluck = _.curry(plucked);


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

function max(xs) {    return Math.max.apply(null, xs);} function plucked(key, collection) {    return _.pluck(collection, key);} var pluck = _.curry(plucked); var findMaxForCollection = _.compose(max, pluck('val')); var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}]; findMaxForCollection(data); // 6


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

var findMaxForCollection = _.compose(max, pluck('val'));


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

var data = [{id: 1, val: 5, active: true},             {id: 2, val: 6, active: false },             {id: 3, val: 2, active: true }];


Назовем эту функцию getMaxIdForActiveItems(data). Она принимает коллекцию объектов, отфильтровывает все активные объекты и возвращает максимальное значение из отфильтрованных.

function getMaxIdForActiveItems(data) {    var filtered = _.filter(data, function(item) {        return item.active === true;    });     var items = _.pluck(filtered, 'val');    return Math.max.apply(null, items);}


А можно сделать этот код поэлегантнее? В нем уже есть функции max и pluck, поэтому нам остается лишь добавить фильтр:

var getMaxIdForActiveItems = _.compose(max, pluck('val'), _.filter); getMaxIdForActiveItems(data, function(item) {return item.active === true; }); // 5


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

function filter(fn) {    return function(arr) {        return arr.filter(fn);    };}


Добавим функцию isActive, которая принимает объект и проверяет, присвоено ли флагу active значение true.

function isActive(item) {    return item.active === true;}


Функцию filter с функцией isActive можно применить частично, поэтому в функцию getMaxIdForActiveItems мы будем передавать только данные.

var getMaxIdForActiveItems = _.compose(max, pluck('val'), filter(isActive));


Теперь нам нужно лишь передать данные:

getMaxIdForActiveItems(data); // 5


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

var isNotActive = _.compose(not, isActive); var getMaxIdForNonActiveItems = _.compose(max, pluck('val'), filter(isNotActive));


Заключение


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

Ссылки


lodash
Hey Underscore, You're Doing It Wrong! (Эй, Underscore, ты все делаешь не так!)
@sharifsbeat



Читать ещё:


Подробнее..

Трансформация Android-разработки с Jetpack Compose и Корутинами

06.08.2020 10:11:52 | Автор: admin

Jetpack Compose одна из наиболее обсуждаемых тем из серии видео про Android 11, заменивших собой Google IO. Многие ожидают от библиотеки, что она решит проблемы текущего UI-фреймворка Android, содержащего много легаси-кода и неоднозначных архитектурных решений. Другим не менее популярным фреймворком, о применении которого я расскажу в этой статье является Kotlin Coroutines, а конкретнее входящий в него Flow API, который может помочь избежать оверинжиниринга при использовании RxJava.
Применение этих инструментов я покажу на примере небольшого приложения для контроля за употреблением кофе, написанного с использованием Jetpack Compose для UI и StateFlow как инструмента для управления состоянием. В нем также используется MVI-архитектура.



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


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


Jetpack Compose


При первом взгляде на Jetpack Compose после продолжительной работы с версткой в XML вы, вероятно, почувствуете дискомфорт из-за смешения UI-кода с другим в файлах на Kotlin и необходимости постоянного контроля состояний. До этого у меня был опыт работы с Flutter который построен на виджетах и их состояниях. Это помогло мне легче понять концепцию декларативного UI и сделать прототип приложения всего за шесть вечеров после работы. Тот же опыт я использую для сравнения этих двух UI-фреймворков.


Помочь с переходом может сайт, на котором можно найти компонент в Compose по его названию в традиционном UI.


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


Свой проект я начал с шаблона Compose-проекта в Android Studio. Ниже приведен код из MainActivity.kt:


class MainActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContent {            CoffeegramTheme {                Greeting("Android")            }        }    }}@Composablefun Greeting(name: String) {    Text(text = "Hello $name!")}@Preview(showBackground = true)@Composablefun DefaultPreview() {    CoffeegramTheme {        Greeting("Android")    }} 

Если вы уже знакомы с Compose, можете просмотреть эту часть бегло
Весь Compose построен на функциях, помеченных аннотацией @Composable. Она позволяет плагину компилятора Котлина сгенерировать необходимый для работы код.
Вместо обычной функции setContentView(), вызываемой внутри Activity.onCreate() для инфлейта лейаутов, нужно вызвать функцию setContent(), передав ей в качестве параметра Composable-функцию.


Недавно появилась аннотация @Preview для Composable-функций, с помощью которой в новых версиях Android Studio (я использовал 4.2 Canary) можно увидеть превью помеченного компонента. Однако для этого нужно будет пересобрать проект. Этот механизм немного напоминает Hot Reload из Flutter, но работает медленнее из-за необходимости пересборки и не предоставляет синхронного анализатора кода, подсвечивающего ошибки компиляции всего проекта. Так у вас не получится увидеть превью, если вы меняете UI в одном файле, а в других остались ошибки компиляции.
Другая проблема, с которой я столкнулся, возникла после удаления директории .idea из Git и с диска после коммита. Превью перестал работать совсем, из-за чего пришлось начать проект с шаблона заново. Надеюсь, что это поправят в следующих версиях студии.
Тем не менее будет полезным оставлять как минимум одну превью функцию в каждом файле, чтобы отслеживать изменения в текущем.
Можно также аннотировать одну Composable-функцию несколькими превью-аннотациями с разными параметрами, чтобы, например, отслеживать вид компонента сразу в светлой и темной теме, нескольких размерах или с тестовыми данными. Сейчас не буду останавливаться подробнее, но пример можно посмотреть тут.


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


image

data class CoffeeType(    @DrawableRes    val image: Int,    val name: String,    val count: Int = 0)@Composablefun CoffeeTypeItem(type: CoffeeType) {    Row(        modifier = Modifier.padding(16.dp)    ) {        Image(            imageResource(type.image), modifier = Modifier                .preferredHeightIn(maxHeight = 48.dp)                .preferredWidthIn(maxWidth = 48.dp)                .fillMaxWidth()                .clip(shape = RoundedCornerShape(24.dp))                .gravity(Alignment.CenterVertically),            contentScale = ContentScale.Crop        )        Spacer(Modifier.preferredWidth(16.dp))        Text(            type.name, style = typography.body1,            modifier = Modifier.gravity(Alignment.CenterVertically).weight(1f)        )        Row(modifier = Modifier.gravity(Alignment.CenterVertically)) {            val count = state { type.count }            Spacer(Modifier.preferredWidth(16.dp))            val textButtonModifier = Modifier.gravity(Alignment.CenterVertically)                .preferredSizeIn(                    maxWidth = 32.dp,                    maxHeight = 32.dp,                    minWidth = 0.dp,                    minHeight = 0.dp                )            TextButton(                onClick = { count.value-- },                padding = InnerPadding(0.dp),                modifier = textButtonModifier            ) {                Text("-")            }            Text(                "${count.value}", style = typography.body2,                modifier = Modifier.gravity(Alignment.CenterVertically)            )            TextButton(                onClick = { count.value++ },                padding = InnerPadding(0.dp),                modifier = textButtonModifier            ) {                Text("+")            }        }    }}

Элемент списка здесь создаётся с использованием аналога ListView с горизонтальной ориентацией виджета Row. Внутри него помещается иконка (загруженный в виджет Image png-файл из drawable); разделительный виджет Spacer; Text с названием напитка, занимающий всё свободное пространство из-за применения модификатора weight(1f) (принцип похож на веса в ListView); и вложенный Row с двумя кнопками и текстом для отображения количества чашек.


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


State


Виджет, показанный в коде выше, уже запускается в интерактивном режиме с возможностью изменить кнопками количество чашек. Это возможно из-за строки val count = state { type.count }, в которой функция state забирает исходное количество из модели type и оповещает окружающую ее Composable-функцию о каждом изменении этого состояния. Внутренние виджеты могут получить и изменить текущее значение через свойство count.value. Когда ему будет присвоено новое значение, поддерево виджетов, начиная с виджета, вызывающего функцию получения состояния, будет перерисовано (помимо state это может быть также collectAsState и прочие).


В отличие от Flutter, в Compose нет разделения на Stateful (с состоянием) и Stateless (без состояния) виджеты. Каждый виджет, содержащий вызов функции получения состояния может условно считаться Stateful, а остальные Stateless.


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


@Composablefun CoffeeList(coffeeTypes: List<CoffeeType>) {    Column {        coffeeTypes.forEach { type ->            CoffeeTypeItem(type)        }    }}@Composablefun ScrollableCoffeeList(coffeeTypes: List<CoffeeType>) {    VerticalScroller(modifier = Modifier.weight(1f)) {        CoffeeList(coffeeTypes: List<CoffeeType>)    }}

Composable функции могут быть вложены внутрь условных операторов и циклов, таких как if, for, when и т.п. Виджет Column представляет собой аналог ListView с вертикальной ориентацией, а VerticalScroller аналог ScrollView.
Проблема в этом коде должна быть очевидной. Список будет не оптимизированным и лагать во время скроллинга. Есть ли в Compose RecyclerView? Да представленный виджетом LazyColumnItems (еще недавно он назывался AdapterList). С ним реализация списка CoffeeList будет выглядеть следующим образом:


@Composablefun CoffeeList( coffeeTypes: List<CoffeeType>, modifier: Modifier = Modifier) {    LazyColumnItems(data = coffeeTypes, modifier = modifier.fillMaxHeight()) { type ->        CoffeeTypeItem(type)    }}

На данный момент нет аналога для RecyclerView с GridLayoutManager (для создания сетки вместо линейного списка). Но для приложения уже сделан один из двух экранов.


image

Перед тем как сделать следующий, нужно подумать о навигации.


Навигационные элементы из Material design реализованы очень похоже во Flutter и Compose. Корневым элементом является виджет Scaffold, обернутый темой. Он может содержать TopAppBar (верхнюю панель меню), BottomAppBar (нижнюю панель меню, с возможностью интегрировать кнопку Floating action button) или Drawer (левое боковое меню). Чтобы реализовать BottomNavigationView из Material я поместил в Scaffold виджет Column с BottomNavigation внутри:


@Composablefun DefaultPreview() {    CoffeegramTheme {        Scaffold() {                Column() {                    var selectedItem by state { 0 }                    when (selectedItem) {                        0 -> {                            Column(modifier = Modifier.weight(1f)){}                        }                        1 -> {                            CoffeeList(listOf(...))                        }                    }                    val items =                        listOf(                            "Calendar" to Icons.Filled.DateRange,                             "Info" to Icons.Filled.Info                        )                    BottomNavigation {                        items.forEachIndexed { index, item ->                            BottomNavigationItem(                                icon = { Icon(item.second) },                                text = { Text(item.first) },                                selected = selectedItem == index,                                onSelected = { selectedItem = index }                            )                        }                    }                }            }    }}

Состояние текущей выбранной вкладки содержится в selectedItem. Его состояние с помощью оператора when формирует контент для вкладки. Изменение вкладки происходит по клику на BottomNavigation с последующим изменением значения selectedItem. Такой способ построения навигации позволяет отказаться от использования фрагментов и активити кроме корневой для дерева Compose.


Реализацию второго экрана с таблицей и покоммитное создание кода приложения можно посмотреть в репозитории. Полезной деталью, которую я встретил тут, стал способ доступа к контексту для получения, например, ресурсов или текущей локали. Для этого нужно вызвать ContextAmbient.current.context внутри любой Composable-функции. Сам же экран с месячной таблицей выглядит так:


image

Во время разработки я заменил используемые png-иконки для типов кофе на векторные. Для этого функцию imageResource внутри виджета Image надо заменить на vectorResource. Можно также попробовать использовать виджет Icon для этой цели (как сделал изначально я), но тогда иконки будут монохромными.


StateFlow


Теперь перейдем ко второй части названия статьи. Flow является аналогом реактивных стримов в составе Корутин. Его можно рассматривать как холодную последовательность данных они начинают поступать только после подписки (вызова терминальной функции). Для передачи состояния между разными компонентами приложения в реактивном стиле нужен аналог BehaviorSubject из RxJava. Таким аналогом является StateFlow. Как и BehaviorSubject, он может иметь несколько подписчиков и должен быть проинициализирован исходным значением.


Для иллюстрации его использования в примере, показанном выше, состояние selectedItem может быть заменено с помощью selectedItemFlow:


val selectedItemFlow = MutableStateFlow(0)@Composablefun DefaultPreview() {    ...    val selectedItem by selectedItemFlow.collectAsState()    when (selectedItem) {        0 -> TablePage()        1 -> CoffeeListPage()    }    ...    BottomNavigationItem(        selected = selectedItem == index,        onSelected = { selectedItemFlow.value = index }    )}

Состояние получается из StateFlow (или другого Flow) с помощью вызова функции collectAsState(). Оно используется для определения необходимости перерисовки, а также получения текущего значения.


Чтобы изменить состояние, нужно присвоить значение свойству selectedItemFlow.value.


Так как текущее значение может быть также получено через это свойство, важно не забыть вызвать collectAsState() внутри виджета. Иначе он не будет обновляться вместе с состоянием. Возможным паттерном тут может быть использование состояния (val selectedItem by selectedItemFlow.collectAsState()) для чтения значения, а свойства MutableStateFlow (selectedItemFlow.value) для изменения.


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


val yearMonthFlow = MutableStateFlow(YearMonth.now())val dateFlow = MutableStateFlow(-1)val daysCoffeesFlow: DaysCoffeesFlow = MutableStateFlow(mapOf())

yearMonthFlow отвечает за текущий отображаемый месяц.
dateFlow за выбранный день в таблице и навигацию между экранами: если текущее значение равно -1 отображается экран с таблицей TablePage. В другом случае это будет экран со списком CoffeeListPage для конкретного дня месяца.
daysCoffeesFlow прототип репозитория, содержащего все записанные чашки кофе. Его внутренняя структура стала решением следующей проблемы.


Когда пользователь переходит из TablePage в CoffeeListPage, состояние этого экрана должно быть частью (за конкретный день) из общего состояния, представленного в daysCoffeesFlow. Состояние элемента списка CoffeeList внутри должно быть также частью состояния целого списка. Когда изменяется количество чашек внутри, сам элемент не может знать, как изменить общее состояние daysCoffeesFlow. Мы помогаем ему, создавая набор мапперов из более общих Flow в более конкретные и наоборот.


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


Эти мапперы привели к трудночитаемому коду внутри UI-функций. Поэтому я решил применить MVI-архитектуру. Существующие решения, такие как MVICore, показались слишком завязанными на RxJava или другие асинхронные фреймворки и слишком сложными для текущей задачи. Моё решение базируется на статье Android MVI with Kotlin Coroutines & Flow article. Базовые концепции MVI с диаграммами можно найти там же. Здесь я покажу код базового класса Store:


abstract class Store<Intent : Any, State : Any>(private val initialState: State) {    protected val _intentChannel: Channel<Intent> = Channel(Channel.UNLIMITED)    protected val _state = MutableStateFlow(initialState)    val state: StateFlow<State>        get() = _state    fun newIntent(intent: Intent) {        _intentChannel.offer(intent)    }    init {        GlobalScope.launch {            handleIntents()        }    }    private suspend fun handleIntents() {        _intentChannel.consumeAsFlow().collect { _state.value = handleIntent(it) }    }    protected abstract fun handleIntent(intent: Intent): State}

Store работает на входящих Intent-ах и предоставляет StateFlow<State> подписчикам. Он содержит несколько вспомогательных функций, которые позволяют наследникам реализовать только похожую на Reducer функцию handleIntent() и иерархию собственных интентов и состояний. Пользователи наследников Store могут получить состояние через свойство state, возвращающее StateFlow; или передать новый интент с помощью функции newIntent().


Ниже приведен пример такого наследника NavigationStore, реализующего снова логику навигации:


class NavigationStore : Store<NavigationIntent, NavigationState>(        initialState = NavigationState.TablePage(YearMonth.now())    ) {    override fun handleIntent(intent: NavigationIntent): NavigationState {        return when (intent) {            NavigationIntent.NextMonth -> {                increaseMonth(_state.value.yearMonth)            }            NavigationIntent.PreviousMonth -> {                decreaseMonth(_state.value.yearMonth)            }            is NavigationIntent.OpenCoffeeListPage -> {                NavigationState.CoffeeListPage(                    LocalDate.of(                        _state.value.yearMonth.year,                        _state.value.yearMonth.month,                        intent.dayOfMonth                    )                )            }            NavigationIntent.ReturnToTablePage -> {                NavigationState.TablePage(_state.value.yearMonth)            }        }    }    private fun increaseMonth(yearMonth: YearMonth): NavigationState {        return NavigationState.TablePage(yearMonth.plusMonths(1))    }    private fun decreaseMonth(yearMonth: YearMonth): NavigationState {        return NavigationState.TablePage(yearMonth.minusMonths(1))    }}sealed class NavigationIntent {    object NextMonth : NavigationIntent()    object PreviousMonth : NavigationIntent()    data class OpenCoffeeListPage(val dayOfMonth: Int) : NavigationIntent()    object ReturnToTablePage : NavigationIntent()}sealed class NavigationState(val yearMonth: YearMonth) {    class TablePage(yearMonth: YearMonth) : NavigationState(yearMonth)    data class CoffeeListPage(val date: LocalDate) : NavigationState(        YearMonth.of(date.year, date.month)    )}

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


Параметр initialState в NavigationStore представляет собой состояние-экран, которое увидит пользователь, только открыв приложение.


Функция handleIntent() содержит бизнес-логику превращения интентов в состояния.
Второй DaysCoffeesStore, отвечающий непосредственно за кофе, как и весь код приложения, можно найти в репозитории.


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


С популяризацией декларативных UI-фреймворков, таких как Compose, Flutter и SwiftUI, мобильная разработка становится всё больше похожей на Web. Это может привести, как к унификации используемых архитектур, так и к увеличению переиспользования кода между большинством клиентских платформ.

Подробнее..

Compose. Jetpack Compose

09.10.2020 12:10:51 | Автор: admin
image

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

Пожалуй, главным трендом мобильной разработки за последние несколько лет стал декларативный UI. Такое решение уже давно успешно применяется в веб и кроссплатформенных решениях и, наконец, добралось и до нативной разработки. На iOS существует SwiftUI (представленный на WWDC 2019), а на Android Jetpack Compose (представленный месяцем ранее на Google I/O 2019). И именно о последнем мы сегодня и поговорим.

Примечание: в данной статье мы не будем рассматривать поэтапное создание первого проекта на Compose, так как этот процесс прекрасно описан в других материалах. Моя цель лишь рассказать о преимуществах и недостатках, которые дает android-разработчикам переход на Jetpack Compose, а решение использовать или нет всегда остаётся за вами.

Появление


Официальная история Jetpack Compose начинается с мая 2019, когда он был представлен публике на конференции Google I/O. Простой, реактивный и Kotlin-only новый декларативный фреймворк от Google выглядел как младший брат Flutter (который к тому моменту уже стремительно набирал популярность).

API design is building future regret

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

Преимущества


Итак, чем же хорош Jetpack Compose и, главное, чем он кардинально отличается от существующего на данный момент UI-фреймворка Android?

  • Unbundled toolkit: JC не зависит от конкретных релизов платформы, а значит, забудем уже про Support Library.
  • Kotlin-only: Больше не нужно переключаться между классами и xml-файлами вся работа с UI происходит в одном Kotlin-файле.
  • Композитный подход: Наследованию нет, композиции да. Каждый UI-компонент представляет собой обычную composable-функцию, отвечающую только за ограниченный функционал, т.е. без лишней логики. Никаких больше View.java на 30 тысяч строк кода.
  • Unidirectional Data Flow: Одна из основополагающих концепций Jetpack Compose, о которой будет рассказано подробнее чуть ниже.
  • Обратная совместимость: Для использования Compose не требуется начинать проект с нуля. Имеется возможность как его встраивания (с помощью ComposeView) в имеющуюся xml-вёрстку, так и наоборот.
  • Меньше кода: Тут, как говорится, лучше один раз увидеть, чем сто раз услышать. В качестве примера возьмём классическое сочетание компонентов два поля ввода и кнопка подтверждения:

В реализации текущего UI-фреймворка вёрстка этих компонентов выглядит так:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="wrap_content"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    android:orientation="vertical"    android:padding="@dimen/padding_16dp">    <com.google.android.material.textfield.TextInputLayout        android:id="@+id/til_login"        android:layout_width="match_parent"        android:layout_height="wrap_content"        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"        android:hint="@string/sign_in_email"        android:layout_marginBottom="@dimen/margin_8dp">        <com.google.android.material.textfield.TextInputEditText            android:id="@+id/et_login"            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:inputType="text"/>    </com.google.android.material.textfield.TextInputLayout>    <com.google.android.material.textfield.TextInputLayout        android:id="@+id/til_password"        android:layout_width="match_parent"        android:layout_height="wrap_content"        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"        android:hint="@string/sign_in_password"        android:layout_marginVertical="@dimen/margin_8dp">        <com.google.android.material.textfield.TextInputEditText            android:id="@+id/et_password"            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:inputType="textPassword"/>    </com.google.android.material.textfield.TextInputLayout>    <Button        android:id="@+id/btn_confirm"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="@string/sign_in_submit"        android:layout_marginTop="@dimen/margin_8dp"        android:padding="@dimen/padding_8dp"        android:background="@color/purple_700"/></LinearLayout>

В то же время, при использовании Jetpack Compose, решение будет выглядеть следующим образом:

@Preview@Composablefun LoginPage(){    var loginValue by remember { mutableStateOf(TextFieldValue("")) }    var passwordValue by remember { mutableStateOf(TextFieldValue("")) }    Surface(color = Color.White) {        Column(modifier = Modifier.padding(16.dp).fillMaxWidth()) {            Surface(color = Color.White, modifier = Modifier.padding( vertical = dimensionResource(id = R.dimen.padding_8dp))) {                OutlinedTextField(                        value = loginValue,                        onValueChange = { loginValue = it },                        label = { Text(text = stringResource(id = R.string.sign_in_email)) },                        placeholder = { Text(text = stringResource(id = R.string.sign_in_email)) },                        modifier = Modifier.fillMaxWidth()                )            }            Surface(color = Color.White, modifier = Modifier.padding( vertical = dimensionResource(id = R.dimen.padding_8dp))) {                OutlinedTextField(                        value = passwordValue,                        onValueChange = { passwordValue = it },                        label = { Text(text = stringResource(id = R.string.sign_in_password)) },                        placeholder = { Text(text = stringResource(id = R.string.sign_in_password)) },                        visualTransformation = PasswordVisualTransformation(),                        modifier = Modifier.fillMaxWidth()                )            }            Button(                    onClick = {},                    modifier = Modifier.padding( vertical = dimensionResource(id = R.dimen.padding_8dp)).fillMaxWidth(),                    backgroundColor = colorResource(R.color.purple_700)) {                Text(text = stringResource(id = R.string.sign_in_submit), modifier = Modifier.padding(8.dp))            }        }    }}

Ну и напоследок сравнительный результат:

image

Недостатки


  • Alpha-версия: Безусловно, более чем за год разработки фреймворк значительно преобразился и стал гораздо стабильнее. Однако это всё ещё альфа, а поэтому за пределами Pet-проектов использовать его не рекомендуется.

Декларативный стиль


Отдельное внимание стоит уделить главной особенности Jetpack Compose декларативному стилю создания UI. Суть подхода заключается в описании интерфейса как совокупности composable-функций (они же виджеты), которые не используют под капотом view, а напрямую занимаются отрисовкой на canvas. Для кого-то это минус, для других возможность попробовать что-то новое. Так или иначе, к концепции верстать UI кодом нативному разработчику, не работавшему ранее с аналогичными технологиями (к примеру, Flutter или React Native), придётся привыкать.

Что за Unidirectional Data Flow?


В современном android-приложении UI-состояние меняется в зависимости от приходящих событий (нажатие на кнопку, переворот экрана и т.д.). Мы нажимаем на компонент, тем самым формируя событие, а компонент меняет свой state и вызывает callback в ответ. Из-за довольно тесной связи UI-состояния с View это потенциально может привести к усложнению поддержки и тестирования такого кода. К примеру, возможна ситуация, когда помимо внутреннего state компонента, мы можем хранить его состояние в поле (например во viewmodel), что теоретически может привести к бесконечному циклу обновления этого самого state.

Что же касается Jetpack Compose, то здесь все компоненты по умолчанию являются stateless. Благодаря принципу однонаправленности нам достаточно скормить модель данных, а любое изменение состояния фреймворк обработает за нас. Таким образом, логика компонента упрощается, а инкапсуляция состояния позволяет избежать ошибок, связанных с его частичным обновлением. В качестве примера возьмем уже рассмотренный ранее composable-код. Перед описание компонентов были определены две переменные:

    var loginValue by remember { mutableStateOf(TextFieldValue("")) }    var passwordValue by remember { mutableStateOf(TextFieldValue("")) }

Мы создаем два текстовых объекта, значения которых будем устанавливать полям ввода (логина и пароля) в качестве value. А благодаря связке remember { mutableStateOf() } любое изменение значений этих объектов (из других частей кода) уведомит об этом соответствующее поле ввода, которое перерисует только значение value, вместо полной рекомпозиции всего компонента.

Вывод


Какой же вывод можно сделать о Jetpack Compose? По моему мнению, у нового решения от Google имеется огромный потенциал. С момента анонса в 2019 году была проделана огромная работа, и не менее долгий путь до релиза у фреймворка ещё впереди. Однако теперь он публично доступен, и я считаю, что это прекрасная возможность познакомиться с ним поближе. Ну а за чем, по вашему мнению, будущее пишите в комментарии, будет интересно узнать ваше мнение. Любите android!
Подробнее..

Jetpack Compose Desktop

11.11.2020 20:10:19 | Автор: admin

Несколько дней назад компания JetBrains в своём блоге рассказала о выходе нового инструмента для создания дескопных приложений Jetpack Compose Desktop. Компания с русскими корнями не особо жалует русскоязычную аудиторию и не спешит рассказать нам о новинке, поэтому возьму на себя роль популяризатора.

Разработчики под Android уже могли слышать про Compose, которая доступна в предварительной версии Android Studio 4.2. Я не любитель устанавливать бета-версии, поэтому не мог пощупать новую технологию своими руками. Но когда услышал новость про Compose Desktop, то не удержался и поставил себе IntelliJ IDEA Early Access Program.

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

Создаём новый проект и выбираем шаблон Desktop uses Kotlin 1.4.0. Минимальная версия Java SDK должна быть не ниже 11. Проверял на Windows 10, других платформ у меня нет.

Шаблон в IDEA Шаблон в IDEA

Среда разработки сгенерирует первый проект и основной файл main.kt будет иметь следующий вид.

import androidx.compose.desktop.Windowimport androidx.compose.material.Textimport androidx.compose.material.Buttonimport androidx.compose.material.MaterialThemeimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValuefun main() = Window {    var text by remember { mutableStateOf("Hello, World!") }    MaterialTheme {        Button(onClick = {            text = "Hello, Desktop!"        }) {            Text(text)        }    }}

Запущенное приложение выглядит следующим образом.

Приложение Compose DesktopПриложение Compose Desktop

Довольно непривычно видеть имена пакетов androidx для десктоп-приложений.

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

import androidx.compose.desktop.Windowimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.material.Textimport androidx.compose.material.Buttonimport androidx.compose.material.MaterialThemeimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.unit.IntSizeimport androidx.compose.ui.unit.dpfun main() = Window(title = "Счётчик котов", size = IntSize(300, 250)) {    var count by remember { mutableStateOf(0) }    MaterialTheme {        Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) {            Button(modifier = Modifier.align(Alignment.CenterHorizontally), onClick = {                count++            }) {                Text(if (count == 0) "Hello Kitty" else "Я насчитал котов: $count!")            }            Button(modifier = Modifier.align(Alignment.CenterHorizontally),                onClick = {                    count = 0                }) {                Text("Сбросить")            }        }    }}

Теперь мы можем считать усатых-полосатых.

Пока сложно сказать, приживётся ли подобный способ создания приложений для Android и десктопа. Многие разработчики уже вовсю восторгаются и пишут хвалебные посты в интернете. Но сама возможность использовать знакомую среду разработки и свои навыки не только для Android, но и для Windows (Mac, Linux) мне нравится. Я буду смотреть за развитием проекта, который пока находится в альфа-версии.

Пост написан на основе собственной статьи.

Подробнее..

Представляем бета-версию Jetpack Compose

04.03.2021 14:05:18 | Автор: admin

Совсем недавно, 24 февраля, мы анонсировали запуск бета-версииJetpack Compose. Этот новый набор инструментов для разработки пользовательского интерфейса позволит легко и быстро создавать оригинальные приложения для всех платформ Android. Jetpack Compose предоставляет современные и декларативные API для языка Kotlin для создания привлекательных и быстрых приложений с меньшим объемом кода. Набор совместим с существующими приложениями для Android и библиотеками Jetpack. Кроме того, его можно использовать вместе с Android Views.

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

Возможности бета-версии

При создании Compose нашей команде помогали и другие разработчики, которые оставляли свои отзывы. С момента открытия исходного кода в 2019 году мы выпустили 30 публичных версий продукта, получили более 700 внешних отчетов об ошибках и больше 200 внешних дополнений. Нам нравится наблюдать за результатами вашей работы с Сompose, и мы внимательно изучили все отзывы и предложения, чтобы усовершенствовать API и расставить приоритеты при разработке. Мы значительно доработали альфа-версию продукта, а также добавили и улучшили функционал. Вот некоторые из них:

  • Поддержка сопрограмм (новое)

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

  • Новый API для простого использованияанимации(новое)

  • Совместимостьс Views

  • Компоненты Material UIс примерами кода

  • Ленивые списки аналог RecyclerView

  • РазметкаConstraint Layoutна основе DSL

  • Модификаторы

  • Тестирование

  • Темы и графика для простого добавления тёмных и светлых тем

  • Ввод и жесты

  • Редактируемый и обычный текст

  • Управление окнами

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

Бета-версия Compose поддерживается в последней версииAndroid Studio Arctic Fox Canary, в которой тоже многоновых инструментов:

  • Live Literals: обновление литералов в реальном времени при предварительном просмотре, на устройстве и в эмуляторе (новое)

  • Предварительный просмотр анимации(новое)

  • Поддержка Compose в инструменте Layout Inspector(новое)

  • Интерактивный предварительный просмотр: воспроизведение сборки, выполненной с помощью Compose, в изолированной среде и взаимодействие с ней (новое)

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

Live Literals в Android EmulatorLive Literals в Android EmulatorLayout Inspector для Jetpack ComposeLayout Inspector для Jetpack Compose

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

Jetpack Compose безупречно работает с Android Views, и вам не придется менять старые привычки. Интерфейсы из Compose можно встроить в Android Views, и наоборот. Вдокументации по совместимостирассказано обо всех возможностях использования этих наборов инструментов.

Compose работает не только с Views, но и с самымираспространенными библиотеками. Вам не придется переписывать приложение. Вот что мы интегрировали:

  • Navigation

  • ViewModel

  • LiveData/RX/Flow

  • Paging

  • Hilt

БиблиотекиMDC-Android Compose Theme AdapterиAccompanistработают с темамиMaterialиAppCompatXML. Вам не придется повторять определения тем. Accompanist также предлагает оболочки распространеннымбиблиотекам для загрузки изображений.

Легкость работы в Compose

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

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

Знакомство с Compose

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

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

Заключение

В бета-версии Jetpack Compose все нужные API и функции готовы к выходу версии 1.0. Самое время знакомиться с набором инструментов и думать о том, где реализовать его возможности. Мы рады вашимотзывамоб использовании Compose. Своими впечатлениями можно также поделиться с другими разработчиками на канале #compose вKotlin Slack.


Выражаем благодарность за помощь в подготовке статьи коллегам: Анна-Кьяра Беллини(менеджер по продуктам),Ник Бутчер(подразделение по работе с разработчиками) и Звиад Кардава (подразделение по работе с разработчиками)

Подробнее..

Категории

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

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru