본문 바로가기

Flutter/11 Quiz

[Flutter] Quiz - level2(GridView)

이번에는 코딩셰프 도장깨기 레벨2 입니다.

개발환경 : 윈도우11, 안드로이드 스튜디오, flutter 3.0.1

소스코드 위치 - https://github.com/mike-bskim/dojang_level2/releases/tag/01_done

 

Release 01_done · mike-bskim/dojang_level2

 

github.com

 

 

 

화면은 아래와 같습니다.

 

 

미션은

1. api 서버에서 데이터를 받을때까지 로딩 인디케이터 처리

2. 카트에 추가된 아이템 개수 표시

 

 

 

프로젝트 구성

 

.\main.dart
.\controller\product_controller.dart
.\model\model_product.dart
.\services\service_product.dart
.\view\my_page.dart
.\view\product_tile.dart

 

 

.\main.dart

 

import 'package:flutter/material.dart';

import 'view/my_page.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyPage(),
    );
  }
}

 

 

.\controller\product_controller.dart

 

import 'package:get/get.dart';

import '../model/model_product.dart';
import '../services/service_product.dart';

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

  // 화면 표시용 객체
  var productList = <Product>[].obs;

  /* *********** 도장깨기 미션 *********** */
  var cartItems = <Product>[].obs;
  int get count => cartItems.length;
  addToItem(Product product){
    cartItems.add(product);
  }
  /* *********** 도장깨기 미션 *********** */

  @override
  void onInit() {
    // TODO: implement onInit
    super.onInit();
    fetchData();
  }

  void fetchData() async {
    var products = await Services.fetchProducts();
    if(products != null){
      productList.value = products;
      // productList(products);
    }
  }

}

 


.\model\model_product.dart

 

// To parse this JSON data, do
//
//     final product = productFromJson(jsonString);

import 'dart:convert';
import 'package:get/get.dart';

List<Product> productFromJson(String str) => List<Product>.from(json.decode(str).map((x) => Product.fromJson(x)));

String productToJson(List<Product> data) => json.encode(List<dynamic>.from(data.map((x) => x.toJson())));

class Product {
  Product({
    required this.id,
    required this.brand,
    required this.name,
    required this.price,
    required this.priceSign,
    required this.currency,
    required this.imageLink,
    required this.productLink,
    required this.websiteLink,
    required this.description,
    required this.rating,
    required this.category,
    required this.productType,
    required this.tagList,
    required this.createdAt,
    required this.updatedAt,
    required this.productApiUrl,
    required this.apiFeaturedImage,
    required this.productColors,
  });

  int id;
  Brand? brand;
  String name;
  String price;
  dynamic priceSign;
  dynamic currency;
  String imageLink;
  String productLink;
  String websiteLink;
  String description;
  double rating;
  String? category;
  String productType;
  List<dynamic> tagList;
  DateTime createdAt;
  DateTime updatedAt;
  String productApiUrl;
  String apiFeaturedImage;
  List<ProductColor> productColors;

  var like = false.obs;

  factory Product.fromJson(Map<String, dynamic> json) => Product(
    id: json["id"],
    brand: brandValues.map[json["brand"]],
    name: json["name"],
    price: json["price"],
    priceSign: json["price_sign"],
    currency: json["currency"],
    imageLink: json["image_link"],
    productLink: json["product_link"],
    websiteLink: json["website_link"],
    description: json["description"],
    rating: json["rating"] == null ? 0.0 : json["rating"].toDouble(),
    category: json["category"],//json["category"] == null ? '' : json["category"],
    productType: json["product_type"],
    tagList: List<dynamic>.from(json["tag_list"].map((x) => x)),
    createdAt: DateTime.parse(json["created_at"]),
    updatedAt: DateTime.parse(json["updated_at"]),
    productApiUrl: json["product_api_url"],
    apiFeaturedImage: json["api_featured_image"],
    productColors: List<ProductColor>.from(json["product_colors"].map((x) => ProductColor.fromJson(x))),
  );

  Map<String, dynamic> toJson() => {
    "id": id,
    "brand": brandValues.reverse[brand],
    "name": name,
    "price": price,
    "price_sign": priceSign,
    "currency": currency,
    "image_link": imageLink,
    "product_link": productLink,
    "website_link": websiteLink,
    "description": description,
    "rating": rating,//rating == null ? null : rating,
    "category": category,//category == null ? null : category,
    "product_type": productType,
    "tag_list": List<dynamic>.from(tagList.map((x) => x)),
    "created_at": createdAt.toIso8601String(),
    "updated_at": updatedAt.toIso8601String(),
    "product_api_url": productApiUrl,
    "api_featured_image": apiFeaturedImage,
    "product_colors": List<dynamic>.from(productColors.map((x) => x.toJson())),
  };
}

enum Brand { MAYBELLINE }

final brandValues = EnumValues({
  "maybelline": Brand.MAYBELLINE
});

class ProductColor {
  ProductColor({
    required this.hexValue,
    required this.colourName,
  });

  String hexValue;
  String? colourName;

  factory ProductColor.fromJson(Map<String, dynamic> json) => ProductColor(
    hexValue: json["hex_value"],
    colourName: json["colour_name"],//json["colour_name"] == null ? '' : json["colour_name"],
  );

  Map<String, dynamic> toJson() => {
    "hex_value": hexValue,
    "colour_name": colourName,//colourName == null ? null : colourName,
  };
}

class EnumValues<T> {
  Map<String, T> map;
  late Map<T, String> reverseMap;

  EnumValues(this.map);

  Map<T, String> get reverse {
    // if (reverseMap == null) {
      reverseMap = map.map((k, v) => MapEntry(v, k));
    // }
    return reverseMap;
  }
}

 

 

.\services\service_product.dart

 

// import 'package:flutter/cupertino.dart';
import 'package:http/http.dart' as http;

import '../model/model_product.dart';

class Services {
  static var client = http.Client();

  static Future<List<Product>?> fetchProducts() async {
    var response = await client.get(Uri.parse('http://makeup-api.herokuapp.com'
        '/api/v1/products.json?brand=maybelline'));

    if (response.statusCode == 200) {
      var jsonData = response.body;
      return productFromJson(jsonData);
    } else {
      return null;
    }
  }
}

 

 

.\view\my_page.dart

 

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:level02_getx/view/product_tile.dart';

import '../controller/product_controller.dart';

class MyPage extends StatelessWidget {
  MyPage({Key? key}) : super(key: key);

  final productController = Get.put(ProductController());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Chef Shop'),
        backgroundColor: Colors.black87,
        elevation: 0,
        leading: const Icon(Icons.arrow_back_ios),
        actions: [
          // IconButton(
          //   onPressed: () {},
          //   icon: const Icon(Icons.view_list_rounded),
          // ),
          IconButton(
            onPressed: () {},
            icon: const Icon(Icons.shopping_cart),
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.fromLTRB(0, 16, 0, 16),
        child: Obx(() {
          return productController.productList.isEmpty
              ? const Center(child: CircularProgressIndicator())
              : GridView.builder(
                  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 2,
                    mainAxisSpacing: 8,
                    crossAxisSpacing: 8,
                  ),
                  itemBuilder: (context, index) {
                    return ProductTile(
                      product: productController.productList[index],
                    );
                  },
                  itemCount: productController.productList.length,
                );
        }),
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () {},
        label: Obx(() {
          return Text(
            'Items: ${productController.count}',
            style: const TextStyle(fontSize: 20),
          );
        }),
        icon: const Icon(Icons.add_shopping_cart_rounded),
        backgroundColor: Colors.redAccent,
      ),
    );
  }
}

 

 

.\view\product_tile.dart

 

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:level02_getx/controller/product_controller.dart';

import '../model/model_product.dart';

class ProductTile extends StatelessWidget {
  final Product product;

  const ProductTile({Key? key, required this.product}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Stack(
              children: [
              // 사진영역
                Container(
                  height: 75,
                  width: 100,
                  clipBehavior: Clip.antiAlias,
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(4),
                  ),
                  child: Image.network(
                    product.imageLink,
                    // fit: BoxFit.fill,
                  ),
                ),
                Obx(() {
                // 좋아요 하트 표시
                  return CircleAvatar(
                    backgroundColor: Colors.white,
                    radius: 15,
                    child: IconButton(
                      icon: product.like.value
                          ? const Icon(Icons.favorite_rounded)
                          : const Icon(Icons.favorite_border),
                      padding: EdgeInsets.zero,
                      onPressed: () {
                        product.like.toggle();
                      },
                      iconSize: 18,
                    ),
                  );
                }),
              ],
            ),
            const SizedBox(height: 8),
            Text(
              product.name,
              maxLines: 1,
              style: const TextStyle(fontWeight: FontWeight.w400),
              overflow: TextOverflow.ellipsis,
            ),
            const SizedBox(height: 8),
            Row(
              children: [
                Container(
                  decoration: BoxDecoration(
                    color: Colors.green,
                    borderRadius: BorderRadius.circular(12),
                  ),
                  padding:
                  const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      Text(
                        product.rating.toDouble().toString(),
                        style: const TextStyle(color: Colors.white),
                      ),
                      const Icon(
                        Icons.star,
                        size: 16,
                        color: Colors.white,
                      ),
                    ],
                  ),
                ),
                const SizedBox(width: 12.0),
                GestureDetector(
                  onTap: () {
                    debugPrint('Clicked add to cart');
                    ProductController.to.addToItem(product);
                  },
                  child: Container(
                    decoration: BoxDecoration(
                      color: Colors.blue,
                      borderRadius: BorderRadius.circular(12),
                    ),
                    padding:
                    const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
                    child: const Text(
                      'Add to cart',
                      style: TextStyle(color: Colors.white),
                    ),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 8),
            Text(
              '\$${product.price}',
              style: const TextStyle(
                fontSize: 20,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

 

 

 

 

 

[참고자료] 코딩셰프 -

https://www.youtube.com/watch?v=IGZ4J9xxLmU&t=561s 

 

'Flutter > 11 Quiz' 카테고리의 다른 글

[Flutter] Quiz - level3(Getx login)  (0) 2022.07.29