Как стать автором
Обновить

Использование примесей во Flutter приложениях

Время на прочтение 5 мин
Количество просмотров 12K


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

Что такое примеси?


Для начального знакомства я приведу выдержку из Википедии.
Примесь (англ. mix in) — элемент языка программирования (обычно класс или модуль), реализующий какое-либо четко выделенное поведение. Используется для уточнения поведения других классов, не предназначен для порождения самостоятельно используемых объектов.
В языке Dart подобные конструкции определяются словом mixin перед названием.

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

Чем то напоминает возможности множественного наследования? Да, но как мне кажется, подход с примесями лучше. А почему, давайте рассмотрим на примере.

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

abstract class Animal {
    void voice();
}

А также классы Cat и Dog, реализующие класс Animal.

class Cat extends Animal {
    void voice() {
        print(“Meow”);
    }
}

class Dog extends Animal {
    void voice() {
        print(“Woof”);
    }
}

И вот нам вдруг потребовался...
catdog

Да да, лично у меня во время разработки и не такое бывает.

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

class CatDog extends Cat, Dog {
}

Но как только мы нашему питомцу озвучим команду голос, получим весьма неприятную ситуацию — непонятно что именно он должен ответить, ведь метод voice реализован в обоих классах. Данная ситуация широко известна и носит название проблема ромба или Deadly Diamond of Death.

Deadly Diamond of Death

В случае реализации через примеси, мы с ней не столкнемся.

сlass Animal {
   void voice() {
      print(“Hakuna Matata!”);
   }
}

mixin Cat {
   void voice() {
      print(“Meow”);
   }
}

mixin Dog {
   void voice() {
      print(“Woof”);
   }
}

class CatDog extends Animal with Cat, Dog {
}

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

Класс Animal я реализовал специально, чтобы отметить особенность появившуюся в версии Dart 2.1. До нее добавлять примеси можно было лишь к классам, наследующимся от Object. Начиная с версии 2.1 реализовано добавление к наследникам любых классов.

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

abstract class Sportsman {
    void readySteadyGo();
}

mixin SkiRunner {
    void run() {
        print(“Ski, ski, ski”);
    }
}

mixin RifleShooter {
    void shot() {
        print(“Pew, pew, pew”);
    }
}

class Shooter() extends Sportsman with RifleShooter {
    void readySteadyGo() {
        shot();
    }
}

class Skier() extends Sportsman with SkiRunner {
    void readySteadyGo() {
        run();
    }
}

class Biathlete() extends Sportsman with SkiRunner, RifleShooter {
    void readySteadyGo() {
        run();
        shot();
    }
}

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

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

Например:

class A {
}

abstract class B {
}

mixin M1 on A {
}

mixin M2 on B {
}

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

class C extends A with M1 {
}
class D implements B with M2 {
}

Но получим ошибку пытаясь объявить подобное:
class E with M1, M2 {
}

Использование во Flutter приложении


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

Нам потребуется интерфейс для закрытия контроллера потока.

/// Interface for disposable objects
abstract class Disposable {
    void dispose();
}

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

/// Mixin for object which support state
mixin StateProvider implements Disposable {
    static const NONE_STATE = "None";

    final _stateController = BehaviorSubject<String>(seedValue: NONE_STATE);
    Observable<String> get stateOut => _stateController.stream;

    String get currentState => _stateController.value;

     void setState(String state) {
        _stateController.sink.add(state);
    }

    @override
    void dispose() {
        _stateController.close();
    }
}

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

/// Example BLoC
class ExampleBloc implements Disposable with StateProvider {
    static const EXAMPLE_STATE_1 = "EX1";
    static const EXAMPLE_STATE_2 = "EX2";

    Timer _timer;

    void init() {
        setState(EXAMPLE_STATE_1);

        _timer = new Timer(const Duration(seconds: 3), () {
            _timer = null;
            setState(EXAMPLE_STATE_2);
        });
    }

    @override
    void dispose() {
        if (_timer != null) {
            _timer.cancel();
            _timer = null;
        }
    }
}

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

class ExampleWidget extends StatelessWidget {
    final bloc = di<HomePageBloc>();

    @override
    Widget build(BuildContext context) {
        return StreamBuilder(
            stream: bloc.stateOut,
            builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
                // build widget by state
            },
        );
    }
}

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

Заключение


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

Ресурсы:
A tour of the Dart language
Википедия
Теги:
Хабы:
+15
Комментарии 5
Комментарии Комментарии 5

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн