AtomGit Flutter鸿蒙客户端:用户资料

双模式设计

用户资料页面支持两种模式:查看自己(username 为空)和查看他人(username 非空),由 UserProvider 自动切换 API。

UserProfile 模型

dart 复制代码
class UserProfile {
  final int id;
  final String login;
  final String? name;
  final String? avatarUrl;
  final String? htmlUrl;
  final String? bio;
  final String? company;
  final String? location;
  final String? email;
  final String? blog;
  final int followers;
  final int following;
  final int publicRepos;
  final int publicGists;
  final DateTime createdAt;
  final DateTime updatedAt;

  factory UserProfile.fromJson(Map<String, dynamic> json) {
    return UserProfile(
      id: parseInt(json['id']),
      login: parseString(json['login']),
      name: json['name'] as String?,
      avatarUrl: json['avatar_url'] as String?,
      bio: json['bio'] as String?,
      company: json['company'] as String?,
      location: json['location'] as String?,
      email: json['email'] as String?,
      blog: json['blog'] as String?,
      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

核心加载逻辑

dart 复制代码
class UserProvider extends ChangeNotifier {
  final AtomGitApiClient _apiClient;
  UserProfile? _user;
  List<Repository> _repos = [];
  bool _isLoading = false;
  String? _error;

  Future<void> load(String username) async {
    _isLoading = true;
    _error = null;
    notifyListeners();

    try {
      final isSelf = username.isEmpty;

      final userApiPath =
          isSelf ? '/user' : '/users/${Uri.encodeComponent(username)}';
      final reposApiPath = isSelf
          ? '/user/repos'
          : '/users/${Uri.encodeComponent(username)}/repos';

      final results = await Future.wait([
        _apiClient.get(userApiPath),
        _apiClient.get(reposApiPath,
            queryParams: {'per_page': '20', 'sort': 'updated'}),
      ]);

      final userData = parseMap(results[0].data);
      if (userData != null) {
        _user = UserProfile.fromJson(userData);
      }

      _repos = (parseList<dynamic>(results[1].data) ?? [])
          .whereType<Map<String, dynamic>>()
          .map(Repository.fromJson)
          .toList();
    } on ApiException catch (e) {
      _error = e.message;
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

通过 isSelf 标志切换到不同的 API 端点:查看自己用 /user,查看他人用 /users/{username}。用户数据和仓库列表通过 Future.wait 并行加载。

ProfileScreen

路由与初始化

dart 复制代码
class ProfileScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final username =
        ModalRoute.of(context)!.settings.arguments as String? ?? '';

    return ChangeNotifierProvider(
      create: (_) =>
          UserProvider(context.read<AtomGitApiClient>())..load(username),
      child: _ProfileBody(username: username),
    );
  }
}

路由参数是一个简单的 String(用户名),空串表示查看自己。

头部卡片

dart 复制代码
Widget _buildProfileHeader(UserProfile user) {
  return Card(
    margin: const EdgeInsets.all(16),
    child: Padding(
      padding: const EdgeInsets.all(20),
      child: Column(children: [
        UserAvatar(avatarUrl: user.avatarUrl, name: user.name, size: 80),
        const SizedBox(height: 12),
        Text(user.name ?? user.login,
            style: Theme.of(context).textTheme.headlineSmall),
        const SizedBox(height: 4),
        Text('@${user.login}',
            style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                  color: Colors.grey,
                )),
        if (user.bio != null) ...[
          const SizedBox(height: 8),
          Text(user.bio!, textAlign: TextAlign.center),
        ],
        const SizedBox(height: 16),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            _StatCount(label: '仓库', count: user.publicRepos),
            _StatCount(label: '关注者', count: user.followers),
            _StatCount(label: '关注中', count: user.following),
          ],
        ),
      ]),
    ),
  );
}

统计数字使用单独的组件:

dart 复制代码
class _StatCount extends StatelessWidget {
  final String label;
  final int count;

  Widget build(BuildContext context) {
    return Column(children: [
      Text('$count',
          style: Theme.of(context).textTheme.titleLarge?.copyWith(
                fontWeight: FontWeight.bold,
              )),
      Text(label, style: Theme.of(context).textTheme.bodySmall),
    ]);
  }
}

附加信息行

dart 复制代码
Widget _buildInfoSection(UserProfile user) {
  return Padding(
    padding: const EdgeInsets.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)}',
      ),
    ]),
  );
}

仓库列表区

dart 复制代码
Widget _buildReposSection(List<Repository> repos) {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Padding(
        padding: const EdgeInsets.all(16),
        child: Text('仓库 (${repos.length})',
            style: Theme.of(context).textTheme.titleSmall),
      ),
      ...repos.map((repo) {
        final info = repo.ownerAndName;
        return RepoCard(
          repo: repo,
          onTap: info != null
              ? () => Navigator.pushNamed(context, '/repo',
                  arguments: {'owner': info.owner, 'name': info.name})
              : null,
        );
      }),
    ],
  );
}

ProfileTab --- "我的"页面

ProfileTab 是底部导航栏中的个人 Tab,与 ProfileScreen 不同,它内嵌了 UserProvider 的生命周期管理。

手动管理 Provider 生命周期

dart 复制代码
class ProfileTab extends StatefulWidget {
  @override
  State<ProfileTab> createState() => _ProfileTabState();
}

class _ProfileTabState extends State<ProfileTab> {
  UserProvider? _userProvider;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final isLoggedIn = context.read<AuthProvider>().isLoggedIn;
    if (isLoggedIn && _userProvider == null) {
      _userProvider = UserProvider(context.read<AtomGitApiClient>());
      WidgetsBinding.instance.addPostFrameCallback((_) {
        _userProvider?.load('');
      });
    } else if (!isLoggedIn && _userProvider != null) {
      _userProvider?.dispose();
      _userProvider = null;
    }
  }
}

为什么不用 ChangeNotifierProvider(create: ...)

因为 ProfileTab 需要:

  1. 响应登录状态变化 ------ 登录时创建 Provider,登出时销毁
  2. Provider 需要在 initState/didChangeDependencies 阶段创建,不能依赖 build 中的 create
  3. 使用 ChangeNotifierProvider.value 包装已存在的实例
dart 复制代码
Widget build(BuildContext context) {
  final isLoggedIn = context.watch<AuthProvider>().isLoggedIn;

  if (!isLoggedIn) {
    return _buildLoginPrompt(context);
  }

  if (_userProvider == null) {
    return const LoadingIndicator();
  }

  return ChangeNotifierProvider.value(
    value: _userProvider!,
    child: Consumer<UserProvider>(
      builder: (context, provider, _) {
        // ... UI
      },
    ),
  );
}

菜单入口

dart 复制代码
ListTile(
  leading: const Icon(Icons.code),
  title: const Text('我的仓库'),
  trailing: Text('${user.publicRepos}'),
  onTap: () => Navigator.pushNamed(context, '/user'),
),
ListTile(
  leading: const Icon(Icons.star_border),
  title: const Text('收藏仓库'),
  onTap: () => Navigator.pushNamed(context, '/starred'),
),
ListTile(
  leading: const Icon(Icons.settings_outlined),
  title: const Text('设置'),
  onTap: () => Navigator.pushNamed(context, '/settings'),
),

仓库数量使用 user.publicRepos(API 返回的总数),而不是 provider.repos.length(当前只加载了前 20 条)。

相关推荐
悟空瞎说1 小时前
Flutter 三大主流本地存储全解:SharedPreferences、Hive、SQLite 实战指南
flutter
SL-staff1 小时前
Web 白板技术架构深度解析:从渲染到协作的选型哲学
前端·架构
悟空瞎说1 小时前
Flutter Isolate 与 compute 全方位实战指南:后台任务优化,保障 UI 60 帧流畅
flutter
前端冒菜师1 小时前
别急着做 Agent,AI 工程化的第一步是 Skill 化
架构·ai编程
风华圆舞2 小时前
Stage 模型下 Flutter 鸿蒙壳工程怎么理解
flutter·华为·harmonyos
Patrick_Wilson2 小时前
为省一次回归测试,该不该把多个改动堆进一条分支?
git·ci/cd·架构
●VON2 小时前
AtomGit Flutter鸿蒙客户端:数据模型
android·服务器·安全·flutter·harmonyos·鸿蒙