jonataslaw / getx

Open screens/snackbars/dialogs/bottomSheets without context, manage states and inject dependencies easily with Get.
MIT License
10.44k stars 1.63k forks source link

GetBuilder - select values from the Controller #489

Closed lukaszdebowski closed 4 years ago

lukaszdebowski commented 4 years ago

Is your feature request related to a problem? Please describe. Sometimes, when we call the update() method inside the controller, some GetBuilder get rebuild unnecessarily, i.e let's say we have a User class with name and email fields.

This is our controller:

class UserController extends GetxController {
  static UserController get I => Get.find();

  User value = User(
    id: 0,
    firstName: "John",
    lastName: "Smith",
    email: "john.smith@gmail.coim",
  );

  void setFirstName(String newName) {
    value.firstName = newName;
    update();
  }

  void setEmail(String newEmail) {
    value.email = newEmail;
    update();
  }
}

Then somewhere inside our view, we have something like this:

Column(
  mainAxisSize: MainAxisSize.min,
  children: [
    Text("Dashboard Screen"),
    GetBuilder<UserController>(
      builder: (_) {
        return Text("Full Name: ${_.value.fullName}");
      },
    ),
    GetBuilder<UserController>(
      builder: (_) {
        return Text("Email: ${_.value.email}");
      },
    ),
  ],
),

Even if we only use the setFirstName method on the controller, the email builder will rebuild (even though the email did not change).

Describe the solution you'd like It would be nice to have something like a select property on the GetBuilder widget, which is basically a function that returns the value, that must be changed inside our controller, for the builder to get rebuild. So something like:

GetBuilder<UserController>(
  select: (controller) => controller.value.email,
  builder: (_) {
    return Text("Email: ${_.value.email}");
  },
),

This would mean that only if the email field is different than it was before the update() call, this builder should get updated.

Describe alternatives you've considered Could't really find anything that would supply me with this functionality, but I could've searched wrong. Maybe there is something like that already?

jonataslaw commented 4 years ago
 GetBuilder<UserController>(
      id: 'name',
      builder: (_) {
        return Text("Full Name: ${_.value.fullName}");
      },
    ),
    GetBuilder<UserController>(
      id: 'email',
      builder: (_) {
        return Text("Email: ${_.value.email}");
      },
    ),

// update only name
 void setFirstName(String newName) {
    value.firstName = newName;
    update(['name']);
  }

// update only email
  void setEmail(String newEmail) {
    value.email = newEmail;
    update(['email']);
  }

// update name and email
void setEmailAndName(String newEmail,String newName) {
    value.email = newEmail;
    value.firstName = newName;
    update(['email', 'name']);
  }
lukaszdebowski commented 4 years ago

Yeah I've found that one, but there is this little scenario: let's say I have 10 different listeners across the app and want 1 particular to not rebuild, it would require me to provide the 9 ids to all the other ones and list them inside my update()?

Another issue with this approach is that I can't have multiple fields with the same ID

Thanks for the response though! Really appreciate the package and your work, it's great 👍

Nipodemos commented 4 years ago

Hey As is written in docs, GetBuilder is part of the simple state manager, it was made to be simple and lightweight, and for simple tasks.

In your case, you want granular control over your widgets and which one will update and when.

In this case, you should use the reactive state management, it have awesome features and let you have a lot of control of state. Following your example, in reactive state would be like this:

class UserController extends GetxController { 
  static UserController get I => Get.find();

  final user = User( // changed the variable name to user
    id: 0,
    firstName: "John",
    lastName: "Smith",
    email: "john.smith@gmail.coim",
  ).obs; //make the user class observable

  void setFirstName(String newName) {
    // now instead of access the user class directly, you use `user.value` to access its values and update
    user.value.firstName = newName;
    // call update() is no longer needed
  }

  void setEmail(String newEmail) {
    user.value.email = newEmail; // same here, using user.value
    // call update() is no longer needed
  }
}

in your view:

Column(
  mainAxisSize: MainAxisSize.min,
  children: [
    Text("Dashboard Screen"),
    GetX<UserController>(
      builder: (controller) {
        return Text("Full Name: ${controller.user.value.fullName}"); // using `user.value` here too
      },
    ),
    GetX<UserController>(
      builder: (controller) {
        return Text("Email: ${controller.user.value.email}"); // same here
      },
    ),
  ],
),

This way, automatically only the changed widget will be rebuilt, and you don't need to use identifiers for each update

jonataslaw commented 4 years ago

Yeah I've found that one, but there is this little scenario: let's say I have 10 different listeners across the app and want 1 particular to not rebuild, it would require me to provide the 9 ids to all the other ones and list them inside my update()?

Another issue with this approach is that I can't have multiple fields with the same ID

Thanks for the response though! Really appreciate the package and your work, it's great

You would only need to provide a single ID for the one you do NOT want to rebuild, and then it will only be rebuilt when the update is called with your ID. I recommend for this case the approach above @Nipodemos. Nothing prevents you from using simple management, but definitely if you need a complex solution, there is something better and reactive ready to use.

joanofdart commented 4 years ago

I would go as far as make it far more optimal...

Widget build(BuildContext context) {
    return GetBuilder<SignInController>(
      global: false,
      init: SignInController(),
      builder: (controller) {
           someWidgetsHereThatUseTheSameController...
      }
   );
}

And then just have a single fieldWidget that handles each formfield.

jonataslaw commented 4 years ago

As the existing tools for this resource were pointed out, and the issue was no longer answered, I am closing this issue.

subzero911 commented 3 years ago

@jonataslaw +1 for selector GetX and Obx are easy in use but heavily ram-consuming. GetBuilder is the fastest. Also, it would be easier to transfer the existing Provider-based project to Get. Ids / tags assignment looks like an antipattern, because it makes your widgets and controllers tightly coupled - you MUST add the same tags in your GetBuilders, which are listed in corresponing update([...]). Normally, controller shouldn't know anything about widgets and their tags, it should care about business logic only. It's better to let widgets to choose on which variables change they should rebuild.

jonataslaw commented 3 years ago

@jonataslaw +1 for selector GetX and Obx are easy in use but heavily ram-consuming. GetBuilder is the fastest. Also, it would be easier to transfer the existing Provider-based project to Get. Ids / tags assignment looks like an antipattern, because it makes your widgets and controllers tightly coupled - you MUST add the same tags in your GetBuilders, which are listed in corresponing update([...]). Normally, controller shouldn't know anything about widgets and their tags, it should care about business logic only. It's better to let widgets to choose on which variables change they should rebuild.

GetBuilder has a filter property where you can filter rebuilds.

subzero911 commented 3 years ago

I've just checked out filter property. I can't find how exactly it filters rebuilds. It just does setState on any change! You can set any filter function, but seems like GetBuilder doesn't use it anywhere.

image image

subzero911 commented 3 years ago

I compiled a minimal reproducible example, and can confirm that filter actually works. Just wanted to make sure. But it some kind of GetX magic, I can't understand the exact mechanism how the filters work.

Without filter - all 3 widgets print "rebuilt" With filter - only widget I clicked prints "rebuilt"

Main
```dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'controller.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return GetMaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State { @override void initState() { super.initState(); Get.put(MyController()); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ GetBuilder( filter: (c) => c.textA, builder: (c) { print('Controller A rebuilt'); return ElevatedButton( onPressed: () => c.changeA(), child: Text(c.textA), ); }, ), GetBuilder( filter: (c) => c.textB, builder: (c) { print('Controller B rebuilt'); return ElevatedButton( onPressed: () => c.changeB(), child: Text(c.textB), ); }, ), GetBuilder( filter: (c) => c.textC, builder: (c) { print('Controller C rebuilt'); return ElevatedButton( onPressed: () => c.changeC(), child: Text(c.textC), ); }, ), ], ), ), ); } } ```
Controller
```dart import 'package:get/get.dart'; class MyController extends GetxController { MyController(); String textA = ''; String textB = ''; String textC = ''; void changeA() { textA.isEmpty ? textA = 'bla bla' : textA = ''; update(); } void changeB() { textB.isEmpty ? textB = 'bla bla' : textB = ''; update(); } void changeC() { textC.isEmpty ? textC = 'bla bla' : textC = ''; update(); } } ```