Search code examples
flutterdartresponsiveconsumer

Flutter Dialog unintentionaly triggers recreating of layout behind


I have an issue with my FLutter code that I am not able to solve neither to understand why this is happening.

First the code in questoin:

import 'dart:io';
import 'dart:ui';
import 'package:app/controller/api/api.dart';
import 'package:app/data/holder/sidebar_entries_holder.dart';
import 'package:app/data/holder/date_holder.dart';
import 'package:app/data/holder/user_holder.dart';
import 'package:app/data/models/user.dart';
import 'package:app/exceptions/login/not_logged_in_exception.dart';
import 'package:app/ui/views/login/login_view.dart';
import 'package:app/ui/views/responsive/home/mobile/home_mobile_view.dart';
import 'package:app/ui/views/responsive/responsive_view.dart';
import 'package:app/utils/common/common_functions.dart';
import 'package:app/utils/log/log.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:responsive_builder/responsive_builder.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Log.initialize();
  HttpOverrides.global = MyHttpOverrides();
  final userHolder = UserHolder();
  final dateHOlder = DateHolder();
  final sideBarEntriesHolder = SideBarEntriesHolder();
  final selectedSideBarEntryHolder = SelectedSideBarEntryHolder();
  final window = PlatformDispatcher.instance.views.first;
  final screenWidth = window.physicalSize.width / window.devicePixelRatio;
  final screenHeight = window.physicalSize.height / window.devicePixelRatio;
  final isHandy = screenWidth < 600;
  if (isHandy) {
    SystemChrome.setPreferredOrientations([
      DeviceOrientation.portraitUp,
      DeviceOrientation.portraitDown,
    ]);
  } else {
    SystemChrome.setPreferredOrientations([
      DeviceOrientation.landscapeLeft,
      DeviceOrientation.landscapeRight,
    ]);
  }

  try {
    final User user = await Api.getUserFromLocalDatabase();
    userHolder.setUser(user);
    Api.setUserHolder(userHolder);
  } catch (e) {
    if (e is NotLoggedInException) {
      Log.warning(
          "No user currently logged in. Will be forwarded to the login view");
    } else {
      Log.severe("Error $e will be forwarded to the login view");
    }
  }
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider.value(value: userHolder),
        ChangeNotifierProvider.value(value: dateHolder),
        ChangeNotifierProvider.value(value: sideBarEntriesHolder),
        ChangeNotifierProvider.value(value: selectedSideBarEntryHolder),
      ],
      child: 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> {
  @override
  Widget build(BuildContext context) {
    return const BaseLayout();
  }
}

class BaseLayout extends StatefulWidget {
  const BaseLayout({Key? key}) : super(key: key);

  @override
  _BaseLayoutState createState() => _BaseLayoutState();
}

class _BaseLayoutState extends State<BaseLayout> {
  late SelectedSideBarEntryHolder selectedSideBarEntryHolder;
  late UserHolder userHolder;

  @override
  void initState() {
    super.initState();
    selectedSideBarEntryHolder = context.read<SelectedSideBarEntryHolder>();
    userHolder = context.read<UserHolder>();
  }

  @override
  Widget build(BuildContext context) {
    print("ME TOO");
    final user = userHolder.user;
    if (user != null && user.isAccessTokenValidForRestOfTheDay()) {
      return Scaffold(
        body: ResponsiveBuilder(builder: (context, sizingInformation) {
          if (sizingInformation.isMobile) {
            return const HomeMobileView();
          } else if (sizingInformation.isTablet) {
            return const ResponsiveView();
          } else {
            return Container();
          }
        }),
        floatingActionButton: Consumer<SelectedSideBarEntryHolder>(
          builder: (context, selectedSideBarEntryHolder, child) {
            return Visibility(
              visible: selectedSideBarEntryHolder.isNotSelected(),
              child: FloatingActionButton(
                onPressed: () {
                  CommonFunctions.showNewDialog(context);
                },
                child: const Icon(Icons.add),
              ),
            );
          },
        ),
      );
    } else {
      return const LoginView();
    }
  }
}

import 'package:app/data/holder/sidebar_entries_holder.dart';
import 'package:app/data/holder/selected_side_bar_entry_holder.dart';
import 'package:app/data/holder/user_holder.dart';
import 'package:app/ui/components/appBar/app_bar.dart';
import 'package:app/ui/components/dateSelector/date_selector.dart';
import 'package:app/ui/components/sidebar/sidebar.dart';
import 'package:app/ui/views/responsive/mobile/edit_view.dart';
import 'package:app/ui/views/responsive/mobile/info_view.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:responsive_builder/responsive_builder.dart';

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

  @override
  _HomeMobileView createState() => _HomeMobileView();
}

class _HomeMobileView extends State<HomeMobileView> {
  late UserHolder userHolder;
  late SideBarEntriesHolder sideBarEntriesHolder;

  @override
  void initState() {
    super.initState();
    userHolder = context.read<UserHolder>();
    sideBarEntriesHolder = context.read<SideBarEntriesHolder>();
  }

  double _calculateSidebarWidth(
      BuildContext context, DeviceScreenType deviceScreenType) {
    var screenWidth = MediaQuery.of(context).size.width;

    if (deviceScreenType == DeviceScreenType.mobile) {
      return screenWidth;
    } else if (deviceScreenType == DeviceScreenType.tablet) {
      return screenWidth * 0.3;
    } else {
      return 300;
    }
  }

  @override
  Widget build(BuildContext context) {
    return ResponsiveBuilder(
      builder: (context, sizingInformation) {
        return Consumer<SelectedSideBarEntryHolder>(
          builder: (context, selectedSideBarEntryHolder, child) {
            if (selectedSideBarEntryHolder.isNotSelected()) {
              return Scaffold(
                appBar: const AppBar(),
                body: Column(
                  children: [
                    const DateSelector(),
                    Expanded(
                      child: Sidebar(
                        sidebarWidth: _calculateSidebarWidth(
                            context, sizingInformation.deviceScreenType),
                      ),
                    ),
                  ],
                ),
              );
            } else {
              if (selectedSideBarEntryHolder.isInEditMode()) {
                return const EditView();
              } else {
                return const Scaffold(
                  body: InfoView(),
                );
              }
            }
          },
        );
      },
    );
  }
}

import 'dart:async';
import 'package:app/data/holder/date_holder.dart';
import 'package:app/data/holder/selected_side_bar_holder.dart';
import 'package:app/ui/modals/error_dialog.dart';
import 'package:app/ui/modals/warning_dialog.dart';
import 'package:app/utils/log/log.dart';
import 'package:app/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:app/controller/api/api.dart';
import 'package:app/data/holder/sidebar_entries_holder.dart';
import 'package:app/data/holder/user_holder.dart';
import 'package:app/data/models/Entry.dart';
import 'package:app/ui/components/sidebar/sidebar_entry.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';

class Sidebar extends StatefulWidget {
  final double sidebarWidth;

  const Sidebar({
    Key? key,
    required this.sidebarWidth,
  }) : super(key: key);

  @override
  _SidebarState createState() => _SidebarState();
}

class _SidebarState extends State<Sidebar> {
  late UserHolder userHolder;
  late DateHolder dateHolder;
  late SideBarEntriesHolder sideBarEntriesHolder;
  late SelectedSideBarEntryHolder selectedSideBarEntryHolder;
  DateTime? lastUpdate;

  @override
  void initState() {
    super.initState();
    userHolder = context.read<UserHolder>();
    dateHolder = context.read<DateHolder>();
    sideBarEntriesHolder = context.read<SideBarEntriesHolder>();
    selectedSideBarEntryHolder = context.read<SelectedSideBarEntryHolder>();
    _reloadEntries();
  }

  Future<List<Entry>> _fetchEntries() async {
    DateHolder dateHolder = Provider.of<DateHolder>(context, listen: false);
    String formattedDate =
        DateFormat('yyyy-MM-dd').format(dateHolder.currentDate);
    List<Entry> entries = [];
    try {
      entries = await Api.getEntriesFromRestEndpoint(
        formattedDate,
        formattedDate,
      );
      final String formattedCurrentDate =
          Utils.formatDate(dateHolder.currentDate);
      sideBarEntriesHolder.setEntries(
          formattedCurrentDate, entries);
      return sideBarEntriesHolder.getEntries(formattedCurrentDate);
    } catch (e) {
      Log.severe('Error while fetching entries: $e');
      return Future.error(e);
    }
  }

  void _reloadEntries() {
    setState(() {
      lastUpdate = DateTime.now();
    });
  }

  @override
  Widget build(BuildContext context) {
    print("I AM CALLED");
    return Consumer<DateHolder>(
      builder: (context, dateHolder, child) {
        var futureEntries = _fetchEntries()();

        return RefreshIndicator(
          onRefresh: () async {
            _reloadEntries();
          },
          child: FutureBuilder<List<Entry>>(
            future: futureEntries,
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return const Center(child: CircularProgressIndicator());
              } else if (snapshot.hasError) {
                return _buildListOnError(snapshot);
              } else if (snapshot.hasData) {
                return _buildList(snapshot.data!);
              } else {
                return _buildEmptyList();
              }
            },
          ),
        );
      },
    );
  }

  Widget _buildListOnError(
      final AsyncSnapshot<List<Entry>> snapshot) {
    String currentDate = Utils.formatDate(dateHolder.currentDate);
    bool listNotEmpty =
        sideBarEntriesHolder.getEntries(currentDate).isNotEmpty;
    if (snapshot.error is Exception && listNotEmpty) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        WarningDialog.show(
            context,
            "Fehler beim Laden der Daten",
            "Wollen Sie es erneut versuchen ?",
            "Ja, nochmal versuchen",
            "Nein, mit den vorhandenen Daten weiterarbeiten",
            onYesPressed: _reloadEntries);
      });
      return _buildList(
          sideBarEntriesHolder.get(currentDate));
    } else {
      if (sideBarEntriesHolder.cached.containsKey(currentDate)) {
        return _buildList(
            sideBarEntriesHolder.cached[currentDate]!);
      } else {
        return _buildEmptyList();
      }
    }
  }

  Widget _buildList(List<Entry> entries) {
    return entries.isEmpty
        ? _buildEmptyList()
        : ListView.builder(
            itemCount: entries.length,
            itemBuilder: (context, index) {
              final entry = entries[index];
              return SidebarEntry(
                entry: entry,
                isSelected: false,
                onTap: () async {
                    ...
                },
                onLongPress: () {
                  showDeleteDialog(entry);
                },
              );
            },
          );
  }

  void showDeleteDialog(final Entry entry) async {
    ...
  }

  void showDeleteInformationDialog() {
    ...
  }

  Future<bool> deleteSideBarEntry(final Entry entry) async {
    ...
  }

  Widget _buildEmptyList() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Text(
            'Keine Einträge',
            style: TextStyle(fontSize: 16),
          ),
          ElevatedButton(
            onPressed: _reloadEntries, // Neuladen der Daten
            child: const Text('Neu laden'),
          ),
        ],
      ),
    );
  }
}

import 'package:flutter/material.dart';

class CommonFunctions {

  static Future<void> showNewDialog(BuildContext context) async {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext context) {
        return const AlertDialog(
            title: Text("My Dialog"),
            content: SingleChildScrollView(
              physics: ClampingScrollPhysics(),
              child: TextField(),
            ));
      },
    );
  }
}

My issue is: When I open a dialog with the help of showNewDialog I get a dialog with a TextField. When I focus (or defocus) the textfield the SideBar is rebuilded. Only the Sidebar. Means that I see the output of print("I AM CALLED"); from my SideBar many times but I do not see the output of print("ME TOO"); from my BaseLayout.

I do not understand why my SideBar is rebuilded when focusing a textfield in the dialog. Especially because I am using the Consumer pattern to only rebuild the view when I call the notify method.

Does anyone have an idea why my view keeps rebuilding ?

------------ EDIT ------------ The SideBar

enter image description here

The Dialog

enter image description here

When I click in the TextField the focus is set and the KeyBoard is shown which triggers the rebuild (multiple times).

enter image description here

Log output when Opening dialog and seting focus

D/InputMethodManager(15863): showSoftInput() view=io.flutter.embedding.android.FlutterView{d6fdf5d VFE...... .F...... 0,0-1080,2296 #1 aid=1073741824} flags=0 reason=SHOW_SOFT_INPUT
I/AssistStructure(15863): Flattened final assist data: 384 bytes, containing 1 windows, 3 views
D/EGL_emulation(15863): app_time_stats: avg=195.49ms min=2.51ms max=2614.66ms count=14
D/InsetsController(15863): show(ime(), fromIme=true)
D/EGL_emulation(15863): app_time_stats: avg=483.08ms min=1.31ms max=9613.11ms count=20
I/flutter (15863): I AM CALLED
I/flutter (15863): [INFO] 2024-04-23 07:49:07.461095: Get Entries from REST endpoint
I/flutter (15863): I AM CALLED
I/flutter (15863): [INFO] 2024-04-23 07:49:07.483933: Get Entries from REST endpoint
I/flutter (15863): I AM CALLED
I/flutter (15863): [INFO] 2024-04-23 07:49:07.527776: Get Entries from REST endpoint
I/flutter (15863): I AM CALLED
I/flutter (15863): [INFO] 2024-04-23 07:49:07.557668: Get Entries from REST endpoint
I/flutter (15863): I AM CALLED
I/flutter (15863): [INFO] 2024-04-23 07:49:07.603329: Start sorting entries
I/flutter (15863): [INFO] 2024-04-23 07:49:07.603692: Notifiying listeners that entries list have changed
I/flutter (15863): [INFO] 2024-04-23 07:49:07.604326: Start sorting entries
I/flutter (15863): [INFO] 2024-04-23 07:49:07.604595: Notifiying listeners that entries list have changed
I/flutter (15863): I AM CALLED
I/flutter (15863): [INFO] 2024-04-23 07:49:07.614896: Get Entries from REST endpoint
I/flutter (15863): I AM CALLED
I/flutter (15863): [INFO] 2024-04-23 07:49:07.646553: Get Entries from REST endpoint
I/flutter (15863): I AM CALLED
I/flutter (15863): [INFO] 2024-04-23 07:49:07.689545: Get Entries from REST endpoint
I/flutter (15863): I AM CALLED
I/flutter (15863): [INFO] 2024-04-23 07:49:07.726604: Get Entries from REST endpoint
I/flutter (15863): I AM CALLED
I/flutter (15863): [INFO] 2024-04-23 07:49:07.761659: Get Entries from REST endpoint
I/flutter (15863): [INFO] 2024-04-23 07:49:07.814218: Start sorting entries
I/flutter (15863): [INFO] 2024-04-23 07:49:07.814865: Notifiying listeners that entries list have changed
I/flutter (15863): [INFO] 2024-04-23 07:49:07.855985: Start sorting entries
I/flutter (15863): [INFO] 2024-04-23 07:49:07.856984: Notifiying listeners that entries list have changed
I/flutter (15863): [INFO] 2024-04-23 07:49:07.869618: Start sorting entries
I/flutter (15863): [INFO] 2024-04-23 07:49:07.871666: Notifiying listeners that entries list have changed
I/flutter (15863): [INFO] 2024-04-23 07:49:07.886194: Start sorting entries
I/flutter (15863): [INFO] 2024-04-23 07:49:07.886997: Notifiying listeners that entries list have changed
I/flutter (15863): [INFO] 2024-04-23 07:49:07.909218: Start sorting entries
I/flutter (15863): [INFO] 2024-04-23 07:49:07.910125: Notifiying listeners that entries list have changed
I/flutter (15863): [INFO] 2024-04-23 07:49:07.939734: Start sorting entries
I/flutter (15863): [INFO] 2024-04-23 07:49:07.940123: Notifiying listeners that entrties list have changed
I/flutter (15863): [INFO] 2024-04-23 07:49:07.941196: Start sorting entries
I/flutter (15863): [INFO] 2024-04-23 07:49:07.941953: Notifiying listeners that entries list have changed
I/flutter (15863): [INFO] 2024-04-23 07:49:07.955544: Start sorting entries
I/flutter (15863): [INFO] 2024-04-23 07:49:07.956654: Notifiying listeners that entries list have changed
D/EGL_emulation(15863): app_time_stats: avg=32.01ms min=3.19ms max=282.24ms count=25
D/EGL_emulation(15863): app_time_stats: avg=493.36ms min=483.87ms max=500.10ms count=3
D/EGL_emulation(15863): app_time_stats: avg=510.94ms min=501.94ms max=519.93ms count=2
D/EGL_emulation(15863): app_time_stats: avg=497.79ms min=492.59ms max=500.79ms count=3
D/EGL_emulation(15863): app_time_stats: avg=504.19ms min=500.70ms max=507.68ms count=2
D/EGL_emulation(15863): app_time_stats: avg=496.64ms min=492.22ms max=501.38ms count=3
D/EGL_emulation(15863): app_time_stats: avg=500.09ms min=482.98ms max=515.92ms count=3
D/EGL_emulation(15863): app_time_stats: avg=500.91ms min=484.92ms max=516.91ms count=2

Similar behaviour can be observed when defocusing the Dialog.It does not happen if I click on a Button in the Dialog or when I am inputing data into the TextField. Only when focusing/defocusing the TextField.


Solution

  • In case that someone is facing a simillar issue: In my case I had to remove the return ResponsiveBuilder(...) in my HomeMobileView. I am not 100% sure why it is responsible to rebuild the whole UI everytime (even if only the KeyBoard is displayed).