How to prevent not to rebuild UI inside Index Stack - flutter

I'm using index stack in home screen to show different screen at one time.
My Problem is that child widget is rebuilt again and make API call when ever I re-enter to any screen
home screen code:
final _currentPage =
context.select<MenuProvider, int>((provider) => provider.currentPage);
void _onItemTapped(int index) {
Provider.of<MenuProvider>(context, listen: false)
.updateCurrentPage(index);
}
List<MenuItem> mainMenu = [
MenuItem(
AppLocalizations.of(context)!.sd_title,
'Home',
Icons.home,
0,
),
MenuItem(
AppLocalizations.of(context)!.profile_title,
'Profile',
Icons.person,
1,
),
MenuItem(
AppLocalizations.of(context)!.sd_calculator,
'Calculator',
Icons.calculate_rounded,
2,
),
];
var screens = [
mainMenu[_currentPage].index == 0 ? const HomeFragment() : Container(),
mainMenu[_currentPage].index == 1 ? const Profile() : Container(),
mainMenu[_currentPage].index == 2
? LoanCalculatorScreen(isHome: true)
: Container(),
];
var container = Container(
alignment: Alignment.center,
color: Colors.white,
child: FadeIndexedStack(
index: mainMenu[_currentPage].index,
children: screens,
),
);
Container is used in body of Scaffold.
Bottom Navigation:
bottomNavigationBar: BottomNavigationBar(
items: mainMenu
.map(
(item) => BottomNavigationBarItem(
label: item.bottomTitle,
icon: Icon(
item.icon,
),
),
)
.toList(),
currentIndex: _currentPage,
selectedItemColor: Colors.amber[800],
onTap: _onItemTapped,
)
Home screen: As statefull to make a call on init state;
#override
void initState() {
var homeData = Provider.of<HomeProvider>(context, listen: false);
homeData.getOffersAndPartnersSlider();
super.initState();
}
I'm using Provider for state management and API call.
any suggestion will help me to write better code and make good performance.
I've Try Page View also same thing happen the inside child is rebuilt.
I just want to make The API call once.

To fix this I've made changes with Pages.
Declare list of pages above built method with PageStorageKey.
final List<Widget> pages = [
const HomeFragment(
key: PageStorageKey('Page1'),
),
const Profile(
key: PageStorageKey('Page2'),
),
Lo
LoanCalculatorScreen(key: const PageStorageKey('Page'), isHome: true),
];
And Call pages inside Indexed Stack Widgets Children
Container(
alignment: Alignment.center,
color: Colors.white,
child: FadeIndexedStack(
index: mainMenu[_currentPage].index,
children: pages,
),
)

Related

How i can change the icon in the Icon button

I am trying to build a whatsapp clone and when I was working on the changing the camera from front and back. I was trying to change the Icon in the Icon button but it was not changing
I will attach my code file below
Widget bottomIcon({Icon icon,double size,Function onpress}){
return IconButton(
icon: icon,
iconSize: size,
color: Colors.white,
onPressed: onpress,
);
}
Icon iconForcam=Icon(Icons.camera_rear);
#override
Widget build(BuildContext context) {
if (!controller.value.isInitialized) {
return Container();
}
return MaterialApp(
home: Padding(
padding: const EdgeInsets.all(1.0),
child: Stack(
fit: StackFit.expand,
children: [
CameraPreview(controller),
Positioned(
bottom: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
SizedBox(width: 20.0,),
bottomIcon(icon: Icon(Icons.flash_on_rounded),size: 50.0),
SizedBox(width: 20.0,),
bottomIcon(icon: Icon(Icons.fiber_manual_record_outlined),size: 100.0),
SizedBox(width: 30.0,),
bottomIcon(icon: iconForcam,size: 50.0,onpress: (){
setState(() {
if(iconForcam == Icon(Icons.camera_front)){
iconForcam = Icon(Icons.camera_rear);
}else if(iconForcam == Icon(Icons.camera_rear)){
print('rearcam');
iconForcam = Icon(Icons.camera_front);
}
});
//toggleCamera();
}),
],
),
),
],
),
),
);
}
}
I have the doubt that in the if I can comapre two icons in the if Statement.
You can define a boolean variable
//Define
bool _isFront = true;
//Usage
bottomIcon(
icon: _isFront ?
Icons.camera_front : Icons.camera_rear,
size: 50.0, onpress: (){
setState(() {
_isFront = !_isFront;
});
//toggleCamera();
})
I tried like this and got that correct
//Defint
int _oldIndex=0;
Icon iconForcam=Icon(Icons.camera_rear);
//Inside code
bottomIcon(icon: iconForcam,size: 50.0,onpress: (){
setState(() {
if(_oldIndex == 0){
iconForcam = Icon(Icons.camera_rear);
_oldIndex = 1;
}else if(_oldIndex == 1){
//print('rearcam');
iconForcam = Icon(Icons.camera_front);
_oldIndex = 0;
}
});
toggleCamera(_oldIndex);
}),
You can store whether the front camera is on or not in shared_prefernces or database, use provider/stream/bloc to expose this value to UI. Now you can use this package to change icon with animation. Install this package to your flutter project, import it in the file, and then replace icon property of the camera button with the below code:
AdvancedIcon(
icon: Icons.camera_front,
secondaryIcon: Icons.camera_rear,
state: isFrontCameraOn ? AdvancedIconState.primary : AdvancedIconState.secondary,
)
Now the icon will automatically change depending on whether the front camera is on or not.
If you have problem with the database or provider part of this question just let me know.

Flutter:How to get a value from flutter/provider change notifier

I have program where I need to add a count next to a bottom navigation icon. So, I am trying to use the ChangeNotifierProvider and wrap it around my root widget. How do I get the count from the IterableModel to my widget?
MAIN.APP
ChangeNotifierProvider(create: (_) => IterableModel()),
HOME.DART
BottomNavigationBarItem(
icon: Container(
margin: EdgeInsets.only(top: 20.0),
child: ImageWithBadge(
text: 'notification',
iconData: Icons.mail,
notificationCount: "NEED MESSAGE COUNT,
),
),
),
IterableModel.dart
class IterableModel extends ChangeNotifier {
List<InAppMessages> inAppMessages;
int cnt;
IterableModel({this.inAppMessages});
void messageCnt(List inAppMessages) {
cnt = inAppMessages.length;
notifyListeners();
}
IterableModel.fromJson(Map<String, dynamic> json) {
if (json['inAppMessages'] != null) {
inAppMessages = [];
json['inAppMessages'].forEach((v) {
inAppMessages.add(new InAppMessages.fromJson(v));
});
messageCnt(inAppMessages);
}
}
}
I think i need a consumer and builder but I don't now where to place it
final count = Provider.of<IterableModel>(context).cnt;
Insert this line at the top of your build method than it would be accessible but also rebuild the entire widget one better aproach is to use Consumer
Consumer<IterableModel>(
builder: (context,model,_) => BottomNavigationBarItem(
icon: Container(
margin: EdgeInsets.only(top: 20.0),
child: ImageWithBadge(
text: 'notification',
iconData: Icons.mail,
notificationCount: model.cnt.toString(),
),
),
),
)

Flutter - getx controller not updated when data changed

I am developing an app that has a bottomnavitaionbar with five pages. I use getx. In first page, i am listing data. My problem is that, when i changed data(first page in bottomnavigationbar) manually from database and thn i pass over pages, came back to first page i could not see changes.
Controller;
class ExploreController extends GetxController {
var isLoading = true.obs;
var articleList = List<ExploreModel>().obs;
#override
void onInit() {
fetchArticles();
super.onInit();
}
void fetchArticles() async {
try {
isLoading(true);
var articles = await ApiService.fetchArticles();
if (articles != null) {
//articleList.clear();
articleList.assignAll(articles);
}
} finally {
isLoading(false);
}
update();
}
}
and my UI;
body: SafeArea(
child: Column(
children: <Widget>[
Header(),
Expanded(
child: GetX<ExploreController>(builder: (exploreController) {
if (exploreController.isLoading.value) {
return Center(
child: SpinKitChasingDots(
color: Colors.deepPurple[600], size: 40),
);
}
return ListView.separated(
padding: EdgeInsets.all(12),
itemCount: exploreController.articleList.length,
separatorBuilder: (BuildContext context, int index) {
thanks to #Baker for the right answer. However, if you have a list and in viewModel and want to update that list, just use the list.refresh() when the list updated
RxList<Models> myList = <Models>[].obs;
when add or insert data act like this:
myList.add(newItem);
myList.refresh();
GetX doesn't know / can't see when database data has changed / been updated.
You need to tell GetX to rebuild when appropriate.
If you use GetX observables with GetX or Obx widgets, then you just assign a new value to your observable field. Rebuilds will happen when the obs value changes.
If you use GetX with GetBuilder<MyController>, then you need to call update() method inside MyController, to rebuild GetBuilder<MyController> widgets.
The solution below uses a GetX Controller (i.e. TabX) to:
hold application state:
list of all tabs (tabPages)
which Tab is active (selectedIndex)
expose a method to change the active/visible tab (onItemTapped())
OnItemTapped()
This method is inside TabX, the GetXController.
When called, it will:
set which tab is visible
save the viewed tab to the database (FakeDB)
rebuild any GetBuilder widgets using update()
void onItemTapped(int index) {
selectedIndex = index;
db.insertViewedPage(index); // simulate database update while tabs change
update(); // ← rebuilds any GetBuilder<TabX> widget
}
Complete Example
Copy/paste this entire code into a dart page in your app to see a working BottomNavigationBar page.
This tabbed / BottomNavigationBar example is taken from
https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html
but edited to use GetX.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyTabHomePage(),
);
}
}
class FakeDB {
List<int> viewedPages = [0];
void insertViewedPage(int page) {
viewedPages.add(page);
}
}
/// BottomNavigationBar page converted to GetX. Original StatefulWidget version:
/// https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html
class TabX extends GetxController {
TabX({this.db});
final FakeDB db;
int selectedIndex = 0;
static const TextStyle optionStyle =
TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
List<Widget> tabPages;
#override
void onInit() {
super.onInit();
tabPages = <Widget>[
ListViewTab(db),
Text(
'Index 1: Business',
style: optionStyle,
),
Text(
'Index 2: School',
style: optionStyle,
),
];
}
/// INTERESTING PART HERE ↓ ************************************
void onItemTapped(int index) {
selectedIndex = index;
db.insertViewedPage(index); // simulate database update while tabs change
update(); // ← rebuilds any GetBuilder<TabX> widget
// ↑ update() is like setState() to anything inside a GetBuilder using *this*
// controller, i.e. GetBuilder<TabX>
// Other GetX controllers are not affected. e.g. GetBuilder<BlahX>, not affected
// by this update()
// Use async/await above if data writes are slow & must complete before updating widget.
// This example does not.
}
}
/// REBUILT when Tab Page changes, rebuilt by GetBuilder in MyTabHomePage
class ListViewTab extends StatelessWidget {
final FakeDB db;
ListViewTab(this.db);
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: db.viewedPages.length,
itemBuilder: (context, index) =>
ListTile(
title: Text('Page Viewed: ${db.viewedPages[index]}'),
),
);
}
}
class MyTabHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
Get.put(TabX(db: FakeDB()));
return Scaffold(
appBar: AppBar(
title: const Text('BottomNavigationBar Sample'),
),
body: Center(
/// ↓ Tab Page currently visible - rebuilt by GetBuilder when
/// ↓ TabX.onItemTapped() called
child: GetBuilder<TabX>(
builder: (tx) => tx.tabPages.elementAt(tx.selectedIndex)
),
),
/// ↓ BottomNavBar's highlighted/active item, rebuilt by GetBuilder when
/// ↓ TabX.onItemTapped() called
bottomNavigationBar: GetBuilder<TabX>(
builder: (tx) => BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
label: 'Business',
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
label: 'School',
),
],
currentIndex: tx.selectedIndex,
selectedItemColor: Colors.amber[800],
onTap: tx.onItemTapped,
),
),
);
}
}
You don't need GetBuilder here, as its not meant for observable variables. Nor do you need to call update() in the fetchArticles function as that's only for use with GetBuilder and non observable variables.
So you had 2 widgets meant to update UI (GetBuilder and Obx) both following the same controller and all you need is just the OBX. So Rahuls answer works, or you can leave the Obx in place, get rid of of the GetBuilder and declare and initialize a controller in the beginning of your build method.
final exploreController = Get.put(ExploreController());
Then use that initialized controller in your OBX widget as the child of your Expanded.
Obx(() => exploreController.isLoading.value
? Center(
child:
SpinKitChasingDots(color: Colors.deepPurple[600], size: 40),
)
: ListView.separated(
padding: EdgeInsets.all(12),
itemCount: exploreController.articleList.length,
separatorBuilder: (BuildContext context, int index) {},
),
)
GetX< ExploreController >(builder: (controller) {
if (controller.isLoading.value) {
return Center(
child: SpinKitChasingDots(
color: Colors.deepPurple[600], size: 40),);
}
return ListView.separated(
padding: EdgeInsets.all(12),
itemCount: controller.articleList.length,
separatorBuilder: (BuildContext context, int index) {});
});
If you change the value in the database 'manually', you need a STREAM to listen to the change on the database.
You can't do:
var articles = await ApiService.fetchArticles();
You need to do something like this:
var articles = await ApiService.listenToArticlesSnapshot();
The way you explained is like if you need the data to refresh after navigating to another page and clicking on a button, then navigating to first page (GetBuilder) OR automatically adds data from the within the first page (Obx). But your case is simple, just retrieve the articles SNAPSHOT, then in the controller onInit, subscribe to the snapshot with the bindStream method, and eventually use the function ever() to react to any change in the observable articleList.
Something like this:
create
final exploreController = Get.put(ExploreController());
Add
init: ExploreController();
body: SafeArea(
child: Column(
children: <Widget>[
Header(),
Expanded(
child: GetX<ExploreController>(builder: (exploreController) {
*** here ***
init: ExploreController();
if (exploreController.isLoading.value) {
return Center(
child: SpinKitChasingDots(
color: Colors.deepPurple[600], size: 40),
);
}
return ListView.separated(
padding: EdgeInsets.all(12),
itemCount: exploreController.articleList.length,
separatorBuilder: (BuildContext context, int index) {
using GetxBuilder approch on ui side and where you want update simple called built in function update();
The simplest way I could.
In the controller create an obs (var indexClick = 1.obs;)
On each Tile test the selected==index...;
On the click of each item change the indexClick sequentially
return Obx(() {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
ListTile(
leading: const Icon(Icons.dns),
title: const Text('Menu1'),
selected: controller.indexClick.value==1?true:false,
onTap: () {
controller.indexClick.value=1;
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.search),
title: const Text('Menu2'),
selected: controller.indexClick.value==2?true:false,
onTap: () {
controller.indexClick.value=2;
Navigator.pop(context);
},
),

bottom navigation bar not switching tabs in flutter

I want to use fancy bottom navigation in flutter. When i switch between tabs, it is showing the tab switching only at the navigation bar but, The body is not switching. It is not showing another tabs as i switch.
Here's my code
return Scaffold(
body: _getPage(currentPage),
bottomNavigationBar: FancyBottomNavigation(
key: bottomNavigationKey,
tabs: [
TabData(iconData: Icons.home,title: 'Home'),
TabData(iconData: Icons.search, title: 'Search'),
TabData(iconData: Icons.person, title: 'Profile'),
],
onTabChangedListener: (position){
setState(() {
currentPage = position;
final FancyBottomNavigationState fState =
bottomNavigationKey.currentState;
fState.setPage(position);
print('currentPage = $currentPage');
});
},
)
);
_getPage(int page){
switch(page) {
case 0:
return Page1();
case 1:
return Search();
case 2:
return Profile();
}
}
You don't need the GlobalKey here. It is enough to just set the currentPage value in the setState.
The Dev Nathan Withers writes here that the key prop defaults to null and is only used for Programmatic Selection of tabs. This special use case is only relevant if you want to change tabs by not clicking on the actual buttons. You don't need this feature. You can check out the example at line 48 to 50 for an actual use-case for the key prop.
Also examine if an IndexedStack suits you. It saves the pages state. You're _getPage() method destroys and builds each page new on each switch.
I put everything together in this example:
class _HomePageState extends State<HomePage> {
int _currentPage = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
top: false,
child: IndexedStack(
index: _currentPage,
children: [StartView(), AllServicesView()],
),
),
bottomNavigationBar: FancyBottomNavigation(
tabs: [
TabData(iconData: Icons.home,title: 'Home'),
TabData(iconData: Icons.search, title: 'Search'),
],
onTabChangedListener: (position){
setState(() {
_currentPage = position;
print('currentPage = $_currentPage');
});
},
)
);
}

Widgets get no update from the listener flutter

I have implemented an listener in the following way:
#override
void didChangeDependencies() {
final SlotDataProvider slotDevice = Provider.of<SlotDataProvider>(context);
spptask.streamdevice.listen((device) {
setState(() {
slotDevice._devices[0].name = device.name;
print("Device data received: ${device.name} ");
});
}, onError: (error) {
print("Error: $error.message");
});
super.didChangeDependencies();
}
I listen on a splitted controller and the print "Device data received:..." is called but the widget is not actualized. In the build method I do the following:
...
#override
Widget build(BuildContext context) {
final slotProvider = Provider.of<SlotDataProvider>(context);
final deviceProvider = Provider.of<DeviceDataProvider>(context);
Device slotDevice = slotProvider.getDevice(widget.slot);
Device device = deviceProvider.getDevice(widget.slot);
_dropdownMenuItems = buildDropdownMenuItems(deviceProvider.get());
return ListTile(
title: Row(
children: <Widget>[
SizedBox(
width: 140,
child: DropdownButton(
isExpanded: true,
disabledHint: Text(slotDevice.name),
hint: Text(slotDevice.name),
value: device,
items: _dropdownMenuItems,
onChanged: (value) {
device.setDevice(value);
slotDevice.setDevice(value);
}),
),
SizedBox(width: 10),
SizedBox(width: 60, child: Text('SLOT#${slotDevice.slot}')),
],
),
subtitle: Text(slotDevice.bdaddr, style: TextStyle(fontSize: 10.0)),
leading: SizedBox(
height: 40,
width: 35,
child: UsbBatteryImageAsset(slot: widget.slot),
),
trailing: Icon(Icons.keyboard_arrow_right),
);
}
}
What is missing in the above code. The SlotDataProvider is a fix list of "Device" with attributes such as name, id and so on.
#EDIT
The problem has to do with the combobox. If I change an other field, it works.
Usually for widgets to be rebuilt based on the data updated we use streambuilders
this will cause the widget to rebuild every time there is a change in the stream
it seams that your widget is being built once with the first listening of the data
have you tried wrapping the gridview in a stateful builder ?