How to simulate disposing widget in flutter test? - flutter

This method will call the init function inside the stateless widget.
But how to emulate the call to dispose function?
var widget = StatelessWidgetExample();
await tester.pumpWidget(widget);
I also tried to emulate the removal from the tree.
await tester.pumpWidget(widget);
await tester.pumpWidget(Container());
but it didn't work

Did it like this
var key2 = Key('a');
var testStateful = _TestStateful(
key: key2,
child: TestInitDispose(),
);
await tester.pumpWidget(testStateful);
/// will call init
var state = tester.firstState<__TestStatefulState>(find.byKey(key2));
state.remove();
await tester.pump();
/// will call dispose
});
...
class _TestStateful extends StatefulWidget {
final Widget child;
const _TestStateful({Key? key, required this.child}) : super(key: key);
#override
__TestStatefulState createState() => __TestStatefulState();
}
class __TestStatefulState extends State<_TestStateful> {
bool showChild = true;
void remove() {
setState(() {
showChild = false;
});
}
#override
Widget build(BuildContext context) {
return showChild ? widget.child : Container();
}
}

You could use a StreamBuilder and replace YourWidget with another widget, then the dispose method for YourWidget is called.
void main() {
late StreamController<Widget> widgetStreamController;
setUp(() async {
widgetStreamController = StreamController<Widget>();
});
tearDown(() async {
await widgetStreamController.close();
});
Widget buildApp() {
return MaterialApp(
home: StreamBuilder<Widget>(
stream: widgetStreamController.stream,
builder: (context, snapshot) {
return snapshot.data ?? Container();
},
),
);
}
testWidgets('dispose widget', (tester) async {
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
widgetStreamController.add(YourWidget());
await tester.pumpAndSettle();
// todo: check here if YourWidget is displayed
widgetStreamController.add(AnotherWidget());
await tester.pumpAndSettle();
// todo: check here if dispose was called
});
}

This worked for me and is relatively simple.
var widget = StatelessWidgetExample();
await tester.pumpWidget(widget);
await tester.pumpAndSettle();
await tester.pumpWidget(Container());
await tester.pumpAndSettle();

Related

Bad state error when fetching data from firebase

I’m fetching data from firebase but i have this error
Unhandled Exception: Bad state: field does not exist within the DocumentSnapshotPlatform
the fields are correct and there is no difference i checked them
this is my method
static final List<ProductModels> _products = [];
Future<void> fetchData() async {
await FirebaseFirestore.instance
.collection('products')
.get()
.then((QuerySnapshot productsSnapshot) {
for (var element in productsSnapshot.docs) {
_products.insert(
0,
ProductModels(
id: element.get('id'),
title: element.get('title'),
imageUrl: element.get('imageUrl'),
productcat: element.get('productCategoryName'),
price: element.get('price'),
salePrice: element.get('salePrice'),
isOnSale: element.get('isOnSale'),
size: element.get('Size'),
color: element.get('Color'),
));
}
});
notifyListeners();
}
and i call it here
class FetchScreen extends StatefulWidget {
const FetchScreen({Key? key}) : super(key: key);
#override
State<FetchScreen> createState() => _FetchScreenState();
}
class _FetchScreenState extends State<FetchScreen> {
#override
void initState() {
Future.delayed(Duration(microseconds: 5),() async{
final productsProvider = Provider.of<ProductProvider>(context,listen: false);
await productsProvider.fetchData();
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context)=> BtmNavBarScreen()));
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Container();
}
}
there is no difference between the firebase and the code i checked each one

Get value from SharedPreferences without future builder

I need to get one stored value from shared preferences and put it into text widget. How can I do this without a future builder?
_currPage() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int page = prefs.getInt('currPage') ?? 0;
return page;
}
class _AllTasksPageState extends State<AllTasksPage> {
#override
Widget build(BuildContext context) {
...
Text(_currPage()); //not working
...
}
}
int page = 0;
#override
void initState() {
super.initState();
readData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('$page'),
),
);
}
void readData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
if (prefs.getInt('currPage') == null)
setState(() => page = 0);
else
setState(() => page = prefs.getInt('currPage')!);
}
create a helper class just for shared preferences
import 'package:shared_preferences/shared_preferences.dart';
class SPHelper {
SPHelper._();
static SPHelper sp = SPHelper._();
SharedPreferences? prefs;
Future<void> initSharedPreferences() async {
prefs = await SharedPreferences.getInstance();
}
Future<void> save(String name, String value) async {
await prefs!.setString(name, value);
}
String? get(String key) {
return prefs!.getString(key);
}
Future<bool> delete(String key) async {
return await prefs!.remove(key);
}
}
in your main function add
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await SPHelper.sp.initSharedPreferences();
...
runApp(MyApp());
...
}
then to get your data just write
SPHelper.sp.get("YOUR_KEY")
and to store your data just write
SPHelper.sp.save("YOUR_KEY","YOUR_VALUE")
This is the best way to use shared preference.
I hope that's will help you in your problem.
The simplest method is using a SharedPreferences provider:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(
MultiProvider(
providers: [
Provider.value(value: await SharedPreferences.getInstance()),
],
child: MaterialApp(
home: AllTasksPage(),
),
),
);
}
class AllTasksPage extends StatelessWidget {
const AllTasksPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final page = context.read<SharedPreferences>().getInt('currPage') ?? 0;
return Scaffold(body: Text('$page'));
}
}
If you don't want to use a future builder, the other solution is if you have a variable that tells you that are you still waiting/loading data and if yes, show a waiting screen:
class _AllTasksPageState extends State<AllTasksPage> {
bool _loading = true;
String? textValue; // String textValue = "";
#override
initState() {
super.initState();
setTextValue();
}
setTextValue() {
SharedPreferences prefs = await SharedPreferences.getInstance();
int page = prefs.getInt('currPage') ?? 0;
setState(() {
textValue = "$page";
_loading = false;
});
}
// then in the build method
#override
Widget build(BuildContext context) {
return _loading ? CircularProgressIndicator() : actualScreen();
}
}

Riverpod Alternative To SetUpLocator

I want to call my Remote Config Instance on Flutter App StartUp
I have set up Riverpod as follows
class ConfigService {
// Will Initialise here
final RemoteConfig _remoteConfig;
ConfigService(this._remoteConfig);
Future<void> initialise() async {
...// Will fetchAndActivate
}
final remoteConfigProvider = Provider<RemoteConfig>((ref) {
return RemoteConfig.instance;
});
final configProvider = Provider<ConfigService>((ref) {
final _config = ref.read(remoteConfigProvider);
return ConfigService(_config);
});
I would want to call it in the main after
...
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
... here
But this can't be done because one needs a Reader and the ProviderScope is below this level
How do I call this provider in my main ?
The short answer is you can't. What you should do is call the Provider within the ProviderScope.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, ScopedReader watch) {
final config = watch(configProvider);
return Container();
}
}

Return async value to initState()

I've the below code that is working fine, reading the csv data from url and printing the output:
import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:io';
import 'dart:async';
import 'package:csv/csv.dart';
void fetchUserData() async {
final request = await HttpClient().getUrl(Uri.parse(
'https://docs.google.com/spreadsheets/d/e/2PACX-1vQvf9tp4-fETDJbC-HRmRKvVFAXEAGO4lrYPpVeiYkB6nqqXdSs3CjX0eBMvjIoEeX9_qU6K2RWmzVk/pub?gid=0&single=true&output=csv'));
final response = await request.close();
List<List<dynamic>> rowsAsListOfValues;
await for (final csvString in response.transform(const Utf8Decoder())) {
rowsAsListOfValues =
const CsvToListConverter().convert(csvString);
}
print(rowsAsListOfValues);
}
class _MyHomePageState extends State<MyHomePage> {
#override
void initState() {
super.initState();
fetchUserData();
}
#override
Widget build(BuildContext context) { // ... // }
}
Instead of getting the output printed, I need it to be returned into a variable, which I can display in y widget, I tried to do it as below:
Future<List<List<dynamic>>> fetchUserData() async { /// change
final request = await HttpClient().getUrl(Uri.parse(
'https://docs.google.com/spreadsheets/d/e/2PACX-1vQvf9tp4-fETDJbC-HRmRKvVFAXEAGO4lrYPpVeiYkB6nqqXdSs3CjX0eBMvjIoEeX9_qU6K2RWmzVk/pub?gid=0&single=true&output=csv'));
final response = await request.close();
List<List<dynamic>> rowsAsListOfValues;
await for (final csvString in response.transform(const Utf8Decoder())) {
rowsAsListOfValues =
const CsvToListConverter().convert(csvString);
}
return rowsAsListOfValues; /// change
}
class _MyHomePageState extends State<MyHomePage> {
var rowsAsListOfValues; /// new
#override
void initState() {
super.initState();
rowsAsListOfValues = fetchUserData(); /// new
print(rowsAsListOfValues); /// new
}
#override
Widget build(BuildContext context) { // ... // }
}
But I got the output as I/flutter ( 7505): Instance of 'Future<List<List<dynamic>>>'
How can I fix it?
You need to switch from initState to didChangeDependency in this case. Because you need to await some process and you cant wait in initState. However you can wait like this
#override
void didChangeDependencies() async {
super.didChangeDependencies();
rowsAsListOfValues = await fetchUserData();
super.setState(() {}); // to update widget data
/// new
print(rowsAsListOfValues);
}
And this is the result
I/flutter (24313): [[vranches], [Dammam, 2], [Khobar, 3]]
You can wrap your code with Future.delayed() as given below.
#override
void initState() {
super.initState();
Future.delayed(Duration.zero,()async{
rowsAsListOfValues =await fetchUserData();
setState(() {});
print(rowsAsListOfValues); // this return correct value
});
print(rowsAsListOfValues); // this return null
}
Full Code
import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:io';
import 'dart:async';
import 'package:csv/csv.dart';
void main() {
runApp(App());
}
class App extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomePage(title: 'Flutter Demo Home Page'),
);
}
}
class HomePage extends StatefulWidget {
HomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_AppState createState() => _AppState();
}
Future<List<List<dynamic>>> fetchUserData() async {
final request = await HttpClient().getUrl(Uri.parse(
'https://docs.google.com/spreadsheets/d/e/2PACX-1vQvf9tp4-fETDJbC-HRmRKvVFAXEAGO4lrYPpVeiYkB6nqqXdSs3CjX0eBMvjIoEeX9_qU6K2RWmzVk/pub?gid=0&single=true&output=csv'));
final response = await request.close();
List<List<dynamic>> rowsAsListOfValues;
await for (final csvString in response.transform(const Utf8Decoder())) {
rowsAsListOfValues =
const CsvToListConverter().convert(csvString);
}
return rowsAsListOfValues;
}
class _AppState extends State<HomePage> {
var rowsAsListOfValues;
#override
void initState() {
super.initState();
Future.delayed(Duration.zero, () async {
rowsAsListOfValues = await fetchUserData();
setState(() {});
print(rowsAsListOfValues);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'$rowsAsListOfValues',
),
],
),
),
);
}
}
The initState method is synchronous, and does not support async. I recommend the use of FutureBuilder, but you can also move the code to an async function.
FutureBuilder
import 'package:flutter/material.dart' show
Widget, FutureBuilder, AsyncSnapshot
;
class _MyHomePageState extends State<MyHomePage> {
static Future<void> fetchUserData() {
return Future().delayed(
Duration(seconds: 10),
() => 'loaded'
);
}
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: Future.wait([
fetchUserData()
]),
builder: (
BuildContext context,
AsyncSnapshot snapshot
) {
if (snapshot.hasData) {
return Text(snapshot.data);
}
return Text('loading...');
}
);
}
}
Async function
#override
void initState () {
super.initState();
(() async {
rowsAsListOfValues = await fetchUserData();
print(rowsAsListOfValues);
})();
}
OR
#override
void initState() {
super.initState();
initLoad();
}
void initLoad() async {
rowsAsListOfValues = await fetchUserData();
print(rowsAsListOfValues);
}
I feel more relaxed when using then() with async functions. You can try this:
fetchUserData().then((value) {
setState(() {
rowsAsListOfValues = value;
});
});
Or you can use await like this.
#override
void initState() async {
super.initState();
rowsAsListOfValues = await fetchUserData();
print(rowsAsListOfValues);
}

Unhandled Exception: A class was used after being disposed. - Flutter

I'm getting this error
E/flutter ( 9610): [ERROR:flutter/lib/ui/ui_dart_state.cc(166)] Unhandled Exception: A CustRegViewModel was used after being disposed.
E/flutter ( 9610): Once you have called dispose() on a CustRegViewModel, it can no longer be used.
I have a View named CustRegView where I take a phone number form the user and send it to ViewModel named CustRegViewModel to authenticate which returns bool based on its authentication status.
class CustRegView extends StatefulWidget {
#override
_CustRegViewState createState() => _CustRegViewState();
}
class CustRegView extends StatelessWidget{
final TextEditingController _controller = TextEditingController();
#override
Widget build(BuildContext context) {
final deviceSize = MediaQuery.of(context).size;
return BaseView<CustRegViewModel>(
builder: (context, model, child) => Scaffold(
...<some code>
FlatButton (
onPressed: () async {
var registerSuccess = await model.register( _controller.text, context);
// ^^^^^ HERE, IN ABOVE LINE, I'M GETTING AN ERROR ^^^^^^
if (registerSuccess) {
Navigator.pushNamed(context, 'newScreen');
} else {
UIHelper().showErrorButtomSheet(context, model.errorMessage);
}
)
}
CustRegViewModel looks like this
class CustRegViewModel extends BaseViewModel {
final AuthService _authService = locator<AuthService>();
final DialogService _dialogService = locator<DialogService>();
dynamic newUserResult;
dynamic verifyResult;
Future<bool> register(String phoneNo, BuildContext context) async {
await verifyPhone;
return verifyResult ? true : false; // From here it returns true
}
Future<void> verifyPhone(phoneNo) async {
await FirebaseAuth.instance.verifyPhoneNumber(
phoneNumber: updatedPhoneNo,
timeout: Duration(seconds: 50),
verificationCompleted: (AuthCredential authCred) async {...... <some code>
verificationFailed: (AuthException authException) {...... <some code>
codeSent: (String verID, [int forceCodeResend]) async {...... <some code>
codeAutoRetrievalTimeout: (String verID) {...
).catchError((error) {...... <some code>
}
}
BaseView looks like this
class BaseView<T extends BaseViewModel> extends StatefulWidget {
final Widget Function(BuildContext context, T model, Widget child) builder;
final Function(T) onModelReady;
BaseView({this.builder, this.onModelReady});
#override
_BaseViewState<T> createState() => _BaseViewState<T>();
}
class _BaseViewState<T extends BaseViewModel> extends State<BaseView<T>> {
T model = locator<T>();
#override
void initState() {
if (widget.onModelReady != null) {
widget.onModelReady(model);
}
super.initState();
}
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<T>(
create: (context) => model,
child: Consumer<T>(builder: widget.builder),
);
}
}
BaseViewModel looks like this
class BaseViewModel extends ChangeNotifier {
ViewState _state = ViewState.Idle;
ViewState get state => _state;
void setState(ViewState viewState) {
_state = viewState;
notifyListeners();
}
}
You may have used RegisterLazySingleton instead of Factory in your depencie injection.