Create a API search func along with textfield , streamController and debounce time of 300ms ?

#api#stream

Answer

Overview

This implements a real-world search pattern: a

text
TextField
that waits for the user to stop typing for 300ms before firing an API call, using a
text
StreamController
and debounce logic.


Complete Implementation

dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class SearchScreen extends StatefulWidget {
  
  _SearchScreenState createState() => _SearchScreenState();
}

class _SearchScreenState extends State<SearchScreen> {
  // Stream controller to receive text input events
  final StreamController<String> _searchController = StreamController<String>();

  List<String> _results = [];
  bool _isLoading = false;
  StreamSubscription? _subscription;

  
  void initState() {
    super.initState();
    _setupSearch();
  }

  void _setupSearch() {
    _subscription = _searchController.stream
        .distinct()                          // Skip if same value as before
        .debounce(Duration(milliseconds: 300)) // Wait 300ms after last event
        .listen((query) {
          if (query.isNotEmpty) {
            _fetchResults(query);
          } else {
            setState(() => _results = []);
          }
        });
  }

  Future<void> _fetchResults(String query) async {
    setState(() => _isLoading = true);
    try {
      final response = await http.get(
        Uri.parse('https://api.example.com/search?q=$query'),
      );
      final data = jsonDecode(response.body) as List;
      setState(() => _results = data.map((e) => e['name'] as String).toList());
    } catch (e) {
      print('Search error: $e');
    } finally {
      setState(() => _isLoading = false);
    }
  }

  
  void dispose() {
    _subscription?.cancel();
    _searchController.close();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Search')),
      body: Column(
        children: [
          Padding(
            padding: EdgeInsets.all(12),
            child: TextField(
              decoration: InputDecoration(
                hintText: 'Search...',
                prefixIcon: Icon(Icons.search),
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(8),
                ),
              ),
              onChanged: (value) => _searchController.sink.add(value),
            ),
          ),
          if (_isLoading)
            LinearProgressIndicator(),
          Expanded(
            child: ListView.builder(
              itemCount: _results.length,
              itemBuilder: (context, index) =>
                  ListTile(title: Text(_results[index])),
            ),
          ),
        ],
      ),
    );
  }
}

Manual Debounce (Without Stream Extension)

If you prefer a simpler approach without stream extensions:

dart
class _SearchScreenState extends State<SearchScreen> {
  Timer? _debounceTimer;
  List<String> _results = [];
  bool _isLoading = false;

  void _onSearchChanged(String query) {
    // Cancel previous timer
    _debounceTimer?.cancel();

    // Start new timer — fires after 300ms of no input
    _debounceTimer = Timer(Duration(milliseconds: 300), () {
      if (query.isNotEmpty) _fetchResults(query);
    });
  }

  
  void dispose() {
    _debounceTimer?.cancel();
    super.dispose();
  }

  Future<void> _fetchResults(String query) async {
    setState(() => _isLoading = true);
    try {
      final response = await http.get(
        Uri.parse('https://dummyjson.com/products/search?q=$query'),
      );
      final data = jsonDecode(response.body);
      setState(() {
        _results = (data['products'] as List)
            .map((p) => p['title'] as String)
            .toList();
      });
    } finally {
      setState(() => _isLoading = false);
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Search')),
      body: Column(
        children: [
          Padding(
            padding: EdgeInsets.all(12),
            child: TextField(
              decoration: InputDecoration(
                hintText: 'Search products...',
                prefixIcon: Icon(Icons.search),
                suffixIcon: _isLoading
                    ? Padding(
                        padding: EdgeInsets.all(12),
                        child: SizedBox(
                          width: 16, height: 16,
                          child: CircularProgressIndicator(strokeWidth: 2),
                        ),
                      )
                    : null,
                border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
              ),
              onChanged: _onSearchChanged,
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _results.length,
              itemBuilder: (context, index) =>
                  ListTile(
                    leading: Icon(Icons.search, color: Colors.blue),
                    title: Text(_results[index]),
                  ),
            ),
          ),
        ],
      ),
    );
  }
}

How Debounce Works

text
User types: F → Fl → Flu → Flut → Flutt → Flutter
              ↑   ↑    ↑     ↑      ↑       ↑
           Timer Timer Timer Timer  Timer  Timer (300ms)
           reset reset reset reset  reset   fires → API call!

Each keystroke resets the timer. Only when the user stops typing for 300ms does the API call fire.


Why StreamController + Debounce?

ApproachDebounceCancel in-flightReactive
text
onChanged
+
text
Timer
✅ Manual✅ Manual
text
StreamController
+ debounce
✅ Reactive
text
.distinct()
text
rxdart
text
BehaviorSubject
✅ Built-in✅ Best

Best Production Approach: Use

text
rxdart
's
text
BehaviorSubject
with
text
.debounceTime()
for the cleanest reactive search implementation.