[Projects] 아이모 잡학사전with알람 1.6.2 업데이트
UI구성 변경
1.6.2 업데이트에서는 화면의 전체적인 UI구성을 변경했다. 기존의 연두색배경의 우디위디 마을숲의 컨셉은 90년대 도스시절을 방자하는 UI구성같다며 질책아닌 질책의 피드백을 받았고, 여타 다른 유명한 어플들처럼 하얀색 배경화면으로 변경하면서 메뉴이름과 연관된 아이콘으로 버튼아이콘역시 변경했다.
화면이 좀더 깔끔해졋고, 중앙 메뉴구성의 카드뷰들의 명암을 좀더 굵게 잡아줫다. (기존 1dp -> 2dp) 그리고 iconfinder에서 검색하여 무료아이콘으로 메뉴이름과 연관성있게 변경했다.
알람화면은 기존에 스피너2개를 통해 보스들을 세부분류하고, 선택된보스에 한해서 버튼을클릭함으로써 타이머를 등록시키는 방식이 매우 사용자편의성에서 불편하다고 피드백을 받았다.
스피너의 항목내에 들어오는 보스의양이 너무많아 찾기가힘들고, 매번 찾아서 선택하는일이 귀찮다는것이다. 또한, 알람의 노티피케이션에 액션을 구현해서 알람어플로 들어오지않고도 바로 타이머를 등록할수 있으면 좋겟다는 피드백역시 받았고, 이부분을 추가 구성했다.
먼저, 알람화면의 리사이클러뷰 내에 사용자가 직접 자주쓰는 보스들을 추가하여 버튼들로써 생성하도록하고, 버튼을누르면 각 버튼의 TextColor를 Color.RED 색상으로 변경한뒤, 선택된 보스를 타이머동작 버튼을 누름으로써 타이머가 등록되도록 구성했다.
또한, 보스들을 한번에 여러개 선택을 방지해야 하므로 이미 선택하여 Color.RED색상으로 변경된 버튼이 있으면 더이상 선택할수 없도록 구성했다. 그리고 Color.RED색상으로 변경된 선택된 보스 버튼을 한번더 누르면 Color.BLACK으로 변경하고 다른버튼들을 다시선택할수 있도록 구현했다.
이 자주쓰는리스트를 구성하는 리사이클러뷰의 아이템뷰로는 버튼하나와 체크박스하나를 두었는데, 각각의 보스들의 타이머가 등록이되면, 체크박스의 체크동작을 통해 화면상단의 Overlay로 체크된 보스들의 젠타임을 볼수있도록 구성했다. 기존에는 버튼을 누름으로써 동작하게 했는데, 지나치게 많은 버튼들로 90년대 도스시절의 UI구성이라는 질타를받고 좀더 깔끔한 방식으로 구성해보려고 노력했다.
따라서 Overlay 를 키고 끄는것 역시 화면상단의 옵션메뉴로 구성하였고, 아이콘역시 iconfinder에서 검색한 아이콘으로 변경하였다. 그외의 옵션메뉴에는 총3가지로, 하나는 Overlay의 ON/OFF 메뉴, 두번째는 보스타이머 커스텀메뉴, 세번째는 로그인/로그아웃 메뉴이다.
서버기능
서버기능은 Firebase를 사용하여 Realtime Database를 이용하여 데이터를 입출력하고, 데이터베이스상의 내용이 변경되면 Client의 Service에 구성된 Firebase의 addValueEventListener()를 통해 백그라운드상에서 변경의 내용이 감지되면 타이머를 등록하도록 구성했다.
즉, 클라이언트의 서비스가 백그라운드상에서 돌아가면서 변경의내용이 감지되면 콜백리스너로 내부코드를 수행해서 타이머를 등록하는 방식인데, 이거말고는 내가직접 서버를 구현하고 firebase를연동해서 하는방식도 생각해봣지만.. 당장 업데이트로 실현하기에는 무리라고 판단되서 보류했다.
물론, 이후 업데이트에는 반영해보는거도 나름 재밌을거 같았다. 기능구현을 할때 몇가지 문제점들이 있었는데, firebase의 addValueEventListener() 말고도 addChildEventListener()로 처음에 구성했다가 똑바로된 데이터가 들어오지않앗었다. addChildEventListener()의 경우 firebase의 Realtime Database의 규칙상 key-value형태로 데이터가 저장되는데 자식노드자체는 각각의 key에 대한 value값을 참조하는 모양이다. 즉 내가원한 시나리오는 보스몬스터리스트의 내용이 변경되면 보스리스트내용만을 가지고 타이머를 등록하고 싶었는데, 각방의 보스리스트 말고도 방id,방pw,방권한코드 까지도 value값으로 연달아서 가져오는 거였다. 즉, 데이터를 입력할때 원하는경로에 그냥 setValue()를하게되면 기존의 데이터위에 엎어쓰기가 되는데 이것자체를 자식노드가 추가되는 방식으로 캐치해서 따라오는 모든데이터들에 대해 value값을 하나하나 참조하는 거다. 그래서 특정된 보스몬스터리스트 의 value값만 참조할수가 없었다.
addValueEventListener()를 사용하게되면 값이 변경되었을때, key값아래에 전체값을 value값으로 받아서 한번에 들어오기때문에, 그내용에서 보스몬스터리스트 부분만 가지고 타이머를 등록하는 코드를 완성시킬수 있었다.
타이머를 등록할때는 보스이름만가지고 앱내부 클라이언트상에 저장된 해당하는 보스의 젠타임값 등을 가져와서 서버에서 가져온 젠타임을 계산한뒤 클라이언트의 노티피케이션과 AlarmManager를 통해 타이머를 등록하였다. 여기서 중요햇던부분은 내부데이터베이스에서 해당보스의 정보를 가져올때 withContext(){} 내부에 데이터베이스에서 내용을 가져오는 메소드를 구성하고 이것을 변수에 할당해서 사용하는 방식임이 중요했다. val monster= withContext(Dispatchers.IO) { repository.getMonsInfo(value.name) } 와같이 구성하여야했다.
withcontext()는 내부로직이 모두수행되고 값이 리턴되기까지 suspend(중지) 하는 로직으로 수행되기 때문에, 이렇게하지않고 launch나 async로 하게되면 monster값이 엉뚱한값이거나 null값일수 있다. launch와 async의 경우 또다른 비동기로직으로 동시에 수행되기때문에 여기서 monster값을 내부저장소에서 받고나서 monster값으로 아래문장이 수행되어야 하면 반드시 withcontext()로 구성하여야만 한다.
그외에 데이터베이스로 구성할필요없이 별다른 key-value 형태로 저장해도 문제없을 작은정보값들에 대해서는 sharedPreference를 사용하도록 구성했다.(아이디,비밀번호,권한코드 등등)
사실 서버기능을 구성시킬때 선택지가없어서 이런방법으로 구현한것이지, 좋은방법으로 구성했다는 생각은 들지않는다. 이게 정답인지 잘모르겠다.
서버상에는 데이터베이스만 있고 기능이라는것이 존재하면 안되는것인지? 만약그렇다면 내가한방식이 옳지만, 그런게아니라면 서버상에서 동작된 기능에따라 클라이언트에 푸시알람이라던지 다른어떤 방법으로 줄수있을거 같았다.
아무튼, 이런방식으로 진행하다보니 데이터베이스상의 각각의 방에 따라 저장된 보스몬스터리스트에 옛날에 등록한 보스들이 현재값에 들어오는 문제가 있었고, 해결하기위해서 가져온 보스몬스터리스트의 젠타임들을 가지고 현재시간을 빼서 계산한값이 0보다 작다면 타이머에 등록하지 않도록 구성했다.
보스몬스터리스트 값을 서버 데이터베이스에 저장할때 서버로 저장하는 클라이언트상에 존재하는 모든 타이머들을 저장시키기 때문에, 클라이언트상에 없는 데이터라면 당연히 서버의데이터도 엎어쓰기가 되면서 초기화 될것이며, 한번등록시키고 일주일간 사용하지않는다면 문제가되겟지만 그럴일이 없다면 사용하면서 데이터가 계속변경되기 때문에 다른문제는 발생하지 않을것같다.
또한 서버상에 아무런 보스몬스터리스트값이 없다면, 즉 방생성을 한 직후일때는 null값이기 때문에 가져왓을때 nullpointerException이 발생하므로, 이부분을 예외처리하여 예외발생시 아무런등록된 보스몬스터가 없음을 토스트메세지로 띄우고 아무런동작을 하지않도록 구성하였다.
예외처리시 try-catch문이 예외가발생하는 문장에 직접적으로 사용하지않으면 예외가 검출되지 않음도 인지하자! 클릭리스너 구현체에 예외발생시 클릭리스너 안에서 try-catch하여야지 클릭리스너 전체를 try-catch에 넣으면 예외가 처리되지 않는다.
보스타이머 커스텀기능
기존의 세부보스로 분류하는 2개의 스피너를 통해 등록버튼을 구성하여 자주쓰는리스트 인 리사이클러뷰에 추가하도록 구성했고, 리사이클러뷰의 클릭리스너를통해 삭제가 가능하도록 구성했다. 내용의 저장은 sharedPreference를 사용해서 데이터를 저장하였다.
또한, 타이머의 간격이 기존에는 5분전과 정시알람으로 구성되었었는데, 이부분을 커스텀할수 있으면 좋겟다는 피드백이 있엇고 커스텀메뉴에 2개의 NumberPicker와 버튼두개로 첫번째알람간격과 두번째알람간격을 설정할수 있도록 구성하였다. 이것역시 sharedPreference에 저장하고 받아올수있도록 하였다.
보스타이머의 커스텀을 가능하도록 리사이클러뷰에 버튼과 체크박스를 아이템뷰로 구성시켯더니, 아이템뷰의 개수가 많아지면서 오류가발생했다.
리사이클러뷰의 경우 내부아이템뷰를 만들어진개수 전체를 메모리에 생성해두는게 아니라, 개수만정해놓고 메모리상에는 일정개수만큼만 생성하고 스크롤링을 통해 위아래 부분의 아이템뷰를 스크롤링 한만큼 메모리에 올리고 삭제하는 방법으로 onBind()가 구동된다.
이부분때문에 맨윗부분의 체크박스에서 체크한 아이템 혹은 선택하여 TextColor가 Color.RED가 되어있는 상태에서 아래로 스크롤링했다가 올라가면 엉뚱한 아이템뷰가 체크되거나 색깔이변하는 문제가 발생하게 된다. 그이유는 메모리에서 제거했다가 다시생성하면서 발생하는 문제였다.
이를해결하는 방법들중 하나가 체크된상태 와 선택된상태(글자색이 변경된상태)를 어답터클래스 내부에 items와함께 혹은 또다른 변수에 저장해두고 onBind()가 될때 마다 저장된 상태값으로 변경시키는 방법을 사용했다.
즉, 어답터코드에 list변수를 두고, 상태값들을 저장한뒤 onbind()로직내부의 각각의 뷰홀더에 따라 bind()메소드 내부에 list의 개수를 bind된 개수만큼 증가시키고 (어답터의 아이템뷰내용이 setItems()를통해 정해질때 공간만큼 false를 할당하는방법이있고, 스크롤링에따라 bind될때마다 상태값의 정보가없으면 추가해나가는 방법이있다), 상태값을 list에 저장한뒤, 상태값에 따라 아이템뷰의 내용을 변경시키는 로직이다.
위와같이 구성하게되면, 상태값들에 따라 bind될때마다 상태값을 변경시키므로 사용자가 선택하고 스크롤링하여도 오류가 발생하지 않게된다.
해결하지못한부분
구글콘솔에서 지원해주는 ANR의 발생빈도와 오류코드가 여태 방치해왔었던 부분이 android.view.WindowManager$BadTokenException 예외오류가 계속에서 발생되고있는데, 원인을 모색중이다. 이부분역시 구글링을통해 원인을찾고 해결할 필요가있다. 아직까지 ANR로 발생되어 앱이강제중지 되는게 아니라 백그라운드(서비스)가중지되는 것같다.
또한, 앱의 기기 화면비율이 다달라서 어떤기기는 짤리는현상이있고, 어떤기기는 아래하단이 너무 비어보이기도하는 이슈가있다. 이부분은 구글링을 해본결과 각각의 화면비율마다 xml을 정의해주는 방법을 해야한다고 봣었는데 다른방법이있는지 확인할 필요가 있다.
모든소스코드는 깃허브주소 에있다.
댓글남기기