Flutter For App - a simple Douban APP

effect video

watercress

Brief description of functions

Function

  • Top250 list
  • movie details
  • Movie Favorites List
  • fuzzy search
  • search history
  • search list
  • clear search history
  • Swipe sideways to delete favorite videos

third party library

name Remark
dio network library
provider state management
sqlfite database
fluttertoast Toast tips
flutter_swiper Banner
pk_skeleton Skeleton screen

Brief description of the interface

Since many Douban interfaces are currently restricted, this project uses the interface address organized by someone on Github . There are a total of four interfaces that can be used. Among them, the search-related two interfaces are only allowed to be accessed once within 30s. Appears code:429,访问频繁异常, the remaining two interfaces are unlimited

  • Get the top 250 movies list:
`https://api.wmdb.tv/api/v1/top?type=Imdb&skip=0&limit=50&lang=Cn`
  • Search by Douban id
https://api.wmdb.tv/movie/api?id=doubanid
  • Search by video keywords
https://api.wmdb.tv/api/v1/movie/search?q=英雄本色&limit=10&skip=0&lang=Cn&year=2002
  • Generation of movie posters
https://api.wmdb.tv/movie/api/generateimage?doubanId=1306123&lang=En

bottom navigation bar

The background color of each BottomNavigationBarItemis different, and each initialization is randomly obtained

renderings

accomplish

Initialize BottomNavigationBarItem

The background colors of the three items are obtained randomly

///底部导航栏数据
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

Define the current page number of the record

var _currentPage = 0;
  • items: corresponding to the sub-items of Navigation
  • currentIndex: the serial number of the current page
  • type: the display method of the bottom navigation bar
  • onTap: click event
bottomNavigationBar: BottomNavigationBar(
        items: navList,
        currentIndex: _currentPage,
        type: BottomNavigationBarType.shifting,
        onTap: (index){
          _changeIndex(index);
        },
      )

To switch pages, make a simple judgment to prevent repeated execution of clicking the current page

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

Switching pages causes bottomNavigationBar subpages to be redrawn

By using IndexedStack, it will not reload when sub-pages are switched

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

Top250 list

By FutureBuilderexecuting the network request method, and then judging whether the data is returned, waitingdisplay it in the status 骨架屏loading, notify the Provider to refresh after the data returns, and then refresh locally电影ListView

renderings

accomplish

Widget tree

The second Widget is just a simple description through Column and Row, without fully drawing the entire Widget tree
insert image description here

FutureBuilder

Among them, getTopList() is a network request to obtain the Top250 list. For the specific content network package, please refer to Dio package

Obtain and analyze the Top250 list data

///电影榜单前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);
}

Build FutureBuilder


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

Then FutureBuildermonitor each state, where waitingthe state returns the skeleton screen component, and then start building after the data is returnedListView

  ///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;
         }
        }
    }
  }

abnormal

When I am FutureBuilderbuilding a ListView, when the data returns, I need to pass Providerthe data and I can start refreshing, but my current ListView is under construction, so it will prompt the following warning, but it will not affect the code execution. I pass the Data refresh is used 帧回调for monitoring, which means that it is executed sequentially. This callback will only be executed after my layout construction is completed, and this callback will only be executed once.

 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 pull up loading

  1. Define the pull-up load controller
 final ScrollController _scrollController = ScrollController();
  1. Add pull-up listener
///这个判断相当于屏幕底部高度和手指滑动到底部高度一致时执行上拉操作
@override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      if(_scrollController.position.pixels == _scrollController.position.maxScrollExtent){
        skip += limit;
        _loadMovieData();
      }
    });
  }

Use providerfor state management, then add the acquired data and perform partial refresh

 ///上拉加载
  Future _loadMovieData() async{
      await getTopList(skip,limit).then((value){
          final movieProvider = context.read<MovieProvider>();
          if(value != null){
            movieProvider.setList = value;
          }
        });
  }
  1. Build the 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);
            });
      },
    );
  }

movie details

renderings

accomplish

Still use FutureBuilder to implement data requests and then fill in after the data is returned

Gaussian blur

The background image is the data after the callback, and then blurred. The blur ratio can be modified by passing in sigmaXand valuesigmaY

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,),
                  )

Network data acquisition

Make a network request by passing in doubanIdas a parameter, and then parse and return

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

movie details

The movie poster and the movie information description wrapped by the white background below are displayed by using Stackand , and the sliding component is used for wrapping.PositionSingleChildScrollView

///电影海报和电影描述重叠
  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))
             ],
           ),
         ),
     );
  }

The specific content Columnis displayed vertically through the layout. Due to space reasons, the specific sub-Widget display content will not be introduced

 ///电影描述
  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)
          ],
        ),
      )
    );
  }

fuzzy search

renderings

accomplish

The search content and the returned data field are distinguished by rich text highlighting

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;
  }

search history

renderings

accomplish

Local storage through sqlitethe database. For details, please refer to the use and packaging of SQLite data . Here, only data insertion, record clearing, and layout construction are introduced.

History search

History title, and by GestureDetectorgiving it a click event, and handling the clearing operation in the click event

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())
          ],
        );
  }

search list

By GridViewcreating a list with 4 columns per row, because the data will change with the change of the search, through providerstate management, when the data is inserted, the listener will be notified to perform a partial update.

parameter Remark
crossAxisCount Number of columns
mainAxisSpacing Spindle spacing
crossAxisSpacing Spacing between cross axes
childAspectRatio item's aspect ratio
 ///构建搜索记录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)));
      },
    );
  }

insert record

First judge whether the same content exists in the database, if it exists, it will not be processed, otherwise it will be inserted into the database, and then providerthe data source will be modified

 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);
  }
}

clear record

It is also divided into two steps, one is to clear the database, and the other is providerthe data source held by the situation

///清空搜索记录
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('清空失败!');
  }

}

search results

renderings

accomplish

get network data

///电影搜索列表解析
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);
}

search result list

Also used FutureBuilder, pass in the above network request in its futureparameters, then buildermonitor the data status, and build a data list

  ///构建搜索结果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
              )
            ),
          );
        });
  }

Movie Item

Since the movie item here is consistent with the item in the favorite list, it is encapsulated into a StatelessWidgetstateless 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),
    );
  }
}

video collection

renderings

accomplish

Since the favorite movie item is the same as the search result movie item, it is encapsulated into a StatelessWidgetstateless Widget, which has been shown above, so it is not redundant here

Add to Favorites

Only when the added movie does not have a corresponding instance in the database, it can be added, and then the providerdata modification is notified by using Toast to prompt

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('此影片已被收藏,请勿重复添加!');
  }
}

Slide to delete

renderings

accomplish

Dismissible side slip

Side sliding deletion is Dismissiblerealized by sliding from left to right without deleting, only a piece of text with blue background is displayed, the content is backgroundprovided on the right, and one is returned Widget; sliding from right to left will display red background text, provided secondaryBackgroundby , and display delete prompt box. The key point is confirmDismissthe method. What he needs to return is Future<bool?>the type value. If it returns ture, it will be deleted from the current list. This deletion is just a static one. The element is removed from the current list. The specific deletion logic needs to be handled by itself, and the falseelement will not be removed if it returns

  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]),
                      ),
                    );
                  });
            }
          },
        )
    );
  }

DismissibleThe swipe directions are as follows, including the mode

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
}

Delete prompt box

Swipe back from right to left true, which means removing the element, as explained above, if the function returns the same, trueremove it, and vice versa

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

By AlertDialogimplementing the prompt box, a title, a content, an OK button, and a cancel button, the buttons handle different logics, which are clearly visible, but the returned value is not reflected in the code bool. Looking at the source code, it can be seen that removing the current top of the heap from the heap context, in addition to the context, there is another boolvalue, which is dialogthe returned value

  /// 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);
              },
            ),
          ],
        );
      },
    );
  }

project address

Gitee

Guess you like

Origin blog.csdn.net/News53231323/article/details/128370744