
双模式设计用户资料页面支持两种模式查看自己username为空和查看他人username非空由 UserProvider 自动切换 API。UserProfile 模型classUserProfile{finalint id;finalStringlogin;finalString?name;finalString?avatarUrl;finalString?htmlUrl;finalString?bio;finalString?company;finalString?location;finalString?email;finalString?blog;finalint followers;finalint following;finalint publicRepos;finalint publicGists;finalDateTimecreatedAt;finalDateTimeupdatedAt;factoryUserProfile.fromJson(MapString,dynamicjson){returnUserProfile(id:parseInt(json[id]),login:parseString(json[login]),name:json[name]asString?,avatarUrl:json[avatar_url]asString?,bio:json[bio]asString?,company:json[company]asString?,location:json[location]asString?,email:json[email]asString?,blog:json[blog]asString?,followers:parseInt(json[followers]),following:parseInt(json[following]),publicRepos:parseInt(json[public_repos]),publicGists:parseInt(json[public_gists]),createdAt:parseDateTime(json[created_at])??DateTime.now(),updatedAt:parseDateTime(json[updated_at])??DateTime.now(),);}}字符串字段保留原始as String?形式因为null未填写和空字符串显式清空是不同的语义。整数字段全部使用parseInt兜底。UserProvider核心加载逻辑classUserProviderextendsChangeNotifier{finalAtomGitApiClient_apiClient;UserProfile?_user;ListRepository_repos[];bool _isLoadingfalse;String?_error;Futurevoidload(Stringusername)async{_isLoadingtrue;_errornull;notifyListeners();try{finalisSelfusername.isEmpty;finaluserApiPathisSelf?/user:/users/${Uri.encodeComponent(username)};finalreposApiPathisSelf?/user/repos:/users/${Uri.encodeComponent(username)}/repos;finalresultsawaitFuture.wait([_apiClient.get(userApiPath),_apiClient.get(reposApiPath,queryParams:{per_page:20,sort:updated}),]);finaluserDataparseMap(results[0].data);if(userData!null){_userUserProfile.fromJson(userData);}_repos(parseListdynamic(results[1].data)??[]).whereTypeMapString,dynamic().map(Repository.fromJson).toList();}onApiExceptioncatch(e){_errore.message;}finally{_isLoadingfalse;notifyListeners();}}}通过isSelf标志切换到不同的 API 端点查看自己用/user查看他人用/users/{username}。用户数据和仓库列表通过Future.wait并行加载。ProfileScreen路由与初始化classProfileScreenextendsStatelessWidget{overrideWidgetbuild(BuildContextcontext){finalusernameModalRoute.of(context)!.settings.argumentsasString???;returnChangeNotifierProvider(create:(_)UserProvider(context.readAtomGitApiClient())..load(username),child:_ProfileBody(username:username),);}}路由参数是一个简单的 String用户名空串表示查看自己。头部卡片Widget_buildProfileHeader(UserProfileuser){returnCard(margin:constEdgeInsets.all(16),child:Padding(padding:constEdgeInsets.all(20),child:Column(children:[UserAvatar(avatarUrl:user.avatarUrl,name:user.name,size:80),constSizedBox(height:12),Text(user.name??user.login,style:Theme.of(context).textTheme.headlineSmall),constSizedBox(height:4),Text(${user.login},style:Theme.of(context).textTheme.bodyMedium?.copyWith(color:Colors.grey,)),if(user.bio!null)...[constSizedBox(height:8),Text(user.bio!,textAlign:TextAlign.center),],constSizedBox(height:16),Row(mainAxisAlignment:MainAxisAlignment.spaceEvenly,children:[_StatCount(label:仓库,count:user.publicRepos),_StatCount(label:关注者,count:user.followers),_StatCount(label:关注中,count:user.following),],),]),),);}统计数字使用单独的组件class_StatCountextendsStatelessWidget{finalStringlabel;finalint count;Widgetbuild(BuildContextcontext){returnColumn(children:[Text($count,style:Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight:FontWeight.bold,)),Text(label,style:Theme.of(context).textTheme.bodySmall),]);}}附加信息行Widget_buildInfoSection(UserProfileuser){returnPadding(padding:constEdgeInsets.symmetric(horizontal:16),child:Column(children:[if(user.company!null)_InfoRow(icon:Icons.business,text:user.company!),if(user.location!null)_InfoRow(icon:Icons.location_on,text:user.location!),if(user.email!null)_InfoRow(icon:Icons.email,text:user.email!),if(user.blog!null)_InfoRow(icon:Icons.link,text:user.blog!),_InfoRow(icon:Icons.calendar_today,text:加入于${DateFormatter.full(user.createdAt)},),]),);}仓库列表区Widget_buildReposSection(ListRepositoryrepos){returnColumn(crossAxisAlignment:CrossAxisAlignment.start,children:[Padding(padding:constEdgeInsets.all(16),child:Text(仓库 (${repos.length}),style:Theme.of(context).textTheme.titleSmall),),...repos.map((repo){finalinforepo.ownerAndName;returnRepoCard(repo:repo,onTap:info!null?()Navigator.pushNamed(context,/repo,arguments:{owner:info.owner,name:info.name}):null,);}),],);}ProfileTab — 我的页面ProfileTab 是底部导航栏中的个人 Tab与 ProfileScreen 不同它内嵌了 UserProvider 的生命周期管理。手动管理 Provider 生命周期classProfileTabextendsStatefulWidget{overrideStateProfileTabcreateState()_ProfileTabState();}class_ProfileTabStateextendsStateProfileTab{UserProvider?_userProvider;overridevoiddidChangeDependencies(){super.didChangeDependencies();finalisLoggedIncontext.readAuthProvider().isLoggedIn;if(isLoggedIn_userProvidernull){_userProviderUserProvider(context.readAtomGitApiClient());WidgetsBinding.instance.addPostFrameCallback((_){_userProvider?.load();});}elseif(!isLoggedIn_userProvider!null){_userProvider?.dispose();_userProvidernull;}}}为什么不用ChangeNotifierProvider(create: ...)因为 ProfileTab 需要响应登录状态变化 —— 登录时创建 Provider登出时销毁Provider 需要在 initState/didChangeDependencies 阶段创建不能依赖 build 中的create使用ChangeNotifierProvider.value包装已存在的实例Widgetbuild(BuildContextcontext){finalisLoggedIncontext.watchAuthProvider().isLoggedIn;if(!isLoggedIn){return_buildLoginPrompt(context);}if(_userProvidernull){returnconstLoadingIndicator();}returnChangeNotifierProvider.value(value:_userProvider!,child:ConsumerUserProvider(builder:(context,provider,_){// ... UI},),);}菜单入口ListTile(leading:constIcon(Icons.code),title:constText(我的仓库),trailing:Text(${user.publicRepos}),onTap:()Navigator.pushNamed(context,/user),),ListTile(leading:constIcon(Icons.star_border),title:constText(收藏仓库),onTap:()Navigator.pushNamed(context,/starred),),ListTile(leading:constIcon(Icons.settings_outlined),title:constText(设置),onTap:()Navigator.pushNamed(context,/settings),),仓库数量使用user.publicReposAPI 返回的总数而不是provider.repos.length当前只加载了前 20 条。