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.
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.
Most helpful comment
Yes, this indeed fixed the problem. I really appreciate your help.