mvasco
mvasco

Reputation: 5107

Correct use of Flutter Provider needed

I am working on a Flutter app. There is a logged in user that wants to change his profile picture. I am using a remote MySQL database to store all users information like email, profile picture and userID. After login, all user information is saved using shared preferences. Now, the user wants to change his profile picture and opens the screen Change Profile Picture (cambiar_foto_perfil.dart). On that screen, there is an image_picker and a button onPressed action to upload the new picture to the remote server, to update the new profile picture on the remote database.

I am using Provider to keep the whole app updated when the user changes his profile picture. The same provider class is updating shared preferences.

This is user_provider.dart:

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

class UsuarioProvider with ChangeNotifier{

  String _imagen;



  usuarioProvider(){
    _imagen = 'No imagen';

    loadPreferences();
  }

  //getters

  String get imagen => _imagen;


  //setters
  void setimagen(String imagen){

    _imagen = imagen;

    notifyListeners();
    savePreferences();
  }


  loadPreferences() async {

    SharedPreferences prefs = await SharedPreferences.getInstance();
    String imagen = prefs.getString('foto');

    if (_imagen != null) setimagen(imagen);


  }
  savePreferences() async {

    SharedPreferences prefs = await SharedPreferences.getInstance();
    prefs.setString('foto', _imagen);

  }

}

I am instantiating the provider at the top level of the widget tree as follows in main.dart:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  SharedPreferences prefs = await SharedPreferences.getInstance();
  var email = prefs.getString('email');

  print(email);

  runApp(
    EasyLocalization(
      child: MultiProvider(
        providers: [
          ChangeNotifierProvider(create: (BuildContext context) => ClinicaProvider()),
          ChangeNotifierProvider(create: (BuildContext context) => UsuarioProvider()),
        ],

          child: MaterialApp(
              debugShowCheckedModeBanner: false,
              home: email == null || email == '' ? Splash2() : HomeScreen())),
      path: "assets/translations",
      saveLocale: true,
      supportedLocales: [Locale('en', 'EN'), Locale('es', 'ES')],
    ),
  );
}

And now, when the user wants to change his profile picture, class cambiar_foto_perfil.dart is called. Here you have the class code:

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_capenergy/classes/user.dart';
import 'package:flutter_capenergy/providers/user_provider.dart';
import 'package:flutter_capenergy/servicios/chech_internet.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http;
import 'package:easy_localization/easy_localization.dart';
import 'package:toast/toast.dart';

class CambiarFotoPerfil extends StatefulWidget {
  @override
  _CambiarFotoPerfilState createState() => _CambiarFotoPerfilState();
}

class _CambiarFotoPerfilState extends State<CambiarFotoPerfil> {
  File _selectedFile;
  bool _inProcess = false;
  final _picker = ImagePicker();

  String miEmail = "";
  String miTel = "";
  String miUltimoEmail = "";
  String miImagen = "";
  String miId = "";
  String _textoInfo = "";
  bool isLoading = false;

  @override
  void initState() {
    super.initState();

    getEmailUsuarioActual();
    getTelUsuarioActual();
    getImagenUsuarioActual();
    getIdUsuarioActual();
    getUltimoEmailUsuarioActual();
    checkInternet().checkConnection(context, "YouAreConnected".tr().toString(),
        "YouAreNotConnected".tr().toString(), "WaitConnection".tr().toString());
  }

  @override
  void dispose() {
    super.dispose();
    print("run dispose");
  }

  Future<String> getEmailUsuarioActual() async {
    final prefs = await SharedPreferences.getInstance();

    miEmail = prefs.getString("email");

    setState(() {});
  }

  Future<String> getTelUsuarioActual() async {
    final prefs = await SharedPreferences.getInstance();

    miTel = prefs.getString("tel");

    setState(() {});
  }

  Future<String> getUltimoEmailUsuarioActual() async {
    final prefs = await SharedPreferences.getInstance();

    miUltimoEmail = prefs.getString("ultimo_email");

    setState(() {});
  }

  Future<String> getImagenUsuarioActual() async {
    final prefs = await SharedPreferences.getInstance();
    miImagen = prefs.getString("foto");

    setState(() {});
  }

  Future<String> getIdUsuarioActual() async {
    final prefs = await SharedPreferences.getInstance();
    miId = prefs.getString("id");

    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    localizationsDelegates:
    context.localizationDelegates;
    supportedLocales:
    context.supportedLocales;
    locale:
    context.locale;

    var usuarioProvider = Provider.of<UsuarioProvider>(context);

    //changes profile picture in the database
    Future<User> cambiarFotoUsuario() async {
      setState(() {
        isLoading = true;
      });

      var url =
          "https://app.com/flutter_api/cambiar_foto_usuario.php";
      final response =
          await http.post(url, body: {"id": miId, "foto": miImagen});

      print("respuesta :" + response.body);

      print(response.statusCode);

      final Map parsed = json.decode(response.body);

      setState(() {
        isLoading = false;
      });
    }

    final String phpEndPoint =
        'https://app.com/administrar/application/admin/usuarios/subir_foto_perfil.php';

    //uploads profile picture to the server
    void _upload(File file) {
      if (file == null) return;
      setState(() {
        _textoInfo = "Subiendo foto al servidor...";
      });
      String base64Image = base64Encode(file.readAsBytesSync());
      String fileName = file.path.split("/").last;

      http.post(phpEndPoint, body: {
        "image": base64Image,
        "name": fileName,
      }).then((res) async {
        print(res.statusCode);
        setState(() {
          _textoInfo = "Foto del perfil actualizada";
          miImagen = fileName;
        });

        // updates provider with new profile image
        usuarioProvider.setimagen(fileName);

        cambiarFotoUsuario();

      }).catchError((err) {
        print(err);
      });
    }

    Widget getImageWidget() {
      if (_selectedFile != null) {
        return Image.file(
          _selectedFile,
          width: 220,
          height: 220,
          fit: BoxFit.cover,
        );
      } else {
        return Image.asset(
          "assets/images/placeholder.png",
          width: 220,
          height: 220,
          fit: BoxFit.cover,
        );
      }
    }

    getImage(ImageSource source) async {
      this.setState(() {
        _inProcess = true;
      });
      File image = await ImagePicker.pickImage(source: source);
      if (image != null) {
        File cropped = await ImageCropper.cropImage(
            sourcePath: image.path,
            aspectRatio: CropAspectRatio(ratioX: 1, ratioY: 1),
            compressQuality: 100,
            maxWidth: 700,
            maxHeight: 700,
            compressFormat: ImageCompressFormat.jpg,
            androidUiSettings: AndroidUiSettings(
              toolbarColor: Colors.deepOrange,
              toolbarTitle: "RPS Cropper",
              statusBarColor: Colors.blueAccent,
              backgroundColor: Colors.white,
            ));

        this.setState(() {
          _selectedFile = cropped;
          print(_selectedFile.toString());
          _inProcess = false;
        });
      } else {
        this.setState(() {
          _inProcess = false;
        });
      }
    }

    return Scaffold(
        appBar: AppBar(
          centerTitle: false,
          title: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                "Capenergy",
                style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                ),
              ),
              Visibility(
                visible: true,
                child: Text(
                  miEmail,
                  style: TextStyle(
                    fontSize: 12.0,
                  ),
                ),
              ),
            ],
          ),
          actions: [
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: CircleAvatar(
                radius: 25.0,
                backgroundImage: NetworkImage(
                    'https://app.com/administrar/application/admin/usuarios/' +
                        usuarioProvider.imagen),
                backgroundColor: Colors.transparent,
              ),
            )
          ],
        ),
        body: Stack(
          children: <Widget>[
            Container(
              child: Padding(
                padding: EdgeInsets.all(20.0),
                child: Column(
                  children: <Widget>[
                    SizedBox(height: 20),
                    Text(
                      "Selecciona la foto para tu perfil de usuario ",
                      style: TextStyle(fontSize: 24.0, color: Colors.black45),
                    ),
                  ],
                ),
              ),
            ),
            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                SizedBox(height: 60),
                getImageWidget(),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: <Widget>[
                    IconButton(
                      onPressed: () {
                        getImage(ImageSource
                            .camera); //  You enter here what you want the button to do once the user interacts with it
                      },
                      icon: Icon(
                        Icons.add_a_photo_rounded,
                        color: Colors.green,
                        size: 62.0,
                      ),
                      iconSize: 20.0,
                    ),
                    IconButton(
                      onPressed: () {
                        getImage(ImageSource.gallery);
                        ; //  You enter here what you want the button to do once the user interacts with it
                      },
                      icon: Icon(
                        Icons.image_search_outlined,
                        color: Colors.lightBlueAccent,
                        size: 62.0,
                      ),
                      iconSize: 20.0,
                    ),
                  ],
                ),
                SizedBox(height: 20),
                RaisedButton.icon(
                  onPressed: () {
                    _upload(_selectedFile);
                    print('Button Clicked.');
                  },
                  shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.all(Radius.circular(10.0))),
                  label: Text(
                    'Selecciona la foto para tu perfil',
                    style: TextStyle(color: Colors.white),
                  ),
                  icon: Icon(
                    Icons.track_changes,
                    color: Colors.white,
                  ),
                  textColor: Colors.white,
                  splashColor: Colors.red,
                  color: Colors.blueAccent,
                ),
                Text(_textoInfo),
              ],
            ),
            (_inProcess)
                ? Container(
                    color: Colors.white,
                    height: MediaQuery.of(context).size.height * 0.95,
                    child: Center(
                      child: CircularProgressIndicator(),
                    ),
                  )
                : Center()
          ],
        ));
  }
}

Here you have a screen shot from this screen:

enter image description here

If the user clicks on the camera button, he can take a picture than is later shown on the placeholder space. The same happens if the user clicks on the search button and selects a picture from the device's gallery, the selected image is shown then on the placeholder space.

Once a picture is taken or selected, if the user clicks on the blue button, the picture is uploaded to the server and the remote database is updated. All this functions are working as expected. On the other hand, I would like to update shared preferences and would like that the provider notifies all other tree widgets that the user profile picture has been changed.

But I am gettin this Exception message at the log console, just after the blue button has been clicked:

======== Exception caught by foundation library ====================================================
The following assertion was thrown while dispatching notifications for UsuarioProvider:
setState() or markNeedsBuild() called during build.

This _InheritedProviderScope<UsuarioProvider> widget cannot be marked as needing to build because the framework is already in the process of building widgets.  A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was: _InheritedProviderScope<UsuarioProvider>
  value: Instance of 'UsuarioProvider'
  listening to value
The widget which was currently being built when the offending call was made was: HomeScreen
  dirty
  dependencies: [_InheritedProviderScope<UsuarioProvider>, MediaQuery, _EasyLocalizationProvider, _InheritedProviderScope<ClinicaProvider>]
  state: _HomeScreenState#ce28a
When the exception was thrown, this was the stack: 
#0      Element.markNeedsBuild.<anonymous closure> (package:flutter/src/widgets/framework.dart:4292:11)
#1      Element.markNeedsBuild (package:flutter/src/widgets/framework.dart:4307:6)
#2      _InheritedProviderScopeElement.markNeedsNotifyDependents (package:provider/src/inherited_provider.dart:491:5)
#3      ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:226:25)
#4      UsuarioProvider.setimagen (package:flutter_capenergy/providers/user_provider.dart:26:5)
...
The UsuarioProvider sending notification was: Instance of 'UsuarioProvider'
====================================================================================================

Please take a look at my code to check the provider class, the provider instantiation, and the provider use in cambiar_foto_perfil.dart to find out what am I doing wrong.

Upvotes: 1

Views: 1042

Answers (1)

rmontemayor0101
rmontemayor0101

Reputation: 156

No need to use setState(), use Consumer() or Provider.of to get values of the Model, Consumer() listens and rebuild UI when notifyListeners() is called

To call provider methods

Provider.of<usuarioProvider>(context, listen: false).setimagen("name_of_image"); 

To get provider values, change your code

            Padding(
              padding: const EdgeInsets.all(8.0),
              child: CircleAvatar(
                radius: 25.0,
                backgroundImage: NetworkImage(
                    'https://app.com/administrar/application/admin/usuarios/' +
                        usuarioProvider.imagen),
                backgroundColor: Colors.transparent,
              ),

to

Consumer<usuarioProvider>(
    builder: (context, provider, _) {
            return Padding(
              padding: const EdgeInsets.all(8.0),
              child: CircleAvatar(
                radius: 25.0,
                backgroundImage: NetworkImage(
                    'https://app.com/administrar/application/admin/usuarios/' +
                        provider.imagen),
                backgroundColor: Colors.transparent,
              ),
    }
)

Upvotes: 3

Related Questions