앞선 포스팅에서는 Internal/External 저장소에 사용자의 데이터를 저장하는 방법에 대해 살펴 보았습니다.
오늘은 데이터베이스를 활용하여 원하는 데이터를 저장하는 방법에 대해 알아보겠습니다.
Intenrnal/External 저장소에 저장하는 것 보다 데이터베이스를 이용하여 저장할 때는, 동일한 형태의 데이터를 저장할 수 있다는 점에서 더 편리합니다.
회원정보를 저장할 경우가 대표적인 예인데, 이 경우 회원의 이름, 성별, 전화번호, 주소 등등이 각각 DB의 속성(Attribute) 가 될 것이고,
실제 들어가는 데이터 (김태희, 여, 010-0000-0000, 경기도.. ) 들이 값(Value) 이 될 것입니다.
1. SQLiteOpenHelper 를 이용한 DB 생성
SQLiteOpenHelper Class 는 Database 를 생성하고 해당 Database 의 Version 을 관리합니다.
해당 Class 를 사용하기 위해서 Database가 처음 생성될 때 불리는 onCreate(..) 와 Database 가 Upgrade 될 경우에 불리는 onUpgrade(..) callback method 를 구현해 주어야 하며, 필요에 따라 onOpen(..) 함수와 onDowngrade(..) 함수도 구현하여 사용할 수 있습니다.
저는 SQLiteOpenHelper를 상속 받은 MySQLiteOpenHelper 를 생성해서 간단하게 onCreate/onUpgrade 만 구현해 주었습니다.
public class MySQLiteOpenHelper extends SQLiteOpenHelper {
private final String TAG = "MySQLiteOpenHelper";
public MySQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
String sql = "create table student (_id integer primary key autoincrement, name text, age integer, address text)";
sqLiteDatabase.execSQL(sql);
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
String sql="drop table if exists student";
sqLiteDatabase.execSQL(sql);
onCreate(sqLiteDatabase);
}
}
onCreate 에서는 Database를 생성해 주었고, 각 Field 는 name, age, address 로 구성될 수 있도록 하였습니다.
"create table student (_id integer primary key autoincrement, name text, age integer, address text)";
위 Query 로 DB생성 후 실제 만들어진 DB 를 열어보면 다음과 같이 data 가 저장됩니다.
그리고 onUpgrade 함수에는 현재 생성되어있는 Database 를 지우고 onCreate 를 통해 다시 생성될 수 있도록만 구현을 해 두었습니다.
2. DB에 데이터 삽입 및 삭제
DB에 데이터 삽입/삭제를 위해 MyDBHandler 클래스를 생성하였습니다.
MyDBHandler 클래스에서는 DBActivity 로 부터 사용자 정보를 받아 MySQLiteOpenHelper 로 전달 합니다.
데이터를 저장 할 때 ContentValues 를 이용했는데, ContentValues 는 Data 를 Key와 Value의 Set 으로 저장할 수 있습니다.
그래서 Database 에 값을 줄때 "name, 홍길동" ,"age, 24" 등과 같이 Key 와 Value 의 형태로 쉽게 전달 가능합니다.
public class MyDBHandler {
private final String TAG = "MyDBHandler";
SQLiteOpenHelper mHelper = null;
SQLiteDatabase mDB = null;
public MyDBHandler(Context context, String name) {
mHelper = new MySQLiteOpenHelper(context, name, null, 1);
}
public static MyDBHandler open(Context context, String name) {
return new MyDBHandler(context, name);
}
public Cursor select()
{
mDB = mHelper.getReadableDatabase();
Cursor c = mDB.query("student", null, null, null, null, null, null);
return c;
}
public void insert(String name, int age, String address) {
Log.d(TAG, "insert");
mDB = mHelper.getWritableDatabase();
ContentValues value = new ContentValues();
value.put("name", name);
value.put("age", age);
value.put("address", address);
mDB.insert("student", null, value);
}
public void delete(String name)
{
Log.d(TAG, "delete");
mDB = mHelper.getWritableDatabase();
mDB.delete("student", "name=?", new String[]{name});
}
public void close() {
mHelper.close();
}
}
select 함수는 Database 의 전체 내용을 return 해주고, insert/delete 함수는 각각 Database에 데이터를 추가/삭제 시 호출 됩니다.
delete 의 경우 name 을 인자로 받아서 이름이 동일 하면 해당 row 를 모두 삭제하도록 하였습니다.
3. 동작 확인
기존에 작성하였던 MainActivity 에 Database 버튼을 하나 추가한다음에, 이 버튼을 눌렀을 때 Activity 를 전환하여 Database 의 내용을 뿌려주도록 하였습니다.
가장 상단에는 Database 에 내용을 추가할 수 있도록 Editbox 를 3개 두었고, 각각 이름/나이/주소 정보를 저장할 수 있도록 하였습니다.
Update Database 를 누르게 되면 Editbox 에 추가한 내용이 Database 에 업데이트 되고 리스트의 내용이 업데이트 됩니다.
리스트의 각 항목을 롱클릭 하게 되면 해당 Field 가 삭제 됩니다. ^^
여기까지 Database 를 이용한 아주 간단한 사용자 데이터 저장에 대해서 살펴보았습니다.
모든 소스는 https://github.com/bettercho/MyGithub/tree/master/storeuserdata 를 참고하시면 됩니다.
아래와 같이 간단하게 완성이 되었습니다. 각 버튼을 눌렀을 때 Internal/External Storage 에 저장하고, Print 버튼을 눌렀을 때 저장된 내용이 출력되도록 한 예제입니다. 작성된 코드는 https://github.com/bettercho/MyGithub/tree/master/storeuserdata 를 참고하세요.
01-06 13:03:14.034 11277-11277/com.example.codetravel.getmainlooper D/MainActivity: changeButtonText method is called from non-main thread
Looper.myLooper() 함수를 로그로 출력하였을 때 내용은 아래와 같습니다.
Looper (main, tid 1) <== 메인 스레드에서 호출
null <== TestThread에서 호출
이것이 의미하는 것을 알기 위해서 Looper 클래스의 toString()함수를 보도록 하겠습니다. (xref : Looper.java)
Looper (" 스레드 이름", "스레드 ID") 임을 알 수 있습니다.
3. 현재 스레드가 Main thread(UI thread)인지 아닌지 검사하고 싶을 경우
public class MainActivity extends AppCompatActivity {
private String TAG = "MainActivity";
private TestThread mTestThread;
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button)findViewById(R.id.button);
}
public void onClickButton(View view) {
Log.d(TAG,"onClickButton()" + " " + Thread.currentThread() + " " + Looper.getMainLooper().getThread());
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
Log.d(TAG,"onClickButton() : This thread is main thread!");
}
else {
Log.d(TAG,"onClickButton() : This thread is not main thread!");
}
mTestThread = new TestThread();
mTestThread.start();
}
class TestThread extends Thread {
@Override
public void run() {
Log.d(TAG,"TestThread run()" + " " + Thread.currentThread() + " " + Looper.getMainLooper().getThread());
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
Log.d(TAG,"TestThread run() : This thread is main thread!");
}
else {
Log.d(TAG,"TestThread run() : This thread is not main thread!");
}
while(true) { // ps 명령어로 스레드 ID를 보기 위해서 스레드가 종료되지 않도록 하기 위한 코드
// 아무 동작도 안하고 그냥 살아 있는 스레드
}
}
}
}
로그를확인하기전에코드만으로예측해보면 onClickButton() 함수는 UI thread에서호출되기때문에 "This thread is main thread!" 가출력될것이라는것을예측할수있습니다.
그리고 TestThread 스레드가생성되어 run() 함수가동작하는스레드는메인스레드가아닌백그라운드스레드입니다.
이번 포스팅에서는 Kotline 의 기본 제어문(반복문/조건문) 사용법을 알아보도록 하겠습니다.
모든 언어들이 비슷하기 때문에 하나의 언어만 잘 알아두면 나머지 언어의 문법도 금방금방 익힐 수 있는 것 같습니다.
Kotline 의 제어문도 다른 언어들과 비슷 하지만, 함축적으로 사용되는 부분이 있어 다른 언어들과는 다르게 Kotline 의 문법을 모르면 잘 알아보기가 힘들게 되어 있는 부분도 있습니다.
익숙해 지면 다른 언어들보다 편할것이라는 생각도 드네요. ^^
1. If 문
Kotline 에서 if 문은 Expression 입니다. 즉, value 를 return 합니다. if 문 자체로 기존의 3항 연산자의 역할을 대체하기 때문에 더이상 삼항 연산자는 사용되지 않습니다. (조건 ? true : false)
기존 우리는 if 문을 아래와 같이 사용했습니다.
max 에 a 변수를 넣고, 만약 a 보다 b 가 크다면 max 변수에 b 의 값을 넣는 코드입니다.
// Traditional usage varmax=aif (a<b) max=b// With else varmax: Intif (a>b) {
max=a
} else {
max=b
}
Kotline 에서는 위의 if 문을 아래와 같이 간단하게 사용할 수 있습니다.
// As expression valmax=if (a>b) aelseb
즉, " if (a>b) a else b " 구문 자체가 a 혹은 b 의 값을 return 하기 때문에, max 변수에 바로 이 값을 대입할 수 있습니다.
만약, if 문 자체에 블럭이 포함되어야 한다면 위 구문은 아래와 같이 사용할 수 있습니다.
valmax=if (a>b) {
print("Choose a")
a
} else {
print("Choose b")
b
}
위의 경우 블럭의 끝에 쓰여진 값이 return 되는 값이 됩니다.
2. When
When 은 일반 언어에서 사용되던 Switch 문을 대체합니다.
when (x) {
1->print("x == 1")
2->print("x == 2")
else-> { // Note the blockprint("x is neither 1 nor 2")
}
}
When 옆에 쓰여진 x 에 값이, 블럭 안의 조건에 충족이 될 때 까지 모든 인수를 순차적으로 검사 합니다.
아무 조건에도 충족되지 않으면 else 분기문으로 들어가게 되고, Switch 문의 default 문은 필수가 아니지만, When 의 else 문은 필수로 들어가야 합니다.
많은 경우가 동일한 방식으로 처리되어야 하는 경우에는 . (콤마) 를 사용하여 조건을 추가할 수 있습니다.
when (x) {
0, 1->print("x == 0 or x == 1")
else->print("otherwise")
}
각 조건의 경우에는 위와 같이 특정 상수 값 (0, 1) 이 올 수도 있지만, 함수가 올 수도 있습니다.
when (x) {
parseInt(s) ->print("s encodes x")
else->print("s does not encode x")
}
위의 경우에는 x 와 parseInt() 에서 return 되는 값과 일치하게 되면 "s encodes x" 를 출력할 것입니다.
그리고 in 키워드를 사용하여 특정 값의 범위를 지정 할 수도 있습니다.
when (x) {
in1..10->print("x is in the range")
invalidNumbers->print("x is valid")
!in10..20->print("x is outside the range")
else->print("none of the above")
}
in 1..10 은 1<= x <=10 의 범위를 의미 합니다. Range 에 대해서는 'https://kotlinlang.org/docs/reference/ranges.html' 를 참고해 보세요 .
마지막으로 when 을 사용할 때 인수를 생략할 수 있습니다.
인수를 생략하게 되면 분기 조건은 단순히 bool 식이 되고, 해당 조건이 참일 때 분기문이 실행 됩니다.
아래의 경우에서는 x 가 홀수 일 경우 "x is odd", 짝수일 경우 'x is even" 둘다 아닐 경우에 "x is funny" 가 출력 됩니다.
when {
x.isOdd() ->print("x is odd")
x.isEven() ->print("x is even")
else->print("x is funny")
}
3. For Loops
for loop 는 비교적 기존에 사용하던 for 문과 크게 다르지 않습니다. iterator 를 제공하는 모든 것들을 반복할 수 있습니다.
아래 예제에서는 item 에 collection 에 있는 모든 값들이 하나씩 대입이 되면서 반복 하게 됩니다.
for (itemincollection) print(item)
배열이나 리스트를 반복할 경우에 index 를 사용하고 싶다면 indices를 사용 합니다.
for (iinarray.indices) {
print(array[i])
}
혹은 withIndex' fun 을 사용해서 index 와 value 를 return 받아 for문을 돌릴 수도 있습니다.
for ((index, value) inarray.withIndex()) {
println("the element at $index is $value")
}
4. While Loops
while 문과 do.. while 문은 java 와 완전히 동일합니다.
아래의 예제에서 do 블록 안에 정의한 멤버변수 y 는 while 의 조건 식에서 참조할 수 있습니다.
while (x>0) {
x--
}
do {
valy=retrieveData()
} while (y!=null) // y is visible here!
여기까지 Kotline 의 if, when, for, while 제어문 사용법에 대해 간단히 알아보았습니다.
기본 문법이기 때문에 잘 익혀두면 Kotline 으로 코딩하는데 많은 도움이 될 것입니다.
Android 에서 제공하는 Recording API 중, 지난번에 보았던 AudioRecorder 말고 더 편리한 MediaRecorder 가 있습니다.
AudioRecorder 는 오디오만 레코딩 가능 하지만, MediaRecorder 의 경우 Audio 및 Video 컨텐츠의 레코딩이 가능 합니다.
다만 인코딩 된 파일을 받기 때문에 AudioRecorder 처럼 PCM Data 를 바로 받아올 수는 없는 단점이 있습니다.
본인이 구현하려는 레코더의 사용 용도에 맞게 AudioRecorder/MediaRecorder 를 선택하여 사용하시면 되겠습니다.
Android Developer 의 MediaRecorder API Guide 를 참고하면, MediaRecorder 는 아래와 같은 state machine 을 갖습니다.
따라서 MediaRecorder API 를 사용할 때는 아래의 State 를 잘 따라서 코딩해야 합니다. 예를들어 Initail 상태에서는 바로 Prepared 상태로 갈 수 없고, Initailized 와 DataConfigured 상태를 거쳐야지 Prepared 상태가 될 수 있습니다.
이를 어기게 되면 StateIllegalException 이 발생하고, 원하는 동작을 얻을 수 없으니 조심해야 합니다.
Android Developer 사이트에서 설명하는 일반적인 Recording Flow 는 아래 코드와 같습니다.
각 단계별로 실제 레코딩시 일어나는 동작들에 대해 살펴보도록 하겠습니다.
MediaRecorder recorder =newMediaRecorder(); recorder.setAudioSource(MediaRecorder.AudioSource.MIC); //----------- (1) recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); // -- (2) recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); // ------(3) recorder.setOutputFile(PATH_NAME); recorder.prepare(); // -----------------------------------------------(4) recorder.start();// Recording is now started ----------------------(5) ... recorder.stop(); recorder.reset();// You can reuse the object by going back to setAudioSource() step recorder.release();// Now the object cannot be reused
(1) AudioSource 설정
AudioSource 의 종류는 아래 표에 정리되어 있습니다.
DEFAULT
0
Default audio source
MIC
1
Microphone audio source
VOICE_UPLINK
2
Voice call uplink (TX) audio source
VOICE_DOWNLINK
3
Voice call downlink (RX) audio source
VOICE_CALL
4
Voice call uplink + downlink audio source
CAMCORDER
5
Microphone audio source tuned for video recording. wit the same orientation as the camera if available
VOICE_RECOGNITION
6
Microphone audio source tuned for voice recognition
VOICE_COMMUNICATION
7
Microphone audio source tuned for voice communications such as VoIP
REMOTE_SUBMIX
8
Audio source for a submix for audio streams to be presented remotely. An application can use this audio source to capture a mix of audio streams that should be transmitted to a remote receiver such as a Wifi display
UNPROCESSED
9
Microphone audio source tuned for unprocessed sound if available.
RADIO_TUNER
1998
Audio source for capturing broadcast radio tuner output
HOTWORD
1999
Audio source for preemptible, low-priority software hotword detection
보통은 마이크로 들어오는 음성을 많이 녹음하기 때문에 MIC 값을 많이 사용하고, 용도에 맞게 그 외의 값들을 설정할 수 있습니다.
제일 아래 RADIO_TUNER 와 HOTWORD 는 System API 로 일반 Application 에서는 사용 불가 합니다.
setAudioSource 함수가 호출되지 않을 경우에는 output file 에 audio track 이 포함되지 않습니다. 즉, 오디오가 녹음되지 않는다는 이야기입니다.
오디오를 녹음하고 싶으면 setAudioSource 함수를 prepare 전에 꼭 호출해 주어야 합니다.
(2) Output Format 설정
레코딩 후 실제 파일로 저장할 때, 저장할 파일의 format 을 설정해 줄 수 있습니다.
DEFAULT
0
THREE_GPP
1
MPEG_4
2
AMR_NB
3
AMR_WB
4
AAC_ADIF
5
AAC_ADTS
6
OUTPUT_FORMAT_RTP_AVP
7
MPEG_2_TS
8
WEBM
9
비디오 포맷으로 가장 많이 사용되는 THREE_GPP 혹은 MPEG_4 가 있고, 오디오만 녹음 할 경우에는 오디오 전용 파일포맷인 AMR 이나 AAC 가 많이 사용 됩니다.
setOutputFormat() 함수는 setAudioSource()/setVideoSource() 호출 이후 / prepare() 이전에 호출되어야 합니다.
(3) AudioEncoder 설정
Audio Recording 시 Audio Encoder 를 설정해 주는 함수 입니다. 이 함수를 호출하지 않을 경우에 녹음된 파일에 audido track 은 포함되지 않습니다.
즉, 이 함수도 setAudioSource 와 동일하게 오디오를 녹음하고 싶다면 setOutputFormat() 이후 / prepare() 이전에 꼭 호출해 주어야 합니다.
DEFAULT
0
AMR_NB
1
AMR_WB
2
AAC
3
HE_AAC
4
AAC_ELD
5
VORBIS
6
(4) prepare
prepare 단계는 앞서 설정한 설정값들로 recording 을 준비하는 단계 입니다.
때문에 이 함수는 audio/video source와 audio/video encoder 를 모두 설정하고 file format 을 확정 지은 후에 불려야 합니다.
이 함수가 불리면 위의 State Machine 에서 recorder 는 prepared 상태가 되어 start 나 reset 을 호출할 수 있습니다.
(5) start
start 함수를 호출하면 정해놓은 audio/video source 에서 실제 레코딩을 시작하게 되고, state 는 Recording 상태가 됩니다.
start 이후 원하는 시점에 stop 을 호출하면, 그 시점까지 레코딩이 진행 됩니다.
이 외에도 API Level 24 부터 제공되는 pause() / resume() 함수를 이용해 레코딩을 잠시 멈추는 것이 가능합니다.
stop() 과 다르게 pause() 함수로 레코딩을 멈추면 기존의 configuration 은 유지된 채로 MediaRecorder 가 잠시 멈추게 됩니다.
이 상태에서 resume() 함수로 레코딩을 다시 시작할 수 있습니다. 당연한 이야기지만 paused 상태에서 레코딩 되는 내용은 버려지게 됩니다.
developer 사이트를 참고하면 위의 간단한 API 이외에도 MediaRecorder 에서 제공하는 다양한 API 들을 볼 수 있으니 참고하시면 되겠습니다.
Java 와의 호환성이 Kotlin 의 큰 장점으로 부곽되는 만큼 Java 와 비교하게 되는 부분이 있습니다.
Kotlin 에서 Class는 Java와 같이 아래처럼 사용합니다.
class Person{ }
그러나 Java 와 다르게 Body 가 없이 아래와 같이 사용될 수도 있습니다.
class Person
Kotlin에서는 primary constructor 과 하나 이상의 secondary constructor 를 가질 수 있습니다.
primary constructor 는 class 선언시 함께 가능합니다.
constructor 키워드는 생략 가능합니다.
annotation 이나 접근자(private, pubilc 등) 와 함께 사용되는 경우 생략할 수 없습니다.
class Person constructor(firstName: String) {
}
class Person(firstName: String) {
}
primary constructor
primary constructor 는 어떤 코드도 포함하지 않으므로 initializer block 을 사용하여 초기화 코드를 구현하면 됩니다.
constructor 가 불리면 객체 생성이 되면서 initializer block 이 불립니다.
primary constructor 의 parameter 는 class 내 어디서든(init 블럭이나 속성값 초기화) 사용 가능합니다.
class Person(name: String) {
val nameInfo = "Name : $name".also(::println)
init {
println("First initializer block that prints ${name}")
}
val secondProperty = "Second property: ${name.length}".also(::println)
init {
println("Second initializer block that prints ${name.length}")
}
}
secondary constructor
constructor 키워드가 반드시 사용되어야 합니다.
class Person {
constructor(parent: Person) {
parent.children.add(this)
}
}
primary constructor 가 있는 경우 모든 second constructor는 primary constructor 에게 위임해야 합니다. 동일 클래스 내에서 다른 생성자로의 위임은 this 키워드를 사용합니다.
initializer block의 코드는 primary constructor 의 일부가 된다. primary constructor 로의 위임은 secondary constructor 코드 실행 전에 실행되므로 모든 initializer block 의 코드는 가장 먼저 실행됩니다.
class Constructors {
init {
println("Init block") // 먼저 실행됨
}
constructor(i: Int) {
println("Constructor") // 나중에 실행됨
}
}
class의 instance 생성하기
class의 instance 를 생성하는데 JAVA에서는 new 키워드를 사용했다면, Kotlin 에서는 constructor 호출만 하면 됩니다.
val invoice = Invoice()
val customer = Customer("Joe Smith")
class 상속받기
Kotlin 에서 class 를 상속받기 위해서는 아래와 같이 사용할 수 있습니다.
C++과 같이 : 기호를 사용하는데 super class의 클래스명에 괄호기호가 붙는 점이 다르네요
물론 함수 override 도 아래와 같이 할 수 있습니다.
open annotation은 class로부터의 상속이 가능하다는 의미입니다. Kotlin에서는 모든 class가 default로 final 로 선언됩니다
open annotation을 사용하는 경우에만 상속이 가능합니다.
method 의 경우에도 open annotation이 있는 method 만 override 가능합니다.
open class Base {
open fun v() {}
fun nv() {}
}
class Derived() : Base() {
override fun v() {}
}
변수의 경우 val 변수(Read only) 를 var 변수로 재정의 할 수 있지만, 반대는 안됩니다.
interface Foo {
val count: Int
}
class Bar1(override val count: Int) : Foo
class Bar2 : Foo {
override var count: Int = 0
}