felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.8k stars 3.39k forks source link

RefreshIndicator is not hidden after doing its function. #498

Closed iCueto closed 5 years ago

iCueto commented 5 years ago

I've follow the instructions of the Example Weather App for the Pull to Refresh feature, and the RefreshIndicator is not hidden after its does the function and request the data.

Here is a video of the behavior: Video of the app with RefreshIndicator

As you can see My Code is something similar to the example of the WeatherApp in the part of the Refresh.

What am I doing wrong?

Thanks for the help...

Here is my Bloc.

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

import 'listing_event.dart';
import 'listing_state.dart';

import '../../repository/listing_repository.dart';

class ListingBloc extends Bloc<ListingEvent, ListingState> {
  final ListingRepository _listingRepository = ListingRepository();

  @override
  Stream<ListingState> transform(
    Stream<ListingEvent> events,
    Stream<ListingState> Function(ListingEvent event) next,
  ) {
    return super.transform(
      (events as Observable<ListingEvent>).debounceTime(
        Duration(milliseconds: 500),
      ),
      next,
    );
  }

  @override
  get initialState => ListingUninitialized();

  @override
  Stream<ListingState> mapEventToState(ListingEvent event) async* {

    if (event is Refresh) {
      try {

        final result = await _listingRepository.fetchListingByCityAndCategory(shouldReload: true, category: event.category, city: event.location);
        yield ListingLoaded(items: result.listings, hasReachedMax: false);

      } catch (_) {
        print(_.toString());
        yield ListingError();
      }
    }

    if (event is Fetch && !_hasReachedMax(currentState)) {
      try {
        if (currentState is ListingUninitialized) {
          final result = await _listingRepository.fetchListingByCityAndCategory(category: event.category, city: event.location);
          yield ListingLoaded(items: result.listings, hasReachedMax: false);
        }

        if (currentState is ListingLoaded) {
          final result =
              await _listingRepository.fetchNextPageListingByCityAndCategory(category: event.category, city: event.location, start: (currentState as ListingLoaded).items.length);
          yield result.listings.isEmpty
              ? (currentState as ListingLoaded).copyWith(hasReachedMax: true)
              : ListingLoaded(
                  items: (currentState as ListingLoaded).items + result.listings,
                  hasReachedMax: false,
                );
        }
      } catch (_) {
        print(_.toString());
        yield ListingError();
      }
    }
  }

  bool _hasReachedMax(ListingState state) =>
      state is ListingLoaded && state.hasReachedMax;

}

here my provider and my repository

class ListingProvider {
  Client client = Client();

  Future<Response> fetchListingsByCityAndCategory({location, category, start}) async {

    final url = new Uri(
      scheme: 'https',
      host: 'clisgo.com',
      path: '/wp-json/wp/v2/listing/',
      queryParameters: {
        'offset': '$start',
        '_embed': '1',
        'location': '$location',
        'listing-category': '$category'
      }
    );

    final response = await client.get(url);

    print(response.headers.toString());

    if (response.statusCode == 200) {
      return response;
    } else {
      throw Exception("Failed to Load the Listings");
    }
  }

}

class ListingRepository {

  final listingProvider = ListingProvider();
  final cache = Cache();

  Future<Listings> fetchListingByCityAndCategory({ city, category, int start = 0, bool shouldReload = false }) async {
    String listingKey = "listing_$city$category$start";

    final removed = shouldReload ? await cache.remove(listingKey) : false;

    final containCache = await cache.check(key: listingKey);

    if (containCache) {
      final listingCache = await cache.get(key: listingKey);

      final decoded = json.decode(listingCache);

      final jsonResponse = json.decode(decoded['json']);

      return Listings.fromJSON(
        json: jsonResponse,
        totalItem: decoded['totalItem'],
        totalPages: decoded['totalPages']
      );

    } else {
      final result = await listingProvider.fetchListingsByCityAndCategory(category: category, location: city, start: start);

      var jsonResponse = json.decode(result.body);

      final toBeCached = {
        'json': result.body,
        'totalItem': result.headers["X-WP-Total"],
        'totalPages': result.headers["X-WP-TotalPages"]
      };

      final cached = await cache.save(key: listingKey, value: json.encode(toBeCached));

      return Listings.fromJSON(
        json:jsonResponse,
        totalItem: result.headers["X-WP-Total"],
        totalPages: result.headers["X-WP-TotalPages"]
      );
    }
  }
}

here my View

import 'dart:async';

import 'package:Clisgo/blocs/listing/listing_bloc.dart';
import 'package:Clisgo/blocs/listing/listing_event.dart';
import 'package:Clisgo/blocs/listing/listing_state.dart';
import 'package:Clisgo/ui/screens/detail_listing_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// import 'package:flutter_advanced_networkimage/provider.dart';

import '../widgets/listing_card.dart';

// import '../screens/category_screen.dart';

class ListingsScreen extends StatefulWidget {

  ListingsScreen({this.locationId, this.categoryId, this.nameCategory});

  final String locationId;
  final String categoryId;
  final String nameCategory;

  @override
  _ListingsScreenState createState() => _ListingsScreenState();
}

class _ListingsScreenState extends State<ListingsScreen> {
  Completer<void> _refreshCompleter;

  ListingBloc _listingBloc;

  final _scrollController = ScrollController();
  final _scrollThreshold = 200.0;

  @override
  void initState() {
    super.initState();

    _scrollController.addListener(_onScroll);
    _refreshCompleter = Completer<void>();

    _listingBloc  = ListingBloc();

    _listingBloc.dispatch(Fetch(location: widget.locationId, category: widget.categoryId));
  }

  @override
  void dispose() {
    _listingBloc.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.nameCategory),
      ),
      body: BlocBuilder<ListingBloc, ListingState>(
        bloc: _listingBloc,
        builder: (BuildContext context, ListingState state) {
          if (state is ListingError) {
          return Center(
            child: Text('No se pudieron cargar los Servicios, intenta conectarte a internet.'),
          );
        }

        if (state is ListingLoaded) {
          if (state.items.isEmpty) {
            return Center(
              child: Text('No hay Servicios'),
            );
          }
          return RefreshIndicator(
            onRefresh: () {
              _listingBloc.dispatch(
                Refresh(category: widget.categoryId, location: widget.locationId)
              );

              return _refreshCompleter.future;
            },
            child: ListView.builder(
            physics: AlwaysScrollableScrollPhysics(),
            // shrinkWrap: true,
            itemBuilder: (BuildContext context, int index) {
              return index >= state.items.length
                  ? Container(
                    alignment: Alignment.center,
                    child: Center(
                      child: CircularProgressIndicator(),
                    ),
                  )
                  : InkWell(
                  onTap: () {
                    Navigator.push(context, MaterialPageRoute(
                      builder: (context) => ListingDetailScreen(listing: state.items[index])
                    ));
                  },
                  child: ListingCard(item: state.items[index])
                  );
            },
            itemCount: state.hasReachedMax
                ? state.items.length
                : state.items.length + 1,
            controller: _scrollController,
          ),
          );
        }
        return Center(
          child: CircularProgressIndicator(),
        );
        },
      )
    );
  }

  void _onScroll() {
    final maxScroll = _scrollController.position.maxScrollExtent;
    final currentScroll = _scrollController.position.pixels;
    if (maxScroll - currentScroll <= _scrollThreshold) {
      _listingBloc.dispatch(Fetch(category: widget.categoryId, location: widget.locationId));
    }
  }
}
felangel commented 5 years ago

Hi @iCueto 👋 Thanks for opening an issue!

I think you are missing completing the Completer. In the weather example, we use BlocListener to detect when the bloc has finished and then complete and reset the Completer.

        BlocListener<WeatherBloc, WeatherState>(          
          listener: (context, state) {
            if (state is WeatherLoaded) {
              BlocProvider.of<ThemeBloc>(context).dispatch(
                WeatherChanged(condition: state.weather.condition),
              );
              _refreshCompleter?.complete();
              _refreshCompleter = Completer();
            }
          },
          ...

Weather Example Source

Hope that helps 👍

iCueto commented 5 years ago

Thank a lot!!! @felangel... It's Work!!!

I have missing that part... Thanks...