How to update a list in Flutter with Bloc/Cubit? - flutter

my UI consists of a table of objects Apples.
Every cell in the table:
has an ADD button, if the apple is NOT present for that cell
it shows the apple and has a DELETE button, if the apple is present
The main page of my application is calling the following widget to LOAD the LIST of apples from an API. And also the ADD and DELETE functions communicate with the same API for adding and deleting the apple.
class ApplesLoader extends StatefulWidget {
#override
_ApplesLoaderState createState() => _ApplesLoaderState();
}
class _ApplesLoaderState extends State<ApplesLoader> {
#override
void initState() {
super.initState();
BlocProvider.of<ApplesCubit>(context).getAll();
}
#override
Widget build(BuildContext context) {
return BlocConsumer<ApplesCubit, ApplesState>(
listener: ...,
builder: (ctx, state) {
if (!(state is ApplesLoaded)) {
return circularProgressIndicator;
}
return ApplesViewer(state.Apples);
},
);
}
}
So after that ApplesViewerhas the list of apples and can correctly display the grid.
But now I have two problems:
When I press the ADD button or the DELETE button, all the application is rebuilt, while I could actually just re-paint the cell. But I don't know how to avoid this, because I also need to communicate to the application that the list of apples is actually changed. I don't want to rebuild the entire UI because it seems inefficient since what is actually changing on my page is only the selected cell.
When I press the ADD button or the DELETE button, I would like to show a circular progress indicator that replaces the button (while waiting for the API to actually creating/deleting the apple). But when the list changes, all the application is rebuilt instead. So, I am not sure how to achieve this desired behavior.
Could you help me in solving those two issues?
Or advise me for a change of infrastructure in case I am making some mistake in dealing with this?
If needed, this is the code for ApplesCubit
class ApplesCubit extends Cubit<ApplesState> {
final ApplesRepository _repository;
ApplesCubit(this._repository) : super(ApplesLoading());
Future<void> getAll() async {
try {
final apples = await _repository.getAll();
emit(ApplesLoaded(List<Apple>.from(apples)));
} on DataError catch (e) {
emit(ApplesError(e));
}
}
Future<void> create(List<Apple> apples, Apple appleToAdd) async {
try {
final newApple = await _repository.create(appleToAdd);
final updatedApples = List<Apple>.from(apples)..add(newApple);
emit(ApplesLoaded(updatedApples));
} on DataError catch (e) {
emit(ApplesError(e));
}
}
Future<void> delete(List<Apple> Appless, Apple appleToDelete) async {
try {
await _repository.deleteApples(appleToDelete);
final updatedApples = List<Apple>.from(Appless)..remove(appleToDelete);
emit(ApplesLoaded(updatedApples));
} on DataError catch (e) {
emit(ApplesError(e));
}
}
}

Related

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

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.

decide the route based on the initialization phase of flutter_native_splash

I'm writing a flutter application using flutter 2.10 and I'm debugging it using an Android Emulator
I included the flutter_native_splash plugin from https://pub.dev/packages/flutter_native_splash and I use version 2.0.1+1
the problem that I'm having is that I decide what's the first screen that the user will see based on the initialization phase. I check the stored user token, see his premissions, verify them with the server, and forward him to him relevant route.
since the runApp() function executes in the background while the initialization phase is running I cannot choose the page that will be shown. and if I try to nativgate to a route in the initialization function I get an exception.
as a workaround for now I created an init_home route with FutureBuilder that awaits for a global variable called GeneralService.defaultRoute to be set and then changes the route.
class _InitHomeState extends State<InitHome> {
#override
Widget build(BuildContext context) {
return FutureBuilder<dynamic>(
future: () async {
var waitCount=0;
while (GeneralService.defaultRoute == "") {
waitCount++;
await Future.delayed(const Duration(milliseconds: 100));
if (waitCount>20) {
break;
}
}
if (GeneralService.defaultRoute == "") {
return Future.error("initialization failed");
}
Navigator.of(context).pushReplacementNamed(GeneralService.defaultRoute);
...
any ideas how to resolve this issue properly ?
I use a Stateful widget as Splash Screen.
In the build method, you just return the 'loading' view such as Container with a background color etc. (with texts or whatever you like but just consider it as the loading screen).
In the initState(), you call a function that we can name redirect(). This should be an async function that performs the queries/checks and at the end, calls the Navigator.of(context).pushReplacementNamed etc.
class _SplashState extends State<Splash> {
#override
void initState() {
super.initState();
redirect();
}
#override
Widget build(BuildContext context) {
return Container(color: Colors.blue);
}
Future<void> redirect() async {
var name = 'LOGIN';
... // make db calls, checks etc
Navigator.of(context).pushReplacementNamed(name);
}
}
Your build function just creates the loading UI, and the redirect function in the initState is the one running in the background and when it has finished computing, calls the Navigator.push to your desired page.

how can I get the other controller's variable inside one controller in flutter using getx

This is an issue related to the getx in flutter.
I have 2 controllers. ContractsController and NotificationController.
In ContractsController I have put the value into observer variable by calling the Api request.
What I want now is to get that variable's data in another controller - NotificationController.
How to get that value using getx functions?
ContractsController
class ContractsController extends GetxController {
ExpiringContractRepository _expiringContractRepository;
final expiringContracts = <ExpiringContract>[].obs; // This is the value what I want in another controller
ContractsController() {
_expiringContractRepository = new ExpiringContractRepository();
}
#override
Future<void> onInit() async {
await refreshContracts();
super.onInit();
}
Future refreshContracts({bool showMessage}) async {
await getExpiringContracts();
if (showMessage == true) {
Get.showSnackbar(Ui.SuccessSnackBar(message: "List of expiring contracts refreshed successfully".tr));
}
}
Future getExpiringContracts() async {
try {
expiringContracts.value = await _expiringContractRepository.getAll(); // put the value from the api
} catch (e) {
Get.showSnackbar(Ui.ErrorSnackBar(message: e.toString()));
}
}
}
The expiringContracts is updated successfully with data after the api request.
Now, I want to get that value in NotificationController
NotificationController
class NotificationsController extends GetxController {
final notifications = <Notification>[].obs;
ContractsController contractsController;
NotificationsController() {
}
#override
void onInit() async {
contractsController = Get.find<ContractsController>();
print(contractsController.expiringContracts); // This shows an empty list ?????
super.onInit();
}
}
Overview
A couple solutions come to mind:
pass the expiringContracts list as a constructor argument to NotificationsController if you only need this done once at instantiation, or
use a GetX worker to update NotificationsController every time expiringContracts is updated
The first solution isn't related to GetX, rather it's just async coordination between ContractsController and NotificationsController, so lets focus on the 2nd solution: GetX Workers.
Details
In NotificationsController, create a method that will receive expiringContracts.
Something like:
class NotificationsController extends GetxController {
void refreshContracts(List<ExpiringContract> contracts) {
// do something
}
}
Please note: none of this code is tested. I'm writing this purely in StackOverflow, so consider this pseudo-code.
In ContractsController we'll supply the above callback method as a constructor arg:
In ContractsController, something like:
class ContractsController {
final expiringContracts = <ExpiringContract>[].obs
final Function(List<ExpiringContract>) refreshContractsCallback;
ContractsController(this.refreshContractsCallback);
#override
void onInit() {
super.onInit();
refreshContracts(); // do your stuff after super.onInit
ever(expiringContracts, refreshContractsCallback);
// ↑ contracts → refreshContractsCallback(contracts)
// when expiringContracts updates, run callback with them
}
}
Here the GetX ever worker takes the observable as first argument, and a function as 2nd argument. That function must take an argument of type that matches the observed variable, i.e. List<ExpiringContract>, hence the Type of refreshContractsCallback was defined as Function(List<ExpiringContract>).
Now whenever the observable expiringContracts is updated in ContractsController, refreshContractsCallback(contracts) will be called, which supplies the list of expiring contracts to NotificationsController via refreshContracts.
Finally, when instantiating the two controllers inside the build() method of your route/page:
NotificationsController nx = Get.put(NotificationsController());
ContractsController cx = Get.put(ContractsController(nx.refreshContracts));
Timeline of Events
NotificationsController gets created as nx.
nx.onInit() runs, slow call of refreshContracts() starts
ContractsController gets created, with nx.refreshContracts callback
your page paints
nx has no contracts data at this point, so you'll prob. need a FutureBuilder or an Obx/ GetX + StatelessWidget that'll rebuild when data eventually arrives
when refreshContracts() finishes, ever worker runs, sending contracts to nx
nx.refreshContracts(contracts) is run, doing something with contracts
Notes
async/await was removed from nx.onInit
ever worker will run when refreshContract finishes
There were some powerful approaches in GetX. I solved this issue with Get.put and Get.find
Here is the code that I added.
ContractsController
class ContractsController extends GetxController {
ExpiringContractRepository _expiringContractRepository;
final expiringContracts = <ExpiringContract>[].obs; // This is the value what I want in another controller
ContractsController() {
_expiringContractRepository = new ExpiringContractRepository();
}
#override
Future<void> onInit() async {
await refreshContracts();
super.onInit();
}
Future refreshContracts({bool showMessage}) async {
await getExpiringContracts();
if (showMessage == true) {
Get.showSnackbar(Ui.SuccessSnackBar(message: "List of expiring contracts refreshed successfully".tr));
}
}
Future getExpiringContracts() async {
try {
expiringContracts.value = await _expiringContractRepository.getAll(); // put the value from the API
// ******************************** //
Get.put(ContractsController()); // Added here
} catch (e) {
Get.showSnackbar(Ui.ErrorSnackBar(message: e.toString()));
}
}
}
NotificationController
class NotificationsController extends GetxController {
final notifications = <Notification>[].obs;
ContractsController contractsController;
NotificationsController() {
}
#override
void onInit() async {
// ******************************** //
contractsController = Get.find<ContractsController>(); // Added here.
print(contractsController.expiringContracts); // This shows the updated value
super.onInit();
}
}
Finally, I have found that GetX is simple but powerful for state management in flutter.
Thanks.

Riverpod provider is always null

I am using riverpod for my state manegement in my flutter app.
Riverpod offers a feature for combined providers, but my dependent provider does not update and always returns null.
By clicking one of the pins (secrets) on the map, my "selectedSecretProvider" is updated (default is null). This should trigger the initialization of my audio player. And by clicking play, the sound of the current _selectedSecret should play. So my "selectedTrackProvder" is dependent on my "selectedSecretProvider":
final selectedTrackProvider = StateNotifierProvider<SelectedTrack, Track>((ref) {
Secret? selectedSecret = ref.watch(selectedSecretProvider);
return SelectedTrack(selectedSecret);
});
Here is my selectedTrack class:
class SelectedTrack extends StateNotifier<Track> {
SelectedTrack(this.selectedSecret) : super(Track.initial());
Secret? selectedSecret;
#override
void dispose() {
...
}
void initAudioPlayer() {
...
}
Future<int> play() async {
print(selectedSecret);
return ...
}
}
So why does it always print null, when clicking play?
(Btw. my bottom_panel_sheet is showing the correct data and also consumes the "selectedSecretProvider".)
I wouldn't say the way you're creating your StateNotifierProvider is wrong, but I think the following is a better approach that should solve your problem.
final selectedTrackProvider = StateNotifierProvider<SelectedTrack, Track>((ref) {
return SelectedTrack(ref);
});
class SelectedTrack extends StateNotifier<Track> {
SelectedTrack(this.ref) : super(Track.initial());
final ProviderReference ref;
Future<int> play() async {
final selectedSecret = ref.read(selectedSecretProvider);
print(selectedSecret);
return ...
}
}
This way you don't create a new StateNotifier every time the selectedSecretProvider updates, instead opting to read the current value of the selectedSecretProvider when attempting to call play.

Best way to handle async backend call Flutter

I have a class called FeedbackEdit that requires data in a variable called feedback to run correctly. I must get feedback from a backend API call. Currently, the code I have runs, but it shows an error for one second while it is retrieving data from the back-end. What would be the best way to fix this so it runs continuously?
class FeedbackEdit extends StatefulWidget {
#override
_FeedbackEditState createState() => _FeedbackEditState();
}
class _FeedbackEditState extends State<FeedbackEdit> {
MyFeedback feedback;
void initState() {
super.initState();
asyncGetFeedback();
}
void asyncGetFeedback() async {
MyFeedback data = await fetchFeedback(http.Client());
setState(() {
feedback = data;
});
}
Widget build(BuildContext context) { ...
it's because you are rendering your view while still fetching data from the backend. To solve the issue, you should use FutureBuilder (see) in your build method. That will make your view to wait the response being fetched from backend.
A sample code I wrote in one of my projects:
FutureBuilder<List<SingleQuestion>>(
future: retrieveFavedQuestions(questionIds),
builder: (context, favQuestionssnapshot) {
if (favQuestionssnapshot.connectionState ==
ConnectionState.done) {
if (favQuestionssnapshot.hasError) {
// check error
}
if (favQuestionssnapshot.hasData) {
// continue working with your data
}
}
);