Working on login

This commit is contained in:
unknown 2018-05-21 11:14:36 -07:00
parent 9487c89307
commit b444ce2e2a
20 changed files with 155 additions and 85 deletions

View File

@ -16,7 +16,7 @@ class FileStorage {
); );
/// LoadProducts /// LoadProducts
Future<List<ProductEntity>> loadProducts() async { Future<List<dynamic>> loadData() async {
final file = await _getLocalFile(); final file = await _getLocalFile();
final contents = await file.readAsString(); final contents = await file.readAsString();
@ -29,7 +29,7 @@ class FileStorage {
*/ */
} }
Future<File> saveProducts(List<ProductEntity> products) async { Future<File> saveData(List<dynamic> products) async {
final file = await _getLocalFile(); final file = await _getLocalFile();
/* /*

View File

@ -3,18 +3,18 @@ import 'package:json_annotation/json_annotation.dart';
part 'entities.g.dart'; part 'entities.g.dart';
@JsonSerializable() @JsonSerializable()
class ProductResponse extends Object with _$ProductResponseSerializerMixin { class BaseResponse extends Object with _$BaseResponseSerializerMixin {
//final String message; //final String message;
@JsonKey(name: "data") @JsonKey(name: "data")
final List<ProductEntity> products; final List<dynamic> data;
ProductResponse( BaseResponse(
//this.message, //this.message,
this.products, this.data,
); );
factory ProductResponse.fromJson(Map<String, dynamic> json) => _$ProductResponseFromJson(json); factory BaseResponse.fromJson(Map<String, dynamic> json) => _$BaseResponseFromJson(json);
} }
@JsonSerializable() @JsonSerializable()

View File

@ -6,16 +6,12 @@ part of 'entities.dart';
// Generator: JsonSerializableGenerator // Generator: JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
ProductResponse _$ProductResponseFromJson(Map<String, dynamic> json) => BaseResponse _$BaseResponseFromJson(Map<String, dynamic> json) =>
new ProductResponse((json['data'] as List) new BaseResponse(json['data'] as List);
?.map((e) => e == null
? null
: new ProductEntity.fromJson(e as Map<String, dynamic>))
?.toList());
abstract class _$ProductResponseSerializerMixin { abstract class _$BaseResponseSerializerMixin {
List<ProductEntity> get products; List<dynamic> get data;
Map<String, dynamic> toJson() => <String, dynamic>{'data': products}; Map<String, dynamic> toJson() => <String, dynamic>{'data': data};
} }
ProductEntity _$ProductEntityFromJson(Map<String, dynamic> json) => ProductEntity _$ProductEntityFromJson(Map<String, dynamic> json) =>

View File

@ -2,7 +2,7 @@ export 'package:invoiceninja/data/models/entities.dart';
class User { class User {
final String token; final String token;
final String id; final int id;
User(this.token, this.id); User(this.token, this.id);

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:core'; import 'dart:core';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:invoiceninja/redux/auth/auth_state.dart';
import 'package:invoiceninja/data/models/entities.dart'; import 'package:invoiceninja/data/models/entities.dart';
import 'package:invoiceninja/data/repositories/repositories.dart'; import 'package:invoiceninja/data/repositories/repositories.dart';
import 'package:invoiceninja/data/file_storage.dart'; import 'package:invoiceninja/data/file_storage.dart';
@ -9,7 +10,7 @@ import 'package:invoiceninja/data/web_client.dart';
/// A class that glues together our local file storage and web client. It has a /// A class that glues together our local file storage and web client. It has a
/// clear responsibility: Load Products and Persist products. /// clear responsibility: Load Products and Persist products.
class ProductsRepositoryFlutter implements ProductsRepository { class ProductsRepositoryFlutter implements BaseRepository {
final FileStorage fileStorage; final FileStorage fileStorage;
final WebClient webClient; final WebClient webClient;
@ -21,29 +22,35 @@ class ProductsRepositoryFlutter implements ProductsRepository {
/// Loads products first from File storage. If they don't exist or encounter an /// Loads products first from File storage. If they don't exist or encounter an
/// error, it attempts to load the Products from a Web Client. /// error, it attempts to load the Products from a Web Client.
@override @override
Future<List<ProductEntity>> loadProducts() async { Future<List<dynamic>> loadData(AuthState auth) async {
print('ProductRepo: loadProducts...'); print('ProductRepo: loadProducts...');
final products = await webClient.fetchData(
auth.url + '/products', auth.token);
//fileStorage.saveProducts(products);
return products.map((product) => ProductEntity.fromJson(product)).toList();
/*
try { try {
return await fileStorage.loadProducts(); return await fileStorage.loadData();
} catch (e) { } catch (exception) {
final products = await webClient.fetchProducts(); final products = await webClient.fetchData(
auth.url + '/products', auth.token);
print('ProductRepo: result'); //fileStorage.saveProducts(products);
print(products);
fileStorage.saveProducts(products);
return products; return products;
} }
*/
} }
// Persists products to local disk and the web // Persists products to local disk and the web
@override @override
Future saveProducts(List<ProductEntity> products) { Future saveData(List<dynamic> products) {
return Future.wait<dynamic>([ return Future.wait<dynamic>([
fileStorage.saveProducts(products), fileStorage.saveData(products),
webClient.postProducts(products), webClient.postProducts(products),
]); ]);
} }

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:core'; import 'dart:core';
import 'package:invoiceninja/redux/auth/auth_state.dart';
import 'package:invoiceninja/data/models/entities.dart'; import 'package:invoiceninja/data/models/entities.dart';
@ -11,11 +12,11 @@ import 'package:invoiceninja/data/models/entities.dart';
/// The domain layer should depend on this abstract class, and each app can /// The domain layer should depend on this abstract class, and each app can
/// inject the correct implementation depending on the environment, such as /// inject the correct implementation depending on the environment, such as
/// web or Flutter. /// web or Flutter.
abstract class ProductsRepository { abstract class BaseRepository {
/// Loads products first from File storage. If they don't exist or encounter an /// Loads products first from File storage. If they don't exist or encounter an
/// error, it attempts to load the Todos from a Web Client. /// error, it attempts to load the Todos from a Web Client.
Future<List<ProductEntity>> loadProducts(); Future<List<dynamic>> loadData(AuthState auth);
// Persists products to local disk and the web // Persists products to local disk and the web
Future saveProducts(List<ProductEntity> products); Future saveData(List<dynamic> data);
} }

View File

@ -13,20 +13,16 @@ class WebClient {
const WebClient(); const WebClient();
/// Mock that "fetches" some Products from a "web service" after a short delay /// Mock that "fetches" some Products from a "web service" after a short delay
Future<List<ProductEntity>> fetchProducts() async { Future<List<dynamic>> fetchData(String url, String token) async {
print('Web Client: fetchProducts...');
var url = "";
final response = await http.Client().get( final response = await http.Client().get(
url, url,
headers: {'X-Ninja-Token': ""}, headers: {'X-Ninja-Token': token},
); );
return ProductResponse return BaseResponse
.fromJson(json.decode(response.body)) .fromJson(json.decode(response.body))
.products .data
.toList(); .toList();
} }

View File

@ -10,6 +10,7 @@ import 'package:invoiceninja/routes.dart';
import 'package:invoiceninja/redux/product/product_actions.dart'; import 'package:invoiceninja/redux/product/product_actions.dart';
import 'package:invoiceninja/redux/product/product_middleware.dart'; import 'package:invoiceninja/redux/product/product_middleware.dart';
import 'package:invoiceninja/redux/app/app_reducer.dart'; import 'package:invoiceninja/redux/app/app_reducer.dart';
import 'package:redux_logging/redux_logging.dart';
void main() { void main() {
@ -25,7 +26,11 @@ class InvoiceNinjaApp extends StatelessWidget {
final store = Store<AppState>( final store = Store<AppState>(
appReducer, appReducer,
initialState: AppState.loading(), initialState: AppState.loading(),
middleware: createStoreProductsMiddleware(), middleware: []
..addAll(createStoreProductsMiddleware())
..addAll([
LoggingMiddleware.printer(),
])
); );
@override @override
@ -40,7 +45,6 @@ class InvoiceNinjaApp extends StatelessWidget {
theme: new ThemeData.dark(), theme: new ThemeData.dark(),
title: 'Invoice Ninja', title: 'Invoice Ninja',
routes: { routes: {
/*
NinjaRoutes.login: (context) { NinjaRoutes.login: (context) {
return StoreBuilder<AppState>( return StoreBuilder<AppState>(
builder: (context, store) { builder: (context, store) {
@ -48,7 +52,6 @@ class InvoiceNinjaApp extends StatelessWidget {
}, },
); );
}, },
*/
NinjaRoutes.dashboard: (context) { NinjaRoutes.dashboard: (context) {
return StoreBuilder<AppState>( return StoreBuilder<AppState>(
builder: (context, store) { builder: (context, store) {

View File

@ -18,7 +18,7 @@ AppState appReducer(AppState state, action) {
return AppState( return AppState(
isLoading: loadingReducer(state.isLoading, action), isLoading: loadingReducer(state.isLoading, action),
auth: authReducer(state.auth, action),
products: productsReducer(state.products, action), products: productsReducer(state.products, action),
auth: authReducer(state.auth, action),
); );
} }

View File

@ -10,8 +10,9 @@ class AppState {
AppState( AppState(
{this.isLoading = false, {this.isLoading = false,
AuthState auth, this.products = const [],
this.products = const []}); AuthState auth}):
auth = auth ?? new AuthState();
factory AppState.loading() => AppState(isLoading: true); factory AppState.loading() => AppState(isLoading: true);
@ -55,6 +56,6 @@ class AppState {
@override @override
String toString() { String toString() {
return 'AppState{isLoading: $isLoading}'; return 'AppState{isLoading: $isLoading, url: ${auth.url}, token ${auth.token}}';
} }
} }

View File

@ -3,7 +3,16 @@ import 'package:redux/redux.dart';
import 'package:invoiceninja/redux/app/app_state.dart'; import 'package:invoiceninja/redux/app/app_state.dart';
import 'package:invoiceninja/data/models/models.dart'; import 'package:invoiceninja/data/models/models.dart';
class UserLoginRequest {} class UserLoginRequest {
final String email;
final String password;
final String url;
final String secret;
final String token;
//UserLoginRequest(this.email, this.password, this.url, this.secret);
UserLoginRequest(this.url, this.token);
}
class UserLoginSuccess { class UserLoginSuccess {
final User user; final User user;
@ -19,21 +28,21 @@ class UserLoginFailure {
class UserLogout {} class UserLogout {}
final Function login = (BuildContext context, String username, String password) { /*
final Function login = (BuildContext context, String url, String token) {
return (Store<AppState> store) { return (Store<AppState> store) {
store.dispatch(new UserLoginRequest()); store.dispatch(new UserLoginRequest(url, token));
if (username == 'asd' && password == 'asd') { store.dispatch(new UserLoginSuccess(new User(token, 1)));
store.dispatch(new UserLoginSuccess(new User('placeholder_token', 'placeholder_id'))); Navigator.of(context).pushNamed('/dashboard');
Navigator.of(context).pushNamedAndRemoveUntil('/main', (_) => false);
} else {
store.dispatch(new UserLoginFailure('Username or password were incorrect.'));
}
}; };
}; };
*/
/*
final Function logout = (BuildContext context) { final Function logout = (BuildContext context) {
return (Store<AppState> store) { return (Store<AppState> store) {
store.dispatch(new UserLogout()); store.dispatch(new UserLogout());
Navigator.of(context).pushNamedAndRemoveUntil('/login', (_) => false); Navigator.of(context).pushNamedAndRemoveUntil('/login', (_) => false);
}; };
}; };
*/

View File

@ -4,21 +4,24 @@ import 'package:invoiceninja/redux/auth/auth_actions.dart';
import 'package:invoiceninja/redux/auth/auth_state.dart'; import 'package:invoiceninja/redux/auth/auth_state.dart';
Reducer<AuthState> authReducer = combineReducers([ Reducer<AuthState> authReducer = combineReducers([
new TypedReducer<AuthState, UserLoginRequest>(userLoginRequestReducer), TypedReducer<AuthState, UserLoginRequest>(userLoginRequestReducer),
new TypedReducer<AuthState, UserLoginSuccess>(userLoginSuccessReducer), TypedReducer<AuthState, UserLoginSuccess>(userLoginSuccessReducer),
new TypedReducer<AuthState, UserLoginFailure>(userLoginFailureReducer), TypedReducer<AuthState, UserLoginFailure>(userLoginFailureReducer),
new TypedReducer<AuthState, UserLogout>(userLogoutReducer), TypedReducer<AuthState, UserLogout>(userLogoutReducer),
]); ]);
AuthState userLoginRequestReducer(AuthState auth, UserLoginRequest action) { AuthState userLoginRequestReducer(AuthState auth, UserLoginRequest action) {
return new AuthState().copyWith( return AuthState().copyWith(
url: action.url,
token: action.token,
secret: action.secret,
isAuthenticated: false, isAuthenticated: false,
isAuthenticating: true, isAuthenticating: true,
); );
} }
AuthState userLoginSuccessReducer(AuthState auth, UserLoginSuccess action) { AuthState userLoginSuccessReducer(AuthState auth, UserLoginSuccess action) {
return new AuthState().copyWith( return AuthState().copyWith(
isAuthenticated: true, isAuthenticated: true,
isAuthenticating: false, isAuthenticating: false,
user: action.user user: action.user
@ -26,7 +29,7 @@ AuthState userLoginSuccessReducer(AuthState auth, UserLoginSuccess action) {
} }
AuthState userLoginFailureReducer(AuthState auth, UserLoginFailure action) { AuthState userLoginFailureReducer(AuthState auth, UserLoginFailure action) {
return new AuthState().copyWith( return AuthState().copyWith(
isAuthenticated: false, isAuthenticated: false,
isAuthenticating: false, isAuthenticating: false,
error: action.error error: action.error
@ -34,5 +37,5 @@ AuthState userLoginFailureReducer(AuthState auth, UserLoginFailure action) {
} }
AuthState userLogoutReducer(AuthState auth, UserLogout action) { AuthState userLogoutReducer(AuthState auth, UserLogout action) {
return new AuthState(); return AuthState();
} }

View File

@ -6,6 +6,9 @@ import 'package:invoiceninja/data/models/models.dart';
class AuthState { class AuthState {
// properties // properties
final String url;
final String secret;
final String token;
final bool isAuthenticated; final bool isAuthenticated;
final bool isAuthenticating; final bool isAuthenticating;
final User user; final User user;
@ -13,6 +16,9 @@ class AuthState {
// constructor with default // constructor with default
AuthState({ AuthState({
this.url,
this.secret,
this.token,
this.isAuthenticated = false, this.isAuthenticated = false,
this.isAuthenticating = false, this.isAuthenticating = false,
this.user, this.user,
@ -21,12 +27,18 @@ class AuthState {
// allows to modify AuthState parameters while cloning previous ones // allows to modify AuthState parameters while cloning previous ones
AuthState copyWith({ AuthState copyWith({
String url,
String secret,
String token,
bool isAuthenticated, bool isAuthenticated,
bool isAuthenticating, bool isAuthenticating,
String error, String error,
User user User user
}) { }) {
return new AuthState( return new AuthState(
url: url ?? this.url,
token: token ?? this.token,
secret: secret ?? this.secret,
isAuthenticated: isAuthenticated ?? this.isAuthenticated, isAuthenticated: isAuthenticated ?? this.isAuthenticated,
isAuthenticating: isAuthenticating ?? this.isAuthenticating, isAuthenticating: isAuthenticating ?? this.isAuthenticating,
error: error ?? this.error, error: error ?? this.error,
@ -35,6 +47,9 @@ class AuthState {
} }
factory AuthState.fromJSON(Map<String, dynamic> json) => new AuthState( factory AuthState.fromJSON(Map<String, dynamic> json) => new AuthState(
url: json['url'],
token: json['token'],
secret: json['secret'],
isAuthenticated: json['isAuthenticated'], isAuthenticated: json['isAuthenticated'],
isAuthenticating: json['isAuthenticating'], isAuthenticating: json['isAuthenticating'],
error: json['error'], error: json['error'],
@ -42,6 +57,9 @@ class AuthState {
); );
Map<String, dynamic> toJSON() => <String, dynamic>{ Map<String, dynamic> toJSON() => <String, dynamic>{
'url': this.url,
'token': this.token,
'secret': this.secret,
'isAuthenticated': this.isAuthenticated, 'isAuthenticated': this.isAuthenticated,
'isAuthenticating': this.isAuthenticating, 'isAuthenticating': this.isAuthenticating,
'user': this.user == null ? null : this.user.toJSON(), 'user': this.user == null ? null : this.user.toJSON(),
@ -51,6 +69,7 @@ class AuthState {
@override @override
String toString() { String toString() {
return '''{ return '''{
url: $url,
isAuthenticated: $isAuthenticated, isAuthenticated: $isAuthenticated,
isAuthenticating: $isAuthenticating, isAuthenticating: $isAuthenticating,
user: $user, user: $user,

View File

@ -2,7 +2,16 @@ import 'package:invoiceninja/data/models/models.dart';
class LoadProductsAction {} class LoadProductsAction {}
class ProductsNotLoadedAction {} class ProductsNotLoadedAction {
final dynamic error;
ProductsNotLoadedAction(this.error);
@override
String toString() {
return 'ProductsNotLoadedAction{products: $error}';
}
}
class ProductsLoadedAction { class ProductsLoadedAction {
final List<ProductEntity> products; final List<ProductEntity> products;

View File

@ -6,9 +6,10 @@ import 'package:invoiceninja/data/repositories/repositories.dart';
import 'package:invoiceninja/data/repositories/product_repository.dart'; import 'package:invoiceninja/data/repositories/product_repository.dart';
import 'package:invoiceninja/data/file_storage.dart'; import 'package:invoiceninja/data/file_storage.dart';
import 'package:invoiceninja/redux/product/product_selectors.dart'; import 'package:invoiceninja/redux/product/product_selectors.dart';
import 'package:invoiceninja/data/models/entities.dart';
List<Middleware<AppState>> createStoreProductsMiddleware([ List<Middleware<AppState>> createStoreProductsMiddleware([
ProductsRepository repository = const ProductsRepositoryFlutter( BaseRepository repository = const ProductsRepositoryFlutter(
fileStorage: const FileStorage( fileStorage: const FileStorage(
'__invoiceninja__', '__invoiceninja__',
getApplicationDocumentsDirectory, getApplicationDocumentsDirectory,
@ -24,7 +25,7 @@ List<Middleware<AppState>> createStoreProductsMiddleware([
]; ];
} }
Middleware<AppState> _createSaveProducts(ProductsRepository repository) { Middleware<AppState> _createSaveProducts(BaseRepository repository) {
return (Store<AppState> store, action, NextDispatcher next) { return (Store<AppState> store, action, NextDispatcher next) {
next(action); next(action);
@ -36,14 +37,12 @@ Middleware<AppState> _createSaveProducts(ProductsRepository repository) {
}; };
} }
Middleware<AppState> _createLoadProducts(ProductsRepository repository) { Middleware<AppState> _createLoadProducts(BaseRepository repository) {
return (Store<AppState> store, action, NextDispatcher next) { return (Store<AppState> store, action, NextDispatcher next) {
print('Product Middleware: createLoadProducts...'); print('Product Middleware: createLoadProducts...');
//repository.loadProducts(); repository.loadData(store.state.auth).then(
repository.loadProducts().then(
(products) { (products) {
store.dispatch( store.dispatch(
ProductsLoadedAction( ProductsLoadedAction(
@ -52,7 +51,7 @@ Middleware<AppState> _createLoadProducts(ProductsRepository repository) {
), ),
); );
}, },
).catchError((_) => store.dispatch(ProductsNotLoadedAction())); ).catchError((error) => store.dispatch(ProductsNotLoadedAction(error)));
next(action); next(action);
}; };

View File

@ -1,6 +1,6 @@
class NinjaRoutes { class NinjaRoutes {
//static final login = "/"; static final login = "/";
static final dashboard = "/"; static final dashboard = "/dashboard";
static final clientList = "/clientList"; static final clientList = "/clientList";
static final productList = "/productList"; static final productList = "/productList";
} }

View File

@ -3,6 +3,7 @@ import 'package:redux/redux.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:invoiceninja/redux/app/app_state.dart'; import 'package:invoiceninja/redux/app/app_state.dart';
import 'package:invoiceninja/redux/auth/auth_actions.dart'; import 'package:invoiceninja/redux/auth/auth_actions.dart';
import 'package:invoiceninja/data/models/models.dart';
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
@override @override
@ -14,6 +15,9 @@ class _LoginScreenState extends State<LoginScreen> {
String _username; String _username;
String _password; String _password;
String _url;
String _token;
String _secret;
void _submit() { void _submit() {
final form = _formKey.currentState; final form = _formKey.currentState;
@ -27,8 +31,11 @@ class _LoginScreenState extends State<LoginScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StoreConnector<AppState, dynamic>( return StoreConnector<AppState, dynamic>(
converter: (Store<AppState> store) { converter: (Store<AppState> store) {
return (BuildContext context, String username, String password) => return (BuildContext context, String url, String token) {
store.dispatch(login(context, username, password)); store.dispatch(UserLoginRequest(url, token));
//store.dispatch(UserLoginSuccess(User(token, 1)));
Navigator.of(context).pushNamed('/dashboard');
};
}, builder: (BuildContext context, loginAction) { }, builder: (BuildContext context, loginAction) {
return Scaffold( return Scaffold(
body: Form( body: Form(
@ -37,26 +44,41 @@ class _LoginScreenState extends State<LoginScreen> {
shrinkWrap: true, shrinkWrap: true,
padding: EdgeInsets.only(left: 24.0, right: 24.0, top: 40.0), padding: EdgeInsets.only(left: 24.0, right: 24.0, top: 40.0),
children: [ children: [
TextFormField(
decoration: InputDecoration(labelText: 'URL'),
validator: (val) =>
val.isEmpty ? 'Please enter your URL.' : null,
onSaved: (val) => _url = val,
),
TextFormField(
decoration: InputDecoration(labelText: 'Token'),
validator: (val) =>
val.isEmpty ? 'Please enter your token.' : null,
onSaved: (val) => _token = val,
),
Padding(
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
child: Text(
'Note: this will be changed to email/password in the future'),
),
/*
TextFormField( TextFormField(
decoration: InputDecoration(labelText: 'Email'), decoration: InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
//contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), //contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
//autofocus: false, //autofocus: false,
/*
validator: (val) => validator: (val) =>
val.isEmpty ? 'Please enter your email.' : null, val.isEmpty ? 'Please enter your email.' : null,
*/
onSaved: (val) => _username = val, onSaved: (val) => _username = val,
), ),
TextFormField( TextFormField(
decoration: InputDecoration(labelText: 'Password'), decoration: InputDecoration(labelText: 'Password'),
/*
validator: (val) => validator: (val) =>
val.isEmpty ? 'Please enter your password.' : null, val.isEmpty ? 'Please enter your password.' : null,
*/
onSaved: (val) => _password = val, onSaved: (val) => _password = val,
obscureText: true, obscureText: true,
), ),
*/
Padding( Padding(
padding: EdgeInsets.only(top: 20.0), padding: EdgeInsets.only(top: 20.0),
child: Material( child: Material(
@ -68,7 +90,7 @@ class _LoginScreenState extends State<LoginScreen> {
height: 42.0, height: 42.0,
onPressed: () { onPressed: () {
_submit(); _submit();
loginAction(context, _username, _password); loginAction(context, _url, _token);
//Navigator.of(context).pushNamed(HomeScreen.tag); //Navigator.of(context).pushNamed(HomeScreen.tag);
}, },
color: Colors.lightBlueAccent, color: Colors.lightBlueAccent,

View File

@ -27,8 +27,6 @@ class ProductList extends StatelessWidget {
} }
ListView _buildListView() { ListView _buildListView() {
print('_buildListView');
print(products);
return ListView.builder( return ListView.builder(
key: NinjaKeys.productList, key: NinjaKeys.productList,
itemCount: products.length, itemCount: products.length,

View File

@ -347,6 +347,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
redux_logging:
dependency: "direct main"
description:
name: redux_logging
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:

View File

@ -9,7 +9,7 @@ dependencies:
cupertino_icons: ^0.1.0 cupertino_icons: ^0.1.0
json_annotation: ^0.2.3 json_annotation: ^0.2.3
path_provider: "^0.4.0" path_provider: "^0.4.0"
redux_logging: "^0.3.0"
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: