MVVM with Provider

4.4
Serik.Uvaissov 2020-05-20 19:11:17 +06:00
parent 6425f34f4f
commit 30db638877
46 changed files with 617 additions and 708 deletions

13
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Flutter",
"request": "launch",
"type": "dart"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/images/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

4
assets/lang/en.json Normal file
View File

@ -0,0 +1,4 @@
{
"main.emptyList.title": "List is empty",
"main.emptyList.firstPaymentNotAdded": "Need add first payment"
}

21
assets/lang/ru.json Normal file
View File

@ -0,0 +1,21 @@
{
"main.emptyList.title": "Список пуст",
"main.emptyList.firstPaymentNotAdded": "Необходимо добавить первый платеж",
"edit.period": "Периодичность платежей",
"edit.year": "Ежегодный",
"edit.quarter": "Ежеквартальный",
"edit.month": "Ежемесячный",
"edit.week": "Еженедельный",
"edit.periodCount": "Всего периодов",
"edit.perPeriodSum": "Сумма платежа за период",
"edit.firstPayDate": "Дата первого платежа",
"edit.weekendWork": "Учитывать выходные дни недели",
"edit.saturdayWork": "Суббота рабочий день",
"edit.name": "Наименование кредита",
"edit.nameBank": "Наименование банка",
"edit.descriptionLabel": "Дополнительная информация",
"edit.calendar": "Календарь",
"edit.calendarDesc": "Календарь платежей",
"edit.description": "Описание",
"edit.descriptionSubtitle": "Дополнительная информация"
}

View File

@ -1,7 +0,0 @@
class AppRoutes {
static const home = "/";
static const addGame = "/addGame";
static const history = "/history";
static const money = "/money";
static const profile = "/profile";
}

View File

@ -0,0 +1,5 @@
import 'package:equatable/equatable.dart';
abstract class BaseModel implements Equatable {
Map<String, Object> toMap();
}

View File

@ -0,0 +1,13 @@
import 'package:logger/logger.dart';
import '../logger.dart';
class BaseService {
Logger log;
BaseService({String title}) {
this.log = getLogger(
title ?? this.runtimeType.toString(),
);
}
}

View File

@ -0,0 +1,49 @@
import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';
import '../logger.dart';
class BaseViewModel extends ChangeNotifier {
String _title;
bool _busy;
Logger log;
bool _isDisposed = false;
BaseViewModel({
bool busy = false,
String title,
}) : _busy = busy,
_title = title {
log = getLogger(title ?? this.runtimeType.toString());
}
bool get busy => this._busy;
bool get isDisposed => this._isDisposed;
String get title => _title ?? this.runtimeType.toString();
set busy(bool busy) {
log.i(
'busy: '
'$title is entering '
'${busy ? 'busy' : 'free'} state',
);
this._busy = busy;
notifyListeners();
}
@override
void notifyListeners() {
if (!isDisposed) {
super.notifyListeners();
} else {
log.w('notifyListeners: Notify listeners called after '
'${title ?? this.runtimeType.toString()} has been disposed');
}
}
@override
void dispose() {
log.i('dispose');
_isDisposed = true;
super.dispose();
}
}

19
lib/core/locator.dart Normal file
View File

@ -0,0 +1,19 @@
import '../core/services/ApiService.dart';
import '../core/logger.dart';
import '../core/services/navigator_service.dart';
import 'package:get_it/get_it.dart';
import 'package:logger/logger.dart';
GetIt locator = GetIt.instance;
class LocatorInjector {
static Logger _log = getLogger('LocatorInjector');
static Future<void> setupLocator() async {
_log.d('Initializing Navigator Service');
locator.registerLazySingleton(() => NavigatorService());
_log.d('Initializing Api Service');
locator.registerLazySingleton(() => ApiService());
}
}

43
lib/core/logger.dart Normal file
View File

@ -0,0 +1,43 @@
import 'dart:developer' as prefix0;
import 'package:logger/logger.dart';
class SimpleLogPrinter extends LogPrinter {
static int counter = 0;
final String className;
SimpleLogPrinter(this.className);
@override
List<String> log(LogEvent event) {
prefix0.log(
event.message,
time: DateTime.now(),
level: () {
switch (event.level) {
case Level.verbose:
return 0;
case Level.debug:
return 500;
case Level.info:
return 0;
case Level.warning:
return 1500;
case Level.error:
return 2000;
case Level.wtf:
return 2000;
default:
return 2000;
}
}(),
name: className,
error: event.error,
sequenceNumber: counter += 1,
);
return [];
}
}
Logger getLogger(String className) {
return Logger(printer: SimpleLogPrinter(className));
}

23
lib/core/models/user.dart Normal file
View File

@ -0,0 +1,23 @@
class User {
final String id;
final String fullName;
final String email;
final String userRole;
User({this.id, this.fullName, this.email, this.userRole});
User.fromData(Map<String, dynamic> data)
: id = data['id'],
fullName = data['fullName'],
email = data['email'],
userRole = data['userRole'];
Map<String, dynamic> toJson() {
return {
'id': id,
'fullName': fullName,
'email': email,
'userRole': userRole,
};
}
}

30
lib/core/providers.dart Normal file
View File

@ -0,0 +1,30 @@
import 'package:provider/single_child_widget.dart';
import '../core/locator.dart';
import '../core/services/navigator_service.dart';
import 'package:provider/provider.dart';
import 'services/ApiService.dart';
import 'services/authentication_service.dart';
class ProviderInjector {
static List<SingleChildWidget> providers = [
..._independentServices,
..._dependentServices,
..._consumableServices,
];
static List<SingleChildWidget> _independentServices = [
Provider.value(value: locator<NavigatorService>()),
Provider.value(value: locator<ApiService>()),
];
static List<SingleChildWidget> _dependentServices = [
ProxyProvider<ApiService, AuthenticationService>(
update: (context, api, authenticationService) =>
AuthenticationService(api: api),
)
];
static List<SingleChildWidget> _consumableServices = [];
}

View File

@ -0,0 +1,3 @@
const String LoginViewRoute = "LoginView";
const String HomeViewRoute = "HomeView";
// Generate the views here

61
lib/core/router.dart Normal file
View File

@ -0,0 +1,61 @@
import './route_names.dart';
import 'package:aman_kassa_flutter/views/home/home_view.dart';
import 'package:aman_kassa_flutter/views/login/login_view.dart';
import 'package:flutter/material.dart';
Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case LoginViewRoute:
return _getPageRoute(
routeName: settings.name,
viewToShow: LoginView(),
);
case HomeViewRoute:
return _getPageRoute(
routeName: settings.name,
viewToShow: HomeView(),
);
// case AddAndEditViewRoute:
// var documentToEdit = settings.arguments as Document;
// return SlideRightRoute(widget:AddAndEditView(
// edittingDocument: documentToEdit,
// ));
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('No route defined for ${settings.name}')),
));
}
}
PageRoute _getPageRoute({String routeName, Widget viewToShow}) {
return MaterialPageRoute(
settings: RouteSettings(
name: routeName,
),
builder: (_) => viewToShow);
}
class SlideRightRoute extends PageRouteBuilder {
final Widget widget;
SlideRightRoute({this.widget})
: super(
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return widget;
},
transitionsBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
return new SlideTransition(
position: new Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: child,
);
},
);
}

View File

@ -0,0 +1,37 @@
import 'dart:convert';
import 'package:aman_kassa_flutter/core/base/base_service.dart';
import 'package:aman_kassa_flutter/core/models/user.dart';
import 'package:http/http.dart' as http;
/// The service responsible for networking requests
class ApiService extends BaseService {
static const endpoint = 'https://jsonplaceholder.typicode.com';
var client = new http.Client();
Future<User> getUserProfile(int userId) async {
// Get user profile for id
var response = await client.get('$endpoint/users/$userId');
// Convert and return
return User.fromData(json.decode(response.body));
}
// Future<List<Post>> getPostsForUser(int userId) async {
// var posts = List<Post>();
// // Get user posts for id
// var response = await client.get('$endpoint/posts?userId=$userId');
// // parse into List
// var parsed = json.decode(response.body) as List<dynamic>;
// // loop and convert each item to Post
// for (var post in parsed) {
// posts.add(Post.fromJson(post));
// }
// return posts;
// }
}

View File

@ -0,0 +1,35 @@
import 'package:aman_kassa_flutter/core/base/base_service.dart';
import 'package:aman_kassa_flutter/core/models/user.dart';
import 'package:flutter/foundation.dart';
import 'ApiService.dart';
class AuthenticationService extends BaseService {
final ApiService _api;
AuthenticationService({ApiService api}) : _api = api;
User _currentUser;
User get currentUser => _currentUser;
Future loginWithEmail({
@required String email,
@required String password,
}) async {
try {
User result = await _api.getUserProfile(123);
_currentUser = result;
return result != null;
} catch (e) {
return e.message;
}
}
// Future<bool> isUserLoggedIn() async {
// var user = await _firebaseAuth.currentUser();
// await _populateCurrentUser(user);
// return user != null;
// }
}

View File

@ -0,0 +1,35 @@
import '../../core/base/base_service.dart';
import 'package:flutter/material.dart';
class NavigatorService extends BaseService {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
Future<T> navigateToPage<T>(MaterialPageRoute<T> pageRoute) async {
log.i('navigateToPage: pageRoute: ${pageRoute.settings.name}');
if (navigatorKey.currentState == null) {
log.e('navigateToPage: Navigator State is null');
return null;
}
return navigatorKey.currentState.push(pageRoute);
}
Future<T> navigateToPageWithReplacement<T>(
MaterialPageRoute<T> pageRoute) async {
log.i('navigateToPageWithReplacement: '
'pageRoute: ${pageRoute.settings.name}');
if (navigatorKey.currentState == null) {
log.e('navigateToPageWithReplacement: Navigator State is null');
return null;
}
return navigatorKey.currentState.pushReplacement(pageRoute);
}
void pop<T>([T result]) {
log.i('goBack:');
if (navigatorKey.currentState == null) {
log.e('goBack: Navigator State is null');
return;
}
navigatorKey.currentState.pop(result);
}
}

View File

@ -1,31 +0,0 @@
import 'package:flutter/material.dart';
class AvatarsStack extends StatelessWidget {
final List<String> names;
AvatarsStack(this.names);
@override
Widget build(BuildContext context) {
final avatars = List<Widget>();
final max = names.length - 1 > 8 ? 8 : names.length - 1;
for (num i = max; i >= 0; i--) {
avatars.add(Positioned(
right: (5.0 + (25 - i) * i),
child: CircleAvatar(
backgroundColor: (i % 2 == 0 ? Colors.yellow : Colors.grey),
radius: 20,
child: Text(names[i].substring(0, 1),
style: TextStyle(fontSize: 30)),
),
));
}
return Container(
height: 40,
child: Stack(
children: avatars,
),
);
}
}

View File

@ -1,72 +0,0 @@
import 'package:flutter/material.dart';
import 'package:aman_kassa_flutter/features/home/avatars_stack.dart';
import 'package:aman_kassa_flutter/model/Game.dart';
class GameListItem extends StatelessWidget {
final Game game;
const GameListItem(this.game, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(5.0),
child: Center(
child: Card(
child: IntrinsicHeight(
child: Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
padding: EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: Colors.grey.shade900,
borderRadius: BorderRadius.horizontal(
left: Radius.circular(4.0),
right: Radius.elliptical(15.0, 25.0))),
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
'DateFormat',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
Text(
'DateToday',
style: TextStyle(color: Colors.white),
),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Text(
'${game.where}',
style: TextStyle(fontSize: 20),
),
Row(
children: <Widget>[
Text('aaa')
],
),
],
),
),
Expanded(
child: AvatarsStack(
game.players.map((player) => player.name).toList())),
],
),
),
),
),
);
}
}

View File

@ -1,33 +0,0 @@
import 'package:flutter/material.dart';
import 'package:aman_kassa_flutter/features/home/game_list_item.dart';
import 'package:aman_kassa_flutter/features/menu/main_menu.dart';
//import 'package:aman_kassa_flutter/testdata/test_data.dart';
class HomePage extends StatefulWidget {
HomePage({Key key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<GameListItem> _games;
_HomePageState() {
//var games = [TestData.getRandomGames(5);
// games.sort((a, b) => a.date.compareTo(b.date));
// _games = games.map((game) => GameListItem(game))
// .toList()];
_games=[];
}
Widget _getBody() => ListView.builder(
itemBuilder: (BuildContext context, int index) => _games[index],
itemCount: _games.length,
);
@override
Widget build(BuildContext context) {
return MainMenu(_getBody());
}
}

View File

@ -1,84 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:aman_kassa_flutter/app_routes.dart';
import 'package:aman_kassa_flutter/redux/actions.dart';
import 'package:aman_kassa_flutter/redux/app_state.dart';
import 'package:aman_kassa_flutter/redux/selectors.dart';
class BottomNavBar extends StatelessWidget {
Widget _addPadding(Widget child) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: child,
);
Widget _getMenuItem(BuildContext context,
{Icon icon, String routeName, @required _ViewModel vm}) {
if (!vm.route.contains(routeName))
return _addPadding(
IconButton(icon: icon, onPressed: () => vm.navigate(routeName)));
else
return _addPadding(IconButton(
icon: icon,
onPressed: () => vm.navigate(routeName),
color: Theme.of(context).accentColor.withOpacity(0.7)));
}
@override
Widget build(BuildContext context) {
return BottomAppBar(
notchMargin: 8,
color: Theme.of(context).primaryColor,
shape: CircularNotchedRectangle(),
child: StoreConnector<AppState, _ViewModel>(
distinct: true,
converter: (store) => _ViewModel.fromStore(store),
builder: (context, vm) => Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
_getMenuItem(context,
icon: Icon(Icons.home), routeName: AppRoutes.home, vm: vm),
_getMenuItem(context,
icon: Icon(Icons.history),
routeName: AppRoutes.history,
vm: vm),
SizedBox(width: 16),
_getMenuItem(context,
icon: Icon(Icons.monetization_on),
routeName: AppRoutes.money,
vm: vm),
_getMenuItem(context,
icon: Icon(Icons.supervised_user_circle),
routeName: AppRoutes.profile,
vm: vm),
],
),
),
);
}
}
class _ViewModel {
final List<String> route;
final Function(String) navigate;
_ViewModel({@required this.route, @required this.navigate});
static _ViewModel fromStore(Store<AppState> store) {
return _ViewModel(
route: currentRoute(store.state),
navigate: (routeName) =>
store.dispatch(NavigateReplaceAction(routeName)));
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is _ViewModel &&
runtimeType == other.runtimeType &&
route == other.route;
@override
int get hashCode => route.hashCode;
}

View File

@ -1,31 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:aman_kassa_flutter/app_routes.dart';
import 'package:aman_kassa_flutter/features/menu/bottom_nav_bar.dart';
import 'package:aman_kassa_flutter/redux/actions.dart';
import 'package:aman_kassa_flutter/redux/app_state.dart';
class MainMenu extends StatelessWidget {
final Widget body;
MainMenu(this.body);
Widget _getInfoBarWorkaround() =>
PreferredSize(child: Container(), preferredSize: Size(0.0, 0.0));
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _getInfoBarWorkaround(),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomNavBar(),
floatingActionButton: FloatingActionButton(
onPressed: () => StoreProvider.of<AppState>(context)
.dispatch(NavigatePushAction(AppRoutes.addGame)),
tooltip: 'Add new game',
child: Icon(Icons.add),
),
body: body,
);
}
}

View File

@ -1,34 +0,0 @@
import 'package:flutter/material.dart';
class NewGame extends StatelessWidget {
Widget _getBody(BuildContext context) => Center(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text('New Game', style: TextStyle(fontSize: 20, color: Colors.white),),
),
RaisedButton(
child: Text('Show Alert'),
onPressed: () {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(20.0))),
title: Text('Alert Title'),
content: Text('Content of alert.')));
})
],
),
));
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Add Game')),
body: _getBody(context),
);
}
}

View File

@ -1,16 +0,0 @@
import 'package:flutter/material.dart';
import 'package:aman_kassa_flutter/features/menu/main_menu.dart';
class StubScreen extends StatelessWidget {
Widget _getBody() => Center(
child: Text('Stub Screen'),
);
@override
Widget build(BuildContext context) {
return MainMenu(_getBody());
}
}

View File

@ -1,98 +1,28 @@
import 'package:aman_kassa_flutter/views/login/login_view.dart';
import 'core/locator.dart';
import 'core/providers.dart';
import 'core/router.dart';
import 'core/services/navigator_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:aman_kassa_flutter/app_routes.dart';
import 'package:aman_kassa_flutter/features/home/home_page.dart';
import 'package:aman_kassa_flutter/features/newgame/new_game.dart';
import 'package:aman_kassa_flutter/features/stub_screen.dart';
import 'package:aman_kassa_flutter/redux/app_state.dart';
import 'package:aman_kassa_flutter/redux/navigation_middleware.dart';
import 'package:aman_kassa_flutter/redux/reducers/app_reducer.dart';
import 'package:aman_kassa_flutter/route_aware_widget.dart';
import 'package:provider/provider.dart';
import 'views/home/home_view.dart';
void main() => runApp(MyApp());
final GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
class MyApp extends StatelessWidget {
final store = Store<AppState>(appReducer,
initialState: AppState.loading(),
middleware: createNavigationMiddleware());
final theme = ThemeData(
primaryColor: Colors.grey.shade900,
//primaryColorLight: Colors.grey.shade800,
//primaryColorDark: Colors.black,
//scaffoldBackgroundColor: Colors.grey.shade800,
// textTheme: TextTheme(
// body1: TextStyle(color: Colors.white),
// display1: TextStyle(color: Colors.white),
// title: TextStyle(color: Colors.white),
// ),
//iconTheme: IconThemeData(color: Colors.white),
//accentColor: Colors.yellow[500],
);
MaterialPageRoute _getRoute(RouteSettings settings) {
switch (settings.name) {
case AppRoutes.home:
return MainRoute(HomePage(), settings: settings);
case AppRoutes.addGame:
return FabRoute(NewGame(), settings: settings);
default:
return MainRoute(StubScreen(), settings: settings);
}
}
void main() async {
await LocatorInjector.setupLocator();
runApp(MainApplication());
}
class MainApplication extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreProvider(
store: store,
return MultiProvider(
providers: ProviderInjector.providers.toList(),
child: MaterialApp(
navigatorKey: navigatorKey,
navigatorObservers: [routeObserver],
title: 'AppLocalizations.appTitle',
localizationsDelegates: [
//AppLocalizationsDelegate(),
],
theme: theme,
onGenerateRoute: (RouteSettings settings) => _getRoute(settings),
navigatorKey: locator<NavigatorService>().navigatorKey,
home: HomeView(),
onGenerateRoute: generateRoute,
),
);
}
}
class MainRoute<T> extends MaterialPageRoute<T> {
MainRoute(Widget widget, {RouteSettings settings})
: super(
builder: (_) => RouteAwareWidget(child: widget),
settings: settings);
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
if (settings.isInitialRoute) return child;
// Fades between routes. (If you don't want any animation,
// just return child.)
return FadeTransition(opacity: animation, child: child);
}
}
class FabRoute<T> extends MaterialPageRoute<T> {
FabRoute(Widget widget, {RouteSettings settings})
: super(
builder: (_) => RouteAwareWidget(child: widget),
settings: settings);
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
if (settings.isInitialRoute) return child;
return SlideTransition(
position: new Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: Offset.zero,
).animate(animation),
child: child);
}
}
}

View File

@ -1,6 +0,0 @@
class Court {
final num number;
final bool wasReserved;
Court({this.number, this.wasReserved});
}

View File

@ -1,12 +0,0 @@
import 'package:meta/meta.dart';
import 'package:aman_kassa_flutter/model/Court.dart';
import 'package:aman_kassa_flutter/model/Player.dart';
class Game {
final String where;
final DateTime date;
final List<Court> courts;
final List<Player> players;
Game({@required this.where, @required this.date, this.players, this.courts});
}

View File

@ -1,5 +0,0 @@
class Player {
final String name;
Player(this.name);
}

View File

@ -1,44 +0,0 @@
import 'package:aman_kassa_flutter/model/Game.dart';
class AddGameAction {
final Game game;
AddGameAction(this.game);
@override
String toString() {
return 'AddGameAction{game: $game}';
}
}
class NavigateReplaceAction {
final String routeName;
NavigateReplaceAction(this.routeName);
@override
String toString() {
return 'MainMenuNavigateAction{routeName: $routeName}';
}
}
class NavigatePushAction {
final String routeName;
NavigatePushAction(this.routeName);
@override
String toString() {
return 'NavigatePushAction{routeName: $routeName}';
}
}
class NavigatePopAction {
@override
String toString() {
return 'NavigatePopAction';
}
}

View File

@ -1,46 +0,0 @@
import 'package:meta/meta.dart';
import 'package:aman_kassa_flutter/app_routes.dart';
import 'package:aman_kassa_flutter/model/Game.dart';
@immutable
class AppState {
final bool isLoading;
final List<Game> games;
final List<String> route;
AppState({
this.isLoading = false,
this.games = const [],
this.route = const [AppRoutes.home],
});
factory AppState.loading() => AppState(isLoading: true);
AppState copyWith({
bool isLoading,
List<Game> games,
}) =>
AppState(
isLoading: isLoading ?? this.isLoading,
games: games ?? this.games,
route: route ?? this.route
);
@override
int get hashCode =>
isLoading.hashCode ^ games.hashCode ^ route.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AppState &&
runtimeType == other.runtimeType &&
isLoading == other.isLoading &&
games == other.games &&
route == other.route;
@override
String toString() {
return 'AppState{isLoading: $isLoading, games: $games, route: $route}';
}
}

View File

@ -1,27 +0,0 @@
import 'package:redux/redux.dart';
import 'package:aman_kassa_flutter/main.dart';
import 'package:aman_kassa_flutter/redux/actions.dart';
import 'package:aman_kassa_flutter/redux/app_state.dart';
List<Middleware<AppState>> createNavigationMiddleware() {
return [
TypedMiddleware<AppState, NavigateReplaceAction>(_navigateReplace),
TypedMiddleware<AppState, NavigatePushAction>(_navigate),
];
}
_navigateReplace(Store<AppState> store, action, NextDispatcher next) {
final routeName = (action as NavigateReplaceAction).routeName;
if (store.state.route.last != routeName) {
navigatorKey.currentState.pushReplacementNamed(routeName);
}
next(action); //This need to be after name checks
}
_navigate(Store<AppState> store, action, NextDispatcher next) {
final routeName = (action as NavigatePushAction).routeName;
if (store.state.route.last != routeName) {
navigatorKey.currentState.pushNamed(routeName);
}
next(action); //This need to be after name checks
}

View File

@ -1,14 +0,0 @@
import 'package:aman_kassa_flutter/redux/app_state.dart';
import 'package:aman_kassa_flutter/redux/reducers/games_reducer.dart';
import 'package:aman_kassa_flutter/redux/reducers/loading_reducer.dart';
import 'package:aman_kassa_flutter/redux/reducers/navigation_reducer.dart';
AppState appReducer(AppState state, action) {
return AppState(
isLoading: loadingReducer(state.isLoading, action),
games: gamesReducer(state.games, action),
route: navigationReducer(state.route, action)
);
}

View File

@ -1,11 +0,0 @@
import 'package:redux/redux.dart';
import 'package:aman_kassa_flutter/model/Game.dart';
import 'package:aman_kassa_flutter/redux/actions.dart';
final gamesReducer = combineReducers<List<Game>>([
TypedReducer<List<Game>, AddGameAction>(_addGame),
]);
List<Game> _addGame(List<Game> games, AddGameAction action) {
return List.from(games)..add(action.game);
}

View File

@ -1,5 +0,0 @@
import 'package:redux/redux.dart';
final loadingReducer = combineReducers<bool>([
]);

View File

@ -1,24 +0,0 @@
import 'package:redux/redux.dart';
import 'package:aman_kassa_flutter/redux/actions.dart';
final navigationReducer = combineReducers<List<String>>([
TypedReducer<List<String>, NavigateReplaceAction>(_navigateReplace),
TypedReducer<List<String>, NavigatePushAction>(_navigatePush),
TypedReducer<List<String>, NavigatePopAction>(_navigatePop),
]);
List<String> _navigateReplace(
List<String> route, NavigateReplaceAction action) =>
[action.routeName];
List<String> _navigatePush(List<String> route, NavigatePushAction action) {
var result = List<String>.from(route);
result.add(action.routeName);
return result;
}
List<String> _navigatePop(List<String> route, NavigatePopAction action) {
var result = List<String>.from(route);
result.removeLast();
return result;
}

View File

@ -1,4 +0,0 @@
import './app_state.dart';
List<String> currentRoute(AppState state) => state.route;

View File

@ -1,46 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:aman_kassa_flutter/redux/actions.dart';
import 'package:aman_kassa_flutter/redux/app_state.dart';
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
class RouteAwareWidget extends StatefulWidget {
final Widget child;
RouteAwareWidget({this.child});
State<RouteAwareWidget> createState() => RouteAwareWidgetState(child: child);
}
class RouteAwareWidgetState extends State<RouteAwareWidget> with RouteAware {
final Widget child;
RouteAwareWidgetState({this.child});
@override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context));
}
@override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
@override
void didPush() {
// Route was pushed onto navigator and is now topmost route.
}
@override
void didPopNext() {
// Covering route was popped off the navigator.
StoreProvider.of<AppState>(context).dispatch(NavigatePopAction());
}
@override
Widget build(BuildContext context) => Container(child: child);
}

View File

@ -0,0 +1,41 @@
library home_view;
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'home_view_model.dart';
class HomeView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ViewModelBuilder<HomeViewModel>.reactive(
viewModelBuilder: () => HomeViewModel(),
onModelReady: (viewModel) {
//viewModel.busy = true;
},
builder: (context, viewModel, child) {
return buildWaitingLogo(context, viewModel, child);
});
}
Widget buildWaitingLogo(
BuildContext context, HomeViewModel viewModel, Widget child) =>
new Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(
width: 300,
height: 100,
child: Image.asset('assets/images/icon_large.png'),
),
CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(
Colors.yellow[300],
),
)
],
),
));
}

View File

@ -0,0 +1,25 @@
import 'package:aman_kassa_flutter/core/base/base_view_model.dart';
import 'package:aman_kassa_flutter/core/locator.dart';
import 'package:aman_kassa_flutter/core/route_names.dart';
import 'package:aman_kassa_flutter/core/services/navigator_service.dart';
import 'package:aman_kassa_flutter/views/login/login_view.dart';
import 'package:flutter/material.dart';
class HomeViewModel extends BaseViewModel {
final NavigatorService _navigationService = locator<NavigatorService>();
int _counter;
HomeViewModel({int counter = 0}) : this._counter = counter;
int get counter => this._counter;
set counter(int value) {
this._counter = value;
notifyListeners();
}
void increment() => this.counter += 1;
void goToLogin() {
_navigationService.navigateToPage(MaterialPageRoute(builder: (context) => LoginView()));
}
}

View File

@ -0,0 +1,22 @@
library login_view;
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'login_view_model.dart';
class LoginView extends StatelessWidget {
@override
Widget build(BuildContext context) {
//LoginViewModel viewModel = LoginViewModel();
return ViewModelBuilder<LoginViewModel>.reactive(
viewModelBuilder: () => LoginViewModel(),
onModelReady: (viewModel) {
// Do something once your viewModel is initialized
},
builder: (context, viewModel, child) {
return Scaffold(
body: Center(child: Text('LoginMobile')),
);
});
}
}

View File

@ -0,0 +1,7 @@
import 'package:aman_kassa_flutter/core/base/base_view_model.dart';
class LoginViewModel extends BaseViewModel {
LoginViewModel();
// Add ViewModel specific code here
}

View File

@ -7,42 +7,42 @@ packages:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
version: "2.0.13"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.2"
version: "1.6.0"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
version: "2.4.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
version: "2.0.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.2"
version: "1.1.3"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.11"
version: "1.14.12"
convert:
dependency: transitive
description:
@ -56,7 +56,7 @@ packages:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
version: "2.1.4"
cupertino_icons:
dependency: "direct main"
description:
@ -64,30 +64,58 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
equatable:
dependency: "direct main"
description:
name: equatable
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_redux:
dependency: "direct main"
description:
name: flutter_redux
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.4"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
get_it:
dependency: "direct main"
description:
name: get_it
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
http:
dependency: "direct main"
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.1"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.4"
image:
dependency: transitive
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
version: "2.1.12"
logger:
dependency: "direct main"
description:
name: logger
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.1"
matcher:
dependency: transitive
description:
@ -102,6 +130,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.8"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4"
observable_ish:
dependency: transitive
description:
name: observable_ish
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
path:
dependency: transitive
description:
@ -123,20 +165,34 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
provider:
dependency: "direct main"
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.2"
provider_architecture:
dependency: "direct main"
description:
name: provider_architecture
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1+1"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
redux:
version: "2.1.3"
responsive_builder:
dependency: "direct main"
description:
name: redux
name: responsive_builder
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
version: "0.1.9"
sky_engine:
dependency: transitive
description: flutter
@ -148,7 +204,7 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.5"
version: "1.7.0"
stack_trace:
dependency: transitive
description:
@ -156,6 +212,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
stacked:
dependency: "direct main"
description:
name: stacked
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.2"
stream_channel:
dependency: transitive
description:
@ -183,7 +246,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.11"
version: "0.2.15"
typed_data:
dependency: transitive
description:
@ -204,6 +267,7 @@ packages:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "3.5.0"
version: "3.6.1"
sdks:
dart: ">=2.4.0 <3.0.0"
dart: ">=2.7.0 <3.0.0"
flutter: ">=1.16.0"

View File

@ -1,59 +1,40 @@
name: aman_kassa_flutter
description: A new Flutter project.
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1
environment:
sdk: ">=2.1.0 <3.0.0"
sdk: '>=2.3.0 <3.0.0'
dependencies:
flutter:
sdk: flutter
redux: ^3.0.0
flutter_redux: ^0.5.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
stacked : ^1.5.2
provider_architecture: ^1.0.3
responsive_builder: ^0.1.4
provider: ^4.1.2
logger: ^0.9.1
get_it: ^3.0.3
equatable: ^1.1.1
http: ^0.12.1
dev_dependencies:
flutter_test:
sdk: flutter
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter.
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
assets:
- assets/images/logo.png
- assets/images/icon_large.png
- assets/lang/en.json
- assets/lang/ru.json
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
@ -72,4 +53,4 @@ flutter:
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
# see https://flutter.dev/custom-fonts/#from-packages

View File

@ -13,7 +13,7 @@ import 'package:aman_kassa_flutter/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());
await tester.pumpWidget(MainApplication());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);