The title and project are heavily referenced by best Practices for product-level Flutter Open Source project FunAndroid, Provider MVVM, and OpenJMU, with special thanks to Phoenixsky and A-Shao.

introduce

  • The project address
  • The cause of

The main reason for this project is that THE update of Flutter is too fast, and all kinds of new plug-ins and apis make me feel itchy. The company’s project, which is based on stability, must be hard to keep up with the update speed of Flutter with such frequency. Therefore, it is better to create a project that can be messed with by myself. So I put all the plugins that I was interested in together and cooked them up as a kind of technical reserve.

  • The plug-in
  1. beamer
  2. riverpod
  3. freezed
  • Completed functions
  • Login and Registration
  • Home page rotation and articles
  • Square and q&A articles
  • Project article and project type switch
  • Search and search results
  • User sharing and articles
  • Leaderboard
  • My points
  • My favorites and favorites to add, edit and delete
  • My share and share add, edit and delete
  • Dark mode and multilingual support
  • Todo
  • Flutter for web
  • .

show

Home page project search
The search results The article The sidebar

Routing management

This part is purely a conclusion after my own use, and most of it has not been verified by Debug, and there are still some bugs in the document by the end, so it is estimated that there will be a lot of changes in the future. Therefore, you can refer to the configuration to run this project, but it is not recommended to refer to it as a guideThe official documentation

When I first learned about Flutter, I read the Complete Guide and Principles of Flutter 2.0. I didn’t understand the concepts of Page, Router, and RouterDelegate. But I remember that it can make A page from A -> B -> C directly into D -> E -> F -> G, and support the URL input navigation of the Web, which is the API I wanted to try for A long time, so AT the beginning of Copy took the code to start tinkering, tinkering for A period of time came up with A problem of login interception. Flutter2.8.0 has several more Navigator2.0 libraries in Flutter Favorites and a route guard feature in beamer.

  • The App was originally developed on the Web side due to Navigator2.0, which was not very adept at imposing beamer on top of it, so I had a lot of weird problems. I chose to use ChangeNotifier to manage the routing state. I then defined a complex HomeState and had to define a bunch of things in it every time I added a page. I felt like I must have done something wrong, but the examples were basically written that way.
  • Because there is a function to switch ProjectType that you want to useshowModalBottomSheet, in the current page in the form of popup window, and then found that there seems to be no corresponding Route to use, because the official corresponding Route is private, so I took out the official Route to change, accidentally realized; At the back of theshowSearchAnd so it goes,Here’s the code

Used to show

  • /lib/navigator/home/home_state.dart

It would be better to call HomeState AppState or RouteState, but it was called HomeState because it was supposed to be able to separate the states of various pages, but it didn’t work out that way (there was a State conversion problem in buildPages, I didn’t come up with a good idea at that time), so I kept using it for the purpose of realizing the function first. When I wrote here, I thought there was an example in the official document, but it seems to have been deleted in the latest version. I have a little irresponsible understanding. Feel that custom multiple locations should apply to A -> B -> C and A1 -> B1 -> C1 where ABC’s routing stack does not have any intersection with A1B1C1

HomeState({
    String initialPath = RouterName.initialPath,
    /// Add new identifying variables, usually strings, numbers, or booleans
  })  : _initialPath = initialPath;
  
/// Configure fromJson, toJson, updateWith, copyWith
/// NotifyListeners in updateWith coordinate with HomeLocation
/// State.addlistener is actually the reason for the switch
/// UpdateWith treats -1 as null when updating an int
/// Because null is the default value when updateWith is passed in, null is ignored

@override
HomeState fromRouteInformation(RouteInformation routeInformation) {
  final Uri uri =
      Uri.parse(routeInformation.location ?? RouterName.home.location);
  LogUtils.d('from routeInformation : $uri');
  final String uriString = uri.toString();
  
  /// RouteInformation. State is the state set in toRouteInformation
  /// And restore, if there is no default value
  final HomeState homeState = HomeState.fromJson(
      routeInformation.state as Map<String.dynamic>? ?? <String.dynamic> {});/// Add a judgment here to handle the new URI
  if (RouterName.homeTabsPath.contains(uriString)) {
    return homeState.copyWith(
      initialPath: uriString,
    );
  }
  /// If it is a URI with parameters, you can get it this way
  if (uri.pathSegments.first == RouterName.article.title.toLowerCase() &&
      uri.pathSegments.contains('id')) {
    return homeState.copyWith(
      articleId: uri.pathSegments.last as int,); }}@override
RouteInformation toRouteInformation() {
  LogUtils.d('$runtimeType to routeInformation ${toJson()}');
  
  /// Attention should be paid to the superposition of various data in HomeState
  /// For example, isSettings and isLanguages are both true when switching languages
  /// So the judgment logic should be handled here in the exact opposite order of HomeLocation's [buildPages]

  /// The order here should be reversed from the Location [buildPages]
  if (showSplash) {
    returnRouteInformation( location: RouterName.splash.location, state: toJson(), ); }}Copy the code
  • /lib/navigator/home/home_location.dart
@override
List<BeamPage> buildPages(BuildContext context, HomeState state) {
  LogUtils.d('$runtimeType build pages: state = $state');

  /// Notice the order, the page the user sees is the last item in the array
  /// If my page stack is [A -> B -> C], then it should be [A,B,C].

  return <BeamPage>[
    BeamPage(
      key: ValueKey<String>(RouterName.home.location),
      title: RouterName.home.title,
      child: HomeScreen(
        initialPath: state.initialPath,
      ),
    ),
    if (state.showSearch)
      BeamPage(
        /// The key value must be passed in, which determines how the Navigator optimizes buildPages
        key: ValueKey<String>(RouterName.search.location),

        /// Determine the title in the browser
        title: RouterName.search.title,
        routeBuilder: (_, RouteSettings settings, Widget child) {
          return SearchPageRoute<void>(
            delegate: HomeSearchDelegate(),
            settings: settings,
          );
        },
        child: const SizedBox.shrink(),

        /// It is important to reset the state of HomeState on return to the state before entry
        /// This way we can call navigator.of (context).maybepop () in the page to return
        onPopPage: (
          _,
          __,
          RouteInformationSerializable<dynamic> state,
          ___,
        ) {
          (state as HomeState).updateWith(
            showSearch: false,);return true; },)]; }Copy the code
  • navigation
/// Navigate through the page
/// At this point, the entire routing stack is determined by the various values of HomeState
/// So we can easily implement all kinds of navigation
AppRouterDelegate.instance.currentBeamState.updateWith(
  articleId: article.id,
);
Copy the code
  • The guards
BeamerDelegate _crateDelegate(Reader reader) => BeamerDelegate(
      initialPath: RouterName.home.location,
      notFoundRedirectNamed: RouterName.unknown.location,
      navigatorObservers: <NavigatorObserver>[
        FlutterSmartDialog.observer,
        Instances.routeObserver,
      ],
      locationBuilder: BeamerLocationBuilder(
        beamLocations: <BeamLocation<RouteInformationSerializable<dynamic>>>[
          HomeLocation(),
        ],
      ),
      guards: <BeamGuard>[
        BeamGuard(
          /// [guardNonMatching] true means everything except [pathPatterns] is blocked
          /// The default false is to intercept only pages in [pathPatterns]
          guardNonMatching: true,
          pathPatterns: <Pattern>[RouterName.splash.location],
          /// Returns a Boolean value
          check: (_, __) => reader.call(splashProvider),
          /// Deal with HomeState
          beamTo: (
            _,
            __,
            BeamLocation<RouteInformationSerializable<dynamic>> target,
          ) =>
              (target asHomeLocation) .. state.updateWith( showSplash:true,
                ),
        ),
        BeamGuard(
          pathPatterns: <Pattern>[ ...RouterName.homeDrawerPath, ], check: (_, __) => reader.call(authorizedProvider) ! =null./// The intercepted HomeState value needs to be set to false, otherwise the routing stack will become [intercepted page -> redirected page]
          beamTo: (
            _,
            __,
            BeamLocation<RouteInformationSerializable<dynamic>> target,
          ) =>
              (target asHomeLocation) .. state.updateWith( isLogin:true,
                  isMyCollections: false,
                  isMyPoints: false,
                  isMyShare: false[, [, [, [, [, [.Copy the code

summary

In fact, the whole experience is not as good as 1.0. For example, I personally like the 1.0 Future-style navigation that can freely return some data without using state management tools. In 2.0, I don’t know how to handle this situation. But that page is out of the 2.0 routing state and needs to be handled as if it were 1.0. The whole HomeState implementation feels a little too complicated, and it is definitely not as clear and intuitive as 1.0. As a Web developer, I still have feelings for the Web, and I still want to challenge the Web of Flutter. So far, I think the challenge failed, and I still don’t understand 2.0 enough.

State management

About the principle of Riverpod, Mr. Guo recently wrote an article entitled “A thorough and in-depth analysis of Flutter Riverpod, why is it officially recommended? , I am a rough person, generally is the side of practice, such as big men out of the article or small book to understand.

My first provider teacher was Phoenixsky’s article, which I still learned a lot from. Therefore, when I want to do this project, I should first consider using RiverPod to realize phoenixsky’s provider architecture

  1. The first use offreezedtheJoint featuresEncapsulate the combined model of three commonly used models and make it naturally have states, namely loading, success and error.Here’s the code.
  2. Construct corresponding abstract classes according to the three models to abstract out the logic such as obtaining data and handling errors. The code is here
  3. Map abstract classes to widgets, in this case, pairsBaseListViewNotifier<T>andBaseRefreshListViewNotifier<T>Widget encapsulation,Here’s the code

At this point, you basically have phoenixsky’s architecture.

Used to show

/// Inherit the required abstract classes based on the requirements
/// Implement the [loadData] method
class QuestionNotifier extends BaseArticleNotifier {
  QuestionNotifier(RefreshListViewState<ArticleModel> state) : super(state);

  @override
  Future<RefreshListViewStateData<ArticleModel>> loadData(
      {required int pageNum, required int pageSize}) async {
    return (awaitWanAndroidAPI.fetchQuestionArticles( pageNum, pageSize, )) .toRefreshListViewStateData(); }}/// Define the provider as required and return the Notifier
/// You need to give Notifier an initial value (the default value)
/// The default value here is RefreshListViewState<ArticleModel>.loading()
final StateNotifierProvider<QuestionNotifier,
        RefreshListViewState<ArticleModel>> questionArticleProvider =
    StateNotifierProvider<QuestionNotifier, RefreshListViewState<ArticleModel>>(
  (_) {
    return QuestionNotifier(
      const RefreshListViewState<ArticleModel>.loading(),
    );
  },
  name: kQuestionArticleProvider,
);

/// Use with components
/// Passing in [provider] determines when to initialize in [onInitState]
RefreshListViewWidget<
    StateNotifierProvider<QuestionNotifier,
        RefreshListViewState<ArticleModel>>,
    ArticleModel>(
  provider: questionArticleProvider,
  onInitState: (Reader reader) {
    reader.call(questionArticleProvider.notifier).initData();
  },
  builder: (_, __, List<ArticleModel> list) {
    return SliverList(
      delegate: CustomSliverChildBuilderDelegate.separated(
        itemBuilder: (_, int index) {
          returnArticleTile( article: list[index], ); }, itemCount: list.length, ), ); },),Copy the code

Interesting usage

/project/list/$pageNum/json? Cid =$categoryId, which depends on a categoryId requested through the /project/tree/json data interface

/// Create the Provider and Notifier of ProjectTypes first
final StateNotifierProvider<ProjectTypeNotifier,
        ListViewState<ProjectTypeModel>> projectTypesProvider =
    StateNotifierProvider<ProjectTypeNotifier, ListViewState<ProjectTypeModel>>(
        (_) {
  return ProjectTypeNotifier(
    const ListViewState<ProjectTypeModel>.loading(),
  );
});

class ProjectTypeNotifier extends BaseListViewNotifier<ProjectTypeModel> {
  ProjectTypeNotifier(ListViewState<ProjectTypeModel> state) : super(state);

  int _selectedIndex = 0;
  int get selectedIndex => _selectedIndex;

  @override
  Future<List<ProjectTypeModel>> loadData() async {
    final List<ProjectTypeModel> data = await WanAndroidAPI.fetchProjectTypes();
    data.first = data.first.copyWith(isSelected: true);
    return data;
  }

  void selected(int index) {
    state.whenOrNull(
      (List<ProjectTypeModel> value) {
        if(_selectedIndex ! = index) { value[_selectedIndex] = value[_selectedIndex].copyWith(isSelected:false);

          value[index] = value[index].copyWith(isSelected: true); _selectedIndex = index; state = ListViewState<ProjectTypeModel>(list: value); }}); }}/// Create a Provider for the currently selected ProjectType
final StateProvider<ViewState<ProjectTypeModel>> currentProjectTypeProvider =
    StateProvider<ViewState<ProjectTypeModel>>(
        (StateProviderRef<ViewState<ProjectTypeModel>> ref) {
  /// Listening for projectTypesProvider status returns ViewState<ProjectTypeModel>
  return ref.watch(projectTypesProvider).when(
        /// ProjectTypesProvider returns the currently selected ProjectTypeModel when it has a value
        (List<ProjectTypeModel> value) => ViewStateData<ProjectTypeModel>(
          value: value[ref.read(projectTypesProvider.notifier).selectedIndex],
        ),
        /// ProjectTypesProvider also returns wait status while waiting
        loading: () => const ViewStateLoading<ProjectTypeModel>(),
        /// Return projectTypesProvider error messages when projectTypesProvider fails
        error: (int? statusCode, String? message, String? detail) =>
            ViewStateError<ProjectTypeModel>(
          statusCode: statusCode,
          message: message,
          detail: detail,
        ),
      );
});

/// Create projectArticleProvider and ProjectNotifier
final StateNotifierProvider<ProjectNotifier, RefreshListViewState<ArticleModel>>
    projectArticleProvider =
    StateNotifierProvider<ProjectNotifier, RefreshListViewState<ArticleModel>>(
  (StateNotifierProviderRef<ProjectNotifier, RefreshListViewState<ArticleModel>>
      ref) {
    /// The state of the listening currentProjectTypeProvider returns RefreshListViewState<ArticleModel>
    return ref.watch(currentProjectTypeProvider).when(
          /// CurrentProjectTypeProvider have categoryId into the value
          /// And perform the initData ()
          (ProjectTypeModel? value) => ProjectNotifier(
            constRefreshListViewState<ArticleModel>.loading(), categoryId: value! .id, ).. initData(),/// CurrentProjectTypeProvider also returned when waiting for a wait state
          loading: () => ProjectNotifier(
            const RefreshListViewState<ArticleModel>.loading(),
            categoryId: null,),/// CurrentProjectTypeProvider error when the error information returned
          /// The error message at this point is actually the projectTypesProvider error message
          error: (int? statusCode, String? message, String? detail) =>
              ProjectNotifier(
            RefreshListViewState<ArticleModel>.error(
              statusCode: statusCode,
              message: message,
              detail: detail,
            ),
            categoryId: null,),); }, name: kProjectArticleProvider, );class ProjectNotifier extends BaseArticleNotifier {
  ProjectNotifier(
    RefreshListViewState<ArticleModel> state, {
    required this.categoryId,
  }) : super(state);

  final int? categoryId;

  @override
  Future<RefreshListViewStateData<ArticleModel>> loadData(
      {required int pageNum, required int pageSize}) async {
    return (await WanAndroidAPI.fetchProjectArticles(pageNum, pageSize,
            categoryId: categoryId!))
        .toRefreshListViewStateData();
  }
}

/// In the Widget
RefreshListViewWidget<StateNotifierProvider<ProjectNotifier, RefreshListViewState<ArticleModel>>, ArticleModel>(
  provider: projectArticleProvider,
  onInitState: (Reader reader) {
    /// Simply initialize the projectTypesProvider
    reader.call(projectTypesProvider.notifier).initData();
  },
  /// Override the default retry method
  onRetry: (Reader reader) {
    /// Retry projectTypesProvider when the projectTypesProvider fails
    if (reader.call(projectTypesProvider) is ListViewStateError<ProjectTypeModel>) {
      reader.call(projectTypesProvider.notifier).initData();
    } else {
      /// Otherwise, use your own retry method
      reader.call(projectArticleProvider.notifier).initData();
    }
  },
  builder: (_, __, List<ArticleModel> list) {
    return SliverList(
      delegate: CustomSliverChildBuilderDelegate.separated(
        itemBuilder: (_, int index) {
          return ArticleTile(
            article: list[index],
          );
        },
        itemCount: list.length,
      ),
    );
  },
  slivers: <Widget>[
    SliverPinnedPersistentHeader(
      delegate:
          _ProjectTypeSwitchSliverPinnedPersistentHeaderDelegate(
        extentProtoType: const _ProjectTypeSwitchExtentProtoType(),
      ),
    ),
  ],
),
Copy the code

So let me just show you the implementation

Strange usage

Wanandroid has the function of collecting articles, because articles displayed in the App end are usually displayed using the built-in WebView. At this time, it will be an awkward problem if you want to operate the state of collecting in the Webview. The state of collecting is in the list of articles. If you manipulate the state in the WebView, it needs to be synchronized externally, and there are four article lists on the home page alone

Phoenixsky’s solution was to maintain an external list of favorite article ids and synchronize the status of the list each time the status was changed

My solution was to talk to Hongyang Dashen, but hongyang Dashen didn’t seem to give me a blank answer, so what to do

/// Name your providers. Here are the four providers on the home page
const List<String> articles = <String>[
  kHomeArticleProvider,
  kSquareArticleProvider,
  kQuestionArticleProvider,
  kProjectArticleProvider,
];

/// Providers using AutoDispose need to be separated from each other
const List<String> autoDisposeArticles = <String>[
  kSearchArticleProvider,
  kMyShareProvider,
];

/// Collection of the Provider
const List<String> collects = <String>[
  kMyCollectedArticleProvider,
  kMyCollectedWebsiteProvider,
];

/// Throw together
const List<String> allArticles = <String>[
  ...articles,
  ...autoDisposeArticles,
  ...collects,
];

final AutoDisposeStateNotifierProviderFamily<ArticleNotifier,
        ViewState<WebViewModel>, int> articleProvider =
    StateNotifierProvider.autoDispose
        .family<ArticleNotifier, ViewState<WebViewModel>, int>((
  AutoDisposeStateNotifierProviderRef<ArticleNotifier, ViewState<WebViewModel>>
      ref,
  int articleId,
) {
  return ArticleNotifier(
    reader: ref.read,
    /// The beginning of all evil
    providerContainer: ref.container,
    id: articleId,
  );
});

class ArticleNotifier extends BaseViewNotifier<WebViewModel> {
  ArticleNotifier({
    required this.reader,
    required this.providerContainer,
    required this.id,
  }) : super(const ViewState<WebViewModel>.loading());

  final Reader reader;
  final ProviderContainer providerContainer;
  final int id;

  late String from;
  late ProviderBase<dynamic> provider;

  final List<String> articleOrigin = <String> [];final List<ProviderBase<dynamic>> articleOriginProvider =
      <ProviderBase<dynamic> > [];@overrideFuture<WebViewModel? > loadData()async {
    late WebViewModel webViewModel;

    CollectedArticleModel? collectedArticle;
    CollectedWebsiteModel? collectedWebsite;

    ArticleModel? article;
    
    /// If the Provider's name is in allArticles, insert it into the map along with the name
    final Map<String, ProviderBase<dynamic>> providers = providerContainer
        .getAllProviderElements()
        .fold(<String, ProviderBase<dynamic> > {},Map<String, ProviderBase<dynamic>> previousValue,
                ProviderElementBase<dynamic> e) {
      if (allArticles.contains(e.provider.name)) {
        return <String, ProviderBase<dynamic> > {... previousValue, e.provider.name! : e.provider, }; }return previousValue;
    });
    
    /// We only have one articleId
    /// We also got all the surviving ProviderContainer providers that may have corresponding articles
    /// You can definitely find it, but you can optimize your search
    /// For example, The Provider of AutoDispose can be pulled out first
    /// Because usually they don't exist or they're destroyed,
    /// If it does, the user is currently on that page,
    /// In the meantime, just look inside

    /// [kMyCollectedArticleProvider] and [kMyCollectedWebsiteProvider] is
    /// autoDispose provider, So if they exist, it can be considered to be
    /// currently in the [MyCollectionsScreen]
    /// that is, they can be searched first from them
    if (providers.keys.contains(kMyCollectedArticleProvider)) {
      if(providers.keys.contains(kMyCollectedWebsiteProvider)) { from = kMyCollectedWebsiteProvider; provider = providers[kMyCollectedWebsiteProvider]! ; collectedWebsite = (reader.call(provider)as ListViewState<CollectedWebsiteModel>)
                .whenOrNull((List<CollectedWebsiteModel> list) => list) ? .firstWhereOrNull((CollectedWebsiteModel e) => e.id == id); }else{ from = kMyCollectedArticleProvider; provider = providers[kMyCollectedArticleProvider]! ; collectedArticle = (reader.call(provider)as RefreshListViewState<CollectedArticleModel>)
            .whenOrNull((_, __, List<CollectedArticleModel> list) => list)
            ?.firstWhereOrNull((CollectedArticleModel e) => e.id == id);
      }
    } else {
      for (final String key in providers.keys) {
        if (<String>[...articles, ...autoDisposeArticles].contains(key)) { from = key; provider = providers[key]! ; article = (reader.call(provider)as RefreshListViewState<ArticleModel>)
                  .whenOrNull((_, __, List<ArticleModel> list) => list) ? .firstWhereOrNull((ArticleModel e) => e.id == id);if(article ! =null) {
            break; }}}}if(collectedArticle ! =null) {
      webViewModel = WebViewModel(
        id: collectedArticle.id,
        link: collectedArticle.link.startsWith('http')? collectedArticle.link :'https://${collectedArticle.link}',
        originId: collectedArticle.originId,
        title: collectedArticle.title,
        collect: collectedArticle.collect,
      );

      if(collectedArticle.originId ! =null) {
        for (final String key in providers.keys) {
          if (<String>[...articles, ...autoDisposeArticles].contains(key)) { articleOrigin.add(key); articleOriginProvider.add(providers[key]!) ; }}}}else if(collectedWebsite ! =null) {
      webViewModel = WebViewModel(
        id: collectedWebsite.id,
        link: collectedWebsite.link.startsWith('http')? collectedWebsite.link :'https://${collectedWebsite.link}'./// Use -2 as the logo of collected sites
        /// because -1 has a role in collected articles :)
        originId: 2 -,
        title: collectedWebsite.name,
        collect: collectedWebsite.collect,
      );
    } else if(article ! =null) {
      webViewModel = WebViewModel(
        id: article.id,
        link: article.link,
        title: article.title,
        collect: article.collect,
      );
    } else {
      /// If the provider does not contain the article, the provider will not find the article
      /// Entering the URL directly into flutter for Web will always return this because the providers have not been initialized
      throw AppException(
        errorCode: 404,
        message: S.current.articleNotFound,
        detail: S.current.articleNotFoundMsg,
      );
    }

    return webViewModel;
  }
  
  /// Since we have already got the provider of the corresponding article when switching the favorite state, it is good to use the method in the provider to synchronize the favorite state
  void collect(bool changedValue) {
    state.whenOrNull((WebViewModel? value) async {
      if(value ! =null) {
        state = ViewStateData<WebViewModel>(
          value: value.copyWith(
            collect: changedValue,
          ),
        );
        try {
          if(value.originId ! =null) {
            /// from MyCollectionsScreen
            if (value.originId == 2 -) {
              /// from MyCollectionsScreen - website
              final AutoDisposeStateNotifierProvider<MyCollectedWebsiteNotifier,
                      ListViewState<CollectedWebsiteModel>> realProvider =
                  provider as AutoDisposeStateNotifierProvider<
                      MyCollectedWebsiteNotifier,
                      ListViewState<CollectedWebsiteModel>>;
              if (changedValue) {
                final CollectedWebsiteModel? newCollectedWebsite =
                    await reader.call(realProvider.notifier).add(
                          title: value.title ?? ' ',
                          link: value.link,
                          needLoading: false,);if(newCollectedWebsite ! =null) {
                  state = ViewStateData<WebViewModel>(
                    value: value.copyWith(
                      id: newCollectedWebsite.id,
                      collect: true,),); }}else {
                await reader
                    .call(realProvider.notifier)
                    .requestCancelCollect(collectId: value.id);
              }

              reader.call(realProvider.notifier).switchCollect(
                    id,
                    changedValue: changedValue,
                  );
            } else {
              /// from MyCollectionsScreen - article
              final AutoDisposeStateNotifierProvider<MyCollectedArticleNotifier,
                      RefreshListViewState<CollectedArticleModel>>
                  realProvider = provider as AutoDisposeStateNotifierProvider<
                      MyCollectedArticleNotifier,
                      RefreshListViewState<CollectedArticleModel>>;
              if (changedValue) {
                await WanAndroidAPI.addCollectedArticleByArticleId(
                    articleId: id);
              } else {
                reader.call(realProvider.notifier).requestCancelCollect(
                      collectId: id,
                      articleId: value.originId,
                    );
              }
              reader.call(realProvider.notifier).switchCollect(
                    id,
                    changedValue: changedValue,
                  );

              if (articleOrigin.isNotEmpty) {
                for (int index = 0; index < articleOrigin.length; index++) {
                  if (autoDisposeArticles.contains(articleOrigin[index])) {
                    reader
                        .call((articleOriginProvider[index]
                                asAutoDisposeStateNotifierProvider< BaseArticleNotifier, RefreshListViewState<ArticleModel>>) .notifier) .switchCollect( value.originId! , changedValue: changedValue, ); }else {
                    reader
                        .call((articleOriginProvider[index]
                                asStateNotifierProvider<BaseArticleNotifier, RefreshListViewState<ArticleModel>>) .notifier) .switchCollect( value.originId! , changedValue: changedValue, ); } } } } }else {
            /// from other article screen
            /// eg. HomeArticleScreen, SearchScreen
            if (changedValue) {
              await WanAndroidAPI.addCollectedArticleByArticleId(articleId: id);
            } else {
              await WanAndroidAPI.deleteCollectedArticleByArticleId(
                  articleId: id);
            }

            if (autoDisposeArticles.contains(from)) {
              reader
                  .call((provider as AutoDisposeStateNotifierProvider<
                          BaseArticleNotifier,
                          RefreshListViewState<ArticleModel>>)
                      .notifier)
                  .switchCollect(
                    id,
                    changedValue: changedValue,
                  );
            } else {
              reader
                  .call((provider asStateNotifierProvider<BaseArticleNotifier, RefreshListViewState<ArticleModel>>) .notifier) .switchCollect( id, changedValue: changedValue, ); }}}catch(e, s) { DialogUtils.danger(getError(e, s).message ?? S.current.failed); }}}); }}Copy the code

Performance issues, when the data volume is too large traversal will certainly have problems, then open up, just as learning a riverpod strange usage 🙂

summary

I recently found that Riverpod is 2.0.0-dev.3, and author Remi Rousselet mentioned in this issue that documentation needs to be done on 2.0.0. I also found some examples of authors doing this themselves, basically all variables can be managed by Riverpod. Even Dio and RiverPod themselves also provide FutureProvider and AsyncValue, but maybe because of preconceptions, it is easier to aggregate states and methods together with StateNotifier. State changes and initialization can be controlled freely. At first touch, riverPod advocates a lot like ReactiveX. Data flows like water, you decide where it goes, and as the water at the source changes, it automatically changes where it goes.

Style related

The styling aspect was also considered a lot before the project. Thanks to some experience in adapting dark mode, I wanted to take a good look at ThemeData before doing it

  1. The most important thing about style is color, so how do you do something so complicated with color?
  2. Then is the font, also refer to the inside and then according to their own aesthetic fine tuning, a little more mention, when I just do Theme actually feelheadline.subtitleandbodyTextIt’s kind of abstract and there’s always a little bit of a semantic problem when you actually use it and then you’re limited by the fact that there’s very little to use, very oftensubtitleandbodyTextYou can choose between the two, but becauseTheme.of(context)The form of use can be easily adapted to dark mode and has to be used, and then usedBeta channelWhen the unexpected discovery inMaterial3In the specificationTextThemeIt was redone directlydisplay.headline.title.bodyandlabelAnd everything is divided intoLarge.MediumandSmallI personally feel that it will be easier to use, but I can’t find a familiar one when I first use itheadline6🙂

3. Finally, there is the long ThemeData, which is actually some boring adaptation, the code is here

summary

Overall perception style adapter always still friendly exchanges between the developers and designers, face various dark pattern in recent years, optimum adaptation of ageing, barrier-free adaptation and so on, in fact, if you can consider from the beginning to some actually in the subsequent development process should be much easier, but who can also hard to say all cases are given at the beginning, I do these are actually just based on some of my understanding to do the exploration, only for your reference, according to the need to learn.

conclusion

The project to do now is basically completed my goal except (Web), all want to realize the function of discovering, with some fancy libraries, but also has not completely over, because my original intention is to do a project, for your own project now, but want something for later certainly will be more and more, If there is a fun library, I will try it out here and try the new version of Flutter for the first time.