MichaelMarner / dart-redux-remote-devtools

Remote Devtools for Dart & Flutter
https://pub.dartlang.org/packages/redux_remote_devtools
MIT License
52 stars 10 forks source link

Dispatched action are visible but state is undefined #13

Closed sahilmob closed 5 years ago

sahilmob commented 5 years ago

I can see the dispatched action, but state is always undignified.

I connect to the store in the main fuction:

var remoteDevtools = RemoteDevToolsMiddleware('192.168.0.105:8000');
  await remoteDevtools.connect();
  final store = DevToolsStore<AppState>(appStateReducer,
      initialState: AppState.initialState(),
      middleware: [remoteDevtools, appStateMiddleWare]);
  remoteDevtools.store = store;

and my reducer looks like

AppState appStateReducer(AppState state, action) {
  return AppState(
    restaurant: restaurantReducer(state.restaurant, action),
    categories: categoriesReducer(state.categories, action),
    products: productsReducer(state.products, action),
    locale: localeReducer(state.locale, action),
    cart: cartReducer(state.cart, action),
    subTotal: subTotalReducer(state, action),
    loading: loadingReducer(state.loading, action),
  );
}

and the reducer is divided into several TypedReducers e.g:

Map<int, CartItem> increaseCartItemCount(Map<int, CartItem> cart, int id) {
  Map<int, CartItem> newCart = {}..addAll(cart);
  CartItem newCartItem = cart[id].copyWith(count: cart[id].count + 1);
  newCart[id] = newCartItem;
  return newCart;
}

Map<int, CartItem> decreaseCartItemCount(Map<int, CartItem> cart, int id) {
  Map<int, CartItem> newCart = {}..addAll(cart);
  CartItem newCartItem = cart[id].copyWith(count: cart[id].count - 1);
  newCart[id] = newCartItem;
  if (newCart[id].count <= 0) newCart.remove(id);
  return newCart;
}

Map<int, CartItem> addProductToCartReducer(
    Map<int, CartItem> prevCart, action) {
  Map<int, CartItem> cart = prevCart;
  if (cart.containsKey(action.product.id)) {
    cart = increaseCartItemCount(cart, action.product.id);
  } else {
    Map<int, CartItem> newCart = {}..addAll(cart);
    newCart[action.product.id] = CartItem(
        1,
        action.product.id,
        action.product.nameEn,
        action.product.nameAr,
        action.product.imageUrl,
        action.product.price,
        action.product.calories,
        action.product.caloriesUnit);
    cart = newCart;
  }
  return cart;
}

Map<int, CartItem> increaseItemCountReducer(
    Map<int, CartItem> prevCart, action) {
  Map<int, CartItem> cart = prevCart;
  return increaseCartItemCount(cart, action.productId);
}

Map<int, CartItem> decreaseItemCountReducer(
    Map<int, CartItem> prevCart, action) {
  Map<int, CartItem> cart = prevCart;
  return decreaseCartItemCount(cart, action.productId);
}

Reducer<Map<int, CartItem>> cartReducer = combineReducers<Map<int, CartItem>>([
  TypedReducer<Map<int, CartItem>, AddProductToCartAction>(
      addProductToCartReducer),
  TypedReducer<Map<int, CartItem>, IncreaseItemCountAction>(
      increaseItemCountReducer),
  TypedReducer<Map<int, CartItem>, DecreaseItemCountAction>(
      decreaseItemCountReducer),
]);

and I'm using

Dart VM version: 2.2.0 (Tue Feb 26 15:04:32 2019 +0100) on "windows_x64"

and fluttrer

Flutter 1.2.1 • channel stable • https://github.com/flutter/flutter.git Framework • revision 8661d8aecd (6 weeks ago) • 2019-02-14 19:19:53 -0800 Engine • revision 3757390fa4 Tools • Dart 2.1.2 (build 2.1.2-dev.0.0 0a7dcf17eb)

MichaelMarner commented 5 years ago

AppState needs to be json encodable for the state to be visible in devtools, which means it needs a toJson method. Can you share the class definition for AppState?

sahilmob commented 5 years ago

Almost working,

import './restaurant.dart';
import './category.dart';
import './product.dart';
import './cart_item.dart';
import 'package:flutter/foundation.dart';

class AppState {
  final Restaurant restaurant;
  final List<ProductsCategory> categories;
  final Map<int, List<Product>> products;
  final Map<int, CartItem> cart;
  final bool loading;
  final String error;
  final String locale;
  final double subTotal;

  AppState(
      {@required this.restaurant,
      this.locale,
      this.categories,
      this.products,
      this.cart,
      this.subTotal,
      this.loading,
      this.error});

  AppState copyWith(
      {Restaurant restaurant,
      String locale,
      List<ProductsCategory> categories,
      Map<int, List<Product>> products,
      Map<int, CartItem> cart,
      double subTotal,
      bool loading,
      String error}) {
    return AppState(
        restaurant: restaurant ?? this.restaurant,
        locale: locale ?? this.locale,
        categories: categories ?? this.categories,
        products: products ?? this.products,
        cart: cart ?? this.cart,
        subTotal: subTotal ?? this.subTotal,
        loading: loading ?? this.loading,
        error: error ?? this.error);
  }

  Map toJson() {
    return {
      "restaurant": restaurant,
      "locale": locale,
      "categories": categories,
      "products": products,
      "cart": cart,
      "subTotal": subTotal,
      "loading": loading,
      "error": error
    };
  }

  @override
  String toString() {
    return toJson().toString();
  }

  AppState.initialState()
      : restaurant = null,
        categories = null,
        products = {},
        cart = {},
        subTotal = 0,
        loading = false,
        error = null,
        locale = null;
}

I can see the state until I start storing products which has the following class definition:

import "package:flutter/foundation.dart";

class Product {
  final int id;
  final String nameEn;
  final String nameAr;
  final String imageUrl;
  final String calories;
  final String caloriesUnit;
  final double price;

  Product(
      {@required this.id,
      @required this.nameEn,
      @required this.nameAr,
      this.calories,
      this.caloriesUnit,
      this.price,
      this.imageUrl});

  Product copyWith(
      {int id,
      String nameEn,
      String nameAr,
      String imageUrl,
      String calories,
      String caloriesUnit,
      double price}) {
    return Product(
        id: id ?? this.id,
        nameEn: nameEn ?? this.nameEn,
        nameAr: nameAr ?? this.nameAr,
        imageUrl: imageUrl ?? this.imageUrl,
        calories: calories ?? this.calories,
        caloriesUnit: caloriesUnit ?? this.caloriesUnit,
        price: price ?? this.price);
  }

  factory Product.fromJson(Map productData) {
    String calories;
    String caloriesUnit;
    (productData['meta_data'] as List).forEach((m) {
      if (m['key'] == 'wccaf_cal') {
        calories = m['value'];
      }
    });

    if (calories != null && calories.isNotEmpty) {
      (productData['meta_data'] as List).forEach((m) {
        if (m['key'] == 'wccaf_per') {
          caloriesUnit = m['value'];
        }
      });
    }
    return Product(
        id: productData['id'],
        nameAr: productData['name'],
        nameEn:
            productData['short_description'].replaceAll(RegExp("<[^>]*>"), ""),
        calories: calories,
        caloriesUnit: caloriesUnit,
        price: double.parse(productData['price']),
        imageUrl:
            productData['images'] != null && productData['images'][0] != null
                ? productData['images'][0]['src']
                : null);
  }

  Map toJson() {
    return {
      "id": id,
      "nameEn": nameEn,
      "nameAr": nameAr,
      "imageUrl": imageUrl,
      "calories": calories,
      "caloriesUnit": caloriesUnit,
      "price": price
    };
  }

  @override
  String toString() {
    return toJson().toString();
  }
}

After instantiating the product class, state becomes undefined.

MichaelMarner commented 5 years ago

You will need to test whether jsonEncode(product) works as expected. One thing I have noticed is Dart is fairly fussy with types - you may need to explicitly define them:

Map<int, dynamic> toJson() {
  ...
}

Unfortunately jsonEncode doesn't give you much information when the encoding fails, so you'll need to add unit tests to make sure each component of your state can be encoded.

sahilmob commented 5 years ago

Somehow the products map in the app state is causing issue

I tried to covert the products in the app state (in toJson method) to string

  Map<String, dynamic> toJson() {
    return {
      "restaurant": restaurant,
      "locale": locale,
      "categories": categories,
      "products": products.toString(),
      "cart": cart,
      "subTotal": subTotal,
      "loading": loading,
      "error": error
    };
  }

after that, I cant see the state, however, the products list is treated as a string.

MichaelMarner commented 5 years ago

I highly recommend adding unit tests for your state classes. For example:

import 'dart:convert';

import 'package:json_test/app_state.dart';
import 'package:json_test/product.dart';
import 'package:json_test/restaurant.dart';
import 'package:test/test.dart';

void main() {
  group(AppState, () {
    test('initial state', () {
      Restaurant restaurant = Restaurant(name: 'Tuno');
      AppState state = AppState.initialState();

      // encode the object to a string, then decode back into a map
      final String encoded = jsonEncode(state);
      final Map<String, dynamic> result = jsonDecode(encoded);
      expect(result, isNotNull);
      expect(result, TypeMatcher<Map<String, dynamic>>());
    });
    test('works with some products', () {
      Restaurant restaurant = Restaurant(name: 'Tuno');
      Map<int, List<Product>> products = {
        12: [Product(id: 1234), Product(id: 4321)]
      };
      AppState state = AppState.initialState().copyWith(products: products);

      // encode the object to a string, then decode back into a map
      final String encoded = jsonEncode(state);
      final Map<String, dynamic> result = jsonDecode(encoded);
      expect(result, isNotNull);
      expect(result, TypeMatcher<Map<String, dynamic>>());
    });
  });
}

If you run this, you will see that the Map<int, Product> cannot be encoded.

Converting object to an encodable object failed: Instance of 'AppState'
dart:convert                    jsonEncode
test/app_state_test.dart 28:30  main.<fn>.<fn>

However, this is intended behaviour. jsonEncode can only convert maps that use a String as the key (Map<String, Product>);

If you change your AppState to use Strings for keys, then the encoding works. Unfortunately we don't get very nice error messages from the jsonEncoder, which is why it is so important to unit test the building blocks of your state.

Related issues:

https://github.com/dart-lang/sdk/issues/23150 https://github.com/dart-lang/sdk/issues/32476