├── .cirrus.yml ├── LICENSE ├── README.md ├── material_bottomnavigation_provider_mobx ├── .gitignore ├── .metadata ├── README.md ├── lib │ ├── constants │ │ ├── enums.dart │ │ └── keys.dart │ ├── main.dart │ ├── pages │ │ ├── dashboard_page.dart │ │ ├── home_page.dart │ │ ├── notifications_page.dart │ │ └── settings_page.dart │ ├── services │ │ └── preferences_service.dart │ └── stores │ │ ├── dashboard_store.dart │ │ ├── dashboard_store.g.dart │ │ ├── destinations_store.dart │ │ ├── destinations_store.g.dart │ │ ├── home_store.dart │ │ ├── home_store.g.dart │ │ ├── notifications_store.dart │ │ ├── notifications_store.g.dart │ │ ├── settings_store.dart │ │ └── settings_store.g.dart ├── pubspec.yaml ├── test │ ├── mocks │ │ └── mock_shared_preferences.dart │ ├── stores │ │ ├── dashboard_store_test.dart │ │ ├── destinations_store_test.dart │ │ ├── home_store_test.dart │ │ ├── notifications_store_test.dart │ │ └── settings_store_test.dart │ └── widgets │ │ ├── app_test.dart │ │ └── pages │ │ ├── dashboard_page_test.dart │ │ ├── home_page_test.dart │ │ ├── notifications_page_test.dart │ │ └── settings_page_test.dart └── test_driver │ ├── app.dart │ └── app_test.dart ├── material_navigationdrawer_provider_mobx ├── .gitignore ├── .metadata ├── README.md ├── lib │ ├── constants │ │ ├── enums.dart │ │ └── keys.dart │ ├── main.dart │ ├── pages │ │ ├── gallery_page.dart │ │ ├── home_page.dart │ │ ├── settings_page.dart │ │ └── slideshow_page.dart │ ├── services │ │ └── preferences_service.dart │ └── stores │ │ ├── destinations_store.dart │ │ ├── destinations_store.g.dart │ │ ├── gallery_store.dart │ │ ├── gallery_store.g.dart │ │ ├── home_store.dart │ │ ├── home_store.g.dart │ │ ├── settings_store.dart │ │ ├── settings_store.g.dart │ │ ├── slideshow_store.dart │ │ └── slideshow_store.g.dart ├── pubspec.yaml ├── test │ ├── mocks │ │ └── mock_shared_preferences.dart │ ├── stores │ │ ├── destinations_store_test.dart │ │ ├── gallery_store.dart │ │ ├── home_store_test.dart │ │ ├── settings_store_test.dart │ │ └── slideshow_store_test.dart │ └── widgets │ │ ├── app_test.dart │ │ └── pages │ │ ├── gallery_page_test.dart │ │ ├── home_page_test.dart │ │ ├── settings_page_test.dart │ │ └── slideshow_page_test.dart └── test_driver │ ├── app.dart │ └── app_test.dart └── material_navigationdrawerroutes_provider_mobx ├── .gitignore ├── .metadata ├── README.md ├── lib ├── constants │ ├── keys.dart │ └── routes.dart ├── main.dart ├── pages │ ├── gallery_page.dart │ ├── home_page.dart │ ├── settings_page.dart │ └── slideshow_page.dart ├── services │ └── preferences_service.dart └── stores │ ├── gallery_store.dart │ ├── gallery_store.g.dart │ ├── home_store.dart │ ├── home_store.g.dart │ ├── settings_store.dart │ ├── settings_store.g.dart │ ├── slideshow_store.dart │ └── slideshow_store.g.dart ├── pubspec.yaml ├── test ├── mocks │ └── mock_shared_preferences.dart ├── stores │ ├── gallery_store.dart │ ├── home_store_test.dart │ ├── settings_store_test.dart │ └── slideshow_store_test.dart └── widgets │ ├── app_test.dart │ └── pages │ ├── gallery_page_test.dart │ ├── home_page_test.dart │ ├── settings_page_test.dart │ └── slideshow_page_test.dart └── test_driver ├── app.dart └── app_test.dart /.cirrus.yml: -------------------------------------------------------------------------------- 1 | task: 2 | name: Unit tests and widget tests 3 | env: 4 | matrix: 5 | template: material_bottomnavigation_provider_mobx 6 | template: material_navigationdrawer_provider_mobx 7 | template: material_navigationdrawerroutes_provider_mobx 8 | container: 9 | image: cirrusci/flutter:stable 10 | pub_cache: 11 | folder: ~/.pub-cache 12 | test_script: 13 | - cd $template 14 | - flutter create . 15 | - flutter pub run build_runner build --delete-conflicting-outputs 16 | 17 | task: 18 | name: Integration tests for $template (Android) 19 | # only_if: $CIRRUS_PR == '' 20 | # skip: "!changesInclude('.cirrus.yml', '$template/*', '$template/**/*')" 21 | env: 22 | EMULATOR_API_LEVEL: 28 23 | ANDROID_ABI: "default;x86" 24 | matrix: 25 | template: material_bottomnavigation_provider_mobx 26 | template: material_navigationdrawer_provider_mobx 27 | template: material_navigationdrawerroutes_provider_mobx 28 | container: 29 | image: cirrusci/flutter:stable 30 | cpu: 4 31 | memory: 10G 32 | kvm: true 33 | pub_cache: 34 | folder: ~/.pub-cache 35 | create_native_projects_script: 36 | - cd $template 37 | - flutter create . 38 | - flutter pub run build_runner build --delete-conflicting-outputs 39 | install_images_script: sdkmanager "system-images;android-$EMULATOR_API_LEVEL;$ANDROID_ABI" 40 | create_device_script: 41 | echo no | avdmanager create avd --force -n test -k "system-images;android-$EMULATOR_API_LEVEL;$ANDROID_ABI" 42 | start_emulator_background_script: 43 | $ANDROID_HOME/emulator/emulator -no-window -verbose -avd test -no-audio -no-window 44 | wait_for_emulator_script: 45 | android-wait-for-emulator 46 | test_script: 47 | - cd $template 48 | - flutter drive --target=test_driver/app.dart 49 | 50 | task: 51 | name: Integration tests for $template (iOS) 52 | # only_if: $CIRRUS_PR == '' 53 | # skip: "!changesInclude('.cirrus.yml', '$template/*', '$template/**/*')" 54 | osx_instance: 55 | image: catalina-flutter 56 | env: 57 | matrix: 58 | template: material_bottomnavigation_provider_mobx 59 | template: material_navigationdrawer_provider_mobx 60 | template: material_navigationdrawerroutes_provider_mobx 61 | pub_cache: 62 | folder: ~/.pub-cache 63 | create_native_projects_script: 64 | - cd $template 65 | - flutter create . 66 | - flutter pub run build_runner build --delete-conflicting-outputs 67 | setup_script: 68 | - xcrun simctl list devicetypes 69 | - xcrun simctl list runtimes 70 | # create simulator 71 | - udid=$(xcrun simctl create "iPhone X" com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-5) 72 | # boot simulator 73 | - xcrun simctl boot $udid 74 | test_script: 75 | - cd $template 76 | - flutter drive --target=test_driver/app.dart 77 | 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Michael Bui 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter application templates 2 | 3 | [![Build Status](https://api.cirrus-ci.com/github/MaikuB/flutter_app_templates.svg)](https://cirrus-ci.com/github/MaikuB/flutter_app_templates) 4 | 5 | This repository is to used to maintain some templates that can form the basis of a Flutter application. These templates may be opinionated in how to structure applications. The intent is that these templates could help other developers get started sooner in the absence of being able specify templates to use to the `flutter create` command. 6 | 7 | **NOTE**: updates may be done to existing templates. As this isn't a published package, if you want to stay notified of updates then it's suggested that you watch the GitHub repository. 8 | 9 | ## Templates 10 | Templates can be found in the following folders 11 | 12 | - `material_bottomnavigation_provider_mobx` - a template for a material design application with bottom navigation. Uses provider and MobX for state management 13 | - `material_navigationdrawer_provider_mobx` - a template for a material design application with a navigation drawer. Selecting a destination updates the content displayed without triggering navigation. Uses provider and MobX for state management 14 | - `material_navigationdrawerroutes_provider_mobx` - a template for a material design application with a navigation drawer. Selecting a destination will trigger navigating to the destination with a back button so that the user can go back to the home page. Uses provider and MobX for state management 15 | 16 | ## Getting started with a template 17 | 18 | When creating a brand new Flutter application, there are few steps that developers may do 19 | 20 | - specify the organisation 21 | - specify the name of the app 22 | - specify the languages used for the Android and iOS projects 23 | 24 | Note that out of the box, Flutter will combine the organisation and name of the app to set that as the bundle identifier of your Android and iOS application. It is still possible to do all of the above with a template. The following steps are required 25 | 26 | - Clone this repository 27 | - Pick the folder/template you'd like to use and copy it to the location of your choice. Let's assume for this example that you have picked the `material_bottomnavigation_provider_mobx` folder and copied it to sit within `projects` folder 28 | - Go to the `projects` folder (i.e. where you've copied the template to). Rename the folder to match the name of your application e.g. `counter`. This is crucial as Flutter's tooling actually uses this as portion of the bundle identifier that represents your application's name 29 | - Open the `counter` application in your IDE of choice, search for all instances of `material_bottomnavigation_provider_mobx` and replace it with `counter` (the name of your application) 30 | - From the root folder of your `counter` application. Run the following command 31 | 32 | ``` 33 | flutter create --org -a -i . 34 | ``` 35 | 36 | where `organisation>` is your organisation in reverse domain name notation. For example, given the `crossingthestreams.io` domain, the organisation would be specified as `io.crossingthestreams`. `` is either `java` or `kotlin` and `` is either `objc` or `swift`. The tooling will generate a `widget_test.dart` file within the `test` folder that can be deleted. Successfully executing all of the steps would create an application with the desired application name, bundle identifier and create the appropriate Android and iOS project files. From there you should be able to run the application. You can then the `pubspec.yaml` to your liking (e.g. to update the dependencies). Note that the templates have unit tests, widget tests and integration tests included. Therefore, if you make changes then these tests may need to be updated to stop them from failing. 37 | 38 | As the templates do not specify the version of the dependencies to pull in, developers may want to explicitly do so, or run the following command to upgrade them to the latest versions whilst avoiding conflicts 39 | 40 | `flutter pub upgrade` 41 | 42 | It's suggested that developers also run the following command once the dependencies have been updated to ensure that generated code (e.g. by MobX's code generator) is also updated 43 | 44 | `flutter pub run build_runner build --delete-conflicting-outputs` 45 | 46 | ## Contributions 47 | 48 | Feel free to submit PRs to share templates that you feel would be useful to others. A short checklist to things to do are 49 | 50 | - Add information to the template's readme 51 | - Include unit tests, widget test and integration tests 52 | - Update the `.cirrus.yml` file to include your template 53 | - Keep the original .gitignore file generated by the `flutter create` command. This would ensure that when a template is used to create an application, the relevent Android and iOS projects can by checked into source control 54 | - Ensure that the steps described above for creating an application from a template works with the template you're submitting 55 | 56 | The `material_bottomnavigation_provider_mobx` folder can be referred to in order mimic what has been done there when submitting a new template 57 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | .dart_tool/ 24 | .flutter-plugins 25 | .flutter-plugins-dependencies 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | /build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 72 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 7a4c33425ddd78c54aba07d86f3f9a4a0051769b 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/README.md: -------------------------------------------------------------------------------- 1 | # Template: material design application with bottom navigation, provider and MobX 2 | 3 | This is a template for creating a Flutter application that uses material design and has a bottom navigation bar. Uses [provider](https://github.com/rrousselGit/provider) and [MobX](https://github.com/mobxjs/mobx.dart) to manage state. 4 | 5 | ## Overview 6 | 7 | The application is similar to the one created when creating an Android application that has a bottom navigation activity. It has four bottom navigation destinations: Home, Dashboard, Notifications and Settings. 8 | 9 | ![Image of home page](https://dexterx.dev/content/images/2019/06/home-2.png) ![Image of dashboard page](https://dexterx.dev/content/images/2019/06/dashboard-2.png) ![Image of notifications page](https://dexterx.dev/content/images/2019/06/notifications-2.png) ![Image of settings page](https://dexterx.dev/content/images/2019/06/settings.png) 10 | 11 | Each destination has its own corresponding page and data store. Tapping on the floating action button will increment the counter related to the page that is currently being viewed. The settings page allows toggling on and off the dark theme. 12 | 13 | The application is structured as follows 14 | 15 | - `lib` 16 | - `constants` - this folder is where all application-level constants are kept 17 | - `enums.dart` - to maintain all the application-level enums. For this application, a `Destination` enum is defined that represents all of the bottom navigation destinations: `Home`, `Dashboard`, `Notifications` and `Settings` 18 | - `keys.dart` - to maintain the keys associated with the various widgets in the application 19 | - `pages` - has the pages associated with the bottom navigation destinations. Each page has a different title will maintain a separate counter to help demonstrate that each page has its own state 20 | - `services` - has all of the application services used e.g. a service for managing shared preferences 21 | - `stores` - has all of the stores associated with each page/destination. There is a separate `DestinationsStore` defined in the `destinations_store.dart` file that maintains the selected destination 22 | - `main.dart` - the entry point for the application and contains the top-level widgets for the application itself 23 | - `test` - 24 | - `mocks` - for storing mocks that will be used by various tests 25 | - `stores` - this folder has unit tests for all of the stores 26 | - `widgets` - this folder stores all of the widget tests 27 | - `pages` - this folder that has files represent widget tests that are tied to pages 28 | - `app_test.dart` - for doing tests on widgets that are at the top-level e.g. checking bottom navigation bar exists and works 29 | - `test_driver` - this folder stores all of the integration tests. Makes use of the `flutter_driver` library 30 | 31 | More details on how state is managed and how the application looks can be found in [this blog post](https://dexterx.dev/flutter-application-templates/) 32 | 33 | ## Executing unit tests and widget tests 34 | 35 | To execute unit tests, run the following command from the root folder of the application 36 | 37 | ``` 38 | flutter test 39 | ``` 40 | 41 | ## Executing integration tests 42 | 43 | To execute integration tests, run the following command from the root folder of the application 44 | 45 | ``` 46 | flutter drive --target=test_driver/app.dart 47 | ``` 48 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/constants/enums.dart: -------------------------------------------------------------------------------- 1 | enum Destination { Home, Dashboard, Notifications, Settings } 2 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/constants/keys.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class Keys { 4 | static const incrementButtonKey = Key('incrementButton'); 5 | static const dashboardPageKey = Key('dashboardPage'); 6 | static const dashboardPageTitleKey = Key('dashboardPageTitle'); 7 | static const dashboardPageCounterKey = Key('dashboardPageCounter'); 8 | static const homePageKey = Key('homePage'); 9 | static const homePageTitleKey = Key('homePageTitle'); 10 | static const homePageCounterKey = Key('homePageCounter'); 11 | static const notificationsPageKey = Key('notificationsPage'); 12 | static const notificationsPageTitleKey = Key('notificationsPageTitle'); 13 | static const notificationsPageCounterKey = Key('notificationsPageCounter'); 14 | static const settingsDrawerItemKey = Key('settingsDrawerItem'); 15 | static const settingsPageKey = Key('settingsPage'); 16 | static const settingsPageTitleKey = Key('settingsPageTitle'); 17 | static const useDarkModeSettingKey = Key('useDarkModeSetting'); 18 | } 19 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:flutter_mobx/flutter_mobx.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | import 'constants/enums.dart'; 6 | import 'constants/keys.dart'; 7 | import 'pages/dashboard_page.dart'; 8 | import 'pages/home_page.dart'; 9 | import 'pages/notifications_page.dart'; 10 | import 'pages/settings_page.dart'; 11 | import 'services/preferences_service.dart'; 12 | import 'stores/dashboard_store.dart'; 13 | import 'stores/destinations_store.dart'; 14 | import 'stores/home_store.dart'; 15 | import 'stores/notifications_store.dart'; 16 | import 'stores/settings_store.dart'; 17 | 18 | Future main() async { 19 | WidgetsFlutterBinding.ensureInitialized(); 20 | final sharedPreferences = await SharedPreferences.getInstance(); 21 | runApp(App(sharedPreferences)); 22 | } 23 | 24 | class App extends StatelessWidget { 25 | const App(this.sharedPreferences, {Key key}) : super(key: key); 26 | 27 | final SharedPreferences sharedPreferences; 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return MultiProvider( 32 | providers: [ 33 | Provider( 34 | create: (_) => DestinationsStore(), 35 | ), 36 | Provider( 37 | create: (_) => HomeStore(), 38 | ), 39 | Provider( 40 | create: (_) => DashboardStore(), 41 | ), 42 | Provider( 43 | create: (_) => NotificationsStore(), 44 | ), 45 | Provider( 46 | create: (_) => PreferencesService(sharedPreferences), 47 | ), 48 | ProxyProvider( 49 | update: (_, preferencesService, __) => 50 | SettingsStore(preferencesService), 51 | ), 52 | ], 53 | child: Consumer( 54 | builder: (context, store, _) { 55 | return Observer( 56 | builder: (_) { 57 | return MaterialApp( 58 | title: 'App title', 59 | theme: store.useDarkMode ? ThemeData.dark() : ThemeData.light(), 60 | home: Consumer( 61 | builder: (context, store, _) { 62 | return Observer( 63 | builder: (_) { 64 | return Scaffold( 65 | appBar: AppBar( 66 | title: AppBarTitle(store.selectedDestination), 67 | ), 68 | body: SafeArea( 69 | child: PageContainer( 70 | store.selectedDestination, 71 | ), 72 | ), 73 | bottomNavigationBar: AppBottomNavigationBar(store), 74 | floatingActionButton: store.selectedDestination == 75 | Destination.Settings 76 | ? null 77 | : FloatingActionButton( 78 | key: Keys.incrementButtonKey, 79 | onPressed: () { 80 | switch (store.selectedDestination) { 81 | case Destination.Home: 82 | Provider.of(context, 83 | listen: false) 84 | .increment(); 85 | break; 86 | case Destination.Dashboard: 87 | Provider.of(context, 88 | listen: false) 89 | .increment(); 90 | break; 91 | case Destination.Notifications: 92 | Provider.of(context, 93 | listen: false) 94 | .increment(); 95 | break; 96 | case Destination.Settings: 97 | break; 98 | } 99 | }, 100 | child: const Icon(Icons.add), 101 | ), 102 | ); 103 | }, 104 | ); 105 | }, 106 | ), 107 | ); 108 | }, 109 | ); 110 | }, 111 | ), 112 | ); 113 | } 114 | } 115 | 116 | class AppBarTitle extends StatelessWidget { 117 | const AppBarTitle(this.destination, {Key key}) : super(key: key); 118 | 119 | final Destination destination; 120 | 121 | @override 122 | Widget build(BuildContext context) { 123 | switch (destination) { 124 | case Destination.Dashboard: 125 | return const Text('Dashboard', key: Keys.dashboardPageTitleKey); 126 | case Destination.Notifications: 127 | return const Text('Notifications', key: Keys.notificationsPageTitleKey); 128 | case Destination.Settings: 129 | return const Text('Settings', key: Keys.settingsPageTitleKey); 130 | default: 131 | return const Text('Home', key: Keys.homePageTitleKey); 132 | } 133 | } 134 | } 135 | 136 | class AppBottomNavigationBar extends StatelessWidget { 137 | const AppBottomNavigationBar( 138 | this.store, { 139 | Key key, 140 | }) : super(key: key); 141 | 142 | final DestinationsStore store; 143 | 144 | @override 145 | Widget build(BuildContext context) { 146 | return BottomNavigationBar( 147 | key: const Key('bottomNavigationBar'), 148 | selectedItemColor: Colors.blue, 149 | unselectedItemColor: Colors.grey, 150 | currentIndex: store.selectedDestinationIndex, 151 | items: DestinationsStoreBase.destinations.map( 152 | (option) { 153 | switch (option) { 154 | case Destination.Home: 155 | return const BottomNavigationBarItem( 156 | icon: Icon(Icons.home), 157 | title: Text('Home'), 158 | ); 159 | case Destination.Dashboard: 160 | return const BottomNavigationBarItem( 161 | icon: Icon(Icons.dashboard), 162 | title: Text('Dashboard'), 163 | ); 164 | case Destination.Notifications: 165 | return const BottomNavigationBarItem( 166 | icon: Icon(Icons.notifications), 167 | title: Text('Notifications'), 168 | ); 169 | case Destination.Settings: 170 | return const BottomNavigationBarItem( 171 | icon: Icon(Icons.settings), 172 | title: Text('Settings'), 173 | ); 174 | default: 175 | return null; 176 | } 177 | }, 178 | ).toList(), 179 | onTap: (index) => store.selectDestination(index), 180 | ); 181 | } 182 | } 183 | 184 | class PageContainer extends StatelessWidget { 185 | const PageContainer(this.destination, {Key key}) : super(key: key); 186 | 187 | final Destination destination; 188 | 189 | @override 190 | Widget build(BuildContext context) { 191 | switch (destination) { 192 | case Destination.Dashboard: 193 | return Consumer( 194 | builder: (_, store, __) => 195 | DashboardPage(store, key: Keys.dashboardPageKey), 196 | ); 197 | case Destination.Notifications: 198 | return Consumer( 199 | builder: (_, store, __) => 200 | NotificationsPage(store, key: Keys.notificationsPageKey), 201 | ); 202 | case Destination.Settings: 203 | return Consumer( 204 | builder: (_, store, __) => 205 | SettingsPage(store, key: Keys.settingsPageKey), 206 | ); 207 | default: 208 | return Consumer( 209 | builder: (_, store, __) => HomePage(store, key: Keys.homePageKey), 210 | ); 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/pages/dashboard_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import '../constants/keys.dart'; 4 | import '../stores/dashboard_store.dart'; 5 | 6 | class DashboardPage extends StatelessWidget { 7 | const DashboardPage(this.store, {Key key}) : super(key: key); 8 | 9 | final DashboardStore store; 10 | @override 11 | Widget build(BuildContext context) { 12 | return Observer( 13 | builder: (context) { 14 | return Center( 15 | child: Text( 16 | 'You have pushed the button on this page ${store.counter} time(s)', 17 | key: Keys.dashboardPageCounterKey, 18 | ), 19 | ); 20 | }, 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/pages/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import '../constants/keys.dart'; 4 | import '../stores/home_store.dart'; 5 | 6 | class HomePage extends StatelessWidget { 7 | const HomePage(this.store, {Key key}) : super(key: key); 8 | 9 | final HomeStore store; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Observer( 14 | builder: (context) { 15 | return Center( 16 | child: Text( 17 | 'You have pushed the button on this page ${store.counter} time(s)', 18 | key: Keys.homePageCounterKey, 19 | ), 20 | ); 21 | }, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/pages/notifications_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import '../constants/keys.dart'; 4 | import '../stores/notifications_store.dart'; 5 | 6 | class NotificationsPage extends StatelessWidget { 7 | const NotificationsPage(this.store, {Key key}) : super(key: key); 8 | 9 | final NotificationsStore store; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Observer( 14 | builder: (context) { 15 | return Center( 16 | child: Text( 17 | 'You have pushed the button on this page ${store.counter} time(s)', 18 | key: Keys.notificationsPageCounterKey, 19 | ), 20 | ); 21 | }, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/pages/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import '../constants/keys.dart'; 4 | import '../stores/settings_store.dart'; 5 | 6 | class SettingsPage extends StatelessWidget { 7 | const SettingsPage(this.store, {Key key}) : super(key: key); 8 | 9 | final SettingsStore store; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Observer( 14 | builder: (context) { 15 | return SwitchListTile( 16 | key: Keys.useDarkModeSettingKey, 17 | value: store.useDarkMode, 18 | title: const Text('Use dark mode'), 19 | onChanged: (value) { 20 | store.setDarkMode(value); 21 | }, 22 | ); 23 | }, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/services/preferences_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | class PreferencesService { 4 | const PreferencesService(this._sharedPreferences); 5 | 6 | final String _useDarkModeKey = 'useDarkMode'; 7 | final SharedPreferences _sharedPreferences; 8 | 9 | set useDarkMode(bool useDarkMode) { 10 | _sharedPreferences.setBool(_useDarkModeKey, useDarkMode); 11 | } 12 | 13 | bool get useDarkMode => _sharedPreferences.getBool(_useDarkModeKey) ?? false; 14 | } 15 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/stores/dashboard_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | 3 | part 'dashboard_store.g.dart'; 4 | 5 | class DashboardStore = DashboardStoreBase with _$DashboardStore; 6 | 7 | abstract class DashboardStoreBase with Store { 8 | @observable 9 | int counter = 0; 10 | 11 | @action 12 | void increment() { 13 | counter++; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/stores/dashboard_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'dashboard_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars 10 | 11 | mixin _$DashboardStore on DashboardStoreBase, Store { 12 | final _$counterAtom = Atom(name: 'DashboardStoreBase.counter'); 13 | 14 | @override 15 | int get counter { 16 | _$counterAtom.context.enforceReadPolicy(_$counterAtom); 17 | _$counterAtom.reportObserved(); 18 | return super.counter; 19 | } 20 | 21 | @override 22 | set counter(int value) { 23 | _$counterAtom.context.conditionallyRunInAction(() { 24 | super.counter = value; 25 | _$counterAtom.reportChanged(); 26 | }, _$counterAtom, name: '${_$counterAtom.name}_set'); 27 | } 28 | 29 | final _$DashboardStoreBaseActionController = 30 | ActionController(name: 'DashboardStoreBase'); 31 | 32 | @override 33 | void increment() { 34 | final _$actionInfo = _$DashboardStoreBaseActionController.startAction(); 35 | try { 36 | return super.increment(); 37 | } finally { 38 | _$DashboardStoreBaseActionController.endAction(_$actionInfo); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/stores/destinations_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | import '../constants/enums.dart'; 3 | part 'destinations_store.g.dart'; 4 | 5 | class DestinationsStore = DestinationsStoreBase with _$DestinationsStore; 6 | 7 | abstract class DestinationsStoreBase with Store { 8 | static const List destinations = Destination.values; 9 | 10 | @observable 11 | int selectedDestinationIndex = destinations.indexOf(Destination.Home); 12 | 13 | @computed 14 | Destination get selectedDestination => destinations[selectedDestinationIndex]; 15 | 16 | @action 17 | void selectDestination(int index) { 18 | selectedDestinationIndex = index; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/stores/destinations_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'destinations_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars 10 | 11 | mixin _$DestinationsStore on DestinationsStoreBase, Store { 12 | Computed _$selectedDestinationComputed; 13 | 14 | @override 15 | Destination get selectedDestination => (_$selectedDestinationComputed ??= 16 | Computed(() => super.selectedDestination)) 17 | .value; 18 | 19 | final _$selectedDestinationIndexAtom = 20 | Atom(name: 'DestinationsStoreBase.selectedDestinationIndex'); 21 | 22 | @override 23 | int get selectedDestinationIndex { 24 | _$selectedDestinationIndexAtom.context 25 | .enforceReadPolicy(_$selectedDestinationIndexAtom); 26 | _$selectedDestinationIndexAtom.reportObserved(); 27 | return super.selectedDestinationIndex; 28 | } 29 | 30 | @override 31 | set selectedDestinationIndex(int value) { 32 | _$selectedDestinationIndexAtom.context.conditionallyRunInAction(() { 33 | super.selectedDestinationIndex = value; 34 | _$selectedDestinationIndexAtom.reportChanged(); 35 | }, _$selectedDestinationIndexAtom, 36 | name: '${_$selectedDestinationIndexAtom.name}_set'); 37 | } 38 | 39 | final _$DestinationsStoreBaseActionController = 40 | ActionController(name: 'DestinationsStoreBase'); 41 | 42 | @override 43 | void selectDestination(int index) { 44 | final _$actionInfo = _$DestinationsStoreBaseActionController.startAction(); 45 | try { 46 | return super.selectDestination(index); 47 | } finally { 48 | _$DestinationsStoreBaseActionController.endAction(_$actionInfo); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/stores/home_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | 3 | part 'home_store.g.dart'; 4 | 5 | class HomeStore = HomeStoreBase with _$HomeStore; 6 | 7 | abstract class HomeStoreBase with Store { 8 | @observable 9 | int counter = 0; 10 | 11 | @action 12 | void increment() { 13 | counter++; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/stores/home_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'home_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars 10 | 11 | mixin _$HomeStore on HomeStoreBase, Store { 12 | final _$counterAtom = Atom(name: 'HomeStoreBase.counter'); 13 | 14 | @override 15 | int get counter { 16 | _$counterAtom.context.enforceReadPolicy(_$counterAtom); 17 | _$counterAtom.reportObserved(); 18 | return super.counter; 19 | } 20 | 21 | @override 22 | set counter(int value) { 23 | _$counterAtom.context.conditionallyRunInAction(() { 24 | super.counter = value; 25 | _$counterAtom.reportChanged(); 26 | }, _$counterAtom, name: '${_$counterAtom.name}_set'); 27 | } 28 | 29 | final _$HomeStoreBaseActionController = 30 | ActionController(name: 'HomeStoreBase'); 31 | 32 | @override 33 | void increment() { 34 | final _$actionInfo = _$HomeStoreBaseActionController.startAction(); 35 | try { 36 | return super.increment(); 37 | } finally { 38 | _$HomeStoreBaseActionController.endAction(_$actionInfo); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/stores/notifications_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | 3 | part 'notifications_store.g.dart'; 4 | 5 | class NotificationsStore = NotificationsStoreBase with _$NotificationsStore; 6 | 7 | abstract class NotificationsStoreBase with Store { 8 | @observable 9 | int counter = 0; 10 | 11 | @action 12 | void increment() { 13 | counter++; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/stores/notifications_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'notifications_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars 10 | 11 | mixin _$NotificationsStore on NotificationsStoreBase, Store { 12 | final _$counterAtom = Atom(name: 'NotificationsStoreBase.counter'); 13 | 14 | @override 15 | int get counter { 16 | _$counterAtom.context.enforceReadPolicy(_$counterAtom); 17 | _$counterAtom.reportObserved(); 18 | return super.counter; 19 | } 20 | 21 | @override 22 | set counter(int value) { 23 | _$counterAtom.context.conditionallyRunInAction(() { 24 | super.counter = value; 25 | _$counterAtom.reportChanged(); 26 | }, _$counterAtom, name: '${_$counterAtom.name}_set'); 27 | } 28 | 29 | final _$NotificationsStoreBaseActionController = 30 | ActionController(name: 'NotificationsStoreBase'); 31 | 32 | @override 33 | void increment() { 34 | final _$actionInfo = _$NotificationsStoreBaseActionController.startAction(); 35 | try { 36 | return super.increment(); 37 | } finally { 38 | _$NotificationsStoreBaseActionController.endAction(_$actionInfo); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/stores/settings_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | import '../services/preferences_service.dart'; 3 | part 'settings_store.g.dart'; 4 | 5 | class SettingsStore = SettingsStoreBase with _$SettingsStore; 6 | 7 | abstract class SettingsStoreBase with Store { 8 | /// when the store is created, we read in the current settings immediately to avoid the scenario where 9 | /// the values displayed will change upon switching to the settings tab 10 | SettingsStoreBase(this._preferencesService) { 11 | useDarkMode = _preferencesService.useDarkMode; 12 | } 13 | 14 | PreferencesService _preferencesService; 15 | 16 | @observable 17 | bool useDarkMode; 18 | 19 | @action 20 | void setDarkMode(bool updatedDarkModePreference) { 21 | _preferencesService.useDarkMode = updatedDarkModePreference; 22 | useDarkMode = updatedDarkModePreference; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/lib/stores/settings_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'settings_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars 10 | 11 | mixin _$SettingsStore on SettingsStoreBase, Store { 12 | final _$useDarkModeAtom = Atom(name: 'SettingsStoreBase.useDarkMode'); 13 | 14 | @override 15 | bool get useDarkMode { 16 | _$useDarkModeAtom.context.enforceReadPolicy(_$useDarkModeAtom); 17 | _$useDarkModeAtom.reportObserved(); 18 | return super.useDarkMode; 19 | } 20 | 21 | @override 22 | set useDarkMode(bool value) { 23 | _$useDarkModeAtom.context.conditionallyRunInAction(() { 24 | super.useDarkMode = value; 25 | _$useDarkModeAtom.reportChanged(); 26 | }, _$useDarkModeAtom, name: '${_$useDarkModeAtom.name}_set'); 27 | } 28 | 29 | final _$SettingsStoreBaseActionController = 30 | ActionController(name: 'SettingsStoreBase'); 31 | 32 | @override 33 | void setDarkMode(bool updatedDarkModePreference) { 34 | final _$actionInfo = _$SettingsStoreBaseActionController.startAction(); 35 | try { 36 | return super.setDarkMode(updatedDarkModePreference); 37 | } finally { 38 | _$SettingsStoreBaseActionController.endAction(_$actionInfo); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: material_bottomnavigation_provider_mobx 2 | description: A new Flutter project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.2.0 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | 23 | # The following adds the Cupertino Icons font to your application. 24 | # Use with the CupertinoIcons class for iOS style icons. 25 | cupertino_icons: 26 | 27 | provider: 28 | mobx: 29 | flutter_mobx: 30 | shared_preferences: 31 | 32 | dev_dependencies: 33 | flutter_driver: 34 | sdk: flutter 35 | flutter_test: 36 | sdk: flutter 37 | test: 38 | build_runner: 1.10.1 39 | mobx_codegen: 40 | mockito: 41 | 42 | 43 | # For information on the generic Dart part of this file, see the 44 | # following page: https://www.dartlang.org/tools/pub/pubspec 45 | 46 | # The following section is specific to Flutter. 47 | flutter: 48 | 49 | # The following line ensures that the Material Icons font is 50 | # included with your application, so that you can use the icons in 51 | # the material Icons class. 52 | uses-material-design: true 53 | 54 | # To add assets to your application, add an assets section, like this: 55 | # assets: 56 | # - images/a_dot_burr.jpeg 57 | # - images/a_dot_ham.jpeg 58 | 59 | # An image asset can refer to one or more resolution-specific "variants", see 60 | # https://flutter.dev/assets-and-images/#resolution-aware. 61 | 62 | # For details regarding adding assets from package dependencies, see 63 | # https://flutter.dev/assets-and-images/#from-packages 64 | 65 | # To add custom fonts to your application, add a fonts section here, 66 | # in this "flutter" section. Each entry in this list should have a 67 | # "family" key with the font family name, and a "fonts" key with a 68 | # list giving the asset and other descriptors for the font. For 69 | # example: 70 | # fonts: 71 | # - family: Schyler 72 | # fonts: 73 | # - asset: fonts/Schyler-Regular.ttf 74 | # - asset: fonts/Schyler-Italic.ttf 75 | # style: italic 76 | # - family: Trajan Pro 77 | # fonts: 78 | # - asset: fonts/TrajanPro.ttf 79 | # - asset: fonts/TrajanPro_Bold.ttf 80 | # weight: 700 81 | # 82 | # For details regarding fonts from package dependencies, 83 | # see https://flutter.dev/custom-fonts/#from-packages 84 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/test/mocks/mock_shared_preferences.dart: -------------------------------------------------------------------------------- 1 | import 'package:mockito/mockito.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | class MockedSharedPreferences extends Mock implements SharedPreferences {} 5 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/test/stores/dashboard_store_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:material_bottomnavigation_provider_mobx/stores/dashboard_store.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('Dashboard store', () { 6 | DashboardStore store; 7 | setUpAll(() { 8 | store = DashboardStore(); 9 | }); 10 | test('counter starts at 0', () { 11 | expect(store.counter, 0); 12 | }); 13 | test('increment works', () { 14 | store.increment(); 15 | expect(store.counter, 1); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/test/stores/destinations_store_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:material_bottomnavigation_provider_mobx/constants/enums.dart'; 2 | import 'package:material_bottomnavigation_provider_mobx/stores/destinations_store.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('Destinations store', () { 7 | DestinationsStore store; 8 | setUpAll(() { 9 | store = DestinationsStore(); 10 | }); 11 | test('starts at home', () { 12 | expect(store.selectedDestinationIndex, 0); 13 | expect(store.selectedDestination, Destination.Home); 14 | }); 15 | test('select dashboard destination works', () { 16 | store.selectDestination(1); 17 | expect(store.selectedDestinationIndex, 1); 18 | expect(store.selectedDestination, Destination.Dashboard); 19 | }); 20 | test('select notifications destination works', () { 21 | store.selectDestination(2); 22 | expect(store.selectedDestinationIndex, 2); 23 | expect(store.selectedDestination, Destination.Notifications); 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/test/stores/home_store_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:material_bottomnavigation_provider_mobx/stores/home_store.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('Home store', () { 6 | HomeStore store; 7 | setUpAll(() { 8 | store = HomeStore(); 9 | }); 10 | test('counter starts at 0', () { 11 | expect(store.counter, 0); 12 | }); 13 | test('increment works', () { 14 | store.increment(); 15 | expect(store.counter, 1); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/test/stores/notifications_store_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:material_bottomnavigation_provider_mobx/stores/notifications_store.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('Notifications store', () { 6 | NotificationsStore store; 7 | setUpAll(() { 8 | store = NotificationsStore(); 9 | }); 10 | test('counter starts at 0', () { 11 | expect(store.counter, 0); 12 | }); 13 | test('increment works', () { 14 | store.increment(); 15 | expect(store.counter, 1); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/test/stores/settings_store_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:material_bottomnavigation_provider_mobx/services/preferences_service.dart'; 2 | import 'package:material_bottomnavigation_provider_mobx/stores/settings_store.dart'; 3 | import 'package:test/test.dart'; 4 | import '../mocks/mock_shared_preferences.dart'; 5 | 6 | void main() { 7 | group('SlideshowStore', () { 8 | SettingsStore store; 9 | setUpAll(() { 10 | store = SettingsStore(PreferencesService(MockedSharedPreferences())); 11 | }); 12 | test('Use dark mode settings default to off', () { 13 | expect(store.useDarkMode, false); 14 | }); 15 | test('Turning on dark mode works', () { 16 | store.setDarkMode(true); 17 | expect(store.useDarkMode, true); 18 | }); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/test/widgets/app_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:material_bottomnavigation_provider_mobx/constants/keys.dart'; 4 | import 'package:material_bottomnavigation_provider_mobx/main.dart'; 5 | import 'package:material_bottomnavigation_provider_mobx/pages/dashboard_page.dart'; 6 | import 'package:material_bottomnavigation_provider_mobx/pages/home_page.dart'; 7 | import 'package:material_bottomnavigation_provider_mobx/pages/notifications_page.dart'; 8 | import 'package:material_bottomnavigation_provider_mobx/pages/settings_page.dart'; 9 | import '../mocks/mock_shared_preferences.dart'; 10 | 11 | void main() { 12 | group('App', () { 13 | final homePageFinder = find.byType(HomePage); 14 | final dashboardPageFinder = find.byType(DashboardPage); 15 | final notificationsPageFinder = find.byType(NotificationsPage); 16 | final settingsPageFinder = find.byType(SettingsPage); 17 | final homeIconFinder = find.byIcon(Icons.home); 18 | final dashboardIconFinder = find.byIcon(Icons.dashboard); 19 | final notificationsIconFinder = find.byIcon(Icons.notifications); 20 | final settingsIconFinder = find.byIcon(Icons.settings); 21 | final incrementButtonFinder = find.byType(FloatingActionButton); 22 | final mockedSharedPreferences = MockedSharedPreferences(); 23 | 24 | testWidgets('Starts on home page', (WidgetTester tester) async { 25 | await tester.pumpWidget(App(mockedSharedPreferences)); 26 | expect(homePageFinder, findsOneWidget); 27 | }); 28 | 29 | testWidgets('Bottom navigation bar exists', (WidgetTester tester) async { 30 | await _pumpWidget(tester, mockedSharedPreferences); 31 | expect(find.byType(BottomNavigationBar), findsOneWidget); 32 | expect(homeIconFinder, findsOneWidget); 33 | expect(dashboardIconFinder, findsOneWidget); 34 | expect(notificationsIconFinder, findsOneWidget); 35 | expect(settingsIconFinder, findsOneWidget); 36 | }); 37 | 38 | testWidgets('Floating action button exists', (WidgetTester tester) async { 39 | await _pumpWidget(tester, mockedSharedPreferences); 40 | expect(incrementButtonFinder, findsOneWidget); 41 | }); 42 | 43 | testWidgets('Select home page', (WidgetTester tester) async { 44 | await _pumpWidget(tester, mockedSharedPreferences); 45 | 46 | // select home page 47 | await tester.tap(homeIconFinder); 48 | await tester.pump(); 49 | 50 | // should now be on gallery page 51 | expect(homePageFinder, findsOneWidget); 52 | expect(dashboardPageFinder, findsNothing); 53 | expect(notificationsPageFinder, findsNothing); 54 | expect(settingsPageFinder, findsNothing); 55 | expect(find.byKey(Keys.homePageTitleKey), findsOneWidget); 56 | expect(find.text('You have pushed the button on this page 0 time(s)'), 57 | findsOneWidget); 58 | expect(incrementButtonFinder, findsOneWidget); 59 | }); 60 | 61 | testWidgets('Select dashboard page', (WidgetTester tester) async { 62 | await _pumpWidget(tester, mockedSharedPreferences); 63 | 64 | // select dashboard page 65 | await tester.tap(dashboardIconFinder); 66 | await tester.pump(); 67 | 68 | // should now be on dashboard page 69 | expect(homePageFinder, findsNothing); 70 | expect(dashboardPageFinder, findsOneWidget); 71 | expect(notificationsPageFinder, findsNothing); 72 | expect(settingsPageFinder, findsNothing); 73 | expect(find.byKey(Keys.dashboardPageTitleKey), findsOneWidget); 74 | expect(find.text('You have pushed the button on this page 0 time(s)'), 75 | findsOneWidget); 76 | expect(incrementButtonFinder, findsOneWidget); 77 | }); 78 | 79 | testWidgets('Select notifications page', (WidgetTester tester) async { 80 | await _pumpWidget(tester, mockedSharedPreferences); 81 | 82 | // select notifications page 83 | await tester.tap(notificationsIconFinder); 84 | await tester.pump(); 85 | 86 | // should now be on notifications page 87 | expect(homePageFinder, findsNothing); 88 | expect(dashboardPageFinder, findsNothing); 89 | expect(notificationsPageFinder, findsOneWidget); 90 | expect(settingsPageFinder, findsNothing); 91 | expect(find.byKey(Keys.notificationsPageTitleKey), findsOneWidget); 92 | expect(find.text('You have pushed the button on this page 0 time(s)'), 93 | findsOneWidget); 94 | expect(incrementButtonFinder, findsOneWidget); 95 | }); 96 | 97 | testWidgets('Select settings page', (WidgetTester tester) async { 98 | await _pumpWidget(tester, mockedSharedPreferences); 99 | 100 | // select settings page 101 | await tester.tap(settingsIconFinder); 102 | await tester.pumpAndSettle(); 103 | 104 | // should now be on settings page 105 | expect(homePageFinder, findsNothing); 106 | expect(dashboardPageFinder, findsNothing); 107 | expect(notificationsPageFinder, findsNothing); 108 | expect(settingsPageFinder, findsOneWidget); 109 | expect(find.byKey(Keys.settingsPageKey), findsOneWidget); 110 | expect(find.byType(SwitchListTile), findsOneWidget); 111 | }); 112 | }); 113 | } 114 | 115 | Future _pumpWidget(WidgetTester tester, 116 | MockedSharedPreferences mockedSharedPreferences) async { 117 | await tester.pumpWidget(App(mockedSharedPreferences)); 118 | } 119 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/test/widgets/pages/dashboard_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:material_bottomnavigation_provider_mobx/pages/dashboard_page.dart'; 4 | import 'package:material_bottomnavigation_provider_mobx/stores/dashboard_store.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | void main() { 8 | group('DashboardPage', () { 9 | final pageFinder = find.byType(DashboardPage); 10 | final store = DashboardStore(); 11 | testWidgets('Counter starts at zero', (WidgetTester tester) async { 12 | await _pumpWidget(tester, store); 13 | 14 | expect(pageFinder, findsOneWidget); 15 | expect(find.text('You have pushed the button on this page 0 time(s)'), 16 | findsOneWidget); 17 | }); 18 | 19 | testWidgets('Counter updates', (WidgetTester tester) async { 20 | await _pumpWidget(tester, store); 21 | 22 | store.increment(); 23 | await tester.pump(); 24 | expect(find.text('You have pushed the button on this page 1 time(s)'), 25 | findsOneWidget); 26 | }); 27 | }); 28 | } 29 | 30 | Future _pumpWidget(WidgetTester tester, DashboardStore store) async { 31 | await tester.pumpWidget( 32 | Provider( 33 | create: (_) => store, 34 | child: MaterialApp( 35 | home: Consumer( 36 | builder: (_, store, __) => DashboardPage(store), 37 | ), 38 | ), 39 | ), 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/test/widgets/pages/home_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:material_bottomnavigation_provider_mobx/pages/home_page.dart'; 4 | import 'package:material_bottomnavigation_provider_mobx/stores/home_store.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | void main() { 8 | group('HomePage', () { 9 | final pageFinder = find.byType(HomePage); 10 | final store = HomeStore(); 11 | 12 | testWidgets('Counter starts at zero', (WidgetTester tester) async { 13 | await _pumpWidget(tester, store); 14 | 15 | expect(pageFinder, findsOneWidget); 16 | expect(find.text('You have pushed the button on this page 0 time(s)'), 17 | findsOneWidget); 18 | }); 19 | 20 | testWidgets('Counter updates', (WidgetTester tester) async { 21 | await _pumpWidget(tester, store); 22 | 23 | store.increment(); 24 | await tester.pump(); 25 | expect(find.text('You have pushed the button on this page 1 time(s)'), 26 | findsOneWidget); 27 | }); 28 | }); 29 | } 30 | 31 | Future _pumpWidget(WidgetTester tester, HomeStore store) async { 32 | await tester.pumpWidget( 33 | Provider( 34 | create: (_) => store, 35 | child: MaterialApp( 36 | home: Consumer( 37 | builder: (_, store, __) => HomePage(store), 38 | ), 39 | ), 40 | ), 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/test/widgets/pages/notifications_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:material_bottomnavigation_provider_mobx/pages/notifications_page.dart'; 4 | import 'package:material_bottomnavigation_provider_mobx/stores/notifications_store.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | void main() { 8 | group('NotificationsPage', () { 9 | final pageFinder = find.byType(NotificationsPage); 10 | final store = NotificationsStore(); 11 | 12 | testWidgets('Counter starts at zero', (WidgetTester tester) async { 13 | await _pumpWidget(tester, store); 14 | 15 | expect(pageFinder, findsOneWidget); 16 | expect(find.text('You have pushed the button on this page 0 time(s)'), 17 | findsOneWidget); 18 | }); 19 | 20 | testWidgets('Counter updates', (WidgetTester tester) async { 21 | await _pumpWidget(tester, store); 22 | 23 | store.increment(); 24 | await tester.pump(); 25 | expect(find.text('You have pushed the button on this page 1 time(s)'), 26 | findsOneWidget); 27 | }); 28 | }); 29 | } 30 | 31 | Future _pumpWidget(WidgetTester tester, NotificationsStore store) async { 32 | await tester.pumpWidget( 33 | Provider( 34 | create: (_) => store, 35 | child: MaterialApp( 36 | home: Consumer( 37 | builder: (_, store, __) => NotificationsPage(store), 38 | ), 39 | ), 40 | ), 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/test/widgets/pages/settings_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:material_bottomnavigation_provider_mobx/constants/keys.dart'; 4 | import 'package:material_bottomnavigation_provider_mobx/pages/settings_page.dart'; 5 | import 'package:material_bottomnavigation_provider_mobx/services/preferences_service.dart'; 6 | import 'package:material_bottomnavigation_provider_mobx/stores/settings_store.dart'; 7 | import 'package:provider/provider.dart'; 8 | import '../../mocks/mock_shared_preferences.dart'; 9 | 10 | void main() { 11 | group('SettingsPage', () { 12 | final mockedSharedPreferences = MockedSharedPreferences(); 13 | final preferencesService = PreferencesService(mockedSharedPreferences); 14 | final store = SettingsStore(preferencesService); 15 | final pageFinder = find.byType(SettingsPage); 16 | final useDarkModeSettingFinder = find.byKey(Keys.useDarkModeSettingKey); 17 | testWidgets('Dark mode setting starts at off', (WidgetTester tester) async { 18 | await _pumpWidget(tester, store); 19 | 20 | expect(pageFinder, findsOneWidget); 21 | expect(useDarkModeSettingFinder, findsOneWidget); 22 | expect(store.useDarkMode, false); 23 | }); 24 | 25 | testWidgets('Turn dark mode on', (WidgetTester tester) async { 26 | await _pumpWidget(tester, store); 27 | 28 | await tester.tap(useDarkModeSettingFinder); 29 | await tester.pumpAndSettle(); 30 | expect(store.useDarkMode, true); 31 | }); 32 | }); 33 | } 34 | 35 | Future _pumpWidget(WidgetTester tester, SettingsStore store) async { 36 | await tester.pumpWidget( 37 | Provider( 38 | create: (_) => store, 39 | child: MaterialApp( 40 | home: Scaffold( 41 | body: Consumer( 42 | builder: (_, store, __) => SettingsPage(store), 43 | ), 44 | ), 45 | ), 46 | ), 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/test_driver/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_driver/driver_extension.dart'; 2 | import 'package:material_bottomnavigation_provider_mobx/main.dart' as app; 3 | 4 | void main() { 5 | // This line enables the extension. 6 | enableFlutterDriverExtension(); 7 | 8 | // Call the `main()` function of the app, or call `runApp` with 9 | // any widget you are interested in testing. 10 | app.main(); 11 | } 12 | -------------------------------------------------------------------------------- /material_bottomnavigation_provider_mobx/test_driver/app_test.dart: -------------------------------------------------------------------------------- 1 | // Imports the Flutter Driver API. 2 | import 'package:flutter_driver/flutter_driver.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('App', () { 7 | FlutterDriver driver; 8 | final bottomNavigationBarFinder = find.byValueKey('bottomNavigationBar'); 9 | final incrementButtonFinder = find.byValueKey('incrementButton'); 10 | final homePageFinder = find.byValueKey('homePage'); 11 | final dashboardPageFinder = find.byValueKey('dashboardPage'); 12 | final notificationsPageFinder = find.byValueKey('notificationsPage'); 13 | final settingsPageFinder = find.byValueKey('settingsPage'); 14 | final homePageCounterFinder = find.byValueKey('homePageCounter'); 15 | final dashboardPageCounterFinder = find.byValueKey('dashboardPageCounter'); 16 | final notificationsPageCounterFinder = 17 | find.byValueKey('notificationsPageCounter'); 18 | final useDarkModeSettingFinder = find.byValueKey('useDarkModeSetting'); 19 | 20 | setUpAll(() async { 21 | driver = await FlutterDriver.connect(); 22 | }); 23 | 24 | tearDownAll(() async { 25 | driver?.close(); 26 | }); 27 | 28 | test('check flutter driver health', () async { 29 | final health = await driver.checkHealth(); 30 | print('flutter driver status: ${health.status}'); 31 | }); 32 | 33 | test('starts on home page', () async { 34 | await driver.waitFor(find.byValueKey('homePage')); 35 | }); 36 | 37 | test('selecting pages works', () async { 38 | await _selectPage( 39 | driver, bottomNavigationBarFinder, 'Dashboard', dashboardPageFinder); 40 | await _selectPage(driver, bottomNavigationBarFinder, 'Notifications', 41 | notificationsPageFinder); 42 | await _selectPage( 43 | driver, bottomNavigationBarFinder, 'Settings', settingsPageFinder); 44 | await _selectPage( 45 | driver, bottomNavigationBarFinder, 'Home', homePageFinder); 46 | }); 47 | 48 | test('increment counter on home page', () async { 49 | expect(await driver.getText(homePageCounterFinder), 50 | 'You have pushed the button on this page 0 time(s)'); 51 | await driver.tap(incrementButtonFinder); 52 | expect(await driver.getText(homePageCounterFinder), 53 | 'You have pushed the button on this page 1 time(s)'); 54 | }); 55 | 56 | test('increment counter on dashboard page', () async { 57 | await _selectPage( 58 | driver, bottomNavigationBarFinder, 'Dashboard', dashboardPageFinder); 59 | expect(await driver.getText(dashboardPageCounterFinder), 60 | 'You have pushed the button on this page 0 time(s)'); 61 | await driver.tap(incrementButtonFinder); 62 | await driver.tap(incrementButtonFinder); 63 | expect(await driver.getText(dashboardPageCounterFinder), 64 | 'You have pushed the button on this page 2 time(s)'); 65 | }); 66 | 67 | test('increment counter on notifications page', () async { 68 | await _selectPage(driver, bottomNavigationBarFinder, 'Notifications', 69 | notificationsPageFinder); 70 | expect(await driver.getText(notificationsPageCounterFinder), 71 | 'You have pushed the button on this page 0 time(s)'); 72 | await driver.tap(incrementButtonFinder); 73 | await driver.tap(incrementButtonFinder); 74 | await driver.tap(incrementButtonFinder); 75 | expect(await driver.getText(notificationsPageCounterFinder), 76 | 'You have pushed the button on this page 3 time(s)'); 77 | }); 78 | 79 | test('toggle dark mode', () async { 80 | await _selectPage( 81 | driver, bottomNavigationBarFinder, 'Settings', settingsPageFinder); 82 | await driver.tap(useDarkModeSettingFinder); 83 | }); 84 | 85 | test('the last value of the counters on each page are as expected', 86 | () async { 87 | await _selectPage( 88 | driver, bottomNavigationBarFinder, 'Home', homePageFinder); 89 | expect(await driver.getText(homePageCounterFinder), 90 | 'You have pushed the button on this page 1 time(s)'); 91 | await _selectPage( 92 | driver, bottomNavigationBarFinder, 'Dashboard', dashboardPageFinder); 93 | expect(await driver.getText(dashboardPageCounterFinder), 94 | 'You have pushed the button on this page 2 time(s)'); 95 | await _selectPage(driver, bottomNavigationBarFinder, 'Notifications', 96 | notificationsPageFinder); 97 | expect(await driver.getText(notificationsPageCounterFinder), 98 | 'You have pushed the button on this page 3 time(s)'); 99 | }); 100 | }); 101 | } 102 | 103 | Future _selectPage( 104 | FlutterDriver driver, 105 | SerializableFinder bottomNavigationBarFinder, 106 | String bottomNavigationBarItemText, 107 | SerializableFinder pageFinder) async { 108 | await driver.waitFor(bottomNavigationBarFinder); 109 | await driver.tap(find.text(bottomNavigationBarItemText)); 110 | await driver.waitFor(pageFinder); 111 | } 112 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | .dart_tool/ 24 | .flutter-plugins 25 | .flutter-plugins-dependencies 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | /build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 72 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 7a4c33425ddd78c54aba07d86f3f9a4a0051769b 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/README.md: -------------------------------------------------------------------------------- 1 | # Template: material design application with navigation drawer, provider and MobX 2 | 3 | This is a template for creating a Flutter application that uses material design and has a navigation drawer. Uses [provider](https://github.com/rrousselGit/provider) and [MobX](https://github.com/mobxjs/mobx.dart) to manage state. 4 | 5 | ## Overview 6 | 7 | The application is similar to the one created when creating an Android application that has a navigation drawer. It has four destinations: Home, Gallery, Slideshow and Settings. The latter allows toggling the dark mode theme on and off. This setting is saved using the [shared_preferences](https://github.com/flutter/plugins/tree/master/packages/shared_preferences) plugin. 8 | 9 | ![Image of navigation drawer](https://dexterx.dev/content/images/2019/06/image-4.png) ![Image of home page](https://dexterx.dev/content/images/2019/06/home-5.png) ![Image of slideshow page](https://dexterx.dev/content/images/2019/06/slideshow-2.png) ![Image of gallery page](https://dexterx.dev/content/images/2019/06/gallery-1.png) ![Image of settings page](https://dexterx.dev/content/images/2019/06/settings-2.png) 10 | 11 | Each destination has its own corresponding page and data store. Tapping on the floating action button will increment the counter related to the page that is currently being viewed. The settings page allows toggling on and off the dark theme. An [alternative template](https://github.com/MaikuB/flutter_app_templates/tree/master/material_navigationdrawerroutes_provider_mobx) that uses routes can be found at repository as well. 12 | 13 | The application is structured as follows 14 | 15 | - `lib` 16 | - `constants` - this folder is where all application-level constants are kept 17 | - `enums.dart` - to maintain all the application-level enums. For this application, a `Destination` enum is defined that represents all of the destinations: `Home`, `Gallery`, `Slideshow` and `Settings` 18 | - `keys.dart` - to maintain the keys associated with the various widgets in the application 19 | - `pages` - has the pages associated with the destinations. Each page has a different title will maintain a separate counter to help demonstrate that each page has its own state 20 | - `services` - has all of the application services used e.g. a service for managing shared preferences 21 | - `stores` - has all of the data stores associated with each page/destination 22 | - `main.dart` - the entry point for the application and contains the top-level widgets for the application itself 23 | - `test` 24 | - `mocks` - for storing mocks that will be used by various tests 25 | - `stores` - this folder has unit tests for all of the stores 26 | - `widgets` - this folder contains all of the widget tests 27 | - `pages` - this folder that has files represent widget tests that are tied to pages 28 | - `app_test.dart` - for doing tests on widgets that are at the top-level e.g. checking bottom navigation bar exists and works 29 | - `test_driver` - this folder has all of the integration tests. Makes use of the `flutter_driver` library 30 | 31 | ## Executing unit tests and widget tests 32 | 33 | To execute unit tests, run the following command from the root folder of the application 34 | 35 | ``` 36 | flutter test 37 | ``` 38 | 39 | ## Executing integration tests 40 | 41 | To execute integration tests, run the following command from the root folder of the application 42 | 43 | ``` 44 | flutter drive --target=test_driver/app.dart 45 | ``` 46 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/constants/enums.dart: -------------------------------------------------------------------------------- 1 | enum Destination { Home, Gallery, Slideshow, Settings } 2 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/constants/keys.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class Keys { 4 | static const incrementButtonKey = Key('incrementButton'); 5 | static const homePageKey = Key('homePage'); 6 | static const homeDrawerItemKey = Key('homeDrawerItem'); 7 | static const homePageTitleKey = Key('homePageTitle'); 8 | static const homePageCounterKey = Key('homePageCounter'); 9 | static const galleryPageKey = Key('galleryPage'); 10 | static const galleryDrawerItemKey = Key('galleryDrawerItem'); 11 | static const galleryPageTitleKey = Key('galleryPageTitle'); 12 | static const galleryPageCounterKey = Key('galleryPageCounter'); 13 | static const slideshowPageKey = Key('slideshowPage'); 14 | static const slideshowDrawerItemKey = Key('slideshowDrawerItem'); 15 | static const slideshowPageTitleKey = Key('slideshowPageTitle'); 16 | static const slideshowPageCounterKey = Key('slideshowPageCounter'); 17 | static const settingsDrawerItemKey = Key('settingsDrawerItem'); 18 | static const settingsPageKey = Key('settingsPage'); 19 | static const settingsPageTitleKey = Key('settingsPageTitle'); 20 | static const useDarkModeSettingKey = Key('useDarkModeSetting'); 21 | } 22 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | import 'constants/enums.dart'; 6 | import 'constants/keys.dart'; 7 | import 'pages/gallery_page.dart'; 8 | import 'pages/home_page.dart'; 9 | import 'pages/settings_page.dart'; 10 | import 'pages/slideshow_page.dart'; 11 | import 'services/preferences_service.dart'; 12 | import 'stores/destinations_store.dart'; 13 | import 'stores/gallery_store.dart'; 14 | import 'stores/home_store.dart'; 15 | import 'stores/settings_store.dart'; 16 | import 'stores/slideshow_store.dart'; 17 | 18 | Future main() async { 19 | WidgetsFlutterBinding.ensureInitialized(); 20 | final sharedPreferences = await SharedPreferences.getInstance(); 21 | runApp(App(sharedPreferences)); 22 | } 23 | 24 | class App extends StatelessWidget { 25 | const App(this.sharedPreferences); 26 | 27 | final SharedPreferences sharedPreferences; 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return MultiProvider( 32 | providers: [ 33 | Provider( 34 | create: (_) => DestinationsStore(), 35 | ), 36 | Provider( 37 | create: (_) => HomeStore(), 38 | ), 39 | Provider( 40 | create: (_) => GalleryStore(), 41 | ), 42 | Provider( 43 | create: (_) => SlideshowStore(), 44 | ), 45 | Provider( 46 | create: (_) => PreferencesService(sharedPreferences), 47 | ), 48 | ProxyProvider( 49 | update: (_, preferencesService, __) => 50 | SettingsStore(preferencesService), 51 | ) 52 | ], 53 | child: Consumer( 54 | builder: (context, store, _) { 55 | return Observer( 56 | builder: (_) { 57 | return MaterialApp( 58 | title: 'App title', 59 | theme: store.useDarkMode ? ThemeData.dark() : ThemeData.light(), 60 | home: const AppScaffold(), 61 | ); 62 | }, 63 | ); 64 | }, 65 | ), 66 | ); 67 | } 68 | } 69 | 70 | class AppScaffold extends StatelessWidget { 71 | const AppScaffold({ 72 | Key key, 73 | }) : super(key: key); 74 | 75 | @override 76 | Widget build(BuildContext context) { 77 | return Consumer( 78 | builder: (context, store, _) { 79 | return Observer( 80 | builder: (context) { 81 | return Scaffold( 82 | appBar: AppBar( 83 | title: AppBarTitle(store.selectedDestination), 84 | ), 85 | body: SafeArea( 86 | child: PageContainer(store.selectedDestination), 87 | ), 88 | drawer: AppDrawer(store), 89 | floatingActionButton: store.selectedDestination == 90 | Destination.Settings 91 | ? null 92 | : FloatingActionButton( 93 | key: const Key('incrementButton'), 94 | onPressed: () { 95 | switch (store.selectedDestination) { 96 | case Destination.Home: 97 | Provider.of(context, listen: false) 98 | .increment(); 99 | break; 100 | case Destination.Gallery: 101 | Provider.of(context, listen: false) 102 | .increment(); 103 | break; 104 | case Destination.Slideshow: 105 | Provider.of(context, listen: false) 106 | .increment(); 107 | break; 108 | default: 109 | break; 110 | } 111 | }, 112 | tooltip: 'Increment', 113 | child: const Icon(Icons.add), 114 | ), 115 | ); 116 | }, 117 | ); 118 | }, 119 | ); 120 | } 121 | } 122 | 123 | class AppBarTitle extends StatelessWidget { 124 | const AppBarTitle(this.destination, {Key key}) : super(key: key); 125 | 126 | final Destination destination; 127 | 128 | @override 129 | Widget build(BuildContext context) { 130 | switch (destination) { 131 | case Destination.Gallery: 132 | return const Text('Gallery', key: Keys.galleryPageTitleKey); 133 | case Destination.Slideshow: 134 | return const Text('Slideshow', key: Keys.slideshowPageTitleKey); 135 | case Destination.Settings: 136 | return const Text('Settings', key: Keys.settingsPageTitleKey); 137 | default: 138 | return const Text('Home', key: Keys.homePageTitleKey); 139 | } 140 | } 141 | } 142 | 143 | class AppDrawer extends StatelessWidget { 144 | const AppDrawer( 145 | this.store, { 146 | Key key, 147 | }) : super(key: key); 148 | 149 | final DestinationsStore store; 150 | 151 | @override 152 | Widget build(BuildContext context) { 153 | return Observer( 154 | builder: (_) { 155 | return Drawer( 156 | child: ListView( 157 | padding: EdgeInsets.zero, 158 | children: [ 159 | const UserAccountsDrawerHeader( 160 | accountName: Text('User'), 161 | accountEmail: Text('user@email.com'), 162 | currentAccountPicture: CircleAvatar( 163 | child: Icon(Icons.android), 164 | ), 165 | ), 166 | ListTile( 167 | key: Keys.homeDrawerItemKey, 168 | leading: const Icon(Icons.home), 169 | title: const Text('Home'), 170 | selected: store.selectedDestination == Destination.Home, 171 | onTap: () { 172 | Navigator.pop(context); 173 | store.selectDestination(Destination.Home); 174 | }, 175 | ), 176 | ListTile( 177 | key: Keys.galleryDrawerItemKey, 178 | leading: const Icon(Icons.photo_library), 179 | title: const Text('Gallery'), 180 | selected: store.selectedDestination == Destination.Gallery, 181 | onTap: () async { 182 | Navigator.pop(context); 183 | store.selectDestination(Destination.Gallery); 184 | }, 185 | ), 186 | ListTile( 187 | key: Keys.slideshowDrawerItemKey, 188 | leading: const Icon(Icons.slideshow), 189 | title: const Text('Slideshow'), 190 | selected: store.selectedDestination == Destination.Slideshow, 191 | onTap: () async { 192 | Navigator.pop(context); 193 | store.selectDestination(Destination.Slideshow); 194 | }, 195 | ), 196 | Divider(), 197 | ListTile( 198 | key: Keys.settingsDrawerItemKey, 199 | leading: const Icon(Icons.settings), 200 | title: const Text('Settings'), 201 | selected: store.selectedDestination == Destination.Settings, 202 | onTap: () async { 203 | Navigator.pop(context); 204 | store.selectDestination(Destination.Settings); 205 | }, 206 | ), 207 | ], 208 | ), 209 | ); 210 | }, 211 | ); 212 | } 213 | } 214 | 215 | class PageContainer extends StatelessWidget { 216 | const PageContainer(this.destination, {Key key}) : super(key: key); 217 | 218 | final Destination destination; 219 | 220 | @override 221 | Widget build(BuildContext context) { 222 | switch (destination) { 223 | case Destination.Gallery: 224 | return Consumer( 225 | builder: (_, store, __) => 226 | GalleryPage(store, key: Keys.galleryPageKey), 227 | ); 228 | case Destination.Slideshow: 229 | return Consumer( 230 | builder: (_, store, __) => 231 | SlideshowPage(store, key: Keys.slideshowPageKey), 232 | ); 233 | case Destination.Settings: 234 | return Consumer( 235 | builder: (_, store, __) => 236 | SettingsPage(store, key: Keys.settingsPageKey), 237 | ); 238 | default: 239 | return Consumer( 240 | builder: (_, store, __) => HomePage(store, key: Keys.homePageKey), 241 | ); 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/pages/gallery_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import '../constants/keys.dart'; 4 | import '../stores/gallery_store.dart'; 5 | 6 | class GalleryPage extends StatelessWidget { 7 | const GalleryPage(this.store, {Key key}) : super(key: key); 8 | 9 | final GalleryStore store; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Observer( 14 | builder: (context) { 15 | return Center( 16 | child: Text( 17 | 'You have pushed the button on this page ${store.counter} time(s)', 18 | key: Keys.galleryPageCounterKey, 19 | ), 20 | ); 21 | }, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/pages/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import '../constants/keys.dart'; 4 | import '../stores/home_store.dart'; 5 | 6 | class HomePage extends StatelessWidget { 7 | const HomePage(this.store, {Key key}) : super(key: key); 8 | 9 | final HomeStore store; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Observer( 14 | builder: (context) { 15 | return Center( 16 | child: Text( 17 | 'You have pushed the button on this page ${store.counter} time(s)', 18 | key: Keys.homePageCounterKey, 19 | ), 20 | ); 21 | }, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/pages/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import '../constants/keys.dart'; 4 | import '../stores/settings_store.dart'; 5 | 6 | class SettingsPage extends StatelessWidget { 7 | const SettingsPage(this.store, {Key key}) : super(key: key); 8 | 9 | final SettingsStore store; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Observer( 14 | builder: (context) { 15 | return SwitchListTile( 16 | key: Keys.useDarkModeSettingKey, 17 | value: store.useDarkMode, 18 | title: const Text('Use dark mode'), 19 | onChanged: (value) { 20 | store.setDarkMode(value); 21 | }, 22 | ); 23 | }, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/pages/slideshow_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import '../constants/keys.dart'; 4 | import '../stores/slideshow_store.dart'; 5 | 6 | class SlideshowPage extends StatelessWidget { 7 | const SlideshowPage(this.store, {Key key}) : super(key: key); 8 | 9 | final SlideshowStore store; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Observer( 14 | builder: (context) { 15 | return Center( 16 | child: Text( 17 | 'You have pushed the button on this page ${store.counter} time(s)', 18 | key: Keys.slideshowPageCounterKey, 19 | ), 20 | ); 21 | }, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/services/preferences_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | class PreferencesService { 4 | const PreferencesService(this._sharedPreferences); 5 | 6 | final String _useDarkModeKey = 'useDarkMode'; 7 | final SharedPreferences _sharedPreferences; 8 | 9 | set useDarkMode(bool useDarkMode) { 10 | _sharedPreferences.setBool(_useDarkModeKey, useDarkMode); 11 | } 12 | 13 | bool get useDarkMode => _sharedPreferences.getBool(_useDarkModeKey) ?? false; 14 | } 15 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/stores/destinations_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | import '../constants/enums.dart'; 3 | 4 | part 'destinations_store.g.dart'; 5 | 6 | class DestinationsStore = DestinationsStoreBase with _$DestinationsStore; 7 | 8 | abstract class DestinationsStoreBase with Store { 9 | static const List destinations = Destination.values; 10 | 11 | @observable 12 | Destination selectedDestination = Destination.Home; 13 | 14 | @action 15 | void selectDestination(Destination destination) { 16 | selectedDestination = destination; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/stores/destinations_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'destinations_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars 10 | 11 | mixin _$DestinationsStore on DestinationsStoreBase, Store { 12 | final _$selectedDestinationAtom = 13 | Atom(name: 'DestinationsStoreBase.selectedDestination'); 14 | 15 | @override 16 | Destination get selectedDestination { 17 | _$selectedDestinationAtom.context 18 | .enforceReadPolicy(_$selectedDestinationAtom); 19 | _$selectedDestinationAtom.reportObserved(); 20 | return super.selectedDestination; 21 | } 22 | 23 | @override 24 | set selectedDestination(Destination value) { 25 | // Since we are conditionally wrapping within an Action, there is no need to enforceWritePolicy 26 | _$selectedDestinationAtom.context.conditionallyRunInAction(() { 27 | super.selectedDestination = value; 28 | _$selectedDestinationAtom.reportChanged(); 29 | }, name: '${_$selectedDestinationAtom.name}_set'); 30 | } 31 | 32 | final _$DestinationsStoreBaseActionController = 33 | ActionController(name: 'DestinationsStoreBase'); 34 | 35 | @override 36 | void selectDestination(Destination destination) { 37 | final _$actionInfo = _$DestinationsStoreBaseActionController.startAction(); 38 | try { 39 | return super.selectDestination(destination); 40 | } finally { 41 | _$DestinationsStoreBaseActionController.endAction(_$actionInfo); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/stores/gallery_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | 3 | part 'gallery_store.g.dart'; 4 | 5 | class GalleryStore = GalleryStoreBase with _$GalleryStore; 6 | 7 | abstract class GalleryStoreBase with Store { 8 | @observable 9 | int counter = 0; 10 | 11 | @action 12 | void increment() { 13 | counter++; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/stores/gallery_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'gallery_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars 10 | 11 | mixin _$GalleryStore on GalleryStoreBase, Store { 12 | final _$counterAtom = Atom(name: 'GalleryStoreBase.counter'); 13 | 14 | @override 15 | int get counter { 16 | _$counterAtom.context.enforceReadPolicy(_$counterAtom); 17 | _$counterAtom.reportObserved(); 18 | return super.counter; 19 | } 20 | 21 | @override 22 | set counter(int value) { 23 | // Since we are conditionally wrapping within an Action, there is no need to enforceWritePolicy 24 | _$counterAtom.context.conditionallyRunInAction(() { 25 | super.counter = value; 26 | _$counterAtom.reportChanged(); 27 | }, name: '${_$counterAtom.name}_set'); 28 | } 29 | 30 | final _$GalleryStoreBaseActionController = 31 | ActionController(name: 'GalleryStoreBase'); 32 | 33 | @override 34 | void increment() { 35 | final _$actionInfo = _$GalleryStoreBaseActionController.startAction(); 36 | try { 37 | return super.increment(); 38 | } finally { 39 | _$GalleryStoreBaseActionController.endAction(_$actionInfo); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/stores/home_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | 3 | part 'home_store.g.dart'; 4 | 5 | class HomeStore = HomeStoreBase with _$HomeStore; 6 | 7 | abstract class HomeStoreBase with Store { 8 | @observable 9 | int counter = 0; 10 | 11 | @action 12 | void increment() { 13 | counter++; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/stores/home_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'home_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars 10 | 11 | mixin _$HomeStore on HomeStoreBase, Store { 12 | final _$counterAtom = Atom(name: 'HomeStoreBase.counter'); 13 | 14 | @override 15 | int get counter { 16 | _$counterAtom.context.enforceReadPolicy(_$counterAtom); 17 | _$counterAtom.reportObserved(); 18 | return super.counter; 19 | } 20 | 21 | @override 22 | set counter(int value) { 23 | // Since we are conditionally wrapping within an Action, there is no need to enforceWritePolicy 24 | _$counterAtom.context.conditionallyRunInAction(() { 25 | super.counter = value; 26 | _$counterAtom.reportChanged(); 27 | }, name: '${_$counterAtom.name}_set'); 28 | } 29 | 30 | final _$HomeStoreBaseActionController = 31 | ActionController(name: 'HomeStoreBase'); 32 | 33 | @override 34 | void increment() { 35 | final _$actionInfo = _$HomeStoreBaseActionController.startAction(); 36 | try { 37 | return super.increment(); 38 | } finally { 39 | _$HomeStoreBaseActionController.endAction(_$actionInfo); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/stores/settings_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | import '../services/preferences_service.dart'; 3 | part 'settings_store.g.dart'; 4 | 5 | class SettingsStore = SettingsStoreBase with _$SettingsStore; 6 | 7 | abstract class SettingsStoreBase with Store { 8 | /// when the store is created, we read in the current settings immediately to avoid the scenario where 9 | /// the values displayed will change upon switching to the settings tab 10 | SettingsStoreBase(this._preferencesService) { 11 | useDarkMode = _preferencesService.useDarkMode; 12 | } 13 | 14 | PreferencesService _preferencesService; 15 | 16 | @observable 17 | bool useDarkMode; 18 | 19 | @action 20 | void setDarkMode(bool updatedDarkModePreference) { 21 | _preferencesService.useDarkMode = updatedDarkModePreference; 22 | useDarkMode = updatedDarkModePreference; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/stores/settings_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'settings_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars 10 | 11 | mixin _$SettingsStore on SettingsStoreBase, Store { 12 | final _$useDarkModeAtom = Atom(name: 'SettingsStoreBase.useDarkMode'); 13 | 14 | @override 15 | bool get useDarkMode { 16 | _$useDarkModeAtom.context.enforceReadPolicy(_$useDarkModeAtom); 17 | _$useDarkModeAtom.reportObserved(); 18 | return super.useDarkMode; 19 | } 20 | 21 | @override 22 | set useDarkMode(bool value) { 23 | // Since we are conditionally wrapping within an Action, there is no need to enforceWritePolicy 24 | _$useDarkModeAtom.context.conditionallyRunInAction(() { 25 | super.useDarkMode = value; 26 | _$useDarkModeAtom.reportChanged(); 27 | }, name: '${_$useDarkModeAtom.name}_set'); 28 | } 29 | 30 | final _$SettingsStoreBaseActionController = 31 | ActionController(name: 'SettingsStoreBase'); 32 | 33 | @override 34 | void setDarkMode(bool updatedDarkModePreference) { 35 | final _$actionInfo = _$SettingsStoreBaseActionController.startAction(); 36 | try { 37 | return super.setDarkMode(updatedDarkModePreference); 38 | } finally { 39 | _$SettingsStoreBaseActionController.endAction(_$actionInfo); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/stores/slideshow_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | 3 | part 'slideshow_store.g.dart'; 4 | 5 | class SlideshowStore = SlideshowStoreBase with _$SlideshowStore; 6 | 7 | abstract class SlideshowStoreBase with Store { 8 | @observable 9 | int counter = 0; 10 | 11 | @action 12 | void increment() { 13 | counter++; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/lib/stores/slideshow_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'slideshow_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars 10 | 11 | mixin _$SlideshowStore on SlideshowStoreBase, Store { 12 | final _$counterAtom = Atom(name: 'SlideshowStoreBase.counter'); 13 | 14 | @override 15 | int get counter { 16 | _$counterAtom.context.enforceReadPolicy(_$counterAtom); 17 | _$counterAtom.reportObserved(); 18 | return super.counter; 19 | } 20 | 21 | @override 22 | set counter(int value) { 23 | // Since we are conditionally wrapping within an Action, there is no need to enforceWritePolicy 24 | _$counterAtom.context.conditionallyRunInAction(() { 25 | super.counter = value; 26 | _$counterAtom.reportChanged(); 27 | }, name: '${_$counterAtom.name}_set'); 28 | } 29 | 30 | final _$SlideshowStoreBaseActionController = 31 | ActionController(name: 'SlideshowStoreBase'); 32 | 33 | @override 34 | void increment() { 35 | final _$actionInfo = _$SlideshowStoreBaseActionController.startAction(); 36 | try { 37 | return super.increment(); 38 | } finally { 39 | _$SlideshowStoreBaseActionController.endAction(_$actionInfo); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: material_navigationdrawer_provider_mobx 2 | description: A new Flutter project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.2.2 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | 23 | # The following adds the Cupertino Icons font to your application. 24 | # Use with the CupertinoIcons class for iOS style icons. 25 | cupertino_icons: ^0.1.2 26 | mobx: 27 | flutter_mobx: 28 | provider: 29 | shared_preferences: 30 | 31 | dev_dependencies: 32 | flutter_driver: 33 | sdk: flutter 34 | flutter_test: 35 | sdk: flutter 36 | test: 37 | build_runner: 1.10.1 38 | mobx_codegen: 39 | mockito: 40 | 41 | 42 | # For information on the generic Dart part of this file, see the 43 | # following page: https://www.dartlang.org/tools/pub/pubspec 44 | 45 | # The following section is specific to Flutter. 46 | flutter: 47 | 48 | # The following line ensures that the Material Icons font is 49 | # included with your application, so that you can use the icons in 50 | # the material Icons class. 51 | uses-material-design: true 52 | 53 | # To add assets to your application, add an assets section, like this: 54 | # assets: 55 | # - images/a_dot_burr.jpeg 56 | # - images/a_dot_ham.jpeg 57 | 58 | # An image asset can refer to one or more resolution-specific "variants", see 59 | # https://flutter.dev/assets-and-images/#resolution-aware. 60 | 61 | # For details regarding adding assets from package dependencies, see 62 | # https://flutter.dev/assets-and-images/#from-packages 63 | 64 | # To add custom fonts to your application, add a fonts section here, 65 | # in this "flutter" section. Each entry in this list should have a 66 | # "family" key with the font family name, and a "fonts" key with a 67 | # list giving the asset and other descriptors for the font. For 68 | # example: 69 | # fonts: 70 | # - family: Schyler 71 | # fonts: 72 | # - asset: fonts/Schyler-Regular.ttf 73 | # - asset: fonts/Schyler-Italic.ttf 74 | # style: italic 75 | # - family: Trajan Pro 76 | # fonts: 77 | # - asset: fonts/TrajanPro.ttf 78 | # - asset: fonts/TrajanPro_Bold.ttf 79 | # weight: 700 80 | # 81 | # For details regarding fonts from package dependencies, 82 | # see https://flutter.dev/custom-fonts/#from-packages 83 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/test/mocks/mock_shared_preferences.dart: -------------------------------------------------------------------------------- 1 | import 'package:mockito/mockito.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | class MockedSharedPreferences extends Mock implements SharedPreferences {} 5 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/test/stores/destinations_store_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:material_navigationdrawer_provider_mobx/constants/enums.dart'; 2 | import 'package:material_navigationdrawer_provider_mobx/stores/destinations_store.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('Destinations store', () { 7 | DestinationsStore store; 8 | setUpAll(() { 9 | store = DestinationsStore(); 10 | }); 11 | test('starts at home', () { 12 | expect(store.selectedDestination, Destination.Home); 13 | }); 14 | test('select home destination works', () { 15 | _selectAndExpectDestination(store, Destination.Home); 16 | }); 17 | test('select gallery destination works', () { 18 | _selectAndExpectDestination(store, Destination.Gallery); 19 | }); 20 | test('select slideshow destination works', () { 21 | _selectAndExpectDestination(store, Destination.Slideshow); 22 | }); 23 | }); 24 | } 25 | 26 | void _selectAndExpectDestination( 27 | DestinationsStore store, Destination destination) { 28 | store.selectDestination(destination); 29 | expect(store.selectedDestination, destination); 30 | } 31 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/test/stores/gallery_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:material_navigationdrawer_provider_mobx/stores/gallery_store.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('GalleryStore', () { 6 | GalleryStore store; 7 | setUpAll(() { 8 | store = GalleryStore(); 9 | }); 10 | test('Counter starts at 0', () { 11 | expect(store.counter, 0); 12 | }); 13 | test('Increment works', () { 14 | store.increment(); 15 | expect(store.counter, 1); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/test/stores/home_store_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:material_navigationdrawer_provider_mobx/stores/home_store.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('HomeStore', () { 6 | HomeStore store; 7 | setUpAll(() { 8 | store = HomeStore(); 9 | }); 10 | test('Counter starts at 0', () { 11 | expect(store.counter, 0); 12 | }); 13 | test('Increment works', () { 14 | store.increment(); 15 | expect(store.counter, 1); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/test/stores/settings_store_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:material_navigationdrawer_provider_mobx/services/preferences_service.dart'; 2 | import 'package:material_navigationdrawer_provider_mobx/stores/settings_store.dart'; 3 | import 'package:test/test.dart'; 4 | import '../mocks/mock_shared_preferences.dart'; 5 | 6 | void main() { 7 | group('SlideshowStore', () { 8 | SettingsStore store; 9 | setUpAll(() { 10 | store = SettingsStore(PreferencesService(MockedSharedPreferences())); 11 | }); 12 | test('Use dark mode settings default to off', () { 13 | expect(store.useDarkMode, false); 14 | }); 15 | test('Turning on dark mode works', () { 16 | store.setDarkMode(true); 17 | expect(store.useDarkMode, true); 18 | }); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/test/stores/slideshow_store_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:material_navigationdrawer_provider_mobx/stores/slideshow_store.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('SlideshowStore', () { 6 | SlideshowStore store; 7 | setUpAll(() { 8 | store = SlideshowStore(); 9 | }); 10 | test('Counter starts at 0', () { 11 | expect(store.counter, 0); 12 | }); 13 | test('Increment works', () { 14 | store.increment(); 15 | expect(store.counter, 1); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/test/widgets/app_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:material_navigationdrawer_provider_mobx/constants/keys.dart'; 4 | import 'package:material_navigationdrawer_provider_mobx/main.dart'; 5 | import 'package:material_navigationdrawer_provider_mobx/pages/gallery_page.dart'; 6 | import 'package:material_navigationdrawer_provider_mobx/pages/home_page.dart'; 7 | import 'package:material_navigationdrawer_provider_mobx/pages/settings_page.dart'; 8 | import 'package:material_navigationdrawer_provider_mobx/pages/slideshow_page.dart'; 9 | import '../mocks/mock_shared_preferences.dart'; 10 | 11 | void main() { 12 | group('App', () { 13 | final drawerMenuButtonFinder = find.byIcon(Icons.menu); 14 | final drawerFinder = find.byType(Drawer); 15 | final homePageFinder = find.byType(HomePage); 16 | final galleryPageFinder = find.byType(GalleryPage); 17 | final slideshowPageFinder = find.byType(SlideshowPage); 18 | final settingsPageFinder = find.byType(SettingsPage); 19 | final incrementButtonFinder = find.byType(FloatingActionButton); 20 | final homeIconFinder = find.byIcon(Icons.home); 21 | final galleryIconFinder = find.byIcon(Icons.photo_library); 22 | final slideshowIconFinder = find.byIcon(Icons.slideshow); 23 | final settingsIconFinder = find.byIcon(Icons.settings); 24 | final mockedSharedPreferences = MockedSharedPreferences(); 25 | testWidgets('Starts on home page with drawer closed', 26 | (WidgetTester tester) async { 27 | await _pumpWidget(tester, mockedSharedPreferences); 28 | expect(drawerMenuButtonFinder, findsOneWidget); 29 | expect(drawerFinder, findsNothing); 30 | expect(homePageFinder, findsOneWidget); 31 | expect(galleryPageFinder, findsNothing); 32 | expect(slideshowPageFinder, findsNothing); 33 | }); 34 | 35 | testWidgets('Floating action button exists', (WidgetTester tester) async { 36 | await _pumpWidget(tester, mockedSharedPreferences); 37 | expect(incrementButtonFinder, findsOneWidget); 38 | }); 39 | 40 | testWidgets('Drawer exists', (WidgetTester tester) async { 41 | await _pumpWidget(tester, mockedSharedPreferences); 42 | await openDrawer( 43 | tester, 44 | drawerMenuButtonFinder, 45 | drawerFinder, 46 | homeIconFinder, 47 | galleryIconFinder, 48 | slideshowIconFinder, 49 | settingsIconFinder); 50 | }); 51 | 52 | testWidgets('Select home page', (WidgetTester tester) async { 53 | await _pumpWidget(tester, mockedSharedPreferences); 54 | await openDrawer( 55 | tester, 56 | drawerMenuButtonFinder, 57 | drawerFinder, 58 | homeIconFinder, 59 | galleryIconFinder, 60 | slideshowIconFinder, 61 | settingsIconFinder); 62 | 63 | // select home page 64 | await tester.tap(homeIconFinder); 65 | await tester.pumpAndSettle(); 66 | 67 | // drawer should now be closed 68 | expect(drawerFinder, findsNothing); 69 | 70 | // should now be on home page 71 | expect(homePageFinder, findsOneWidget); 72 | expect(galleryPageFinder, findsNothing); 73 | expect(slideshowPageFinder, findsNothing); 74 | expect(find.byKey(Keys.homePageTitleKey), findsOneWidget); 75 | expect(find.text('You have pushed the button on this page 0 time(s)'), 76 | findsOneWidget); 77 | expect(incrementButtonFinder, findsOneWidget); 78 | }); 79 | 80 | testWidgets('Select gallery page', (WidgetTester tester) async { 81 | await _pumpWidget(tester, mockedSharedPreferences); 82 | await openDrawer( 83 | tester, 84 | drawerMenuButtonFinder, 85 | drawerFinder, 86 | homeIconFinder, 87 | galleryIconFinder, 88 | slideshowIconFinder, 89 | settingsIconFinder); 90 | 91 | // select gallery page 92 | await tester.tap(galleryIconFinder); 93 | await tester.pumpAndSettle(); 94 | 95 | // drawer should now be closed 96 | expect(drawerFinder, findsNothing); 97 | 98 | // should now be on gallery page 99 | expect(homePageFinder, findsNothing); 100 | expect(galleryPageFinder, findsOneWidget); 101 | expect(slideshowPageFinder, findsNothing); 102 | expect(find.byKey(Keys.galleryPageTitleKey), findsOneWidget); 103 | expect(find.text('You have pushed the button on this page 0 time(s)'), 104 | findsOneWidget); 105 | expect(incrementButtonFinder, findsOneWidget); 106 | }); 107 | 108 | testWidgets('Select slideshow page', (WidgetTester tester) async { 109 | await _pumpWidget(tester, mockedSharedPreferences); 110 | await openDrawer( 111 | tester, 112 | drawerMenuButtonFinder, 113 | drawerFinder, 114 | homeIconFinder, 115 | galleryIconFinder, 116 | slideshowIconFinder, 117 | settingsIconFinder); 118 | 119 | // select slideshow page 120 | await tester.tap(slideshowIconFinder); 121 | await tester.pumpAndSettle(); 122 | 123 | // drawer should now be closed 124 | expect(drawerFinder, findsNothing); 125 | 126 | // should now be on slideshow page 127 | expect(homePageFinder, findsNothing); 128 | expect(galleryPageFinder, findsNothing); 129 | expect(slideshowPageFinder, findsOneWidget); 130 | expect(find.byKey(Keys.slideshowPageTitleKey), findsOneWidget); 131 | expect(find.text('You have pushed the button on this page 0 time(s)'), 132 | findsOneWidget); 133 | expect(incrementButtonFinder, findsOneWidget); 134 | }); 135 | 136 | testWidgets('Select settings page', (WidgetTester tester) async { 137 | await _pumpWidget(tester, mockedSharedPreferences); 138 | await openDrawer( 139 | tester, 140 | drawerMenuButtonFinder, 141 | drawerFinder, 142 | homeIconFinder, 143 | galleryIconFinder, 144 | slideshowIconFinder, 145 | settingsIconFinder); 146 | 147 | // select settings page 148 | await tester.tap(settingsIconFinder); 149 | await tester.pumpAndSettle(); 150 | 151 | // drawer should now be closed 152 | expect(drawerFinder, findsNothing); 153 | 154 | // should now be on settings page 155 | expect(homePageFinder, findsNothing); 156 | expect(galleryPageFinder, findsNothing); 157 | expect(slideshowPageFinder, findsNothing); 158 | expect(settingsPageFinder, findsOneWidget); 159 | expect(find.byKey(Keys.settingsPageKey), findsOneWidget); 160 | expect(find.byType(SwitchListTile), findsOneWidget); 161 | }); 162 | }); 163 | } 164 | 165 | Future _pumpWidget(WidgetTester tester, 166 | MockedSharedPreferences mockedSharedPreferences) async { 167 | await tester.pumpWidget(App(mockedSharedPreferences)); 168 | } 169 | 170 | Future openDrawer( 171 | WidgetTester tester, 172 | Finder drawerMenuButtonFinder, 173 | Finder drawerFinder, 174 | Finder homeIconFinder, 175 | Finder galleryIconFinder, 176 | Finder slideshowIconFinder, 177 | Finder settingsIconFinder) async { 178 | // open the drawer 179 | await tester.tap(drawerMenuButtonFinder); 180 | // wait for drawer animation to finish 181 | await tester.pumpAndSettle(); 182 | expect(drawerFinder, findsOneWidget); 183 | expect(homeIconFinder, findsOneWidget); 184 | expect(galleryIconFinder, findsOneWidget); 185 | expect(slideshowIconFinder, findsOneWidget); 186 | expect(settingsIconFinder, findsOneWidget); 187 | } 188 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/test/widgets/pages/gallery_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:material_navigationdrawer_provider_mobx/pages/gallery_page.dart'; 4 | import 'package:material_navigationdrawer_provider_mobx/stores/gallery_store.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | void main() { 8 | group('GalleryPage', () { 9 | final pageFinder = find.byType(GalleryPage); 10 | final store = GalleryStore(); 11 | testWidgets('Counter starts at zero', (WidgetTester tester) async { 12 | await _pumpWidget(tester, store); 13 | 14 | expect(pageFinder, findsOneWidget); 15 | expect(find.text('You have pushed the button on this page 0 time(s)'), 16 | findsOneWidget); 17 | }); 18 | testWidgets('Counter updates', (WidgetTester tester) async { 19 | await _pumpWidget(tester, store); 20 | 21 | store.increment(); 22 | await tester.pump(); 23 | expect(find.text('You have pushed the button on this page 1 time(s)'), 24 | findsOneWidget); 25 | }); 26 | }); 27 | } 28 | 29 | Future _pumpWidget(WidgetTester tester, GalleryStore store) async { 30 | await tester.pumpWidget( 31 | Provider( 32 | create: (_) => store, 33 | child: MaterialApp( 34 | home: Consumer( 35 | builder: (_, store, __) => GalleryPage(store), 36 | ), 37 | ), 38 | ), 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/test/widgets/pages/home_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:material_navigationdrawer_provider_mobx/pages/home_page.dart'; 4 | import 'package:material_navigationdrawer_provider_mobx/stores/home_store.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | void main() { 8 | group('HomePage', () { 9 | final pageFinder = find.byType(HomePage); 10 | final store = HomeStore(); 11 | testWidgets('Counter starts at zero', (WidgetTester tester) async { 12 | await _pumpWidget(tester, store); 13 | 14 | expect(pageFinder, findsOneWidget); 15 | expect(find.text('You have pushed the button on this page 0 time(s)'), 16 | findsOneWidget); 17 | }); 18 | testWidgets('Counter updates', (WidgetTester tester) async { 19 | await _pumpWidget(tester, store); 20 | 21 | store.increment(); 22 | await tester.pump(); 23 | expect(find.text('You have pushed the button on this page 1 time(s)'), 24 | findsOneWidget); 25 | }); 26 | }); 27 | } 28 | 29 | Future _pumpWidget(WidgetTester tester, HomeStore store) async { 30 | await tester.pumpWidget( 31 | Provider( 32 | create: (_) => store, 33 | child: MaterialApp( 34 | home: Consumer( 35 | builder: (_, store, __) => HomePage(store), 36 | ), 37 | ), 38 | ), 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/test/widgets/pages/settings_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:material_navigationdrawer_provider_mobx/constants/keys.dart'; 4 | import 'package:material_navigationdrawer_provider_mobx/pages/settings_page.dart'; 5 | import 'package:material_navigationdrawer_provider_mobx/services/preferences_service.dart'; 6 | import 'package:material_navigationdrawer_provider_mobx/stores/settings_store.dart'; 7 | import 'package:provider/provider.dart'; 8 | import '../../mocks/mock_shared_preferences.dart'; 9 | 10 | void main() { 11 | group('SettingsPage', () { 12 | final mockedSharedPreferences = MockedSharedPreferences(); 13 | final preferencesService = PreferencesService(mockedSharedPreferences); 14 | final store = SettingsStore(preferencesService); 15 | final pageFinder = find.byType(SettingsPage); 16 | final useDarkModeSettingFinder = find.byKey(Keys.useDarkModeSettingKey); 17 | testWidgets('Dark mode setting starts at off', (WidgetTester tester) async { 18 | await _pumpWidget(tester, store); 19 | 20 | expect(pageFinder, findsOneWidget); 21 | expect(useDarkModeSettingFinder, findsOneWidget); 22 | expect(store.useDarkMode, false); 23 | }); 24 | 25 | testWidgets('Turn dark mode on', (WidgetTester tester) async { 26 | await _pumpWidget(tester, store); 27 | 28 | await tester.tap(useDarkModeSettingFinder); 29 | await tester.pumpAndSettle(); 30 | expect(store.useDarkMode, true); 31 | }); 32 | }); 33 | } 34 | 35 | Future _pumpWidget(WidgetTester tester, SettingsStore store) async { 36 | await tester.pumpWidget( 37 | Provider( 38 | create: (_) => store, 39 | child: MaterialApp( 40 | home: Scaffold( 41 | body: Consumer( 42 | builder: (_, store, __) => SettingsPage(store)), 43 | ), 44 | ), 45 | ), 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/test/widgets/pages/slideshow_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:material_navigationdrawer_provider_mobx/pages/slideshow_page.dart'; 4 | import 'package:material_navigationdrawer_provider_mobx/stores/slideshow_store.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | void main() { 8 | group('SlideshowPage', () { 9 | final pageFinder = find.byType(SlideshowPage); 10 | final store = SlideshowStore(); 11 | 12 | testWidgets('Counter starts at zero', (WidgetTester tester) async { 13 | await _pumpWidget(tester, store); 14 | 15 | expect(pageFinder, findsOneWidget); 16 | expect(find.text('You have pushed the button on this page 0 time(s)'), 17 | findsOneWidget); 18 | }); 19 | 20 | testWidgets('Counter updates', (WidgetTester tester) async { 21 | await _pumpWidget(tester, store); 22 | 23 | store.increment(); 24 | await tester.pump(); 25 | expect(find.text('You have pushed the button on this page 1 time(s)'), 26 | findsOneWidget); 27 | }); 28 | }); 29 | } 30 | 31 | Future _pumpWidget(WidgetTester tester, SlideshowStore store) async { 32 | await tester.pumpWidget( 33 | Provider( 34 | create: (_) => store, 35 | child: MaterialApp( 36 | home: Consumer( 37 | builder: (_, store, __) => SlideshowPage(store), 38 | ), 39 | ), 40 | ), 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/test_driver/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_driver/driver_extension.dart'; 2 | import 'package:material_navigationdrawer_provider_mobx/main.dart' as app; 3 | 4 | void main() { 5 | // This line enables the extension. 6 | enableFlutterDriverExtension(); 7 | 8 | // Call the `main()` function of the app, or call `runApp` with 9 | // any widget you are interested in testing. 10 | app.main(); 11 | } 12 | -------------------------------------------------------------------------------- /material_navigationdrawer_provider_mobx/test_driver/app_test.dart: -------------------------------------------------------------------------------- 1 | // Imports the Flutter Driver API. 2 | import 'package:flutter_driver/flutter_driver.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('App', () { 7 | FlutterDriver driver; 8 | // Note: the approach for finding the drawer menu button and back button is more of a workaround. See https://github.com/flutter/flutter/issues/9002 9 | final drawerMenuButtonFinder = find.byTooltip('Open navigation menu'); 10 | final galleryDrawerItemFinder = find.byValueKey('galleryDrawerItem'); 11 | final galleryPageFinder = find.byValueKey('galleryPage'); 12 | final settingsDrawerItemFinder = find.byValueKey('settingsDrawerItem'); 13 | final settingsPageFinder = find.byValueKey('settingsPage'); 14 | final slideshowDrawerItemFinder = find.byValueKey('slideshowDrawerItem'); 15 | final slideshowPageFinder = find.byValueKey('slideshowPage'); 16 | final incrementButtonFinder = find.byValueKey('incrementButton'); 17 | final homePageFinder = find.byValueKey('homePage'); 18 | final homeDrawerItemFinder = find.byValueKey('homeDrawerItem'); 19 | final homePageCounterFinder = find.byValueKey('homePageCounter'); 20 | final galleryPageCounterFinder = find.byValueKey('galleryPageCounter'); 21 | final slideshowPageCounterFinder = find.byValueKey('slideshowPageCounter'); 22 | final useDarkModeSettingFinder = find.byValueKey('useDarkModeSetting'); 23 | setUpAll(() async { 24 | driver = await FlutterDriver.connect(); 25 | }); 26 | 27 | tearDownAll(() async { 28 | driver?.close(); 29 | }); 30 | 31 | test('check flutter driver health', () async { 32 | final health = await driver.checkHealth(); 33 | print('flutter driver status: ${health.status}'); 34 | }); 35 | 36 | test('starts on home page', () async { 37 | await driver.waitFor(homePageFinder); 38 | }); 39 | 40 | test('select pages works', () async { 41 | await _selectPage(driver, drawerMenuButtonFinder, galleryDrawerItemFinder, 42 | galleryPageFinder); 43 | await _selectPage(driver, drawerMenuButtonFinder, 44 | slideshowDrawerItemFinder, slideshowPageFinder); 45 | await _selectPage(driver, drawerMenuButtonFinder, 46 | settingsDrawerItemFinder, settingsPageFinder); 47 | await _selectPage( 48 | driver, drawerMenuButtonFinder, homeDrawerItemFinder, homePageFinder); 49 | }); 50 | 51 | test('increment counter on home page', () async { 52 | expect(await driver.getText(homePageCounterFinder), 53 | 'You have pushed the button on this page 0 time(s)'); 54 | await driver.tap(incrementButtonFinder); 55 | expect(await driver.getText(homePageCounterFinder), 56 | 'You have pushed the button on this page 1 time(s)'); 57 | }); 58 | 59 | test('increment counter on gallery page', () async { 60 | await _selectPage(driver, drawerMenuButtonFinder, galleryDrawerItemFinder, 61 | galleryPageFinder); 62 | expect(await driver.getText(galleryPageCounterFinder), 63 | 'You have pushed the button on this page 0 time(s)'); 64 | await driver.tap(incrementButtonFinder); 65 | await driver.tap(incrementButtonFinder); 66 | expect(await driver.getText(galleryPageCounterFinder), 67 | 'You have pushed the button on this page 2 time(s)'); 68 | }); 69 | 70 | test('increment counter on slideshow page', () async { 71 | await _selectPage(driver, drawerMenuButtonFinder, 72 | slideshowDrawerItemFinder, slideshowPageFinder); 73 | expect(await driver.getText(slideshowPageCounterFinder), 74 | 'You have pushed the button on this page 0 time(s)'); 75 | await driver.tap(incrementButtonFinder); 76 | await driver.tap(incrementButtonFinder); 77 | await driver.tap(incrementButtonFinder); 78 | expect(await driver.getText(slideshowPageCounterFinder), 79 | 'You have pushed the button on this page 3 time(s)'); 80 | }); 81 | 82 | test('toggle dark mode', () async { 83 | await _selectPage(driver, drawerMenuButtonFinder, 84 | settingsDrawerItemFinder, settingsPageFinder); 85 | await driver.tap(useDarkModeSettingFinder); 86 | }); 87 | 88 | test('the last value of the counters on each page are as expected', 89 | () async { 90 | await _selectPage( 91 | driver, drawerMenuButtonFinder, homeDrawerItemFinder, homePageFinder); 92 | expect(await driver.getText(homePageCounterFinder), 93 | 'You have pushed the button on this page 1 time(s)'); 94 | await _selectPage(driver, drawerMenuButtonFinder, galleryDrawerItemFinder, 95 | galleryPageFinder); 96 | expect(await driver.getText(galleryPageCounterFinder), 97 | 'You have pushed the button on this page 2 time(s)'); 98 | await _selectPage(driver, drawerMenuButtonFinder, 99 | slideshowDrawerItemFinder, slideshowPageFinder); 100 | expect(await driver.getText(slideshowPageCounterFinder), 101 | 'You have pushed the button on this page 3 time(s)'); 102 | }); 103 | }); 104 | } 105 | 106 | Future _selectPage( 107 | FlutterDriver driver, 108 | SerializableFinder drawerMenuButtonFinder, 109 | SerializableFinder drawerItemFinder, 110 | SerializableFinder pageFinder) async { 111 | await driver.tap(drawerMenuButtonFinder); 112 | await driver.tap(drawerItemFinder); 113 | await driver.waitFor(pageFinder); 114 | } 115 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | .dart_tool/ 24 | .flutter-plugins 25 | .flutter-plugins-dependencies 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | /build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 72 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 7a4c33425ddd78c54aba07d86f3f9a4a0051769b 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/README.md: -------------------------------------------------------------------------------- 1 | # Template: material design application with navigation drawer, routes, provider and MobX 2 | 3 | This is a template for creating a Flutter application that uses material design and has a navigation drawer. Selecting a destination other than the home triggers navigating to another. Uses [provider](https://github.com/rrousselGit/provider) and [MobX](https://github.com/mobxjs/mobx.dart) to manage state. 4 | 5 | ## Overview 6 | 7 | The application is similar to the one created when creating an Android application that has a navigation drawer. It has four destinations: Home, Gallery, Slideshow and Settings. The latter allows toggling the dark mode theme on and off. This setting is saved using the [shared_preferences](https://github.com/flutter/plugins/tree/master/packages/shared_preferences) plugin. This template is similar to the [`material_navigationdrawer_provider_mobx` template](https://github.com/MaikuB/flutter_app_templates/tree/master/material_navigationdrawer_provider_mobx) except routes are used to take users the destination. 8 | 9 | Each destination has its own corresponding page and data store. Tapping on the floating action button will increment the counter related to the page that is currently being viewed. Selecting a destination other than the home page will trigger navigating to the selected destination using routes. From there, the user has the option of tapping on the back button to go back to the home page that is configured as the initial route. The settings page allows toggling on and off the dark theme. 10 | The application is structured as follows: 11 | 12 | - `lib` 13 | - `constants` - this folder is where all application-level constants are kept 14 | - `routes.dart` - to maintain all of the routes that represent the destinations 15 | - `keys.dart` - to maintain the keys associated with the various widgets in the application 16 | - `pages` - has the pages associated with the destinations. Each page has a different title will maintain a separate counter to help demonstrate that each page has its own state 17 | - `services` - has all of the application services used e.g. a service for managing shared preferences 18 | - `stores` - has all of the data stores associated with each page/destination 19 | - `main.dart` - the entry point for the application and contains the top-level widgets for the application itself 20 | - `test` 21 | - `mocks` - for storing mocks that will be used by various tests 22 | - `stores` - this folder has unit tests for all of the stores 23 | - `widgets` - this folder contains all of the widget tests 24 | - `pages` - this folder that has files represent widget tests that are tied to pages 25 | - `app_test.dart` - for doing tests on widgets that are at the top-level e.g. checking bottom navigation bar exists and works 26 | - `test_driver` - this folder has all of the integration tests. Makes use of the `flutter_driver` library 27 | 28 | ## Executing unit tests and widget tests 29 | 30 | To execute unit tests, run the following command from the root folder of the application 31 | 32 | ``` 33 | flutter test 34 | ``` 35 | 36 | ## Executing integration tests 37 | 38 | To execute integration tests, run the following command from the root folder of the application 39 | 40 | ``` 41 | flutter drive --target=test_driver/app.dart 42 | ``` 43 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/lib/constants/keys.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class Keys { 4 | static const incrementButtonKey = Key('incrementButton'); 5 | static const homePageKey = Key('homePage'); 6 | static const homeDrawerItemKey = Key('homeDrawerItem'); 7 | static const homePageTitleKey = Key('homePageTitle'); 8 | static const homePageCounterKey = Key('homePageCounter'); 9 | static const galleryPageKey = Key('galleryPage'); 10 | static const galleryDrawerItemKey = Key('galleryDrawerItem'); 11 | static const galleryPageTitleKey = Key('galleryPageTitle'); 12 | static const galleryPageCounterKey = Key('galleryPageCounter'); 13 | static const slideshowPageKey = Key('slideshowPage'); 14 | static const slideshowDrawerItemKey = Key('slideshowDrawerItem'); 15 | static const slideshowPageTitleKey = Key('slideshowPageTitle'); 16 | static const slideshowPageCounterKey = Key('slideshowPageCounter'); 17 | static const settingsDrawerItemKey = Key('settingsDrawerItem'); 18 | static const settingsPageKey = Key('settingsPage'); 19 | static const settingsPageTitleKey = Key('settingsPageTitle'); 20 | static const useDarkModeSettingKey = Key('useDarkModeSetting'); 21 | } 22 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/lib/constants/routes.dart: -------------------------------------------------------------------------------- 1 | class Routes { 2 | static const home = '/'; 3 | static const gallery = '/gallery'; 4 | static const slideshow = '/slideshow'; 5 | static const settings = '/settings'; 6 | } 7 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import 'package:material_navigationdrawerroutes_provider_mobx/pages/settings_page.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | import 'constants/keys.dart'; 7 | import 'constants/routes.dart'; 8 | import 'pages/gallery_page.dart'; 9 | import 'pages/home_page.dart'; 10 | import 'pages/slideshow_page.dart'; 11 | import 'services/preferences_service.dart'; 12 | import 'stores/gallery_store.dart'; 13 | import 'stores/home_store.dart'; 14 | import 'stores/settings_store.dart'; 15 | import 'stores/slideshow_store.dart'; 16 | 17 | void main() async { 18 | final sharedPreferences = await SharedPreferences.getInstance(); 19 | runApp(App(sharedPreferences)); 20 | } 21 | 22 | class App extends StatelessWidget { 23 | const App(this.sharedPreferences); 24 | 25 | final SharedPreferences sharedPreferences; 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return MultiProvider( 30 | providers: [ 31 | Provider( 32 | create: (_) => HomeStore(), 33 | ), 34 | Provider( 35 | create: (_) => GalleryStore(), 36 | ), 37 | Provider( 38 | create: (_) => SlideshowStore(), 39 | ), 40 | Provider( 41 | create: (_) => PreferencesService(sharedPreferences), 42 | ), 43 | ProxyProvider( 44 | update: (_, preferencesService, ___) => 45 | SettingsStore(preferencesService), 46 | ) 47 | ], 48 | child: Consumer( 49 | builder: (context, store, _) { 50 | return Observer( 51 | builder: (_) { 52 | return MaterialApp( 53 | title: 'App title', 54 | theme: store.useDarkMode ? ThemeData.dark() : ThemeData.light(), 55 | initialRoute: Routes.home, 56 | routes: { 57 | Routes.home: (_) => Consumer( 58 | builder: (_, store, __) => 59 | HomePage(store, key: Keys.homePageKey), 60 | ), 61 | Routes.gallery: (_) => Consumer( 62 | builder: (_, store, __) => 63 | GalleryPage(store, key: Keys.galleryPageKey), 64 | ), 65 | Routes.slideshow: (context) => Consumer( 66 | builder: (_, store, __) => 67 | SlideshowPage(store, key: Keys.slideshowPageKey), 68 | ), 69 | Routes.settings: (context) => Consumer( 70 | builder: (_, store, __) => 71 | SettingsPage(store, key: Keys.settingsPageKey), 72 | ), 73 | }, 74 | ); 75 | }, 76 | ); 77 | }, 78 | ), 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/lib/pages/gallery_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import '../constants/keys.dart'; 4 | import '../stores/gallery_store.dart'; 5 | 6 | class GalleryPage extends StatelessWidget { 7 | const GalleryPage(this.store, {Key key}) : super(key: key); 8 | 9 | final GalleryStore store; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | title: const Text('Gallery', key: Keys.galleryPageTitleKey), 16 | ), 17 | body: Observer( 18 | builder: (context) { 19 | return Center( 20 | child: Text( 21 | 'You have pushed the button on this page ${store.counter} time(s)', 22 | key: Keys.galleryPageCounterKey, 23 | ), 24 | ); 25 | }, 26 | ), 27 | floatingActionButton: FloatingActionButton( 28 | key: const Key('incrementButton'), 29 | onPressed: () { 30 | store.increment(); 31 | }, 32 | tooltip: 'Increment', 33 | child: const Icon(Icons.add), 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/lib/pages/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import '../constants/keys.dart'; 4 | import '../constants/routes.dart'; 5 | import '../stores/home_store.dart'; 6 | 7 | class HomePage extends StatelessWidget { 8 | const HomePage(this.store, {Key key}) : super(key: key); 9 | 10 | final HomeStore store; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Scaffold( 15 | appBar: AppBar( 16 | title: const Text('Home', key: Keys.homePageTitleKey), 17 | ), 18 | body: Observer( 19 | builder: (context) { 20 | return Center( 21 | child: Text( 22 | 'You have pushed the button on this page ${store.counter} time(s)', 23 | key: Keys.homePageCounterKey, 24 | ), 25 | ); 26 | }, 27 | ), 28 | drawer: Drawer( 29 | child: ListView( 30 | padding: EdgeInsets.zero, 31 | children: [ 32 | const UserAccountsDrawerHeader( 33 | accountName: Text('User'), 34 | accountEmail: Text('user@email.com'), 35 | currentAccountPicture: CircleAvatar( 36 | child: Icon(Icons.android), 37 | ), 38 | ), 39 | ListTile( 40 | key: Keys.homeDrawerItemKey, 41 | leading: const Icon(Icons.home), 42 | title: const Text('Home'), 43 | onTap: () { 44 | Navigator.pop(context); 45 | }, 46 | ), 47 | ListTile( 48 | key: Keys.galleryDrawerItemKey, 49 | leading: const Icon(Icons.photo_library), 50 | title: const Text('Gallery'), 51 | onTap: () async { 52 | Navigator.pop(context); 53 | Navigator.pushNamed(context, Routes.gallery); 54 | }, 55 | ), 56 | ListTile( 57 | key: Keys.slideshowDrawerItemKey, 58 | leading: const Icon(Icons.slideshow), 59 | title: const Text('Slideshow'), 60 | onTap: () async { 61 | Navigator.pop(context); 62 | Navigator.pushNamed(context, Routes.slideshow); 63 | }, 64 | ), 65 | Divider(), 66 | ListTile( 67 | key: Keys.settingsDrawerItemKey, 68 | leading: const Icon(Icons.settings), 69 | title: const Text('Settings'), 70 | onTap: () async { 71 | Navigator.pop(context); 72 | Navigator.pushNamed(context, Routes.settings); 73 | }, 74 | ), 75 | ], 76 | ), 77 | ), 78 | floatingActionButton: FloatingActionButton( 79 | key: const Key('incrementButton'), 80 | onPressed: () { 81 | store.increment(); 82 | }, 83 | tooltip: 'Increment', 84 | child: const Icon(Icons.add), 85 | ), 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/lib/pages/settings_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import '../constants/keys.dart'; 4 | import '../stores/settings_store.dart'; 5 | 6 | class SettingsPage extends StatelessWidget { 7 | const SettingsPage(this.store, {Key key}) : super(key: key); 8 | 9 | final SettingsStore store; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | title: const Text('Settings', key: Keys.settingsPageTitleKey), 16 | ), 17 | body: Observer( 18 | builder: (context) { 19 | return SwitchListTile( 20 | key: Keys.useDarkModeSettingKey, 21 | value: store.useDarkMode, 22 | title: const Text('Use dark mode'), 23 | onChanged: (value) { 24 | store.setDarkMode(value); 25 | }, 26 | ); 27 | }, 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/lib/pages/slideshow_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_mobx/flutter_mobx.dart'; 3 | import '../constants/keys.dart'; 4 | import '../stores/slideshow_store.dart'; 5 | 6 | class SlideshowPage extends StatelessWidget { 7 | const SlideshowPage(this.store, {Key key}) : super(key: key); 8 | 9 | final SlideshowStore store; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: AppBar( 15 | title: const Text( 16 | 'Slideshow', 17 | key: Keys.slideshowPageTitleKey, 18 | ), 19 | ), 20 | body: Observer( 21 | builder: (context) { 22 | return Center( 23 | child: Text( 24 | 'You have pushed the button on this page ${store.counter} time(s)', 25 | key: Keys.slideshowPageCounterKey, 26 | ), 27 | ); 28 | }, 29 | ), 30 | floatingActionButton: FloatingActionButton( 31 | key: const Key('incrementButton'), 32 | onPressed: () { 33 | store.increment(); 34 | }, 35 | tooltip: 'Increment', 36 | child: const Icon(Icons.add), 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/lib/services/preferences_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | class PreferencesService { 4 | const PreferencesService(this._sharedPreferences); 5 | 6 | final String _useDarkModeKey = 'useDarkMode'; 7 | final SharedPreferences _sharedPreferences; 8 | 9 | set useDarkMode(bool useDarkMode) { 10 | _sharedPreferences.setBool(_useDarkModeKey, useDarkMode); 11 | } 12 | 13 | bool get useDarkMode => _sharedPreferences.getBool(_useDarkModeKey) ?? false; 14 | } 15 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/lib/stores/gallery_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | 3 | part 'gallery_store.g.dart'; 4 | 5 | class GalleryStore = GalleryStoreBase with _$GalleryStore; 6 | 7 | abstract class GalleryStoreBase with Store { 8 | @observable 9 | int counter = 0; 10 | 11 | @action 12 | void increment() { 13 | counter++; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/lib/stores/gallery_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'gallery_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars 10 | 11 | mixin _$GalleryStore on GalleryStoreBase, Store { 12 | final _$counterAtom = Atom(name: 'GalleryStoreBase.counter'); 13 | 14 | @override 15 | int get counter { 16 | _$counterAtom.context.enforceReadPolicy(_$counterAtom); 17 | _$counterAtom.reportObserved(); 18 | return super.counter; 19 | } 20 | 21 | @override 22 | set counter(int value) { 23 | _$counterAtom.context.conditionallyRunInAction(() { 24 | super.counter = value; 25 | _$counterAtom.reportChanged(); 26 | }, _$counterAtom, name: '${_$counterAtom.name}_set'); 27 | } 28 | 29 | final _$GalleryStoreBaseActionController = 30 | ActionController(name: 'GalleryStoreBase'); 31 | 32 | @override 33 | void increment() { 34 | final _$actionInfo = _$GalleryStoreBaseActionController.startAction(); 35 | try { 36 | return super.increment(); 37 | } finally { 38 | _$GalleryStoreBaseActionController.endAction(_$actionInfo); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/lib/stores/home_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | 3 | part 'home_store.g.dart'; 4 | 5 | class HomeStore = HomeStoreBase with _$HomeStore; 6 | 7 | abstract class HomeStoreBase with Store { 8 | @observable 9 | int counter = 0; 10 | 11 | @action 12 | void increment() { 13 | counter++; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/lib/stores/home_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'home_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars 10 | 11 | mixin _$HomeStore on HomeStoreBase, Store { 12 | final _$counterAtom = Atom(name: 'HomeStoreBase.counter'); 13 | 14 | @override 15 | int get counter { 16 | _$counterAtom.context.enforceReadPolicy(_$counterAtom); 17 | _$counterAtom.reportObserved(); 18 | return super.counter; 19 | } 20 | 21 | @override 22 | set counter(int value) { 23 | _$counterAtom.context.conditionallyRunInAction(() { 24 | super.counter = value; 25 | _$counterAtom.reportChanged(); 26 | }, _$counterAtom, name: '${_$counterAtom.name}_set'); 27 | } 28 | 29 | final _$HomeStoreBaseActionController = 30 | ActionController(name: 'HomeStoreBase'); 31 | 32 | @override 33 | void increment() { 34 | final _$actionInfo = _$HomeStoreBaseActionController.startAction(); 35 | try { 36 | return super.increment(); 37 | } finally { 38 | _$HomeStoreBaseActionController.endAction(_$actionInfo); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/lib/stores/settings_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | import '../services/preferences_service.dart'; 3 | part 'settings_store.g.dart'; 4 | 5 | class SettingsStore = SettingsStoreBase with _$SettingsStore; 6 | 7 | abstract class SettingsStoreBase with Store { 8 | /// when the store is created, we read in the current settings immediately to avoid the scenario where 9 | /// the values displayed will change upon switching to the settings tab 10 | SettingsStoreBase(this._preferencesService) { 11 | useDarkMode = _preferencesService.useDarkMode; 12 | } 13 | 14 | PreferencesService _preferencesService; 15 | 16 | @observable 17 | bool useDarkMode; 18 | 19 | @action 20 | void setDarkMode(bool updatedDarkModePreference) { 21 | _preferencesService.useDarkMode = updatedDarkModePreference; 22 | useDarkMode = updatedDarkModePreference; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/lib/stores/settings_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'settings_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars 10 | 11 | mixin _$SettingsStore on SettingsStoreBase, Store { 12 | final _$useDarkModeAtom = Atom(name: 'SettingsStoreBase.useDarkMode'); 13 | 14 | @override 15 | bool get useDarkMode { 16 | _$useDarkModeAtom.context.enforceReadPolicy(_$useDarkModeAtom); 17 | _$useDarkModeAtom.reportObserved(); 18 | return super.useDarkMode; 19 | } 20 | 21 | @override 22 | set useDarkMode(bool value) { 23 | _$useDarkModeAtom.context.conditionallyRunInAction(() { 24 | super.useDarkMode = value; 25 | _$useDarkModeAtom.reportChanged(); 26 | }, _$useDarkModeAtom, name: '${_$useDarkModeAtom.name}_set'); 27 | } 28 | 29 | final _$SettingsStoreBaseActionController = 30 | ActionController(name: 'SettingsStoreBase'); 31 | 32 | @override 33 | void setDarkMode(bool updatedDarkModePreference) { 34 | final _$actionInfo = _$SettingsStoreBaseActionController.startAction(); 35 | try { 36 | return super.setDarkMode(updatedDarkModePreference); 37 | } finally { 38 | _$SettingsStoreBaseActionController.endAction(_$actionInfo); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/lib/stores/slideshow_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | 3 | part 'slideshow_store.g.dart'; 4 | 5 | class SlideshowStore = SlideshowStoreBase with _$SlideshowStore; 6 | 7 | abstract class SlideshowStoreBase with Store { 8 | @observable 9 | int counter = 0; 10 | 11 | @action 12 | void increment() { 13 | counter++; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/lib/stores/slideshow_store.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'slideshow_store.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars 10 | 11 | mixin _$SlideshowStore on SlideshowStoreBase, Store { 12 | final _$counterAtom = Atom(name: 'SlideshowStoreBase.counter'); 13 | 14 | @override 15 | int get counter { 16 | _$counterAtom.context.enforceReadPolicy(_$counterAtom); 17 | _$counterAtom.reportObserved(); 18 | return super.counter; 19 | } 20 | 21 | @override 22 | set counter(int value) { 23 | _$counterAtom.context.conditionallyRunInAction(() { 24 | super.counter = value; 25 | _$counterAtom.reportChanged(); 26 | }, _$counterAtom, name: '${_$counterAtom.name}_set'); 27 | } 28 | 29 | final _$SlideshowStoreBaseActionController = 30 | ActionController(name: 'SlideshowStoreBase'); 31 | 32 | @override 33 | void increment() { 34 | final _$actionInfo = _$SlideshowStoreBaseActionController.startAction(); 35 | try { 36 | return super.increment(); 37 | } finally { 38 | _$SlideshowStoreBaseActionController.endAction(_$actionInfo); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: material_navigationdrawerroutes_provider_mobx 2 | description: A new Flutter project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.2.2 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | 23 | # The following adds the Cupertino Icons font to your application. 24 | # Use with the CupertinoIcons class for iOS style icons. 25 | cupertino_icons: 26 | 27 | provider: 28 | mobx: 29 | flutter_mobx: 30 | shared_preferences: 31 | 32 | dev_dependencies: 33 | flutter_driver: 34 | sdk: flutter 35 | flutter_test: 36 | sdk: flutter 37 | test: 38 | build_runner: 1.10.1 39 | mobx_codegen: 40 | mockito: 41 | 42 | 43 | # For information on the generic Dart part of this file, see the 44 | # following page: https://www.dartlang.org/tools/pub/pubspec 45 | 46 | # The following section is specific to Flutter. 47 | flutter: 48 | 49 | # The following line ensures that the Material Icons font is 50 | # included with your application, so that you can use the icons in 51 | # the material Icons class. 52 | uses-material-design: true 53 | 54 | # To add assets to your application, add an assets section, like this: 55 | # assets: 56 | # - images/a_dot_burr.jpeg 57 | # - images/a_dot_ham.jpeg 58 | 59 | # An image asset can refer to one or more resolution-specific "variants", see 60 | # https://flutter.dev/assets-and-images/#resolution-aware. 61 | 62 | # For details regarding adding assets from package dependencies, see 63 | # https://flutter.dev/assets-and-images/#from-packages 64 | 65 | # To add custom fonts to your application, add a fonts section here, 66 | # in this "flutter" section. Each entry in this list should have a 67 | # "family" key with the font family name, and a "fonts" key with a 68 | # list giving the asset and other descriptors for the font. For 69 | # example: 70 | # fonts: 71 | # - family: Schyler 72 | # fonts: 73 | # - asset: fonts/Schyler-Regular.ttf 74 | # - asset: fonts/Schyler-Italic.ttf 75 | # style: italic 76 | # - family: Trajan Pro 77 | # fonts: 78 | # - asset: fonts/TrajanPro.ttf 79 | # - asset: fonts/TrajanPro_Bold.ttf 80 | # weight: 700 81 | # 82 | # For details regarding fonts from package dependencies, 83 | # see https://flutter.dev/custom-fonts/#from-packages 84 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/test/mocks/mock_shared_preferences.dart: -------------------------------------------------------------------------------- 1 | import 'package:mockito/mockito.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | class MockedSharedPreferences extends Mock implements SharedPreferences {} 5 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/test/stores/gallery_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:material_navigationdrawerroutes_provider_mobx/stores/gallery_store.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('GalleryStore', () { 6 | GalleryStore store; 7 | setUpAll(() { 8 | store = GalleryStore(); 9 | }); 10 | test('Counter starts at 0', () { 11 | expect(store.counter, 0); 12 | }); 13 | test('Increment works', () { 14 | store.increment(); 15 | expect(store.counter, 1); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/test/stores/home_store_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:material_navigationdrawerroutes_provider_mobx/stores/home_store.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('HomeStore', () { 6 | HomeStore store; 7 | setUpAll(() { 8 | store = HomeStore(); 9 | }); 10 | test('Counter starts at 0', () { 11 | expect(store.counter, 0); 12 | }); 13 | test('Increment works', () { 14 | store.increment(); 15 | expect(store.counter, 1); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/test/stores/settings_store_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:material_navigationdrawerroutes_provider_mobx/services/preferences_service.dart'; 2 | import 'package:material_navigationdrawerroutes_provider_mobx/stores/settings_store.dart'; 3 | import 'package:test/test.dart'; 4 | import '../mocks/mock_shared_preferences.dart'; 5 | 6 | void main() { 7 | group('SlideshowStore', () { 8 | SettingsStore store; 9 | setUpAll(() { 10 | store = SettingsStore(PreferencesService(MockedSharedPreferences())); 11 | }); 12 | test('Use dark mode settings default to off', () { 13 | expect(store.useDarkMode, false); 14 | }); 15 | test('Turning on dark mode works', () { 16 | store.setDarkMode(true); 17 | expect(store.useDarkMode, true); 18 | }); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/test/stores/slideshow_store_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:material_navigationdrawerroutes_provider_mobx/stores/slideshow_store.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('SlideshowStore', () { 6 | SlideshowStore store; 7 | setUpAll(() { 8 | store = SlideshowStore(); 9 | }); 10 | test('Counter starts at 0', () { 11 | expect(store.counter, 0); 12 | }); 13 | test('Increment works', () { 14 | store.increment(); 15 | expect(store.counter, 1); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/test/widgets/app_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:material_navigationdrawerroutes_provider_mobx/pages/settings_page.dart'; 4 | import 'package:material_navigationdrawerroutes_provider_mobx/constants/keys.dart'; 5 | import 'package:material_navigationdrawerroutes_provider_mobx/main.dart'; 6 | import 'package:material_navigationdrawerroutes_provider_mobx/pages/gallery_page.dart'; 7 | import 'package:material_navigationdrawerroutes_provider_mobx/pages/home_page.dart'; 8 | import 'package:material_navigationdrawerroutes_provider_mobx/pages/slideshow_page.dart'; 9 | import '../mocks/mock_shared_preferences.dart'; 10 | 11 | void main() async { 12 | group('App', () { 13 | final drawerMenuButtonFinder = find.byTooltip('Open navigation menu'); 14 | final backButtonFinder = find.byTooltip('Back'); 15 | final drawerFinder = find.byType(Drawer); 16 | final homePageFinder = find.byType(HomePage); 17 | final galleryPageFinder = find.byType(GalleryPage); 18 | final slideshowPageFinder = find.byType(SlideshowPage); 19 | final settingsPageFinder = find.byType(SettingsPage); 20 | final incrementButtonFinder = find.byType(FloatingActionButton); 21 | final homeIconFinder = find.byIcon(Icons.home); 22 | final galleryIconFinder = find.byIcon(Icons.photo_library); 23 | final slideshowIconFinder = find.byIcon(Icons.slideshow); 24 | final settingsIconFinder = find.byIcon(Icons.settings); 25 | final mockedSharedPreferences = MockedSharedPreferences(); 26 | testWidgets('Starts on home page with drawer closed', 27 | (WidgetTester tester) async { 28 | await _pumpWidget(tester, mockedSharedPreferences); 29 | expect(drawerMenuButtonFinder, findsOneWidget); 30 | expect(drawerFinder, findsNothing); 31 | expect(homePageFinder, findsOneWidget); 32 | expect(galleryPageFinder, findsNothing); 33 | expect(slideshowPageFinder, findsNothing); 34 | }); 35 | 36 | testWidgets('Floating action button exists', (WidgetTester tester) async { 37 | await tester.pumpWidget(App(mockedSharedPreferences)); 38 | expect(incrementButtonFinder, findsOneWidget); 39 | }); 40 | 41 | testWidgets('Drawer exists', (WidgetTester tester) async { 42 | await _pumpWidget(tester, mockedSharedPreferences); 43 | await openDrawer( 44 | tester, 45 | drawerMenuButtonFinder, 46 | drawerFinder, 47 | homeIconFinder, 48 | galleryIconFinder, 49 | slideshowIconFinder, 50 | settingsIconFinder); 51 | }); 52 | 53 | testWidgets('Select home page', (WidgetTester tester) async { 54 | await _pumpWidget(tester, mockedSharedPreferences); 55 | await openDrawer( 56 | tester, 57 | drawerMenuButtonFinder, 58 | drawerFinder, 59 | homeIconFinder, 60 | galleryIconFinder, 61 | slideshowIconFinder, 62 | settingsIconFinder); 63 | 64 | // select home page 65 | await tester.tap(homeIconFinder); 66 | await tester.pumpAndSettle(); 67 | 68 | expect(drawerMenuButtonFinder, findsOneWidget); 69 | expect(drawerFinder, findsNothing); 70 | expect(backButtonFinder, findsNothing); 71 | 72 | // should now be on home page 73 | expect(homePageFinder, findsOneWidget); 74 | expect(galleryPageFinder, findsNothing); 75 | expect(slideshowPageFinder, findsNothing); 76 | expect(find.byKey(Keys.homePageTitleKey), findsOneWidget); 77 | expect(find.text('You have pushed the button on this page 0 time(s)'), 78 | findsOneWidget); 79 | expect(incrementButtonFinder, findsOneWidget); 80 | }); 81 | 82 | testWidgets('Select gallery page', (WidgetTester tester) async { 83 | await _pumpWidget(tester, mockedSharedPreferences); 84 | await openDrawer( 85 | tester, 86 | drawerMenuButtonFinder, 87 | drawerFinder, 88 | homeIconFinder, 89 | galleryIconFinder, 90 | slideshowIconFinder, 91 | settingsIconFinder); 92 | 93 | // select gallery page 94 | await tester.tap(galleryIconFinder); 95 | await tester.pumpAndSettle(); 96 | 97 | // drawer should now be closed 98 | expect(drawerFinder, findsNothing); 99 | expect(backButtonFinder, findsOneWidget); 100 | 101 | // should now be on gallery page 102 | expect(homePageFinder, findsNothing); 103 | expect(galleryPageFinder, findsOneWidget); 104 | expect(slideshowPageFinder, findsNothing); 105 | expect(find.byKey(Keys.galleryPageTitleKey), findsOneWidget); 106 | expect(find.text('You have pushed the button on this page 0 time(s)'), 107 | findsOneWidget); 108 | expect(incrementButtonFinder, findsOneWidget); 109 | }); 110 | 111 | testWidgets('Select slideshow page', (WidgetTester tester) async { 112 | await _pumpWidget(tester, mockedSharedPreferences); 113 | await openDrawer( 114 | tester, 115 | drawerMenuButtonFinder, 116 | drawerFinder, 117 | homeIconFinder, 118 | galleryIconFinder, 119 | slideshowIconFinder, 120 | settingsIconFinder); 121 | 122 | // select slideshow page 123 | await tester.tap(slideshowIconFinder); 124 | await tester.pumpAndSettle(); 125 | 126 | // drawer should now be closed 127 | expect(drawerFinder, findsNothing); 128 | expect(backButtonFinder, findsOneWidget); 129 | 130 | // should now be on slideshow page 131 | expect(homePageFinder, findsNothing); 132 | expect(galleryPageFinder, findsNothing); 133 | expect(slideshowPageFinder, findsOneWidget); 134 | expect(find.byKey(Keys.slideshowPageTitleKey), findsOneWidget); 135 | expect(find.text('You have pushed the button on this page 0 time(s)'), 136 | findsOneWidget); 137 | expect(incrementButtonFinder, findsOneWidget); 138 | }); 139 | 140 | testWidgets('Select settings page', (WidgetTester tester) async { 141 | await _pumpWidget(tester, mockedSharedPreferences); 142 | await openDrawer( 143 | tester, 144 | drawerMenuButtonFinder, 145 | drawerFinder, 146 | homeIconFinder, 147 | galleryIconFinder, 148 | slideshowIconFinder, 149 | settingsIconFinder); 150 | 151 | // select settings page 152 | await tester.tap(settingsIconFinder); 153 | await tester.pumpAndSettle(); 154 | 155 | // drawer should now be closed 156 | expect(drawerFinder, findsNothing); 157 | expect(backButtonFinder, findsOneWidget); 158 | 159 | // should now be on settings page 160 | expect(homePageFinder, findsNothing); 161 | expect(galleryPageFinder, findsNothing); 162 | expect(slideshowPageFinder, findsNothing); 163 | expect(settingsPageFinder, findsOneWidget); 164 | expect(find.byKey(Keys.settingsPageKey), findsOneWidget); 165 | expect(find.byType(SwitchListTile), findsOneWidget); 166 | }); 167 | }); 168 | } 169 | 170 | Future _pumpWidget(WidgetTester tester, 171 | MockedSharedPreferences mockedSharedPreferences) async { 172 | await tester.pumpWidget(App(mockedSharedPreferences)); 173 | } 174 | 175 | Future openDrawer( 176 | WidgetTester tester, 177 | Finder drawerMenuButtonFinder, 178 | Finder drawerFinder, 179 | Finder homeIconFinder, 180 | Finder galleryIconFinder, 181 | Finder slideshowIconFinder, 182 | Finder settingsIconFinder) async { 183 | // open the drawer 184 | await tester.tap(drawerMenuButtonFinder); 185 | // wait for drawer animation to finish 186 | await tester.pumpAndSettle(); 187 | expect(drawerFinder, findsOneWidget); 188 | expect(homeIconFinder, findsOneWidget); 189 | expect(galleryIconFinder, findsOneWidget); 190 | expect(slideshowIconFinder, findsOneWidget); 191 | expect(settingsIconFinder, findsOneWidget); 192 | } 193 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/test/widgets/pages/gallery_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:material_navigationdrawerroutes_provider_mobx/pages/gallery_page.dart'; 4 | import 'package:material_navigationdrawerroutes_provider_mobx/stores/gallery_store.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | void main() { 8 | group('GalleryPage', () { 9 | final pageFinder = find.byType(GalleryPage); 10 | final store = GalleryStore(); 11 | testWidgets('Counter starts at zero', (WidgetTester tester) async { 12 | await _pumpWidget(tester, store); 13 | 14 | expect(pageFinder, findsOneWidget); 15 | expect(find.text('You have pushed the button on this page 0 time(s)'), 16 | findsOneWidget); 17 | }); 18 | 19 | testWidgets('Counter updates', (WidgetTester tester) async { 20 | await _pumpWidget(tester, store); 21 | 22 | store.increment(); 23 | await tester.pump(); 24 | expect(find.text('You have pushed the button on this page 1 time(s)'), 25 | findsOneWidget); 26 | }); 27 | }); 28 | } 29 | 30 | Future _pumpWidget(WidgetTester tester, GalleryStore store) async { 31 | await tester.pumpWidget( 32 | Provider( 33 | create: (_) => store, 34 | child: MaterialApp( 35 | home: Consumer( 36 | builder: (_, store, __) => GalleryPage(store), 37 | ), 38 | ), 39 | ), 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/test/widgets/pages/home_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:material_navigationdrawerroutes_provider_mobx/pages/home_page.dart'; 4 | import 'package:material_navigationdrawerroutes_provider_mobx/stores/home_store.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | void main() { 8 | group('HomePage', () { 9 | final pageFinder = find.byType(HomePage); 10 | final store = HomeStore(); 11 | testWidgets('Counter starts at zero', (WidgetTester tester) async { 12 | await _pumpWidget(tester, store); 13 | 14 | expect(pageFinder, findsOneWidget); 15 | expect(find.text('You have pushed the button on this page 0 time(s)'), 16 | findsOneWidget); 17 | }); 18 | 19 | testWidgets('Counter updates', (WidgetTester tester) async { 20 | await _pumpWidget(tester, store); 21 | 22 | store.increment(); 23 | await tester.pump(); 24 | expect(find.text('You have pushed the button on this page 1 time(s)'), 25 | findsOneWidget); 26 | }); 27 | }); 28 | } 29 | 30 | Future _pumpWidget(WidgetTester tester, HomeStore store) async { 31 | await tester.pumpWidget( 32 | Provider( 33 | create: (_) => store, 34 | child: MaterialApp( 35 | home: Consumer(builder: (_, store, __) => HomePage(store)), 36 | ), 37 | ), 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/test/widgets/pages/settings_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:material_navigationdrawerroutes_provider_mobx/constants/keys.dart'; 4 | import 'package:material_navigationdrawerroutes_provider_mobx/pages/settings_page.dart'; 5 | import 'package:material_navigationdrawerroutes_provider_mobx/services/preferences_service.dart'; 6 | import 'package:material_navigationdrawerroutes_provider_mobx/stores/settings_store.dart'; 7 | import 'package:provider/provider.dart'; 8 | import '../../mocks/mock_shared_preferences.dart'; 9 | 10 | void main() { 11 | group('SettingsPage', () { 12 | final mockedSharedPreferences = MockedSharedPreferences(); 13 | final preferencesService = PreferencesService(mockedSharedPreferences); 14 | final store = SettingsStore(preferencesService); 15 | final pageFinder = find.byType(SettingsPage); 16 | final useDarkModeSettingFinder = find.byKey(Keys.useDarkModeSettingKey); 17 | testWidgets('Dark mode setting starts at off', (WidgetTester tester) async { 18 | await _pumpWidget(tester, store); 19 | 20 | expect(pageFinder, findsOneWidget); 21 | expect(useDarkModeSettingFinder, findsOneWidget); 22 | expect(store.useDarkMode, false); 23 | }); 24 | 25 | testWidgets('Turn dark mode on', (WidgetTester tester) async { 26 | await _pumpWidget(tester, store); 27 | 28 | await tester.tap(useDarkModeSettingFinder); 29 | await tester.pumpAndSettle(); 30 | expect(store.useDarkMode, true); 31 | }); 32 | }); 33 | } 34 | 35 | Future _pumpWidget(WidgetTester tester, SettingsStore store) async { 36 | await tester.pumpWidget( 37 | Provider( 38 | create: (_) => store, 39 | child: MaterialApp( 40 | home: Consumer( 41 | builder: (_, store, __) => SettingsPage(store), 42 | ), 43 | ), 44 | ), 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/test/widgets/pages/slideshow_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:material_navigationdrawerroutes_provider_mobx/pages/slideshow_page.dart'; 4 | import 'package:material_navigationdrawerroutes_provider_mobx/stores/slideshow_store.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | void main() { 8 | group('SlideshowPage', () { 9 | final pageFinder = find.byType(SlideshowPage); 10 | final store = SlideshowStore(); 11 | testWidgets('Counter starts at zero', (WidgetTester tester) async { 12 | await _pumpWidget(tester, store); 13 | 14 | expect(pageFinder, findsOneWidget); 15 | expect(find.text('You have pushed the button on this page 0 time(s)'), 16 | findsOneWidget); 17 | }); 18 | 19 | testWidgets('Counter updates', (WidgetTester tester) async { 20 | await _pumpWidget(tester, store); 21 | 22 | store.increment(); 23 | await tester.pump(); 24 | expect(find.text('You have pushed the button on this page 1 time(s)'), 25 | findsOneWidget); 26 | }); 27 | }); 28 | } 29 | 30 | Future _pumpWidget(WidgetTester tester, SlideshowStore store) async { 31 | await tester.pumpWidget( 32 | Provider( 33 | create: (_) => store, 34 | child: MaterialApp( 35 | home: Consumer( 36 | builder: (_, store, __) => SlideshowPage(store), 37 | ), 38 | ), 39 | ), 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/test_driver/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_driver/driver_extension.dart'; 2 | import 'package:material_navigationdrawerroutes_provider_mobx/main.dart' as app; 3 | 4 | void main() { 5 | // This line enables the extension. 6 | enableFlutterDriverExtension(); 7 | 8 | // Call the `main()` function of the app, or call `runApp` with 9 | // any widget you are interested in testing. 10 | app.main(); 11 | } 12 | -------------------------------------------------------------------------------- /material_navigationdrawerroutes_provider_mobx/test_driver/app_test.dart: -------------------------------------------------------------------------------- 1 | // Imports the Flutter Driver API. 2 | import 'package:flutter_driver/flutter_driver.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('App', () { 7 | FlutterDriver driver; 8 | // Note: the approach for finding the drawer menu button and back button is more of a workaround. See https://github.com/flutter/flutter/issues/9002 9 | final drawerMenuButtonFinder = find.byTooltip('Open navigation menu'); 10 | final backButtonFinder = find.byTooltip('Back'); 11 | final galleryDrawerItemFinder = find.byValueKey('galleryDrawerItem'); 12 | final galleryPageFinder = find.byValueKey('galleryPage'); 13 | final settingsDrawerItemFinder = find.byValueKey('settingsDrawerItem'); 14 | final settingsPageFinder = find.byValueKey('settingsPage'); 15 | final slideshowDrawerItemFinder = find.byValueKey('slideshowDrawerItem'); 16 | final slideshowPageFinder = find.byValueKey('slideshowPage'); 17 | final incrementButtonFinder = find.byValueKey('incrementButton'); 18 | final homePageFinder = find.byValueKey('homePage'); 19 | final homePageCounterFinder = find.byValueKey('homePageCounter'); 20 | final galleryPageCounterFinder = find.byValueKey('galleryPageCounter'); 21 | final slideshowPageCounterFinder = find.byValueKey('slideshowPageCounter'); 22 | final useDarkModeSettingFinder = find.byValueKey('useDarkModeSetting'); 23 | setUpAll(() async { 24 | driver = await FlutterDriver.connect(); 25 | }); 26 | 27 | tearDownAll(() async { 28 | driver?.close(); 29 | }); 30 | 31 | test('check flutter driver health', () async { 32 | final health = await driver.checkHealth(); 33 | print('flutter driver status: ${health.status}'); 34 | }); 35 | 36 | test('starts on home page', () async { 37 | await driver.waitFor(find.byValueKey('homePage')); 38 | }); 39 | 40 | test('select pages works', () async { 41 | await _selectPage(driver, drawerMenuButtonFinder, galleryDrawerItemFinder, 42 | galleryPageFinder); 43 | await _goBackToHomePage(driver, backButtonFinder, homePageFinder); 44 | await _selectPage(driver, drawerMenuButtonFinder, 45 | slideshowDrawerItemFinder, slideshowPageFinder); 46 | await _goBackToHomePage(driver, backButtonFinder, homePageFinder); 47 | await _selectPage(driver, drawerMenuButtonFinder, 48 | settingsDrawerItemFinder, settingsPageFinder); 49 | await _goBackToHomePage(driver, backButtonFinder, homePageFinder); 50 | }); 51 | 52 | test('increment counter on home page', () async { 53 | expect(await driver.getText(homePageCounterFinder), 54 | 'You have pushed the button on this page 0 time(s)'); 55 | await driver.tap(incrementButtonFinder); 56 | expect(await driver.getText(homePageCounterFinder), 57 | 'You have pushed the button on this page 1 time(s)'); 58 | }); 59 | 60 | test('increment counter on gallery page', () async { 61 | await _selectPage(driver, drawerMenuButtonFinder, galleryDrawerItemFinder, 62 | galleryPageFinder); 63 | expect(await driver.getText(galleryPageCounterFinder), 64 | 'You have pushed the button on this page 0 time(s)'); 65 | await driver.tap(incrementButtonFinder); 66 | await driver.tap(incrementButtonFinder); 67 | expect(await driver.getText(galleryPageCounterFinder), 68 | 'You have pushed the button on this page 2 time(s)'); 69 | }); 70 | 71 | test('increment counter on slideshow page', () async { 72 | await _goBackToHomePage(driver, backButtonFinder, homePageFinder); 73 | await _selectPage(driver, drawerMenuButtonFinder, 74 | slideshowDrawerItemFinder, slideshowPageFinder); 75 | expect(await driver.getText(slideshowPageCounterFinder), 76 | 'You have pushed the button on this page 0 time(s)'); 77 | await driver.tap(incrementButtonFinder); 78 | await driver.tap(incrementButtonFinder); 79 | await driver.tap(incrementButtonFinder); 80 | expect(await driver.getText(slideshowPageCounterFinder), 81 | 'You have pushed the button on this page 3 time(s)'); 82 | }); 83 | 84 | test('toggle dark mode', () async { 85 | await _goBackToHomePage(driver, backButtonFinder, homePageFinder); 86 | await _selectPage(driver, drawerMenuButtonFinder, 87 | settingsDrawerItemFinder, settingsPageFinder); 88 | await driver.tap(useDarkModeSettingFinder); 89 | }); 90 | 91 | test('the last value of the counters on each page are as expected', 92 | () async { 93 | await _goBackToHomePage(driver, backButtonFinder, homePageFinder); 94 | expect(await driver.getText(homePageCounterFinder), 95 | 'You have pushed the button on this page 1 time(s)'); 96 | await _selectPage(driver, drawerMenuButtonFinder, galleryDrawerItemFinder, 97 | galleryPageFinder); 98 | expect(await driver.getText(galleryPageCounterFinder), 99 | 'You have pushed the button on this page 2 time(s)'); 100 | await _goBackToHomePage(driver, backButtonFinder, homePageFinder); 101 | await _selectPage(driver, drawerMenuButtonFinder, 102 | slideshowDrawerItemFinder, slideshowPageFinder); 103 | expect(await driver.getText(slideshowPageCounterFinder), 104 | 'You have pushed the button on this page 3 time(s)'); 105 | }); 106 | }); 107 | } 108 | 109 | Future _goBackToHomePage( 110 | FlutterDriver driver, 111 | SerializableFinder backButtonFinder, 112 | SerializableFinder homePageTitleFinder) async { 113 | await driver.tap(backButtonFinder); 114 | await driver.waitFor(homePageTitleFinder); 115 | } 116 | 117 | Future _selectPage( 118 | FlutterDriver driver, 119 | SerializableFinder drawerMenuButtonFinder, 120 | SerializableFinder drawerItemFinder, 121 | SerializableFinder pageFinder) async { 122 | await driver.tap(drawerMenuButtonFinder); 123 | await driver.tap(drawerItemFinder); 124 | await driver.waitFor(pageFinder); 125 | } 126 | --------------------------------------------------------------------------------