Separating UI and Logic in Flutter

4,410

Solution 1

I've modified your code a little bit. If you change your code as like the following code, hopefully, you will get the expected output.

class NewAccountComponent extends StatelessWidget {
  final NewAccountComponentLogic logic = NewAccountComponentLogic(
    '123456',
    true,
    TextEditingController(),
  );
  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text('Enter a Unique Account Number'),
      titlePadding: EdgeInsets.all(20.0),
      content: TextFormField(
        controller: logic.controller,
        
      ),
      actions: <Widget>[
          TextButton(
            child: Text('Done'),
            onPressed: () {
              print(logic.controller.text);
              logic.clearTextFormField();
            },
          ),
        ],
    );
  }
}

class NewAccountComponentLogic {
  String accountNumber;
  bool existsAccountNumber;
  TextEditingController controller;
  NewAccountComponentLogic(
    this.accountNumber,
    this.existsAccountNumber,
    this.controller,
  );

  void clearTextFormField() {
    controller.text = '';
    accountNumber = '';
  }

@Ignacior has also given a nice solution which you can follow.

Solution 2

You can separate widget logic and presentation in many ways. One that I've seen (and that you mention) is using the WidgetView pattern. You can do it without any dependency:

  1. Create an abstract class thats contains the logic that all WidgetViews should be implement:

For Stateless widgets:

abstract class StatelessView<T1> extends StatelessWidget {
  final T1 widget;
  const StatelessView(this.widget, {Key key}) : super(key: key);
  
  @override
  Widget build(BuildContext context);
}

For Stateful widgets:

abstract class WidgetView<T1, T2> extends StatelessWidget {
  final T2 state;
  T1 get widget => (state as State).widget as T1;

  const WidgetView(this.state, {Key key}) : super(key: key);
  
  @override
  Widget build(BuildContext context);
}
  1. Create your widget normmally:
// Note it's a StatefulWidget because accountNumber mutates
class NewAccountComponent extends StatefulWidget {
  @override
  _NewAccountComponentState createState() => _NewAccountComponentState();
}

class _NewAccountComponentState extends State<NewAccountComponent> {
  String accountNumber;
  bool existsAccountNumber;
  final TextEditingController controller = TextEditingController();

  clearTextFormField() {
    controller.text = '';
    accountNumber = '';
  }

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text('Enter a Unique Account Number'),
      titlePadding: EdgeInsets.all(20.0),
      content: TextFormField(
        controller: controller,
        onSaved: (value) => clearTextFormField(),
      ),
    );
  }
}
  1. If the widget is a Stateful
class NewAccountComponent extends StatefulWidget {
  @override
  _NewAccountComponentController createState() => _NewAccountComponentController();
}

// State suffix renamed to Controller
// This class has all widget logic
class _NewAccountComponentController extends State<NewAccountComponent> {
  String accountNumber;
  bool existsAccountNumber;
  final TextEditingController controller = TextEditingController();

  clearTextFormField() {
    controller.text = '';
    accountNumber = '';
  }

  // In build, returns a new instance of your view, sending the current state
  @override
  Widget build(BuildContext context) => _NewAccountComponentView(this);
}

// View extends of WidgetView and has a current state to access widget logic
// with widget you can access to StatefulWidget parent
class _NewAccountComponentView
    extends WidgetView<NewAccountComponent, _NewAccountComponentController> {

  _NewAccountComponentView(_NewAccountComponentController state): super(state);

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text('Enter a Unique Account Number'),
      titlePadding: EdgeInsets.all(20.0),
      content: TextFormField(
        controller: state.controller,
        onSaved: (value) => state.clearTextFormField(),
      ),
    );
  }
}
  1. If it's Stateless, change from:
class MyStatelessWidget extends StatelessWidget {
  final String textContent = "Hello!";

  const MyStatelessWidget({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text(textContent),
    );
  }
}

to:

// Widget and logic controller are unit
class MyStatelessWidget extends StatelessWidget {
  final String textContent = "Hello!";

  const MyStatelessWidget({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) => _MyStatelessView(this);
}

// The view is separately
class _MyStatelessView extends StatelessView<MyStatelessWidget> {
  _MyStatelessView(MyStatelessWidget widget) : super(widget);

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text(widget.textContent),
    );
  }
}

References:

Flutter: WidgetView — A Simple Separation of Layout and Logic

Share:
4,410
S Das
Author by

S Das

Updated on December 29, 2022

Comments

  • S Das
    S Das over 1 year

    Normally, I use a separate class with an object declared on the top of the widget. I wish to know what is the problem with that architecture.

    I came across an entire package in Flutter, WidgetView, which needs to declare a dependency, then make a state object, and then do the same thing.

    Why not just a simple class for achieving the same. like below

    class NewAccountComponent extends StatelessWidget {  
    final NewAccountComponentLogic logic = NewAccountComponentLogic();
      @override
      Widget build(BuildContext context) {
        return AlertDialog(
          title: Text('Enter a Unique Account Number'),
          titlePadding: EdgeInsets.all(20.0),
          content: TextFormField(
            controller: logic.controller,
                onPressed: () => logic.clearTextFormField(),
              ),
            ),
    }
    class NewAccountComponentLogic {
      static String accountNumber;
      static bool existsAccountNumber;
      TextEditingController controller = TextEditingController();
      clearTextFormField() {
        controller.text = '';
        accountNumber = '';
    }
    
  • S Das
    S Das about 3 years
    Thanks for your detailed explaination.But why not a simple class, can do the job...why do I require to extend the stateless widget for the logic as well.
  • Ignacior
    Ignacior about 3 years
    Sure, the idea of separating it into a single class that contains all the logic is valid. I'm just showing one way to do it and following the rules of the pattern: Each State (or StatelessWidget), has a child WidgetView, which contains the declarative view code. The State acts as a stand-in Controller / Mediator / Presenter for the WidgetView, responding to view events and providing access to state. The WidgetView is a just a StatelessWidget that is pure layout.
  • Ignacior
    Ignacior about 3 years
    You can follow the approach that fits the problem better. For widgets with little logic I think it is not necessary to divide into multiple parts, much less apply the pattern that I have mentioned.
  • S Das
    S Das about 3 years
    Thanks a lot. I am very clear of the issue now.