Flutter: reusable widget and BuildContext - flutter

In a flutter app, let say I have this widget class with multiple widgets inside (ie. just a typical single long Widget build() class with multiple widgets inside). Then these are split into multiple children widgets, with their own classes: As an example,
Before:
class Parents extend StatelessWidget{
Widget build(BuildContext context){
//Parent
return Column{
children: <Widget>[
//Child 1
Container('something inside'),
//Child 2
Container('something inside'),
//Child 3
Container('something inside'),
...
]
}
}
}
Now:
class Parents extend StatelessWidget{
Widget build(BuildContext context){
//Parent
return Column{
children: <Widget>[
//Child 1
myContainer('first child'),
//Child 2
myContainer('first child'),
//Child 3
myContainer('first child'),
...
]
}
}
}
class myContainer extend StatelessWidget{
Widget build(BuildContext context){
//child reusable widget
return Container{
'something inside'
}
}
}
The question that I have is this Widget build(BuildContext context).
In the above example, I called the myContainer class three times in the parent class. In my mind, it means that the build widget is called four times (one with parent, 3 times from each child).
I mean, I saw a bunch of examples online that above approach is recommended, but is it really a proper way of doing it? I may not understand the flutter completely yet, but since it is a widget tree, would it be more efficient (in terms of performance wise) to simply pass the parent context down to the children? like below:
class myContainer extend StatelessWidget{
final BuildContext parentContext;
Widget build(parentContext){
//child reusable widget
return Container{
'something inside'
}
}
}
Both approaches seem to work but wanted to see if I am way off with my way of thinking. I don't fully understand the mechanism of Context and any clarification would be super appreciated!
Thanks guys!

From the docs:
Each widget has its own BuildContext, which becomes the parent of the
widget returned by the StatelessWidget.build or State.build function.
(And similarly, the parent of any children for RenderObjectWidgets.)
In particular, this means that within a build method, the build
context of the widget of the build method is not the same as the build
context of the widgets returned by that build method. This can lead to
some tricky cases. For example, Theme.of(context) looks for the
nearest enclosing Theme of the given build context. If a build method
for a widget Q includes a Theme within its returned widget tree, and
attempts to use Theme.of passing its own context, the build method for
Q will not find that Theme object. It will instead find whatever Theme
was an ancestor to the widget Q. If the build context for a subpart of
the returned tree is needed, a Builder widget can be used: the build
context passed to the Builder.builder callback will be that of the
Builder itself.
So, this basically means that the BuildContext inside the build() method is actually that of it's parent. Hence, their is no need to explicitly pass it.

Related

How does Text Widget get marked for rebuild on parent setState()

When setState is called in a widget's state, the corresponding element in the element tree gets marked as dirty, and the widget gets rebuilt. However, how does it handle descendents? For example, the Text widget below gets rebuilt when its ancestor SampleWidgetState gets rebuilt.
Why?
class SampleWidget extends StatefulWidget {
#override
SampleWidgetState createState() => SampleWidgetState();
}
class SampleWidgetState extends State<SampleWidget> {
String text = "text1";
#override
Widget build(BuildContext context) {
return Column(
children: [
Text(text),
ElevatedButton(
child: Text('call SetState'),
onPressed: () {
setState(() {
text = "text2";
});
},
),
],
);
}
}
from Flutter's official documentation, inside Flutter:
In response to user input (or other stimuli), an element can become dirty, for example if the developer calls setState() on the associated state object. The framework keeps a list of dirty elements and jumps directly to them during the build phase, skipping over clean elements. During the build phase, information flows unidirectionally down the element tree, which means each element is visited at most once during the build phase. Once cleaned, an element cannot become dirty again because, by induction, all its ancestor elements are also clean.
I guess this answer what Flutter does under the hood in the updating process of the widget's descendents.
SampleWidgetState is a state class, when you calling the setState() its mean build() method will reinvoke, everything inside will rebuild. thats how its works.
if you want to prevent the descendents to not rebuild, there is several ways,
use const keyword.
warp the widget you want to change its own state, example use StatefullBuilder
refactor widget to statefulwidget so its have its own state
in your case, Text widget consume SampleWidgetState : String text = "text1";, its mean Text widget is not independent, its dependent on that state.

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.

Why BuildContext only avaliable in StatelessWidget.build and what is the good way to use it?

I already known that build context can be used in StatefulWidget any where but only in build function when using Stateless Widget. There is so many content in widget need to reference the build context like Theme, showDialog,Navigator,Provider...
For Example, I have some code below in StatelessWidget:
#override
Widget build(BuildContext context){
...
_getFirstWidget();
...
}
...
Widget _getFirstWidget(){
return _getSecondWidget();
}
Widget _getSecondWidget(){
return _getThirdWidget();
}
Widget _getThirdWidget(){
// use build context here
}
...
If I want to use the build context at the end Widget, I think of 3 ways:
Pass the build context layer by layer
Convert to StatefulWidget
Convert the last widget to a Stateless Widget itself (and use the build context in build)
Why flutter make this restriction in StatelessWidget?
I'm not really sure but I think you want the use the BuildContext from the build method in the function '_getThirdWidget()'. You could just pass it as a parameter like below:
Widget _getThirdWidget(BuildContext context) {
// Use the context here
}
// Call the function like this in the parent widget
#override
Widget build(BuildContext context) {
return _getThirdWidget(context);
}
Let me know if this answers your question!
If you use the method of adding an argument to use context,
Almost every function needs a context argument
this is stupid behavior
StatelessWidget is inconvenient
I try to use StatelessWidget, but end up using Statefulwidget

In Flutter, is there a diference if I place my methods inside or outside a widget's build method?

I'm starting with Flutter and got some questions about where is the right place to put my methods, inside or outside the widget's build method?
Example:
I have my Widget and create a method showText. Is there a diference if I place this method inside the widget's build method or outside it(as a method of the class itself)?
It seems to work either way.
Thanks
If you have some reusable piece of code, consider outsourcing it into its own Widget.
If that's too much boilerplate, considering helper build-methods is a valid option.
To the Dart compiler, it doesn't really matter where you put these methods, but for less indention and better readability, I recommend putting them inside the class.
Also, consider naming the methods _build.... That makes it clear to readers that they are helper build methods. The underscore also ensures that the analyzer warns you if you change the original build method and the helper method becomes unused.
Here's an example:
class A extends StatelessWidget {
Widget build(BuildContext context) {
return Column(
children: [
_buildTitle(context),
_buildContent(context),
],
);
}
Widget _buildTitle(BuildContext context) { ... }
Widget _buildContent(BuildContext context) { ... }
}

Dart Provider: not available at the immediate child

#override
Widget build(BuildContext context) {
return BlocProvider<HomeBloc>(
create: (context) {
return HomeBloc(homeRepo: HomeRepository());
},
child: BlocProvider.of<HomeBloc>(context).state is HomeStateLoading
? CircularProgressIndicator()
: Container());
}
I am confused with the error:
BlocProvider.of() called with a context that does not contain a Bloc of type HomeBloc.
No ancestor could be found starting from the context that was passed to
BlocProvider.of<HomeBloc>().
Didn't I just create the HomeBloc at its immediate parent? What does it want?
You are using the context passed into the build method of your widget class to look for a parent BlocProvider. However, that context is the widget tree as far as your widget class sees it. Because of this, your BlocProvider.of is looking for a BlocProvider that is a parent of your widget class. If you want to get the provider that is the immediate parent, you need a new context object in which the BlocProvider is an ancestor in the widget tree. The easiest way to do this is with a Builder widget:
#override
Widget build(BuildContext context) {
return BlocProvider<HomeBloc>(
create: (context) {
return HomeBloc(homeRepo: HomeRepository());
},
child: Builder(
builder: (newContext) => BlocProvider.of<HomeBloc>(newContext).state is HomeStateLoading
? CircularProgressIndicator()
: Container(),
),
);
}
That being said, it's pretty redundant to create a provider and then immediately reverence the provider. Providers are for retrieving stuff further down the widget tree, not typically for immediate descendants. In this case, using a provider is overkill and there isn't really any reason to not just have the bloc be a field of your class and reference it directly.
From the documentation :
The easiest way to read a value is by using the static method
Provider.of(BuildContext context).
This method will look up in the widget tree starting from the widget
associated with the BuildContext passed and it will return the nearest
variable of type T found (or throw if nothing is found).
In your case it starts looking up the widget tree from your whole widget (associated to the BuildContext).
So you need to move your BlocProvider to be an ancestor of this widget.
If for some reason this is not possible, you can use Consumer, which allows obtaining a value from a provider when you don't have a BuildContext that is a descendant of the said provider.
Read https://pub.dev/documentation/provider/latest/provider/Consumer-class.html