I am trying to build a chat application. but whenever i call a method to update the chat list
everything works, but the widget is not rebuilding.
i have tried with context.read() and context.watch() methods : Not working
i have tried with Provider.of(context,listen:true); : Not working
i have tried with ChangeNotifierProxyProvider : Not working
only restarting rebuilds the ui. any help would be appreciated.
below is my code.
void main() {
runApp(MultiProvider(providers: [
ChangeNotifierProvider(create: (context) => UsersProvider()),
ChangeNotifierProvider(create: (context) => ChatProvider()),
], child: const MyApp()));
}
class UsersProvider extends ChangeNotifier {
UsersProvider(){
getChats();
}
List<Chats> get chats=> _chats;
List<Chats> _chats = [];
Future getChats() async {
final json = await UsersRepo().getChatLists();
if (json == null) {
return;
}
if (json['status']) {
ChatListModel _model = ChatListModel.fromJson(json);
_chats = _model.chats ?? [];
notifyListeners();
}
}
}
and the ui looks like
class ChatList extends StatelessWidget {
const ChatList({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.appBg,
appBar: AppBar(
title: Text('Users'),
backgroundColor: AppColors.appBgLight,
),
body: chatListWidget(context),
);
}
Widget chatListWidget(BuildContext context) {
return Consumer<UsersProvider>(
builder: (BuildContext context, provider, Widget? child) {
return ListView.separated(
padding: EdgeInsets.symmetric(vertical: 20),
itemCount: provider.chats.length,
separatorBuilder: (c, i) => sbh(15),
itemBuilder: (c, i) {
Chats chat = provider.chats[i];
return ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (c) {
return ChatScreen(
user: Users(
name: chat.receiver?.name, sId: chat.receiver?.id),
);
},
),
);
context.read<ChatProvider>().connectToUser(chat.receiver?.id);
},
leading:
userImageWidget((chat.receiver?.name ?? "")[0].toUpperCase()),
subtitle: Text(
chat.lastMessage ?? "",
style: TextStyle(
color: Colors.white70,
fontSize: 12,
),
),
trailing: CircleAvatar(
radius: 15,
backgroundColor: Colors.green,
child: Center(
child: Text(
'${chat.unReadMessageCount}',
style: TextStyle(fontSize: 12, color: Colors.white),
)),
),
title: nameWidget(chat.receiver?.name ?? ''),
);
},
);
},
);
}
}
Chats model class
class Chats {
String? lastMessage;
String? messageTime;
int? unReadMessageCount;
Chats(
{this.lastMessage,
this.messageTime,
this.unReadMessageCount});
Chats.fromJson(Map<String, dynamic> json) {
lastMessage = json['lastMessage'];
messageTime = json['messageTime'];
unReadMessageCount = json['unReadMessageCount'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['lastMessage'] = this.lastMessage;
data['messageTime'] = this.messageTime;
data['unReadMessageCount'] = this.unReadMessageCount;
return data;
}
}
I suspect that your getChats() method is failing before it can execute the notifyListeners method.
You see, any "if" statement in dart has to evaluate to a boolean, so unless json['status'] returns true/false then the expression:
if (json['status'])
will throw an error that will end up in a failed future to be returned from your getChats() method.
It would be better to use a more specific condition for your if statement, maybe something like this (if json['status'] is supposed to contain the http response code for the call):
if (json['status'] == 200)
In the case json['status'] does indeed contains a boolean then there could be a few other reasons for your widget not rebuilding:
1- json['status'] always evaluates to false
2- The call to get the chats fails but you get a silent error since you don't have a handler for the failed future that is returned by the getChats method
Related
I already have data stored on firestore , but now i'd like to add this data into an empty list using ChangeNotifier , similar to this example -https://docs.flutter.dev/development/data-and-backend/state-mgmt/simple .
so below i'd like to add data stored on firebase into the _cart list , with the method I tried below I got the error The argument type 'List<Menu>' can't be assigned to the parameter type 'Menu'(would like to know why?) , it also contains the ui for where i'd like to map the data from cart list into the dialog sheet:
class AddList extends ConsumerWidget {
const AddList({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final menuAsync = ref.watch(menuProvider);
final model = ref.read(cartProvider);
return Scaffold(
appBar: AppBar(
title: const Text("menu"),
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.black,
onPressed: () async {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("cart"),
content: Column(
children: const [
...model.cart.map((item) => Text(item.mealName)),
],
),
);
});
},
),
body: menuAsync.when(
data: (menu) => Column(
children: menu
.map(
(e) => Card(
child: ListTile(
title: Text(e.mealName),
subtitle: Text(e.price),
trailing: IconButton(
onPressed: () {
model.addProduct(menu);//where im getting error
},
icon: const Icon(Icons.add)))),
)
.toList(),
),
error: (e, s) => Center(child: Text("$e")),
loading: () => const Center(child: CircularProgressIndicator())),
);
}
}
below im using changenotifier to modify the cart list:
final cartProvider =
ChangeNotifierProvider<CartNotifier>((ref) => CartNotifier());
class CartNotifier extends ChangeNotifier {
final List<Menu> _cart = [];
List<Menu> get cart => _cart;
void addProduct(Menu menu) {
_cart.add(menu);
notifyListeners();
}
void removeProduct(Menu menu) {
_cart.remove(menu);
notifyListeners();
}
}
how im reading data from firestore :
final menuProvider = StreamProvider<List<Menu>>(
(ref) => ref.read(addMealRespositoryProvider).menuSearchStream);
Stream<List<Menu>> get menuSearchStream =>
_firestore.collection("menu").snapshots().map(
(event) => event.docs.map((e) => Menu.fromFirestore(e)).toList(),
);
snippet of my data model:
class Menu {
String mealName;
String price;
Menu({
required this.mealName,
required this.price,
});
Map<String, dynamic> toMap() {
return {
"mealName": mealName,
"price": price, },
factory Menu.fromFirestore(DocumentSnapshot doc) {
final map = doc.data() as Map<String, dynamic>;
return Menu(
mealName: map["mealName"] ?? '',
price: map["price"] ?? '',
);
}
}
Below is the list of data that i am fetching
[
{
"name": "kamchatka11111",
"email": "kamchatka#mail.com",
"index": 1,
"picture": "https://static.themoscowtimes.com/image/1360/73/TASS40183186.jpg",
"imageFetchType": "networkImage"
},
{
"name": "Valjakutse",
"email": "valjakutse#mail.com",
"index": 2,
"picture": "https://images.unsplash.com/photo-1561406636-b80293969660?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8YmxhY2slMjBsYWR5fGVufDB8fDB8fA%3D%3D&w=1000&q=80",
"imageFetchType": "imageNetwork"
},
{
"name": "einstein",
"email": "einstein#mail.com",
"index": 12,
"picture": "https://static.dw.com/image/45897958_303.jpg",
"imageFetchType": "fadeInImage"
}
]
Below is how i am populating this list in my flutter class
import 'package:flutter/material.dart';
import 'package:master_learn/classes/async_futures.dart';
import 'package:master_learn/classes/config.dart';
import 'package:master_learn/lists_grids_screens/user_details.dart';
import 'package:master_learn/widgets/marquee_widget.dart';
class FutureBuilderListUsingHashMap extends StatefulWidget {
const FutureBuilderListUsingHashMap({Key? key}) : super(key: key);
#override
_FutureBuilderListUsingHashMapState createState() =>
_FutureBuilderListUsingHashMapState();
}
class _FutureBuilderListUsingHashMapState
extends State<FutureBuilderListUsingHashMap> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(
Icons.arrow_back,
color: Colors.white,
)),
title: const SizedBox(
child: MarqueeWidget(
direction: Axis.horizontal,
child: Text(
"Fetch"),
),
),
),
body: SizedBox(
child: FutureBuilder(
future: AsyncFutures.fetchListsOfStringDynamicHashMap(),
builder: (BuildContext context, AsyncSnapshot asyncSnapshot) {
if (asyncSnapshot.data == null) {
return const Center(child: CircularProgressIndicator());
} else {
return ListView.builder(
itemCount: asyncSnapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
contentPadding: const EdgeInsets.all(10),
leading: CircleAvatar(
backgroundImage: NetworkImage(
asyncSnapshot.data[index]["picture"] ??
""),
),
title:
Text(asyncSnapshot.data[index]["name"] ?? ''),
subtitle: Text(
"Email: ${asyncSnapshot.data[index]["email"] ?? ''}"),
);
},
);
}
}),
),
);
}
}
The problem i am having is how can i use AsyncSnapshot with FutureBuilder to populate data that is not of a list for example the data below
{
"name": "einstein",
"email": "einstein#mail.com",
"index": 12,
"picture": "https://static.dw.com/image/45897958_303.jpg",
"imageFetchType": "fadeInImage"
}
This may be for example when i am fetching the information for a single profile instead of a list of information of all the profiles
Use the FutureBuilder instead of ListView.builder to fetch a single object. Refer to this awesome explanation in the flutter docs:
https://docs.flutter.dev/cookbook/networking/fetch-data#1-add-the-http-package
To encode your json data into proper flutter object you can visit this website
https://app.quicktype.io/
Hi Emmanuel hope you are well
A recommended practice would be to map your class, converting it from JSON to a class.
Profile class:
class Profile{
final String name;
final String image;
Profile({
#required this.name,
#required this.image,
});
factory Profile.fromJson(Map<String, dynamic> json) {
return Profile(
name: json['name'] as String,
image: json['image'] as String,
);
}
}
Your data request class
class HttpService {
final String profilesURL = "https://sourceDataUrl.example";
Future<List<Profile>> getProfiles() async {
Response res = await get(profilesURL);
if (res.statusCode == 200) {
List<dynamic> body = jsonDecode(res.body);
List<Profile> profiles = body
.map(
(dynamic item) => Profile.fromJson(item),
)
.toList();
return profiles;
} else {
throw "Unable to retrieve profiles.";
}
}
}
and get the request data in a FutureBuilder
class ProfilesPage extends StatelessWidget {
final HttpService httpService = HttpService();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Profile"),
),
body: FutureBuilder(
future: httpService.getProfiles(),
builder: (BuildContext context, AsyncSnapshot<List<Profile>> snapshot) {
if (snapshot.hasData) {
List<Profile> listProfile = snapshot.data;
return ListView(
children: listProfile
.map(
(Profile profile) => ListTile(
name: Text(profile.name),
),
)
.toList(),
);
} else {
return Center(child: CircularProgressIndicator());
}
},
),
);
}
}
You can refer to this link for more information about http requests.
I have a view with a list of job applications,
when you click on one of the job applications, it sends the context to the next page, where you can edit the job application.
when I click a job application, it sends its context to the next page I think. it then populates the local variables I think.
the goal is to update the job application on the server, when the change is recorded, I should get the new data back from the server, and it should then update the local variables. right now I only have the button 'submit inquiry' to make the code easy to follow.
Below is the view that has all job applications
class JobApplicationsView extends StatefulWidget {
const JobApplicationsView({Key? key}) : super(key: key);
#override
_JobApplicationsViewState createState() => _JobApplicationsViewState();
}
class _JobApplicationsViewState extends State<JobApplicationsView> {
late final FirebaseCloudStorage _jobsService;
String get userId => AuthService.firebase().currentUser!.id;
#override
void initState() {
_jobsService = FirebaseCloudStorage();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Your job applications'),
actions: [
PopupMenuButton<MenuAction>(
onSelected: (value) async {
switch (value) {
case MenuAction.logout:
final shouldLogout = await showLogOutDialog(context);
if (shouldLogout) {
await AuthService.firebase().logOut();
Navigator.of(context).pushNamedAndRemoveUntil(
loginRoute,
(_) => false,
);
}
}
},
itemBuilder: (context) {
return const [
PopupMenuItem<MenuAction>(
value: MenuAction.logout,
child: Text('Log out'),
),
];
},
)
],
),
body: StreamBuilder(
stream: _jobsService.allJobApplications(userId),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
case ConnectionState.active:
if (snapshot.hasData) {
final allJobApplications =
snapshot.data as Iterable<CloudJobApplication>;
return JobApplicationsListView(
allowScroll: false,
jobApplications: allJobApplications,
onTap: (job) {
Navigator.of(context).pushNamed(
myJobApplicationsRoute,
arguments: job,
);
},
);
} else {
return const CircularProgressIndicator();
}
default:
return const CircularProgressIndicator();
}
},
),
);
}
}
the function used to get all job applications.
final jobApplication = FirebaseFirestore.instance.collection('job application');
Stream<Iterable<CloudJobApplication>> allJobApplications(userId) =>
jobApplication.snapshots().map((event) => event.docs
.map((doc) => CloudJobApplication.fromSnapshot(doc))
.where((jobApplication) => jobApplication.jobApplicatorId == userId));
here is the view that allows the job application to be edited:
The function 'getExistingJobApplication' is where the issue might be.
class JobApplicationView extends StatefulWidget {
const JobApplicationView({Key? key}) : super(key: key);
#override
_JobApplicationViewState createState() => _JobApplicationViewState();
}
// https://youtu.be/VPvVD8t02U8?t=90350
class _JobApplicationViewState extends State<JobApplicationView> {
CloudJobApplication? _jobApplication;
late final FirebaseCloudStorage _cloudFunctions;
final _formKey = GlobalKey<FormState>();
final currentUser = AuthService.firebase().currentUser!;
// state varibles
String _jobApplicationState = 'default';
String? _jobApplicationSubState;
String? _jobCreatorId;
String? _jobApplicatorId;
String? _jobApplicationEstimate;
late Timestamp? _jobApplicationStartDate;
late Timestamp? _jobApplicationEndDate;
late final TextEditingController _jobDescriptionController;
bool? _formFieldsEditable;
#override
void initState() {
super.initState();
_cloudFunctions = FirebaseCloudStorage();
_jobDescriptionController = TextEditingController();
}
Future<CloudJobApplication> getExistingJobApplication(
BuildContext context) async {
// if the local state variables are default, use the information from the context
// if the local state variables are not default, that means an update has occur on the server. fetch the new data from the server
var widgetJobApplication;
if (_jobApplicationState == 'default') {
log('using job application from context');
widgetJobApplication = context.getArgument<CloudJobApplication>();
_jobApplication = widgetJobApplication;
_jobApplicationState =
widgetJobApplication?.jobApplicationState as String;
_jobApplicationEstimate = widgetJobApplication.jobApplicationEstimate;
_jobDescriptionController.text =
widgetJobApplication?.jobApplicationDescription as String;
_jobApplicationStartDate = widgetJobApplication.jobApplicationStartDate;
_jobApplicationEndDate = widgetJobApplication.jobApplicationEndDate;
_jobCreatorId = widgetJobApplication.jobCreatorId;
_jobApplicatorId = widgetJobApplication.jobApplicatorId;
_formFieldsEditable = _jobApplicationState == jobApplicationStateNew ||
_jobApplicationState == jobApplicationStateOpen
? true
: false;
} else {
log('using job application from server');
widgetJobApplication =
_cloudFunctions.getJobApplication(_jobApplication!.documentId);
}
return widgetJobApplication;
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('update job application'),
actions: [],
),
body: FutureBuilder(
future: getExistingJobApplication(context),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
return Form(
key: _formKey,
child: ListView(padding: const EdgeInsets.all(32.0), children: [
JobApplicationStateVariablesWidget(
jobApplicationState: _jobApplicationState),
const Divider(
height: 20,
thickness: 5,
indent: 0,
endIndent: 0,
color: Colors.blue,
),
ElevatedButton(
style: ElevatedButton.styleFrom(
textStyle: const TextStyle(fontSize: 13),
backgroundColor: false ? Colors.black : Colors.blue,
),
// HERE IS BUTTON AT PLAY
onPressed: () async {
setState(() {
_jobApplicationState = jobApplicationStatePending;
});
await _cloudFunctions.updateJobApplicationColumn(
documentId: _jobApplication?.documentId as String,
fieldNameColumn: jobApplicationStateColumn,
fieldNameColumnValue: jobApplicationStatePending);
},
child: const Text('Submit inquiry'),
),
]),
);
default:
return const CircularProgressIndicator();
}
},
),
);
}
}
The way its set up right now works but in a crappy way...
for example: if the job was closed, then the job application should become closed. the way I have the code right now wont work with that apprach because I am hitting the local state variables directly
I have a ListView consists of several ListTiles which have a trailing icon. The color of icon should change from transparent to green based on user tap. However the UI is not updating on user interaction.
The ServiceModel is like this.
class ProviderService extends ChangeNotifier {
final List<String> totalNames = ['Somesh', 'Tarulata', 'Indranil', 'Satyajyoti', 'Biswas', 'Sajal', 'Kumar', 'Slipa', 'Sonam', 'Neelam'];
List<String> _selectedNames = [];
List<String> get selectedNames => _selectedNames;
void updateselectedNames(String name) {
bool isExists = _selectedNames.contains(name);
if (isExists)
_selectedNames.remove(name);
else
_selectedNames.add(name);
notifyListeners();
}
}
The ListView goes like this.
class Members extends StatelessWidget {
#override
Widget build(BuildContext context) {
ProviderService plService = Provider.of<ProviderService>(context, listen: false);
return Scaffold(
body: SafeArea(
child: Selector<ProviderService, List<String>>(
selector: (_, service) => service.selectedNames,
builder: (context, selNames, child) {
if (plService.totalNames.isEmpty) return child;
return ListView.separated(
shrinkWrap: true,
itemBuilder: (context, index) {
String _name = plService.totalNames[index];
return ListTile(
title: Text('$_name'),
trailing: Icon(
Icons.check_circle,
color: selNames.contains(_name) ? Colors.lightGreen : Colors.transparent,
),
onTap: () {
plService.updateselectedNames(_name),
print(selNames);
},
);
},
separatorBuilder: (_, __) => Divider(),
itemCount: plService.totalNames.length,
);
},
child: Center(
child: Text('No names have been found', textAlign: TextAlign.center),
),
),
),
);
}
}
and of course the main.dart is like this.
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ChangeNotifierProvider(
create: (context) => ProviderService(),
child: Members(),
),
);
}
}
Even though the list selectedNames updated, the UI remains same. What's going on wrong here ?
You may add the shouldRebuild parameter of Selector and return true。like this:
Selector<ProviderService, List<String>>(
selector: (_, service) => service.selectedNames,
builder: (context, selNames, child) {...},
shouldRebuild: (previous, next) => true,
)
When you use a Selector, you have to make sure that the selected object is immutable.
Selector<ProviderService, List<String>>(
selector: (_, service) => service.selectedNames,
builder: (context, selNames, child) { ...},
),
builder will only get called once because your selectedNames object always stays the same. You are removing and adding items in the same array Object.
So, you should instead provide a new array in your updateselectedNames:
void updateselectedNames(String name) {
_selectedNames = _selectedNames.contains(name)
? _selectedNames.where((item) => item != name).toList()
: [..._selectedNames, name];
notifyListeners();
}
My way would be like this for your scenario.
class ProviderService extends ChangeNotifier {
final List<Name> totalNames = [
Name(name: 'Somesh', isTransparent: false),
Name(name: 'Tarulata', isTransparent: false),
];
List<Name> _selectedNames = [];
List<Name> get selectedNames => _selectedNames;
void updateselectedNames(int index) {
var exist = _isExist(totalNames[index]);
if(exist){
_selectedNames.remove(totalNames[index]);
} else {
_selectedNames.add(totalNames[index]);
}
totalNames[index].isTransparent = !totalNames[index].isTransparent;
notifyListeners();
}
bool _isExist(Name name) {
var filter = _selectedNames.singleWhere(
(element) => element.name == name.name,
orElse: () => null,
);
return filter != null;
}
}
class Name {
String name;
bool isTransparent;
Name({this.name, this.isTransparent});
}
And you can use Selector in ListView for every ListTile
Selector<ProviderService, Name>(
selector: (_, service) => service.totalNames[index],
builder: (context, name, child) {
return ListTile(
title: Text('${name.name}'),
trailing: Icon(
Icons.check_circle,
color: !name.isTransparent ? Colors.lightGreen : Colors.transparent,
),
onTap: () {
plService.updateselectedNames(index),
},
);
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
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.