Separating UI and Logic in Flutter

Viewed 3967

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 = '';
}
2 Answers

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

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.

Related