[Flutter] GridView, CustomScrollView 프로필 페이지 만들기

Woojin·2023년 9월 16일

Flutter

목록 보기
24/27

GridView

  • 격자 레이아웃

  • grid가 몇개 생성될지 미리 알고 있으면 GridView.count 쓰고
  • grid가 몇개 생성될지 아직 모르겠으면 GridView.builder
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2 ),
  itemCount: list자료.length, 
  itemBuilder: (c, i) {
    return Container(color : Colors.grey);
  }
),
  • itemCount : 개수
  • gridDelegate : 가로로 보여질 개수

예제

body: GridView.builder(
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2), // 가로 개수
            itemBuilder: (c, i) { return Container(color: Colors.grey); },
            itemCount: 3,
        )


스크롤바 만들어주는 CustomScrollView()

CustomScrollView(
  slivers: [ 위젯1, 위젯2, 위젯3 ]
)

스크롤 영역 안에 ListView 만들고 싶으면 SliverList()

SliverList(
  delegate: SliverChildListDelegate(
    [ Text('리스트'), Text('리스트'), Text('리스트') ]
  )
),

CustomScrollView안에 GridView 만들고 싶으면 SliverGrid()

SliverGrid(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
  delegate: SliverChildListDelegate(
    [ Container(Colors.blue), Container(Colors.green), Container(Colors.yellow) ],
  ),
),

CustomScrollView 안에 박스 넣고 싶으면 SliverToBoxAdapter()

  • child 안에 원하는 위젯 넣으면 됨
SliverToBoxAdapter(
              child: ProfileHeader(),
            ),

CustomScrollView 안에 예쁜 헤더 만들고 싶으면 SliverAppBar()

  • 맨 위 헤더 영역을 만들어줌
  • 스크롤 시작되면 뒷 배경 사라짐
  • pinned:true (상단고정옵션)
  • floating:true (위로 스크롤시 등장여부)
  • expandedHeight: 300 (높이 설정)
  • flexibleSpace: FlexibleSpaceBar() (앱바에 넣을 내용)

⇒ 참고

FlexibleSpaceBar class - material library - Dart API

인스타그램 프로필 페이지 같은 레이아웃

  • 위엔 Container, 아래엔 GridView
  • 이 전체를 묶어서 스크롤바를 만들고 싶으면 CustomScrollView 안에 Container + GridView 넣으면 끝
body : CustomScrollView(
        slivers: [
          SliverToBoxAdapter(child: ProfileHeader()),
          SliverGrid(
              delegate: SliverChildBuilderDelegate(
                      (c,i) => Container(color : Colors.grey),
                childCount: 3,
              ),
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2 ))
        ],
     )

전체 코드

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import './style.dart' as style;
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:flutter/rendering.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import 'package:provider/provider.dart';

void main() {
  runApp(
      MultiProvider(
        providers: [
          ChangeNotifierProvider( create: (c) => Store1()),
          ChangeNotifierProvider( create: (c) => Store2()),
        ],

        child: MaterialApp(
            theme: style.theme,
            home: MyApp()
        ),
      ));
}

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

  
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  var tab = 0;  // 1. 현재 상태 저장
  var list = [1, 2, 3];
  var map = {'name':'john', 'age':20};
  var data = [];
  var userImage; // 유저가 삽입한 이미지 저장 공간
  var userContent; // 유저가 입력한 글 저장 공간

  saveData() async {
    var storage = await SharedPreferences.getInstance();

    var map = {'age' : 20};
    storage.setString('map', jsonEncode(map));
    var result = storage.getString('map') ?? '업는데요';
    print(jsonDecode(result)['age']);
  }

  addMyData() {
    var myData = {
      'id': data.length,
      'image': userImage,
      'likes': 5,
      'date': 'July 25',
      'content': userContent,
      'liked': false,
      'user': 'John Kim'
    };
    setState(() {
      data.insert(0, myData);
    });
  }
  setUserContent(a) {
    setState(() {
      userContent = a;
    });
  }

  addData(a) {
    setState(() {
      data.add(a);
    });
  }

  getData() async {
    var result = await http.get(Uri.parse('https://codingapple1.github.io/app/data.json'));

    var result2 = jsonDecode(result.body);
    setState(() {
      data = result2;
    });

  }

  
  void initState() {
    super.initState();
    saveData();
    getData();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: Text('Instagram'),
          actions: [
            IconButton(
              icon: Icon(Icons.add_box_outlined),
              onPressed: () async {
                var picker = ImagePicker();
                var image = await picker.pickImage(source: ImageSource.gallery);
                if(image != null)  {
                  setState(() {
                    userImage = File(image.path);
                  });
                }

                Navigator.push(context,
                    MaterialPageRoute(builder: (c) => Upload( userImage : userImage, setUserContent : setUserContent, addMyData : addMyData) )
                );
              },
              iconSize: 30,
            )
          ]),
      body: [Home(data : data, addData : addData), Text('샵페이지')][tab],  // 2. state에 따라 tab 보이는 상태 변경
      bottomNavigationBar: BottomNavigationBar(
        showSelectedLabels: false,
        showUnselectedLabels: false,
        onTap: (i){
          setState(() {
            tab = i;
          });
        },
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home_outlined), label: '홈'),
          BottomNavigationBarItem(icon: Icon(Icons.shopping_bag_outlined), label: '샵')
        ],
      ),
    );
  }
}

class Home extends StatefulWidget {
  const Home({Key? key, this.data, this.addData}) : super(key: key);
  final data, addData;

  
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {

  var scroll = ScrollController();

  getMore() async {
    var result = await http.get(Uri.parse('https://codingapple1.github.io/app/more1.json'));
    var result2 = jsonDecode(result.body);

    widget.addData(result2);
  }

  
  void initState() {
    super.initState();
    scroll.addListener(() {   // 스크롤바 높이 측정 코드
      if(scroll.position.pixels == scroll.position.maxScrollExtent) {
        getMore();
      }
    });
  }

  
  Widget build(BuildContext context) {

    if (widget.data.isNotEmpty) {
      return ListView.builder(
          itemCount: widget.data.length,
          controller: scroll,
          itemBuilder: (c, i) {
            return Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                widget.data[i]['image'].runtimeType == String ? Image.network(widget.data[i]['image']) : Image.file(widget.data[i]['image']),
                Container(
                  constraints: BoxConstraints(maxWidth: 600),
                  padding: EdgeInsets.all(20),
                  width: double.infinity,
                  child: Column(
                    children: [
                      GestureDetector(
                        child: Text(widget.data[i]['user']),
                        onTap: () {
                          Navigator.push(context,
                              PageRouteBuilder(pageBuilder: (c, a1, a2) => Profile(),
                                  transitionsBuilder: (c, a1, a2, child) =>
                                      FadeTransition(opacity: a1, child: child)
                              )
                          );
                        },
                      ),
                      Text('좋아요 ${widget.data[i]['likes'].toString()}'),
                      Text(widget.data[i]['date']),
                      Text(widget.data[i]['content'])
                    ],
                  ),
                )
              ],
            );
          }
      );
    } else {
      return Text('로딩중임');
    }
  }
}

class Upload extends StatelessWidget {
  const Upload({Key? key, this.userImage, this.setUserContent, this.addMyData}) : super(key: key);
  final userImage, setUserContent, addMyData;

  

  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(actions: [
          IconButton(onPressed: () {
            addMyData();
          }, icon: Icon(Icons.send))
        ],),
        body: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            IconButton(
                onPressed: (){
                  Navigator.pop(context);
                },
                icon: Icon(Icons.close)
            ),
            Image.file(userImage),
            Text('이미지업로드화면'),
            TextField(onChanged: (text) {  // text는 유저가 입력한 글
              setUserContent(text);  // TextField()에 입력값 변할 때마다 onChanged 안의 함수가 실행됨
            },),
          ],
        )
    );
  }
}

class Store2 extends ChangeNotifier {
  var name = 'john kim';

  changeName() {
    name = 'john park';
    notifyListeners();  // 재렌더링
  }
}

class Store1 extends ChangeNotifier {  // state 보관함
  var follower = 0;
  var friend = false;
  var profileImage = [];

  getData() async {
    var result = await http.get(Uri.parse('https://codingapple1.github.io/app/profile.json'));
    var result2 = jsonDecode(result.body);
    profileImage = result2;
    notifyListeners();
  }

  addFollower() {
    if(friend == false) {
      friend = true;
      follower++;
    } else {
      friend = false;
      follower--;
    }
    notifyListeners();
  }
}

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

  
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text(context.watch<Store2>().name),),
        body: CustomScrollView(
          slivers: [
            SliverToBoxAdapter(
              child: ProfileHeader(),
            ),
            SliverGrid(
                delegate: SliverChildBuilderDelegate(
                    (c, i) => Container(color: Colors.grey),
                    childCount: 13,
                ),
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2))
          ],
        )
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          CircleAvatar(radius: 30, backgroundColor: Colors.grey,),
          Text('팔로우 ${context.watch<Store1>().follower}명'),
          ElevatedButton(onPressed: () {
            context.read<Store1>().addFollower();
          }, child: Text('팔로우')),
          ElevatedButton(onPressed: () {
            context.read<Store1>().getData();
          }, child: Text('사진 가져오기')),
        ]
    );
  }
}

프로필 이미지 가져오기 (숙제)

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import './style.dart' as style;
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:flutter/rendering.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import 'package:provider/provider.dart';

void main() {
  runApp(
      MultiProvider(
        providers: [
          ChangeNotifierProvider( create: (c) => Store1()),
          ChangeNotifierProvider( create: (c) => Store2()),
        ],

        child: MaterialApp(
            theme: style.theme,
            home: MyApp()
        ),
      ));
}

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

  
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  var tab = 0;  // 1. 현재 상태 저장
  var list = [1, 2, 3];
  var map = {'name':'john', 'age':20};
  var data = [];
  var userImage; // 유저가 삽입한 이미지 저장 공간
  var userContent; // 유저가 입력한 글 저장 공간

  saveData() async {
    var storage = await SharedPreferences.getInstance();

    var map = {'age' : 20};
    storage.setString('map', jsonEncode(map));
    var result = storage.getString('map') ?? '업는데요';
    print(jsonDecode(result)['age']);
  }

  addMyData() {
    var myData = {
      'id': data.length,
      'image': userImage,
      'likes': 5,
      'date': 'July 25',
      'content': userContent,
      'liked': false,
      'user': 'John Kim'
    };
    setState(() {
      data.insert(0, myData);
    });
  }
  setUserContent(a) {
    setState(() {
      userContent = a;
    });
  }

  addData(a) {
    setState(() {
      data.add(a);
    });
  }

  getData() async {
    var result = await http.get(Uri.parse('https://codingapple1.github.io/app/data.json'));

    var result2 = jsonDecode(result.body);
    setState(() {
      data = result2;
    });

  }

  
  void initState() {
    super.initState();
    saveData();
    getData();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: Text('Instagram'),
          actions: [
            IconButton(
              icon: Icon(Icons.add_box_outlined),
              onPressed: () async {
                var picker = ImagePicker();
                var image = await picker.pickImage(source: ImageSource.gallery);
                if(image != null)  {
                  setState(() {
                    userImage = File(image.path);
                  });
                }

                Navigator.push(context,
                    MaterialPageRoute(builder: (c) => Upload( userImage : userImage, setUserContent : setUserContent, addMyData : addMyData) )
                );
              },
              iconSize: 30,
            )
          ]),
      body: [Home(data : data, addData : addData), Text('샵페이지')][tab],  // 2. state에 따라 tab 보이는 상태 변경
      bottomNavigationBar: BottomNavigationBar(
        showSelectedLabels: false,
        showUnselectedLabels: false,
        onTap: (i){
          setState(() {
            tab = i;
          });
        },
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home_outlined), label: '홈'),
          BottomNavigationBarItem(icon: Icon(Icons.shopping_bag_outlined), label: '샵')
        ],
      ),
    );
  }
}

class Home extends StatefulWidget {
  const Home({Key? key, this.data, this.addData}) : super(key: key);
  final data, addData;

  
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {

  var scroll = ScrollController();

  getMore() async {
    var result = await http.get(Uri.parse('https://codingapple1.github.io/app/more1.json'));
    var result2 = jsonDecode(result.body);

    widget.addData(result2);
  }

  
  void initState() {
    super.initState();
    scroll.addListener(() {   // 스크롤바 높이 측정 코드
      if(scroll.position.pixels == scroll.position.maxScrollExtent) {
        getMore();
      }
    });
  }

  
  Widget build(BuildContext context) {

    if (widget.data.isNotEmpty) {
      return ListView.builder(
          itemCount: widget.data.length,
          controller: scroll,
          itemBuilder: (c, i) {
            return Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                widget.data[i]['image'].runtimeType == String ? Image.network(widget.data[i]['image']) : Image.file(widget.data[i]['image']),
                Container(
                  constraints: BoxConstraints(maxWidth: 600),
                  padding: EdgeInsets.all(20),
                  width: double.infinity,
                  child: Column(
                    children: [
                      GestureDetector(
                        child: Text(widget.data[i]['user']),
                        onTap: () {
                          Navigator.push(context,
                              PageRouteBuilder(pageBuilder: (c, a1, a2) => Profile(),
                                  transitionsBuilder: (c, a1, a2, child) =>
                                      FadeTransition(opacity: a1, child: child)
                              )
                          );
                        },
                      ),
                      Text('좋아요 ${widget.data[i]['likes'].toString()}'),
                      Text(widget.data[i]['date']),
                      Text(widget.data[i]['content'])
                    ],
                  ),
                )
              ],
            );
          }
      );
    } else {
      return Text('로딩중임');
    }
  }
}

class Upload extends StatelessWidget {
  const Upload({Key? key, this.userImage, this.setUserContent, this.addMyData}) : super(key: key);
  final userImage, setUserContent, addMyData;

  

  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(actions: [
          IconButton(onPressed: () {
            addMyData();
          }, icon: Icon(Icons.send))
        ],),
        body: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            IconButton(
                onPressed: (){
                  Navigator.pop(context);
                },
                icon: Icon(Icons.close)
            ),
            Image.file(userImage),
            Text('이미지업로드화면'),
            TextField(onChanged: (text) {  // text는 유저가 입력한 글
              setUserContent(text);  // TextField()에 입력값 변할 때마다 onChanged 안의 함수가 실행됨
            },),
          ],
        )
    );
  }
}

class Store2 extends ChangeNotifier {
  var name = 'john kim';

  changeName() {
    name = 'john park';
    notifyListeners();  // 재렌더링
  }
}

class Store1 extends ChangeNotifier {  // state 보관함
  var follower = 0;
  var friend = false;
  var profileImage = [];

  getData() async {
    var result = await http.get(Uri.parse('https://codingapple1.github.io/app/profile.json'));
    var result2 = jsonDecode(result.body);
    profileImage = result2;
    notifyListeners();
  }

  addFollower() {
    if(friend == false) {
      friend = true;
      follower++;
    } else {
      friend = false;
      follower--;
    }
    notifyListeners();
  }
}

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

  
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text(context.watch<Store2>().name),),
        body: CustomScrollView(
          slivers: [
            SliverToBoxAdapter(
              child: ProfileHeader(),
            ),
            SliverGrid(
                delegate: SliverChildBuilderDelegate(
                    (c, i) => Image.network(context.watch<Store1>().profileImage[i]),
                    childCount: context.watch<Store1>().profileImage.length,
                ),
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2))
          ],
        )
    );
  }
}

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

  
  Widget build(BuildContext context) {
    return Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          CircleAvatar(radius: 30, backgroundColor: Colors.grey,),
          Text('팔로우 ${context.watch<Store1>().follower}명'),
          ElevatedButton(onPressed: () {
            context.read<Store1>().addFollower();
          }, child: Text('팔로우')),
          ElevatedButton(onPressed: () {
            context.read<Store1>().getData();
          }, child: Text('사진 가져오기')),
        ]
    );
  }
}

0개의 댓글