From f2c63a2645de8450dafb78b7737a3d318cdef375 Mon Sep 17 00:00:00 2001 From: Muhammed OKUR Date: Tue, 5 Mar 2024 00:06:56 +0300 Subject: [PATCH] feat: Added TickerProvider for ViewModel , Sample usage added --- example/router_example/lib/app/app.dart | 4 +- .../router_example/lib/app/app.router.dart | 251 ++++++++++-------- .../router_example/lib/ui/home/home_view.dart | 4 + .../lib/ui/home/home_viewmodel.dart | 4 + .../lib/ui/ticker/ticker_view.dart | 45 ++++ .../lib/ui/ticker/ticker_viewmodel.dart | 15 ++ .../view_models/helpers/ticker_helper.dart | 135 ++++++++++ lib/stacked.dart | 1 + test/ticker_functionality_test.dart | 35 +++ 9 files changed, 385 insertions(+), 109 deletions(-) create mode 100644 example/router_example/lib/ui/ticker/ticker_view.dart create mode 100644 example/router_example/lib/ui/ticker/ticker_viewmodel.dart create mode 100644 lib/src/view_models/helpers/ticker_helper.dart create mode 100644 test/ticker_functionality_test.dart diff --git a/example/router_example/lib/app/app.dart b/example/router_example/lib/app/app.dart index 2627619c..b2c84113 100644 --- a/example/router_example/lib/app/app.dart +++ b/example/router_example/lib/app/app.dart @@ -21,6 +21,7 @@ import 'package:stacked_services/stacked_services.dart'; import 'package:stacked_shared/stacked_shared.dart'; import 'package:stacked_themes/stacked_themes.dart'; +import 'package:example/ui/ticker/ticker_view.dart'; import 'custom_route_transition.dart'; @StackedApp( @@ -39,7 +40,7 @@ import 'custom_route_transition.dart'; page: FavoritesView, children: [ MaterialRoute(page: MultipleFuturesExampleView), - CustomRoute(page: HistoryView), + //CustomRoute(page: HistoryView), ], ), CustomRoute( @@ -54,6 +55,7 @@ import 'custom_route_transition.dart'; page: NonReactiveView, transitionsBuilder: CustomRouteTransition.sharedAxis, ), + MaterialRoute(page: TickerView), ], dependencies: [ // Lazy singletons diff --git a/example/router_example/lib/app/app.router.dart b/example/router_example/lib/app/app.router.dart index eebd5ee1..ebbb2639 100644 --- a/example/router_example/lib/app/app.router.dart +++ b/example/router_example/lib/app/app.router.dart @@ -5,39 +5,40 @@ // ************************************************************************** // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:flutter/foundation.dart' as _i15; -import 'package:flutter/material.dart' as _i13; -import 'package:stacked/stacked.dart' as _i12; -import 'package:stacked_services/stacked_services.dart' as _i11; +import 'package:flutter/foundation.dart' as _i16; +import 'package:flutter/material.dart' as _i14; +import 'package:stacked/stacked.dart' as _i13; +import 'package:stacked_services/stacked_services.dart' as _i12; -import '../datamodels/clashable_one.dart' as _i16; -import '../datamodels/clashable_two.dart' as _i17; +import '../datamodels/clashable_one.dart' as _i17; +import '../datamodels/clashable_two.dart' as _i18; import '../datamodels/home_type.dart' as _i1; import '../ui/bottom_nav/bottom_nav_example.dart' as _i3; -import '../ui/bottom_nav/favorites/favorites_view.dart' as _i7; -import '../ui/bottom_nav/history/history_view.dart' as _i8; -import '../ui/bottom_nav/profile/profile_view.dart' as _i9; +import '../ui/bottom_nav/favorites/favorites_view.dart' as _i8; +import '../ui/bottom_nav/history/history_view.dart' as _i9; +import '../ui/bottom_nav/profile/profile_view.dart' as _i10; import '../ui/form/example_form_view.dart' as _i5; import '../ui/home/home_view.dart' as _i2; import '../ui/multiple_futures_example/multiple_futures_example_view.dart' - as _i10; + as _i11; import '../ui/nonreactive/nonreactive_view.dart' as _i6; import '../ui/stream_view/stream_counter_view.dart' as _i4; -import 'custom_route_transition.dart' as _i14; +import '../ui/ticker/ticker_view.dart' as _i7; +import 'custom_route_transition.dart' as _i15; final stackedRouter = - StackedRouterWeb(navigatorKey: _i11.StackedService.navigatorKey); + StackedRouterWeb(navigatorKey: _i12.StackedService.navigatorKey); -class StackedRouterWeb extends _i12.RootStackRouter { - StackedRouterWeb({_i13.GlobalKey<_i13.NavigatorState>? navigatorKey}) +class StackedRouterWeb extends _i13.RootStackRouter { + StackedRouterWeb({_i14.GlobalKey<_i14.NavigatorState>? navigatorKey}) : super(navigatorKey); @override - final Map pagesMap = { + final Map pagesMap = { HomeViewRoute.name: (routeData) { final args = routeData.argsAs(orElse: () => const HomeViewArgs()); - return _i12.MaterialPageX( + return _i13.MaterialPageX( routeData: routeData, child: _i2.HomeView( key: args.key, @@ -49,14 +50,14 @@ class StackedRouterWeb extends _i12.RootStackRouter { ); }, BottomNavExampleRoute.name: (routeData) { - return _i12.MaterialPageX( + return _i13.MaterialPageX( routeData: routeData, child: const _i3.BottomNavExample(), ); }, StreamCounterViewRoute.name: (routeData) { final args = routeData.argsAs(); - return _i12.MaterialPageX( + return _i13.MaterialPageX( routeData: routeData, child: _i4.StreamCounterView( key: args.key, @@ -66,7 +67,7 @@ class StackedRouterWeb extends _i12.RootStackRouter { }, ExampleFormViewRoute.name: (routeData) { final args = routeData.argsAs(); - return _i12.MaterialPageX( + return _i13.MaterialPageX( routeData: routeData, child: _i5.ExampleFormView( key: args.key, @@ -75,20 +76,26 @@ class StackedRouterWeb extends _i12.RootStackRouter { ); }, NonReactiveViewRoute.name: (routeData) { - return _i12.CustomPage( + return _i13.CustomPage( routeData: routeData, child: const _i6.NonReactiveView(), - transitionsBuilder: _i14.CustomRouteTransition.sharedAxis, + transitionsBuilder: _i15.CustomRouteTransition.sharedAxis, opaque: true, barrierDismissible: false, ); }, + TickerViewRoute.name: (routeData) { + return _i13.MaterialPageX( + routeData: routeData, + child: const _i7.TickerView(), + ); + }, FavoritesViewRoute.name: (routeData) { final args = routeData.argsAs( orElse: () => const FavoritesViewArgs()); - return _i12.AdaptivePage( + return _i13.AdaptivePage( routeData: routeData, - child: _i7.FavoritesView( + child: _i8.FavoritesView( key: args.key, id: args.id, ), @@ -96,96 +103,96 @@ class StackedRouterWeb extends _i12.RootStackRouter { ); }, HistoryViewRoute.name: (routeData) { - return _i12.CustomPage( + return _i13.CustomPage( routeData: routeData, - child: const _i8.HistoryView(), + child: const _i9.HistoryView(), + transitionsBuilder: _i13.TransitionsBuilders.fadeIn, opaque: true, barrierDismissible: false, ); }, ProfileViewRoute.name: (routeData) { - return _i12.CupertinoPageX( + return _i13.CupertinoPageX( routeData: routeData, - child: const _i9.ProfileView(), + child: const _i10.ProfileView(), ); }, MultipleFuturesExampleViewRoute.name: (routeData) { - return _i12.MaterialPageX( + return _i13.MaterialPageX( routeData: routeData, - child: const _i10.MultipleFuturesExampleView(), + child: const _i11.MultipleFuturesExampleView(), ); }, }; @override - List<_i12.RouteConfig> get routes => [ - _i12.RouteConfig( + List<_i13.RouteConfig> get routes => [ + _i13.RouteConfig( HomeViewRoute.name, path: '/', ), - _i12.RouteConfig( + _i13.RouteConfig( BottomNavExampleRoute.name, path: '/bottom-nav-example', children: [ - _i12.RouteConfig( + _i13.RouteConfig( '#redirect', path: '', parent: BottomNavExampleRoute.name, redirectTo: 'favorites', fullMatch: true, ), - _i12.RouteConfig( + _i13.RouteConfig( FavoritesViewRoute.name, path: 'favourites', parent: BottomNavExampleRoute.name, children: [ - _i12.RouteConfig( + _i13.RouteConfig( MultipleFuturesExampleViewRoute.name, path: 'multiple-futures-example-view', parent: FavoritesViewRoute.name, - ), - _i12.RouteConfig( - HistoryViewRoute.name, - path: 'history-view', - parent: FavoritesViewRoute.name, - ), + ) ], ), - _i12.RouteConfig( + _i13.RouteConfig( HistoryViewRoute.name, path: 'history-view', parent: BottomNavExampleRoute.name, ), - _i12.RouteConfig( + _i13.RouteConfig( ProfileViewRoute.name, path: 'profile-view', parent: BottomNavExampleRoute.name, ), ], ), - _i12.RouteConfig( + _i13.RouteConfig( StreamCounterViewRoute.name, path: '/stream-counter-view', ), - _i12.RouteConfig( + _i13.RouteConfig( ExampleFormViewRoute.name, path: '/example-form-view', ), - _i12.RouteConfig( + _i13.RouteConfig( NonReactiveViewRoute.name, path: '/non-reactive-view', ), + _i13.RouteConfig( + TickerViewRoute.name, + path: '/ticker-view', + ), ]; } /// generated route for /// [_i2.HomeView] -class HomeViewRoute extends _i12.PageRouteInfo { +class HomeViewRoute extends _i13.PageRouteInfo { HomeViewRoute({ - _i15.Key? key, + _i16.Key? key, String? title = 'hello', bool? isLoggedIn = false, - _i16.Clashable Function(String)? clashableGetter, + _i17.Clashable Function(String)? clashableGetter, List<_i1.HomeType> homeTypes = const [ _i1.HomeType.apartment, _i1.HomeType.house @@ -214,13 +221,13 @@ class HomeViewArgs { this.homeTypes = const [_i1.HomeType.apartment, _i1.HomeType.house], }); - final _i15.Key? key; + final _i16.Key? key; final String? title; final bool? isLoggedIn; - final _i16.Clashable Function(String)? clashableGetter; + final _i17.Clashable Function(String)? clashableGetter; final List<_i1.HomeType> homeTypes; @@ -232,8 +239,8 @@ class HomeViewArgs { /// generated route for /// [_i3.BottomNavExample] -class BottomNavExampleRoute extends _i12.PageRouteInfo { - const BottomNavExampleRoute({List<_i12.PageRouteInfo>? children}) +class BottomNavExampleRoute extends _i13.PageRouteInfo { + const BottomNavExampleRoute({List<_i13.PageRouteInfo>? children}) : super( BottomNavExampleRoute.name, path: '/bottom-nav-example', @@ -245,10 +252,10 @@ class BottomNavExampleRoute extends _i12.PageRouteInfo { /// generated route for /// [_i4.StreamCounterView] -class StreamCounterViewRoute extends _i12.PageRouteInfo { +class StreamCounterViewRoute extends _i13.PageRouteInfo { StreamCounterViewRoute({ - _i15.Key? key, - required List<_i17.Clashable> clashableTwo, + _i16.Key? key, + required List<_i18.Clashable> clashableTwo, }) : super( StreamCounterViewRoute.name, path: '/stream-counter-view', @@ -267,9 +274,9 @@ class StreamCounterViewArgs { required this.clashableTwo, }); - final _i15.Key? key; + final _i16.Key? key; - final List<_i17.Clashable> clashableTwo; + final List<_i18.Clashable> clashableTwo; @override String toString() { @@ -279,10 +286,10 @@ class StreamCounterViewArgs { /// generated route for /// [_i5.ExampleFormView] -class ExampleFormViewRoute extends _i12.PageRouteInfo { +class ExampleFormViewRoute extends _i13.PageRouteInfo { ExampleFormViewRoute({ - _i15.Key? key, - required _i16.Clashable clashableOne, + _i16.Key? key, + required _i17.Clashable clashableOne, }) : super( ExampleFormViewRoute.name, path: '/example-form-view', @@ -301,9 +308,9 @@ class ExampleFormViewArgs { required this.clashableOne, }); - final _i15.Key? key; + final _i16.Key? key; - final _i16.Clashable clashableOne; + final _i17.Clashable clashableOne; @override String toString() { @@ -313,7 +320,7 @@ class ExampleFormViewArgs { /// generated route for /// [_i6.NonReactiveView] -class NonReactiveViewRoute extends _i12.PageRouteInfo { +class NonReactiveViewRoute extends _i13.PageRouteInfo { const NonReactiveViewRoute() : super( NonReactiveViewRoute.name, @@ -324,12 +331,24 @@ class NonReactiveViewRoute extends _i12.PageRouteInfo { } /// generated route for -/// [_i7.FavoritesView] -class FavoritesViewRoute extends _i12.PageRouteInfo { +/// [_i7.TickerView] +class TickerViewRoute extends _i13.PageRouteInfo { + const TickerViewRoute() + : super( + TickerViewRoute.name, + path: '/ticker-view', + ); + + static const String name = 'TickerView'; +} + +/// generated route for +/// [_i8.FavoritesView] +class FavoritesViewRoute extends _i13.PageRouteInfo { FavoritesViewRoute({ - _i15.Key? key, + _i16.Key? key, String? id, - List<_i12.PageRouteInfo>? children, + List<_i13.PageRouteInfo>? children, }) : super( FavoritesViewRoute.name, path: 'favourites', @@ -349,7 +368,7 @@ class FavoritesViewArgs { this.id, }); - final _i15.Key? key; + final _i16.Key? key; final String? id; @@ -360,8 +379,8 @@ class FavoritesViewArgs { } /// generated route for -/// [_i8.HistoryView] -class HistoryViewRoute extends _i12.PageRouteInfo { +/// [_i9.HistoryView] +class HistoryViewRoute extends _i13.PageRouteInfo { const HistoryViewRoute() : super( HistoryViewRoute.name, @@ -372,8 +391,8 @@ class HistoryViewRoute extends _i12.PageRouteInfo { } /// generated route for -/// [_i9.ProfileView] -class ProfileViewRoute extends _i12.PageRouteInfo { +/// [_i10.ProfileView] +class ProfileViewRoute extends _i13.PageRouteInfo { const ProfileViewRoute() : super( ProfileViewRoute.name, @@ -384,8 +403,8 @@ class ProfileViewRoute extends _i12.PageRouteInfo { } /// generated route for -/// [_i10.MultipleFuturesExampleView] -class MultipleFuturesExampleViewRoute extends _i12.PageRouteInfo { +/// [_i11.MultipleFuturesExampleView] +class MultipleFuturesExampleViewRoute extends _i13.PageRouteInfo { const MultipleFuturesExampleViewRoute() : super( MultipleFuturesExampleViewRoute.name, @@ -395,17 +414,17 @@ class MultipleFuturesExampleViewRoute extends _i12.PageRouteInfo { static const String name = 'MultipleFuturesExampleView'; } -extension RouterStateExtension on _i11.RouterService { +extension RouterStateExtension on _i12.RouterService { Future navigateToHomeView({ - _i15.Key? key, + _i16.Key? key, String? title = 'hello', bool? isLoggedIn = false, - _i16.Clashable Function(String)? clashableGetter, + _i17.Clashable Function(String)? clashableGetter, List<_i1.HomeType> homeTypes = const [ _i1.HomeType.apartment, _i1.HomeType.house ], - void Function(_i12.NavigationFailure)? onFailure, + void Function(_i13.NavigationFailure)? onFailure, }) async { return navigateTo( HomeViewRoute( @@ -420,7 +439,7 @@ extension RouterStateExtension on _i11.RouterService { } Future navigateToBottomNavExample( - {void Function(_i12.NavigationFailure)? onFailure}) async { + {void Function(_i13.NavigationFailure)? onFailure}) async { return navigateTo( const BottomNavExampleRoute(), onFailure: onFailure, @@ -428,9 +447,9 @@ extension RouterStateExtension on _i11.RouterService { } Future navigateToStreamCounterView({ - _i15.Key? key, - required List<_i17.Clashable> clashableTwo, - void Function(_i12.NavigationFailure)? onFailure, + _i16.Key? key, + required List<_i18.Clashable> clashableTwo, + void Function(_i13.NavigationFailure)? onFailure, }) async { return navigateTo( StreamCounterViewRoute( @@ -442,9 +461,9 @@ extension RouterStateExtension on _i11.RouterService { } Future navigateToExampleFormView({ - _i15.Key? key, - required _i16.Clashable clashableOne, - void Function(_i12.NavigationFailure)? onFailure, + _i16.Key? key, + required _i17.Clashable clashableOne, + void Function(_i13.NavigationFailure)? onFailure, }) async { return navigateTo( ExampleFormViewRoute( @@ -456,17 +475,25 @@ extension RouterStateExtension on _i11.RouterService { } Future navigateToNonReactiveView( - {void Function(_i12.NavigationFailure)? onFailure}) async { + {void Function(_i13.NavigationFailure)? onFailure}) async { return navigateTo( const NonReactiveViewRoute(), onFailure: onFailure, ); } + Future navigateToTickerView( + {void Function(_i13.NavigationFailure)? onFailure}) async { + return navigateTo( + const TickerViewRoute(), + onFailure: onFailure, + ); + } + Future navigateToFavoritesView({ - _i15.Key? key, + _i16.Key? key, String? id, - void Function(_i12.NavigationFailure)? onFailure, + void Function(_i13.NavigationFailure)? onFailure, }) async { return navigateTo( FavoritesViewRoute( @@ -478,7 +505,7 @@ extension RouterStateExtension on _i11.RouterService { } Future navigateToHistoryView( - {void Function(_i12.NavigationFailure)? onFailure}) async { + {void Function(_i13.NavigationFailure)? onFailure}) async { return navigateTo( const HistoryViewRoute(), onFailure: onFailure, @@ -486,7 +513,7 @@ extension RouterStateExtension on _i11.RouterService { } Future navigateToProfileView( - {void Function(_i12.NavigationFailure)? onFailure}) async { + {void Function(_i13.NavigationFailure)? onFailure}) async { return navigateTo( const ProfileViewRoute(), onFailure: onFailure, @@ -494,7 +521,7 @@ extension RouterStateExtension on _i11.RouterService { } Future navigateToMultipleFuturesExampleView( - {void Function(_i12.NavigationFailure)? onFailure}) async { + {void Function(_i13.NavigationFailure)? onFailure}) async { return navigateTo( const MultipleFuturesExampleViewRoute(), onFailure: onFailure, @@ -502,15 +529,15 @@ extension RouterStateExtension on _i11.RouterService { } Future replaceWithHomeView({ - _i15.Key? key, + _i16.Key? key, String? title = 'hello', bool? isLoggedIn = false, - _i16.Clashable Function(String)? clashableGetter, + _i17.Clashable Function(String)? clashableGetter, List<_i1.HomeType> homeTypes = const [ _i1.HomeType.apartment, _i1.HomeType.house ], - void Function(_i12.NavigationFailure)? onFailure, + void Function(_i13.NavigationFailure)? onFailure, }) async { return replaceWith( HomeViewRoute( @@ -525,7 +552,7 @@ extension RouterStateExtension on _i11.RouterService { } Future replaceWithBottomNavExample( - {void Function(_i12.NavigationFailure)? onFailure}) async { + {void Function(_i13.NavigationFailure)? onFailure}) async { return replaceWith( const BottomNavExampleRoute(), onFailure: onFailure, @@ -533,9 +560,9 @@ extension RouterStateExtension on _i11.RouterService { } Future replaceWithStreamCounterView({ - _i15.Key? key, - required List<_i17.Clashable> clashableTwo, - void Function(_i12.NavigationFailure)? onFailure, + _i16.Key? key, + required List<_i18.Clashable> clashableTwo, + void Function(_i13.NavigationFailure)? onFailure, }) async { return replaceWith( StreamCounterViewRoute( @@ -547,9 +574,9 @@ extension RouterStateExtension on _i11.RouterService { } Future replaceWithExampleFormView({ - _i15.Key? key, - required _i16.Clashable clashableOne, - void Function(_i12.NavigationFailure)? onFailure, + _i16.Key? key, + required _i17.Clashable clashableOne, + void Function(_i13.NavigationFailure)? onFailure, }) async { return replaceWith( ExampleFormViewRoute( @@ -561,17 +588,25 @@ extension RouterStateExtension on _i11.RouterService { } Future replaceWithNonReactiveView( - {void Function(_i12.NavigationFailure)? onFailure}) async { + {void Function(_i13.NavigationFailure)? onFailure}) async { return replaceWith( const NonReactiveViewRoute(), onFailure: onFailure, ); } + Future replaceWithTickerView( + {void Function(_i13.NavigationFailure)? onFailure}) async { + return replaceWith( + const TickerViewRoute(), + onFailure: onFailure, + ); + } + Future replaceWithFavoritesView({ - _i15.Key? key, + _i16.Key? key, String? id, - void Function(_i12.NavigationFailure)? onFailure, + void Function(_i13.NavigationFailure)? onFailure, }) async { return replaceWith( FavoritesViewRoute( @@ -583,7 +618,7 @@ extension RouterStateExtension on _i11.RouterService { } Future replaceWithHistoryView( - {void Function(_i12.NavigationFailure)? onFailure}) async { + {void Function(_i13.NavigationFailure)? onFailure}) async { return replaceWith( const HistoryViewRoute(), onFailure: onFailure, @@ -591,7 +626,7 @@ extension RouterStateExtension on _i11.RouterService { } Future replaceWithProfileView( - {void Function(_i12.NavigationFailure)? onFailure}) async { + {void Function(_i13.NavigationFailure)? onFailure}) async { return replaceWith( const ProfileViewRoute(), onFailure: onFailure, @@ -599,7 +634,7 @@ extension RouterStateExtension on _i11.RouterService { } Future replaceWithMultipleFuturesExampleView( - {void Function(_i12.NavigationFailure)? onFailure}) async { + {void Function(_i13.NavigationFailure)? onFailure}) async { return replaceWith( const MultipleFuturesExampleViewRoute(), onFailure: onFailure, diff --git a/example/router_example/lib/ui/home/home_view.dart b/example/router_example/lib/ui/home/home_view.dart index a00182bd..2259ffe6 100644 --- a/example/router_example/lib/ui/home/home_view.dart +++ b/example/router_example/lib/ui/home/home_view.dart @@ -27,6 +27,10 @@ class HomeView extends StatelessWidget { viewModelBuilder: () => HomeViewModel(), onViewModelReady: (viewModel) => viewModel.initialise(), builder: (context, viewModel, child) => Scaffold( + appBar: AppBar( + title: Text('Home View'), + actions: [IconButton(onPressed: viewModel.navigateToTicker, icon: Icon(Icons.tab))], + ), body: Center( child: Padding( padding: const EdgeInsets.fromLTRB(12, 20, 12, 40), diff --git a/example/router_example/lib/ui/home/home_viewmodel.dart b/example/router_example/lib/ui/home/home_viewmodel.dart index 41dbbdd7..47f76ff2 100644 --- a/example/router_example/lib/ui/home/home_viewmodel.dart +++ b/example/router_example/lib/ui/home/home_viewmodel.dart @@ -24,6 +24,10 @@ class HomeViewModel extends BaseViewModel with MessageStateHelper { _routerService.navigateTo(const NonReactiveViewRoute()); } + void navigateToTicker() { + _routerService.navigateToTickerView(); + } + void initialise() { setMessage('initialise'); log.i('initialise'); diff --git a/example/router_example/lib/ui/ticker/ticker_view.dart b/example/router_example/lib/ui/ticker/ticker_view.dart new file mode 100644 index 00000000..28d3c4a6 --- /dev/null +++ b/example/router_example/lib/ui/ticker/ticker_view.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; + +import 'ticker_viewmodel.dart'; + +class TickerView extends StackedView { + const TickerView({super.key}); + + @override + Widget builder( + BuildContext context, + TickerViewModel viewModel, + Widget? child, + ) { + return Scaffold( + appBar: AppBar( + title: const Text('Ticker View'), + bottom: TabBar(controller: viewModel.tabController, tabs: const [ + Tab(text: 'Tab 1'), + Tab(text: 'Tab 2'), + Tab(text: 'Tab 3'), + ]), + ), + body: TabBarView( + controller: viewModel.tabController, + children: const [ + Center(child: Text('Tab 1')), + Center(child: Text('Tab 2')), + Center(child: Text('Tab 3')), + ], + )); + } + + @override + TickerViewModel viewModelBuilder(BuildContext context) => TickerViewModel(); + + @override + void onViewModelReady(TickerViewModel viewModel) { + viewModel.initialise(); + super.onViewModelReady(viewModel); + } +} + + + diff --git a/example/router_example/lib/ui/ticker/ticker_viewmodel.dart b/example/router_example/lib/ui/ticker/ticker_viewmodel.dart new file mode 100644 index 00000000..c1094747 --- /dev/null +++ b/example/router_example/lib/ui/ticker/ticker_viewmodel.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; + +class TickerViewModel extends BaseViewModel with StackedSingleTickerProviderStateMixin { + late TabController tabController; + initialise() { + tabController = TabController(length: 3, vsync: this); + } + + @override + void dispose() { + tabController.dispose(); + super.dispose(); + } +} diff --git a/lib/src/view_models/helpers/ticker_helper.dart b/lib/src/view_models/helpers/ticker_helper.dart new file mode 100644 index 00000000..168c80c0 --- /dev/null +++ b/lib/src/view_models/helpers/ticker_helper.dart @@ -0,0 +1,135 @@ + + + +import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; + + +import '../base_view_models.dart'; + +mixin StackedSingleTickerProviderStateMixin on BaseViewModel + implements TickerProvider { + Ticker? _ticker; + + @override + Ticker createTicker(TickerCallback onTick) { + assert(() { + if (_ticker == null) return true; + throw FlutterError.fromParts([ + ErrorSummary( + '$runtimeType is a StackedSingleTickerProviderStateMixin but multiple tickers were created.'), + ErrorDescription( + 'A StackedSingleTickerProviderStateMixin can only be used as a TickerProvider once.'), + ErrorHint( + 'If a State is used for multiple AnimationController objects, or if it is passed to other ' + 'objects and those objects might use it more than one time in total, then instead of ' + 'mixing in a StackedSingleTickerProviderStateMixin, use a regular StackedTickerProviderStateMixin.', + ), + ]); + }()); + _ticker = + Ticker(onTick, debugLabel: kDebugMode ? 'created by $this' : null); + // We assume that this is called from initState, build, or some sort of + // event handler, and that thus TickerMode.of(context) would return true. We + // can't actually check that here because if we're in initState then we're + // not allowed to do inheritance checks yet. + return _ticker!; + } + + void didChangeDependencies(BuildContext context) { + if (_ticker != null) _ticker!.muted = !TickerMode.of(context); + } + + @override + void dispose() { + assert(() { + if (_ticker == null || !_ticker!.isActive) return true; + throw FlutterError.fromParts([ + ErrorSummary('$this was disposed with an active Ticker.'), + ErrorDescription( + '$runtimeType created a Ticker via its StackedSingleTickerProviderStateMixin, but at the time ' + 'dispose() was called on the mixin, that Ticker was still active. The Ticker must ' + 'be disposed before calling super.dispose().', + ), + ErrorHint( + 'Tickers used by AnimationControllers ' + 'should be disposed by calling dispose() on the AnimationController itself. ' + 'Otherwise, the ticker will leak.', + ), + _ticker!.describeForError('The offending ticker was'), + ]); + }()); + super.dispose(); + //super.onClose(); + } +} + +mixin StackedTickerProviderStateMixin on BaseViewModel implements TickerProvider { + Set? _tickers; + + @override + Ticker createTicker(TickerCallback onTick) { + _tickers ??= <_WidgetTicker>{}; + final result = _WidgetTicker(onTick, this, + debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null); + _tickers!.add(result); + return result; + } + + void _removeTicker(_WidgetTicker ticker) { + assert(_tickers != null); + assert(_tickers!.contains(ticker)); + _tickers!.remove(ticker); + } + + void didChangeDependencies(BuildContext context) { + final muted = !TickerMode.of(context); + if (_tickers != null) { + for (final ticker in _tickers!) { + ticker.muted = muted; + } + } + } + + @override + void dispose() { + assert(() { + if (_tickers != null) { + for (final ticker in _tickers!) { + if (ticker.isActive) { + throw FlutterError.fromParts([ + ErrorSummary('$this was disposed with an active Ticker.'), + ErrorDescription( + '$runtimeType created a Ticker via its StackedTickerProviderStateMixin, but at the time ' + 'dispose() was called on the mixin, that Ticker was still active. All Tickers must ' + 'be disposed before calling super.dispose().', + ), + ErrorHint( + 'Tickers used by AnimationControllers ' + 'should be disposed by calling dispose() on the AnimationController itself. ' + 'Otherwise, the ticker will leak.', + ), + ticker.describeForError('The offending ticker was'), + ]); + } + } + } + return true; + }()); + super.dispose(); + } +} + +class _WidgetTicker extends Ticker { + _WidgetTicker(TickerCallback onTick, this._creator, {String? debugLabel}) + : super(onTick, debugLabel: debugLabel); + + final StackedTickerProviderStateMixin _creator; + + @override + void dispose() { + _creator._removeTicker(this); + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/stacked.dart b/lib/stacked.dart index 48b11e8f..e7656d4f 100644 --- a/lib/stacked.dart +++ b/lib/stacked.dart @@ -53,6 +53,7 @@ export 'src/view_models/index_tracking_viewmodel.dart'; export 'src/view_models/selector_view_model_builder.dart'; export 'src/view_models/selector_view_model_builder_widget.dart'; export 'src/view_models/stacked_view.dart'; +export 'src/view_models/helpers/ticker_helper.dart'; /// ui export 'src/view_models/ui/skeleton_loader.dart'; diff --git a/test/ticker_functionality_test.dart b/test/ticker_functionality_test.dart new file mode 100644 index 00000000..9f18efda --- /dev/null +++ b/test/ticker_functionality_test.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stacked/stacked.dart'; + +class TestTickerViewModel extends BaseViewModel with StackedSingleTickerProviderStateMixin { + late TabController tabController; + + initialise() { + tabController = TabController(length: 3, vsync: this); + } + + @override + void dispose() { + tabController.dispose(); + super.dispose(); + } +} + +void main() { + group('StackedSingleTickerProviderStateMixin', () { + test('When tabController is initialised, it should not be null', () { + final viewModel = TestTickerViewModel(); + viewModel.initialise(); + expect(viewModel.tabController, isNotNull); + }); + test('When tabController is disposed, it should be null', () { + final viewModel = TestTickerViewModel(); + viewModel.initialise(); + viewModel.dispose(); + expect(viewModel.tabController.animation, isNull); + }); + + + }); +}