I am trying to include biometric authentication using local_auth package. This is used when the app starts. The fingerprint is used to determine whether the user is the owner of the phone. If it is confirmed, they will be taken to the home page. The below code works but what I would like to apply on the below code is MVC or design pattern. Can someone guide me?
class LoginOptionState extends State<LoginOption> {
final LocalAuthentication auth = LocalAuthentication();
String _authorized = 'Not Authorized';
#override
Widget build(BuildContext context) {
return Scaffold(
body: new Container(
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
new Column(
children: <Widget>[
Text("Touch ID"),
SizedBox(height: 10),
GestureDetector(
child: Image.asset(
"assets/",
),
onTap: _authenticate),
],
),
],
),
)));
}
Future<void> _authenticate() async {
bool authenticated = false;
try {
authenticated = await auth.authenticateWithBiometrics(
localizedReason: 'Scan your fingerprint to authenticate',
useErrorDialogs: true,
stickyAuth: false);
} on PlatformException catch (e) {
print(e);
}
if (!mounted) return;
setState(() {
_authorized = authenticated
? Navigator.pushNamed(context, homePageViewRoute)
: 'Not Authorized';
});
}
}
Use the excellent library by Greg Perry mvc_pattern. Quick start sample code and explanation is provided on the link.
Here is a quick start example of the classic counter app, from the link above:
The view:
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
// Fields in a Widget subclass are always marked "final".
static final String title = 'Flutter Demo Home Page';
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final Controller _con = Controller.con;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
widget.title,
),
Text(
'${_con.counter}',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(
_con.incrementCounter
);
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Controller class:
class Controller extends ControllerMVC {
/// Singleton Factory
factory Controller() {
if (_this == null) _this = Controller._();
return _this;
}
static Controller _this;
Controller._();
/// Allow for easy access to 'the Controller' throughout the application.
static Controller get con => _this;
int get counter => _counter;
int _counter = 0;
void incrementCounter() => _counter++;
}
Now the above code refactored, to add a model:
int get counter => Model.counter;
void incrementCounter() {
/// The Controller knows how to 'talk to' the Model. It knows the name, but Model does the work.
Model._incrementCounter();
}
And finally the model class:
class Model {
static int get counter => _counter;
static int _counter = 0;
static int _incrementCounter() => ++_counter;
}
However make sure you upgrade flutter to version 1.13.0. At least for me, I was getting several build errors in lower versions.
Karee is a set of tools that implementes MVC design in Flutter. It help you to manage your Controllers, your routes, your screens and more. Refer to karee github wiki to get documentation.
You Can use Karee . It supports Flutter 2.X.X
To installation run
npm install -g karee
Then karee create
After creating a New Flutter project based on Karee you can add new controller
Sample Code
Creating à New controller
Karee generate --controller --path auth Authentication
Generated file under lib/app/controllers/auth/AuthenticationController
#Controller
class AuthenticationController {
login(username, password) {
/// Your custom code
}
}
Add route
Route.on('/login', 'AuthenticationController#login');
Use in your Screen
var authUser = KareeRouter.goto('/login', parameter:[username, password]);
Related
I'm totally new to Flutter/Dart, I've done all the layouts for my application, and now it's time to make my application's API calls. I'm trying to manage the forms as cleanly as possible.
I created a class that manages TextFields data (values and errors), if my API returns an error I would like the screen to update without having to call setState(() {}), is this possible?
In addition, many of my application's screens use values that the user enters in real time, if that happened I would have to call the setState(() {}) methodmany times.
Any idea how to do this with the excess calls to the setState(() {}) method?
I created a test project for demo, these are my files:
File path: /main.dart
import 'package:flutter/material.dart';
import 'login_form_data.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Test App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final LoginFormData _loginFormData = LoginFormData();
void _submitLoginForm() {
// Validate and then make a call to the login api
// If the api returns any erros inject then in the LoginFormData class
_loginFormData.setError('email', 'Invalid e-mail');
setState(() {}); // Don't want to call setState
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Test App'),
),
body: Padding(
padding: const EdgeInsets.all(30),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
decoration: InputDecoration(
errorText: _loginFormData.firstError('email'),
labelText: 'E-mail',
),
onChanged: (value) => _loginFormData.setValue('email', value),
),
TextField(
decoration: InputDecoration(
errorText: _loginFormData.firstError('password'),
labelText: 'Password',
),
obscureText: true,
onChanged: (value) =>
_loginFormData.setValue('password', value),
),
ElevatedButton(
onPressed: _submitLoginForm,
child: const Text('Login'),
)
],
),
),
),
);
}
}
File path: /login_form_data.dart
import 'form/form_data.dart';
import 'form/form_field.dart';
class LoginFormData extends FormData {
#override
Map<String, FormField> fields = {
'email': FormField(),
'password': FormField(),
'simple_account': FormField(
value: true,
),
};
LoginFormData();
}
File path: /form/form_data.dart
class FormData {
final Map<String, dynamic> fields = {};
dynamic getValue(
String key, {
String? defaultValue,
}) {
return fields[key]?.value ?? defaultValue;
}
void setValue(
String key,
String value,
) {
fields[key].value = value;
}
void setError(
String key,
String error,
) {
fields[key]?.errors.add(error);
}
dynamic firstError(
String key,
) {
return fields[key]?.errors.length > 0 ? fields[key]?.errors[0] : null;
}
FormData();
}
File path: /form/form_field.dart
class FormField {
dynamic value;
List errors = [];
FormField({
this.value,
});
}
You are essentially looking for a State Management solution.
There are multiple solutions (you can read about them here: https://docs.flutter.dev/development/data-and-backend/state-mgmt/options)
State Management allows you to declare when you want your widgets to change state instead of having to imperatively call a setState method.
Flutter recommends Provider as a beginner solution, and you can find many tutorials online.
With that being said, let me show you how to achieve this result with a very basic solution: Change Notifier
Quoting flutter documentation :
” A class that can be extended or mixed in that provides a change
notification API using VoidCallback for notifications.”
We are going to make FormData a Change notifier, and them we are going to make your app listen to changes on the instance, and rebuild itself based on them.
Step 1:
Based on the code you posted, I can tell that you will interact with LoginFormData based on the methods setValue and setError from the parent class FormData. So we are going to make FormData inherit ChangeNotifer, and make a call to notifyListeners() on these two methods.
class FormData extends ChangeNotifier {
final Map<String, dynamic> fields = {};
dynamic getValue(
String key, {
String? defaultValue,
}) {
return fields[key]?.value ?? defaultValue;
}
void setValue(
String key,
String value,
) {
fields[key].value = value;
notifyListeners();
}
void setError(
String key,
String error,
) {
fields[key]?.errors.add(error);
notifyListeners();
}
dynamic firstError(
String key,
) {
return fields[key]?.errors.length > 0 ? fields[key]?.errors[0] : null;
}
FormData();
}
Now, every time you call either setValue or setError, the instance of FormData will notify the listeners.
Step2:
Now we have to setup a widget in your app to listen to these changes. Since your app is still small, it’s easy to find a place to put this listener. But as your app grows, you will see that it gets harder to do this, and that’s where packages like Provider come in handy.
We are going to wrap your Padding widget that is the first on the body of your scaffold, with a AnimatedBuilder. Despite of the misleading name, animated builder is not limited to animations. It is a widget that receives any listenable object as a parameter, and rebuilds itself every time it gets notified, passing down the updated version of the listenable.
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final LoginFormData _loginFormData = LoginFormData();
void _submitLoginForm() {
// Validate and then make a call to the login api
// If the api returns any erros inject then in the LoginFormData class
_loginFormData.setError('email', 'Invalid e-mail');
//setState(() {}); No longer necessary
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Test App'),
),
body: AnimatedBuilder(
animation: _loginFormData,
builder: (context, child) {
return Padding(
padding: const EdgeInsets.all(30),
child: Center(
child: Column(
//... The rest of your widgets
),
),
);
}
),
);
}
}
I have a welcome page where i can click a button and go to my login screen and i use Get.toNamed to navigate my screens
//Click button on welcome.dart
InkWell(
onTap: () => goto("login"),
child: ....,
)
//goto function for navigating
goto(String path, {dynamic args}) {
Get.toNamed('/$path', arguments: args);
}
My GetPages
const welcome = '/welcome';
const login = '/login';
List<GetPage<dynamic>> getPages = [
pages(welcome, const Welcome()),
pages(login, const Login()),
];
GetPage pages(String route, dynamic screen,
{dynamic args, bool dialog = false, bool opaque = false}) {
return GetPage(
name: route,
page: () => screen,
transition: Transition.leftToRightWithFade,
fullscreenDialog: dialog,
opaque: opaque);
}
Now in my login page am using flutter_hooks instead of statelesswidget
class Login extends HookWidget {
const Login({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final emailController = useTextEditingController();
final passwordController = useTextEditingController();
return SafeArea(
child: Scaffold(
body: ListView(
padding: const EdgeInsets.all(15),
children: [
const SizedBox(height: 50),
const CustomText(
"Welcome Back",
size: 30,
font: "FlexR",
),
const SizedBox(height: 20),
InputField(
controller: emailController,
label: "Email Address",
suffixIcon: IconlyLight.message,
),
//...,
main.dart
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List<ThemeData> themes = [
ThemeManager.lightTheme,
ThemeManager.lightBlueTheme,
ThemeManager.lightPurpleTheme,
ThemeManager.lightGreenTheme,
ThemeManager.lightOrangeTheme,
ThemeManager.lightYellowTheme,
ThemeManager.darkTheme,
ThemeManager.darkBlueTheme,
ThemeManager.darkPurpleTheme,
ThemeManager.darkGreenTheme,
ThemeManager.darkOrangeTheme,
ThemeManager.darkYellowTheme,
];
#override
void initState() {
init();
super.initState();
}
#override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter',
theme: themes[0],
initialRoute: "/",
unknownRoute: GetPage(name: "/404", page: () => Container()),
getPages: getPages,
home: const Welcome());
}
}
My welcome page is a stateless widget since am not using any hooks in it, but now whenever I click on go to login page and I made a change and decide to hot reload it to see the changes, it immediately takes me back to my welcome page and I don't know what's causing this since everything is correct or am I missing something. Please how can I fix this issue and also if you any more code or explanation, let me know.
Okay i found out what was wrong with it
Instead of using
InkWell(
onTap: () => goto("login"), //from here
child: ....,
)
//goto function for navigating
goto(String path, {dynamic args}) {
Get.toNamed('/$path', arguments: args); //from here
}
I used it like the normal way, which is
InkWell(
onTap: () => goto("/login"), //change here
child: ....,
)
//goto function for navigating
goto(String path, {dynamic args}) {
Get.toNamed(path, arguments: args); //change here
}
I still don't know what's the difference between them, but yeah that's it
My app allows people to post text and switch between different pages on a navbar. On the users page, there is a button, when clicked, will show an overlay so the user can create a post. The overlay includes a back button that calls a function to close the overlay. I want to keep the navbar available at the bottom so user can back out of the post that way if they want to.
The problem is, when the user uses the navbar, the overlay does not close because the close overlay function is on the user page and the navbar page does not have access to it.
How do I give another class on another dart file access to a method or function? If you are able to answer, can you please use my code instead of another example to help me follow better? Thank you.
User Page File #1
class UserPage extends StatefulWidget {
#override
_UserPageState createState() => _UserPageState();
}
class _UserPageState extends State<UserPage> {
OverlayEntry? entry;
#override
Widget build(BuildContext context) {
return Scaffold(
ElevatedButton(
child: const Text('New Post'),
onPressed: showOverlay,
),
),
}
void showOverlay() {
(...)
}
void closeOverlay() {
entry?.remove();
entry = null;
}
}
Nav Bar File #2 (Need help with "OnTap")
class Nav extends StatefulWidget {
const Nav({Key? key}) : super(key: key);
#override
_NavState createState() => _NavState();
}
class _NavState extends State<Nav> {
int currentTab = 1; // makes the home page the default when loading up the app
#override
void initState() {
super.initState();
}
List<Widget> tabs = <Widget>[
const Other(),
const Home(),
const UserPage(),
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: tabs.elementAt(currentTab),
),
// BOTTOM NAVIGATION BAR
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentTab,
onTap: (value) {
setState(() => currentTab = value);
const _UserPageState().closeOverlay(); //HERE IS WHERE I NEED HELP WITH THE CODE
},
items: const [
BottomNavigationBarItem(
label: 'Other',
),
BottomNavigationBarItem(
label: 'Home',
),
BottomNavigationBarItem(
label: 'User Page',
),
],
),
);
}
}
You can try to make your _UserPageState public by removing - from it, and then call it UserPageState().closeOverlay();
I'm currently building a Flutter app where I'm struggling to figure out the best way to implement navigation.
I have 2 pages which are:
HomePage: from there I want to use an IndexedStack to manage the feed.
ProfilePage: the profile page, which (graphically) shares the same AppBar and the same Drawer as the home page.
In my App the user reaches the HomePage immediately after logging in. There is no navigation involved.
From there, I now have a TextButton, which calls Navigator.of(context).pushNamed(AppRoutes.profile).
As I said, both pages share the same Appbar and Drawer, so I created a custom myScaffold.
Both pages use this scaffold.
So the behavior is correct, since after clicking the button, the ProfilePage is moved over the HomePage.
My problem is that graphically the appbar should remain the same, but when the profile page is pushed, the animation makes it clear that it is not the same app bar.
Is it possible to animate the entry of the profile page, without
animating the rebuilding of the appbar?
Or is it possible to push a route directly into the scaffold content?
As an alternative I was just thinking of writing a function which
returns the page widget to be displayed within the scaffold content.
But this kind of approach doesn't seem right to me, since there are
routes.
From the official documentation you can see from the Interactive example what I mean:
Docs
When the second route is built over the first one, a new Appbar is built over the previous one.
But what if I need the appbar to stay the same?
You can create a sub-navigator using Navigator class.
I created a routes library (routes.dart) in my current project for navigating to other screens while bottomNavigationBar is still displayed. Using the same idea, you can perform navigations while using the same AppBar.
Here's the sample codes for your scenario.
main.dart
import 'package:flutter/material.dart';
import 'package:flutter2sample/routes.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
navigatorKey: Routes.rootNavigatorKey,
initialRoute: Routes.PAGE_INITIAL,
onGenerateRoute: Routes.onGenerateRoute,
);
}
}
routes.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter2sample/pages/home_page.dart';
import 'package:flutter2sample/pages/initial_page.dart';
import 'package:flutter2sample/pages/main_page.dart';
import 'package:flutter2sample/pages/profile_page.dart';
class Routes {
Routes._();
static const String PAGE_INITIAL = '/';
static const String PAGE_MAIN = '/main';
static const String PAGE_HOME = '/home';
static const String PAGE_PROFILE = '/profile';
static final GlobalKey<NavigatorState> rootNavigatorKey =
GlobalKey<NavigatorState>();
static final GlobalKey<NavigatorState> mainNavigatorKey =
GlobalKey<NavigatorState>();
static String currentSubNavigatorInitialRoute;
static CupertinoPageRoute<Widget> onGenerateRoute(RouteSettings settings) {
Widget page;
switch (settings.name) {
case PAGE_INITIAL:
page = InitialPage();
break;
case PAGE_MAIN:
page = MainPage();
break;
case PAGE_HOME:
page = HomePage();
break;
case PAGE_PROFILE:
page = ProfilePage();
break;
}
if (settings.name == PAGE_INITIAL &&
currentSubNavigatorInitialRoute != null) {
// When current sub-navigator initial route is set,
// do not display initial route because it is already displayed.
return null;
}
return CupertinoPageRoute<Widget>(
builder: (_) {
if (currentSubNavigatorInitialRoute == settings.name) {
return WillPopScope(
onWillPop: () async => false,
child: page,
);
}
return page;
},
settings: settings,
);
}
/// [MaterialApp] navigator key.
///
///
static NavigatorState get rootNavigator => rootNavigatorKey.currentState;
/// [PAGE_MAIN] navigator key.
///
///
static NavigatorState get mainNavigator => mainNavigatorKey.currentState;
/// Navigate to screen via [CupertinoPageRoute].
///
/// If [navigator] is not set, it will use the [rootNavigator].
static void push(Widget screen, {NavigatorState navigator}) {
final CupertinoPageRoute<Widget> route = CupertinoPageRoute<Widget>(
builder: (_) => screen,
);
if (navigator != null) {
navigator.push(route);
return;
}
rootNavigator.push(route);
}
/// Navigate to route name via [CupertinoPageRoute].
///
/// If [navigator] is not set, it will use the [rootNavigator].
static void pushNamed(
String routeName, {
NavigatorState navigator,
Object arguments,
}) {
if (navigator != null) {
navigator.pushNamed(routeName, arguments: arguments);
return;
}
rootNavigator.pushNamed(routeName, arguments: arguments);
}
/// Pop current route of [navigator].
///
/// If [navigator] is not set, it will use the [rootNavigator].
static void pop<T extends Object>({
NavigatorState navigator,
T result,
}) {
if (navigator != null) {
navigator.pop(result);
return;
}
rootNavigator.pop(result);
}
}
//--------------------------------------------------------------------------------
/// A navigator widget who is a child of [MaterialApp] navigator.
///
///
class SubNavigator extends StatelessWidget {
const SubNavigator({
#required this.navigatorKey,
#required this.initialRoute,
Key key,
}) : super(key: key);
final GlobalKey<NavigatorState> navigatorKey;
final String initialRoute;
#override
Widget build(BuildContext context) {
final _SubNavigatorObserver _navigatorObserver = _SubNavigatorObserver(
initialRoute,
navigatorKey,
);
Routes.currentSubNavigatorInitialRoute = initialRoute;
return WillPopScope(
onWillPop: () async {
if (_navigatorObserver.isInitialPage) {
Routes.currentSubNavigatorInitialRoute = null;
await SystemNavigator.pop();
return true;
}
final bool canPop = navigatorKey.currentState.canPop();
if (canPop) {
navigatorKey.currentState.pop();
}
return !canPop;
},
child: Navigator(
key: navigatorKey,
observers: <NavigatorObserver>[_navigatorObserver],
initialRoute: initialRoute,
onGenerateRoute: Routes.onGenerateRoute,
),
);
}
}
//--------------------------------------------------------------------------------
/// [NavigatorObserver] of [SubNavigator] widget.
///
///
class _SubNavigatorObserver extends NavigatorObserver {
_SubNavigatorObserver(this._initialRoute, this._navigatorKey);
final String _initialRoute;
final GlobalKey<NavigatorState> _navigatorKey;
final List<String> _routeNameStack = <String>[];
bool _isInitialPage = false;
/// Flag if current route is the initial page.
///
///
bool get isInitialPage => _isInitialPage;
#override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
_routeNameStack.add(route.settings.name);
_isInitialPage = _routeNameStack.last == _initialRoute;
}
#override
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
_routeNameStack.remove(route.settings.name);
_isInitialPage = _routeNameStack.last == _initialRoute;
}
#override
void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) {
_routeNameStack.remove(route.settings.name);
_isInitialPage = _routeNameStack.last == _initialRoute;
}
#override
void didReplace({Route<dynamic> newRoute, Route<dynamic> oldRoute}) {
_routeNameStack.remove(oldRoute.settings.name);
_routeNameStack.add(newRoute.settings.name);
_isInitialPage = _routeNameStack.last == _initialRoute;
}
}
initial_page.dart
import 'package:flutter/material.dart';
import 'package:flutter2sample/routes.dart';
class InitialPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Initial Page'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('This is INITIAL page'),
TextButton(
onPressed: () => Routes.pushNamed(Routes.PAGE_MAIN),
child: const Text('To Main page'),
),
],
),
),
);
}
}
main_page.dart
import 'package:flutter/material.dart';
import 'package:flutter2sample/routes.dart';
class MainPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Main Page'),
),
body: SubNavigator(
navigatorKey: Routes.mainNavigatorKey,
initialRoute: Routes.PAGE_HOME,
),
);
}
}
home_page.dart
import 'package:flutter/material.dart';
import 'package:flutter2sample/routes.dart';
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.yellow,
body: SafeArea(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('This is HOME page'),
TextButton(
onPressed: () => Routes.pushNamed(
Routes.PAGE_PROFILE,
navigator: Routes.mainNavigator,
),
child: const Text('To Profile page'),
),
],
),
),
),
);
}
}
profile_page.dart
import 'package:flutter/material.dart';
import 'package:flutter2sample/routes.dart';
class ProfilePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey,
body: SafeArea(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('This is PROFILE page'),
TextButton(
onPressed: () => Routes.pop(navigator: Routes.mainNavigator),
child: const Text('Back to Home page'),
),
],
),
),
),
);
}
}
I create some tab functions dynamically. I need only 10 tabs in that dynamic function can you respond to the condition for this method.
Dynamic list of Tabs
Here is a small example app displaying a random dynamic list of Sports.
Domain Layer
Each TabItem is defined with a name and a description.
part '66457422.dynamic_tabs.freezed.dart';
#freezed
abstract class TabItem implements _$TabItem {
const factory TabItem({
String name,
String description,
}) = _TabItem;
const TabItem._();
String get initials =>
name.split(RegExp(r'\W+')).map((x) => x[0].toUpperCase()).join('');
static TabItem get random {
final faker = Faker();
return TabItem(
name: faker.sport.name(),
description: faker.lorem.sentence(),
);
}
}
I used the freezed package that provides immutability for my TabItem. freezed uses the build_runner package to generate the class implementation inside 66457422.dynamic_tabs.freezed.dart.
In my TabItem, I defined to getters:
String get initials to get the initials of each Sport to be used as the Tabs labels.
static TabItem get random to generate a random Sport thanks to the faker package.
State Management
To maintain the list of TabItems, you will need some kind of State Management. In this example, I use the riverpod package (in its flutter_hooks flavor) from the same author as Provider. But, you may use other State Management systems if you prefer. Check this curated list.
My tabsProvider is a StateNotifierProvider defined as follows:
final tabsProvider = StateNotifierProvider<TabsNotifier>(
(ref) => TabsNotifier()..addTab()..addTab()..addTab());
class TabsNotifier extends StateNotifier<List<TabItem>> {
TabsNotifier([List<TabItem> state]) : super(state ?? []);
void addTab() {
state = [
...state,
TabItem.random,
];
}
}
Presentation Layer
Now, we just need to display everything!
I used the DefaultTabController Widget and maps on the List<TabItem> provided by my tabsProvider to display the Tabs and the TabViews:
class HomePage extends HookWidget {
#override
Widget build(BuildContext context) {
final tabs = useProvider(tabsProvider.state);
return DefaultTabController(
length: tabs.length,
child: Scaffold(
appBar: AppBar(
title: Text('Dynamic Tabs Demo'),
bottom: TabBar(
tabs: tabs.map((t) => Tab(text: t.initials)).toList(),
),
),
body: TabBarView(
children: tabs.map((t) => TabItemWidget(tabItem: t)).toList(),
),
floatingActionButton: tabs.length >= 10
? null
: FloatingActionButton(
onPressed: () => context.read(tabsProvider).addTab(),
child: Icon(Icons.add),
),
),
);
}
}
The FloatingActionButton will addTab() on my tabsProvider only if the number of TabItems is smaller than 10. Otherwise, it disappears.
Voilà! That's about it.
Full source code for easy copy-paste
import 'package:faker/faker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
part '66457422.dynamic_tabs.freezed.dart';
void main() {
runApp(
ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Dynamic Tabs Demo',
home: HomePage(),
),
),
);
}
class HomePage extends HookWidget {
#override
Widget build(BuildContext context) {
final tabs = useProvider(tabsProvider.state);
return DefaultTabController(
length: tabs.length,
child: Scaffold(
appBar: AppBar(
title: Text('Dynamic Tabs Demo'),
bottom: TabBar(
tabs: tabs.map((t) => Tab(text: t.initials)).toList(),
),
),
body: TabBarView(
children: tabs.map((t) => TabItemWidget(tabItem: t)).toList(),
),
floatingActionButton: tabs.length >= 10
? null
: FloatingActionButton(
onPressed: () => context.read(tabsProvider).addTab(),
child: Icon(Icons.add),
),
),
);
}
}
class TabItemWidget extends StatelessWidget {
final TabItem tabItem;
const TabItemWidget({Key key, this.tabItem}) : super(key: key);
#override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
Text(tabItem.name),
const SizedBox(height: 24.0),
Text(tabItem.description),
],
),
);
}
}
final tabsProvider = StateNotifierProvider<TabsNotifier>(
(ref) => TabsNotifier()..addTab()..addTab()..addTab());
class TabsNotifier extends StateNotifier<List<TabItem>> {
TabsNotifier([List<TabItem> state]) : super(state ?? []);
void addTab() {
state = [
...state,
TabItem.random,
];
}
}
#freezed
abstract class TabItem implements _$TabItem {
const factory TabItem({
String name,
String description,
}) = _TabItem;
const TabItem._();
String get initials =>
name.split(RegExp(r'\W+')).map((x) => x[0].toUpperCase()).join('');
static TabItem get random {
final faker = Faker();
return TabItem(
name: faker.sport.name(),
description: faker.lorem.sentence(),
);
}
}