在这里插入图片描述

完成了底部导航栏的搭建,现在我们要实现应用的门面——首页。首页是用户打开应用后第一眼看到的界面,它的设计直接影响用户对应用的第一印象。一个好的首页不仅要美观,还要能快速引导用户找到他们想要的内容。

首页的设计思路

在设计首页时,我考虑了几个关键点。首先是信息层次,最重要的内容要放在最显眼的位置。其次是操作便捷性,用户常用的功能要能快速访问。最后是视觉吸引力,要用合适的配色和布局吸引用户继续探索。

基于这些考虑,我把首页分成了几个区域。顶部是一个醒目的横幅,展示应用的主题。接下来是四个快速入口,让用户能直达常用功能。然后是几个内容区块,分别展示今日推荐、热门菜谱、菜系分类等内容。

这种布局方式在很多应用中都能看到,因为它确实有效。用户不需要学习就能理解界面的结构,这就是好设计的标志。

搭建页面框架

首页使用 StatelessWidget 就够了,因为它不需要维护复杂的状态。所有的数据都会从外部传入或者从数据库读取。

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'recommended_recipes_page.dart';
import 'popular_recipes_page.dart';
import 'daily_recommendation_page.dart';
import 'cuisine_category_page.dart';
import 'ingredient_category_page.dart';
import 'difficulty_filter_page.dart';

class CookingHomePage extends StatelessWidget {
  const CookingHomePage({super.key});

导入语句中包含了几个子页面,这些是用户点击"更多"按钮后会跳转到的页面。使用 GetX 的路由管理,页面跳转会变得很简单。

flutter_screenutil 的导入是为了使用响应式尺寸单位。所有的宽度、高度、字体大小都会使用 .w、.h、.sp 这些扩展方法,确保在不同屏幕上显示效果一致。

构建页面结构

首页的整体结构使用 Scaffold 包裹,这是 Flutter 中最常用的页面容器。

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('美食烹饪助手'),
        actions: [
          IconButton(
            icon: const Icon(Icons.search),
            onPressed: () {
              Get.snackbar('提示', '请前往菜谱库进行搜索');
            },
          ),
        ],
      ),

AppBar 显示应用的标题,右侧放了一个搜索图标。点击搜索图标会提示用户去菜谱库进行搜索,因为那里有更完整的搜索功能。这种设计既保持了首页的简洁,又给用户提供了明确的指引。

Get.snackbar 是 GetX 提供的一个便捷方法,可以快速显示一个提示消息。相比 ScaffoldMessenger,它的使用更简单,不需要传递 context。

      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _buildBanner(),
            SizedBox(height: 20.h),
            _buildQuickEntry(),
            SizedBox(height: 20.h),
            _buildSection('今日推荐', Icons.star, () {
              Get.to(() => const DailyRecommendationPage());
            }),
            _buildSection('热门菜谱', Icons.local_fire_department, () {
              Get.to(() => const PopularRecipesPage());
            }),
            _buildSection('菜系分类', Icons.restaurant_menu, () {
              Get.to(() => const CuisineCategoryPage());
            }),
            _buildSection('食材分类', Icons.shopping_basket, () {
              Get.to(() => const IngredientCategoryPage());
            }),
            SizedBox(height: 20.h),
          ],
        ),
      ),
    );
  }

body 使用 SingleChildScrollView 包裹,这样当内容超过屏幕高度时,用户可以滚动查看。Column 的 crossAxisAlignment 设置为 start,让所有子组件左对齐。

各个组件之间用 SizedBox 分隔,高度使用 20.h,这样在不同屏幕上间距都是合适的。把页面拆分成多个小方法,每个方法负责构建一个区域,这样代码结构清晰,也便于维护。

实现横幅区域

横幅是首页最显眼的部分,我们用渐变色背景配合图标和文字来吸引用户注意。

  Widget _buildBanner() {
    return Container(
      height: 180.h,
      margin: EdgeInsets.all(16.w),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(12.r),
        gradient: const LinearGradient(
          colors: [Colors.orange, Colors.deepOrange],
        ),
      ),

Container 的高度设置为 180.h,这个高度既能容纳内容,又不会占用太多屏幕空间。margin 设置为 16.w,让横幅和屏幕边缘保持一定距离。

decoration 使用 BoxDecoration 来设置样式。borderRadius 让容器有圆角,看起来更柔和。gradient 使用线性渐变,从橙色渐变到深橙色,这种渐变效果能让横幅更有层次感。

      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.restaurant, size: 60.sp, color: Colors.white),
            SizedBox(height: 10.h),
            Text(
              '发现美味,享受烹饪',
              style: TextStyle(
                color: Colors.white,
                fontSize: 20.sp,
                fontWeight: FontWeight.bold,
              ),
            ),
          ],
        ),
      ),
    );
  }

内容使用 Center 和 Column 组合,让图标和文字垂直居中排列。restaurant 图标大小设置为 60.sp,颜色为白色,在橙色背景上很醒目。

文字使用白色粗体,字号 20.sp。这句"发现美味,享受烹饪"简洁地传达了应用的核心价值。在设计文案时,简短有力比长篇大论更有效。

构建快速入口

快速入口让用户能直达常用功能,这是提升用户体验的重要设计。

  Widget _buildQuickEntry() {
    final entries = [
      {'icon': Icons.recommend, 'title': '推荐菜谱', 'page': const RecommendedRecipesPage()},
      {'icon': Icons.whatshot, 'title': '热门', 'page': const PopularRecipesPage()},
      {'icon': Icons.star, 'title': '今日推荐', 'page': const DailyRecommendationPage()},
      {'icon': Icons.filter_alt, 'title': '难度筛选', 'page': const DifficultyFilterPage()},
    ];

我定义了一个列表来存储入口信息,每个入口包含图标、标题和目标页面。使用 Map 来组织数据,虽然不如定义一个类那么严谨,但对于这种简单的场景已经够用了。

这四个入口覆盖了用户最常用的功能。推荐菜谱和热门菜谱帮助用户发现新内容,今日推荐提供每日更新的精选,难度筛选让用户能根据自己的水平选择合适的菜谱。

    return Padding(
      padding: EdgeInsets.symmetric(horizontal: 16.w),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: entries.map((entry) {
          return GestureDetector(
            onTap: () => Get.to(() => entry['page'] as Widget),
            child: Column(
              children: [
                Container(
                  width: 60.w,
                  height: 60.w,
                  decoration: BoxDecoration(
                    color: Colors.orange.shade50,
                    borderRadius: BorderRadius.circular(12.r),
                  ),
                  child: Icon(
                    entry['icon'] as IconData,
                    color: Colors.orange,
                    size: 30.sp,
                  ),
                ),
                SizedBox(height: 8.h),
                Text(
                  entry['title'] as String,
                  style: TextStyle(fontSize: 12.sp),
                ),
              ],
            ),
          );
        }).toList(),
      ),
    );
  }

Row 的 mainAxisAlignment 设置为 spaceAround,让四个入口均匀分布。使用 map 方法遍历列表,为每个入口创建一个组件。

GestureDetector 包裹整个入口,让它可以响应点击。点击时使用 Get.to 跳转到对应的页面。这里需要把 page 转换为 Widget 类型,因为 Map 的值类型是 dynamic。

每个入口是一个垂直排列的图标和文字。图标放在一个圆角矩形容器中,背景色使用浅橙色,和主题色呼应。图标本身使用深橙色,形成对比。

实现内容区块

内容区块是首页的主体部分,展示各种菜谱内容。

  Widget _buildSection(String title, IconData icon, VoidCallback onTap) {
    return Column(
      children: [
        Padding(
          padding: EdgeInsets.symmetric(horizontal: 16.w),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Row(
                children: [
                  Icon(icon, color: Colors.orange),
                  SizedBox(width: 8.w),
                  Text(
                    title,
                    style: TextStyle(
                      fontSize: 18.sp,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ],
              ),
              TextButton(
                onPressed: onTap,
                child: const Text('更多'),
              ),
            ],
          ),
        ),

每个区块的标题行包含一个图标、标题文字和"更多"按钮。图标使用橙色,和应用主题保持一致。标题使用粗体,字号 18.sp,比正文大一些,形成层次感。

"更多"按钮使用 TextButton,点击后会跳转到对应的列表页面。这种设计让用户既能在首页快速浏览,又能深入查看更多内容。

        SizedBox(
          height: 200.h,
          child: ListView.builder(
            scrollDirection: Axis.horizontal,
            padding: EdgeInsets.symmetric(horizontal: 16.w),
            itemCount: 5,
            itemBuilder: (context, index) {
              return _buildRecipeCard(index);
            },
          ),
        ),
      ],
    );
  }

内容列表使用 ListView.builder 横向滚动。高度固定为 200.h,这样可以显示完整的卡片。scrollDirection 设置为 horizontal,让列表横向排列。

itemCount 设置为 5,表示每个区块显示 5 个菜谱。实际应用中,这个数字应该根据数据动态确定。使用 builder 模式可以实现懒加载,只有可见的卡片才会被创建。

设计菜谱卡片

菜谱卡片是展示菜谱信息的基本单元,设计要简洁明了。

  Widget _buildRecipeCard(int index) {
    return Container(
      width: 150.w,
      margin: EdgeInsets.only(right: 12.w),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.r),
        boxShadow: [
          BoxShadow(
            color: Colors.grey.withOpacity(0.1),
            blurRadius: 5,
            offset: const Offset(0, 2),
          ),
        ],
      ),

卡片宽度固定为 150.w,右侧留 12.w 的间距。背景色使用白色,在浅灰色的页面背景上能突出显示。

boxShadow 添加了一个轻微的阴影效果。阴影颜色使用半透明的灰色,模糊半径 5,向下偏移 2。这种阴影很微妙,但能让卡片有悬浮的感觉,增加层次感。

      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Container(
            height: 120.h,
            decoration: BoxDecoration(
              color: Colors.orange.shade100,
              borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)),
            ),
            child: Center(
              child: Icon(
                Icons.restaurant,
                size: 50.sp,
                color: Colors.orange,
              ),
            ),
          ),

卡片顶部是图片区域,这里暂时用图标代替。实际应用中,这里应该显示菜谱的图片。容器高度 120.h,背景色使用浅橙色。

borderRadius 只设置顶部的圆角,这样和卡片的整体圆角配合,形成统一的视觉效果。图标居中显示,大小 50.sp,颜色使用深橙色。

          Padding(
            padding: EdgeInsets.all(8.w),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  '美味菜谱 ${index + 1}',
                  style: TextStyle(
                    fontSize: 14.sp,
                    fontWeight: FontWeight.bold,
                  ),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
                SizedBox(height: 4.h),
                Row(
                  children: [
                    Icon(Icons.timer, size: 12.sp, color: Colors.grey),
                    SizedBox(width: 4.w),
                    Text(
                      '30分钟',
                      style: TextStyle(fontSize: 11.sp, color: Colors.grey),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

卡片底部显示菜谱的基本信息。标题使用粗体,字号 14.sp。maxLines 设置为 1,overflow 设置为 ellipsis,这样如果标题太长,会显示省略号。

下面一行显示烹饪时间,使用小图标配合文字。图标和文字都使用灰色,字号较小,作为辅助信息。这种设计让信息层次分明,用户一眼就能抓住重点。

优化滚动体验

首页内容较多,滚动体验很重要。SingleChildScrollView 默认的滚动效果已经不错,但我们还可以做一些优化。

比如可以添加滚动监听,当用户滚动到底部时自动加载更多内容:

final ScrollController _scrollController = ScrollController();


void initState() {
  super.initState();
  _scrollController.addListener(() {
    if (_scrollController.position.pixels == 
        _scrollController.position.maxScrollExtent) {
      // 加载更多
    }
  });
}

不过对于首页来说,内容是固定的,不需要分页加载。这个技巧更适合用在列表页面。

另一个优化是添加下拉刷新功能。用户下拉页面时,可以刷新首页内容:

RefreshIndicator(
  onRefresh: () async {
    // 刷新数据
  },
  child: SingleChildScrollView(
    // ...
  ),
)

RefreshIndicator 会在用户下拉时显示一个加载指示器,并调用 onRefresh 回调。在回调中,我们可以重新获取数据,更新界面。

处理空状态

虽然首页通常都有内容,但我们还是要考虑空状态的情况。比如网络请求失败,或者数据还没加载完成。

可以定义一个状态变量来跟踪加载状态:

enum LoadingState { loading, success, error, empty }

LoadingState _state = LoadingState.loading;

然后根据状态显示不同的界面:

Widget build(BuildContext context) {
  if (_state == LoadingState.loading) {
    return Center(child: CircularProgressIndicator());
  }
  if (_state == LoadingState.error) {
    return Center(child: Text('加载失败,请重试'));
  }
  if (_state == LoadingState.empty) {
    return Center(child: Text('暂无内容'));
  }
  // 正常内容
}

这种处理方式让应用更健壮,即使出现异常情况,用户也能得到明确的反馈。

性能优化技巧

首页是用户最常访问的页面,性能优化尤为重要。

首先是图片优化。如果使用网络图片,要使用缓存机制,避免重复下载。可以使用 cached_network_image 包:

CachedNetworkImage(
  imageUrl: recipe.imageUrl,
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.error),
)

其次是列表优化。ListView.builder 已经实现了懒加载,但如果列表项很复杂,还可以使用 AutomaticKeepAliveClientMixin 来保持列表项的状态。

最后是避免不必要的重建。使用 const 构造函数,把不变的组件标记为 const,这样 Flutter 可以复用它们。

响应式设计考虑

虽然我们使用了 flutter_screenutil 进行适配,但还要考虑一些特殊情况。

比如横屏模式,快速入口可能需要调整布局:

Widget _buildQuickEntry() {
  return LayoutBuilder(
    builder: (context, constraints) {
      final isWide = constraints.maxWidth > 600;
      return isWide ? _buildWideLayout() : _buildNormalLayout();
    },
  );
}

LayoutBuilder 可以获取父容器的约束,根据宽度判断是否需要使用不同的布局。这样可以充分利用大屏幕的空间。

总结

首页的实现看似简单,但其中包含了很多设计考虑。从信息架构到视觉设计,从交互体验到性能优化,每个细节都影响着最终的效果。

我们使用了渐变色横幅吸引注意,快速入口提供便捷访问,内容区块展示丰富信息。整个页面结构清晰,操作流畅,能给用户留下良好的第一印象。

下一篇文章我们将实现推荐菜谱功能,深入探讨列表页面的设计和实现。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐