I would like to create my own theme properties which can be set in runtime dynamically.
I tried to create an extension for the TextTheme which looks like that:
extension CustomTextTheme on TextTheme {
TextStyle get heading => themeMode == ThemeMode.light
? TextStyle(
color: GlobalTheme.defaultLightTheme.textTheme.headline.color,
fontSize: GlobalTheme.defaultLightTheme.textTheme.headline.fontSize,
)
: TextStyle(
color: GlobalTheme.defaultDarkTheme.textTheme.headline.color,
fontSize: GlobalTheme.defaultLightTheme.textTheme.headline.fontSize,
);
}
The question is how I can change the extension properties on runtime dynamically.
What I want to archive with this is, that I can load a "theme config" from the server and set that theme on each device dynamically.
To pass and get values inside the widget tree you need InheritedWidget.
This is a special type of Widgets that just transfer the information between widgets (like Theme delivers ThemeData).
You can not extend ThemeData with new fields as extensions will not trigger updates on Theme.
But you can create your own CustomTheme which will coexist with the original.
class CustomThemeData {
const CustomThemeData(this.heading);
final TextStyle heading;
#override
bool operator ==(Object other) =>
identical(this, other) ||
other is CustomThemeData &&
runtimeType == other.runtimeType &&
heading == other.heading;
#override
int get hashCode => heading.hashCode;
}
Now create an InheritedWidget that wll provide the custom theme data value (via of) and will add a possibility to update the data (via update)
class CustomTheme extends InheritedWidget {
const CustomTheme({
Key? key,
required this.data,
required this.onUpdate,
required Widget child,
}) : super(key: key, child: child);
final CustomThemeData data;
final void Function(CustomThemeData) onUpdate;
static CustomThemeData of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CustomTheme>()!.data;
}
static void update(BuildContext context, CustomThemeData data) {
context.dependOnInheritedWidgetOfExactType<CustomTheme>()!.onUpdate(data);
}
#override
bool updateShouldNotify(covariant CustomTheme oldWidget) {
return data != oldWidget.data;
}
}
Here is a holder for custom theme
class ThemeSwitcherWidget extends StatefulWidget {
final CustomThemeData initialTheme;
final Widget child;
const ThemeSwitcherWidget({
Key? key,
required this.initialTheme,
required this.child,
}) : super(key: key);
#override
_ThemeSwitcherWidgetState createState() => _ThemeSwitcherWidgetState();
}
class _ThemeSwitcherWidgetState extends State<ThemeSwitcherWidget> {
CustomThemeData? _updatedTheme;
#override
Widget build(BuildContext context) {
return CustomTheme(
data: _updatedTheme ?? widget.initialTheme,
onUpdate: (newData) => setState(() {
_updatedTheme = newData;
}),
child: widget.child,
);
}
}
And here is an example on how to use all this beauty:
void main() {
runApp(
const ThemeSwitcherWidget(
initialTheme: CustomThemeData(TextStyle()),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'text',
style: CustomTheme.of(context).heading,
),
ElevatedButton(
onPressed: () {
CustomTheme.update(
context,
const CustomThemeData(TextStyle(
color: Colors.red,
fontSize: 44,
)));
},
child: const Text('Change theme')),
],
),
),
),
);
}
}
To make the code less verbose you may use provider which will do all that magic with updates for you.
Related
I'm using an InhertiedWidget to manage the state of a counter in this app.
However, if I update the state and change tabs at the same time, the update doesn't seem to pass through to the individual tabs.
Repro steps
Go to tab B
Press + button
Expected behaviour
The tab changes to A, and the counter updates in the tab
Actual behaviour
The tab changes to A, but the counter does not update (even though the actual value changes)
Here's a video to explain what I mean - https://user-images.githubusercontent.com/19492893/186385977-7185afd5-0bec-4291-8085-d2c7f07fc50a.mov
Run on dartpad
import 'package:flutter/material.dart';
void main() => runApp(MainApp());
class MainApp extends StatefulWidget {
const MainApp({
Key? key,
}) : super(key: key);
#override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
int count = 1;
#override
Widget build(BuildContext context) {
return CounterInheritedWidget(
count: count,
child: MaterialApp(
home: DefaultTabController(
length: 2,
child: CounterTabs(
increment: increment,
),
),
),
);
}
void increment() {
setState(() {
count += 1;
});
}
}
class CounterInheritedWidget extends InheritedWidget {
const CounterInheritedWidget({
Key? key,
required this.count,
required Widget child,
}) : super(key: key, child: child);
final int count;
static CounterInheritedWidget of(BuildContext context) {
final CounterInheritedWidget? result =
context.dependOnInheritedWidgetOfExactType<CounterInheritedWidget>();
assert(result != null, 'No CounterInheritedWidget found in context');
return result!;
}
#override
bool updateShouldNotify(CounterInheritedWidget old) => count != old.count;
}
class CounterTabs extends StatelessWidget {
final VoidCallback increment;
const CounterTabs({Key? key, required this.increment}) : super(key: key);
#override
Widget build(BuildContext context) {
int count = CounterInheritedWidget.of(context).count;
return Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
DefaultTabController.of(context)?.animateTo(0);
increment();
});
},
),
appBar: AppBar(
title: Text("Actual value: $count"),
bottom: TabBar(
tabs: [
Tab(text: "A"),
Tab(text: "B"),
],
),
),
body: TabBarView(
children: [
Center(child: Text("Tab value: $count")),
Center(child: Text("Tab value: $count")),
],
),
);
}
}
increment UI update call event is canceled because animationTo, wait for UI update to finish then call animationTo, or set shouldUpdateNotify to always true
Future.delayed(const Duration(milliseconds:100),()=>DefaultTabController.of(context)?.animateTo(0));
I am working in Riverpod Auth flow boilerplate application.
I want to use common loading screen for all async function even login and logout. Currently I have AppState provider if Appstate loading i show loading screen. it's working fine for login but i wonder it’s good way or bad way.
Can i use this loading screen for all async task in the App?
AuthWidget:
class AuthWidget extends ConsumerWidget {
const AuthWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
AppState appState = ref.watch(appStateProvider);
if(appState.isLoading){
return const Center(child: CircularProgressIndicator(color: Colors.red),);
}
return appState.isAuthenticated ? const HomePage() : const SignIn();
}
}
AppState:
class AppState {
User? user;
bool isLoading;
bool isAuthenticated;
AppState(this.user, this.isLoading, this.isAuthenticated);
}
AuthRepository:
class AuthRepository extends StateNotifier<AppState>{
AuthRepository() : super(AppState(null,false,false));
Future<void> signIn()async {
state = AppState(null,true,false);
await Future.delayed(const Duration(seconds: 3));
User user = User(userName: 'FakeUser', email: 'user#gmail.com');
AppState appState = AppState(user, false, true);
state = appState;
}
}
final appStateProvider = StateNotifierProvider<AuthRepository,AppState>((ref){
return AuthRepository();
});
To answer your question : Yes you can.
The only thing I'd change here is the content of your AppState : I'd use a LoadingState dedicated to trigger your Loader instead.
Here is how I like to manage screens with a common loader in my apps.
1 - Create a LoadingState and provide it
final loadingStateProvider = ChangeNotifierProvider((ref) => LoadingState());
class LoadingState extends ChangeNotifier {
bool isLoading = false;
void startLoader() {
if (!isLoading) {
isLoading = true;
notifyListeners();
}
}
void stopLoader() {
if (isLoading) {
isLoading = false;
notifyListeners();
}
}
}
2 - Define a base page with the "common" loader
class LoadingContainer extends ConsumerWidget {
const LoadingContainer({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
#override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(loadingStateProvider);
return Stack(
children: [
child,
if (state.isLoading)
const Center(child: CircularProgressIndicator())
else
const SizedBox(),
],
);
}
}
3 - Implement this widget whenever I need to handle loading datas.
return Scaffold(
backgroundColor: AppColor.blue,
body: LoadingContainer(
child: ...
And then I simply have to update my loadingStateProvider and it's isLoading value from a Controller or the Widget directly
If you want a centralized/common async calls, the InheritedWidget is ideal for that, you can just add a method and call it from anywhere down stream and because the call is offloaded with async, you can attach extra arguments and add usefull functionality such as a live update instead of relying on stuff like .then(). This example might not be as simple as FDuhen's but you can mix them together if you want to not use keys
AppState now is a widget and contains trigers that rely on global keys to rebuild the correct components, here i assumed that you actualy want to have an common overlay and not a loading screen widget, if not using a Navigator would be batter
Using keys is specially good if you end up implementing something this line, <token> been just a number that references a group of widgets
key: AppState.of(ctx).rebuild_on_triger(<token>)
class App_State_Data {
GlobalKey? page_key;
bool is_logged = false;
bool loading_overlay = false;
String loading_message = '';
}
class AppState extends InheritedWidget {
final App_State_Data _state;
bool get is_logged => _state.is_logged;
bool get should_overlay => _state.loading_overlay;
String get loading_message => _state.loading_message;
void page_rebuild() {
(_state.page_key!.currentState as _Page_Base).rebuild();
}
GlobalKey get page_key {
if (_state.page_key == null) {
_state.page_key = GlobalKey();
}
return _state.page_key!;
}
void place_overlay(String msg) {
_state.loading_message = msg;
_state.loading_overlay = true;
page_rebuild();
}
void clear_overlay() {
_state.loading_message = '';
_state.loading_overlay = false;
page_rebuild();
}
Future<void> triger_login(String message) async {
place_overlay(message);
await Future.delayed(const Duration(seconds: 2));
_state.is_logged = true;
clear_overlay();
}
Future<void> triger_logout(String message) async {
place_overlay(message);
await Future.delayed(const Duration(seconds: 1));
_state.is_logged = false;
clear_overlay();
}
AppState({Key? key, required Widget child})
: this._state = App_State_Data(),
super(key: key, child: child);
static AppState of(BuildContext ctx) {
final AppState? ret = ctx.dependOnInheritedWidgetOfExactType<AppState>();
assert(ret != null, 'No AppState found!');
return ret!;
}
#override
bool updateShouldNotify(AppState old) => true;
}
Here i added it as the topmost element making it like a global data class with is not necessary, you can split the state content and add just the necessary to where its needed
void main() => runApp(AppState(child: App()));
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
#override
Widget build(BuildContext ctx) {
return MaterialApp(
home: Scaffold(
body: Page_Base(
key: AppState.of(ctx).page_key,
),
),
);
}
}
class Page_Base extends StatefulWidget {
final GlobalKey key;
const Page_Base({
required this.key,
}) : super(key: key);
#override
_Page_Base createState() => _Page_Base();
}
class _Page_Base extends State<Page_Base> {
Widget build_overlay(BuildContext ctx) {
return Center(
child: Container(
width: double.infinity,
height: double.infinity,
color: Color(0xC09E9E9E),
child: Center(
child: Text(AppState.of(ctx).loading_message),
),
),
);
}
#override
Widget build(BuildContext ctx) {
return Stack(
children: [
AppState.of(ctx).is_logged ? Page_Home() : Page_Login(),
AppState.of(ctx).should_overlay ? build_overlay(ctx) : Material(),
],
);
}
void rebuild() {
// setState() is protected and can not be called
// from outside of the this. scope
setState(() => null);
}
}
Using AppState is the best part, just because the widget does not have to call more than 1 function and it will rebuild with the correct data on complition
class Page_Login extends StatelessWidget {
const Page_Login({Key? key}) : super(key: key);
#override
Widget build(BuildContext ctx) {
return Center(
child: InkWell(
onTap: () => AppState.of(ctx).triger_login('Login'),
child: Container(
width: 200,
height: 200,
color: Colors.greenAccent,
child: Text('Page_Login'),
),
),
);
}
}
class Page_Home extends StatelessWidget {
const Page_Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext ctx) {
return Center(
child: InkWell(
onTap: () => AppState.of(ctx).triger_logout('Logout'),
child: Container(
width: 200,
height: 200,
color: Colors.blueAccent,
child: Text('Page_Home'),
),
),
);
}
}
Global loading indicator
If you want a centralized loading indicator to use in your whole app you could take advantage of Overlay's, which flutter already uses for dialogs, popups, bottom sheets etc. This way we don't introduce new widget in the widget tree.
If you only want to toggle between loading states you can use a StateProvider to handle the simple boolean value, else you could create a State/Change Notifier. This way you decouple your loading state from your AppState
final loadingProvider = StateProvider<bool>((ref) => false);
void main() => runApp(const ProviderScope(child: MaterialApp(home: GlobalLoadingIndicator(child: Home()))));
// This widget should wrap your entire app, but be below MaterialApp in order to have access to the Overlay
class GlobalLoadingIndicator extends ConsumerStatefulWidget {
final Widget child;
const GlobalLoadingIndicator({required this.child, Key? key}) : super(key: key);
#override
ConsumerState createState() => _GlobalLoadingIndicatorState();
}
class _GlobalLoadingIndicatorState extends ConsumerState<GlobalLoadingIndicator> {
//We need to cache the overlay entries we are showing as part of the indicator in order to remove them when the indicator is hidden.
final List<OverlayEntry> _entries = [];
#override
Widget build(BuildContext context) {
ref.listen<bool>(loadingProvider, (previous, next) {
// We just want to make changes if the states are different
if (previous == next) return;
if (next) {
// Add a modal barrier so the user cannot interact with the app while the loading indicator is visible
_entries.add(OverlayEntry(builder: (_) => ModalBarrier(color: Colors.black12.withOpacity(.5))));
_entries.add(OverlayEntry(
builder: (_) =>const Center(
child: Card(child: Padding(padding: EdgeInsets.all(16.0), child: CircularProgressIndicator())))));
// Insert the overlay entries into the overlay to actually show the loading indicator
Overlay.of(context)?.insertAll(_entries);
} else {
// Remove the overlay entries from the overlay to hide the loading indicator
_entries.forEach((e) => e.remove());
// Remove the cached overlay entries from the widget state
_entries.clear();
}
});
return widget.child;
}
}
We insert the GlobalLoadingIndicator high up in the widget tree although anywhere below the MaterialApp is fine (as long as it can access the Overlay via context).
The GlobalLoadingIndicator wont create extra widgets in the widget tree, and will only manage the overlays, here I add two overlays, one is a ModalBarrier which the user from interacting with widgets behind itself. And the other the actual LoadingIndicator. You are free to not add the ModalBarrier, or make it dismissible (or even if you decide to create a more complex loadingProvider, customize it in case you need to cater different use cases).
A sample usage after you have this set up is just switching the state of the loadingProvider, most of the times you would do this programatically, but for interactiveness I'll use a Switch :
class Home extends ConsumerWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, ref) {
final isLoading = ref.watch(loadingProvider);
return Scaffold(
appBar: AppBar(),
body: Center(
child: SwitchListTile(
value: isLoading,
onChanged: (value) {
ref.read(loadingProvider.notifier).state = value;
Future.delayed(const Duration(seconds: 4)).then((value) {
ref.read(loadingProvider.notifier).state = false;
});
},
title: const FlutterLogo(),
),
));
}
}
You can fiddle with this snippet in dartpad
Result:
Per Screen/Section loading indicator
As a side note when displaying loading states inside components of the app I recommend you to use an AnimatedSwitcher , as it fades between the widgets , super handy when dealing with screens which can change content abruptly.
final loadingProvider = StateProvider<bool>((ref) => false);
void main() => runApp(ProviderScope(child: MaterialApp(home: Home())));
class Home extends ConsumerWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, ref) {
final isLoading = ref.watch(loadingProvider);
return Scaffold(
appBar: AppBar(),
body: Center(
child: SwitchListTile(
value: isLoading,
onChanged: (value) {
ref.read(loadingProvider.notifier).state = value;
},
title: AnimatedSwitcher(
duration: Duration(milliseconds: 400),
child: isLoading?CircularProgressIndicator():FlutterLogo()
),
),
));
}
}
Get the working code sample here
I have an RxList of addOnProducts which contains product and selected attributes.
I am trying to implement the simple multiSelectable grid View, but on clicking the checkBox the selected attribute changes but it is not reflected back to the ui,
If i refresh it will be updated.
I tried Obx()=> (); widget , It is still not updating
My ProductController
class ProductsController extends GetxController {
late Worker worker;
static ProductsController instance = Get.find();
RxList<ProductModel> products = RxList<ProductModel>([]);
RxList<CheckProduct> addOnProducts = <CheckProduct>[].obs;
String collection = "products";
#override
void onReady() {
super.onReady();
products.bindStream(getAllProducts());
worker = once(products, (List<ProductModel> value) {
fillAddOnProducts(value);
}, condition: () => products.isNotEmpty);
}
Stream<List<ProductModel>> getAllProducts() => FirebaseFirestore.instance
.collection(collection)
.snapshots()
.map((query) => query.docs
.map((item) => ProductModel.fromMap(item.data(), item.id))
.toList());
void fillAddOnProducts(List<ProductModel> products) => {
products.forEach((element) {
addOnProducts.add(CheckProduct(product: element, selected: false));
})
};
}
class CheckProduct {
ProductModel product;
bool selected;
CheckProduct(
{required ProductModel this.product, required bool this.selected});
}
My Grid View
class AddOns extends StatelessWidget {
const AddOns({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [],
title: Text("Select Addons"),
),
body: Obx(() => GridView.count(
crossAxisCount: 2,
children: productsController.addOnProducts
.map((element) => ProductWidget(product: element))
.toList(),
)));
}
}
class ProductWidget extends StatelessWidget {
final CheckProduct product;
const ProductWidget({Key? key, required this.product}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
margin: EdgeInsets.all(10),
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: 4,
left: 4,
child: Checkbox(
value: product.selected,
onChanged: (value) {
print("value of the value is : $value");
print("value of product selected before is: " +
product.selected.toString());
product.selected = value!;
print("value of product selected after is: " +
product.selected.toString());
},
),
),
],
));
}
}
Therefore in the console it is :
I/flutter (20067): value of the value is : true
I/flutter (20067): value of product selected before is: false
I/flutter (20067): value of product selected after is: true
But the checkBox is not updating, it updates only when i refresh, How to overCome this? Adding Obx() to the parent isn't helping..
Find the github link to code below here which has just the question and and the problem faced..
After going through your code. I've implemented the following that will change state without hot reload:
In your main dart you do not need to put your product controller here as you are not using it
main.dart
import 'package:flutter/material.dart';
import 'grid.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: GridSelect(),
);
}
}
Next, I have changed your grid class to generate a list of product widget as the size of the addProduct list length. In my opinion this is a better way to write GridView counts children. Remove obx from your gridview and change your stateful widget to stateless as you are using Getx. It will manage your state even in a stateless widget. Add your product controller here as you will access addProduct list from the controller class.
grid.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:test_project/controllers/productController.dart';
import 'package:test_project/productWidget.dart';
class GridSelect extends StatelessWidget {
final _controller = Get.put(ProductController());
GridSelect({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: GridView.count(
crossAxisCount: 2,
children: List.generate(_controller.addOnProducts.length, (index) => ProductWidget(index: index))
),
);
}
}
In your product controller class, remove the instance as it is not important. That is the only change here:
ProductController.dart
import 'package:get/get.dart';
import 'package:test_project/models/productModel.dart';
class ProductController extends GetxController {
RxList<CheckProduct> addOnProducts = <CheckProduct>[].obs;
#override
void onReady() {
super.onReady();
addOnProducts.add(CheckProduct(product: ProductModel('productOne', 20)));
addOnProducts.add(CheckProduct(product: ProductModel('productTwo', 25)));
addOnProducts.add(CheckProduct(product: ProductModel('productThree', 30)));
addOnProducts.add(CheckProduct(product: ProductModel('productFour', 40)));
}
}
class CheckProduct {
ProductModel product;
RxBool selected = false.obs;
CheckProduct({
required this.product,
});
}
Lastly, your productWidget class needs a required value index. So, the widget knows which index in gridview the user is clicking and use Obx() here in checkbox as you have an observable value selected here. Remember to always use Obx() when you have an obs value. This will update the widget whenever an obs value changes. Here, if you notice we are using Get.find() instead of Put as Get.put is already inside the scope so all you need to do is find the controller that you will use. You can find or put multiple controllers and update values as much as you want.
productWidget.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:test_project/controllers/productController.dart';
class ProductWidget extends StatelessWidget {
final ProductController _controller = Get.find();
final int index;
ProductWidget({Key? key, required this.index}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
margin: EdgeInsets.all(20),
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: 4,
left: 4,
child: Obx(()=>Checkbox(
value: _controller.addOnProducts[index].selected.value,
onChanged: (value) {
print("value of the value is : $value");
print("value of product selected before is: " +
_controller.addOnProducts[index].selected.toString());
_controller.addOnProducts[index].selected.value = value!;
print("value of product selected after is: " +
_controller.addOnProducts[index].selected.toString());
},
)),
)
],
),
);
}
}
Go through GetX documentation for proper use of GetX. Even though I have 2 apps in Playstore with GetX, I still go through documentation from time to time. They have a clear documentation on how to manage state.
In ProductWidget adding an additional Obx() solved my problem
class ProductWidget extends StatelessWidget {
final CheckProduct product;
const ProductWidget({Key? key, required this.product}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.yellow,
margin: EdgeInsets.all(10),
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: 4,
left: 4,
// Even the child needs Obx() ; The parent's Obx() is not reflected here
child: Obx(()=>(Checkbox(
value: product.selected,
onChanged: (value) {
print("value of the value is : $value");
print("value of product selected before is: " +
product.selected.toString());
product.selected = value!;
print("value of product selected after is: " +
product.selected.toString());
},
),))
),
],
));
}
I've only been coding in Flutter for a few weeks now and I would like to know if it is possible just to navigate to a page using named routes that has received arguments from another page? The main objective is to navigate to the Cart Screen from two different pages where one passes an argument while the other doesn't. Here is my code below to explain my question:
This is the first part of the code which navigates to the cart screen after passing arguments id and quantity
class ItemDetailsState extends State<ItemDetails> {
int quantity = 1; //quantity
#override
Widget build(BuildContext context) {
final routes =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
final id = routes["id"]; //id
return Scaffold(
......
InkWell(
onTap: () {
Navigator.of(context).pushNamed('/cart-screen', arguments: { //This navigates to the cart screen passing arguments id and quantity
'id': routes["id"],
'quantity': quantity,
});
Provider.of<CartItemProvider>(context, listen: false)
.addItems(id, name, restaurantName, price, quantity);
},
);
}
}
This is the Cart Screen that receives the arguments and filters data from a Provider Class:
class CartScreen extends State<CartScreenState> {
#override
Widget build(BuildContext context) {
final routes =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
final id = routes['id']; //Received Arguments
final quantity = routes['quantity']; //Received Arguments
final provider =
Provider.of<PopularDishesProvider>(context).getProductById(id); //Provider that filters the data as per ID
My idea is to navigate to the Cart Screen page from another page like this but it throws the below error:
class HomeScreenState extends State<HomeScreen> {
Widget build(BuildContext context) {
return Scaffold(
..............
body: Row(
children: [
InkWell(
onTap: () => Navigator.of(context)
.pushReplacementNamed('/cart-screen'), //Navigate to the Cart Screen
child: const Icon(
Icons.shopping_cart_outlined,
color: Colors.grey,
size: 30,
),
),
InkWell(
onTap: () {},
child: const Icon(
Icons.notifications_none_outlined,
color: Colors.grey,
size: 30,
),
)
],
)
The method '[]' was called on null.
Receiver: null
Tried calling: []("id")
The above error I believe is owing to the fact that I'm trying to just navigate to '/cart-screen' without passing any argument in the HomeScreenState widget. I need suggestions to know if there's any way to get around this?
The route is declared in the main.dart file as it should like
routes : {
'/cart-screen': (context) => CartScreen(),
}
You can check null value using
#override
Widget build(BuildContext context) {
var arguments3 = ModalRoute.of(context)!.settings.arguments;
var routes=
arguments3!=null? arguments3 as Map<String, dynamic>:{};
final id = routes['id']??0; //Received Arguments
final quantity = routes['quantity']??0; //Received Arguments
final provider =
Provider.of<PopularDishesProvider>(context).getProductById(id);
We can pass argument with the help of argument property in pushnamed method
Navigator.pushNamed(context, AppRoutes.Page1,
arguments: {"name": "lava", "body": "chi"});
Receive value
var arguments3 = ModalRoute.of(context)!.settings.arguments;
var arguments2 =
arguments3!=null? arguments3 as Map<String, dynamic>:{};
May like this
SAmple Code
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: "/",
routes: {
AppRoutes.home: (context) => Home(),
AppRoutes.Page1: (context) => Page1(),
},
title: _title,
// home: ,
);
}
}
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("title")),
body: const Center(
child: MyStatelessWidget(),
),
);
}
}
var _color = Colors.black;
var _value = 0.0;
class MyStatelessWidget extends StatefulWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
#override
State<MyStatelessWidget> createState() => _MyStatelessWidgetState();
}
class _MyStatelessWidgetState extends State<MyStatelessWidget> {
#override
Widget build(BuildContext context) {
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, AppRoutes.Page1);
},
child: Text("Without Argument")),
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, AppRoutes.Page1,
arguments: {"name": "lava", "body": "chi"});
},
child: Text("With Argument")),
],
),
);
}
#override
void initState() {}
}
class Page1 extends StatelessWidget {
const Page1({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
var arguments3 = ModalRoute.of(context)!.settings.arguments;
var arguments2 =
arguments3!=null? arguments3 as Map<String, dynamic>:{};
// {"name": "nodata", "body": "no data"};
return Material(
child: Center(
child: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(arguments2["name"] ?? "Nodata",
style: TextStyle(fontSize: 30)),
Text(
arguments2["body"] ?? "No DAta",
style: TextStyle(fontSize: 30),
),
],
),
),
),
);
}
}
class AppRoutes {
static String failed = "/page2";
static String Page1 = "/page1";
static String home = "/";
}
your design is a little confusing.
if you are trying to get the ID and Quantity in the Cart-screen, then why do you want to navigate to it without the arguments?
any how, I guess you have a use case where you want to do different thing if the arguments are not passed. then the only thing you need is to check if the arguments are null. right?
#override
Widget build(BuildContext context) {
final routes =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
if (routes != null) {
final id = routes['id']; //Received Arguments
final quantity = routes['quantity']; //Received Arguments
final provider =
Provider.of<PopularDishesProvider>(context).getProductById(id);
} else {
// do the things here when no argument is passed.
}
I want to reuse different types of fields in different forms and I have created a separate Widget that returns TextFormField.
Logically, different types of fields have their own validations and other properties, so I have started looking into inheritance and so on to avoid rewriting same chunks of code.
From what I have learnt, Flutter does not encourage inheritance of widgets, so my question is on the best practices of reusing code for various form fields in flutter to remain readability and keep the code clean.
Any tips?
In my experience, I rarely had the need to use other widgets than the original form fields provided by flutter. What I found useful to reuse are validation functions for each fields, since they often have common needs in term of validation.
These are just two basic samples. I pass them to the validator argument of the form field whenever it's needed.
String? validatorForMissingFields(String? input) {
if (input == null || input.isEmpty || input.trim().isEmpty) {
return "Mandatory field";
}
return null;
}
String? validatorForMissingFieldsAndLength(String? input, int length) {
if (input == null || input.isEmpty || input.trim().isEmpty) {
return "Mandatory field";
}
if (input.length != length) {
return 'Not long enough';
}
return null;
}
In any case, instead of extending a basic widget, I prefer to create a new one containing the basic widget with some fixed properties, and others that can be customized. This example does not involve form fields, but I think it can better explain my point.
///if text is not null, icon is ignored
class RectButton extends StatelessWidget {
final Function()? onPressed;
final String? text;
final IconData? icon;
final Color color;
const RectButton({this.text, this.icon, required this.onPressed, Key? key, this.color = mainLightColor}) : super(key: key);
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(12.0),
child: OutlinedButton(
style: ButtonStyle(
side: MaterialStateProperty.all(BorderSide(color: color)),
overlayColor: MaterialStateColor.resolveWith((states) => color.withOpacity(0.5)),
backgroundColor: MaterialStateColor.resolveWith((states) => color.withOpacity(0.3)),
),
onPressed: onPressed,
child: text != null
? Text(
text!,
style: TextStyle(fontWeight: FontWeight.bold, color: color),
)
: Icon(
icon,
color: color,
)),
);
}
}
In order to maintain the same look&feel in all the app, I created a custom button with some 'invisible' widgets above it that allowed me to set some properties without extending a basic widget. The properties I needed to be customized are passed to the constructor.
You can create a class to store only the important things like a label or a controller and then use a wrap widget and a for loop to generate the widgets.
Here's an example:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final List<TextFieldData> _allFieldData = [
TextFieldData(
label: 'field 1',
validator: numberOnlyValidator,
autovalidateMode: AutovalidateMode.onUserInteraction,
),
TextFieldData(
label: 'field 2',
validator: canBeEmptyValidator,
),
TextFieldData(
label: 'field 3',
validator: numberOnlyValidator,
),
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: Form(
child: Wrap(
runSpacing: 16,
spacing: 16,
children: [
for (var fieldData in _allFieldData)
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 250),
child: TextFormField(
decoration: InputDecoration(label: Text(fieldData.label)),
controller: fieldData.controller,
autovalidateMode: fieldData.autovalidateMode,
validator: fieldData.validator,
),
)
],
),
),
),
),
);
}
}
const String numbersOnlyError = 'Only numbers';
const String requiredFieldError = 'Required field';
RegExp numbersOnlyRegexp = RegExp(r'^[0-9]\d*(,\d+)?$');
String? numberOnlyValidator(String? value) {
if (value == null || value.isEmpty) {
return requiredFieldError;
} else if (!numbersOnlyRegexp.hasMatch(value)) {
return numbersOnlyError;
}
return null;
}
String? canBeEmptyValidator(String? value) {
return null;
}
class TextFieldData {
final String label;
final String? Function(String?)? validator;
final AutovalidateMode autovalidateMode;
TextEditingController controller = TextEditingController();
TextFieldData({
required this.label,
required this.validator,
this.autovalidateMode = AutovalidateMode.disabled,
});
}
And then you can do whatever you want using the .controller of each item inside _allFieldData
Note: I put everything in the same file for simplicity but you would normally have the class and the validators in separate files.