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('Ожидаем загрузки...'),
          ),
        ),
    );
  }
}

Flutter: импорт всех файлов из папки

Как такового способа типа:

import '../bloc/notify/*.dart';

к сожалению нет. Чтобы включить в проект все файлы папки, можно воспользоваться небольшим трюком — создать в корне папки еще один файл с содержимым вида:

library notify;
export 'bloc.dart';
export 'event.dart';
export 'state.dart';

И уже далее, в основном проекте его добавлять как:

import '../bloc/notify/index.dart';

Flutter: архитектура BLOC

Основная идея данной архитектуры — отделить отрисовку от логики. Побочная возможность — еще один способ изменения данных виджета из другого виджета, без использования StreamController (ну на самом деле он таки используется но «внутри») и передергивания SetState

Итак, для использования нужно в pubspec.yaml добавить:

» Читать далее

Flutter: особенности отображения listview внутри виджета showdialog

А именно, проблема заключается в том, что ничего не отображается, пока listview не будет обёрнут в контейнер с указанной высотой. И это на самом деле проблема, т.к. в этом случае нельзя указать «резиновую» высоту. Обёртывание в виджет Expanded тоже не поможет. Остаётся один выход — рассчитывать высоту в случае динамического списка. Например:

            Container(
              height: EzsInfo["connectors"].length.toDouble()*72,
              width: double.maxFinite,
              child:  ListView.builder(
                  shrinkWrap: true,
                  physics: NeverScrollableScrollPhysics(),
                  padding: const EdgeInsets.all(8),
                  itemCount: EzsInfo["connectors"].length,
                  itemBuilder: (BuildContext context, int index) {
                    return Container(
                      //padding: EdgeInsets.symmetric(vertical: 10),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Row(
                              mainAxisSize: MainAxisSize.max,
                              mainAxisAlignment: MainAxisAlignment.start,
                              children: [
                                Padding(
                                  padding: const EdgeInsets.only(bottom: 0,left: 0,right: 10),
                                  child: Text(EzsInfo["connectors"][index]["station_name"],style: TextStyle(fontWeight: FontWeight.w100, fontSize: 8.0, color: Colors.black)),
                                ),
                                Container(
                                  width: 6.0,
                                  height: 6.0,
                                  decoration: BoxDecoration(
                                    color: EzsInfo["connectors"][index]["status"]==3?Colors.grey:Colors.green,
                                    shape: BoxShape.circle,
                                  ),),
                                Padding(
                                  padding: const EdgeInsets.only(bottom: 0,left: 2,right: 0),
                                  child: Text(EzsInfo["connectors"][index]["status_name"],style: TextStyle(fontWeight: FontWeight.w100, fontSize: 8.0, color: Colors.black)),
                                ),

                              ],
                            ),
                            Row(
                              children: [
                                IconButton (
                                  padding: const EdgeInsets.only(bottom: 0,left: 0,right: 0),
                                  icon: Image.asset('lib/images/'+EzsInfo["connectors"][index]["url_connector"],height: 32,),
                                  onPressed: () {print('IconButton pressed ...');},
                                ),
                                Container(
                                  alignment: Alignment.topLeft,
                                  width: 150,
                                  child: Column(
                                    children: [
                                      Align(
                                        alignment: Alignment.topLeft,
                                        child: Text(EzsInfo["connectors"][index]["connector_name"],style: TextStyle(fontSize: 12.0, color: Colors.black),textAlign: TextAlign.left,),
                                      ),
                                      Align(
                                        alignment: Alignment.topLeft,
                                        child: Text(EzsInfo["connectors"][index]["maxpower"].toString()+" кВт",style: TextStyle(fontSize: 12.0, color: Colors.black),textAlign: TextAlign.left),
                                      ),
                                    ],
                                  ),
                                ),
                                Text(EzsInfo["connectors"][index]["price"].toString()+"Р кВт*ч",style: TextStyle(fontSize: 12.0, color: Colors.black)),
                              ],
                            ),
                            new Divider(),
                          ],
                        )
                    );
                  }
              ),
            )

Результат:

Flutter: показ прелоадера при загрузке данных в виджете showDialog

Задача: при отображении данных при помощи виджета showDialog, необходимо показывать «прелоадер» во время загрузки данных со стороннего ресурса.

Решение: для отображения прелоадера, используем пакет card_loader. Однако! дело осложняется тем, что виджет showDialog не имеет метода setState, соответственно у нас нет возможности изменить уже отображенные данные, после окончания загрузки.

Для обхода этого ограничения, оформим showDialog как полноценный StatefulWidget с получением в связи с этим стандартных плюшек в виде setState, а так-же возможность вызова своих процедур после окончания отображения виджета:

import 'package:flutter/material.dart';
import 'package:card_loading/card_loading.dart';
import 'dart:convert';

class MyMeDialogInfo extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _MyMeDialogInfoState();
  }
}
class _MyMeDialogInfoState extends State<MyMeDialogInfo> {
  bool info_is_load=false;

  void RefreshMeDialogData(context){
    TRequests req=new TRequests();
    req.request("adfserfserf", {}), (List result){
      setState(() {
        //info_is_load=true;
      });
    }, (String error){
    });

  }

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => RefreshMeDialogData(context));  // эвент после того как страница отобразилась - обновим данные по пользователю
  }

  @override
  Widget build(BuildContext context) {
    return Align (
        alignment: Alignment.bottomCenter,
        child:
        Container(
            width: double.infinity,
            decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.only(
                    topLeft: Radius.circular(32.0),
                    topRight: Radius.circular(32.0))),
            child:Column(
                mainAxisSize: MainAxisSize.min,
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  // заголовок окна
                  Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Material(
                        child: Text("Заголовок окна", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14.0, color: Colors.black))),
                  ),
                  Container(
                    width: MediaQuery.of(context).size.width,
                    height: MediaQuery.of(context).size.height - 220,
                    decoration: BoxDecoration(),
                    child:
                    SingleChildScrollView(
                      child: Stack(
                        children: <Widget>[
                          Visibility(
                              visible: info_is_load,
                              child:
                              Padding(
                                padding: const EdgeInsets.all(16.0),
                                child: Material(
                                    child: Text("Тут перечисляем какую инфу загружаемую асинхронно", style: TextStyle(fontSize: 14.0, color: Colors.black))),
                              )
                          ),
                          Visibility(
                              visible: !info_is_load,
                              child:
                              Padding(
                                padding: const EdgeInsets.only(bottom: 20,left: 20,right: 20),
                                child: Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: const [
                                    CardLoading(
                                      height: 30,
                                      borderRadius: BorderRadius.all(Radius.circular(15)),
                                      width: 100,
                                      margin: EdgeInsets.only(bottom: 10),
                                    ),
                                    CardLoading(
                                      height: 100,
                                      borderRadius: BorderRadius.all(Radius.circular(15)),
                                      margin: EdgeInsets.only(bottom: 10),
                                    ),
                                    CardLoading(
                                      height: 30,
                                      width: 200,
                                      borderRadius: BorderRadius.all(Radius.circular(15)),
                                      margin: EdgeInsets.only(bottom: 10),
                                    ),
                                  ],
                                ),
                              )
                          )
                        ],
                      ),
                    ),
                  )
                ]
            )
        )
    );
  }
}

class TMeDialog {
  void MeDialogInfoDialog(BuildContext context,Map ezs){
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return MyMeDialogInfo(ezs);
      },
    );
  }
}
1 2 3 4 9