如何在Flutter中使用需要较长时间的`build()`来初始化ViewModel?

huangapple go评论70阅读模式
英文:

How to make ViewModel to be initialized with build() that takes too long in Flutter?

问题

以下是您要翻译的内容:

"I have 3 menus in my Flutter web: A, B, C.

And in menu B, it has sub menu 'week' and 'month'.

When I click menu 'month', It gets several data from Firestore and compile it and draw UI.

But the problem is that Firestore is very slow to get several data.

(Of course, month data is bigger than week data, so It takes much more time)

So If It start getting data when I click sub-menu 'month', It's too long to wait for loading finished.

My Idea is to start getting data when I access menu A, which means my loading start point moves forward in order to shorten loading time from user's point.

ViewModel to extract month data is below:

class MonthRankingViewModel extends AsyncNotifier<List<UserModel?>> {
  late RankingRepository _rankingRepository;
  DateTime now = DateTime.now();
  late DateTime firstDateOfMonth;
  late List<UserModel?> monthUserList;

  @override
  FutureOr<List<UserModel?>> build() async {
    _rankingRepository = RankingRepository();
    DateTime now = DateTime.now();

    firstDateOfMonth = DateTime(now.year, now.month, 1);
    firstDateOfMonth = DateTime(firstDateOfMonth.year, firstDateOfMonth.month,
        firstDateOfMonth.day, 0, 0);

    ref.watch(contractConfigProvider).when(
          data: (data) async {
            final String getUserContractType = data.contractType;
            final String getUserContractName = data.contractName;
            if (getUserContractType == "지역") {
              final userDataList = await ref
                  .watch(userRepo)
                  .getRegionUserData(getUserContractName);
              final monthUserList =
                  await updateUserScore(userDataList, "종합 점수");
              state = AsyncValue.data(monthUserList);
            } else if (getUserContractType == "기관") {
              final userDataList = await ref
                  .watch(userRepo)
                  .getCommunityUserData(getUserContractName);
              final monthUserList =
                  await updateUserScore(userDataList, "종합 점수");
              state = AsyncValue.data(monthUserList);
            } else if (getUserContractType == "마스터" ||
                getUserContractType == "전체") {
              final userDataList = await ref.watch(userRepo).getAllUserData();
              final monthUserList =
                  await updateUserScore(userDataList, "종합 점수");
              state = AsyncValue.data(monthUserList);
            }
          },
          error: (error, stackTrace) => print(error),
          loading: () {},
        );

    state = const AsyncValue.data([]);
    return [];
  }

  Future<List<UserModel?>> updateUserScore(
      List<UserModel?> users, String sortOrder) async {
    List<UserModel?> updateScoreList = [];
    state = const AsyncValue.loading();

    if (state.value!.isEmpty) {
      await Future.forEach(
        users,
        (user) async {
          final scoreUser = await calculateUserScore(user);
          updateScoreList.add(scoreUser);
        },
      );

      updateScoreList.sort((a, b) => b!.totalScore!.compareTo(a!.totalScore!));
      final indexList = ref
          .read(userProvider.notifier)
          .indexUserModel(sortOrder, updateScoreList);

      state = AsyncValue.data(indexList);
      return indexList;
    } else {
      return state.value!;
    }
  }

  Future<UserModel?> calculateUserScore(UserModel? user) async {
    final String userId = user!.userId;
    List<int> results;

    List<Future<int>> futures = [
      _rankingRepository.getUserStepScores(userId, firstDateOfMonth, now),
      _rankingRepository.getUserDiaryScores(userId, firstDateOfMonth, now),
      _rankingRepository.getUserCommentScores(userId, firstDateOfMonth, now),
    ];
    results = await Future.wait(futures);

    int userTotalScore = results[0] + results[1] + results[2];

    final updatedUser = user.copyWith(
      totalScore: userTotalScore,
      stepScore: results[0],
      diaryScore: results[1],
      commentScore: results[2],
    );
    return updatedUser;
  }
}

final monthRankingProvider =
    AsyncNotifierProvider<MonthRankingViewModel, List<UserModel?>>(
  () => MonthRankingViewModel(),
);

And from screen A's initState() I call week and month viewmodels.

  @override
  void initState() {
    super.initState();
    ref.read(weekRankingProvider);
    ref.read(monthRankingProvider);
  }

I works well with week data.

But error comes, when I tap 'month' sub-menu.

I guess, since build method of month viewModel takes long to finished, when I go menu B and tap 'month' it is not still finished.

So according to my setting, when tapping month, because I set If I tap week, It draws week data, and if tapping month, draws month data.

 void updateOrderPeriod(String value) async {
    setState(() {
      _sortOrderPeriod = value;
      loadingFinished = false;
    });
    switch (value) {
      case "이번달":
        List<UserModel?> monthList = await ref
            .read(monthRankingProvider.notifier)
            .updateUserScore(_userDataList, _sortOrderStandard);
        setState(() {
          _userDataList = monthList;
          loadingFinished = true;
        });
        break;
      case "이번주":
        setState(() {
          _sortOrderPeriod = "이번주";
        });
        List<UserModel?> weekList = await ref
            .read(weekRankingProvider.notifier)
            .updateUserScore(_userDataList, _sortOrderStandard);
        setState(() {
          _userDataList = weekList;
          loadingFinished = true;
        });
        break;
    }
  } 

So even though build method is not finished,

await ref.read(monthRankingProvider.notifier).updateUserScore(_userDataList, _sortOrderStandard)

updateUserScore of this viewModel is ran again.

And It throws error since they crush.

I'm quite a newbie in Flutter, Please see my code and help me.

英文:

I have 3 menus in my Flutter web: A, B, C.

And in menu B, it has sub menu 'week' and 'month'.

When I click menu 'month', It gets several data from Firestore and compile it and draw UI.

But the problem is that Firestore is very slow to get several data.

(Ofcouse, month data is bigger than week data, so It takes much more time)

So If It start getting data when I click sub-menu 'month', It's too long to wait for loading finished.

My Idea is to start getting data when I access menu A, which means my loading start point moves forward in order to shorten loading time from user's point.

ViewModel to extract month data is below:

class MonthRankingViewModel extends AsyncNotifier&lt;List&lt;UserModel?&gt;&gt; {
late RankingRepository _rankingRepository;
DateTime now = DateTime.now();
late DateTime firstDateOfMonth;
late List&lt;UserModel?&gt; monthUserList;
@override
FutureOr&lt;List&lt;UserModel?&gt;&gt; build() async {
_rankingRepository = RankingRepository();
DateTime now = DateTime.now();
firstDateOfMonth = DateTime(now.year, now.month, 1);
firstDateOfMonth = DateTime(firstDateOfMonth.year, firstDateOfMonth.month,
firstDateOfMonth.day, 0, 0);
ref.watch(contractConfigProvider).when(
data: (data) async {
final String getUserContractType = data.contractType;
final String getUserContractName = data.contractName;
if (getUserContractType == &quot;지역&quot;) {
final userDataList = await ref
.watch(userRepo)
.getRegionUserData(getUserContractName);
final monthUserList =
await updateUserScore(userDataList, &quot;종합 점수&quot;);
state = AsyncValue.data(monthUserList);
} else if (getUserContractType == &quot;기관&quot;) {
final userDataList = await ref
.watch(userRepo)
.getCommunityUserData(getUserContractName);
final monthUserList =
await updateUserScore(userDataList, &quot;종합 점수&quot;);
state = AsyncValue.data(monthUserList);
} else if (getUserContractType == &quot;마스터&quot; ||
getUserContractType == &quot;전체&quot;) {
final userDataList = await ref.watch(userRepo).getAllUserData();
final monthUserList =
await updateUserScore(userDataList, &quot;종합 점수&quot;);
state = AsyncValue.data(monthUserList);
}
},
error: (error, stackTrace) =&gt; print(error),
loading: () {},
);
state = const AsyncValue.data([]);
return [];
}
Future&lt;List&lt;UserModel?&gt;&gt; updateUserScore(
List&lt;UserModel?&gt; users, String sortOrder) async {
List&lt;UserModel?&gt; updateScoreList = [];
state = const AsyncValue.loading();
if (state.value!.isEmpty) {
await Future.forEach(
users,
(user) async {
final scoreUser = await calculateUserScore(user);
updateScoreList.add(scoreUser);
},
);
updateScoreList.sort((a, b) =&gt; b!.totalScore!.compareTo(a!.totalScore!));
final indexList = ref
.read(userProvider.notifier)
.indexUserModel(sortOrder, updateScoreList);
state = AsyncValue.data(indexList);
return indexList;
} else {
return state.value!;
}
}
Future&lt;UserModel?&gt; calculateUserScore(UserModel? user) async {
final String userId = user!.userId;
List&lt;int&gt; results;
List&lt;Future&lt;int&gt;&gt; futures = [
_rankingRepository.getUserStepScores(userId, firstDateOfMonth, now),
_rankingRepository.getUserDiaryScores(userId, firstDateOfMonth, now),
_rankingRepository.getUserCommentScores(userId, firstDateOfMonth, now),
];
results = await Future.wait(futures);
int userTotalScore = results[0] + results[1] + results[2];
final updatedUser = user.copyWith(
totalScore: userTotalScore,
stepScore: results[0],
diaryScore: results[1],
commentScore: results[2],
);
return updatedUser;
}
}
final monthRankingProvider =
AsyncNotifierProvider&lt;MonthRankingViewModel, List&lt;UserModel?&gt;&gt;(
() =&gt; MonthRankingViewModel(),
);

And from screen A's initState() I call week and month viewmodels.

  @override
void initState() {
super.initState();
ref.read(weekRankingProvider);
ref.read(monthRankingProvider);
}

I works well with week data.

But error comes, when I tap 'month' sub-menu.

I guess, since build method of month viewModel takes long to finished, when I go menu B and tap 'month' it is not still finished.

So according to my setting, when tapping month, because I set If I tap week, It draws week data, and if tapping month, draws month data.

 void updateOrderPeriod(String value) async {
setState(() {
_sortOrderPeriod = value;
loadingFinished = false;
});
switch (value) {
case &quot;이번달&quot;:
List&lt;UserModel?&gt; monthList = await ref
.read(monthRankingProvider.notifier)
.updateUserScore(_userDataList, _sortOrderStandard);
setState(() {
_userDataList = monthList;
loadingFinished = true;
});
break;
case &quot;이번주&quot;:
setState(() {
_sortOrderPeriod = &quot;이번주&quot;;
});
List&lt;UserModel?&gt; weekList = await ref
.read(weekRankingProvider.notifier)
.updateUserScore(_userDataList, _sortOrderStandard);
setState(() {
_userDataList = weekList;
loadingFinished = true;
});
break;
}
} 

So even though build method is not finished,

await ref.read(monthRankingProvider.notifier).updateUserScore(_userDataList, _sortOrderStandard)

updateUserScore of this viewModel is ran again.

And It throws error since they crush.

I'm quite a newbie in Flutter, Please see my code and help me.

答案1

得分: 0

我看到了问题。问题在于,尽管构建方法尚未完成,但您正在调用MonthRankingViewModel上的updateUserScore方法。这是因为updateUserScore方法是异步的,所以它不会在从Firestore获取数据之前返回。

为了解决这个问题,您需要将updateUserScore方法包装在FutureBuilder中。这将确保构建方法在数据被获取之前不会完成。

以下代码显示了如何执行此操作:

void updateOrderPeriod(String value) async {
    setState(() {
      _sortOrderPeriod = value;
      loadingFinished = false;
    });
    switch (value) {
      case "이번달":
        final monthList = await FutureBuilder(
            future: ref
                .read(monthRankingProvider.notifier)
                .updateUserScore(_userDataList, _sortOrderStandard),
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return const Text("Loading...");
              } else if (snapshot.hasError) {
                return Text(snapshot.error.toString());
              } else {
                _userDataList = snapshot.data;
                loadingFinished = true;
                return Text("");
              }
            });
        setState(() {
          _userDataList = monthList;
          loadingFinished = true;
        });
        break;
      case "이번주":
        setState(() {
          _sortOrderPeriod = "이번주";
        });
        List<UserModel?> weekList = await ref
            .read(weekRankingProvider.notifier)
            .updateUserScore(_userDataList, _sortOrderStandard);
        setState(() {
          _userDataList = weekList;
          loadingFinished = true;
        });
        break;
    }
  }

这段代码首先将updateUserScore方法包装在FutureBuilder中。这将创建一个Future,直到从Firestore获取数据之前都不会完成。

然后,FutureBuilder将用于构建UI。如果Future尚未完成,FutureBuilder将返回加载指示器。一旦Future完成,FutureBuilder将返回用户列表。

希望这有所帮助!如果您有任何其他问题,请随时告诉我。

英文:

I see the problem. The issue is that you are calling updateUserScore on the MonthRankingViewModel even though the build method has not finished. This is because the updateUserScore method is asynchronous, so it will not return until the data has been fetched from Firestore.

To fix this, you need to wrap the updateUserScore method in a FutureBuilder. This will ensure that the build method does not complete until the data has been fetched.

The following code shows how to do this:

void updateOrderPeriod(String value) async {
setState(() {
_sortOrderPeriod = value;
loadingFinished = false;
});
switch (value) {
case &quot;이번달&quot;:
final monthList = await FutureBuilder(
future: ref
.read(monthRankingProvider.notifier)
.updateUserScore(_userDataList, _sortOrderStandard),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text(&quot;Loading...&quot;);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
_userDataList = snapshot.data;
loadingFinished = true;
return Text(&quot;&quot;);
}
});
setState(() {
_userDataList = monthList;
loadingFinished = true;
});
break;
case &quot;이번주&quot;:
setState(() {
_sortOrderPeriod = &quot;이번주&quot;;
});
List&lt;UserModel?&gt; weekList = await ref
.read(weekRankingProvider.notifier)
.updateUserScore(_userDataList, _sortOrderStandard);
setState(() {
_userDataList = weekList;
loadingFinished = true;
});
break;
}
}

This code will first wrap the updateUserScore method in a FutureBuilder. This will create a Future that will not complete until the data has been fetched from Firestore.

The FutureBuilder will then be used to build the UI. If the Future is not yet complete, the FutureBuilder will return a loading indicator. Once the Future is complete, the FutureBuilder will return the list of users.

I hope this helps! Let me know if you have any other questions.

huangapple
  • 本文由 发表于 2023年7月18日 11:18:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/76709335.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定