#목차 (Github commit 확인)
BottomNavigationBar, nested navigation, list 이용한 history back button, showDialog, GridView 사용자 객체,
- Image 객체
- BottomNavigationBar 객체
- Getx binding 새로운 방법
- IndexedStack 객체
- Navigator 를 만들지 않고
List<int> = [0];
를 사용해서 navigation 을 하고 있네.. - SingleChildScrollView 객체 - List & Row 사용
- CachedNetworkImage 객체
- ClipRRect 객체 : 이름을 잘 생각해 봐라. 컨테이너 객체 가장자리 둥글게 만들어준다.
- ExpandableText 객체
- GestureDetector 객체
- Container 객체 가로전체 채우기, Container width & height
- Container Circle 모양으로 만들기
- AppBar 객체 사이즈 알아내기
- TabBar 객체 알아야 할 내용 (Padding 으로 감싸서 클릭 면적 넓히기도 숙지)
- 중첩 라우팅 - nested navigating
- BottomNavigationBar 객체
- enum 객체
- PageView 객체도 사용해 보도록 하자. (애니메이션과 손으로 넘기기도 되는것 같다.)
- SingleChildScrollView 객체 기억나지?
- Wrap 객체 - 너무 많으면 다음줄로 넘긴다. Warning 피할 수 있슴
- WillPopScope 객체
- Material 객체 - 스타일을 입히기 위해서
- Elevated Button 객체
- showDialog 객체 (callback 함수 호출에 대한 착각)
- CachedNetworkImage 객체
- List 를 사용해서 children 에 배열을 계속 나열하고 싶을 때
- List 변수 초기화 하는 방법 2가지
- Stack 객체 (Positioned 객체 사용법)
- ExpandableText 객체 (... 으로 늘렸다 줄였다 하는 위젯)
- SafeArea 객체
- 화면에 인스타그램처럼 이미지를 뿌려주는 방법 (Stateful, List, )
- 값을 참조하는 방법 2가지 추가
- GridView 객체
- PhotoManager 객체 (Device 의 사진을 불러와 보여주는 기능)
- callback function 사용법
디바이스 pixel ratio 에 따라 값이 조절되도록 하고 싶을 때
return Image.asset(_icon, width: _width! / Get.mediaQuery.devicePixelRatio,); // 디바이스 전체 크기 비율로 줄여주는구나.
이미지는 항상 SVG 파일로 만들는게 좋다는 걸 기억하자.
일단 static 을 포함한 객체를 이용해서 경로를 가진 IconPath 를 넘겨주면 좋고.
type : BottomNavigationBarType.fixed, // 아이콘이 뜨는걸 방지
showSelectedLabels : false, // label 을 보이지 않도록
showUnselectedLabels : false, // label 을 보이지 않도록
elevation : 0,
GetxMaterialApp() 쪽에
initbinding = initBining()
해주고
class InitBinding extends Bindings {
@override
void dependencies() {
Get.put(BottomNavController(), permanant: true); // permanant 로 인해서 계속 살아있게 된다.
// 그말은 여기에 계속 추가해서 넣어줄 수 있다는 건가??? 한번보자.
}
}
알지? 또다른방법이긴하지만 static get 으로 만들어줘서 Get.find 를 매번안해도 되도록 할 수도 있다.
내가 배웠던 Stack 과 Offset 위젯을 합쳐 놓은거네.. ㅎㅎㅎ
return SingleChildScrollView(
scrollDirection : Axis.horizontal, child: Row(
children: List.generate(100, (index) => Container(width: 60, height: 60,
decoration: BoxDecoration(shape: BoxShape.circle, color: Color.grey))))
);
Widget _myBodyStoryBoardList() {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children:
// [question] The element type 'List<Container>' can't be assigned to the list type 'Widget'.
// [answer] Row 의 children 이 List [] 인데 지금 List 를 리턴하니깐 [] 를 없애주어야지..
List.generate(
100,
(index) => Container(
decoration: const BoxDecoration(
shape: BoxShape.circle, color: Colors.grey),
)),
),
);
}
child : CachedNetworkImage(imageUrl: thumbPath, fit: Boxfit.cover),
컨테이너 객체 가장자리 둥글게 만들어준다.
ClipRRect(borderRadius: BorderRadius.circular(65), )
만약 Container() 안에다가 바로 하고 싶다면
List.generate(100, (index) => Container(decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.grey),))
ExpandableText(prefixText: '개발하는남자', prefixStyle: TextStyle(fontWeight: FontWeight.bold),
onPrefixTap: () => print(" 개발하는 남자 링크로 가즈아!") ,
'콘텐츠1입니다.콘텐츠1입니다.콘텐츠1입니다.', expandText: '더보기', collapseText: '접기',
maxLines: 3, expandOnTextTap: true, collapseOnTextTap: true,
linkColor: Colors.grey,),
return GestureDetector(onTap: (){}, child: Text('댓글 199개 모두보기', style: TextStyle(),), )
Container(width: Size:infinite.width),
Container
width=200, height=200
미리 입력하는 습관을 들이도록 하자.
Container(width: 25, height: 25,
decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.purple,
border: Border.all(color: Colors.white, width: 2)),
child: Icon(Icons.add, color: Colors.white,),
),
그리고 한가지 더 기억할게 자식을 Expanded() 객체로 감싸면 남은 여백을 다 차지한다는걸 알고있제?
Scaffold( bottom: PreferredSize(preferredSize: Size.fromHeight(AppBar().preferredSize.height))), // 이런게 참 중요한 건데..
TabBar(), TabController(), TabBarView() // 이건 이벤트를 받기에 인터페이스 상속받고, TabBarView() 를 이용해서 내부만든다.
class _SearchFocusState extends State<SearchFocus> with TickerProviderStateMixin {
TabBarView() 안에 그냥 5개의 자식들을 넣어주면 알아서 테그를 할 때 알아서 들어간다.
Navigator 로 감싸진 객체 즉 nested Navigator 로 들어가서는 그 안에서 네비게이션이 이루어진다. IndexedStack 과 TabBarView 는 여전히 1개의 페이지이므로 Navigator 가 필요가 없다. 따라서 nested Navigator 에서 나오려면 WillPopScope 을 구현해 주어야 한다. 그리고 해당 중첩 Navigator 의 Navigation 을 조절하려면 Global Key 를 이용해 주어야 한다. 그래서 Global Key 를 루트에 선언해 주는거다.
// pop 할게 있으면 하는데 그래서 true 가 되고 그러면 앱이 종료되는데 종료되는걸 막기 위해서 !await 를 해주는 거다.
return !await bottomNavigationBarController.searchFocusNavigationKey.currentState!.maybePop();
Rx type 으로 넣을 수 없다는 문제
enum PageName {HOME, SEARCH, UPLOAD, ACTIVITY, MYPAGE}
var page = PageName.values[value];
Wrap(
direction: Axis.vertical,
children: [
MyWidget(),
MyWidget(),
MyWidget(),
MyWidget(),
MyWidget(),
],
),
false 는 계속 유지, true 는 종료 강제 종료하는 방법
Future<bool> willPopAction() async {
if (bottomHistory.length == 1) {
Get.defaultDialog(title: 'close', content: Text('Do you want to close?'), textConfirm: "Close", textCancel: "Cancel",
// 여기 잘봐라. exit(0) 아주 중요하다. 그리고 Get.back()
onConfirm: ()=> exit(0), onCancel: ()=> Get.back());
return true;
} else {
print('goto before page!'); // 현재는 아무것도 움직이지 않잖아????
bottomHistory.removeLast(); // 마지막걸 지우고..
print(bottomHistory);
var index = bottomHistory.last;
changeBottomNav(index, isTapped: false); // 여기 보이나? 탭으로 바꿔주고 있다는 걸... 결국 이게 맞네.. 내가 한게 맞았네..
return false;
}
}
nested navigation 이 적용된 개선된 버전
Future<bool> willPopActionByShowDialog() async {
if (bottomHistory.length == 1) {
// [question] The argument type 'BuildContext?' can't be assigned to the parameter type 'BuildContext'.
// [answer] Get.context!
showDialog(context: Get.context!, builder: (context) {
return MessagePopup( // 여기 잘보자. cancel Callback : Get.back 만 했다. 왜냐하면 함수가 넘어가야 하니깐.
okCallback: () => exit(0), title: '시스템', message: '종료하시겠습니까?', cancelCallback : Get.back,);}
);
return true;
} else {
// 여기부분부터 nested Navigation 부분의 willPop 부분이다. 꼭 필요한 부분이지..
var page = PageName.values[bottomHistory.last];
if (page == PageName.SEARCH) { // search 페이지 안이라면 그안에서 팝할게 있을지도 모르니깐..
// https://stackoverflow.com/questions/49672706/flutter-navigation-pop-to-index-1
var value = await Get.nestedKey(1)!.currentState!.maybePop(); // 여기 안에서 팝할게있으면 여기서 하고
if (value) return false;
/*
value 가 true 이면 이조건문을 빠져나가는데 그말을 팝할게 있었다는 거잖아. 말그대로 그리드뷰로 간다는거고
value 가 false 이면 이제 팝할게 없다는 거고. 그래서 이조건문을 빠져나가지 못하고 다음문장을 계속실행한다.
*/
}
bottomHistory.removeLast();
var index = bottomHistory.last;
changeBottomNav(index, isTapped: false);
return false;
}
}
// TODO
showDialog(context: Get.context!, builder: (context) {
return MessagePopup( // 여기 잘보자. cancel Callback : Get.back 만 했다. 왜냐하면 함수가 넘어가야 하니깐.
okCallback: (){exit(0);}, title: '시스템', message: '종료하시겠습니까?', cancelCallback: Get.back,
);
////
final Function()? _cancelCallback;
final Function()? _okCallback;
////
ElevatedButton(onPressed: () {_okCallback;}, child: const Text("확인"),),
ElevatedButton(onPressed: () {_cancelCallback;}, style: ElevatedButton.styleFrom(primary: Colors.grey), child: const Text("취소"),),
함수를 호출해야한다는 의미는 함수의 이름만 1:1로 대응이 되어야하는데 너는 실수로 () => 함수를 실행시켰다. 완전 삽질을 한거지..
프로젝트를 새로 시작해라. 그래야지 제대로 불러들인다. 왜냐고? PUb sub 새로 들어왔잖아. Indicator 도 사용할 수 있고.. 다재다능하네.
Widget type1Widget() {
// Gradation 이 들어가야하는 위젯이다.
return Container(
margin: const EdgeInsets.symmetric(horizontal: 5),
padding: EdgeInsets.all(2),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomLeft,
colors: [Colors.purple, Colors.orange]),
shape: BoxShape.circle),
child: ClipRRect(
borderRadius: BorderRadius.circular(65), // 이걸 감싸주어야 동그랗게 잘라줄 수 있게 된다.
child: SizedBox(
width: 65,
height: 65,
child: CachedNetworkImage(
imageUrl: thumbPath,
fit: BoxFit.cover,
),
),
),
);
}
}
Widget type1WidgetTempBackup() {
// Gradation 이 들어가야하는 위젯이다.
return Container(
width: 160,
height: 160,
margin: const EdgeInsets.symmetric(horizontal: 5),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomLeft,
colors: [Colors.purple, Colors.orange]),
shape: BoxShape.circle),
child: CachedNetworkImage(
imageUrl: 'https://i.guim.co.uk/img/media/fe1e34da640c5c56ed16f76ce6f994fa9343d09d/0_174_3408_2046/master/3408.jpg?width=1200&height=900&quality=85&auto=format&fit=crop&s=0d3f33fb6aa6e0154b7713a00454c83d',
placeholder: (context, url) => const CircleAvatar(
backgroundColor: Colors.amber,
radius: 150,
),
imageBuilder: (context, image) => CircleAvatar(
backgroundImage: image,
radius: 150,
),
),
);
}
child: Row(
children: [
_myStory(),
// [question] The element type 'List<Container>' can't be assigned to the list type 'Widget'.
// [answer] Row 의 children 이 List [] 인데 지금 List 를 리턴하니깐 [] 를 없애주어야지..
...List.generate( // 배열을 계속 나열하겠다는 뜻임.
100,
(index) => AvatarWidget(thumbPath: 'https://media-cldnry.s-nbcnews.com/image/upload/t_fit-760w,f_auto,q_auto:best/rockcms/2022-04/220428-dog-breeds-mb-1019-95b354.jpg', type: AvatarType.TYPE1)),
]
),
return Column(
children:
List.generate(50, (index) => PostWidget()).toList(),);
}
List<AssetPathEntity> albums = [];
// var albums = <AssetPathEntity>[]; // 이거 똑같은 뜻인거 맞지? 확실하지?
Widget _myStory() {
return Stack(
children: [
AvatarWidget(thumbPath: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS5W2qWDdF7ztjYI9lL_odxd3LY3uv8BOI9Ug&usqp=CAU', type: AvatarType.TYPE2, size: 70,),
Positioned(
right: 1,bottom: 0, child: Container(width: 25, height: 25,
decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.purple,
border: Border.all(color: Colors.white, width: 2)),
child: Icon(Icons.add, color: Colors.white,),
),
)
],
);
}
상태바를 침범하지 않고 안전영역에 화면을 그리도록 한다. 왜 이런문제가 생기냐면 body 에다가 앱바 모양을 만들게 되면 스크롤해서 숨길 수 있게 되기 때문이다. 그럼 기존의 앱바를 이용하면 고정이 되어있다는 건가?
기본적으로 Stateful 객체를 사용하면 initState 함수를 사용할 수 있고 컨테이너가 참고할 List 객체를 미리 정해놓을 수 있다. 뭐 꼭 그렇게 하지 않아도 되긴한다. 항상 리스트나 맵을 이용해서 화면에 뿌려주는 방법을 생각해 보도록 하자. Random 사용하는것도 생각해보고 List.generate 도 항상 생각하고
/// Stateful 안에서
List<List<int>> groupBox = [[], [], []]; //3개의 컬럼에 있는 리스트별로 사이즈가 들어간다. (1, 2) 중에 하나로. // QnADartGrammar 2차배열
List<int> groupIndex = [0,0,0]; // 전체 사이즈 합계를 체크하기 위한 리스트 // 라이브러리를 추가한다.quiver, 각 컬럼별 사이즈가 합계된다.
@override
void initState() {
// 이게 왜 실행이 안되지???? // 최초 실행시 한번만 돌아간다. 이름 보면 모르겠나??? // 그러니깐 다시 빌드해야 보이지.!
super.initState();
for (var i = 0; i < 100; i++) {
var gi = groupIndex.indexOf(
min<int>(groupIndex)!); // 여기 봐라. 0,1,2 중에서 제일 작은거 찾아서 인덱스(0,1,2) 를 넘겨줌.
//var gi = i % 3;
var size = 1;
if (gi != 1) { // 그러니깐 중간값 1이 아니면
print('gi 값은 $gi 입니다.');
size = Random().nextInt(100) % 2 == 0 ? 1 : 2;
}
groupBox[gi].add(
size); // 각 컬럼 리스트에 1씩 들어간다. 결국 groupBox[0] 에는 34개, groupBox[1]과 groupBox[2] 에는 33개
groupIndex[gi] += size; // 각 리스트값에 사이즈를 더해주면 위에서 제일 작은 값을 찾을 거고
}
print('groupBox[0] 값은 $groupBox[0]');
print('groupBox[1] 값은 $groupBox[1]');
print('groupBox[2] 값은 $groupBox[2]');
}
Widget _myBody() {
// 컬럼이 3개가 있고.. 이미지를 하나씩 축적하면 어떨까?
print("groupBox.length 의 값은 ${groupBox.length} 입니다.");
return SingleChildScrollView(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children:
List.generate(groupBox.length, (index) => Expanded(
child: Column(children: // 총 3개의 확장된 컬럼이 들어가는거고..
List.generate(groupBox[index].length, (jndex) => Container(
height: Get.width * 0.33 * groupBox[index][jndex], // groupBox[index][jndex] 의 값은 1 이나 2 가 들어간다.
decoration: BoxDecoration(border: Border.all(color: Colors.white),
color: Colors.primaries[Random().nextInt(Colors.primaries.length)]),
child: CachedNetworkImage(fit: BoxFit.cover,imageUrl: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS5W2qWDdF7ztjYI9lL_odxd3LY3uv8BOI9Ug&usqp=CAU'),
)).toList(),
),
),
).toList(),
),
);
}
아주 중요한 내용이다.
PreferredSizeWidget _myAppBarBottom() {
return PreferredSize(
child: Container(
width: Size.infinite.width, // 사이즈 객체의 static 변수 infinite 를 이용해서 Size 객체를 무조건 하나 생성하고 그 width 를 받는다.
child: Text("개남"), color: Color(0xff9ca5f2),),
preferredSize: Size.fromHeight(AppBar().preferredSize.height), // 이렇게 객체를 임시로 만들어 값을 가져올 수도 있다.
);
}
여러가지 생각해야 할게 많구나.
Widget _imageSelectList() {
/*
[question] ======== Exception caught by rendering library =====================================================
The following assertion was thrown during performLayout():
RenderBox was not laid out: RenderViewport#565b6 NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE
'package:flutter/src/rendering/box.dart':
Failed assertion: line 1978 pos 12: 'hasSize'
[Answer] Scroll 이 겹치는 문제이다.
*/
return GridView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4, mainAxisSpacing: 1, crossAxisSpacing: 1,
childAspectRatio: 1, // 0.5 로 하면 세로로 직사각형, 1.5 하면 가로로 직사각형
),itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return Container(color: Colors.red,);
},);
}
Device 의 사진을 불러와 보여주는 기능을 하는 위젯
Widget _photoWidget(
AssetEntity assetEntity, int size, Widget Function(Uint8List) builder) {
// Future 가넘어오는 값을 실시간으로 데이터를 바꿔서 위젯을 생성할 때 사용한다. 알고 있지? 단발성으로 사용할 때 쓰는거
return FutureBuilder(
future: assetEntity.thumbnailDataWithSize(ThumbnailSize(size, size)),
builder: (BuildContext context, AsyncSnapshot<Uint8List?> snapshot) {
if (snapshot.hasData) {
return builder(snapshot.data!);
} else {
return Container();
}
});
}
}
itemBuilder: (BuildContext context, int index) {
return _photoWidget(
imageList[index], 200, (data) { return Opacity(
opacity: imageList[index] == selectedImage ? 0.6 : 1,
child: Image.memory(data, fit: BoxFit.cover,));},
);
},
Instagram Clone Coding
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
For help getting started with Flutter development, view the online documentation, which offers tutorials, samples, guidance on mobile development, and a full API reference.