How Do I Properly Use Equatable To Recognise Deeply Nested Data Using Flutter_bloc 8.0.0+

Viewed 45

Introduction: I have been working on this problem for a while. The problem involves updating the state of deeply nested data with the Bloc 8.0.0+ paradigm. I am using a class that holds deeply nested data in it (in this minimum viable code: a ‘Collection’ that contains a name and children are meant to be a Show Series, and the Series has children that are meant to be Seasons, and Seasons have children that are meant to be Episodes).

The nested structure is something like this:

List<CollectionState>
|-- List<CollectionState>
|   |-- List<CollectionState>
|   |   |-- List<CollectionState>

An important functionality of the code is that a child is added to the children list of the correct parent so it will display in the correct order of the hierarchy of parents to their children in the ListView; i.e. Collection has one Series (8768), and that Series has two Seasons (1817 and 7623), and when pressing on a Season, an Episode is added to its correct parent Season instead of being added to the bottom of the ListView. In this case, pressing on Season 1817 four times adds Episodes 2175, 2773, 5420 and 8826 under itself instead of adding to Season 7623.

enter image description here

Problem: As I understand it, a good practice while working with BLoC 8.0.0+ would be extending the CollectionState class with Equatable. The following code I provide works; however, it does not use this best practice. I want it to do so, but I am having problems which I will explain shortly. I have commented in the code of collection_state.dart with:

// 1) where Equatable should be extended  

I have located in the code where the issue occurs in collection_bloc.dart when the CollectionState class is extended with Equatable (please note that the problem happens only with changing the code by extending CollectionState class with Equatable, which the code does not do). I have commented in the code with this comment:

//TODO: Here is the problem. This code does not work properly when I extend the CollectionState class to Equatable.

Surprisingly, there is little information online that I could find about using deeply nested data with BLoC 8.0.0+.

I am new to BLoC 8.0.0+ and even newer to Equatable (I’ve always used Provider up to this point) and I don’t understand why my code is not updating correctly when extending Equatable. I guess I am having an immutability issue because the update to the class with the AddInfo bloc event is not considered different to Equatable. I am at a loss to understand how to change my code to use the best practices with deeply nested data with BLoC.

Question:

  1. How do I change my code to extend the CollectionState class with Equatable and still have it update my UI correctly?

  2. Bearing in mind that I have a cursory understanding of Equatable, I would like to know more about the underlying root of the problem. Is it the case that the bloc event method is not producing a class different enough to Equatable, so it is not updating or is something entirely different happening here?

Note: When I extend Equatable and add the props for name, children, showType; and click on the collection, it adds Series as normal. But, when I click on Series, the app does not update to show the addition of the Seasons and Episodes. However, when I press Hot Reload, the app is refreshed with all the correct items. I believe that it is not comparing the nested objects, and I don't know how to allow it to compare them with my code.

pub spec.yaml

dependencies:
  flutter:
    sdk: flutter

  flutter_bloc: ^8.1.1
  equatable: ^2.0.5

main.dart

import 'package:deeply_nested_objects/bloc/add_to_collection_logic.dart';
import 'package:deeply_nested_objects/bloc/collection_bloc.dart';
import 'package:deeply_nested_objects/bloc/collection_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => CollectionBloc(),
      child: const MaterialApp(
        home: MyHomePage(),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<CollectionBloc, CollectionState>(
      builder: (context, state) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Deeply nested data and Bloc 8.0.0+'),
          ),
          body: ListView.builder(
            itemCount: state.getAllNodes(state).length,
            itemBuilder: (context, index) {
              var nodes = state.getAllNodes(state)[index];
              Color textColor = getColor(nodes);
              double distance = getPaddingDistance(nodes);
              return Padding(
                padding: EdgeInsets.only(left: distance),
                child: ListTile(
                  onTap: () => addToCollectionLogic(nodes.showType, index,
                      nodes.children.length + 1, context),
                  leading: Card(
                    child: Text(nodes.name, style: TextStyle(color: textColor)),
                  ),
                ),
              );
            },
          ),
        );
      },
    );
  }

  double getPaddingDistance(CollectionState nodes) {
    switch (nodes.showType) {
      case ShowType.collection:
        return 0;
      case ShowType.series:
        return 20;
      case ShowType.season:
        return 40;
      case ShowType.episode:
        return 60;
    }
  }

  Color getColor(CollectionState nodes) {
    switch (nodes.showType) {
      case ShowType.collection:
        return Colors.black;
      case ShowType.series:
        return Colors.blue;
      case ShowType.season:
        return Colors.green;
      case ShowType.episode:
        return Colors.red;
    }
  }
}



 

add_collection_logic.dart

import 'package:deeply_nested_objects/bloc/collection_bloc.dart';
import 'package:deeply_nested_objects/bloc/collection_event.dart';
import 'package:deeply_nested_objects/bloc/collection_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void addToCollectionLogic(
    ShowType showType, int index, int count, BuildContext context) {
  void passToBloc(String name, ShowType showType) =>
      BlocProvider.of<CollectionBloc>(context).add(
        AddInfo(
          index: index,
          child: CollectionState(name: name, showType: showType, children: []),
        ),
      );

  switch (showType) {
    case ShowType.collection:
      passToBloc('Series $count', ShowType.series);
      break;
    case ShowType.series:
      passToBloc('Season $count', ShowType.season);
      break;
    case ShowType.season:
      passToBloc('Episode $count', ShowType.episode);
      break;
    case ShowType.episode:
      break;
  }
}

collection_event.dart

import 'package:deeply_nested_objects/bloc/collection_state.dart';
import 'package:equatable/equatable.dart';

abstract class CollectionEvents extends Equatable {
  @override
  List<Object> get props => [];
}

class AddInfo extends CollectionEvents {
  AddInfo({required this.index, required this.child});

  final int index;
  final CollectionState child;
}

collection_bloc.dart

import 'package:deeply_nested_objects/bloc/collection_event.dart';
import 'package:deeply_nested_objects/bloc/collection_state.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class CollectionBloc extends Bloc<CollectionEvents, CollectionState> {
  CollectionBloc() : super(CollectionState.initial()) {
    on<AddInfo>((event, emit) {
      if (event.child.showType == ShowType.series) {
        emit(state.copyWith(children: [...state.children, event.child]));
      }
      if (event.child.showType == ShowType.season ||
          event.child.showType == ShowType.episode) {
      //TODO: Here is the problem. This code does not work properly when I extend the CollectionState class to Equatable.
        // get the list of all nodes
        List<CollectionState> list = state.getAllNodes(state);
        // find the parent node while still in the list
        CollectionState parent = list[event.index];
        // add the child to the parent
        parent.children.add(event.child);
        // update the state
        emit(state.copyWith(children: [...state.children]));
      }
    });
  }
}

collection_state.dart

enum ShowType { collection, series, season, episode }

// 1) where should be Equatable
class CollectionState {
  const CollectionState({
    required this.name,
    required this.children,
    required this.showType,
  });
  final String name;
  final List<CollectionState> children;
  final ShowType showType;

  factory CollectionState.initial() {
    return const CollectionState(
      name: "Collection",
      showType: ShowType.collection,
      children: [],
    );
  }

  List<CollectionState> getAllNodes(CollectionState node) {
    // empty list to store the result
    List<CollectionState> result = [];
    // add the current node
    result.add(node);
    // add the children too
    for (CollectionState child in node.children) {
      // composite design pattern seek and find
      result.addAll(getAllNodes(child));
    }
    return result;
  }

  CollectionState copyWith({
    String? name,
    List<CollectionState>? children,
    ShowType? showType,
  }) {
    return CollectionState(
      name: name ?? this.name,
      children: children ?? this.children,
      showType: showType ?? this.showType,
    );
  }
}
1 Answers

You have to assign properties in equatable that you want to check equality on.

Example:

class SomeClass extends Equatable {
  SomeClass({required this.index});

  final int index;

  @override
  List<Object> get props => [];
}

If I would check some SomeClass(index: 10) == SomeClass(index: 9) it would be true because I didn't say equatable what properties it should look for on == operator

If I update my code to

class SomeClass extends Equatable {
  SomeClass({required this.index});

  final int index;

  @override
  List<Object> get props => [index];
}

Now same check would be false, cause it is looking on index property

Related