marcglasberg / async_redux

Flutter Package: A Redux version tailored for Flutter, which is easy to learn, to use, to test, and has no boilerplate. Allows for both sync and async reducers.
Other
234 stars 40 forks source link

Precisando esperar para a Store atualizar #81

Closed LeandroMoura3 closed 4 years ago

LeandroMoura3 commented 4 years ago

Olá,

Eu estive enfrentando o seguinte problema: eu chamo uma ação assíncrona, esperando sua finalização, em um dos viewmodels. O esperado seria que, assim que ela terminasse, a store do viewmodel fosse atualizada. Porém, para que isto aconteça, eu tive que colocar uma espera de alguns segundos. Estou achando este comportamento estranho. Em códigos, está acontecendo o seguinte:

class NavBarSync extends BaseModel<MyAppState> {
  NavBarSync();

  void Function(BuildContext) selectPage;
  ...
  GpsStatus gpsStatus;

  NavBarSync.build({
    ...
    @required this.nearClub,
  }) : super(equals: [nearClub]);

  @override
  NavBarSync fromStore() => NavBarSync.build(
    nearClub: state.user?.nearClub,
    selectPage: (BuildContext context) async {
         if (state.logged){
                 await dispatchFuture(UpdateNearClubAction());
                 await Future.delayed(Duration(seconds: 2));
                 ...outros afazeres
            }
   }
}

Se eu ponho esta espera de dois segundos, funciona como esperado. Se não, é como se a store desta viewmodel (NavBarSync) não fosse atualizada.

Aqui está a action:

class UpdateNearClubAction extends ReduxAction<MyAppState> {
  @override
  Future<MyAppState> reduce() async {
    Club nearClub;
    try {
      nearClub = await state.api.userInStore(state.user.memberNumber);
    } catch (e) {
      if (e.runtimeType == InternetException) {
        return state.copyWith(gpsStatus: GpsStatus.internet_error);
      } else if (e.runtimeType == GpsException) {
        return state.copyWith(gpsStatus: GpsStatus.gps_error);
      }
    }
    return state.copyWith(
      user: state.user.copyWith(
        nearClub: nearClub,
      ),
      gpsStatus: GpsStatus.ok,
    );
  }
}

Estou escrevendo a issue em português, posso passá-la para o inglês (embora ele seja péssimo, hehehe) caso tenha entendido algo errado.

marcglasberg commented 4 years ago

Você colocou nearClub no equals do ViewModel, mas se a sua action falhar ele vai mudar somente o gpsStatus. Por isso, imagino que o seu equals deveria conter o gpsStatus também.

Fora isso tudo parece certo. Pode ser um erro no copyWith do state ou do user. Ou pode ser um problema no equals/hashcode no nearClub. Ou pode ser outra coisa.

Não faz sentido esse await de 2 segundos depois do seu dispatch, pois daí o dispatch já terminou. O que importa se vc espera dois segundos depois? Talvez o que você marcou como "...outros afazeres" é que tenha alguma influência, não sei.

Para eu dar mais informação você precisa criar pra mim um código minimo que demonstre o problema. Por favor, crie um arquivo único, com um método main que eu possa rodar e ver o problema por mim mesmo. Sugiro você usar http://numbersapi.com em vez do GPS, para facilitar a criação desse arquivo de demonstração.

LeandroMoura3 commented 4 years ago

Dentro dos meus conhecimentos, verifiquei os hashcodes/equals, assim como os copyWiths, tbm coloquei o gpsStatus no equals, ainda continua o mesmo erro. Sem o delay não atualiza, com o delay atualiza. Realmente estranhíssimo este comportamento. Vou trabalhar em breve num código de arquivo único para tentar reproduzir este erro (acho que amanhã, no máximo, devo começar).

Agradeço.

LeandroMoura3 commented 4 years ago

Boa noite,

Consegui isolar o erro:

import 'package:async_redux/async_redux.dart';
import 'package:flutter/material.dart';

//Store
class Club {
  final String name;

  const Club({
    this.name,
  });

  Club copyWith({
    String name,
  }) =>
      Club(
        name: name ?? this.name,
      );

  @override
  String toString() {
    return 'Club{name: $name}';
  }

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Club && runtimeType == other.runtimeType && name == other.name;

  @override
  int get hashCode => name.hashCode;
}

class MyAppState {
  final List<Club> clubList;

  const MyAppState({
    this.clubList,
  });

  MyAppState copyWith({
    List<Club> clubList,
  }) =>
      MyAppState(
        clubList: clubList ?? this.clubList,
      );

  static MyAppState initialState() => MyAppState(
        clubList: [],
      );

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is MyAppState &&
          runtimeType == other.runtimeType &&
          clubList == other.clubList;

  @override
  int get hashCode => clubList.hashCode;

  @override
  String toString() {
    return 'MyAppState{clubList: $clubList}';
  }
}

Store<MyAppState> store = Store<MyAppState>(initialState: MyAppState.initialState());

//Actions
class UpdateClubListAction extends ReduxAction<MyAppState> {
  @override
  MyAppState reduce() {
    List<Club> currentClubList = List.generate(
      15,
      (index) => Club(name: "name$index"),
    ).toList();

    return state.copyWith(
      clubList: currentClubList,
    );
  }

  @override
  void after() => print("db1:" + state.clubList.length.toString());
}

class PrintClubListAction extends ReduxAction<MyAppState> {
  @override
  MyAppState reduce() {
    print("db2:" + state.clubList.length.toString());
    return state.copyWith();
  }
}

//Viewmodel
class HomeSync extends BaseModel<MyAppState> {
  HomeSync();

  void Function(BuildContext) pushSelectClub;
  int clubListLength;

  HomeSync.build({
    @required this.clubListLength,
    @required this.pushSelectClub,
  }) : super(equals: [clubListLength]);

  @override
  HomeSync fromStore() => HomeSync.build(
        clubListLength: state.clubList.length,
        pushSelectClub: (BuildContext context) async {
          dispatch(UpdateClubListAction());
          dispatch(PrintClubListAction());
          print("db3:" + state.clubList.length.toString());
          print("db4:" + clubListLength.toString());
        },
      );
}

//app
void main() => runApp(MyApp(store));

class MyApp extends StatefulWidget {
  final store;

  MyApp(this.store);

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return StoreProvider<MyAppState>(
      store: widget.store,
      child: MaterialApp(
        title: 'Async Redux Management State',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: HomeTab(),
      ),
    );
  }
}

class HomeTab extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<MyAppState, HomeSync>(
      model: HomeSync(),
      builder: (BuildContext context, HomeSync sync) {
        return Scaffold(
          appBar: AppBar(
            title: Text("TestWidget"),
          ),
          body: Center(
            child: RaisedButton(
              onPressed: () => sync.pushSelectClub(context),
              child: Text("Click me"),
            ),
          ),
        );
      },
    );
  }
}

O recebido é: I/flutter (18834): db1:15 I/flutter (18834): db2:15 I/flutter (18834): db3:0 I/flutter (18834): db4:null

O esperado seria: I/flutter (18834): db1:15 I/flutter (18834): db2:15 I/flutter (18834): db3:15 I/flutter (18834): db4:15

Esse ainda não é o caso em que, se esperar, funciona, mas acredito que tenha uma forte relação.

LeandroMoura3 commented 4 years ago

Olá,

Consegui isolar a achar a inconsistência que comentei, que tratava-se sobre a necessidade de inserir um delay pra surtir o efeito desejado. Percebi que não tinha haver com o tipo de chamada ser assíncrona ou não. Segue o código demonstrativo:

import 'package:async_redux/async_redux.dart';
import 'package:flutter/material.dart';

//Store
class Club {
  final String name;

  const Club({
    this.name,
  });

  Club copyWith({
    String name,
  }) =>
      Club(
        name: name ?? this.name,
      );

  @override
  String toString() {
    return 'Club{name: $name}';
  }

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
          other is Club && runtimeType == other.runtimeType && name == other.name;

  @override
  int get hashCode => name.hashCode;
}

class MyAppState {
  final List<Club> clubList;
  final bool statusBool;

  const MyAppState({
    this.clubList,
    this.statusBool,
  });

  MyAppState copyWith({
    List<Club> clubList,
    bool statusBool,
  }) =>
      MyAppState(
        clubList: clubList ?? this.clubList,
        statusBool: statusBool ?? this.statusBool,
      );

  static MyAppState initialState() => MyAppState(
    clubList: [],
    statusBool: false,
  );

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is MyAppState &&
          runtimeType == other.runtimeType &&
          clubList == other.clubList &&
          statusBool == other.statusBool;

  @override
  int get hashCode => clubList.hashCode ^ statusBool.hashCode;

  @override
  String toString() {
    return 'MyAppState{clubList: $clubList, statusBool: $statusBool}';
  }
}

Store<MyAppState> store = Store<MyAppState>(initialState: MyAppState.initialState());

//Actions
class UpdateClubListAction extends ReduxAction<MyAppState> {
  @override
  MyAppState reduce() {
    List<Club> currentClubList = List.generate(
      15,
          (index) => Club(name: "name$index"),
    ).toList();

    return state.copyWith(
      clubList: currentClubList,
    );
  }

  @override
  void after() => print("db1:" + state.clubList.length.toString());
}

class PrintClubListAction extends ReduxAction<MyAppState> {
  @override
  MyAppState reduce() {
    print("db2:" + state.clubList.length.toString());
    return state.copyWith();
  }
}

class ChangeStatusBoolAction extends ReduxAction<MyAppState> {
  @override
  MyAppState reduce() {
    return state.copyWith(
      statusBool: !state.statusBool,
    );
  }
  @override
  void after() {
    print("db5:"+state.statusBool.toString());
    super.after();
  }
}

//Viewmodel
class HomeSync extends BaseModel<MyAppState> {
  HomeSync();

  void Function(BuildContext) pushSelectClub;
  VoidCallback changeStatusBool;
  int clubListLength;
  bool statusBool;

  HomeSync.build({
    @required this.clubListLength,
    @required this.pushSelectClub,
    @required this.statusBool,
    @required this.changeStatusBool,
  }) : super(equals: [clubListLength, statusBool]);

  @override
  HomeSync fromStore() => HomeSync.build(
    statusBool: state.statusBool,
    clubListLength: state.clubList.length,
    changeStatusBool: (){
      dispatch(ChangeStatusBoolAction());
      print("db6:"+statusBool.toString());
      print("db7:"+state.statusBool.toString());
    },
    pushSelectClub: (BuildContext context) async {
      dispatch(UpdateClubListAction());
      await Future.delayed(Duration(seconds: 2));
      dispatch(PrintClubListAction());
      print("db3:" + state.clubList.length.toString());
      print("db4:" + clubListLength.toString());
    },
  );
}

//app
void main() => runApp(MyApp(store));

class MyApp extends StatefulWidget {
  final store;

  MyApp(this.store);

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return StoreProvider<MyAppState>(
      store: widget.store,
      child: MaterialApp(
        title: 'Async Redux Management State',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: HomeTab(),
      ),
    );
  }
}

class HomeTab extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<MyAppState, HomeSync>(
      model: HomeSync(),
      builder: (BuildContext context, HomeSync sync) {
        return Scaffold(
          appBar: AppBar(
            title: Text("TestWidget"),
          ),
          body: Center(
            child: Column(
              children: <Widget>[
                RaisedButton(
                  onPressed: sync.changeStatusBool,
                  child: Text("Click me first"),
                ),
                RaisedButton(
                  onPressed: () => sync.pushSelectClub(context),
                  child: Text("Click me second"),
                ),
                Text(sync.statusBool.toString()),
                Text(sync.clubListLength.toString()),
              ],
            ),
          ),
        );
      },
    );
  }
}

Aqui este código se comporta da seguinte maneira: Buildei o app, apertei no primeiro botão, em seguida no segundo, segue o resultado: I/flutter (21302): db5:true I/flutter (21302): db6:null I/flutter (21302): db7:false I/flutter (21302): db1:15 I/flutter (21302): db2:15 I/flutter (21302): db3:15 I/flutter (21302): db4:null

Dou hot-restart. Aperto somente no segundo botão, recebo o seguinte resultado: I/flutter (21302): db1:15 I/flutter (21302): db2:15 I/flutter (21302): db3:0 I/flutter (21302): db4:null

Comentei o delay, salvei e dei hot-restart, apertei no primeiro botão, depois no segundo, recebo o seguinte resultado: I/flutter (21302): db5:true I/flutter (21302): db6:null I/flutter (21302): db7:false I/flutter (21302): db1:15 I/flutter (21302): db2:15 I/flutter (21302): db3:0 I/flutter (21302): db4:null

Ainda com o delay comentado, dei hot-restart, apertei no segunte botão, recebi o seguinte resultado: I/flutter (21302): db1:15 I/flutter (21302): db2:15 I/flutter (21302): db3:0 I/flutter (21302): db4:null

Tirei o async da assinatura da função anônima do pushSelectClub e deu o mesmo resultado.

Como é possível perceber, a presença do delay alterou os resultados. Novamente, o que eu esperava nestes resultados é que fosse 15 em todas as consultas de tamanho.

No entanto, eu percebi também que o resultado do viewmodel na view, ou seja, o Text(sync.clubListLength.toString()), está funcionando perfeitamente, o que me faz perguntar se sou eu que estou fazendo uma enorme confusão. Se for o caso, desculpe-me, mas, de qualquer forma, ainda acho inconsistente essa questão de ter ou não ter o delay, pois em teoria ele não deveria surtir efeito.

Se for do resultado esperado for o que está ai, ou seja, 15, 15, 0 e null lá no primeiro teste do comentário anterior, existe uma forma de eu forçar a atualização do store na viewmodel? Pois eu gerencio nas viewmodels tanto as views (através de globalkeys) como a store(através das actions) e apis, e acessar as views por meio de actions não seria tão bom pro padrão de projeto que estou estabelecendo.

marcglasberg commented 4 years ago

Como vc mesmo disse está tudo funcionando corretamente, você implementou correto. Você só está fazendo uma confusão em relação a como você acha que isso tudo deveria funcionar. Que tal eu explicar tudo pra você por telefone? Me passa seu whatsapp por comentário aqui (e depois você dá um delete no comentário), ou então me manda por email, se vc quiser, e eu te ligo, que tal?

leonardo2204 commented 4 years ago

Estou com o mesmo problema atualmente num bloco de catch da minha aplicacao, como foi solucionado esse problema?

Obrigado!

marcglasberg commented 4 years ago

Leonardo, é dificil saber o que vc quer dizer com problema num bloco de catch. Melhor passar um código aqui. De preferência o menor código possível que demonstre o problema.