Safe usage for useScrollController? (Flutter Hooks) - flutter

Would the following code be considered safe?
class SomeWidget extends HookWidget {
#override
Widget build(BuildContext context) {
final controller = useScrollController();
controller.addListener(_someCallback);
return ...;
}
}
I'm specifically referring to the addListener. In this ResoCoder hooks tutorial he adds the listener inside the initHook function of a custom hook.
I know that ResoCoder wrote the custom hook to dispose of the scrollController...I'm more curious as to how the controller listener behaves (I have no idea what is allowed and not allowed for listeners). Any resources on where I can learn about them would be great.
Thanks :)

Side-effects such as adding listeners should not be done directly inside build. If the widget rebuilt, that would cause the listener to be added again
Instead, you can use useEffect:
final controller = useScrollController();
useEffect(() {
controller.addListener(_someCallback);
return () => controller.removeListener(_someCallback);
}, [controller]);

Related

why use initState() in flutter, when we can just initailize a variable in the very first line of code

is there a difference in these 3 codes:
First: when i call my function inside onInit().
#override
void onInit() {
super.onInit();
fetchProductsFromAPI();
}
Second: when i call my function inside of build method, in stateless widget.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
fetchProductsFromAPI();
return GetMaterialApp(
home: ShoppingPage(),
);
}
}
Third: when i call my function outside of build method, in stateless widget.
class MyApp extends StatelessWidget {
fetchProductsFromAPI();
#override
Widget build(BuildContext context) {
return GetMaterialApp(
home: ShoppingPage(),
);
}
}
Yes, there is a difference. You can read about flutter widget life cycle to have more details:
Life cycle in flutter
https://medium.flutterdevs.com/app-lifecycle-in-flutter-c248d894b830
In summary
When you call your method outside of build method (your 3rd example).
This is what is usually recommended when you can do it.
See is there any difference between assigning value to the variable inside of initState or not in Flutter StatefulWidget?
This will be run only once, when the class is created.
Inside the initState (your 1st example)
At this moment, your widget is being created. Some getters are already available, like the context. This method is called only once.
Inside the build method (your 2nd example)
This is usually the worst approach. Your method will be called for each and every build (you can consider 1 build = 1 frame) which can lead to poor performances. It is recommended to move those calls out of the build method when possible (and if it makes sense)
See How to deal with unwanted widget build?
First:
Put it on initState then the function fetchProductsFromAPI will only call first time your widget create
Second:
I highly recommend you do not use this approach, because build method will be trigger many time when widget need to rebuild, if you put it there, your app will be fetchProductsFromAPI at a lot of unexpected times.
Example when you need to call setState() for some changes, you don't want to call fetch API
Third:
This way will cause compile error, I don't think you can put it there like your code above

Flutter GetX: Where Does Get.put Go in a Widget?

I am new to GetX and am trying to learn how to use it. I have read different tutorials that inject the controller outside of the widget's build method, and others that put it inside it.
class MyWidget extends StatelessWidget{
const MyWidget({Key? key}) : super(key:key);
//Outside...
final controller = Get.put(Controller()); //<---
#override
Widget build(BuildContext context) {
//Inside...
final controller = Get.put(Controller()); //<---
return Obx(
() => Text(controller.name)
);
}
}
Is there a difference between those two locations? If so, why?
Also, where should it go in a StatefulWidget? It seems it should not go inside the build method because it causes a stack overflow error for me.
Does the location of Get.put() matter inside a widget?
The normal way is to put the controller outside the widget, so it will be created once. if you but it inside the widget a new instance of the controller will be created each time you refresh (update) the widget.
Also, with GetX there is no need to use StatefulWidget.
When you put it inside the build method, it will create a new instance every time the widget is rebuilt, so it is better to but it outside and in most cases I think you do not need to use StatefulWidgetwith GetX even in animation.
The answers shared are correct. Thank you! I wanted to post another answer that I consider to be an even better way to do it as referenced in the documentation.
This method allows for controller access without ever putting Get.put in any of your widgets. This is also really helpful if you have a lot of controllers and need to reference them in widgets as well as from other controllers.
Here is an example:
//ControllerA
class ControllerA extends GetxController {
static ControllerA get to => Get.find();
final name = 'Bob'.obs;
someMethod(){
ControllerB.to.anotherMethod(); //'I am inside ControllerB!'
}
}
//ControllerB
class ControllerB extends GetxController {
//+++
static ControllerB get to => Get.find();
anotherMethod(){
print('I am inside ControllerB!');
}
}
And then inside Widgets:
class MyWidget extends StatelessWidget{
const MyWidget({Key? key}) : super(key:key);
#override
Widget build(BuildContext context) {
return Obx(
() => Text(ControllerA.to.name) //Bob
);
}
}
This requires that you add your Get.put declarations in main.dart so that you make sure they are all ready:
void main() {
Get.put(ControllerA());
Get.put(ControllerB());
runApp(
GetMaterialApp(...)
);
}
In my opinion, it's really clean like this--and very convenient!
If we want to keep a GetX controller in memory forever, then we should indeed instantiate it outside of a Widget's build() function (such as within main() as Clifton shows).
(Note that we can also use GetXService for persistent controllers, which allows manual disposal.)
Placing controllers inside build() functions, is the correct place when we want GetX to free memory & dispose the controller when the widget goes "out of scope". (e.g. when the user has "popped" the route from the stack.) There's also no danger in "recreating" the controller when its instantiated inside build(): Get checks for existence of a controller when Get.put() is called and skips it if the controller is already instantiated.
See this answer (and link to an explanation by one of Get's maintainers) for more info on why we should Get.put() inside build().

Flutter Provider - Call function on value change without calling build()

I am wondering if it is possible to watch for a value change inside an initState which then simply calls a function?
I basically need to start and stop a timer given the status of a class i am observing with Provider (listen: true) and was hoping there was some callback functionality that i could trigger instead of build() being called each time?
For example something like..
void initState() {
super.initState();
Provider.of<MyService>(context, listen: true).serviceRunning => {
//do stuff (start/stop my local timer)
if(serviceRunning) {
serviceRunning()
} else {
serviceStopped()
}
}
}
void serviceRunning() {
//start local timer and other bits
}
void serviceStopped() {
//stop local timer and other bits
}
Widget build(BuildContext context) {
..
}
I don't recall Provider being able to do this, so would appreciate any suggestions on how to achieve this. As mentioned above, i am just trying to save having build() get called unnecessarily.
Not a provider user here (bloc fan) but you should be able to do what you want with a ValueNotifier exposed by your service. And in your initState you would add a listener to the notifier.
And I believe you can avoid build call by using context.select<T, R>(R cb(T value)), which allows a widget to listen to only a small part of T.
Extract of the provider doc:
Widget build(BuildContext context) {
final name = context.select((Person p) => p.name);
return Text(name);
}
But I'm not sure that this is necessary.
If you find a cleaner solution please share it, it's always nice learning what works in other part of the flutter community.

Mock a Widget in Flutter tests

I am trying to create tests for my Flutter application. Simple example:
class MyWidget extends StatelessWidget {
#override
build(BuildContext context) {
return MySecondWidget();
}
}
I would like to verify that MyWidget is actually calling MySecondWidget without building MySecondWidget.
void main() {
testWidgets('It should call MySecondWidget', (WidgetTester tester) async {
await tester.pumpWidget(MyWidget());
expect(find.byType(MySecondWidget), findsOneWidget);
}
}
In my case this will not work because MySecondWidget needs some specific and complex setup (like an API key, a value in a Provider...). What I would like is to "mock" MySecondWidget to be an empty Container (for example) so it doesn't raise any error during the test.
How can I do something like that ?
There is nothing done out of the box to mock a widget. I'm going to write some examples/ideas on how to "mock"/replace a widget during a test (for example with a SizedBox.shrink().
But first, let me explain why I think this is not a good idea.
In Flutter you are building a widget tree. A specific widget has a parent and usually has one or several children.
Flutter chose a single pass layout algorithm for performance reasons (see this):
Flutter performs one layout per frame, and the layout algorithm works in a single pass. Constraints are passed down the tree by parent objects calling the layout method on each of their children. The children recursively perform their own layout and then return geometry up the tree by returning from their layout method. Importantly, once a render object has returned from its layout method, that render object will not be visited again until the layout for the next frame. This approach combines what might otherwise be separate measure and layout passes into a single pass and, as a result, each render object is visited at most twice during layout: once on the way down the tree, and once on the way up the tree.
From this, we need to understand that a parent needs its children to build to get their sizes and then render itself properly. If you remove its children, it might behave completely differently.
It is better to mock the services if possible. For example, if your child makes an HTTP request, you can mock the HTTP client:
HttpOverrides.runZoned(() {
// Operations will use MyHttpClient instead of the real HttpClient
// implementation whenever HttpClient is used.
}, createHttpClient: (SecurityContext? c) => MyHttpClient(c));
If the child needs a specific provider you can provide a dummy one:
testWidgets('My test', (tester) async {
tester.pumpWidget(
Provider<MyProvider>(
create: (_) => MyDummyProvider(),
child: MyWidget(),
),
);
});
If you still want to change a widget with another one during your tests, here are some ideas:
1. Use Platform.environment.containsKey('FLUTTER_TEST')
You can either import Platform from dart:io (not supported on web) or universal_io (supported on web).
and your build method could be:
#override
Widget build(BuildContext context) {
final isTest = Platform.environment.containsKey('FLUTTER_TEST');
if (isTest) return const SizedBox.shrink();
return // Your real implementation.
}
2. Use the annotation #visibleForTesting
You can annotate a parameter (ex: mockChild) that is only visible/usable in a test file:
class MyWidget extends StatelessWidget {
const MyWidget({
#visibleForTesting this.mockChild,
});
final Widget? child;
#override
Widget build(BuildContext context) {
return mockChild ?? // Your real widget implementation here.
}
}
And in your test:
tester.pumpWidget(
MyWidget(
mockChild: MyMockChild(),
),
);
You can mock MySecondWidget (eg using Mockito) but you do need to change your real code to create a MockMySecondWidget when in test mode, so it's not pretty. Flutter does not support object instantiation based on a Type (except through dart:mirrors but that is not compatible with Flutter), so you cannot 'inject' the type as a dependency. To determine if you are in test mode use Platform.environment.containsKey('FLUTTER_TEST') - best to determine this once upon startup and set the result as a global final variable, which will make any conditional statements quick.
One way to do it, is to wrap the child widget into a function, and pass the function to parent widget's constructor:
class MyWidget extends StatelessWidget {
final Widget Function() buildMySecondWidgetFn;
const MyWidget({
Key? key,
this.buildMySecondWidgetFn = _buildMySecondWidget
}): super(key: key);
#override
build(BuildContext context) {
return buildMySecondWidgetFn();
}
}
Widget _buildMySecondWidget() => MySecondWidget();
Then you can make up your mock widget, pass it thru buildMySecondWidgetFn in test.

Extracting Class Members like Widget Builders to a Different File?

In developing some of the screens for my flutter app, I regularly need to dynamically render widgets based on the state of the screen. For circumstances where it makes sense to create a separate widget and include it, I do that.
However, there are many use cases where what I need to render is not fit for a widget, and leverages existing state from the page. Therefore I use builder methods to render the appropriate widgets to the page. As anyone who uses Flutter knows, that can lead to lengthy code where you need to scroll up/down a lot to get to what you need to work on.
For better maintainability, I would love to move those builder methods into separate files, and then just include them. This would make it much easier to work on specific code widgets rendered and make the screen widget much cleaner.
But I haven't found a proper way to extract that dynamic widget code, which makes use of state, calls to update state, etc. I'm looking for a type of "include" file that would insert code into the main screen and render as if it's part of the core code.
Is this possible? How to achieve?
With the introduction of extension members, I came across this really neat way of achieving exactly what your described!
Say you have a State class defined like this:
class MyWidgetState extends State<MyWidget> {
int cakes;
#override
void initState() {
super.initState();
cakes = 0;
}
#override
Widget build(BuildContext context) {
return Builder(
builder: (context) => Text('$cakes'),
);
}
}
As you can see, there is a local variable cakes and a builder function. The very neat way to extract this builder now is the following:
extension CakesBuilderExtension on MyWidgetState {
Widget cakesBuilder(BuildContext context) {
return Text('$cakes');
}
}
Now, the cakes member can be accessed from the extension even if the extension is placed in another file.
Now, you would update your State class like this (the builder changed):
class MyWidgetState extends State<MyWidget> {
int cakes;
#override
void initState() {
super.initState();
cakes = 0;
}
#override
Widget build(BuildContext context) {
return Builder(
builder: cakesBuilder,
);
}
}
The cakesBuilder can be referenced from MyWidgetState, even though it is only declared in the CakesBuilderExtension!
Note
The extension feature requires Dart 2.6. This is not yet available in the stable channel, but should be around the end of 2019 I guess. Thus, you need to use the dev or master channels: flutter channel dev or flutter channel master and update the environment constraint in your pubspec.yaml file:
environment:
sdk: '>=2.6.0-dev.8.2 <3.0.0'