개발 툴 : Flutter
개발 언어 : Dart
개발 일시 : 2023-01-28 ~ 2023-02-05
개발자 : Won Chi Hyeon
https
url_launcher
shared_preferences
screens 폴더 안에 화면의 위젯들을 보여주는 home_screen.dart 파일을 만들고 그 안에 위젯들을 배치하였습니다.
앱 바는 배경색을 하얀색, 폰트색을 검은색으로 하고 Scaffold 위젯에 하얀색 바탕색을 주었습니다.
텍스트는 오늘의 웹툰을 작성하고 폰트크기를 24로 지정하였습니다.
lib 폴더 안에 services 폴더를 만들고 그 안에 ApiService 클래스를 만들어줍니다.
이 클래스는 baseUrl 프로퍼티를 가지고 있고 값으로 https://webtoon-crawler.nomadcoders.workers.dev/ url을 가집니다.
또 하나의 today 프로퍼티로 값으로 today를 가지고 있습니다.
baseUrl = 네이버 웹툰 Unofficial API
서버에서 데이터를 가져오기 위해서는 http 라이브러리 설치가 필요합니다.
flutter pub add http 명령어를 사용하여 설치한 후에 프로젝트에 사용하였습니다.
그냥 http의 get메서드를 사용해도 되지만 namespace를 사용하기 위해
import 문 뒤에 as http 를 사용하여 http.get의 형식으로 사용하였습니다.
url을 Uri 메서드로 파싱해서 가져온 다음 get 메서드로 넘겨줍니다.
get 방식으로 요청을 처리할 때는 요청의 결과인 응답 결과가 중요한 데이터이면서 future타입(미래에 완료)을
반환한다는 것을 알 수 있습니다. 이런 함수는 비동기 함수 async로 만들어서 요청의 결과가 끝난 후에
진행되어야 하므로 await 구문을 사용하였습니다.
get메서드를 사용해서 요청한 응답을 response 변수에 담고 response의 상태코드가 200일때(요청성공일때)
response.body 즉 데이터 부분을 콘솔에 print 하도록 하고 실패할 경우 Error를 반환하도록 하였습니다.
메인 함수에서 ApiServices 클래스의 getTodaysToons 메서드를 호출하여 콘솔에서 데이터를 잘 가져오는 지 확인하였습니다.
이후에 가져온 데이터를 활용하여 앱을 만들어보겠습니다.
lib 아래 models 폴더를 만들고 webtoon_model.dart 파일을 생성합니다.
요청에 대한 응답의 키 문자열은 title, thumb, id 세 가지 입니다.
응답의 문자열을 JSON 데이터로 먼저 변환해줘야 합니다.
jsonDecode를 사용해서 문자열을 JSON으로 변환해 주었습니다.
콘솔에 웹툰을 하나씩 출력합니다.
이름 생성자로 fromJson이라는 생성자를 만든 후에 각 문자열을 Json의 데이터와 대응시킵니다.
이렇게 생성된 모델을 toon 변수에 담고 title을 콘솔에 출력하였습니다.
빈 리스트 webtoonInstances를 생성하고 add 메서드로 webtoon Json 데이터를 넣어준 뒤 return 합니다.
위젯의 화면을 나타내는 home_screen.dart 파일의 stless 클래스를 stateful 클래스로 바꾼 뒤
웹툰을 받아오는 함수를 비동기 함수 안에 넣고 initState()에서 호출하도록 하였습니다.
state를 사용해서 데이터를 가져오는 방법은 선호되지 않기에 home_screen.dart 파일의
stateful클래스를 다시 stless로 변경 후 위에서 작업한 initState()와 비동기 함수는 지워줍니다.
후에 webtoons를 통해서 getTodaysToons함수를 호출한 다음
Scaffold의 바디에 FutureBuilder를 사용합니다.
FutureBuilder는 future와 builder를 사용합니다.
future는 위에서 만든 webtoons를 주고
builder는 context와 snapshot을 가집니다.
snapshot이란 future 데이터의 상태를 나타내고
hasData 메서드를 통해서 데이터의 유무 상태를 조작할 수 있습니다.(isLoading 조작 x)
state를 사용하기보다 futureBuilder를 사용하면 setState, isLoading 조작이 필요업다는 장점이 있습니다.
데이터를 가져올 때 (hasData ? false일 때) 로딩 Indicator가 나타나도록 수정하였습니다.
여러 항목들을 나열하기 위해서는 ListView 위젯을 사용합니다. for 구문을 사용해서 future의 데이터
중 웹툰의 제목들을 하나씩 나열하였습니다.
ListView를 사용하면 한 번에 데이터를 다 로딩하기 때문에 메모리 사용 측면에서 굉장히 부정적인 영향을 끼칩니다.
따라서 좀 더 전문적이고 많은 아이템에 최적화된 ListView.builder를 사용하였습니다.
ListView를 사용하면 화면에 보이는 아이템만 로딩해서 보여주는 itemBuilder를 사용할 수 있고
메모리의 재활용을 하거나 낭비를 줄일 수 있습니다.
데이터를 인덱스 번호에 매칭해서 화면에 보이는 아이템만 로딩합니다.
ListView.builder에서 builder를 separated로 수정합니다.
separatedBuilder를 통해 데이터 간 구분자를 설정할 수 있습니다.
너비 20의 sizebox를 구분자로 설정하였습니다.
썸네일 이미지를 넣을 공간을 만들기 위해서 ListView를 Expanded로 감싸고 다시 Column으로 감싸줍니다.
Container를 이용해 공간을 만듭니다. SizedBox를 사용할 때모다 decoration로 옵션을 주어서 더 꾸며줄 수 있습니다.
Container 안에 Image.network(url) (url:webtoon.thumb에 있습니다)를 사용해서 이미지를 추가해줍니다.
웹툰 이미지를 추가해주고 제목과의 간격을 SizedBox로 조정합니다.
불투명도 0.5 , 15크기의 범위를 가지는 그림자를 썸네일에 추가하였습니다.
웹툰 썸네일을 리턴하는 부분을 따로 detail_screen.dart파일로 분리한 후에
Container를 GestureDectector로 감싸준 뒤 onTap에서 Navigator를 작성하였습니다.
어떤 웹툰인지 구별하기 위해서 생성자로 title, thumb, id의 정보를 넘겨주도록 하였습니다.
썸네일 이미지를 누르면 Navigator의 MaterialRoute가 detail_screen.dart의 stless 화면 위젯 클래스를 route로
build 해서 화면 전환 애니메이션 효과를 줄 수 있습니다.
AppBar는 전체적인 통일감을 주기 위해서 homescreen 에서 사용한 appbar 코드를 재활용하되 title은
웹툰의 title이 올 수 있도록 수정하였습니다.
맨 앞 웹툰의 썸네일을 클릭한 후 화면을 전환하여 해당하는 웹툰의 상세 페이지로 넘어가도록 구현하였습니다.
버튼을 클릭했을 때 route의 종류에 따라 옵션을 달리하여 다양한 애니메이션 효과를 줄 수 있습니다.
MaterialRoute를 PageRouteBuilder로 수정하고 fullscreenDialog 옵션을 주어서 애니메이션 효과를 주었습니다.
썸네일 이미지를 클릭하면 아래에서 위로 페이지가 날라오면서 AppBar의 뒤로가기 버튼이 X 로 바뀝니다.
두 개의 화면의 위젯을 하나의 같은 태그로 연결하면 이미지가 연결되어 떠다니듯이 이동합니다.
Home 스크린의 Container를 Hero 위젯으로 감싸고 tag 값으로 id를 주었습니다.
마찬가지로 detail 스크린의 Container를 Hero 위젯으로 감싸고 tag 값으로 id를 주었습니다.
이렇게 하면 두 개의 이미지가 서로 연결되어 화면 전환이 일어날 때 위젯이 마치 연결된 듯한 움직임을
보여줄 수 있습니다.
상세 페이지에 들어갈 내용을 official API URL에 Webtoon의 ID를 붙인 url에서 가져옵니다.
해당 Webtoon에 대한 디테일한 정보(title, about, genre, age, thumb)를 가지고 있습니다.
해당 정보를 가지는 모델(클래스)를 models/webtoon_detail_model.dart 파일에 작성합니다.
Official Api url/id 에서 title, about, genre, age, thumb에 대한 정보를 json 형식의 데이터에서
프로젝트에 적용하기 위해서 전에 했던 방식처럼 Uri.parse, jsonDecode를 사용해서 데이터를
가지고 옵니다.
official API URL에 Webtoon의 ID 뒤에 episodes를 붙이면 최근 에피소드의 정보를 받아올 수 있습니다.
정보는 id, title, rating, date(id, 제목, 평점, 날짜)를 가지고 있습니다.
url의 정보가 리스트이기 때문에 리스트를 리턴하는 getLatestEpisodesModel 함수를 작성하였습니다.
앞서 만든 apiurl/id 에서 데이터를 가져오는 함수인 getToonById를 호출합니다.
detail_screen의 화면을 stless에서 stful 위젯으로 바꾸어 줍니다.
생성자에서 다른 클래스의 id를 참조하는 것은 불가능하기 때문에 late와
initState()를 사용하여 다른 클래스의 id를 참조하였습니다.
getLatestEpisodeById 함수 역시 late와 initState()를 사용하여 다른 클래스의 id를 참조한 뒤에
호출하였습니다.
FutureBuilder를 사용해서 webtoon(apiurl/id)의 about 프로퍼티의 데이터를
텍스트로 출력하였습니다.
마찬가지로 장르(genre)프로퍼티의 데이터 값 역시도 텍스트로 출력하고
설명(about)와 장르(genre) 간 여백을 주고 배치하였습니다.
문자열 바인딩(Text interpolation) 이란 ${Text}의 형식으로 여러 개의 문자열을
이어서 출력할 수 있게 도와줍니다.
문자열 바인딩을 사용해서 장르 옆에 나이를 출력하였습니다.
에피소드의 Future 타입은 리스트가 반환되는 것을 알 수 있습니다.
리스트의 크기를 모르고 최적화가 중요 리스트라면 ListView나 ListView.builder를 사용하지만
리스트의 크기를 10개로 알고 있고 그 크기가 크지 않기에 Column을 사용합니다.
에피소드를 버튼처럼 표현하기 위해 Container와 Row를 사용하였습니다.
에피소드 버튼에 색상과 폰트 색상, 여백, 요소들간 배치 등을 추가하여 ui를 꾸몄습니다.
이 전에 전체 위젯을 ListView로 감쌌는 데 SingleChildScrollView가 문제 해결에 더 좋을 것 같아서
수정하였습니다.
에피소드 버튼을 클릭했을 때 해당하는 회차의 웹툰 웹페이지로 이동시키기 위해서는
flutter의 url_launcher 라이브러리를 사용해야 합니다.
flutter pub add url_launcher 명령어를 사용해서 패키지를 설치한 후에
안드로이드라면 다음 코드를 android/main/AndroidManifest.xml 파일에 추가해줍니다.(최상단 root의 바로아래 child)
scheme에는 sms나 tel 등이 올 수 있지만 https url을 사용하기 때문에 https를 넣어주었습니다.
에피소드 버튼을 생성하는 부분을 Refactoring 합니다.
안드로이드 스튜디오의 Extract Flutter Widget(플러터 위젯으로 추출하기) 기능을 사용하여 이름이 Episode인
Episode widget을 생성합니다.
에피소드 버튼을 누르면 해당하는 웹툰 회차의 네이버 웹툰 사이트로 이동하게끔하는 기능을 추가해야합니다.
웹툰 사이트 url은 https://comic.naver.com/webtoon/detail?titleId=${webtoonId}&no=${episode.id}
즉 titleId에 웹툰의 id값이 no에 에피소드의 id값이 필요합니다.
epsiode의 Id 값은 webtoonEpisodeModel의 생성자에서 id를 얻어올 수 있고
webtoon의 Id 값은 detail_screen.dart파일에서 생성자로 넘겨주어야 합니다.
이렇게 받아온 webtoonId와 episodeId를 문자열 바인딩을 사용해서 url을 완성시켜줍니다.
각 에피소드 버튼에 GestureDetector를 생성하고 클릭하면 해당하는 웹툰 회차의 네이버 웹툰으로
이동하는 기능을 구현하였습니다.
하트를 눌렀을 때 하트가 채워지면서 좋아요를 뜻하는 기능을 구현하고자 하였습니다.
먼저 detail_screen의 AppBar의 actions 프로퍼티에 onTap 함수와 빈 하트 아이콘을 추가하였습니다.
핸드폰 저장소에 좋아요 버튼에 대한 정보를 저장할 필요가 있습니다.
저번에는 eqlite를 사용하여 개인 데이터베이스에 접근하였지만 이번에는 shared_preferences 라이브러리를 설치하여 사용하였습니다.
flutter pub add shared_preferences 명령어로 shared_preferences 라이브러리를 설치하였습니다.
shared_preferences의 개발문서에 따르면 getInstance() 한 줄 만으로 저장소와 연결하여
저장소에 다양한 타입의 데이터를 ('key', value)의 형태로 write 저장할 수 있다고 합니다.
사용자의 저장소와 프로젝트를 연결하기 위해서 getInstance() 메서드를 initPrefs() 메서드에서
비동기적으로 호출하였습니다.
likedToons는 웹툰 id 리스트를 읽어서 likedToons에 저장하고 (getStringList)
웹툰 id 리스트의 데이터가 없다면 빈 리스트를 likedToons에 저장합니다.
웹툰 id 리스트에 데이터가 있고 웹툰 id를 포함한다면 (contains) isLiked 좋아요 버튼의 상태를 true로 변경합니다.
버튼 메서드 onHeartTap를 생성하고 좋아요 버튼이 이미 true 눌러져있다면
리스트에서 해당하는 웹툰id를 제거하고 false 눌러져있지않다면 리스트에 해당하는 웹툰 id를 추가하는
로직을 추가하였습니다. 리스트를 로직에 의해 생성된 리스트로 새로 초기화해줍니다.
버튼에 생성한 onHeartTap 함수를 호출하도록하고 좋아요 버튼이 잘 작동하나 테스트하였습니다.
좋아요 버튼을 맨 처음 눌렀을 때는 빈 하트가 채워진 하트로 변경되고
채워진 하트를 눌렀을 때는 빈 하트로 잘 변경되는 것을 알 수 있었습니다.
또한 이러한 bool, List 값은 사용자의 디바이스 데이터베이스에 저장되므로
rebuilding했을 때도 데이터가 사라지지 않고 똑같이 작동하는 것을 확인하였습니다.