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

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();
})),
]),
);
}));
}
}

Related

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

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

I am failing to get data from cloud firestore while using flutter

At first, when i started writing my calls to get data from firestore, it worked. But when i tried writing more docs to my collection, it failed to bring data for the docs i recently added. Then, when i deleted the first one i added, i stopped receiveing data from firestore all together. I have tried several methods, but have all ended in failure.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
class collect extends StatefulWidget {
#override
_collectState createState() => _collectState();
}
class _collectState extends State<collect>
{
Future _data;
void initState()
{
super.initState();
_data = getStuff();
}
Future getStuff()
async {
var firestore = FirebaseFirestore.instance;
QuerySnapshot qn = await firestore.collection("buses").get();
return qn.docs;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: _data,
builder: (_, snapshot)
{
if(snapshot.connectionState == ConnectionState.waiting)
{
return Center(
child:Text("Loading")
);
}
else if(snapshot.connectionState == ConnectionState.done)
{
return ListView.builder(itemCount: snapshot.data.length,itemBuilder:(_, index)
{
return Container(
child: ListTile(
title: Text(snapshot.data[index].data()["name"].toString()),
subtitle: Text(snapshot.data[index].data()["price"].toString()),
),
);
});
}
},
),
);
}
}
```![enter image description here](https://i.stack.imgur.com/L7FqF.jpg)
Define your database call as,
Future getStuff() async {
var docs;
await FirebaseFirestore.instance
.collection("buses")
.get()
.then((querySnapshot) {
docs = querySnapshot.docs;
});
return docs;
}
Then use the FutureBuilder in the build() function as,
return Scaffold(
body: Center(
child: FutureBuilder<dynamic>(
future: getStuff(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (_, index) {
return Container(
child: ListTile(
title: Text(
snapshot.data[index].data()["name"].toString()),
subtitle: Text(
snapshot.data[index].data()["price"].toString()),
),
);
});
} else {
return CircularProgressIndicator();
}
},
),
),
);
I wrapped the FutureBuilder inside a Center just for clarity, you may remove that Center widget.

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";
}
}

ScanStreamTransformer alternative that's called once per event instead of once per listener

I think, now I see what's going on with respect to transform() in Bloc.dart class. ScanStreamTransformer inside the constructor of BloC.dart is called once for each listener (there are 3 listeners inside the One.dart class so it's called 3 times). I'd like to change this behaviour so that it's called once per event, regardless of how many listeners attached e.g. calling _mainBloc.inValue(widget.value) would invoke transform only once, right now it's called 3 times because there are 3 listeners (see streamBuilder()) inside the One.dart build() function.
I hope this is a little bit more clearer compared to previous post (now deleted).
class OneState extends State<One>{
#override
void didChangeDependencies() {
super.didChangeDependencies();
_mainBloc.inValue(widget.value);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: <Widget>[
streamBuilder(),
streamBuilder(),
streamBuilder()
],
),
);
}
streamBuilder(){
return StreamBuilder(
stream: _mainBloc.values$,
builder: (context, AsyncSnapshot<Map<String, Future<String>>> snapshot){
if(snapshot.connectionState == ConnectionState.waiting) return Center(child: Container(child: new CircularProgressIndicator()));
if(!snapshot.hasData) return Center(child: Container(child: Text("No Data"),));
return FutureBuilder(
future: snapshot.data[widget.value],
builder: (contextFuture, AsyncSnapshot<String> snapshotFuture){
if(snapshotFuture.connectionState == ConnectionState.waiting)
return Center(child: Container(child: new CircularProgressIndicator()));
return Center(
child: Container(
child: Text(snapshotFuture.data),
),
);
},
);
},
);
}
}
BloC
class MainBloc{
final ApiRequest _provider;
MainBloc(this._provider){
values$ = _value.stream.transform(
ScanStreamTransformer((Map<String, Future<String>> cache, String symbol, index){
print('transformer');
cache[symbol] = _provider.fetchData();
return cache;
},
<String, Future<String>>{},
));
}
final _value = BehaviorSubject<String>();
Observable<Map<String, Future<String>>> values$;
Function(String symbol) get inValue => _value.sink.add;
dispose(){
_value.close();
}
}
Add a .asBroadcastStream(). Like:
values$ =_value.stream.transform(
ScanStreamTransformer((Map<String, Future<String>> cache, String symbol, index){
print('transformer');
cache[symbol] = _provider.fetchData();
return cache;
},
<String, Future<String>>{},
)).asBroadcastStream();
It should stop the triple-time calling.

FLUTTER - Futurebuilder keeps returning null after Location Permission

The Problem
Futurebuilder keeps returning "null" after the user has given permission to acces it's location so it can calculate the distance between 2 locations.
What I want it to do
It does give the location when the page is refreshed but I want the distance between 2 objects when the user gives acces to it's location, not when the user refreshed their page.
The Main Code to run the app
import 'package:flutter/material.dart';
import 'mainlist.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
routes: {
'/second': (context) => mainlist()
},
title: "testapp",
debugShowCheckedModeBanner: false,
home: mainlist(),
);
}
}
The code where the problem happens - Futurebuilder + getCurrenPosition Future
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:async/async.dart';
import 'package:geolocator/geolocator.dart';
import 'package:permission/permission.dart';
import 'mainlist.dart';
class mainlist extends StatefulWidget {
#override
_mainlistpage createState() => _mainlistpage();
}
class _mainlistpage extends State<mainlist> {
Future<String> getCurrentPosition(DocumentSnapshot document) async{
Position position = await Geolocator().getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
double distanceInMeters = await Geolocator().distanceBetween(position.latitude, position.longitude, document['lat'], document['lat']);
return distanceInMeters.toString();
}
var sortBy = "";
Widget homePage() {
return StreamBuilder(
stream: Firestore.instance.collection("Test").orderBy(sortBy).snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return Text("Loading");
return ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (context, index) =>
_mainListItem(context, snapshot.data.documents[index]));
},
);
}
#override
Widget _mainListItem(BuildContext context, DocumentSnapshot document) {
return Scaffold(
body: Container(
child: Center(
child: Column(
children: <Widget>[
FutureBuilder(
future: getCurrentPosition(document),
builder: (BuildContext context,AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('none');
case ConnectionState.active:
case ConnectionState.waiting:
return Text('waiting');
case ConnectionState.done:
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
return Text(snapshot.data.toString());
}
return null; // unreachable
}
),
]
),
),
),
);
}
Widget build(BuildContext context){
return new Scaffold();
}
}
What have I tried
Using streambuilder
Messing with the cases in the Futurebuilder
Reading Stackoverflow
I have added the permissions in plist and manifest