Mobiludvikling med Flutter · Lektion

Stream-transformere og event-debouncing i BLoC

Anvend concurrency-transformere til at begrænse, debounce og sekvensere indkommende events.

Lektion 2 af 413 trin

Stream-transformere og event-debouncing i BLoC er en gratis Mobiludvikling med Flutter-lektion på CoddyKit. Dette er lektion 2 af 4. Du kan læse alle 3 lektioner i dette læringsspor gratis i deres fulde længde — derefter låser CoddyKit PRO alle lektioner op samt praktiske øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Den er en del af læringsforløbet i Mobiludvikling med Flutter, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Mobiludvikling med Flutter-kurset indeholder 4 lektioner i alt.

Hvorfor samtidighed for begivenheder er vigtig

I flutter_bloc skubber hvert kald til add(event) en begivenhed ind i en intern strøm. Som standard kører hver begivenhedsbehandler samtidigt, efterhånden som begivenhederne ankommer. Det er fint for de fleste begivenheder, men nogle kilder udløser dem alt for ofte:

  • Søgefelter udsender en TextChanged-begivenhed ved hvert tastetryk.
  • Rullelyttere udsender dusinvis af ScrolledToBottom-begivenheder i sekundet.
  • Knapper kan trykkes hurtigt flere gange og udløse dublerede SubmitPressed-begivenheder.

En netværksanmodning for hvert tastetryk spilder båndbredde og kan vise forældede resultater. Denne lektion viser, hvordan du throttler, debouncer og sekvenserer disse begivenheder ved hjælp af strømtransformere i BLoC.

Transformer-parameteren

Den moderne on<Event>-API accepterer et valgfrit transformer-argument. En transformer er en EventTransformer<E> – en funktion, der modtager den indkommende Stream<E> med begivenheder samt en mapper og returnerer en transformeret strøm.

Ved at angive en transformer styrer du hvordan begivenheder af den type behandles: debouncet, throttlet, bortkastet eller kørt én ad gangen. Signaturen er:

  • typedef EventTransformer<Event> = Stream<Event> Function(Stream<Event> events, EventMapper<Event> mapper);

Du skriver sjældent transformere i hånden – pakken bloc_concurrency leverer de almindelige.

// Registering a handler with a custom transformer
on<SearchTermChanged>(
  _onSearchTermChanged,
  transformer: (events, mapper) => events
      .debounceTime(const Duration(milliseconds: 300))
      .switchMap(mapper),
);

Debounce: Vent på pausen

Debouncing ignorerer begivenheder, indtil der er gået en stille periode. Hvis brugeren stadig skriver, nulstiller vi timeren; først når brugeren for eksempel holder pause i 300 ms, behandler vi den seneste begivenhed.

Dette er det rigtige valg til et søgefelt: Du vil forespørge API'et, når brugeren holder op med at skrive, ikke ved hvert tastetryk. Debounce bortkaster alle mellemliggende begivenheder og beholder kun den sidste i hvert udbrud.

  • Reducerer antallet af netværkskald markant.
  • Bytter en smule ventetid (debounce-forsinkelsen) for effektivitet.

Debounce i almindelig Dart

Før vi kobler det til BLoC, kan du se debounce-idéen udtrykt som et kørbart Dart-program. Vi simulerer tastetryk, der ankommer med varierende mellemrum, og udskriver kun søgeordet, når skrivningen holder pause i 300 ms.

RxDart-operatoren gør dette for dig, men timerlogikken gør det tydeligere, hvad debounce betyder.

import 'dart:async';

void main() async {
  Timer? debounce;
  String? pending;
  final done = Completer<void>();

  void onChanged(String term) {
    pending = term;
    debounce?.cancel();
    debounce = Timer(const Duration(milliseconds: 300), () {
      print('search: $pending');
      if (pending == 'flutter') done.complete();
    });
  }

  // Fast burst, then a pause, then more typing.
  onChanged('f');
  await Future.delayed(const Duration(milliseconds: 50));
  onChanged('fl');
  await Future.delayed(const Duration(milliseconds: 50));
  onChanged('flu');
  await Future.delayed(const Duration(milliseconds: 400)); // pause -> fires
  onChanged('flutter');
  await done.future;
}

Throttle: Én pr. tidsvindue

Throttling lader den første begivenhed slippe igennem og ignorerer derefter yderligere begivenheder i et fast tidsvindue. I modsætning til debounce venter throttle ikke på en pause – den udsender straks og begrænser derefter hastigheden.

Dette passer til uendelig rulning og hurtige knaptryk: Du vil reagere på den første ScrolledToBottom med det samme, men ignorere stormen af dubletter, der følger under den samme rullebevægelse.

  • throttleTime med trailing: false = kun forkanten (reagér nu, og køl derefter ned).
  • Forhindrer dublerede sideindlæsninger eller dobbelte indsendelser.
// Infinite-scroll feed: react immediately, then cool down 500ms
on<FeedScrolledToEnd>(
  _onScrolledToEnd,
  transformer: (events, mapper) => events
      .throttleTime(const Duration(milliseconds: 500))
      .asyncExpand(mapper),
);

Transformere i bloc_concurrency

Den officielle pakke bloc_concurrency indeholder fire færdige transformere, der styrer, hvordan overlappende begivenheder håndteres:

  • concurrent() — behandlere kører parallelt (standard).
  • sequential() — begivenheder sættes i kø og behandles strengt én efter én.
  • droppable() — mens en behandler kører, kasseres nye begivenheder af den type.
  • restartable() — en ny begivenhed annullerer den igangværende behandler og starter forfra.

Disse kan kombineres med tidsoperatorer: f.eks. debounce først og derefter restartable() for at annullere en forældet søgning.

import 'package:bloc_concurrency/bloc_concurrency.dart';

// Submit button: ignore extra taps while the first submit is in flight
on<FormSubmitted>(_onSubmit, transformer: droppable());

// Saving steps that must run in order
on<StepSaved>(_onStepSaved, transformer: sequential());

Kombination af debounce og restartable

Til en søge-BLoC er den ideelle opskrift debounce og derefter restartable:

  • Debounc TextChanged-begivenhederne, så du kun forespørger, efter at brugeren holder pause.
  • Brug restartable(), så en nyere forespørgsel annullerer den forældede anmodning, hvis den tidligere stadig indlæses, og den forældede anmodning aldrig overskriver de nye resultater.

Tilsammen fjerner de både spildte kald og svar, der kommer i forkert rækkefølge. Du omslutter bloc_concurrencys transformer med en lille hjælper, der anvender debounceTime først.

import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:rxdart/rxdart.dart';

EventTransformer<E> debounceRestartable<E>(Duration duration) {
  return (events, mapper) =>
      restartable<E>().call(events.debounceTime(duration), mapper);
}

// Usage inside a Bloc constructor:
on<SearchTermChanged>(
  _onSearchTermChanged,
  transformer: debounceRestartable(const Duration(milliseconds: 300)),
);

En komplet søge-Bloc

Her kan du se, hvordan delene passer sammen i en rigtig SearchBloc. Bemærk, at behandleren er async og kan emitte flere tilstande (indlæsning og derefter resultater eller fejl). Fordi transformeren er restartable, holder en forældet anmodning op med at udsende tilstande, så snart et nyere søgeord ankommer.

class SearchBloc extends Bloc<SearchEvent, SearchState> {
  final SearchRepository repo;

  SearchBloc(this.repo) : super(const SearchState.initial()) {
    on<SearchTermChanged>(
      _onTermChanged,
      transformer: debounceRestartable(const Duration(milliseconds: 300)),
    );
  }

  Future<void> _onTermChanged(
    SearchTermChanged event,
    Emitter<SearchState> emit,
  ) async {
    final term = event.term.trim();
    if (term.isEmpty) {
      emit(const SearchState.initial());
      return;
    }
    emit(const SearchState.loading());
    try {
      final results = await repo.search(term);
      emit(SearchState.success(results));
    } catch (e) {
      emit(SearchState.failure(e.toString()));
    }
  }
}

Mappingsoperatorer: switchMap kontra asyncExpand

Inde i en brugerdefineret transformer beslutter du, hvordan mapperen anvendes på hver begivenhed:

  • switchMap(mapper) — annullerer den forrige indre strøm, når en ny begivenhed ankommer. Svarer i princippet til restartable.
  • exhaustMap(mapper) — ignorerer nye begivenheder, mens en anden er aktiv. Svarer til droppable.
  • asyncExpand(mapper) — kører sekventielt; hver begivenhed venter på, at den forrige behandler afsluttes. Svarer til sequential.
  • flatMap(mapper) — kører alle samtidigt. Svarer til concurrent.

Foretræk de navngivne bloc_concurrency-transformere for tydelighedens skyld; brug kun rå RxDart, når du er nødt til at kombinere tidsstyring og mapping i ét udtryk.

Demonstration af throttle-sekvens i Dart

Dette kørbare eksempel modellerer throttling uden noget framework. Den første begivenhed i hvert 200 ms-vindue behandles; begivenheder, der ankommer under nedkølingen, bortkastes. Se, hvordan kun begivenhederne i forkanten overlever.

import 'dart:async';

void main() async {
  DateTime? lastAccepted;
  const window = Duration(milliseconds: 200);
  final accepted = <int>[];

  void onEvent(int id) {
    final now = DateTime.now();
    if (lastAccepted == null || now.difference(lastAccepted!) >= window) {
      lastAccepted = now;
      accepted.add(id);
    }
  }

  // Fire 6 events; some land inside the cooldown window.
  for (var i = 1; i <= 6; i++) {
    onEvent(i);
    await Future.delayed(const Duration(milliseconds: 90));
  }

  // Only leading-edge events per 200ms window are kept.
  print('accepted: $accepted');
}

Valg af den rigtige strategi

Vælg den transformer, der passer til brugerens hensigt:

  • Søgning/autofuldførelse: debounce + restartable — vent på pausen, og annullér forældede forespørgsler.
  • Indlæsning af side ved uendelig rulning: throttle + droppable — indlæs én gang, og ignorér resten af bevægelsen.
  • Indsendelse af formular/betaling: droppable — blokér dublerede indsendelser, mens en allerede er i gang.
  • Sekventielle skrivninger (lagringstrin, analyse): sequential — bevar rækkefølgen, og tillad ingen overlap.

Tilføj altid bloc_concurrency og rxdart til pubspec.yaml, og husk, at en transformer kun påvirker den ene hændelsestype, som den er registreret på.

Hurtigt tjek

Test din forståelse af scenariet med søgefeltet.

Opsummering

Du har lært at styre samtidigheden af hændelser i BLoC med stream-transformere:

  • Håndteringen on<Event> modtager en transformer, der omformer den indgående hændelsesstream.
  • Debounce venter på en pause (godt til søgning), mens throttle reagerer ved den forreste kant og derefter holder pause (godt til rulning og hurtige tryk).
  • bloc_concurrency tilbyder concurrent, sequential, droppable og restartable, som svarer til RxDart's flatMap, asyncExpand, exhaustMap og switchMap.
  • Den kanoniske opskrift til søgning er debounce derefter restartable; indsendelser bruger droppable, og sekventielle skrivninger bruger sequential.

Vælg den transformer, der passer til brugerens hensigt, og husk, at den kun gælder for den hændelsestype, den er registreret på.

Gratis at komme i gang

Lær Dart med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
22
Lektioner
88

Ofte stillede spørgsmål

Er lektionen “Stream-transformere og event-debouncing i BLoC” gratis?

Ja — alle 3 lektioner i læringssporet Mobiludvikling med Flutter, inklusive “Stream-transformere og event-debouncing i BLoC”, kan læses gratis i deres fulde længde her på webstedet. Derefter låser CoddyKit PRO alle lektioner op samt interaktive øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Mobiludvikling med Flutter-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “Stream-transformere og event-debouncing i BLoC”?

Anvend concurrency-transformere til at begrænse, debounce og sekvensere indkommende events. Du øver dig i Mobiludvikling med Flutter med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på Mobiludvikling med Flutter?

Der kræves ingen tidligere erfaring. Mobiludvikling med Flutter på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 2 af 4.

Hvor lang tid tager lektionen “Stream-transformere og event-debouncing i BLoC”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne Mobiludvikling med Flutter-lektion?

Ja. Alle Mobiludvikling med Flutter-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. Events, states og valget mellem Cubit og Bloc
  2. Stream-transformere og event-debouncing i BLoC
  3. Persistens af state med HydratedBloc
  4. Test af BLoCs med bloc_test og Mocktail
← Tilbage til Mobiludvikling med Flutter