I am trying to learn both state management & dependancy injection with this demo project. I'm trying to demo injecting some methods all over the place like I may need to in my program. I'm using GetX because I like being able to do this without context in non-widget classes.
So my problem here is the last method, summationReturns(), in the last class below. Attempts to take methods with return statements and add them together. I call this in two places. In the floating button, this works fine but in my text widget I get a dirty state error.
Why is this not working when everything else works? And I assume this will be a corollary from the last question, what is a dirty state? Seems like two questions but I would imagine that they are one in the same.
///
///
/// DEMO PROJECT WORKING OUT GETX
/// WORKOUT DEPENDANCY INJECTION AND STATE MANAGEMENT
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_state_manager/get_state_manager.dart';
void main() {
runApp(GetMaterialApp(
home: Home(),
debugShowCheckedModeBanner: false,
));
}
class Home extends StatelessWidget {
// Injection of dependancy
final Controller controller = Get.put(Controller());
final Observable observable = Get.put(Observable());
final SimpleMath simpleMath = Get.put(SimpleMath());
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Get builders:'),
GetBuilder<Controller>(builder: (controller) {
return Text(controller.count.toString());
}),
GetBuilder<Controller>(builder: (controller) {
return Text(controller.countList.toString());
}),
GetBuilder<Controller>(builder: (controller) {
return Text(controller.returnCount().toString());
}),
GetBuilder<Controller>(builder: (controller) {
return Text(controller.returnList().toString());
}),
SizedBox(height: 20.0),
Text('Get observables:'),
Obx(() => Text(observable.count.value.toString())),
Obx(() => Text(observable.countList.value.toString())),
Obx(() => Text(observable.returnCount().toString())),
Obx(() => Text(observable.returnList().toString())),
SizedBox(height: 20.0),
Text('Get from other class:'),
GetBuilder<SimpleMath>(builder: (simpleMath) {
return Text('Variable summation: ' + simpleMath.summationVariables().toString());
}),
GetBuilder<SimpleMath>(builder: (simpleMath) {
return Text(simpleMath.summationReturns().toString());
}),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
controller.crunch();
observable.crunch();
simpleMath.summationVariables();
simpleMath.summationReturns();
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
class Controller extends GetxController {
int count = 0;
List<int> countList = [];
void crunch() {
count += 1;
countList.add(count);
update();
}
int returnCount() {
return count;
}
List<int> returnList() {
return countList;
}
}
class Observable extends GetxController {
RxInt count = 0.obs;
Rx<RxList> countList = RxList().obs;
void crunch() {
count.value += 1;
countList.value.add(count.value);
}
int returnCount() {
return count.value;
}
List<dynamic> returnList() {
return countList.value.toList();
}
}
class SimpleMath extends GetxController {
final Controller controller = Get.find<Controller>();
final Observable observable = Get.find<Observable>();
int summationVariables() {
int sum = controller.count + observable.count.value;
update();
return sum;
}
int summationReturns() {
int sum = controller.returnCount() + observable.returnCount();
print('Summation of return values: ' + sum.toString());
update();
return sum;
}
}
Error:
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following assertion was thrown building GetBuilder<SimpleMath>(dirty, state:
GetBuilderState<SimpleMath>#4d62d):
setState() or markNeedsBuild() called during build.
This GetBuilder<SimpleMath> widget cannot be marked as needing to build because the framework is
already in the process of building widgets. A widget can be marked as needing to be built during
the build phase only if one of its ancestors is currently building. This exception is allowed
because the framework builds parent widgets before children, which means a dirty descendant will
always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was:
GetBuilder<SimpleMath>
The widget which was currently being built when the offending call was made was:
GetBuilder<SimpleMath>
The relevant error-causing widget was:
GetBuilder<SimpleMath>
file:///Users/robertobuttazzoni/Documents/Flutter%20Tutorials/Flutter%20Learning/getx_basics/getx_basics/lib/main.dart:57:13
Calling update while build is ongoing is an example of dirty scenario. To fix your issue, do not call update inside the GetBuilder.
Sample...
In Home
GetBuilder<SimpleMath>(
builder: (simpleMath) => Text('Variable summation: ' +
simpleMath
.summationVariables(shouldUpdate: false)
.toString())),
GetBuilder<SimpleMath>(
builder: (simpleMath) => Text(simpleMath
.summationReturns(shouldUpdate: false)
.toString())),
In SimpleMath
int summationVariables({bool shouldUpdate = true}) {
int sum = controller.count + observable.count.value;
if (shouldUpdate) update();
return sum;
}
int summationReturns({bool shouldUpdate = true}) {
int sum = controller.returnCount() + observable.returnCount();
print('Summation of return values: ' + sum.toString());
if (shouldUpdate) update();
return sum;
}
Related
I'm new to flutter BloC and I'm confused about its state usage. From the counter app, I built another basic app to show a different widget based on its state. But not sure how to do that.
Cubits:
part 'home_bloc_state.dart';
class HomeBlocCubit extends Cubit<HomeBlocState> {
HomeBlocCubit() : super(HomeBlocInitial());
void showHome() {
emit(HomeBlocInitial());
}
void showStudents() {
emit(HomeBlocStudents());
}
}
//state
part of 'home_bloc_cubit.dart';
#immutable
abstract class HomeBlocState {}
class HomeBlocInitial extends HomeBlocState {}
class HomeBlocStudents extends HomeBlocState {}
With a button widget I can add/toggle events:
final myBloc = BlocProvider.of<HomeBlocCubit>(context);
return Container(
child: ElevatedButton(
onPressed: () {
myBloc.showHome();
},
child: Text('Home'),
), )
To show events on UI I would like to return a Text widget based on state.
#override
Widget build(BuildContext context) {
return Container(child: Align(child:
BlocBuilder<HomeBlocCubit, HomeBlocState>(
builder: (context, state) {
//do something based on state.
//How to check or compare or get data from this state?
switch (state) {
case HomeBlocState: //error, - how to use?
break;
}
return Text(
"Welcome to this page: " + state.toString(),
style: TextStyle(
fontSize: 60 ),
);
})));
}
How to show UI based on state?
You may check whether state is in a required state by calling
if (state is HomeBlocState) {
...
}
If you prefer switches:
switch (state.runtimeType) {
case HomeBlocInitial:
return const HomeInitial();
default:
// probably throw
return const SizedBox();
}
A more mature approach (if you're good with code generation) would be to use freezed for states.
In that case, your state would be declared as:
#freezed
class HomeBlocState with _$Failfast{
const factory HomeBlocState.initial() = _Initial;
const factory HomeBlocState.students() = _Students;
}
And inside BlocBuilder you'll have:
return state.map(
initial: (initialState) => ...,
students: (studentsState) => ...,
);
You can do it like this:
if(state is HomeBlocInitial){
return MyInitWidget();
} else if( state is HomeBlocStudents){
return StudentsWidget();
} else {
return SomethingWentWrongWidget();
}
For a good example check this out: https://bloclibrary.dev/#/recipesfluttershowsnackbar?id=ui-layer
I'm using the Provider package for state management in a Flutter app and I have a list model extending ChangeNotifier.
In the list model there is a method to replace a certain element in the list like this:
class MyListModel extends ChangeNotifier {
List<MyListItem> _myList = [];
void replace(Data data) {
int index = _findById(data.id);
if(index == -1) {
return;
}
_myList[index] = MyListItem(data);
log("After replace: " + _myList.toString());
notifyListeners();
}
void add(MyListItem myItem) {
_myList.add(myItem);
notifyListeners();
}
void remove(MyListItem myItem) {
_myList.remove(myItem);
notifyListeners();
}
}
This is the lis and the list item class where the provider is consumed:
class _MyListView extends StatelessWidget {
final Data _data;
const _SelectUpcomingMealList(this.upcomingMeal);
#override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: (context, index) {
return MyListItem(_data);
}
);
}
}
class MyListItem extends StatelessWidget {
final Data _data;
MyListItem(this._data);
#override
Widget build(BuildContext context) {
return Consumer<MyListModel>(
builder: (context, myListModel, children) => ListTile(
title: Text(_data.name),
subtitle: Text(_data.description),
trailing: const Icon(Icons.add),
onTap: () => replaceMyItem(myListModel, context),
)
);
}
void replaceMyItem(MyListModel myListModel, BuildContext context) {
myListModel.replace(_data);
Navigator.pop(context);
}
}
For some reason the UI is not updating and the replaced item is not displayed, the old item is visible. The logging shows that the list is properly updated (the index also properly calculated), the replaced element is there, but the UI does not update.
The add() and remove() methods work, in these cases the UI properly reflects the change.
Is there something I'm missing in case of an item being replaced?
I'm trying to change the variable from another stateful class.
class first extends statefulwidget {
bool text = false;
Widget build(BuildContext context) {
setState((){});
return Container(
child: text ? Text('Hello') : Text('check')
);
}
}
class second extends statefulwidget {
Widget build(BuildContext context) {
return Container(
child: IconButton(
onPressed: () {
first fir = first();
setState((){
fir.test = true;
});
}
)
);
}
}
widget shows only check not showing Hello
This is my code...Ignore spelling mistakes and camelcase
Give me the solutions if you know..
If you are trying to access data on multiple screens, the Provider package could help you. It stores global data accessible from all classes, without the need of creating constructors. It's good for big apps.
Here are some steps to use it (there is also a lot of info online):
Import provider in pubspec.yaml
Create your provider.dart file. For example:
class HeroInfo with ChangeNotifier{
String _hero = 'Ironman'
get hero {
return _hero;
}
set hero (String heroName) {
_hero = heroName;
notifyListeners();
}
}
Wrap your MaterialApp (probably on main.dart) with ChangeNotifierProvider.
return ChangeNotifierProvider(
builder: (context) => HeroInfo(),
child: MaterialApp(...),
);
Use it on your application! Call the provider inside any build method and get data:
#override
Widget build(BuildContext context){
final heroProvider = Provider.of<HeroInfo>(context);
return Column {
children: [
Text(heroProvider.hero)
]
}
}
Or set data:
heroProvider.hero = 'Superman';
try to reference to this answer, create function to set boolean in class1 and pass as parameter to class 2 and execute it :
typedef void MyCallback(int foo);
class MyClass {
void doSomething(int i){
}
MyOtherClass myOtherClass = new MyOtherClass(doSomething);
}
class MyOtherClass {
final MyCallback callback;
MyOtherClass(this.callback);
}
Flutter riverpod is not notifying the Consumer on the state change when the StateNotifier's type is List, while the same implementation works just fine for other types.
here, I provided a minimal reproducable example:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
home: MyHomePage(),
),
);
}
}
class CounterState extends StateNotifier<List<int>> {
static final provider = StateProvider(
(ref) => CounterState(),
);
int get last {
print('last');
return state.last;
}
int get length {
print('len');
return state.length;
}
// the body of this will be provided below
add(int p) {}
CounterState() : super(<int>[0]);
}
class MyHomePage extends ConsumerWidget {
#override
Widget build(BuildContext context, watch) {
void _incrementCounter() {
final _count = Random.secure().nextInt(100);
context.read(CounterState.provider.notifier).state.add(_count);
}
var count = watch(CounterState.provider.notifier).state.length;
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text(
'You have pushed the button this many times: $count',
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: Icon(Icons.add),
),
);
}
}
as for the add method, I tried implementing it in a lot of ways, but neither works.
here is what I tried:
1: just add it straight away:
add(int p) {
state.add(p);
}
2: I also tried the solution suggested in this answer:
add(int p) {
state = [...state, p];
}
3: I tried to destroy the list entirely, and reassign it:
add(int p) {
final _state = [];
// copy [state] to [_state]
for (var item in state) {
_state.add(item);
}
// empty the state
state = [];
// add the new element
_state.add(p);
// refill [state] from [_state]
for (var item in _state) {
state.add(item);
}
print(state.length); // it continues until here and prints
}
Firstly, you are not creating the correct provider to listen to a StateNotifier. You need to change this:
static final provider = StateProvider(
(ref) => CounterState(),
);
to this:
static final provider = StateNotifierProvider<CounterState, List<int>>(
(ref) => CounterState(),
);
Please refer to the Riverpod documentation about the different types of providers.
Secondly, you are not actually watching for state changes, but you are just getting the state object from the notifier.
Change this line:
var count = watch(CounterState.provider.notifier).state.length;
to this:
final count = watch(CounterState.provider).length;
also, your increment method is not correct for StateNotifier providers. Please change this:
context.read(CounterState.provider.notifier).state.add(_count);
to this:
context.read(CounterState.provider.notifier).add(_count);
It should rebuild now when the state changes. However, you do need an implementation of your add method that actually changes the state object itself. I would suggest the second variant you mentioned, that is in my opinion the nicest way to do this:
add(int p) {
state = [...state, p];
}
#TmKVU explained well, so I'm skipping that part. You can also follow riverpod document.
here is my example of riverPod:
stateNotifierProvider
stateProvider
Your widget
import 'dart:math';
import 'package:stack_overflow/exports.dart';
class CounterState extends StateNotifier<List<int>> {
static final provider = StateNotifierProvider(
(ref) => CounterState(),
);
int get last {
print('last');
return state.last;
}
int get length {
print('len');
return state.length;
}
// the body of this will be provided below
add(int p) {}
CounterState() : super(<int>[0]);
}
class MyHomePageSSSS extends ConsumerWidget {
#override
Widget build(BuildContext context, watch) {
void _incrementCounter() {
final _count = Random.secure().nextInt(100);
context.read(CounterState.provider.notifier).state =
context.read(CounterState.provider.notifier).state..add(_count);
}
final countprovider = watch(CounterState.provider);
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text(
'You have pushed the button this many times: ${countprovider.length}',
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: Icon(Icons.add),
),
);
}
}
my use case is to create a list view of articles (each item have the same look, there could be huge amount of articles, e.g. > 10000). I tried with
- ListView with ListView.builder: it supposes only to render the item when the item is displayed
- ScrollController: to determine when to load the next items (pagination)
- then I use List to store the data fetched from restful API using http, by adding the data from http to the List instance
this approach is OK, but in case the user keeps on scrolling pages, the List instance will have more and more items, it can crash with stack Overflow error.
If I don't call List.addAll(), instead I assign the data fetched from api, like: list = data;
I have problem that when the user scroll up, he/she won't be able to see the previous items.
Is there a good approach to solve this? Thanks!
import 'package:flutter/material.dart';
import 'package:app/model.dart';
import 'package:app/components/item.dart';
abstract class PostListPage extends StatefulWidget {
final String head;
DealListPage(this.head);
}
abstract class PostListPageState<T extends PostListPage> extends State<PostListPage> {
final int MAX_PAGE = 2;
DealListPageState(String head) {
this.head = head;
}
final ScrollController scrollController = new ScrollController();
void doInitialize() {
page = 0;
try {
list.clear();
fetchNextPage();
}
catch(e) {
print("Error: " + e.toString());
}
}
#override
void initState() {
super.initState();
this.fetchNextPage();
scrollController.addListener(() {
double maxScroll = scrollController.position.maxScrollExtent;
double currentScroll = scrollController.position.pixels;
double delta = 200.0; // or something else..
if ( maxScroll - currentScroll <= delta) {
fetchNextPage();
}
});
}
#override
void dispose() {
scrollController.dispose();
super.dispose();
}
void mergeNewResult(List<PostListItem> result) {
list.addAll(result);
}
Future fetchNextPage() async {
if (!isLoading && mounted) {
page++;
setState(() {
isLoading = true;
});
final List<PostListItem> result = await doFetchData(page);
setState(() {
if (result != null && result.length > 0) {
mergeNewResult(result);
} else {
//TODO show notification
}
isLoading = false;
});
}
}
Future doFetchData(final int page);
String head;
List<PostListItem> list = new List();
var isLoading = false;
int page = 0;
int pageSize = 20;
final int scrollThreshold = 10;
Widget buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: isLoading ? 1.0 : 0.0,
child: new CircularProgressIndicator(),
),
),
);
}
#override
Widget build(BuildContext context) {
ListView listView = ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: (BuildContext context, int index) {
if (index == list.length) {
return buildProgressIndicator();
}
if (index > 0) {
return Column(
children: [Divider(), PostListItem(list[index])]
);
}
return PostListItem(list[index]);
},
controller: scrollController,
itemCount: list.length
);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text(head),
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
onPressed: () {
},
),
// action button
IconButton(
icon: Icon(Icons.more_horiz),
onPressed: () {
},
),
]
),
body: new RefreshIndicator(
onRefresh: handleRefresh,
child: listView
),
);
}
Future<Null> handleRefresh() async {
doInitialize();
return null;
}
}
in my case, when the list length is 600, I start to get stack overflow error like:
I/flutter ( 8842): Another exception was thrown: Stack Overflow
I/flutter ( 8842): Another exception was thrown: Stack Overflow
screen:
enter image description here
somehow flutter doesn't show any more details of the error.
I wrote some sample code for a related question about paginated scrolling, which you could check out.
I didn't implement cache invalidation there, but it would easily be extendable using something like the following in the getPodcast method to remove all items that are more than 100 indexes away from the current location:
for (key in _cache.keys) {
if (abs(key - index) > 100) {
_cache.remove(key);
}
}
An even more sophisticated implementation could take into consideration the scroll velocity and past user behavior to lay out a probability curve (or a simpler Gaussian curve) to fetch content more intelligently.