본문 바로가기

Flutter/00 Legacy

[Flutter] Getx 실전

참고 영상: (https://www.youtube.com/watch?v=qKIgy26u-uE&t=6s)

참고영상을 바탕으로 provider 방식을 Getx 방식으로 변경한 코딩입니다.

 

  • 전체 흐름도 (이해를 돕기위해서 코딩을 의역한 부분이 있습니다)

구성 : main.dart, app.dart, movie.dart, movie_repository.dart, movie_controller.dart, category_movie_list.dart

 

 

 

main.dart

 

import 'package:flutter/material.dart';
import 'package:flutter_tmdb/src/app.dart';
import 'package:flutter_tmdb/src/controller/movie_controller.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: App(),
    );
  }
}

 

app.dart(view)

 

import 'package:flutter/material.dart';
import 'package:flutter_tmdb/src/components/category_movie_list.dart';
import 'package:flutter_tmdb/src/controller/movie_controller.dart';
import 'package:get/get.dart';

class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {

  final MovieController _movieController = Get.put(MovieController());

  @override
  void initState() {
    super.initState();
  }

  Widget _genreTag(Map<String, dynamic> genre) {
    var isActive = _movieController.activeGenreId.value == genre['id'];
    return GestureDetector(
      onTap: () {
        _movieController.changeCategory(genre);
      },
      child: Container(
        padding: const EdgeInsets.all(10),
        margin: const EdgeInsets.all(5),
        decoration: BoxDecoration(
          border: Border.all(
            width: 1,
            color: Colors.grey,
          ),
          borderRadius: BorderRadius.all(Radius.circular(30)),
          color: isActive ? Colors.grey : Colors.white,
        ),
        child: Text(
          genre['name'],
          style: TextStyle(
            color: isActive ? Colors.white : Colors.grey, //
            fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Movie Catergory'),
      ),
      body: Column(
        children: <Widget>[
          FutureBuilder(
            future: _movieController.loadGenre(),
            builder: (BuildContext context,
                AsyncSnapshot<List<Map<String, dynamic>>> snapshot) {
              if (snapshot.hasData) {
                return SingleChildScrollView(
                      scrollDirection: Axis.horizontal,
                      child: Obx(()=>Row(
                        children: List.generate(snapshot.data!.length,
                                (index) => _genreTag(snapshot.data![index])),
                      )),
                    );

              } else {
                return Center(
                  child: CircularProgressIndicator(),
                );
              }

          }),
          CategoryMovieList(), // => 장르별 영화정보를 표시하는 부분
        ],
      ),
    );
  }
}

 

movide.dart(model)

 

class Movie {
  Movie({
    required this.adult,
    required this.backdropPath,
    required this.genreIds,
    required this.id,
    required this.originalLanguage,
    required this.originalTitle,
    required this.overview,
    required this.popularity,
    required this.posterPath,
    required this.releaseDate,
    required this.title,
    required this.video,
    required this.voteAverage,
    required this.voteCount,
  });

  bool adult;
  String backdropPath;
  List<int> genreIds;
  int id;
  String originalLanguage;
  String originalTitle;
  String overview;
  double popularity;
  String posterPath;
  DateTime? releaseDate;
  String title;
  bool video;
  double voteAverage;
  int voteCount;

  factory Movie.fromJson(Map<String, dynamic> json) => Movie(
    adult: json["adult"],
    backdropPath: json["backdrop_path"] ?? '',
    genreIds: List<int>.from(json["genre_ids"].map((x) => x)),
    id: json["id"],
    originalLanguage: json["original_language"],
    originalTitle: json["original_title"],
    overview: json["overview"],
    popularity: json["popularity"].toDouble(),
    posterPath: json["poster_path"],
    releaseDate: json["release_date"] != null
        ? DateTime.parse(json["release_date"])
        : null,
    title: json["title"],
    video: json["video"],
    voteAverage: json["vote_average"].toDouble(),
    voteCount: json["vote_count"],
  );

  String get posterUrl => "https://image.tmdb.org/t/p/w500/$posterPath";
}

 

movie_controller.dart(controller)

 

import 'package:flutter_tmdb/src/models/movie.dart';
import 'package:flutter_tmdb/src/repository/movie_repository.dart';
import 'package:get/get.dart';

class MovieController extends GetxController {
  static MovieController get to => Get.find();

  var _movieRepository = MovieRepository();
  RxList<Movie> movies = <Movie>[].obs;
  RxInt activeGenreId = (-1).obs;


  Future<List<Map<String, dynamic>>> loadGenre() async {
    var genreList = await _movieRepository.loadGenre();
    print('genreList: ${genreList.toString()}');
    if (genreList.isNotEmpty) {
      activeGenreId.value = genreList.first['id'].toInt();
      _loadMovieListWithGenre();
    }
    return genreList;
  }

  void _loadMovieListWithGenre() async {
//    await Future.delayed(Duration(seconds: 2));
    movies.value = await _movieRepository.loadMovieListWithGenre(activeGenreId.value);
  }

  void changeCategory(Map<String, dynamic> genre) {
    activeGenreId.value = genre["id"];
    _loadMovieListWithGenre();
  }

}

 

movie_repository.dart (API)

 

import 'package:dio/dio.dart';
import 'package:flutter_tmdb/src/models/movie.dart';

class MovieRepository {
  late var _dio;
  MovieRepository() {
    _dio = Dio(
      BaseOptions(
        baseUrl: "https://api.themoviedb.org",
        queryParameters: {
          'api_key': '본인이 신청한 키값을 입력',
        },
      ),
    );
  }
  Future<List<Map<String, dynamic>>> loadGenre() async {
    var response = await _dio.get('/3/genre/movie/list');
    if (response.data != null) {
      var data = response.data['genres'] as List;
      // 모델을 만들면 아래 부분은 fromJson 으로 처리
      return data.map((genre) => genre as Map<String, dynamic>).toList();// 19:20초 근처
    } else {
      return [];
    }
  }

  Future<List<Movie>> loadMovieListWithGenre(int activeGenreId) async {
    var response = await _dio.get('/3/discover/movie',
        queryParameters: {'with_genres': activeGenreId});
    if (response.data != null && response.data['results'] != null) {
      var data = response.data['results'] as List;
      return data.map((movie) => Movie.fromJson(movie)).toList();
    } else {
      return [];
    }
  }
}

 

category_movie_list.dart(view component)

 

import 'package:flutter/material.dart';
import 'package:flutter_tmdb/src/controller/movie_controller.dart';
import 'package:flutter_tmdb/src/models/movie.dart';
import 'package:get/get.dart';


class CategoryMovieList extends StatelessWidget {

  final MovieController _movieController = Get.put(MovieController());

  Widget _movieWidget(Movie movie) {
    return Container(
      margin: const EdgeInsets.only(top: 10),
      padding: const EdgeInsets.only(right: 10),
      width: 150,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          ClipRRect(
            borderRadius: BorderRadius.circular(15),
            child: Image.network(movie.posterUrl),
          ),
          SizedBox(height: 10),
          Text(
            movie.title,
            style: TextStyle(
              fontSize: 12,
            ),
            overflow: TextOverflow.ellipsis,
          ),
          Text(
            movie.voteAverage.toString(),
            style: TextStyle(
              fontSize: 12,
            ),
          )
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(15),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          Text('NEW PLAYING'),
          SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            child: Obx(()=>Row(
              children: List.generate(
                  _movieController.movies.length,
                (movieIndex) => _movieWidget(_movieController.movies[movieIndex]),
                growable: false,
              ),
            ))
          ),
        ],
      ),
    );
  }
}

 

여기까지 Getx 변환에 대한 설명입니다.

 

아래는 추가로 Getx 의 Obx 가 다시 그리는 영역에 대해서 비교해보았습니다. Obx가 _genreTag 밖에서 호출

Obx 초기 위치
문자 출력 추가
문자가 정상출력되고 장르 선택시 버튼 색상이 변경되는것을 확인함
Adventure 장르를 선택시 버튼과 화면내용

 

 

Obx 위치를 변경하였습니다. Obx가 _genreTag 내부의 Container 에서 호출

 

 

Obx 가 Container 만 다시 생성하기 때문에 activeGenreId 값이 변한것은 인지하지만 isActive 값을 변경하지 못해서 버튼의 색상 및 글자색을 반영하지 못하는 문제가 발생한다.  

 

Adventure 장르 선택했으나 버튼은 Action 장르를 가리키고 있는 상태, 장르ID만 28에서 12로 변경됨