Flutter url_launcher Unhandled Exception: Could not launch youtube url (Caused by canLaunch)

I'm trying to use the url_launcher plugin to open youtube videos by link but the canLaunch function keeps throwing an error. I'm able to bypass this error only by completely removing the canLaunch function but can't figure out what is wrong.

Code not working:

_goToVideo(YoutubeVideoData video) async {
  if (await canLaunch(video.url)) {
    await launch(video.url);
  } else {
    throw 'Could not launch ${video.url}';
  }
}

Code working:

_goToVideo(YoutubeVideoData video) async {
  await launch(video.url);
}

I'm not quite sure why I can't use the canLaunch method as written in the README Example

Error:

E/flutter (12574): [ERROR:flutter/lib/ui/ui_dart_state.cc(166)] Unhandled Exception: Could not launch https://www.youtube.com/watch?v=-3g5WlqJtIo
E/flutter (12574): #0      _goToVideo (package:esfandapp/widgets/newsList/videoCard.dart:71:5)
E/flutter (12574): <asynchronous suspension>
E/flutter (12574): #1      VideoCard.build.<anonymous closure> (package:esfandapp/widgets/newsList/videoCard.dart:13:20)
E/flutter (12574): #2      _InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:992:19)
E/flutter (12574): #3      _InkResponseState.build.<anonymous closure> (package:flutter/src/material/ink_well.dart:1098:38)
E/flutter (12574): #4      GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:184:24)
E/flutter (12574): #5      TapGestureRecognizer.handleTapUp (package:flutter/src/gestures/tap.dart:524:11)
E/flutter (12574): #6      BaseTapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:284:5)
E/flutter (12574): #7      BaseTapGestureRecognizer.acceptGesture (package:flutter/src/gestures/tap.dart:256:7)
E/flutter (12574): #8      GestureArenaManager.sweep (package:flutter/src/gestures/arena.dart:158:27)
E/flutter (12574): #9      GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:224:20)
E/flutter (12574): #10     GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:200:22)
E/flutter (12574): #11     GestureBinding._handlePointerEvent (package:flutter/src/gestures/binding.dart:158:7)
E/flutter (12574): #12     GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:104:7)
E/flutter (12574): #13     GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:88:7)
E/flutter (12574): #14     _rootRunUnary (dart:async/zone.dart:1206:13)
E/flutter (12574): #15     _CustomZone.runUnary (dart:async/zone.dart:1100:19)
E/flutter (12574): #16     _CustomZone.runUnaryGuarded (dart:async/zone.dart:1005:7)
E/flutter (12574): #17     _invoke1 (dart:ui/hooks.dart:267:10)
E/flutter (12574): #18     _dispatchPointerDataPacket (dart:ui/hooks.dart:176:5)

Widget using the function:

class VideoCard extends StatelessWidget {
  final YoutubeVideoData video;
  VideoCard({this.video});

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () => _goToVideo(video),
      child: Container(
        child: Card(
          child: Container(
            child: Column(
              children: [
                Align(
                  child: Padding(
                    child: Text(
                      video.title,
                      style: TextStyle(
                        fontFamily: 'Roboto Condensed',
                        fontSize: 16,
                      ),
                    ),
                    padding: EdgeInsets.fromLTRB(15, 0, 15, 10),
                  ),
                  alignment: Alignment.centerLeft,
                ),
                Container(
                    child: Image.network(video.thumbnails[1], fit: BoxFit.cover,),
                  width: MediaQuery.of(context).size.width,
                ),
                Align(
                  child: Container(
                    child: Text(
                      video.date.toString() + "",
                      style: TextStyle(
                        fontFamily: 'Roboto Condensed',
                        fontSize: 14,
                        fontWeight: FontWeight.w300,
                      ),
                    ),
                    padding: EdgeInsets.fromLTRB(15, 5, 15, 0),
                  ),
                  alignment: Alignment.centerLeft,
                ),
              ],
            ),
            width: MediaQuery.of(context).size.width - 32,
            padding: EdgeInsets.symmetric(
              horizontal: 0,
              vertical: 10,
            ),
            alignment: Alignment.center,
          ),
          shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.all(Radius.circular(25))),
        ),
      ),
    );
  }
}

Solution 1:

Starting from API30 (Android 11), your Android app has to list all apps it interacts with.

You can add:

<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>

in your android manifest to bypass it or specifically list them.

For more info: https://developer.android.com/about/versions/11/privacy/package-visibility

Solution 2:

Personally, I don't like the uncertainty that seems to come with using the QUERY_ALL_PACKAGES permission (because Google could stop letting people use it in the future). For that reason, I did some investigation and found that adding the following to my AndroidManifest.xml allows my app to open the browser, phone app and email app on API 30:

<manifest>

    <!-- Nest within the manifest element, not the application element-->
    <queries>
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="https" />
        </intent>
        <intent>
            <action android:name="android.intent.action.DIAL" />
            <data android:scheme="tel" />
        </intent>
        <intent>
            <action android:name="android.intent.action.SEND" />
            <data android:mimeType="*/*" />
        </intent>
    </queries>

    <application>
        ....
    </application>
</manifest>

To get the same functionality working on iOS, it may be necessary to add the following to your info.plist file:

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>https</string>
    <string>http</string>
    <string>tel</string>
    <string>mailto</string>
</array> 

Just wanted to share in case it helps someone else out.

Solution 3:

Even after trying accepted answer, If its not working for you then Please try followed code.

PRE STEPS: Do as accepted answer suggested.

Add following tags in AndroidManifest.xml before <application/>

<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>

<queries>
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="https" />
    </intent>
    <intent>
        <action android:name="android.intent.action.DIAL" />
        <data android:scheme="tel" />
    </intent>
    <intent>
        <action android:name="android.intent.action.SEND" />
        <data android:mimeType="*/*" />
    </intent>
</queries>

NOW WHAT I HAVE DONE

Create one method:

Future<void> _makeSocialMediaRequest(String url) async {
    if (await canLaunch(url)) {
      await launch(url);
    } else {
      throw 'Could not launch $url';
    }
  }

and Called it by following way:

  //FOR EMAIL
  final Uri _emailLaunchUri = Uri(
      scheme: 'mailto',
      path: '[email protected]',
      queryParameters: {'subject': 'Pratik Butani'});
  _makeSocialMediaRequest(_emailLaunchUri.toString());

  //FOR PHONE NUMBER:
  final Uri _phoneLaunchUri =
      Uri(scheme: 'tel', path: postOffice.mobileNo);
  _makeSocialMediaRequest(_phoneLaunchUri.toString());

  //FOR ANY URL.. YOU CAN PASS DIRECT URL..
  _makeSocialMediaRequest("http://pratikbutani.com");

Its working for me. Hopefully it will work for you too. Thank you.

Solution 4:

bro only put '!' before "if (!await canLaunch(url))" Use this -->

 if (!await canLaunch(url)){
  await launch(
    url,
    forceSafariVC: false,
    forceWebView: false,
    headers: <String, String>{'my_header_key': 'my_header_value'},
  );
} else {
  
  throw 'Could not launch $url';
}