Search code examples
flutterdartnested-lists

Why does Flutter/Dart List.from() work while .toList() doesn't


I spent a day and half racking my brain trying to figure out how to create a chained selector for my redux store to get IDs from a set of nested objects. In the docs and all the examples I've read, I know that Iterable.map() returns an Iterable<T> and requires .toList() if you want to treat it as a list. Eg. as a return type List<int>.

What I can't figure out is why my selectors below function while the other does not. The only thing I can come up with is that it's a nested list that I'm trying to flatten. I've seen dozens of examples with the same error where JSON was used in a map and the answer was to use .toList().

The only reason I care is because I've work with very large very complex redux stores which required multiple levels of chaining - usually split into sub-selectors.

//models
@JsonSerializable()
class Schedule {
  /* the data returned from the API is a little confusing.  Each scheduled item
   * has a primary and secondary task, so each team is usually listed twice for each
   * task.  Once the JSON is consumed and converted into models, I try to get all
   * of the items task IDs so I can get more information about those tasks.
   * Having come from a JSX/JavaScript background learning redux, I'm familiar
   * with that style of chaining.  I've tried to find information about the 
   * "cascade" ( .. ) operator, but information is hard to come by aside form a 
   * brief description.  And didn't make a difference when I tried to use it.
   * The reason each team is listed twice for a schedule under primaryTask and
   * secondaryTask is a schedule items may have 2 teams.  99.999% of the time 
   * it's a single team, however, the odd time a second team will be configured 
   * as only a subset of the team will be assigned to the secondary task.  For
   * the purpose of the "get all task IDs" selector, which team that task is
   * assigned to doesn't matter whatsoever.
   * Example API JSON response:
[
  schedule: {
    teams:[ {
      primaryTask:{
        teamName:String, 
        taskId:Integer
      },//primary task
      secondaryTask:{
        teamName:String, 
        taskId:Integer
      }//secondary task
    } //team
,... ]//teams
  }//schedule
]
   */
  //the map index is either "primaryTask" or "secondaryTask".
  //each "Schedule" item is for a single team.
  final Map<String, Team> teams;

  Schedule({
    required this.teams,
  });

  factory Schedule.fromJson(Map<String, dynamic> json) => _$ScheduleFromJson(json);

  Map<String, dynamic> toJson() => _$ScheduleToJson(this);
}

@JsonSerializable()
class Team {
  final int id;
  final String name;
  
  Team({
    required this.id,
    required this.name,
  });

  factory Team.fromJson(Map<String, dynamic> json) => _$TeamFromJson(json);

  Map<String, dynamic> toJson() => _$TeamToJson(this);
}

//redux selector that works
List<int> getAllScheduledTeamsTaskIds( store ){
  return List<int>.from( 
    List<List<int>>.from( 
      store.state.schedule
      .map( ( schedule ) => [ 
        schedule.teams['primaryTask'].id as int, 
        schedule.teams['secondaryTask'].id as int ]
      )
    )
    .expand((i)=>i)
  );
}

//The first selector I got to work but I didn't like it.
List<int> getAllScheduledTeamsTaskIdsMergingTwoLists( store ){
  // merge two lists composed of primaryTask IDs and secondaryTask IDs
  return store.state.schedule
  .map( ( schedule ) => schedule.teams['primaryTask'].id as int )
  .toList()
  + 
  store.state.schedule
  .map( ( schedule ) => schedule.teams['secondaryTask'].id as int )
  .toList()
}

  //redux selector that doesn't work
List<int> getAllScheduledTeamsTaskIdsError( store ){
  return store.state.schedule
  .map( ( schedule ) => [ 
    schedule.teams['primaryTask'].id as int, 
    schedule.teams['secondaryTask'].id as int ]
  )
  .toList()
  .expand((ident)=>ident)
  .toList();
  //error message: type '(dynamic) => (dynamic)' is not a subtype of (dynamic) => Iterable(dynamic)
}

I've been reading docs and examples and source on GitHub and the behaviour I'm experiencing isn't expected based on what I've read. However, I can't find an example exactly like mine. I'm not sure what I'm missing but I'm sure there's some nuance as to why toList() isn't working while List.from() is. I've tried cast(), reduce(), and any method I could find in the docs for Iterable<T> that might give me a single List<int> from a list of nested objects which I could get into the form of List<List<int>>.


Solution

  • Apparently calling the expand method on a dynamic value causes the error. The result of store.state.schedule.map(...).toList() is a dynamic (try assigning it to a variable). To prevent the error you can cast store.state.schedule to an Iterable.

    List<int> getAllScheduledTeamsTaskIdsError( store ){
      return (store.state.schedule as Iterable)
      .map( ( schedule ) => [ 
        schedule.teams['primaryTask'].id as int, 
        schedule.teams['secondaryTask'].id as int ]
      )
      .expand((ident)=>ident)
      .toList();
    }
    

    Notice that I also removed the first toList() call since the expand method works on Iterable too.