I am trying to learn Flutter and BLoC pattern. So, I created a simple counter app. Initially count value is 0 and it increases/decreases as respective buttons are pressed. The initial value(zero) is sent to the initial state as follows.
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterInitialState(0));
The Counter app worked as expected, but whenever I restart the app count starts from zero. Now I wish to start the count from where I left. I read about SharedPreferences and could successfully save the current value. But I can't load the value and send it to CounterInitialState()(The argument type 'Future' can't be assigned to the parameter type 'int'). How can I achieve this?
My counter_bloc.dart looks like this;
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:sampleOne/config/config.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'counter_event.dart';
part 'counter_state.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterInitialState(0));
#override
Stream<CounterState> mapEventToState(
CounterEvent event,
) async* {
if (event is CounterIncrementEvent) {
yield* _mapIncrementEvent(event.countVal);
}
if (event is CounterDecrementEvent) {
yield* _mapDecrementEvent(event.countVal);
}
}
}
Stream<CounterState> _mapIncrementEvent(val) async* {
await saveData(val);
yield CounterIncrementState(val + 1);
}
Stream<CounterState> _mapDecrementEvent(val) async* {
await saveData(val);
yield CounterDecrementState(val + 1);
}
Future<bool> saveData(int val) async {
SharedPreferences pref = await SharedPreferences.getInstance();
return pref.setInt('key', val + 1);
}
Future<int> loadData() async {
SharedPreferences pref = await SharedPreferences.getInstance();
return pref.getInt('key') ?? 0;
}
Please help.
you need to wrap your widget with a FutureBuilder widget.
for example
FutureBuilder<int>(
future: _bloc.loadData(),
initalData: 0,
builder: (context, snapshot) {
return Text(snapshot.data.toString());
}
)
Where do you call loadData()?
Maybe you need to to put an await before like this:
Future<int> data = loadData();
int data = await loadData();
Your function is asynchronous so it returns a Future, add await to get an integer
Related
I'm trying to test a widget that receives and displays some data. This widget uses a controller. In the constructor I start receiving data, after which I execute the parser in a separate isolate. During the tests, the function passed to the compute is not executed until the end, and the widget state does not change. In fact, the structure of the widget looks a little more complicated, but I wrote smaller widget that saves my problem:
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:rxdart/rxdart.dart';
class TestObj {
int id;
String name;
String number;
TestObj(this.id, this.name, this.number);
static List<TestObj> jsonListParser(String data) {
List mapObjs = json.decode(data) as List;
if (mapObjs.isEmpty) return [];
List<TestObj> testObjs = [];
for (final Map mapObj in mapObjs as List<Map>)
testObjs.add(
TestObj(
mapObj['id'] as int,
mapObj['name'] as String,
mapObj['number'] as String,
),
);
return testObjs;
}
}
class TestController {
final BehaviorSubject<List<TestObj>> testSubj;
final String responseBody =
'[{"id":2,"number":"1","name":"Объект 1"},{"id":1,"number":"2","name":"Объект 2"}]';
TestController(this.testSubj) {
getData(responseBody, testSubj);
}
Future<void> getData(
String responseBody, BehaviorSubject<List<TestObj>> testSubj) async {
List<TestObj> data = await compute(TestObj.jsonListParser, responseBody);
testSubj.sink.add(data);
}
}
class TestWidget extends StatelessWidget {
final BehaviorSubject<List<TestObj>> testSubj;
final TestController controller;
const TestWidget(this.testSubj, this.controller);
#override
Widget build(BuildContext context) {
return StreamBuilder<List<TestObj>>(
stream: testSubj.stream,
builder: (context, snapshot) => snapshot.data == null
? const CircularProgressIndicator()
: ListView.builder(
itemBuilder: (context, index) => Text(snapshot.data[index].name),
),
);
}
}
void main() {
testWidgets('example test', (tester) async {
final BehaviorSubject<List<TestObj>> testSubj =
BehaviorSubject.seeded(null);
final TestController testController = TestController(testSubj);
await tester.pumpWidget(
TestWidget(testSubj, testController),
);
expect(find.byType(CircularProgressIndicator), findsNothing);
});
}
I have tried using tester.pump, tester.pumpAndSettle (crashed by timeout) and tester.runAsync, but so far without success. What are the solutions of this problem?
As indicated in runAsync docs, it is not supported to have isolates/compute in tests that are proceeded by pump().
To make a self-contained solution, check if you run in test environment or not in your code and skip isolates when you run in a test:
import 'dart:io';
if (!kIsWeb && Platform.environment.containsKey('FLUTTER_TEST')) {
calc()
} else {
calcInIsolate()
}
I'm new to Flutter, (comming from web and especially JS/VueJS)
I'm have a db in firebase that has a collection called edito and inside, i have different artist with a specific Id to call Deezer Api with it.
So what i want to do is first called my db and get the Id for each of artist and then put this id in a function as parameter to complete the url.
I did 2 Future function, one to call the db and one to call the api.
But i don't understand how to use one with the others in the build to get a listview with the information of the api of deezer for each data.
i'm getting the list but it's stuck in and endless loop.
All of my app will be on this nested function, is it possible to do this and call it in any widget that i want ?
here is my code, thanks
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class GetAlbum extends StatefulWidget {
#override
_GetAlbumState createState() => _GetAlbumState();
}
class _GetAlbumState extends State<GetAlbum> {
Map mapResponse;
Future<QuerySnapshot> getDocument() async{
return FirebaseFirestore.instance.collection("edito").get();
}
Future<dynamic> fetchData(id) async{
http.Response response;
response = await http.get('https://api.deezer.com/album/' + id);
if(response.statusCode == 200){
setState(() {
mapResponse = json.decode(response.body);
});
}
}
Future<dynamic> getDocut;
Future<dynamic> getArtist;
#override
void initState() {
getDocut = getDocument();
getArtist = fetchData(null);
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<QuerySnapshot>(
future : getDocut,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot){
if(!snapshot.hasData) {
return CircularProgressIndicator();
}else{
return new ListView(
children: snapshot.data.docs.map<Widget>((document){
print(document.data().length);
return FutureBuilder(
future: fetchData(document.data()['idDeezer'].toString()),
builder: (context, snapshot){
return Container(
child: mapResponse==null?Container(): Text(mapResponse['title'].toString(), style: TextStyle(fontSize: 30),),
);
}
);
}).toList(),
);
}
},
);
}
}
Here's a simplified example of making two linked Future calls where the 2nd depends on data from the first, and using the results in a FutureBuilder:
import 'package:flutter/material.dart';
class FutureBuilder2StatefulPage extends StatefulWidget {
#override
_FutureBuilder2StatefulPageState createState() => _FutureBuilder2StatefulPageState();
}
class _FutureBuilder2StatefulPageState extends State<FutureBuilder2StatefulPage> {
Future<String> _slowData;
#override
void initState() {
super.initState();
_slowData = getAllSlowData(); // combined async calls into one future
}
// linked async calls
Future<String> getAllSlowData() async {
int id = await loadId(); // make 1st async call for id
return loadMoreData(id: id); // use id in 2nd async call
}
Future<int> loadId() async {
int _id = await Future.delayed(Duration(seconds: 2), () => 42);
print('loadId() completed with: $_id'); // debugging
return _id;
}
Future<String> loadMoreData({int id}) async {
return await Future.delayed(Duration(seconds: 2), () => 'Retrieved data for id:$id');
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('FutureBldr Stateful'),
),
body: FutureBuilder<String>(
future: _slowData,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Center(child: Text(snapshot.data));
}
return Center(child: Text('Loading...'));
},
),
);
}
}
This avoids having to nest the FutureBuilder which may be error prone.
And calling future methods directly from a FutureBuilder is not recommended since the call could be made many times if its containing widget is rebuilt (which can happen a lot).
I tried to add firebase in the first one but i get null for the id in the get AllSlowDAta but i got it right with the Future.delayed.
// linked async calls
Future<String> getAllSlowData() async {
String id = await loadId(); // make 1st async call for id
return loadMoreData(id: id); // use id in 2nd async call
}
Future<dynamic> loadId() async {
//return await Future.delayed(Duration(seconds: 2), () => '302127');
await FirebaseFirestore.instance.collection("edito")
.get()
.then((QuerySnapshot querySnapshot) {
querySnapshot.docs.forEach((doc) {
return doc.data()["idDeezer"];
});
});
}
Future<dynamic> loadMoreData({String id}) async {
http.Response response;
response = await http.get('https://api.deezer.com/album/' + id);
if(response.statusCode == 200){
setState(() {
return json.decode(response.body);
});
}
}
I'm trying to navigate to a screen where depending on 3 factors:
If the app has run before or not
If the user is null or not
If the email is verified or not.
import 'package:client/providers/auth_provider.dart';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../screens/home_screen.dart';
import '../screens/login_screen.dart';
class Wrapper extends StatefulWidget {
#override
_WrapperState createState() => _WrapperState();
}
class _WrapperState extends State<Wrapper> {
FirebaseAuth auth = FirebaseAuth.instance;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext ctx) {
return FutureBuilder(
future: hasAlreadyStarted(),
builder: (ctx, snapshot) {
return Provider.of<User>(ctx) != null &&
snapshot.data == true &&
AuthProvider().isEmailVerified
? HomeScreen()
: LoginScreen();
});
}
Future<bool> hasAlreadyStarted() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
try {
if (prefs.getBool("hasRun") == null) {
await AuthProvider().logout();
setState(() {
prefs.setBool("hasRun", true);
});
return true;
} else {
return true;
}
} catch (error) {
setState(() {
prefs.setBool("hasRun", null);
});
return false;
}
}
}
The screen works fine on it's own, but when i Navigate from a different screen it gives me the error.
Could not find the correct Provider<User> above this FutureBuilder<bool> Widget
Can you explain the error and give me a solution. Thanks :D
I just needed to wrap the entire MaterialApp with the StreamProvider
I am using this weather API which requires latitude and longitude to get weather. I am able to get latitude and longitude of my location(tried printing them and they are correct) but when I enter them in my API link, it shows weather of some other latitude and longitude. I have tried putting lats and longs of my location manually in the link, and it works absolutely fine. What is the issue?
import 'package:flutter/material.dart';
import 'package:clima/services/location.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
double key;
class LoadingScreen extends StatefulWidget {
#override
_LoadingScreenState createState() => _LoadingScreenState();
}
class _LoadingScreenState extends State<LoadingScreen> {
var lati;
var longi;
#override
void initState() {
super.initState();
}
void getData() async {
http.Response r = await http.get('http://api.weatherstack.c'
'om/current?access_key=41eb36e0c5f82e3ddce66ef01af877a1&query=$lati,$longi');
String s = r.body;
var data1 = jsonDecode(s)['location']['lat'];
var data2 = jsonDecode(s)['location']['lon'];
print(data1);
print(data2);
print(lati);
print(longi);
}
void getlocation() async {
location a = location();
await a.getclocation();
lati = a.lat;
longi = a.long;
}
Widget build(BuildContext context) {
getlocation();
getData();
return Scaffold();
}
}
Another class named location
import 'package:geolocator/geolocator.dart';
class location{
double lat;
double long;
Future<void> getclocation() async{
// this function is defined in another class
Position position = await Geolocator().getCurrentPosition(desiredAccuracy: LocationAccuracy.low);
lat=position.latitude;
long=position.longitude;
}
}
Outputs are
41.250
1.300
28.7041
77.1025
You need to wait until your operations actually complete.
For a broader explanation on how Future works, see What is a Future and how do I use it?
If you have an async function, make it return a Future<T>, even if it's a Future<void>. Then make sure you await it if you need it's result. In the UI, you cannot just stop and freeze everything, you need a FutureBuilder to play nice and show a little animation while you wait for the result.
class _LoadingScreenState extends State<LoadingScreen> {
Future<String> getDataFuture;
#override
void initState() {
super.initState();
getDataFuture = getData();
}
Future<String> getData() async {
final position = await Geolocator().getCurrentPosition(desiredAccuracy: LocationAccuracy.low);
final r = await http.get('http://api.weatherstack.com/current?access_key=41eb36e0c5f82e3ddce66ef01af877a1&query=$position.latitude,$position.longitude');
return r.body;
}
Widget build(BuildContext context) {
return FutureBuilder(
future: getDataFuture ,
builder: (context, snapshot) {
if(snapshot.hasData) {
return Text('Received response: ${snapshot.data}');
} else if(snapshot.hasError) {
return Text('Error: ${snapshot.error.toString()}');
} else {
return CircularProgressIndicator();
}
}
}
Im using the library Injectable for Dependency Injection in flutter but Im getting a error where I cannot use SharedPreferences.
Error:
Exception has occurred.
FlutterError (ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized.
If you're running an application and need to access the binary messenger before runApp() has been called (for example, during plugin initialization), then you need to explicitly call the WidgetsFlutterBinding.ensureInitialized() first.
If you're running a test, you can call the TestWidgetsFlutterBinding.ensureInitialized() as the first line in your test's main() method to initialize the binding.)
I've tryed creating a class and put #lazySingleton
Future<SharedPreferences> get prefs => SharedPreferences.getInstance();
and I tryed to put WidgetsFlutterBinding.ensureInitialized()
void main() {
WidgetsFlutterBinding.ensureInitialized();
configureInjection(Environment.prod);
runApp(MyApp());
}
you can pre-await the future in SharedPreference by annotating with #preResolve
#module
abstract class InjectionModule {
//injecting third party libraries
#preResolve
Future<SharedPreferences> get prefs => SharedPreferences.getInstance();
}
and on the configureInjection class
final GetIt getIt = GetIt.instance;
#injectableInit
Future<void> configureInjection(String env) async {
await $initGetIt(getIt, environment: env);
}
and also on the main class
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await configureInjection(Environment.prod);
runApp(MyApp());
}
To actually use:
final prefs = getIt<SharedPreferences>();
await prefs.setString('city', city);
NOT:
final module = getIt<InjectionModule>();
module.prefs.setString('test', test);
Note differences between SharedPreferences and InjectionModule.
Below is the way i got it to work, no guarantee it's the best method.
Await the configureInjection method in the main method.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await configureInjection(Env.prod);
runApp(App());
}
And wrap you app in FutureBuilder that makes use of getIt.allReady().
Widget build(context) {
return FutureBuilder(
future: getIt.allReady(),
builder: (context, snapshot) {
if (snapshot.hasData) {
// ... your app widgets
} else {
// ... some progress indicator widget
}
}
);
}
Helpfull links:
https://pub.dev/documentation/injectable/latest/#registering-asynchronous-injectables
https://pub.dev/packages/get_it#synchronizing-asynchronous-initialisations-of-singletons