Home > Back-end >  Flutter/Dart - Update state from an external class
Flutter/Dart - Update state from an external class

Time:02-02

I'm totally new to Flutter/Dart, I've done all the layouts for my application, and now it's time to make my application's API calls. I'm trying to manage the forms as cleanly as possible.

I created a class that manages TextFields data (values and errors), if my API returns an error I would like the screen to update without having to call setState(() {}), is this possible?

In addition, many of my application's screens use values that the user enters in real time, if that happened I would have to call the setState(() {}) methodmany times.

Any idea how to do this with the excess calls to the setState(() {}) method?

I created a test project for demo, these are my files:

File path: /main.dart

import 'package:flutter/material.dart';
import 'login_form_data.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Test App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final LoginFormData _loginFormData = LoginFormData();

  void _submitLoginForm() {
    // Validate and then make a call to the login api
    // If the api returns any erros inject then in the LoginFormData class
    _loginFormData.setError('email', 'Invalid e-mail');

    setState(() {}); // Don't want to call setState
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Test App'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(30),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              TextField(
                decoration: InputDecoration(
                  errorText: _loginFormData.firstError('email'),
                  labelText: 'E-mail',
                ),
                onChanged: (value) => _loginFormData.setValue('email', value),
              ),
              TextField(
                decoration: InputDecoration(
                  errorText: _loginFormData.firstError('password'),
                  labelText: 'Password',
                ),
                obscureText: true,
                onChanged: (value) =>
                    _loginFormData.setValue('password', value),
              ),
              ElevatedButton(
                onPressed: _submitLoginForm,
                child: const Text('Login'),
              )
            ],
          ),
        ),
      ),
    );
  }
}

File path: /login_form_data.dart

import 'form/form_data.dart';
import 'form/form_field.dart';

class LoginFormData extends FormData {
  @override
  Map<String, FormField> fields = {
    'email': FormField(),
    'password': FormField(),
    'simple_account': FormField(
      value: true,
    ),
  };

  LoginFormData();
}

File path: /form/form_data.dart

class FormData {
  final Map<String, dynamic> fields = {};

  dynamic getValue(
    String key, {
    String? defaultValue,
  }) {
    return fields[key]?.value ?? defaultValue;
  }

  void setValue(
    String key,
    String value,
  ) {
    fields[key].value = value;
  }

  void setError(
    String key,
    String error,
  ) {
    fields[key]?.errors.add(error);
  }

  dynamic firstError(
    String key,
  ) {
    return fields[key]?.errors.length > 0 ? fields[key]?.errors[0] : null;
  }

  FormData();
}

File path: /form/form_field.dart

class FormField {
  dynamic value;

  List errors = [];

  FormField({
    this.value,
  });
}

CodePudding user response:

You are essentially looking for a State Management solution. There are multiple solutions (you can read about them here: https://docs.flutter.dev/development/data-and-backend/state-mgmt/options)

State Management allows you to declare when you want your widgets to change state instead of having to imperatively call a setState method.

Flutter recommends Provider as a beginner solution, and you can find many tutorials online.

With that being said, let me show you how to achieve this result with a very basic solution: Change Notifier Quoting flutter documentation :

” A class that can be extended or mixed in that provides a change notification API using VoidCallback for notifications.”

We are going to make FormData a Change notifier, and them we are going to make your app listen to changes on the instance, and rebuild itself based on them. Step 1: Based on the code you posted, I can tell that you will interact with LoginFormData based on the methods setValue and setError from the parent class FormData. So we are going to make FormData inherit ChangeNotifer, and make a call to notifyListeners() on these two methods.

class FormData extends ChangeNotifier {
  final Map<String, dynamic> fields = {};

  dynamic getValue(
    String key, {
    String? defaultValue,
  }) {
    return fields[key]?.value ?? defaultValue;
  }

  void setValue(
    String key,
    String value,
  ) {
    fields[key].value = value;
    notifyListeners();
  }

  void setError(
    String key,
    String error,
  ) {
    fields[key]?.errors.add(error);
    notifyListeners();
  }

  dynamic firstError(
    String key,
  ) {
    return fields[key]?.errors.length > 0 ? fields[key]?.errors[0] : null;
  }

  FormData();
}

Now, every time you call either setValue or setError, the instance of FormData will notify the listeners. Step2: Now we have to setup a widget in your app to listen to these changes. Since your app is still small, it’s easy to find a place to put this listener. But as your app grows, you will see that it gets harder to do this, and that’s where packages like Provider come in handy. We are going to wrap your Padding widget that is the first on the body of your scaffold, with a AnimatedBuilder. Despite of the misleading name, animated builder is not limited to animations. It is a widget that receives any listenable object as a parameter, and rebuilds itself every time it gets notified, passing down the updated version of the listenable.

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final LoginFormData _loginFormData = LoginFormData();

  void _submitLoginForm() {
    // Validate and then make a call to the login api
    // If the api returns any erros inject then in the LoginFormData class
    _loginFormData.setError('email', 'Invalid e-mail');

    //setState(() {}); No longer necessary
  }

    @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Test App'),
      ),
      body: AnimatedBuilder(
        animation: _loginFormData,
        builder: (context, child) {
          return Padding(
            padding: const EdgeInsets.all(30),
            child: Center(
              child: Column(
                //... The rest of your widgets
              ),
            ),
          );
        }
      ),
    );
  }
}
  • Related