Flutter para la aplicación: una aplicación simple de Douban

efecto de vídeo

berro

Breve descripción de las funciones

Función

  • lista Top250
  • detalles de la pelicula
  • Lista de películas favoritas
  • Búsqueda borrosa
  • buscar Historia
  • lista de búsqueda
  • limpiar historial de búsqueda
  • Desliza hacia los lados para eliminar videos favoritos

biblioteca de terceros

nombre Observación
Dio biblioteca de red
proveedor administración del Estado
sqlfite base de datos
tostadas Consejos para tostadas
flutter_swiper Bandera
pk_esqueleto Pantalla de esqueleto

Breve descripción de la interfaz.

Dado que muchas interfaces de Douban están actualmente restringidas, este proyecto utiliza la dirección de la interfaz organizada por alguien en Github. Hay un total de cuatro interfaces que se pueden usar. Entre ellas, las dos interfaces relacionadas con la búsqueda solo se pueden acceder una vez dentro de los 30 segundos. Aparece code:429,访问频繁异常, las dos interfaces restantes son ilimitadas

  • Obtenga la lista de las 250 mejores películas:
`https://api.wmdb.tv/api/v1/top?type=Imdb&skip=0&limit=50&lang=Cn`
  • Buscar por ID de Douban
https://api.wmdb.tv/movie/api?id=doubanid
  • Buscar por palabras clave de video
https://api.wmdb.tv/api/v1/movie/search?q=英雄本色&limit=10&skip=0&lang=Cn&year=2002
  • Generación de carteles de cine.
https://api.wmdb.tv/movie/api/generateimage?doubanId=1306123&lang=En

barra de navegación inferior

El color de fondo de cada uno BottomNavigationBarItemes diferente, y cada inicialización se obtiene aleatoriamente

representaciones

lograr

Inicializar BottomNavigationBarItem

Los colores de fondo de los tres elementos se obtienen aleatoriamente

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

Definir el número de página actual del registro

var _currentPage = 0;
  • ítems: correspondientes a los subítems de Navegación
  • currentIndex: el número de serie de la página actual
  • tipo: el método de visualización de la barra de navegación inferior
  • onTap: haga clic en evento
bottomNavigationBar: BottomNavigationBar(
        items: navList,
        currentIndex: _currentPage,
        type: BottomNavigationBarType.shifting,
        onTap: (index){
          _changeIndex(index);
        },
      )

Para cambiar de página, haga un juicio simple para evitar la ejecución repetida de hacer clic en la página actual

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

Cambiar de página hace que se vuelvan a dibujar las subpáginas bottomNavigationBar

Al usar IndexedStack, no se recargará cuando se cambien las subpáginas

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

lista Top250

Al FutureBuilderejecutar el método de solicitud de red y luego evaluar si se devuelven los datos, waitingmostrarlos en el estado 骨架屏loading, notificar al proveedor que actualice después de que se devuelvan los datos y luego actualice localmente.电影ListView

representaciones

lograr

Árbol de widgets

El segundo Widget es solo una descripción simple a través de Columna y Fila, sin dibujar completamente todo el árbol de Widget
inserte la descripción de la imagen aquí

FuturoConstructor

Entre ellos, getTopList() es una solicitud de red para obtener la lista Top 250. Para el paquete de red de contenido específico, consulte el paquete Dio

Obtener y analizar los datos de la lista 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);
}

Construir FutureBuilder


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

Luego FutureBuildermonitoree cada estado, donde waitingel estado devuelve el componente de pantalla de esqueleto, y luego comience a construir después de que se devuelvan los datosListView

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

anormal

Cuando estoy FutureBuilderconstruyendo un ListView, cuando regresan los datos, necesito pasar Providerlos datos y puedo comenzar a actualizar, pero mi ListView actual está en construcción, por lo que aparecerá la siguiente advertencia, pero no afectará la ejecución del código. pasar la actualización de datos se usa 帧回调para monitorear, lo que significa que se ejecuta secuencialmente. Esta devolución de llamada solo se ejecutará después de que se complete la construcción de mi diseño, y esta devolución de llamada solo se ejecutará una vez.

 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 cargando

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

Úselo providerpara la administración del estado, luego agregue los datos adquiridos y realice una actualización parcial

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

detalles de la pelicula

representaciones

lograr

Todavía use FutureBuilder para implementar solicitudes de datos y luego complete después de que se devuelvan los datos

desenfoque gaussiano

La imagen de fondo son los datos después de la devolución de llamada y luego se desdibujan. La relación de desenfoque se puede modificar pasando sigmaXy valorsigmaY

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

Adquisición de datos de red

Realice una solicitud de red pasándola doubanIdcomo un parámetro, y luego analice y devuelva

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

detalles de la pelicula

El póster de la película y la descripción de la información de la película envueltos por el fondo blanco a continuación se muestran usando Stacky , y el componente deslizante se usa para envolver.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))
             ],
           ),
         ),
     );
  }

El contenido específico Columnse muestra verticalmente a través del diseño. Debido a razones de espacio, el contenido de visualización del subwidget específico no se presentará.

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

Búsqueda borrosa

representaciones

lograr

El contenido de la búsqueda y el campo de datos devuelto se distinguen por el resaltado de texto enriquecido

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

buscar Historia

representaciones

lograr

Almacenamiento local a través sqlitede la base de datos. Para obtener más información, consulte el uso y el empaquetado de datos de SQLite . Aquí, solo se introducen la inserción de datos, la limpieza de registros y la construcción de diseños.

Búsqueda de historial

Título del historial, dándole GestureDetectorun evento de clic y manejando la operación de compensación en el evento de clic

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

lista de búsqueda

Al GridViewcrear una lista con 4 columnas por fila, porque los datos cambiarán con el cambio de la búsqueda, a través de la providergestión de estado, cuando se inserten los datos, se le notificará al oyente para realizar una actualización parcial.

parámetro Observación
número de ejes cruzados Número de columnas
espaciado del eje principal Distancia entre husillos
crossAxisSpacing Espaciado entre ejes transversales
childAspectRatio relación de aspecto del elemento
 ///构建搜索记录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)));
      },
    );
  }

insertar registro

Primero juzgue si el mismo contenido existe en la base de datos, si existe, no se procesará, de lo contrario, se insertará en la base de datos y luego se providermodificará la fuente de datos.

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

borrar registro

También se divide en dos pasos, uno es borrar la base de datos y el otro es providerla fuente de datos en poder de la situación.

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

}

Resultados de la búsqueda

representaciones

lograr

obtener datos de red

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

lista de resultados de búsqueda

También se utiliza FutureBuilder, pase la solicitud de red anterior en sus futureparámetros, luego buildercontrole el estado de los datos y cree una lista de datos

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

Elemento de película

Dado que el elemento de la película aquí es coherente con el elemento de la lista de favoritos, se encapsula en un StatelessWidgetwidget sin estado

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

colección de videos

representaciones

lograr

Dado que el elemento de la película favorita es el mismo que el elemento de la película del resultado de la búsqueda, se encapsula en un StatelessWidgetwidget sin estado, como se muestra arriba, por lo que no es redundante aquí.

Agregar a los favoritos

Solo cuando la película agregada no tiene una instancia correspondiente en la base de datos, se puede agregar y luego providerse notifica la modificación de datos mediante Toast para solicitar

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

Deslizar para eliminar

representaciones

lograr

Deslizamiento lateral descartable

La eliminación del deslizamiento lateral se Dismissiblerealiza deslizando de izquierda a derecha sin eliminar, solo se muestra un fragmento de texto con fondo azul, el contenido se backgroundproporciona a la derecha y se devuelve uno Widget; al deslizar de derecha a izquierda se mostrará el texto de fondo rojo, siempre que secondaryBackgroundpor y mostrar el cuadro de solicitud de eliminación. El punto clave es confirmDismissel método. Lo que necesita devolver es Future<bool?>el valor de tipo. Si devuelve ture, se eliminará de la lista actual. Esta eliminación es solo estática. El elemento se elimina de la lista actual. La eliminación específica la lógica debe manejarse por sí misma, y ​​el falseelemento no se eliminará si regresa

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

DismissibleLas direcciones de deslizamiento son las siguientes, incluido el modo

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
}

Eliminar cuadro de solicitud

Deslice hacia atrás de derecha a izquierda true, lo que significa eliminar el elemento, como se explicó anteriormente, si la función devuelve lo mismo, trueelimínelo y viceversa

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

Al AlertDialogimplementar el cuadro de diálogo, un título, un contenido, un botón Aceptar y un botón Cancelar, los botones manejan diferentes lógicas, que son claramente visibles, pero el valor devuelto no se refleja en el código bool. Mirando el código fuente, se puede ver que eliminando la parte superior actual del montón del montón context, además del contexto, hay otro boolvalor, que es dialogel valor devuelto

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

dirección del proyecto

casa rural

Supongo que te gusta

Origin blog.csdn.net/News53231323/article/details/128370744
Recomendado
Clasificación