How to test widgets which relies on data which was set by other widget through provider? - testing

a novice programmer here who just started in flutter and trying to explore flutter testing. The following is my userInfoProvider code;
import 'package:flutter/cupertino.dart';
class UserInfoProvider with ChangeNotifier{
String _username;
String _userRole;
getUsername() => _username;
getUserRole() => _userRole;
setUsername(String username){
_username = username;
notifyListeners();
}
setUserRole(String userRole){
_userRole = userRole;
notifyListeners();
}
getCurrentUserInformation(String uid) async {
Query q = await Firestore.instance
.collection('users')
.document(uid)
.get()
.then((DocumentSnapshot ds) {
setUsername(ds.data['userName']);
setUserRole(ds.data['userRole']);
return;
});
}
}
I have a homepage widget which has an Uid property. This widget uses this uid to identify from the "users" collection in firestore to set the username and userRole my UserInfoProvider.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'UserInfoProvider.dart';
class HomePage extends StatefulWidget {
final String uid;
HomePage(this.uid);
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
void afterFirstLayout(BuildContext context) async {
var user = Provider.of<UserInfoProvider>(context);
await user.getCurrentUserInformation(widget.uid);
}
#override
Widget build(BuildContext context) {
return Container();
}
}
SecondPage widget has 2 text widget which displays the username and userRole from UserInfoProvider get methods.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'UserInfoProvider.dart';
class SecondPage extends StatefulWidget {
#override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends State<SecondPage> {
#override
Widget build(BuildContext context) {
final user = Provider.of<UserInfoProvider>(context);
return Column(
children: <Widget>[
Text(user.getUsername()),
Text(user.getUserRole())
],
);
}
}
In such scenarios how would one write the widget test whereby i would like to test the SecondPage Widget to ensure that the text displayed (e.g. the userRole) is the same as the one the mock Uid which i have built in HomePage widget? Following is my test code which fails the test;
testWidgets("Test secondpage widget to display correct userRole", (WidgetTester tester) async{
final uid = "ekkMH1sKW1dN0r3ZTQGCngJS1cV2";
await tester.pumpWidget(ChangeNotifierProvider(
builder: (_) => UserInfoProvider(),
child: MaterialApp(
home: HomePage(uid)
)
));
await tester.pumpWidget(ChangeNotifierProvider(
builder: (_) => UserInfoProvider(),
child: MaterialApp(
home: SecondPage()
)
));
final UserRoleTextFinder = find.text("someUserRole");
expect(UserRoleTextFinder, findsOneWidget);
});
I would guess is that i have build two instances of userInfoProvider which the state in Homepage is not the same state as the SecondPage based on my test code? Anyways appreciate any advice for my case.

Related

The method '[]' was called on null exception flutter

I am using an API call to get some data. And I am calling that method in the init state so that it fetches the data as soon as the widget is added into the tree.
I am using that data in a Text widget, but it shows an error that "The method '[]' was called on null".
Here is the code for your reference:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class Profile extends StatefulWidget {
final String username;
Profile({this.username});
#override
_ProfileState createState() => _ProfileState();
}
class _ProfileState extends State<Profile> {
var data;
void getData() async {
http.Response response = await http
.get('http://codeforces.com/api/user.info?handles=${widget.username}');
if (response.statusCode == 200) {
data = jsonDecode(response.body);
} else {
print('Something went wrong.');
}
}
#override
void initState() {
super.initState();
getData();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Column(
children: [
Text('Handle - ${widget.username}'),
Text(data['result'][0]['firstName']), /*This line causes error*/
],
),
),
);
}
}
The question is why the data field is null even though it is being called in the init state and the API call is also successful?
I have marked the line which gives the error using a comment.
I think it could be happen because in the time that the request are being done, your Profile widget was already rendered, and you're not using setState in your getData() function, so, the widget will not render again.
In this case, you can do some changes in your code:
Use a setState in your getData method
void getData() async {
http.Response response = await http
.get('http://codeforces.com/api/user.info?handles=${widget.username}');
if (response.statusCode == 200) {
setState(() {
data = jsonDecode(response.body);
});
} else {
print('Something went wrong.');
}
}
Set a default value to your Text widget
Text(data != null ? data['result'][0]['firstName'] : ''),
not the best and efficient solution but if you just want to show some data like username or email you can do it like this:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
void main() => runApp(Profile());
class Profile extends StatefulWidget {
final String username;
Profile({this.username});
#override
_ProfileState createState() => _ProfileState();
}
class _ProfileState extends State<Profile> {
var email;
void getData() async {
http.Response response = await http.get(Uri.parse(
'http://codeforces.com/api/user.info?handles=${widget.username}'));
if (response.statusCode == 200) {
setState(() {
Map<String, dynamic> map = json.decode(response.body);
email = map['result'][0]['email'];
print(email);
});
} else {
print('Something went wrong.');
}
}
#override
void initState() {
super.initState();
getData();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: email == null
? CircularProgressIndicator()
: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('Handle - ${widget.username}'),
Text(email),
],
),
),
),
);
}
}
output:

State not changing in flutter for progress indicator [duplicate]

This question already has answers here:
What is a Future and how do I use it?
(6 answers)
Closed 2 years ago.
I am trying to parse an api in flutter. the asynchronous request delays the response which is shown by the progress indicator. but the progress indicator does not fade away after the response is fetched. it remains still on the screen.
how to change the state of the progress loader to listview.builder when the response is fetched?
this is my code.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'users.dart';
import 'package:http/http.dart' as http;
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool isLoading = false;
List<Users> _users;
Future<List<Users>> getUsers() async {
var response = await http.get(Uri.encodeFull('http://jsonplaceholder.typicode.com/users'));
_users = usersFromJson(response.body);
isLoading = true;
}
#override
void initState() {
setState(() {
getUsers();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(isLoading ? "Parsed Json" : "Loading..."),
backgroundColor: Colors.deepPurple,
),
body: isLoading ? ListView.builder(
itemCount: _users.length,
itemBuilder: (BuildContext context,int index){
return ListTile(
leading: Image.network('https://images.unsplash.com/photo-1488426862026-3ee34a7d66df?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=334&q=80'),
title: Text(_users[index].name),
);
}) : Center(child: CircularProgressIndicator()),
);
}
}
Working code, try this
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool isLoading = false;
List<Users> _userList = [];
Future<void> getUsers() async {
isLoading = true;
var response = await http
.get(Uri.encodeFull('http://jsonplaceholder.typicode.com/users'));
var jsonResponse = jsonDecode(response.body);
// print(jsonResponse);
jsonResponse.forEach((data) {
Users user = Users.fromMap(data);
print(user);
_userList.add(user);
});
isLoading = false;
}
#override
void initState() {
getUsers().then((value) => setState(() {}));
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(isLoading ? "Loading..." : "Parsed Json"),
backgroundColor: Colors.deepPurple,
),
body: isLoading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: _userList.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(_userList[index].name),
);
}));
}
}
class Users {
int id;
String name;
String username;
Users({
this.id,
this.name,
this.username,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'username': username,
};
}
factory Users.fromMap(Map<String, dynamic> map) {
if (map == null) return null;
return Users(
id: map['id'],
name: map['name'],
username: map['username'],
);
}
String toJson() => json.encode(toMap());
factory Users.fromJson(String source) => Users.fromMap(json.decode(source));
#override
String toString() => 'Users(id: $id, name: $name, username: $username)';
}
Check your _user != null instead of bool then show list otherwise indicator
void initState () method Called when this object is inserted into the tree. The framework will call this method exactly once for each State object it creates. so there is no need for setState in initState() method.
try set setState after the response, so
#override
void initState() {
super.initState();
getUsers();
}
Future<List<Users>> getUsers() async {
isLoading = false;
var response = await http.get(Uri.encodeFull('http://jsonplaceholder.typicode.com/users'));
_users = usersFromJson(response.body);
print(_users);
setState(() {
isLoading = true;
});
return _users;
}

Deep Link after GitHub Authentication in Flutter

I want my Login Screen to be shown if a user isn't logged in and the MainPage to be shown if the user is logged in from the device. For GitHub authentication, the browser is being opened and once the authentication is successful, I want the user to be redirected to the MainPage and not the login screen, but with my code, the user is redirected again to the Login Screen. The code for the login_screen.dart is,
import 'package:flutter/material.dart';
import 'package:githubapp/custom_widgets/custom_login_page.dart';
import 'package:githubapp/screens/user_initial_screen.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:async';
import 'package:uni_links/uni_links.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
class LoginPage extends StatefulWidget {
#override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
StreamSubscription _subs;
#override
void initState() {
_initDeepLinkListener();
super.initState();
}
#override
void dispose() {
_disposeDeepLinkListener();
super.dispose();
}
void _initDeepLinkListener() async {
_subs = getLinksStream().listen((String link) {
_checkDeepLink(link);
}, cancelOnError: true);
}
void _checkDeepLink(String link) {
if (link != null) {
String code = link.substring(link.indexOf(RegExp('code=')) + 5);
loginWithGitHub(code)
.then((firebaseUser) {
print("LOGGED IN AS: " + firebaseUser.displayName);
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (BuildContext context){
return MainPage();
}));
}).catchError((e) {
print("LOGIN ERROR: " + e.toString());
});
}
}
void _disposeDeepLinkListener() {
if (_subs != null) {
_subs.cancel();
_subs = null;
}
}
#override
Widget build(BuildContext context) {
return customLoginPage(context);
}
}
const String GITHUB_CLIENT_ID = "My Client Id";
const String GITHUB_CLIENT_SECRET = "My Client Secret";
void onClickGitHubLoginButton() async {
const String url = "https://github.com/login/oauth/authorize" +
"?client_id=" + GITHUB_CLIENT_ID +
"&scope=public_repo%20read:user%20user:email";
if (await canLaunch(url)) {
await launch(
url,
forceSafariVC: false,
forceWebView: false,
);
} else {
print("CANNOT LAUNCH THIS URL!");
}
}
Future<FirebaseUser> loginWithGitHub(String code) async {
//ACCESS TOKEN REQUEST
final response = await http.post(
"https://github.com/login/oauth/access_token",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: jsonEncode(GitHubLoginRequest(
clientId: GITHUB_CLIENT_ID,
clientSecret: GITHUB_CLIENT_SECRET,
code: code,
)),
);
GitHubLoginResponse loginResponse =
GitHubLoginResponse.fromJson(json.decode(response.body));
final AuthCredential credential = GithubAuthProvider.getCredential(
token: loginResponse.accessToken,
);
final FirebaseUser user = (await FirebaseAuth.instance.signInWithCredential(credential)).user;
return user;
}
class GitHubLoginRequest {
String clientId;
String clientSecret;
String code;
GitHubLoginRequest({this.clientId, this.clientSecret, this.code});
dynamic toJson() => {
"client_id": clientId,
"client_secret": clientSecret,
"code": code,
};
}
class GitHubLoginResponse {
String accessToken;
String tokenType;
String scope;
GitHubLoginResponse({this.accessToken, this.tokenType, this.scope});
factory GitHubLoginResponse.fromJson(Map<String, dynamic> json) =>
GitHubLoginResponse(
accessToken: json["access_token"],
tokenType: json["token_type"],
scope: json["scope"],
);
}
The code on the main.dart is,
import 'package:flutter/material.dart';
import 'package:githubapp/auth/login_screen.dart';
import 'package:githubapp/screens/user_initial_screen.dart';
import 'package:githubapp/screens/user_profile_page.dart';
import 'package:firebase_auth/firebase_auth.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
// This widget is the root of your application.
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String routeToMainPage = 'mainPage';
String routeToLoginPage = 'loginPage';
String routeInitial = '';
getCurrentUser() async {
FirebaseUser user = await FirebaseAuth.instance.currentUser();
return user;
}
#override
void initState() {
// TODO: implement initState
super.initState();
if(getCurrentUser() != null){
routeInitial = routeToMainPage;
} else {
routeInitial = routeToLoginPage;
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'GitHub',
initialRoute: '/${routeInitial}',
routes: {
'/mainPage': (context) => MainPage(),
'/loginPage': (context) => LoginPage(),
'/userprofile': (context) => UserProfilePage(),
},
theme: ThemeData(
primarySwatch: Colors.blue,
),
);
}
}
The code where the MainPage() function is present is,
import 'package:flutter/material.dart';
import 'package:githubapp/custom_widgets/custom_appbar.dart';
class MainPage extends StatefulWidget {
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
#override
Widget build(BuildContext context) {
return GestureDetector(
child: Scaffold(
appBar: customAppBar(context),
// body: ,
),
onTap: () {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.unfocus();
}
},
);
}
}
For the Authorization callback URL, which we have to enter while getting the ClientId and ClientSecret from GitHub is,
appname://auth?code=auth/
you should navigate user to mainPage after login was successfully
void _checkDeepLink(String link) {
if (link != null) {
String code = link.substring(link.indexOf(RegExp('code=')) + 5);
loginWithGitHub(code)
.then((firebaseUser) {
print("LOGGED IN AS: " + firebaseUser.displayName);
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (BuildContext context){
return MainPage();
}));
}).catchError((e) {
print("LOGIN ERROR: " + e.toString());
});
}
}

How passing widget dynamically in class constructor parameter

This is my first question in Stackoverflow.
I'm trying to call a widget in another StatefulWidget when we passing it as class constuctor parameter.
So i have this custom widget :
class MyTextWidget extends StatelessWidget {
final String text;
MyTextWidget({ #required this.text });
#override
Widget build(BuildContext context) {
return Text(this.text);
}
}
i want to use in another widget like this (with pass in class constructor parameter)
import 'package:myproject/MyTextWidget.dart';
.
.
.
// Passing widget like this
child: Example(display: MyTextWidget,text: this.widget.text);
.
.
.
// And use this widget like this
this.widget.display(text: this.widget.text);
this is my first code :
class Example extends StatefulWidget {
final Widget display;
final String text;
Example({ #required this.display, #required this.text });
#override
State<StatefulWidget> createState() {
return ExampleState();
}
}
class ExampleState extends State<Example> {
#override
Widget build(BuildContext context) {
return this.widget.display(text: this.widget.text); // here we have error
}
}
but i have this error
The expression doesn't evaluate to a function, so it can't be invoked.dart(invocation_of_non_function_expression)
I realize i need to make a typedef for this problem so i improve code like this:
typedef Widget DisplayType({ #required String text });
class Example extends StatefulWidget {
final DisplayType display;
final String text;
Example({ #required this.display, #required this.text });
#override
State<StatefulWidget> createState() {
return ExampleState();
}
}
class ExampleState extends State<Example> {
#override
Widget build(BuildContext context) {
return this.widget.display(text: this.widget.text);
}
}
Our error is fixed but when i want to pass MyTextWidget in another widget:
child: Example(display: MyTextWidget,text: this.widget.text);
Finally i get this error:
The argument type 'Type' can't be assigned to the parameter type 'Widget Function({String text})'.dart(argument_type_not_assignable)
how can i fix this problem?
I think you want display to be a builder function that returns a MyTextWidget.
import 'package:flutter/material.dart';
Future<void> main() async {
return runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(final BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(final BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Home")),
body: Center(
child: Example(
display: ({#required final String text}) {
assert(text != null);
return MyTextWidget(text: text);
},
text: "some text",
),
),
);
}
}
class MyTextWidget extends StatelessWidget {
final String text;
MyTextWidget({#required this.text}) : assert(text != null);
#override
Widget build(final BuildContext context) {
return Text(this.text);
}
}
typedef Widget DisplayType({#required final String text});
class Example extends StatefulWidget {
final DisplayType display;
final String text;
Example({
#required this.display,
#required this.text,
}) : assert(display != null),
assert(text != null);
#override
State<StatefulWidget> createState() {
return ExampleState();
}
}
class ExampleState extends State<Example> {
#override
Widget build(final BuildContext context) {
return this.widget.display(text: this.widget.text);
}
}
You don't need to pass the down the widget, just import it in the class that you want to use it.
When you want to instantiate it, you need to use parenthesis MyTextWidget().
You don't need to use this when calling the parameters passed to a Stateful Widget, just widget.text will work.
You seem to be having some misunderstandings about Flutter. There are very rare occasions where you need to use this.

pass data over the tree flutter

I search a simple solution to pass simple string variable over the tree widget, inherite function is to complicated to me.
to resume :
I tried to pass data to page 1 to body of page 2
class page1 extends StatefulWidget
string textvariable
onPressed: () {
Navigator.push(
context,
new MaterialPageRoute(builder: (context) => new page2(
textvariable: this,
)),
);
},
in my next page I have a StatelessWidget
class page2 extends StatelessWidget {
var textvariable;
page2({this.textvariable});
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(title: const Text("Localisation")),
body: testwidget (textvariable),
);
}
}
I try to passe textvariable in the widget 3 who is a stateful
class testwidget extends StatefulWidget
...
new Text(
"$textvariable",
),