프로젝트명 | Todo 리스트 |
기능 |
|
핵심구성요소 |
|
라이브러리 설정 |
|
|
준비하기
Anko라이브러리 추가
참조 : https://abstractask.tistory.com/21
벡터 드로어블 하위 호환 설정
안드로이드 5.0 미만의 기기에서도 벡터 이미지가 잘 표시되도록 설정한다.
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
Basic Activity 작성
책의 예제에서는 Use a Fragment 여부가 있어 프래그먼트생성을 생략할 수 있게 되어 있지만 현재(4.0버전) 에서는 해당 옵션이 없고 기본적으로 프래그먼트를 생성하도록 되어 있다.
스텝1 레이아웃 작성
Basic Activity분석
- activity_main.xml, content_main.xml, fragment_first.xml, fragment_second.xml 총 네개의 파일이 생성된다.
- activity_main.xml은 다음과 같이 구성되어 있다.
- 액션바(AppbarLayout, toolbar)
- content_main.xml (include 되어 있음)
- 플로팅 액션 버튼 (fab)
첫 번째 화면의 레이아웃 작성
우선 예제와 맞추기 위해 content_main.xml 파일에 다음 컴포넌트를 삭제한다.
content_main.xml 에서 Autoconnect모드 Lagacy 카테고리 -> ListView를 중앙에 배치한다.
ID | listView |
layout_width | match_constraint |
layout_height | match_constraint |
두 번째 액티비티 추가
- File -> New -> Activity -> Empty Activity를 클릭
- Activity Name 을 EditActivity로 입력하고 저장한다.
- 생성된 activit_edit.xml의 레이아웃을 수정한다.
달력 표시용 CalendarView 배치
ID | calendarView |
layout_width | wrap_content |
layout_height | wrap_content |
위,오른쪽,왼쪽 여백 | 0 |
할일을 입력하는 EditText 추가
ID | todoEditText |
layout_width | match_constraint |
layout_height | wrap_content |
위,오른쪽,왼쪽 여백 | 8 |
inputType | text |
hint | 할 일 |
text | (공백) |
필요한 이미지 리소스 추가
- File -> New -> Vector Asset을 클릭 하여 다음 아이콘들을 추가한다.
- 팝업이 뜨면 Clip Art아이콘 클릭 한다.
- done, delete, add 이미지 리소스를 추가한다.
완료 버튼 추가
배치 | Button 카테고리 FloatingActionButton 클릭 후 레이아웃 오른쪽 하단에 배치 |
scrCompat | @drawable/ic_baseline_done_24 |
ID | doneFab |
layout_width | wrap_content |
layout_height | wrap_content |
아래,오른쪽 여백 | 16 |
backgroundTint | @android:color/holo_orange_light |
hint | @android:color/white |
삭제 버튼 추가
배치 | Button 카테고리 FloatingActionButton 클릭 후 레이아웃 왼쪽 하단에 배치 |
scrCompat | @drawable/ic_baseline_delete_24 |
ID | deleteFab |
layout_width | wrap_content |
layout_height | wrap_content |
아래,왼쪽 여백 | 16 |
backgroundTint | @android:color/holo_red_dark |
hint | @android:color/white |
activity_edit.xml의 배치된 결과
activity_main.xml 에서 추가 버튼의 변경
처음 Basic Activity로 선택 후 프로젝트를 작성하면 activity_main.xml 에는 이미 ID가 fab인 FloatingActionButton이 오른쪽 하단에 이미 배치가 되어 있다. 이것을 재사용하도록 한다. 아래 이미지와 같이 fab 버튼이 변경 되었다.
ID | fab |
scrCompat | @drawable/ic_baseline_add_24 |
backgroundTint | @android:color/holo_red_dark |
hint | @android:color/white |
스텝2 Realm 데이터 베이스
안드로이드에서는 SQLite를 지원하지만 좀 더 개발하기 쉬운 Realm을 사용한다. Realm은 SQL문법을 전혀 몰라도 사용가능하다.
Realm 데이터 베이스 준비 및 사용법
./build.gradle 파일 플러그인 추가
buildscript {
....
dependencies {
....
classpath 'io.realm:realm-gradle-plugin:5.2.0'
}
}
./app/build.gradle 파일 플러그인 추가
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
// 순서도 중요하다.
apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'
android {
....
}
dependencies {
....
}
Realm 초기화
File -> New -> Kotlin/Class 클릭 MyApplication 이름으로 클래스 생성 하고 다음 과 같이 작성
package abstractask.example.todolist
import android.app.Application
import io.realm.Realm
class MyApplication: Application() {
override fun onCreate() {
super.onCreate()
// Realm 초기화
Realm.init(this)
}
}
매니페스트 파일에서 다음을 추가 수정
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="abstractask.example.todolist">
<application
....
android:name=".MyApplication"
>
....
</application>
</manifest>
다음 데이터 베이스 설계에 맞는 모델 클래스 생성
데이터베이스명 | todolist |
테이블명 | todo |
열 |
|
package abstractask.example.todolist
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class Todo ( // 클래스 명이 테이블명이 됨
@PrimaryKey var id: Long = 0
,var title: String = ""
,var date: Long = 0
): RealmObject()
Realm 사용
MainActivity에서 EditActivity 로 이동하기 위해 다음과 같이 코드 추가
package abstractask.example.todolist
import android.os.Bundle
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import androidx.appcompat.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.startActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
// 새 할일 추가
fab.setOnClickListener{
startActivity<EditActivity>()
}
/*
findViewById<FloatingActionButton>(R.id.fab).setOnClickListener { view ->
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show()
}
*/
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
}
}
EditActivity 에서 사용
package abstractask.example.todolist
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where
import kotlinx.android.synthetic.main.activity_edit.*
import org.jetbrains.anko.alert
import org.jetbrains.anko.calendarView
import org.jetbrains.anko.yesButton
import java.util.*
class EditActivity : AppCompatActivity() {
// 1 realm 인스턴스 얻기
val realm = Realm.getDefaultInstance()
val calendar: Calendar = Calendar.getInstance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_edit)
/*
getLongExtra(name: String, defaultValue: Long)
*/
// 업데이트 조건
val id = intent.getLongExtra("id", -1L)
if(id == -1L){
insertMode()
}else {
updateMode(id)
}
// 캘린터 뷰의 날짜를 선택했을 때 Calendar 객체에 설정
calendarView.setOnDateChangeListener{ view, year, month, dayOfMonth ->
calendar.set(Calendar.YEAR, year)
calendar.set(Calendar.MONTH, month)
calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth)
}
}
// 추가 모드 초기화
private fun insertMode(){
/*
VISIBLE : 보이게 함
INVISIBLE : 영역은 차지하지만 보이지 않음
GONE : 보이지 않음
*/
// 삭제 버튼을 감추기
deleteFab.visibility = View.GONE
// 완료 버튼을 클릭하면 추가
doneFab.setOnClickListener{
insertTodo()
}
}
// 수정 모드 초기화
private fun updateMode(id: Long){
// id에 해당하는 객체를 화면에 표시
val todo = realm.where<Todo>().equalTo("id",id).findFirst()!!
todoEditText.setText(todo.title)
calendarView.date = todo.date
doneFab.setOnClickListener{
updateTodo(id)
}
// 삭제 버튼을 클릭하면 삭제
deleteFab.setOnClickListener{
deleteTodo(id)
}
}
private fun insertTodo(){
// 트랜잭션 시작
realm.beginTransaction()
// 새 객체 셍성
/*
createObject<T: RealmModel>(primaryKeyValue: Any?)
- primaryKeyValue: 기본키를 지정한다.
*/
val newItem = realm.createObject<Todo>(nextId())
// 값 설정
newItem.title = todoEditText.text.toString()
newItem.date = calendar.timeInMillis
// 트랜잭션 종료
realm.commitTransaction()
// 다이얼로그 표시 : anko 라이브러리 추가가 되어 있어야 사용이 가능하다.
alert("내용이 변경되었습니다."){
yesButton {
// 다이얼 로그 확인 버튼을 누르면 현재 액티비티를 종료한다.
finish()
}
}.show()
}
private fun nextId(): Int {
/*
현재 열명의 가장 큰 값을 리턴
max(fieldName: String)
- fieldName : 찾고자 하는 열명
*/
val maxId = realm.where<Todo>().max("id")
return if(maxId != null) maxId.toInt()+1 else 0
}
private fun updateTodo(id: Long) {
realm.beginTransaction()
val updateItem = realm.where<Todo>().equalTo("id", id).findFirst()!!
// 값 수정
updateItem.title = todoEditText.text.toString()
updateItem.date = calendar.timeInMillis
realm.commitTransaction()
alert("내용이 변경되었습니다."){
yesButton {
finish()
}
}.show()
}
private fun deleteTodo(id: Long){
realm.beginTransaction()
val deleteItem = realm.where<Todo>().equalTo("id", id).findFirst()!!
// 삭제할 객체
deleteItem.deleteFromRealm()
realm.commitTransaction()
alert("내용이 삭되었습니다."){
yesButton {
finish()
}
}.show()
}
override fun onDestroy() {
super.onDestroy()
// 2 인스턴스 해제
realm.close()
}
}
스텝3 리스트 뷰와 데이터 베이스 연동
어댑터 작성
- ListView는 실제 데이터와 연동을 하려면 어댑터를 작성해야 한다.
- 어댑터는 데이터를 ListView에 어떻게 표시할지를 정의하는 객체이다.
- 기본적으로 BaseAdapter를 상속받아서 작성하지만 Realm은 아래 의존성을 추가하고 RealmBaseAdapter클래스를 상속받아 구현하게 된다.
dependencies {
....
// adapter 의존성 추가
implementation 'io.realm:android-adapters:2.1.1'
....
}
RealmBaseAdapter 를 상속 받은 TodoListAdapter 기본 틀을 작성한다.
package abstractask.example.todolist
import android.view.View
import android.view.ViewGroup
import io.realm.OrderedRealmCollection
import io.realm.RealmBaseAdapter
class TodoListAdapter(realmResult: OrderedRealmCollection<Todo>): RealmBaseAdapter<Todo>(realmResult) {
/*
아이템에 표시하는 뷰를 구성한다.
getView(position: Int, convertView: View?, parent: ViewGroup?)
- position: 리스트 뷰의 아이템 위치
- convertView: 재활용되는 아이템의 뷰
- parent: 부모 뷰 즉 여기서는 리스트 뷰의 참조
*/
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
TODO("Not yet implemented")
}
}
/*
뷰 홀더 패턴
- 리스트를 표시할 때 성능을 향상시킬 목적으로 뷰 홀더 패턴을 적용한다.
- getView 메서드가 아이템에 화면에 표시될 때마다 호출되므로 최대한 효율적인 코드를 작성해야 한다.
*/
class ViewHolder(view: View) {
}
아이템 레이아웃 작성
res/layout 디렉터리에서 우클릭 File -> New -> Layout resource file 클릭 res/layout/item_todo.xml 파일을 생성한다.
날짜를 표시할 TextView를 추가한다.
ID | text1 |
layout_width | match_constraint |
위,오른쪽,왼쪽 여백 | 8 |
text | (공백) |
🔧 text (디자인 시 보이는 텍스트) |
2018/06/12 |
위,오른쪽,왼쪽 여백 | 8 |
textAppearance | AppCompat.Body1 |
title을 추가할 TextView 를 추가한다.
ID | text2 |
layout_width | match_constraint |
위 여백 | 0 |
위,오른쪽,왼쪽 여백 | 8 |
text | (공백) |
🔧 text (디자인 시 보이는 텍스트) |
청소하기 |
오른쪽,왼쪽,아래 여백 | 8 |
textAppearance | AppCompat.Body2 |
두개의 TextView를 포함하는 ConstraintLayout의 속성을 다음과 같이 수정한다.
layout_height | wrap_content |
최종적인 item_todo.xml의 레이아웃은 다음과 같다.
뷰 홀더 패턴을 이용하여 다음과 같이 TodoListAdapter 에 코드를 추가한다.
package abstractask.example.todolist
import android.text.format.DateFormat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import io.realm.OrderedRealmCollection
import io.realm.RealmBaseAdapter
class TodoListAdapter(realmResult: OrderedRealmCollection<Todo>)
: RealmBaseAdapter<Todo>(realmResult) {
/*
아이템에 표시하는 뷰를 구성한다.
getView(position: Int, convertView: View?, parent: ViewGroup?)
- position: 리스트 뷰의 아이템 위치
- convertView: 재활용되는 아이템의 뷰
- parent: 부모 뷰 즉 여기서는 리스트 뷰의 참조
*/
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val vh: ViewHolder
val view: View
if(convertView == null) {
/*
xml 레이아웃 파일을 읽어서 뷰로 변환한다.
inflate(resource: Int, root: ViewGroup, attachToRoot: Boolean)
- resource: 불러올 레이아웃 XML 리소스ID를 지정
- root: 불러온 레이아웃 파일이 붙을 뷰그룹인 parent를 지정
- attachToRoot: XML파일을 불러올 때는 false지정
*/
view = LayoutInflater
.from(parent?.context)
.inflate(R.layout.item_todo,parent,false)
vh = ViewHolder(view)
view.tag = vh;
} else {
view = convertView
vh = view.tag as ViewHolder
}
if(adapterData != null){
val item = adapterData!![position]
vh.textTextView.text = item.title
vh.dateTextView.text = DateFormat.format("yyyy/mm/dd",item.date)
}
return view
}
// 리스트 뷰를 클릭하여 이벤트를 처리할 때 인자로 position, id등이 넘어오게 되는데
// 이때 넘어오는 id값을 결정한다.
// 데이터 베이스를 다룰 때마다 고유한 아이디를 가지고 있는데, 그것을 반환하도록 결정한다.
override fun getItemId(position: Int): Long {
if(adapterData != null ) {
return adapterData!![position].id
}
return super.getItemId(position)
}
}
/*
뷰 홀더 패턴
- 리스트를 표시할 때 성능을 향상시킬 목적으로 뷰 홀더 패턴을 적용한다.
- getView 메서드가 아이템에 화면에 표시될 때마다 호출되므로 최대한 효율적인 코드를 작성해야 한다.
*/
class ViewHolder(view: View) {
val dateTextView: TextView = view.findViewById(R.id.text1)
val textTextView: TextView = view.findViewById(R.id.text2)
}
할일 목록 표시
package abstractask.example.todolist
import android.os.Bundle
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import androidx.appcompat.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
import io.realm.Realm
import io.realm.Sort
import io.realm.kotlin.where
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.content_main.*
import org.jetbrains.anko.startActivity
class MainActivity : AppCompatActivity() {
val realm = Realm.getDefaultInstance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
// 전체 할 일 정보를 가져와서 날짜 순으로 내림차순 정렬
val realmResult = realm.where<Todo>()
.findAll()
.sort("date", Sort.DESCENDING)
val adapter = TodoListAdapter(realmResult)
listView.adapter = adapter
// 데이터가 변경되면 어뎁터에 적용
// 어뎁터에 notifyDataSetChange() 메서드를 호출하면
// 데이터 변경을 통지하여 리스트를 다시 표시하게 된다.
realmResult.addChangeListener { _ -> adapter.notifyDataSetChanged() }
listView.setOnItemClickListener{ parent, view, position, id ->
//할 일 수정
startActivity<EditActivity>("id" to id)
}
// 새 할일 추가
fab.setOnClickListener{
startActivity<EditActivity>()
}
/*
findViewById<FloatingActionButton>(R.id.fab).setOnClickListener { view ->
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show()
}
*/
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
}
override fun onDestroy() {
super.onDestroy()
realm.close()
}
}
빌드 시 오류발생처리
2020-06-26 11:54:27.436 11989-11989/abstractask.example.todolist D/AndroidRuntime: Shutting down VM
2020-06-26 11:54:27.437 11989-11989/abstractask.example.todolist E/AndroidRuntime: FATAL EXCEPTION: main
Process: abstractask.example.todolist, PID: 11989
java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/appcompat/R$drawable;
at androidx.appcompat.widget.AppCompatDrawableManager$1.<init>(AppCompatDrawableManager.java:63)
확인 해본 결과 android studio 4.0으로 올린 후 버젼이 변경되어서 그런듯 하다.
다음으로 버젼 변경 후 재 빌드
buildscript {
....
dependencies {
....
//classpath 'io.realm:realm-gradle-plugin:5.2.0'
classpath 'io.realm:realm-gradle-plugin:6.0.2'
}
}
결과
'책 > 오준석의 안드로이드 생존코딩 코틀린편' 카테고리의 다른 글
15. 별도의 이야기(앱아이콘준비, 자동완성다른함수인자명) (0) | 2020.06.26 |
---|---|
12. 실로폰 (0) | 2020.06.24 |
11. 손전등 (0) | 2020.06.23 |
10. 지도와 GPS (0) | 2020.06.21 |
8. 수평 측정기 (0) | 2020.06.16 |