Flutter For App——一个简单的豆瓣APP

news2024/11/25 4:29:52

一个简单的豆瓣APP

  • 效果视频
  • 功能简述
    • 功能
    • 第三方库
  • 接口简述
  • 底部导航栏
    • 效果图
    • 实现
      • 初始化BottomNavigationBarItem
      • bottomNavigationBar
      • 切换页面导致bottomNavigationBar子页面重绘
  • Top250榜单
    • 效果图
    • 实现
      • Widget树
      • FutureBuilder
    • 异常
      • ListView上拉加载
  • 电影详情
    • 效果图
    • 实现
      • 高斯模糊
      • 网络数据获取
      • 电影详情
  • 模糊搜索
    • 效果图
    • 实现
  • 搜索记录
    • 效果图
    • 实现
      • 历史搜索
      • 搜索列表
      • 插入记录
      • 清空记录
  • 搜索结果
    • 效果图
    • 实现
      • 获取网络数据
      • 搜索结果列表
      • 电影Item
  • 影片收藏
    • 效果图
    • 实现
      • 添加收藏
  • 侧滑删除
    • 效果图
    • 实现
      • Dismissible侧滑
      • 删除提示框
  • 项目地址

效果视频

豆瓣

功能简述

功能

  • 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/101081.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

设计模式原则 - 单一职责原则(一)

单一职责原则一 官方定义基本介绍二 案例演示普通方式实现解决方案解决方案一解决方案案例分析解决方案二解决方案案例分析案例总结三 注意事项&细节四 如何遵守单一职责原则&#xff1f;一 官方定义 单一职责原则&#xff08;Single Responsibility Principle, SRP&#x…

用Spark写入Mysql的特别注意事项

相信有部分刚入门的小伙伴对于spark写入Mysql的过程不太熟悉。特意写一篇文章讲一下这个注意事项&#xff0c;以免“上大当” 我们先看一个小伙伴写的一段spark写入mysql的代码 public static void trans(SparkSession spark,String pro_table, String pro_url, String pro_dr…

微服务框架 SpringCloud微服务架构 服务异步通讯 52 惰性队列 52.1 消息堆积问题

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式&#xff0c;系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 服务异步通讯 文章目录微服务框架服务异步通讯52 惰性队列52.1 消息堆积问题52.1.1 消息堆积问题52 惰性队列 52.1 消息堆积问题 52.1.1 消…

11基于主从博弈理论的共享储能与综合能源微网优化运行研究(MATLAB程序)

参考文献&#xff1a; 基于主从博弈理论的共享储能与综合能源微网优化运行研究——帅轩越&#xff08;2022电网技术&#xff09; 主要内容; 代码主要做的是基于主从博弈理论的共享储能与综合能源微网优化运行研究&#xff0c;首先介绍了系统运行框架&#xff0c;分析了系统内…

一文了解各种高精度室内定位技术

在消费需求和创新技术驱动下&#xff0c;可穿戴设备和物联网产品的发展驶入了快车道&#xff0c;GNSS定位功能在无人驾驶、智能设备、资产追踪等日趋智能化中广泛应用&#xff0c;而随着万物互联时代的到临&#xff0c;物联网技术围绕人员、资产的室内位置服务需求也愈加强烈。…

vue使用

目录 路由History模式打包页面空白 项目放根目录 -- 配置 项目放二级目录 -- 配置 路由History模式打包页面空白 项目放根目录 -- 配置 router > index.js 修改 base const router new VueRouter({mode: history,// base: process.env.BASE_URL,base: /,routes, }) ngi…

【Redis集群专题】「集群技术三部曲」介绍一下常用的Redis集群机制方案的原理和指南(入门篇)

集群化的方案 Redis的Sentinel解决了主从复制故障不能自动迁移的问题&#xff0c;但是主节点的写性能和存储能力依然是受到了Redis单机容量有限的限制&#xff0c;所以使用Redis集群去解决这个问题&#xff0c;将Redis的数据根据一定的规则分配到多台机器。 Redis集群方案 R…

【git】

目录第一章 简介 1&#xff0e;1 版本控制 1.1.1 本地版本控制1.1.2 集中式版本控制1.1.3 分布式版本控制 第二章 基础篇 2.1 下载代码 2.2 更新代码 2.2.1 清空本地未被跟踪内容2.2.2更新代码使之与库上同步 2.3 修改 2.3.1 Vim2.3.2 Sed2.3.3 Awk 2.4 查看状态 2.5 保存代…

通俗易懂的java设计模式(3)-观察者设计模式

什么是观察者设计模式 观察者模式主要应用在对象存在一对多关系的情况下&#xff0c;那么如果一个对象&#xff0c;依赖于另一个对象&#xff0c;那个被依赖的对象一旦被修改&#xff0c;依赖于他的那个对象也会被观察者所告知。 观察者模式又被称作为发布-订阅模式&#xff0c…

2022 UUCTF

目录 <1> Web (1) websign(禁用js绕过) (2) ez_rce(?>闭合 rce) (3) ez_unser(引用传递) (4) ez_upload(apache后缀解析漏洞) (5) ezsql(union注入) (6) funmd5(代码审计 %0a绕过preg_replace) (7) phonecode(伪随机数漏洞) (8) ezpop(反序列化字符串逃逸) …

[附源码]Nodejs计算机毕业设计教师业绩考核和职称评审系统Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…

[附源码]Nodejs计算机毕业设计教务管理系统Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…

燃尽图——项目管理中的必备工具

燃尽图可以使项目经理和团队可以快速查看其工作负荷的进度以及项目按计划完成的情况。 在项目管理中&#xff0c;通过燃尽图不仅能了解项目进度&#xff0c;还能分析出项目中问题&#xff0c;及时进行风险控制。 燃尽图是用于表示剩余工作量的工作图表&#xff0c;由横轴&…

WebDAV之葫芦儿•派盘+RS文件管理器

RS文件管理器 支持WebDAV方式连接葫芦儿派盘。 手机本地文件,网盘、共享文件,如何集中管理?推荐您使用Rs文件管理器,还支持WebDAV等功能。 Rs文件管理器是一款功能强大的手机文件管理器。有强大的本地和网络文件管理功能,让您更方便的管理你的手机、平板、电脑和网盘。…

MOMO CODE SEC INSPECTOR-Java项目静态代码安全审计idea插件工具

工具地址 https://github.com/momosecurity/momo-code-sec-inspector-java安装 1、确认IDE版本&#xff1a;Intellij IDEA ( Community / Ultimate ) > 2018.32、IDEA插件市场搜索"immomo"安装。使用方式 被动&#xff1a;装完愉快的打代码&#xff0c;一边它会提…

AI作画的背后是怎么一步步实现的?一文详解AI作画算法原理+性能评测

前言 “AI作画依赖于多模态预训练&#xff0c;实际上各类作画AI模型早已存在&#xff0c;之所以近期作品质量提升很多&#xff0c;可能是因为以前预训练没有受到重视&#xff0c;还达不到媲美人类的程度&#xff0c;但随着数据量、训练量的增多&#xff0c;最终达到了现在呈现…

无约束优化:线搜索最速下降

文章目录无约束优化&#xff1a;线搜索最速下降无约束优化问题线搜索最速下降精确线搜索非精确线搜索Armijo准则Goldstein准则Wolfe准则参考文献无约束优化&#xff1a;线搜索最速下降 无约束优化问题 线搜索最速下降 对于光滑函数f(x)f(x)f(x)&#xff0c;沿着函数负梯度方向…

入门力扣自学笔记215 C++ (题目编号:1971)

1971. 寻找图中是否存在路径 题目&#xff1a; 有一个具有 n 个顶点的 双向 图&#xff0c;其中每个顶点标记从 0 到 n - 1&#xff08;包含 0 和 n - 1&#xff09;。图中的边用一个二维整数数组 edges 表示&#xff0c;其中 edges[i] [ui, vi] 表示顶点 ui 和顶点 vi 之间…

安装 DbVisualizer pro 10.0.16,编辑器中文乱码的解决

DbVisualizer pro 10.0.16 双击安装&#xff0c; 选择下载 JRE运行后 选择 创建 桌面快捷方式 安装成功后 ping 报错无法连接到mysql &#xff0c; 驱动 所致 官方下载最新驱动 MySQL :: Download MySQL Connector/J (Archived Versions)https://downloads.mysql.com/archi…

springcloudAlibaba之seata的使用

1.seata的基础角色&#xff1a; TC(Transaction Coordainator):事务协调者&#xff0c;它维护全局和分支事务的状态&#xff0c;驱动全局事务提交和回滚 TM(Transaction Manager):事务管理器&#xff0c;它定义了全局事务的范围&#xff0c;主要包括开始全局事务、提交全局事务…