0

My app is intended to consume live sensors data from an API using flutter scoped_model. The data is a JSON array like these:

[
  {
    "id": 4,
    "device_name": "fermentero2",
    "active_beer": 4,
    "active_beer_name": "Sourgobo",
    "controller_fridge_temp": "Fridge --.-   1.0 ░C",
    "controller_beer_temp": "Beer   28.6  10.0 ░C",
    "active_beer_temp": 28.63,
    "active_fridge_temp": null,
    "active_beer_set": 10,
    "active_fridge_set": 1,
    "controller_mode": "b"
  },
  {
    "id": 6,
    "device_name": "brewpi",
    "active_beer": 1,
    "active_beer_name": "Amber Ale",
    "controller_fridge_temp": null,
    "controller_beer_temp": null,
    "active_beer_temp": null,
    "active_fridge_temp": null,
    "active_beer_set": null,
    "active_fridge_set": null,
    "controller_mode": null
  }
]

Those are devices. My Device model is as follow (json annotation):

@JsonSerializable(nullable: false)
class Device {
  int id;
  String device_name;
  @JsonKey(nullable: true) int active_beer;
  @JsonKey(nullable: true) String active_beer_name;
  @JsonKey(nullable: true) String controller_mode; // manual beer/fridge ou perfil
  @JsonKey(nullable: true) double active_beer_temp;
  @JsonKey(nullable: true) double active_fridge_temp;
  @JsonKey(nullable: true) double active_beer_set;
  @JsonKey(nullable: true) double active_fridge_set;


  Device({
    this.id,
    this.device_name,
    this.active_beer,
    this.active_beer_name,
    this.controller_mode,
    this.active_beer_temp,
    this.active_beer_set,
    this.active_fridge_set,
  });

  factory Device.fromJson(Map<String, dynamic> json) => _$DeviceFromJson(json);
  Map<String, dynamic> toJson() => _$DeviceToJson(this);

}

My scoped model class for the Device is as follow:

class DeviceModel extends Model {

  Timer timer;

  List<dynamic> _deviceList = [];
  List<dynamic> get devices => _deviceList;

  set _devices(List<dynamic> value) {
    _deviceList = value;
    notifyListeners();
  }



  List _data;

  Future getDevices() async {
    loading = true;
    _data = await getDeviceInfo()
        .then((response) {
      print('Type of devices is ${response.runtimeType}');
      print("Array: $response");
      _devices = response.map((d) => Device.fromJson(d)).toList();
      loading = false;
      notifyListeners();
    });
  }



  bool _loading = false;

  bool get loading => _loading;

  set loading(bool value) {
    _loading = value;
    notifyListeners();
  }


    notifyListeners();
}

My UI is intended to be a list of devices showing live data (rebuild ui as sensor data change) and a detail page of each Device, also showing live data. For that I'm using a timer. The page to list Devices is working as expected and "refreshing" every 30 seconds:

class DevicesPage extends StatefulWidget {
  @override
  State<DevicesPage> createState() => _DevicesPageState();
}

class _DevicesPageState extends State<DevicesPage> {
  DeviceModel model = DeviceModel();

  Timer timer;

  @override
  void initState() {
    model.getDevices();
    super.initState();
    timer = Timer.periodic(Duration(seconds: 30), (Timer t) => model.getDevices());
  }

  @override
  Widget build(BuildContext) {
    return Scaffold(
      appBar: new AppBar(
        title: new Text('Controladores'),
      ),
      drawer: AppDrawer(),
      body: ScopedModel<DeviceModel>(
        model: model,
        child: _buildListView(),
      ),
    );
  }

  _buildListView() {
    return ScopedModelDescendant<DeviceModel>(
      builder: (BuildContext context, Widget child, DeviceModel model) {
        if (model.loading) {
          return UiLoading();
        }
        final devicesList = model.devices;
        return ListView.builder(
          itemBuilder: (context, index) => InkWell(
            splashColor: Colors.blue[300],
            child: _buildListTile(devicesList[index]),
            onTap: () {
              Route route = MaterialPageRoute(
                builder: (context) => DevicePage(devicesList[index]),
              );
              Navigator.push(context, route);
            },
          ),
          itemCount: devicesList.length,
        );
      },
    );
  }

  _buildListTile(Device device) {
    return Card(
      child: ListTile(
        leading: Icon(Icons.devices),
        title: device.device_name == null
        ? null
            : Text(
        device.device_name.toString() ?? "",
        ),
        subtitle: device.active_beer_name == null
            ? null
            : Text(
          device.active_beer_temp.toString() ?? "",
        ),
      ),
    );
  }
}

class UiLoading extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          CircularProgressIndicator(),
          SizedBox(height: 12),
          Text(
            'Loading',
            style: TextStyle(
              fontWeight: FontWeight.bold,
            ),
          ),
        ],
      ),
    );
  }
} 

The problem happens with the detail page UI that is also supposed to show live Data but it behaves like a statelesswidget and do not rebuild itself after the Model gets updated:

class DevicePage extends StatefulWidget {

  Device device;
  DevicePage(this.device);

  @override
  //State<DevicePage> createState() => _DevicePageState(device);
  State<DevicePage> createState() => _DevicePageState();
}

class _DevicePageState extends State<DevicePage> {

  DeviceModel model = DeviceModel();

  Timer timer;

  @override
  void initState() {
    DeviceModel model = DeviceModel();
    super.initState();
    timer = Timer.periodic(Duration(seconds: 30), (Timer t) => model.updateDevice());

  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: new AppBar(
        title: new Text(widget.device.device_name),
      ),

      drawer: AppDrawer(),
      body: ScopedModel<DeviceModel>(
        model: model,
        child: _buildView(widget.device),
      ),
    );
  }

  _buildView(Device device) {
    return ScopedModelDescendant<DeviceModel>(
      builder: (BuildContext context, Widget child, DeviceModel model) {
        if (model.loading) {
          return UiLoading();
        }
        return Card(
          child: ListTile(
            leading: Icon(Icons.devices),
            title: device.device_name == null
                ? null
                : Text(
              device.device_name.toString() ?? "",
            ),
            subtitle: device.active_beer_name == null
                ? null
                : Text(
              device.active_beer_temp.toString() ?? "",
            ),
          ),
        );
      },
    );
  }
}

class UiLoading extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          CircularProgressIndicator(),
          SizedBox(height: 12),
          Text(
            'Loading',
            style: TextStyle(
              fontWeight: FontWeight.bold,
            ),
          ),
        ],
      ),
    );
  }

What am I missing ? many thanks in advance

deedos
  • 1
  • 3

1 Answers1

0

It looks like you're building a new DeviceModel for your DevicePage which means that model will be the one your ui would react to, not the one higher up the widget tree - your DevicesPage.

ScopedModel<DeviceModel>(
        model: model,
        child: _buildView(widget.device),
      )

On your DevicePage where you add a the body to your Scaffold replace the ScopedModel with just:

_buildView(widget.device)

That should solve your issue.

Adrian Murray
  • 2,170
  • 1
  • 13
  • 15
  • Also, you need to pull the device info directly from the model's list of devices, not from the DevicesPage as a property. Use the DevicePage's property to get an ID and then get the device from the array: model.devices.singleWhere((d)=>d.id == deviceID,orElse()=>null); – Adrian Murray May 28 '19 at 19:46
  • Many thanks Adrian! Only replacing as you stated firstly, gave me an " Another exception was thrown: Error: Could not find the correct ScopedModel." Now, I completely agree with you for getting the device from the array and not from the property!! I just could not figure it ou how to do it only with this snippet provided "model.devices.singleWhere((d)=>d.id == deviceID,orElse()=>null); " How do I define this new variable ? – deedos May 28 '19 at 20:11
  • Your device list shouldn't be dynamic. It should be casted as List so that it knows about the variables available (such as your Device class' 'id' variable). As for the error, you need to make sure that both the DevicesPage and the DevicePage are under a shared parent widget. Since you want to move between pages with the navigator you should place the model above the MaterialApp widget. This will allow you to pass the data to anywhere in the app. – Adrian Murray May 28 '19 at 20:16
  • Thanks again for the quick reply! How do I cast List for my list ? Could you enlight me with some code snippets ? Other doubt: My DevicePage and DevicesPages are separate ones. How to unite those under a shared parent widget ? – deedos May 28 '19 at 20:46
  • Declaring it as List is the snippet. It will do what you want. Place the Model above the MaterialApp widget and that will ensure they're under the same parent. This is required if you're going to pass data to children that are in different screens. – Adrian Murray May 28 '19 at 21:05
  • Hi. When I try to : List_deviceList = []; List get devices => _deviceList; set _devices(List value) { _deviceList = value; notifyListeners(); } I get a Error: "Unhandled Exception: type 'List' is not a subtype of type 'List' – deedos May 28 '19 at 22:36
  • when you try to... I see you've got a getter and setter but you didn't list the issue you're seeing. I typically don't set my lists upon receiving data. That'll overwrite any non changing values to not exist anymore. Instead I do a push or replace values. If you're expecting the whole list to change than that's probably okay, but if you're only getting portions to change on each check then you should not set the entire list. You may want to consider doing maps of maps instead of a list/array. – Adrian Murray May 28 '19 at 22:44
  • Hi Adrian. I have managed to 1: Cast my response to List 2: place the model above the MaterialApp for both pages being under it and now I 'm trying to pull the device info from the model's list of devices and here I ḿ stuck: every time i get a null response from "model.devices". For example: thisId = widget.device.id.toString(); final _device = model.devices.singleWhere((d) => d.id == thisId, orElse: () => null); Null all the time.. – deedos May 30 '19 at 20:54
  • Why are you calling .toString() on the id? The id property of your device class is an int which means if you are making it a string it's going to try to match a string and an integer, which isn't going to work. – Adrian Murray May 30 '19 at 21:18
  • I could sort it out (problem was placing my models above MaterialApp). Now I 'm getting the data from the right Device!! only one problem: it's not automatically refreshing yet as it happens at DevicesPage. Any clue ? – deedos May 30 '19 at 21:32
  • I'm guessing this has to do with using a timer in both your lists as your method to refresh data. I'm not sure what your api looks like for grabbing this information but I would avoid a timer if possible and instead use a stream then update the ui on data changes. If you need a timer to update, then something you could do is to place the periodic timer in the model itself and then let it make refresh calls on its own and then the data changes would be sent down to all the listeners below. – Adrian Murray May 30 '19 at 21:45