블로그 이미지
개발자로서 현장에서 일하면서 새로 접하는 기술들이나 알게된 정보 등을 정리하기 위한 블로그입니다. 운 좋게 미국에서 큰 회사들의 프로젝트에서 컬설턴트로 일하고 있어서 새로운 기술들을 접할 기회가 많이 있습니다. 미국의 IT 프로젝트에서 사용되는 툴들에 대해 많은 분들과 정보를 공유하고 싶습니다.
솔웅

최근에 받은 트랙백

글 보관함

calendar

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        

[Android] Optional SQLite Tutorial

2016.10.23 04:43 | Posted by 솔웅


Optional SQLite Tutorial

Sunshine 앱에서는 날씨 정보를 저장하기 위해 SQLite database를 사용할 것입니다. SQLite에 대한 기본적인 이해가 있어야 합니다. 그리고 기본적인 명령어들도 익혀야 합니다. 이 글은 SQL 데이터베이스에 초보인 사람들 혹은 refresher를 위한 글입니다.


Introduction

SQLite은 관계형 데이터베이스 관리 시스템 입니다. SQL (Structured Query Language)을 사용합니다. C++ 라이브러리처럼 Android OS에 패키지로 있어서 각 앱들마다 private database를 가질 수 있습니다 SQL은 create, search 그리고 maintain database 등에 이용됩니다. 이 글에서는 SQL의 신택스와 사용법에 대해 다룹니다. 그리고 어떻게 작은 규모의 데이터베이스를 생성하고 관리하는지에 대해서도 다룹니다. 여기서 다루는 commands들은 Sunshine에서 사용될 SQL과 비슷합니다.

Get SQLite

    1. 이곳에서 SQLite을 다운받아 설치하면 컴퓨터에서 사용할 수 있습니다.
    http://sqlite.org/download.html
    SQLite Installation

    2. 설치 후 윈도우 커맨드 창을 열어서 데이터베이스를 저장할 폴더로 이동합니다. sunshine.db라는 데이터베이스를 아래와 같이 설치 합니다.
    sqlite3 sunshine.db

    3. 모든 commands들을 보려면 이렇게 타입 합니다.
    .help

    4. 그중에 모든 데이터베이스를 보여주는 명령어가 있습니다. 현재로서는 sunshine.db에 대한 정보가 보여질 겁니다.

    .databases


    앱에서 여러분은 여러개의 데이터베이스들을 만들 수 있습니다. Sunshine 엡에서는 하나의 데이터베이스만을 가질 겁니다. 이 데이터베이스 안에는 여러개의 테이블을 만들 수 있습니다.


Create A Database Table

  1. A table is a collection of rows and columns like a spreadsheet. Use the CREATE TABLE statement to create a new database table called “weather.” Each row will be one day’s worth of weather data. It should have 6 columns of data: ID, date, min temperature, max temperature, humidity, and pressure.

    In the CREATE TABLE statement, each column definition is separated by commas, where you provide the column name and datatype for that column. We also specify that the column should be non-null. We specify the _id column to be the primary key and it’s an integer.

    CREATE TABLE weather( _id INTEGER PRIMARY KEY, date TEXT NOT NULL, min REAL NOT NULL, max REAL NOT NULL, humidity REAL NOT NULL, pressure REAL NOT NULL);

    The list of possible SQLite data types is a useful resource, or you can see this tutorial.

    Note: SQLite keywords, such as CREATE TABLE or PRIMARY KEY, are capitalized for ease of readability to distinguish them from the table and column names that we’ve selected, but you can lowercase the keywords if you want.

    Note: This is not the full table you’ll be using in Sunshine, this is just a simpler version of the table.

  2. Use this command to list out all tables. Ensure that the weather table was created.

    .tables

  3. Use a SELECT statement to return out all rows in the weather table. The * is a symbol that means “all of the columns”. At this time, nothing will be returned because the table is created, but there is no data in the table yet.

    SELECT * FROM weather;

  4. At any point, you can find out the schema of how the tables were created in the database

    .schema

Insert rows

  1. Use an INSERT statement to insert a new row of data into the weather table. The following INSERT statement inserts a row into the weather table for June 25th, 2014, which had a low of 16 degrees, a high of 20 degrees, 0 humidity and 1029 pressure. The _id of this row is 1.

    INSERT INTO weather VALUES(1,'20140625',16,20,0,1029);

  2. Query for all rows in the weather table, and you should see the one row of data you just inserted.

    SELECT * FROM weather;

  3. To have the column name be printed out as well (for easier readability as to what value corresponds to which column), turn the header on. Then do the query again.

    .header on
    SELECT * FROM weather;

  4. Experiment by inserting another 3 rows of data into the weather table. INSERT INTO weather VALUES(2,'20140626',17,21,0,1031); INSERT INTO weather VALUES(3,'20140627',18,22,0,1055); INSERT INTO weather VALUES(4,'20140628',18,21,10,1070);

    Query for all rows to verify they were inserted properly.

    SELECT * FROM weather;

Query rows

  1. Practice doing queries where you provide a selection WHERE clause to narrow down the number of rows that are returned in the result. Always remember the semicolon at the end of a statement!

    For all possible SQLite operators, see this link.

    This query returns rows from the weather table where the date column exactly equals the 20140626.

    SELECT * FROM weather WHERE date == 20140626;

  2. This query returns rows from the weather table where the date column is between 20140625 and 20140628. However, all columns are not returned, we just return the 4 specified columns (_id, date, min, and max) of the rows that match the query.

    SELECT _id,date,min,max FROM weather WHERE date > 20140625 AND date < 20140628;

  3. This query returns rows where the minimum temperature is greater than or equal to 18. Based on those matching rows, we order them based on increasing (also known as ascending or “ASC” for short) max temperature. The first row of the result that is printed out to the command line will be the row (with min temperature >= 18) with max temperature that is lowest out of all rows, so that subsequent rows will have higher max temperature.

    SELECT * FROM weather WHERE min >= 18 ORDER BY max ASC;

Update rows

  1. You can also update existing rows in the database with an UPDATE statement. This statement updates the weather table by setting the minimum temperature to be 0 and maximum temperature to be 100 for rows where the date is greater than 20140626 but less than 20140627.

    UPDATE weather SET min = 0, max = 100 where date >= 20140626 AND date <= 20140627;

    When you print out the whole weather table again, you can see that 2 rows were changed.

    SELECT * FROM weather;

Delete rows

  1. Use a DELETE statement to delete rows from a database table that match the given selection clause. In this case, we delete any rows from the weather table where humidity is not equal to 0.

    DELETE FROM weather WHERE humidity != 0;

Add columns

  1. If you have released a version of your app to users, and then decide you need to change the database schema, such as adding columns, then you’ll need to upgrade your database. You can alter existing tables, by using the ALTER TABLE command.

    Note: In general, you shouldn’t alter a table to remove a column because you’re deleting data and other tables could depend on that column. Instead you can just null out all values in that column.

    This statement alters the weather table by adding another column to the table called description, which will always be non-null and contain text. It will also default to the value ‘Sunny’ if no value is provided. In reality, you would choose a more reasonable default, but this is just for example purposes.

    ALTER TABLE weather ADD COLUMN description TEXT NOT NULL DEFAULT 'Sunny';

    Verify that the new description column exists when you query all rows and all columns.

    SELECT * FROM weather;

Delete table

  1. Delete the weather table by using the DROP TABLE command. Verify there are no more tables in the database.

    DROP TABLE weather;
    .tables

These are just the basics. Feel free to play around with SQLite some more. See this link: http://www.sqlite.org/cli.html

When you’re done, enter .quit to exit, and you can move onto the quiz on the next page!

저작자 표시 비영리 동일 조건 변경 허락
신고

Udacity 강좌 - Lesson 4 - Activity

2016.10.23 02:12 | Posted by 솔웅


4과에서는 Activity Persistence Storage (SQLite) 다룹니다.

 

우선 액티비티를 보겠습니다.

 

안드로이드에서는 주로 하나의 화면이 하나의 액티비티 입니다.

 

선샤인 내에서도 여러 화면이 있습니다. 그래서 여러 화면을 다니다 보면 디바이스 내에 여러개의 액티비티가 있게 됩니다.

 

선샤인을 사용하다가 다른 앱들도 사용하게 되는데요. 이러면 앱들의 액티비티들도 디바이스내에서 작동하게 됩니다.

 

디바이스 내에는 하나의 화면만 보이겠지만 뒤에 지금까지 실행했던 화면들이 숨어 있게 됩니다.

 

이럴 화면들을 계속 띄우다 보면 디바이스의 메모리가 부족해 때가 있습니다.

 

사용자게 의해서이건 시스템에 의해서이건 특정 Activity들이 Kill 있습니다.

 

이렇게 액티비티가 화면에 떴다가 Background 있다가 Kill 되고 하는 과정들은 Activity Lifecycle 보면 이해하기 쉽습니다.

 

Activity와 관련해서 좀 더 자세한 사항을 알고 싶으면 이곳에서 보시면 됩니다.



http://developer.android.com/training/basics/activity-lifecycle/starting.html



위 그림에 나와있는 Android Activity Life Cycle을 보겠습니다.
우선 처음에 앱을 시작하면 onCreate가 실행 되서 앱이  Create 됩니다.


그리고 나서 onStart가 실행되고 나면 비로서 화면이 디바이스에 보기게 됩니다.
화면이 보이는 상태가 Active 상태인데요. 이 때 onResume을 거쳐서 Active됩니다.


다음 단계는 Paused 상태인데요.
이 상태는 Alert 화면이 떴다던가 다른 화면이 떴지만 반투명 상태라서 이전 화면이 현재화면 뒤에 보이게 되는 상태입니다.


그러니까 완전하게 눈에 보이지 않게 되는 상황이 아니라 어스름하게라도 보이는 상황인데요.
이때 그 Activity는 onPause를 거쳐서 Paused한 상황이 됩니다.


이 상황에서 다시 화면에 보이게 될 수 있는데요 이때는 onResume을 거쳐서 화면이 활성화 됩니다.
Paused상태에서 다른 화면이 완전히 덮어서 사용자 눈에 보이지 않는 상황이 될 수도 있는데요.
그 때는 Stopped 상태입니다. onStop 과정을 거쳐서 이 상태가 됩니다.


해당 Activity는 눈에 보이지는 않지만 Background에서 계속 살아 있습니다.


즉 일정정도 메모리가 할당 돼 있는 상황입니다.


여기서 다시 이 뒤의 화면을 foreground로 불러올 경우도 있습니다. 이때는 Restart를 하게 됩니다.
이땐 onRestart가 실행 됩니다.


아까 onResume은 Paused 상태에서 실행 되는 것이고 이 onRestart는 Stopped상태에서 실행되는 것입니다.


onRestart가 실행되면 해당 액티비티가 다시 화면에 보이게 됩니다.


onStart를 거쳐서 보이게 되고 onResume을 거쳐서 활성화 됩니다.


한편 Stopped 상태에서 Background에서도 완전히 사라질 수도 있는데요.


즉 메모리에서 해당 Activity가 사라지게 되는 상황은 onDestroy를 거쳐서 발생됩니다.


해당 Activity가 완전히 Destroy되는 거죠.


이 Activity Life Cycle 을 잘 이용해서 적당한 때에 적당한 정보를 저장하고 다시 불러오고 연결을 끊고 재연결하고 하는 작업들을 잘 해야 앱이 무난하게 작동하게 됩니다.


게임을 하다가 상태정보를 저장하지도 않고 Destroy되면 그 다음에 앱을 시작하면 벌써 깼던 stage를 다시 새로 해야 되는 상황이 발생할 수 있으니까요.



그러면 Device 가 Rotation이 될 때는 어떤 과정을 거칠까요?
앱에서 대개 가로 화면과 세로 화면이 다른 경우가 있는데요. 이 화면 전환을 할 때는 어떤 과정을 거칠까요.
 
onPause - onStop - onDestroy - onCreate - onStart - onResume

그 때는 위와 같은 과정을 거칩니다.
 
Memory가 다해 시스템이 강제로 백그라운드 액티비티를 터미네이트 할 경우를 대비해서 관련된 작업을 해 둬야 합니다.
밧데리가 다 돼서 강제로 터미네이트 되는 경우도 있습니다. 이럴 때도 관련된 작업을 해 둬야 합니다.
(onPause 나 onStop 등등에)
 
Sensor Listeners, Location Updates, Dynamic Broadcast Receivers, Game Physics Engine
이런 것들은 onPause 나 onStop 시 disconnect 시키거나 stop 시켜야 합니다.
 


시스템에 의해 강제로 종료될 때를 대비해서 상태 정보는 onPause시에 저장하고 다시 onCreate시에 restore 합니다.


Active -> onSaveInstanceState -> onPause -> Terminated
-> onCreate -> onRestoreInstanceState



여기까지가 Activity와 관련된 이론들 이었습니다.
이후에는 데이터를 저장하는 것과 관련된 내용이 있습니다.


예를 들어 현재 작업하고 있는 Sunshine앱에서...
현재는 날씨를 표시할 때마다 날씨 웹싸이트에 접속해서 화면에 뿌려 줍니다.


하지만 이러면 네트워크에 접속하고 처리하는 과정을 매번 거치게 되서 시간이 많이 걸릴 수 있습니다.
네트워크 사정에 따라서 화면이 아주 늦게 뜰 수도 있구요.


또 밧데리 소모도 더 빨리 됩니다.


해당 서버의 접속량도 아주 많아 질 거고...


비행기 안이라던지 네트워크를 사용할 수 없는 경우에는 아예 날씨를 볼 수가 없습니다.
 


이런 이유로 한번 날씨를 받으면 이것을 저장해 뒀다가 일정기간 사용을 하면 위와 같은 단점들을 극복할 수 있습니다.


 
이럴때 모바일 데이터베이스인 SQLite를 사용하게 됩니다.
자세한 내용은 다음 글에서 이어 나가겠습니다.



저작자 표시 비영리 동일 조건 변경 허락
신고

[Android] Settings - 3 -

2016.10.13 05:18 | Posted by 솔웅


Building a Custom Preference

안드로이드 프레임워크는 여러가지 모양으로 세팅 화면을 꾸밀 수 있도록 다양한 Preference subclass들을 제공합니다. 하지만 어떤 경우 이런 built-in solution 이 아닌 다른 모양으로 꾸며야 될 때가 있습니다. 예를 들어 number picker 나 date picker 등이 있습니다. 이런 경우 custom preference를 만들 수 있는데요. pPreference 클래스나 다른 subclass 를 extend 해서 사용하면 됩니다.

Preference class를 extend 할 때 주의해야 할 점 몇가지가 있습니다.

    사용자가 세팅을 클릭했을 때 보여 질 UI를 정합니다.
    적당한 때에 세팅 값들을 저장합니다.
    Preference를 view에 값들이 나올 때 Initialize 해 줍니다.
    시스템에 의해 request 될 때는 default 값을 줍니다.
    Preference 가 자신의 UI (dialog 같은)를 제공할 때 lifecycle 변화를 컨트롤 하기 위해 state를 저장하고 restore 합니다. (예를 들어 사용자가 스크린을 rotate 할 때)

   
아래 섹션들에서 이른 일들을 하려면 어떻게 해야 하는지를 설명 할 겁니다.


Specifying the user interface


만약 Preference class 를 direct로 extend 한다면 사용자가 해당 아이템을 선택 했을 때 어떤 action이 일어나도록 하기 위해 onClick() 메소드를 implemet 해야 합니다. 대부분의 custom settings는 DialogPreference를 extend 합니다. dialog를 보여야 하니까요. 그러면 일의 진행을 좀 더 간단하게 처리할 수 있죠. DialogPreference를 extend 할 때 그 클래스에서 layout을 생성하는 동안 setDialogLayoutResource()를 호출해야 합니다.


예를 들어 여기 custom DialogPreference 를 위한 constructor 가 있습니다. layout을 정의하고 positive, negative 버튼을 정의 합니다.


public class NumberPickerPreference extends DialogPreference {
    public NumberPickerPreference(Context context, AttributeSet attrs) {
        super(context, attrs);

        setDialogLayoutResource(R.layout.numberpicker_dialog);
        setPositiveButtonText(android.R.string.ok);
        setNegativeButtonText(android.R.string.cancel);

        setDialogIcon(null);
    }
    ...
}



Saving the setting's value




세팅에 대한 value들은 Preference class의 persist*() 메소드를 호출하면 어느때든지 저장할 수 있습니다. 예를 들어 값이 Integer 일 경우는 persistInt()를 Boolean 일 경우는 persistBoolean()을 호출하면 됩니다.


Note:  각각의 Preference 는 한가지의 data type으로 정의 됩니다. 그리고 거기에 해당하는 persist*() 메소드를 사용하여야 합니다.


persist를 선택하면 세팅은 extend 한 Preference class에 따라 진행 됩니다. DialogPreference 를 extend 했다면 사용자가 OK 버튼을 눌러서 이 창이 닫힐 때 해당 값을 persist 해야 합니다.


DialogPreference 가 닫힐 때, 시스템은 onDialogClosed() 메소드를 호출합니다. 이 메소드는 boolean argument를 가지고 있습니다. 사용자가 선택한 것이 OK 인지 Cancel 인지를 구분하기 위한 것이죠.
OK 를 누르면 해당 값을 persist 합니다.


@Override
protected void onDialogClosed(boolean positiveResult) {
    // When the user selects "OK", persist the new value
    if (positiveResult) {
        persistInt(mNewValue);
    }
}


이 예제에서는 mNewValue가 class member 입니다. 세팅의 현재 선택 된 값을 가지고 있는 변수죠. persistInt를 호출해서 이 값을 SharedPreference file 에 저장합니다.
(automatically using the key that's specified in the XML file for this Preference).


Initializing the current value

시스템이 화면에 여러분의  Preference를 추가 할 때 onSetInitialValue() 메소드가 호출됩니다. 이 메소드는 이 세팅이 persisted value 인지 여부를 notify 하게 됩니다. 만약 persisted value가 없으면 default value를 제공합니다.

onSetInitialValue() method는 boolean, restorePersistedValue 를 pass 합니다. 세팅에 값이 persisted 됐는지 여부를 알려주죠. true 이면 해당 Preference class의 getPersisted*() 메소드를 호출해서 persisted 된 값을 retrieve 해야 합니다. Integer value 인 경우는 getPersistedInt() 가 되겠죠. 최신의 값을 화면에 표시하려면 제 때애 UI를 update 해 줘야 합니다.

만약 restorePersistedValue가 false 이면 두번째 argument로 전달 된 default value를 사용해야 합니다.


@Override
protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {
    if (restorePersistedValue) {
        // Restore existing state
        mCurrentValue = this.getPersistedInt(DEFAULT_VALUE);
    } else {
        // Set default state from the XML attribute
        mCurrentValue = (Integer) defaultValue;
        persistInt(mCurrentValue);
    }
}


각각의 getPersisted*() 메소드는 persist 값이 없거나 key 가 존재하지 않을 경우 사용될 default 값을 받게 됩니다. 위 예제에서 local constant는 getPersistedInt()가 persisted 값을 return 하지 못학 경우 사용될 default 값을 지정해 놓았습니다.

Caution : getPersisted*() 메소드에서 defaultValue를 default 값으로 사용할 수 없습니다. 왜냐하면 restorePersistedValue 가 true 이면 이 갑은 항상 null 일 것이기 때문입니다.



Providing a default value


당신의 Preference class 의 instance 가 default value를 가리키고 있다면 (with the android:defaultValue attribute) 시스템은 값을 가져오기 위해 해당 object를 instantiate할 때 onGetDefaultValue() 를 호출할 것입니다.  이 메스드를 반드시 implement 해서 시스템이 이 default value를 SharedPreferences 파일에 저장할 수 있도록 해야 합니다.


@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
    return a.getInteger(index, DEFAULT_VALUE);
}


이 method argument들은 여러분이 필요로 하는 모든 것들을 제공 합니다. : the array of attributes and the index position of the android:defaultValue, 여러분이 반드시 retrieve 해야 할 것들이죠. 이 메소드를 attribute에서 디폴트 값을 추출하기 위해 반드시 implement 해야 하는 이유는 해당 값이 undefined 됐을 경우 attribute에 대해 local default 값을 specify 해야만 하기 때문입니다.



Saving and restoring the Preference's state


layout 의 View 처럼, 여러분의 Preference subclass는 액티비티나 fragment 가 restart 되는 경우 그 상태를 save 하거나 restoring 해야 하기 때문입니다. (사용자가 화면을 rotate 시켰을 경우). Preference class의 상태를 정확하게 저장하거나 restore 하기 위해서는 lifecycle callback 메소드인 onSaveInstanceState() and onRestoreInstanceState()를 호출 해야만 합니다.

Preference의 state는 Parcelable interface 를 implement 한 object 에 의해 정의 됩니다. 안드로이드 프레임워크는 여러분의 state object를 정의하기 위한 starting point로서 이런 object를 제공합니다.

어떻게 이 Preference class 가 그 상태를 저장하는지 정의하기 위해 Preference.BaseSavedState class 를 extend 해야 합니다. 그리고 몇개의 메소드들을 override 하고 CREATOR 객체를 정의합니다.

대부분의 앱들은, 아래와 같이 implementation을 사용할 수 있습니다. Integer 가 아닌 경우 아래 에제에서 몇 줄만 바꿔서 사용하시면 됩니다.



private static class SavedState extends BaseSavedState {
    // Member that holds the setting's value
    // Change this data type to match the type saved by your Preference
    int value;

    public SavedState(Parcelable superState) {
        super(superState);
    }

    public SavedState(Parcel source) {
        super(source);
        // Get the current preference's value
        value = source.readInt();  // Change this to read the appropriate data type
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        super.writeToParcel(dest, flags);
        // Write the preference's value
        dest.writeInt(value);  // Change this to write the appropriate data type
    }

    // Standard creator object using an instance of this class
    public static final Parcelable.Creator<SavedState> CREATOR =
            new Parcelable.Creator<SavedState>() {

        public SavedState createFromParcel(Parcel in) {
            return new SavedState(in);
        }

        public SavedState[] newArray(int size) {
            return new SavedState[size];
        }
    };
}

Preference.BaseSavedState implementation을 여러분 앱에 추가 했다면 (usually as a subclass of your Preference subclass), 여러분은 Preference subclass에 대해 onSaveInstanceState() and onRestoreInstanceState()를 implement 합니다.


For example:


@Override
protected Parcelable onSaveInstanceState() {
    final Parcelable superState = super.onSaveInstanceState();
    // Check whether this Preference is persistent (continually saved)
    if (isPersistent()) {
        // No need to save instance state since it's persistent,
        // use superclass state
        return superState;
    }

    // Create instance of custom BaseSavedState
    final SavedState myState = new SavedState(superState);
    // Set the state's value with the class member that holds current
    // setting value
    myState.value = mNewValue;
    return myState;
}

@Override
protected void onRestoreInstanceState(Parcelable state) {
    // Check whether we saved the state in onSaveInstanceState
    if (state == null || !state.getClass().equals(SavedState.class)) {
        // Didn't save the state, so call superclass
        super.onRestoreInstanceState(state);
        return;
    }

    // Cast state to custom BaseSavedState and pass to superclass
    SavedState myState = (SavedState) state;
    super.onRestoreInstanceState(myState.getSuperState());

    // Set this Preference's widget to reflect the restored state
    mNumberPicker.setValue(myState.value);
}

저작자 표시 비영리 동일 조건 변경 허락
신고

[Android] Settings - 2 -

2016.10.10 09:07 | Posted by 솔웅


Settings 2


Using Preference Headers

드문 경우 이지만 세팅 화면을 제일 처음에 보여야 할 때도 있을 것입니다. (예 figures 4 and 5). 안드로이드 3.0 이상에서는 headers 기능을 사용해야 합니다. (nested preferenceScreen element로 subscreen들을 구현하는 대신에...)

To build your settings with headers, you need to:
headers를 이용해 세팅을 만들려면

    세팅의 각 그룹을 구분해 PreferenceFragment의 별도의 instance들로 만듭니다. 즉 각 그룹들은 별도의 XML 파일로 나눠져야 한다는 의미입니다.
    각 세팅 그룹별로 XML headers 파일을 생성합니다. 그리고 해당 세팅을 갖고 있는 fragment를 정의합니다.
    setting을 host 하기 위해 PreferenceActivity class 를 extend 합니다.
    headers file을 지정하기 위해 onBuildHeaders() callback을 implement 합니다.

이 디자인을 사용하는 가장 큰 잇점은 큰 화면에서 작동할 시 PreferenceActivity 가 2개의 pane layout으로 자동적으로 표현한다는 겁니다. (figure 4)

안드로이드 3.0 이전의 버전에서도 이 PreferenceFragment 를 사용해 2개의 pane을 표시할 수 있습니다. (see the section about Supporting older versions with preference headers)



Figure 4. Two-pane layout with headers.



1. The headers are defined with an XML headers file.
2. Each group of settings is defined by a PreferenceFragment that's specified by a <header> element in the headers file.





Figure 5. A handset device with setting headers. When an item is selected, the associated PreferenceFragment replaces the headers.



Creating the headers file



headers list에 있는 세팅 그룹은 root <preference-headers> element 안에 하나의 <header> element 로 정의 됩니다.



예제.

<?xml version="1.0" encoding="utf-8"?>
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
    <header
        android:fragment="com.example.prefs.SettingsActivity$SettingsFragmentOne"
        android:title="@string/prefs_category_one"
        android:summary="@string/prefs_summ_category_one" />
    <header
        android:fragment="com.example.prefs.SettingsActivity$SettingsFragmentTwo"
        android:title="@string/prefs_category_two"
        android:summary="@string/prefs_summ_category_two" >
        <!-- key/value pairs can be included as arguments for the fragment. -->
        <extra android:name="someKey" android:value="someHeaderValue" />
    </header>
</preference-headers>


android:fragment attribute와 함께 각 header 는 PreferenceFragment 의 instance 를 정의 합니다. 사용자가 header를 선택했을 때 open 되는 대상이죠.



<extras> element는 key-value 값을 fragment로 pass 할 수 있도록 해 줍니다. ( in a Bundle). 이 fragment는 getArguments()를 호출함으로서 arguments들을 수집할 수 있습니다. argument들을 fragment로 pass 해야 하는 이유는 여러가지가 있을 수 있습니다. 그 중 하나는 PreferenceFragment 의 같은 subclass를 각 그룹에 대해 reuse 할 수 있다는 겁니다. 그리고 이 argument를 어떤 fragment의 preference XML 파일을 load 해야 하는지 특정할 수 있습니다.



예를 들어, 아래를 보면 여러개의 settings group들에서 reuse 될 수 있는 fragment가 있습니다. 각 header가 <extra> argument를 "settings" key와 함께 정의하고 있습니다. :



public static class SettingsFragment extends PreferenceFragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        String settings = getArguments().getString("settings");
        if ("notifications".equals(settings)) {
            addPreferencesFromResource(R.xml.settings_wifi);
        } else if ("sync".equals(settings)) {
            addPreferencesFromResource(R.xml.settings_sync);
        }
    }
}





Displaying the headers


preference headers를 표시하려면 onBuildHeaders() callback 메소드를 implement 해야 합니다. 그리고 loadHeaderFromResource() 를 호출합니다.


예제


public class SettingsActivity extends PreferenceActivity {
    @Override
    public void onBuildHeaders(List<Header> target) {
        loadHeadersFromResource(R.xml.preference_headers, target);
    }
}



headers 의 리스트에서 사용자가 한 아이템을 선택했을 때 시스템은 해당 PreferenceFragment를 open 합니다.


Note : preference headers를 사용 할 때 PreferenceActivity의 subclass는 onCreate() 메소드를 implement 할 필요가 없습니다. 이 activity 에서 필요한 작업은 headers를 load 하는 것이기 때문입니다.



Supporting older versions with preference headers



앱이 Android 3.0 이전의 버전을 지원 할 때에도 headers를 사용해 Android 3.0 이사의 버전에서 작동할 때 2개의 pane layout을 제공할 수 있습니다. 이렇게 하려면 추가적인 preferences XML을 만들어야 하는데요 basic <Preference> element를 사용해서 header items 처럼 사용되도록 하면 됩니다. (오래된 안드로이드 버전에서 사용되도록 하기 위해)

새로운 PreferenceScreen을 여는 대신 각 <Preference> element가 PreferenceActivity에 Intent를 보냅니다. (어떤 XML file이 로드되는지를 지정합니다.)



예제) preference headers 를 위한 XML 파일입니다. 안드로이드 3.0 이상에서 사용됩니다. (res/xml/preference_headers.xml):



<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
    <header
        android:fragment="com.example.prefs.SettingsFragmentOne"
        android:title="@string/prefs_category_one"
        android:summary="@string/prefs_summ_category_one" />
    <header
        android:fragment="com.example.prefs.SettingsFragmentTwo"
        android:title="@string/prefs_category_two"
        android:summary="@string/prefs_summ_category_two" />
</preference-headers>



안드로이드 3.0 이하 버전들에 같은 headers를 제공하기 위한 preference file 입니다.(res/xml/preference_headers_legacy.xml):


<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <Preference
        android:title="@string/prefs_category_one"
        android:summary="@string/prefs_summ_category_one"  >
        <intent
            android:targetPackage="com.example.prefs"
            android:targetClass="com.example.prefs.SettingsActivity"
            android:action="com.example.prefs.PREFS_ONE" />
    </Preference>
    <Preference
        android:title="@string/prefs_category_two"
        android:summary="@string/prefs_summ_category_two" >
        <intent
            android:targetPackage="com.example.prefs"
            android:targetClass="com.example.prefs.SettingsActivity"
            android:action="com.example.prefs.PREFS_TWO" />
    </Preference>
</PreferenceScreen>


<preference-headers> 를 지원하는 것은 안드로이드 3.0에서 추가 되었기 때문에 시스템은 안드로이드 3.0 이상에서 작동 될 때에만 PreferenceActivity 안의 onBuildHeaders()를 호출합니다. legacy headers file을 로딩하려면 (preference_headers_legacy.xml) 우선 안드로이드 버전을 체크 합니다. 안드로이드 3.0 (HONEYCOMB)보다 오래 됐으면 addPreferencesFromResource() 를 호출해서 legacy header file을 로딩합니다.



예제


@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ...

    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
        // Load the legacy preferences headers
        addPreferencesFromResource(R.xml.preference_headers_legacy);
    }
}

// Called only on Honeycomb and later
@Override
public void onBuildHeaders(List<Header> target) {
   loadHeadersFromResource(R.xml.preference_headers, target);
}



이제 남은 것은 어떤 preference file을 load 해야 하는지를 지정하는 activity로 pass 된 Intent를 처리하는 것입니다. 그 intent의 action을 retrieve 하고 preference XML의 <intent> 태그에서 사용돼 알고 있는 action string과 비교 합니다.

final static String ACTION_PREFS_ONE = "com.example.prefs.PREFS_ONE";
...

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    String action = getIntent().getAction();
    if (action != null && action.equals(ACTION_PREFS_ONE)) {
        addPreferencesFromResource(R.xml.preferences);
    }
    ...

    else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
        // Load the legacy preferences headers
        addPreferencesFromResource(R.xml.preference_headers_legacy);
    }
}


addPreferencesFromResource()에 대한 연속적인 호출은 single list안의 모든 preference들이 stack 되게 할 겁니다. 그러니 if-else 구문을 사용해서 오직 한번만 호출되도록 하세요.





Reading Preferences



디폴트로 preference들은 여러분 앱 내부 어디에서든지 접근 가능한 파일에 저장됩니다. 접근은 PreferenceManager.getDefaultSharedPreferences() 라는 static method를 호출함으로서 이뤄집니다. 이 메소드는 SharedPreferences 객체를 return 하는데요 여기에는 PreferenceActivity에서 사용된 Preference 객체들과에 상응하는 key-value 값들이 포함돼 있습니다.



예제

SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
String syncConnPref = sharedPref.getString(SettingsActivity.KEY_PREF_SYNC_CONN, "");



Listening for preference changes

사용자가 preference를 변경하자 마자 통보를 받아야 할 때가 있을 수 있습니다. 이렇게 변화가 있었을 때 callback을 받으려면 SharedPreference.OnSharedPreferenceChangeListener 인터페이스를 implement 합니다. 그리고 그 리스너를 registerOnSharedPreferenceChangeListener() 를 호출함으로서 SharedPreferences 객체에 등록해 놓습니다.

이 인터페이스는 한개의 callback 메소드를 가지고 있습니다. onSharedPreferenceChanged() 메소드 입니다.



예제



public class SettingsActivity extends PreferenceActivity
                              implements OnSharedPreferenceChangeListener {
    public static final String KEY_PREF_SYNC_CONN = "pref_syncConnectionType";
    ...

    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
        String key) {
        if (key.equals(KEY_PREF_SYNC_CONN)) {
            Preference connectionPref = findPreference(key);
            // Set summary to be the user-description for the selected value
            connectionPref.setSummary(sharedPreferences.getString(key, ""));
        }
    }
}


이 예제에서는 이미 알고 있는 preference key 에 대해 체크하고 있습니다. findePreference() 메소드를 호출해서 Preference 객체를 get 합니다. 이 preference는 변경된 preference 입니다. 그래서 그 아이템의 해당 summary로 내용을 업데이트 할 수 있습니다. List나 다른 multiple choice setting 인 경우에는 현재의 상태를 표시하기 위해 setSummary()를 호출해야 할 것입니다. such as the Sleep setting shown in figure 5)

Note : Android Design document에 Settings에 대해 나와 있듯이 사용자가 preference를 변경할 때마다 ListPreference의 Summary를 업데이트 해 줄 것을 권장합니다

Activit의 제대로 된 lifecycle management를 위해 SharedPreferences.OnSharedPreferenceChangeListeneronResume(), onPause() callback에서 사용할 것을 권장합니다.



@Override
protected void onResume() {
    super.onResume();
    getPreferenceScreen().getSharedPreferences()
            .registerOnSharedPreferenceChangeListener(this);
}

@Override
protected void onPause() {
    super.onPause();
    getPreferenceScreen().getSharedPreferences()
            .unregisterOnSharedPreferenceChangeListener(this);
}



주의 : registerOnSharedPreferenceChangeListener()를 호출할 때 preference manager는 strong reference 를 listener에 저장하지 않습니다. 여러분이 직접 저장해야 합니다. 그렇지 않으면 garbage collection으로 가게 될 것입니다. 그 reference를 객체의 instance data 안의 리스너에 보관할 것을 권장합니다. 그러면 리스너를 필요로 하는 한 존재하게 할 수 있습니다.



아래 예제에서는 caller가 리스너에 reference를 keep 하지 않습니다. 그래서 이것은 garbage collection으로 가게 될 겁니다. 그러면 언젠가 앱의 독장이 fail 될 겁니다.


prefs.registerOnSharedPreferenceChangeListener(
  // Bad! The listener is subject to garbage collection!
  new SharedPreferences.OnSharedPreferenceChangeListener() {
  public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    // listener implementation
  }
});

대신에 객체의 인스턴스 데이터 필드안에 있는 리스너에 reference를 저장하면 해당 리스너가 필요할 때 까지 계속 존재하게 될 겁니다.

SharedPreferences.OnSharedPreferenceChangeListener listener =
    new SharedPreferences.OnSharedPreferenceChangeListener() {
  public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    // listener implementation
  }
};
prefs.registerOnSharedPreferenceChangeListener(listener);


Managing Network Usage

안드로이드 4.0 부터 Settings application은 사용자에게 앱들이 얼마나 많은 네트워크 데이터를 사용했는지를 보여줍니다. 사용자는 이것을 보고 각각의 앱에 대해 background data 사용을 금지하는 등의 조치를 할 수 있습니다. 그러니까 사용자가 여러분의 앱의 데이터 access를 잠그지 않도록 하기 위해서는 데이터를 가급적 적게 효율적으로 사용하도록 개발 하여야 합니다.

예를 들어 얼마나 자주 데이터를 동기화 하는지 혹은 Wi-Fi 하에서만 파일 업로드 다운로드를 가능하도록 하던지 혹은 로밍시에만 가능하도록 하는 등등의 기능을 넣어서 데이터 사용을 줄일 수 있습니다.


Once you've added the necessary preferences in your PreferenceActivity to control your app's data habits, you should add an intent filter for

PreferenceActivity에 필요한 preference들을 추가 했다면 다음은 intent filter를 추가해야 합니다.

manifest파일의 ACTION_MANAGE_NETWORK_USAGE 에 추가 합니다.


<activity android:name="SettingsActivity" ... >
    <intent-filter>
       <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
       <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

이 intent filter는 이 activity는 어플리케이션의 데이터를 컨트롤 한다는 것을 알려 줍니다.

저작자 표시 비영리 동일 조건 변경 허락
신고



Lesson 3의 나머지 부분에서는 Intent에 대한 설명이 나옵니다.
Intent 는 다른 Activity를 실행시키는 명령어로 같은 앱 내의 Activity 이외에 다른 앱의 액티비티를 실행 시킬 수 있습니다.
그러니까 내 앱을 사용하다가 전화거는 서비스를 제공해야 하면 Intent를 사용해서 다른 전화거는 앱 즉 안드로이드 자체에 실려있는 전화거는 앱을 이 Intent를 사용해서 실행하면 됩니다
문자메세지, 바코드 스캐닝, 카메라 등등의 기능도 그냥 다른 앱을 불러서 사용하도록 하면 됩니다.

이 때 Intent를 사용하면 Activity의 정확한 이름 즉 클래스 이름을 제공하는데 다른 앱의 액티비티 클래스 이름을 알 수는 없습니다.
이럴 때 Implicit Intents를 사용할 수 있습니다.

안드로이드는 이 Implicit Intents에서 사용한 이름의 액티비티가 여러개 있을 때 그 중 하나를 선택할 수 있는 메뉴를 보여 줍니다.

실습을 해 보겠습니다.

지금까지 만든 일기예보 앱에 Map을 보여주는 기능을 추가 할 겁니다.
지도는 구글맵을 불러와서 해당 지역을 그 구글 맵에 표시하도록 할 겁니다.

우선 menu option을 추가해서 지도에 사용자가 정한 지역을 표시하도록 할 겁니다.


res-menu-main.xml 파일에 위와 같이 지도를 위한 메뉴 아이템을 추가 합니다.



그리고 MainActivity에 아래와 같이 openPreferredLocationInMap()을 추가 합니다.

    private void openPreferredLocationInMap() {
        SharedPreferences sharedPrefs =
                PreferenceManager.getDefaultSharedPreferences(this);
        String location = sharedPrefs.getString(
                getString(R.string.pref_location_key),
                getString(R.string.pref_location_default));

        // Using the URI scheme for showing a location found on a map.  This super-handy
        // intent can is detailed in the "Common Intents" page of Android's developer site:
        // http://developer.android.com/guide/components/intents-common.html#Maps
        Uri geoLocation = Uri.parse("geo:0,0?").buildUpon()
                .appendQueryParameter("q", location)
                .build();

        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setData(geoLocation);

        if (intent.resolveActivity(getPackageManager()) != null) {
            startActivity(intent);
        } else {
            Log.d(LOG_TAG, "Couldn't call " + location + ", no receiving apps installed!");
        }
    }


내용을 보면 SharedPreference를 불러오고 location 값을 get합니다.

그리고 URI에 이 location값을 넣어 줍니다.
우편번호로 지도상의 위도와 경도를 가져와서 URI에 넣어 줍니다.
이 부분을 자세히 보시려면 이곳에 가시면 됩니다.
http://developer.android.com/guide/components/intents-common.html#Maps

그리고 Intent에서 Intent.ACTION_VIEW 라는 Implicit Intent를 사용합니다.

그리고 이 intent에 Uri (위도와 경도가 있는)를 setting 합니다.
그러면 이 위도와 경도 정보를 Intent.ACTION_VIEW라는 액티비티로 넘겨 줄 준비가 돼어 있는 겁니다.

그리고 if 문에서 resolveActivity를 사용해서 이 Implicit Intent에 해당하는 액티비티가 있는 지 확인합니다.
있으면 startActivity를 하고 없으면 로그를 뿌려 줍니다.


그리고 나서  onOptionsItemSelected()에 아래와 같이 map 관련 부분을 추가 합니다.
 
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // 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.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            startActivity(new Intent(this, SettingsActivity.class));
            return true;
        }

        if (id == R.id.action_map) {
            openPreferredLocationInMap();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

이제 Setting에서 Map Location을 누르면 해당 지역의 위도 경도가 구글맵 애플리케이션에 전달되서 구글맵 앱에 해당 지역 위치가 보일 겁니다.



참고로 Share Intent 에 대해 자세히 보려면 이곳으로 가면 됩니다.
https://developer.android.com/training/sharing/shareaction.html

Lesson 3의 다음 부분을 보면 Detail 화면에서 날씨를 Share 하거나 문자 메세지로 보내는 기능이 추가 됩니다.

이 때 Intent Share의 Share Provider를 사용합니다.


이 기능을 구현하는 순서는 우선 res-menu 에 detailfragment.xml을 아래와 같이 만듭니다.
여기서 ShareActionProvider를 사용했습니다.

<?xml version="1.0" encoding="utf-8"?>

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/action_share"
        android:title="@string/action_share"
        app:showAsAction="always"
        app:actionProviderClass="android.support.v7.widget.ShareActionProvider" />
</menu>

그 다음 DetailActivity 클래스로 갑니다.

그리고 나서 DetailFragment 클래스를 아래와 같이 업데이트 합니다.

   /**
     * A placeholder fragment containing a simple view.
     */
    public static class DetailFragment extends Fragment {

        private static final String LOG_TAG = DetailFragment.class.getSimpleName();

        private static final String FORECAST_SHARE_HASHTAG = " #SunshineApp";
        private String mForecastStr;

        public DetailFragment() {
            setHasOptionsMenu(true);
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {

            View rootView = inflater.inflate(R.layout.fragment_detail, container, false);

            // The detail Activity called via intent.  Inspect the intent for forecast data.
            Intent intent = getActivity().getIntent();
            if (intent != null && intent.hasExtra(Intent.EXTRA_TEXT)) {
                mForecastStr = intent.getStringExtra(Intent.EXTRA_TEXT);
                ((TextView) rootView.findViewById(R.id.detail_text))
                        .setText(mForecastStr);
            }

            return rootView;
        }

        @Override
        public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
            // Inflate the menu; this adds items to the action bar if it is present.
            inflater.inflate(R.menu.detailfragment, menu);

            // Retrieve the share menu item
            MenuItem menuItem = menu.findItem(R.id.action_share);

            // Get the provider and hold onto it to set/change the share intent.
            ShareActionProvider mShareActionProvider =
                    (ShareActionProvider) MenuItemCompat.getActionProvider(menuItem);

            // Attach an intent to this ShareActionProvider.  You can update this at any time,
            // like when the user selects a new piece of data they might like to share.
            if (mShareActionProvider != null ) {
                mShareActionProvider.setShareIntent(createShareForecastIntent());
            } else {
                Log.d(LOG_TAG, "Share Action Provider is null?");
            }
        }

        private Intent createShareForecastIntent() {
            Intent shareIntent = new Intent(Intent.ACTION_SEND);
            shareIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
            shareIntent.setType("text/plain");
            shareIntent.putExtra(Intent.EXTRA_TEXT,
                    mForecastStr + FORECAST_SHARE_HASHTAG);
            return shareIntent;
        }
    }
   
일단 onCreateView 에서 mForecastStr 라는 member variable 을 사용해서 text를 세팅했습니다.

그리고 createShareForecastIntent()를 만듭니다.
여기서는 문자메세지를 보내기 위해 필요한 세팅들을 합니다.
putExtra를 사용해서 내용을 추가하는데 그 내용은 해당 detail 페이지에 있는 날씨 정보 입니다.
그리고 이 shareIntent를 return 합니다.

이 클래스는 onCreateOptionsMenu에서 해당 아이콘이 클릭되면 호출 됩니다.

이제 Detail 화면에서 메세지 아이콘을 누르면 아래와 같이 문자메세지 앱이 실행됩니다.



그 이외에 Broadcast Intent 가 있습니다.

예를 들어 핸드폰이 충전되고 있는지를 Broadcast해 주는데요.  이 Broadcast되는 정보를 받아서 앱에서 활용할 수 있습니다.

ACTION_POWER_CONEECTED 이벤트를 사용하고 리시버는 Manifest Receiver를 사용해서 구현합니다.

자세한 내용은 이곳을 참고 하세요.

https://developer.android.com/reference/android/content/BroadcastReceiver.html



저작자 표시 비영리 동일 조건 변경 허락
신고


이번엔 Settings 를 꾸밀 겁니다.


Setting 관련 한 Android Developer API Guide는 여기에 있습니다.

SharedPreference API


이 Sunshine  앱에서는 Setting UX 가 아래와 같이 진행 될 겁니다.



우편번호를 입력하는 화면과 화씨/섭씨 를 선택할 수 있는 기능을 제공할 겁니다.


먼저 SettingsActivity를 생성합니다.


안드로이드 버전에 따라 PreferenceActivity와 PreferenceFragment를 사용합니다.



이렇게 SettingsActivity.java  를 생성합니다.


public class SettingsActivity extends PreferenceActivity
        implements Preference.OnPreferenceChangeListener {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Add 'general' preferences, defined in the XML file
        addPreferencesFromResource(R.xml.pref_general);

        // For all preferences, attach an OnPreferenceChangeListener so the UI summary can be
        // updated when the preference changes.
        bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_location_key)));
        bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_units_key)));
    }

    /**
     * Attaches a listener so the summary is always updated with the preference value.
     * Also fires the listener once, to initialize the summary (so it shows up before the value
     * is changed.)
     */
    private void bindPreferenceSummaryToValue(Preference preference) {
        // Set the listener to watch for value changes.
        preference.setOnPreferenceChangeListener(this);

        // Trigger the listener immediately with the preference's
        // current value.
        onPreferenceChange(preference,
                PreferenceManager
                        .getDefaultSharedPreferences(preference.getContext())
                        .getString(preference.getKey(), ""));
    }

    @Override
    public boolean onPreferenceChange(Preference preference, Object value) {
        String stringValue = value.toString();

        if (preference instanceof ListPreference) {
            // For list preferences, look up the correct display value in
            // the preference's 'entries' list (since they have separate labels/values).
            ListPreference listPreference = (ListPreference) preference;
            int prefIndex = listPreference.findIndexOfValue(stringValue);
            if (prefIndex >= 0) {
                preference.setSummary(listPreference.getEntries()[prefIndex]);
            }
        } else {
            // For other preferences, set the summary to the value's simple string representation.
            preference.setSummary(stringValue);
        }
        return true;
    }

}


PreferenceActivity를 extends 하고 Preference.OnPreferenceChangeListener 를 implements 합니다.

처음 onCreate() 메소드에서는 해당 preference xml을 불러옵니다.
그리고 나서 Listener를 구현해서 해당 Preference에 어떤 변화가 있을 때 UI Summary를 update 할 수 있도록 합니다.

그 구체적인 내용은 bindPreferenceSummaryToValue()에 구현해 놓습니다.

그리고 Override한 onPreferenceChange()메소드가 있습니다.

여기에서는 ListPreference일 경우 해당하는 아이템을 고르도록 하고 그렇지 않은 경우는 그냥 Summary를 가져 오도록 합니다.

이렇게 되면 사용자가 우편번호를 바꾸거나 섭씨/화씨 조건을 바꿨을 때 그에 해당하는 값을 가져올 수 있게 됩니다.


이렇게 SettingsActivity  클래스를 만들었으면 이를 AndroidManifest에 등록해야 합니다.


        <activity
            android:name=".SettingsActivity"
            android:label="@string/title_activity_settings"
            android:parentActivityName=".MainActivity" >
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value="com.example.android.sunshine.app.MainActivity" />
        </activity>


label은  string xml에 지정해 놓은 값을 사용합니다.

그리고 parentActivityName은 MainActivity로 합니다.

이렇게 되면 Back  버튼을 누르게 되면 MainActivity로 가게 됩니다.


이제 MainActivity에서도 약간 손을 좀 봐야 하는데요.


onOptionsItemSelected()를 아래와 같이 코딩합니다.


    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // 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.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            startActivity(new Intent(this, SettingsActivity.class));
            return true;
        }

        return super.onOptionsItemSelected(item);
    }


Option item이 선택 됐을 경우 이것이 action_settings일 경우 SettingsActivity를 실행하도록 Intent를 사용합니다.


여기까지 하면 MainActivity에서 Setting를 선택하면 SettingsActivity  화면으로 넘어가게 됩니다.

아직 빈화면만 보일 겁니다.


DetailActivity에서도 이 onOptionsItemSelected()를 똑 같이 구현해 놓으면 이곳에서도 Settings를 선택하면 SettingsActivity화면으로 넘어가도록 할 수 있습니다.


아직 SettingsActivity는 빈화면이니까 이 화면을 구현할 차례입니다.


일단 화면의 Layout을 잡아줍니다.


그러려면 res에 xml폴더를 생성해서 아래와 같이 pref_general.xml을 만듭니다.



한개의 EditTextPreference와 한개의 ListPreference를 만들었습니다.


참고로 strings.xml에는 아래와 같은 내용들이 있습니다.


  <!-- Label for the location preference [CHAR LIMIT=30] -->
    <string name="pref_location_label">Location</string>

    <!-- Key name for storing location in SharedPreferences [CHAR LIMIT=NONE] -->
    <string name="pref_location_key" translatable="false">location</string>

    <!-- Default postal code for location preference [CHAR LIMIT=NONE] -->
    <string name="pref_location_default" translatable="false">55347</string>

    <!-- Label for the temperature units preference [CHAR LIMIT=30] -->
    <string name="pref_units_label">Temperature Units</string>

    <!-- Label for metric option in temperature unit preference [CHAR LIMIT=25] -->
    <string name="pref_units_label_metric">Imperial</string>

    <!-- Label for imperial option in temperature unit preference [CHAR LIMIT=25] -->
    <string name="pref_units_label_imperial">Imperial</string>

    <!-- Key name for temperature unit preference in SharedPreferences [CHAR LIMIT=NONE] -->
    <string name="pref_units_key" translatable="false">units</string>

    <!-- Value in SharedPreferences for metric temperature unit option [CHAR LIMIT=NONE] -->
    <string name="pref_units_metric" translatable="false">metric</string>

    <!-- Value in SharedPreferences for imperial temperature unit option [CHAR LIMIT=NONE] -->
    <string name="pref_units_imperial" translatable="false">imperial</string>



SettingsActivity에 보시면 addPreferencesFromResource(R.xml.pref_general); 이있습니다.

방금 만든 pref_general을 불러오는 부분이죠.


그리고 나서 아래와 같이 리스너를 물려 줍니다.

        bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_location_key)));
        bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_units_key)));


이미 위 SettingsActivity에서 다 구현해 놨던 겁니다.



여기서 사용자가 우편번호를 입력하면 그 값은 자동으로 SharedPreference에 저장이 됩니다.

그래서 날씨를 표시할 때 사용자가 입력한 우편번호를 SharedPreference에서 가져와서 그 지역 날씨를 표시하게 되죠.

만약 사용자가 입력한 우편번호가 없다면 저 strings.xml에 있는 default값인 55347을 사용하게 됩니다.


이것은 ForecastFragment에 있는 onOptionsItemSelected() 를 아래와 같이 코딩하면 구현할 수 있습니다.


    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // 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.
        int id = item.getItemId();
        if (id == R.id.action_refresh) {
            updateWeather();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }


옵현 아이템이 선택되면 그 ID 가 refresh 이면 updateWeather()를 돌리도록 합니다.


    private void updateWeather() {
        FetchWeatherTask weatherTask = new FetchWeatherTask();
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
        String location = prefs.getString(getString(R.string.pref_location_key),
                getString(R.string.pref_location_default));
        weatherTask.execute(location);
    }


updateWeather에서는 location 값을 받아서 weatherTask.execute()에 넘겨 줍니다.


이 weatherTask는 이전에 구현해 놓은 FetchWeatherTask로 웹사이트에 날씨를 요청하고 받아서 이를 뿌려주도록 합니다.


참고로 ForecaseFragment 클래스의 onStart()를 아래와 같이 구현합니다.


    @Override
    public void onStart() {
        super.onStart();
        updateWeather();
    }


그러면 앱이 처음 실행할 때도 디폴트 값을 가져와서 그 지역의 날씨를 표시하게 됩니다.


이제 섭씨/화씨를 선택하는 부분을 볼건데요.


위에 소스코드에도 있듯이 여기서는 ListPreference를 사용할 겁니다.


참고로 imperial은 화씨를 말하고 metric은 섭씨를 말합니다.


T



emperature Units  부분은 stings.xml을 변경하는 과정에서 실수가 있어서 Imperial만 뜨네요.

다시 strings.xml을 수정해야 겠습니다.


일단 Settings 와 관련 된 부분은 모두 마쳤습니다.


여기까지의 소스코드는 아래에서 받을 수 있습니다.

https://github.com/udacity/Sunshine-Version-2/tree/3.11_add_units_setting




저작자 표시 비영리 동일 조건 변경 허락
신고

[Android] Settings - 1 -

2016.09.26 04:39 | Posted by 솔웅


Settings

앱에는 사용자가 기능 선택을 할 수 있는 세팅 메뉴를 가지고 있는 경우가 많다. 예를 들어 어떤 앱은 notification 가능 여부를 사용자가 선택할 수 있도록 하기도 하고 클라우드에 동기화 되는 주가 등을 설정하도록 하기도 합니다.
이 세팅 메뉴를 넣으려면 안드로이드의 Preference API를 사용해서 만들어야 합니다.

Setting Design


세팅을 어떤 모양으로 개발하느냐 하는 것은 Settings design guide를 살펴 보세요.





Figure 1. Screenshots from the Android Messaging app's settings. Selecting an item defined by a Preference opens an interface to change the setting.


안드로이드의 메세지 앱의 세팅 화면 입니다. 아이템을 누르면 지정된 Preference 의 세팅 화면이 열립니다.



Overview



User Interface를 위해 View object를 사용하는 대신 Settings는 XML 파일에 정의하도록 하는 이미 만들어진 Preference 클래스의 여러가지 subclass 등을 사용해서 표현 합니다.
Preference 객체는 한개의 세팅에 대한 building block 입니다. 각각의 Preference는 리스의 한 아이템으로 나타나고 사용자에게 해당 UI를 제공해 사용자가 setting을 modify 할 수 있도록 합니다.
예를 들어 CheckBoxPreference 는 체크박스를 표시하는 리스트 아이템을 생성합니다. 그리고 ListPreference는 여러개 중에 하나를 선택할 수 있는 리스트와 함께 dialog를 여는 아이템을 만듭니다.

Preference는 key-value 의 쌍으로 이뤄져 있고 이를 default SharedPreference file에 세팅된 내용을 저장하도록 합니다.
사용자가 세팅을 바꾸면 시스템은 이 SharedPreference 파일의 해당 내용을 업데이트 합니다. 이 SharedPreferences 파일을 직접 접근해서 다루는 것은 세팅된 내용을 read 해야 할 때 입니다.

각 세팅에 대해 SharedPreferences에 저장된 값들은 아래 data type들로 이뤄져 있습니다.

    Boolean
    Float
    Int
    Long
    String
    String Set

여러분 앱의 세팅 UI는 Preference 객체를 사용해 만들어 졌기 때문에 (View 객체가 아니라) 특별한 Activity나 Fragment subclass를 사용해서 이 리스트 세팅을 표시해야 합니다.

- 안드로이드 버전 3.0 미만 (API level 10 미만) 에서는 액티비티를 반드시 PreferenceActivity 클래스의 extension으로 build 해야 합니다.
- 안드로이드 버전 3.0 이상 에서는 PreferenceFragment를 가지고 있는 traditional Activity를 사용해서 앱 세팅을 표시해야 합니다. 여러 그룹의 세팅이 있다면 큰 화면을 둘로 나눠 이를 표시하기 위해 PreferenceActivity를 사용할 수도 있습니다.

어떻게 PreferenceActivityPreferenceFragment를 세팅하느냐 하는것은 Creating a Preference Activity Using Preference Fragment 에 대해 설명할 때 다를 겁니다.



Preference



앱에서 사용되는 세팅은 Preference 클래스의 subclass 를 사용해서 표현합니다. 각각의 sub 클래스는 property들의 세트로 이뤄져 있어 세팅의 타이틀과 디폴트값 같은 것들을 특정할 수 있도록 합니다.
각각의 subclass는 각자 자신들의 특징을 나타내는 프로퍼티와 인터페이스들을 제공합니다. 예를 들어 figure 1은 메세징 앱의 세팅에서 화면을 캡쳐한 것입니다 세팅 화면에 있는 각 리스트 아이템은 각기 다른 Preference 객체들을 지원하고 있습니다.

일반적으로 사용하는 preference들 몇개를 보면

 
CheckBoxPreference
    Shows an item with a checkbox for a setting that is either enabled or disabled. The saved value is a boolean (true if it's checked).


ListPreference
    Opens a dialog with a list of radio buttons. The saved value can be any one of the supported value types (listed above).


EditTextPreference
    Opens a dialog with an EditText widget. The saved value is a String.
   
   
다른 subclass들과 거기에 속하는 프로퍼티들을 보시려면 Preference 클래스를 참조하세요.

물론 이렇게 제공되는 클래스들이 모든것들을 커버하지는 못합니다. 어떤 경우에는 다른 특정한 형태가 필요할 수 있습니다. 예를 들어 숫자나 날짜를 picking 하는 preference 클래스는 제공되지 않습니다. 그래서 여러분만의 Preference Subclass를 만들어야 될 수도 있습니다. 이것에 대해 알고 싶으시면 Building a Custom Preference 섹션을 보세요.



Defining Preferences in XML



runtime에 Preference object를 instantiate 할수도 있지만 세팅 리스트는 XML에 Preference objects 의 hierarchy 와 함께 정의 해 놔야 합니다.
settings의 collection을 정의하기 위해 XML 파일을 사용하는 것이 더 좋습니다. 왜냐하면 이렇게 하면 쉽게 읽을 수 있고 update 하기도 간단하기 때문이죠.
또한 세팅은 일반적으로 pre-determined 됩니다. 그리고 runtime 때 이 collection들을 수정할 수도 있습니다.

Preference subclass는 클래스 이름을 사용해서 XML element로 정의 될 수 있습니다. i.e. <CheckBoxPreference>
이 XML 파일은 res/xml 디렉토리에 저장해야만 합니다. 파일 이름은 마음대로 해도 되지만 일반적으로 preferences.xml 로 합니다.
대개 1개의 파일만 필요합니다. 왜냐하면 hierarchy의 branch 들은 PreferenceScreen 대신 nested instance들에 의해 정의되기 때문입니다.

Note : 세팅에 multi-pane layout을 만드려면 각 fragment당 별도의 XML 파일을 말들어야 합니다.

XML 파일의 root node는 반드시 element 여야 합니다. 이 element 안에 각각의 Preference를 추가 합니다. <PreferenceScreen> element 안에 추가한 각 child는 세팅 리스트안의 각 아이템으로 표시됩니다.




예제



<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <CheckBoxPreference
        android:key="pref_sync"
        android:title="@string/pref_sync"
        android:summary="@string/pref_sync_summ"
        android:defaultValue="true" />
    <ListPreference
        android:dependency="pref_sync"
        android:key="pref_syncConnectionType"
        android:title="@string/pref_syncConnectionType"
        android:dialogTitle="@string/pref_syncConnectionType"
        android:entries="@array/pref_syncConnectionTypes_entries"
        android:entryValues="@array/pref_syncConnectionTypes_values"
        android:defaultValue="@string/pref_syncConnectionTypes_default" />
</PreferenceScreen>




이 예제에서는 CheckBoxPreference ListPreference 가 있습니다. 각 아이템들은 아래의 3가지 attribute들을 포함하고 있습니다.


android:key
    This attribute is required for preferences that persist a data value. It specifies the unique key (a string) the system uses when saving this setting's value in the SharedPreferences.

    The only instances in which this attribute is not required is when the preference is a PreferenceCategory or PreferenceScreen, or the preference specifies an Intent to invoke (with an <intent> element) or a Fragment to display (with an android:fragment attribute).


android:title
    This provides a user-visible name for the setting.


android:defaultValue
    This specifies the initial value that the system should set in the SharedPreferences file. You should supply a default value for all settings.
   
attribute에 대한 정보를 얻으려면 Preference (and respective subclass) 문서를 참조하세요.

세팅 리스트가 10 아이템을 초과하면 세팅의 그룹별로 title을 추가하고 싶을 겁니다. 혹은 그룹을 별도의 화면에 포시하던가요. 이러한 기능은 아래 섹션을 보세요.



Creating Setting Groups




Figure 2. Setting categories with titles.
1. The category is specified by the <PreferenceCategory> element.
2. The title is specified with the android:title attribute.



10개 이상의 세팅 리스트가 있으면 사용자가 보기 어려워 질 겁니다. 이럴 경우 세팅 리스트를 그룹화 시키면 도움이 될 겁니다. 이렇게 세팅을 그룹화 시키려면 아래 두가지 방법 중 하나를 사용하시면 됩니다.

   
    Using titles
    Using subscreens

이 중 하나만 사용해도 되고 둘 다 사용해도 됩니다. 어떤 걸 사용할 것인지는 어떻게 구분할 지에 따라 결정됩니다. 자세한 것은 Android Design's Settings guide를 참조하세요.



Using titles



세팅내 그룹들 사이에 divider와 heading을 제공하려면 (figure 2) PreferenceCategory 안에 Preference 객체들을 각 그룹으로 묶어 놓습니다.



<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory
        android:title="@string/pref_sms_storage_title"
        android:key="pref_key_storage_settings">
        <CheckBoxPreference
            android:key="pref_key_auto_delete"
            android:summary="@string/pref_summary_auto_delete"
            android:title="@string/pref_title_auto_delete"
            android:defaultValue="false"... />
        <Preference
            android:key="pref_key_sms_delete_limit"
            android:dependency="pref_key_auto_delete"
            android:summary="@string/pref_summary_delete_limit"
            android:title="@string/pref_title_sms_delete"... />
        <Preference
            android:key="pref_key_mms_delete_limit"
            android:dependency="pref_key_auto_delete"
            android:summary="@string/pref_summary_delete_limit"
            android:title="@string/pref_title_mms_delete" ... />
    </PreferenceCategory>
    ...
</PreferenceScreen>




Using subscreens



subscreen으로 세팅 그룹을 나누고 싶으면 (figure 3) PreferenceScreen 안에 Preference 객체들의 그룹을 묶어 놓습니다.



Figure 3. Setting subscreens. The <PreferenceScreen> element creates an item that, when selected, opens a separate list to display the nested settings.



예제



<PreferenceScreen  xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- opens a subscreen of settings -->
    <PreferenceScreen
        android:key="button_voicemail_category_key"
        android:title="@string/voicemail"
        android:persistent="false">
        <ListPreference
            android:key="button_voicemail_provider_key"
            android:title="@string/voicemail_provider" ... />
        <!-- opens another nested subscreen -->
        <PreferenceScreen
            android:key="button_voicemail_setting_key"
            android:title="@string/voicemail_settings"
            android:persistent="false">
            ...
        </PreferenceScreen>
        <RingtonePreference
            android:key="button_voicemail_ringtone_key"
            android:title="@string/voicemail_ringtone_title"
            android:ringtoneType="notification" ... />
        ...
    </PreferenceScreen>
    ...
</PreferenceScreen>




Using intents



어떤 경우는 preference item을 settings screen이 아니라 다른 activity에서 열기를 원할 때도 있을 수 있습니다.
예를 들어 웹페이지를 보기 위한 웹브라우저 같은 경우.
사용자가 preference item을 선택했을 때 Intent 를 invoke 하려면 <intent> element를 해당 <preference> element의 child로 추가 합니다.

예를 들어 웹 페이지를 열 경우 이렇게 합니다.



<Preference android:title="@string/prefs_web_page" >
    <intent android:action="android.intent.action.VIEW"
            android:data="http://www.example.com" />
</Preference>



다음의 attribute들을 사용해서 implicit/explicit intent를 사용할 수 있습니다.


android:action
    The action to assign, as per the setAction() method.


android:data
    The data to assign, as per the setData() method.


android:mimeType
    The MIME type to assign, as per the setType() method.


android:targetClass
    The class part of the component name, as per the setComponent() method.


android:targetPackage
    The package part of the component name, as per the setComponent() method.

   
Creating a Preference Activity



settings를 activity 안에 표시하려면 PreferenceActivity class를 extend 합니다.
Preference objects 의 hierarchy에 기반해 settings 리스트를 표시하는 Activity class의 extension 입니다.
PreferenceActivity는 사용자가 변경을 했을 때 각 preference와 연관된 세팅들을 자동적으로 persist 시켜 줍니다.

Note : Android 3.0 이상이면 대신에 PreferenceFragment를 사용해야 합니다. 사용법은 Using Preference Fragments section에서 설명 됩니다.



기억해야 할 가장 중요한 부분은 onCreate() callback 동안 view들의 레이아웃을 로딩하지 않는다는 겁니다. 대신에 addPreferencesFromResource() 를 호출해서 XML 파일에 선언한 preference를 Activity에 추가하게 됩니다.
예를 들어 PreferenceActivity를 위한 아주 간단한 코드는 아래와 같습니다.



public class SettingsActivity extends PreferenceActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.preferences);
    }
}



이 정도면 어떤 경우에는 충분할 겁니다. 왜냐하면 사용자가 preference를 수정하면 시스템은 이 변경된 값을 default SharedPreferences 파일에 저장을 할 것이기 때문입ㅂ니다.
이렇게 되면 다른 어플리케이션 컴포넌트들에서 이 사용자의 세팅값을 읽어서 사용할 수 있게 됩니다.
하지만 대부분의 경우에는 좀 더 코딩이 필요할 겁니다. 변경이 될 떄 listen 하기 위한 경우 일 텐데요. 이와 관련해서는 Reading Preferences section을 참조하세요.



Using Preference Fragments



Android 3.0 (API level11) 이상에서는 Preference objects의 리스트를 표시하기 위해 PreferenceFragment를 사용해야 합니다.
어느 activity에나 이 PreferenceFragment를 추가할 수 있습니다. (PreferenceActivity를 사용할 필요가 없습니다.)

onCreate() 메소드에서 addPreferencesFromResource() 를 사용하면 아주 간단하게 PreferenceFragment를 implement 할 수 있습니다.



예제



public static class SettingsFragment extends PreferenceFragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Load the preferences from an XML resource
        addPreferencesFromResource(R.xml.preferences);
    }
    ...
}



이렇게 한 후 이 fragment를 Activity에 추가하려면 다른 Fragment를 추가하는 것과 똑 같이 하시면 됩니다.




예제



public class SettingsActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Display the fragment as the main content.
        getFragmentManager().beginTransaction()
                .replace(android.R.id.content, new SettingsFragment())
                .commit();
    }
}



Note : PreferenceFragment는 Context object를 가지고 있지 않습니다. 만약에 Context 객체가 필요하면 getActivity()를 call 하실 수 있습니다.
이 경우 주의해야 할 사항은 activity에 fragment가 attach됐을 때 사용해야 한다는 겁니다. attach 되지 않았을 경우나 lifecycle이 종료되서 detached 됐을 경우 getActivity() 는 null을 return 하게 됩니다.



Setting Default Values



생성된 preferences들은 앱에서 중요한 behavior들이 정의 돼 있을 겁니다. 그래서 관련된 SharedPreferences 파일을 default value들과 함께 initialize 시키는 것이 중요합니다.
이 작업은 사용자가 앱을 처음 open 할 떄 이뤄져야 합니다.

이를 위해 첫번째로 해야 할 것은 XML 파일에 각 Preference objects 마다 default value를 정해줘야 한다는 겁니다. 이 경우 android:defaultValue attribute를 사용합니다.
이 값은 어떤 data type도 될 수 있습니다.



예제



<!-- default value is a boolean -->
<CheckBoxPreference
    android:defaultValue="true"
    ... />

<!-- default value is a string -->
<ListPreference
    android:defaultValue="@string/pref_syncConnectionTypes_default"
    ... />

그런 다음 main activity의 onCreate() 메소드에서 setDefaultValues();를 호출합니다.



PreferenceManager.setDefaultValues(this, R.xml.advanced_preferences, false);



onCreate()에서 호출 할 때 앱에는 디폴트 세팅이 제대로 초기화 되어 있어야 합니다. 예를 들어 cellular network 에서 다운로드를 할 것인지 여부 같은 것들이 제대로 세팅 되어 있어야 할 겁니다.



이 메소드는 세개의 arguments를 가지고 있습니다.



    Your application Context.
    The resource ID for the preference XML file for which you want to set the default values.
    A boolean indicating whether the default values should be set more than once.


    false일 경우 시스템은 이 메소드가 이전에 한번도 호출되지 않았을 경우에만 디폴트 값들을 세팅합니다. (or the KEY_HAS_SET_DEFAULT_VALUES in the default value shared preferences file is false).

3번째 argument를 false로 세팅하면 사용자가 저장한 preference를 디폴트 값으로 overriding 하지 않고 reseting 함으로서 activity가 시작될 때마다 안전하게 호출하도록 할 수 있습니다.
이 값을 true로 설정하면 이전의 저장된 값이 디폴트 값으로 override 하게 됩니다.

저작자 표시 비영리 동일 조건 변경 허락
신고


3과에서는 날씨 리스트에서 한 라인을 클릭하면 그 날의 자세한 날씨가 표시되도록 할 겁니다.
클릭하면 다른 화면으로 넘어가는데요. 이것은 또 다른 Activity를 생성하고 Main Activity 에서 이 새로운 Activity로 전환할 수 있는 기능이 필요합니다.
그리고 새로운 화면 (Activity)로 넘어간 후 그곳에서 표시해 줄 정보를 출력해서 보여주는 UI를 만드는 일도 해야 하구요.

대충 이런 기능들을 구현할 것 같습니다.

리스트에서 아이템을 클릭했을 경우 작동하는 메소드는 setOnItemClickListener 입니다.
https://developer.android.com/reference/android/widget/ListView.html

이 메소드를 기존 코드에서 구현할 겁니다.

ForecastFragment class의 onCreateView 로 가서 맨 아래에 아래와 같이 코드를 추가합니다.



       // Get a reference to the ListView, and attach this adapter to it.
        ListView listView = (ListView) rootView.findViewById(R.id.listview_forecast);
        listView.setAdapter(mForecastAdapter);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {

            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
                String forecast = mForecastAdapter.getItem(position);
                Toast.makeText(getActivity(), forecast, Toast.LENGTH_SHORT).show();
            }


listView.setAdapter(mForecastAdapter); 까지는 이전에 있었습니다.

그 이후에 setOnItemClickListener() 를 구현했는데요.

이 메소드 안의 onItemClick()을 Override 했습니다.


이 안에서는 클릭한 아이템의 포지션을 Toast로 보이도록 했습니다.



이렇게 하면 위에 보이는 것처럼 아이템을 클릭했을 시 그 아이템 내용이 Toast에 잠깐 보였다가 사라집니다.


이렇게 되면 onItemClick 까지 제대로 잘 작동 되고 있는 것을 알 수 있습니다.



이 다음에는 클릭한 날의 자세한 날씨를 보여주는 새로운 페이지로 이동해야 합니다.


그러려면 이동할 페이지를 만들어야 하고 이 새로운 페이지를 AndroidManifest파일에 등록해야 합니다.


이 새로운 페이지에서는 이전 화면으로 돌아가는 버튼을 만들겁니다.

그리고 그 아래에 해당 날짜의 자세한 정보를 보여 줄것이구요.


우선 DetailActivity를 생성합니다.


New-Activity-Blank Activity 를 선택합니다.



Activity 이름은 DetailActivity로 하고 Hierarchical Parent는 MainActivity로 합니다.

그리고 Use a Fragment도 선택을 합니다.



그러면 DetailActivity클래스와 함께 DetailActivityFragment 클래스가 자동으로 생성됩니다.

그리고 관련된 xml파일들도 생성됩니다. (activity_detail.xml, fragment_detail.xml)


일단 DetailActivity는 이렇게 만듭니다.



/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.sunshine.app;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.app.ActionBarActivity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;

public class DetailActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detail);
        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.container, new PlaceholderFragment())
                    .commit();
        }
    }


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.detail, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // 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.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    /**
     * A placeholder fragment containing a simple view.
     */
    public static class PlaceholderFragment extends Fragment {

        public PlaceholderFragment() {
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {

            View rootView = inflater.inflate(R.layout.fragment_detail, container, false);
            return rootView;
        }
    }
}


그리고 activity_detail.xml은 이렇게 합니다.


<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container"
    android:layout_width="match_parent" android:layout_height="match_parent"
    tools:context="com.example.android.sunshine.app.DetailActivity" tools:ignore="MergeRootFrame" />


그리고 menu_detail.xml은 detail.xml로 이름을 바꿉니다.

해당 xml에서 오른쪽 마우스 클릭을 한 후 Refactor - Rename을 선택하면 이름을 바꿀 수 있습니다.



그리고 DetailActivityFragment.java는 delete합니다.

이건 현재 DetailActivity.java에 있는 PlaceholderFragment.java로 replace될 겁니다.


이렇게 된 코드는 3.02_create_detail_activity  에서 받아 볼 수 있습니다.





Activity들 간에 이동을 하려면 startActivity(Intent)를 사용하면 됩니다.


위에서 Toast를 구현했던 부분에 이 startActivity()를 구현할 겁니다.


setOnItemClick()을 아래와 같이 바꿉니다.


        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {

            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
                String forecast = mForecastAdapter.getItem(position);
                //Toast.makeText(getActivity(), forecast, Toast.LENGTH_LONG).show();

Intent intent = new Intent(getActivity(), DetailActivity.class).putExtra(Intent.EXTRA_TEXT, forcast);

startActivity(intent)

            } 


이렇게 하면 DetailActivity 클래스로 이동합니다.


현재 상태에서는 DetailActivity로 넘어가면 아무것도 안 보일 겁니다.


fragment_detail.xml 에 있는 내용이 뿌려질건데 현재 그 내용은 아래와 같습니다.


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="com.example.android.sunshine.app.DetailActivityFragment">


</RelativeLayout>


아무것도 내용이 없습니다.


여기에 해당 날짜의 forcast data를 읽어서 뿌려 주도록 하겠습니다.


DetailActivity의 onCreateView() 안에 아래와 같이 코등합니다.


        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {

            View rootView = inflater.inflate(R.layout.fragment_detail, container, false);

            // The detail Activity called via intent.  Inspect the intent for forecast data.
            Intent intent = getActivity().getIntent();
            if (intent != null && intent.hasExtra(Intent.EXTRA_TEXT)) {
                String forecastStr = intent.getStringExtra(Intent.EXTRA_TEXT);
                ((TextView) rootView.findViewById(R.id.detail_text))
                        .setText(forecastStr);
            }

            return rootView;
        }


그리고 fragment_detail.xml은 아래와 같이 합니다.


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="com.example.android.sunshine.app.DetailActivity.DetailFragment">

    <TextView android:text="@string/hello_world" android:layout_width="wrap_content"
        android:id="@+id/detail_text"
        android:layout_height="wrap_content" />

</RelativeLayout>


이제 리스트 아이템을 클릭하면 Toast가 나오는 대신 아래와 같이 DetailActivity가 표시 될 겁니다.




여기까지 진행된 소스코드는 이곳에서 받으실 수 있습니다.


https://github.com/udacity/Sunshine-Version-2/tree/3.04_populate_detail_text




이제 DetailActivity에 해당 날짜의 자세한 날씨를 표시하는 화면을 구성할 차례입니다.


이 작업은 다음 글에서 이어 나가겠습니다.




저작자 표시 비영리 동일 조건 변경 허락
신고

Android Toast 정리

2016.09.07 22:32 | Posted by 솔웅


Toasts



Toast 는 LogCat 과 함께 Android App을 개발하면서 간단하게 테스트 할 때 유용하게 사용되는 기능입니다.
관련된 Android API Guide 는 이곳에서 보실 수 있습니다.


https://developer.android.com/guide/topics/ui/notifiers/toasts.html#Basics



If user response to a status message is required, consider instead using a Notification 



The Basics



makeText() 메소드를 사용해 Toast 객체를 초기화합니다. 이 메소드는 3개의 파라미터를 갖습니다. (the application Context, the text message, and the duration for the toast)
show()를 사용해서 toast notification을 display 할 수 있습니다.

Context context = getApplicationContext();
CharSequence text = "Hello toast!";
int duration = Toast.LENGTH_SHORT;

Toast toast = Toast.makeText(context, text, duration);
toast
.show();



이 예제 형식을 가장 많이 사용합니다. 다른 형식은 거의 사용할 일이 없을 겁니다. Toast 메세지의 표시 위치를 다르게 한다거나 Toast 메세지의 layout 을 여러분이 원하는 대로 꾸미고 싶을 경우 추가적으로 코딩을 하면 됩니다.

아래와 같이 한줄로 처리할 수 있습니다.

Toast.makeText(context, text, duration).show();



Positioning your Toast



기본적으로 Toast notification은 화면의 아랫쪽 중앙에 표시됩니다. 이 위치를 바꿀 수 있는데요. setGravity(int, int, int) 메소드를 사용하시면 됩니다.
이 메소드는 3개의 파라미터를 갖습니다. (a Gravity constant, an x-position offset, and a y-position offset)

예를 들어 toast가 top-left 에 표시되기를 원하시면 아래와 같이 하면 됩니다.

toast.setGravity(Gravity.TOP|Gravity.LEFT, 0, 0);



오른쪽으로 좀 옮기고 싶으면 두번째 파라미터의 값을 높이면 됩니다. 아랫쪽으로 옮기고 싶으면 마지막 파라미터의 값을 올리면 됩니다.



Creating a Custom Toast View



메세지가 간단하지 않으면 layout을 customize 할 수 있습니다. custom layout을 만들려면  View layout을 XML 파일에 정의해야 합니다. 혹은 어플리케이션 안에 정의 할 수도 있ㅅ브니다. 그리고 root ViewsetView(View) 메소드로 pass 합니다.

예제 (saved as layout/custom_toast.xml)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
             
android:id="@+id/custom_toast_container"
             
android:orientation="horizontal"
             
android:layout_width="fill_parent"
             
android:layout_height="fill_parent"
             
android:padding="8dp"
             
android:background="#DAAA"
             
>
   
<ImageView android:src="@drawable/droid"
               
android:layout_width="wrap_content"
               
android:layout_height="wrap_content"
               
android:layout_marginRight="8dp"
               
/>
   
<TextView android:id="@+id/text"
             
android:layout_width="wrap_content"
             
android:layout_height="wrap_content"
             
android:textColor="#FFF"
             
/>
</LinearLayout>



LinearLayout element의 ID는 "custom_toast_container" 입니다. 이 xml 파일의 이름은 custom_toast 이구요.




LayoutInflater inflater = getLayoutInflater();
View layout = inflater.inflate(R.layout.custom_toast,
               
(ViewGroup) findViewById(R.id.custom_toast_container));

TextView text = (TextView) layout.findViewById(R.id.text);
text
.setText("This is a custom toast");

Toast toast = new Toast(getApplicationContext());
toast
.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
toast
.setDuration(Toast.LENGTH_LONG);
toast
.setView(layout);
toast
.show();



우선 이 xml layout 파일을 inflate 시켰습니다. 그리고 infalte 시킬때 위에 언급한 ID 인 custom_toast_container 를 사용했구요.
이러면 해당 LinearLayout 이 성공적으로 inflate 되게 됩니다.
getLayoutflater() (혹은 getSystemService()) 를 사용해 LayoutInflater를 retrieve 합니다. 그리고 inflate(int, ViewGroup) 을 사용해서 layout 을 inflate 시키구요.
첫번째 파라미터는 layout resource의 ID 이구요. 두번째는 root View 입니다. 이 inflate 된 layout 에 다른 View 객체들을 넣을 수 있는데요. 여기서는 TextView를 넣었습니다. Image View를 넣을 수도 있습니다.

마지막으로 Toast(Context)를 사용해서 새로운 Toast를 생성하고 property들을 세팅합니다. 여기서는 Gravity 와 Duration을 세팅했습니다. 그 다음은 setView(View) 를 호출하고 이것을 inflate 된 layout에 pass 합니다. 이렇게 하면 show() 를 사용해서 customize 된 layout으로 toast를 표시할 수 있게 됩니다.

Note: Do not use the public constructor for a Toast unless you are going to define the layout with setView(View). If you do not have a custom layout to use, you must use makeText(Context, int, int) to create the Toast.



저작자 표시 비영리 동일 조건 변경 허락
신고


오늘은 Debug mode로 돌리면서 2과 소스코드의 실행 순서를 하나하나 따라가 보기로 하겠습니다.


2과 소스코드는  아래 GitHub 에 가시면 받을 수 있습니다.


https://github.com/udacity/Sunshine-Version-2/tree/2.09_display_data

 



Break point 를 MainActivity.java 의 onCreate() 메소드의 if 문에 찍어 놨습니다.

보시듯이 이 앱을 실행하면 MainActivity 의 onCreate()메소드가 실행됩니다.

savedInstanceState는 Null 이구요 즉 처음 실행되는 거란 얘기죠.

그러면 if문의 조건을 만족하니까 그 내부의 코드들이 실행 될 겁니다.


그 전에 setContentView()에서 activity_main.xml에서 설정된 layout을 세팅합니다.

이 xml에는 한개의 FrameLayout이 세팅돼 있습니다.



여기서 그 다음 step 으로 넘어가면 if 문 안의 것이 실행될 겁니다.

안에는 위 xml에서 container라고 정의된 id 에 ForecastFragment() 클래스를 세팅합니다.



이제 ForecastFragment.java 파일로 넘어 갑니다.


제일 먼저 생성자인 ForecastFragment()로 갑니다. 일단 여기는 empty 입니다.

그 다음은 다시 MainActivity 로 넘어가서 if 문 안의 commit()부분이 실행됩니다.


그 다음 step over를 해 보면 안드로이드 자체 클래스들에 있는 메소드들이 실행 됩니다.

Activity.java > performCreate() 를 거쳐서 ActivityThread.java  에 있는 코드들이 실행 됩니다.


그 이후 다시 ForecastFragment클래스의 onCreate() 메소드로 가게 됩니다.



여기서는 setHasOptionsMenu(true); 를 설정해 놨죠.

옵션메뉴를 사용하겠다는 건데요.


그 다음 step over를 해 보면 Fragment.java > performCreate()를 거쳐 FragmentManager.java 클래스의 여러 코드들이 실행 됩니다.


그 다음에 가는 것이 ForecastFragment 클래스의 onCreateView() 메소드로 가게 됩니다.


아직 까지는 에뮬레이터에 아무것도 표시되지 않는데요. 이 onCreateView()를 지나게 되면 view가 표시 될 겁니다.





이 onCreateView()에서는 날씨 정보를 하드코딩해서 리스트에 넣고 ArrayAdapter를 사용해서 listview에 뿌릴 준비를 했습니다.


그리고 inflater를 사용해서 fragment_main.xml에 있는 layout을 로딩했죠.




이 Layout에는 ListView를 사용하도록 해 놨습니다.


보시면 이 ListView의 id 는 listview_forecast 인데요. onCreateView()에서는 이 아이디에 방금전에 사용했던 ArrayAdapter 를 대입해서 하드코딩한 데이터 정보들을 ListVIew에 넣었습니다.

그리고 이 ListView를 rootView에 넣어서 return 하게 됩니다.



그 다음 단계는 Fragment.java > performCreateView() 를 거쳐 FragmentManager, BackStackRecord , Instrumentation 클래스의  callActivityOnStart() 메소드, Activity, ActivityThread, Handler, Looper 클래스 등 여러 코드들을 실행합니다.


그러면 드디어 에뮬레이터에 데이터가 뿌려지게 됩니다.




그런데 잘 보시면 이 화면에는 옵션 메뉴가 없습니다.

아직 옵션메뉴 관련된 부분은 실행이 되지 않은 겁니다.


현재 디버거는 MainActivity 클래스의 onCreateOptionMenu()메소드에 와 있습니다.




여기서는 MenuInflater를 사용해서 manu에 있는 main.xml을 처리했습니다.



그 다음엔 안드로이드의 아래와 같은 클래스들을 거치면서 많은 메소드들이 실행됩니다.


Activity.java > onCreatePanelMenu() >>> FragmentActivity.java > onCreatePanelMenu() >>> ForecastFragment.java > onCreateOptionsMenu() >>> FragmentManager class >>> ActionBarActivity class >>> ActionBarActivityDelegate class >>> ActionBarActivityDelegateBase class >>> Handler class >>> Looper class 


그러면 옵션 메뉴가 표시됩니다.




위에서 ForecastFragment 클래스의 onCreateOptionMenu() 메소드도 실행 됐는데요.


여기서는 forecastfragment.xml 을 inflate 시켰습니다.


<?xml version="1.0" encoding="utf-8"?>

<
menu xmlns:android="http://schemas.android.com/apk/res/android"
   
xmlns:app="http://schemas.android.com/apk/res-auto">
    <
item android:id="@+id/action_refresh"
       
android:title="@string/action_refresh"
       
app:showAsAction="never" />
</
menu>


refresh 메뉴를 만든 겁니다.


이제 MainActivity클래스의 onOptionsItemSelected()도 로딩이 될 겁니다.



아직 Setting을 눌렀을 떄는 어떻게 해야 하는지는 코딩을 하지 않았습니다.

대신 Refresh 를 눌렀을 경우에는 ForecastFragment class의 onOptionsItemSelected() 메소드에서 처리 되도록 만들었습니다.


@Override
public boolean onOptionsItemSelected(MenuItem item) {
    // 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.
   
int id = item.getItemId();
    if (id == R.id.action_refresh) {
        FetchWeatherTask weatherTask = new FetchWeatherTask();
        weatherTask.execute("55347");
        return true;
    }
    return super.onOptionsItemSelected(item);
}


Refresh 를 누르면 FetchWeatherTask클래스에 55347이라는 파라미터를 던져줘서 그 클래스가 처리 되도록 합니다.


이 클래스에서는 이전 글에서 다뤘듯이 AsyncTask를 사용해서 네트워크 연결 부분을 Background Thread로 돌리고 날씨 웹에 request를 던져서 respons를 JSON형식으로 받은 다음에 이것을 파싱해서 화면에 뿌려주는 작업을 합니다.




그러면 이렇게 제가 사는 지역의 일주일 날씨 예보가 출력되게 됩니다.




자 여기까지 앱을 실행하면 실제로 소스코드가 어떤 순서로 실행되는지 하나하나 살펴 봤습니다.








저작자 표시 비영리 동일 조건 변경 허락
신고
이전 1 2 3 4 다음