← Назад к вопросам

Что такое реактивное программирование в контексте Flutter?

2.7 Senior🔥 111 комментариев
#Архитектура Flutter#Асинхронность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Реактивное программирование в Flutter

Реактивное программирование (Reactive Programming) — это парадигма, основанная на потоках данных (streams) и распространении изменений. В Flutter это реализуется через пакет Rx (RxDart) и встроенные Stream API.

Основные концепции

1. Поток данных (Stream)

Stream — это последовательность событий во времени:

// Простой Stream, который emits значения
Stream<int> numberStream = Stream.fromIterable([1, 2, 3, 4, 5]);

// Слушаем Stream
numberStream.listen((number) {
  print('Received: $number');
});

// Output:
// Received: 1
// Received: 2
// Received: 3
// Received: 4
// Received: 5

2. Реактивные переменные (BehaviorSubject, PublishSubject)

import 'package:rxdart/rxdart.dart';

// BehaviorSubject всегда помнит последнее значение
final counterSubject = BehaviorSubject<int>(seedValue: 0);

// Emitting значения
counterSubject.add(1);
counterSubject.add(2);
counterSubject.add(3);

// Слушаем
counterSubject.listen((count) => print('Count: $count'));

// Output:
// Count: 0 (seed value)
// Count: 1
// Count: 2
// Count: 3

BLoC паттерн (Business Logic Component)

BLoC — это архитектурный паттерн, который отделяет business logic от UI через Streams:

// 1. Определяем Events (действия пользователя)
abstract class CounterEvent {}

class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}

// 2. Определяем States (состояния)
abstract class CounterState {}

class CounterInitial extends CounterState {
  final int count = 0;
}

class CounterUpdated extends CounterState {
  final int count;
  CounterUpdated(this.count);
}

// 3. Создаём BLoC
class CounterBloc {
  int _count = 0;
  
  // Inputs
  final _eventController = StreamController<CounterEvent>();
  Stream<CounterEvent> get events => _eventController.stream;
  
  // Outputs
  final _stateController = BehaviorSubject<CounterState>(
    seedValue: CounterInitial(),
  );
  Stream<CounterState> get states => _stateController.stream;
  
  CounterBloc() {
    events.listen(_handleEvent);
  }
  
  void _handleEvent(CounterEvent event) {
    if (event is IncrementEvent) {
      _count++;
    } else if (event is DecrementEvent) {
      _count--;
    }
    _stateController.add(CounterUpdated(_count));
  }
  
  void increment() => _eventController.add(IncrementEvent());
  void decrement() => _eventController.add(DecrementEvent());
  
  void dispose() {
    _eventController.close();
    _stateController.close();
  }
}

// 4. Используем в UI
class CounterPage extends StatefulWidget {
  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  late CounterBloc _bloc;
  
  @override
  void initState() {
    super.initState();
    _bloc = CounterBloc();
  }
  
  @override
  void dispose() {
    _bloc.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: StreamBuilder<CounterState>(
        stream: _bloc.states,
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return CircularProgressIndicator();
          }
          
          final state = snapshot.data as CounterUpdated;
          return Center(
            child: Text(
              'Count: ${state.count}',
              style: TextStyle(fontSize: 32),
            ),
          );
        },
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: _bloc.increment,
            child: Icon(Icons.add),
          ),
          SizedBox(height: 16),
          FloatingActionButton(
            onPressed: _bloc.decrement,
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

StreamBuilder vs FutureBuilder

// FutureBuilder — для единовременной операции
FutureBuilder<User>(
  future: fetchUser(),
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return Text(snapshot.data!.name);
    }
    return CircularProgressIndicator();
  },
)

// StreamBuilder — для потока событий
StreamBuilder<int>(
  stream: timerStream,
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return Text('Seconds: ${snapshot.data}');
    }
    return CircularProgressIndicator();
  },
)

RxDart операторы (Reactive Extensions)

RxDart предоставляет мощные операторы для трансформации потоков:

import 'package:rxdart/rxdart.dart';

// map — трансформация
Stream<String> numberStream = Stream.fromIterable([1, 2, 3]);
numberStream.map((n) => 'Number: $n').listen(print);
// Output: Number: 1, Number: 2, Number: 3

// where — фильтрация
numberStream.where((n) => n > 1).listen(print);
// Output: 2, 3

// debounceTime — ждёт пока поток затихнет
searchController.stream
  .debounceTime(Duration(milliseconds: 500))
  .listen((query) => performSearch(query));

// throttleTime — пропускает события, если раньше был недавний
clickStream.throttleTime(Duration(seconds: 1)).listen((_) {
  print('Button clicked (but not more than once per second)');
});

// combineLatest — объединяет несколько потоков
Rx.combineLatest2(
  emailSubject,
  passwordSubject,
  (email, password) => email.isNotEmpty && password.isNotEmpty,
).listen((isValid) {
  setState(() => isButtonEnabled = isValid);
});

// switchMap — переключение между потоками
searchSubject.switchMap((query) =>
  searchAPI(query)
).listen((results) => setState(() => this.results = results));

Практический пример: поле поиска

class SearchBloc {
  final _searchSubject = BehaviorSubject<String>(seedValue: '');
  final _resultsSubject = BehaviorSubject<List<Item>>(seedValue: []);
  
  Stream<List<Item>> get results => _resultsSubject.stream;
  
  SearchBloc() {
    _searchSubject
      .debounceTime(Duration(milliseconds: 500))
      .switchMap((query) => _search(query))
      .listen(_resultsSubject.add);
  }
  
  void search(String query) => _searchSubject.add(query);
  
  Stream<List<Item>> _search(String query) async* {
    if (query.isEmpty) {
      yield [];
      return;
    }
    
    try {
      final results = await API.search(query);
      yield results;
    } catch (e) {
      yield [];
    }
  }
  
  void dispose() {
    _searchSubject.close();
    _resultsSubject.close();
  }
}

// В UI
class SearchPage extends StatefulWidget {
  @override
  State<SearchPage> createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
  late SearchBloc _bloc;
  
  @override
  void initState() {
    super.initState();
    _bloc = SearchBloc();
  }
  
  @override
  void dispose() {
    _bloc.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          TextField(
            onChanged: _bloc.search,
            decoration: InputDecoration(hintText: 'Search...'),
          ),
          Expanded(
            child: StreamBuilder<List<Item>>(
              stream: _bloc.results,
              builder: (context, snapshot) {
                if (!snapshot.hasData) {
                  return SizedBox.shrink();
                }
                
                final items = snapshot.data!;
                return ListView.builder(
                  itemCount: items.length,
                  itemBuilder: (context, index) {
                    return ListTile(title: Text(items[index].name));
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

Преимущества реактивного подхода

  • Разделение логики и UI — BLoC содержит всю бизнес-логику
  • Тестируемость — легко тестировать Streams без UI
  • Повторное использование — BLoC можно применить в разных UI
  • Обработка асинхронности — Stream и Future встроены в язык
  • Управление состоянием — чистое и предсказуемое

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

  • ✅ Используй BLoC для сложной логики
  • ✅ Разделяй Events, States и BLoC в отдельные файлы
  • ✅ Всегда dispose Streams и Controllers
  • ✅ Используй RxDart операторы для трансформации
  • ✅ Тестируй BLoC отдельно от UI
  • ❌ Не выводи Streams в UI — оборачивай в BLoC
  • ❌ Не забывай про memory leaks от незакрытых Streams

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

Что такое реактивное программирование в контексте Flutter? | PrepBro