
代码浏览的导航路径从仓库详情页点击代码Tab 进入文件树页面RepoDetailScreen → FileTreeScreen → CodeViewScreen /repo /repo/code /repo/blob参数传递链// 从详情页跳转文件树Navigator.pushNamed(context,/repo/code,arguments:{owner:owner,name:name,branch:repository.defaultBranch??main,});// 从文件树跳转代码查看Navigator.pushNamed(context,/repo/blob,arguments:{owner:owner,name:name,branch:branch,path:node.path,});CodeProvider负责文件树和文件内容的加载是代码浏览的数据核心classCodeProviderextendsChangeNotifier{finalAtomGitApiClient_apiClient;ListFileNode_tree[];String?_fileContent;bool _isLoadingfalse;String?_error;FuturevoidloadFileTree(Stringowner,Stringrepo,Stringbranch,[Stringpath])async{_isLoadingtrue;_errornull;notifyListeners();try{finalencodedOwnerUri.encodeComponent(owner);finalencodedRepoUri.encodeComponent(repo);StringapiPath;if(path.isEmpty){apiPath/repos/$encodedOwner/$encodedRepo/contents;}else{apiPath/repos/$encodedOwner/$encodedRepo/contents/$path;}finalresponseawait_apiClient.get(apiPath,queryParams:{ref:branch});finalitemsparseListdynamic(response.data)??[];_treeitems.whereTypeMapString,dynamic().map(FileNode.fromJson).toList();}onApiExceptioncatch(e){_errore.message;}finally{_isLoadingfalse;notifyListeners();}}}路径为空时加载根目录内容非空时加载子目录内容复用同一个方法。文件内容加载FuturevoidloadFileContent(Stringowner,Stringrepo,Stringpath)async{_isLoadingtrue;_errornull;notifyListeners();try{finalencodedOwnerUri.encodeComponent(owner);finalencodedRepoUri.encodeComponent(repo);finalsegmentspath.split(/);finalencodedPathsegments.map((s)Uri.encodeComponent(s)).join(/);finalresponseawait_apiClient.get(/repos/$encodedOwner/$encodedRepo/contents/$encodedPath,);finaldataparseMap(response.data);if(data!nulldata[content]isString){finalcontentdata[content]asString;_fileContentutf8.decode(base64.decode(content));}}onApiExceptioncatch(e){_errore.message;}finally{_isLoadingfalse;notifyListeners();}}路径中的每个/分段单独编码再拼接保证特殊字符的路径能正确处理。FileNode 模型classFileNode{finalStringname;finalStringpath;finalString?sha;finalint?size;finalStringtype;// blob 或 treefinalListFileNode?children;boolgetisDirectorytypetree;}目录type‘tree’可能有嵌套的 children文件type‘blob’则是叶子节点。FileTreeScreen — 原地目录导航核心交互点击目录不是推入新页面而是在当前页原地刷新内容classFileTreeScreenextendsStatelessWidget{overrideWidgetbuild(BuildContextcontext){finalargsModalRoute.of(context)!.settings.argumentsasMapString,dynamic;finalownerargs[owner]asString;finalnameargs[name]asString;finalbranchargs[branch]asString???main;returnChangeNotifierProvider(create:(_)CodeProvider(context.readAtomGitApiClient())..loadFileTree(owner,name,branch),child:_FileTreeBody(owner:owner,name:name,branch:branch),);}}FileTile 组件根据类型展示不同图标Widget_FileTile(BuildContextcontext,FileNodenode,Stringowner,Stringname,Stringbranch){finalisDirnode.isDirectory;returnListTile(leading:Icon(isDir?Icons.folder:Icons.insert_drive_file_outlined,color:isDir?Theme.of(context).colorScheme.primary:null,),title:Text(node.name),trailing:isDir?constIcon(Icons.chevron_right):Text(_formatSize(node.size??0)),onTap:(){finalprovidercontext.readCodeProvider();if(isDir){provider.loadFileTree(owner,name,branch,node.path);}else{Navigator.pushNamed(context,/repo/blob,arguments:{owner:owner,name:name,branch:branch,path:node.path,});}},);}文件大小格式化String_formatSize(int bytes){if(bytes1024)return$bytesB;if(bytes1024*1024)return${(bytes/1024).toStringAsFixed(1)}KB;return${(bytes/(1024*1024)).toStringAsFixed(1)}MB;}CodeViewScreen — 带行号的代码展示双行 AppBarAppBar(title:Text(fileName),bottom:PreferredSize(preferredSize:constSize.fromHeight(24),child:Padding(padding:constEdgeInsets.only(left:16,bottom:8),child:Align(alignment:Alignment.centerLeft,child:Text(fullPath,style:Theme.of(context).textTheme.bodySmall),),),),)标题显示文件名底部显示完整路径。带行号渲染Widget_buildCodeView(Stringcontent){finallinescontent.split(\n);returnSingleChildScrollView(scrollDirection:Axis.horizontal,child:SingleChildScrollView(child:Column(crossAxisAlignment:CrossAxisAlignment.start,children:List.generate(lines.length,(index){return_CodeLine(lineNumber:index1,content:lines[index],);}),),),);}每行代码由行号和内容组成class_CodeLineextendsStatelessWidget{finalint lineNumber;finalStringcontent;Widgetbuild(BuildContextcontext){finalisDarkTheme.of(context).brightnessBrightness.dark;returnRow(crossAxisAlignment:CrossAxisAlignment.start,children:[Container(width:48,padding:constEdgeInsets.only(right:8),alignment:Alignment.centerRight,color:isDark?Colors.grey[900]:Colors.grey[200],child:Text($lineNumber,style:TextStyle(color:Colors.grey,fontSize:12,fontFamily:monospace,)),),constSizedBox(width:8),Text(content,style:constTextStyle(fontFamily:monospace,fontSize:13,)),],);}}行号固定 48px 宽度右对齐深色模式下自动切换背景色。代码使用等宽字体渲染。API 内容接口两个核心 API接口用途GET /repos/{owner}/{repo}/contents获取根目录文件列表GET /repos/{owner}/{repo}/contents/{path}获取子目录或文件内容两个接口都支持?ref{branch}参数指定分支。文件内容的content字段为 Base64 编码。