Search code examples
flutterdartflutter-provider

How to navigate text to one widget and not to all widgets on the screen at the same time using Provider?


I'm working on a planner app that has a screen with multiple widgets('Monday','Tuesday',etc).When I tap on a widget, I should be able to use TextField on pop up screen and navigate text to the widget I tapped. The issue now is that provider navigates the text to all widgets at the same time and not to only one I tapped. How could I solve that? Appreciate your help

This is a planner screen

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:my_planner_app/weekday_card.dart';

class PlannerScreen extends StatefulWidget {
  static const String id = 'planner_screen';

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

class _PlannerScreenState extends State<PlannerScreen>
    with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation animation;

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

    controller =
        AnimationController(duration: Duration(seconds: 3), vsync: this);
    animation = ColorTween(begin: Colors.grey[800], end: Colors.white)
        .animate(controller);
    controller.forward();
    controller.addListener(() {
      setState(() {});
    });
  }

  Widget build(BuildContext context) {
    var size = MediaQuery.of(context).size;

    final double itemHeight = (size.height - 24) / 2;
    final double itemWidth = size.width / 2;
    return Scaffold(
      backgroundColor: Color(0xFFcf9e9f),
      body: Container(
        child: GridView(
          gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 3,
            childAspectRatio: (itemWidth / itemHeight),
          ),
          children: <Widget>[
            WeekDayCard(
              text: '',
            ),
            WeekDayCard(text: 'Monday'),
            WeekDayCard(text: 'Tuesday'),
            WeekDayCard(text: 'Wednesday'),
            WeekDayCard(text: 'Thursday'),
            WeekDayCard(text: 'Friday'),
            WeekDayCard(text: 'Saturday'),
            WeekDayCard(text: 'Sunday'),
            WeekDayCard(text: 'Notes'),
          ],
        ),
      ),
    );
  }
}

This is associated widget


        import 'package:flutter/material.dart';
   import 'package:my_planner_app/screens/addPlan_screen.dart';
import 'package:provider/provider.dart';
import 'package:my_planner_app/widgets/plan_widget.dart';

class WeekDayCard extends StatelessWidget {
  WeekDayCard({@required this.text, this.name});
  final String name;
  final String text;
  @override
  Widget build(BuildContext context) {
    return Consumer<MyProvider>(builder: (context, myProvider, child) {
      return Card(
        color: Color(0xFFFEEFCD),
        elevation: 10,
        child: Column(
          children: [
            Text(text),
            Text(Provider.of<MyProvider>(context).name),
            Expanded(
              child: InkWell(
                onTap: () {
                  showModalBottomSheet(
                    backgroundColor: Color(0xFFFEEFCD),
                    context: context,
                    builder: (context) => AddPlanScreen(),
                  );
                },
              ),
            ),
          ],
        ),
      );
    });
  }
}

This is associated pop up AddScreen

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:my_planner_app/widgets/plan_widget.dart';

class AddPlanScreen extends StatelessWidget {
  static String name;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: TextFormField(
            onChanged: (text) {
              name = text;
            },
            decoration: InputDecoration(
              border: InputBorder.none,
            ),
            minLines: 10,
            maxLines: 30,
            autocorrect: false,
          ),
        ),
        FlatButton(
          onPressed: () {
            print(name);
            Provider.of<MyProvider>(context, listen: false).setName(name);
          },
          color: Colors.blue,
        ),
      ],
    );
  }
}

Provider

import 'package:flutter/material.dart';

class MyProvider extends ChangeNotifier {
  String _name = '';
  String get name => _name;
  void setName(String newString) {
    _name = newString;
    print(_name);
    notifyListeners();
  }
}

ChangeNotifierProvider placed before MaterialApp


void main() {
  runApp(MyPlanner());
}

class MyPlanner extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyProvider(),
      child: MaterialApp(
        theme: ThemeData(fontFamily: 'IndieFlower'),
        initialRoute: WelcomeScreen.id,
        routes: {
          WelcomeScreen.id: (context) => WelcomeScreen(),
          RegisterScreen.id: (context) => RegisterScreen(),
          LogInScreen.id: (context) => LogInScreen(),
          PlannerScreen.id: (context) => PlannerScreen(),
        },
      ),
    );
  }
}


Solution

  • Quick Solution 1

    (to stay close to your current code base)

    Your ChangeNotifier should keep a Map<String, String> instead of a String, one entry per weekday.

    Provider

    class MyProvider extends ChangeNotifier {
      Map<String, String> _names = {};
      
      String name(String key) => _names[key];
      
      void setName(String key, String newString) {
        _names[key] = newString;
        notifyListeners();
      }
    }
    

    Then, you will need the following changes:

    WeekDayCard

    Instead of Text(Provider.of<MyProvider>(context).name), use the key text to get the name for the day: Text(Provider.of<MyProvider>(context).name(text) ?? '').

    When you open the modal bottom sheet, pass the weekday name: AddPlanScreen(weekdayName: text).

    class WeekDayCard extends StatelessWidget {
      WeekDayCard({@required this.text, this.name});
      final String name;
      final String text;
      @override
      Widget build(BuildContext context) {
        return Consumer<MyProvider>(builder: (context, myProvider, child) {
          return Card(
            color: Color(0xFFFEEFCD),
            elevation: 10,
            child: Column(
              children: [
                Text(text),
                Text(Provider.of<MyProvider>(context).name(text) ?? ''),
                Expanded(
                  child: InkWell(
                    onTap: () {
                      showModalBottomSheet(
                        backgroundColor: Color(0xFFFEEFCD),
                        context: context,
                        builder: (context) => AddPlanScreen(weekdayName: text),
                      );
                    },
                  ),
                ),
              ],
            ),
          );
        });
      }
    }
    

    AddPlanScreen

    1. First, it should be a StatefulWidget instead of a StatelessWidget with a static variable.
    2. It should accept a parameter weekdayName
    3. When setting the name, it should pass the weekdayName as a key: Provider.of<MyProvider>(context, listen: false).setName(widget.weekdayName, name);
    class AddPlanScreen extends StatefulWidget {
      final String weekdayName;
    
      const AddPlanScreen({Key key, this.weekdayName}) : super(key: key);
    
      @override
      _AddPlanScreenState createState() => _AddPlanScreenState();
    }
    
    class _AddPlanScreenState extends State<AddPlanScreen> {
      String name;
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            Expanded(
              child: TextFormField(
                onChanged: (text) {
                  name = text;
                },
                decoration: InputDecoration(
                  border: InputBorder.none,
                ),
                minLines: 10,
                maxLines: 30,
                autocorrect: false,
              ),
            ),
            ElevatedButton(
              onPressed: () {
                Provider.of<MyProvider>(context, listen: false)
                    .setName(widget.weekdayName, name);
              },
              child: Text('UPDATE'),
            ),
          ],
        );
      }
    }
    

    Further refactoring for Solution 2

    In this Solution, I will use Riverpod instead of Provider. Both packages have been authored by Remi ROUSSELET. Riverpod comes in several flavors, my preference goes for hooks_riverpod.

    enter image description here

    I kept rather the same structure:

    • MyPlanner is the MaterialApp. I encapsulated inside Riverpod's ProviderScope
    • PlannerScreen is the main screen. It is now a StatelessWidget. It is also responsive showing a grid of 4x2 or 2x4 depending on the orientation
    • WeekdayCard is a HookWidget, it takes a weekday and listens to the planProvider
    • AddPlanScreen is a HookWidget. This allows to maintain a TextEditingController without needing a StatefulWidget. It also changes the state of the planProvider using a context.read

    What about the provider?

    final planProvider = StateProvider.family<String, int>((ref, weekday) => '');
    

    It's a simple StateProvider using the .family provider modifier. (more info)

    This allows us to listen and modify the plan for a specific weekday:

    listen:

    final String plan = useProvider(planProvider(weekday)).state;
    

    modify:

    context.read(planProvider(weekday)).state = plan;
    

    Full source code

    import 'package:flutter/material.dart';
    import 'package:flutter_hooks/flutter_hooks.dart';
    import 'package:hooks_riverpod/hooks_riverpod.dart';
    
    void main() {
      runApp(MyPlanner());
    }
    
    class MyPlanner extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return ProviderScope(
          child: MaterialApp(
            debugShowCheckedModeBanner: false,
            theme: ThemeData(fontFamily: 'IndieFlower'),
            initialRoute: PlannerScreen.id,
            routes: {
              // WelcomeScreen.id: (context) => WelcomeScreen(),
              // RegisterScreen.id: (context) => RegisterScreen(),
              // LogInScreen.id: (context) => LogInScreen(),
              PlannerScreen.id: (context) => PlannerScreen(),
            },
          ),
        );
      }
    }
    
    // SCREENS
    
    class PlannerScreen extends StatelessWidget {
      static const String id = 'planner_screen';
    
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Color(0xFFD04E43),
          body: LayoutBuilder(
            builder: (_, constraints) {
              final mainAxisCount =
                  MediaQuery.of(context).orientation == Orientation.landscape
                      ? 2
                      : 4;
              final crossAxisCount = mainAxisCount == 2 ? 4 : 2;
              final aspectRatio =
                  constraints.biggest.aspectRatio * mainAxisCount / crossAxisCount;
              return GridView.count(
                crossAxisCount: crossAxisCount,
                childAspectRatio: aspectRatio,
                children: weekdayNames.keys
                    .map((weekday) => WeekdayCard(weekday: weekday))
                    .toList(),
              );
            },
          ),
        );
      }
    }
    
    // WIDGETS
    
    class WeekdayCard extends HookWidget {
      final int weekday;
    
      WeekdayCard({@required this.weekday});
    
      @override
      Widget build(BuildContext context) {
        final plan = useProvider(planProvider(weekday)).state;
        return InkWell(
          onTap: () {
            showModalBottomSheet(
              backgroundColor: Color(0xFFAFBDB8),
              barrierColor: Colors.black38,
              context: context,
              builder: (context) => AddPlanScreen(weekday: weekday),
            );
          },
          child: Card(
            color: Color(0xFFAFBDB8),
            elevation: 10,
            child: Column(
              children: [
                Text(weekdayNames[weekday]),
                Text(plan),
              ],
            ),
          ),
        );
      }
    }
    
    class AddPlanScreen extends HookWidget {
      final int weekday;
    
      const AddPlanScreen({Key key, this.weekday}) : super(key: key);
    
      void submit(BuildContext context, String plan) {
        context.read(planProvider(weekday)).state = plan;
        Navigator.pop(context);
      }
    
      @override
      Widget build(BuildContext context) {
        final _controller = useTextEditingController(text: '');
        return Container(
          padding: EdgeInsets.all(16.0),
          alignment: Alignment.center,
          child: Column(
            children: [
              Text('What are you planning for ${weekdayNames[weekday]}?'),
              const SizedBox(height: 16.0),
              TextFormField(
                controller: _controller,
                decoration: InputDecoration(
                  enabledBorder: OutlineInputBorder(
                    borderRadius: new BorderRadius.circular(25.0),
                    borderSide: BorderSide(
                      color: Colors.black45,
                      width: 2.0,
                    ),
                  ),
                  focusedBorder: OutlineInputBorder(
                    borderRadius: new BorderRadius.circular(25.0),
                    borderSide: BorderSide(
                      color: Color(0xFFD04E43),
                      width: 2.0,
                    ),
                  ),
                ),
                autofocus: true,
                onEditingComplete: () => submit(context, _controller.text),
              ),
              const SizedBox(height: 16.0),
              ElevatedButton(
                style: ElevatedButton.styleFrom(
                  primary: Color(0xFF548279),
                  onPrimary: Colors.white,
                ),
                onPressed: () => submit(context, _controller.text),
                child: Text('UPDATE'),
              ),
            ],
          ),
        );
      }
    }
    
    // PROVIDERS
    
    final planProvider = StateProvider.family<String, int>((ref, weekday) => '');
    
    // DOMAIN
    
    const weekdayNames = {
      0: 'Notes',
      DateTime.monday: 'Monday',
      DateTime.tuesday: 'Tuesday',
      DateTime.wednesday: 'Wednesday',
      DateTime.thursday: 'Thursday',
      DateTime.friday: 'Friday',
      DateTime.saturday: 'Saturday',
      DateTime.sunday: 'Sunday',
    };