Mobiilikehitys Flutterilla · Oppitunti

Stream-muuntimet ja tapahtumien debouncing BLoCissa

Käytä samanaikaisuuden muuntimia saapuvien tapahtumien rajoittamiseen, debouncingiin ja järjestämiseen.

Oppitunti 2/413 vaihetta

Stream-muuntimet ja tapahtumien debouncing BLoCissa on ilmainen Mobiilikehitys Flutterilla-oppitunti CoddyKitissä. Tämä on oppitunti 2/4. Voit lukea tästä oppimispolusta kokonaan mitkä tahansa 3 oppituntia ilmaiseksi — sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä käytännön harjoittelun sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. Oppitunti kuuluu Mobiilikehitys Flutterilla-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. Mobiilikehitys Flutterilla-kurssilla on yhteensä 4 oppituntia.

Miksi tapahtumien samanaikaisuudella on merkitystä

flutter_bloc-kirjastossa jokainen add(event)-kutsu lisää tapahtuman sisäiseen virtaan. Oletusarvoisesti kunkin tapahtuman käsittelijä suoritetaan samanaikaisesti tapahtumien saapuessa. Useimpien tapahtumien kohdalla tämä on sopivaa, mutta jotkin lähteet tuottavat tapahtumia aivan liian usein:

  • Hakupalkit tuottavat TextChanged-tapahtuman jokaisella näppäinpainalluksella.
  • Vierityskuuntelijat tuottavat kymmeniä ScrolledToBottom-tapahtumia sekunnissa.
  • Painikkeita voidaan napauttaa nopeasti, jolloin syntyy toistuvia SubmitPressed-tapahtumia.

Verkkopyynnön tekeminen jokaisella näppäinpainalluksella tuhlaa kaistanleveyttä ja voi näyttää vanhentuneita tuloksia. Tässä oppitunnissa opitte rajoittamaan nopeutta, odottamaan syötteen hiljentymistä ja käsittelemään tapahtumat järjestyksessä BLoCin stream-muuntimilla.

transformer-parametri

Moderni on<Event>-rajapinta hyväksyy valinnaisen transformer-argumentin. Muunnin on EventTransformer<E> — funktio, joka vastaanottaa saapuvien tapahtumien Stream<E>-virran ja mapperin ja palauttaa muunnetun virran.

Muuntimen avulla hallitsette, miten kyseisen tyypin tapahtumat käsitellään: viivästettyinä, nopeusrajoitettuina, hylättyinä tai yksi kerrallaan. Allekirjoitus on:

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

Muuntimia kirjoitetaan harvoin itse — bloc_concurrency-paketti tarjoaa yleisimmät valmiina.

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

Debounce: odottakaa taukoa

Debounce-käsittely jättää tapahtumat huomiotta, kunnes hiljainen ajanjakso on kulunut. Jos käyttäjä kirjoittaa edelleen, ajastin käynnistetään aina uudelleen; vasta kun käyttäjä on esimerkiksi 300 ms tauolla, käsittelemme uusimman tapahtuman.

Tämä sopii hakukenttään: haluatte kysellä rajapinnasta vasta, kun käyttäjä lopettaa kirjoittamisen, ettekä jokaisella näppäinpainalluksella. Debounce hylkää kaikki tapahtumaryöpyn välissä olevat tapahtumat ja säilyttää vain viimeisen.

  • Vähentää verkkokutsujen määrää huomattavasti.
  • Tehokkuuden saavuttamiseksi hyväksyy hieman viivettä (debounce-viiveen).

Debounce tavallisella Dartilla

Ennen kuin yhdistämme sen BLoCiin, tarkastelemme debounce-ideaa suoritettavana Dart-ohjelmana. Simuloimme näppäinpainalluksia, joiden väliset tauot vaihtelevat, ja tulostamme termin vasta, kun kirjoittamiseen tulee 300 ms:n tauko.

RxDart-operaattori hoitaa tämän puolestanne, mutta ajastinlogiikan näkeminen selventää, mitä debounce tarkoittaa.

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: yksi tapahtuma aikavälissä

Throttle-käsittely päästää ensimmäisen tapahtuman läpi ja jättää sitten uudet tapahtumat huomiotta kiinteän aikavälin ajan. Toisin kuin debounce, throttle ei odota taukoa — se tuottaa tapahtuman heti ja rajoittaa sen jälkeen tapahtumatiheyttä.

Tämä sopii loputtomaan vieritykseen ja nopeisiin painikkeen napautuksiin: haluatte reagoida ensimmäiseen ScrolledToBottom-tapahtumaan heti, mutta jättää saman vierityseleen aikana seuraavan tapahtumatulvan huomiotta.

  • throttleTime yhdessä trailing: false-asetuksen kanssa = vain etureuna (toimi nyt ja jäähdyttele sitten).
  • Estää sivujen lataamisen tai lomakkeen lähettämisen kahdesti.
// Infinite-scroll feed: react immediately, then cool down 500ms
on<FeedScrolledToEnd>(
  _onScrolledToEnd,
  transformer: (events, mapper) => events
      .throttleTime(const Duration(milliseconds: 500))
      .asyncExpand(mapper),
);

bloc_concurrency-muuntimet

Virallinen bloc_concurrency-paketti sisältää neljä valmista muunninta, jotka hallitsevat päällekkäisten tapahtumien käsittelyä:

  • concurrent() — käsittelijät suoritetaan rinnakkain (oletus).
  • sequential() — tapahtumat jonotetaan ja käsitellään täsmälleen yksi kerrallaan.
  • droppable() — käsittelijän ollessa käynnissä uudet saman tyypin tapahtumat hylätään.
  • restartable() — uusi tapahtuma peruuttaa käynnissä olevan käsittelijän ja aloittaa alusta.

Nämä voidaan yhdistää ajoitusoperaattoreihin: käyttäkää esimerkiksi ensin debouncea ja sitten restartable()-muunninta vanhentuneen haun peruuttamiseen.

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());

Debouncen ja restartablen yhdistäminen

Hakua käsittelevälle BLoCille ihanteellinen ratkaisu on debounce ja sitten restartable:

  • Debounce-käsitelkää TextChanged-tapahtumat, jotta teette kyselyn vasta käyttäjän pidettyä tauon.
  • Käyttäkää restartable()-muunninta, jotta uusi kysely peruuttaa aiemman pyynnön sen ollessa vielä latauksessa eikä vanhentunut pyyntö koskaan korvaa tuoreita tuloksia.

Yhdessä nämä poistavat sekä turhat kutsut että väärässä järjestyksessä saapuvien vastausten ongelman. Kääräisette bloc_concurrency-paketin muuntimen pieneen apufunktioon, joka käyttää ensin debounceTime-operaattoria.

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)),
);

Täydellinen hakua käsittelevä Bloc

Näin osat yhdistyvät oikeassa SearchBloc-toteutuksessa. Huomatkaa, että käsittelijä on async ja voi välittää emit-kutsulla useita tiloja (latauksen ja sitten tulokset tai virheen). Koska muunnin on restartable, vanhentunut pyyntö lakkaa välittämästä tiloja heti, kun uudempi hakutermi saapuu.

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()));
    }
  }
}

Muunnosoperaattorit: switchMap ja asyncExpand

Mukautetussa muuntimessa päätätte, miten mapperia sovelletaan kuhunkin tapahtumaan:

  • switchMap(mapper) — peruuttaa edellisen sisäisen virran, kun uusi tapahtuma saapuu. Vastaa periaatteessa restartable-muunninta.
  • exhaustMap(mapper) — jättää uudet tapahtumat huomiotta, kun yksi on aktiivinen. Vastaa droppable-muunninta.
  • asyncExpand(mapper) — suorittaa tapahtumat peräkkäin; kukin tapahtuma odottaa edellisen käsittelijän valmistumista. Vastaa sequential-muunninta.
  • flatMap(mapper) — suorittaa kaikki tapahtumat samanaikaisesti. Vastaa concurrent-muunninta.

Suosikaa nimettyjä bloc_concurrency-muuntimia selkeyden vuoksi; käyttäkää suoraa RxDartia vain, kun ajoitus ja muunnos on yhdistettävä samaan lausekkeeseen.

Throttle-järjestysesimerkki Dartilla

Tämä suoritettava esimerkki mallintaa throttle-käsittelyä ilman mitään frameworkia. Kunkin 200 ms:n aikavälin ensimmäinen tapahtuma käsitellään; jäähdyttelyn aikana saapuvat tapahtumat hylätään. Seuratkaa, kuinka vain etummaiset tapahtumat jäävät jäljelle.

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');
}

Oikean strategian valitseminen

Yhdistä muunnin käyttäjän tarkoitukseen:

  • Haku / automaattinen täydennys: debounce + restartable — odota taukoa ja peru vanhentuneet kyselyt.
  • Äärettömän vierityksen sivulataus: throttle + droppable — lataa kerran ja ohita loput eleen aikana.
  • Lomakkeen lähetys / maksu: droppable — estä päällekkäiset lähetykset yhden ollessa käynnissä.
  • Järjestyksessä tehtävät kirjoitukset (vaiheiden tallennus, analytiikka): sequential — säilytä järjestys äläkä salli päällekkäisyyksiä.

Lisää aina bloc_concurrency ja rxdart tiedostoon pubspec.yaml ja muista, että muunnin vaikuttaa vain siihen tapahtumatyyppiin, johon se on rekisteröity.

Pikatarkistus

Testaa ymmärryksesi hakukenttää koskevasta tilanteesta.

Kertaus

Opit hallitsemaan tapahtumien samanaikaisuutta BLoCissa stream-muuntimien avulla:

  • on<Event>-käsittelijä vastaanottaa transformer-muuntimen, joka muokkaa saapuvaa tapahtumavirtaa.
  • Debounce odottaa taukoa (sopii erinomaisesti hakuun); throttle käsittelee tapahtuman ensin ja pitää sitten tauon (sopii vieritykseen ja nopeisiin napautuksiin).
  • bloc_concurrency tarjoaa muuntimet concurrent, sequential, droppable ja restartable, jotka vastaavat RxDartin muuntimia flatMap, asyncExpand, exhaustMap ja switchMap.
  • Hakujen vakiintunut ratkaisu on debounce ja sitten restartable; lähetyksissä käytetään droppable-muunninta ja järjestyksessä tehtävissä kirjoituksissa sequential-muunninta.

Valitse käyttäjän tarkoitukseen sopiva muunnin ja muista, että se vaikuttaa vain siihen tapahtumatyyppiin, johon se on rekisteröity.

Aloita maksutta

Opi Dart tekoälytuutorin avulla — ilmaiseksi

Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.

Kurssit
22
Oppitunnit
88

Usein kysytyt kysymykset

Onko oppitunti ”Stream-muuntimet ja tapahtumien debouncing BLoCissa” ilmainen?

Kyllä — voit lukea täällä verkossa kokonaan ilmaiseksi mitkä tahansa Mobiilikehitys Flutterilla-oppimispolun 3 oppituntia, myös oppitunnin “Stream-muuntimet ja tapahtumien debouncing BLoCissa”. Sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä interaktiiviset harjoitukset sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. Mobiilikehitys Flutterilla-kurssilla on yhteensä 4 oppituntia.

Mitä opin oppitunnilla ”Stream-muuntimet ja tapahtumien debouncing BLoCissa”?

Käytä samanaikaisuuden muuntimia saapuvien tapahtumien rajoittamiseen, debouncingiin ja järjestämiseen. Harjoittelet Mobiilikehitys Flutterilla-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.

Tarvitsenko kokemusta aloittaakseni Mobiilikehitys Flutterilla-opiskelun?

Aiempi kokemus ei ole tarpeen. CoddyKitin Mobiilikehitys Flutterilla-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 2/4.

Kuinka kauan ”Stream-muuntimet ja tapahtumien debouncing BLoCissa”-oppitunnin suorittaminen kestää?

Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.

Voinko kirjoittaa ja suorittaa koodia tällä Mobiilikehitys Flutterilla-oppitunnilla?

Kyllä. Jokainen Mobiilikehitys Flutterilla-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.

Kaikki tämän kurssin oppitunnit

  1. Tapahtumat, tilat ja Cubit-vs-Bloc-valinta
  2. Stream-muuntimet ja tapahtumien debouncing BLoCissa
  3. Tilan säilyttäminen HydratedBlocilla
  4. BLoCien testaaminen bloc_testillä ja Mocktaililla
← Takaisin: Mobiilikehitys Flutterilla