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


1. xcode 설치
firebase 사이트에서 프로젝트를 생성한다.
firebase tools 설치
firebase에 로그인을 한다
firebase login
path 설정
flutter의 bin 폴더를 path로 잡아준다
터미널에서 다음 명령어를 실행해 vi 편집기를 실해준다.
sudo vi /etc/paths
i 를 눌러 편집 모드로 편한한다.
flutter의 bin 폴더까지의 경로를 입력해준다
esc 키를 눌러준다.
:wq 를 입력후 엔터를 눌러 저장한다.
터미널을 종료한다.
안드로이드 스튜디오의 터미널을 실행한다.
firebase 사이트에 나와있는 첫 번째 명령어를 터미널에 입력하여 실행해준다.
dart pub global activate flutterfire_cli
설정해야할 경로를 복사해둔다.
다음 명령어를 실행하여 vi 편집기를 실행한다
vi ~/.zshrc
i 를 눌러 편집 모드로 변경한다.
복사한 경로를 붙혀넣어준다.
esc를 눌러준다.
:wq 를 입력후 엔터를 눌러 저장하고 나간다.
안드로이드 스튜디오의 터미널을 종료하고 터미널을 다시 실행해준다.
dart pub global activate flutterfire_cli
15 사이트에 나와있는 두 번째 명령어를 터미널에서 실행한다
flutterfire configue --project=프로젝트명
return SearchBar(
// 좌측에 배치되는 아이콘
leading: Icon(Icons.search),
// 내부 여백
padding: MaterialStatePropertyAll(EdgeInsets.fromLTRB(10, 0, 10, 0)),
// 키보드의 submit 버튼을 눌렀을 때
// value : 사용자가 입력한 내용이 들어온다.
onSubmitted: (value) {
},
);
class TabPageIndexProvider extends ChangeNotifier{
// 사용자가 입력한 검색어를 담을 변수
String _searchKeyword = "";
String get searchKeyword => _searchKeyword;
}
class TabPageIndexProvider extends ChangeNotifier{
// 사용자가 입력한 검색어를 담을 변수
String _searchKeyword = "";
String get searchKeyword => _searchKeyword;
void setKeyword(String keyword){
_searchKeyword = keyword;
// 모든 리스너를 동작시킨다.
notifyListeners();
}
}
class _SearchScreenState extends State<SearchScreen> {
Widget build(BuildContext context) {
// Provider를 가져온다.
var searchScreenProvider = Provider.of<TabPageIndexProvider>(context, listen: false);
return SearchBar(
// 좌측에 배치되는 아이콘
leading: Icon(Icons.search),
// 내부 여백
padding: MaterialStatePropertyAll(EdgeInsets.fromLTRB(10, 0, 10, 0)),
// 키보드의 submit 버튼을 눌렀을 때
// value : 사용자가 입력한 내용이 들어온다.
onSubmitted: (value) {
// Provider의 set 메서드를 호출해준다.
searchScreenProvider.setKeyword(value);
},
);
class _SearchListViewState extends State<SearchListView> {
Widget build(BuildContext context) {
// Provider를 가져온다.
var searchScreenProvider = Provider.of<TabPageIndexProvider>(context, listen: false);
// 리스너를 연결해준다.
searchScreenProvider.addListener(() {
print("SearchListView : ${searchScreenProvider.searchKeyword}");
});
class _SearchListViewState extends State<SearchListView> {
// 검색 결과 데이터를 담을 리스트
List<Map<String, dynamic>> searchResult = [];
// 검색 결과 영화 포스터를 담을 리스트
List<Image> posterData = [];
return ListView.builder(
itemCount: searchResult.length,
itemBuilder: (context, index) => makeListItem(context),
);
return ListView.builder(
itemCount: searchResult.length,
itemBuilder: (context, index) => makeListItem(context, searchResult, posterData, index),
);
// 리스트뷰의 항목 하나를 구성하는 함수
// 리스트뷰의 항목은 ListTitle을 사용해도 된다 대신 아이콘 사이즈 조절 불가
Widget makeListItem(BuildContext context, List<Map<String, dynamic>> searchResult, List<Image> posterData, int index){
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)
),
],
)
],
),
// 전체 영화 데이터에서 검색어에 해당하는 것만 모아 반환한다.
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;
}
// 검색된 영화의 포스터를 만들어 반환한다.
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;
}
// 리스너를 연결해준다.
searchScreenProvider.addListener(() async {
// print("SearchListView : ${searchScreenProvider.searchKeyword}");
// 검색 결과를 가져온다.
List<Map<String, dynamic>> tempSearchResult = await getSearchResult(searchScreenProvider.searchKeyword);
// 검색 결과 포스터를 가져온다.
posterData = await getSearchPoster(tempSearchResult);
// 상태를 설정해준다.
setState(() {
searchResult = tempSearchResult;
});
});

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

텍스트 위젯에 overflow를 TextOverflow.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 대신 softWrap을 false로 설정하면 ... 없이 잘리는 부분까지만 표시된다.
Text(
'출연진 : ${searchResult[index]['movie_actor']}',
style: TextStyle(fontSize: 12),
softWrap: false,
),

pubspec.yaml 파일에 라이브러리를 추가한다.
MyPageScreen에 프로필 이미지를 보여줄 이미지 객체를 생성한다.
// 사용자 프로필 이미지
Image profileImage = Image.asset('lib/assets/images/youtube_logo.png');
child: profileImage,
// 앨범이나 카메라에서 사진을 가져오기 위한 객체
ImagePicker imagePicker = ImagePicker();
// 앨범이나 카메라에서 사진을 가져오면 단말기 로컬 저장소에 저장이 된다.
// 저장된 사진의 경로를 담을 변수
String pickedImagePath = "";
// 카메라나 앨범으로 부터 사진을 가져온다.
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));
});
}
}
IconButton(
icon: Icon(Icons.camera_alt),
onPressed: () {
getImage(ImageSource.camera);
},
),
IconButton(
icon: Icon(Icons.photo_album),
onPressed: () {
getImage(ImageSource.gallery);
},
),
// Storage에 이미지를 저장하는 함수
// localImagePath : 단말기에 저장되어 있는 이미지의 경로
// serverImagePath : 서버상에서의 이미지의 경로
Future<void> uploadImage(String localImagePath, String serverImagePath) async {
await FirebaseStorage.instance.ref().child("user_image").child(serverImagePath).putFile(File(localImagePath));
}
// 이미지를 업로드 한다.
if(pickedImagePath != "") {
uploadImage(pickedImagePath, "user_profile.jpg");
}
// 입력 요소들과 연결될 컨트롤러
TextEditingController nameController = TextEditingController();
TextEditingController nickNameController = TextEditingController();
controller: nameController,
controller: nickNameController,
// 사용자 정보를 서버에 저장한다.
// 여기에서는 사용자가 한명 임을 가정한다.
// 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
});
}
onPressed: () async {
// 이미지를 업로드 한다.
if(pickedImagePath != "") {
await uploadImage(pickedImagePath, "user_profile.jpg");
}
// 사용자 정보를 저장한다.
await saveUserInfo(nameController.text, nickNameController.text, "user_profile.jpg");
FocusScope.of(context).unfocus();
},
// 서버에 저장되어 있는 사용자 정보를 읽어와 반환한다.
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;
}
// 사용자 정보를 가져와 설정하는 함수
Future<void> getUserData() async {
// 사용자 정보를 가져온다
var map1 = await getUserInfo();
setState(() {
nameController.text = map1!['user_name'].toString();
nickNameController.text = map1!['user_nickname'].toString();
});
}
void initState() {
// TODO: implement initState
super.initState();
// 사용자 정보를 가져와 설정해준다.
getUserData();
}
// 이미지 데이터를 가져온다
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;
}
profileImage = await getProfileImageData(map1!['user_profilePath']);
if(map1 != null) {
profileImage = await getProfileImageData(map1!['user_profilePath']);
setState(() {
nameController.text = map1!['user_name'].toString();
nickNameController.text = map1!['user_nickname'].toString();
});
}

