Создание безопасного Flutter приложения используя JWT

При разработке приложений крайне важно обеспечивать безопасность данных пользователей. Один из способов сделать это — использовать аутентификацию JWT (JSON Web Token). В Flutter JWT помогает убедиться, что только правильные пользователи могут получить доступ к определённым частям вашего приложения и безопасно взаимодействовать с вашим бэкендом.

В этом посте мы разберём, как работает JWT в Flutter, как его настроить, хранить токены и безопасно взаимодействовать с API. Мы охватим всё, что нужно знать для начала, без усложнений.

Что такое JWT?

JSON Web Tokens — это открытый, промышленный стандарт RFC 7519 метод безопасного представления утверждений между двумя сторонами. JSON Web Token (JWT) похож на цифровое удостоверение личности для пользователей вашего приложения. Он помогает подтвердить, кто они и на что они имеют право. JWT часто используется в приложениях для управления аутентификацией и авторизацией пользователей.

Как работает JWT:

Структура токена:
JWT состоит из трёх частей, разделённых точками (.):

  • Header (Заголовок): Указывает тип токена (JWT) и используемый алгоритм подписи (например, HMAC SHA256).
  • Payload (Полезная нагрузка): Содержит передаваемые данные или утверждения (например, ID пользователя, роль). Это основная информация.
  • Signature (Подпись): Проверяет целостность токена, объединяя закодированный заголовок, полезную нагрузку и секретный ключ.

Мы не будем углубляться в детали работы JWT — наш основной фокус будет на том, как использовать JWT в Flutter для безопасного управления сессиями пользователей и запросами к API.

Понимание аутентификации в JWT

Когда мы говорим об аутентификации с использованием JWT, мы имеем в виду, как приложения подтверждают личность пользователя. После того как пользователь входит в систему, сервер генерирует JWT, содержащий информацию, такую как ID пользователя. Этот JWT затем отправляется обратно в приложение и хранится безопасно на стороне клиента (например, в локальном хранилище или куки).

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

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

Настройка проекта Flutter:

  1. Убедитесь, что Flutter установлен правильно на вашем устройстве. Для проверки выполните в терминале:
   flutter doctor
  1. Создайте новый проект. Если вы уже работаете над проектом для JWT, перейдите в этот каталог.
   flutter create jwt_auth_flutter # если вы создаёте новый проект
  1. Добавьте следующие зависимости в pubspec.yaml:
   dependencies:
     provider: ^6.1.2
     dio: ^5.5.0+1
     get_it: ^7.7.0
     flutter_secure_storage: ^9.2.2
     jwt_decode: ^0.3.1
  • provider: Эффективно управляет состоянием приложения.
  • dio: Обрабатывает HTTP-запросы и API-вызовы.
  • get_it: Управляет внедрением зависимостей для лёгкого доступа к сервисам.
  • flutter_secure_storage: Безопасно хранит конфиденциальные данные, такие как JWT.
  • jwt_decode: Декодирует JWT для извлечения информации о пользователе и проверки токенов.
  1. Создайте страницы входа и главную страницу. Также можно добавить страницу регистрации в зависимости от доступных конечных точек API.
  2. Создайте директорию services и настройте auth_service.dart и token_service.dart.

Token_Service

Цель: Управляет JWT и связанными операциями.

Обязанности:

  • Безопасно сохраняет и извлекает JWT.
  • Декодирует токены для доступа к данным пользователя.
  • Обновляет или аннулирует токены по мере необходимости.

Эти сервисы работают вместе, чтобы обеспечить безопасную аутентификацию пользователей и правильное управление токенами в вашем приложении.

// services/token_service.dart
class TokenService {
  final FlutterSecureStorage _storage = FlutterSecureStorage();

  Future<void> saveToken(String token) async {
    await _storage.write(key: 'jwt', value: token);
  }

  Future<String?> getToken() async {
    return await _storage.read(key: 'jwt');
  }

  Future<void> deleteToken() async {
    await _storage.delete(key: 'jwt');
  }
}

Auth_Service

Цель: Управляет процессами аутентификации пользователя.

Обязанности:

  • Обрабатывает вход и выход пользователя.
  • Проверяет учетные данные пользователя с бэкендом.
  • Управляет сессиями пользователей и состоянием аутентификации.
// services/auth_service.dart
class AuthService {
  Dio _dio = Dio();
  final FlutterSecureStorage _storage = FlutterSecureStorage();

  AuthService(this._dio);

  Future<bool> login(String email, String password) async {
    try {
      final response = await _dio.post(
        '/login',
        data: jsonEncode({'email': email, 'password': password}),
      );

      if (response.statusCode == 200) {
        final token = response.data; // Убедитесь, что это соответствует ответу вашего API
        if (token != null) {
          await _storage.write(key: 'jwt', value: token);
          return true;
        }
      }
    } catch (e) {
      print('Ошибка входа: $e');
    }
    return false;
  }

  Future<bool> signup(String name, String email, String number, String password,
      String confirmPassword) async {
    try {
      final response = await _dio.post(
        '/register',
        data: jsonEncode({
          'fullName': name,
          'email': email,
          'phoneNumber': number,
          'password': password,
          'confirmPassword': confirmPassword,
        }),
      );

      if (response.statusCode == 200) {
        final token = response.data; // Убедитесь, что это соответствует ответу вашего API
        if (token != null) {
          await _storage.write(key: 'jwt', value: token);
          return true;
        }
      }
    } catch (e) {
      print('Ошибка регистрации: $e');
    }
    return false;
  }

  Future<String?> getToken() async {
    return await _storage.read(key: 'jwt');
  }

  Future<void> logout() async {
    await _storage.delete(key: 'jwt');
  }

  Map<String, dynamic> parseJwt(String token) {
    final parts = token.split('.');
    if (parts.length != 3) {
      throw Exception('Неверный токен');
    }
    final payload =
        utf8.decode(base64Url.decode(base64Url.normalize(parts[1])));
    return json.decode(payload);
  }
}

Регистрация этих сервисов с использованием GetIt в приложении:

// di/service_locator.dart
final GetIt locator = GetIt.instance;

void setupLocator() {
  locator.registerLazySingleton<Dio>(() {
    final dio = Dio(BaseOptions(baseUrl: API_URL)); 
    // API_URL может быть URL вашего сервера, например: 'http://localhost:7087/api/'
    dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) async {
        // Добавить JWT токен в заголовки запроса
        final token = await locator<TokenService>().getToken();
        if (token != null) {
          options.headers['Authorization'] = 'Bearer $token';
        }
        return handler.next(options);
      },
    ));
    return dio;
  });

  locator.registerLazySingleton<AuthService>(() => AuthService(locator<Dio>()));
  locator.registerLazySingleton<TokenService>(() => TokenService());
  locator.registerLazySingleton<FlutterSecureStorage>(() => FlutterSecureStorage());
}

Настройка auth_provider.dart

// provider/auth_provider.dart
class AuthProvider with ChangeNotifier {
  final AuthService _authService;
  bool _isAuthenticated = false;

  AuthProvider(this._authService);

  bool get isAuthenticated => _isAuthenticated;

  Future<bool> login(String email, String password) async {
    var success = await _authService.login(email, password);
    print(success);
    if (success) {
      _isAuthenticated = true;
      await _updateUser();
      notifyListeners();
    }
    return success;
  }

  Future<bool> signup(String name, String email, String number, String password, String confirmPassword) async {
    bool success = await _authService.signup(name, email, number, password, confirmPassword);
    print(success);
    if (success) {
      _isAuthenticated = true;
      await _updateUser();
      notifyListeners();
    }
    return success;
  }

  Future<void> logout() async {
    await _authService.logout();
    _isAuthenticated = false;
    _user = null;
    notifyListeners();
  }

  // Метод _updateUser() должен быть реализован для обновления данных пользователя
}

Создание представлений для входа и регистрации:

Простой код представления для входа:

// lib/views/login_view.dart
class LoginView extends StatelessWidget {
  const LoginView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final loginViewModel = Provider.of<LoginViewModel>(context);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Вход'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            TextField(
              controller: loginViewModel.emailController,
              decoration: const InputDecoration(labelText: 'Электронная почта'),
            ),
            TextField(
              controller: loginViewModel.passwordController,
              obscureText: true,
              decoration: const InputDecoration(labelText: 'Пароль'),
            ),
            const SizedBox(height: 20),
            loginViewModel.isLoading
                ? const Center(child: CircularProgressIndicator())
                : ElevatedButton(
                    onPressed: () {
                      loginViewModel.login(context);
                    },
                    child: const Text('Войти'),
                  ),
          ],
        ),
      ),
    );
  }
}

ViewModel для LoginView:

// lib/viewmodels/Login_viewmodel.dart
class LoginViewModel extends ChangeNotifier {
  final TextEditingController emailController = TextEditingController();
  final TextEditingController passwordController = TextEditingController();

  bool _isLoading = false;

  bool get isLoading => _isLoading;

  void setLoading(bool value) {
    _isLoading = value;
    notifyListeners();
  }

  Future<void> login(BuildContext context) async {
    final authProvider = Provider.of<AuthProvider>(context, listen: false);

    final email = emailController.text.trim();
    final password = passwordController.text.trim();

    if (email.isEmpty || password.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Пожалуйста, заполните все поля')),
      );
      return;
    }

    setLoading(true);

    final success = await authProvider.login(email, password);
    setLoading(false);

    if (success) {
      Navigator.pushReplacementNamed(context, Routename.home);
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Вход не удался! Пожалуйста, попробуйте снова.')),
      );
    }
  }

  @override
  void dispose() {
    emailController.dispose();
    passwordController.dispose();
    super.dispose();
  }
}

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

Представление и ViewModel для регистрации:

// lib/viewmodels/signup_viewmodel.dart
class SignupPasswordViewModel extends ChangeNotifier {
  final TextEditingController passwordController = TextEditingController();
  final TextEditingController confirmPasswordController = TextEditingController();

  String? name;
  String? email;
  String? phone;

  bool _isLoading = false;

  bool get isLoading => _isLoading;

  void setLoading(bool value) {
    _isLoading = value;
    notifyListeners();
  }

  void updateName(String? name) {
    this.name = name;
    notifyListeners();
  }

  void updateEmail(String? email) {
    this.email = email;
    notifyListeners();
  }

  void updatePhone(String? phone) {
    this.phone = phone;
    notifyListeners();
  }

  Future<void> signup(BuildContext context) async {
    if (_validatePasswords()) {
      setLoading(true);

      final authProvider = Provider.of<AuthProvider>(context, listen: false);
      final success = await authProvider.signup(
        name!,
        email!,
        phone!,
        passwordController.text.trim(),
        confirmPasswordController.text.trim(),
      );

      setLoading(false);
      print(success);
      if (success) {
        print('Регистрация успешна');
        Navigator.pushNamed(context, Routename.home); // Вы можете настроить маршрутизацию по своему методу
      } else {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Регистрация не удалась! Пожалуйста, попробуйте снова.')),
        );
      }
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Пароли не совпадают или недействительны')),
      );
    }
  }

  bool _validatePasswords() {
    final password = passwordController.text;
    final confirmPassword = confirmPasswordController.text;

    return password == confirmPassword &&
        password.isNotEmpty &&
        confirmPassword.isNotEmpty;
  }

  @override
  void dispose() {
    passwordController.dispose();
    confirmPasswordController.dispose();
    super.dispose();
  }
}
// lib/views/signup_view.dart
class SignupView extends StatelessWidget {
  const SignupView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final signupViewModel = Provider.of<SignupPasswordViewModel>(context);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Регистрация'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            TextField(
              onChanged: signupViewModel.updateName,
              decoration: const InputDecoration(labelText: 'Имя'),
            ),
            TextField(
              onChanged: signupViewModel.updateEmail,
              decoration: const InputDecoration(labelText: 'Электронная почта'),
            ),
            TextField(
              onChanged: signupViewModel.updatePhone,
              decoration: const InputDecoration(labelText: 'Телефон'),
            ),
            TextField(
              controller: signupViewModel.passwordController,
              obscureText: true,
              decoration: const InputDecoration(labelText: 'Пароль'),
            ),
            TextField(
              controller: signupViewModel.confirmPasswordController,
              obscureText: true,
              decoration: const InputDecoration(labelText: 'Подтвердите пароль'),
            ),
            const SizedBox(height: 20),
            signupViewModel.isLoading
                ? const Center(child: CircularProgressIndicator())
                : ElevatedButton(
                    onPressed: () {
                      signupViewModel.signup(context);
                    },
                    child: const Text('Зарегистрироваться'),
                  ),
          ],
        ),
      ),
    );
  }
}

И всё! Мы рассмотрели основы реализации аутентификации с использованием JWT в Flutter.

Но следующий вопрос: как использовать JWT токен в качестве ключа авторизации в заголовке API-запроса?

Ответ прост:
Мы создали token_service.dart, который выполняет операции CRUD с токеном.
Мы можем получить токен, используя метод locator<TokenService>().getToken();.

Например: вот простой API-вызов, который запрашивает данные профиля пользователя.

Future<UserModel?> loadUserProfile() async {
  try {
    final token = await locator<TokenService>().getToken();
    if (token == null) return null;

    final response = await _dio.get(
      '/profile',
      options: Options(
        headers: {'Authorization': 'Bearer $token'},
      ),
    );

    if (response.statusCode == 200) {
      final user = UserModel.fromJson(response.data);
      return user;
    }
  } catch (e) {
    print('Ошибка загрузки профиля пользователя: $e');
  }
  return null;
}
// Убедитесь, что у вас настроены модели для всех вышеупомянутых сервисов

Я постарался как можно лучше объяснить и продемонстрировать, «как использовать JWT токен для аутентификации и выполнения API-вызовов на бэкенд.»
Не стесняйтесь экспериментировать с кодом и настраивать его под нужды вашего проекта. Понимая, как работает JWT и как его реализовать, вы добавляете важный уровень безопасности в свои мобильные приложения.


Источник: https://medium.com/@areesh-ali/building-a-secure-flutter-app-with-jwt-and-apis-e22ade2b2d5f

5 Основных шаблонов Проектирования, которые должен знать каждый Flutter инженер

5 Essential Design Patterns Every Flutter Engineer Should Master

Шаблоны проектирования важны для разработчиков, так как они предлагают решения для распространённых проблем, встречающихся в разработке ПО. Для Flutter-разработчиков понимание и использование этих шаблонов может значительно повысить эффективность, масштабируемость и поддержку их приложений. В этой статье рассматриваются пять ключевых шаблонов проектирования, которые должен знать каждый Flutter-разработчик: Singleton, Provider, Builder, Observer и MVC (Model-View-Controller).

1. Паттерн Singleton

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

Во Flutter паттерн Singleton можно реализовать с помощью приватного конструктора и статического экземпляра. Вот пример:

class MySingleton {
  static final MySingleton _instance = MySingleton._internal();

  // Приватный конструктор
  MySingleton._internal();

  // Публичный фабричный метод для возврата одного и того же экземпляра
  factory MySingleton() {
    return _instance;
  }

  void someMethod() {
    print("Вызван метод экземпляра Singleton");
  }
}

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

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

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

2. Паттерн Provider

Паттерн Provider, являющийся основным в сообществе Flutter, упрощает управление состоянием, предоставляя простой способ доступа к данным и бизнес-логике из дерева виджетов. Он придерживается принципов инверсии управления (IoC) и внедрения зависимостей (DI).

Пакет Provider предлагает простой способ реализации этого паттерна. Вот базовый пример:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class Counter with ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => Counter(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Пример использования Provider')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'Вы нажали кнопку столько раз:',
              ),
              Consumer<Counter>(
                builder: (context, counter, child) => Text(
                  '${counter.count}',
                  style: Theme.of(context).textTheme.headline4,
                ),
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => context.read<Counter>().increment(),
          tooltip: 'Увеличить',
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

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

  • Управление состоянием: Управление и передача состояния в разных частях приложения.
  • Внедрение зависимостей: Легкое внедрение зависимостей в дерево виджетов.

3. Паттерн Builder

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


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

import 'package:flutter/material.dart';

class MyCustomWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Builder(
      builder: (BuildContext context) {
        return Text(
          'Привет, Flutter!',
          style: Theme.of(context).textTheme.headline4,
        );
      },
    );
  }
}

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

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

4. Паттерн Observer

Паттерн Observer определяет зависимость «один ко многим» между объектами, так что при изменении состояния одного объекта все его зависимые объекты уведомляются и автоматически обновляются. Этот паттерн важен для реализации систем обработки событий.

В Flutter этот паттерн широко используется в решениях для управления состоянием, таких как ChangeNotifier и ValueNotifier.

import 'package:flutter/material.dart';

class Counter extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

void main() {
  final counter = Counter();

  counter.addListener(() {
    print('Счетчик изменился: ${counter.count}');
  });

  counter.increment();
}

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

  • Обработка событий: Уведомление и обновление слушателей при изменении состояния.
  • Реактивное программирование: Реализация реактивных потоков данных.

5. Паттерн MVC (Model-View-Controller)

Паттерн MVC разделяет приложение на три взаимосвязанных компонента: Model (логика данных), View (логика интерфейса) и Controller (бизнес-логика). Такое разделение помогает управлять сложностью приложения путем декомпозиции кода на управляемые части.
Хотя Flutter не навязывает определенный архитектурный шаблон, MVC можно реализовать вручную. Вот простой пример:

Model: Определяет структуру данных и бизнес-логику.

class CounterModel {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
  }
}

View: Представляет пользовательский интерфейс.

import 'package:flutter/material.dart';

class CounterView extends StatelessWidget {
  final int count;
  final VoidCallback onIncrement;

  CounterView({required this.count, required this.onIncrement});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Пример MVC')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Вы нажали кнопку столько раз:'),
            Text(
              '$count',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: onIncrement,
        tooltip: 'Увеличить',
        child: Icon(Icons.add),
      ),
    );
  }
}

Controller: Связывает Model и View, обрабатывает ввод пользователя и обновляет Model.

import 'package:flutter/material.dart';

class CounterController {
  final CounterModel _model;

  CounterController(this._model);

  int get count => _model.count;

  void increment() {
    _model.increment();
  }
}

void main() {
  final model = CounterModel();
  final controller = CounterController(model);

  runApp(MyApp(controller: controller));
}

class MyApp extends StatelessWidget {
  final CounterController controller;

  MyApp({required this.controller});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: CounterView(
        count: controller.count,
        onIncrement: controller.increment,
      ),
    );
  }
}

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

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

Заключение
Шаблоны проектирования — это важные инструменты в арсенале Flutter-разработчика. Освоив паттерны Singleton, Provider, Builder, Observer и MVC, вы сможете создавать надёжные, масштабируемые и поддерживаемые приложения. Эти паттерны не только решают распространённые проблемы, но и способствуют применению лучших практик и принципов проектирования в разработке ПО. Независимо от того, управляете ли вы состоянием, создаете ли сложные объекты, обрабатываете ли события или организуете архитектуру приложения, эти паттерны помогут вам найти эффективные и действенные решения.

Источник: https://medium.com/@ayhamxv12instagram/5-essential-design-patterns-every-flutter-engineer-should-master-09206ec077ee

Переход на Flutter 3.22 для Web

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

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

Процесс инициализации веб-приложения
Процесс инициализации веб-приложения Flutter упрощен, но поддается настройке. Он вращается вокруг сценария flutter_bootstrap.js, который генерируется в процессе сборки и управляет последовательностью запуска приложения.

Сценарий flutter_bootstrap.js
Когда вы собираете свое веб-приложение Flutter с помощью команды flutter build web, в каталоге build/web создается сценарий flutter_bootstrap.js. Этот сценарий необходим для инициализации и запуска вашего приложения.

Чтобы включить этот сценарий в ваш index.html, добавьте следующий код:

<html>
  <body>
    <script src="flutter_bootstrap.js" async></script>
  </body>
</html>

Кроме того, вы можете встроить сценарий, вставив токен {{flutter_bootstrap_js}}:

<html>
  <body>
    <script>
      {{flutter_bootstrap_js}}
    </script>
  </body>
</html>

Настройка инициализации
По умолчанию команда flutter build web генерирует базовый сценарий flutter_bootstrap.js. Однако вам может понадобиться настроить эту инициализацию по различным причинам, таким как установка пользовательской конфигурации или изменение настроек сервис-воркера.

Для настройки процесса инициализации создайте собственный файл flutter_bootstrap.js в каталоге web вашего проекта. Этот пользовательский сценарий заменит стандартный, сгенерированный в процессе сборки.

Токены для настройки
В своем пользовательском файле flutter_bootstrap.js вы можете использовать несколько токенов:

  • {{flutter_js}}: Делает объект _flutter.loader доступным.
  • {{flutter_build_config}}: Устанавливает метаданные для FlutterLoader.
  • {{flutter_service_worker_version}}: Представляет версию сборки сервис-воркера.

Написание пользовательского flutter_bootstrap.js
Пользовательский сценарий flutter_bootstrap.js должен содержать три основных компонента:

  • Токен {{flutter_js}}.
  • Токен {{flutter_build_config}}.
  • Вызов _flutter.loader.load() для запуска приложения.

Базовый пример выглядит так:

{{flutter_js}}
{{flutter_build_config}}

_flutter.loader.load();

API _flutter.loader.load()
Функция _flutter.loader.load() может принимать необязательные аргументы для более индивидуализированной инициализации:

  • config: Объект конфигурации для вашего приложения.
  • onEntrypointLoaded: Функция обратного вызова, вызываемая при готовности движка к инициализации.
  • serviceWorkerSettings: Конфигурация для сервис-воркера.

Настройки конфигурации
Объект config может включать различные поля:

  • assetBase: Базовый URL для каталога с ресурсами.
  • canvasKitBaseUrl: Базовый URL для загрузки canvaskit.wasm.
  • canvasKitVariant: Указывает вариант CanvasKit (auto, full, или chromium).
  • canvasKitForceCpuOnly: Принудительное использование только CPU-рендеринга, если установлено значение true.
  • canvasKitMaximumSurfaces: Максимальное количество наложенных поверхностей.
  • debugShowSemanticNodes: Отображение дерева семантики на экране для отладки.
  • hostElement: HTML-элемент, в котором Flutter отрисовывает приложение.
  • renderer: Указывает веб-рендерер (canvaskit или html).

Настройка конфигурации Flutter на основе параметров URL-запроса
Вот пример, который настраивает конфигурацию на основе параметров URL-запроса:

{{flutter_js}}
{{flutter_build_config}}

const searchParams = new URLSearchParams(window.location.search);
const forceCanvaskit = searchParams.get('force_canvaskit') === 'true';
const userConfig = forceCanvaskit ? {'renderer': 'canvaskit'} : {};
_flutter.loader.load({
  config: userConfig,
  serviceWorkerSettings: {
    serviceWorkerVersion: {{flutter_service_worker_version}},
  },
});

Использование обратного вызова onEntrypointLoaded
Функция onEntrypointLoaded позволяет вам выполнять пользовательскую логику на различных этапах инициализации:

{{flutter_js}}
{{flutter_build_config}}

const loading = document.createElement('div');
document.body.appendChild(loading);
loading.textContent = "Загрузка точки входа...";
_flutter.loader.load({
  onEntrypointLoaded: async function(engineInitializer) {
    loading.textContent = "Инициализация движка...";
    const appRunner = await engineInitializer.initializeEngine();
    loading.textContent = "Запуск приложения...";
    await appRunner.runApp();
  }
});

Обновление старого проекта
Если вы обновляете проект с версии Flutter 3.21 или более ранней, выполните следующие шаги для создания нового файла index.html:

  1. Удалите существующие файлы из вашего каталога /web.
  2. Выполните следующую команду в каталоге вашего проекта:
   flutter create . --platforms=web

Заключение
Переход на Flutter 3.22 для веб-разработки предлагает улучшенную гибкость и возможность настройки инициализации приложений. Понимая и используя сценарий flutter_bootstrap.js, вы можете точно настроить процесс запуска в соответствии с конкретными потребностями вашего приложения. Независимо от того, настраиваете ли вы конфигурацию или обновляете старый проект, это руководство предоставляет подробные шаги для начала работы.

Источник: https://yawarosman.medium.com/switching-to-flutter-3-22-for-web-7e405e9b56f6

Как хранить ключи API во Flutter: —dart-define vs .env

Если ваше приложение на Flutter использует API стороннего сервиса, для которого требуется API-ключ, то где его нужно хранить?

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

  • API-ключ должен храниться на вашем защищенном сервере (и никогда на клиенте).
  • Он никогда не должен передаваться обратно на клиент (чтобы предотвратить атаки типа «человек посередине»).
  • Клиент должен общаться только с вашим сервером, который выступает в качестве прокси для стороннего API, который вы планируете использовать.

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

Однако не все ключи одинаковы: некоторые ключи могут быть доступны клиенту, тогда как другие должны храниться в секрете на сервере (документация Stripe хорошо объясняет это).

На самом деле, один из ответов на StackOverflow хорошо подытоживает:

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

Без сомнения, безопасность мобильного приложения — это обширная тема (на эту тему можно написать целые книги).

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

Контекст

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

И когда дело доходит до API-ключей и безопасности, вам следует избегать двух основных ошибок:

  1. Загрузка секретного ключа в систему контроля версий, делая его видимым для всех в Интернете 🤯.
  2. Отказ от шифрования API-ключей, что упрощает задачу злоумышленникам по обратной разработке вашего приложения и извлечению ключей 🛠.

В этом руководстве мы узнаем, как избежать этих ошибок.

Что рассматривается в этом руководстве

Мы рассмотрим три различных метода хранения API-ключей на клиенте (вашем приложении на Flutter), а также их компромиссы:

  1. Жесткое кодирование ключей в файле .dart.
  2. Передача ключей в качестве аргументов командной строки с использованием --dart-define или --dart-define-from-file.
  3. Загрузка ключей из файла .env с помощью пакета ENVied.

По ходу дела мы будем помнить о следующих правилах:

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

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

⚠️ Эти методы не являются непогрешимыми. Если у вас есть API-ключ, который вы не можете позволить себе потерять, храните его на сервере (а если вы используете Firebase, посмотрите мой гайд о защите API-ключей с помощью функций второго поколения в облаке). Безопасная клиент-серверная коммуникация включает в себя множество аспектов, выходящих за рамки данной статьи (см. ссылки внизу для получения дополнительной информации).

Готовы? Давайте начнем! 👇

1. Жесткое кодирование ключа в файле Dart

Простой и эффективный способ сохранить наш API-ключ — сохранить его в Dart-файле, например, так:

// api_key.dart
final tmdbApiKey = 'a1b2c33d4e5f6g7h8i9jakblc';

Чтобы убедиться, что ключ не добавлен в git, мы можем добавить файл .gitignore в ту же папку с таким содержимым:

# Скрыть ключ от контроля версий
api_key.dart

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

Пример кода, использующего пакет dio для получения данных из API TMDB:

import 'api_key.dart'; // импортируем здесь
import 'package:dio/dio.dart';

Future<TMDBMoviesResponse> fetchMovies() async {
  final url = Uri(
    scheme: 'https',
    host: 'api.themoviedb.org',
    path: '3/movie/now_playing',
    queryParameters: {
      'api_key': tmdbApiKey, // читаем здесь
      'include_adult': 'false',
      'page': '$page',
    },
  ).toString();
  final response = await Dio().get(url);
  return TMDBMoviesResponse.fromJson(response.data);
}

Этот подход очень прост, но у него есть несколько недостатков:

  • Трудно управлять различными API-ключами для разных сред/конфигураций.
  • Ключ хранится в виде открытого текста в файле api_key.dart, что облегчает работу злоумышленнику.

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

Давайте рассмотрим второй вариант. 👇

2. Передача ключа с помощью —dart-define

Альтернативный подход — передать API-ключ с помощью флага --dart-define во время компиляции.

Это означает, что мы можем запустить приложение вот так:

flutter run --dart-define TMDB_KEY=a1b2c33d4e5f6g7h8i9jakblc

Затем в коде Dart можно сделать:

const tmdbApiKey = String.fromEnvironment('TMDB_KEY');
if (tmdbApiKey.isEmpty) {
  throw AssertionError('TMDB_KEY is not set');
}
// TODO: использовать API-ключ

Основное преимущество использования --dart-define заключается в том, что мы больше не храним чувствительные ключи в исходном коде.

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

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

flutter run \
  --dart-define TMDB_KEY=a1b2c33d4e5f6g7h8i9jakblc \
  --dart-define STRIPE_PUBLISHABLE_KEY=pk_test_aposdjpa309u2n230ibt23908g \
  --dart-define SENTRY_KEY=https://aoifhboisd934y2fhfe@a093qhq4.ingest.sentry.io/2130923

Есть ли лучший способ?

Новинка в Flutter 3.7: использование —dart-define-from-file

С Flutter 3.7 мы можем хранить все API-ключи в JSON-файле и передавать его в новый флаг --dart-define-from-file из командной строки.

Таким образом, мы можем сделать:

flutter run --dart-define-from-file=api-keys.json

Затем мы можем добавить все ключи в файл api-keys.json (который должен быть .gitignored):

{
  "TMDB_KEY": "a1b2c33d4e5f6g7h8i9jakblc",
  "STRIPE_PUBLISHABLE_KEY": "pk_test_aposdjpa309u2n230ibt23908g",
  "SENTRY_KEY": "https://aoifhboisd934y2fhfe@a093qhq4.ingest.sentry.io/2130923"
}

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

Если вы используете IntelliJ или Android Studio, вы можете использовать конфигурации запуска/отладки для достижения того же результата.

3. Загрузка ключа из .env файла

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

Чтобы использовать это с Flutter, мы можем добавить .env файл в корень проекта.

Пример .env файла:

TMDB_KEY=a1b2c33d4e5f6g7h8i9jakblc
# добавьте другие ключи при необходимости

Заключение

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

Источник: https://codewithandrea.com/articles/flutter-api-keys-dart-define-env-files/

Отложенная загрузка во Flutter: преимущества, недостатки и реализация.

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

Зачем существует отложенная загрузка?

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

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

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

Недостатки отложенной загрузки

  • Сложность: Реализация отложенной загрузки может добавить сложности в кодовую базу.
  • Потенциальные задержки: При неправильном управлении может привести к заметным задержкам при загрузке отложенных ресурсов во время выполнения.
  • Управление зависимостями: Требует тщательного управления зависимостями и понимания того, как они загружаются.

Реализация отложенной загрузки в Flutter

Отложенная загрузка в Flutter обычно реализуется с помощью ключевого слова deferred as в операторе импорта Dart. Это позволяет загружать библиотеки лениво.

Основная реализация

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

  1. Определение отложенной библиотеки:
// В отдельном файле, например, heavy_library.dart
class HeavyLibrary {
  void performHeavyTask() {
    // Реализация тяжелой задачи
  }
}
  1. Импорт библиотеки с отложенной загрузкой:
import 'heavy_library.dart' deferred as heavyLib;

Future<void> loadHeavyLibrary() async {
  await heavyLib.loadLibrary();
  heavyLib.HeavyLibrary().performHeavyTask();
}

Сценарии использования

  • Большие ассеты: Отложенная загрузка для изображений высокого разрешения или видео, которые не требуются немедленно.
  • Модули функций: В приложениях с несколькими функциями загружайте функции по требованию. Например, в приложении для электронной коммерции можно отложить загрузку модуля отзывов о товаре.
  • Код, специфичный для платформы: Откладывайте загрузку кода, специфичного для определённой платформы, который не требуется на других платформах.

Управление зависимостями

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

Лучшие практики

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

Источник: https://yawarosman.medium.com/understanding-deferred-loading-in-flutter-benefits-drawbacks-and-implementation-07d15e91c65e

Как использовать flavor во Flutter и почему это так важно

flautter flavors

В современной разработке приложений управление различными версиями приложения необходимо для их целевой направленности на разные среды, группы пользователей или рыночные сегменты. Эти версии, называемые «флейворами» (flavors), позволяют разработчикам легко настраивать поведение и внешний вид приложения, поддерживать его, включать или отключать функции для различных версий и конфигураций без необходимости поддерживать несколько кодовых баз.

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

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

Использование флейворов во Flutter с помощью flutter_flavorizr

flutter_flavorizr упрощает создание и управление множественными флейворами в приложениях на Flutter. Он автоматизирует процесс конфигурации как для Android, так и для iOS, помогая разработчикам поддерживать единообразие конфигурации и минимизировать время, затрачиваемое на ручную настройку. Как это сделать? Итак…

Шаг 1 — Установка flutter_flavorizr

Добавьте flutter_flavorizr в ваш файл pubspec.yaml в разделе dev_dependencies.

Шаг 2 — Настройка флейворов

Создайте новый файл с именем flavorizr.yaml в корне проекта и определите имена флейворов, затем запустите команду

flutter pub run flutter_flavorizr

для генерации необходимых файлов и конфигураций.

flavors:
  development:
    app:
      name: "MyApp DEV"
    android:
      applicationId: "com.example.myapp.dev"
    ios:
      bundleId: "com.example.myapp.dev"
    macos:
      bundleId: "com.example.myapp.dev"
  production:
    app:
      name: "MyApp"
    android:
      applicationId: "com.example.myapp"
    ios:
      bundleId: "com.example.myapp"
    macos:
      bundleId: "com.example.myapp"

Эта команда создаёт соответствующие файлы и каталоги для каждого флейвора, включая отдельные файлы main.dart и конфигурационные файлы.

Шаг 3 — Доступ к конфигурациям флейворов в коде

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

import 'package: flutter/material.dart';
import 'package:flutter_flavorizr/flutter_flavorizr.dart';

void main {
    runApp (MyApp) ;
}

class MyApp extends StatelessWidget {
  @override
  Widget build (BuildContext context) {
    final flavor = FlavorConfig.instance.name;

    return MaterialApp( 
      title: 'Flutter Flavors - $flavor',
      home: HomeScreen,
    );
  }
}

Шаг 4 — Сборка и запуск флейворов

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

flutter run --flavor development -t lib/main_development.dart
flutter run --flavor production -t lib/main_production.dart

И всё готово!

Почему это важно?

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

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

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

Источник: https://bilalrehman08.medium.com/how-to-use-flavor-in-flutter-and-why-its-important-for-you-0f53c71b59ff

Реализация изолятов: spawn() и run()

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

Также я создал репозиторий на GitHub с кодом этого проекта, с которым вы можете ознакомиться здесь.

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

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

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

В этой статье мы будем вычислять сумму первых 1 000 000 чисел — это довольно ресурсоемкая задача. Если вы попробуете сделать это напрямую или с использованием async-await, то заметите, что интерфейс приложения начинает «лагать». Здесь же мы рассмотрим две разные реализации изолятов. Вы можете выбрать ту, которая лучше подходит под ваши требования.

Метод Isolate.run() [Короткоживущие изоляты]

Самый простой способ перенести процесс в изолят в Flutter — использовать метод Isolate.run. Этот метод создает изолят, передает ему функцию обратного вызова для начала вычислений, возвращает значение вычислений и затем завершает изолят после окончания вычислений. Все это происходит параллельно с основным изолятом и не блокирует его.

Метод Isolate.run требует одного аргумента — функции обратного вызова, которая будет запущена в новом изоляте. Подпись функции обратного вызова должна содержать ровно один обязательный неименованный аргумент. Когда вычисление завершается, функция возвращает значение обратно в основной изолят и завершает работу созданного изолята.

Пример: мы вычисляем сумму первых 10 000 000 чисел и возвращаем её. Не забудьте, что функция должна быть объявлена вне всех классов.

Future<double> complexTask3() async {
  const double iteration = 1000000000;
  final double result = await Isolate.run<double>(() {
    double total = 0.0;
    for (var i = 0; i < iteration; i++) {
      total += i;
    }
    return total;
  });
  return result;
}

Метод spawn() [Долгоживущие изоляты]

Короткоживущие изоляты удобны в использовании, но они имеют накладные расходы на создание новых изолятов и копирование объектов из одного изолята в другой. Если вы выполняете одно и то же вычисление с использованием Isolate.run несколько раз, возможно, вам будет выгоднее использовать изоляты, которые не завершаются сразу.

Когда вы используете метод Isolate.run, новый изолят завершает работу сразу после возвращения единственного сообщения в основной изолят. Иногда вам нужны изоляты, которые будут работать долго и смогут передавать друг другу несколько сообщений с течением времени. В Dart вы можете достичь этого с помощью API изолятов и портов. Такие долгоживущие изоляты называют фоновые рабочие процессы (background workers).

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

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

complexTask(SendPort sendPort) {
  var total = 0.0;
  for (var i = 0; i < 1000000000; i++) {
    total += i;
  }
  sendPort.send(total);
}

Здесь мы будем использовать функцию complexTask при нажатии на кнопку.

ElevatedButton(
  onPressed: () async {
    final receivePort = ReceivePort();
    await Isolate.spawn(complexTask, receivePort.sendPort);
    receivePort.listen((total) {
      debugPrint('Result 2: $total');
    });
  },
  child: const Text('Task 1'),
)

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

Предположим, вы хотите передать некоторые данные в изолят, что тогда? Это тоже можно сделать.

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

ElevatedButton(
  onPressed: () async {
    final receivePort = ReceivePort();
    await Isolate.spawn(complexTask3, (iteration: 1000000000, sendPort: receivePort.sendPort));
    receivePort.listen((total) {
      debugPrint('Result 2: $total');
    });
  },
  child: const Text('Task 2'),
),
complexTask3(({int iteration, SendPort sendPort}) data) {
  var total = 0.0;
  for (var i = 0; i < data.iteration; i++) {
    total += i;
  }
  data.sendPort.send(total);
}

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

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

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

Источник: https://medium.com/codex/isolate-implementation-explained-spawn-and-run-2cd8462c91ba


Flutter: async или isolate. Параллелизм

Многие не понимают разницу между async и isolate (параллелизмом и конкурентностью). В этой статье мы разберём эти понятия, а в следующей покажу, как реализовать изоляты в Dart.

ASYNC
Сначала разберём async, так как большинство из нас уже использовало этот подход хотя бы раз.

void readData() async {
  var url = "www.example.com";
  var content = await http.get(url); // Асинхронный вызов
  var data = parsingData(content); // Здесь будем использовать изолят, так как операция тяжелая
}

Этот код выполняется в обычном режиме до вызова загрузки файла, после чего программа ждёт, пока файл будет загружен. Пока она ждёт, другие части программы могут продолжать работать. Однако, обратите внимание, что в любой момент времени работает только одна часть приложения — ведь у нас всего один поток, один процессор. Асинхронность не делает ваше приложение многопоточным автоматически. После загрузки файла метод продолжит выполнение. Это и есть async. Когда задача (future) завершается, выполнение продолжается с следующей строки кода. В большинстве случаев async достаточно.

В примере выше наш код будет ждать на второй строке (await http.get(url)) до тех пор, пока данные не будут получены с интернета, и после этого мы начнем их обработку.

Теперь представьте, что файл очень большой, и его обработка требует много ресурсов процессора. Это может вызвать задержки в работе приложения? Многие могут подумать, что нет, так как вычисления происходят в асинхронном методе. Но это не так. Почему? Даже если метод асинхронный, в нём всё равно может происходить много синхронных операций. Эта обработка данных (parsingData) выполняется синхронно. Она занимает процессор, и другие части вашего приложения не смогут выполняться. Интерфейс пользователя не обновится, пока тяжелая работа не будет завершена, и пользователь увидит задержки и «подтормаживания».

Решение? Многопоточность с помощью изолятов!

ИЗОЛЯТЫ
Dart позволяет выполнять код параллельно с помощью изолятов. Изолят запускает другой процесс Dart, фактически, на другом потоке, на другом процессоре. Эти два изолята полностью изолированы друг от друга. Один не блокирует другой. Таким образом, ваше приложение может выполнять тяжелые вычисления и одновременно оставаться отзывчивым для пользователя. В этой статье я не буду углубляться в реализацию изолятов, так как это немного сложнее, и цель статьи — разъяснить разницу между async и изолятами.

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

Когда использовать изоляты?

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

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

Подведем итоги:
Async позволяет ждать выполнения других операций без блокировки текущего потока. Async не создаёт отдельный поток; Dart просто переключается между разными частями программы по мере необходимости. Но если необходимо выполнение параллельных задач, вам нужны изоляты. Они сложнее в настройке, но позволяют решать задачи, требующие интенсивных вычислений, без задержек в работе пользовательского интерфейса.

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

Источник: https://medium.com/@AryanBeast/async-vs-isolate-in-flutter-parallelism-in-flutter-ae3954fb5d4c

Освоение циклов в Dart: while и for

Циклы — это фундаментальные конструкции в программировании, которые позволяют выполнять блок кода многократно, основываясь на условии или наборе значений. В Dart основными конструкциями для организации циклов являются while и for. В этой статье мы подробно рассмотрим эти конструкции и дадим вам знания, необходимые для их эффективного использования в программах на Dart.

Циклы while

Цикл while выполняет блок кода до тех пор, пока определённое условие истинно. Это полезно, когда количество итераций заранее неизвестно и зависит от условия.

Синтаксис

while (condition) {
// Код выполняется пока condition истинно
}

Пример использования

void main() {
    int counter = 0;
    
    while (counter < 5) {
      print( 'Counter: $counter');
      counter++;
  }
}

В этом примере программа выводит значение переменной counter от 0 до 4. Цикл продолжает выполняться, пока counter меньше 5.

Бесконечный цикл

Будьте осторожны с циклами while, чтобы избежать создания бесконечных циклов, где условие никогда не становится ложным.

void main() {
    int counter = 0;
    
    while (true) {
      print( 'Counter: $counter');
      counter++;
      if (counter >= 5) {
       break;
      }
   }
}

В этом примере цикл while (true) управляется оператором break, который завершает выполнение цикла, когда counter достигает 5.

Циклы do-while

Цикл do-while похож на while, но гарантирует, что блок кода будет выполнен хотя бы один раз, так как условие проверяется после выполнения блока.

Синтаксис

do {
// Исполняемый код
} while (condition);
void main() {
  int counter = 0;

  do {
    print( 'Counter: $counter');
    counter++;
  } while (counter < 5);
}

В этом примере программа выводит значение переменной counter от 0 до 4. Цикл гарантирует, что блок кода выполнится хотя бы один раз перед проверкой условия.

Циклы for

Цикл for обычно используется, когда количество итераций известно заранее. Он состоит из инициализации, условия и операции инкремента/декремента.

for (initialization; condition; increment/decrement) {
  // Исполняемый код
}
void main () {
  for (int i = 0; i < 5; i++) {
    print('i: $i');
  }
}

В этом примере программа выводит значение переменной i от 0 до 4. Цикл инициализирует i значением 0, проверяет условие i < 5 и увеличивает i на 1 в каждой итерации.

Вложенные циклы for

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

void main) {
  for (int i = 0; i < 3; i++) {
    for(intj=0; j<3; j++) {
      print('i: $i, j: $j');
    }
  }
}

В этом примере внешний цикл выполняется 3 раза, и для каждой итерации внешнего цикла внутренний цикл выполняется 3 раза, что в сумме дает 9 итераций и выводит значения i и j.

Циклы for-in

Цикл for-in используется для обхода элементов коллекции, такой как список или множество.

void main ( ) {
  List<String> fruits = ['Apple', 'Banana', 'Cherry'];
    
  for (var fruit in fruits) {
    print(fruit);
  }
}

В этом примере программа перебирает список fruits и выводит в консоль каждый фрукт.

Метод forEach

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

void main ( ) {
  List<String> fruits = ['Apple', 'Banana', 'Cherry'];
  fruits. forEach((fruit) => print(fruit));
}

В этом примере метод forEach перебирает список fruits и выполняет заданную функцию для каждого элемента.

Лучшие практики при использовании циклов

  1. Избегайте бесконечных циклов: Убедитесь, что в ваших циклах есть условие завершения, чтобы предотвратить бесконечные циклы.
  2. Используйте описательные имена переменных: Применяйте понятные имена для переменных-счётчиков и элементов, чтобы улучшить читаемость кода.
  3. Сохраняйте циклы простыми: Избегайте сложной логики внутри циклов. При необходимости рефакторьте код в функции.
  4. Используйте break и continue разумно: Используйте break для досрочного выхода из цикла и continue для пропуска текущей итерации. Применяйте их с осторожностью, чтобы избежать путаницы в коде.
  5. Предпочитайте for-in и forEach для коллекций: При обходе коллекций используйте for-in или forEach для более чистого и читаемого кода.

Заключение

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

Источник: https://azimdev.medium.com/mastering-loops-in-dart-while-and-for-4d5e48aa9a03

Подсветка кода C++, python, C#, Dart и других в quicklook MacOs

Syntax Highlight

Итак я всё ещё на яблочной продукции и в версии Mac Os Sonoma перестал работать прошлый описанный мной способ подсветки в quicklook для файлов C++, python, C#, Dart, потому пришлось найти новый причем еще более легкий и удобный.

Берем устанавливаем с помощью brew приложение командой

brew install --cask --no-quarantine syntax-highlight

Запустить Syntax Highlight впервые, после чего проследовать в Системные настройки, в поиске там вбиваем Extensions и в разделе Quick Look Extension ставим галочку если она не стоит на Syntax Highlight

Подсветка кода C++, python, C#, Dart и других в quicklook MacOs

После чего в самой программе настраиваем как и что вам нравится. У меня пока я не выбрал Render Engine HTML не применялись изменения в QuickLook

Подсветка кода C++, python, C#, Dart и других в quicklook MacOs

Вот собственно и всё