Flutter call Firebase Function before loading Widget - flutter

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

Related

Loading data with Provider: 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.

How to use a variable within a method elsewhere in Flutter?

I know this is a stupid question, but I'm asking as a newbie to flutter.
I created a getData() method to call Firebase's User data and display it on the app. And to call it, result.data() is saved as a variable name of resultData.
But as you know I can't use Text('user name: $resultData'). How do I solve this? It's a difficult problem for me, since I don't have any programming basics. thank you.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:shipda/screens/login/login_screen.dart';
import 'package:get/get.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final _authentication = FirebaseAuth.instance;
User? loggedUser;
final firestore = FirebaseFirestore.instance;
void getData() async {
var result = await firestore.collection('user').doc('vUj4U27JoAU6zgFDk6sSZiwadQ13').get();
final resultData = result.data();
}
#override
void initState() {
super.initState();
getCurrentUser();
getData();
}
void getCurrentUser(){
try {
final user = _authentication.currentUser;
if (user != null) {
loggedUser = user;
print(loggedUser!.email);
}
} catch (e){
print(e);
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
children: [
Text('Home Screen'),
IconButton(
onPressed: () {
FirebaseAuth.instance.signOut();
Get.to(()=>LoginScreen());
},
icon: Icon(Icons.exit_to_app),
),
IconButton(
onPressed: () {
Get.to(() => LoginScreen());
},
icon: Icon(Icons.login),
),
Text('UserInfo'),
Text('user name: ')
],
),
),
);
}
}
What you are referring to is called state.
It is a complex topic and you will have to start studying it to correctly develop any web based app.
Anyway, as for your situation, you should have resultData be one of the attributes of the _HomeScreenState class.
Then change resultData in a setState method, like this:
setState(() {
resultData = result.data();
});
Then, in the Text widget, you can actually do something like:
Text("My data: " + resultData.ToString())
Instead of ToString of course, use anything you need to actually access the data.
By writing
void getData() async {
var result = await firestore.collection('user').doc('vUj4U27JoAU6zgFDk6sSZiwadQ13').get();
final resultData = result.data();
}
you make resultData only local to the function getData(). You should declare it outside. Also you need to put it in a setState to make it rebuild the screen after loading. I don't know what type it is, but if it's a String for example you could write
String? resultData;
void getData() async {
var result = await firestore.collection('user').doc('vUj4U27JoAU6zgFDk6sSZiwadQ13').get();
setState(() {
resultData = result.data();
});
}
Then you can use Text('user name: $resultData') for example

Flutter async methods for widget initialize

Let's say I create a new screen team_screen which is the first parent of the tree.
Now for my team screen there are many widgets, some of theme have their own request, I want to show loader until every widget/request finished and ready.
I thought on 2 approaches.
All the requests are executed in team_screen with future builder and I pass the props to my widgets by demand.
Every widget with request get function that get executed in the async function in the initState function, then in my parent I make to every widget state parameter that is equal to true by the function I passed and when all is don't I stop the loader.
To sum up my problem is how to maintain a widget with many children and requests and showing one loader for entire page, making all the request on same widget? Pass isInitialize function to every widget?.
Which approach is better and if there are more approaches, I would like to hear.
Thank you for your help
Example for the second approach:
import 'package:flutter/material.dart';
import 'package:info_striker/locator.dart';
import 'package:info_striker/models/fixture/fixture.dart';
import 'package:info_striker/models/odds/bookmaker.dart';
import 'package:info_striker/models/synced-team/synced_team.dart';
import 'package:info_striker/services/fixture_service.dart';
import 'package:info_striker/utils/date_utilities.dart';
class TeamNextMatch extends StatefulWidget {
Function isInitialized;
SyncedTeam team;
TeamNextMatch({
Key? key,
required this.isInitialized,
required this.team,
}) : super(key: key);
#override
State<TeamNextMatch> createState() => _TeamNextMatchState();
}
class _TeamNextMatchState extends State<TeamNextMatch> {
Fixture? _fixture;
Bookmaker? _matchResult;
bool _isInitialized = false;
#override
void initState() {
super.initState();
init();
}
init() async {
final response = await locator<FixturesService>().getData(widget.team.id);
if (response != null) {
setState(() {
_fixture = Fixture.fromMap(response["fixture"]);
_matchResult = Bookmaker.fromMap(response["matchResultOdds"]);
});
}
widget.isInitialized(true);
}
#override
Widget build(BuildContext context) {
String? _date;
bool show = _fixture != null && _matchResult != null;
_fixture != null ? "${DateUtilities.getShortDateString(_fixture!.date)}, ${DateUtilities.getTimeString(_fixture!.date)}" : null;
return show
? Column(
children: [
Text(_fixture?.league?["name"]),
if (_date != null) Text(_date),
],
)
: const SizedBox();
}
}
You can show loader as described below -
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_application_1/data_model.dart';
import 'package:http/http.dart' as http;
class APiTest extends StatefulWidget {
const APiTest({Key? key}) : super(key: key);
#override
_APiTestState createState() => _APiTestState();
}
class _APiTestState extends State<APiTest> {
final String _url = "https://jsonplaceholder.typicode.com/todos/";
bool _isLoading = true;
final List<DataModel> _allData = [];
#override
void initState() {
super.initState();
_initData().then((value) {
setState(() {
_isLoading = false;
});
});
}
Future<void> _initData() async {
final response = await http.get(Uri.parse(_url));
final List res = jsonDecode(response.body);
res.forEach((element) {
_allData.add(DataModel.fromJson(element));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Loading Demo"),
),
body: Stack(
children: [
ListView.separated(
itemCount: _allData.length,
controller: ScrollController(),
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: ((context, index) {
return ListTile(
tileColor: Colors.grey[200],
title: Text(_allData[index].title!),
subtitle: Text(_allData[index].id.toString()),
);
}),
),
if (_isLoading)
const Center(
child: CircularProgressIndicator(),
)
],
),
);
}
}

OnSharedPreferenceChangeListener for Flutter

In Android, you can do the following to listen to shared preference change
SharedPreferences.OnSharedPreferenceChangeListener spChanged = new
SharedPreferences.OnSharedPreferenceChangeListener() {
#Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
// your stuff here
}
};
Is it possible to do this using flutter? I have read through the official flutter shared_preference and this features seems not yet implemented.
Is there any other library or ways to achieve the above without diving into native code. Thanks.
You can easily "listen" to SharedPreferences using a package like flutter_riverpod.
Initialize sharedPreferences
SharedPreferences? sharedPreferences;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
sharedPreferences = await SharedPreferences.getInstance();
runApp(const ProviderScope(child: MyApp()));
}
Create the stateProvider
import 'package:hooks_riverpod/hooks_riverpod.dart';
final keepOnTopProvider = StateProvider<bool>((ref) {
return sharedPreferences?.getBool('on_top') ?? true;
});
Update your UI when something changes
class SettingsView extends ConsumerWidget {
const SettingsView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
bool onTop = ref.watch(keepOnTopProvider);
return Scaffold(
appBar: AppBar(title: const Text('Settings'), centerTitle: false),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 12),
children: [
SwitchListTile(
title: const Text('Keep on top'),
value: onTop,
onChanged: (value) async {
sharedPreferences?.setBool('on_top', value);
ref.read(keepOnTopProvider.notifier).state = value;
await windowManager.setAlwaysOnTop(value);
},
),
],
),
);
}
}
As a work around, add the following codes to your main():
void funTimerMain() async {
// here check any changes to SharedPreferences, sqflite, Global Variables etc...
if (bolAnythingChanged) {
// do something
// 'refresh' any page you want (below line using Redux as example)
GlobalVariables.storeHome.dispatch(Actions.Increment);
}
// recall this timer every x milliseconds
new Future.delayed(new Duration(milliseconds: 1000), () async {
funTimerMain();
});
}
// call the timer for the first time
funTimerMain();

Flutter - Drawer as sub-class not updating

I'm a fairly inexperienced coder.
I have a Drawer which I have created as a separate class. The issue I'm having is the dynamic data for the Drawer is not populating.
I am expecting the data being retrieved from Shared Preferences should populate the third line of my view with the value of currUserL.
It's being evaluated correctly, and returns the value of currUserL to the console, but is not updated in the Drawer.
I've loaded up a about button (triggering the update method) that works when pressed manually, but data persists only while the drawer remains open. It reverts when the drawer is closed.
drawerPatient.dart
class DrawerPatient extends StatefulWidget {
DrawerPatient({Key key}) : super(key: key);
#override
_DrawerPatientState createState() => new _DrawerPatientState();
}
class _DrawerPatientState extends State<DrawerPatient> {
String currUserL = "nv3";
Future getPref() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
currUserL = prefs.getString('currUserLast');
debugPrint('user: $currUserL');
}
#override
void initState() {
getPref();
}
void update() {
setState(() {
getPref();
});
}
#override
Widget build(BuildContext context) {
return Drawer(
child: new ListView(
children: <Widget>[
new DrawerHeader(
child: new Text('Patient Management'),
),
new ListTile(
title: new Text('search'),
onTap: () {},
),
new ListTile(
title: new Text(currUserL),
onTap: () {},
),
new Divider(),
new ListTile(
title: new Text('About'),
onTap: update,
),
],
));
}
}
userList.dart
class UserList extends StatefulWidget {
UserList({Key key, this.title}) : super(key: key);
final String title;
final String titleHead = "User List";
#override
_UserListState createState() => new _UserListState();
}
class _UserListState extends State<UserList> {
: sortStr}, headers: {"Accept": "application/json"});
setState(() {
data = json.decode(response.body);
});
}
#override
void initState() {
this.makeRequest();
// DrawerPatient().createState().update();
}
void _refresh() {
setState(() {});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Patient List"),
drawer: new DrawerPatient(key: new UniqueKey()),
...
Drawer when opened
Drawer after clicking about (update)
So I found the answer, thanks to #Dinesh for pointing me in the right direction.
The answer was to put the setState as a dependency on the async get prefs.
Future getPref() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
currUserI = prefs.getString('currUserId');
currUserF = prefs.getString('currUserFirst');
currUserL = prefs.getString('currUserLast');
debugPrint('user: $currUserL');
});
}
Can you try this,
Future getCurrentUser() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString('currUserLast');
}
void update() {
val tempName = getCurrentUser();
setState(() {
currUserL = tempName;
});
}
Reason: Basically wait for the async method before calling setState