Flutter: отображение веб страницы в виджете

Работа с webview во Flutter не так проста как на «чистом» Java/Kotlin. В основном из-за того что, на pub.dev есть большое разнообразие собственно вапперов к webview, а потому выбрать рабочую и полноценную реализацию не так просто. Я например потратил с полдня, чтобы выяснить, что:

webview_flutter — не отображает страницы с «плохими» сертификатами SSL. Например самоподписанными, или с сертификатами с истёкшим сроком действия, или с сертификатом к которому нет доверия (теперь это все сайты госучреждений и некоторых банков). И нет никакого способа (или не нашел), заставить это сделать.

flutter_inappwebview — та же самая проблема что и в вышеуказанном плагине. Да, там есть ключ «игнорировать ошибки SSL», но он не работает

flutter_webview_plugin — выпущен 21 месяц назад, и при компиляции уже ругается на отсутствие поддержки Android 12. Хотя как раз этот плагин, работает со страницами с плохим SSL именно так как нужно

flutter_webview_plugin_ios_android — а вот этот плагин, это как я понял «подхваченый из ослабеших» рук разработчиков flutter_webview_plugin, и доработаный уже под Android 12. На нём мои поиски и закончились. Ниже минимальный пример для работы с ним:

import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin_ios_android/flutter_webview_plugin_ios_android.dart';
import 'dart:async';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      routes: {
         '/': (_) => const MyHomePage(),
      }
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String selectedUrl = 'https://грибовы.рф';
  final flutterWebViewPlugin = FlutterWebviewPlugin();

  // при изменении url
  late StreamSubscription<String> _onUrlChanged;
  // при ошибке
  late StreamSubscription<WebViewHttpError> _onHttpError;
  // изменение процента загрузки страницы
  late StreamSubscription<double> _onProgressChanged;
  // поскролили вверх-вниз
  late StreamSubscription<double> _onScrollYChanged;
  // поскролили вправо-влево
  late StreamSubscription<double> _onScrollXChanged;

  @override
  void dispose() {
    _onUrlChanged.cancel();
    _onHttpError.cancel();
    _onProgressChanged.cancel();
    _onScrollXChanged.cancel();
    _onScrollYChanged.cancel();
    flutterWebViewPlugin.dispose();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    flutterWebViewPlugin.close();

    // Слушатель изменения url страницы
    _onUrlChanged = flutterWebViewPlugin.onUrlChanged.listen((String url) {
      print("URL: $url");
    });
    // изменение прогресса загрузки
    _onProgressChanged =
        flutterWebViewPlugin.onProgressChanged.listen((double progress) {
            setState(() {
              print('onProgressChanged: $progress');
            });
        });

    _onScrollYChanged =
        flutterWebViewPlugin.onScrollYChanged.listen((double y) {
        });

    _onScrollXChanged =
        flutterWebViewPlugin.onScrollXChanged.listen((double x) {
        });


    _onHttpError =
        flutterWebViewPlugin.onHttpError.listen((WebViewHttpError error) {
          print("Ошибка загрузки:  ${error.code} ${error.url}");
        });


  }

  // перехват вызовов Javascript
  final Set<JavascriptChannel> jsChannels = [
    JavascriptChannel(
        name: 'Print',
        onMessageReceived: (JavascriptMessage message) {
          print(message.message);
        }),
  ].toSet();



  @override
  Widget build(BuildContext context) {
    return  WebviewScaffold(
        ignoreSSLErrors: true,
        url: selectedUrl,
        javascriptChannels: jsChannels,
        mediaPlaybackRequiresUserGesture: false,
        appBar: AppBar(
          title: const Text('Widget WebView'),
        ),
        withZoom: true,
        withLocalStorage: true,
        hidden: false,
        initialChild: Container(
          color: Colors.redAccent,
          child: const Center(
            child: Text('Ожидаем загрузки...'),
          ),
        ),
    );
  }
}

Android: Не работает диалог выбора файла в webview

Задача: в контексте webview приложения для android, не открывается диалоговое окно выбора файла, в виду отсутствия в «базовом» варианте работы webview необходимых обработчиков. Решение описываемое в статье 2020года не работает в 2022 году

Решение: в конце 2022 года, рабочая минимальная обвязка для обработки выбора файлов такова:

public class MainActivity extends AppCompatActivity {
    private ValueCallback<Uri[]> fileChooserCallback;
    private WebView mbrowser;
    Uri home = Uri.parse("https://цукмсцумкцу.ru/choser.php");
    //загрузки изображений
    private ValueCallback<Uri> mUploadMessage;
    public ValueCallback<Uri[]> uploadMessage;
    public static final int REQUEST_SELECT_FILE = 100;
    public static final int REQUEST_QR_CODE = 200;
    private final static int FILECHOOSER_RESULTCODE = 1;
    public final static int CHILD_FINISH = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mbrowser=(WebView) findViewById(R.id.main_webview);

        mbrowser.getSettings().setJavaScriptEnabled(true); // разрешен javascript

        mbrowser.getSettings().setDatabaseEnabled(true); // хранение данных во встроенной БД в браузере
        mbrowser.getSettings().setDomStorageEnabled(true);
        mbrowser.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);   // разрешать открывать окна

        mbrowser.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView vw, WebResourceRequest request) {
                if (request.getUrl().toString().contains(home.getHost())) {
                    vw.loadUrl(request.getUrl().toString());
                } else {
                    Intent intent = new Intent(Intent.ACTION_VIEW, request.getUrl());
                    vw.getContext().startActivity(intent);
                }

                return true;
            }
        });

        mbrowser.setWebChromeClient(new WebChromeClient() {
            @Override
            public void onPermissionRequest(final PermissionRequest request) {
                request.grant(request.getResources());
            }

            @Override
            public boolean onShowFileChooser(WebView vw, ValueCallback<Uri[]> filePathCallback,
                                             FileChooserParams fileChooserParams) {
                if (fileChooserCallback != null) {
                    fileChooserCallback.onReceiveValue(null);
                }
                fileChooserCallback = filePathCallback;

                Intent selectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
                selectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
                selectionIntent.setType("*/*");

                Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
                chooserIntent.putExtra(Intent.EXTRA_INTENT, selectionIntent);
                startActivityForResult(chooserIntent, 0);

                return true;
            }
        });

        mbrowser.setOnKeyListener((v, keyCode, event) ->
        {
            WebView vw = (WebView) v;
            if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK && vw.canGoBack()) {
                vw.goBack();

                return true;
            }

            return false;
        });
        mbrowser.setDownloadListener((uri, userAgent, contentDisposition, mimetype, contentLength) -> handleURI(uri));
        mbrowser.setOnLongClickListener(v -> {
            handleURI(((WebView) v).getHitTestResult().getExtra());

            return true;
        });

        mbrowser.loadUrl(home.toString());
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
        super.onActivityResult(requestCode, resultCode, intent);

        fileChooserCallback.onReceiveValue(new Uri[]{Uri.parse(intent.getDataString())});
        fileChooserCallback = null;
    }

    private void handleURI(String uri) {
        if (uri != null) {
            Intent i = new Intent(Intent.ACTION_VIEW);
            i.setData(Uri.parse(uri.replaceFirst("^blob:", "")));

            startActivity(i);
        }
    }
}

Краткое выделение всей строчки при клике в radiobutton

Проблема обнаруживается в webview приложений на android. А именно, если где-то в коде есть radiobutton обёрнутый чем угодно, то при клике по нему, кратковременно (на долю секунды) выделяется вся строчка целиком.

Решение: достаточно добавить в стилевое оформление этого элемента cursor:default

Update: достаточно применить к body -webkit-tap-highlight-color: rgba(255, 255, 255, 0);

Ошибка определения текущих координат пользователя

С выходом новых версий Android (начиная с 11), почему то сломалось определение текущих координат пользователя в webview. Пока сделал следующий костыль:

 function fetchLocation() {
        if (navigator.geolocation) {
            navigator.geolocation.getCurrentPosition(onGeoSuccess, onGeoError)
        } else {            
            console.log("- без передачи геолокации ((")            
        }
     }
    function onGeoSuccess(event){
        latitude=event.coords.latitude;
        longitude=event.coords.longitude;
        accuracy=event.coords.accuracy; //точность
        console.log("- можно снимать координаты в браузере способ №2")
        geosupport=true;
    }
    function onGeoError(event){
            console.log("- без передачи геолокации ((")
    }
    
        console.log("--способ 1");
        navigator.geolocation.getCurrentPosition(
        function(location) {
            latitude=location.coords.latitude;
            longitude=location.coords.longitude;
            accuracy=location.coords.accuracy; //точность
            console.log("- можно снимать координаты в браузере способ №1")
          },
            function(error){
                console.log("-пробую способ 2");
                fetchLocation();
           },{enableHighAccuracy: true,timeout: 2000,
                             maximumAge: 60000}
      );        
    

Android WebView: не работает выбор файла на странице в теге input type=»file»

Неожиданно обнаружилось что в странице загружаемой в виджет webview  не работаю инпуты файлов. Т.е. при нажатии кнопки браузер не реагирует от слова никак. Как оказалось при дальнейших раскопках, Google подразумевает, что обработку файлов «вы берете на себя».

Решение:

Update: Решение в 2022 году

Update: Решение в 2022 году

1 2