Bloc: BlocBuilder is not rebuilding when appending a list

Created on 29 Sep 2020  路  2Comments  路  Source: felangel/bloc

Hello, I am a newbie to bloc pattern in Flutter.

I tried to build an app which user can tap on an image and label some circle on top of the image. I also need undo-redo function so replay_bloc seems a perfect package for this situation.

I think the task is not difficult so I basically modified the example of the Counter app from here. When it was the first tap, the app correctly shows the first point. But when I continue clicking on the image, I see from log that points are appended in the state but there is no update on the UI. On the other hand, when I make plenty of clicks and reset, I see those points appear after I undo the bloc. So I would like to know if I misunderstand the usage of flutter_bloc, so there is no change in the stream.

Minimal code to reproduce the problem:
Event:

class PointEvent extends ReplayEvent {}

class AddPoint extends PointEvent {
  final Offset point;
  AddPoint(this.point);
}

class ResetPoint extends PointEvent {}```
State

abstract class PointState extends Equatable {
  const PointState();
  @override
  List<Object> get props => [];
}

class PointTapped extends PointState {
  final List<Offset> points;
  const PointTapped({
    this.points,
  });

  PointTapped addPoints({Offset point,}) {
    return PointTapped(
      points: this.points..add(point),
    );
  }

  @override
  List<Object> get props => [points];
}

Bloc

class PointBloc extends ReplayBloc<PointEvent, PointState>{
  PointBloc() : super(PointTapped(points: []));

  @override
  Stream<PointState> mapEventToState(PointEvent event) async* {
    final currentState = state;
    if (event is AddPoint) {
      if (currentState is PointTapped){
        print(currentState.points);
        yield currentState.addPoints(point: event.point);
      }
    } else if (event is ResetPoint) {
      yield PointTapped(points: []);
    }
  }
}

Main component

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:replay_bloc/replay_bloc.dart';
import 'dart:ui';
void main() async {
  runApp(App());
}

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => PointBloc(),
      child: MaterialApp(
        home: PointPage(),
      ),
    );
  }
}

class PointPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Pointer'),
        actions: [
          BlocBuilder<PointBloc, PointState>(
            builder: (context, state) {
              final bloc = context.bloc<PointBloc>();
              return IconButton(
                icon: const Icon(Icons.undo),
                onPressed: bloc.canUndo ? bloc.undo : null,
              );
            },
          ),
          BlocBuilder<PointBloc, PointState>(
            builder: (context, state) {
              final bloc = context.bloc<PointBloc>();
              return IconButton(
                icon: const Icon(Icons.redo),
                onPressed: bloc.canRedo ? bloc.redo : null,
              );
            },
          ),
        ],
      ),
      body: Center(
        child: Container(
          width: MediaQuery.of(context).size.width*0.75,
          height: MediaQuery.of(context).size.height*0.6,
          decoration: BoxDecoration(
            color: Colors.black.withOpacity(0.4),),
          child: FrontBuild(),
        ),
      ),
      floatingActionButton: Padding(
        padding: const EdgeInsets.symmetric(vertical: 4.0),
        child: FloatingActionButton(
          child: const Icon(Icons.delete_forever),
          onPressed: () => context.bloc<PointBloc>().add(ResetPoint()),
        ),
      ),
    );
  }
}

class FrontBuild extends StatelessWidget {
  final Image pic = Image.network("https://static.wikia.nocookie.net/dogelore/images/9/97/Doge.jpg");
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Stack(
        children: <Widget>[
          Container(
            child: pic,
          ),
          Container(
              decoration: BoxDecoration(
                  borderRadius: BorderRadius.all(Radius.circular(20.0)),
                  boxShadow: [
                    BoxShadow(
                      color: Colors.red.withOpacity(0.4),
                      blurRadius: 5.0,
                      spreadRadius: 1.0,
                    )
                  ]),
              child: BlocBuilder<PointBloc, PointState>(
                  builder: (context, state) {
                    if (state is PointTapped){
                      return GestureDetector( //recieve user tap onscreen
                        behavior: HitTestBehavior.translucent,
                        onTapUp: (details){
                          context.bloc<PointBloc>().add(AddPoint(details.localPosition));
                        },
                        child: SizedBox.expand(
                          child: CustomPaint(
                            painter: UserCustomPainter(points: state.points),
                          ),
                        )
                      );
                    }
                    else{
                      return null;
                    }
                  })
          ),
        ],
      ),
    );
  }
}

class UserCustomPainter extends CustomPainter{
  List <Offset> points;
  UserCustomPainter({this.points});
  @override
  void paint(Canvas canvas, Size size) {
    Paint background = Paint()..color = Colors.transparent;
    Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
    canvas.drawRect(rect, background);
    canvas.clipRect(rect);

    Paint paint =Paint();
    paint.color = Colors.red;
    paint.strokeWidth = 2.0;
    paint.isAntiAlias = true;
    paint.style = PaintingStyle.stroke;
    paint.strokeCap = StrokeCap.round;

    for(int x = 0; x < points.length; x++){
      Rect circleRect = Rect.fromCircle(center: points[x], radius: 20);
      canvas.drawOval(circleRect, paint);}
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

Any help will be appreciated, sorry for asking such a simple question.

question

Most helpful comment

Yes, this indeed fixed the problem. I really appreciate your help.

All 2 comments

Hi @hyiip 馃憢
Thanks for opening an issue!

I believe the issue is you're mutating the list of points rather than creating a new instance. This causes the bloc to think that nothing has changed because the list of points has the same reference (is identical) to the previous list of points.

You should be able to just update addPoints to create a new list:

PointTapped addPoints({Offset point,}) {
  return PointTapped(
    points: List.of(this.points)..add(point),
  );
}

Closing for now but feel free to comment with additional questions if you're still having trouble and I'm happy to continue the conversation 馃憤

Yes, this indeed fixed the problem. I really appreciate your help.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Reidond picture Reidond  路  3Comments

timtraversy picture timtraversy  路  3Comments

nerder picture nerder  路  3Comments

wheel1992 picture wheel1992  路  3Comments

tigranhov picture tigranhov  路  3Comments