Using getIt to access Ably stream and Graph up data doesn't work unless its the home screen

79 views Asked by At

I have an MQTT sensor sending messages to the Ably platform (messaging platform) and then I am trying to subscribe to that channel and recieve the data to the app... My issue (i think) is in the getIt package as if i start my app on the dashboard page, it works, if i navigate to the page, it stays on CircularProgressIndicator(). I have removed all non-relevant code.

What is odd, is that starting the app on dashboard works everytime, but starting on homepage and navigating to the dashboard generally doesn't work, but i have had a few occurances where it does. Where it does work the build widget is called again. I have talked with Ably and don't think its an issue their side as even though the graphs aren't showing, the debug console shows that data is being recieved.

import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:mre/ably_service.dart';
import 'package:mre/officeDashboardView.dart';
import 'package:mre/home_page.dart';

GetIt getIt = GetIt.instance;

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  getIt.registerSingletonAsync<AblyService>(() => AblyService.init());
  runApp(MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'B.O.B',
      theme: ThemeData.light(useMaterial3: true).copyWith(
        scaffoldBackgroundColor: const Color.fromARGB(255, 255, 255, 255),
        appBarTheme: const AppBarTheme(
          backgroundColor: Color.fromARGB(255, 235, 235, 235),
        ),
        bottomNavigationBarTheme: const BottomNavigationBarThemeData(
            //backgroundColor: Color.fromARGB(255, 235, 235, 235),
            backgroundColor: Color.fromARGB(200, 200, 200, 200),
            selectedItemColor: Color.fromARGB(255, 0, 0, 0),
            unselectedItemColor: Color.fromARGB(255, 148, 148, 148)),
      ),
      initialRoute: '/',
      routes: {
        '/': (context) => const HomePage(),
        '/Office': (context) => const OfficeDashboardView(),
      },
    );
  }
}
import 'package:flutter/material.dart';

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text('B.O.B'),
          actions: [
            IconButton(
              icon: Icon(Icons.chat),
              padding: EdgeInsets.only(right: 10),
              onPressed: () => Navigator.pushNamed(context, '/Office'),
            )
          ],
          centerTitle: true,
        ),
        body: Container());
  }
}

import 'dart:collection';
import 'package:mre/main.dart';
import 'package:mre/ably_service.dart';
import 'package:ably_flutter/ably_flutter.dart' as ably;
import 'package:flutter/material.dart';
import 'package:intl/intl.dart' as intl;
import 'package:syncfusion_flutter_charts/charts.dart';

class OfficeDashboardView extends StatelessWidget {
  const OfficeDashboardView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Office Conditions", style: TextStyle(fontSize: 16)),
        bottom: PreferredSize(
          preferredSize: const Size.fromHeight(1.0),
          child: Container(
            color: const Color.fromARGB(255, 0, 0, 0),
            height: 1.0,
          ),
        ),
      ),
      body: FutureBuilder(
        future: getIt.allReady(),
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return const Center(child: CircularProgressIndicator());
          } else {
            return const GraphsList();
          }
        },
      ),
    );
  }
}

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

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

class _GraphsListState extends State<GraphsList> {
  List<DataUpdates> values = [];

  @override
  void initState() {
    values = getIt<AblyService>().getDataUpdates();
    super.initState();
  }

  @override
  void dispose() {
    print("dispose Top");
    getIt<AblyService>().detachDataChannels();
    //getIt<AblyService>().detachChatChannel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: StreamBuilder<ably.ConnectionStateChange>(
        stream: getIt<AblyService>().connection,
        builder: (context, snapshot) {
          print("Build Called");
          print("SnapShot = ${snapshot}");
          print("SnapShot Data = ${snapshot.hasData}");
          print("SnapShot Data event = ${snapshot.data?.event}");

          if (snapshot.hasData &&
              snapshot.data!.event == ably.ConnectionEvent.connected) {
            return SingleChildScrollView(
              child: Column(
                children: [
                  for (DataUpdates update in values)
                    DataGraphItem(dataUpdates: update),
                ],
              ),
            );
          } else if (snapshot.hasData &&
              snapshot.data!.event == ably.ConnectionEvent.failed) {
            return const Center(child: Text("No connection."));
          } else {
            return const CircularProgressIndicator();
          }
        },
      ),
    );
  }
}

class DataGraphItem extends StatefulWidget {
  const DataGraphItem({Key? key, required this.dataUpdates}) : super(key: key);
  final DataUpdates dataUpdates;
  @override
  DataGraphItemState createState() => DataGraphItemState();
}

class DataGraphItemState extends State<DataGraphItem> {
  Queue<Data> queue = Queue();
  String dataName = '';
  late VoidCallback _listener;

  @override
  void initState() {
    widget.dataUpdates.addListener(
      _listener = () {
        setState(() {
          queue.add(widget.dataUpdates.data);
        });

        if (queue.length > 100) {
          queue.removeFirst();
        }
      },
    );

    if (dataName.isEmpty) dataName = widget.dataUpdates.name;

    super.initState();
  }

  @override
  void dispose() {
    print("Disposal");
    widget.dataUpdates.removeListener(_listener);

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(15),
      padding: const EdgeInsets.all(15),
      height: 410,
      decoration: BoxDecoration(
          color: const Color(0xffEDEDED).withOpacity(0.05),
          borderRadius: BorderRadius.circular(8.0)),
      child: AnimatedSwitcher(
        duration: const Duration(milliseconds: 500),
        child: queue.isEmpty
            ? Center(
                key: UniqueKey(),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: const [
                    CircularProgressIndicator(),
                    SizedBox(
                      height: 24,
                    ),
                    Text('Waiting for data...')
                  ],
                ),
              )
            : Column(
                key: ValueKey(dataName),
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      AnimatedSwitcher(
                        duration: const Duration(milliseconds: 200),
                        child: Text(
                          "\$${widget.dataUpdates.data.value.toStringAsFixed(2)}",
                          key: ValueKey(widget.dataUpdates.data.value),
                          style: const TextStyle(
                            fontSize: 20,
                          ),
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 25),
                  SfCartesianChart(
                    enableAxisAnimation: true,
                    primaryXAxis: DateTimeAxis(
                      dateFormat: intl.DateFormat.Hms(),
                      intervalType: DateTimeIntervalType.minutes,
                      desiredIntervals: 10,
                      axisLine: const AxisLine(width: 2, color: Colors.white),
                      majorTickLines:
                          const MajorTickLines(color: Colors.transparent),
                    ),
                    primaryYAxis: NumericAxis(
                      numberFormat: intl.NumberFormat('##.##'),
                      desiredIntervals: 5,
                      decimalPlaces: 2,
                      axisLine: const AxisLine(width: 2, color: Colors.white),
                      majorTickLines:
                          const MajorTickLines(color: Colors.transparent),
                    ),
                    plotAreaBorderColor:
                        const Color.fromARGB(255, 0, 255, 0).withOpacity(0.2),
                    plotAreaBorderWidth: 0.2,
                    series: <LineSeries<Data, DateTime>>[
                      LineSeries<Data, DateTime>(
                        animationDuration: 0.0,
                        width: 2,
                        color: Theme.of(context).primaryColor,
                        dataSource: queue.toList(),
                        xValueMapper: (Data data, _) => data.dateTime,
                        yValueMapper: (Data data, _) => data.value,
                      )
                    ],
                  )
                ],
              ),
      ),
    );
  }
}
import 'dart:async';
import 'package:mre/secrets.dart';
import 'package:ably_flutter/ably_flutter.dart' as ably;
import 'package:flutter/foundation.dart';

const List<Map> _dataTypes = [
  {
    "name": "Temperature",
    "code": "Temp",
  },
  {
    "name": "Humidity",
    "code": "Hum",
  },
];

class Data {
  final String code;
  final double value;
  final DateTime? dateTime;

  Data({
    required this.code,
    required this.value,
    required this.dateTime,
  });
}

class DataUpdates extends ChangeNotifier {
  DataUpdates({required this.name});
  final String name;

  late Data _data;

  Data get data => _data;
  updateData(estimate) {
    print("updateData() called with estimate: $estimate");
    _data = estimate;
    notifyListeners();
  }
}

class AblyService {
  final ably.Realtime _realtime;
  List<ably.RealtimeChannel> _dataChannels = [];

  Stream<ably.ConnectionStateChange> get connection =>
      _realtime.connection.on();

  AblyService._(this._realtime);

  static Future<AblyService> init() async {
    final _clientOptions = ably.ClientOptions(
        key: AblyAPIKey,
        clientId: "PhoneAndroid1",
        logLevel: ably.LogLevel.debug);

    final _realtime = ably.Realtime(options: _clientOptions);
    await _realtime.connect();
    return AblyService._(_realtime);
  }

  List<DataUpdates> _dataUpdates = [];

  List<DataUpdates> getDataUpdates() {
    if (_dataUpdates.isEmpty) {
      for (int i = 0; i < _dataTypes.length; i++) {
        String dataName = _dataTypes[i]['name'];
        String dataCode = _dataTypes[i]['code'];

        _dataUpdates.add(DataUpdates(name: dataName));

        //launch a channel for each data type
        ably.RealtimeChannel channel =
            _realtime.channels.get('MQTT:Office:TempandHumidity:$dataCode');

        // Add the channel to the list of channels
        _dataChannels.add(channel);

        //subscribe to receive channel messages
        final Stream<ably.Message> messageStream = channel.subscribe();

        //map each stream event to a Data and start listining
        messageStream.where((event) => event.data != null).listen((message) {
          print(message);

          String charString =
              String.fromCharCodes((message.data as Iterable<int>));
          double myDouble = double.parse(charString);

          _dataUpdates[i].updateData(
            Data(
              code: dataCode,
              value: myDouble, //double.parse('${message.data}'),
              dateTime: message.timestamp,
            ),
          );
        });
      }
    }
    return _dataUpdates;
  }

  void detachDataChannels() {
    if (_dataChannels.isNotEmpty) {
      for (final channel in _dataChannels) {
        channel.detach();
      }
      _dataChannels.clear();
    }
  }
}

I would like that everytime I access the dashboard view, it loads up the graphs and starts plotting the data received. As said above, if i have the dashboard as my homepage it works, but accessing it any other way it doesn't seem to recieve the ably.ConnectionEvent.connected. I can post the Debug console but aware that this post might be very long with all the code above.

Thanks.

0

There are 0 answers