Loading data with Provider: Flutter - flutter

I am working on a Flutter App in which I want to load data using a REST API.
The provider class is as follows-
CategoryProvider-
class CategoryProvider with ChangeNotifier {
SharedPreferences? sharedPreferences;
late LoadingStatus loadingStatus;
late CategoryService _categoryService;
List<Category>? allCategories;
CategoryProvider.initialze(SharedPreferences sf) {
_initializePrefs(sf);
loadingStatus = LoadingStatus.NOT_STARTED;
_categoryService = CategoryService();
}
void _initializePrefs(SharedPreferences sf) {
log('Initializing sharedPreferences');
sharedPreferences = sf;
log('Shared preference initialized');
}
void fetchAllCategories() async {
allCategories = [];
loadingStatus = LoadingStatus.LOADING;
Map<String, dynamic> result = await _categoryService.fetchAllCategories(
token: sharedPreferences!.getString(BEARER_TOKEN) ?? 'null');
if (result['code'] == '2000') {
allCategories = categoryFromJson(result['data']);
} else {
log('No categories: code: $result');
allCategories = [];
}
loadingStatus = LoadingStatus.COMPLETED;
notifyListeners();
}
}
UI Code-
class HomePage extends StatefulWidget {
const HomePage({super.key});
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final List<Widget> _pages = [
PersonalFeedPage(),
ExplorePage(),
ProfilePage(),
SettingsPage(),
];
late PageController _pageController;
int _selectedIndex = 0;
#override
void initState() {
super.initState();
_pageController = PageController();
}
#override
void dispose() {
_pageController.dispose();
super.dispose();
}
_onTapped(int index) {
setState(() {
_selectedIndex = index;
});
_pageController.jumpToPage(index);
}
void onPageChanged(int index) {
setState(() {
_selectedIndex = index;
});
}
#override
Widget build(BuildContext context) {
final categoryProvider = Provider.of<CategoryProvider>(context);
if (categoryProvider.loadingStatus == LoadingStatus.NOT_STARTED) {
// log('Bottom Navigation: loading user for email: ${userProvider.sharedPreferences!.getString(EMAIL)} ');
log("Bottom Navigation: fetching all categories");
categoryProvider.fetchAllCategories();
}
return Scaffold(
body: (!(categoryProvider.loadingStatus == LoadingStatus.LOADING ||
categoryProvider.loadingStatus == LoadingStatus.NOT_STARTED))
? PageView(
children: _pages,
controller: _pageController,
onPageChanged: onPageChanged,
)
: Center(
child: Container(
height: 100,
width: 100,
child: CustomLoadingIndicator(),
),
),
bottomNavigationBar:
(!(categoryProvider.loadingStatus == LoadingStatus.LOADING ||
categoryProvider.loadingStatus == LoadingStatus.NOT_STARTED))
? BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: _onTapped,
items: [
const BottomNavigationBarItem(
label: "Feed",
icon: Icon(FontAwesomeIcons.rss),
),
const BottomNavigationBarItem(
label: "Explore",
icon: Icon(FontAwesomeIcons.borderAll),
),
const BottomNavigationBarItem(
label: "Profile",
icon: Icon(FontAwesomeIcons.user),
),
const BottomNavigationBarItem(
label: "Settings",
icon: Icon(FontAwesomeIcons.gears),
),
],
)
: null,
);
}
}
What I want to do-
The HomePage consists of a bottom navigation bar which can be used to navigate between the four pages. Home Page is the first widget to be built when app is opened.
Now, when the app is opened, I want to fetch all the data using the fetchAllCategories() method (which are stored in the allCategories variable).
The same fetchAllCategories() might be called from other parts of app to refresh data.
My approach-
I am using the loadingStatus variable in the CategoryProvider to keep track of the data loaded.
If the data is getting loaded, it will be set as LOADING, else if not started fetching then as NOT_STARTED else if loaded then COMPLETED.
The widgets will get built accordingly.
My Question-
I am fetching data in the build() method of the Home Page because I can't access context outside it. So, will this approach be efficient in loading data or is there some more efficient approach for this functionality? Although, this would work but I am not sure this is efficient and correct approach when I have to re-fetch the data?

With ObjectBox instead of SharedPreferences you could read the data in sync. This would make the code a bit cleaner. I am using BLoC and with this package I would load the data in a quite different pattern. However, with ObjectBox you might be able to streamline your code such that it doesn't matter.

Related

How to call init method or specific function again when we click on already activated bottom menu

I have implemented following BottomNavigation
class AppMenu extends StatefulWidget {
const AppMenu({Key? key}) : super(key: key);
#override
State<AppMenu> createState() => _AppMenuState();
}
class _AppMenuState extends State<AppMenu> {
int current = 0;
final List<String> titles = [
"Home 1",
"Home 2"
];
final List<Widget> views = [
const HomeView1(),
const HomeView2(),
];
final List<String> icons = [
"icon_1",
"icon_2",
];
final List<String> barTitles = ["Home1", "Home2"];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: HomeAppBar(
title: titles[current],
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: (index) {
setState(() {
current = index;
});
},
selectedItemColor: const Color(0xff6B6B6B),
showUnselectedLabels: true,
showSelectedLabels: true,
unselectedItemColor: const Color(0xff6B6B6B),
selectedLabelStyle: const TextStyle(fontSize: 12),
unselectedLabelStyle: const TextStyle(fontSize: 12),
items: views.map((e) {
final itemIndex = views.indexOf(e);
return BottomNavigationBarItem(
icon: Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Image.asset(
"assets/images/${icons[itemIndex]}${itemIndex == current ? "" : "_disabled"}.png",
width: 25,
),
),
label: barTitles[itemIndex],
);
}).toList()),
body: Column(
children: [
Expanded(child: views[current]),
],
),
);
}
}
Now it works perfect when I click on home1 and home2 bottom menu and it shows respected widget and load all the content which I have wrote on initState of home1 and home2 but now assume that I am on home1 and if I click again home1 then it is not calling initState again.
I want to call initState or specific function if user click on that menu even if it is selected.
Is there any way to do it?
You can create a initialize or initXXX function to initialize something in initState or somewhere. If parent widget call setState(), then child widget will call didUpdateWidget().
void initialize() {
// do something
}
Call initialize() in initState().
void initState() {
super.initState();
initialize();
}
Call initialize() in didUpdateWidget() of page(child widget).
#override
void didUpdateWidget(covariant PageTest oldWidget) {
super.didUpdateWidget(oldWidget);
initialize();
}
To handle the case in a simple way. You can add your method in onTap of BottomNavigationBar and then pass your data down to the widget tree.
It's only a demonstration to handle your case, you can adjust it with your own liking
For example
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: (index) {
if(current == index){
foo = yourMethodHere();
}
setState(() {
current = index;
});
},
Pass the variable in the tree
List<Widget> get views => [
HomeView1(foo),
HomeView2(foo),
];

Flutter provider profile picture not updating

I am building a method that the user can select a prefered profile picture to show arround the app, using provider package. I used shared_preferences to save the profile picture preferences on locally as a int value. And it worked, means I can save the profile picture to local system. But the problem is, the provider package completely became useless in this case, because I have to convert the widget to statefull and call the setState method when ever I insert a profilePicture widget inside the widget tree. And even the profilePicture widget in the HomeScreen not updating this way. I want to know how can I use the provider package for this issue instead of using statefulWidgets.
watch the Gif or video
This is the Provider class I created:
class ProfilePicProvider with ChangeNotifier {
ProfilePicPref profilePicPreferences = ProfilePicPref();
int _svgNumber = 1;
int get svgNumber => _svgNumber;
set svgNumber(int value) {
_svgNumber = value;
profilePicPreferences.setProfilePic(value);
notifyListeners();
}
void changePic(int val) {
_svgNumber = val;
profilePicPreferences.setProfilePic(val);
notifyListeners();
}
}
This is the sharedPreferences class
class ProfilePicPref {
static const PRO_PIC_STS = 'PROFILESTATUS';
setProfilePic(int svgNo) async {
SharedPreferences profilePref = await SharedPreferences.getInstance();
profilePref.setInt(PRO_PIC_STS, svgNo);
}
Future<int> getProfilePicture() async {
SharedPreferences profilePref = await SharedPreferences.getInstance();
return profilePref.getInt(PRO_PIC_STS) ?? 1;
}
}
This is the image selection screen and save that data to sharedPreferences class
class SelectProfilePicture extends StatefulWidget {
const SelectProfilePicture({Key? key}) : super(key: key);
#override
State<SelectProfilePicture> createState() => _SelectProfilePictureState();
}
class _SelectProfilePictureState extends State<SelectProfilePicture> {
int svgNumber = 1;
ProfilePicProvider proProvider = ProfilePicProvider();
#override
void initState() {
getCurrentProfilePicture();
super.initState();
}
void getCurrentProfilePicture() async {
proProvider.svgNumber =
await proProvider.profilePicPreferences.getProfilePicture();
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
CurrentAccountPicture(
path: 'assets/svg/${proProvider.svgNumber}.svg'),
Expanded(
child: GridView.builder(
itemCount: 15,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
setState(() {
svgNumber = index + 1;
});
proProvider.changePic(index + 1);
proProvider.svgNumber = index + 1;
},
child: SvgPicture.asset('assets/svg/${index + 1}.svg'),
);
},
),
),
],
),
);
}
}
This is the HomeScreen which is not updating the profile image whether it is statefull or stateless
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final proPicProvider = Provider.of<ProfilePicProvider>(context);
return Scaffold(
body:
Column(
children: [
Row(
children: [
CurrentAccountPicture(
path: 'assets/svg/${proPicProvider.svgNumber}.svg'),
],
),
],
),
);
}
}
example:
I have to convert the widget to statefull and call setState method to get the current profile picture from sharedPreferences. You may find this screen from the GIF I provided.
class Progress extends StatefulWidget {
const Progress({Key? key}) : super(key: key);
#override
State<Progress> createState() => _ProgressState();
}
class _ProgressState extends State<Progress> {
ProfilePicProvider proProvider = ProfilePicProvider();
#override
void initState() {
getCurrentProfilePicture();
super.initState();
}
void getCurrentProfilePicture() async {
proProvider.svgNumber =
await proProvider.profilePicPreferences.getProfilePicture();
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SizedBox(
height: 130.0,
width: 130.0,
child: SvgPicture.asset(
'assets/svg/${proProvider.svgNumber}.svg'),
),
),
);
}
}
The problem is in _SelectProfilePictureState when you create new instance of your ChangeNotifier:
ProfilePicProvider proProvider = ProfilePicProvider();. It means you are not using the provider available across the context but creating new one every time. So when the value of your provider changed it has effect only inside _SelectProfilePictureState. Instead of creating new instance you must call it always using the context:
class SelectProfilePicture extends StatefulWidget {
const SelectProfilePicture({Key? key}) : super(key: key);
#override
State<SelectProfilePicture> createState() => _SelectProfilePictureState();
}
class _SelectProfilePictureState extends State<SelectProfilePicture> {
int svgNumber = 1;
// [removed] ProfilePicProvider proProvider = ProfilePicProvider();
//removed
/*void getCurrentProfilePicture() async {
proProvider.svgNumber =
await proProvider.profilePicPreferences.getProfilePicture();
setState(() {});
}*/
#override
Widget build(BuildContext context) {
//use provider from the context
final proProvider = Provider.of<ProfilePicProvider>(context,listen:true);
return Scaffold(
body: Column(
children: [
CurrentAccountPicture(
path: 'assets/svg/${proProvider.svgNumber}.svg'),
Expanded(
child: GridView.builder(
itemCount: 15,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
setState(() {
svgNumber = index + 1;
});
proProvider.changePic(index + 1);
proProvider.svgNumber = index + 1;
},
child: SvgPicture.asset('assets/svg/${index + 1}.svg'),
);
},
),
),
],
),
);
}
}
If you enter the application you may want send initially selected image to your provider:
Add parameter to the constructor of ProfilePicProvider:
ProfilePicProvider(SharedPreferences prefs): _svgNumber = prefs.getInt(ProfilePicPref.PRO_PIC_STS) ?? 1;
In main.dart:
Future<void> main()async{
WidgetsFlutterBinding.ensureInitialized();
var prefs = await SharedPreferences.getInstance();
runApp(
MultiProvider(
providers:[
ChangeNotifierProvider( create:(_) => ProfilePicProvider(prefs))
],
child: yourtopWidget
)
);
}

Flutter call Firebase Function before loading Widget

I am still relatively new to Flutter and I am trying to figure out how to construct a Widget UI using external data... I have written a Firebase Function that when called generates a GetStream.io user token so that I can send that token to the Feed page of my app. I have posted my code below, it doesn't work but if someone could help it would be much appreciated.
What I am attempting to do is when this Widget initiates, set isLoading to true, call the function 'getStreamUserToken' which returns a token, once retrieved, set 'isLoading' to false and pass the token to the FeedScreen widget in the main body so that it can handle the Stream Feed.
import 'package:clubs/components/loading.dart';
import 'package:clubs/home/feed_screen.dart';
import 'package:cloud_functions/cloud_functions.dart';
import 'package:flutter/material.dart';
class ClubsApp extends StatefulWidget {
const ClubsApp({Key? key}) : super(key: key);
#override
State<ClubsApp> createState() => _ClubsAppState();
}
class _ClubsAppState extends State<ClubsApp> {
bool isLoading = false;
String? token;
void setIsLoading() {
setState(() {
isLoading = !isLoading;
});
}
#override
void initState() {
super.initState();
asyncMethod();
print('Token: ' + token!);
setIsLoading();
}
void asyncMethod() async {
token = await _getStreamToken();
}
#override
Widget build(BuildContext context) {
return isLoading
? const Loading()
: Scaffold(
body: FeedScreen(token: token!), // TODO: implement page switching
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"),
BottomNavigationBarItem(
icon: Icon(Icons.groups_outlined), label: "Members"),
BottomNavigationBarItem(
icon: Icon(Icons.water_outlined), label: "Pontoon"),
],
selectedItemColor: const Color(0xff202441),
unselectedItemColor: const Color(0xFF02BFAB),
showUnselectedLabels: true,
),
);
}
Future<String?> _getStreamToken() async {
HttpsCallable callable =
FirebaseFunctions.instance.httpsCallable('getStreamUserToken');
final results = await callable();
String token = results.data;
return token;
}
}
You need to wait for asyncMethod to complete, before calling setIsLoading.
The simplest way to do that, is to hook it's then callback:
#override
void initState() {
super.initState();
asyncMethod().then((token) {
print('Token: ' + token!);
setIsLoading();
})
}
If you want to return the value, you can:
#override
void initState() {
super.initState();
return asyncMethod().then((token) {
setIsLoading();
return token;
})
}
I recommend reading up on Dart Futures and taking the Asynchronous programming: futures, async, await codelab

Flutter send and show data between BottomNavigationBarItem

I want to share and show data between BottomNavigationBarItems
It is my BottomNavigationBar page:
class BottomNavigationBarController extends StatefulWidget {
#override
_BottomNavigationBarControllerState createState() =>
_BottomNavigationBarControllerState();
}
class _BottomNavigationBarControllerState
extends State<BottomNavigationBarController> {
List<Widget> pages = [];
#override
void initState() {
pages.add(HomePage(
key: PageStorageKey('HomePage'),
));
pages.add(MapPage(
key: PageStorageKey('MapPage'),
));
super.initState();
}
int _currentIndex = 0;
Widget _bottomNavigationBar(int selectedIndex) => BottomNavigationBar(
onTap: (int index) => setState(() => _currentIndex = index),
currentIndex: selectedIndex,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"),
BottomNavigationBarItem(icon: Icon(Icons.map), label: "MapPage"),
],
);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("iArrived"),
actions: _currentIndex != 1
? <Widget>[
IconButton(
icon: Icon(Icons.keyboard_arrow_right),
onPressed: () {
setState(() {
_currentIndex++;
});
})
]
: null),
backgroundColor: Colors.white,
bottomNavigationBar: _bottomNavigationBar(_currentIndex),
body: IndexedStack(
index: _currentIndex,
children: pages,
),
);
}
}
And I want to send my location from the MapPage to the HomePage.
There is a function in the MapPage that is executed when the place change:
void _setLocation(LatLng latlng) {
setState(() {
lat = latlng.latitude;
lng = latlng.longitude;
});
}
I think I have to call some function like a callback inside _setLocation() but I don't know how to implement it to show the location on my HomePage and have it refresh every time I change it.
Thanks for the help.
If your answer is to use shared_preferences plugin, can you tell me how would you implement it? Because I tried it and it did not work for me.
Thanks again.
You can use provider plugin for state management:
Create a model that store a shared value:
class LocationModel extends ChangeNotifier {
Location location;
void updateLocation(Location location) {
this.location = location;
notifyListeners();
}
}
Wrap your root widget with Provider class:
#override
Widget build(BuildContext build) {
return ChangeNotifierProvider(
create: (_) => LocationModel(),
child: App(),
);
}
When you changing a shared value in a model (you need to get a model from Provider, it depends on concrete implementation). For example:
onTap() {
model.updateLocation(newLocation):
}

How to prevent calling http request everytime we click into the page on bottom navigation bar in flutter?

On the home page I have 4 different api calls fetching 4 different data from a wordpress site. The current way I have coded is that everytime we enter home page, in the initState(), the get _getHomePageData() function is called where the async api fetching is happening. while this is happening allDataLoaded boolean is set to false in the first place.
Once the data is loaded allDataLoaded is set to true, the loading stops and the widgets are shown.
Here is the homepage widget:
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final String homePageLatestArtworkApi =
'https://example.com/wp-json/wp/v2/artworks?
per_page=1&_embed';
final String homePageAllArtworkApi =
'https://example.com/wp-json/wp/v2/artworks?
per_page=6&_embed';
final String homePageAllEventsApi =
'https://example.com/wp-json/wp/v2/events?
per_page=6&_embed';
final String homePageAllVenuesApi =
'https:example.com/wp-json/wp/v2/venues?
per_page=6&_embed';
List homePageLatestArtwork;
List homePageAllArtworks;
List homePageAllEvents;
List homePageAllVenues;
var allDataLoaded = false;
#override
void initState(){
super.initState();
print('init state is called everytime the page is loaded');
_getHomePageData();
}
Future<String> _getHomePageData() async{
var responseLatestArtwork = await http.get(Uri.encodeFull(homePageLatestArtworkApi));
var responseAllArtworks = await http.get(Uri.encodeFull(homePageAllArtworkApi));
var responseAllEvents = await http.get(Uri.encodeFull(homePageAllEventsApi));
var responseAllVenues = await http.get(Uri.encodeFull(homePageAllVenuesApi));
setState(() {
//latest artwork
var convertDataToJsonLatestArtwork = json.decode(responseLatestArtwork.body);
homePageLatestArtwork = convertDataToJsonLatestArtwork;
//All Artworks
var convertDataToJsonAllArtworks = json.decode(responseAllArtworks.body);
homePageAllArtworks = convertDataToJsonAllArtworks;
// All Events
var convertDataToJsonAllEvents = json.decode(responseAllEvents.body);
homePageAllEvents = convertDataToJsonAllEvents;
//All venues
var convertDataToJson = json.decode(responseAllVenues.body);
homePageAllVenues = convertDataToJson;
if(homePageLatestArtwork != null && homePageAllArtworks != null && homePageAllEvents != null && homePageAllVenues != null){
allDataLoaded = true;
}
// print('converted data is here');
//print(homePageLatestArtwork);
//print('the title is here :');
//print(homePageLatestArtwork[0]['title']['rendered']);
//print(homePageLatestArtwork[0]['_embedded']['wp:featuredmedia'][0]['source_url']);
});
#override
Widget build(BuildContext context) {
if(allDataLoaded){ //wait for the data to load and show spinning loader untill data is completely loaded
return Scaffold(
// body: Text('title'),
// body: Text("title: ${homePageLatestArtwork[0]['title']['rendered']} "),
body: Container(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Expanded(
Container(
height: 200.0,
child: Image.network(homePageLatestArtwork[0]['_embedded']['wp:featuredmedia'][0]['source_url'],
fit: BoxFit.fill,
width: double.maxFinite,),
),
Container(
padding: EdgeInsets.only(left:15.0,right:15.0,top:10.0),
child: Text(homePageLatestArtwork[0]['title']['rendered'],textAlign: TextAlign.left,
style: new TextStyle(
fontSize: 20.0,
fontFamily: 'Montserrat-Regular',
color: Color(0XFF212C3A),
fontWeight: FontWeight.bold
),),
),
])
),
),
);
}else{
return new Center(
child: new CircularProgressIndicator(
valueColor: new AlwaysStoppedAnimation<Color> .
(Theme.of(context).primaryColor),
),
);
}
} //end of _HOMEPAGESTATE
I would like to not load every single time the home page is viewed, or let's say I just want to fetch the api data once when the app starts and let users manually pull down to refresh the data.
Method 1:
If you are just talking about data then you should have a singleton object that you can init once in initState();
class _HomePageData {
var allDataLoaded = false;
String contents = "";
Future<String> _getHomePageData() async {
// Assuming contents is the data model and you load the data into contents
this.contents = "Loaded";
this.allDataLoaded = true;
return this.contents;
}
}
final _HomePageData data = _HomePageData();
class _HomePageState extends State<HomePage> {
String contents;
#override
void initState(){
super.initState();
if (!data.allDataLoaded) {
data._getHomePageData().then((contents) {
setState(() {
this.contents = contents;
})
});
} else {
this.contents = data.contents;
}
}
}
Method 2:
Previously I was dealing with tabs that constantly reload every time I move to another tab. Flutter actively remove state when they are not attached to any active widget tree. The idea is to have the widget in a Stack, placed into Offstage & TickerMode to control visibility and animation.
class MyTabState extends State<MyTabPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: MyBottomBar(
onTab: _onTab,
currentIndex: _currentIndex,
),
body: Stack(
children: List.generate(3, (index) {
return Offstage(
offstage: _currentIndex != index,
child: TickerMode(
enabled: _currentIndex == index,
child: getChild(index),
),
);
}, growable: false),
),
);
}
}
I hope I am not enough late. Anyhow if anyone else get the same situation not to refresh again and again you can add the following line at the end of your state. For example
with AutomaticKeepAliveClientMixin
It should be like this:
class _HomeScreenState extends State<HomeScreen>
with AutomaticKeepAliveClientMixin
then you can add an override
#override
bool get wantKeepAlive => true;
You are ready to rock.