How to pass 'context' to another widget outside buildContext widget - flutter

I have this code
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('All users'),
),
body: StreamBuilder<List<User>>(
stream: readUsers(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Text('error fetching data');
} else if (snapshot.hasData) {
if (snapshot.data!.isEmpty) {
// return const Text('no data to fect');
return Container(
padding: const EdgeInsets.all(10.0),
child: const Text('no data'),
);
} else {
final users = snapshot.data!;
return ListView(
children: users.map(buildUser).toList(),
);
}
} else {
return const Center(child: CircularProgressIndicator());
}
},
),
}
Then at this point
return ListView(
children: users.map(buildUser).toList(),
);
I want to return data from another widget outside buildContext widget but the issue here is that I don't know how to pass the 'context' in the users.map(buildUser).toList() unorder to eliminate the error in the image below.

Create a class like bellow
import 'package:flutter/material.dart';
class GlobalContextService {
static GlobalKey<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>();
}
now assign this key to the MaterialApp in main.dart just like bellow
return MaterialApp(
navigatorKey: GlobalContextService.navigatorKey, // set property
);
Now you can access the context any where you want by using the following line of code
GlobalContextService.navigatorKey.currentContext

try this:
Widget buildUser(User user, BuildContext context) =>

Recommended approach ->User helper widget instead of helper method.
or
you can pass context as parameter to method

Related

Flutter: How to await currentUrl() from WebViewController in FutureBuilder?

What I try to do
I implemented a webview and want to show the current url on another page using provider.
So onWebViewCreated I try to set the controller value via setController and consume it in the Consumer widget together with a FutureBuilder and an if statement. If hasData is truthy, I want to access the controller e.g. to get the currentUrl().
Where I'm stuck
The Text with controller.data?.currentUrl() returns Instance of 'Future<String?>'. I know I need to await it, but I don't know how.
Code
profile.page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:quirion_flutter/providers/router.providers.dart';
import 'package:quirion_flutter/widgets/webview.widgets.dart';
import 'package:webview_flutter/webview_flutter.dart';
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
#override
Widget build(BuildContext context) {
return Consumer<WebviewRouter>(builder: ((context, value, child) {
return SafeArea(
child: Stack(children: [
const BankingWebView(
initialUrl: 'https://banking-dev.quirion.de/setup/personal-data',
),
FutureBuilder(
future: value.controller.future,
builder: ((BuildContext context,
AsyncSnapshot<WebViewController> controller) {
if (controller.hasData) {
return Column(
children: [
Text('${controller.data?.currentUrl()}'),
Text(value.route),
],
);
}
return const SafeArea(child: Text('Nothing here'));
})),
]),
);
}));
}
}
References
https://medium.com/flutter/the-power-of-webviews-in-flutter-a56234b57df2
https://codelabs.developers.google.com/codelabs/flutter-webview#11
https://discord.com/channels/420324994703163402/421445316617961502/1039197342231973898
I went with a probably fairly simply solution. I just used another FutureBuilder which future then is controller.data?.currentUrl() (we remember, it returned Instance of Future<String?>) and then the snapshot to access the resolved data. Worked for me. Though, if there are better solution, I'm still happy for additional answers.
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
#override
Widget build(BuildContext context) {
return Consumer<WebviewRouter>(builder: ((context, value, child) {
return SafeArea(
child: Stack(children: [
const BankingWebView(
initialUrl: 'https://my-url.com',
),
FutureBuilder(
future: value.controller.future,
builder: ((BuildContext context,
AsyncSnapshot<WebViewController> controller) {
if (controller.hasData) {
// SOLUTION START
return FutureBuilder(
future: controller.data?.currentUrl(),
builder: (context, AsyncSnapshot<String?> snapshot) {
if (snapshot.hasData) {
return Text('${snapshot.data}');
}
return const SafeArea(child: Text('Loading...'));
});
// SOLUTION END
}
return Container();
})),
]),
);
}));
}
}

How to get ListView Component to scroll up on TextFieldInput

UPDATE 7/15/2021:
I found another similar question that helped me a bit:
Programmatically scrolling to the end of a ListView
As of now I am able to get it somewhat looking like what I want it to, but I am still getting an overflow issue.
Here is the updated Code
home.dart
class _HomePageState extends State<HomePage> {
// added a scroll controller to control my ListView
ScrollController scrollController = ScrollController();
void addEntry() {
setState(() {
entryTextController.text = '';
this.newEntry = !this.newEntry;
});
// on adding an item, I will scroll up in the ListView
Timer(
Duration(milliseconds: 100),
() =>
scrollController.jumpTo(scrollController.position.maxScrollExtent)
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Container(),
// instead of just the EntryList, I added a container
// with a fixed height. I'll probably change this later
Container(
height: 500.0,
child: EntriesList(props)
)
],
),
floatingActionButton: FloatingActionButton(onTap: addEntry)
);
}
}
EntryList.dart
class _EntriesListState extends State<EntriesList> {
List<Widget> getEntries(snapshot) {
myList = entryList.map<Widget>((entry) {
return Entry();
}).toList();
myList.add(Visibility(
visible: widget.newEntry,
child: Card(
child: ListTile(
title: TextFormField(),
))));
// This is the newest change. By adding a Placeholder, I am able to
// bring the screen up on the scrollController bringing this into
// position so that the user can then see the TextFormField
myList.add(Opacity(opacity: 0.0, child: Placeholder()))
return myList;
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _entryStream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
// Async code here ...
return ListView(
children: getEntries(snapshot),
padding: EdgeInsets.all(8.0),
shrinkWrap: true,
);
},
);
}
}
ORIGINAL:
I'm working on adding an entry to my ListView widget. Within my ListView Widget, I have embedded a hidden TextFieldInput that is shown once a user clicks the addEntry button.
My main issue is that once the list gets long enough, my list does not scroll up to allow the user to see their new entry that they are typing.
Here is my current code. In order to keep my question concise, I have removed some unrelated code.
main.dart
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Container(), // top widget here
// Scrollable list of dates
EntriesList(),
],
),
floatingActionButton: FloatingActionButton()
);
}
}
EntriesList.dart
class _EntriesListState extends State<EntriesList> {
List<Widget> getEntries(snapshot) {
myList = entryList.map<Widget>((entry) { // getting my entry widgets here
return Entry();
}).toList();
myList.add(Visibility( // adding my hidden text input widget here
visible: widget.newEntry,
child: Card(
child: ListTile(
title: TextFormField(),
))));
return myList;
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _entryStream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
return ListView( // I added several properties in hopes that it would
children: getEntries(snapshot), // work but to no avail
padding: EdgeInsets.all(8.0),
shrinkWrap: true,
physics:
BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
);
},
);
}
}
Use the SingleChildScrollView widget. Can be used in HomePage or EntriesList by simply wrapping it with the SingleChildScrollView widget. Like this in the home page:
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Container(), // top widget here
// Scrollable list of dates
SingleChildScrollView(child:widgetEntriesList()),
],
),
floatingActionButton: FloatingActionButton()
);
}
}
Or like this in the EntriesList:
class _EntriesListState extends State<EntriesList> {
List<Widget> getEntries(snapshot) {
myList = entryList.map<Widget>((entry) { // getting my entry widgets here
return Entry();
}).toList();
myList.add(Visibility( // adding my hidden text input widget here
visible: widget.newEntry,
child: Card(
child: ListTile(
title: TextFormField(),
))));
return myList;
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _entryStream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
return SingleChildScrollView(
child:ListView(
children: getEntries(snapshot), // work but to no avail
padding: EdgeInsets.all(8.0),
shrinkWrap: true,
physics:BouncingScrollPhysics(),
)
);
},
);
}
}
Instead of wrapping ListView, you could instead wrap the StreamBuilder as well.

How can I pass a parameter to the future property function in FutureBuilder

My problem is that I don't know how to pass the argument of a route to my function which is inside the FutureBuilder.
Please see my code below.
class StudyDetailsArguments {
final String listid;
StudyDetailsArguments(this.listid);
}
// A widget that extracts the necessary arguments from the ModalRoute.
class ExtractStudyDetails extends StatelessWidget {
static const routeName = '/studydetails';
#override
Widget build(BuildContext context) => FutureBuilder(
// Here I want to pass in the args.listid but I cant figure out how to get it in there
future: getDetails("cc0e5c1f-02b0-4f4f-9f51-fa70ac7e9c08"),
builder: (context, snapshot) {
// here is fine to use the argument
final StudyDetailsArguments args = ModalRoute.of(context).settings.arguments;
if (snapshot.hasData) {
// Build the widget with data.
//return Text('hasData: ${snapshot.data}');
return Scaffold(
appBar: AppBar(
title: Text(snapshot.data),
),
body:
Center(
child:
Text('${snapshot.data}'))
);
} else {
// We can show the loading view until the data comes back.
return Scaffold(
appBar: AppBar(
title: Text("Loading..."),
),
body:
Center(
child:
Text('Loading...'))
);
}
},
);
}
Future<String> getDetails(listid) async {
var details = "";
await Firestore.instance
.collection('list')
.document(listid)
.get()
.then((DocumentSnapshot ds) {
print(ds.data["title"]);
// use ds as a snapshot
details = ds.data["title"];
return ds.data;
});
return details;
}
I want to use this line args.listid instead of cc0e5c1f-02b0-4f4f-9f51-fa70ac7e9c08 but I can't seem to figure out the way to pass the parameter. How I send the parameter value (which now is not used) to the widget is this way in a widget:
onTap: () => {
Navigator.pushNamed(context, ExtractStudyDetails.routeName,
arguments: StudyDetailsArguments(
study.listid,
))
},
#override
Widget build(BuildContext context) {
final StudyDetailsArguments args = ModalRoute.of(context).settings.arguments;
return FutureBuilder(
future: getDetails(args.listid),
[...]
)
}
Read more on docs

Flutter: async await

In the profile page of my app, I want to save a future list of objects from a firebase collection to a variable (myRecipes), using an async/await function. Depending on the outcome list, I want to display different widgets (using ifHasRecipes()) - if the list turns out to be null or empty, I want to display a text widget, otherwise I want to display the objects in the list using a listview builder (FavoritesHomePage class).
class Profile extends StatefulWidget {
final FirebaseAuth _auth = FirebaseAuth.instance;
#override
_ProfileState createState() => _ProfileState();
}
class _ProfileState extends State<Profile> {
List<Recipe> myRecipes;
Future<List<Recipe>> getUserRecipes(UserData userData) async {
return myRecipes = await DatabaseService().findUserRecipes(userData.uid);
}
Widget ifHasRecipes() {
if (myRecipes != null && myRecipes != []) {
return FavoritesHomePage(
recipes: myRecipes, scrollDirection: Axis.vertical, title: 'Your recipes',);
} else {
return Text('You have no favorites yet');
}
}
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return StreamBuilder<UserData>(
stream: DatabaseService(uid: user.uid).userData,
builder: (context, snapshot) {
if (snapshot.hasData) {
UserData userData = snapshot.data;
getUserRecipes(userData);
return Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
//widgets using userData
ifHasRecipes(),
],
),
),
);
} else {
return Scaffold(
body: Center(
child: SpinKitRipple(),),
);
}
});
}
}
How can I make this code synchronized? I want to run the getUserRecipes() and when it's done, return different widgets depending on the outcome.
If I do a hot reload, the code "works" as I want it to, but sometimes when I navigate to this profile page via my pageview widget, the async/await function returning the variable myRecipes isn't done before the ifHasRecipes() is built, and then myRecipes is null (even though it shouldn't be)... Hope this isn't too confusing, sorry.
In this case you can use a FutureBuilder, with this one you will have different states, just like the StreamBuilder, and you can show different widgets depending on the state, until the Future is resolved and you have the data.
I've done a little bit of refactoring to your code to make it work with the FutureBuilder, also I've changed it to Stateless, in this case it will display a CircularProgressIndicator until the Future is resolved, it will also handle errors and the lack of data.
class Profile extends StatelessWidget {
const Profile({Key key}) : super(key: key);
Future<List<Recipe>> getUserRecipes(UserData userData) async {
return await DatabaseService().findUserRecipes(userData.uid);
}
Widget ifHasRecipes(List<Recipe> myRecipes) {
if (myRecipes != null && myRecipes != []) {
return FavoritesHomePage(
recipes: myRecipes,
scrollDirection: Axis.vertical,
title: 'Your recipes',
);
} else {
return Text('You have no favorites yet');
}
}
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return StreamBuilder<UserData>(
stream: DatabaseService(uid: user.uid).userData,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Scaffold(
body: SafeArea(
child: FutureBuilder(
future: getUserRecipes(snapshot.data),
builder: (context, futureSnapshot) {
if (futureSnapshot.hasError)
return Text('Error: ${futureSnapshot.error}');
switch (futureSnapshot.connectionState) {
case ConnectionState.none:
return Center(child: CircularProgressIndicator());
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
case ConnectionState.active:
return Center(child: CircularProgressIndicator());
case ConnectionState.done:{
if (futureSnapshot.hasData) {
List<Recipe> myRecipes = futureSnapshot.data;
return Column(
children: <Widget>[
//widgets using userData
ifHasRecipes(myRecipes),
],
);
}
return Text('There\'s no available data.');
}
}
return null;
},
),
),
);
} else {
return Scaffold(
body: Center(
child: SpinKitRipple(),
),
);
}
},
);
}
}
If I understand the code correctly, the solution is to rebuild the widget when the future is resolved by adding setState((){}); to the getUserRecipes() method :
Future<void> getUserRecipes(UserData userData) async {
myRecipes = await DatabaseService().findUserRecipes(userData.uid);
setState((){});
}
(You don't have to return the value if you're assigning it to the state, but rather access it directly.)
By the way, you can use the ternary operator (or just regular conditions) to do conditional UI. Put this instead of ifHasRecipes(), :
(myRecipes != null && myRecipes != []) ?
FavoritesHomePage(
recipes: myRecipes, scrollDirection: Axis.vertical, title: 'Your recipes',)
: Text('You have no favorites yet')
If you get an error with this, increase your minimum SDK version to 2.6.0 in the pubspec.yaml

Auto navigation on login page in flutter app

My app needs to automatically initiate biometric login and navigate to a page based on outcome.
This is a common need and I followed advice similar to this solution
Code below
class _LoginPageState extends State<LoginPage> {
#override
Widget build(BuildContext context) {
final UserModel userModel = Provider.of<UserModel>(context);
return (userModel.biometricLoginEnabled && !userModel.isAuthenticated)
? _attemptBiometricAuthentication(context, userModel)
: _buildLoginForm(context, userModel);
}
Widget _attemptBiometricAuthentication(
BuildContext context, UserModel userModel) {
return FutureBuilder(
future: _initiateBiometricAuthentication(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data == true) {
// make sure user is marked as authenticated
userModel.setAuthenticationWithoutNotification(true);
return HomePage(); // <-- WHOA!!
} else if (snapshot.hasData && snapshot.data == false) {
// we should have an updated error from _initiateBiometricAuthentication
return _buildLoginForm(context, userModel);
} else if (snapshot.hasError) {
return _buildLoginForm(context, userModel);
} else {
// we're waiting
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height,
minHeight: MediaQuery.of(context).size.height,
),
alignment: Alignment.center,
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Image(
image: AssetImage('images/Logo.png'),
fit: BoxFit.fitWidth,
),
CircularProgressIndicator(),
],
),
),
);
}
},
);
}
}
The problem is with the line to return HomePage() if authentication succeeds.
If there is a call to setState() and a rebuild occurs I have the HomePage being rebuilt inside LoginPage. Routing is also a little messed up because the app thinks it's on route /login but its actually on /home.
I feel like I'm missing something entirely in triggering routing automatically.
You need to listen result from the Future method and navigate to other Page. (never do it inside build Widget).
Demo:
Code example:
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class FutureNavigation extends StatefulWidget {
#override
_FutureNavigationState createState() => _FutureNavigationState();
}
class _FutureNavigationState extends State<FutureNavigation> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Demo Future Navigator"),
),
body: buildBody(context),
);
}
Widget buildBody(BuildContext context) {
return FutureBuilder(
future: _login(),
builder: (context, snapshot) {
return Center(child: CircularProgressIndicator());
},
);
}
Future<String> _login() async {
await Future.delayed(Duration(seconds: 3)).then((value) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (BuildContext context) {
return HomePage();
},
),
);
});
return "Logined";
}
}