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()
}
Related
I'm trying to better understand Futures in Flutter. In this example, my app makes an API call to get some information of type Future<String>. I'd like to display this information in a Text() widget. However, because my String is wrapped in a Future I'm unable to put this information in my Text() widget, and I'm not sure how to handle this without resorting to a FutureBuilder to create the small widget tree.
The following example uses a FutureBuilder and it works fine. Note that I've commented out the following line near the bottom:
Future<String> category = getData();
Is it possible to turn category into a String and simply drop this in my Text() widget?
import 'package:flutter/material.dart';
import 'cocktails.dart';
class CocktailScreen extends StatefulWidget {
const CocktailScreen({super.key});
#override
State<CocktailScreen> createState() => _CocktailScreenState();
}
class _CocktailScreenState extends State<CocktailScreen> {
#override
Widget build(BuildContext context) {
Cocktails cocktails = Cocktails();
Future<String> getData() async {
var data = await cocktails.getCocktailByName('margarita');
String category = data['drinks'][0]['strCategory'];
print('Category: ${data["drinks"][0]["strCategory"]}');
return category;
}
FutureBuilder categoryText = FutureBuilder(
initialData: '',
future: getData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
return Text(snapshot.data);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
}
return const CircularProgressIndicator();
},
);
//Future<String> category = getData();
return Center(
child: categoryText,
);
}
}
Here's my Cocktails class:
import 'networking.dart';
const apiKey = '1';
const apiUrl = 'https://www.thecocktaildb.com/api/json/v1/1/search.php';
class Cocktails {
Future<dynamic> getCocktailByName(String cocktailName) async {
NetworkHelper networkHelper =
NetworkHelper('$apiUrl?s=$cocktailName&apikey=$apiKey');
dynamic cocktailData = await networkHelper.getData();
return cocktailData;
}
}
And here's my NetworkHelper class:
import 'package:http/http.dart' as http;
import 'dart:convert';
class NetworkHelper {
NetworkHelper(this.url);
final String url;
Future<dynamic> getData() async {
http.Response response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
String data = response.body;
var decodedData = jsonDecode(data);
return decodedData;
} else {
//print('Error: ${response.statusCode}');
throw 'Sorry, there\'s a problem with the request';
}
}
}
Yes, you can achieve getting Future value and update the state based on in without using Using FutureBuilder, by calling the Future in the initState(), and using the then keyword, to update the state when the Future returns a snapshot.
class StatefuleWidget extends StatefulWidget {
const StatefuleWidget({super.key});
#override
State<StatefuleWidget> createState() => _StatefuleWidgetState();
}
class _StatefuleWidgetState extends State<StatefuleWidget> {
String? text;
Future<String> getData() async {
var data = await cocktails.getCocktailByName('margarita');
String category = data['drinks'][0]['strCategory'];
print('Category: ${data["drinks"][0]["strCategory"]}');
return category;
}
#override
void initState() {
super.initState();
getData().then((value) {
setState(() {
text = value;
});
});
}
#override
Widget build(BuildContext context) {
return Text(text ?? 'Loading');
}
}
here I made the text variable nullable, then in the implementation of the Text() widget I set to it a loading text as default value to be shown until it Future is done0
The best way is using FutureBuilder:
FutureBuilder categoryText = FutureBuilder<String>(
future: getData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Text('Loading....');
default:
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
var data = snapshot.data ?? '';
return Text(data);
}
}
},
),
but if you don't want to use FutureBuilder, first define a string variable like below and change your adasd to this :
String category = '';
Future<void> getData() async {
var data = await cocktails.getCocktailByName('margarita');
setState(() {
category = data['drinks'][0]['strCategory'];
});
}
then call it in initState :
#override
void initState() {
super.initState();
getData();
}
and use it like this:
#override
Widget build(BuildContext context) {
return Center(
child: Text(category),
);
}
remember define category and getData and cocktails out of build method not inside it.
I'm fairly new to Flutter providers. I use Riverpod.
I have a Future provider that provide some data from a JSON file - in the future it will be from a API response.
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/pokemon.dart';
final pokemonProvider = FutureProvider<List<Pokemon>>((ref) async {
var response =
await rootBundle.loadString('assets/mock_data/pokemons.json');
List<dynamic> data = jsonDecode(response);
return List<Pokemon>.from(data.map((i) => Pokemon.fromMap(i)));
});
I subscribe to with ref.watch in ConsumerState widgets, e.g.:
class PokemonsPage extends ConsumerStatefulWidget {
const PokemonsPage({Key? key}) : super(key: key);
#override
ConsumerState<PokemonsPage> createState() => _PokemonsPageState();
}
class _PokemonsPageState extends ConsumerState<PokemonsPage> {
#override
Widget build(BuildContext context) {
final AsyncValue<List<Pokemon>> pokemons =
ref.watch(pokemonProvider);
return pokemons.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
data: (pokemons) {
return Material(
child: ListView.builder(
itemCount: pokemons.length,
itemBuilder: (context, index) {
Pokemon pokemon = pokemons[index];
return ListTile(
title: Text(pokemon.name),
);
},
));
},
);
}
}
But in that case, what is the best practice to write/update data to the JSON file/API?
It seems providers are used for reading/providing data, not updating it, so I'm confused.
Should the same provider pokemonProvider be used for that? If yes, what is the FutureProvider method that should be used and how to call it? If not, what is the best practice?
I am new to riverpod too but I'll try to explain the approach we took.
The examples with FutureProviders calling to apis are a little bit misleading for me, because the provider only offers the content for a single api call, not access to the entire api.
To solve that, we found the Repository Pattern to be very useful. We use the provider to export a class containing the complete api (or a mock one for test purposes), and we control the state (a different object containing the different situations) to manage the responses and updates.
Your example would be something like this:
First we define our state object:
enum PokemonListStatus { none, error, loaded }
class PokemonListState {
final String? error;
final List<Pokemon> pokemons;
final PokemonListStatus status;
const PokemonListState.loaded(this.pokemons)
: error = null,
status = PokemonListStatus.loaded,
super();
const PokemonListState.error(this.error)
: pokemons = const [],
status = PokemonListStatus.error,
super();
const PokemonListState.initial()
: pokemons = const [],
error = null,
status = PokemonListStatus.none,
super();
}
Now our provider and repository class (abstract is optional, but let's take that approach so you can keep the example for testing):
final pokemonRepositoryProvider =
StateNotifierProvider<PokemonRepository, PokemonListState>((ref) {
final pokemonRepository = JsonPokemonRepository(); // Or ApiRepository
pokemonRepository.getAllPokemon();
return pokemonRepository;
});
///
/// Define abstract class. Useful for testing
///
abstract class PokemonRepository extends StateNotifier<PokemonListState> {
PokemonRepository()
: super(const PokemonListState.initial());
Future<void> getAllPokemon();
Future<void> addPokemon(Pokemon pk);
}
And the implementation for each repository:
///
/// Class to manage pokemon api
///
class ApiPokemonRepository extends PokemonRepository {
ApiPokemonRepository() : super();
Future<void> getAllPokemon() async {
try {
// ... calls to API for retrieving pokemon
// updates cached list with recently obtained data and call watchers.
state = PokemonListState.loaded( ... );
} catch (e) {
state = PokemonListState.error(e.toString());
}
}
Future<void> addPokemon(Pokemon pk) async {
try {
// ... calls to API for adding pokemon
// updates cached list and calls providers watching.
state = PokemonListState.loaded([...state.pokemons, pk]);
} catch (e) {
state = PokemonListState.error(e.toString());
}
}
}
and
///
/// Class to manage pokemon local json
///
class JsonPokemonRepository extends PokemonRepository {
JsonPokemonRepository() : super();
Future<void> getAllPokemon() async {
var response =
await rootBundle.loadString('assets/mock_data/pokemons.json');
List<dynamic> data = jsonDecode(response);
// updates cached list with recently obtained data and call watchers.
final pokemons = List<Pokemon>.from(data.map((i) => Pokemon.fromMap(i)));
state = PokemonListState.loaded(pokemons);
}
Future<void> addPokemon(Pokemon pk) async {
// ... and write json to disk for example
// updates cached list and calls providers watching.
state = PokemonListState.loaded([...state.pokemons, pk]);
}
}
Then in build, your widget with a few changes:
class PokemonsPage extends ConsumerStatefulWidget {
const PokemonsPage({Key? key}) : super(key: key);
#override
ConsumerState<PokemonsPage> createState() => _PokemonsPageState();
}
class _PokemonsPageState extends ConsumerState<PokemonsPage> {
#override
Widget build(BuildContext context) {
final statePokemons =
ref.watch(pokemonRepositoryProvider);
if (statePokemons.status == PokemonListStatus.error) {
return Text('Error: ${statePokemons.error}');
} else if (statePokemons.status == PokemonListStatus.none) {
return const CircularProgressIndicator();
} else {
final pokemons = statePokemons.pokemons;
return Material(
child: ListView.builder(
itemCount: pokemons.length,
itemBuilder: (context, index) {
Pokemon pokemon = pokemons[index];
return ListTile(
title: Text(pokemon.name),
);
},
));
}
}
}
Not sure if this is the best approach but it is working for us so far.
you can try it like this:
class Pokemon {
Pokemon(this.name);
final String name;
}
final pokemonProvider =
StateNotifierProvider<PokemonRepository, AsyncValue<List<Pokemon>>>(
(ref) => PokemonRepository(ref.read));
class PokemonRepository extends StateNotifier<AsyncValue<List<Pokemon>>> {
PokemonRepository(this._reader) : super(const AsyncValue.loading()) {
_init();
}
final Reader _reader;
Future<void> _init() async {
final List<Pokemon> pokemons;
try {
pokemons = await getApiPokemons();
} catch (e, s) {
state = AsyncValue.error(e, stackTrace: s);
return;
}
state = AsyncValue.data(pokemons);
}
Future<void> getAllPokemon() async {
state = const AsyncValue.loading();
/// do something...
state = AsyncValue.data(pokemons);
}
Future<void> addPokemon(Pokemon pk) async {}
Future<void> updatePokemon(Pokemon pk) async {}
Future<void> deletePokemon(Pokemon pk) async {}
}
appreciate the help! I've looked through some of the other responses on here and I can't find an answer.
I have a Provider, in which I have an async function defined. It reaches out to an external API, gets data, and then is meant to update the attributes in the Provider with the data received.
The Widget that uses the provider is meant to build a ListView with that data. projects is null until the response is received. That's why I need the async await functionality to work here. The error I'm getting says that "length can't be called on null", which means projects is still null at the time is reaches that line. That is because the async functionality isn't working.
Here is the Provider, in which my async function is defined:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../../constants/urls.dart';
import 'project.dart';
class Projects with ChangeNotifier{
List<Project> _projects;
List<Project> _myProjects;
final String authToken;
final List<Project> previousProjects;
final bool _initialLoad = true;
Projects(this.authToken, this.previousProjects);
List<Project> get projects {
return _projects;
}
List<Project> get myProjects {
return _myProjects;
}
bool get initialLoad {
return _initialLoad;
}
Future<void> fetchProjects() async {
print('inside future, a');
try {
var response = await http.get(
Uri.parse(Constants.fetchProjectsURL),
headers: {"Authorization": "Bearer " + authToken},
);
print('inside future, b');
if (response.statusCode == 200) {
final extractedData = json.decode(response.body) as List;
final List<Project> tempLoadedProjects = [];
extractedData.forEach((project) {
tempLoadedProjects.add(
Project(
// insert project params
),
);
});
_projects = tempLoadedProjects;
print(_projects);
print(projects);
notifyListeners();
} else {
print('something happened');
}
} catch (error) {
throw error;
}
}
}
Then, I used this provider in the following Widget:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../providers/projects/projects_provider.dart';
class ProjectsColumn extends StatelessWidget {
Future<void> fetchProjects(ctx) async {
await Provider.of<Projects>(ctx).fetchProjects();
}
Widget build(BuildContext context) {
print('Before fetch');
fetchProjects(context);
print('After fetch');
final projects = Provider.of<Projects>(context, listen: false).projects;
return ListView.builder(
itemCount: projects.length,
itemBuilder: (BuildContext ctx, int index) {
return Card(
child: Text(
'Project Name:${projects[index]}',
),
);
});
}
}
Thoughts?
You need to put await before the method to a wait, but you can't do this in build() method, So you can use future builder like the answer of #jamesdlin
or you can call fetchProjects method in intState first like this way:
class ProjectsColumn extends StatefulWidget {
#override
State<ProjectsColumn> createState() => _ProjectsColumnState();
}
class _ProjectsColumnState extends State<ProjectsColumn> {
bool _isLoading = true;
Future<void> _fetchProjects() async {
await Provider.of<Projects>(context, listen: false).fetchProjects();
_isLoading = false;
if (mounted) setState(() {});
}
#override
void initState() {
super.initState();
_fetchProjects();
}
#override
Widget build(BuildContext context) {
return _isLoading
? const Center(child: CircularProgressIndicator())
: Consumer<Projects>(
builder: (context, builder, child) => builder.projects.isEmpty
? const Center(child: Text('No Projects Found'))
: ListView.builder(
shrinkWrap: true,
itemCount: builder.projects.length,
itemBuilder: (BuildContext ctx, int index) {
return Card(
child: Text(
'Project Name:${builder.projects[index]}',
),
);
},
),
);
}
}
EDIT:
a) From the docs HERE BuildContext objects are passed to WidgetBuilder functions (such as StatelessWidget.build), and are available from the State.context member., and in the previous example I used StatefulWidget widget that extends state class, then you can use context outside build but inside the class extends state, not like StatelessWidget.
b) mounted condition, it represents whether a state is currently in the widget tree, i used it to prevent the famous error: setState() called after dispose()
see docs HERE, also this useful answer HERE
I'm create a project on Flutter. And I'm using a provider to change screens in my app.
Here is my main.dart file:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:school_app/services/auth_service.dart';
import 'package:school_app/wrapper.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => AuthService().auth,
child: MaterialApp(
home: Wrapper(),
),
);
}
}
Also this is my wrapper.dart file where the screens choose:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:school_app/screens/authenticate/auth.dart';
import 'package:school_app/models/user.dart';
import 'package:school_app/screens/school/home.dart';
import 'package:school_app/services/auth_service.dart';
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<AuthProvider>(context);
print(user.auth);
if(!user.auth) return Auth();
return Home();
}
}
And it is my AuthProvider class:
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AuthService {
/* AuthUser _user(User user) {
return user != null ? AuthUser(uid: user.uid) : null;
}*/
AuthProvider auth = new AuthProvider();
//sign in
Future signIn(String username, String password) async {
try {
SharedPreferences prefs = await SharedPreferences.getInstance();
var dio = Dio();
Response user = await dio.post('url', data: {
'username': username,
'password': password
});
if(user.data['success'] == false) return user.data['msg'];
await prefs.setString('token', user.data['token']);
auth.setAuth(true);
print("SUCCESS");
} catch(e) {
print('Error ' + e.toString());
}
}
}
class AuthProvider with ChangeNotifier {
bool _auth;
AuthProvider() {
_auth = false;
}
bool get auth => _auth;
void setAuth(bool auth) {
_auth = auth;
notifyListeners();
}
}
And when I call a function in AuthProvider class setAuth, nothing changed. Can you help me and find my mistake?
EDIT
I'm making all changes that you writes but it is not working. Here is my main.dart:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:school_app/services/auth_service.dart';
import 'package:school_app/wrapper.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => AuthProvider(),
child: MaterialApp(
home: Wrapper(),
),
);
}
}
Also wrapper.dart:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:school_app/screens/authenticate/auth.dart';
import 'package:school_app/screens/school/home.dart';
import 'package:school_app/services/auth_service.dart';
class Wrapper extends StatefulWidget {
#override
_WrapperState createState() => _WrapperState();
}
class _WrapperState extends State<Wrapper> {
#override
void initState() {
// TODO: implement initState
super.initState();
AuthService().auth;
}
#override
Widget build(BuildContext context) {
return Consumer<AuthProvider>(builder: (context, authProvider, child) {
print(authProvider.auth);
if (!authProvider.auth) {
return Auth();
} else {
return Home();
}
});
}
}
And AuthService and AuthProvider classes:
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AuthService {
/* AuthUser _user(User user) {
return user != null ? AuthUser(uid: user.uid) : null;
}*/
AuthProvider auth = new AuthProvider();
//sign in
Future signIn(String username, String password) async {
try {
SharedPreferences prefs = await SharedPreferences.getInstance();
var dio = Dio();
Response user = await dio.post('url', data: {
'username': username,
'password': password
});
if(user.data['success'] == false) return user.data['msg'];
await prefs.setString('token', user.data['token']);
auth.setAuth(true);
print("SUCCESS");
} catch(e) {
print('Error ' + e.toString());
}
}
}
class AuthProvider with ChangeNotifier {
bool _auth;
AuthProvider() {
_auth = false;
}
bool get auth => _auth;
void setAuth(bool auth) {
_auth = auth;
notifyListeners();
}
}
Notice, that here two classes and in AuthService I'm calling function .setAuth(true).
In your current implementation of Wrapper, you are rendering the widget once and not listening to whether the values changed. You could use Consumer as suggested above. You could also choose to watch the value for changes - like this:
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = context.watch<AuthProvider>();
print(user.auth);
if(!user.auth) return Auth();
return Home();
}
}
When you use a watch or Consumer pattern, the widget will be rendered when the values of the underlying store (which is AuthProvider here) gets changed.
The only missing part here is that you never Consume the AuthProvider to listen to the notifyListeners() trigger.
The correct implementation looks like the following (I didn't try it, you may have to correct some typo errors, but you'll get the idea !)
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<AuthProvider>(
builder: (context, authProvider, child) {
if (!authProvider.auth) {
return Auth();
} else {
return Home();
}
}
);
}
}
EDIT
I didn't notice you weren't injecting the right Class in your ChangeNotifierProvider. You'll also have to update your widget MyApp
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => AuthProvider(),
child: MaterialApp(
home: Wrapper(),
),
);
}
And in this case you probably should transform your Wrapper widget to a Stateful widget, and in the initState method you should call AuthService().auth.
I strongly recommend you to read the official documentation of Provider, looks like things aren't crystal clear yet in your mind
EDIT 2
You're still missing the point of the Provider library.
The goal of this lib is to provide an instance of a class to your widget tree so you don't have to re-create an instance in each widget.
Here, in AuthService class you're re-creating a AuthProvider with AuthProvider auth = new AuthProvider(); instead of referring to the existing instance created in the parent Widget.
To refer to a previously created instance, you should use Provider.of<AuthProvider>(context); in the AuthService class, or, even better, pass the instance of AuthProvider as a parameter in the signIn method.
I'm new to flutter and dart and trying to fetch data from firestore as a stream and feed to my ListView but I keep getting this error:
type 'MappedListIterable<DocumentSnapshot, Product>' is not a subtype
of type 'List<Product>'
I have seen a couple of other posts on stackoverflow like this but they either do not help me or do not apply to my situation.
This is my products page widget:
import 'package:xxx/models/Product.dart';
import 'package:agrogator/screens/products/widgets/products_list.dart';
import 'package:xxx/services/product.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ProductsScreen extends StatelessWidget {
ProductsScreen({Key key}) : super(key: key);
final product = ProductService();
// This widget is the productsucts page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
#override
#override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return StreamProvider<List<Product>>.value(
value: product.streamProducts(),
child: new Scaffold(
appBar: new AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: new Text("xxx"),
),
body: new ProductsList(),
floatingActionButton: new FloatingActionButton(
onPressed: () {},
tooltip: 'Increment',
child: new Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
),
);
}
}
This is my ProductsList widget:
import 'package:xxx/models/Product.dart';
import 'package:xxx/screens/products/widgets/product_item.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ProductsList extends StatelessWidget {
#override
Widget build(BuildContext context) {
var products = Provider.of<List<Product>>(context);
return Container(
height: 100,
child: ListView(
children: products.map((product) {
return new ProductItem(product: product);
}).toList(),
),
);
}
}
This is my ProductItem widget:
import 'package:xxx/models/Product.dart';
import 'package:flutter/material.dart';
class ProductItem extends StatelessWidget {
final Product product;
ProductItem({this.product});
#override
Widget build(BuildContext context) {
return Text(product.name, style: TextStyle(color: Colors.black));
}
}
This is my Product Model:
import 'package:cloud_firestore/cloud_firestore.dart';
class Product {
String uid;
String name;
String unit;
int avgQuantity;
double avgWeight;
double previousAvgPrice;
double currentAvgPrice;
String lastUpdatedBy;
String lastUpdatedAt;
String remarks;
Product(
{this.uid,
this.name,
this.unit,
this.avgQuantity,
this.avgWeight,
this.previousAvgPrice,
this.currentAvgPrice,
this.lastUpdatedBy,
this.lastUpdatedAt,
this.remarks});
factory Product.fromFirestore(DocumentSnapshot doc) {
Map data = doc.data;
return Product(
uid: doc.documentID,
name: data["name"],
unit: data["unit"],
avgQuantity: data["avgQuantity"],
avgWeight: data["avgWeight"],
previousAvgPrice: data["previousAvgPrice"],
currentAvgPrice: data["ccurrentAvgPrice"],
lastUpdatedBy: data["lastUpdatedBy"],
lastUpdatedAt: data["lastUpdatedAt"],
remarks: data["remarks"]);
}
}
And my service:
import 'package:xxx/models/Product.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class ProductService {
final Firestore _db = Firestore.instance;
Stream<List<Product>> streamProducts() {
var ref = _db.collection("products");
return ref
.snapshots()
.map((list) => list.documents.map((doc) => Product.fromFirestore(doc)));
}
}
In your Service add .toList()
Like this below
import 'package:xxx/models/Product.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class ProductService {
final Firestore _db = Firestore.instance;
Stream<List<Product>> streamProducts() {
var ref = _db.collection("products");
return ref
.snapshots()
.map((list) => list.documents.map((doc) => Product.fromFirestore(doc))).toList(); // <= here
}
}
You can try this. Hope this helps
Product Model
class Product {
String uid;
String name;
String unit;
int avgQuantity;
double avgWeight;
double previousAvgPrice;
double currentAvgPrice;
String lastUpdatedBy;
String lastUpdatedAt;
String remarks;
Product({this.uid,
this.name,
this.unit,
this.avgQuantity,
this.avgWeight,
this.previousAvgPrice,
this.currentAvgPrice,
this.lastUpdatedBy,
this.lastUpdatedAt,
this.remarks});
}
Product Service
class ProductService {
Stream<List<Product>> streamProducts() {
return _FirestoreStream<List<Product>>(
apiPath: "products",
parser: FirestoreProductsParser(),
).stream;
}
}
abstract class FirestoreNodeParser<T> {
T parse(QuerySnapshot querySnapshot);
}
class FirestoreProductsParser extends FirestoreNodeParser<List<Product>> {
List<Product> parse(QuerySnapshot querySnapshot) {
var products = querySnapshot.documents.map((documentSnapshot) {
return Product(
uid: doc.documentID,
name: data["name"],
unit: data["unit"],
avgQuantity: data["avgQuantity"],
avgWeight: data["avgWeight"],
previousAvgPrice: data["previousAvgPrice"],
currentAvgPrice: data["ccurrentAvgPrice"],
lastUpdatedBy: data["lastUpdatedBy"],
lastUpdatedAt: data["lastUpdatedAt"],
remarks: data["remarks"]
);
}).toList();
products.sort((lhs, rhs) => rhs.uid.compareTo(lhs.uid));
return products;
}
}
class _FirestoreStream<T> {
_FirestoreStream({String apiPath, FirestoreNodeParser<T> parser}) {
CollectionReference collectionReference = Firestore.instance.collection(apiPath);
Stream<QuerySnapshot> snapshots = collectionReference.snapshots();
stream = snapshots.map((snapshot) => parser.parse(snapshot));
}
Stream<T> stream;
}