Flutter RouterDelegate: Android back button closes the whole app despite RootBackButtonDispatcher - android

I am using PopNavigatorRouterDelegateMixin and RootBackButtonDispatcher and I would expect that the back button press would call the onPopPage method of the navigator built in the router delegate. Instead the back button pops the whole app.
This is the app code
GetMaterialApp(
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
home: Router(
routerDelegate: AppRouterDelegate(),
backButtonDispatcher: RootBackButtonDispatcher(),
),
and this is the RouterDelegate
class AppRouterDelegate extends RouterDelegate<AppRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppRoutePath> {
final GlobalKey<NavigatorState> navigatorKey;
AppRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
#override
Widget build(BuildContext context) {
return Navigator(
pages: [
if (!context.watch<AppState>().loadingDone)
MaterialPage(child: LaunchWidget())
else
MaterialPage(child: AvailableMatches()),
if (context.watch<AppState>().selectedMatch != null)
MaterialPage(
key: ValueKey("MatchDetails"),
name: "MatchDetails",
child: MatchDetails(matchId: context.read<AppState>().selectedMatch)
)
],
onPopPage: (route, result) {
print("popping");
print(route.settings.name);
if (!route.didPop(result)) {
return false;
}
if (route.settings.name == "MatchDetails") {
context.read<AppState>().setSelectedMatch(null);
}
notifyListeners();
return true;
},
);
}
#override
Future<void> setNewRoutePath(AppRoutePath configuration) {}
}

Related

How to check for internet connection once for every screen in Flutter?

I want to check for internet connection at every screen on my app just like Telegram does and whenever user goes offline, show an Offline banner on the top of the screen.
I have tried using connectivity_plus and internet_connection_checker plugins to check for this but the problem is I have to subscribe to a stream for this and if the device goes offline once then there is no way to subscribe to it again without clicking a button.
getConnectivity() =>
subscription = Connectivity().onConnectivityChanged.listen(
(ConnectivityResult result) async {
isDeviceConnected = await InternetConnectionChecker().hasConnection;
if (!isDeviceConnected && isAlertSet == false) {
setState(() {
constants.offline = true;
print('Constants().offline ${constants.offline}');
isAlertSet = true;
});
}
print('off');
},
);
I'm using this code right now to check this issue but I don't want to replicate this code on each and every screen and even if I do replicate it then there will be a lot of subscriptions that I'll be subscribing to, which will mean that all the subscriptions will be disposed at the same time causing all sorts of issues.
If you have custom Scaffold, then you have to edit it. Otherwise, create a new one and change all Scaffolds to your custom one. This allows you to easily apply changes that should be on all pages.
Then, in the CustomScaffold create a Stack that contains page content and ValueListenableBuilder that listens to connection changes and if there is no internet displays error banner.
class CustomScaffold extends StatefulWidget {
const CustomScaffold({Key? key}) : super(key: key);
#override
State<CustomScaffold> createState() => _CustomScaffoldState();
}
class _CustomScaffoldState extends State<CustomScaffold> with WidgetsBindingObserver {
StreamSubscription? connectivitySubscription;
ValueNotifier<bool> isNetworkDisabled = ValueNotifier(false);
void _checkCurrentNetworkState() {
Connectivity().checkConnectivity().then((connectivityResult) {
isNetworkDisabled.value = connectivityResult == ConnectivityResult.none;
});
}
initStateFunc() {
_checkCurrentNetworkState();
connectivitySubscription = Connectivity().onConnectivityChanged.listen(
(ConnectivityResult result) {
isNetworkDisabled.value = result == ConnectivityResult.none;
},
);
}
#override
void initState() {
WidgetsBinding.instance.addObserver(this);
initStateFunc();
super.initState();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
_checkCurrentNetworkState();
}
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
connectivitySubscription?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
Scaffold(
...
),
ValueListenableBuilder(
valueListenable: isNetworkDisabled,
builder: (_, bool networkDisabled, __) =>
Visibility(
visible: networkDisabled,
child: YourErrorBanner(),
),
),
],
);
}
}
First I created an abstract class called BaseScreenWidget
used bloc state management to listen each time the internet connection changed then show toast or show upper banner with Blocbuilder
abstract class BaseScreenWidget extends StatelessWidget {
const BaseScreenWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Column(
children: [
baseBuild(context),
BlocConsumer<InternetConnectionBloc, InternetConnectionState>(
listener: (context, state) {
// if (!state.isConnected) {
// showToast("No Internet Connection");
// }
},
builder: (context, state) {
if (!state.isConnected) {
return const NoInternetWidget();
}
return const SizedBox.shrink();
},
),
],
);
}
Widget baseBuild(BuildContext context);
}
Made each screen only screen widgets contains Scaffold to extends BaseScreenWidget
class MainScreen extends BaseScreenWidget {
const MainScreen({super.key});
#override
Widget baseBuild(BuildContext context) {
return const Scaffold(
body: MainScreenBody(),
);
}
}
it's very helpful to wrap the Column with SafeArea in the build method in BaseScreen.
USE THIS SIMPLE TECHNIQUE only need this package: Internet Connection Checker. If you turn off your network it will tell you
connection_checker.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:internet_connection_checker/internet_connection_checker.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
class CheckMyConnection {
static bool isConnect = false;
static bool isInit = false;
static hasConnection(
{required void Function() hasConnection,
required void Function() noConnection}) async {
Timer.periodic(const Duration(seconds: 1), (_) async {
isConnect = await InternetConnectionChecker().hasConnection;
if (isInit == false && isConnect == true) {
isInit = true;
hasConnection.call();
} else if (isInit == true && isConnect == false) {
isInit = false;
noConnection.call();
}
});
}
}
base.dart
import 'package:flutter/material.dart';
import 'connection_checker.dart';
class Base extends StatefulWidget {
final String title;
const Base({Key? key, required this.title}) : super(key: key);
#override
State<Base> createState() => _BaseState();
}
class _BaseState extends State<Base> {
final snackBar1 = SnackBar(
content: const Text(
'Internet Connected',
style: TextStyle(color: Colors.white),
),
backgroundColor: Colors.green,
);
final snackBar2 = SnackBar(
content: const Text(
'No Internet Connection',
style: TextStyle(color: Colors.white),
),
backgroundColor: Colors.red,
);
#override
void initState() {
super.initState();
CheckMyConnection.hasConnection(hasConnection: () {
ScaffoldMessenger.of(navigatorKey.currentContext!)
.showSnackBar(snackBar1);
}, noConnection: () {
ScaffoldMessenger.of(navigatorKey.currentContext!)
.showSnackBar(snackBar2);
});
}
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
key: navigatorKey,
appBar: AppBar(
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike)),
],
),
title: const Text('Tabs Demo'),
),
body: const TabBarView(
children: [
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
],
),
),
);
}
}
I myself use connectivity_plus and I have never found the problem you mentioned (if the device goes offline once then there is no way to subscribe to it again without clicking a button), you can use my example.
If the user's internet is disconnected, a modal will appear. If the user is connected again, the modal will be deleted automatically.
Anyway, I put the option to check the internet again in the modal
class CheckConnectionStream extends GetxController {
bool isModalEnable = false;
final loadingCheckConnectivity = false.obs;
ConnectivityResult _connectionStatus = ConnectivityResult.none;
final Connectivity _connectivity = Connectivity();
late StreamSubscription<ConnectivityResult> _connectivitySubscription;
Future<void> initConnectivity() async {
late ConnectivityResult result;
try {
result = await _connectivity.checkConnectivity();
loadingCheckConnectivity.value = false;
} on PlatformException {
return;
}
return _updateConnectionStatus(result);
}
Future<void> _updateConnectionStatus(ConnectivityResult result) async {
_connectionStatus = result;
if (result == ConnectivityResult.none) {
if (isModalEnable != true) {
isModalEnable = true;
showDialogIfNotConnect();
}
} else {
if (isModalEnable) {
Get.back();
}
isModalEnable = false;
}
}
showDialogIfNotConnect() {
Get.defaultDialog(
barrierDismissible: false,
title: "check your network".tr,
onWillPop: () async {
return false;
},
middleText: "Your device is not currently connected to the Internet".tr,
titleStyle: TextStyle(
color: Get.isDarkMode ? Colors.white : Colors.black,
),
middleTextStyle: TextStyle(
color: Get.isDarkMode ? Colors.white : Colors.black,
),
radius: 30,
actions: [
Obx(() => loadingCheckConnectivity.value
? const CustomLoading(
height: 30.0,
radius: 30.0,
)
: ElevatedButton(
onPressed: () async {
loadingCheckConnectivity.value = true;
EasyDebounce.debounce(
'check connectivity',
const Duration(milliseconds: 1000), () async {
await initConnectivity();
});
},
child: Text(
'try again'.tr,
style: const TextStyle(color: Colors.white),
),
))
]);
}
#override
void onInit() {
super.onInit();
initConnectivity();
_connectivitySubscription =
_connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
}
#override
void onClose() {
_connectivitySubscription.cancel();
super.onClose();
}
}

LocalStorage getItem() giving null and then true in Material App on app start: Flutter LocalStorage

class MyApp extends StatelessWidget {
final LocalStorage status = new LocalStorage('status');
#override
Widget build(BuildContext context) {
print(' Status: ${status.getItem('status')}'); //here I get null and then true after some time
return MaterialApp(
title: ‘Sample App’,
debugShowCheckedModeBanner: false,
home: status.getItem('status') == 'true'
? Home()
: RegisterDetails(),
I am storing the status using setItem(), to true if the user logs in successfully. But the next I open my app, I get redirected again to the register page, not the home screen. I am getting null in status and after some time I get true which I want. Please help!
Make sure LocalStorage is ready for operations.
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder(
future: status.ready,
builder: (context, snapshot) {
if (snapshot.hasData) {
return status.getItem('status') == 'true'
? Home()
: RegisterDetails();
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
);
}

How to redirect a page from a splash screen (Flutter)

I am developing a splash screen which should show an image as the background of the page and subsequently execute a rest call whose result is necessary to understand which other page to load (login or homepage), I cannot understand why the redirect is not working
void main() {
runApp(const MainPage());
}
// ----------------------------------------------------------------------------
//
// ----------------------------------------------------------------------------
class MainPage extends StatefulWidget {
const MainPage({Key? key}) : super(key: key);
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
#override
void initState() {
super.initState();
}
// ----------------------------------------------------------------------------
// Redirect managed from the server
// ---------------------------------------------------------
Future<void> getRedirectPage(BuildContext context) async {
await initializeSharedPrefs();
ServerResponse serverResponse = await makeRequestToServer(Null);
if (serverResponse.state.compareTo("") != 0) {
if (serverResponse.state.compareTo("true") == 0) {
if (serverResponse.redirect.compareTo("login") == 0) {
Navigator.of(context).pushReplacement(PageRouteBuilder(pageBuilder: (_,__,___)=> LoginPage()));
} else {
Navigator.of(context).pushReplacement(PageRouteBuilder(pageBuilder: (_,__,___)=> Homepage()));
}
} else {
showAlertDialog(context, "Server error", "Something went wrong: " + serverResponse.msg);
}
} else {
showAlertDialog(context, "Server error", "Something went wrong: " + serverResponse.msg);
}
}
// ----------------------------------------------------------------------------
// Initialization of UI
// ----------------------------------------------------------------------------
#override
Widget build(BuildContext context) {
getRedirectPage(context);
return MaterialApp(
title: 'ID - IoT',
home: Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/background.jpeg'),
fit: BoxFit.cover,
),
),
),
theme: ThemeData(
primarySwatch: Colors.blue,
),
);
}
}
This is the error showed from the logcat:
E/flutter (18073): [ERROR:flutter/shell/common/shell.cc(93)] Dart Unhandled Exception: Navigator operation requested with a context that does not include a Navigator.
E/flutter (18073): The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator
You are passing a context to a function that does not store the navigator (the navigator is inside the material app). you can use builder widget.
return MaterialApp(
home: Builder(
builder: (context) {
getRedirectPage(context);
return Container();
},
),
);
or use navigation key, but its not safety at this situation
final navKey = GlobalKey<NavigatorState>();
navKey.currentState!.pop();
return MaterialApp(
navigatorKey: navKey,
home: Container(),
);

How to reload screen on future data

I have TabBarView with six tabs.
I'm trying to show all the installed apps in first tab.
Even after apps is filled CircularProgressIndicator is displayed. Apps are listed once the first tab is revisited.
AppScreenC is called for first tab.
final model = AppModel();
class AppScreenC extends StatefulWidget {
#override
_AppScreenCState createState() => _AppScreenCState();
}
class _AppScreenCState extends State<AppScreenC> {
List<Application> apps = model.loadedApps();
#override
Widget build(BuildContext context) => _buildApps();
Widget _buildApps() => apps != null
? ListView.builder(
itemCount: apps.length,
itemBuilder: (BuildContext context, int index) =>
_buildRow(apps[index]))
: Center(child: CircularProgressIndicator());
Widget _buildRow(ApplicationWithIcon app) {
final saved = model.getApps().contains(app.apkFilePath);
return ListTile(
leading: Image.memory(app.icon, height: 40),
trailing: saved
? Icon(Icons.check_circle, color: Colors.deepPurple[400])
: Icon(Icons.check_circle_outline),
title: Text(app.appName),
onTap: () => setState(() => saved
? model.removeApp(app.apkFilePath)
: model.addApp(app.apkFilePath)),
);
}
}
AppModel class has all the necessary methods.
class AppModel{
final _saved = Set<String>();
List<Application> apps;
AppModel() {
loadApps();
}
Set<String> getApps() {
return _saved;
}
addApp(String apkPath) {
_saved.add(apkPath);
}
removeApp(String apkPath) {
_saved.remove(apkPath);
}
loadApps() async {
apps = await DeviceApps.getInstalledApplications(
onlyAppsWithLaunchIntent: true,
includeSystemApps: true,
includeAppIcons: true);
apps.sort((a, b) => a.appName.compareTo(b.appName));
}
loadedApps() => apps;
}
This is happening because apps is null, when the screen was called first time. It loads the apps in background. Upon visiting the screen again, apps are displayed.
Any help is welcome
What you can do is calling setState() after your Function is done. You need to change loadedApp to return a Future:
class AppScreenC extends StatefulWidget {
#override
_AppScreenCState createState() => _AppScreenCState();
}
class _AppScreenCState extends State<AppScreenC> {
#override
void initState(){
super.initState();
model.loadApps().then((loadedApps){ //loadApps() will return apps and you don't need loadedApps() method anymore
setState((){ //rebuilds the screen
apps = loadedApps
})});
}
#override
Widget build(BuildContext context) => _buildApps();
Widget _buildApps() => apps != null
? ListView.builder(
itemCount: apps.length,
itemBuilder: (BuildContext context, int index) =>
_buildRow(apps[index]))
: Center(child: CircularProgressIndicator());
Widget _buildRow(ApplicationWithIcon app) {
final saved = model.getApps().contains(app.apkFilePath);
return ListTile(
leading: Image.memory(app.icon, height: 40),
trailing: saved
? Icon(Icons.check_circle, color: Colors.deepPurple[400])
: Icon(Icons.check_circle_outline),
title: Text(app.appName),
onTap: () => setState(() => saved
? model.removeApp(app.apkFilePath)
: model.addApp(app.apkFilePath)),
);
}
}
And your AppModel will look like this:
class AppModel{
final _saved = Set<String>();
List<Application> apps;
AppModel() {
loadApps();
}
Set<String> getApps() {
return _saved;
}
addApp(String apkPath) {
_saved.add(apkPath);
}
removeApp(String apkPath) {
_saved.remove(apkPath);
}
Future loadApps() async {
apps = await DeviceApps.getInstalledApplications(
onlyAppsWithLaunchIntent: true,
includeSystemApps: true,
includeAppIcons: true);
apps.sort((a, b) => a.appName.compareTo(b.appName));
return Future.value(apps);
}
}
You can also use FutureBuilder as suggested in the comments

How do I make the build function wait until a button is pressed in an alert in init?

I am trying to display an Alert that shows a disclaimer to the user as soon as the app is opened. The build method will run, that is the app will start its processing only after the user presses okay on the alert.
I've managed to show the alert in init using
SchedulerBinding.instance.addPostFrameCallback((_) => AlertWindow().showAlert(context));
or
Future.delayed(Duration.zero, () => AlertWindows().showAlert(context));
This shows the alert, but the app starts building in the background. I want the app to run/build only after OKAY button is pressed, and after the alert is popped.
Hey I implemented some code, you can try this code directly on dartPad Paste the code in this Editor
I used setState, if it is for real time project you can use Providers or bloc, for performance.
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
Widget viewHolder;
void initState() {
viewHolder = Container();
WidgetsBinding.instance
.addPostFrameCallback((_) => afterPostFrameCallBack());
super.initState();
}
afterPostFrameCallBack() {
_showDialog();
}
#override
Widget build(BuildContext context) {
return viewHolder;
}
Widget _buildView() {
return Container(child: Text('This is after okay button'));
}
void _showDialog() {
// flutter defined function
showDialog(
context: context,
builder: (BuildContext context) {
// return object of type Dialog
return AlertDialog(
title: Text("App Update Available"),
content: Text(
"We have fixed some issues and added some cool features in this update"),
actions: <Widget>[
// usually buttons at the bottom of the dialog
FlatButton(
child: new Text("ok"),
onPressed: () {
Navigator.of(context).pop();
setState(() {
viewHolder = _buildView();
});
},
),
],
);
},
);
}
}

Categories

Resources