This is why your flutter state management makes no sense – part 2

by easazade
9 minutes read

In part 1 of this series, we discussed how many of the problems we face in our flutter state management are actually from how we define our state And that these problems usually start when our state gets bigger and bigger. This section will discuss what issues will appear in our flutter state management when breaking a state into smaller ones.

Let’s continue where we left off

If you haven’t read part 1, make sure you go and read that part first.

In the last part, our example of a single page of an app evolved into something bigger. We started with a simple page, then in the course of 3 iterations of changes, the page became more complex, and so did the state management unit that populated the page. Below you can see our page.

Above, we have our user profile screen. A screen where the user’s profile image is shown and can be changed. There are also two lists of posts and pictures. And each time a new tag is selected from the tag section, posts, and images relevant to the selected tag will be loaded.

Below is the state class we use for our flutter state management unit (imagine Bloc, for example). It’s big and a mess.

class UserState {
  final UserProfile profile;
  final Tag selectedTag; // new property
  final List<Post> posts;
  final List<String> images; // new property
  final bool isLoading;
  final bool isLoadingAvatar;
  final bool isLoadingPosts; // new property
  final bool isLoadingImages; // new property
  final Error error;
  final Error postsError; // new property
  final Error imagesError; // new property

  UserState(...);
}

// tags shown in screen are read from profile.tags 

Separating flutter state management units into multiple ones

Ok, we decided to fix the mess we made above, And one of the most common ways is to separate the state into smaller ones. which also means separating our logic into smaller units. If we are using Bloc, then we will have multiple blocs. Let’s see how it will turn out.

class UserState {
  final UserProfile profile;
  final Tag selectedTag;
  final bool isLoading;
  final bool isLoadingAvatar;
  final Error error;


  UserState(...);
}

class UserPostsState{
  final List<Post> posts;
  final bool isLoading;
  final Error error;

  UserPostsState(...);
}

class UserImagesState{
  final List<String> images;
  final bool isLoading;
  final Error error;

  UserImagesState(...);
}

Was that a solution or just a new problem?

Was separating states and the logic that manipulates and manages that state into multiple classes fixed our problem? Or did we trade our problems for new ones? If you look at the code above, we fixed our problem. The problem was that we had a state object that, when someone would have looked at it, would have made no sense. It was hardly readable and understandable. But at what cost? Let’s see what problems we might have caused by separating our state like this.

Did it make sense to separate it logic-wise?

Ok, We separated the Bloc into multiple blocs because our state was big and unreadable. Ok, we solved the problem of readability. But if we look at our Bloc from the business logic perspective, did it make sense to break this Bloc into multiple blocs? In some cases, it might make sense, and in others, it doesn’t make sense to separate the state and the logic that manipulates that state into multiple units. This will introduce more complexity and can even make the code more unreadable in some other parts, like the UI or our test code. In that case, our first attempt to improve our blocs’ readability has decreased our code’s readability in other places.

Logic code is a mess

We have separated our state into three Blocs, UserBloc, UserPostsBloc, and UserImagesBloc. Even though our states are now separated, the logic that manages them still depends on other states. In our case, we need to know what tag is selected in UserBloc to fetch relevant posts and images from the backend server and update the UserPostsState and UserImagesState. So that makes our blocs tightly coupled.

We also need to write extra codes in our blocs so they can listen to each other. And if the dependency graph between them is complex and there is no way we can make it simpler, the communication between them will also be complex.

Wait, didn’t separating the state supposed to solve this mess? How did we get into a more considerable lot?!!!

Test code is a mess too

We have made three blocs out of one Bloc. If our entire app was going to be just one Bloc turned into three, we could have gotten away with it. But what if it was ten blocs separated into thirty blocs? Without a doubt, we need to write more tests. But it doesn’t end there. Since many of our blocs will depend on each other, we will want to have some tests that test them against each other. Testing how they react against each other. This second issue will result not only in more tests but also in more complex tests.

Also, tests are meant to be readable. If your colleague is reviewing your code and reading the tests you wrote to see how things are supposed to work, or a new developer joins the team, They will have difficulty reading those tests. Also, it will take more of their Time. And let’s mention that this would add to all the excuses developers use not to write tests.

Remember, we said the point we are referring to is that poorly defined states will waste development time. Well, Time being wasted will not be our only problem. Because when we change our states too often and too much, our tests will brake too often. And that will also cost development time, of course. But when tests brake too much and too often, At some point, developers will stop writing tests.

Our UI code is a mess too !!!?

The UI code needs to be updated as well. Because We need to build our UserProfilePage from three blocs instead of one, our UserProfilePage code will probably look like this.

class UserProfilePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        children:[
          BlocBuilder<UserBloc, UserState>(
            builder: (context, state) => ProfileSection(...),
          ),
          BlocBuilder<UserPostsBloc, UserPostsState>(
            builder: (context, state) => PostsSection(...),
          ),
          BlocBuilder<UserImagesBloc, UserImagesState>(
            builder: (context, state) => ImagesSection(...),
          ),
        ],
      ),
    );
  }
}

In our example, the code will be as clean as above. It is clean. But could it have been a mess?

What if we needed to process data from all three states to get the data required to be shown on our screen? What would our code look like?

In the best-case scenario, that part of our code would have been like

// insde the build method of UserProfilePage
MultiBlocBuilder(
    blocs: [userBloc, userPostsBloc, userImagesBloc],
    builder: (context, states) {
        final userState = states.get<UserState>();
        final userPostsState = states.get<UserPostsState>();
        final userImagesState = states.get<UserImagesState>();
        
        final someData = _processData(userState, userPostsState, userImagesState);
        
        return SomeWidget(someData);
    }
),
.
.

In the worst-case scenario, that part of our code would have been like

// insde the build method of UserProfilePage
MultiBlocBuilder(
    blocs: [userBloc, userPostsBloc, userImagesBloc],
    builder: (context, states) {
        final userState = states.get<UserState>();
        final userPostsState = states.get<UserPostsState>();
        final userImagesState = states.get<UserImagesState>();
        
        if(userState.isDoingThis){
          return SomeWidget(
            child: Text('some text'),
          );
        }else if(userState.isDoingSomethingElse && userPostsState.isLoading){
          return SomeOtherWidget(
            child: Item(
              padding: const EdgeInsets.all(24),
              item: ...
              .
              .
            ),
          );
        }else if(userImagesState.hasError && userState.selectedTag != null){
          if(isThis){
            return ThisWidtet(
              padding: const EdgeInsets.all(24),
              item: ...
              .
              .
            );
            
          } else if(isThat){
            return ThatWidget(
              padding: const EdgeInsets.all(24),
              item: ...
              .
              .
            );
          } else{
            return SomethingDifferent(
              padding: const EdgeInsets.all(24),
              item: ...
              .
              .
            );
          }
        }
    }
),
.
.

Take a look at the above example. We have some complex logic code to process the data that must be shown on our page. This convoluted logic could have been encapsulated within our flutter state management if we had only one instead of three. This logic code leaks inside our UI simply because our states are defined poorly.

Move the logic out of the UI

One of the points of having a class (unit) to manage our state is to avoid writing logic code in our UI layer as much as possible. But in the above example, we are failing to do that. The solution to the above mess is simple. Move the logic code to another unit. We can move the code written above to one of our state management units that makes sense, but what if it doesn’t make sense to move the code to any of them ???

Ok, we know we don’t want our logic code mixed with our UI code, And we know that we can’t move them to any of the three blocs we defined above because it doesn’t make sense. So the only other options are either to leave the mess be or to create another bloc to process the data we need, and We build UserProfilePage from that Bloc. Let’s see the code for that Bloc.

class UserProfileBloc extends Cubit<UserProfile> {
  CounterCubit(
    this.userBloc,
    this.userPostsBloc,
    this.userImagesBloc,
  ) : super(UserProfile()) {
  
    [userBloc, userPostsBloc, userImagesBloc].listenAll(state1, state2, state3) {
      // calcualte the new state and update state !!!
    }
  }
  
  final UserBloc userbloc;
  final UserPostsBloc userPostsBloc;
  final UserImagesBloc userImagesBloc;

}

Too many blocs in on page

We have removed our logic code from our UI code, But now we use four blocs in our page. If we could find a way to combine some of these blocs, it would have been much cleaner, hmm. Well, we just did that in UserProfileBloc. What if we could listen to our other three blocs in UserProfileBloc and process the state of those three blocs? Then we need UserPorifleBloc to build the UserProfilePage. Let’s update the state for UserProfileBloc first.

class UserProfileState {
  final UserProfile profile;
  final Tag selectedTag;
  final List<Post> posts;
  final List<String> images;
  final bool isLoading;
  final bool isLoadingAvatar;
  final bool isLoadingPosts;
  final bool isLoadingImages;
  final Error error;
  final Error postsError;
  final Error imagesError;

  UserProfileState(...);
}

Wait, What?!!! We are back at square one. You may think, Ok, there is no way I make this mistake. But many of us do. Because the codes we wrote above are not created in one day, It is developed over several iterations of changes during the development process.

Don’t miss the point

Separating the states is not always a wrong solution. The chances that you simultaneously hit all or none of the problems explained above are low. But when they appear in your project, they’re not going anywhere until you deal with them. Of course, if you put in enough time, you can figure out how to make it work and eliminate said problems while your logic code still makes sense. But should you? The point is you’re still wasting your time on something that shouldn’t require this much Time. Remember, in the software industry, wasting Time is wasting money.

Related Posts

Leave a Comment