Project
Hi,
I recently discovered flutter bloc and now I'm trying to understand how exactly this works. My goal is to separate logic from widget classes in order to easily manage my projects.
Problem
I'm stuck with something that is very simple using classic setState, but I was trying to achieve this with bloc.
Here is an old widget of mine
Widget build(BuildContext context) {
return AnimatedOpacity(
duration: Duration(milliseconds: 200),
opacity: _opacity,
curve: Curves.easeInOut,
child: Text(
_currentTitle,
style: TitleTextStyle,
),
);
}
Is it possible to control _opacity and _currentTitle from a bloc? Something like this:
List<String> titles = ['title1', 'title2', ....];
int myIndex;
#override
Stream<SomeBlocState> mapEventToState(SomeEvent event,)
async* {
....
if (event is SomeSpecificEvent)
setWidgetTitle(titles[myIndex]);
....
}
I am trying to avoid buiding different state for each possible title, that would be a mess
Thanks
You can try and make the title widget build in a StreamBuilder instead. That will give you a clean separation of responsibilities. So that the BLoC doesn't know of any state changes or widget specific things.
Widget build(BuildContext context) {
return AnimatedOpacity(
duration: Duration(milliseconds: 200),
opacity: _opacity,
curve: Curves.easeInOut,
child: StreamBuilder<String>(
stream: myBloc.title,
builder: (_, snap) {
return Text(
snap?.data ?? 'Some default',
style: TitleTextStyle,
);
}
),
);
}
And you create some logic in your BLoC that emits the title as needed
List<String> titles = ['title1', 'title2', ....];
int myIndex;
final _myObservableTitles = PublishSubject();
Stream<String> get title => _myObservableTitles.stream;
....
if (event is SomeSpecificEvent)
_myObservableTitles.add(titles[myIndex]);
....
Related
I've built a PageView and want to control page swiping programmatically using a Bloc.
I'm using a PageController and listening for my bloc's ShowPage state. Then calling pageController.animateTo to animate the PageView to the desired page.
The bloc builder builds the PageView.
I suspect the problem I'm having is caused by the BlocBuilder building a new PageView on every state change and thus negating the pageController.animateTo.
My question is firstly ... can the BlockBuilder be prevented from firing/building for certain states (so it doesn't 'overwrite' my pageController changes).
or
Is there a better (correct) way of implementing a PageView with bloc?
I've copied the screen code the basic cubit below for info.
class _TestScreenState extends State<TestScreen> {
final PageController _pageController = PageController(initialPage: 4);
double page = 0;
#override
Widget build(BuildContext context) {
return BlocConsumer<TestCubit, TestState>(listener: (context, state) async {
if (state is DeviceLogsInitial) {
await _animateTo(3);
}
if (state is ShowPage) {
await _animateTo(state.page);
}
}, builder: (context, state) {
// _log.info('Building dialog : page ${_pageController.page}');
return Scaffold(
appBar: AppBar(
title: const Text('Test'),// ${_pageController.page}'),
actions: [
TextButton(
onPressed: () {
page = _nextPage(page);
BlocProvider.of<TestCubit>(context).animate(page);
},
child: const Text('swipe'))
],
),
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: _pageController,
children: const [
Page0(),
Page1(),
Page2(),
Page3(),
],
),
);
});
}
double _nextPage(double page) {
if (page > 3) {
page = 0;
} else {
page = page + 1;
}
return page;
}
Future<void> _animateTo(double _page) async {
_pageController.animateTo(
_page,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
}
class TestCubit extends Cubit<TestState> {
TestCubit() : super(TestInitial());
Future<void> animate(double page) async {
emit(ShowPage(page));
}
}
This is simply a case of using the wrong PageController method for animating between pages.
The method you should be calling is pageController.animateToPage and not pageController.animateTo.
You could also use jumpToPage if you didn't require animation (and not jumpTo which would cause a similar issue).
I am using widget Image in flutter and it contains ternary or conditional statement.. here is the code:
#override
Widget build(BuildContext context) {
print("build widget");
return Image(
height: 100,
width: 100,
image: AssetImage('flutter.png'),
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded)
return child;
else {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
child: frame != null ? child : widget.placeholder,
);
}
},
);
}
So, it depends on frame value, when the value changes.. the widget will be rebuilt from the start. In my case, print("build widget"); will be printed 2 times. Actually I am wrapping my widget with ValueListenableBuilder, so that it will be built only for widget that changes, it seems that I can not wrap the code abode using ValueListenableBuilder. So, is there a way to prevent the code above to build 2 times ?
I'm trying to implement pagination but I can't find any examples of how I should create the controller Listener function - or where I should put it. Please advise. Let me know if I should add more info too.
Currently, my listener function looks like this:
(within initState)
pagecontroller.addListener(() {
print(pagecontroller.page);
if (pagecontroller.page == _postslist.length-1) {
fetchMore();
}
});
What happens currently is that the function is only called once, and subsequently never called later on.
I don't know if this problem still exists (it's been six months since you've asked), but since this question still doesn't have an answer that is marked as correct I'll try.
If I understand correctly you want to load more items into your PageView once you've reached the last item of your PageView. You don't need a listener in your initState for that. You can just check if you've reached the last item in onPageChanged and then load more items.
It should work like this:
PageView.builder(
controller: _pageController,
itemCount: _items.length,
onPageChanged: (i) {
if (i == _items.length - 1) {
getMoreItems().then((value) {
setState(() {
_items= value;
});
});
}
},
)
I guess you are trying to listen to pageController to get the currentPage. If that's the case, you should fire an event using the PageController by using its methods (animateToPage, jumpToPage, nextPage, previousPage), so that it can evoke your listener.
I assume my page transitions are handled by the PageView.builder
You can find the PageView.builder description in the documentation like this:
This constructor is appropriate for page views with a large (or infinite) number of children because the builder is called only for those children that are actually visible.
So it supports you in building the screens efficiently in case of large number of pages. You'll still need to handle navigation between pages on your own.
The link I've included above has an example you can refer to in terms of PageController usage. I'll include it here for convenience:
class MyPageView extends StatefulWidget {
MyPageView({
Key key
}): super(key: key);
_MyPageViewState createState() => _MyPageViewState();
}
class _MyPageViewState extends State < MyPageView > {
PageController _pageController;
#override
void initState() {
super.initState();
_pageController = PageController();
}
#override
void dispose() {
_pageController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: PageView(
controller: _pageController,
children: [
Container(
color: Colors.red,
child: Center(
child: RaisedButton(
color: Colors.white,
onPressed: () {
if (_pageController.hasClients) {
_pageController.animateToPage(
1,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
}
},
child: Text('Next'),
),
),
),
Container(
color: Colors.blue,
child: Center(
child: RaisedButton(
color: Colors.white,
onPressed: () {
if (_pageController.hasClients) {
_pageController.animateToPage(
0,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
}
},
child: Text('Previous'),
),
),
),
],
),
),
);
}
}
so I am trying to build up a list in my provider from a Future Call.
So far, I have the following ChangeNotifier class below:
class MainProvider extends ChangeNotifier {
List<dynamic> _list = <dynamic>[];
List<dynamic> get list => _list;
int count = 0;
MainProvider() {
initList();
}
initList() async {
var db = new DatabaseHelper();
addToList(Consumer<MainProvider>(
builder: (_, provider, __) => Text(provider.count.toString())));
await db.readFromDatabase(1).then((result) {
result.forEach((item) {
ModeItem _modelItem= ModeItem.map(item);
addToList(_modelItem);
});
});
}
addToList(Object object) {
_list.add(object);
notifyListeners();
}
addCount() {
count += 1;
notifyListeners();
}
}
However, this is what happens whenever I use the list value:
I can confirm that my initList function is executing properly
The initial content from the list value that is available is the
Text() widget that I firstly inserted through the addToList function, meaning it appears that there is only one item in the list at this point
When I perform Hot Reload, the rest of the contents of the list seems to appear now
Notes:
I use the value of list in a AnimatedList widget, so I am
supposed to show the contents of list
What appears initially is that the content of my list value is only one item
My list value doesn't seem to automatically update during the
execution of my Future call
However, when I try to call the addCount function, it normally
updates the value of count without needing to perform Hot Reload -
this one seems to function properly
It appears that the Future call is not properly updating the
contents of my list value
My actual concern is that on initial loading, my list value doesn't
properly initialize all it's values as intended
Hoping you guys can help me on this one. Thank you.
UPDATE: Below shows how I use the ChangeNotifierClass above
class ParentProvider extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<MainProvider>(
create: (context) => MainProvider(),
),
],
child: ParentWidget(),
);
}
}
class ParentWidget extends StatelessWidget {
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
#override
Widget build(BuildContext context) {
var mainProvider = Provider.of<MainProvider>(context);
buildItem(BuildContext context, int index, Animation animation) {
print('buildItem');
var _object = mainProvider.list[index];
var _widget;
if (_object is Widget) {
_widget = _object;
} else if (_object is ModelItem) {
_widget = Text(_object.unitNumber.toString());
}
return SizeTransition(
key: ValueKey<int>(index),
axis: Axis.vertical,
sizeFactor: animation,
child: InkWell(
onTap: () {
listKey.currentState.removeItem(index,
(context, animation) => buildItem(context, index, animation),
duration: const Duration(milliseconds: 300));
mainProvider.list.removeAt(index);
mainProvider.addCount();
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: _widget,
),
),
),
);
}
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(32.0),
child: mainProvider.list == null
? Container()
: AnimatedList(
key: listKey,
initialItemCount: mainProvider.list.length,
itemBuilder:
(BuildContext context, int index, Animation animation) =>
buildItem(context, index, animation),
),
),
),
);
}
}
You are retrieving your provider from a StatelessWidget. As such, the ChangeNotifier can't trigger your widget to rebuild because there is no state to rebuild. You have to either convert ParentWidget to be a StatefulWidget or you need to get your provider using Consumer instead of Provider.of:
class ParentWidget extends StatelessWidget {
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
#override
Widget build(BuildContext context) {
return Consumer<MainProvider>(
builder: (BuildContext context, MainProvider mainProvider, _) {
...
}
);
}
As an aside, the way you are using provider is to add the MainProvider to its provider and then retrieve it from within its immediate child. If this is the only place you are retrieving the MainProvider, this makes the provider pattern redundant as you can easily just declare it within ParentWidget, or even just get your list of images using a FutureBuilder. Using provider is a good step toward proper state management, but also be careful of over-engineering your app.
im new to bloc pattern in flutter.
One of my states classes have a list of widget and an index as a field. My goal is to update the child of an Animated Switcher using this state's widgets.
return AnimatedSwitcher(
duration: Duration(milliseconds: 500),
child: BlocBuilder<WelcomeBloc, WelcomeBlocState>(
builder: (context, state) {
if(state is MyState)
return state.widgetList[state.index];
else return Container();
},
),
);
I have also tried the other way around, returning the animated switcher in the bloc builder and the result is the same
When yield is called, the widget is changed but without any animation.
What am I missing?
A child widget of AnimatedSwitcher has to change:
return BlocBuilder<WelcomeBloc, WelcomeBlocState>(
builder: (context, state) {
return AnimatedSwitcher(
duration: Duration(milliseconds: 500),
child: state is MyState ? state.widgetList[state.index] : Container(key: Key('key2')),
);
},
);
And don't forget to set different keys for child widgets.