4. Activity Lifecycle and Data


Activity Lifecycle

우리는 기본적으로 Intent를 더 살펴 봅시다
MainActivity에서 항목을 클릭하면 DetailActivity가 나옵니다
back button을 클릭하면 다시 MainActivity로 돌아갑니다
계속 같은 앱에서 동작을 하는 것 입니다

또, 지도를 보거나 메세지를 전송하는 창에서도 동일하게 작동합니다
홈 버튼을 눌러서 홈 화면으로 갈 수도 있습니다

이런식으로 Android에서는 여러개의 작업들이 뒤에서 돌아가고 있습니다
그러나 리소스는 제한적이기 때문에 백그라운드에 깔아놓으면 안됩니다

이것을 TaskKiller Application을 사용하면 안됩니다 자동으로 해줍니다

우리는 이것을 방지하기 위해서 Application에서 Lifetime에서 어디쯤 위치하는지 알려주는 Framework 신호들Activity의 Lifecycle을 이해해야 합니다

active life cycle은 activity가 foreground에 있고 focus가 잡혀 있을 때를 뜻합니다


Android 화면을 회전 했을 때 Lifecycle의 순서를 생각해보세요

  1. onPause
  2. onStop
  3. onCreate
  4. onResume
  5. onStart
  6. onDestroy

onPause -> onStop -> onDestroy -> onCreate -> onStart -> onResume


허니콤부터는 Android Application이 종료 위기에 몰리기 전에 Stopped가 호출이 됩니다.
하지만, 허니콤 이전 기기를 지원하려면 Paused때 부터 준비해야합니다

앱이 생각지도 못하게 종료되는 경우에 대해서 미리 준비하고 예방해야 합니다

App 사용자 입장에서는 신경 쓸 것이 없습니다
메모리가 부족하닌깐 Background에 있는 Application이 종료됩니다 하는 메세지가 나오지 않거든여

Foreground의 Application이 중요합니다 그렇기 때문에 Background의 Application은 종료됩니다
그렇다고 해서 완전 종료는 아닙니다 다시 살아날 준비를 하고 있습니다

onStoponPause는 언제든지 우리의 Application이 종료 될 수 있는다는 것을 의미합니다

onPause or onStop에서 중지/연결해제가 되어야 하는 몇가지 예들

  • Sensor Listeners
  • Location Updates
  • Dynamic Broadcast Receivers
  • Game Physics Engine

모든 Application은 Background에서 다시 실행 되기를 기다리고 있습니다

우리는 이러한 환상을 지켜줘야 합니다!
다시 Application이 불려졌을 때 예전에 보았던 UI 그대로가 나타나야합니다


라이프 사이클에 영향을 받지 않도록 Application을 작성해야합니다

우리는 한번 생각해봅시다
인터넷 연결을 할 수 없다면?
어플리케이션이 Battery를 너무 많이 사용한다면?
데이터 통신을 하는데 많은 비용을 내기 싫다면?

이 모든 상황을 준비하기 위해서는 날씨를 미리 저장해 두면 됩니다

중요한건은 UX입니다
Application을 실행 했을 때 바로 날씨를 보여줘야 하는것이죠

인터넷을 계속 사용한다면 Battery소모가 심합니다!
이것은 우리가 만든 Application 유저를 버리는 행동입니다

우리는 데이터를 저장해서 보여주도록 합시다

다시한번 정리 합시다

  1. 인터넷 접속이 많으면 배터리 소모가 심하다
  2. 인터넷 접속은 많은 요금을 발생 시킨다
  3. 인터넷 접속 환경은 생각보다 좋지 않다
  4. 온라인, 오프라인을 구분하면 안된다 다 사용 가능해야한다
  5. 어떤 환경에서도 Application은 구동 가능해야한다

DataBase

Android 내에서 File들은 수없이 많습니다

이것을 개발자에게 필요한 정보들로 구조화 시킨 스토리지가 다수 있습니다
우리도 이중에 한가지를 배웠습니다

많이 사용하는 것중에 우리는 2가지를 사용하겠습니다
SharedPreferences와 SQLite Database를 사용하겠습니다

SharedPreferences는 key-value 형태 이기 때문에 유연성이 많이 떨어집니다
제한된 정보만 저장하기도 하고여

SQLite를 이용합시다
경량의 관계형 데이터베이스를 구현하고 있습니다
SQL문을 사용해서 편하게 할 수 있습니다!!

우리는 이러한 테이블을 이용하고 많은 Schma를 사용합니다
기본적인 Database에 대한 지식이 필요합니다

Database수업이 아니닌깐 sqlite에서 배우도록 합시다

1
SELECT * FROM weather ORDER BY max DESC LIMIT 1

이정도 sql문의 결과를 예측 가능하면 충분히 따라 올 수 있습니다

대부분의 Android Application에서는 sqlite3를 사용하고 있습니다
Android 내부에서도 많이 도움을 주고 있습니다

http://developer.android.com/intl/ko/guide/topics/data/index.html

싸이트에서 한번 읽어보고 필요한것을 찾아 쓰도록 하세여

위 그림은 Sunshine Application의 예상되는 Database Struct 입니다
많은 내용이 있고 생각보다 어려울 것입니다

천천히 해봐여 항상 그래왔듯이 앞으로도 그렇게 하면 됩니다
모르면 하나하나 천천히!!


Contract(계약)

Database를 설계 할 때는 Contract를 먼저해야합니다
필요한 정보는 무엇이고 어떤것을 표현하고 해야하는지를 알아야 합니다
ContactsContract

우리는 날씨정보와 위치 정보를 따로 분리해서 각각의 테이블로 저장 할 것입니다

왜 그렇게 하는지는 Database를 공부하고 배우도록 합시다

이것을 실제로 코드로 옮겨봅시다

data package를 만들어서 WeatherContract 클래스를 만들어 봅시다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import android.provider.BaseColumns;
import android.text.format.Time;

public class WeatherContract {

public static long normalizeDate(long startDate) {
Time time = new Time();
time.set(startDate);
int julianDay = Time.getJulianDay(startDate, time.gmtoff);
return time.setJulianDay(julianDay);
}

public static final class LocationEntry implements BaseColumns {

public static final String TABLE_NAME = "location";

public static final String COLUMN_LOCATION_SETTING = "location_setting";

public static final String COLUMN_CITY_NAME = "city_name";

public static final String COLUMN_COORD_LAT = "coord_lat";
public static final String COLUMN_COORD_LONG = "coord_long";
}

public static final class WeatherEntry implements BaseColumns {

public static final String TABLE_NAME = "weather";

public static final String COLUMN_LOC_KEY = "location_id";
public static final String COLUMN_DATE = "date";
public static final String COLUMN_WEATHER_ID = "weather_id";

public static final String COLUMN_SHORT_DESC = "short_desc";

public static final String COLUMN_MIN_TEMP = "min";
public static final String COLUMN_MAX_TEMP = "max";

public static final String COLUMN_HUMIDITY = "humidity";

public static final String COLUMN_PRESSURE = "pressure";

public static final String COLUMN_WIND_SPEED = "wind";

public static final String COLUMN_DEGREES = "degrees";
}
}

아직 Database를 만든것이 아닙니다
단지, Contract를 했을 뿐이져

실제로 Database와 작업을 하는 것은 DB Helper입니다


SQLiteOpenHelper

DB Helper를 이용해서 데이터베이스를 생성하고 테이블을 만들고 데이터를 수정합니다
DB Helper 중에서 SQLiteOpenHelper를 사용해 봅시다
data package에서 WeatherDbHelper를 만들어 보세여

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class WeatherDbHelper extends SQLiteOpenHelper {

private static final int DATABASE_VERSION = 2;
// 데이터 베이스의 버전을 의미합니다
static final String DATABASE_NAME = "weather.db";
// 데이터 베이스 파일의 이름을 정합니다
public WeatherDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
// 생성자를 이용해서 데이터 베이스 이름, 버전을 정하게 됩니다
}

@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
final String SQL_CREATE_WEATHER_TABLE = "CREATE TABLE " + WeatherEntry.TABLE_NAME + " (" +
WeatherEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +

WeatherEntry.COLUMN_LOC_KEY + " INTEGER NOT NULL, " +
WeatherEntry.COLUMN_DATE + " INTEGER NOT NULL, " +
WeatherEntry.COLUMN_SHORT_DESC + " TEXT NOT NULL, " +
WeatherEntry.COLUMN_WEATHER_ID + " INTEGER NOT NULL," +

WeatherEntry.COLUMN_MIN_TEMP + " REAL NOT NULL, " +
WeatherEntry.COLUMN_MAX_TEMP + " REAL NOT NULL, " +

WeatherEntry.COLUMN_HUMIDITY + " REAL NOT NULL, " +
WeatherEntry.COLUMN_PRESSURE + " REAL NOT NULL, " +
WeatherEntry.COLUMN_WIND_SPEED + " REAL NOT NULL, " +
WeatherEntry.COLUMN_DEGREES + " REAL NOT NULL, " +

" FOREIGN KEY (" + WeatherEntry.COLUMN_LOC_KEY + ") REFERENCES " +
LocationEntry.TABLE_NAME + " (" + LocationEntry._ID + "), " +

" UNIQUE (" + WeatherEntry.COLUMN_DATE + ", " +
WeatherEntry.COLUMN_LOC_KEY + ") ON CONFLICT REPLACE);";

sqLiteDatabase.execSQL(SQL_CREATE_WEATHER_TABLE);
}

@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + LocationEntry.TABLE_NAME);
sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + WeatherEntry.TABLE_NAME);
onCreate(sqLiteDatabase);
}
}

우리는 이제까지 작업을 진행하면서 될지 안될지 테스트를 항상 에뮬레이터나 폰으로 직접 테스트를 해봤습니다
과연 이게 좋은것일까요??

Android Studio라는 좋은 IDE에 이런거 하나 없을까여??
그래서 준비 되어있습니다

JUnit testing 과 Android testing 기능입니다
사실 너무 어려운 내용이라서 이것을 수업을 할까 말까 고민했습니다
안해도 되거든여 그냥 폰으로 한다고 해서 문제 생길일은 없습니다만!
제대로된 개발을 하자고여 제대로된!!!


Android Test

다음 파일들이 준비되어있습니다
파일을 다 만들고 자세한 설명을 하겠습니다

FullTestSuite.javaview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.study.sunshine;

import android.test.suitebuilder.TestSuiteBuilder;

import junit.framework.Test;
import junit.framework.TestSuite;


public class FullTestSuite extends TestSuite {
public static Test suite() {
return new TestSuiteBuilder(FullTestSuite.class)
.includeAllPackagesUnderHere().build();
}

public FullTestSuite() {
super();
}
}
TestDb.javaview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import android.test.AndroidTestCase;

public class TestDb extends AndroidTestCase {

public static final String LOG_TAG = TestDb.class.getSimpleName();

void deleteTheDatabase() {
mContext.deleteDatabase(WeatherDbHelper.DATABASE_NAME);
}

public void setUp() {
deleteTheDatabase();
}

// public void testCreateDb() throws Throwable {
// final HashSet<String> tableNameHashSet = new HashSet<String>();
// tableNameHashSet.add(WeatherContract.LocationEntry.TABLE_NAME);
// tableNameHashSet.add(WeatherContract.WeatherEntry.TABLE_NAME);
//
// mContext.deleteDatabase(WeatherDbHelper.DATABASE_NAME);
// SQLiteDatabase db = new WeatherDbHelper(
// this.mContext).getWritableDatabase();
// assertEquals(true, db.isOpen());
//
// Cursor c = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'", null);
//
// assertTrue("Error: This means that the database has not been created correctly",
// c.moveToFirst());
//
// do {
// tableNameHashSet.remove(c.getString(0));
// } while( c.moveToNext() );
//
// assertTrue("Error: Your database was created without both the location entry and weather entry tables",
// tableNameHashSet.isEmpty());
//
// c = db.rawQuery("PRAGMA table_info(" + WeatherContract.LocationEntry.TABLE_NAME + ")",
// null);
//
// assertTrue("Error: This means that we were unable to query the database for table information.",
// c.moveToFirst());
//
// final HashSet<String> locationColumnHashSet = new HashSet<String>();
// locationColumnHashSet.add(WeatherContract.LocationEntry._ID);
// locationColumnHashSet.add(WeatherContract.LocationEntry.COLUMN_CITY_NAME);
// locationColumnHashSet.add(WeatherContract.LocationEntry.COLUMN_COORD_LAT);
// locationColumnHashSet.add(WeatherContract.LocationEntry.COLUMN_COORD_LONG);
// locationColumnHashSet.add(WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING);
//
// int columnNameIndex = c.getColumnIndex("name");
// do {
// String columnName = c.getString(columnNameIndex);
// locationColumnHashSet.remove(columnName);
// } while(c.moveToNext());
//
// assertTrue("Error: The database doesn't contain all of the required location entry columns",
// locationColumnHashSet.isEmpty());
// db.close();
// }

public void testLocationTable() {

}

public void testWeatherTable() {
}


public long insertLocation() {
return -1L;
}
}
TestPractice.javaview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import android.test.AndroidTestCase;

public class TestPractice extends AndroidTestCase {
/*
This gets run before every test.
*/
@Override
protected void setUp() throws Exception {
super.setUp();
}

public void testThatDemonstratesAssertions() throws Throwable {
int a = 5;
int b = 3;
int c = 5;
int d = 10;

assertEquals("X should be equal", a, c);
assertTrue("Y should be true", d > a);
assertFalse("Z should be false", a == b);

if (b > d) {
fail("XX should never happen");
}
}

@Override
protected void tearDown() throws Exception {
super.tearDown();
}
}
TestUtilities.javaview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import android.content.ContentValues;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.test.AndroidTestCase;

import com.study.sunshine.utils.PollingCheck;

import java.util.Map;
import java.util.Set;


public class TestUtilities extends AndroidTestCase {
static final String TEST_LOCATION = "Seoul,kr";
static final long TEST_DATE = 1485700000L; // 2017 - 01 - 29;

static void validateCursor(String error, Cursor valueCursor, ContentValues expectedValues) {
assertTrue("Empty cursor returned. " + error, valueCursor.moveToFirst());
validateCurrentRecord(error, valueCursor, expectedValues);
valueCursor.close();
}

static void validateCurrentRecord(String error, Cursor valueCursor, ContentValues expectedValues) {
Set<Map.Entry<String, Object>> valueSet = expectedValues.valueSet();
for (Map.Entry<String, Object> entry : valueSet) {
String columnName = entry.getKey();
int idx = valueCursor.getColumnIndex(columnName);
assertFalse("Column '" + columnName + "' not found. " + error, idx == -1);
String expectedValue = entry.getValue().toString();
assertEquals("Value '" + entry.getValue().toString() +
"' did not match the expected value '" +
expectedValue + "'. " + error, expectedValue, valueCursor.getString(idx));
}
}

static ContentValues createWeatherValues(long locationRowId) {
ContentValues weatherValues = new ContentValues();
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_LOC_KEY, locationRowId);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_DATE, TEST_DATE);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_DEGREES, 1.1);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_HUMIDITY, 1.2);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_PRESSURE, 1.3);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_MAX_TEMP, 75);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_MIN_TEMP, 65);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_SHORT_DESC, "Asteroids");
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_WIND_SPEED, 5.5);
weatherValues.put(WeatherContract.WeatherEntry.COLUMN_WEATHER_ID, 321);

return weatherValues;
}

// static ContentValues createNorthPoleLocationValues() {
// ContentValues testValues = new ContentValues();
// testValues.put(WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING, TEST_LOCATION);
// testValues.put(WeatherContract.LocationEntry.COLUMN_CITY_NAME, "North Pole");
// testValues.put(WeatherContract.LocationEntry.COLUMN_COORD_LAT, 64.7488);
// testValues.put(WeatherContract.LocationEntry.COLUMN_COORD_LONG, -147.353);
//
// return testValues;
// }

// static long insertNorthPoleLocationValues(Context context) {
// WeatherDbHelper dbHelper = new WeatherDbHelper(context);
// SQLiteDatabase db = dbHelper.getWritableDatabase();
// ContentValues testValues = TestUtilities.createNorthPoleLocationValues();
//
// long locationRowId;
// locationRowId = db.insert(WeatherContract.LocationEntry.TABLE_NAME, null, testValues);
//
// // Verify we got a row back.
// assertTrue("Error: Failure to insert North Pole Location Values", locationRowId != -1);
//
// return locationRowId;
// }

static class TestContentObserver extends ContentObserver {
final HandlerThread mHT;
boolean mContentChanged;

static TestContentObserver getTestContentObserver() {
HandlerThread ht = new HandlerThread("ContentObserverThread");
ht.start();
return new TestContentObserver(ht);
}

private TestContentObserver(HandlerThread ht) {
super(new Handler(ht.getLooper()));
mHT = ht;
}

@Override
public void onChange(boolean selfChange) {
onChange(selfChange, null);
}

@Override
public void onChange(boolean selfChange, Uri uri) {
mContentChanged = true;
}

public void waitForNotificationOrFail() {
new PollingCheck(5000) {
@Override
protected boolean check() {
return mContentChanged;
}
}.run();
mHT.quit();
}
}

static TestContentObserver getTestContentObserver() {
return TestContentObserver.getTestContentObserver();
}
}
PollingCheck.javaview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import junit.framework.Assert;

import java.util.concurrent.Callable;

public abstract class PollingCheck {
private static final long TIME_SLICE = 50;
private long mTimeout = 3000;

public PollingCheck() {
}

public PollingCheck(long timeout) {
mTimeout = timeout;
}

protected abstract boolean check();

public void run() {
if (check()) {
return;
}

long timeout = mTimeout;
while (timeout > 0) {
try {
Thread.sleep(TIME_SLICE);
} catch (InterruptedException e) {
Assert.fail("unexpected InterruptedException");
}

if (check()) {
return;
}

timeout -= TIME_SLICE;
}

Assert.fail("unexpected timeout");
}

public static void check(CharSequence message, long timeout, Callable<Boolean> condition)
throws Exception {
while (timeout > 0) {
if (condition.call()) {
return;
}

Thread.sleep(TIME_SLICE);
timeout -= TIME_SLICE;
}

Assert.fail(message.toString());
}
}


다음과 같이 실행을 하면 실패할 것입니다
기본적으로 가상머신이나 실제 폰으로 돌린다면 아무 화면의 결과는 표시되지 않습니다

다음과 같이 실행을 하면 테스트가 잘 끝나고 정상적으로 성공했다는 메시지가 나옵니다
왼쪽에는 성공하면 초록색으로 실패하면 빨강색으로 표시됩니다.
오른쪽에는 메시지가 출력됩니다

1
2
3
assertEquals("X should be equal", a, c);
assertTrue("Y should be true", d > a);
assertFalse("Z should be false", a == b);

3가지 종류의 테스트 방법을 이용해서 하면 됩니다

1
2
3
if (b > d) {
fail("XX should never happen");
}

IF문을 통해서 검사하고 fail 문구를 표시할 수도 있습니다

TestDb 클래스에서 testCreateDb() 주석을 해제합니다
TestUtilities 클래스에서 createNorthPoleLocationValues(), insertNorthPoleLocationValues() 도 주석을 제거해봅시다

그리고 TestDb 클래스를 테스트를 시작해 봅시다

다음과 같은 애러를 볼 수 있습니다
이제 해결해 봅시다

Location 테이블 없이 생성하려고 했기 때문에 애러가 발생했습니다.
Location 테이블부터 생성해 봅시다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
final String SQL_CREATE_LOCATION_TABLE = "CREATE TABLE " + LocationEntry.TABLE_NAME + " (" +
LocationEntry._ID + " INTEGER PRIMARY KEY," +
LocationEntry.COLUMN_LOCATION_SETTING + " TEXT UNIQUE NOT NULL, " +
LocationEntry.COLUMN_CITY_NAME + " TEXT NOT NULL, " +
LocationEntry.COLUMN_COORD_LAT + " REAL NOT NULL, " +
LocationEntry.COLUMN_COORD_LONG + " REAL NOT NULL " +
" );";

// 생략

sqLiteDatabase.execSQL(SQL_CREATE_LOCATION_TABLE);
sqLiteDatabase.execSQL(SQL_CREATE_WEATHER_TABLE);
}

확인을 위해서 테스트를 돌려 봅시다


데이터베이스를 사용하다 보면 업그레이드할 일이 생깁니다.
Column이 변경(추가/삭제)이 되는 경우도 있고 테이블이 추가되어서 관계를 맺는 경우도 있습니다

이런 상황을 대비 해 뒀습니다

1
2
3
4
5
6
7
8
9
private static final int DATABASE_VERSION = 2;
// 데이터 베이스의 버전을 의미합니다
static final String DATABASE_NAME = "weather.db";

// 데이터 베이스 파일의 이름을 정합니다
public WeatherDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
// 생성자를 이용해서 데이터 베이스 이름, 버전을 정하게 됩니다
}

만약 버전이 변경되어서 DATABASE_VERSION 값이 3이 되면

1
2
3
4
5
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + LocationEntry.TABLE_NAME);
sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + WeatherEntry.TABLE_NAME);
onCreate(sqLiteDatabase);
}

onUpgrade() 메서드가 실행됩니다
우리는 현재 버전이 변경되면 테이블이 DROP합니다
다른 형태로 변경하고 싶다면 SQLite를 참고하시기 바랍니다

다음과 같은 구조에서 Data Contract, DB Helper, SQLiteDatabase 을 구축하였습니다

데이터베이스를 읽기/쓰기/검사 하기 위해서 여러 가지를 사용합니다

1
SQLiteDatabase db = new WeatherDbHelper(this.mContext).getWritableDatabase();

데이터베이스 쓰기 용으로 준비합니다

데이터를 삽입하기 위해서는 contentvalues가 필요합니다

1
2
3
4
5
6
7
8
9
static ContentValues createNorthPoleLocationValues() {
ContentValues testValues = new ContentValues();
testValues.put(WeatherContract.LocationEntry.COLUMN_LOCATION_SETTING, TEST_LOCATION);
testValues.put(WeatherContract.LocationEntry.COLUMN_CITY_NAME, "North Pole");
testValues.put(WeatherContract.LocationEntry.COLUMN_COORD_LAT, 64.7488);
testValues.put(WeatherContract.LocationEntry.COLUMN_COORD_LONG, -147.353);

return testValues;
}

ContentValues는 key-value 형으로 되어있습니다

ContentValues형태로 삽입할 데이터가 준비를 끝냈으면 insert하면 됩니다

1
int ret = db.insert(WeatherContract.LocationEntry.TABLE_NAME, null, testValues);

삽입할 데이터가 너무 길므로 자동으로 _id 포함 되어있습니다
_id 값을 통해서 정상적으로 들어갔는지 확인할 수 있습니다
만약 비정상이라면 _id 값은 -1이 됩니다

Android에서는 Query를 통해서 얻은 결과를 Cursor 라는 것을 통해서 접근하게 됩니다

Cursor는 어려운 개념이 아닙니다
결과값으로 데이터의 그냥 한줄 한줄이라고 생각하면 됩니다
한 줄의 정보를 다 읽었다면 cursor.move를 통해서 다음 줄로 이동하면 됩니다
Cursor의 사용이 끝나면 close 해주면 됩니다

연습 testDb.testLocationTable() 완성해 보세요

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public void testLocationTable() {
WeatherDbHelper dbHelper = new WeatherDbHelper(mContext);
SQLiteDatabase db = dbHelper.getWritableDatabase();

ContentValues testValues = TestUtilities.createNorthPoleLocationValues();

long locationRowId = db.insert(WeatherContract.LocationEntry.TABLE_NAME, null, testValues);

assertTrue("Error : No Insert Data ", locationRowId != -1);

Cursor cursor = db.query(WeatherContract.LocationEntry.TABLE_NAME, // Table to Query
null, // leaving "columns" null just returns all the columns.
null, // cols for "where" clause
null, // values for "where" clause
null, // columns to group by
null, // columns to filter by row groups
null // sort order
);

assertTrue("Error : No Records retuned from location query", cursor.moveToFirst());

TestUtilities.validateCurrentRecord("Error : Location Query Validation Failed", cursor, testValues);

assertFalse("Error: More than one record returned from location query", cursor.moveToNext());

cursor.close();
db.close();
}


연습 testDb.testWeatherTable(), testDb.insertLocation() 완성해 보세요

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public void testWeatherTable() {
long locationRowId = insertLocation();

assertFalse("Error: Location Not Inserted Correctly", locationRowId == -1L);

ContentValues weatherValues = TestUtilities.createWeatherValues(locationRowId);

WeatherDbHelper dbHelper = new WeatherDbHelper(mContext);
SQLiteDatabase db = dbHelper.getWritableDatabase();

long weatherRowId = db.insert(WeatherContract.WeatherEntry.TABLE_NAME, null, weatherValues);
assertTrue(weatherRowId != -1);

Cursor weatherCursor = db.query(
WeatherContract.WeatherEntry.TABLE_NAME, // Table to Query
null, // leaving "columns" null just returns all the columns.
null, // cols for "where" clause
null, // values for "where" clause
null, // columns to group by
null, // columns to filter by row groups
null // sort order
);

assertTrue("Error: No Records returned from location query", weatherCursor.moveToFirst());

TestUtilities.validateCurrentRecord("testInsertReadDb weatherEntry failed to validate",
weatherCursor, weatherValues);

assertFalse("Error: More than one record returned from weather query",
weatherCursor.moveToNext());

weatherCursor.close();
dbHelper.close();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public long insertLocation() {
WeatherDbHelper dbHelper = new WeatherDbHelper(mContext);
SQLiteDatabase db = dbHelper.getWritableDatabase();

ContentValues testValues = TestUtilities.createNorthPoleLocationValues();

long locationRowId;
locationRowId = db.insert(WeatherContract.LocationEntry.TABLE_NAME, null, testValues);

assertTrue(locationRowId != -1);


Cursor cursor = db.query(
WeatherContract.LocationEntry.TABLE_NAME, // Table to Query
null, // all columns
null, // Columns for the "where" clause
null, // Values for the "where" clause
null, // columns to group by
null, // columns to filter by row groups
null // sort order
);

assertTrue( "Error: No Records returned from location query", cursor.moveToFirst() );

TestUtilities.validateCurrentRecord("Error: Location Query Validation Failed",
cursor, testValues);

assertFalse( "Error: More than one record returned from location query",
cursor.moveToNext() );

cursor.close();
db.close();
return locationRowId;
}

복습은 필수

  • Activity Lifecycle
  • SQLite
  • Database Contract
  • SQLiteOpenHelper
  • Android test
  • ContentValues
  • Cursor