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

Cross-platform

Из песочницы Эволюция Material Design для AvaloniaUI

18.11.2020 00:09:51 | Автор: admin

image


Material.Avalonia быстрый способ стилизовать под Material Desing приложение, написанное на AvaloniaUI кросс-платформенном XAML фреймворке для .NET.


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


Сейчас наше демо приложение выглядит вот так:


image


Еще примеры приложений, использующих Material.Avalonia


Начало использования


Сначала установим необходимый Nuget пакет:


dotnet add package Material.Avalonia

После этого изменим файл App.xaml, если нам нужно стилизовать все приложение. Либо, если нужно изменить оформление только одного окна или другого элемента управления, то вместо Application.Styles (Application.Resources) у нас будут Window.Styles или UserControl.Styles соответственно.


<Application ...             xmlns:themes="clr-namespace:Material.Styles.Themes;assembly=Material.Styles"             ...>    <Application.Resources>        <themes:BundledTheme BaseTheme="Light" PrimaryColor="Teal" SecondaryColor="Amber"/>    </Application.Resources>    <Application.Styles>        <StyleInclude Source="avares://Material.Avalonia/Material.Avalonia.Templates.xaml" />    </Application.Styles></Application>

Все, после этого все наше приложение будет использовать стили Material Design.
Однако, не все элементы управления уже стилизованы. Если некоторые из них не работают, то измените Application.Styles следующим образом:


<Application.Styles>    <StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>    <StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>    <StyleInclude Source="avares://Material.Avalonia/Material.Avalonia.Templates.xaml" /></Application.Styles>

Данное изменение добавит стандартные темы контролов Avalonia "под" темы Material.Avalonia.


Темы


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


Теперь для настройки темы по умолчанию нужно модифицировать свойства BundledTheme в App.xaml.


Базовая тема может быть светлой Light, темной Dark и наследуемой Inherit. Последний вариант будет пытаться получить тему, используемую системой, но в данный момент это еще не реализовано.


BundledTheme поддерживает задание всех, доступных в Material Design, "стандартных" цветов.


Смена цвета, например на Teal, из кода происходит подобным образом:


var paletteHelper = new PaletteHelper();var theme = paletteHelper.GetTheme();theme.SetPrimaryColor(SwatchHelper.Lookup[(MaterialColor) PrimaryColor.Teal]);paletteHelper.SetTheme(theme);

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


Кастомные контролы


Card

Спецификация на сайте Material Design


image


Для Card можно менять размер тени используя Attached Property:


<styles:Card assists:ShadowAssist.ShadowDepth="Depth1">...</styles:Card>

ColorZone

Цветовая зона позволяет легко переключать цвета фона и переднего плана из выбранной палитры Material Design или пользовательских цветов.


image


<styles:ColorZone Margin="4" Padding="8" Mode="Accent">Accent</styles:ColorZone>

FloatingButton

image


<styles:FloatingButton Content="+"  /><styles:FloatingButton Content="My long text" />

Тени


В Avalonia поддержка теней "из коробки" ограничена заданием BoxShadows для Border.
Однако уже реализован AttachedProperty ShadowAssist, который может быть использован для Card, FloatingButton и Border.


Когда для Border задается ShadowAssist.ShadowDepth то он самостоятельно корректирует BoxShadows для соответствия выбранному уровню ShadowDepth.


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


Демонстрация Card с разными ShadowDepth


image


Многое уже сделано, еще больше запланировано.
Ознакомиться или помочь с разработкой можно на GitHub
Скачать пакет на NuGet


Issue/PR и просто отзывы категорически приветствуются.
Поддержку от разработчиков Avalonia и всех сочувствующих можно получить в Telegram (ru) и Gitter (en), а документация по стилизации элементов управления доступна тут.

Подробнее..

Детальный разбор навигации в Flutter

24.07.2020 14:12:14 | Автор: admin

Навигация во Flutter


image


Flutter набирает популярность среди разработчиков. Большенство подходов впостроении приложений уже устоялись иприменяются ежедневно вразработке E-commerce приложений. Тема навигации опускают навторой или третий план. Какой API навигации предоставляет Фреймворк? Какие подходы выработаны? Как использовать эти подходы иначто они годятся?


Введение


Начнём с того, что такое навигация? Навигация это метод который позволяет перемещаться между пользовательским интерфейсом с заданными параметрами.
К примеру в IOS мире организовывает навигацию UIViewController, а в Android Navigation component. А чтопредоставляет Flutter?



Экраны в Flutter называются route. Для перемещениями между route существует класс Navigator который имеющий обширный API для реализации различных видов навигации.



Начнём спростого.Навигация нановый экран(route) вызывается методом push() который принимает всебя один аргумент это Route.


Navigator.push(MaterialPageRoute(builder: (BuildContext context) => MyPage()));

Давайте детальнее разберёмся ввызове метода push:


Navigator Виджет, который управляет навигацией.
Navigator.push() метод который добавляет новый route виерархию виджетов.
MaterialPageRoute() Модальный route, который заменяет весь экран адаптивным кплатформе переходом анимации.
builder обязательный аргумент конструктора MaterialPageRoute, который возвращает пользовательский интерфейс Фреймворк для отрисовки.
[MyPage](https://miro.medium.com/max/664/1Xm96KtLeIAAMtAYWcr1-MA.png)* пользовательский интерфейс реализованный при помощи Stateful/Stateless Widget


Возвращение на предыдущий route


Для возвращения с экрана на предыдущий необходимо использовать метод pop().


Navigator.pop();

Переда данных между экранами


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


Navigator.push(context, MaterialPageRoute(builder: (context) => MyPage(someData: data)));

В примере продемонстрирована передача данных в класс MyPage (в этом классе хранится пользовательский интерфейс).


Для того чтобы передать данные на предыдущий экран нужно вызвать метод pop() и передать опциональным аргументом туда данные.


Navigator.pop(data);


Состояние виджета Navigator, который вызван внутри одного из видов MaterialApp/CupertinoApp/WidgetsApp. State отвечает за хранение истории навигации и предоставляет API для управления историей.
Базовые методы навигации повторяют структуру данных Stack. В диаграмме можно наблюдать методы и "call flow" NavigatorState.


http://personeltest.ru/aways/habrastorage.org/webt/5w/dg/nb/5wdgnb-tjlngub4c8y4rlpqkeqi.png


Императивный vs Декларативный подход в навигации


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


Давайте на простом примере:


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


Декларативный подход, отвечает на вопрос что?
Пример: Столик для двоих, пожалуйста.


Для более глубокого понимания разницы советую прочитать эту статью Imperative vs Declarative Programming


Императивная навигация


Вернёмся к реализации навигации. В императивном подходе описывается детали работы в вызывающем коде. В нашем случае это поля Route. В Flutter много типов route, например MaterialPageRoute и CupertinoPageRoute. Например в CupertinoPageRoute задаётся title, или settings.


Пример:


Navigator.push(    context,    CupertinoPageRoute<T>(        title: "Setting",        builder: (BuildContext context) => MyPage(),        settings: RouteSettings(name:"my_page"),    ),);

Этот код и знания о новом route будут хранитьсяв ViewModel/Controller/BLoC/ У этот подхода существует недостаток.


Представим что потребовалось внести изменения в конструкторе в MyPage или в CupertinoPageRoute. Нужно искать каждый вызов метода push в проекте и изменять кодовую базу.


Вывод:


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

Декларативная навигация


Принцип декларативной навигации заключается в использовании декларативного подхода в программировании. Давайте разберём на примере.


Пример:


Navigator.pushNamed(context, '/my_page');

Принцип императивной навигации выглядит куда проще. Говорите "Отправь пользователя на экран настроек" передавая путь одним из аргументов навигации.
Для хранении реализации роста в самом Фреймворке предусмотрен механизм у MaterialApp/CupertinoApp/WidgetsApp. Это 2 колбэка onGenerateRoute и onUnknownRoute отвеспющие за хранение деталей реализации.


Пример:


MaterialApp(    onUnknownRoute: (settings) => CupertinoPageRoute(      builder: (context) {                return UndefinedView(name: settings.name);            }    ),  onGenerateRoute: (settings) {    if (settings.name == '\my_page') {      return CupertinoPageRoute(                title: "MyPage",                settings: settings,        builder: (context) => MyPage(),      );    }        // Тут будут описание других роутов  },);

Разберёмся подробнее в реализации:
Метод onGenerateRoute данный метод срабатывает когда был вызван Navigator.pushNamed(). Метод должен вернуть route.
Метод onUnknownRoute срабатывает когда метод onGenerateRoute вернул null. должен вернуть дефолтный route, по аналогии с web сайтами 404 page.


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

Диалоговые и модальные окна


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


Методы для вызова диалоговых и модальных окон:


  • showAboutDialog
  • showBottomSheet
  • showDatePicker
  • showGeneralDialog
  • showMenu
  • showModalBottomSheet
  • showSearch
  • showTimePicker
  • showCupertinoDialog
  • showDialog
  • showLicensePage
  • showCupertinoModalPopup

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


Как работает это под капотом?


Давайте рассмотрим исходный код одного из методов, например showGeneralDialog.


Исходный код:


Future<T> showGeneralDialog<T>({  @required BuildContext context,  @required RoutePageBuilder pageBuilder,  bool barrierDismissible,  String barrierLabel,  Color barrierColor,  Duration transitionDuration,  RouteTransitionsBuilder transitionBuilder,  bool useRootNavigator = true,  RouteSettings routeSettings,}) {  assert(pageBuilder != null);  assert(useRootNavigator != null);  assert(!barrierDismissible || barrierLabel != null);  return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(_DialogRoute<T>(    pageBuilder: pageBuilder,    barrierDismissible: barrierDismissible,    barrierLabel: barrierLabel,    barrierColor: barrierColor,    transitionDuration: transitionDuration,    transitionBuilder: transitionBuilder,    settings: routeSettings,  ));}

Давайте детальнее разберёмся в устройстве этого метода. showGeneralDialog вызывает метод push у NavigatorState с _DialogRoute(). Нижнее подчёркивание обозначает что этот класс приватный и используется только в пределах области видимости в которой сам описан, то есть в пределах этого файла.


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

Типы route в фреймворке


Теперь понятно что"every thins is a route", то есть что связанное с навигацией. Давайте взглянем на то, какие route уже реализованы в Фреймворке.


Два основных route в Flutter это PageRoute и PopupRoute.


PageRoute Модальный route, который заменяет весь экран.
PopupRoute Модальный route, который накладывает виджет поверх текущего route.


Реализации PageRoute:


  • MaterialPageRoute
  • CupertinoPageRoute
  • _SearchPageRoute
  • PageRouteBuilder

Реализации PopupRoute:


  • _ContexMenuRoute
  • _DialogRoute
  • _ModalBottomSheetRoute
  • _CupertinoModalPopupRoute
  • _PopupMenuRoute

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


Вывод:


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

Best practices


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


Начнём с того что мы сделаем некий сервис который будет будет соблюдать следующим аспектам:


  • Декларативный вызов навигации.
  • Отказ от использования BuildContext для навигации (Это критично если сервис навигации будет вызываться в компонентах, в которых нет возможности получить BuildContext).
  • Модульность. Можно вызвать любой route, CupertinoPageRoute, BottomSheetRoute, DialogRoute и т.д.

Для нашего сервиса навигации нам понадобится интерфейс:


abstract class IRouter {  Future<T> routeTo<T extends Object>(RouteBundle bundle);  Future<bool> back<T extends Object>({T data, bool rootNavigator});  GlobalKey<NavigatorState> rootNavigatorKey;}

Разберём методы:
routeTo - выполняет навигацию на новый экран.
back возвращает на предыдущий экран.
rootNavigatorKey GlobalKey умеющий вызывать методы NavigatorState.

После того как мы сделали интерфейс навигации, давайте сделаем реализацию этого интерфейса.


class Router implements IRouter {    @override  GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();    @override  Future<T> routeTo<T>(RouteBundle bundle) async {   // Push logic here  }    @override  Future<bool> back<T>({T data, bool rootNavigator = false}) async {        // Back logic here    }}

Супер, теперь нам нужно реализовать метод routeTo().


@override  Future<T> routeTo<T>(RouteBundle bundle) async {        assert(bundle != null, "The bundle [RouteBundle.bundle] is null");    NavigatorState rootState = rootNavigatorKey.currentState;    assert(rootState != null, 'rootState [NavigatorState] is null');    switch (bundle.route) {      case "/routeExample":        return await rootState.push(          _buildRoute<T>(            bundle: bundle,            child: RouteExample(),          ),        );      case "/my_page":        return await rootState.push(          _buildRoute<T>(            bundle: bundle,            child: MyPage(),          ),        );      default:        throw Exception('Route is not found');    }  }

Данный метод вызывает у root NavigatorState (который описан в WidgetsApp) метод push и конфигурирует его относительно RouteBundle который приходит одним из аргументов в данный метод.


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


enum ContainerType {  /// The parent type is [Scaffold].  ///  /// In IOS route with an iOS transition [CupertinoPageRoute].  /// In Android route with an Android transition [MaterialPageRoute].  ///  scaffold,  /// Used for show child in dialog.  ///  /// Route with [DialogRoute].  dialog,  /// Used for show child in [BottomSheet].  ///  /// Route with [ModalBottomSheetRoute].  bottomSheet,  /// Used for show child only.  /// [AppBar] and other features is not implemented.  window,}class RouteBundle {  /// Creates a bundle that can be used for [Router].  RouteBundle({    this.route,    this.containerType,  });  /// The route for current navigation.  ///  /// See [Routes] for details.  final String route;  /// The current status of this animation.  final ContainerType containerType;}

enum ContainerType тип контейнера, котрый будет задаваться декларативно из вызываемого кода.
RouteBundle класс-холдер данных отвечающих конфигурацию нового route.


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


Route<T> _buildRoute<T>({@required RouteBundle bundle, @required Widget child}) {    assert(bundle.containerType != null, "The bundle.containerType [RouteBundle.containerType] is null");    switch (bundle.containerType) {      case ContainerType.scaffold:        return CupertinoPageRoute<T>(          title: bundle.title,          builder: (BuildContext context) => child,          settings: RouteSettings(name: bundle.route),        );      case ContainerType.dialog:        return DialogRoute<T>(          title: '123',          settings: RouteSettings(name: bundle.route),          pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {                        return child;                    },        );      case ContainerType.bottomSheet:        return ModalBottomSheetRoute<T>(          settings: RouteSettings(name: bundle.route),          isScrollControlled: true,          builder: (BuildContext context) => child,        );      case ContainerType.window:        return CupertinoPageRoute<T>(          settings: RouteSettings(name: bundle.route),          builder: (BuildContext context) => child,        );      default:        throw Exception('ContainerType is not found');    }  }

Думаю что в этой функции стоит рассказать о ModalBottomSheetRoute и DialogRoute, которые использую. Исходный код этих route позаимствован из раздела Material исходного кода Flutter.


Осталось сделать метод back.


@overrideFuture<bool> back<T>({T data, bool rootNavigator = false}) async {    NavigatorState rootState = rootNavigatorKey.currentState;  return await (rootState).maybePop<T>(data);}

Ну и конечно перед использованием сервиса необходимо передать rootNavigatorKey в App следующим образом:


MaterialApp(    navigatorKey: widget.router.rootNavigatorKey,    home: Home());

Кодовая база для нашего сервиса готова, давайте вызовем наш route. Для этого создадим инстанс нашего сервиса и каким-либо образом "прокинуть" в объект, который будет вызывать этот инстанс, например при помощи Dependency Injection.



router.routeTo(RouteBundle(route: '/my_page', containerType: ContainerType.window));

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


  • Декларативный вызов навигации
  • Отказ от BuildContext по средствам GlobalKey
  • Модульность достигнута возможностью конфигурирования route относительно имени пути и контейнера для View

Итог


В Фреймворке Flutter существуют различные методы для навигации, которые дают преимущества и недостатки.


Ну и конечно полезные ссылки:
Мой телеграм канал
Мои друзья Flutter Dev Podcast
Вакансии Flutter разработчиков

Подробнее..

Архитектурный шаблон MVI в Kotlin Multiplatform. Часть 3 тестирование

27.08.2020 16:05:58 | Автор: admin


Эта статья является заключительной в серии о применении архитектурного шаблона MVI в Kotlin Multiplatform. В предыдущих двух частях (часть 1 и часть 2) мы вспомнили, что такое MVI, создали общий модуль Kittens для загрузки изображений котиков и интегрировали его в iOS- и Android-приложения.

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

Обновлённый пример проекта доступен на нашем GitHub.

Пролог


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

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

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

Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. ...[Therefore,] making it easy to read makes it easier to write. Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship

Kotlin Multiplatform расширяет возможности тестирования. Эта технология добавляет одну важную особенность: каждый тест автоматически выполняется на всех поддерживаемых платформах. Если поддерживаются, например, только Android и iOS, то количество тестов можно умножить на два. И если в какой-то момент добавляется поддержка ещё одной платформы, то она автоматически становится покрытой тестами.

Тестирование на всех поддерживаемых платформах важно, потому что могут быть различия в поведении кода. Например, у Kotlin/Native особенная модель памяти, Kotlin/JS тоже иногда даёт неожиданные результаты.

Прежде чем идти дальше, стоит упомянуть о некоторых ограничениях тестирования в Kotlin Multiplatform. Самое большое из них это отсутствие какой-либо библиотеки моков для Kotlin/Native и Kotlin/JS. Это может показаться большим недостатком, но я лично считаю это преимуществом. Мне довольно трудно давалось тестирование в Kotlin Multiplatform: приходилось создавать интерфейсы для каждой зависимости и писать их тестовые реализации (fakes). На это уходило много времени, но в какой-то момент я понял, что трата времени на абстракции это инвестиция, которая приводит к более чистому коду.

Я также заметил, что последующие модификации такого кода требуют меньше времени. Почему так? Потому что взаимодействие класса с его зависимостями не прибито гвоздями (моками). В большинстве случаев достаточно просто обновить их тестовые реализации. Нет необходимости углубляться в каждый тестовый метод, чтобы обновить моки. В результате я перестал использовать библиотеки моков даже в стандартной Android-разработке. Я рекомендую прочитать следующую статью: "Mocking is not practical Use fakes" (автор Pravin Sonawane).

План


Давайте вспомним, что у нас есть в модуле Kittens и что нам стоит протестировать.

  • KittenStore основной компонент модуля. Его реализация KittenStoreImpl содержит бОльшую часть бизнес-логики. Это первое, что мы собираемся протестировать.
  • KittenComponent фасад модуля и точка интеграции всех внутренних компонентов. Мы покроем этот компонент интеграционными тестами.
  • KittenView публичный интерфейс, представляющий UI, зависимость KittenComponent.
  • KittenDataSource внутренний интерфейс для доступа к Сети, который имеет платформенно-зависимые реализации для iOS и Android.

Для лучшего понимания структуры модуля приведу его UML-диаграмму:



План следующий:

  • Тестирование KittenStore
    • Создание тестовой реализации KittenStore.Parser
    • Создание тестовой реализации KittenStore.Network
    • Написание модульных тестов для KittenStoreImpl

  • Тестирование KittenComponent
    • Создание тестовой реализации KittenDataSource
    • Создание тестовой реализации KittenView
    • Написание интеграционных тестов для KittenComponent

  • Запуск тестов
  • Выводы


Модульное тестирование KittenStore


Интерфейс KittenStore имеет свой класс реализации KittenStoreImpl. Именно его мы и собираемся тестировать. Он имеет две зависимости (внутренние интерфейсы), определённые прямо в самом классе. Начнём с написания тестовых реализаций для них.

Тестовая реализация KittenStore.Parser


Этот компонент отвечает за сетевые запросы. Вот как выглядит его интерфейс:


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

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

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

Тестовая реализация KittenStore.Parser


Этот компонент отвечает за разбор ответов от сервера. Вот его интерфейс:


Как и в случае с Network, используется TestScheduler для замораживания подписчиков и проверки их совместимости с моделью памяти Kotlin/Native. Ошибки обработки ответов моделируются, если входная строка пуста.

Модульные тесты для KittenStoreImpl


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

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


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


Этапы теста:

  • сгенерировать исходные изображения;
  • создать экземпляр KittenStoreImpl;
  • сгенерировать новые изображения;
  • отправить Intent.Reload;
  • убедиться, что состояние содержит новые изображения.

И наконец давайте проверим следующий сценарий: когда в состоянии установлен флаг isLoading во время загрузки изображений.


Есть две зависимости: KittenDataSource и KittenView. Нам понадобятся тестовые реализации для них, прежде чем мы сможем начать тестирование.

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



Тестовая реализация KittenDataSource


Этот компонент отвечает за сетевые запросы. У него есть отдельные реализации для каждой платформы, и нам нужна ещё одна реализация для тестов. Вот как выглядит интерфейс KittenDataSource:


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

Для формирования JSON-массива используется библиотека kotlinx.serialization. Кстати, тестируемый KittenStoreParser использует её же для декодирования.

Тестовая реализация KittenView


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


Нам просто нужно запоминать последнюю принятую модель это позволит проверить правильность отображаемой модели. Мы также можем отправлять события от имени KittenView с помощью метода dispatch(Event), который объявлен в наследуемом классе AbstractMviView.

Интеграционные тесты для KittenComponent


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

Как и раньше, давайте начнём с создания экземпляров зависимостей и инициализации:


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


Этапы:

  • сгенерировать исходные ссылки на изображения;
  • создать и запустить KittenComponent;
  • сгенерировать новые ссылки;
  • отправить Event.RefreshTriggered от имени KittenView;
  • убедиться, что новые ссылки достигли TestKittenView.


Запуск тестов


Чтобы запустить все тесты, нам нужно выполнить следующую Gradle-задачу:

./gradlew :shared:kittens:build

Это скомпилирует модуль и запустит все тесты на всех поддерживаемых платформах: Android и iosx64.

А вот JaCoCo-отчёт о покрытии:



Заключение


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

  • KittenStoreImpl содержит бОльшую часть бизнес-логики;
  • KittenStoreNetwork отвечает за сетевые запросы высокого уровня;
  • KittenStoreParser отвечает за разбор сетевых ответов;
  • все преобразования и связи.

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

Такие тесты имеют следующие преимущества:

  • не используют платформенные API;
  • выполняются очень быстро;
  • надёжные (не мигают);
  • выполняются на всех поддерживаемых платформах.

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

Надеюсь, это поможет вам в ваших проектах. Спасибо, что читали мои статьи! И не забудьте подписаться на меня в Twitter.



Бонусное упражнение


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

Рефакторинг KittenDataSource


В модуле существуют две реализации интерфейса KittenDataSource: одна для Android и одна для iOS. Я уже упоминал, что они отвечают за доступ к сети. Но на самом деле у них есть ещё одна функция: они генерируют URL-адрес для запроса на основе входных аргументов limit и page. В то же время у нас есть класс KittenStoreNetwork, который ничего не делает, кроме делегирования вызова в KittenDataSource.

Задание: переместить логику генерирования URL-запроса из KittenDataSourceImpl (на Android и iOS) в KittenStoreNetwork. Вам нужно изменить интерфейс KittenDataSource следующим образом:



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

Добавление постраничной загрузки


TheCatAPI поддерживает разбивку на страницы, поэтому мы можем добавить эту функцию для лучшего взаимодействия с пользователем. Вы можете начать с добавления нового события Event.EndReached для KittenView, после чего код перестанет компилироваться. Затем вам нужно будет добавить соответствующий Intent.LoadMore, преобразовать новый Event в Intent и обработать последний в KittenStoreImpl. Вам также потребуется изменить интерфейс KittenStoreImpl.Network следующим образом:



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

Подробнее..

От WPF к Авалонии

16.02.2021 12:20:24 | Автор: admin

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

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

Стили

На первый взгляд, стили в Авалонии выглядят точно также, как и в WPF они задаются в блоке Styles с помощью селекторов и сеттеров. Первые выбирают набор блоков, к которым применяются стили, вторые задают непосредственно стили. Давайте сравним два одинаковых стиля в WPF и Авалонии:

<Style TargetType="TextBlock">  <Setter Property="HorizontalAlignment" Value="Center" />  <Setter Property="FontSize" Value="24"/></Style>
<Style Selector="TextBlock"><Setter Property="HorizontalAlignment" Value="Center" /><Setter Property="FontSize" Value="24"/></Style>

Как видите, в данном фрагменте различается только объявление тега Style в WPF для выбора целевого блока используется параметр TargetType, а в Авалонии - Selector. Однако селекторы в Авалонии куда мощнее, чем TargetType в WPF. Больше всего они напоминают селекторы из CSS с классами, псевдоклассами и кастомными обращениями.

Например, вот так мы можем задать размер шрифта для всех текстовых блоков с классом h1

<Styles>  <Style Selector="TextBlock.h1">    <Setter Property="FontSize" Value="24"/>  </Style></Styles><TextBlock Classes="h1">Header</TextBlock>

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

<Styles>  <Style Selector="Button:pointerover">    <Setter Property="Button.Foreground" Value="Red"/>  </Style></Styles><Button>I will have red text when hovered.</Button>

И, конечно же, селекторы в Авалонии позволяют гибко выбирать целевые контролы для стилей через цепочки дочерних элементов, по совпадению нескольких классов, по шаблонам и по значениям определенных свойств. Опять же, это очень похоже на селекторы в CSS. Например, вот так мы можем выбрать кнопку, являющуюся прямым наследником элемента с классом block, имеющую значение свойства IsDefault = true:

.block > Button[IsDefault=true]

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

Обновленный синтаксис XAML

Как и в случае со стилями, синтаксис XAML не слишком сильно отличается от WPF. Объявление контролов, параметры, биндинги все выглядит по-прежнему. Однако некоторые отличия все же есть синтаксис стал более емким и понятным, а где-то добавились новые возможности. Посмотрим на изменения по порядку.

Начнем с упрощений в синтаксисе. Самый простой пример такого упрощения это объявление строк и столбцов в Grid. Классическое, привычное с WPF объявление будет выглядеть следующим образом:

<Grid>  <Grid.RowDefinitions>    <RowDefinition Height="*"></RowDefinition>    <RowDefinition Height="Auto"></RowDefinition>    <RowDefinition Height="32"></RowDefinition>  </Grid.RowDefinitions></Grid>

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

<Grid RowDefinitions="*,Auto,32,"/>

Упростилось и подключение зависимостей в XAML файлах. Теперь clr-namespace можно заменить на using. Такое изменение позволяет сделать подключение сторонних библиотек короче и читаемее.

Было: xmlns:styles="clr-namespace:Material.Styles;assembly=Material.Styles"

Стало: xmlns:styles="using=Material.Styles"

Другое любопытное изменение это вынесение DataTemplates и Styles в отдельные теги. Раньше они размещались внутри Resources.

<UserControl xmlns:viewmodels="clr-namespace:MyApp.ViewModels;assembly=MyApp">  <UserControl.DataTemplates>    <DataTemplate DataType="viewmodels:FooViewModel">      <Border Background="Red" CornerRadius="8">        <TextBox Text="{Binding Name}"/>      </Border>    </DataTemplate>  </UserControl.DataTemplates>  <UserControl.Styles>    <Style Selector="ContentControl.Red">      <Setter Property="Background" Value="Red"/>    </Style>  </UserControl.Styles><UserControl>

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

<TextBox Name="source"/><!-- Binds to the Text property of the "source" control --><TextBlock Name=other Text="{Binding #source.Text}"/>

Конструкция $parent позволяет обращаться к родительским компонентам.

<Border Tag="Hello World!">  <TextBlock Text="{Binding $parent.Tag}"/></Border>

Кстати, такое обращение поддерживает индексирование. Иначе говоря, конструкция $parent[1] позволит вам обратиться к родителю родителя вашего компонента. А конструкция $parent[0] эквивалентна $parent.

Помимо индексов здесь также можно использовать обращение по типу. $parent[Border] позволит вам обратиться к первому предку с типом Border. А еще такое обращение можно совместить с индексированием.

<Border Tag="Hello World!">  <Border>    <Decorator>      <TextBlock Text="{Binding $parent[Border;1].Tag}"/>    </Decorator>  </Border></Border>

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

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

<StackPanel>  <TextBox Name="input" IsEnabled="{Binding AllowInput}"/>  <TextBlock IsVisible="{Binding !AllowInput}">Sorry, no can do!</TextBlock></StackPanel>

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

<Panel>  <ListBox Items="{Binding Items}"/>  <TextBlock IsVisible="{Binding !Items.Count}">No results found</TextBlock></Panel>

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

<Panel>  <ListBox Items="{Binding Items}" IsVisible="{Binding !!Items.Count}"/></Panel>

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

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

<TextBlock Text="{Binding MyText}" IsVisible="{Binding MyText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>

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

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

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

<Application>  <NativeMenu.Menu>    <NativeMenu>      <NativeMenuItem Header="About MyApp" Command="{Binding AboutCommand}" />    </NativeMenu>  </NativeMenu.Menu></Application>

Декларативный UI via F#

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

Сообщество Авалонии разработало отличную библиотеку Avalonia.FuncUI, позволяющую вам писать UI на Авалонии в декларативном стиле. Получающийся код напоминает реализацию UI с помощью Elm или Jetpack Compose.

module Counter =    type CounterState = {    count : int  }  let init = {    count = 0  }  type Msg =  | Increment  | Decrement      let update (msg: Msg) (state: CounterState) : CounterState =   match msg with    | Increment -> { state with count =  state.count + 1 }    | Decrement -> { state with count =  state.count - 1 }  let view (state: CounterState) (dispatch): IView =    DockPanel.create [      DockPanel.children [        Button.create [          Button.onClick (fun _ -> dispatch Increment)          Button.content "click to increment"        ]        Button.create [          Button.onClick (fun _ -> dispatch Decrement)          Button.content "click to decrement"         ]        TextBlock.create [          TextBlock.dock Dock.Top          TextBlock.text (sprintf "the count is %i" state.count)        ]      ]    ]

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

Недостатки

Перейдем к самому интересному а что же не так с Авалонией в сравнении с WPF?

Если смотреть на функциональность, то Авалония не только приобрела новые фичи, но и потеряла некоторые возможности WPF. Например, в Авалонии из коробки отсутствуют триггеры (обсуждение этой фичи можно найти в репозитории Авалонии, а пока триггеры можно использовать с помощью пакета AvaloniaBehaviors), не работают биндинги через стили и так далее.

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

Ну и самая заметная проблема, о которой говорят многие это отсутствие гарантий. Авалония это инструмент, создаваемый исключительно сообществом, что не слишком-то привычно для .NET разработчиков. За ним не стоит Microsoft или какая-то другая крупная компания. В этих условиях многим не хочется рисковать, надеясь на open source продукт кто знает, вдруг завтра мейнтейнер потеряет интерес, и разработка встанет? Однако это же дает вам возможность заметно влиять на развитие Авалонии, исправляя существующие проблемы и предлагая новые фичи.

Заключение

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

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

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

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

Подробнее..

DLang, Vibe.d и кросс-компиляция для RPi4

18.06.2021 10:22:12 | Автор: admin

Добрый вечер!

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

Недавно я написал сервер на DLang с использованием библиотеки Vibe.d. Для него я пишу кросс-платформенный клиент. Основной моей системой является Arch, и мне этого более чем достаточно, но для тестирования некоторых платформозависимых вещей я перезагружаюсь в Windows 10.

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

Попытка 1. Компиляция на микрокомпьютере

Первое, что пришло мне в голову - скинуть исходники на целевое устройство, скачать туда компилятор, систему сборки (в моем случае DUB) и собрать все там. Я предполагал, что это будет немного дольше, но я всегда мог оставить этот процесс выполняться на ночь, но не тут-то было...

Исходники я быстро скинул при помощи scp, после чего подключился к микрокомпьютеру по ssh и начал разбираться с компилятором.

На целевом устройстве стоит основанный на Debian Raspbian 10 Buster. Для поиска пакетов придется использовать apt... По сравнению с pacman он гораздо менее удобный, выдает кучу лишнего мусора (ИМХО).

Эталонным компилятором для D является dmd, но поиск по пакетам не выдал ничего полезного. Как оказалось : первое - dmd нет в стандартном репозитории Debian и Ubuntu, второе - он не поддерживает arm.

Чтож... Ничего страшного, ведь для D есть еще минимум 2 полноценных компилятора. Один из них, GDC, является частью GNU-Compiler-Collection и, вероятно, найдется везде, где есть Linux. Второй же, ldc, использует LLVM для генерации кода, поэтому его вы можете найти практически везде (Можно даже скачать его на Android через Termux).

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

sudo apt install ldc2 gdc

Оставался только DUB - de facto система сборки и менджер пакетов для D. Он есть в стандартном репозитории, но он не работает... Проекты не инициализируются, при загрузке зависимостей не может установить соединение с сервером. Сплошное расстройство!

В одной из статей про сборку D для arm было указано про проблемы с DUB. В качестве решения там предлагалось собрать его самостоятельно из исходников. Почему нет? Спускаемя к исходникам : https://github.com/dlang/dub

Для сборки в репозитории есть скрипт build.d (Код на D может быть запущен как скрипт при помощи специального интерпретатора - rdmd для dmd, ldmd для ldc, gdmd для gdc). gdmd не установился вместе с gdc, так что будем использовать ldmd. Собираем :

ldmd -run build.d

Собрать-то собрали, но лучше от этого не стало. dub работает, но очень медленно. Загрузить все зависимости для Vibe.d ему так и не удалось...

Попытка 2. Кросс-компиляция через GDC

Первая статья, которая была про компиляцию D кода под arm предлагала использовать кросс версию gdc. Почему нет?

Идем на официальный сайт и скачиваем последнюю доступную версию : https://gdcproject.org/old/downloads . Первоначально меня смутила дата сборки компилятора, но что же поделать.

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

import std.stdio;int main(string [] args) {  writeln("Hello, World!");  return 0;}

Теперь нужно это откомпилить и проверить на целевой платформе. С тулчейном это делается достаточно просто (только вместо arm-linux-gnueabihf-gdc нужно указывать полный путь) :

arm-linux-gnueabihf-gdc app.d

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

Оставалось только собрать vibe.d. Для работы с HTTPS он использует криптографическую библиотеку : OpenSSl или Botan. В той же статье был представлен интересный метод, позволяющий избежать их ручной компиляции : смонтировать всю файловую систему микрокомпьютера и указать компилятору путь до библиотек, хранящихся на нем.

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

Статья, которую я использовал для этого пункта.

Попытка 3. LDC и мутки с библиотеками

Остался только один компилятор, способный откомпилить нужную мне программу для arm - ldc. Он установлен у меня на основном компьютере при помощи pacman. Какие-то дополнительные действия не нужны.

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

ldc-build-runtime --ninja --dFlags="-w;-mtriple=arm-linux-gnueabihf"

Появиться папка ldc-build-runtime.tmp/lib в которой храниться уже откомпилированная стандартная библиотека для arm. Для того чтобы скормить ее компилятору указываем следующий флаг : -L=-Lldc-build-runtime.tmp/lib
Пробуем собрать Hello World - ошибка линковки (не может найти точку входа). "Приехали", - подумал я. Собрал для начала объектный файл и решил попробовать слинковать его уже на raspberry - ld послал меня куда подальше.

Подумав и погадав, я решил скачать кросс-компилятор для C, взять линковщик оттуда и собрать все на основной машине. В Ubuntu и Debian arm-linux-gnueabihf есть в стандартном репозитории, а в Arch его можно было или собрать из AUR, или скачать уже собранный.

Раз уже делать, так делать - собираем из AUR. Процесс долгий, но интересный. Компиляция происходит в 3 этапа, постепенно собирая binutils, glibc и тп.

Спустя пару часов компилятор все же собрался. Я установил путь до него и собрал тестовую программу. Заработало!

Пробуем таким же образом слинковать другие библиотеки для проекта с Vibe.d... Не получилось. Линковщик стал таскать абсолютно все библиотеки из файловой системы raspberry и потерял половину ссылок. Undefined reference на malloc я еще не видел. До этого момента.

Не будем расстраиваться. Раз без включения библиотек от Raspberry все работает, просто локально соберем OpenSSL и ZLib (так же нужен), а потом прилинкуем их.

ZLib собрался довольно просто и быстро. Прилинковать его при помощи -L=-Lzlib/lib тоже не составило труда. Но вот OpenSSL отказался собираться от слова совсем. То ему компилятор не нравиться, то ему просто я не нравлюсь.

Из решений я нашел только одно - собрать OpenSSL на малине, скачать оттуда собранную и прилинковать на основной машине. На микрокомпьютере OpenSSL собрать было нетрудно. Я скачал его и добавил флаг -L=-Lopenssl/lib.

При компиляции я увидел всего 2 ошибки : неопределенная ссылка на SSL_get_peer_certificate и на ERR_put_error. Это меня доконало, я решил оставить это дело и лечь спать. Если бы я знал, как я был близок к победе на этом моменте.

Попытка 4. Toolchain и скрипты

На следующий день я решил попробовать все сначала, учесть все предыдущие ошибки и сделать что-то наподобие toolchainа для сборки D кода под ARM.

Начать я решил с кросс-компилятора для C. Загуглив что-то наподобие "arm-linux-gnueabihf gcc" я нашел вот этот вот сайт.

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

Для проверки работоспособности компилятора я решил запустить Hello World, но уже на C :

#include <stdio.h>int main(int argc, char ** argv) {  printf("Hello, World!\n");  return 0;}

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

#!/bin/bashgcc_arm/bin/arm-linux-gnueabihf-gcc main.c -o app.o

К счастью, все заработало с первого раза. Теперь я решил приступить к настройке компилятора для D. Как самый успешный, был выбран ldc. Я скачал последний релиз с GitHub (http://personeltest.ru/aways/github.com/ldc-developers/ldc) и также поместил рядом с C-компилятором.

Теперь было необходимо собрать стандартную библиотеку D для arm. Так появился build_ldc_runtime.sh :

#!/bin/bashexport CURRENT_DIR=$(pwd)export LDC_PATH=$CURRENT_DIR/"ldc2/bin"export GCC_PATH=$CURRENT_DIR/"gcc_arm/bin"export CC=$GCC_PATH/"arm-linux-gnueabihf-gcc"$LDC_PATH/ldc-build-runtime --ninja --dFlags="-w;-mtriple=arm-linux-gnueabihf"

Сборка прошла успешно, и я перешел к тестированию HelloWorld. Также откомпилил его и запустил на Raspberry, но получил слегка неожиданную ошибку. Исполняемый файл требовал GLIBC не ниже, чем 2.29. Погуглив, как посмотреть версию GLIBC, я понял, что на малине стоит всего-лишь 2.28...

Так как, по моему мнению, clang и llvm, а следовательно и ldc, не пропагандируют GLIBC и вроде как даже могут обходиться без нее, был сделан вывод о том, что проблема в gcc.

Я решил скачать Toolchain с той же версией gcc, что и на целевом устройстве, то есть 8.3.0. К счастью, на том же сайте был найден подходящий компилятор. Я удалил старый, распаковал новый, пересобрал ldc_runtime и все заработало.

Чудесно. Оставалось присобачить сюда ZLib и OpenSSL. Начал я, конечно, с ZLib, так как с ним меньше проблем :

#!/bin/bashexport CURRENT_DIR=$(pwd)export GCC_PATH=$CURRENT_DIR/"gcc_arm/bin"export CC=$GCC_PATH/"arm-linux-gnueabihf-gcc"mkdir tmp_zlibcd tmp_zlibgit clone https://github.com/madler/zlib.gitcd zlib./configure --prefix=$CURRENT_DIR/zlibmakemake installcd ../..rm -rf tmp_zlib

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

Не имею никакого желания с этим разбираться, я решил отказаться от OpenSSL и использовать Botan (Vibe.d имеет такую возможность). К слову, все собралось и слинковалось с первого раза :

#!/bin/bashexport CURRENT_DIR=$(pwd)export GCC_PATH=$CURRENT_DIR/"gcc_arm/bin"export CCX=$GCC_PATH/"arm-linux-gnueabihf-g++"mkdir tmp_botancd tmp_botangit clone https://github.com/randombit/botan.gitcd botanpython configure.py --cpu=arm --cc-bin=${CCX} --prefix=${CURRENT_DIR}/botanmake && make installcd ../..rm -rf tmp_botan

Приложение с Vibe.d И Botan успешно откомпилилось и ДАЖЕ ЗАПУСТИЛОСЬ на raspberry. Просто замечательно. Но есть одно "но" - OpenSSL. Мы так и не разобрались с ним. Проблема оказалась достаточно глупой и легко решаемой.

Поискав SSL_get_peer_certificate и ERR_put_error в репозитории OpenSSL я понял, что в последних alpha версиях они были объявлены deprecated и удалены. Vibe.d официально поддерживает OpenSSL версии 1.1, а я скачал alpha 3.x, где этих функций просто не было.

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

#!/bin/bashexport CURRENT_DIR=$(pwd)mkdir tmp_opensslcd tmp_opensslgit clone https://github.com/openssl/openssl -b OpenSSL_1_1_1-stablecd openssl./Configure linux-generic32 shared \    --prefix=$CURRENT_DIR/openssl --openssldir=$CURRENT_DIR/tmp_openssl/openssl \    --cross-compile-prefix=$CURRENT_DIR/gcc_arm/bin/arm-linux-gnueabihf-make && make installcd ../..rm -rf tmp_openssl

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

#!/bin/bashexport CURRENT_DIR=/home/test_user/Projects/rpi4_d_toolchainexport LDC_PATH=$CURRENT_DIR/ldc2/binexport GCC_PATH=$CURRENT_DIR/gcc_arm/binexport LDC_RUNTIME_PATH=$CURRENT_DIR/ldc-build-runtime.tmp/libexport OPENSSL_PATH=$CURRENT_DIR/openssl/libexport ZLIB_PATH=$CURRENT_DIR/zlib/libexport BOTAN_PATH=$CURRENT_DIR/botan/libexport CC=$GCC_PATH/"arm-linux-gnueabihf-gcc"$LDC_PATH/ldc2 -mtriple=arm-linux-gnueabihf -gcc=$CC -L=-L${LDC_RUNTIME_PATH} -L=-L${OPENSSL_PATH} -L=-L${ZLIB_PATH} -L=-L${BOTAN_PATH} $@

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

dub build --compiler=~/ldc_rpi

Прототипом и основным источником информации послужил вот этот GitHub репозиторий.

Заключение

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

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

  2. Версии компиляторов основной и целевой платформы должны совпадать, иначе что-то может пойти не так

  3. Скрипты автоматизируют и упростят вашу жизнь. Используйте их

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

А если вы когда-нибудь соберетесь собирать что-то для raspberry pi 4, то можно скачать отсюда тот Toolchain, который в итоге у меня получился (тут все компиляторы и библиотеки).

Только в файле ldc_rpi, поменяйте значение CURRENT_DIR на путь до папки с toolchainом.

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

Подробнее..

Категории

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

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