사전캠프 6일차
스레드 앱 화면 구성 실습으로 2주차 강의를 마무리했다.
그리고, 3주차 강의에서는 상태 관리에 대해 학습했다.
Widget _quickFeedWriteView() { return Column( children: [ Row( children: [ Image.asset( 'assets/images/profile_image.png', width: 50, ), SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( 'Kimsungduck', style: TextStyle( fontWeight: FontWeight.bold, color: Color(0xff262626), ), ), Text( '새로운 소식이 있나요?', style: TextStyle(color: Color(0xff9a9a9a), fontSize: 14), ), ], ), ) ], ), SizedBox(height: 15), Row( mainAxisAlignment: MainAxisAlignment.start, children: [ SizedBox(width: 60), GestureDetector( child: Image.asset('assets/images/photo_icon.png', width: 30)), SizedBox(width: 10), GestureDetector( child: Image.asset('assets/images/photo_icon.png', width: 30)), SizedBox(width: 10), GestureDetector( child: Image.asset('assets/images/gif_icon.png', width: 30)), SizedBox(width: 10), GestureDetector( child: Image.asset('assets/images/mic_icon.png', width: 30)), SizedBox(width: 10), GestureDetector( child: Image.asset('assets/images/align_icon.png', width: 30)), ], ) ], ); } // 예제 일부
Row와Column을 같이 쓰게 될 경우, 사이즈 관련하여 오류가 발생할 수 있다.
💡 이때는Container가 아닌Expended으로 감싸주면 대부분 해결할 수 있다.
SizedBox를 사용할 수도 있지만, 값을 지정했기 때문에 텍스트가 영역 밖으로 넘어간다면 원하는 화면으로 구성하기 어렵다.
Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: ListView( children: [ _header(), SizedBox(height: 20), Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: _quickFeedWriteView(), ), Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: _singleFeed(), ), ], ), ), ); }
Divider를 사용하면 구분선 등과 같이 선을 넣어줄 수 있다.
child: Stack( children: [ Align( alignment: Alignment.centerLeft, child: ClipRRect( borderRadius: BorderRadius.circular(50), child: Image.network( 'https://~생략', width: 50, ), ), ), Positioned( right: 5, bottom: 2, child: SizedBox( width: 20, height: 20, child: ClipRRect( borderRadius: BorderRadius.circular(20), child: Container( decoration: BoxDecoration(color: Colors.black), child: Icon( Icons.add, color: Colors.white, size: 17, ), ), ), )) ], ), ), SizedBox(height: 15), Container( width: 2, height: 200, color: Color(0xffe5e5e5), ), ],
Container에 width값을 2로 주어 세로선을 만들 수도 있다.- 이미지를 겹치기 위해
Stack을 사용했다.
ClipRRect는BorderRadius를 주어 이미지의 형태를 둥근 사각형 혹은 원로 바꿀 수 있다.- 이미지 위치를 조정하기 위해
Align를 썼으며,alignment: Alignment.centerLeft로 설정했다.
SizedBox( height: 200, child: PageView( padEnds: false, controller: PageController(viewportFraction: 0.75), children: [ Image.asset('assets/images/2.png'), Image.asset('assets/images/1.png'), Image.asset('assets/images/3.png'), ], ), ),
viewportFraction은 이미지 뷰를 캐러셀 슬라이드로 만들기 위해서 사용한다.
여기서 값을 0.75를 주었는데, 이는 이미지 하나당 배분되는 영역의 값이다.
그래서 이미지가 1개가 전체가 보이고, 다른 이미지 1개는 일부만 보이는 화면을 만들 수 있는 것이다.
- 또한,
padEnds값을 True로 바꾸게 되면,viewportFraction값이 1일 때 이미지가 위치하는 곳으로 이동한다. 그 곳부터 콘텐츠를 시작한다는 뜻이다.
📌 상태 관리
✔ 변화되는 데이터나 정보를 의미하며, 이는 상태를 효율적으로 관리하여 UI와 동기화하는 과정을 말한다.
📌 UI 동기화
✔ 상태는 사용자의 상호작용으로 변화하는데, 변화되는 상태를 시각적으로도 표시가 즉각적으로 이루어져야 사용자가 이를 인지할 수 있다. 이러한 과정을 UI에 데이터를 주입하는 동기화 과정을 한다고 표현한다.
📌 Flutter의 상태 관리
✔ StatefulWidget의 setState 방식
✔ 서드 파트 라이브러리를 이용하여 상태를 관리하는 방식
void _incrementCounter() { _counter++; print(_counter); } // 예제- 이와 같은 코드의 경우 데이터 동기화가 이루어지지 않기 때문에 값이 변화하지 않는다.
📌 build 함수에 위젯을 배치시키면 화면에 반영되는데, 변경된 상태를 UI에 동기화 하는 과정을 거쳐야 화면에 반영되기 때문에setState함수를 호출해준다.
void _incrementCounter() { setState(() { _counter++; }); print(_counter); } // 예제 1void _incrementCounter() { _counter++; print(_counter); setState(() {}); } // 예제 2
📌 여기서 첫번째 코드는 setState로 감쌌고, 두번째 코드는 그냥 호출만 하였는데 이는 큰차이가 없다.
단순히setState는build함수를 호출하기 때문에, 그 안에 상태값을 넣을 필요는 없다.
❌ StatefulWidget의 목적은 컨포넌트별 상태 관리로, 전체적인 상태 관리에는 목적에 맞지 않는다.
📌 상태 공유의 어려움
✔ setState는 개별 StatefulWidget 내부에서만 상태를 관리한다.
여러 위젯 간에 상태를 공유해야 할 때, 상태를 전달하는 것이 복잡해지고 비효율적일 수 있다.
📌 규모가 커질 때 복잡도 증가
✔ 애플리케이션이 커지고 상태를 관리해야 하는 위젯이 많아질수록, setState를 통한 상태 관리가 복잡해진다.
모든 상태 변경이 중앙에서 관리되지 않기 때문에 유지 보수가 어려워진다.
📌 성능 문제
✔ setState는 해당 StatefulWidget 전체를 다시 빌드하는데, 복잡한 UI에서 작은 상태 변경이 전체 위젯 트리를 다시 빌드하게 만든다.
📌 전역 상태 관리의 부재
✔ setState는 위젯 트리의 특정 부분에서만 동작하기 때문에, 전역 상태를 관리하거나 앱 전체에서 상태를 접근하기 어렵다.
복잡한 작업의 경우 이벤트 발생과 상태의 변화를 부모StatefulWidget을 통해 전달하는 방식이다.
이러한 방식에는 setState의 한계가 있기 때문에 중앙 집중적인 상태 관리 라이브러리를 사용한다.
📌 대표적인 중앙 집중적 상태 관리 라이브러리
📕Provider
📙Getx
📒Bloc/Cubit
📗Riverpod
- 중앙 집중 상태 관리 라이브러리를 이용하면 이를 통해 이벤트를 전달 받아 바로 상태를 전달할 수도 있다.
불필요한 전달 과정도 없고, 화면 갱신도 해당 영역만 다시 그리기 때문에 성능적으로도 안정화 된다.
Getx는 가장 인기 있는 라이브러리이며, 사용이 쉽고, 다른 라이브러리에 비해 진입 장벽이 낮다.
🔍 단, Provider, Getx, Bloc/Cubit, Riverpod 도 공부가 필요하다.
📌 설치
✔ 왼쪽 빈공간 우클릭 > open in integrated Terminal 클릭 > flutter pub add get 입력
혹은
✔ pubspec.yaml 파일의cupertino_icons: ^1.0.8하단에get: ^4.7.2입력
💡 중앙 집중 상태는 말이 길기 때문에 컨트롤러 라고 정의한다.
DefaultItem 내부 클래스 수정import 'package:get/get.dart'; // class ProductController extends GetxController{} // 예제
extends를 통해GetxController를 상속 받고 있는 컨트롤러를 만들었다.
(이 말이 좀 어려운데class Cat extends Animal,class Apple extends Fruits와 같이 오른쪽이 대분류 느낌이라고 한다.)
Widget build(BuildContext context) { Get.put(ProductController()); return MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: StatefulWidgetSample(), ); } // 예제
- main.dart 파일에 컨트롤러를 등록한다.
이 때 오류가 나는데, Get과 ProductController를 클릭해 Import를 해주면 해결된다.
📌 DEBUG CONSOLE에 아래 두 문장이 나타났다면, 올바르게 생성되었고, 이를 사용 가능하다는 뜻이다.
✔ [GETX] Instance "ProductController" has been created
✔ [GETX] Instance "ProductController" has been initializedimport 'package:get/get.dart'; // class ProductController extends GetxController { Set<String> products = {}; Set<String> leftProducts = {}; Set<String> rightProducts = {}; // void addProduct(String product) { products.add(product); update(); } } // 예제
- product_controller.dart에 상태 변수를 작성하고, 상품 등록 이벤트를 만든다.
floatingActionButton: Builder( builder: (context) => FloatingActionButton( onPressed: () { Get.find<ProductController>() .addProduct(StringUtils.getRandomString(2)); }, child: const Icon(Icons.add), ), ), // 예제
- statful_widget_sample.dart에서 상품 등록 이벤트를 호출한다.
main.dart에서Get.put을 사용해 넣었고, 여기서Get.find로 가져왔다.
GetBuilder<ProductController>( builder: (controller) { return Column( children: List.generate( controller.products.length, (index) => DefaultItem( item: controller.products.toList()[index], pushZone: (bool isLeftZone, String item) { if (isLeftZone) { leftProducts.add(item); } else { rightProducts.add(item); } products.remove(item); update(); }, ), ), ); }, ),
- 동기화를 위해
Getbuilder로 감쌌는데, GetBuilder는 Generic 타입을 원하는 참조 controller의 클래스를 등록해주면 해당 컨트롤러의 상태가 변경된다.
update함수가 호출이 되면 builder가 호출되어 변경된 상태의 값에 따라 화면을 변화시켜준다.
❌ 해당 부분은 파일과 코드가 복잡하기 때문에 따로 정리하지는 않을 것이다.
class DefaultItem extends StatelessWidget {
final String item;
const DefaultItem({Key? key, required this.item}) : super(key: key);
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(10),
child: Row(children: [
Expanded(
child: ElevatedButton(
onPressed: () {
Get.find<ProductController>().pushZone(true, item);
},
child: const Text('left'),
),
),
const SizedBox(width: 20),
Expanded(
child: Text(
item,
textAlign: TextAlign.center,
)),
const SizedBox(width: 20),
Expanded(
child: ElevatedButton(
onPressed: () {
Get.find<ProductController>().pushZone(false, item);
},
child: const Text('right'),
),
)
]),
);
}
}
ProductController 이벤트 등록
void pushZone(bool isLeft, String product) {
if (isLeft) {
leftProducts.add(product);
} else {
rightProducts.add(product);
}
products.remove(product);
update();
}
DefaultWidgetTree 내부 클래스 수정
class DefaultWidgetTree extends StatelessWidget {
const DefaultWidgetTree({
Key? key,
}) : super(key: key);
Widget build(BuildContext context) {
print('DefaultWidgetTree build!');
return Container(
padding: const EdgeInsets.all(10),
child: Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.all(8),
height: 200,
decoration: BoxDecoration(border: Border.all(color: Colors.grey)),
child: SingleChildScrollView(
child: GetBuilder<ProductController>(
builder: (controller) {
return Wrap(
children: List.generate(
controller.leftProducts.length,
(index) => Container(
margin: const EdgeInsets.all(3),
child: ItemTag(
tag: controller.leftProducts.toList()[index],
removeItemTag: (String item) {},
)),
),
);
},
),
),
)),
const SizedBox(width: 8),
Expanded(
child: Container(
padding: const EdgeInsets.all(8),
height: 200,
decoration: BoxDecoration(border: Border.all(color: Colors.grey)),
child: SingleChildScrollView(
child: GetBuilder<ProductController>(
builder: (controller) {
return Wrap(
children: List.generate(
controller.rightProducts.length,
(index) => Container(
margin: const EdgeInsets.all(3),
child: ItemTag(
tag: controller.rightProducts.toList()[index],
removeItemTag: (String item) {},
)),
),
);
},
),
),
),
),
],
),
);
}
}
스레드 앱 화면 구성이 알람 앱보다 조금 복잡하고 어려웠던 것 같다.
하지만 이에 대한 새로운 지식이나, 팁같은 걸 알 수 있었다.
그리고 상태 관리에 대한 이론과 예제 실습을 해보았다.
혼자 코틀린 책을 봤을 때랑은 다른 느낌인 것 같다.
조금 더 이해되고, 흐름을 파악할 수 있었다.
이렇게 setState에 대해 이해하나 싶었는데 서드 파티 라이브러리라는 새로운 것이 나온게 당황스러웠다.
강의에서는 Getx를 배우지만, 그중에 아는 개발자분은 Provider를 쓴다고 한다.
이 강의를 마친다면 Provider에 대한 공부도 필요할 것 같다.
이해는 어찌저찌했는데 Getx 상태 관리를 위해 예제 파일을 수정하다보니 좀 어렵게 느껴졌다.
예제 파일이 여러개를 수행하니 어려운건지, 아직 익숙하지 않아서 연결하고, 가져오는게 어려웠던건지는 잘 모르겠다.