본문 바로가기

Flutter/12 Clone 'Used Goods app'

[Flutter] Clone - 당근마켓37(Item read & Get.arguments)

이번에는 글목록 페이지에서 더미 데이터를 실제 데이터로 변경하는 부분 및 상세 페이지 추가해보겠습니다.

글목록에서 상세페이지로 이동시, arguments 를 통해서 "아이템 키값" 을 전달하는 방법도 구현해보겠습니다.

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

 

 

 

구현된 화면은 아래와 같습니다.

 

 

 

 

./src/repo/item_service.dart - 저장된 글목록 로딩 함수 추가

 

Future<ItemModel2> getItem(String itemKey) async {
  if (itemKey[0] == ':') {
    String orgItemKey = itemKey;
    itemKey = itemKey.substring(1);
    logger.d('[${orgItemKey.substring(0, 10)}...], ==>> [${itemKey.substring(0, 9)}...]');
  }
  DocumentReference<Map<String, dynamic>> docRef =
      FirebaseFirestore.instance.collection(COL_ITEMS).doc(itemKey);
  final DocumentSnapshot<Map<String, dynamic>> documentSnapshot = await docRef.get();

  // fromSnapshot 을 대체하려면, 2줄 코딩으로 변경필요함,
  ItemModel2 itemModel = ItemModel2.fromJson(documentSnapshot.data()!);
  itemModel.reference = documentSnapshot.reference;

  return itemModel;
}

Future<List<ItemModel2>> getItems(String userKey) async {
  logger.d('userKey[' + userKey.toString() + ']');
  CollectionReference<Map<String, dynamic>> collectionReference =
      FirebaseFirestore.instance.collection(COL_ITEMS);
  // collection.get() is Future, collection.snapshots() is Stream
  // 모든 게시글 가져오기
  QuerySnapshot<Map<String, dynamic>> snapshots = await collectionReference.get();
  // 자신의 게시글 제외하고 가져오기
  // QuerySnapshot<Map<String, dynamic>> snapshots =
  //     await collectionReference.where(DOC_USERKEY, isNotEqualTo: userKey).get();

  List<ItemModel2> items = [];

  for (var snapshot in snapshots.docs) {
    //fromQuerySnapshot 대체품 만들것,
    // ItemModel2 itemModel = ItemModel2.fromQuerySnapshot(snapshot);
    ItemModel2 itemModel = ItemModel2.fromJson(snapshot.data());
    itemModel.reference = snapshot.reference;
    items.add(itemModel);
  }

  return items;
}

 

 

 

./src/screens/home/items_screen.dart - item_page 에서 이름변경

1. future 대상 객체 변경.

2. _listView(imgSize, snapshot.data!) - 추가된 인자를 통해서 데이터 전달.

 

return FutureBuilder<List<ItemModel2>>(
    // future: Future.delayed(const Duration(seconds: 2), () => 100),
    future: ItemService().getItems(UserController.to.user.value!.uid),
    //(화면전화 이전에 매핑되므로 사용가능)FirebaseAuth.instance.currentUser!.uid
    //(화면전화 이전에 매핑되므로 사용가능)UserController.to.user.value!.uid
    //(화면전화 이후에 매핑되므로 사용불가)UserController.to.userModel.value!.userKey
    builder: (context, snapshot) {
      return AnimatedSwitcher(
        duration: const Duration(seconds: 1),
        // child: (snapshot.connectionState == ConnectionState.done)
        child: (snapshot.hasData && snapshot.data!.isNotEmpty)
            ? _listView(imgSize, snapshot.data!)
            : _shimmerListView(imgSize),
      );
    });

 

3. 전달받은 인자를 이용하여 화면에 게시글 정보 표시.

4. Get 를 통한 라우팅시 인자를 통해서 상세 정보를 로딩할 "아이템 키값" 전달

Get.toNamed(ROUTE_ITEM_DETAIL, arguments: {'itemKey': _item.itemKey});

 

Widget _listView(double imgSize, List<ItemModel2> items) {
  return ListView.separated(
    padding: const EdgeInsets.all(padding_16),
    separatorBuilder: (context, index) {
      return Divider(
        thickness: 1,
        // 실제 라인 두께
        color: Colors.grey[400],
        height: padding_16 * 2 + 1,
        // 라인 위/아래의 공간
        indent: padding_16,
        // 시작 부분 공간
        endIndent: padding_16, // 끝나는 부분 공간
      );
    },
    itemCount: items.length,
    itemBuilder: (context, index) {
      ItemModel2 _item = items[index];
      return InkWell(
        //InkWell
        onTap: () {
          debugPrint('call Item detail page[$index/${_item.itemKey}]');
          Get.toNamed(ROUTE_ITEM_DETAIL, arguments: {'itemKey': _item.itemKey});
        },
        child: SizedBox(
          height: imgSize,
          child: Row(
            children: <Widget>[
              SizedBox(
                  height: imgSize,
                  width: imgSize,
                  child: ExtendedImage.network(
                    _item.imageDownloadUrls[0],
                    fit: BoxFit.cover,
                    // borderRadius 를 사용하기 위해서는, BoxShape.rectangle 설정도 같이 해야함,
                    shape: BoxShape.rectangle,
                    borderRadius: BorderRadius.circular(15.0),
                  )),
              const SizedBox(
                width: padding_16,
              ),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(_item.title, style: Theme.of(context).textTheme.subtitle1),
                    Text(
                      '50일전',
                      style: Theme.of(context).textTheme.subtitle2,
                    ),
                    Text(_item.price.toString() + '원'),
                    // 금액과 하트 사이에 공백을 최대한 주기위해서 Expanded 사용함,
                    Expanded(child: Container()),
                    // Row 가 2번 사용된 이유는 아래와 같다.
                    Row(
                      // 오론쪽 끝으로 정렬하기
                      mainAxisAlignment: MainAxisAlignment.end,
                      children: [
                        // 폰트 사이즈를 줄이기 위해서 사이즈박스로 처리
                        SizedBox(
                          height: 16,
                          // 이걸로 다시 FittedBox 로 감싸면, 위젯 밖으로 나가지 못한다.
                          // 하지만 하위에 위치한 Row 정렬이 제 기능을 하지 못한다.
                          // 그래서 SizedBox 를 다시 한번더 Row 로 감싸고 정렬을 추가한다
                          child: FittedBox(
                            fit: BoxFit.fitHeight,
                            child: Row(
                              mainAxisAlignment: MainAxisAlignment.end,
                              children: const [
                                Icon(CupertinoIcons.chat_bubble_2, color: Colors.grey),
                                Text('23', style: TextStyle(color: Colors.grey)),
                                Icon(CupertinoIcons.heart, color: Colors.grey),
                                Text('123', style: TextStyle(color: Colors.grey)),
                              ],
                            ),
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      );
    },
  );
}

 

 

 

./src/router/locations.dart - 새로운 화면 라우팅 정보 추가

 

GetPage(
  name: ROUTE_ITEM_DETAIL,
  page: () => const ItemDetailPage(),
  transition: Transition.fadeIn,
  middlewares: [CheckAuth()], // 미들웨어를 먼저 확인(로그인 여부 확인)하고 "page:" 로 이동함
),

 

 

 

./src/screens/home/item_detail_page.dart - 전달된 인자를 추출하는 방법

 

import 'package:flutter/material.dart';
import 'package:get/get.dart';

class ItemDetailPage extends StatefulWidget {
  const ItemDetailPage({Key? key}) : super(key: key);

  @override
  State<ItemDetailPage> createState() => _ItemDetailPageState();
}

class _ItemDetailPageState extends State<ItemDetailPage> {
  late String newItemKey;

  @override
  void initState() {
    newItemKey = Get.arguments['itemKey'];
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(child: Text(newItemKey)),
    );
  }
}