Search code examples
flutterdartgoogle-mapsflutter-provider

How to pass context in ChangeNotifier class


void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  await FirebaseMessageApi().initNotifications();

  final GoogleMapsFlutterPlatform mapsImplementation =
      GoogleMapsFlutterPlatform.instance;
  if (mapsImplementation is GoogleMapsFlutterAndroid) {
    mapsImplementation.useAndroidViewSurface = true;
    mapsImplementation.initializeWithRenderer(AndroidMapRenderer.latest);
  }

  runApp(MaterialApp(
    home: RootScreen(),
  ));
}

main.dart

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

  @override
  State<RootScreen> createState() => _RootScreenState();
}

class _RootScreenState extends State<RootScreen> with TickerProviderStateMixin{
  final PageController _controller = PageController();

  ...

  @override
  Widget build(BuildContext context) {
    print('RootScreen build');
    return ChangeNotifierProvider(
      create: (context) => GoogleMapModel(),
      child: Scaffold(
        body: PageView(
          controller: _controller,
          onPageChanged: onItemTapped,
          children: widgetOptions,
        ),
        bottomNavigationBar: SizedBox(
            height: 70,
            child: renderBottomNavigationBar()),
        extendBody: false,
        extendBodyBehindAppBar: true,
      ),
    );
  }
}

part of RootScreen.dart

In some widget, it passes marker item (cluster) and circles. So I used provider to pass the data globally. The problem is that I need to pass context when creating circles.


class GoogleMapModel with ChangeNotifier {
  final dio = Dio();
  late RestClient client = RestClient(dio);

  List<Circle> _circleItems = [];
  List<ClusterData> _markerItems = [];
  List<ScrollableSheetData> _sheetItems = [];
  String _sheetTitle = "";
  String _bottomSheetTitle = "";

  List<Circle> get circleItems => _circleItems;
  List<ClusterData> get markerItems => _markerItems;
  List<ScrollableSheetData> get sheetItems => _sheetItems;
  String get sheetTitle => _sheetTitle;
  String get bottomSheetTitle => _bottomSheetTitle;

  void RemoveItems() {
    _circleItems.clear();
    _markerItems.clear();
    _sheetItems.clear();
    notifyListeners();
  }

  void EarthQuakeItems() async {
    _circleItems.clear();
    _markerItems.clear();
    _sheetItems.clear();
    try {
      List<EarthQuake> value = await client.getEarthQuakeRecent(); // Retrofit rest api
      if (value.isNotEmpty) {
        for (int i = 0; i < value.length; i++) {
          _circleItems.add(Circle(
            circleId: CircleId(value[i].id.toString()),
            center: LatLng(value[i].latitude, value[i].longitude),
            fillColor: Colors.blue.withOpacity(0.5),
            radius: value[i].magnitude * 10000,
            strokeColor: Colors.blueAccent,
            strokeWidth: 1,
            onTap: () {
              BottomSheets.showItemBottomSheet(context, value[i].id.toString());
            },
            consumeTapEvents: true,
          ));
        }
      }
    } catch (error) {
      // TODO
    }
    notifyListeners();
  }
class BottomSheets {
  static void showItemBottomSheet(BuildContext context, String data){
    showBottomSheet(
        context: context,
        builder: (context) {

BottomSheets are class something like this. It requires context, but I cannot figure how to pass context in GoogleMapModel class. Is there any way that I can pass context? Or is there any good way to show a container when circle is tap? I just have to show a container so showBottomSheet is not necessary.

I tried GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); this thing. but it was no use. Or maybe i called it wrong

+) How I call method

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

  @override
  State<EQ_Info> createState() => _EQ_InfoState();
}

class _EQ_InfoState extends State<EQ_Info> {
  List<Circle> circleItems = [];
  List<ClusterData> markerItems = [];

  ...

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Stack(
        children: [
          Google_Map(
            circleItems: context.watch<GoogleMapModel>().circleItems,
            markerItems: context.watch<GoogleMapModel>().markerItems,
            mode: GoogleMapMode.shelter,
          ),
          const CustomCategory(),
          CustomScrollableSheet(),
        ],
      ),
    );
  }
}

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

  @override
  State<CustomCategory> createState() => _CustomCategoryState();
}

class _CustomCategoryState extends State<CustomCategory> {
  int? _selectedIndex;

  List<Widget> choiceChips() {
    List<Widget> chips = [];
    for (int i = 0; i < _choiceChipsList.length; i++) {
      Widget item = Padding(
        padding: const EdgeInsets.only(left: 2.5, right: 2.5),
        child: ChoiceChip(
          avatar: CircleAvatar(
            child: Icon(_choiceChipsList[i].iconData),
          ),
          label: Text(_choiceChipsList[i].label),
          backgroundColor: Colors.white,
          selected: _selectedIndex == i,
          selectedColor: Colors.deepOrange,
          onSelected: (bool selected) {
            setState(() {
              _selectedIndex = selected ? i : null;
              _handleSelection(_selectedIndex);
            });
          },
        ),
      );
      chips.add(item);
    }
    return chips;
  }

  @override
  Widget build(BuildContext context) {

    return SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      child: Padding(
        padding: const EdgeInsets.only(top: 5, left: 5, right: 5),
        child: Row(
          children: choiceChips(),
        ),
      ),
    );
  }

  void _handleSelection(int? selectedIndex) {
    switch(_selectedIndex){
      case null:
      case 2:
      case 3:
        context.read<GoogleMapModel>().RemoveItems();
        break;
      case 0:
        context.read<GoogleMapModel>().EarthQuakeItems();
        break;
      case 1:
        context.read<GoogleMapModel>().ShelterItems();
        break;
      default:
        context.read<GoogleMapModel>().RemoveItems();
        break;
    }
  }

..

The function needs to be called above of google map widget


Solution

  • Using navigator key should do the work, here is how to use it:

    GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
    // ...
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        title: 'Flutter Demo',
        navigatorKey: navigatorKey,
        // ...
    )
    

    Then you need to pass it to your RootScreen and then pass to GoogleMapModel

    // ...
    return ChangeNotifierProvider(
      create: (context) => GoogleMapModel(navigatorKey),
      // ...
    

    in GoogleMapModel you can store it and then use it like this

    class GoogleMapModel with ChangeNotifier {
      GoogleMapModel(this.navigatorKey);
      final GlobalKey<NavigatorState> navigatorKey;
      void somewhereYouNeedContext() {
        final context = navigatorKey.currentContext;
      }
    }
    

    However, wouldn't it be easier to just pass onTap as an argument in a EarthQuakeItems method? It'd work only if you call this from a place that has context like Widget. You didn't specify where you call EarthQuakeItems, but I suppose it is somewhere like initState The implementation would look something like this:

    void EarthQuakeItems(VoidCallback onTap) async {
      // ...
      _circleItems.add(Circle(
        onTap: onTap,
       ))
      // ...
    }