felangel / bloc

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

Flutter Bloc Infinite List (Example) #1939

Closed rrifafauzikomara closed 3 years ago

rrifafauzikomara commented 3 years ago

First of all, I already try your example of Infinite List from here (https://github.com/felangel/bloc/tree/master/examples/flutter_infinite_list) and I try to my project. But in my project, it's not like expected like your example because the loading still appears at the bottom of ListView like this.

The length of data is 4 like this bellow, but why the loading still appear it looks like I always scrolled but actually not?

enter image description here

This is my model:

class LogAttendanceResponse {
  LogAttendanceResponse({
    this.code,
    this.message,
    this.data,
  });

  int code;
  String message;
  Data data;

  factory LogAttendanceResponse.fromJson(Map<String, dynamic> json) =>
      LogAttendanceResponse(
        code: json["code"],
        message: json["message"],
        data: json["data"] != null ? Data.fromJson(json["data"]) : null,
      );
}

class Data {
  Data({
    this.currentPage,
    this.data,
  });

  int currentPage;
  List<Attendance> data;

  factory Data.fromJson(Map<String, dynamic> json) => Data(
        currentPage: json["current_page"],
        data: List<Attendance>.from(
            json["data"].map((x) => Attendance.fromJson(x))),
      );
}

class Attendance {
  Attendance({
    this.id,
    this.employeeId,
    this.date,
    this.datumIn,
    this.inNote,
    this.out,
    this.outNote,
    this.late,
    this.totalLate,
    this.status,
    this.workingHourId,
    this.workingHourName,
    this.timeFrom,
    this.timeTo,
    this.createdAt,
    this.updatedAt,
  });

  int id;
  int employeeId;
  String date;
  String datumIn;
  String inNote;
  String out;
  String outNote;
  int late;
  int totalLate;
  String status;
  int workingHourId;
  String workingHourName;
  String timeFrom;
  String timeTo;
  String createdAt;
  String updatedAt;

  factory Attendance.fromJson(Map<String, dynamic> json) => Attendance(
        id: json["id"],
        employeeId: json["employee_id"],
        date: json["date"],
        datumIn: json["in"],
        inNote: json["in_note"],
        out: json["out"] == null ? null : json["out"],
        outNote: json["out_note"],
        late: json["late"],
        totalLate: json["total_late"],
        status: json["status"],
        workingHourId: json["working_hour_id"],
        workingHourName: json["working_hour_name"],
        timeFrom: json["time_from"],
        timeTo: json["time_to"],
        createdAt: json["created_at"],
        updatedAt: json["updated_at"],
      );
}

This is my API function:

Future<LogAttendanceResponse> getLogAttendance(int page, int pageSize) async {
    try {
      var token = await prefHelper.getToken();
      print("Token --> $token");

      var headers = {
        'content-type': 'application/json',
        'accept': 'application/json',
        'authorization': 'Bearer $token',
      };

      final response = await dio.get("attendances",
          queryParameters: {"paginate": true, "take": pageSize, "page": page},
          options: Options(method: "GET", headers: headers));
      return LogAttendanceResponse.fromJson(response.data);
    } on DioError catch (e) {
      return e.error;
    }
  }

This is the state:

import 'package:bozzetto/core/network/model/log_attendance_response.dart';
import 'package:equatable/equatable.dart';

enum LogAttendanceStatus { initial, success, failure }

class LogAttendanceState extends Equatable {
  const LogAttendanceState({
    this.status = LogAttendanceStatus.initial,
    this.data = const <Attendance>[],
    this.hasReachedMax = false,
  });

  final LogAttendanceStatus status;
  final List<Attendance> data;
  final bool hasReachedMax;

  LogAttendanceState copyWith({
    LogAttendanceStatus status,
    List<Attendance> data,
    bool hasReachedMax,
  }) {
    return LogAttendanceState(
      status: status ?? this.status,
      data: data ?? this.data,
      hasReachedMax: hasReachedMax ?? this.hasReachedMax,
    );
  }

  @override
  List<Object> get props => [status, data, hasReachedMax];
}

This is the event:

import 'package:equatable/equatable.dart';

abstract class LogAttendanceEvent extends Equatable {
  @override
  List<Object> get props => [];
}

class LogAttendanceFetched extends LogAttendanceEvent {}

This is the bloc:

import 'package:bozzetto/core/bloc/logattendance/log_attendance_event.dart';
import 'package:bozzetto/core/bloc/logattendance/log_attendance_state.dart';
import 'package:bozzetto/core/network/api/api_service.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';

class LogAttendanceBloc extends Bloc<LogAttendanceEvent, LogAttendanceState> {
  LogAttendanceBloc({@required this.apiService})
      : super(const LogAttendanceState());

  final ApiService apiService;

  @override
  Stream<Transition<LogAttendanceEvent, LogAttendanceState>> transformEvents(
    Stream<LogAttendanceEvent> events,
    TransitionFunction<LogAttendanceEvent, LogAttendanceState> transitionFn,
  ) {
    return super.transformEvents(
      events.debounceTime(const Duration(milliseconds: 500)),
      transitionFn,
    );
  }

  @override
  Stream<LogAttendanceState> mapEventToState(LogAttendanceEvent event) async* {
    if (event is LogAttendanceFetched) {
      yield await _mapPostFetchedToState(state);
    }
  }

  Future<LogAttendanceState> _mapPostFetchedToState(
      LogAttendanceState state) async {
    if (state.hasReachedMax) return state;
    try {
      if (state.status == LogAttendanceStatus.initial) {
        final response = await apiService.getLogAttendance(1, 10);
        return state.copyWith(
          status: LogAttendanceStatus.success,
          data: response.data.data,
          hasReachedMax: false,
        );
      }
      final response = await apiService.getLogAttendance(state.data.length, 10);
      return response.data.data.isEmpty
          ? state.copyWith(hasReachedMax: true)
          : state.copyWith(
              status: LogAttendanceStatus.success,
              data: List.of(state.data)..addAll(response.data.data),
              hasReachedMax: false,
            );
    } on Exception {
      return state.copyWith(status: LogAttendanceStatus.failure);
    }
  }
}

This is the UI:

class AttendanceScreen extends StatefulWidget {
  static const routeName = "/attendance_screen";

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

class _AttendanceScreenState extends State<AttendanceScreen> {
  ...

  final _scrollController = ScrollController();
  LogAttendanceBloc _logAttendanceBloc;

  ...

  void _onScroll() {
    if (_isBottom) _logAttendanceBloc.add(LogAttendanceFetched());
  }

  bool get _isBottom {
    if (!_scrollController.hasClients) return false;
    final maxScroll = _scrollController.position.maxScrollExtent;
    final currentScroll = _scrollController.offset;
    return currentScroll >= (maxScroll * 0.9);
  }

  @override
  void initState() {
    super.initState();
    ...
    _scrollController.addListener(_onScroll);
    _logAttendanceBloc = context.bloc<LogAttendanceBloc>();
  }

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

  @override
  Widget build(BuildContext context) {
    var width = context.width;
    return Scaffold(
      appBar: AppBar(
        title: Text(AppConstant.liveAttendance),
      ),
      backgroundColor: Colors.white,
      body: Container(
        width: width,
        margin: EdgeInsets.only(
          left: 18,
          right: 18,
        ),
        child: ListView(
          children: [
            ...
            Center(
              child: BlocConsumer<LogAttendanceBloc, LogAttendanceState>(
                listener: (context, state) {
                  if (!state.hasReachedMax && _isBottom) {
                    _logAttendanceBloc.add(LogAttendanceFetched());
                  }
                },
                builder: (context, state) {
                  switch (state.status) {
                    case LogAttendanceStatus.failure:
                      return Center(child: Text(AppConstant.error));
                    case LogAttendanceStatus.success:
                      if (state.data.isEmpty) {
                        return Center(child: Text(AppConstant.noLogToday));
                      }
                      return ListView.builder(
                        shrinkWrap: true,
                        itemBuilder: (BuildContext context, int index) {
                          return index >= state.data.length
                              ? Center(child: CircularProgressIndicator())
                              : Card(
                                  child: Padding(
                                    padding: const EdgeInsets.all(8.0),
                                    child: Column(
                                      crossAxisAlignment:
                                          CrossAxisAlignment.start,
                                      children: [
                                        Text("Date: ${state.data[index].date}"),
                                        Text(
                                            "In: ${state.data[index].datumIn}"),
                                        Text(
                                            "Note In: ${state.data[index].inNote}"),
                                        Text("Out: ${state.data[index].out} "),
                                        Text(
                                            "Note Out: ${state.data[index].outNote}"),
                                      ],
                                    ),
                                  ),
                                );
                        },
                        itemCount: state.hasReachedMax
                            ? state.data.length
                            : state.data.length + 1,
                        controller: _scrollController,
                      );
                    default:
                      return Center(child: CircularProgressIndicator());
                  }
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
  ...
}

What I'm missing?

cemanganas commented 3 years ago

Hi, @rrifafauzikomara

Maybe you could try this

In your BLoc

try { if (state.status == LogAttendanceStatus.initial) { final response = await apiService.getLogAttendance(1, 10); return state.copyWith( status: LogAttendanceStatus.success, data: response.data.data, //Update yours to This one hasReachedMax: response.data.data.length <= 10 ? true : false, ); } final response = await apiService.getLogAttendance(state.data.length, 10); return response.data.data.isEmpty ? state.copyWith(hasReachedMax: true) : state.copyWith( status: LogAttendanceStatus.success, data: List.of(state.data)..addAll(response.data.data), //Update yours to This one hasReachedMax: response.data.data.length <= 10 ? true : false, ); } on Exception { return state.copyWith(status: LogAttendanceStatus.failure); }

In your UI

itemBuilder: (BuildContext context, int index) { //Update yours to This one return index >= state.data.length && !state.hasReachedMax ? Center(child: CircularProgressIndicator()) : Card( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Date: ${state.data[index].date}"), Text( "In: ${state.data[index].datumIn}"), Text( "Note In: ${state.data[index].inNote}"), Text("Out: ${state.data[index].out} "), Text( "Note Out: ${state.data[index].outNote}"), ], ), ), ); },

rrifafauzikomara commented 3 years ago

Thanks @cemanganas

cemanganas commented 3 years ago

Thanks @cemanganas

Sama Sama Mas Bro @rrifafauzikomara 👍