Простое приложение на flutter

Which Flutter channel should I use?

It really depends on your use case. If you are developing an app that is supposed to be released in a production environment, only use the stable channel.

If you want to try out features that are already in beta status, you can try the beta channel. Keep in mind that there can still be issues with the builds in that channel because of the ongoing stabilization period. However, compared to the stable channel, there is no extra testing so you are probably fine and encounter no issues.

If you feel like two weeks to a month is too old, you might want to use the dev channel. Automatic testing has been successful on the builds in this channel, but there can still be issues as the age of the build varies from a day to a week.

Master channel is really only meant for development purposes and not recommended to form the basis of your app.

Downloading Images With NetworkImage

In GitHub, each member has a URL for their avatar. Your next improvement is to add that avatar to the class and show the avatars in the app.

Update to add an property. It should look like this now:

class Member {
  Member(this.login, this.avatarUrl);
  final String login;
  final String avatarUrl;
}

Since is now a required parameter, Flutter complains at you in . Replace the callback in with the following updated version:

setState(() {
  final dataList = json.decode(response.body) as List;
  for (final item in dataList) {
    final login = item as String? ?? '';
    final url = item as String? ?? '';
    final member = Member(login, url);
    _members.add(member);
  }
});

The code above uses the key to look up the URL value in the map parsed from JSON, then set it to the string, which you pass on to .

Now that you have access to the URL for the avatar, add it to your . Replace with the following:

Widget _buildRow(int i) {
  return Padding(
    padding: const EdgeInsets.all(16.0),
    child: ListTile(
      title: Text('${_members.login}', style: _biggerFont),
      leading: CircleAvatar(
        backgroundColor: Colors.green,
        backgroundImage: NetworkImage(_members.avatarUrl),
      ),
    ),
  );
}

This adds a to the leading edge of your . While you’re waiting for the images to download, the background of the will be green.

Do a hot restart rather than a hot reload. You’ll see your member avatars in each row:

RenderView и RenderObject

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

Как уже говорилось ранее, всё в конечном итоге преобразуется в пиксели, которые будут отображаться на экране, и Flutter Framework преобразует Widgets, которые мы используем для разработки приложения, в визуальные блоки, которые будут отображаться на экране.

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

  • определения некоторой области экрана с точки зрения размеров, положения, геометрии, а также с точки зрения «rendered content»
  • определения зон экрана, на которые могут повлиять жесты (= касания пальцев)

Набор всех RenderObject формирует дерево, называемое Render Tree. В верхней части этого дерева (= root) мы находим RenderView.

RenderView представляет общую поверхность для объектов Render Tree и является специальной версией RenderObject.

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

Cвязь между Widget и RenderObject будет рассмотрена далее. А пока пришло время немного углубиться…

Использование объекта RelativeLayout

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

Например:

<?xml version=»1.0″ encoding=»utf-8″?>
<RelativeLayout xmlns:android=»http://schemas.android.com/apk/res/android»
    android:layout_width=»match_parent»
    android:layout_height=»match_parent»>
    <TextView
        android:id=»@+id/label»
        android:layout_width=»match_parent»
        android:layout_height=»wrap_content»
        android:text=»Type here:»/>
    <EditText
        android:id=»@+id/entry»
        android:layout_width=»match_parent»
        android:layout_height=»wrap_content»
        android:layout_below=»@id/label»/>
    <Button
        android:id=»@+id/ok»
        android:layout_width=»wrap_content»
        android:layout_height=»wrap_content»
        android:layout_below=»@id/entry»
        android:layout_alignParentRight=»true»
        android:layout_marginLeft=»10dp»
        android:text=»OK» />
    <Button
        android:layout_width=»wrap_content»
        android:layout_height=»wrap_content»
        android:layout_toLeftOf=»@id/ok»
        android:layout_alignTop=»@id/ok»
        android:text=»Cancel» />
</RelativeLayout>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

<?xml version=»1.0″encoding=»utf-8″?>

<RelativeLayout xmlnsandroid=»http://schemas.android.com/apk/res/android»

  androidlayout_width=»match_parent»

  androidlayout_height=»match_parent»>

  <TextView

    androidid=»@+id/label»

    androidlayout_width=»match_parent»

    androidlayout_height=»wrap_content»

    androidtext=»Type here:»>

  <EditText

    androidid=»@+id/entry»

    androidlayout_width=»match_parent»

    androidlayout_height=»wrap_content»

    androidlayout_below=»@id/label»>

  <Button

    androidid=»@+id/ok»

    androidlayout_width=»wrap_content»

    androidlayout_height=»wrap_content»

    androidlayout_below=»@id/entry»

    androidlayout_alignParentRight=»true»

    androidlayout_marginLeft=»10dp»

    androidtext=»OK»>

  <Button

    androidlayout_width=»wrap_content»

    androidlayout_height=»wrap_content»

    androidlayout_toLeftOf=»@id/ok»

    androidlayout_alignTop=»@id/ok»

    androidtext=»Cancel»>

<RelativeLayout>

На рис. 2 показано, как этот макет выглядит на экране QVGA.

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

Обратите внимание: несмотря на изменение размера компонентов их взаимное расположение остается прежним, так как оно задано объектом

В чем суть Flutter?

Начнем с основ. Flutter — это крайне удачный SDK, представленный Google в 2017 году. Платформа с открытым исходным кодом позволяет писать кроссплатформенные мобильные приложения, которые работают так же быстро и красиво, как и нативные — созданные конкретно под Android или iOS. 

Фреймворк очень дружелюбен к разработчикам. Разработка во Flutter ведется с помощью языка Dart, потом написанное компилируется в C++

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

Аналогичные фреймворки на сегодня ничего подобного дать не могут. Тот же React Native, который номинально конкурирует с Flutter, на самом деле ему не конкурент, ведь он работает на порядок медленнее.

Wrapping up and resources

Great job! You have created your first Flutter application. Now you should have a good idea of what Flutter is and why it’s so popular.

This is just the beginning of your Flutter journey. From different kinds of widgets to Flutter themes and much more, there’s a lot to cover before you can use Flutter like a pro, such as refreshing AlertDialog to create confirmation when users make changes in data.

If you’re looking for a streamlined approach to learning Flutter, check out our Beginning Flutter course. In this course, you’ll dive more in-depth into widgets, layouts, themes, and finally, you’ll develop your own Movie Listing App using TheMovieDB REST API.

Continue Reading about Android app development

  • Introducing Dart 2 language features: mixins, enums, and more
  • Android Tutorial: How to Develop an Android App
  • Top 7 Dart tips and tricks for cleaner Flutter apps

В чем особенности iOS-разработки, и сильно ли она отличается от работы с Android?

Начать карьеру iOS-разработчика непросто хотя бы потому, что нужные девайсы стоят недешево. На Windows писать под платформу от Apple без разных хаков и обходов нельзя, нужен MacBook или Mac mini, желательно iPhone. Не каждый студент или начинающий инженер могут себе их позволить. По этим причинам я свою карьеру начинал с Android.

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

В последнее время чаще стараются создавать приложения сразу и для Android, и для iOS, то есть кроссплатформенные. Из-за этого в тренде такие инструменты и технологии, как React Native и ставший очень популярным Flutter. Последний позволяет писать не только мобильные приложения, но и для веба и десктопа, причем, делать это на Windows.

3: Изучение кода

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

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

Теперь откройте файл lib/main.dart в редакторе кода.

Раздел MyApp

Первая часть файла определяет раздел MyApp. Этот класс расширяет StatelessWidget:

import 'package:flutter/material.dart';
void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
// ...

Сначала код импортирует пакет Material из Flutter. Этот пакет придает нашему приложению вид Material Design и открывает последующий доступ к виджетам и функциям в стиле Material.

Затем код регистрирует MyApp в качестве основного виджета нашего нового приложения; это делается с помощью метода runApp.

Внутри класса MyApp мы возвращаем метод build типа Widget, который, в свою очередь, возвращает MaterialApp. MaterialApp содержит метаданные, такие как текущие ThemeData, название приложения, домашний маршрут и т.п.

Примечание: Использовать здесь MaterialApp не обязательно. Мы также можем использовать CupertinoApp (в стиле iOS) или пользовательский стиль с помощью WidgetsApp.

Раздел MyHomePage

Следующая часть файла определяет MyHomePage. Этот класс расширяет StatefulWidget.

// ...
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyHomePageState createState() => _MyHomePageState();
}
// ...

Последняя часть этого файла определяет _MyHomePageState. Этот класс расширяет State для виджета и метода сборки. Если вы когда-либо ранее работали с React, это должно вам напомнить JSX-метод render.

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

// ...
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Следовательно, состояние _counter можно изменить с помощью setState(). Затем мы определяем метод сборки build, который создает Scaffold для нашего приложения, содержащего appBar и body.

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

Каждый раз, когда мы вызываем setState(), вместе с ним вызывается метод build виджета, таким образом обновляя представление и перерисовывая его с учетом нового состояния. Как можно видеть в нашем примере, этот вызов setState выполняется внутри FloatingActionButton через функцию onPressed: _incrementCounter.

Простота освоения (относительная)

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

Ещё одной причиной столь стремительного роста популярности Flutter в среде разработчиков (помимо высокой производительности) стало наличие подробной документации и большого числа примеров

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

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

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

Создание нового проекта

В редакторе VS Code с установленным расширением Flutter откройте командную палитру, выбрав «Обзор» — «Командная палитра», или использовав комбинацию клавиш Cmd-Shift-P на macOS, или Ctrl-Shift-P на Windows и Linux. Наберите в строке Flutter: New Project и нажмите Enter.

В качестве названия проекта введите ghflutter и нажмите Enter. Выберите директорию для сохранения проекта, и подождите, пока Flutter завершит создание рабочих файлов в VS Code. Когда все будет готово, файл main.dartоткроется в окне редактора.

В редакторе VS Code в левой части экрана отображается структура вашего проекта. Там есть папки для файлов iOS и Android, а также директория lib, в которой содержится файл main.dart с кодом, предназначенным для обеих платформ. В этом руководстве мы будем работать только с папкой lib.

Замените код в файле main.dart на приведенный ниже:

import 'package:flutter/material.dart';
void main() => runApp(GHFlutterApp());
class GHFlutterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'GHFlutter',
      home: Scaffold(
        appBar: AppBar(
          title: Text('GHFlutter'),
        ),
        body: Center(
          child: Text('GHFlutter'),
        ),
      ),
    );
  }
}

Функция main() в начале программы использует оператор => для запуска приложения GHFlutterApp. Так же называется класс GHFlutterApp, используемый программой.

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

В нашем проекте тестовый файл widget_test.dart, расположенный в папке test, не потребуется – кликните по нему правой кнопкой мыши, выберите команду «Удалить» из контекстного меню и подтвердите удаление.

Если вы работаете в macOS, запустите iOS-симулятор. Также на macOS, Linux и Windows можно использовать эмулятор Android. Можно запустить и симулятор, и эмулятор сразу и переключаться между ними, используя меню, доступное в правом нижнем углу окна редактора VS Code.

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

Обратите внимание на то, что конфигурация не определена. Нажмите на «Нет конфигурации» в выберите в выпадающем меню «Добавить конфигурацию»

При этом редактор VS Code создаст файл launch.json следующего вида:

Примечание: этот файл создается автоматически после нажатия кнопки «Добавить конфигурацию». В этом руководстве вносить изменения в код этого файла не требуется.

Теперь все готово – запустите проект нажатием клавиши F5 на клавиатуре или выбором пункта «Запуск отладки» в меню «Отладка», или нажатием зеленой кнопки «Пуск». После этого откроется консоль отладки, либо, при использовании iOS, запустится среда Xcode. В случае использования Android запустится Gradle.

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

А в случае использования эмулятора Android окно выглядит, так как показано ниже:

Ярлык slow mode («медленный режим») означает, что приложение выполняется в режиме отладки.

Выполнение приложения можно остановить нажатием кнопки «Стоп», расположенной с правой стороны верхней панели инструментов в окне редактора VS Code.

Вернуться к проекту можно нажатием иконки, расположенной в верхнем левом углу окна редактора VS Code, или выбором опции «Обозреватель» в меню «Обзор».

Flutter vs React Native: Is There a Winner of This Face-off?

No. You are confused right now. Let us give you a more detailed response. Chances are, there will be a winner in the future. As for now neither Flutter nor React Native can be called the absolute winner of the face-off since they both have their own strengths and weaknesses. React Native might be a more reliable and supported cross-platform framework that uses one of the most popular programming languages – JavaScript. However, it seems that Flutter is evolving faster, adjusting to the needs of the modern tech world, and becoming more and more popular among software developers each day.

Now that you know enough about each platform, you can make a thought-through decision and pick the one that meets your needs and expectations better.

Преимущества кроссплатформенной разработки приложений

К преимуществам кроссплатформенной разработки относят скорость разработки (она выше) и стоимость разработки (она ниже). Расскажем о преимуществах на примере фреймворка Flutter:

  • как мы уже говорили, мультиплатформенная разработка быстрее и экономичнее. Опыт показал, что экономия времени на разработку приложения составляет до 40% по сравнению с нативной;
  • скорость Flutter выше, чем у других фреймворков: частота работы — частота обновления экрана близка к 60 FPS (количество кадров в секунду);  
  • Flutter позволяет быстрее выйти на рынок с готовым или минимально жизнеспособным продуктом;
  • экономия на штате программистов — достаточно одной команды;
  • высокое качество кода благодаря детальному code review (проверка кода на предмет ошибок и неправильных архитектурных решений). Так как команда программистов Flutter работает с единой кодовой базой, все члены максимально погружены и разбираются в проекте;
  • довольно простое масштабирование разработки in-house и через подрядчиков. Можно создать библиотеку-компонент для приложений бренда;
  • неограниченность в создании функционала и интерфейса приложения;
  • низкие затраты на ликвидацию ошибок и обновления;
  • каждый программист может ознакомиться с исходным кодом Flutter и даже внести вклад в его развитие; 
  • в Flutter используется собственный высокопроизводительный движок рендеринга для рисования виджетов без зависимости от нативных элементов;
  • нет необходимости синхронизировать создание Android- и iOS-приложения и тратить дополнительные ресурсы на менеджмент разработки. В нативном подходе, как правило, разработка на одной из платформ идет быстрее; 
  • Flutter имеет только один слой написанный на C/C++, что позволяет добиться производительности очень близкой к нативной.

 Google Trends подтверждает растущий интерес к фреймворку Flutter, это видно из сравнительного графика ниже.

Данные сайта trends.google.ru

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

Adding a Theme

Your final improvement is to easily add a theme to the app by adding a theme attribute to the you created in main.dart.

Find and replace it with the following:

theme: ThemeData(primaryColor: Colors.green.shade800), 

Here, you’re using a shade of green as a Material Design color value for the theme.

Save and hot reload to see the new theme in action:

The app screenshots so far have been from the Android emulator. You can also run the final themed app in iOS Simulator:

And here’s how it looks on the Chrome web browser:

Feel free to run it as a Windows, Mac or Linux app as well. It only requires a little extra setup and to your app. On macOS, you should also give the app permission to access the internet.

Here’s the app running on macOS:

Now that’s what you call cross-platform! :]

Добавление разделителей

Для добавления разделителей в список нам нужно удвоить количество позиций, а затем вставить виджет Divider в каждую нечетную позицию. Отредактируйте метод build в GHFlutterState следующим образом:

body: ListView.builder(
  itemCount: _members.length * 2,
  itemBuilder: (BuildContext context, int position) {
    if (position.isOdd) return Divider();

    final index = position ~/ 2;
    
    return _buildRow(index);
  }),

Убедитесь, что добавили * 2 в количество позиций itemCount. При использовании разделителей определение межстрочных интервалов не требуется. В itemBuilder вы либо используете виджет-разделитель Divider (), либо вместо этого вычисляете новый индекс путем целочисленного деления и используете _buildRow() для создания элемента строки.

Используйте «горячую перезагрузку» и убедитесь, что теперь в списке есть разделители.

Для добавления межстрочных интервалов к каждой строке используйте виджет Padding внутри _buildRow():

Widget _buildRow(int i) {
  return Padding(
    padding: const EdgeInsets.all(16.0),
    child: ListTile(
      title: Text("${_members}", style: _biggerFont)
    )
  );
}

Теперь ListTile является дочерним виджетом родительского виджета Padding. Выполните горячую перезагрузку – после этого интервалы будут применены к строкам списка, но не к разделителям.

Настройка среды разработки

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

  1. Скачайте установочный пакет, соответствующий вашей операционной системе.
  2. Извлеките установочные файлы в нужную папку.
  3. Добавьте путь к директории Flutter в переменную среды PATH.
  4. Выполните команду flutter doctor, которая устанавливает фреймворк, интерпретатор Dart, а также уведомляет об отсутствии необходимых системных компонентов.
  5. Установите отсутствующие компоненты.
  6. Добавьте в свою интегрированную среду разработки плагин или расширение для Flutter.
  7. Проверьте работоспособность среды разработки запуском тестового приложения.

Инструкция на официальном сайте Flutter очень подробная и позволяет с легкостью установить этот фреймворк на любую платформу. Это руководство предполагает, что вы установили плагин для Flutter в редактор VS Code, а также все недостающие системные компоненты, указанные flutter doctor.

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

Примечание: для создания сборки и запуска приложения на iOS-устройстве или на iOS-симуляторе вам потребуется операционная система macOS с установленной средой Xcode.

Python (бэк) + Kotlin (фронт)

Довольно интересная связка, где в качестве бэка используется Python (работа с БД), и языком программирования Kotlin в качестве графической оболочки (GUI). В Pynhon есть бесплатные фреймворки для разработки мобильных приложений (Kivy или Beeware), но как «фронт» питон для программирования UI приложений не очень удобен (здесь как раз подойдет Kotlin), зато его можно задействовать в бэкенде, т.к язык располагает к жонглированию больших объёмов данных и применению машинного обучения (не зря же его используют в Data Sciense).

Котлин же как раз очень удобен в плане разработки интерфейса, благодаря user-friendly синтаксису и получивший полную поддержку  установочных пакетов Google и IDE, включая Android и SDK.

Импорт файла

Чтобы не хранить весь код в одном файле main.dart, нужно реализовать импорт кода из других пользовательских классов. Рассмотрим пример импорта, который поможет локализовать строки, обращенные к пользователю.

Создайте файл с именем strings.dart в папке lib, кликнув правой кнопкой мыши и выбрав в контекстном меню опцию «Новый файл»:

Добавьте приведенный ниже класс в только что созданный файл:

class Strings {
  static String appTitle = "GHFlutter";
}

Теперь добавьте следующую строку в начало файла main.dart:

import 'strings.dart';

Внесите изменения в свой виджет для использования нового строкового класса – для этого класс GHFlutterAppдолжен выглядеть следующим образом:

class GHFlutterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: Strings.appTitle,
      home: Scaffold(
        appBar: AppBar(
          title: Text(Strings.appTitle),
        ),
        body: Center(
          child: Text(Strings.appTitle),
        ),
      ),
    );
  }
}

Запустите приложение нажатием клавиши F5 на клавиатуре – внешних изменений не видно, но теперь программа использует строки из файла.

Рейтинг
( Пока оценок нет )
Editor
Editor/ автор статьи

Давно интересуюсь темой. Мне нравится писать о том, в чём разбираюсь.

Понравилась статья? Поделиться с друзьями:
Люкс-хост
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: