brianegan / flutter_redux

A library that connects Widgets to a Redux Store
MIT License
1.65k stars 219 forks source link

StoreConnector doesn't support nullable ViewModel #209

Closed PiN73 closed 3 years ago

PiN73 commented 3 years ago
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';

class State {
  State({this.data});

  final int? data;
}

State reducer(State state, dynamic action) {
  return state;
}

void main() {
  final store = Store<State>(
    reducer,
    initialState: State(),
  );
  runApp(
    StoreProvider<State>(
      store: store,
      child: App(),
    ),
  );
}

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: StoreConnector<State, int?>(
            converter: (store) => store.state.data,
            builder: (context, int? data) {
              return Text(
                data != null ? '$data' : 'no data',
              );
            },
          ),
        ),
      ),
    );
  }
}
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following _CastError was thrown building StreamBuilder<int?>(dirty, state:
_StreamBuilderBaseState<int?, AsyncSnapshot<int?>>#aad39):
Null check operator used on a null value

The relevant error-causing widget was:
  StreamBuilder<int?>
  file:///Applications/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_redux-0.8.0/lib/flutter_redux.dart:510:11

When the exception was thrown, this was the stack:
#0      _StoreStreamListenerState.build.<anonymous closure> (package:flutter_redux/flutter_redux.dart:517:29)
#1      StreamBuilder.build (package:flutter/src/widgets/async.dart:545:81)
#2      _StreamBuilderBaseState.build (package:flutter/src/widgets/async.dart:124:48)
#3      StatefulElement.build (package:flutter/src/widgets/framework.dart:4612:27)
#4      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4495:15)
#5      StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4667:11)
#6      Element.rebuild (package:flutter/src/widgets/framework.dart:4189:5)
#7      ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4474:5)
#8      StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:4658:11)
#9      ComponentElement.mount (package:flutter/src/widgets/framework.dart:4469:5)
...     Normal element mounting (36 frames)
#45     Element.inflateWidget (package:flutter/src/widgets/framework.dart:3541:14)
#46     MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6094:32)
...     Normal element mounting (230 frames)
#276    Element.inflateWidget (package:flutter/src/widgets/framework.dart:3541:14)
#277    MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:6094:32)
...     Normal element mounting (321 frames)
#598    Element.inflateWidget (package:flutter/src/widgets/framework.dart:3541:14)
#599    Element.updateChild (package:flutter/src/widgets/framework.dart:3306:18)
#600    RenderObjectToWidgetElement._rebuild (package:flutter/src/widgets/binding.dart:1182:16)
#601    RenderObjectToWidgetElement.mount (package:flutter/src/widgets/binding.dart:1153:5)
#602    RenderObjectToWidgetAdapter.attachToRenderTree.<anonymous closure> (package:flutter/src/widgets/binding.dart:1095:18)
#603    BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2647:19)
#604    RenderObjectToWidgetAdapter.attachToRenderTree (package:flutter/src/widgets/binding.dart:1094:13)
#605    WidgetsBinding.attachRootWidget (package:flutter/src/widgets/binding.dart:934:7)
#606    WidgetsBinding.scheduleAttachRootWidget.<anonymous closure> (package:flutter/src/widgets/binding.dart:915:7)
(elided 11 frames from class _RawReceivePortImpl, class _Timer, dart:async, and dart:async-patch)

════════════════════════════════════════════════════════════════════════════════════════════════════
PiN73 commented 3 years ago

In this simplified example we can of course move int? to String conversion logic to converter

StoreConnector<State, String>(
  converter: (store) {
    final data = store.state.data;
    return data != null ? '$data' : 'no data';
  },
  builder: (context, String data) {
    return Text(data);
  },
)

and it will work.

However I think in general it is better to allow nullable ViewModel for greater flexibility or at least for backward compatibility.

brianegan commented 3 years ago

Thanks, good catch. I never use nullable ViewModels and was missing tests for this case. I've added tests so we can support this flow, fixed it up, and deployed a new version 0.8.1