Создайте свой проект Flutter с использованием шаблона BLOC (Часть 2)

Привет народ! Эта статья является продолжением моей предыдущей статьи «Создайте свой проект Flutter» . Как и было обещано в моей предыдущей статье, я устраню некоторые недостатки в текущей архитектуре и добавлю несколько новых функций в приложение, которое мы создавали. Прежде чем отправиться в путешествие, позвольте мне показать вам 4 места, которые мы посетим, и узнать о них. Ниже приведены темы, которые мы рассмотрим в этой статье.

Темы, которые мы рассмотрим:

  1. Устранение недостатков в текущей архитектуре
  2. Одиночный экземпляр против ограниченного экземпляра (доступ к BLoC)
  3. Навигация
  4. Трансформеры RxDart

Примечание : Прежде чем читать дальше. Я настоятельно рекомендую прочитать мою предыдущую статью, чтобы лучше понять приложение, которое мы создаем, и дизайн архитектуры (шаблон BLoC), которому мы следуем.

Недостатки в нынешнем архитектурном проекте

Перед решением проблемы или обсуждением изменений. Я думаю, у вас всех должен быть вопрос в уме (если вы еще не прочитали его). Позвольте мне ответить на него.

Почему вы не дополнили предыдущую статью изменениями вместо того, чтобы написать новую?

Да! конечно, я бы сделал это. Я могу прямо представить вам работающий и свободный от ошибок код, который сделал бы вас счастливым. Но здесь я хочу научить вас тому, через что я прошел, создавая это приложение. Проблемы или неудачи, с которыми я столкнулся. Таким образом, вы можете создавать любые приложения Flutter и отлаживать или решать любые проблемы, с которыми вы сталкиваетесь в процессе. Так что без дальнейших задержек. Вернемся к нашей цели.

Если вы ознакомились с моей предыдущей статьей и кодом, первый недостаток заключается в том, что я создал метод с именем dispose() внутри MoviesBloc учебный класс. Этот метод отвечает за Закрыть или удалить все открытые потоки, чтобы избежать утечек памяти. Я создал метод, но никогда не вызывал его нигде в моем movie_list.dart файл. Это приведет к утечке памяти. Другим серьезным недостатком является то, что я совершаю сетевой вызов внутри build метод, который является довольно рискованным. Давайте попробуем решить эти 2 основных недостатка.

В настоящее время MovieList класс это StatelessWidget и способ работы StatelessWidget, build будет вызываться всякий раз, когда он будет добавлен в дерево виджетов, и все его свойства неизменяемы. build метод является точкой входа и может вызываться несколько раз из-за изменений конфигурации. Так что это не самое подходящее место для сетевых звонков (что я и сделал в своей предыдущей статье). У нас даже нет метода внутри StatelessWidget, где мы могли бы вызвать dispose метод блока. Мы должны найти место, где мы можем сделать сетевой вызов и, в конце концов, позвонить на dispose метод.

Дело в том, что у меня нет initState а также dispose метод в StatelessWidget, как указано в StatefulWidget . initState метод в StatefulWidget вызывается первым для выделения ресурсов и dispose метод вызывается при удалении этих выделенных ресурсов (подробнее о них читайте здесь). Итак, давайте конвертировать MovieList класс из StatelessWidget в StatefulWidget и сделать сетевой вызов внутри initState() а также MovieBloc’s dispose() внутри StatefulWidget dispose().

Просто замените movie_list.dart код с приведенной ниже реализацией.

import 'package:flutter/material.dart';
import '../models/item_model.dart';
import '../blocs/movies_bloc.dart';

class MovieList extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return MovieListState();
  }
}

class MovieListState extends State<MovieList> {
  @override
  void initState() {
    super.initState();
    bloc.fetchAllMovies();
  }

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Popular Movies'),
      ),
      body: StreamBuilder(
        stream: bloc.allMovies,
        builder: (context, AsyncSnapshot<ItemModel> snapshot) {
          if (snapshot.hasData) {
            return buildList(snapshot);
          } else if (snapshot.hasError) {
            return Text(snapshot.error.toString());
          }
          return Center(child: CircularProgressIndicator());
        },
      ),
    );
  }

  Widget buildList(AsyncSnapshot<ItemModel> snapshot) {
    return GridView.builder(
        itemCount: snapshot.data.results.length,
        gridDelegate:
            new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
        itemBuilder: (BuildContext context, int index) {
          return GridTile(
            child: Image.network(
              '
                  .results[index].poster_path}',
              fit: BoxFit.cover,
            ),
          );
        });
  }
}

В приведенном выше коде я назвал bloc.fetchAllMovies() внутри initState() а также bloc.dispose() внутри dispose() принадлежащий MovieListState учебный класс. Запустите приложение, и вы увидите, как приложение загружает список фильмов, как обычно. Вы не увидите никаких визуальных изменений, но внутренне вы убедились, что не будет множественных сетевых вызовов и не будет утечек памяти. Ух ты! это выглядит аккуратно. 😍

Основной доклад : никогда не делайте никаких сетевых вызовов или вызовов БД внутри build метод и всегда убедитесь, что вы удаляете или закрываете открытые потоки.

Реализация новой функции 😃

Пришло время добавить новую функцию в наше существующее приложение. Прежде чем мы начнем обсуждать или внедрять новую функцию. Позвольте мне показать вам найти продукт. Вот небольшое видео об этом.

Как видите, мы добавили новый экран, где вы можете увидеть детали конкретного фильма, который вы выбрали из списка.

Давайте спланируем поток приложений

Перед добавлением в приложение какой-либо новой функции всегда рекомендуется выполнить некоторую бумажную работу (определить поток). Итак, здесь я делюсь потоком приложений, который я придумал после анализа всех функций приложения.

Я думаю, что большинство из вас легко поймут поток, посмотрев на диаграмму. Но все же позвольте мне объяснить вам приведенную выше диаграмму, поскольку я использовал несколько новых терминов.

  1. Экран списка фильмов: на этом экране вы видите список всех фильмов в виде сетки.
  2. Блок списка фильмов: это мост, который будет получать данные из репозитория по запросу и передавать их на экран списка фильмов (об одном экземпляре я объясню позже).
  3. Экран сведений о фильме: на этом экране вы увидите детали фильма, выбранного на экране списка. Здесь вы можете увидеть название фильма, рейтинг, дату выхода, описание и трейлеры (позже я объясню о Scoped Instance).
  4. Репозиторий: это центральная точка, откуда контролируется поток данных.
  5. Поставщик API: содержит реализацию сетевого вызова.

Теперь вам должно быть интересно. Что такое » Один экземпляр и экземпляр с ограниченной областью действия » на схеме. Давайте разберемся в них подробно.

Один экземпляр против экземпляра с ограниченной областью действия

Как вы можете видеть на диаграмме, оба экрана имеют доступ к своему соответствующему классу BLoC. Вы можете выставить эти классы BLoC на соответствующие экраны двумя способами, т. е. с помощью одного экземпляра или экземпляра с ограниченной областью. Когда я говорю об одном экземпляре, я имею в виду, что на экран будет выведена одна ссылка (Singleton) класса BLoC. Этот тип класса BLoC может быть доступен из любой части приложения. Любой экран может использовать класс Single Instance BLoC.

Но класс Scoped Instance BLoC имеет ограниченный доступ. Я имею в виду, что он доступен только для экрана, с которым он связан или подвергается воздействию. Вот небольшая диаграмма, чтобы объяснить это.


Экземпляр с заданной областью

Как вы можете видеть на приведенной выше диаграмме, блок доступен только для виджета экрана и двух других пользовательских виджетов под экраном. Мы используем InheritedWidget который будет держать BLoC внутри него. InheritedWidget завершит виджет «Экран» и позволит виджету «Экран» вместе с виджетами под ним иметь доступ к BLoC. Никакие родительские виджеты виджета экрана не будут иметь доступа к BLoC.

Надеюсь, вы понимаете разницу между отдельным экземпляром и экземпляром с заданной областью. Одиночный способ доступа к BLoC полезен, когда вы работаете над небольшим приложением. Но если вы работаете над большим проектом, то Scoped Instance будет предпочтительным способом.

Добавление подробного экрана

Пришло время добавить экран подробностей в наше приложение. Логика подробного экрана заключается в том, что пользователь нажимает на элемент фильма из списка фильмов. Пользователь попадет на экран подробностей, где пользователь может просмотреть детали фильма. Некоторые детали (название фильма, рейтинг, дата выхода, описание, постер) будут переданы с экрана списка на экран сведений. Трейлер будет загружен с сервера. Давайте опустим часть трейлера и сосредоточимся на отображении данных, которые передаются из экрана списка.

Прежде чем создавать файл, я надеюсь, что вы следуете той же структуре проекта, о которой я упоминал в своей предыдущей статье. Если да, то создайте файл с именем movie_detail.dart внутри пользовательский интерфейс package. Скопируйте приведенный ниже код внутри файла.

import 'package:flutter/material.dart';

class MovieDetail extends StatefulWidget {
  final posterUrl;
  final description;
  final releaseDate;
  final String title;
  final String voteAverage;
  final int movieId;

  MovieDetail({
    this.title,
    this.posterUrl,
    this.description,
    this.releaseDate,
    this.voteAverage,
    this.movieId,
  });

  @override
  State<StatefulWidget> createState() {
    return MovieDetailState(
      title: title,
      posterUrl: posterUrl,
      description: description,
      releaseDate: releaseDate,
      voteAverage: voteAverage,
      movieId: movieId,
    );
  }
}

class MovieDetailState extends State<MovieDetail> {
  final posterUrl;
  final description;
  final releaseDate;
  final String title;
  final String voteAverage;
  final int movieId;

  MovieDetailState({
    this.title,
    this.posterUrl,
    this.description,
    this.releaseDate,
    this.voteAverage,
    this.movieId,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        top: false,
        bottom: false,
        child: NestedScrollView(
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return <Widget>[
              SliverAppBar(
                expandedHeight: 200.0,
                floating: false,
                pinned: true,
                elevation: 0.0,
                flexibleSpace: FlexibleSpaceBar(
                    background: Image.network(
                  "
                  fit: BoxFit.cover,
                )),
              ),
            ];
          },
          body: Padding(
            padding: const EdgeInsets.all(10.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Container(margin: EdgeInsets.only(top: 5.0)),
                Text(
                  title,
                  style: TextStyle(
                    fontSize: 25.0,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                Container(margin: EdgeInsets.only(top: 8.0, bottom: 8.0)),
                Row(
                  children: <Widget>[
                    Icon(
                      Icons.favorite,
                      color: Colors.red,
                    ),
                    Container(
                      margin: EdgeInsets.only(left: 1.0, right: 1.0),
                    ),
                    Text(
                      voteAverage,
                      style: TextStyle(
                        fontSize: 18.0,
                      ),
                    ),
                    Container(
                      margin: EdgeInsets.only(left: 10.0, right: 10.0),
                    ),
                    Text(
                      releaseDate,
                      style: TextStyle(
                        fontSize: 18.0,
                      ),
                    ),
                  ],
                ),
                Container(margin: EdgeInsets.only(top: 8.0, bottom: 8.0)),
                Text(description),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Как видите, конструктор этого класса ожидает несколько параметров. Эти данные будут предоставлены этому классу на экране списка. Следующим шагом является реализация логики навигации, которая приведет нас от экрана списка к экрану подробностей.

Во Flutter, если вы хотите перейти с одного экрана на другой, мы используем Навигатор учебный класс. Давайте реализуем логику навигации внутри movie_list.dart файл.

Итак, идея состоит в том, что при нажатии на каждый элемент сетки мы открываем экран сведений и показываем содержимое, которое мы передали из экрана списка на экран сведений. Вот код файла movie_list.dart.

import 'package:flutter/material.dart';
import '../models/item_model.dart';
import '../blocs/movies_bloc.dart';
import 'movie_detail.dart';

class MovieList extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return MovieListState();
  }
}

class MovieListState extends State<MovieList> {
  @override
  void initState() {
    super.initState();
    bloc.fetchAllMovies();
  }

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Popular Movies'),
      ),
      body: StreamBuilder(
        stream: bloc.allMovies,
        builder: (context, AsyncSnapshot<ItemModel> snapshot) {
          if (snapshot.hasData) {
            return buildList(snapshot);
          } else if (snapshot.hasError) {
            return Text(snapshot.error.toString());
          }
          return Center(child: CircularProgressIndicator());
        },
      ),
    );
  }

  Widget buildList(AsyncSnapshot<ItemModel> snapshot) {
    return GridView.builder(
        itemCount: snapshot.data.results.length,
        gridDelegate:
        new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
        itemBuilder: (BuildContext context, int index) {
          return GridTile(
            child: InkResponse(
              enableFeedback: true,
              child: Image.network(
                '
                    .results[index].poster_path}',
                fit: BoxFit.cover,
              ),
              onTap: () => openDetailPage(snapshot.data, index),
            ),
          );
        });
  }

  openDetailPage(ItemModel data, int index) {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) {
        return MovieDetail(
          title: data.results[index].title,
          posterUrl: data.results[index].backdrop_path,
          description: data.results[index].overview,
          releaseDate: data.results[index].release_date,
          voteAverage: data.results[index].vote_average.toString(),
          movieId: data.results[index].id,
        );
      }),
    );
  }
}

В приведенном выше коде, как вы можете видеть, метод openDetailPage() имеет навигационную логику. Мы передаем данные, которые покажем на подробном экране. Запустите приложение, и вы сможете перейти к новому экрану.

Ух ты! я умею ориентироваться 😍

Теперь пришло время показать трейлеры на экране подробностей. Давайте разберемся с API, который мы будем использовать для получения трейлеров с сервера. Ниже приведена ссылка, по которой мы будем нажимать, чтобы получить ответ JSON.

https://api.themoviedb.org/3/movie//videos?api_key=your_api_key

В приведенном выше API мы должны ввести две вещи. Во-первых, это movie_id, а во-вторых, ключ API. Вот как выглядит ответ после того, как вы нажмете на API.

{
  "id": 299536,
  "results": [
    {
      "id": "5a200baa925141033608f5f0",
      "iso_639_1": "en",
      "iso_3166_1": "US",
      "key": "6ZfuNTqbHE8",
      "name": "Official Trailer",
      "site": "YouTube",
      "size": 1080,
      "type": "Trailer"
    },
    {
      "id": "5a200bcc925141032408d21b",
      "iso_639_1": "en",
      "iso_3166_1": "US",
      "key": "sAOzrChqmd0",
      "name": "Action...Avengers: Infinity War",
      "site": "YouTube",
      "size": 720,
      "type": "Clip"
    },
    {
      "id": "5a200bdd0e0a264cca08d39f",
      "iso_639_1": "en",
      "iso_3166_1": "US",
      "key": "3VbHg5fqBYw",
      "name": "Trailer Tease",
      "site": "YouTube",
      "size": 720,
      "type": "Teaser"
    },
    {
      "id": "5a7833440e0a26597f010849",
      "iso_639_1": "en",
      "iso_3166_1": "US",
      "key": "pVxOVlm_lE8",
      "name": "Big Game Spot",
      "site": "YouTube",
      "size": 1080,
      "type": "Teaser"
    },
    {
      "id": "5aabd7e69251413feb011276",
      "iso_639_1": "en",
      "iso_3166_1": "US",
      "key": "QwievZ1Tx-8",
      "name": "Official Trailer #2",
      "site": "YouTube",
      "size": 1080,
      "type": "Trailer"
    },
    {
      "id": "5aea2ed2c3a3682bf7001205",
      "iso_639_1": "en",
      "iso_3166_1": "US",
      "key": "LXPaDL_oILs",
      "name": "\"Legacy\" TV Spot",
      "site": "YouTube",
      "size": 1080,
      "type": "Teaser"
    },
    {
      "id": "5aea2f3e92514172a7001672",
      "iso_639_1": "en",
      "iso_3166_1": "US",
      "key": "PbRmbhdHDDM",
      "name": "\"Family\" Featurette",
      "site": "YouTube",
      "size": 1080,
      "type": "Featurette"
    }
  ]
}

Для приведенного выше ответа нам нужен класс POJO. Давайте сначала построим это. Создайте файл с именем trailer_model.dart внутри модели упаковка. Скопируйте и вставьте в него приведенный ниже код.

class TrailerModel {
  int _id;
  List<_Result> _results = [];

  TrailerModel.fromJson(Map<String, dynamic> parsedJson) {
    _id = parsedJson['id'];
    List<_Result> temp = [];
    for (int i = 0; i < parsedJson['results'].length; i++) {
      _Result result = _Result(parsedJson['results'][i]);
      temp.add(result);
    }
    _results = temp;
  }

  List<_Result> get results => _results;

  int get id => _id;
}

class _Result {
  String _id;
  String _iso_639_1;
  String _iso_3166_1;
  String _key;
  String _name;
  String _site;
  int _size;
  String _type;

  _Result(result) {
    _id = result['id'];
    _iso_639_1 = result['iso_639_1'];
    _iso_3166_1 = result['iso_3166_1'];
    _key = result['key'];
    _name = result['name'];
    _site = result['site'];
    _size = result['size'];
    _type = result['type'];
  }

  String get id => _id;

  String get iso_639_1 => _iso_639_1;

  String get iso_3166_1 => _iso_3166_1;

  String get key => _key;

  String get name => _name;

  String get site => _site;

  int get size => _size;

  String get type => _type;
}

Теперь давайте реализуем сетевой вызов внутри movie_api_provider.dart файл. Скопируйте и вставьте ниже в файл.

import 'dart:async';
import 'package:http/http.dart' show Client;
import 'dart:convert';
import '../models/item_model.dart';
import '../models/trailer_model.dart';

class MovieApiProvider {
  Client client = Client();
  final _apiKey = '802b2c4b88ea1183e50e6b285a27696e';
  final _baseUrl = "

  Future<ItemModel> fetchMovieList() async {
    final response = await client.get("$_baseUrl/popular?api_key=$_apiKey");
    if (response.statusCode == 200) {
      // If the call to the server was successful, parse the JSON
      return ItemModel.fromJson(json.decode(response.body));
    } else {
      // If that call was not successful, throw an error.
      throw Exception('Failed to load post');
    }
  }

  Future<TrailerModel> fetchTrailer(int movieId) async {
    final response =
        await client.get("$_baseUrl/$movieId/videos?api_key=$_apiKey");

    if (response.statusCode == 200) {
      return TrailerModel.fromJson(json.decode(response.body));
    } else {
      throw Exception('Failed to load trailers');
    }
  }
}

fetchTrailer(movie_id) это метод, с помощью которого мы обращаемся к API и преобразуем ответ JSON в TrailerModel объект и вернуть Future<TrailerModel>.

Теперь давайте обновим репозиторий.dart файл, добавив эту новую реализацию сетевого вызова. Скопируйте и вставьте приведенный ниже код внутри репозиторий.dart файл.

import 'dart:async';
import 'movie_api_provider.dart';
import '../models/item_model.dart';
import '../models/trailer_model.dart';

class Repository {
  final moviesApiProvider = MovieApiProvider();

  Future<ItemModel> fetchAllMovies() => moviesApiProvider.fetchMovieList();

  Future<TrailerModel> fetchTrailers(int movieId) => moviesApiProvider.fetchTrailer(movieId);
}

Теперь пришло время реализовать подход Scoped Instance BLoC. Создать новый файл movie_detail_bloc.dart внутри блоки упаковка. Создайте еще один файл movie_detail_bloc_provider.dart внутри одного и того же пакета блоков.

Вот код для movie_detail_bloc_provider.dart файл.

import 'package:flutter/material.dart';
import 'movie_detail_bloc.dart';
export 'movie_detail_bloc.dart';

class MovieDetailBlocProvider extends InheritedWidget {
  final MovieDetailBloc bloc;

  MovieDetailBlocProvider({Key key, Widget child})
      : bloc = MovieDetailBloc(),
        super(key: key, child: child);

  @override
  bool updateShouldNotify(_) {
    return true;
  }

  static MovieDetailBloc of(BuildContext context) {
    return (context.inheritFromWidgetOfExactType(MovieDetailBlocProvider)
            as MovieDetailBlocProvider)
        .bloc;
  }
}

Этот класс расширяет InheritedWidget и обеспечивает доступ к блоку через of(context) метод. Как вы можете видеть of(context) ожидает контекст в качестве параметра. Этот контекст принадлежит экрану, который запаковал InheritedWidget. В нашем случае это экран деталей фильма.

Напишем код для movie_detail_bloc.dart. Скопируйте и вставьте приведенный ниже код в файл блока.

import 'dart:async';

import 'package:rxdart/rxdart.dart';
import '../models/trailer_model.dart';
import '../resources/repository.dart';

class MovieDetailBloc {
  final _repository = Repository();
  final _movieId = PublishSubject<int>();
  final _trailers = BehaviorSubject<Future<TrailerModel>>();

  Function(int) get fetchTrailersById => _movieId.sink.add;
  Observable<Future<TrailerModel>> get movieTrailers => _trailers.stream;

  MovieDetailBloc() {
    _movieId.stream.transform(_itemTransformer()).pipe(_trailers);
  }

  dispose() async {
    _movieId.close();
    await _trailers.drain();
    _trailers.close();
  }

  _itemTransformer() {
    return ScanStreamTransformer(
      (Future<TrailerModel> trailer, int id, int index) {
        print(index);
        trailer = _repository.fetchTrailers(id);
        return trailer;
      },
    );
  }
}

Позвольте мне немного объяснить вам приведенный выше код. Идея получения списка трейлеров с сервера заключается в том, что мы должны передать movieId API трейлера, а в ответ он отправит нам список трейлеров. Для реализации этой идеи мы будем использовать одну важную особенность RxDart то есть Трансформеры .

Трансформеры

Трансформеры в основном помогают соединить два или более Предметы и получить окончательный результат. Идея в том, если вы хотите передать данные от одного субъекта к другому после выполнения некоторых операций над данными. Мы будем использовать преобразователи для выполнения операций с входными данными от первого субъекта и передачи их следующему субъекту.

В нашем приложении мы добавим movieId в _movieId который является Опубликовать Тему. Мы передадим movieId в ScanStreamTransformer который, в свою очередь, заставит сеть вызывать API-интерфейс трейлера, получать результаты и передавать их в _trailers который является ПоведениеТема. Вот небольшая диаграмма, чтобы проиллюстрировать мое объяснение.


Роль трансформатора

Последний шаг, который нам остался, это сделать movieDetailBloc доступным для экрана MovieDetail. Для этого нам нужно обновить openDetailPage() метод. Вот обновленный код для файла movie_list.dart.

import 'package:flutter/material.dart';
import '../models/item_model.dart';
import '../blocs/movies_bloc.dart';
import 'movie_detail.dart';
import '../blocs/movie_detail_bloc_provider.dart';

class MovieList extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return MovieListState();
  }
}

class MovieListState extends State<MovieList> {
  @override
  void initState() {
    super.initState();
    bloc.fetchAllMovies();
  }

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Popular Movies'),
      ),
      body: StreamBuilder(
        stream: bloc.allMovies,
        builder: (context, AsyncSnapshot<ItemModel> snapshot) {
          if (snapshot.hasData) {
            return buildList(snapshot);
          } else if (snapshot.hasError) {
            return Text(snapshot.error.toString());
          }
          return Center(child: CircularProgressIndicator());
        },
      ),
    );
  }

  Widget buildList(AsyncSnapshot<ItemModel> snapshot) {
    return GridView.builder(
        itemCount: snapshot.data.results.length,
        gridDelegate:
        new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
        itemBuilder: (BuildContext context, int index) {
          return GridTile(
            child: InkResponse(
              enableFeedback: true,
              child: Image.network(
                '
                    .results[index].poster_path}',
                fit: BoxFit.cover,
              ),
              onTap: () => openDetailPage(snapshot.data, index),
            ),
          );
        });
  }

  openDetailPage(ItemModel data, int index) {
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) {
        return MovieDetailBlocProvider(
          child: MovieDetail(
            title: data.results[index].title,
            posterUrl: data.results[index].backdrop_path,
            description: data.results[index].overview,
            releaseDate: data.results[index].release_date,
            voteAverage: data.results[index].vote_average.toString(),
            movieId: data.results[index].id,
          ),
        );
      }),
    );
  }
}

Как вы видите внутри MaterialPageRoute мы возвращаем MovieDetailBlocProvider (InheritedWidget) и упаковка MovieDetail экран в него. Таким образом MovieDetailBloc класс будет доступен внутри экрана сведений и для всех виджетов под ним.

Наконец, вот код для movie_detail.dart файл.

import 'dart:async';

import 'package:flutter/material.dart';
import '../blocs/movie_detail_bloc_provider.dart';
import '../models/trailer_model.dart';

class MovieDetail extends StatefulWidget {
  final posterUrl;
  final description;
  final releaseDate;
  final String title;
  final String voteAverage;
  final int movieId;

  MovieDetail({
    this.title,
    this.posterUrl,
    this.description,
    this.releaseDate,
    this.voteAverage,
    this.movieId,
  });

  @override
  State<StatefulWidget> createState() {
    return MovieDetailState(
      title: title,
      posterUrl: posterUrl,
      description: description,
      releaseDate: releaseDate,
      voteAverage: voteAverage,
      movieId: movieId,
    );
  }
}

class MovieDetailState extends State<MovieDetail> {
  final posterUrl;
  final description;
  final releaseDate;
  final String title;
  final String voteAverage;
  final int movieId;

  MovieDetailBloc bloc;

  MovieDetailState({
    this.title,
    this.posterUrl,
    this.description,
    this.releaseDate,
    this.voteAverage,
    this.movieId,
  });

  @override
  void didChangeDependencies() {
    bloc = MovieDetailBlocProvider.of(context);
    bloc.fetchTrailersById(movieId);
    super.didChangeDependencies();
  }

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        top: false,
        bottom: false,
        child: NestedScrollView(
          headerSliverBuilder: (BuildContext context,
              bool innerBoxIsScrolled) {
            return <Widget>[
              SliverAppBar(
                expandedHeight: 200.0,
                floating: false,
                pinned: true,
                elevation: 0.0,
                flexibleSpace: FlexibleSpaceBar(
                    background: Image.network(
                      "
                      fit: BoxFit.cover,
                    )),
              ),
            ];
          },
          body: Padding(
            padding: const EdgeInsets.all(10.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Container(margin: EdgeInsets.only(top: 5.0)),
                Text(
                  title,
                  style: TextStyle(
                    fontSize: 25.0,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                Container(margin: EdgeInsets.only(top: 8.0,
                    bottom: 8.0)),
                Row(
                  children: <Widget>[
                    Icon(
                      Icons.favorite,
                      color: Colors.red,
                    ),
                    Container(
                      margin: EdgeInsets.only(left: 1.0,
                          right: 1.0),
                    ),
                    Text(
                      voteAverage,
                      style: TextStyle(
                        fontSize: 18.0,
                      ),
                    ),
                    Container(
                      margin: EdgeInsets.only(left: 10.0,
                          right: 10.0),
                    ),
                    Text(
                      releaseDate,
                      style: TextStyle(
                        fontSize: 18.0,
                      ),
                    ),
                  ],
                ),
                Container(margin: EdgeInsets.only(top: 8.0,
                    bottom: 8.0)),
                Text(description),
                Container(margin: EdgeInsets.only(top: 8.0,
                    bottom: 8.0)),
                Text(
                  "Trailer",
                  style: TextStyle(
                    fontSize: 25.0,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                Container(margin: EdgeInsets.only(top: 8.0,
                    bottom: 8.0)),
                StreamBuilder(
                  stream: bloc.movieTrailers,
                  builder:
                      (context, AsyncSnapshot<Future<TrailerModel>> snapshot) {
                    if (snapshot.hasData) {
                      return FutureBuilder(
                        future: snapshot.data,
                        builder: (context,
                            AsyncSnapshot<TrailerModel> itemSnapShot) {
                          if (itemSnapShot.hasData) {
                            if (itemSnapShot.data.results.length > 0)
                              return trailerLayout(itemSnapShot.data);
                            else return noTrailer(itemSnapShot.data);
                          } else {
                            return Center(child: CircularProgressIndicator());
                          }
                        },
                      );
                    } else {
                      return Center(child: CircularProgressIndicator());
                    }
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget noTrailer(TrailerModel data) {
    return Center(
      child: Container(
        child: Text("No trailer available"),
      ),
    );
  }

  Widget trailerLayout(TrailerModel data) {
    if (data.results.length > 1) {
      return Row(
        children: <Widget>[
          trailerItem(data, 0),
          trailerItem(data, 1),
        ],
      );
    } else {
      return Row(
        children: <Widget>[
          trailerItem(data, 0),
        ],
      );
    }
  }

  trailerItem(TrailerModel data, int index) {
    return Expanded(
      child: Column(
        children: <Widget>[
          Container(
            margin: EdgeInsets.all(5.0),
            height: 100.0,
            color: Colors.grey,
            child: Center(child: Icon(Icons.play_circle_filled)),
          ),
          Text(
            data.results[index].name,
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
          ),
        ],
      ),
    );
  }
}

Здесь есть несколько вещей, которые следует отметить. Мы инициализируем MovieDetailBloc внутри didChangeDependencies() из-за это. Также вы можете увидеть StreamBuilder’s данные моментального снимка содержат Future<TrailerModel> который может потреблять только FutureBuilder.

Я не буду подробно объяснять этот материал, так как эта статья уже разрослась, и я представил много нового. Пожалуйста, не стесняйтесь связаться со мной по адресу LinkedIn, Твиттер или оставьте свои комментарии ниже, если вам нужна помощь.

Надеюсь, вам понравился материал и вы узнали много нового. Я просто хотел сказать вам, что сначала будет трудно это понять. Но поверьте мне, если вы еще немного прочитаете обо всех темах, которые я здесь затронул, вы обнаружите, что это очень просто. Пожалуйста, хлопайте вслух 😄.

Если вам нужен полный код. Здесь репозиторий github проекта.


Флаттер Паб — это среднее издание, предлагающее вам последние и удивительные ресурсы, такие как статьи, видео, коды, подкасты и т. д. об этой замечательной технологии, чтобы научить вас создавать с ее помощью красивые приложения. Вы можете найти нас на Фейсбук, Твиттера также Середина или узнайте больше о нас здесь. Мы хотели бы подключиться! И если вы писатель, заинтересованный в том, чтобы писать для нас, вы можете это сделать. с помощью этих руководящих принципов.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *