Open the youtube app from flutter app on iOS - ios

I basically want to open a specific youtube video from my app, when a button is pressed. If the youtube app is installed on the user's device, then the video should be opened in the youtube app (and not in the browser or a separate webview).
I used the url_launcher package for that, and it works fine on android. However on iOS the youtube app is not opened even if it is installed, instead a separate web window is opened, where the corresponding youtube url is shown as a webpage.
I thought, that I could override this behaviour like so:
_launchURL() async {
if (Platform.isIOS) {
if (await canLaunch('youtube://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw')) {
await launch('youtube://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw');
} else {
if (await canLaunch('https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw')) {
await launch('https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw');
} else {
throw 'Could not launch https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw';
}
}
} else {
const url = 'https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw';
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
}
but it didn’t work. In case you wonder, I use the following imports:
import 'dart:io' show Platform;
import 'package:url_launcher/url_launcher.dart';
I am pretty sure, the youtube:// URL-Scheme works (launches the YouTube app), because I tested it on third party apps (Launch Center Pro and Pythonista).
The last thing I was not able to test, is if the Platform.isIOS is really true on my IPhone.
Is there a working way, to open the YouTube App from flutter?

I fixed it. I had to set forceSafariVC: false, because it is true on default, which causes the url to be opened inside a sort of webview inside the app.
_launchURL() async {
if (Platform.isIOS) {
if (await canLaunch('youtube://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw')) {
await launch('youtube://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw', forceSafariVC: false);
} else {
if (await canLaunch('https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw')) {
await launch('https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw');
} else {
throw 'Could not launch https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw';
}
}
} else {
const url = 'https://www.youtube.com/channel/UCwXdFgeE9KYzlDdR7TG9cMw';
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
}
This is actually documented in the url_launcher docs, but somewhat hidden...

You don't have to have all that if/else clauses. The most important thing to take into consideration is that whether the device has the YouTube app or not, regardless of the O.S (and remember to define your function as Future because of the async):
Future<void> _launchYoutubeVideo(String _youtubeUrl) async {
if (_youtubeUrl != null && _youtubeUrl.isNotEmpty) {
if (await canLaunch(_youtubeUrl)) {
final bool _nativeAppLaunchSucceeded = await launch(
_youtubeUrl,
forceSafariVC: false,
universalLinksOnly: true,
);
if (!_nativeAppLaunchSucceeded) {
await launch(_youtubeUrl, forceSafariVC: true);
}
}
}
}
The thing to highlight here to avoid several if/else si the attribute universalLinksOnly set to true.

I have solved the issue. You can try the below code:
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
void main() => runApp(const HomePage());
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: MyApp(),
);
}
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Future<void>? _launched;
#override
Widget build(BuildContext context) {
const String toLaunch = 'https://www.youtube.com/watch?v=WcZ8lTRTNM0';
return Scaffold(
appBar: AppBar(title: const Text('Flutter Demo')),
body: Center(
child: ElevatedButton(
onPressed: () => setState(() {
_launched = _launchInBrowser(toLaunch);
}),
child: const Text('Launch in browser'),
),
), );
}
Future<void> _launchInBrowser(String url) async {
if (!await launch(
url,
forceSafariVC: true,
forceWebView: false,
headers: <String, String>{'my_header_key': 'my_header_value'},
)) {
throw 'Could not launch $url';
}
}
}

Using same package:
https://pub.dev/packages/url_launcher
Here is latest working example. Most answers above is outdated or using deprecated package. Default mode is LaunchMode.platformDefault. Change to LaunchMode.externalApplication will open youtube app. Hope this helps
Future<dynamic> openUrl(String url, callback) async {
try {
if (await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication)) {
callback(true);
} else {
toastMessage('#1: Could not launch $url');
callback(false);
}
} catch (e) {
toastMessage('#2: Could not launch $url');
callback(false);
}
}

Related

StateError (Bad state: No element) on IOS only

This error does not occur on Android or web but only on IOS. It seem very trivial but I can't figure out what's wrong.
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'dart:convert';
import 'package:qr_code_scanner/qr_code_scanner.dart';
class ScanQrPage extends StatefulWidget {
#override
_ScanQrPageState createState() => _ScanQrPageState();
}
class _ScanQrPageState extends State<ScanQrPage> {
final qrKey = GlobalKey();
late QRViewController qrViewController;
late Barcode barcode;
// In order to get hot reload to work we need to pause the camera if the platform
// is android, or resume the camera if the platform is iOS.
#override
void reassemble() {
super.reassemble();
if (Platform.isAndroid) {
qrViewController.pauseCamera();
} else if (Platform.isIOS) {
qrViewController.resumeCamera();
}
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
Navigator.of(context).pop("");
return new Future(() => true);
},
child: Scaffold(
body: Stack(
children: [
buildQrView(context),
],
),
),
);
}
Widget buildQrView(BuildContext context) {
return QRView(
onQRViewCreated: onQRViewCreated,
key: qrKey,
overlay: QrScannerOverlayShape(
cutOutSize: MediaQuery.of(context).size.width * 0.8),
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
);
}
void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) {
log('${DateTime.now().toIso8601String()}_onPermissionSet $p');
if (!p) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('no Permission')),
);
}
}
#override
void dispose() {
qrViewController.dispose();
super.dispose();
}
void onQRViewCreated(QRViewController qrViewController) {
setState(() {
this.qrViewController = qrViewController;
});
qrViewController.scannedDataStream.listen((event) {
setState(() {
this.barcode = event;
if (Platform.isAndroid) {
qrViewController.pauseCamera();
} else if (Platform.isIOS) {
qrViewController.resumeCamera();
}
String rawData = event.code;
Uri data = Uri.dataFromString(rawData);
String para1 = data.queryParameters["buy"] ??
""; //get parameter with attribute "para1"
Codec<String, String> stringToBase64 = utf8.fuse(base64);
if (para1 != "") {
placer = stringToBase64.decode(para1);
}
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
Navigator.pop(context, placer);
});
});
});
}
}
I've tried all the solutions with the same error found on stackoverflow (addPostFrameCallback and Future(Duration.zero)) but none of them are exactly the same and does not seem to fix my problem.
I don't think I have having the same issue as any of the other questions.
The exception is happening on the Navigator.pop(context, placer);
Does anyone have any idea how to overcome this?
Why does this only happen on IOS?

Error signing in using FirebaseAuth in a Flutter app using a physical iOS device

I have implemented a simple application using Flutter and FirebaseAuth where I want a user to sign in giving an email and a password, this application works as intended in the iOS simulators however, when I try side loading the application on to a physical iOS device I get several errors and the signing in process fails and the app doesn't continue there onwards. I've shown the code, the errors that arises and I have listed the steps that I've taken so far to mitigate this of which none has worked.
Code
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'screens/other/LoadingScreen.dart';
import 'screens/other/ErrorScreen.dart';
import 'screens/other/SignupScreen.dart';
import 'screens/other/HomeScreen.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
// This widget is the root of your application.
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<CovidHound> {
bool _initialized = false;
bool _error = false;
String _email = "";
String _password = "";
void initializeFlutterFire() async {
try {
await Firebase.initializeApp();
print("Init firebase");
setState(() {
_initialized = true;
});
} catch (e) {
print("Error init firebase:${e}");
setState(() {
_error = true;
});
}
}
Future<void> onTapSignIn() async {
try {
await FirebaseAuth.instance
.signInWithEmailAndPassword(email: _email, password: _password);
} on FirebaseAuthException catch (e) {
if (e.code == 'user-not-found') {
print('No user found for that email.');
} else if (e.code == 'wrong-password') {
print('Wrong password provided for that user.');
}
} catch (e) {
print("Error signing in: $e");
}
if (FirebaseAuth.instance.currentUser != null) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HomeScreen(),
fullscreenDialog: true,
),
);
}
}
#override
void initState() {
super.initState();
initializeFlutterFire();
}
#override
Widget build(BuildContext context) {
if(_error) {
return ErrorScreen();
}
if (!_initialized) {
return LoadingScreen();
}
return MaterialApp(
home: Scaffold(
body: Center(
child: Column(
children: [
TextField(
decoration: InputDecoration(hintText: "Email"),
onChanged: (value) {
_email = value;
},
),
TextField(
decoration: InputDecoration(hintText: "Password"),
onChanged: (value) {
_password = value;
},
),
TextButton(
onPressed: () {
onTapSignIn();
},
child: Text("Sign In"),
),
],
),
),
),
);
}
}
Errors
So far I have tried the following,
Properly configuring Firebase according to the documentation.
Cleaning Xcode workspace and builds using flutter clean.
Updating iOS and Xcode to latest versions.
Upgrading Flutter.
Adding permissions for Privacy - Local Network Usage Description in the info.plist as demonstrated in ( https://flutter.dev/docs/development/add-to-app/ios/project-setup#local-network-privacy-permissions )
Currently, you do not await your initializeFlutterFire() function, which could lead to your error message, because the subsequent code is executed before the initializing of Firebase.
Move your initializeFlutterFire() outside the MyApp or it's State class, then try to change the return type to Future<void>, then call this function in main() (instead of in initState()) for example:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeFlutterFire();
runApp(MyApp());
}
Firebase (FlutterFire) requires you to initialise the plugin before you start your App's instance to avoid errors like this.

Flutter - Firebase Dynamic Link is not caught by onLink but open the app on iOS

Everything works fine on android but on ios when the app is already opened clicking the link takes the app in the foreground but the onLink method is not call.
Link:
https://<url>/?link=<link>&apn=<apn>&ibi=<bundle>&isi=<isi>
Package:
firebase_dynamic_links: ^0.6.3
Code
import 'package:firebase_dynamic_links/firebase_dynamic_links.dart';
import 'package:flutter/material.dart';
class DynamicLinksService {
Future handleDynamicLinks(BuildContext context) async {
final PendingDynamicLinkData data =
await FirebaseDynamicLinks.instance.getInitialLink();
await _handleDynamicLink(context, data);
FirebaseDynamicLinks.instance.onLink(
onSuccess: (PendingDynamicLinkData dynamicLinkData) async {
await _handleDynamicLink(context, dynamicLinkData);
}, onError: (OnLinkErrorException e) async {
print('Dynamic link failed: ${e.message}');
});
}
Future _handleDynamicLink(
BuildContext context, PendingDynamicLinkData data) async {
final Uri deepLink = data?.link;
if (deepLink != null) {
print('_handleDeepLink | deepLink $deepLink');
await _doSomething(context, deepLink.toString());
} else {
print('no deepLink');
}
}
}
From my experimentation, onLink is not called on iOS however you can call getInitialLink() and it will contain the link. I'm uncertain if this is by design or a bug, but it seems to be across a few versions.
Example service code:
Future<Uri> retrieveDynamicLink(BuildContext context) async {
try {
final PendingDynamicLinkData data = await FirebaseDynamicLinks.instance.getInitialLink();
final Uri deepLink = data?.link;
return deepLink;
} catch (e) {
print(e.toString());
}
return null;
}
Widget snippet
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed){
final _timerLink = Timer(
const Duration(milliseconds: 1000),
() async {
final auth = Provider.of<FirebaseAuthService>(context, listen: false);
final link = await auth.retrieveDynamicLink(context);
_handleLink(link);
},
);
}
}
Make sure to add the WidgetsBindingObserver
class _SignInPageState extends State<SignInPage> with TickerProviderStateMixin, WidgetsBindingObserver{
I'm not sure why this works but you'd first have to call FirebaseMessaging.instance.getInitialMessage() at least once before your onLink callback is activated by Firebase.
Not sure if this is by design or a bug.
Let me know if this works.

Why initState dont work after url changes on WebView?

I am new in flutter and am trying to block the phone back button, it works when the app starts but when i search something in google for example and the URL changes the button keeps working
I ve tried WillPopScope but never worked for me
here is my app
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_statusbar_manager/flutter_statusbar_manager.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
import 'package:back_button_interceptor/back_button_interceptor.dart';
String mainURL = "http://google.com";
void main() async {
await FlutterStatusbarManager.setHidden(true, animation:StatusBarAnimation.SLIDE);
runApp(MaterialApp(home: App()));
}
class App extends StatefulWidget {
#override
AppState createState() => new AppState();
}
class AppState extends State<App> {
#override
void initState() {
super.initState();
BackButtonInterceptor.add(btnInterceptor);
final fWPlugin = new FlutterWebviewPlugin();
fWPlugin.onDestroy.listen((_) => exit(0));
fWPlugin.onUrlChanged.listen((String url) {
print("URL: " + url);
});
}
#override
void dispose() {
BackButtonInterceptor.remove(btnInterceptor);
super.dispose();
}
bool btnInterceptor(bool stopDefaultButtonEvent) {
return true;
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "Webview",
theme: ThemeData.dark(),
routes: {
"/": (_) =>
WebviewScaffold(
url: mainURL,
withLocalUrl: true,
withJavascript: true,
withLocalStorage: true,
clearCache: false,
clearCookies: false,
withZoom: false,
enableAppScheme: true,
)
});
}
}
I want that when you search something in the explorer the back button still be blocked`
That did not work for me neither. But I suggest you go to https://github.com/fluttercommunity/flutter_webview_plugin/issues and file an issue report.

Flutter set startup page based on Shared Preference

I've been trying without success to load different pages according to my Shared Preference settings.
Based on several posts found in stackoverflow, i end up with the following solution:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:testing/screens/login.dart';
import 'package:testing/screens/home.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Widget page = Login();
Future getSharedPrefs() async {
String user = Preferences.local.getString('user');
if (user != null) {
print(user);
this.page = Home();
}
}
#override
void initState() {
super.initState();
this.getSharedPrefs();
}
#override
Widget build(BuildContext context) {
return MaterialApp(home: this.page);
}
}
class Preferences {
static SharedPreferences local;
/// Initializes the Shared Preferences and sets the info towards a global variable
static Future init() async {
local = await SharedPreferences.getInstance();
}
}
The variable user is not null because the print(user) returns a value as expected, but the login screen is always being opened.
Your problem is that your build method returns before your getSharedPrefs future is complete. The getSharedPrefs returns instantly as soon as it's called because it's async and you're treating it as a "Fire and Forget" by not awaiting. Seeing that you can't await in your initState function that makes sense.
This is where you want to use the FutureBuilder widget. Create a Future that returns a boolean (or enum if you want more states) and use a future builder as your home child to return the correct widget.
Create your future
Future<bool> showLoginPage() async {
var sharedPreferences = await SharedPreferences.getInstance();
// sharedPreferences.setString('user', 'hasuser');
String user = sharedPreferences.getString('user');
return user == null;
}
When user is null this will return true. Use this future in a Future builder to listen to the value changes and respond accordingly.
#override
Widget build(BuildContext context) {
return MaterialApp(home: FutureBuilder<bool>(
future: showLoginPage(),
builder: (buildContext, snapshot) {
if(snapshot.hasData) {
if(snapshot.data){
// Return your login here
return Container(color: Colors.blue);
}
// Return your home here
return Container(color: Colors.red);
} else {
// Return loading screen while reading preferences
return Center(child: CircularProgressIndicator());
}
},
));
}
I ran this code and it works fine. You should see a blue screen when login is required and a red screen when there's a user present. Uncomment the line in showLoginPage to test.
There is a much pretty way of doing this.
Assuming that you have some routes and a boolean SharedPreference key called initialized.
You need to use the WidgetsFlutterBinding.ensureInitialized() function before calling runApp() method.
void main() async {
var mapp;
var routes = <String, WidgetBuilder>{
'/initialize': (BuildContext context) => Initialize(),
'/register': (BuildContext context) => Register(),
'/home': (BuildContext context) => Home(),
};
print("Initializing.");
WidgetsFlutterBinding.ensureInitialized();
await SharedPreferencesClass.restore("initialized").then((value) {
if (value) {
mapp = MaterialApp(
debugShowCheckedModeBanner: false,
title: 'AppName',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: routes,
home: Home(),
);
} else {
mapp = MaterialApp(
debugShowCheckedModeBanner: false,
title: 'AppName',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: routes,
home: Initialize(),
);
}
});
print("Done.");
runApp(mapp);
}
The SharedPreference Class Code :
class SharedPreferencesClass {
static Future restore(String key) async {
final SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
return (sharedPrefs.get(key) ?? false);
}
static save(String key, dynamic value) async {
final SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
if (value is bool) {
sharedPrefs.setBool(key, value);
} else if (value is String) {
sharedPrefs.setString(key, value);
} else if (value is int) {
sharedPrefs.setInt(key, value);
} else if (value is double) {
sharedPrefs.setDouble(key, value);
} else if (value is List<String>) {
sharedPrefs.setStringList(key, value);
}
}
}

Resources