Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

onInitialBuild called multiple time on child container when parent container changes #59

Closed
AlexandreRoba opened this issue Jun 20, 2018 · 12 comments

Comments

@AlexandreRoba
Copy link

Hi all, I have a question regarding container that have other container. Basically I have a container that can display loading status and error message. It is a sort of "shell" container. On this container I have a child container with its own viewmodel and nothing shared. I can see the onInitialBuild method on the child container is called every time the mainshell onInitialBuild is changed. Is this an expected behavior?

@brianegan
Copy link
Owner

Sounds like a bug! Hrm, I'm curious as to why this could be happening... I'll have to take a closer look and see what's causing the double-callback.

Thanks for reporting :)

@AlexandreRoba
Copy link
Author

Thanks YOU for building all this :)

@brianegan
Copy link
Owner

brianegan commented Jun 24, 2018

Hey there -- do you happen to have a code sample that exhibits this behavior? I've written some tests and tried a few ways myself, but the onInitialBuild callback is only called once, even if a parent StatefulWidget or StoreConnector triggers a rebuild.

Are you perhaps creating a new StoreBuilder / StoreConnector with a Unique key on each build? Thanks for any help reproducing this issue!

@AlexandreRoba
Copy link
Author

Hey Brian, Thanks for spending time on this. I cannot share the code as I'm not the legal owner of it but I can describe more precisely the problem. I have a main_shell.dart that is a container. It looks like that:

class MainShell extends StatelessWidget {
  final Widget page;

  MainShell({@required this.page})
      : super(key: DemoKeys.mainShell(page.key));

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(children: <Widget>[
        this.page,  ///<- This is one of the widget for which the InitialBuild is called multiple time. Those are actually other containers...
        StoreConnector<AppState, _ViewModel>(
          distinct: true,
          converter: _ViewModel.fromStore,
          onWillChange: (vm) {
            if (vm.loginRequested) Navigator.pushNamed(context, 'login');
          },
          builder: (BuildContext context, vm) {
            if (vm.lastMessageKey != "") {
              Timer(Duration(milliseconds: 1), () {
                final snackBar = SnackMessage(
                    message: SweetNestLocalizations
                        .of(context)
                        .localize(vm.lastMessageKey),
                    type:vm.lastMessageType);
                Scaffold.of(context).showSnackBar(snackBar);
                vm.onErrorDisplayed();
              });
            }
            return _loading(context, vm.isLoading);//<- This display a transparent overlay on top of the 'page' widget...
          },
        ),
      ])
    );
  }

  Widget _loading(BuildContext context, bool isLoading) {
    if (isLoading)
      return Container(
        color: Color(0x55000000),
        child:Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
          new Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              CircularProgressIndicator(),
            ],
          )
        ],)
      );

    return Container();
  }
}

As you can see this mainshell has its own view model whit a isLoading flag. The layer is displayed when isloading is true...

Then you have a page widget which is also a container. The isloading flag is only part of the main_shell view model. Meaning the page diget should not trigger an initial build as they are on the screen and ti is only the isLoading overlay that is drawn on top of them... this is done using a the stack on the main_shell.

Then on the 'Page' widget I perform and action and on a middleware I do something like:

Middleware<AppState> _propertyHandler(
    AuthenticationRepository authRepo,
    LocationRepository locationRepo,
    UserRepository userRepository,
    PropertyRepository propertyRepository) {
  return (Store<AppState> store, action, NextDispatcher next) async {
    //We get the current user information
    //var user = await authRepo.getSignedInUser();
    //TODO:Region selection should we removed...
    if (action is HomeSearchPropertiesQuery) {
      store.dispatch(SetIsLoading(true)); //<-Trigger a change on the isLoading of the shell
      propertyRepository.searchProperty('FR', action.query).then((properties) {
        store.dispatch(
            HomeSearchPropertiesQueryCompleted(action.query, properties));//<-//update the view model of the 'page' widget
        store.dispatch(SetIsLoading(false));//<-Change back the isLoading flag
      });
    } else if (action is PropertyDetailsGetProperty) {
      //store.dispatch(SetIsLoading(true));
      propertyRepository.getProperty('FR', action.propertyId).then((property) {
        store.dispatch(PropertyDetailsGetPropertyCompleted(property));
        //store.dispatch(SetIsLoading(false));
      });
    }
  };

Then on a page widget I have added the following code on the store connector:

class HomeTabSearch extends StatelessWidget {
  HomeTabSearch() : super(key: SweetNestKeys.homeTab(HomeTab.Search));

  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, _ViewModel>(
        distinct: true,
        onInitialBuild: (vm)=>print('Initial Build Home Search'),
        onWillChange: (vm)=>print('on will change'),
        onDidChange: (vm)=>print('On did change'),
        converter: _ViewModel.fromStore,
        builder: (context, vm) {
          Timer(Duration(milliseconds: 600), () {
            if (!vm.firstQueryExecuted) vm.onQuery();
          });
          if (vm.viewMode == HomeSearchViewMode.Map) {
            return PropertiesMap(
              query: vm.query,
              center: vm.mapCenter,
              zoom: vm.mapZoom,
              properties: vm.properties,
              selectedPropertyId: vm.selectedPropertyId,
              previousSelectedPropertyId: vm.previousSelectedPropertyId,
              onPropertySelected: vm.onPropertySelected,
              onViewChanged: vm.onViewChanged,
              onViewModeChanged: vm.onViewModeChanged,
              onPropertyBlocked: vm.onPropertyBlocked,
              onPropertyFavorited: vm.onPropertyFavorited,
              onPropertyUnfavorited: vm.onPropertyUnfavorited,
              favoriteProperties: vm.favoritePropertyIds,
              queryShouldBeRecomputed: vm.queryShouldBeRecomputed,
              onRegionChangeRequested: vm.onRegionChangeRequested,
              onPropertyDetailsViewRequested: vm.onPropertyDetailsViewRequested,
              onSearchFilterClicked: vm.onSearchFilterClicked,
              onApplyQueryChange: vm.onApplyQueryChange,
              canBeQueried: vm.canBeQueried,
              chooseNewRegion: vm.chooseNewRegion,
              newQueryRegionType: vm.newQueryRegionType,
              onchangeQueryRegionType: vm.onChangeQueryRegionType,
              handSelectionComplete: vm.handSelectionComplete,
              onHandSelectionComplete: vm.onHandSelectionComplete,
              onAddHandSelectionPoint: vm.onAddHandSelectionPoint,
              handSelectionPoints: vm.handSelectionPoints,
              queryCanBeSaved: vm.queryCanBeSaved,
              onSaveQuery: vm.onSaveQuery,
              isCenterByCode: vm.isCenterByCode,
              onCenterByCodeSet: vm.onCenterByCodeSet,
            );
          } else {
            return PropertiesList(
              properties: vm.properties,
              selectedPropertyId: vm.selectedPropertyId,
              favoriteProperties: vm.favoritePropertyIds,
              onPropertySelected: vm.onPropertySelected,
              onPropertyFavorited: vm.onPropertyFavorited,
              onPropertyUnfavorited: vm.onPropertyUnfavorited,
              onPropertyBlocked: vm.onPropertyBlocked,
              onViewModeChanged: vm.onViewModeChanged,
              onPropertyDetailsViewRequested: vm.onPropertyDetailsViewRequested,
              onSearchFilterClicked: vm.onSearchFilterClicked,
            );
          }
        });
  }
}

You can see there is no isLoading on the viewmodel

This is the log I have

flutter: [INFO] LoggingMiddleware: {Action: InitializationCompleted{uid: <> //<-This action display the widget so it is normal that an initial build is called
flutter: Initial Build Home Search //<-Expected
flutter: [INFO] LoggingMiddleware: {Action: SetIsLoading{isLoading:true}, <>//<- This update the loading overlay it should not trigger an initial build of the page again... :(
flutter: [INFO] LoggingMiddleware: {Action: HomeSearchPropertiesQuery{query:Query{id:, <>
flutter: Initial Build Home Search //<- This should not be there.... :(
flutter: [INFO] LoggingMiddleware: {Action: HomeSearchPropertiesQueryCompleted{properties:<> //<- This change the viewmodel of page widget
flutter: [INFO] LoggingMiddleware: {Action: SetIsLoading{isLoading:false}, State: <>
flutter: on will change //<- This is expected as the viewmodel of the page has changed....
flutter: On did change

If I comment out the SetLoading action dispatch on the middleware then I end up with a log that looks like:

flutter: [INFO] LoggingMiddleware: {Action: InitializationCompleted{uid: <>
flutter: Initial Build Home Search
flutter: [INFO] LoggingMiddleware: {Action: HomeSearchPropertiesQuery{query:<>
flutter: [INFO] LoggingMiddleware: {Action: HomeSearchPropertiesQueryCompleted{<>
flutter: on will change
flutter: On did change

This is from my understanding the kind of behavior I should expect... Does the fact that something else is rendered on top of the page pigdet (overlay) causing an initial Build? This is where my expertize reach is limitation :)

Let me know If I can help you more.

Alex.

@brianegan
Copy link
Owner

Hrm, this is interesting -- since the StoreConnector in MainShell does not wrap this.page, when the ViewModel changes it should not cause the Stack with this.page to be rebuilt, only the code inside the builder function.

Could you check if onInit and onDispose on HomeTabSearch are also called multiple times? If so, this indicates that the this.page Widget's corresponding State class is somehow being initialized then disposed over and over. Generally, that will only happen when the StatefulWidget it's associated with is removed from the tree and added again, or if the key associated with this.page changes on rebuild. Do you know if either of these are a possibility?

@AlexandreRoba
Copy link
Author

AlexandreRoba commented Jun 24, 2018 via email

@brianegan
Copy link
Owner

brianegan commented Jun 25, 2018

No worries! Whenever ya get a chance :)

@brianegan
Copy link
Owner

Hey hey :) Any update on this one @AlexandreRoba?

@AlexandreRoba
Copy link
Author

AlexandreRoba commented Jul 4, 2018 via email

@brianegan
Copy link
Owner

brianegan commented Jul 4, 2018

Oh dang, sorry to interrupt! No problem at all, please take your time and enjoy the vacation :)

@AlexandreRoba
Copy link
Author

Hi Brian, I've refactored significantly the app since and can seems to reproduce this. :(
I guess it must be something I've done wrong somewhere... Sorry for this. I suggest to cloase this one and If I face this again I will reopen it with a working exemple. Thanks for your help. cheers.

@brianegan
Copy link
Owner

Sounds good, will do! Definitely let me know if ya need more help :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants