본문 바로가기

Flutter/12 Clone 'Used Goods app'

[Flutter] Clone - 당근마켓19(Shimmer)

이번에는 데이터가 로딩될때 인디케이터 방식 대신 Shimmer 를 이용하여 로딩시 대기 화면을 구현해보겠습니다.

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

 

 

 

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

 

 

 

 

필요한 패키지는 아래와 같습니다.

 

shimmer: ^2.0.0

 

 

./src/screens/home/items_page.dart 에 shimmer 및 SizeTransition 구현

FutureBuilder 를 이용하여 5초동안 shimmer 가 보여지고 이후에는 정상 데이터가 보여진다.

shimmer 가 보여지는 동안 animationController 를 이용하여 일부 컨테이너의 사이즈를 가변 시켰다.

 

// 메인 build 에 _shimmerListView 추가.
@override
Widget build(BuildContext context) {
  // 사진 사이즈를 화면 비율에 맞춰서 비례적으로 주기 위해서 LayoutBuilder 사용함,
  return LayoutBuilder(
    builder: (context, constraints) {
      Size size = MediaQuery.of(context).size;
      final imgSize = size.width / 4;

      return FutureBuilder(
          future: Future.delayed(const Duration(seconds: 5)),
          builder: (context, snapshot) {
            return AnimatedSwitcher(
              duration: const Duration(seconds: 1),
              child: (snapshot.connectionState == ConnectionState.done)
                  ? _listView(imgSize)
                  : _shimmerListView(imgSize),
            );
          });
    },
  );
}


// _shimmerListView 위젯 생성
  Widget _shimmerListView(double imgSize) {
    // BoxDecoration 에서 색상을 설정시, Container 에서는 색상정보를 제거해야 한다,
    BoxDecoration containerDeco({required double radius}) {
      return BoxDecoration(
          shape: BoxShape.rectangle,
          color: Colors.white,
          borderRadius: BorderRadius.circular(radius));
    }

    _containerSample({required double height, required double width, required double radius}) {
      return Container(height: height, width: width, decoration: containerDeco(radius: radius));
    }

    return Shimmer.fromColors(
      highlightColor: Colors.grey[100]!,
      enabled: true,
      baseColor: Colors.grey[300]!,
      period: const Duration(seconds: 2),
      child: ListView.separated(
        padding: const EdgeInsets.all(padding_16),
        separatorBuilder: (context, index) {
          return const Divider(
            thickness: 1,
            color: Colors.black26,
            height: padding_16 * 2 + 1,
            indent: padding_16,
            endIndent: padding_16,
          );
        },
        itemCount: 10,
        itemBuilder: (context, index) {
          return SizedBox(
            height: imgSize,
            child: Row(
              children: <Widget>[
                _containerSample(height: imgSize, width: imgSize, radius: 12.0),
                const SizedBox(width: padding_16),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
// 동일한 위젯을 호출하기 위해서는 키값이 필요하다
                      SizeTransitionAnimation(key: UniqueKey(), height: 14, width: 140, radius: 4),
                      const SizedBox(height: 8),
                      SizeTransitionAnimation(key: UniqueKey(), height: 12, width: 70, radius: 4),
                      const SizedBox(height: 8),
                      SizeTransitionAnimation(key: UniqueKey(), height: 14, width: 100, radius: 4),
                      Expanded(child: Container()),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.end,
                        children: [
                          SizeTransitionAnimation(key: UniqueKey(), height: 14, width: 150, radius: 4),
                        ],
                      ),
                    ],
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
  
  
// 새로운 에니메이션을 위한 클래스를 StatefulWidget 으로 생성. 
class SizeTransitionAnimation extends StatefulWidget {
  final double height;
  final double width;
  final double radius;

  const SizeTransitionAnimation({
    Key? key,
    required this.height,
    required this.width,
    required this.radius,
  }) : super(key: key);

  @override
  State<SizeTransitionAnimation> createState() => _SizeTransitionAnimationState();
}

class _SizeTransitionAnimationState extends State<SizeTransitionAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller = AnimationController(
    vsync: this,
    duration: const Duration(seconds: 1),
    // 에니메이션이 70 ~ 100% 사이로 움직이게 설정
    lowerBound: 0.7,
    // 에니메이션이 계속 반복
  )..repeat(reverse: true);

  late Animation<double> animation = CurvedAnimation(
    parent: controller,
    curve: Curves.easeInOut,
  );

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

  BoxDecoration containerDeco({required double radius}) {
    return BoxDecoration(
      shape: BoxShape.rectangle,
      color: Colors.white,
      borderRadius: BorderRadius.circular(radius),
      // Shimmer 때문에, 실제 gradient 는 동작하지 않음,
      gradient: const LinearGradient(
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
        colors: [
          Colors.deepOrange,
          Colors.deepPurple,
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return SizeTransition(
      sizeFactor: animation,
      axis: Axis.horizontal,
      axisAlignment: 0.5,
      child: Container(
        height: widget.height,
        width: widget.width,
        decoration: containerDeco(radius: widget.radius),
      ),
    );
  }
}