[Android/Flutter 교육] 88일차

MSU·2024년 5월 13일

Android-Flutter

목록 보기
83/85
post-thumbnail

지니모션(Genymotion)

https://www.genymotion.com/
안드로이드 스튜디오에서 지니모션 플러그인을 설치하면 에뮬레이터를 지니모션에서 실행시킬 수 있음
안드로이드 스튜디오에서 실행하는 기본 에뮬레이터보다 더 빠름
macOS에서도 지니모션을 사용하여 안드로이드 에뮬레이터를 실행할 수 있음

macOS 플러터 개발 환경 설정

Xcode

macOS 에서는 플러터 개발 시 Xcode를 사용


1. xcode 설치

  • xcode 맥에서 맥, 아이폰, 아이패드용 애플리케이션을 개발하기 위한 개발도구
  • 아이폰 에뮬레이터 사용때문에 설치한다.
  • 맥 앱스토어에서 내려받아 설치한다.
  1. xcode 에뮬레이터 설치
  • xcode 설치가 완료되면 xcode를 실행한다.
  • 약관 화면이 나오면 약관에 동의한다.
  • 약관 동의 후 나오는 첫 화면에서 iOS를 선택하여 설치해준다.
  • 설치가 완료되면 xcode가 실행될때까지 기다린다.
  • iOS 시뮬레이터 파일이 다운로드 완료될까지 기다린다.
  • 완료가 되면 메뉴에서 window > Devices and Sumulators 를 선택한다.
  • 시뮬레이터가 추가되어 있는지 확인한다.
  1. 아이폰 시뮬레이터 실행
  • 상단 메뉴에서 Xcode > Open Developer Tool > Simulator 를 클릭하면 시뮬레이터가 실행된다.
  1. flutter 개발환경 구축
  • 맥용 sdk 를 내려받아 적당한 곳에 압축을 풀어준다.
  1. android studio 를 내려받아 flutter 개발환경을 구축한다.
  • windows 와 동일합니다.
  1. firebase 사이트에서 프로젝트를 생성한다.

  2. firebase tools 설치

  • 터미널에서 다음 명령어를 이용하여 firebase tools 를 설치한다.
    curl -sL https://firebase.tools | bash
  1. firebase에 로그인을 한다
    firebase login

  2. path 설정

  • flutter의 bin 폴더를 path로 잡아준다

  • 터미널에서 다음 명령어를 실행해 vi 편집기를 실해준다.
    sudo vi /etc/paths

  • i 를 눌러 편집 모드로 편한한다.

  • flutter의 bin 폴더까지의 경로를 입력해준다

  • esc 키를 눌러준다.

  • :wq 를 입력후 엔터를 눌러 저장한다.

  1. 터미널을 종료한다.

  2. 안드로이드 스튜디오의 터미널을 실행한다.

  3. firebase 사이트에 나와있는 첫 번째 명령어를 터미널에 입력하여 실행해준다.

dart pub global activate flutterfire_cli

  1. 12번 과정을 진행하면 path 설정하라고 나오는데 이를 수행해준다.
  • 설정해야할 경로를 복사해둔다.

  • 다음 명령어를 실행하여 vi 편집기를 실행한다
    vi ~/.zshrc

  • i 를 눌러 편집 모드로 변경한다.

  • 복사한 경로를 붙혀넣어준다.

  • esc를 눌러준다.

  • :wq 를 입력후 엔터를 눌러 저장하고 나간다.

  • 안드로이드 스튜디오의 터미널을 종료하고 터미널을 다시 실행해준다.

  1. 명령어를 다시 실행한다

dart pub global activate flutterfire_cli

15 사이트에 나와있는 두 번째 명령어를 터미널에서 실행한다

flutterfire configue --project=프로젝트명

  1. 명령어를 입력하여 설정하는 과정 중에 오류가 발생한다면 다음과 같이 설치한다.
    sudo gem install drb -v 2.0.6
    sudo gem install activesupport -v 6.1.7.7
    sudo gem install cocoapods && pods install

넷플릭스 클론 코딩 (플러터) - 5일차

검색 기능 구현

  1. 서치바에 이벤트 처리를 넣어준다.
   return SearchBar(
      // 좌측에 배치되는 아이콘
      leading: Icon(Icons.search),
      // 내부 여백
      padding: MaterialStatePropertyAll(EdgeInsets.fromLTRB(10, 0, 10, 0)),
      // 키보드의 submit 버튼을 눌렀을 때
      // value : 사용자가 입력한 내용이 들어온다.
      onSubmitted: (value) {
      
      },
   );
  1. TabPageIndexProvider에 검색어를 담을 변수를 선언해준다.
class TabPageIndexProvider extends ChangeNotifier{

   // 사용자가 입력한 검색어를 담을 변수
   String _searchKeyword = "";

   String get searchKeyword => _searchKeyword;

}
  1. 키워드를 변수에 담는 메서드를 만들어준다.
  • notifyListeners()를 호출하여 연결된 모든 리스너를 동작시킨다.
class TabPageIndexProvider extends ChangeNotifier{

   // 사용자가 입력한 검색어를 담을 변수
   String _searchKeyword = "";

   String get searchKeyword => _searchKeyword;

   void setKeyword(String keyword){
      _searchKeyword = keyword;
      // 모든 리스너를 동작시킨다.
      notifyListeners();
   }

}
  1. 프로바이더를 가져온다.
class _SearchScreenState extends State<SearchScreen> {

   
   Widget build(BuildContext context) {

      // Provider를 가져온다.
      var searchScreenProvider = Provider.of<TabPageIndexProvider>(context, listen: false);
  1. 엔터키를 누르면 Provider의 메서드를 호출해준다.
   return SearchBar(
      // 좌측에 배치되는 아이콘
      leading: Icon(Icons.search),
      // 내부 여백
      padding: MaterialStatePropertyAll(EdgeInsets.fromLTRB(10, 0, 10, 0)),
      // 키보드의 submit 버튼을 눌렀을 때
      // value : 사용자가 입력한 내용이 들어온다.
      onSubmitted: (value) {
         // Provider의 set 메서드를 호출해준다.
         searchScreenProvider.setKeyword(value);
      },
   );
  1. SearchListView에서 Provider에 리스너를 연결해준다.
class _SearchListViewState extends State<SearchListView> {
   
   Widget build(BuildContext context) {

      // Provider를 가져온다.
      var searchScreenProvider = Provider.of<TabPageIndexProvider>(context, listen: false);

      // 리스너를 연결해준다.
      searchScreenProvider.addListener(() {
         print("SearchListView : ${searchScreenProvider.searchKeyword}");
      });
  1. 검색 결과를 담을 리스트를 정의해준다.
class _SearchListViewState extends State<SearchListView> {
   // 검색 결과 데이터를 담을 리스트
   List<Map<String, dynamic>> searchResult = [];
   // 검색 결과 영화 포스터를 담을 리스트
   List<Image> posterData = [];
  1. 리스트뷰의 항목의 개수를 변경한다.
   return ListView.builder(
    itemCount: searchResult.length,
    itemBuilder: (context, index) => makeListItem(context),
   );
  1. makeListItem 메서드를 호출할 때 전달하는 값을 추가해준다.
   return ListView.builder(
    itemCount: searchResult.length,
    itemBuilder: (context, index) => makeListItem(context, searchResult, posterData, index),
   );
  1. makeListItem 메서드의 매개변수를 변경해준다.
// 리스트뷰의 항목 하나를 구성하는 함수
// 리스트뷰의 항목은 ListTitle을 사용해도 된다 대신 아이콘 사이즈 조절 불가
Widget makeListItem(BuildContext context, List<Map<String, dynamic>> searchResult, List<Image> posterData, int index){ 
  1. 전달받은 데이터로 항목을 구성하도록 코드를 수정해준다.
      child: Row(
         children: [
            posterData[index],
            const Padding(padding: EdgeInsets.only(right: 10)),
            Column(
               crossAxisAlignment: CrossAxisAlignment.start,
               children: [
                  Text(
                     searchResult[index]['movie_title'],
                     style: TextStyle(fontSize: 15)
                  ),
                  Text(
                     '출연진 : ${searchResult[index]['movie_actor']}',
                     style: TextStyle(fontSize: 12)
                  ),
                  Text(
                     '제작진 : ${searchResult[index]['movie_director']}',
                     style: TextStyle(fontSize: 12)
                  ),
               ],
            )
         ],
      ),
  1. 검색 결과를 가져오는 메서드를 만들어준다.
// 전체 영화 데이터에서 검색어에 해당하는 것만 모아 반환한다.
Future<List<Map<String, dynamic>>> getSearchResult(String keyword) async {
   // 검색 결과를 담을 리스트
   List<Map<String, dynamic>> tempSearchResult = [];
   // 모든 영화 정보를 가져온다.
   List<Map<String, dynamic>> movieData = await getMovieData();

   // 가져온 전체 영화의 수 만큼 반복한다.
   for(var map in movieData){
      // 현재 영화의 제목이 검색어를 포함하고 있다면 결과 리스트에 담아준다.
      if(map['movie_title'].toString().contains(keyword)){
         // 결과 리스트에 담아준다.
         tempSearchResult.add(map);
      }
   }

   return tempSearchResult;
}
  1. 검색된 영화의 포스터를 생성하여 반환하는 메서드를 만들어준다.
// 검색된 영화의 포스터를 만들어 반환한다.
Future<List<Image>> getSearchPoster(List<Map<String, dynamic>> searchResult) async {

   // 포스터를 담을 리스트
   List<Image> searchPoster = [];

   // 검색된 영화의 수 만큼 반복한다.
   for(var map in searchResult){
      // 이미지를 생성한다.
      var tempImage = await getImageData(map['movie_poster']);
      // 가로가 100사이즈의 이미지로 다시 생성해준다.
      var tempImage2 = Image(image: tempImage.image, width: 100);
      // 리스트에 담는다.
      searchPoster.add(tempImage2);
   }

   return searchPoster;
}
  1. 프로바이더의 리스너 코드를 변경한다.
   // 리스너를 연결해준다.
   searchScreenProvider.addListener(() async {
      // print("SearchListView : ${searchScreenProvider.searchKeyword}");
      // 검색 결과를 가져온다.
      List<Map<String, dynamic>> tempSearchResult = await getSearchResult(searchScreenProvider.searchKeyword);
      // 검색 결과 포스터를 가져온다.
      posterData = await getSearchPoster(tempSearchResult);
      // 상태를 설정해준다.
      setState(() {
        searchResult = tempSearchResult;
      });
   });

  1. 출연진 등이 화면 밖을 벗어나는걸 대비하기 위해 overflow를 설정한다.

텍스트 내용에 따라 화면을 벗어나는 경우가 있는데

텍스트 위젯에 overflowTextOverflow.ellipsis로 넣어주면 ... 표시로 처리 가능하지만,
overflow 기능은 문자열의 길이와 해당 내용을 감싸는 위젯의 가로 길이가 확정이 되어야 한다.
하지만 여기서 보여주는 Text위젯은 가로길이가 정해져있지 않은 Container 안에 있고 이 Container는 ListView의 항목이기 때문에 overflow가 적용되지 않는다.
ListView 항목의 가로, 세로 길이는 항목이 구성되기 전까지는 길이가 0이었다가 구성된 항목에 맞춰서 길이가 변경된다.
따라서 Expanded위젯을 Text위젯에 감싸주게되면 ListView를 Expanded위젯으로 감쌌던 것과 마찬가지로 Text의 길이를 항목 생성 후의 최대 길이 값으로 정해줄 수 있다.
ListView의 항목을 구성할 때에도 해당 항목을 Expanded위젯으로 감싸주면 길이 관련 이슈를 피할 수 있다.

결국 ListView를 만들 때 길이와 관련된 작업이 필요할 경우,
ListView위젯을 Expanded로 감싸주고 ListView의 항목도 Expanded로 감싸주면 된다.

      child: Row(
        children: [
          posterData[index],
          const Padding(padding: EdgeInsets.only(right: 10)),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  searchResult[index]['movie_title'],
                  style: TextStyle(fontSize: 15),
                  overflow: TextOverflow.ellipsis,
                ),
                Text(
                  '출연진 : ${searchResult[index]['movie_actor']}',
                  style: TextStyle(fontSize: 12),
                  overflow: TextOverflow.ellipsis,
                ),
                Text(
                  '제작진 : ${searchResult[index]['movie_director']}',
                  style: TextStyle(fontSize: 12),
                  overflow: TextOverflow.ellipsis,
                ),
              ],
            ),
          ),
        ],
      ),

overflow를 지우거나(기본값) TextOverflow.clip(기본값)으로 설정해주면 줄바꿈(wrap)으로 바뀐다.

                Text(
                  '출연진 : ${searchResult[index]['movie_actor']}',
                  style: TextStyle(fontSize: 12),
                  overflow: TextOverflow.clip,
                ),

overflow 대신 softWrapfalse로 설정하면 ... 없이 잘리는 부분까지만 표시된다.

                Text(
                  '출연진 : ${searchResult[index]['movie_actor']}',
                  style: TextStyle(fontSize: 12),
                  softWrap: false,
                ),

카메라, 앨범 연동하기

  1. pubspec.yaml 파일에 라이브러리를 추가한다.

  2. MyPageScreen에 프로필 이미지를 보여줄 이미지 객체를 생성한다.


  // 사용자 프로필 이미지
  Image profileImage = Image.asset('lib/assets/images/youtube_logo.png');
  1. 프로필 이미지를 보여주는 부분에 변수를 적용해준다.
                child: profileImage,
  1. ImagePicker 객체를 생성한다.

  // 앨범이나 카메라에서 사진을 가져오기 위한 객체
  ImagePicker imagePicker = ImagePicker();
  // 앨범이나 카메라에서 사진을 가져오면 단말기 로컬 저장소에 저장이 된다.
  // 저장된 사진의 경로를 담을 변수
  String pickedImagePath = "";
  1. 카메라, 앨범에 이미지를 가져오는 함수를 만들어준다.

  // 카메라나 앨범으로 부터 사진을 가져온다.
  Future<void> getImage(ImageSource imageSource) async {
    // 사진을 가져온다.
    XFile? pickedImage = await imagePicker.pickImage(source: imageSource);
    // 사진을 가져왔다면
    if(pickedImage != null){
      setState(() {
        // 가져온 사진의 경로를 가져온다.
        var xfileImage = XFile(pickedImage.path);
        pickedImagePath = xfileImage.path;
        // 사진을 보여준다.
        profileImage = Image.file(File(pickedImagePath));
      });
    }
  }
  1. 카메라나 앨범 버튼을 누르면 메서드를 호출한다.

                IconButton(
                  icon: Icon(Icons.camera_alt),
                  onPressed: () {
                    getImage(ImageSource.camera);
                  },
                ),
                IconButton(
                  icon: Icon(Icons.photo_album),
                  onPressed: () {
                    getImage(ImageSource.gallery);
                  },
                ),
  1. 파일을 서버에 업로드 하는 코드를 작성해준다.
// Storage에 이미지를 저장하는 함수
// localImagePath : 단말기에 저장되어 있는 이미지의 경로
// serverImagePath : 서버상에서의 이미지의 경로
Future<void> uploadImage(String localImagePath, String serverImagePath) async {
  await FirebaseStorage.instance.ref().child("user_image").child(serverImagePath).putFile(File(localImagePath));
}
  1. 저장 버튼을 눌렀을 때 업로드 되게 한다.
                // 이미지를 업로드 한다.
                if(pickedImagePath != "") {
                  uploadImage(pickedImagePath, "user_profile.jpg");
                }
  1. TextEditingcontroller를 만들어준다.
  // 입력 요소들과 연결될 컨트롤러
  TextEditingController nameController = TextEditingController();
  TextEditingController nickNameController = TextEditingController();
  1. 각 입력 요소에 controller를 설정해준다.
              controller: nameController,

              controller: nickNameController,          
                  
  1. 사용자 정보를 저장할 함수를 만들어준다.
// 사용자 정보를 서버에 저장한다.
// 여기에서는 사용자가 한명 임을 가정한다.
// add : 문서 추가
// set : 문서 내의 모든 필드를 삭제 하고 다시 저장
// update : 문서 내의 필드 일부를 수정
Future<void> saveUserInfo(String name, String nickName, String profilePath) async {
  await FirebaseFirestore.instance.collection("user_data").doc("user_profile").set({
    "user_name" : name,
    "user_nickname" : nickName,
    "user_profilePath" : profilePath
  });
}
  1. 사용자 정보를 저장하는 메서드를 호출해준다.
              onPressed: () async {
                // 이미지를 업로드 한다.
                if(pickedImagePath != "") {
                  await uploadImage(pickedImagePath, "user_profile.jpg");
                }
                // 사용자 정보를 저장한다.
                await saveUserInfo(nameController.text, nickNameController.text, "user_profile.jpg");
                FocusScope.of(context).unfocus();
              },

              
  1. 사용자 정보를 가져오는 메서드를 만들어준다
// 서버에 저장되어 있는 사용자 정보를 읽어와 반환한다.
Future<Map<String, dynamic>?> getUserInfo() async {
var querySnapShot = await FirebaseFirestore.instance.collection("user_data").doc("user_profile").get();
// 반환할 사용자 정보
Map<String, dynamic>? map = querySnapShot.data();

return map;
}
  1. 사용자 정보를 가져와 출력해준다.
  // 사용자 정보를 가져와 설정하는 함수
  Future<void> getUserData() async {
    // 사용자 정보를 가져온다
    var map1 = await getUserInfo();

    setState(() {
      nameController.text = map1!['user_name'].toString();
      nickNameController.text = map1!['user_nickname'].toString();
    });
  }
  1. initState에서 호출해준다.
  
  void initState() {
    // TODO: implement initState
    super.initState();
    // 사용자 정보를 가져와 설정해준다.
    getUserData();
  }

  1. 사용자 이미지를 가져오는 함수를 만들어준다.
// 이미지 데이터를 가져온다
Future<Image> getProfileImageData(String fileName) async {
  // 이미지를 가져올 수 있는 주소를 가져온다
  String imageUrl = await FirebaseStorage.instance.ref('user_image/$fileName').getDownloadURL();
  // print(imageUrl);

  // 이미지를 관리하는 객체
  Image resultImage = Image.network(imageUrl);

  return resultImage;
}
  1. 사용자 이미지를 가져와 변수에 넣어준다.
    profileImage = await getProfileImageData(map1!['user_profilePath']);
  1. 받아온 사용자 정보가 있을 때만 동작하도록 한다.
    if(map1 != null) {
      profileImage = await getProfileImageData(map1!['user_profilePath']);

      setState(() {
        nameController.text = map1!['user_name'].toString();
        nickNameController.text = map1!['user_nickname'].toString();
      });
    }

profile
안드로이드공부

0개의 댓글