Flutter using Provider - context.watch<T>() for specific items in a list and ignores the other items updates - flutter

I am a newbie in Flutter and I am trying to build an app using Provider. I will try to provide an oversimplified example here. My app includes a model of a room.
class Room {
String roomDisplayName;
String roomIdentifier;
Image image;
List<IDevices> devices = [];
Room(this.roomDisplayName, this.roomIdentifier, this.image, this.devices);
}
Rooms have list of devices like a temperature sensor
class TempSensor implements IDevices {
late String tempSensorName;
late double temperatureValue;
late double humidityValue;
late int battery;
TempSensor(this.displayName, this.zigbeeFriendlyName);
UpdateTempSensor(double temperature, double humidiy, int battery) {
this.temperatureValue = temperature;
this.humidityValue = humidiy;
this.battery = battery;
}
I have a RoomProvider class that implements ChangeNotifier that is responsible for updating devices in List<Room> rooms
class RoomsRepositoryProvider with ChangeNotifier {
List<Room> get rooms {
//return _rooms;
return _rooms;
}
UpdateTemperatureSensor(TempSensor tempSensor) {
TempSensor? foundTempSensor = null;
_rooms.forEach((room) {
room.devices.forEach((element) {
if (element.displayName == tempSensor.displayName) {
foundTempSensor = element as TempSensor;
}
});
});
if (foundTempSensor != null) {
foundTempSensor?.UpdateTempSensor(tempSensor.temperatureValue,
tempSensor.humidityValue, tempSensor.battery);
notifyListeners();
}
}
I also have a Stateful widget page to show Room information like temperature/humidity value.
class DetailPage extends StatefulWidget {
final Room room;
DetailPage({required this.room});
#override
_DetailPageState createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
#override
Widget build(BuildContext context) {
context.watch<RoomsRepositoryProvider>().rooms;
return Text ("Temperature is ${widget.room.devices[0].temperatureValue}");
}
Here is question:
The problem I am facing is that, if I am showing the Living Room in DetailPage and the temperature sensor from Bedroom gets updated in the List<Room> rooms, the whole DetailPage gets rebuild. Since it is not an issue in the flutter and the app works good. I would still like to know how to solve this architecture problem, that the DetailPage only gets build for the room updates related to the room being shown?
PS: please ignore any build, indentation or naming convention mistakes.

To only rebuild the specific widget, you can wrap that widget inside Consumer widget provider by Provider in flutter. Consumer takes a builder function and will build the widget returned by this builder function only when the data changes.
Consumer(
builder:(context,_,__){
return Container();
},
),

To implement this, you can use a Comsumer widget
Consumer<RoomsRepositoryProvider>(
builder:(context,value,child) => Text("Temperature is ${value.room}");
),
A StatelessWidget is also sufficient. Don't forget the index by room. It should work like this

So, I solved my problem by creating a separate provider DevicesProvider that contains the list of devices modified in the room. I provide the current room by calling the method SetCurrentRoom(String currentRoomIdentifier) from the DetailPage and the provider does its job whenever the devices list in the current room updates.
class DevicesProvider with ChangeNotifier {
String _currentRoomIdentifier = "";
List<IDevices> _listCurrentRoomDevices = [];
List<IDevices> get ListCurrentRoomDevices => _listCurrentRoomDevices;
void SetCurrentRoom(String currentRoomIdentifier) {
_currentRoomIdentifier = currentRoomIdentifier;
}
UpdateDevicesList(IDevices device) {
if (serviceLocator<RoomProviderService>()
.rooms
.any((room) => room.roomIdentifier == _currentRoomIdentifier)) &&
IsDeviceUpdateComingFromCurrentRoom(device)
{
_listCurrentRoomDevices.clear();
var devices = serviceLocator<RoomProviderService>()
.rooms
.firstWhere(
(room) => room.roomIdentifier == _currentRoomIdentifier)
.devices;
_listCurrentRoomDevices.addAll(devices);
notifyListeners();
}
}
bool IsDeviceUpdateComingFromCurrentRoom(IDevices device) {
bool isUpdateFromCurrentRoom = false;
if (device.Name.contains(_currentRoomIdentifier)) {
isUpdateFromCurrenRoom = true;
}
return isUpdateFromCurrentRoom;
}
}
Maybe this can be solved in a different way which is more elegant or efficient, but for now my problem is solved with this approach.

Related

How do I update Flutter's Riverpod values from business logic?

When using Flutter and Riverpod, how do I update its values from my business logic?
I understand that I can get and set values from the UI side.
class XxxNotifier extends StateNotifier<String> {
XxxNotifier() : super("");
}
final xxxProvider = StateNotifierProvider<XxxNotifier, int>((ref) {
return XxxNotifier();
});
class MyApp extends HookConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
// getValue
final String value = ref.watch(xxxProvider);
// setValue
context.read(xxxProvider).state = "val";
return Container();
}
}
This method requires a context or ref.
How do I get or set these states from the business logic side?
Passing a context or ref from the UI side to the business logic side might do that, but I saw no point in separating the UI and business logic. Perhaps another method exists.
Perhaps I am mistaken about something. You can point it out to me.
You can pass ref in your XxxNotifier class:
class XxxNotifier extends StateNotifier<String> {
XxxNotifier(this._ref) : super("");
final Ref _ref;
void setNewState() {
state = 'to setting';
// use `_ref.read` to read state other provider
}
}
final xxxProvider = StateNotifierProvider<XxxNotifier, int>((ref) {
return XxxNotifier(ref);
});
// or using tear-off
final xxxProvider = StateNotifierProvider<XxxNotifier, int>(XxxNotifier.new);
You can create methods in your XxxNotifier class to modify the state of your provider.
For example, your notifier class can look like this.
class TodosNotifier extends StateNotifier <List<Todo>> {
TodosNotifier(): super([]);
void addTodo(Todo todo) {
state = [...state, todo];
}
}
You can then read the provider in a callback.
ref.read(xxxProvider.notifier).addTodo(todo);

Flutter Getx builder not updating UI

I'm trying to use a GetX Builder in combination with a bool in the controller to display a loading spinner while fetching data and then the data afterwards.
Printing the update to the bool shows it finishes loading and changes the variable but the UI is never updated to reflect this.
Controller
class AuthController extends GetxController {
//final RxBool isLoading = true.obs;
//var isLoading = true.obs;
final Rx<bool> isLoading = Rx<bool>(true);
setLoading(bool status) {
isLoading.value = status;
print(isLoading.value);
update();
}
fetchUserData() async {
setLoading(true);
_firebaseUser.bindStream(_auth.authStateChanges());
if (_auth.currentUser != null) {
bool profileRetrieved =
await FirestoreDB().getUserProfile(_auth.currentUser!.uid).then((_) {
return true;
});
setLoading(!profileRetrieved);
}
}
}
Profile Page
class ProfileCard extends StatelessWidget {
const ProfileCard({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return GetBuilder<AuthController>(
init: AuthController(),
initState: (_) => AuthController().fetchUserData(),
builder: (controller) {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return Container(...);
},
);
}
}
That widget is called within another as part of the wider UI. Let me know if you'd need to see that and/or anything else.
As you can see I've tried different ways of setting up the bool and none of them seem to trigger a change of UI in the builder.
Probably doing something stupid but I've tried a few different approaches and looked around for people having similar problems but not been able to crack it.
Thanks in advance.
You are using isLoading as a Rx<bool>. In that case you need to change GetBuilder into GetX. And no need to call update().
GetBuilder is for non-reactive, non-Rx, simple state management
GetX is for reactive, Rx state management

events management flutter bloc pattern

everyone. I am new in Flutter and BLoC pattern.
I needed to implement contact page so I created event GetContacts and passed it into context.read().add() after that I called this event into initState() of contacts screen.
Here my event:
abstract class ContactEvent extends Equatable {
const ContactEvent([List props = const []]) : super();
}
class GetContacts extends ContactEvent {
const GetContacts() : super();
#override
List<Object> get props => [];
}
Here is my bloc:
class ContactsBloc extends Bloc<ContactEvent, ContactsState> {
final ContactsRepo contactsRepo;
ContactsBloc({required this.contactsRepo}) : super(ContactInitial());
#override
Stream<ContactsState> mapEventToState(ContactEvent event,) async* {
yield ContactsLoading();
//
// if (event is UpdatePhoto) {
// yield PhotoLoading();
//
// print("LOADING STARTED");
//
// final photo = await contactsRepo.updatePhoto(event.identifier, event.photo);
// print("LOADING FINISHED");
//
// yield PhotoLoaded(photo: photo);
// }
if (event is GetContacts) {
print("get contacts photoBloc");
try {
final contacts = await contactsRepo.getContacts();
yield ContactsLoaded(contacts);
} on AccessException {
yield ContactsError();
}
}
}
}
That works right and contacts page renders contacts as it is supposed.
[contacts screen][1]
[1]: https://i.stack.imgur.com/Gx3JA.png
But then I decided to implement new feature: when user clicks on any contact he is offered to change its photo.
If I understand BLoC pattern correctly then if I want to change my state I need to create new event. Then I created new action UpdatePhoto and passed it into the same Bloc as it shown at 2nd part of code (in comments). Exactly there I encounter a misunderstanding of architecture expansion. This action is not supposed to return ContactsLoaded state so when I tried to catch this into my another bloc builder it broke my previous bloc builder that caught GetContact event.
ContactState:
abstract class ContactsState extends Equatable {
const ContactsState([List props = const []]) : super();
}
// class PhotoLoading extends PhotoState {
// #override
// List<Object?> get props => [];
// }
//
// class PhotoLoaded extends PhotoState {
// final Uint8List photo;
// const PhotoLoaded({required this.photo});
// #override
// List<Object?> get props => [photo];
// }
class ContactInitial extends ContactsState {
#override
List<Object> get props => [];
}
class ContactsLoading extends ContactsState {
#override
List<Object> get props => [];
}
class ContactsLoaded extends ContactsState {
final List<MyContact> contacts;
ContactsLoaded(this.contacts) : super([contacts]);
#override
List<Object> get props => [contacts];
}
class ContactsError extends ContactsState {
#override
List<Object?> get props => [];
}
Question: If I want to create new event (for example UpdatePhoto) which is not supposed to return the state that I caught before at the same bloc then I need to create new bloc for that purpose and cover my screen by multiProvider?
You should also post your ContactState code.
However you do not necessarely need a new Bloc. It all depends on what you are trying to achieve.
I suppose than when you yield PhotoLoading() you want to show a loader.
But when you update the photos, if I understand what you are trying to achieve you should yield an updated list of contacts using again yield ContactsLoaded(contacts) or add(GetContacts())instead of yield PhotoLoaded(photo: photo).
If you want to show a confirmation message, you can keep your PhotoLoaded state, but you need to build your UI taking into account the different state the bloc may emit.
Remember in BloC architecture event can yield to multiple states in successions and the UI decide if and how to react to each state.
I guess use optional parameter buildWhen in BlocBuilder is the best way to avoid creating new bloc for each event.

Inner method in specific state is not recognized, BloC pattern

I build a flutter project by using Bloc State management. But There are something that i don't understand. I know how to make State as Abstract class which it's implement for every it's children.
I have class state lake below, please focus to class MainHomeLoaded
abstract class MainHomeState extends Equatable{
MainHomeState();
}
class MainHomeUnInitialized extends MainHomeState{
#override
List<Object> get props => null;
}
class MainHomeLoading extends MainHomeState{
#override
List<Object> get props => null;
}
class MainHomeLoaded extends MainHomeState{
final List<Article> listArticle;
final bool hasReachedMax;
MainHomeLoaded({#required this.listArticle, this.hasReachedMax});
MainHomeLoaded copyWith({
List<Article> article,
bool hasReacedMax,
}){
return MainHomeLoaded(
listArticle: article ?? this.listArticle,
hasReachedMax: hasReacedMax ?? this.hasReachedMax);
}
#override
List<Object> get props => null;
}
class MainHomeError extends MainHomeState{
final String errorMsg;
MainHomeError({#required this.errorMsg});
#override
List<Object> get props => [errorMsg];
}
then i have MainHomeBloc class with implement Bloc method like mapEventtoState() and inside this method i made conditional like below(again please focus to conditional MainHomeLoaded):
#override
Stream<MainHomeState> mapEventToState(MainHomeEvent event) async*{
if(event is CallHomeLatestNews && !_hasReachedMax(state)){
if(state is MainHomeUnInitialized){
ResponseArticle responseArticle = await mainHomeRepository.latestNews(event.page);
if(responseArticle.status == 'success'){
List<Article> data = responseArticle.data;
yield MainHomeLoaded(listArticle: data);
}else{
yield MainHomeError(errorMsg: responseArticle.message);
}
}
if(state is MainHomeLoaded){
ResponseArticle responseArticle = await mainHomeRepository.latestNews(event.page);
if(responseArticle.status == 'success'){
List<Article> data = responseArticle.data;
yield data.isEmpty ? state.copyWith(hasReacedMax: true)
: MainHomeLoaded(listArticle: state.listArticle + data, hasReachedMax: false);
}
}
}
This is part that i don't understand at all, as you can see we have consider that state are in MainHomeLoaded because inside if conditional, but i got error building and my IDE show red line and also method copyWith()doesn't recognize. The error display like this:
what IDE says is
method copyWith() is not define for the class 'MainHomeState'
Can someone help me to give simple explanation for this case? Thanks
FYI i used Flutter in this version 1.12.13 and Dart version 2.7.0
Finally i found what my main problem. Perhaps it's bit different when we using state in BlocBuilder (in Screens) that automatically known specific state. So what i have to do is just casting it to be child that i wanted. So the solution of this case is like this:
if(state is MainHomeLoaded){
MainHomeLoaded mainHomeLoaded = state as MainHomeLoaded; // what i need
ResponseArticle responseArticle = await mainHomeRepository.latestNews(defaultPage);
if(responseArticle.status == 'success'){
List<Article> newData = responseArticle.data;
if(newData.isEmpty){
mainHomeLoaded.copyWith(hasReacedMax: true);
yield mainHomeLoaded;
}
defaultPage++;
}else{
print('gagal');
yield MainHomeError(errorMsg: responseArticle.message);
}
}
i hope it will help someone in future.

Controlling State from outside of a StatefulWidget

I'm trying to understand the best practice for controlling a StatefulWidget's state outside of that Widgets State.
I have the following interface defined.
abstract class StartupView {
Stream<String> get onAppSelected;
set showActivity(bool activity);
set message(String message);
}
I would like to create a StatefulWidget StartupPage that implements this interface. I expect the Widget to do the following:
When a button is pressed it would send an event over the onAppSelected stream. A controller would listen to this event and perform some action ( DB call, service request, etc ).
The controller can call showActivity or set message to have the view show progress with a message.
Because a Stateful Widget does not expose its State as a property, I don't know the best approach for accessing and modifying the State's attributes.
The way I would expect to use this would be something like this:
Widget createStartupPage() {
var page = new StartupPage();
page.onAppSelected.listen((app) {
page.showActivity = true;
//Do some work
page.showActivity = false;
});
}
I've thought about instantiating the Widget by passing in the state I want it to return in createState() but that feels wrong.
Some background on why we have this approach: We currently have a Dart web application. For view-controller separation, testability, and forward-thinking towards Flutter, we decided that we would create an interface for every view in our application. This would allow a WebComponent or a Flutter Widget to implement this interface and leave all of the controller logic the same.
There are multiple ways to interact with other stateful widgets.
1. findAncestorStateOfType
The first and most straightforward is through context.findAncestorStateOfType method.
Usually wrapped in a static method of the Stateful subclass like this :
class MyState extends StatefulWidget {
static of(BuildContext context, {bool root = false}) => root
? context.findRootAncestorStateOfType<_MyStateState>()
: context.findAncestorStateOfType<_MyStateState>();
#override
_MyStateState createState() => _MyStateState();
}
class _MyStateState extends State<MyState> {
#override
Widget build(BuildContext context) {
return Container();
}
}
This is how Navigator works for example.
Pro:
Easiest solution
Con:
Tempted to access State properties or manually call setState
Requires to expose State subclass
Don't use this method when you want to access a variable. As your widget may not reload when that variable change.
2. Listenable, Stream and/or InheritedWidget
Sometimes instead of a method, you may want to access some properties. The thing is, you most likely want your widgets to update whenever that value changes over time.
In this situation, dart offer Stream and Sink. And flutter adds on the top of it InheritedWidget and Listenable such as ValueNotifier. They all do relatively the same thing: subscribing to a value change event when coupled with a StreamBuilder/context.dependOnInheritedWidgetOfExactType/AnimatedBuilder.
This is the go-to solution when you want your State to expose some properties. I won't cover all the possibilities but here's a small example using InheritedWidget :
First, we have an InheritedWidget that expose a count :
class Count extends InheritedWidget {
static of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<Count>();
final int count;
Count({Key key, #required Widget child, #required this.count})
: assert(count != null),
super(key: key, child: child);
#override
bool updateShouldNotify(Count oldWidget) {
return this.count != oldWidget.count;
}
}
Then we have our State that instantiate this InheritedWidget
class _MyStateState extends State<MyState> {
int count = 0;
#override
Widget build(BuildContext context) {
return Count(
count: count,
child: Scaffold(
body: CountBody(),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
count++;
});
},
),
),
);
}
}
Finally, we have our CountBody that fetch this exposed count
class CountBody extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Text(Count.of(context).count.toString()),
);
}
}
Pros:
More performant than findAncestorStateOfType
Stream alternative is dart only (works with web) and is strongly integrated in the language (keywords such as await for or async*)
Automic reload of the children when the value change
Cons:
More boilerplate
Stream can be complicated
3. Notifications
Instead of directly calling methods on State, you can send a Notification from your widget. And make State subscribe to these notifications.
An example of Notification would be :
class MyNotification extends Notification {
final String title;
const MyNotification({this.title});
}
To dispatch the notification simply call dispatch(context) on your notification instance and it will bubble up.
MyNotification(title: "Foo")..dispatch(context)
Note: you need put above line of code inside a class, otherwise no context, can NOT call notification.
Any given widget can listen to notifications dispatched by their children using NotificationListener<T> :
class _MyStateState extends State<MyState> {
#override
Widget build(BuildContext context) {
return NotificationListener<MyNotification>(
onNotification: onTitlePush,
child: Container(),
);
}
bool onTitlePush(MyNotification notification) {
print("New item ${notification.title}");
// true meaning processed, no following notification bubbling.
return true;
}
}
An example would be Scrollable, which can dispatch ScrollNotification including start/end/overscroll. Then used by Scrollbar to know scroll information without having access to ScrollController
Pros:
Cool reactive API. We don't directly do stuff on State. It's State that subscribes to events triggered by its children
More than one widget can subscribe to that same notification
Prevents children from accessing unwanted State properties
Cons:
May not fit your use-case
Requires more boilerplate
You can expose the state's widget with a static method, a few of the flutter examples do it this way and I've started using it as well:
class StartupPage extends StatefulWidget {
static StartupPageState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher<StartupPageState>());
#override
StartupPageState createState() => new StartupPageState();
}
class StartupPageState extends State<StartupPage> {
...
}
You can then access the state by calling StartupPage.of(context).doSomething();.
The caveat here is that you need to have a BuildContext with that page somewhere in its tree.
There is another common used approach to have access to State's properties/methods:
class StartupPage extends StatefulWidget {
StartupPage({Key key}) : super(key: key);
#override
StartupPageState createState() => StartupPageState();
}
// Make class public!
class StartupPageState extends State<StartupPage> {
int someStateProperty;
void someStateMethod() {}
}
// Somewhere where inside class where `StartupPage` will be used
final startupPageKey = GlobalKey<StartupPageState>();
// Somewhere where the `StartupPage` will be opened
final startupPage = StartupPage(key: startupPageKey);
Navigator.push(context, MaterialPageRoute(builder: (_) => startupPage);
// Somewhere where you need have access to state
startupPageKey.currentState.someStateProperty = 1;
startupPageKey.currentState.someStateMethod();
I do:
class StartupPage extends StatefulWidget {
StartupPageState state;
#override
StartupPageState createState() {
this.state = new StartupPageState();
return this.state;
}
}
class DetectedAnimationState extends State<DetectedAnimation> {
And outside just startupPage.state
While trying to solve a similar problem, I discovered that ancestorStateOfType() and TypeMatcher have been deprecated. Instead, one has to use findAncestorStateOfType(). However as per the documentation, "calling this method is relatively expensive". The documentation for the findAncestorStateOfType() method can be found here.
In any case, to use findAncestorStateOfType(), the following can be implemented (this is a modification of the correct answer using the findAncestorStateOfType() method):
class StartupPage extends StatefulWidget {
static _StartupPageState of(BuildContext context) => context.findAncestorStateOfType<_StartupPageState>();
#override
_StartupPageState createState() => new _StartupPageState();
}
class _StartupPageState extends State<StartupPage> {
...
}
The state can be accessed in the same way as described in the correct answer (using StartupPage.of(context).yourFunction()). I wanted to update the post with the new method.
You can use eventify
This library provide mechanism to register for event notifications with emitter
or publisher and get notified in the event of an event.
You can do something like:
// Import the library
import 'package:eventify/eventify.dart';
final EventEmitter emitter = new EventEmitter();
var controlNumber = 50;
List<Widget> buttonsGenerator() {
final List<Widget> buttons = new List<Widget>();
for (var i = 0; i < controlNumber; i++) {
widgets.add(new MaterialButton(
// Generate 10 Buttons afterwards
onPressed: () {
controlNumber = 10;
emitter.emit("updateButtonsList", null, "");
},
);
}
}
class AState extends State<ofYourWidget> {
#override
Widget build(BuildContext context) {
List<Widget> buttons_list = buttonsGenerator();
emitter.on('updateButtonsList', null, (event, event_context) {
setState(() {
buttons_list = buttonsGenerator();
});
});
}
...
}
I can't think of anything which can't be achieved by event driven programming. You are limitless!
"Freedom cannot be bestowed — it must be achieved."
- Elbert Hubbard
Have you considered lifting the state to the parent widget? It is a common, though less ideal than Redux, way to manage state in React as far as I know, and this repository shows how to apply the concept to a Flutter app.