Оценок пока нет Введение в понимание виджетов во Flutter

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

[info]Примечание. Если вы хотите лучше познакомиться с Flutter, углубившись в программный код, то ознакомьтесь Basic Flutter layout codelab, Building Layouts in Flutter и Adding Interactivity to Your Flutter App.[/info]

Hello World

Давайте глянем на маленький пример. Минимальное приложение Flutter, которое просто вызывает функцию runApp() с виджетами Center и Text:

import 'package:flutter/material.dart';
void main() {
  runApp(
    Center(
      child: Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
      ),
    ),
  );
}

Функция runApp() получает экземпляр класса Widget и делает его корневым элементом в дереве виджетов. В данном примере дерево виджетов состоит из двух виджетов: виджет Center и его потомок в виде виджета Text , который выводит текст. Фреймворк заставляет корневой виджет охватить весь экран, что означает вывести на экран словосочетание «Hello World», который будет центрирован в экране. Направление текста необходимо указывать в случае, когда используется MaterialApp, как это будет показано позже.

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

[info] StatelessWidget – он как дорожный знак, нужен там где внутреннее состояние одно и оно сформировано параметрами и данными которые известны заранее. [/info]

[info] StatefulWidget – он как светофор, нужен когда внутренних состояний больше одного и они могут сменять друг друга. [/info]

Базовые виджеты

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

  • Text: Виджет Text позволяет вам создавать стилизованный текст в вашем приложении;
  • RowColumn: Эти гибкие виджеты позволяют создавать гибкие макеты, как в горизонтальном (Row), так и в вертикальном (Column) направлениях. Их дизайн основан на модели макета flexbox в WEB.
  • Stack: Вместо линейной ориентации (по горизонтали или вертикали), виджет Stack позволяет размещать виджеты друг над другом в порядке рисования. Затем можно использовать виджет Positioned для дочерних элементов стека, чтобы расположить их относительно верхнего, правого, нижнего или левого края стека. Стеки основаны на модели макета абсолютного позиционирования в WEB, т.е. поведение подобно поведению CSS — свойства {position: absolute};
  • Container: Виджет Container позволяет создавать прямоугольный визуальный элемент. Контейнер может быть украшен с помощью BoxDecoration, например фоном, рамкой или тенью. Контейнер также может иметь поля, отступы и ограничения, применяемые к его размеру. Кроме того, Container может быть преобразован в трехмерном пространстве с использованием матрицы.

Ниже приведены несколько простых виджетов, которые объединяют эти и другие виджеты:

import 'package:flutter/material.dart';

class MyAppBar extends StatelessWidget {
  MyAppBar({this.title});

  //Fields in a Widget subclass are always marked "final".

  final Widget title;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 56.0, // in logical pixels
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      decoration: BoxDecoration(color: Colors.blue[500]),
      // Row is a horizontal, linear layout.
      child: Row(
        //  is the type of items in the list.
        children: [
          IconButton(
            icon: Icon(Icons.menu),
            tooltip: 'Navigation menu',
            onPressed: null, // null disables the button
          ),
          // Expanded expands its child to fill the available space.
          Expanded(
            child: title,
          ),
          IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
    );
  }
}

class MyScaffold extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Material is a conceptual piece of paper on which the UI appears.
    return Material(
      // Column is a vertical, linear layout.
      child: Column(
        children: [
          MyAppBar(
            title: Text(
              'Example title',
              style: Theme.of(context).primaryTextTheme.title,
            ),
          ),
          Expanded(
            child: Center(
              child: Text('Hello, world!'),
            ),
          ),
        ],
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    title: 'My app', // used by the OS task switcher
    home: MyScaffold(),
  ));
}

[info]Поля в подклассе Widget всегда отмечены как final[/info]

[info]Виджет Material — это концептуальный лист бумаги, на котором появляется пользовательский интерфейс.[/info]

[info]Виджет Row необходим для горизонтальной линейной планировки, а Column — для вертикальной линейной планировки.[/info]

[info]Виджет Expanded расширяет свой дочерний элемент, чтобы заполнить доступное пространство.[/info]

Убедитесь, что в разделе flutter файла pubspec.yaml есть запись use-material-design: true. Это позволяет использовать предопределенный набор иконок материального дизайна:

name: my_app
flutter:
  uses-material-design: true

Многие виджеты Material Design должны быть внутри MaterialApp для правильного отображения, чтобы наследовать данные темы. Поэтому запустите приложение с MaterialApp.

Виджет MyAppBar создает Container с высотой 56 независимых от устройства пикселей с внутренним заполнением 8 пикселей, как слева, так и справа. Внутри контейнера MyAppBar используется макет Row для организации своих дочерних элементов. Средний дочерний элемент, виджет заголовка title, помечается как расширенный (Expanded), что означает, что он расширяется, чтобы заполнить все оставшееся доступное пространство, которое не было использовано другими дочерними элементами. Вы можете иметь несколько дочерних элементов Expanded и определить соотношение, в котором они используют доступное пространство, используя аргумент flex для Expanded.

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

Для получения дополнительной информации см. Layouts.

Использование Material Components

Flutter предоставляет несколько виджетов, которые помогут вам создавать приложения, которые следуют за Material Design. Приложение Material запускается с виджета MaterialApp, который создает ряд полезных виджетов в корне вашего приложения, включая Navigator, который управляет стеком виджетов, идентифицированных строками, также известными как маршруты( routes). Navigator позволяет плавно переходить между экранами вашего приложения. Использование виджета MaterialApp совершенно необязательно, но это хорошая практика

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Flutter Tutorial',
    home: TutorialHome(),
  ));
}

class TutorialHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Scaffold is a layout for the major Material Components.
    return Scaffold(
      appBar: AppBar(
        leading: IconButton(
          icon: Icon(Icons.menu),
          tooltip: 'Navigation menu',
          onPressed: null,
        ),
        title: Text('Example title'),
        actions: [
          IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
      // body is the majority of the screen.
      body: Center(
        child: Text('Hello, world!'),
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Add', // used by assistive technologies
        child: Icon(Icons.add),
        onPressed: null,
      ),
    );
  }
}

Теперь, когда код переключился с MyAppBar и MyScaffold на виджеты AppBar и Scaffold, а также с material.dart, приложение начинает смотреться более в стиле Material. Например, панель приложения имеет тень, а текст заголовка автоматически наследует правильный стиль. Также добавлена кнопка с плавающим действием.

Обратите внимание, что виджеты передаются в качестве аргументов другим виджетам. Виджет Scaffold принимает несколько различных виджетов в качестве именованных аргументов, каждый из которых размещается в макете Scaffold в соответствующем месте. Аналогично, виджет AppBar позволяет нам передавать виджеты для ведущего(leading) и действий(actions) виджета заголовка(title). Этот шаблон повторяется во всей структуре и это то, что вы можете учитывать при разработке ваших собственных виджетов.

Для получения дополнительной информации см. Material components.

[info] Примечание. Компонент Material является одним из двух комплектов, включенных в Flutter. Для более iOS-ориентированного дизайна, смотрите пакет компонентов Cupertino, который имеет свои собственные версии CupertinoApp и CupertinoNavigationBar. [/info]

Обработка жестов

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

class MyButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        print('MyButton was tapped!');
      },
      child: Container(
        height: 36.0,
        padding: const EdgeInsets.all(8.0),
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(5.0),
          color: Colors.lightGreen[500],
        ),
        child: Center(
          child: Text('Engage'),
        ),
      ),
    );
  }
}

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

Многие виджеты используют GestureDetector для предоставления необязательных обратных вызовов для других виджетов. Например, виджеты IconButton, RaisedButton и FloatingActionButton имеют обратные вызовы onPressed(), которые запускаются, когда пользователь касается виджета.

Для получения дополнительной информации см. Gestures in Flutter .

Изменение виджетов в ответ на ввод

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

Чтобы создать более сложный опыт, например, чтобы более интересным образом реагировать на ввод данных пользователем, приложения обычно имеют некоторое состояние. Flutter использует StatefulWidgets, чтобы уловить эту идею. StatefulWidgets — это специальные виджеты, которые знают, как генерировать объекты State, которые затем используются для хранения состояния. Рассмотрим этот базовый пример, используя RaisedButton, упомянутый ранее:

class Counter extends StatefulWidget {
  // This class is the configuration for the state. It holds the
  // values (in this case nothing) provided by the parent and used by the build
  // method of the State. Fields in a Widget subclass are always marked "final".

  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State {
  int _counter = 0;

  void _increment() {
    setState(() {
      // This call to setState tells the Flutter framework that
      // something has changed in this State, which causes it to rerun
      // the build method below so that the display can reflect the
      // updated values. If you change _counter without calling
      // setState(), then the build method won't be called again,
      // and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance
    // as done by the _increment method above.
    // The Flutter framework has been optimized to make rerunning
    // build methods fast, so that you can just rebuild anything that
    // needs updating rather than having to individually change
    // instances of widgets.
    return Row(
      children: [
        RaisedButton(
          onPressed: _increment,
          child: Text('Increment'),
        ),
        Text('Count: $_counter'),
      ],
    );
  }
}

[info]Класс, порожденный от StatefulWidget является конфигурацией для состояния. Он держит значения (выше в коде, в данном случае ничего), предоставляемые родителем и используемые методом build состояния State. Поля в подклассе Widget всегда помечаются как final [/info]

[info]Вызов setState() сообщает платформе Flutter, что что-то изменилось в этом состоянии, что заставляет его перезапускать метод сборки build ниже, чтобы дисплей мог отражать обновленные значения. Если вы измените _counter без вызова setState(), то метод сборки build больше не будет вызываться, и, покажется, ничего не проиcходит.[/info]

[info]Метод build перезапускается каждый раз, когда вызывается setState() (например как сделано методом _increment выше). Платформа Flutter была оптимизирована для быстрого повторного запуска метода build, так что вы можете просто перестроить все, что требует обновления, а не индивидуальное изменение экземпляра виджетов.[/info]

Вы можете спросить, почему StatefulWidget и State являются отдельными объектами. Во Flutter эти два типа объектов имеют разные жизненные циклы. Виджеты — это временные объекты, используемые для построения представления приложения в его текущем состоянии. С другой стороны, объекты состояний постоянны между вызовами в build(), что позволяет им запоминать информацию.

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

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

class CounterDisplay extends StatelessWidget {
  CounterDisplay({this.count});

  final int count;

  @override
  Widget build(BuildContext context) {
    return Text('Count: $count');
  }
}

class CounterIncrementor extends StatelessWidget {
  CounterIncrementor({this.onPressed});

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: onPressed,
      child: Text('Increment'),
    );
  }
}

class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State {
  int _counter = 0;

  void _increment() {
    setState(() {
      ++_counter;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(children: [
      CounterIncrementor(onPressed: _increment),
      CounterDisplay(count: _counter),
    ]);
  }
}

Обратите внимание на создание двух новых виджетов без сохранения состояния, четко разделяющих задачи отображения счетчика (CounterDisplay) и изменения счетчика (CounterIncrementor). Хотя чистый результат такой же, как и в предыдущем примере, разделение ответственности позволяет инкапсулировать большую сложность в отдельных виджетах, сохраняя при этом простоту в родительском элементе.

Для получения дополнительной информации см .:

Собираем все вместе

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

class Product {
  const Product({this.name});
  final String name;
}

typedef void CartChangedCallback(Product product, bool inCart);

class ShoppingListItem extends StatelessWidget {
  ShoppingListItem({Product product, this.inCart, this.onCartChanged})
      : product = product,
        super(key: ObjectKey(product));

  final Product product;
  final bool inCart;
  final CartChangedCallback onCartChanged;

  Color _getColor(BuildContext context) {
    // The theme depends on the BuildContext because different parts of the tree
    // can have different themes.  The BuildContext indicates where the build is
    // taking place and therefore which theme to use.

    return inCart ? Colors.black54 : Theme.of(context).primaryColor;
  }

  TextStyle _getTextStyle(BuildContext context) {
    if (!inCart) return null;

    return TextStyle(
      color: Colors.black54,
      decoration: TextDecoration.lineThrough,
    );
  }

  @override
  Widget build(BuildContext context) {
    return ListTile(
      onTap: () {
        onCartChanged(product, inCart);
      },
      leading: CircleAvatar(
        backgroundColor: _getColor(context),
        child: Text(product.name[0]),
      ),
      title: Text(product.name, style: _getTextStyle(context)),
    );
  }
}

Виджет ShoppingListItem следует общему шаблону для виджетов без сохранения состояния. Он хранит значения, которые он получает в своем конструкторе, в final переменных-членах, которые он затем использует во время своей функции сборки build. Например, логическое значение inCart переключается между двумя визуальными образами: один использует основной цвет из текущей темы, а другой — серый.

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

Когда родитель получает функцию обратного вызова onCartChanged, родитель обновляет свое внутреннее состояние, в результате чего родитель перестраивается и создает новый экземпляр ShoppingListItem с новым значением inCart. Хотя родительский объект создает новый экземпляр ShoppingListItem при перестройке, эта операция является плохой, поскольку платформа сравнивает вновь созданные виджеты с ранее созданными виджетами и применяет только различия к базовому RenderObject.

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

class ShoppingList extends StatefulWidget {
  ShoppingList({Key key, this.products}) : super(key: key);

  final List products;

  // The framework calls createState the first time a widget appears at a given
  // location in the tree. If the parent rebuilds and uses the same type of
  // widget (with the same key), the framework re-uses the State object
  // instead of creating a new State object.

  @override
  _ShoppingListState createState() => _ShoppingListState();
}

class _ShoppingListState extends State {
  Set _shoppingCart = Set();

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      // When a user changes what's in the cart, you need to change
      //_shoppingCart inside a setState call to trigger a rebuild.
      // The framework then calls // build, below,
      // which updates the visual appearance of the app.

      if (!inCart)
        _shoppingCart.add(product);
      else
        _shoppingCart.remove(product);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Shopping List'),
      ),
      body: ListView(
        padding: EdgeInsets.symmetric(vertical: 8.0),
        children: widget.products.map((Product product) {
          return ShoppingListItem(
            product: product,
            inCart: _shoppingCart.contains(product),
            onCartChanged: _handleCartChanged,
          );
        }).toList(),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    title: 'Shopping App',
    home: ShoppingList(
      products: [
        Product(name: 'Eggs'),
        Product(name: 'Flour'),
        Product(name: 'Chocolate chips'),
      ],
    ),
  ));
}

[info]Фреймворк вызывает createState при первом появлении виджета в заданном расположение в дереве. Если родитель перестраивает и использует тот же тип виджета (с тем же ключом), то фреймворк повторно использует объект State вместо создания нового объекта State. [/info]

[info]Когда пользователь меняет то, что находится в корзине, вам нужно изменить _shoppingCart внутри вызова setState для запуска перестроения. Фреймворк затем вызывает build, ниже, который обновляет внешний вид приложения.[/info]

Класс ShoppingList расширяет StatefulWidget, что означает, что этот виджет хранит изменяемое состояние. Когда виджет ShoppingList впервые вставляется в дерево, фреймворк вызывает функцию createState(), чтобы создать новый экземпляр _ShoppingListState для связи с этим местоположением в дереве. (Обратите внимание, что подклассы State обычно именуются с начальными подчеркиваниями, чтобы указать, что они являются частными деталями реализации.) Когда родительский объект этого виджета перестраивается, родительский объект создает новый экземпляр ShoppingList, но фреймворк повторно использует экземпляр _ShoppingListState, который уже находится в дереве, вместо того, чтобы снова вызывать createState.

Чтобы получить доступ к свойствам текущего ShoppingList, _ShoppingListState можно использовать его свойство widget. Если родитель перестраивает и создает новый ShoppingList, _ShoppingListState перестраивает с новым значением виджета widget. Если вы хотите получать уведомления при изменении свойства виджета widget, переопределите функцию didUpdateWidget(), которая передается как oldWidget, чтобы позволить вам сравнить старый виджет с текущим виджетом widget.

При обработке обратного вызова onCartChanged объект _ShoppingListState изменяет свое внутреннее состояние, добавляя или удаляя продукт из _shoppingCart. Чтобы сообщить фреймворку, что она изменила свое внутреннее состояние, она оборачивает эти вызовы в вызов setState(). Вызов setState помечает этот виджет как использованный и планирует его восстановление в следующий раз, когда вашему приложению потребуется обновить экран. Если вы забудете вызвать setState при изменении внутреннего состояния виджета, фреймворк не будет знать, что ваш виджет использован, и может не вызвать функцию build() виджета, что означает, что пользовательский интерфейс может не обновляться, чтобы отражать измененное состояние.
Управляя состоянием таким образом, вам не нужно писать отдельный код для создания и обновления дочерних виджетов. Вместо этого вы просто реализуете функцию сборки, которая обрабатывает обе ситуации.

Отвечая на события жизненного цикла виджета

После вызова createState для StatefulWidget фреймворк вставляет новый объект состояния State в дерево и затем вызывает initState для объекта состояния. Подкласс State может переопределить initState для выполнения работы, которая должна произойти только один раз. Например, переопределит initState, чтобы настроить анимацию или подписаться на сервисы платформы. Реализации initState должны начинаться с вызова super.initState.

Когда объект состояния State больше не нужен, фреймворк вызывает dispose для объекта состояния. Переопределите функцию dispose для выполнения очистки. Например, нужно переопределить dispose, чтобы отменить таймеры или отказаться от подписки на сервисы платформы. Реализации dispose обычно заканчиваются вызовом super.dispose.

Для получения дополнительной информации см. State.

Ключи

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

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

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

Для получения дополнительной информации см. Key API.

Глобальные ключи

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

Для получения дополнительной информации см. API GlobalKey.

Для данного материала использовался источник на официальном сайте Flutter Introduction to widgets.

Пожалуйста, оцените материал

WebSofter

Web - технологии