IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Flutter For App——一个简单的豆瓣APP -> 正文阅读

[移动开发]Flutter For App——一个简单的豆瓣APP

效果视频

豆瓣

功能简述

功能

  • Top250榜单
  • 电影详情
  • 电影收藏列表
  • 模糊搜索
  • 搜索记录
  • 搜索列表
  • 清空搜索记录
  • 侧滑删除收藏影片

第三方库

名称备注
dio网路库
provider状态管理
sqlfite数据库
fluttertoastToast提示
flutter_swiperBanner
pk_skeleton骨架屏

接口简述

由于豆瓣接口现在很多都有了限制,此项目使用的是Github某人整理的接口地址,能用的总共有四个接口,其中搜索相关的两个接口,30s内只允许访问一次,连续访问会出现code:429,访问频繁异常,剩余两个接口没有限制

  • 获取前250名电影榜单:
`https://api.wmdb.tv/api/v1/top?type=Imdb&skip=0&limit=50&lang=Cn`
  • 根据豆瓣id进行搜索
https://api.wmdb.tv/movie/api?id=doubanid
  • 根据影片关键字进行搜索
https://api.wmdb.tv/api/v1/movie/search?q=英雄本色&limit=10&skip=0&lang=Cn&year=2002
  • 电影宣传海报生成
https://api.wmdb.tv/movie/api/generateimage?doubanId=1306123&lang=En

底部导航栏

每一个BottomNavigationBarItem的背景颜色都不相同,每次初始化都是随机获取

效果图

实现

初始化BottomNavigationBarItem

三个Item的背景颜色通过随机进行获取

///底部导航栏数据
final pageList = [const HomePage(),const SearchPage(),const MinePage()];

final navList = [
  BottomNavigationBarItem(
      icon: const Icon(Icons.home),
      label: '首页',
      backgroundColor: Color.fromARGB(255, Random().nextInt(255), Random().nextInt(255), Random().nextInt(255))
  ),
  BottomNavigationBarItem(
      icon: const Icon(Icons.search),
      label: '搜索',
      backgroundColor: Color.fromARGB(255, Random().nextInt(255), Random().nextInt(255), Random().nextInt(255))
  ),
  BottomNavigationBarItem(
      icon: const Icon(Icons.person),
      label: '我的',
      backgroundColor: Color.fromARGB(255, Random().nextInt(255), Random().nextInt(255), Random().nextInt(255))
  )
];

bottomNavigationBar

定义记录当前页面序号

var _currentPage = 0;
  • items:对应的是Navigation的子项
  • currentIndex:当前页面的序号
  • type:底部导航栏的显示方式
  • onTap:点击事件
bottomNavigationBar: BottomNavigationBar(
        items: navList,
        currentIndex: _currentPage,
        type: BottomNavigationBarType.shifting,
        onTap: (index){
          _changeIndex(index);
        },
      )

切换页面事情,做一个简单判断,防止点击当前页面重复执行

void _changeIndex(int index){
    if(index != _currentPage){
      setState(() {
        _currentPage = index;
      });
    }
  }

切换页面导致bottomNavigationBar子页面重绘

通过使用IndexedStack,在进行子页面进行切换的时候就不会重新进行加载

body: IndexedStack(
        index: _currentPage,
        children: pageList,
      )

Top250榜单

通过FutureBuilder执行网络请求方法,然后判断数据是否返法,在waiting状态下显示骨架屏loading,等数据返回之后通知Provider进行刷新,然后在局部刷新电影ListView

效果图

实现

Widget树

次Widget只是通过Column和Row进行一个简单的描述,没有完全绘制整个Widget树
在这里插入图片描述

FutureBuilder

其中getTopList()是获取Top250榜单的网络请求,具体内容网络封装请看Dio封装

获取Top250榜单数据并进行解析

///电影榜单前250名接口数据解析
Future<List<ITopEntity>?> getTopList(int skip,int limit) async {
    var url = HttpPath.getTop250(skip, limit);
    final result = await HttpUtil.getInstance().get(url, null);
    return jsonConvert.convertListNotNull<ITopEntity>(result);
}

构建FutureBuilder


  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(10.0),
      color: Colors.white,
      child: FutureBuilder(
          future: getTopList(skip, limit),
          builder: _buildFutureBuild
      ),
    );
  }

然后对FutureBuilder每个状态进行监听,其中waiting状态返回的是骨架屏组件,然后在数据返回之后开始构建ListView

  ///FutureBuild+骨架屏
  ///FutureBuild会执行两次,第一次状态为waiting,第二次为done(在正常情况下)
  Widget _buildFutureBuild(BuildContext context, AsyncSnapshot<List<ITopEntity>?> snapshot){
    switch(snapshot.connectionState){
      case ConnectionState.none:
        return const Text('未开始网络请求...');
      case ConnectionState.active:
        return const Text('开始网络请求...');
      case ConnectionState.waiting:
        return PKCardPageSkeleton(totalLines: 6);
      case ConnectionState.done:
        if(snapshot.hasError) {
          return const Text('请求过于频繁,请稍后再试...');
        }else{
         if(snapshot.data == null) {
           return const Center(child: Text('请求过于频繁,请稍后再试...'));
         } else {
           Widget widget = _buildMovie(snapshot.data!);
           WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
             _loadMovieList(snapshot.data!);
           });
           return widget;
         }
        }
    }
  }

异常

当我在使用FutureBuilder构建ListView的时候,当数据返回时,我需要通过Provider数据已经获取,可以开始刷新,但是我当前的ListView正在构建中,所以它会提示如下警告,但是不影响代码执行,我通过将数据刷新用帧回调进行监听,意味按顺序执行,只有我的布局构建完成之后,才会进行此回调并且此回调只会执行一次。

 WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
             _loadMovieList(snapshot.data!);
           });
 // The following assertion was thrown while dispatching notifications for MovieProvider:
  // setState() or markNeedsBuild() called during build.
  //
  // This _InheritedProviderScope<MovieProvider> widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
  // The widget on which setState() or markNeedsBuild() was called was: _InheritedProviderScope<MovieProvider>
  //   value: Instance of 'MovieProvider'
  //   listening to value
  // The widget which was currently being built when the offending call was made was: FutureBuilder<List<ITopEntity>?>
  //   dirty
  //   state: _FutureBuilderState<List<ITopEntity>?>#aba8e

ListView上拉加载

  1. 定义上拉加载控制器
 final ScrollController _scrollController = ScrollController();
  1. 添加上拉监听
///这个判断相当于屏幕底部高度和手指滑动到底部高度一致时执行上拉操作
@override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      if(_scrollController.position.pixels == _scrollController.position.maxScrollExtent){
        skip += limit;
        _loadMovieData();
      }
    });
  }

使用provider进行状态管理,然后将获取的数据进行添加,并进行局部刷新

 ///上拉加载
  Future _loadMovieData() async{
      await getTopList(skip,limit).then((value){
          final movieProvider = context.read<MovieProvider>();
          if(value != null){
            movieProvider.setList = value;
          }
        });
  }
  1. 构建ListView
///电影ListView
  ///电影Provider修改作用域
  Widget _buildMovie(List<ITopEntity> entityList){
    return Consumer<MovieProvider>(
      builder: (ctx,movieProvider,child){
        return ListView.builder(
            itemCount: movieProvider.getList.length,
            controller: _scrollController,
            itemBuilder:(context,index){
              return createMovieItem(movieProvider.getList[index],index);
            });
      },
    );
  }

电影详情

效果图

实现

依旧使用FutureBuilder实现数据请求然后在数据返回之后进行填充

高斯模糊

背景图片是通过回调之后数据,然后进行模糊处理,模糊比例可以通过传入sigmaXsigmaY值进行修改

ImageFiltered(
                      imageFilter: ImageFilter.blur(sigmaX:10,sigmaY: 10),
                      child: Image.network(
                          snapshot.data?.data[0].poster ?? '',
                          fit: BoxFit.fill,
                          width: double.infinity,
                          height: double.infinity,),
                  )

网络数据获取

通过传入的doubanId作为参数进行网络请求,然后进行解析返回

///电影详情数据解析
Future<IMovieDetailEntity> getMovieDetail(String doubanid) async{
  var url = HttpPath.getMovieDetail(doubanid);
  final result = await HttpUtil.getInstance().get(url, null);
  return IMovieDetailEntity.fromJson(result);
}

电影详情

电影海报和下面由白色背景的包裹的电影信息描述,通过使用StackPosition进行显示,同时使用了SingleChildScrollView滑动组件进行包裹。

///电影海报和电影描述重叠
  Widget buildMovieDetail(BuildContext context,IMovieDetailEntity? entity){
    if(entity == null) return const Text('请求过于频繁,请稍后再试...');
    final height = MediaQuery.of(context).size.height;
    final width = MediaQuery.of(context).size.width;
   return SingleChildScrollView(
     scrollDirection: Axis.vertical,
     physics: const BouncingScrollPhysics(),
     reverse: false,
     child: Container(
           width: width,
           height: height,
           padding: const EdgeInsets.only(top: 20.0),
           //color: Colors.grey,
           alignment: Alignment.topCenter,
           child: Stack(
             children: [
               _buildMovieDescribe(entity),
               Positioned(
                   top: 0,
                   left: 0,
                   right: 0,
                   child: buildMovieImage(entity.data[0].poster,250.0))
             ],
           ),
         ),
     );
  }

具体内容通过Column布局垂直显示,由于篇幅原因,具体子Widget展示内容就不进行介绍

 ///电影描述
  Widget _buildMovieDescribe(IMovieDetailEntity entity){
    return Container(
      width: double.infinity,
      height: double.infinity,
      margin: const EdgeInsets.only(top: 180.0,left: 15.0,right: 15.0,bottom: 20.0),
      decoration: const BoxDecoration(
          shape: BoxShape.rectangle,
          color: Colors.white,
          borderRadius: BorderRadius.all(Radius.circular(10.0))
      ),
      child: Padding(
        padding: const EdgeInsets.only(top: 100.0,left: 10,right: 10,bottom: 0),
        child:Column(
          children: [
            getText(entity.data[0].name,textSize: 20.0,fontWeight: FontWeight.bold),
            const SizedBox(height: 5.0),
            getText(entity.data.length > 1 ? entity.data[1].name : 'unknown',textSize: 16.0,color: Colors.grey),
            const SizedBox(height: 20.0),
            _buildYearCountryWidget(entity),
            const SizedBox(height: 10.0),
            _buildGenreWidget(entity),
            const SizedBox(height: 20.0),
            _buildCollection(entity),
            const SizedBox(height: 20.0),
            getText(entity.data[0].description,textSize: 12,color: Colors.black,maxLine: 10),
            const SizedBox(height: 10.0),
            getText(entity.data.length > 1 ? entity.data[1].description : 'unknown...',textSize: 12,color: Colors.black,maxLine: 10),
            const SizedBox(height: 20.0),
            splitLine,
            const SizedBox(height: 20.0),
            _buildActorRowWidget('作者:',entity.writer,0),
            const SizedBox(height: 10.0),
            _buildActorRowWidget('导演:',entity.director,1),
            const SizedBox(height: 10.0),
            _buildActorRowWidget('演员:',entity.actor,2),
            const SizedBox(height: 10.0),
            _buildMovieDateWidget('日期:',0,1,entity.dateReleased.isNotEmpty ? entity.dateReleased: '未知'),
            const SizedBox(height: 10.0),
            _buildMovieDateWidget('片长:',entity.duration,2)
          ],
        ),
      )
    );
  }

模糊搜索

效果图

实现

搜索内容与返回数据字段使用富文本高亮显示进行区别

Widget _buildSearchListView(String key) {
    if(key.isNotEmpty && key == _lastSearch){
      return defaultContent!;
    }else{
      _lastSearch = key;
    }

    ///模糊搜索列表,添加富文本高亮显示
    Widget displayContent = const Center(child: CircularProgressIndicator());
    List<ITopEntity> resultList = [];
    getSearchList(key)
        .then((value) => {
          if(value != null){
            resultList = value
          }})
        .catchError(
            (e) => {displayContent = const Center(child: Text('加载失败!'))})
        .whenComplete(() => {
              displayContent = ListView.builder(
                  itemCount: resultList.length,
                  itemBuilder: (context, index) {
                    return ListTile(
                      leading: const Icon(Icons.search, size: 20),
                      title: Transform(
                        transform: Matrix4.translationValues(-16, 0.0, 0.0),
                        child: RichText(
                          overflow: TextOverflow.ellipsis,
                          text: TextSpan(
                              text: resultList[index].data[0].name.substring(0, key.length),
                              style: const TextStyle(
                                  color: Colors.black,
                                  fontWeight: FontWeight.bold),
                              children: [
                                TextSpan(
                                  text: resultList[index].data[0].name.substring(key.length),
                                  style: const TextStyle(color: Colors.grey),
                                )
                              ]),
                        ),
                      ),
                      onTap: () {
                        exeSearchInsert(SearchRecord(resultList[index].data[0].name.toString()), context);
                        Navigator.of(context).push(MaterialPageRoute(builder: (context) => SearchResultPage(resultList[index].data[0].name)));
                      },
                    );
                  }),
              setState(() {
                if (resultList.isEmpty) {
                  displayContent = const Center(child: Text('没有搜索到相关内容!'));
                }else{
                  defaultContent = displayContent;
                }
              }),
            });
    return displayContent;
  }

搜索记录

效果图

实现

通过sqlite数据库进行本地存储,具体内容请看SQLite数据的使用与封装,此处只介绍数据插入和记录清空,以及布局构建

历史搜索

历史记录标题,并通过GestureDetector赋予其点击事件,并在点击事件中处理清空操作

Widget _buildHistory(){
    return Column(
          children: [
            GestureDetector(
              child: Row(
                  mainAxisAlignment: MainAxisAlignment.start,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    getText('历史记录',color: Colors.black,textSize: 16.0,fontWeight: FontWeight.bold),
                    const Icon(Icons.delete_forever,color: Colors.grey)
                  ]
              ),
              onTap: (){
                ///清空搜索记录
                exeSearchClear(context);
              },
            ),
            Flexible(child: _buildHistoryList())
          ],
        );
  }

搜索列表

通过GridView建立每行4列的一个列表,因为数据会随着搜索的变化而变化,所以通过provider进行状态管理,当数据进行插入时,通知监听处进行局部更新。

参数备注
crossAxisCount列数
mainAxisSpacing主轴之间间距
crossAxisSpacing交叉轴之间间距
childAspectRatioitem的宽高比
 ///构建搜索记录List列表
  Widget _buildHistoryList() {
    return Container(
      margin: const EdgeInsets.only(top: 10.0),
      child: Consumer<SearchProvider>(
          builder: (ctx, searchProvider, child) {
            return GridView.builder(
                itemCount: searchProvider.searchList.length,
                gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 4,
                    mainAxisSpacing: 10,
                    crossAxisSpacing: 10,
                    childAspectRatio: 3),
                itemBuilder: (ctx, index) {
                  return _buildHistoryItem(searchProvider.searchList[index].searchKey);
                });
          }),
    );
  }

  ///搜索记录item
  Widget _buildHistoryItem(String key){
    return GestureDetector(
      child: Container(
        alignment: Alignment.center,
        padding: const EdgeInsets.all(5.0),
        decoration: const BoxDecoration(
            color: Colors.grey,
            borderRadius: BorderRadius.all(Radius.circular(10.0))
        ),
        child: getText(key,color: Colors.white),
      ),
      onTap: (){
        exeSearchInsert(SearchRecord(key),context);
        Navigator.of(context).push(MaterialPageRoute(builder: (context) => SearchResultPage(key)));
      },
    );
  }

插入记录

首先判断数据库中是否存在相同内容,如果存在则不进行处理,反之插入数据库,然后通知provider修改数据源

 exeSearchInsert(SearchRecord(_searchContent.toString()), context);
exeSearchInsert(SearchRecord entity,BuildContext context) async {
  bool isExist = await SearchDao.getInstance().isExist(entity.searchKey);
  if(!isExist){
    final provider = context.read<SearchProvider>();
    provider.addSingleBean = entity;
    await SearchDao.getInstance().insert(entity);
  }
}

清空记录

同样分为两步,一步是清空数据库,另一部是情况provider持有的数据源

///清空搜索记录
exeSearchClear(BuildContext context) async{
  final provider = context.read<SearchProvider>();
  if(provider.searchList.isEmpty){
    showFailedToast('记录为空,无需清空!');
    return;
  }

  int count = await SearchDao.getInstance().deleteAll();
  if(count > 0){
    provider.clear();
    showSuccessToast('清空完成!');
  }else{
    showFailedToast('清空失败!');
  }

}

搜索结果

效果图

实现

获取网络数据

///电影搜索列表解析
Future<List<ITopEntity>?> getSearchList(String key) async {
  var url = HttpPath.getSearchContent(key);
  final result = await HttpUtil.getInstance().get(url, null);
  return jsonConvert.convertListNotNull<ITopEntity>(result);
}

搜索结果列表

同样使用FutureBuilder,在其future参数中传入上述网络请求,然后在builder监听数据状态,并建立数据列表

  ///构建搜索结果list列表
  Widget _buildMovieList(List<ITopEntity>? entityList){
    if(entityList == null || entityList.length == 0) return const Center(child: Text('没有搜索到相关内容!'));
    return ListView.builder(
        itemCount: entityList.length,
        itemBuilder: (BuildContext context, int index){
          return Container(
            padding: const EdgeInsets.all(10.0),
            child: MovieItem(
              MovieFavorite(
                  entityList[index].doubanId,
                  entityList[index].data[0].poster,
                  entityList[index].data[0].name,
                  entityList[index].data[0].country,
                  entityList[index].data[0].language,
                  entityList[index].data[0].genre,
                  entityList[index].data[0].description
              )
            ),
          );
        });
  }

电影Item

由于此处的电影item与收藏列表中的item一致,所以封装成啦一个StatelessWidget无状态的Widget

class MovieItem extends StatelessWidget {
  final MovieFavorite entity;
  const MovieItem(this.entity,{Key? key}) : super(key: key);

  ///构建电影item右边部分
  Widget _buildItemRight(MovieFavorite entity,double height){
    return Container(
      height: height,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const SizedBox(height: 10.0),
          getText(entity.movieName,textSize: 16.0,fontWeight: FontWeight.bold,color: Colors.black),
          const SizedBox(height: 10.0),
          getText(entity.movieCountry,textSize: 12.0),
          const SizedBox(height: 5.0),
          getText(entity.movieLanguage,textSize: 12.0),
          const SizedBox(height: 5.0),
          getText(entity.movieGenre.isNotEmpty ? entity.movieGenre : '未知',textSize: 12.0),
          const SizedBox(height: 5.0),
          Expanded(child: getText(entity.movieDescription,textSize: 12.0,color: Colors.grey,maxLine: 5)),
          const SizedBox(height: 10.0),
        ],
      ),
    );
  }

  ///构建每一个搜索列表item
  Widget _buildMovieItem(MovieFavorite entity,BuildContext context){
    return GestureDetector(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          buildMovieImage(entity.moviePoster, 200),
          const SizedBox(width: 10.0),
          Expanded(child: _buildItemRight(entity,200))
        ],
      ),
      onTap: (){
        Navigator.of(context).push(MaterialPageRoute(builder: (context) => MovieDetailPage(entity.doubanId, entity.movieName)));
      },
    );
  }

  
  Widget build(BuildContext context) {
    return Container(
      child: _buildMovieItem(entity,context),
    );
  }
}

影片收藏

效果图

实现

由于收藏的影片item和搜索结果的影片item一致,封装成一个StatelessWidget无状态的Widget,在上述已经展示,此处就不在冗余

添加收藏

只有当添加的影片在数据库中没有相对应的实例时,才能被添加,然后通知provider进行数据修改,通过使用Toast进行提示

exeFavoriteInsert(
                        MovieFavorite(
                            entity.doubanId,
                            entity.data[0].poster,
                            entity.data[0].name,
                            entity.data[0].country,
                            entity.data[0].language,
                            entity.data[0].genre,
                            entity.data[0].description),
                        context
                    );
exeFavoriteInsert(MovieFavorite entity,BuildContext context) async {
  bool isExist = await FavoriteDao.getInstance().isExist(entity.doubanId);
  if(!isExist){
    int flag = await FavoriteDao.getInstance().insert(entity);
    final favoriteProvide = context.read<FavoriteProvider>();
    final List<MovieFavorite> list = [entity];
    favoriteProvide.favoriteList = list;

    if (flag > 0) {
      showSuccessToast('收藏成功!');
    } else {
      showFailedToast('收藏失败!');
    }
  }else{
    showFailedToast('此影片已被收藏,请勿重复添加!');
  }
}

侧滑删除

效果图

实现

Dismissible侧滑

侧滑删除通过Dismissible实现,其中从左到右滑动不进行删除,只显示蓝色背景一段文字,内容右background提供,返回的是一个Widget;从右到左滑动会显示红色背景文字,由secondaryBackground提供,并显示删除提示框。重点是confirmDismiss方法,他需要返回的是Future<bool?>类型值,如果返回ture则从当前列表删除,此删除只是一个静态的,从当前列表移除该元素,具体删除逻辑需要自己处理,返回false则不移除该元素

  Widget _buildFavoriteList(){
    return Container(
        margin: const EdgeInsets.all(10.0),
        child: Consumer<FavoriteProvider>(
          builder: (context,favoriteProvider,child){
            if(favoriteProvider.favoriteList.isEmpty){
              return const Center(child: Text('暂未收藏任何影片!'));
            }else{
              return ListView.builder(
                  itemCount: favoriteProvider.favoriteList.length,
                  itemBuilder: (BuildContext context, int index){
                    return Dismissible(
                      key: Key(favoriteProvider.favoriteList[index].movieName),
                      background: Container(
                          alignment: Alignment.center,
                          color: Colors.blue,
                          child: ListTile(
                              leading: const Icon(Icons.tag_faces_rounded,color: Colors.white,),
                              title: getText('就不让你删,气死你!!!',color: Colors.white))
                      ),///从右到左的背景颜色
                      secondaryBackground: Container(
                          alignment: Alignment.center,
                          color: Colors.red,
                          child: ListTile(
                              leading: const Icon(Icons.delete_forever,color: Colors.white),
                              title: getText('删就删咯,反正不爱了...',color: Colors.white))),///从左到右的背景颜色
                      confirmDismiss: (direction) async{
                        switch(direction){
                          case DismissDirection.endToStart:
                            return await _showDeleteDialog(context, favoriteProvider.favoriteList[index],index,favoriteProvider) == true;
                          case DismissDirection.vertical:
                          case DismissDirection.horizontal:
                          case DismissDirection.startToEnd:
                          case DismissDirection.up:
                          case DismissDirection.down:
                          case DismissDirection.none:
                            break;
                        }
                        return false;
                      },
                      child: Container(
                        padding: const EdgeInsets.all(10.0),
                        child: MovieItem(favoriteProvider.favoriteList[index]),
                      ),
                    );
                  });
            }
          },
        )
    );
  }

Dismissible的滑动方向如下所示,包括其中模式

enum DismissDirection {
  /// The [Dismissible] can be dismissed by dragging either up or down.
  vertical,

  /// The [Dismissible] can be dismissed by dragging either left or right.
  horizontal,

  /// The [Dismissible] can be dismissed by dragging in the reverse of the
  /// reading direction (e.g., from right to left in left-to-right languages).
  endToStart,

  /// The [Dismissible] can be dismissed by dragging in the reading direction
  /// (e.g., from left to right in left-to-right languages).
  startToEnd,

  /// The [Dismissible] can be dismissed by dragging up only.
  up,

  /// The [Dismissible] can be dismissed by dragging down only.
  down,

  /// The [Dismissible] cannot be dismissed by dragging.
  none
}

删除提示框

从右到左滑动返回true,即代表移除该元素,上述也进行了说明,如果该函数返回的也是true则移除,反之亦然

 return await _showDeleteDialog(context, favoriteProvider.favoriteList[index],index,favoriteProvider) == true;

通过AlertDialog实现提示框,一个标题、一个内容、一个确定按钮、一个取消按钮,按钮分别处理不同的逻辑,清晰可见,可是在代码中并没有体现返回的bool值是什么。看源码可以看出,从堆移除当前堆顶context,除了上下文之外,还有一个bool值,此值就是dialog返回的值

  /// A dialog box might be closed with a result:
  ///
  /// ```dart
  /// void _accept() {
  ///   Navigator.pop(context, true); // dialog returns true
  /// }
  /// ```
  ///删除提示框
  Future<bool?> _showDeleteDialog(BuildContext context, MovieFavorite bean,int index,FavoriteProvider provider) {
    return showDialog<bool>(
      context: context,
      barrierDismissible: true,
      builder: (BuildContext context) {
        return AlertDialog(
          title: getText('系统提示',textAlign: TextAlign.center,fontWeight: FontWeight.bold,textSize: 20),
          content: getText('是否要从收藏列表中删除影片《${bean.movieName}》?',textSize: 14,maxLine: 2),
          actions: <Widget>[
            TextButton(
              child: getText('确定',color: Colors.red),
              onPressed: () async{
                await FavoriteDao.getInstance().delete(bean.doubanId);
                provider.removeItem = index;
                showSuccessToast('删除成功!');
                Navigator.pop(context,true);
              },
            ),
            TextButton(
              child: getText('取消',color: Colors.blue),
              onPressed: () {
                showSuccessToast('取消删除!');
                Navigator.pop(context,false);
              },
            ),
          ],
        );
      },
    );
  }

项目地址

Gitee

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-12-25 11:21:32  更:2022-12-25 11:25:56 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年3日历 -2024/3/28 17:31:28-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码