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

Как использовать FutureBuilder и StreamBuilder?

2.3 Middle🔥 162 комментариев
#Flutter виджеты#Асинхронность

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

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

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

FutureBuilder и StreamBuilder

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

FutureBuilder — для одноразовых операций

Используется когда результат получается один раз.

FutureBuilder<User>(
  future: fetchUser(userId),  // Future завершится один раз
  builder: (context, snapshot) {
    // snapshot содержит состояние Future
    
    // 1. Проверяем состояние загрузки
    if (snapshot.connectionState == ConnectionState.waiting) {
      return Center(child: CircularProgressIndicator());
    }
    
    // 2. Проверяем ошибки
    if (snapshot.hasError) {
      return Center(child: Text('Ошибка: ${snapshot.error}'));
    }
    
    // 3. Проверяем наличие данных
    if (!snapshot.hasData) {
      return Center(child: Text('Нет данных'));
    }
    
    // 4. Используем данные
    final user = snapshot.data!;
    return UserProfile(user: user);
  },
)

StreamBuilder — для потока данных

Используется когда данные приходят постоянно.

StreamBuilder<List<Message>>(
  stream: messagesStream,  // Stream будет эмитировать множество значений
  initialData: [],  // Начальное значение
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return Center(child: CircularProgressIndicator());
    }
    
    if (snapshot.hasError) {
      return Center(child: Text('Ошибка: ${snapshot.error}'));
    }
    
    final messages = snapshot.data ?? [];
    return ListView.builder(
      itemCount: messages.length,
      itemBuilder: (context, index) {
        return MessageTile(message: messages[index]);
      },
    );
  },
)

Жизненный цикл ConnectionState

enum ConnectionState {
  none,       // Stream ещё не подписан
  waiting,    // Ждём первого результата
  active,     // Stream активный, данные могут приходить
  done,       // Stream завершён (только для Stream)
}

// Пример проверки
if (snapshot.connectionState == ConnectionState.none) {
  return Text('Ещё не подключено');
} else if (snapshot.connectionState == ConnectionState.waiting) {
  return CircularProgressIndicator();
} else if (snapshot.connectionState == ConnectionState.active) {
  return Text('Данные: ${snapshot.data}');
} else if (snapshot.connectionState == ConnectionState.done) {
  return Text('Завершено: ${snapshot.data}');
}

Практический пример: REST API

class UserRepository {
  Future<User> fetchUser(String id) async {
    final response = await http.get(Uri.parse('https://api.example.com/users/$id'));
    if (response.statusCode == 200) {
      return User.fromJson(jsonDecode(response.body));
    } else {
      throw Exception('Ошибка загрузки пользователя');
    }
  }
}

class UserScreen extends StatelessWidget {
  final String userId;
  final repository = UserRepository();
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Пользователь')),
      body: FutureBuilder<User>(
        future: repository.fetchUser(userId),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          
          if (snapshot.hasError) {
            return Center(child: Text('Ошибка: ${snapshot.error}'));
          }
          
          final user = snapshot.data!;
          return Padding(
            padding: EdgeInsets.all(16),
            child: Column(
              children: [
                CircleAvatar(radius: 50, backgroundImage: NetworkImage(user.avatar)),
                SizedBox(height: 16),
                Text(user.name, style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                Text(user.email),
              ],
            ),
          );
        },
      ),
    );
  }
}

Практический пример: Real-time данные

class ChatRepository {
  Stream<List<Message>> getMessages(String chatId) {
    return FirebaseFirestore.instance
        .collection('chats')
        .doc(chatId)
        .collection('messages')
        .orderBy('timestamp')
        .snapshots()
        .map((snapshot) => snapshot.docs
            .map((doc) => Message.fromJson(doc.data()))
            .toList());
  }
}

class ChatScreen extends StatelessWidget {
  final String chatId;
  final repository = ChatRepository();
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Чат')),
      body: StreamBuilder<List<Message>>(
        stream: repository.getMessages(chatId),
        initialData: [],
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          }
          
          if (snapshot.hasError) {
            return Center(child: Text('Ошибка: ${snapshot.error}'));
          }
          
          final messages = snapshot.data ?? [];
          return ListView.builder(
            reverse: true,  // Новые сообщения внизу
            itemCount: messages.length,
            itemBuilder: (context, index) {
              final message = messages[messages.length - 1 - index];
              return MessageBubble(message: message);
            },
          );
        },
      ),
    );
  }
}

Сравнение

АспектFutureBuilderStreamBuilder
ИспользованиеОдин результатМножество результатов
ПримерыAPI запрос, загрузка файлаРеал-тайм чат, live-данные
ЗавершениеВсегда завершаетсяМожет бежать бесконечно
connectionStatewaiting → active/error/donewaiting → active → done
ПроизводительностьЛучше для простых случаевОптимально для потоков

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

1. Кэширование Future

class DataNotifier extends ChangeNotifier {
  Future<Data>? _cachedFuture;
  
  Future<Data> getData() {
    return _cachedFuture ??= _loadData();
  }
  
  Future<Data> _loadData() async {
    final response = await http.get(Uri.parse('...'));
    return Data.fromJson(jsonDecode(response.body));
  }
}

2. Отписка от Stream

class LiveDataWidget extends StatefulWidget {
  @override
  State<LiveDataWidget> createState() => _LiveDataWidgetState();
}

class _LiveDataWidgetState extends State<LiveDataWidget> {
  late StreamSubscription subscription;
  
  @override
  void initState() {
    super.initState();
    // Если нужна явная подписка
    subscription = dataStream.listen((data) {
      print('Получены данные: $data');
    });
  }
  
  @override
  void dispose() {
    subscription.cancel();  // Всегда отписываться!
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return StreamBuilder<Data>(
      stream: dataStream,
      builder: (context, snapshot) => ...,
    );
  }
}

3. Избегать множественных подписок

// ❌ Плохо — перестраивается часто
StreamBuilder(
  stream: repository.getData(),  // Новый Stream каждый раз!
  builder: ...,
)

// ✅ Хорошо — один Stream
class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  late final stream = repository.getData();  // Один Stream
  
  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: stream,
      builder: ...,
    );
  }
}