지난 번엔 플러터로 내가 만든 서버와 앱을 연결하여
DB에서 테스트로 데이터를 하나만 가져와서 표기해보았다.
이번에는 테스트로 가져온 데이터 리스트를 ListView에 담아 보기 좋게 정렬하고,
핸드폰의 UI를 조작하여 새로운 악보를 DB에 원격으로 추가하도록 하였다.
이때 DB조작을 위해 사용한 코드가 기존코드와 달라서 조금 애를 먹었다.
백엔드에서 사용하는 코드도 조금 수정해야 했다.
우선 앱의 메인 코드는 다음과 같다.
class _MainFrameState extends State<MainFrame> {
var _selectedIndex = 0;
List<Widget> _bodyWidgets = [
SearchSheet(),
Text("1"),
Text("2"),
];
List<PreferredSizeWidget> _appBarWidgets = [
AppBar(
title: Text("악보 검색"),
actions: [
Builder(builder: (context) {
return IconButton(
onPressed: () {
showSearch(context: context, delegate: MySearchDelegate())
.toString(); // MySearchDelegate is declared in page_searchSheet.dart
},
icon: Icon(Icons.search),
);
})
],
),
AppBar(
title: Text("그룹"),
),
AppBar(
title: Text("내 악보"),
),
];
List<Widget?> _floatingButtonWidgets = [
SearchSheetFloatingButton(), // page_searchSheet.dart
null,
null,
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appBarWidgets[_selectedIndex],
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: "악보검색",
),
BottomNavigationBarItem(
icon: Icon(Icons.people),
label: "그룹",
),
BottomNavigationBarItem(
icon: Icon(Icons.my_library_music), label: "내 악보"),
],
currentIndex: _selectedIndex,
onTap: onTapBottomNavigationItem,
),
body: Center(
child: _bodyWidgets.elementAt(_selectedIndex),
),
floatingActionButton: _floatingButtonWidgets[_selectedIndex],
);
}
void onTapBottomNavigationItem(int index) {
setState(() {
_selectedIndex = index;
});
}
}
앱의 시작지점으로 작용하는 statefulWidget으로 Scaffold() 위젯을 반환하도록 하여 큰 틀을 잡았다.
여기에서 바텀네비게이션과 앱바, 바디를 설정하였다.
바텀네비게이션은 3종류로 되어있으며,
각각에 맞게 들어갈 바디와 앱바를 리스트에 담아 바텀네비게이션 인덱스로 관리하도록 하였다.
이때 바디는 별도 클래스로 떼어서 별도 dart 파일로 관리하였다.
원래는 앱바도 별도 클래스로 떼어서 관리하려고 하였으나, 내가 현재 아는 플러터 지식으로는 별도 관리가 되지 않았다.
내가 아는 한에서는, 따로 떼어낼 위젯을 별도 StatefulWidget 클래스나 StatelessWidget 클래스로 관리해야 했다.
그러나 AppBar() 클래스는 Widget() 클래스와 직접적으로 관련이 없어
StatefulWidget, StatelessWidget으로 관리할 수 없었다.
AppBar() 클래스는 PreferredSizeWidget() 클래스로 관리가 되어
AppBar() 클래스를 StatefulWidget의 Build() 메소드로 반환하면 타입이 맞지 않는다고 컴파일 오류를 띄웠다.
사실 FloatingButton을 저렇게 리스트로 관리하는 데에도 애를 먹었다.
처음에는 별도 파일로 관리하지 않고 이 파일내부에서 저렇게 리스트로 관리하려고 했다.
그러나 이렇게하자 context를 쓸 수가 없어 FloatingButton을 터치했을 때 다이얼로그를 띄울 수가 없었다.
(컴파일 오류 발생)
처음에는 이유를 몰랐으나, 이유는 간단했다.
context는 위젯을 Build 한 이후에 사용이 가능한데, 클래스의 변수로 선언하면
이때는 아직 위젯이 빌드되기 전이라 context를 사용할 수가 없는 것이다.
그래서 이 문제를 해결하기 위해 Floating 버튼도 결국 첫번째 페이지 내부 내용에 속한다고 판단하여
첫번째 페이지와 관련된 dart 파일 내부로 옮겨서서 사용했다.
이렇게 하자 context를 마음껏 사용할 수 있었다.
Scaffold()의 body 속성은 내가 생각한대로 구현할 내용이 엄청나게 많아 별도 파일로 분리하여 관리하였다.
body에 넣을 위젯을 반환하는 StatefulWidget Class를 만들어 별도 파일에 작성하였다.
악보검색과 관련된 모든 기능은 page_SearchSheet.dart 파일에 관리하도록 하였다.
악보 검색 페이지에서 띄울 플로팅 버튼 코드이다.
단순하게 클릭하면 Dialog를 띄운다.
기능이 고정적이고 버튼의 디자인 역시 변하지 않으므로 StatelessWidget 클래스로 하였다.
NewSheetDialog() 코드는 아래와 같다.
다이어로그 내부가 스크롤이 되어야 하기 때문에 스택오버플로우 검색결과를 통해 알게된
SingleShildScrollView() 위젯을 사용했다.
버튼은 OK버튼, Cancel 버튼으로 무난하게 구성했다.
실행 시 모습은 다음과 같다.
아직 곡 제목 입력란에 필수 입력 조치를 해두지는 않았다.
현재는 그냥 TextField() 위젯을 사용하고 있지만,
TextFormField() 위젯에 대해 공부하고, 폼의 유효성 검사 부분을 공부한 뒤 수정할 예정이다.
우선 나는 폼 입력값을 웹서버에 전달하여, 해당 내용으로 데이터를 만들어서 DB에 추가하는 걸 목표로 삼았다.
(테이블 구조상 '곡 제목'이 not null 이라서, 현재는 백엔드에서 널체크, 정확히는 빈 문자열 체크를 하고 있다.)
이 파트를 만들면서 AlertDialog위젯, TextFeild위젯, DropButtonFormFeild 위젯에 대한 사용법을 익혔다.
제일 중요한, DB에 등록된 악보 데이터를 가져와 ListView로 나열해서 보여주는 기능은 다음과 같이 구현하였다.
이 위젯이 생성될 때(initState), 웹서버로부터 악보 데이터를 json형태로 가져온다.
악보데이터는 여러개가 들어오는데, 미리 정의한 Sheet 클래스에 해당데이터를 넣고,
각각의 Sheet 객체를 리스트에 담아 저장해둔다.
이후 위젯을 빌드하면서 ListVIew 위젯을 만들 때 이 리스트에 담긴 데이터를 가지고
ListView를 채우도록 하였다.
이 코드는 플러터 공식 http 통신 예제를 변형하여 작성하였다.
이 코드에서 가장 중요한 부분은 list를 return하는 코드 바로 위
setState(); 함수를 실행하는 부분이다.
이 fetchSheet 함수는 비동기 함수로, 이 함수는 일단 Future 객체를 임시로 리턴해놓고
나머지 코드를 이어서 실행한다.
이때 Future객체가 아직 다 차지 않은 상태에서 Build() 메소드가 실행되는데
이 경우 ListView를 빌드해도 아무런 값이 없다.
따라서 웹서버로부터 데이터를 가져온 이후, setState() 함수를 직접 실행하여
ListVIew를 다시 명시적으로 빌드해주도록 하여 리스트뷰가 채워지도록 했다.
리스트뷰를 채우는 코드는 다음과 같이 하였다.
우선 짝수열에는 리스트 타일을, 홀수열에는 구분선을 넣었다.
리스트타일은 Future 객체로부터 데이터를 가져와 만들어야해서 별도로 함수를 만들었다.
Future객체를 제네릭에 들어온 타입의 객체로 구체화하여 활용하였다.
실행 시 결과는 아래와 같다.
마지막으로 아까 만들었던 다이어로그를 클릭하면 악보를 만들 수 있는 창이 새로 나오고,
해당 창에서 확인 버튼을 누르면 DB에 악보가 추가되도록 만들어보았다.
다이어로그의 확인버튼 클릭 시 코드이다.
NewSheet 라는 새로운 화면을 만들도록 하였다.
이때, 해당 화면에서 사용할 곡제목, 가수, 노래 키 정보를 넘겨주도록 하였다.
NewSheet 클래스는 새로운 곡을 추가할 때 사용하는 화면에 대한 클래스로 Scaffold 위젯을 반환한다.
이 클래스는 page_NewSheet.dart 파일로 별도 분리하여 작성하였다.
아직은 이렇게 아무것도 없는 빈 화면이지만,
이제부터 실질적인 악보를 그릴 수 있도록 채울 예정이다.
우측 상단의 확인버튼(v)를 누르면 이 악보를 저장하게 된다.
코드로는 createSheet() 함수를 실행하고나서 현재 창을 닫는 단순한 기능이다.
createSheet() 함수는 다음과 같다.
테스트를 위해서 reponse.body를 출력해보았다.
이 코드 역시 플러터 공식 문서에서 웹서버에 http로 데이터를 전송하는 예제를 보고 만들었는데,
예제대로 작동하지 않아 당황했었다.
그래서 이렇게 header 속성에 대해 적혀있는 설명을 보고 코드를 직접 작성했다.
만약 body가 Map 이라면 폼필드가 사용하는 인코딩대로 인코딩 될 것이고,
content-type은 "application/x-www-form-urlencoded" 라고 한다.
그래서 이대로 코딩을 작성해주었다.
하지만 그래도 웹서버에서 데이터를 가져오지 못했다.
다행히 statusCode는 200으로 오는것을 보니 통신 자체에는 문제가 없고,
php에서 돌려주는 데이터에 문제가 있다고 판단했고, 내 생각이 맞았었다.
그래서 php의 코드를 아래와 같이 수정했다.
<?php
error_reporting(E_ALL);
ini_set('display_errors',1);
include('DbConn.php');
$song_name =$_POST['song_name'];
$singer =$_POST['singer'];
if(empty($song_name)){
$errMSG = "곡 이름을 입력하세요.";
}
if(!isset($errMSG)) // 이름과 나라 모두 입력이 되었다면
{
try {
// SQL문을 실행하여 데이터를 MySQL 서버의 person 테이블에 저장합니다.
$stmt = $con->prepare(
'INSERT INTO sheet_list (song_name, singer)
VALUES (:song_name
, :singer)'
);
$stmt->bindParam(':song_name', $song_name);
$stmt->bindParam(':singer' , $singer);
if($stmt->execute()) {
$successMSG = "새로운 악보를 추가했습니다.";
$successMSG = iconv("euckr", "utf-8", $successMSG);
} else {
$errMSG = "악보 추가 에러";
$successMSG = iconv("euckr", "utf-8", $errMSG);
}
} catch(PDOException $e) {
die("Database error: " . $e->getMessage());
}
}
?>
<?php
if (isset($errMSG)) echo $errMSG;
if (isset($successMSG)) echo $successMSG;
?>
아직 PHP언어에 대한 이해가 높지 않아 다른 분이 작성하신 안드로이드 DB통신 예제 코드를 참고하여 수정한 코드이다.
그 코드에서는 아래와 같은 '안드로이드 체크 코드'가 있었다.
$android = strpos($_SERVER['HTTP_USER_AGENT'], "Android");
if( (($_SERVER['REQUEST_METHOD'] == 'POST') && isset($_POST['submit'])) || $android )
{...}
이 코드에서 $android 부분이 java로 앱을 만들었던 때와는 달리 작동하지 않았다.
플러터는 네이티브와 조금 다른 방식으로 http통신을 하는건지도 모르겠다
그래서 이런 조건 확인없이 그냥 바로 통신하도록 했다.
나중에는 가능하면 현재 php로 만든 백엔드를 파이썬의 Flask로 수정해보는 것이 목표이다.
물론 아직은 Flask도 너무 어려워서 힘들게 공부중이다.
(그래서 장고를 먼저 공부해볼까 고민하고 있다..)
악보를 추가하면 다음과 같이 print문이 나온다.
플러터에서 작성한 코드가 아닌, 웹서버에서 받아온 response 내용이다.
아직은 이렇게 DB에 데이터를 저장하더라도, 리스트뷰가 바로 갱신되지 않는다.
Navigator.pop() 메소드로 인해 이전 화면으로 돌아온 경우, initState() 메소드가 실행되지 않기 때문이다.
현재 이 문제를 어떻게 해결할지 고민중이다.
Navigator.pop()에 내용을 담아 pop하더라도,
새로운 창을 호출하는 코드는 이미 사라져버린 다이어로그에 있어서
이 악보검색 페이지에서는 새로운 창 객체에 접근이 안되기 때문이다.
현재 떠오르는 것은, 라우트를 명시적으로 정해서 데이터를 넘겨줄 수 있지 않나 하는 것 정도라
좀 더 공부를 해봐야 할 것 같다.
일단 현재로서 새로고침을 하기 위해서는 바텀네비게이션 창을 다른 화면으로 바꿨다가 돌아와서
initState()메소드를 실행시키고 화면을 갱신하는 방법이 유일하다.
(그렇다고 빌드를 다시하기는 귀찮으니까..)
아직도 해야할 일은 많이 남았다.
우선 검색버튼은 만들어두었지만, 검색기능은 작동하지 않고있고
악보를 추가할 수 있게되었지만, 추가한 악보에는 아무 내용물이 없어 내용물을 추가할 수 있게 해야한다.
이를 위해서 내용물을 어떻게 저장할 지 DB테이블 레이아웃부터 짜서 테이블을 만들고
앱 UI 레이아웃을 어떻게 구성할지 고민해야한다.
그리고 이걸 다하게 되면 드디어 '악보검색 / 그룹 / 내 악보' 3가지 기능 중
첫 번째 '악보검색'의 핵심 기능이 끝난 것이다!!
우선은 '악보'만이라도 CRUD를 제대로 구현해보고자 한다.
일단 악보만 성공하면, 그룹이나 '내 악보' 같은 경우는 좀 더 쉽게 할 수 있을 거라고 생각한다..
'개인 프로젝트 > [2021] 코드악보 공유APP' 카테고리의 다른 글
11. 악보 편집 기능 만들기 - 코드 줄넘김/줄넘김 취소 & 커스텀 키보드 (0) | 2021.07.15 |
---|---|
10. 악보 편집 기능 만들기 - 커스텀 탭, wrap 위젯, 커스텀 키보드 (0) | 2021.07.13 |
8. 플러터로 프로젝트 이전 (1) - 바텀네비게이션, 검색UI, http통신 (2) | 2021.07.06 |
7. 악보 검색 / 등록 페이지 제작 (5) - 악보 추가 기능 만들기(1) (2) | 2021.03.18 |
6. 악보 검색 / 등록 페이지 제작 (4) - 검색기능 구현 & 악보 뷰어 제작 (0) | 2021.03.13 |