I'm trying to make a custom ElevatedButton widget whose constructor will receive one of two options:
either a new page to push into the Navigator (my custom StatefulWidget),
or a callback method to override the button's onPressed entirely.
I tried to create two separate constructors - one for each of the possibilities but the constructor which is supposed to receive the callback method doesn't seem to get it. When I debug the code I see the received object is Closure.
What am I doing wrong?
import 'package:flutter/material.dart';
// ignore: must_be_immutable
class OrdinaryButton extends StatelessWidget {
final String? text;
Widget? goto;
late Function()? onPressed;
OrdinaryButton({this.text, this.goto});
OrdinaryButton.overrideOnPressed({this.text, required this.onPressed})
{
this.goto = null;
}
#override
Widget build(BuildContext context) {
return ElevatedButton(
child: Text("$text"),
onPressed: () {
if(goto != null)
{
print("LOG: opening a new page...");
Navigator.push(context, MaterialPageRoute(builder: (context) => goto!));
}
else if(this.onPressed != null)
{
print("LOG: calling a custom function...");
this.onPressed; // This has the 'Closure' object instead of my callback.
}
},
);
}
}
you missed "!()"after "onPressed" your code works for me
hello() {
print("hello");
}
//Call this widget in my code
Column(
children: [
OrdinaryButton.overrideOnPressed(
onPressed: hello,
text: "Salut",
)
],
),
class OrdinaryButton extends StatelessWidget {
final String? text;
Widget? goto;
late Function()? onPressed;
OrdinaryButton({Key? key, this.text, this.goto}) : super(key: key);
OrdinaryButton.overrideOnPressed({this.text, required this.onPressed}) {
goto = null;
}
#override
Widget build(BuildContext context) {
return ElevatedButton(
child: Text("$text"),
onPressed: () {
if (goto != null) {
print("LOG: opening a new page...");
Navigator.push(
context, MaterialPageRoute(builder: (context) => goto!));
} else if (onPressed != null) {
print("LOG: calling a custom function...");
onPressed!();
//you missed "!()" here after onPressed
}
},
);
}
}
Related
I would like to break down my Scaffold into smaller pieces for easy read. I separate widgets into functions and return to the scaffold tree. But I don't know how to make use of the function declared inside the stateful widget which need to setState the UI.
Part of my code:
Future<List<dataRecord>>? dataList;
class _clientDetailState extends State<clientDetail> {
#override
void initState() {
super.initState();
}
List<dataRecord> parseJson(String responseBody) {
final parsed =
convert.jsonDecode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<dataRecord>((json) => dataRecord.fromJson(json)).toList();
}
Future<List<dataRecord>> fetchData(http.Client client) async {
final response = await client
.get(Uri.parse('test.php'));
return parseJson(response.body);
}
Body: myButton,
ListView,
Widget myButton() {
return TextButton(
child: Text('test'),
onTap: () {
dataList = fetchData(http.Client()); //Method not found
},
}
Here is simple way to do
class ClientDetail extends StatefulWidget {
const ClientDetail({Key? key}) : super(key: key);
#override
State<ClientDetail> createState() => _ClientDetailState();
}
class _ClientDetailState extends State<ClientDetail> {
List<dataRecord> dataList = [];
#override
Widget build(BuildContext context) {
return ListView(
children: [
myButton(),
...dataList.map((e) => Text(e)).toList(),
],
);
}
List<dataRecord> parseJson(String responseBody) {
final parsed =
convert.jsonDecode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<dataRecord>((json) => dataRecord.fromJson(json)).toList();
}
Future<List<dataRecord>> fetchData(http.Client client) async {
final response = await client.get(Uri.parse('test.php'));
return parseJson(response.body);
}
Widget myButton() {
return TextButton(
child: const Text('test'),
onPressed: () async {
setState(() async {
dataList = await fetchData(http.Client());
});
});
}
}
Tip: always start class name with capital letter, e.g. ClientDetail instead of clienDetail also DataRecord instead of dataRecord
Regards
You can pass your actual function as a parameter to the widget's function and then call it directly from state;
Body: myButton(onPressed: () => fetchData(http.Client())),
ListView,
Widget myButton({required void Function()? onPressed}) {
return TextButton(
child: Text('test'),
onPressed: onPressed,
);
}
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()
),
),
));
}
}
The used Getx Arguments are cleared after the showDialog method is executed.
_someMethod (BuildContext context) async {
print(Get.arguments['myVariable'].toString()); // Value is available at this stage
await showDialog(
context: context,
builder: (context) => new AlertDialog(
//Simple logic to select between two buttons
); // get some Confirmation to execute some logic
print(Get.arguments['myVariable'].toString()); // Variable is lost and an error is thrown
Also I would like to know how to use Getx to show snackbars without losing the previous arguments as above.
One way to do this is to duplicate the data into a variable inside the controller and make a use from it instead of directly using it from the Get.arguments, so when the widget tree rebuild, the state are kept.
Example
class MyController extends GetxController {
final myArgument = ''.obs;
#override
void onInit() {
myArgument(Get.arguments['myVariable'] as String);
super.onInit();
}
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: Center(child: Obx(() => Text(controller.myArgument()))),
),
);
}
}
UPDATE
Since you are looking for solution without page transition, another way to achieve that is to make a function in the Controller or directly assign in from the UI. Like so...
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 2 (If you don't use GetView)
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends StatelessWidget {
final controller = Get.put(MyController());
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 3 (NOT RECOMMENDED)
If you really really really want to avoid using Controller at any cost, you can assign it to a normal variable in a StatefulWidget, although I do not recommend this approach since it was considered bad practice and violates the goal of the framework itself and might confuse your team in the future.
class MyPage extends StatefulWidget {
const MyPage({ Key? key }) : super(key: key);
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
String _myArgument = 'empty';
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Text(_myArgument),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
setState(() {
_myArgument = Get.arguments['myVariable'] as String;
});
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(_myArgument); // This should work
}
}
So, I'm new to Flutter and was trying to code a simple notes app to learn my way around it. The layout of the app is a HomePage with a ListView of NoteTiles that when tapped open a corresponding NotePage where you can write down things. The issue I got is that every time I leave the NotePage and then re-open it from the HomePage, the NotePage loses its content.
My first idea was to keep the content in the corresponding NoteTile so that when I leave the NotePage I would pop with the content, and when needed I would push to the NotePage with the previously saved content. The problem is that I didn't find any simple way to push and set the content. I've seen there are Notification and Route methods but they come with quite a lot of boilerplate and they look like it's more for passing data from child to parent, which I can do easily when popping.
So, is there a way to avoid the reset of the content of the NotePage? Or maybe is there a simple way to initState with the content previously saved?
Here is the code I have so far:
class NoteTile extends ListTile {
final NotePage note;
final Text title;
final BuildContext context;
NoteTile(this.title, this.note, this.context) : super(
title: title,
onTap: () => {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => note),
),
},
onLongPress: () => null,
);
void switchToNote() async {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => note),
);
}
}
onLongPress will later be used to delete the note.
class NotePage extends StatefulWidget {
final String title;
NotePage({Key key, this.title}) : super(key: key);
#override
_NotePageState createState() => _NotePageState();
}
class _NotePageState extends State<NotePage> {
TextEditingController _controller;
String _value;
void initState() {
super.initState();
_controller = TextEditingController();
_controller.addListener(_updateValue);
_value = '';
}
void dispose() {
_controller.dispose();
super.dispose();
}
void _updateValue(){
_value = _controller.text;
}
Future<bool> _onWillPop() async {
Navigator.pop(context, _value);
return true;
}
#override
Widget build(BuildContext context) {
return WillPopScope(
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
child: TextField(
decoration: InputDecoration(
border: InputBorder.none,
hintStyle: TextStyle(fontStyle: FontStyle.italic),
hintText: 'New note',
),
maxLines: null,
controller: _controller,
),
padding: EdgeInsets.all(12.0),
),
),
onWillPop: _onWillPop,
);
}
}
The _onWillPop is to send back the content to the NoteTile, which currently disregards the return data because I failed to find a way to use that data later when pushing the NotePage again.
ListTile is StatelessWidget widget, so you cannot reserve the state, the NoteTile should be StatefulWidget.
Here is a sample of NoteTile:
class NoteTile extends StatefulWidget {
final Text title;
const NoteTile({Key key, this.title}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _NoteTileState();
}
}
class _NoteTileState extends State<NoteTile> {
String _result;
#override
void initState() {
super.initState();
_result = "";
}
void _onOpenNote() async {
String result = await Navigator.of(context).push(
MaterialPageRoute(builder: (context) => NotePage(title: _result)),
);
if (result != null) {
_result = result;
}
}
#override
Widget build(BuildContext context) {
return ListTile(
title: widget.title,
onTap: _onOpenNote,
);
}
}
And NotePage should be edit in some lines:
TextEditingController can have initialize string & have to initialize by title:
_controller = TextEditingController(text: widget.title);
Navigator.pop can post result and you did it correctly, but if you want to get the result, should wait for Navigator.of(context).push, because its run in another thread.
I am new to Flutter and trying to trigger a snack bar on page load if a message was returned from the page I navigated from. I have managed to get the message to display on a button click, but get an error stating that my context does not have a Scaffold if I try to do it elsewhere.
I am also struggling to find an example of how to show a sack bar without user interaction, so if anyone has a reference, that would surely go a long way in helping as well.
Here is a simplified version of my view:
class LandingView extends StatefulWidget {
final LandingViewModel viewModel;
LandingView(this.viewModel);
#override
State<StatefulWidget> createState() {
return new _ViewState();
}
}
class _ViewState extends State<LandingView> {
#override
void initState() {
super.initState();
}
void _showSnackbar(context, message) {
final snackBar = SnackBar(
content: Text(message),
);
Scaffold.of(context).showSnackBar(snackBar);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: new GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(new FocusNode());
},
child: _buildLayout(context),
),
),
);
}
Widget _buildLayout(BuildContext context) {
Map<String, dynamic> args = getArgs(context); //get value from previous page
if (args != null &&
args["Toast Message"] != null) //check if a value was returned from the previous page. This has been tested and a valid string is being returned
_showSnackbar(
context, args["Toast Message"]); //if so call snack bar function
// this throws an error saying "Scaffold.of() called with a context that does not contain a Scaffold"
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints boxConstraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: boxConstraints.maxHeight),
child: RaisedButton(
child: Text(
"Show Snack Bar",
),
onPressed:
() {
if (args != null &&
args["Toast Message"] !=
null) //check if a value was returned from the previous page. This has been tested and a valid string is being returned
_showSnackbar(context,
args["Toast Message"]); //if so call snack bar function
//this works perfectly
}),
),
);
});
}
}
Any advice would be greatly appreciated
You're getting that because your LandingView widget is not in a Scaffold. You can fix this by putting the LandingView widget inside a StatelessWidget with a Scaffold and changing any references to LandingView to LandingViewPage:
class LandingViewPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: LandingView()
);
}
}
We can do this with addPostFrameCallback method
#override
void initState(){
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => scaffold.showSnackBar(SnackBar(content: Text("snackbar")));
}
In a stateful widget put:
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Error")));
});
}