Search code examples
flutterpostwidgetstatefulwidget

how to Access TextEditingController of multiple same Stateful Widgets in flutter?


I am making an app where a user can add multiple stateful widgets in a list view inside a home stateful widget. Example: list of ingredients. Every ingredient widget has a TextFormField. A user can add as many ingredients as wish to and eventually the data entered will be updated using a POST request from the home stateful widget.

Question: How do i access the TextFormField controller of every added ingredient widget from the home state. Also is this the best way to approcah this or is there a better way?

class home extends StatefulWidget {
  const home({Key? key}) : super(key: key);

  @override
  _homeState createState() => _homeState();
}

class _homeState extends State<home> {
  List<Widget> listRecipe = [];

  addIngredient(){
    listRecipe.add(new Ingredient());
  }

  Future<http.Response> postData(){
    return http.post(
      Uri.parse(recipeData),
      headers: <String, String>{
        'Authorization':'token $token',
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode(<String, String>{
        "name":"something",
        "ingredients":'{"ingredient1": "value", "ingredient2": "value", ...... }'
      }),
    );
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Container(
          child: Wrap(
            children:[ ListView(
              children:listRecipe,
            ),
            ],
          ),
        ),
    ]  ),
      bottomNavigationBar: Row(
          children:
      [FloatingActionButton(
        onPressed: addIngredient,
      ),
        FloatingActionButton(onPressed: postData)
    ]));
  }
}



class Ingredient extends StatefulWidget {
  const Ingredient({Key? key}) : super(key: key);

  @override
  _IngredientState createState() => _IngredientState();
}

class _IngredientState extends State<Ingredient> {
  final _typeController = TextEditingController();
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Padding(
        padding: EdgeInsets.only(left: 22, top: 20, right: 15),
        child: Column(
          children: [
            Container(
              width: MediaQuery.of(context).size.width,
              child: Padding(
                padding: const EdgeInsets.only(left: 5.0),
                child: Text(
                  "Process",
                  style: TextStyle(
                    fontFamily: 'Nunito',
                    color: Colors.grey.shade500,
                    fontSize: 12,
                  ),
                  textAlign: TextAlign.left,
                ),
              ),
            ),
            Container(
              height: 45,
              child: TextFormField(
                keyboardType: TextInputType.text,
                maxLines: null,
                controller: _typeController,
                style: TextStyle(fontSize: 15, fontFamily: 'Nunito'),
                decoration: InputDecoration(
                  hintText: 'Enter Process',
                  hintStyle: TextStyle(color: Colors.grey.shade400),
                  contentPadding: EdgeInsets.only(left: 5),
                  enabledBorder: UnderlineInputBorder(
                      borderSide: BorderSide(color: Colors.green)),
                  focusedBorder: UnderlineInputBorder(
                    borderSide: BorderSide(color: Colors.green, width: 2),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );;
  }
}

Solution

  • tl:dr

    • Add final String property to your StatefulWidget
    • in initState assign value of property to TextEditingController: textEditingController.text = widget.stringValue
    • override didUpdateWidget and also set TextEditingControllerValue textEditingController.text = widget.stringValue

    I would suggest to add a model for your Ingredient, e.g:

    class Ingredient {
      String? data;
    
      Ingredient({this.data});
    }
    

    Rename the Ingredient Widget to e.g. IngredientWidget. The type of your listReceipe should stay List<Ingredient>:

      List<Ingredient> listRecipe = [];
    
      addIngredient() {
        setState(() {
          listRecipe.add(Ingredient());
        });
      }
    

    Change the body of your Home Widget:

     body: Container(
                  child: ListView.builder(
                      itemCount: listRecipe.length,
                      itemBuilder: (context, index) =>
                          IngredientWidget(listRecipe[index]))),
    

    Now change your IngredientWidget like this:

    class IngredientWidget extends StatefulWidget {
      const IngredientWidget(this.ingredient, {Key? key}) : super(key: key);
    
      final Ingredient ingredient;
    
      @override
      _IngredientWidgetState createState() => _IngredientWidgetState();
    }
    
    class _IngredientWidgetState extends State<IngredientWidget> {
      final _typeController = TextEditingController();
    
      @override
      void didUpdateWidget(covariant IngredientWidget oldWidget) {
        super.didUpdateWidget(oldWidget);
        _typeController.text = widget.ingredient.data ?? '';
      }
    
      @override
      void initState() {
        super.initState();
        _typeController.text = widget.ingredient.data ?? '';
      }
    
      @override
      Widget build(BuildContext context) {
        // stays the same
      }
    }
    

    You can pass a Value to your Ingredient widget:

    Ingredient(data: 'Hello World')
    

    So you can also update an Ingredient in listReceipe with

      setState(() {
        listRecipe[index] = Ingredient(data: anyStringValue);
      });
    

    and your list should update accordingly.