Flutter:手把手教你使用滚动型列表组件:ListView

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter:手把手教你使用滚动型列表组件:ListView相关的知识,希望对你有一定的参考价值。

参考技术A

ListView的基础创建使用有三种方式:

通过默认构造函数来创建列表,应用场景 = 短列表

这种方式创建的列表存在一个问题:对于那些长列表或者需要较昂贵渲染开销的子组件,即使还没有出现在屏幕中但仍然会被ListView所创建,这将是一项较大的开销,使用不当可能引起性能问题甚至卡顿。

长列表

列表子项之间需要分割线

ListView的进阶使用主要包括:下拉刷新 & 上拉加载

在Flutter中,ListView结合RefreshIndicator组件实现下拉刷新

通过包裹一层RefreshIndicator,自定义onRefresh回调方法实现

方式有两种:

通过ListView.controller属性可以判断ListView是否滑动到了底部,再进行上拉加载

NotificationListener是一个Widget,可监听子Widget发出的Notification

ListView在滑动时中会发出ScrollNotification类型的通知,可通过监听该通知得到ListView的滑动状态,判断是否滑动到了底部,从而进行上拉加载

NotificationListener有一个onNotification属性,定义了监听的回调方法,通过它来处理加载更多逻辑

不定期分享关于 安卓开发 的干货,追求 短、平、快 ,但 却不缺深度

[项目实战,源码完整]手把手教你怎么封装组件,React 重写学成在线 III

看完这篇教程,你能学到以下的知识点:

  • 根据业务需求拆分组件

  • 对复用组件有所了解

  • 子组件如何接受父组件传来的数据——上一期学的是怎么从父组件传递数据给子组件

  • 能使用 useState hook 进行状态管理

  • 使用 JavaScript 中的类

  • 使用 useHistory 钩子函数

  • 使用 history.push() 完成页面的重定向

注: useState 对应 class based component 中的状态,是一个效果非常强大的钩子函数

前情回顾

系列文的第一篇 [万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解 中,主要完成了关于 Header 和 Footer 的封装:

在上一篇 还在烦恼没有项目?手把手带你从 0 开始用 React 重写学成在线 II 中,实现了完成了 banner 的动态实现 和 下面的精品推荐 部分:

在插播的内容 写好了功能/项目不知道怎么展示?手把手带你白嫖 Git Pages 部署自己的项目去惊艳面试官 中讲了如何利用 GitHub 去部署自己的项目,这个项目的在线浏览地址在这里:https://goldenaarcher.com/xczx-react/#/

到这里,整个页面已经可以说完成了 40%了,只要再模块化三四个组件,首页剩下的内容几乎都能够动态生成。

进行业务分析

在开始具体的实现之前,我们还是应该对业务进行分析,看看怎么实现才能够达到最大效果的代码复用。

首先,看看剩下还没有实现的几个模块:

  • 一个精品推荐

  • 一个领域推荐

  • 一个领域推荐

  • 一个课程推荐

  • 一个课程推荐

  • 一个导师推荐

可以看出,整体的结构其实很相似,都是一个 header + 内容 的表现方式,而且所有的 header 的样式基本是一样的,只是包含的内容多少而已。

拆分一下,header 的格式就是这样的:

最左边的 title + 中间的热门点击 + 最右边的查看更多

所以第一步就先将这个 header 封装出来。

内容区其实也可以被合理的分成 3 个大模块:

  1. 精品推荐和课程推荐

    这里其实能看到,精品推荐和课程推荐中的内容区从结构上来说都是一样的——都是一排排的课程

    也因此可以将这两个内容合并成同一个模块,让它们渲染给定的课程即可

    对比:

    精品推荐课程推荐
  2. 领域推荐

    两个领域的结构也是一样的,分为:

    • 左边的 banner
    • 上面的 banner
    • 一系列的课程推荐

    鉴于这一块的布局会和 精品/课程 推荐不一样,所以再单独拆分一个模块

  3. 导师推荐

    这个是和其他的模块差异最大的,自然而然需要被分出来

这一期内容的目的就是:

  • 完成 sub header 的封装
  • 完成 精品推荐和课程推荐 的基础实现

内容实现

这次就只完成一个模块:

当完成了之后你就会发现,剩下的内容渲染起来是真的很快。

subHeader 的实现

我把出现的 3 个 subHeader 全都放在了一起,这么一对比,应该就能看出结构是多么的相似,并且是可以实现复用的:

毕竟,subHeader 分割下来其实有三个部分:最左边的标题,中间的选项,以及右边的查看更多。

实现基础结构

首先,新建一个组件,这个组件需要明确接收 3 个值:

  • 最左的标题,必须

    这里命名为 subHeaderName

  • 中间的选取,可选

    这里命名为 midConent

  • 右侧的查看更多,可选

    这里命名为 checkMore,指查看更多

代码实现:

import React from 'react';

const SubHeader = (props) => {
  const { subHeaderName, midConent, checkMore } = props;

  return (
    <div className="flex space-between">
      <div>{subHeaderName}</div>
      <div>{midConent.map((val) => val)}</div>
      <div>{checkMore}</div>
    </div>
  );
};

export default SubHeader;

什么是 props

这里的 props 是用来接收父组件传来的数据的,在上一期轮播图中讲了父组件如何向子组件传递数据,这里讲的就是子组件如何从父组件中接收数据。

父组件向子组件传递数据的方法:

const settings = {
  data: 'test',
}

<Child {...settings} />

父组件传到子组件的这些属性最终会被 React 压缩到一个对象中,约定俗成的这个对象叫做 props,这也是上文使用的。

从 props 中获取数据的方法也很简单,就像从一般的对象中获取属性一样,直接使用 props.data,或者像上文一样解构数据都可以获得:

// using props directly
alert(props.data);

// using object destruct
const { data } = props;
alert(data);

尝试渲染

加上一点点 CSS 后,做一个测试数据来看看渲染效果吧。

先从父组件中传递一些假数据进去:

const Home = () => {
  return (
    // 省略其他
    <div className="homepage-main">
      <SubHeader
        subHeaderName="编程入门"
        midConent={[123]}
        checkMore={'true'}
      />
    </div>
  );
};

看起来效果还不错,现在需要设计的就是传进去的数据类型了。

设计数据类型

已知 sub header 会接受 3 个数据类型:subHeaderName, midConent, 和 checkMore

  • subHeaderName 是一个纯字符串,这个也是最简单的。

  • midConent 最终的决定是由一个由 对象 组成的数组:

    [
      { title: '热门', url: '/#' },
      { title: '初级', url: '/#' },
      { title: '中级', url: '/#' },
      { title: '高级', url: '/#' },
    ];
    
  • checkMore 则是单独的一个对象:

    {
      title: '查看全部',
      url: '/#',
    };
    

其实为了省事儿单独的写 <a href='/#> 一些标题 </a> 也不是不可以,嫌麻烦的也可以直接这么写。

这种就属于每个人都会不太一样的编码风格和习惯问题,对于我来说将这些常量提取出去,一旦要修改或是清理数据都会方便很多。毕竟真实的项目中,如果后台数据没有准备好的话,大多数情况下是要自己准备 伪数据,而不是等后台开发完毕之后再使用后台传来的数据进行开发。

‼️ 所以,一定要和你的后台提前沟通好传来的数据结构是什么样子的,有文档最好,没有文档 一定 要留下邮件或是微信聊天记录作为证据,以防 被甩锅

修改实现方法,重新渲染 sub header

中间的内容区依旧会使用 ul > li > a 的经典结构,并且使用 arr.map() 去进行遍历。

import React from 'react';

const SubHeader = (props) => {
  const { subHeaderName, midConent, checkMore } = props;

  const getMidContent = () => {
    return (
      <ul className="flex">
        {midConent.map((val) => (
          <li key={val.title}>
            <a href={val.url}>{val.title}</a>
          </li>
        ))}
      </ul>
    );
  };

  return (
    <div className="sub-header flex space-between flex-center">
      <h3>{subHeaderName}</h3>
      {getMidContent()}
      <div>
        <a href={checkMore.url}>{checkMore.title}</a>
      </div>
    </div>
  );
};

到这一步,距离 sub header 实现完毕还差最后一步——选中高亮的实现。

以下为静态的实现效果:

使用 useState 去实现选中高亮

选中高亮的逻辑实现起来其实并不复杂,具体步骤如下:

  1. 需要一个变量保存当前选中的选项,默认为 热门
  2. 在点击事件发生时,需要替换被选中的选项

这个部分的实现就需要借助 React 自带的一个钩子函数:setStatesetState 的存在是为了替换 类组件(class-based component) 中的 状态(state) 属性。

语法为:

建议使用 ES6 中的 const
const [property, propertyHandler] = useState(defaultValue);

其中

  • property 为该属性的变量名

  • propertyHandler 为事件发生时用来更新状态的函数

    这个函数只是一个标识符,它不负责处理其他的逻辑,只负责更新传进去的值

  • useState 中传进去的为默认值

  • 在需要更新状态的事件函数中调用 propertyHandler 即可。

以本业务场景为例,初始值的代码如下;

const [activeTitle, setTitle] = useState(midConent[0].title);

// 点击事件
function clickHandler(e, val) {
  // 防止新页面打开 and/or 重定向的发生
  e.preventDefault();
  // 更新状态
  setTitle(val);
}
  • activeTitle 定义的是当前被选中,需要被设为高亮的选项

  • setTitle 为 点击标签时设置当前高亮选项 的函数

    它会接受一个值,并且直接替换 activeTitle

  • useState 设置的是默认值,在这个情况下也就是热门——第一个选项的标题

点击事件 clickHandler 会还给每一个 a 标签,在 a 标签被鼠标点击后就会触发这个状态,更新当前显示高亮的标签。

实现后的动态效果:

到这一步,标题的实现就完成了。

完整 JSX 代码如下:

  • SubHeader

    import React, { useState } from 'react';
    import PropTypes from 'prop-types';
    
    const SubHeader = (props) => {
      // =================新增部分===================
      const [activeTitle, setTitle] = useState(midConent[0].title);
    
      function clickHandler(e, val) {
        e.preventDefault();
        setTitle(val);
      }
      // =================新增部分===================
      const getMidContent = () => {
        return (
          <ul className="flex">
            {midConent.map((val) => {
              const { title } = val;
              return (
                <li key={title}>
                  <a
                    href={val.url}
                    onClick={(e) => clickHandler(e, title)}
                    className={activeTitle === title ? 'selected' : null}
                  >
                    {title}
                  </a>
                </li>
              );
            })}
          </ul>
        );
      };
    };
    
    export default SubHeader;
    
  • Home

    const Home = () => {
      return (
        <div className="homepage relative">
          <HomeBanner />
          <div className="container">
            <FieldSuggestion />
            <div className="homepage-main">
              <SubHeader
                subHeaderName="编程入门"
                midConent={subHeaderOl}
                checkMore={checkMore}
              />
            </div>
          </div>
        </div>
      );
    };
    

精品推荐和课程推荐 的实现

标题已经实现了,剩下的内容就是一排排的课程:

很明显,每一个课程的结构都是一样的:

  • 课程图片
  • 课程标题
  • 课程描述
  • 新款课程(右上角的小标题)
  • 爆款课程(右上角的小标题)

那么,第一步可以先将单独的 课程 封装起来,随后再循环遍历所有的课程列表,就能够获得一排排的课表了。

封装课程组件

这就是之前在 common 里就设计好的 CourseItem 组件。

根据上面列出的课程结构,所以课程组件会需要获取以下这些属性:

  • id,上文没显示的内容,这里是作为一个标识符而存在的
  • title, 标题,也就是课程名称
  • img, 课程图片
  • peopleStudying,多少人在学习,属于课程描述的内容
  • courseLevel,初级中级高级,也属于课程描述的内容
  • isNew 或 isHot,用或是因为 PSD 的设计中只显示了一个,不过具体的检查我偷懒了,也没做
课程主体

暂时不考虑 isNew 或 isHot,先将主体结构实现:

import React from 'react';

const CourseItem = (props) => {
  const { peopleStudying, courseLevel, id, title, img, isNew, isHot } = props;

  return (
    <div className="course-item">
      <img src={img} />
      <h4>{title}</h4>
      <p className="course-info">
        <span className="course-level">{courseLevel}</span>· {peopleStudying}
        人正在学习
      </p>
    </div>
  );
};

export default CourseItem;

随后放入假数据:

import php from '../asset/img/courses/php.png';

class Course {
  // 偷一下懒,学习人数和等级写死了
  // 也可以传到构造函数中去,但是PSD上都一样,我就……
  peopleStudying = 1125;
  courseLevel = '高级';
  constructor(id, title, img, isNew = false, isHot = false) {
    this.id = id;
    this.title = title;
    this.img = img;
    this.isNew = isNew;
    this.isHot = isHot;
  }
}

const coursePhp = new Course(1, 'test', php, false, true);

// course list for home page
export const courseSuggestion1 = [coursePhp];

和 CSS:

.course-item {
  width: 228px;
  height: 270px;
  background-color: #fff;
  margin: 0 15px 15px 0;
}
.course-img {
  width: 100%;
}
.course-item h4 {
  margin: 20px;
  font-size: 14px;
  color: #050505;
  font-weight: 400;
}
.course-info {
  margin: 0 20px;
  font-size: 12px;
  color: #999;
}
.course-level {
  color: #ff2c2d;
}

这样一来,效果就已经有了:

完整实现课程

引入之前拉下来的两个图片,这里偷懒了没有检查排他性——即只能存在 isNewisHot,而不能二者同时存在。

正式开发中如果有需求的话,这个检查时一定要做的。

然后利用三元表达是去检查 isNewisHot 是否为 true,如果是的话就渲染对应的组件,如果不是的话就渲染 null。

又因为两个图标的效果是完全一致的,我这里继续抽了一个函数出来去实现 icon:

import React from 'react';
import './courseItem.css';
import hotLabel from '../../asset/img/courses/hot.png';
import newLabel from '../../asset/img/courses/new.png';

const getLabelImg = (img, label) => {
  return <img src={img} alt={label} class={label} />;
};

const CourseItem = (props) => {
  const { peopleStudying, courseLevel, id, title, img, isNew, isHot } = props;

  const isNewCourse = isNew
    ? getLabelImg(newLabel, 'new-course absolute')
    : null;
  const isHotCourse = isHot
    ? getLabelImg(hotLabel, 'hot-course absolute')
    : null;

  return (
    <div className="course-item relative">
      <img src={img} alt={title} className="course-img" />
      <h4>{title}</h4>
      <p className="course-info">
        <span className="course-level">{courseLevel}</span>· {peopleStudying}
        人正在学习
      </p>
      {isNewCourse}
      {isHotCourse}
    </div>
  );
};

export default CourseItem;

实现效果:

有点这个意思了。

添加课程列表的数据

一个个手动复制黏贴就是非常浪费时间的事情了,所以下一步就是封装伪数据。

这里主要就是创建一个课程的类,这样可以快速的实例化课程,以及创造几个假数据:

import php from '../asset/img/courses/php.png';
import andriod from '../asset/img/courses/andriod.png';
import angular from '../asset/img/courses/angular.png';
import androidHybrid from '../asset/img/courses/andriod-hybrid.png';

class Course {
  // 偷一下懒,学习人数和等级写死了
  // 也可以传到构造函数中去,但是PSD上都一样,我就……
  peopleStudying = 1125;
  courseLevel = '高级';
  constructor(id, title, img, isNew = false, isHot = false) {
    this.id = id;
    this.title = title;
    this.img = img;
    this.isNew = isNew;
    this.isHot = isHot;
  }
}

const coursePhp = new Course(
  1,
  'Think PHP 5.0 博客系统实战项目演练',
  php,
  false,
  true
);
const courseAndriod = new Course(
  2,
  'Android 网络图片加载框架详解',
  andriod,
  true
);
const courseAngular = new Course(
  3,
  'Angular 2 最新框架+主流技术+项目实战',
  angular
);
const courseAndroidHybrid = new Course(
  4,
  'Android Hybrid APP开发实战 H5+原生!',
  androidHybrid
);
const courseAndroidHybrid2 = new Course(
  5,
  'Android Hybrid APP开发实战 H5+原生!',
  androidHybrid
);

// course list for home page
export const courseSuggestion1 = [
  coursePhp,
  courseAndriod,
  courseAngular,
  courseAndroidHybrid,
  courseAndroidHybrid2,
];

第一组伪数据就实现好了,接下来在 CourseSuggestion 组建中引入这个列表中,进行循环遍历出所有的课程:

import React from 'react';
import SubHeader from '../subHeader';
import { subHeaderOl, checkMore } from '../../../constants/home';
import { courseSuggestion1 } from '../../../constants/courseList';
import CourseItem from '../../../common/courseItem';

// 渲染课程列表,不含其他的内容模块
const CourseSuggestion = (props) => {
  return (
    <div>
      <SubHeader
        subHeaderName="编程入门"
        midConent={subHeaderOl}
        checkMore={checkMore}
      />
      <div className="flex">
        {courseSuggestion1.map((course) => (
          <CourseItem {...course} key={course.id以上是关于Flutter:手把手教你使用滚动型列表组件:ListView的主要内容,如果未能解决你的问题,请参考以下文章

Flutter滚动型容器组件 - ListView篇

手把手教你编译Flutter engine

[项目实战,源码完整]手把手教你怎么封装组件,React 重写学成在线 III

Android Flutter:手把手教你如何进行Android 与 Flutter的相互通信

京东架构师手把手教你如何使用异步编程,提升服务性能

解放前端工程师——手把手教你开发自己的自定义列表和自定义表单系列之三表格