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

최근에 올라온 글

최근에 달린 댓글

최근에 받은 트랙백

글 보관함

카테고리

Udacity 강좌 - Lesson 2 실습 05

2016. 8. 29. 03:00 | Posted by 솔웅


반응형

안드로이드에서 메뉴는 3가지 종류가 있음
- Options menu and app bar
- Context menu and contextual action mode
- Popup menu


https://developer.android.com/guide/topics/ui/menus.html


예제

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
   
<item android:id="@+id/new_game"
         
android:icon="@drawable/ic_new_game"
         
android:title="@string/new_game"
         
android:showAsAction="ifRoom"/>
   
<item android:id="@+id/help"
         
android:icon="@drawable/ic_help"
         
android:title="@string/help" />
</menu>

<item> 에는 몇가지 attribute 들이 있다.

android:id
A resource ID that's unique to the item, which allows the application to recognize the item when the user selects it.
android:icon
A reference to a drawable to use as the item's icon.
android:title
A reference to a string to use as the item's title.
android:showAsAction
Specifies when and how this item should appear as an action item in the app bar.

Sunshine source code 의 메뉴 xml을 보면 아래와 같이 되어 있다.


여기서 보면 title attribute 가 있는데 여기에 Settings 라고 돼 있음


* Refresh Button


forecastfragment.xml 이라는 새로운 메뉴 레이아웃을 만든다. 여기에는 아이디가 action_refresh 라는 아이템을 만든다. Label 은 Refresh 라고 한다.<?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>


Strings.xml

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

    <!--
         Used in Action Bar, and in AndroidManifest to tell the device the name of this app.
         It's to keep this short, so your launcher icon isn't displayed with "The greatest Wea"
         or something similar.
    -->
    <string name="app_name">Sunshine</string>

    <!--
         By convention, "action" denotes that this String will be used as the label for an Action,
         typically from the action bar.  The ActionBar is limited real estate, so shorter is better.
    -->
    <string name="action_settings">Settings</string>

    <!-- Menu label to fetch updated weather info from the server -->
    <string name="action_refresh" translatable="false">Refresh</string>
    <string name="hello_world">Hello world!</string>

</resources>


* forecastFragment에 있는 옵션 메뉴 inflate 하기
* onOptionsItemSelected() 셋업하기
* Fragment에 있는 setHasOptionsMenu(true) 메소드를 호출 함으로서 메뉴 옵션이 있다는 것을 알린다.



위 소스 코드는 2과를 모두 완료 했을 때의 코드임.

onCreate() 에서 setHasOptionsMenu(true) 를 함으로서 옵션 메뉴가 있다는 것을 생성 단계에 알리고 onCreateOptionsMenu() 에서 forecastfragment xml 파일에 있는 메뉴를 inflate 합니다.

그 다음에 해당 메뉴가 선택됐을 때 무엇을 할 지는 onOptionsItemSelected() 에서 구현 합니다.


여기서 보면 해당 메뉴 (Refresh - 아이디는 action_refresh) 가 선택 되면 FetchWeatherTask() 클래스를 호출하고 이 때 파라미터로 55347 이라는 우편번호를 전달해 줍니다.


그럼 사용자가 이 메뉴를 선택하면 호출된 클래스에 구현된 내용들이 실행될 겁니다.


전 글에서 이 FetchWeatherTask 클래스는 구현해 놨었죠?


여기서 한가지 추가해야 할 것은 날씨 싸이트와 소통을 하려면 안드로이드에서는 퍼미션을 세팅해 줘야 합니다.


이 퍼미션은 Manifest xml파일에서 users-permission을 세팅해야 합니다.



여기서는 android.permission.INTERNET 을 추가해야 합니다.


위에보시면 해당 라인이 추가 돼 있는 것을 볼 수 있습니다.



이제 Refresh 버튼을 누르면 FetchWeatherTask 를 실행 할 텐데요.

여기에 우편번호를 받아서 날씨 싸이트에 전달하고 이것을 JSON 형식으로 받은 후 파싱해서 화면에 뿌려줄 수 있도록 만듭니다.



   public class FetchWeatherTask extends AsyncTask<String, Void, String[]> {

        private final String LOG_TAG = FetchWeatherTask.class.getSimpleName();

        /* The date/time conversion code is going to be moved outside the asynctask later,
         * so for convenience we're breaking it out into its own method now.
         */
        private String getReadableDateString(long time){
            // Because the API returns a unix timestamp (measured in seconds),
            // it must be converted to milliseconds in order to be converted to valid date.
            SimpleDateFormat shortenedDateFormat = new SimpleDateFormat("EEE MMM dd");
            return shortenedDateFormat.format(time);
        }

        /**
         * Prepare the weather high/lows for presentation.
         */
        private String formatHighLows(double high, double low) {
            // For presentation, assume the user doesn't care about tenths of a degree.
            long roundedHigh = Math.round(high);
            long roundedLow = Math.round(low);

            String highLowStr = roundedHigh + "/" + roundedLow;
            return highLowStr;
        }

        /**
         * Take the String representing the complete forecast in JSON Format and
         * pull out the data we need to construct the Strings needed for the wireframes.
         *
         * Fortunately parsing is easy:  constructor takes the JSON string and converts it
         * into an Object hierarchy for us.
         */
        private String[] getWeatherDataFromJson(String forecastJsonStr, int numDays)
                throws JSONException {

            // These are the names of the JSON objects that need to be extracted.
            final String OWM_LIST = "list";
            final String OWM_WEATHER = "weather";
            final String OWM_TEMPERATURE = "temp";
            final String OWM_MAX = "max";
            final String OWM_MIN = "min";
            final String OWM_DESCRIPTION = "main";

            JSONObject forecastJson = new JSONObject(forecastJsonStr);
            JSONArray weatherArray = forecastJson.getJSONArray(OWM_LIST);

            // OWM returns daily forecasts based upon the local time of the city that is being
            // asked for, which means that we need to know the GMT offset to translate this data
            // properly.

            // Since this data is also sent in-order and the first day is always the
            // current day, we're going to take advantage of that to get a nice
            // normalized UTC date for all of our weather.

            Time dayTime = new Time();
            dayTime.setToNow();

            // we start at the day returned by local time. Otherwise this is a mess.
            int julianStartDay = Time.getJulianDay(System.currentTimeMillis(), dayTime.gmtoff);

            // now we work exclusively in UTC
            dayTime = new Time();

            String[] resultStrs = new String[numDays];
            for(int i = 0; i < weatherArray.length(); i++) {
                // For now, using the format "Day, description, hi/low"
                String day;
                String description;
                String highAndLow;

                // Get the JSON object representing the day
                JSONObject dayForecast = weatherArray.getJSONObject(i);

                // The date/time is returned as a long.  We need to convert that
                // into something human-readable, since most people won't read "1400356800" as
                // "this saturday".
                long dateTime;
                // Cheating to convert this to UTC time, which is what we want anyhow
                dateTime = dayTime.setJulianDay(julianStartDay+i);
                day = getReadableDateString(dateTime);

                // description is in a child array called "weather", which is 1 element long.
                JSONObject weatherObject = dayForecast.getJSONArray(OWM_WEATHER).getJSONObject(0);
                description = weatherObject.getString(OWM_DESCRIPTION);

                // Temperatures are in a child object called "temp".  Try not to name variables
                // "temp" when working with temperature.  It confuses everybody.
                JSONObject temperatureObject = dayForecast.getJSONObject(OWM_TEMPERATURE);
                double high = temperatureObject.getDouble(OWM_MAX);
                double low = temperatureObject.getDouble(OWM_MIN);

                highAndLow = formatHighLows(high, low);
                resultStrs[i] = day + " - " + description + " - " + highAndLow;
            }

            for (String s : resultStrs) {
                Log.v(LOG_TAG, "Forecast entry: " + s);
            }
            return resultStrs;

        }
        @Override
        protected String[] doInBackground(String... params) {

            // If there's no zip code, there's nothing to look up.  Verify size of params.
            if (params.length == 0) {
                return null;
            }

            // These two need to be declared outside the try/catch
            // so that they can be closed in the finally block.
            HttpURLConnection urlConnection = null;
            BufferedReader reader = null;

            // Will contain the raw JSON response as a string.
            String forecastJsonStr = null;

            String format = "json";
            String units = "metric";
            int numDays = 7;

            try {
                // Construct the URL for the OpenWeatherMap query
                // Possible parameters are avaiable at OWM's forecast API page, at
                // http://openweathermap.org/API#forecast
                final String FORECAST_BASE_URL =
                        "http://api.openweathermap.org/data/2.5/forecast/daily?";
                final String QUERY_PARAM = "q";
                final String FORMAT_PARAM = "mode";
                final String UNITS_PARAM = "units";
                final String DAYS_PARAM = "cnt";
                final String APPID_PARAM = "APPID";

                Uri builtUri = Uri.parse(FORECAST_BASE_URL).buildUpon()
                        .appendQueryParameter(QUERY_PARAM, params[0])
                        .appendQueryParameter(FORMAT_PARAM, format)
                        .appendQueryParameter(UNITS_PARAM, units)
                        .appendQueryParameter(DAYS_PARAM, Integer.toString(numDays))
                        //.appendQueryParameter(APPID_PARAM, BuildConfig.OPEN_WEATHER_MAP_API_KEY)
                        .appendQueryParameter(APPID_PARAM, "84ac1b7cfb5b87edaa1fa4626d0fcbc8")
                        .build();

                URL url = new URL(builtUri.toString());

                Log.v(LOG_TAG, "Built URI " + builtUri.toString());

                // Create the request to OpenWeatherMap, and open the connection
                urlConnection = (HttpURLConnection) url.openConnection();
                urlConnection.setRequestMethod("GET");
                urlConnection.connect();

                // Read the input stream into a String
                InputStream inputStream = urlConnection.getInputStream();
                StringBuffer buffer = new StringBuffer();
                if (inputStream == null) {
                    // Nothing to do.
                    return null;
                }
                reader = new BufferedReader(new InputStreamReader(inputStream));

                String line;
                while ((line = reader.readLine()) != null) {
                    // Since it's JSON, adding a newline isn't necessary (it won't affect parsing)
                    // But it does make debugging a *lot* easier if you print out the completed
                    // buffer for debugging.
                    buffer.append(line + "\n");
                }

                if (buffer.length() == 0) {
                    // Stream was empty.  No point in parsing.
                    return null;
                }
                forecastJsonStr = buffer.toString();

                Log.v(LOG_TAG, "Forecast string: " + forecastJsonStr);
            } catch (IOException e) {
                Log.e(LOG_TAG, "Error ", e);
                // If the code didn't successfully get the weather data, there's no point in attemping
                // to parse it.
                return null;
            } finally {
                if (urlConnection != null) {
                    urlConnection.disconnect();
                }
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (final IOException e) {
                        Log.e(LOG_TAG, "Error closing stream", e);
                    }
                }
            }

            try {
                return getWeatherDataFromJson(forecastJsonStr, numDays);
            } catch (JSONException e) {
                Log.e(LOG_TAG, e.getMessage(), e);
                e.printStackTrace();
            }

            // This will only happen if there was an error getting or parsing the forecast.
            return null;
        }

        @Override
        protected void onPostExecute(String[] result) {
            if (result != null) {
                mForecastAdapter.clear();
                for(String dayForecastStr : result) {
                    mForecastAdapter.add(dayForecastStr);
                }
                // New data is back from the server.  Hooray!
            }
        }
    }


코드를 쭉 훑어 보면은요.


우선 LOG_TAG 에 현재 클래스 이름을 담고 getReadableDateString() 메소드를 private 으로 만듭니다.

여기서는 날짜 정보를 받아서 이것을 특정 형식에 넣어서 반환하는 일을 합니다.


그리고 formatHighLows() 메소드에서는 최고기온/최저기온 을 표시하도록 만듭니다.


getWeatherDataFromJson() 에서는 파싱하는 메소드인데요.


우선 날씨 기온 최고기온 최저기온 등 표시할 데이터의 제목들을 스트링 변후에 대입시킵니다.


그리고JSONObject 변수에 JSON 정보를 담습니다.

다음엔 이것을 배열 형식으로 변환시켜서 OWM_LIST 에 담습니다.


다음엔 현재 날짜, 시간을 설정하고, 현지시각을 사용하도록 만듭니다.


그리고 나서 for 문을 돌려서 아까 받았던 배열들을 처리합니다.


이 구문 안에서는 day, description, highANdLow 를 처리할 겁니다.


그리고 이 처리한 결과 값들을 처음 설정한 스트링에 담습니다.

day, description, highAndLow


그리고 for 문을 하나 더 만들어서 이 값을을 로그로 뿌리도록 합니다.


그리고 마지막으로 그 값을 반환합니다.


그 다음 메소드은 doInBackground() 는 눈에 익은 코드들이 보입니다.

지난 글에서 만들었던 코드들입니다.

날씨 싸이트에 요청을 보내고 이것을 JSON 형식으로 응답을 받는 겁니다.

이 작업은 백그라운드 쓰레드로 돌아가게 됩니다.


여기서는 우편번호를 파라미터로 받아서 사용합니다.


자세한 설명은 대부분 이전 글하고 중복 되게 되서 생략하겠습니다.


이 부분을 실행해서 로그를 살펴 보면 아래와 같은 형식으로 정보를 받는 걸 볼 수 있습니다.



08-28 12:33:24.293 1645-1836/? V/FetchWeatherTask: Forecast string: {"city":{"id":5043944,"name":"Rowland","coord":{"lon":-93.425507,"lat":44.860241},"country":"US","population":0},"cod":"200","message":0.0543,"cnt":7,"list":[{"dt":1472407200,"temp":{"day":27.02,"min":21.97,"max":28.3,"night":21.97,"eve":26.79,"morn":23.28},"pressure":1000.68,"humidity":81,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"02d"}],"speed":4.11,"deg":164,"clouds":8},{"dt":1472493600,"temp":{"day":29.55,"min":19.71,"max":30.15,"night":19.71,"eve":28.72,"morn":21.13},"pressure":1001.03,"humidity":77,"weather":[{"id":502,"main":"Rain","description":"heavy intensity rain","icon":"10d"}],"speed":5.26,"deg":203,"clouds":12,"rain":17.8},{"dt":1472580000,"temp":{"day":24.5,"min":15.42,"max":24.85,"night":15.42,"eve":22.39,"morn":18.77},"pressure":1001.02,"humidity":88,"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"speed":2.66,"deg":1,"clouds":32},{"dt":1472666400,"temp":{"day":23.55,"min":12.55,"max":23.55,"night":16.48,"eve":22.84,"morn":12.55},"pressure":1000.12,"humidity":0,"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"speed":1.98,"deg":73,"clouds":1},{"dt":1472752800,"temp":{"day":22.07,"min":13.01,"max":22.07,"night":16.97,"eve":20.8,"morn":13.01},"pressure":997.94,"humidity":0,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":4.83,"deg":151,"clouds":70,"rain":0.75},{"dt":1472839200,"temp":{"day":22.26,"min":16.52,"max":24.14,"night":22.41,"eve":24.14,"morn":16.52},"pressure":992.68,"humidity":0,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":6.92,"deg":159,"clouds":64,"rain":0.92},{"dt":1472925600,"temp":{"day":27.25,"min":22.06,"max":27.76,"night":23.4,"eve":27.76,"morn":22.06},"pressure":985.79,"humidity":0,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":3.1,"deg":228,"clouds":20,"rain":2.75}]}

여기에 어떤 정보가 있는지 보기 어려우면 JSON 관련 웹사이트에서 좀 알아보기 쉽도록 처리해서 볼 수 있습니다.


https://jsonformatter.curiousconcept.com/




이렇게 하면 받은 데이터 중 어떤 자료를 어떻게 처리 해야할 지 계획을 세울 때 좀 더 편리하게 이용할 수 있을 겁니다.


여기서는 main, max, min 같은 자료를 사용할 겁니다.


위에 코드가 있지만 JSON 데이터를 받는 방법은 아래와 같습니다.


JSONObject temperatureObject = dayForecast.getJSONObject(OWM_TEMPERATURE);



마지막으로 이 부분도 잘 봐야 하는데요.


@Override
        protected void onPostExecute(String[] result) {
            if (result != null) {
                mForecastAdapter.clear();
                for(String dayForecastStr : result) {
                    mForecastAdapter.add(dayForecastStr);
                }
                // New data is back from the server.  Hooray!
            }
        }


여기선 AsyckTask 의 작업이 다 끝난 후에 이것을 clear 해 주고 새로운 데이터를 뿌려주도록 합니다.

해당 작업이 끝난 후 UI 단에서 실행되는 메소드 입니다.




Refresh 버튼을 누른 후의 화면입니다.


제가 사는 지역의 일기예보인데요.


월요일이 좀 덥고 나머지는 지낼만 할 것 같습니다.


여기까지 2과 내용이었습니다.


아마 가장 기본적인 기능들은 구현 된 것 같습니다.


3과에서는 리스트 뷰에서 아이템을 누르면 그 아이템에 대한 좀 더 자세한 정보를 보여주는 기능을 구현할 겁니다.

반응형


반응형

요즘 회사일 하느라고 진도를 못 나갔습니다.
흐름이 끊기지 않기 위해 일단 2과 완성 소스를 다운 받아서 실행해 봤습니다.

소스코드는 여기서 받으시면 됩니다.

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

혹은 이 github 로 가셔서 아래처럼 브랜치를 선택하면 됩니다.



2.09 display data 가 2과의 완성된 소스코드 입니다.

이 소스코드를 받은 다음 OpenWeather 앱 아이디를 제 것으로 수정한 다음에 실행했습니다.
요거 하는데도 좀 헤맸는데요. 어쨌든 됐습니다.

이 소스코드를 실행하면 처음에는 지금까지 봐 왔던 하드 코딩된 날짜들이 표시됩니다.

그런데 잘 보면 우상단에 메뉴 버튼이 있는데요.
이 버튼을 누르고 Refresh 를 선택하면 그 날씨 웹 사이트에서 현재 날씨를 받아서 표시해 줍니다.

저는 제가 사는 곳의 우편번호를 넣어서 제가 사는 곳이 표시되도록 했습니다.





Click on Refresh


그러면 날씨 웹 싸이트로 부터 아래와 같은 정보를 받습니다.


{"cod":"200","message":0.0243,"city":{"id":0,"name":"Eden Prairie","country":"US","coord":{"lat":44.8342,"lon":-93.4389}},"cnt":7,"list":[{"dt":1471888800,"temp":{"day":19.77,"min":19.77,"max":19.77,"night":19.77,"eve":19.77,"morn":19.77},"pressure":993.75,"humidity":57,"weather":[{"id":800,"main":"Clear","description":"sky is clear","icon":"02n"}],"speed":6.47,"deg":205,"clouds":8},{"dt":1471975200,"temp":{"day":26.23,"min":18.85,"max":28.32,"night":24.13,"eve":27.26,"morn":18.85},"pressure":993.82,"humidity":65,"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"speed":6.66,"deg":192,"clouds":48},{"dt":1472061600,"temp":{"day":21.63,"min":17.19,"max":23.82,"night":17.19,"eve":22.65,"morn":20.18},"pressure":990.44,"humidity":97,"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"speed":3.48,"deg":313,"clouds":92,"rain":3.3},{"dt":1472148000,"temp":{"day":19.95,"min":13.88,"max":20.64,"night":13.88,"eve":20.64,"morn":14.4},"pressure":995.57,"humidity":0,"weather":[{"id":800,"main":"Clear","description":"sky is clear","icon":"01d"}],"speed":7.56,"deg":291,"clouds":2},{"dt":1472234400,"temp":{"day":20.98,"min":10.21,"max":20.98,"night":15.51,"eve":18.56,"morn":10.21},"pressure":998.44,"humidity":0,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":1.87,"deg":124,"clouds":86,"rain":1.08},{"dt":1472320800,"temp":{"day":18.07,"min":14.4,"max":18.07,"night":17.53,"eve":17.6,"morn":14.4},"pressure":993.44,"humidity":0,"weather":[{"id":502,"main":"Rain","description":"heavy intensity rain","icon":"10d"}],"speed":4.91,"deg":122,"clouds":98,"rain":13.41},{"dt":1472407200,"temp":{"day":20.11,"min":13.46,"max":20.86,"night":13.46,"eve":20.86,"morn":16.38},"pressure":992.58,"humidity":0,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":3.43,"deg":343,"clouds":67,"rain":0.95}]}


이 정보를 필요한 것만 추려서 아래와 같이 표시합니다.




Weather in Eden Prairie



날씨가 좀 시원한 편이네요.
온도는 섭씨로 표시됩니다.

이렇게 표시되도록 하려면 지난번 소스에다 메뉴를 추가 하고 refresh button 을 만들어서 이 버튼을 선택했을 경우 이를 처리할 수 있도록 해야 합니다.

버튼 선택시에 지난번에 만들었던 AsyncTask 가 실행되도록 해야겠죠.
지난번에 살펴 봤듯이 이곳에서는 날씨 웹사이트에 정보를 요청한 후 이를 받아서 처리해야 합니다.

그런데 여기서 정보를 받는것은 JSON 포맷으로 받을 것이기 떄문에 이를 파싱하는 로직도 넣어야 합니다.

그리고 날씨 웹사이트에 정보를 요청하려면 Android Permission 들 중에 Internet Permission 을 Manifesto xml 파일에 세팅해야 합니다.

이 퍼미션 세팅에 대해서도 배울 겁니다.

대개 이런 기능들을 추가하면 위와 같이 작동되도록 할 수 있겠네요.

시간이 허락하는 대로 다시 이 강좌 공부를 계속 이어 나가야겠습니다.

반응형

Udacity 강좌 - Lesson 2 실습 03

2016. 8. 17. 10:38 | Posted by 솔웅


반응형

오늘은 어제에 이어 Refactoring 한 소스코드를 분석해 보겠습니다.


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


https://github.com/udacity/Sunshine-Version-2/tree/2.02_refactor_forecast_fragment


Branch 를 2.02 로 한 후 소스코드를 받아 보세요.


먼저 MainActivity 를 살펴 보겠습니다.


public class MainActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.container, new ForecastFragment())
                    .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.main, 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);
    }
}


ForecastFragment 클래스가 별도의 파일로 옮겨지는 바람에 소스가 아주 간단해 졌습니다.


먼저 onCreate() 메소드를 보면 이전에 다 설명 됐던 부분인데요.

activity_main 레이아웃을 View로 세팅하고 첫번째 실행일 경우 ForecastFragment 클래스를 container라는 id를 가진 곳에 할당해 줍니다.


그러면 이제 container라는 id를 가진 곳에 ForecastFragment 클래스에서 구현한 내용이 표시 될 겁니다. 이 클래스는 조금 있다가 보구요.


보니까 onCreateOptionsMenu() 메소드에서는 menu 라는 파라미터를 받아서 이를 메뉴로 표시할 수 있도록 구현돼 있구요.


onOptionsItemSelected() 메소드에서는 메뉴 아이템이 선택 됐을 경우 어떻게 할지를 구현하는 곳입니다.



이제 ForecastFragment 클래스를 보겠습니다.


/**
 * Encapsulates fetching the forecast and displaying it as a {@link ListView} layout.
 */
public class ForecastFragment extends Fragment {

    private ArrayAdapter<String> mForecastAdapter;

    public ForecastFragment() {
    }

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

        // Create some dummy data for the ListView.  Here's a sample weekly forecast
        String[] data = {
                "Mon 6/23 - Sunny - 31/17",
                "Tue 6/24 - Foggy - 21/8",
                "Wed 6/25 - Cloudy - 22/17",
                "Thurs 6/26 - Rainy - 18/11",
                "Fri 6/27 - Foggy - 21/10",
                "Sat 6/28 - TRAPPED IN WEATHERSTATION - 23/18",
                "Sun 6/29 - Sunny - 20/7"
        };
        List<String> weekForecast = new ArrayList<String>(Arrays.asList(data));

        // Now that we have some dummy forecast data, create an ArrayAdapter.
        // The ArrayAdapter will take data from a source (like our dummy forecast) and
        // use it to populate the ListView it's attached to.
        mForecastAdapter =
                new ArrayAdapter<String>(
                        getActivity(), // The current context (this activity)
                        R.layout.list_item_forecast, // The name of the layout ID.
                        R.id.list_item_forecast_textview, // The ID of the textview to populate.
                        weekForecast);

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

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

        return rootView;
    }

    public class FetchWeatherTask extends AsyncTask<Void, Void, Void> {

        private final String LOG_TAG = FetchWeatherTask.class.getSimpleName();

        @Override
        protected Void doInBackground(Void... params) {
            // These two need to be declared outside the try/catch
            // so that they can be closed in the finally block.
            HttpURLConnection urlConnection = null;
            BufferedReader reader = null;

            // Will contain the raw JSON response as a string.
            String forecastJsonStr = null;

            try {
                // Construct the URL for the OpenWeatherMap query
                // Possible parameters are avaiable at OWM's forecast API page, at
                // http://openweathermap.org/API#forecast
                String baseUrl = "http://api.openweathermap.org/data/2.5/forecast/daily?q=94043&mode=json&units=metric&cnt=7";
                String apiKey = "&APPID=" + BuildConfig.OPEN_WEATHER_MAP_API_KEY;
                URL url = new URL(baseUrl.concat(apiKey));

                // Create the request to OpenWeatherMap, and open the connection
                urlConnection = (HttpURLConnection) url.openConnection();
                urlConnection.setRequestMethod("GET");
                urlConnection.connect();

                // Read the input stream into a String
                InputStream inputStream = urlConnection.getInputStream();
                StringBuffer buffer = new StringBuffer();
                if (inputStream == null) {
                    // Nothing to do.
                    return null;
                }
                reader = new BufferedReader(new InputStreamReader(inputStream));

                String line;
                while ((line = reader.readLine()) != null) {
                    // Since it's JSON, adding a newline isn't necessary (it won't affect parsing)
                    // But it does make debugging a *lot* easier if you print out the completed
                    // buffer for debugging.
                    buffer.append(line + "\n");
                }

                if (buffer.length() == 0) {
                    // Stream was empty.  No point in parsing.
                    return null;
                }
                forecastJsonStr = buffer.toString();
            } catch (IOException e) {
                Log.e(LOG_TAG, "Error ", e);
                // If the code didn't successfully get the weather data, there's no point in attemping
                // to parse it.
                return null;
            } finally {
                if (urlConnection != null) {
                    urlConnection.disconnect();
                }
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (final IOException e) {
                        Log.e(LOG_TAG, "Error closing stream", e);
                    }
                }
            }
            return null;
        }
    }
}


좀 긴데요.


우선 MainActivity 에서 이 앱을 처음 실행할 때 ForecastFragment 클래스를 실행하도록 onCreate() 메소드에서 구현해 놨습니다.


이렇게 되면 이 클래스의 생성자가 불려와서 실행 될  텐데... 아직 생성자는 Empty 상태 입니다.


그러면 다음에 onCreateView() 메소드가 참조 될 겁니다.


이 코드도 지난번에 다 분석했던 겁니다.


일단 배열에 요일별 날씨를 하드코딩해 놓았구요.

이것을 리스트에 담은 다음 ArrayAdapter 에서 처리를 했습니다.

그리고 View를 정의해 놓은 다음 이 뷰를 리스트 뷰에 할당하고 이 리스트뷰에 ArrayAdapter를 세팅합니다.


그리고 이 rootView를 return 합니다.


이렇게 하면 이전에 실행했던 대로 변하지 않고 앱이 표시될 겁니다.


그 다음에 AsyncTask 를 구현한 클래스가 있는데요. 아직 이 클래스를 호출하는 곳은 없습니다.

즉 이 클래스가 아직은 실행이 되지는 않을 건데요.


다음 단원에서 이 분을 코딩할 것 같습니다.


일단 이 클래스 (FetchWeatherTask) 를 분석해 보겠습니다.


일단 AsyncTask 를 extends 했습니다. 이 부분을 구현하겠다는 얘기이지요.


안에를 보면 getSimpleName() 메소드를 사용해 이 클래스의 이름을 구해서 LOG_TAG string 변수에 할당합니다.


그리고 AsyncTask 의 메소드인 doInBackground() 메소드가 있습니다.


바로 여기에 네트워킹 작업을 구현해서 Main Thread 가 아닌 Background Thread에서 이 일을 처리하도록 할 겁니다.


HttpURLConnection 와 BufferedReader 를 try 구문 밖에서 선언했는데요. 이것은 나중에 finally 블럭에서 이 두개를 close 시킬 수 있게 하기 위해서 이렇게 했습니다.


그리고 JSON 정보를 받을 forecastJsonStr 이라는 이름의 스트링 변수를 선언했습니다.


이제 try 구문이 시작되는데요.


OpenWeatherMap 에 정보를 요청할 쿼리부터 만듭니다.


baseUrl 에 기본 정보를 담고 apiKey 에 AppID를 넣습니다.

이 AppID는 본인의 것을 넣어야 합니다.

여기서는 BuildConfig.OPEN_WEATHER_MAP_API_KEY 라고 해서 XML에서 불러오도록 했는데요.

저는 아직 이 부분을 구현해 놓지 않아서 이부분에 그냥 제 AppID를 하드코딩 해 넣었습니다.


그런다음 이 전체 정보 (URL+AppID) 를 URL에 담습니다.


그 다음엔 connection을 오픈하고 request method는 GET 형식으로 정하고 마지막에 connect() 합니다.


이제 OpenWeatherMap 에 요청 정보를 보냈고 이곳에서 보내오는 응답을 처리할 차례입니다.


정보를 InputStream에 담은 후 BufferReader에 이 inputStream을 담습니다.


그리고 난 다음에 이 BufferReader에 있는 내용을 while문을 돌려서 하나하나 읽으면서 line 이라는 String에 담습니다.


그리고 이 String을 아까 선언했든 forecastJsonStr 이라는 String에 담습니다.


여기까지 Try 구문이구요.


catch 구문에서는 예외사항이 발생했을 때 이 에러 내용을 로그에 표시하도록 합니다.

LOG_TAG에는 이 클래스 이름을 담았었는데요. 이 이름을 로그 앞에 표시하도록 했습니다.


그리고 finally 구문에서는 connection 들을 담습니다.



여기 까지 인데요. 이렇게 한 다음에 이 앱을 실행하면 지난번에 나왔던 하드코딩한 날씨정보가 그대로 나옵니다.




아직 FetchWeatherTask 를 실행하도록 하지 않았기 때문입니다.


여기서는 단지 NetworkOnMainThreadException 이 나오지 않도록 Refactoring 한 겁니다.


이제 다음 단원에서 실제로 이 AsyncTask 로 처리해서 Network 가 Background로 돌아가게 하고 또 실제 날씨 정보를 OpenWeatherMap 에서 받는 작업을 할 겁니다.



반응형

Udacity 강좌 - Lesson 2 실습 02

2016. 8. 16. 10:51 | Posted by 솔웅


반응형

오늘은 지난번에 분석했던 네트워크 연결 관련 소스코드를 실제로 프로젝트에 집어 넣겠습니다.


우선 https://gist.github.com/anonymous/1c04bf2423579e9d2dcd 에 있는 소스코드를 MainActivity 의 PlaceholderFragment class에 있는 onCreateView Method 에 삽입합니다.


이 때 몇가지 import 해야 하는데요. 이건 Auto Import를 활성화 해 놓으면 이클립스가 알아서 처리합니다.


Mac instructions: To enable auto-import, go to Android Studio > Preferences > Editor > Auto Import. Check all the boxes and insert all imports on paste.

Windows instructions: To enable auto-import, go to File > Settings > Editor > Auto Import > Java. Check all the boxes and insert all imports on paste.


이렇게 한 다음에 실행 하면 NetworkOnMainThreadException 이 일어납니다.

Android Device Monitor 에서 프로세스 아이디 (여기서는 7762)로 LOGCAT을 검색해 보면 위 그림과 같이 NetworkOnMainThreadException 이 일어났다는 메세지가 나오고 어느 클래스의 몇번째 줄에 있는 코드에서 예외가 발생했다는 로그들을 찾아 볼 수 있습니다.



저 로그를 보면 에러는 115번째 줄에서 난 것으로 나옵니다.


바로  urlConnection.connect(); 부분 입니다.

그 이유는 이 연결은 Main Thread 에서는 구현하면 안되는 것이기 때문입니다.

그럼 이제 이 소스코드를 올바르게 Refactoring 해야 하는데요. 일단 그 전에 프로세스와 쓰레드에 대해 간단히 공부하고 넘어가겠습니다.

메인 쓰레드에서는 대개 사용자와 상호 작용하는 UI 관련 작업들을 담당하게 됩니다.

백그라운드 쓰레드에서는 long-running work 을 주로 담당하게 됩니다.

Background Thread 를 생성하려면 AsyncTask 를 사용하면 간단하게 처리할 수 있습니다.

이 클래스의 핵심 메소드는 doInBackground() 입니다.

이 글 쓰기 전에 프로세스와 쓰레드에 대해 두어개의 글을 올렸는데 그것도 도움이 될 겁니다.

이제 네트워크 관련 소스코드들을 AsyncTask 로 옮겨 보겠습니다.

옮기는 순서는 

1. PlaceholderFragment class 를 ForecastFragment class 로 이름을 바꾼다

2. ForecastFragment 라는 새로운 자바 파일을 만들어서 이 클래스를 옮긴다.

3. FetchWeatherTask 라는 이름의 AsyncTask 클래스를 만든다.

이렇게 하면 되는데요.

이 과정은 다음 글에서 코드를 분석한 후에 실습을 이어 나가겠습니다.


반응형

Android Network Connection

2016. 8. 11. 08:53 | Posted by 솔웅


반응형

오늘은 어제 Udacity 강좌에서 참고 자료로 사용했던 Android Network Connection 관련 글 두개를 공부 해 봤습니다.


From Android Developer Training Material


Connecting to the Network

이 과정에서는 네트워크에 연결하는 간단한 어플리케이션을 구현하는 방법을 보여 드릴 겁니다. 아주 간단한 network-connected app을 생성하기 위한 가장 좋은 방법을 설명합니다.

네트워크를 사용하려면 manifest 에 아래 두 permission이 설정 돼 있어야 합니다.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />


Choose an HTTP Client

대부분의 network-connected Android app 들은 데이터를 주고 받기 위해 HTTP를 사용합니다. 안드로이드 플랫폼에는 HTTPS, streaming uploads and downloads,configurable timeouts, IPv6 그리고 connection pooling을 지원하는 HttpURLConnection client 를 포함하고 있습니다.



Check the Network Connection

여러분의 앱이 네트워크에 접속하려고 하기 전에 네트워크 접속이 가능한지 여부에 대해서 getActiveNetworkInfo() 와 isConnected() 를 사용해서 체크해야 합니다. 해당 전화기가 네트워크가 안되는 상황일 수 있기 때문입니다. 이와 관련해서 자세한 사항은 Managing Network Usage 를 참조하세요.

public void myClickHandler(View view) {
    ...
    ConnectivityManager connMgr = (ConnectivityManager)
        getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
    if (networkInfo != null && networkInfo.isConnected()) {
        // fetch data
    } else {
        // display error
    }
    ...
}

Perform Network Operations on a Separate Thread

네트워크 작업은 예상치 못한 지연이 있을 수 있습니다. 이런 상황에서 사용자에게 불편을 주지 않기 위해 네트워크 작업은 UI를 다루는 쓰레드와는 별도의 쓰레드에서 이루어지도록 해야 합니다. AsyncTask 클래스는 UI 쓰레드와 별도의 쓰레드를 사용하도록 하는 간단한 방법을 제공합니다. 여기에 대한 자세한 설명은 Multithreading For Performance 를 참조하세요.

아래 소스코드는 myClickHandler() 메소드가 new DownloadWebpageTask().execute(stringUrl) 를 invoke 하는 것을 다루고 있습니다. DownloadWebpageTask 클래스는 AsyncTask 의 subclass 입니다. DownloadWebpageTask 클래스는 AsyncTask methods 를 implement 합니다.

    doInBackground() 는 downloadUrl() 메소드를 실행합니다. 파라미터로 웹페이지의 URL을 전달합니다. downloadUrl() 메소드는 웹페이지 connection을 수행합니다. 이 작업이 끝나면 결과를 String 형식으로 전달해 줍니다.

    onPostExecute() 는 return 된 string을 받아서 UI에 display 합니다.

public class HttpExampleActivity extends Activity {
    private static final String DEBUG_TAG = "HttpExample";
    private EditText urlText;
    private TextView textView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        urlText = (EditText) findViewById(R.id.myUrl);
        textView = (TextView) findViewById(R.id.myText);
    }

    // When user clicks button, calls AsyncTask.
    // Before attempting to fetch the URL, makes sure that there is a network connection.
    public void myClickHandler(View view) {
        // Gets the URL from the UI's text field.
        String stringUrl = urlText.getText().toString();
        ConnectivityManager connMgr = (ConnectivityManager)
            getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
        if (networkInfo != null && networkInfo.isConnected()) {
            new DownloadWebpageTask().execute(stringUrl);
        } else {
            textView.setText("No network connection available.");
        }
    }

     // Uses AsyncTask to create a task away from the main UI thread. This task takes a
     // URL string and uses it to create an HttpUrlConnection. Once the connection
     // has been established, the AsyncTask downloads the contents of the webpage as
     // an InputStream. Finally, the InputStream is converted into a string, which is
     // displayed in the UI by the AsyncTask's onPostExecute method.
     private class DownloadWebpageTask extends AsyncTask<String, Void, String> {
        @Override
        protected String doInBackground(String... urls) {

            // params comes from the execute() call: params[0] is the url.
            try {
                return downloadUrl(urls[0]);
            } catch (IOException e) {
                return "Unable to retrieve web page. URL may be invalid.";
            }
        }
        // onPostExecute displays the results of the AsyncTask.
        @Override
        protected void onPostExecute(String result) {
            textView.setText(result);
       }
    }
    ...
}

이 소스코드의 실행 순서는 아래와 같습니다.

    1. 사용자가 버튼을 클릭하면 myClickHandler() 가 invoke 됩니다. 사용자가 입력한 URL을 AsyncTask subclass 인 DownloadWebpageTask 로 전달합니다.
     2. AsyncTask doInBackground() 메소드는 downloadUrl() 메소드를 호출합니다.
    3. downloadUrl() 메소드는 파라미터로 URL string을 받고 URL 객체를 생성하는데 이를 사용합니다.
    4. URL 객체는 HttpURLConnection 를 만드는데 사용됩니다.
    5. connection이 이뤄지면 HttpURLConnection 객체는 InputStream으로 웹페이지의 내용을 받아서 담습니다.
    6. InputStream은 readIt() 메소드에 전달됩니다. 이 메소드는 stream을 string으로 변환합니다.

    7. 마지막으로 AsyncTask의 onPostExecute() 메소드가 main activity의 UI에 string을 display 합니다.



Connect and Download Data



네트워크 트랜잭션을 다루는 쓰레드에서 데이터를 GET 하고 다운로드하도록 하기 위해 HttpURLConnection 를 사용할 수 있습니다. connect()를 호출한 후 getInputStream()을 호출함으로서 데이터의 InputStream을 받을 수 있습니다.

다음 소스코드에서는 doInBackground() 메소드가 downloadUrl() 메소드를 호출합니다. 이 downloadUrl() 메소드는 주어진 URL을 받아서 HttpURLConnection을 거쳐 네트워크에 연결하는데 사용합니다.

// Given a URL, establishes an HttpUrlConnection and retrieves
// the web page content as a InputStream, which it returns as
// a string.
private String downloadUrl(String myurl) throws IOException {
    InputStream is = null;
    // Only display the first 500 characters of the retrieved
    // web page content.
    int len = 500;

    try {
        URL url = new URL(myurl);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setReadTimeout(10000 /* milliseconds */);
        conn.setConnectTimeout(15000 /* milliseconds */);
        conn.setRequestMethod("GET");
        conn.setDoInput(true);
        // Starts the query
        conn.connect();
        int response = conn.getResponseCode();
        Log.d(DEBUG_TAG, "The response is: " + response);
        is = conn.getInputStream();

        // Convert the InputStream into a string
        String contentAsString = readIt(is, len);
        return contentAsString;

    // Makes sure that the InputStream is closed after the app is
    // finished using it.
    } finally {
        if (is != null) {
            is.close();
        }
    }
}

getResponseCode() 메소드가 connection의 status code를 return 하는 것을 기억해 두세요. connection에 대한 추가적인 정보를 얻는에 아주 유용한 방법입니다. status code 가 200 이면 성공한 겁니다.



Convert the InputStream to a String



InputStream은 byte로 이뤄져 있습니다. 이 InputStream을 받으면 일반적으로 이것을 특정 데이터 타입으로 decode 하거나 변환합니다. 예를 들어 이미지 데이터를 다운로드 한다면 이것을 아래와 같이 decode 해야 할 것입니다.

InputStream is = null;
...
Bitmap bitmap = BitmapFactory.decodeStream(is);
ImageView imageView = (ImageView) findViewById(R.id.image_view);
imageView.setImageBitmap(bitmap);

아래 예제에서는 InputStream에 웹페이지의 문자를 받은 경우 입니다. InputStream을 String으로 변환하는 방법이죠. 이렇게 한 후 UI에 display 하게 됩니다.

// Reads an InputStream and converts it to a String.
public String readIt(InputStream stream, int len) throws IOException, UnsupportedEncodingException {
    Reader reader = null;
    reader = new InputStreamReader(stream, "UTF-8");
    char[] buffer = new char[len];
    reader.read(buffer);
    return new String(buffer);
}




Android’s HTTP Clients
Jesse Wilson

[This post is by Jesse Wilson from the Dalvik team. —Tim Bray]


대부분의 network-connected Android app 들은 데이터를 주고 받기 위해 HTTP를 사용합니다. 안드로이드 플랫폼에는 HTTPS, streaming uploads and downloads,configurable timeouts, IPv6 그리고 connection pooling을 지원하는 HttpURLConnection client 를 포함하고 있습니다.



Apache HTTP Client



DefaultHttpClient 와 그 사촌인 AndroidHttpClient 는 웹브라우저에 적합한 확장 가능한 HTTP 클라이언트 입니다. 이들은 방대하고 유연한 API들을 가지고 있습니다. 이들을 구현하는 것은 유용하지만 몇개의 버그들도 있습니다.

하지만 방대한 규모의 이 API는 오히려 사용하는데 헛갈리게 만들기도 합니다. 안드로이드 팀은 Apache HTTP Client 와 관련해 적극적으로 대응해서 일하지 않는 것 같습니다.



HttpURLConnection



HttpURLConnection은 일반적이고 간단한 HTTP client에 대해 사용됩니다. 이 클래스는 작게 시작했지만 지속적으로 개선해서 아주 사용하기 편리하게 만들어 졌습니다.

Froyo 이전에는 HttpURLConnection 에 버그들이 좀 있었습니다. 특히 readable InputStream에서 close()를 호출하는 경우 connection pool에 문제가 발생했었습니다. 이것을 해결하는 방법은 connection pooling을 disabling 하는 것 이었습니다.

private void disableConnectionReuseIfNecessary() {
    // HTTP connection reuse which was buggy pre-froyo
    if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) {
        System.setProperty("http.keepAlive", "false");
    }
}


Gingerbread에서는 transparent response compression를 추가했습니다. HttpURLConnection 은 이 헤더를 outgoing request에 자동적으로 추가했습니다. 그리고 해당 response에서도 그에 상응하도록 처리합니다.



Accept-Encoding: gzip



이것의 장점은 웹 서버에서 response를 압축하도록 client에서 설정하도록 한다는 것입니다. 프로그램에서 response 압축을 하도록 한 경우 이것을 어떻게 disable 하게 할 것인가가 이 문서에 나와 있습니다.

HTTP의 Content-Length header가 압축된 사이즈를 return 하기 때문에 압축되지 않은 데이터의 buffer의 사이즈를 재는 getContentLength()를 이용하면 에러가 납니다. 이 대신에 InputStream.read() returns -1 일 경우 response 에 대해 byte를 읽어야 합니다.

Gingerbread 에서는 이 HTTPS에 대해 여러가지 개선을 했습니다. HttpsURLConnectionSNI (Server Name Indication) 에 연결하는 것을 시도합니다. 이것은 IP 주소를 공유하기 위한 여러개의 HTTPS를 가능하도록 하는 겁니다. 또한 압축과 세션 티켓을 가능하도록 했습니다. connection이 실패하면 자동적으로 이러한 features 없이 retire 합니다. 이것은 up-to-date sever들에 접속할 때 오래된 서버들과의 정합성을 해치지 않으면서 HttpsURLConnection 를 효율적으로 사용할 수 있도록 합니다.

Ice Cream Sandwich 에서는 response cashe를 추가했습니다. cashe 가 인스톨 되면 HTTP request는 3개중 하나에 속하게 됩니다.

    * Fully cached responses가 local storage 로부터 직접적으로 serve 된다. 왜냐하면 어떤 네트워크 connection도 이러한 response를 즉시 가능하게 할 필요가 없기 때문입니다.

    * 조건부로 cached responses들은 웹서버에 의해 검증된 최신 정보를 가지고 있어야 한다. 클라이언트는 “Give me /foo.png if it changed since yesterday” 같은 request를 보냅니다. 그러면 서버는 업데이트 된 내용이나 304 Not Modified status를 return 하게 됩니다. 만약 내용이 변동되지 않았다면 download 되지 않을 것입니다.

    Uncached responses들은 웹에서 serve 된다. 이러한 responses들은 나중에 response cache에 저장될 것입니다.

HTTP response가 이것을 지원하는 기기에 대해 cache 하도록 하기 위해서는 reflection을 사용합니다. 이 예제는 Ice Cream Sandwich에 대한 response cache를 turn on 할 것입니다.

private void enableHttpResponseCache() {
    try {
        long httpCacheSize = 10 * 1024 * 1024; // 10 MiB
        File httpCacheDir = new File(getCacheDir(), "http");
        Class.forName("android.net.http.HttpResponseCache")
            .getMethod("install", File.class, long.class)
            .invoke(null, httpCacheDir, httpCacheSize);
    } catch (Exception httpResponseCacheNotAvailable) {
    }
}

여러분은 HTTP response에 대한 cache header를 세팅하기 위해 웹 서버를 configure 해야 합니다.


Which client is best?



Apache HTTP 클라이언트는 Eclair와 Froyo에서 비교적 버그가 적게 나타난다.

Gingerbread 이상에서는 HttpURLConnection이 최상이다. 이것은 간단한 API이자 적은 노력으로 큰효과를 보인다. Transparent compression과 response caching를 사용하면 네트워크 사용량을 줄일 수 있다. 또한 속도를 높이고 배터리를 절약할 수 있다. 새로운 어플리케이션은 HttpURLConnection을 사용해야 한다.


반응형

Udacity 강좌 - Lesson 2 실습 01

2016. 8. 10. 10:14 | Posted by 솔웅


반응형

날씨 정보를 받을 싸이트

http://openweathermap.org/

정보를 받으려면 API Key 를 사용해야 함. 내 API Key 받는 곳

http://openweathermap.org/appid#get

API Key 관련 update 된 Sunshine 소스 코드

https://github.com/udacity/Sunshine-Version-2

https://docs.google.com/document/d/1e8LXahedBlCW1_dp_FyvQ3ugUAwUBJDuJCoKf3tgNVs/pub?embedded=true

Permission과 Privacy 에 대한 정보

https://developer.android.com/preview/features/runtime-permissions.html


Open weather map 싸이트로 부터 날씨 정보 받는 신택스

api.openweathermap.org/data/2.5/weather?q=London,uk&APPID={APIKEY}

날씨정보는 우편번호, 도시 이름 등으로 받을 수 있다. i.e. 55347, USA

7일치 날씨 정보를 볼 수 있는 곳

http://openweathermap.org/forecast

16일치 날씨 정보

http://openweathermap.org/forecast#16days



Weather 에 대한 HTTP Request 하기
1. HTTP Request 만들기 : HttpURLConnection
2. input stream 으로부터 response 읽기 : BufferedReader , InputStream
3. 로그 만들기

https://gist.github.com/udacityandroid/d6a7bb21904046a91695

// These two need to be declared outside the try/catch
            // so that they can be closed in the finally block.
            HttpURLConnection urlConnection = null;
            BufferedReader reader = null;

            // Will contain the raw JSON response as a string.
            String forecastJsonStr = null;

            try {
                // Construct the URL for the OpenWeatherMap query
                // Possible parameters are avaiable at OWM's forecast API page, at
                // http://openweathermap.org/API#forecast
                URL url = new URL("http://api.openweathermap.org/data/2.5/forecast/daily?q=94043&mode=json&units=metric&cnt=7");

                // Create the request to OpenWeatherMap, and open the connection
                urlConnection = (HttpURLConnection) url.openConnection();
                urlConnection.setRequestMethod("GET");
                urlConnection.connect();

                // Read the input stream into a String
                InputStream inputStream = urlConnection.getInputStream();
                StringBuffer buffer = new StringBuffer();
                if (inputStream == null) {
                    // Nothing to do.
                    return null;
                }
                reader = new BufferedReader(new InputStreamReader(inputStream));

                String line;
                while ((line = reader.readLine()) != null) {
                    // Since it's JSON, adding a newline isn't necessary (it won't affect parsing)
                    // But it does make debugging a *lot* easier if you print out the completed
                    // buffer for debugging.
                    buffer.append(line + "\n");
                }

                if (buffer.length() == 0) {
                    // Stream was empty.  No point in parsing.
                    return null;
                }
                forecastJsonStr = buffer.toString();
            } catch (IOException e) {
                Log.e("PlaceholderFragment", "Error ", e);
                // If the code didn't successfully get the weather data, there's no point in attemping
                // to parse it.
                return null;
            } finally{
                if (urlConnection != null) {
                    urlConnection.disconnect();
                }
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (final IOException e) {
                        Log.e("PlaceholderFragment", "Error closing stream", e);
                    }
                }
            }

            return rootView;
        }
    }
}


소스 코드 분석

우선 HttpURLConnection과 BufferedReader 변수를 선언한다.

이렇게 하면 나중에 finally block 에서 connection을 해제 할 수 있다.

그리고 날씨 정보를 요청할 쿼리를 담을 String 변수도 try 만 밖에 선언한다.

try 문 안에서는 날씨를 요청할 쿼리 (URL)을 URL 변수에 담는다.

그리고 아까 만들어 놨던 HttpURLConnection 변수 (urlConnection)를 사용해서 connection을 open 하고 Request Method를 GET으로 선언하고 실제로 connect 한다.


이제 URL을 String에 넣는 작업을 한다.

먼저 URL 을 InputStream 변수에 넣은 다음에 처음에 만들어 놨던 BufferedReader 변수(reader) 에 이 InputStream 을 담는다.

그리고 이 reader를 while 문을 통해 StringBuffer 에 담는다.

마지막으로 try문 밖에서 선언했던 String 변수 (forecastJsonStr) 에 이 StringBuffer (buffer)의 내용을 담는다.


이렇게 하면 URL 형식의 날씨 요청 쿼리를 사용해서 connection을 열고 이 쿼리를 String 변수에 담는 것까지 완료 됐습니다.


catch 문에는 Exception이 발생 했을 경우 Log를 남기고 null을 return 하도록 하구요.


Exception이 있던 없던 실행되는 finally 구문에서는 urlConnection을 disconnect 합니다.

그리고 BurreredReader 도 close 하구요.


새로 추가할 소스 분석은 여기까지구요.

참고로 HTTP 연결은 HTTPUrlConnection 과 HttpClient 두개 다 사용 가능한다. HTTPUrlConnection을 추천한답니다.

https://developer.android.com/training/basics/network-ops/connecting.html

http://android-developers.blogspot.com/2011/09/androids-http-clients.html?_sm_au_=iRVfk4LV6Z6647f6



Android Studio에는 로그를 볼 수 있는 Logcat이 있는데요.

Android Studio의 안드로이드 아이콘을 클릭하면 DDMS가 실행되고 이 DDMS 안에 LogCat 창이 있습니다.

https://developer.android.com/studio/command-line/adb.html?utm_source=udacity&utm_medium=mooc&utm_term=android&utm_content=adb&utm_campaign=training

Log Level은 아래와 같습니다.

- ERROR
- WARN
- INFO
- DEBUG
- VERBOSE

다음 글에서는 위의 새로운 소스 코드를 실제로 파일에 추가하고 refactoring 하고 실행해 보는 것 까지 할 것 같습니다.



반응형


반응형

Udacity - Developing Android Apps Lesson 2 Summary


New Concepts


    HttpURLConnection
    Logcat
    MainThread vs. Background Thread
    AsyncTask
    Adding Menu Buttons
    values/strings.xml
    Permissions
    JSON Parsing

   
* HttpURLConnection

HttpURLConnection 는 자바 클래스로 웹을 통해 데이터를 주고 받을 때 주로 사용합니다. 우리는 이것을 OpenWeatherMap API 로부터 JSON 데이터를 받기 위해 사용합니다. 이 코드가 해야할 일은 이 비디오와 gist 에서 설명하고 있습니다.


* Logcat

Logcat 은 안드로이드 디바이스의 logging output을 사용할 수 있도록 하는 프로그램입니다. 이 코스에서는 3가지 방법을 사용하고 있습니다.

1. 안드로이드 스튜디오에는 DDMS 윈도우가 있는데 여기에 logcat output을 포함하고 있습니다. Android Window가 선택되어 있어야 합니다.



2. 터미널을 열고 adb logcat 을 칩니다. 필요한 output을 얻고자 한다면 developer guide를 보시면 좀 더 자세한 내용들을 보실 수 있습니다.

3. Android DDMS 를 별도의 창에서 열고 logcat 으로 갑니다.



log 들이 5가지로 분류되서 제공됩니다. Verbose, Debug, Info, Warn, Error. log level을 선택함으로서 원하시는 log만 보실 수 있습니다. 예를 들어 warning 이상의 로그들을 선택하면 Warning 과 Error 로그들을 보실 겁니다.

여러분의 코드에 이 logcat 에 보낼 로그를 설정하실 수 있습니다. 사용하시는 방법은 Log 클래스를 사용해서 v,d,i,w,e 메소드들을 활용합니다. 각각의 메소드는 두개의 string을 갖는데요 하나는 어디서 로그가 나오는지 하고 다른 파라미터는 log 메세지 입니다.
tag에 대해 이 코스에서 사용하는 규칙은 해당 상수가 있는 클래스의 이름과 같은 String 상수 LOG_TAG를 생성한다는 겁니다. class 이름을 프로그램에서 얻을 수 있습니다.

private final String LOG_TAG = MainActivity.class.getSimpleName();


* MainThread vs. Background Thread

안드로이드에는 Main Thread 혹은 UI Thread 라는 개념이 있습니다. 쓰레드에 대해 알고 싶으시면 여기 wikipedia article을 보세요. main thread는 UI가 부드럽게 진행되도록 합니다. 그리고 사용자의 input에 반응 하는 것을 담당합니다. 한번에 하나의 task 만 구동할 수 있습니다. Main Thread 에서 아주 오랫동안 프로세스를 시작하면, 예를 들어 아주 복잡한 계산이나 프로세스 로딩 등, 이 프로세스가 끝날 때까지 사용자는 input에 대한 반응을 받지 못할 겁니다.
그래서 긴 프로세스를 시작해야 되면 Background thread를 사용하는 것을 검토 해야 합니다. 그러면 Main Thread를 block 하는 일은 없을 겁니다. 이렇게 하는 방법 중에 AsyncTask 의 subclass를 생성하는 것도 하나의 방법 입니다.


* AsyncTask

AsyncTask는 쉽게 background thread 에서 일을 진행할 수 있도록 하는 안드로이드 클래스 입니다. 이것을 사용하면 Main Thread를 방해하지 않게 됩니다. 이 AsyncTask를 사용하려면 FetchWeatherTask에서 했던 대로 이것을 subclass 화 해야 합니다. 아래는 이를 override 하기 위한 4가지 중요한 부분입니다.

- onPreExecute : 이 메소드는 task 가 시작되기 전에 UI 단에서 실행됩니다. 셋업이라든지 작업이 시작되기 전에 이루어 져야 할 부분들을 이 메소드에서 정의 합니다.
- doInBackground : 이 부분이 바로 main thread를 방해하지 않으면서 작업을 진행 할 수 있도록 하는 부분 입니다. 이 부분에 코드가 작성되면 이는 background thread에서 작동되며 UI 단을 방해하지 않습니다.
- onProgressUpdate : 이것은 UI thread에서 구동되는 메소드 입니다. 주로 해당 작업의 진행상황을 보여줍니다. 예를 들어 loading bar 같은 것을 보여줍니다.
- onPostExecute :  해당 작업이 끝난 후 UI 단에서 실행되는 메소드 입니다.

AsyncTask를 시작할 때 현재 있는 activity에서 이 작업이 이루어 지고 있다는 것을 유념하세요. 그 activity 가 destroy 되면 (예를 들어 핸드폰이 ratate 됐다던가 해서..) 이 AsyncTask 는 새로 활성화 된 Activity 가 아니라 destroy 된 activity를 참고 하게 될 겁니다. 이게 AsyncTask를 사용할 때 위험한 부분 입니다.



* Adding menu buttons

메뉴 버튼을 만들기 위한 기본적인 과정들 입니다.
1. xml 파일을 res/menu 에 추가한다. 여기에는 추가할 버튼들이 정의 된다. 순서, 정렬 같은 속성들도 같이 정의 됩니다.
2. 메뉴 버튼들이 fragment와 연관돼 있다면 그 fragment의 onCreate 메소드 안에서 sethasOptionsMenu(ture) 를 호출해야 합니다.
3. onCreateOptionsMenu 안에서 다음과 같이 inflate 합니다.
inflater.inflate(R.menu.forecastfragment, menu);
4. onOptionsItemSelected 에서 어떤 아이템이 선택 됐는지를 체크하고 거기에 대한 반응을 설정 할 수 있습니다. Refresh 의 경우 FetchWeatherTask를 생성하고 실행하게 될 겁니다.


* values/strings.xml

안드로이드에는 앱의 모든 string을 가지고 있는 파일을 만들 수 있습니다. values/strings.xml이 그 파일인데요. 이렇게 모든 string을 하나의 파일에서 관리하는것이 권장되는 이유는 layout들에서 그 내용들을 관리하는 것 보다는 한 곳에서 관리하는 것이 좀 더 유용하기 때문입니다. values-lanuage/strings.xml을 만들어서 특정 언어에 해당하는 string들을 따로 관리할 수도 있습니다. 예를 들어 앱의 일본어 버전을 만들고 싶으면 values-ja/string.xml을 생성하실 수 있습니다. string에 translatable='false' 가 설정돼 있으면 해당 string은 따로 번역되지 않는다는 의미입니다. 이 기능은 고유명사 등에 이용하시면 유용할 겁니다.


* Permissions

디폴트로 안드로이드 내의 앱들은 sandbox화 돼 있습니다. 이 의미는 각 앱들은 고유의 username이 있다는 겁니다. 그리고 virtual machine에서 고유의 instance에서 실행된다는 것이구요. 또한 고유의 파일들과 메모리를 관리한다는 겁니다. 그래서 어떤 앱이 다른 앱 혹은 다른 전화기 등과 상호작용을 하도록 하려면 해당 permission을 요청해야 합니다.

Permission들은 AndroidManifest.xml에서 선언됩니다. 예를 들어 인터넷에 접근한다거나 SMS를 보내거나 전화기에 저장된 연락처들을 보거나 할 때 해당 퍼미션을 선언해야 합니다. 사용자가 앱을 다운받을 때 그 앱이 요청하는 퍼미션들을 보게 될 겁니다. 꼭 필요한 permission만 선언하는게 바람직합니다.


* JSON Parsing

API를 통해 데이터를 요청하면 종종 JSON 같은 format으로 데이터를 받게 됩니다. Open Weather Map API 를 사용할 때도 마찬가지인데요. JSON string을 받고 난 후 이를 이용하려면 일단 parse 해야 합니다.
JSON에 대해 알고 싶으면 이 tutorial을 참조하세요.

JSON의 구조에 대해 확실하게 모르면 formatter를 사용하실 수 있습니다. 브라우저에서 사용하실 수 있는 이런 formatter도 있습니다.

안드로이드에서는 JSONObject 클래스를 사용하실 수 있습니다. 이에 대한 설명은 여기에 있습니다. 이 클래스를 이용하려면 JSON string을 받아서 거기에 대한 객체를 생성해야 합니다.

JSONObject myJson = new JSONObject(myString);

그리고 나서 다양한 get 메소드를 사용해서 데이터를 추출할 수 있습니다. getJSONArray, getLong etc.

반응형

Udacity 강좌 - Lesson 1 실습

2016. 8. 8. 06:54 | Posted by 솔웅


반응형

Lesson 1 에서는 Sunshine Project 를 생성하고 리스트 뷰를 만드는 과정을 다뤘습니다.

우선 MainActivity 클래스를 한번 보겠습니다.


이 클래스가 시작되면 제일 먼저 onCreate 메소드가 실행됩니다.

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

View는 layout에 있는 activity_main 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=".MainActivity" tools:ignore="MergeRootFrame" />



그 다음 if문에서는 첫번째 인스턴스가 생성될 때 즉 처음 실행되는 경우라면 이 구문 안의 코드를 실행합니다.

이 구문 안에는 위 xml의 아이디인 container를 사용해서 그곳에 PlaceholderFragment() 클래스를 세팅합니다.


그러면 이제 PlaceholderFragment() 클래스를 살펴 봐야 합니다.

/**
* 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_main, container, false);
return rootView;
}
}

이 클래스에서는 onCreateView 메소드 내용을 실행하게 됩니다.

이곳에서는 inflater를 사용해서 layout에 있는 fragment_main xml 파일을 container 에 세팅합니다.


<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=".MainActivity$PlaceholderFragment">

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

</RelativeLayout>

이 파일에는 RelativeLayout 과 TextView 가 있는데 이 파일은 나중에 수정 할 겁니다.


이 프로젝트 구조는 이렇게 돼 있습니다.




이제 여기에 list_item_forecast.xml 파일을 layout 폴더에 추가하겠습니다.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="New Text"
android:id="@+id/list_item_forecast_textview"/>

</LinearLayout>

그리고 나서 fragment_main 파일의 TextView를 ListView로 바꾸고 레이아웃을 FrameLayout으로 바꿉니다.


<FrameLayout 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=".MainActivity$PlaceholderFragment">

<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/listview_forecast"/>

</FrameLayout>


이 파일은 나중에 사용하고 임시로 MainActivity클래스의 PlaceholderFragment 클래스에 배열을 사용해 Fake Data를 만들어 보겠습니다.


@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
String[] forecastArray = {
"Today - Sunny - 88/63",
"Tomorrow - Foggy - 70/40",
"Weds - Cloudy - 72/63",
"Thurs - Asteroids - 75/65",
"Fri - Heavy Rain - 65/56",
"Sat - HELP TRAPPED IN WEATHERSTATION - 60/51",
"Sun - Sunny - 80/68"
};

List<String> weekForecast = new ArrayList<String>(
Arrays.asList(forecastArray)
);

return rootView;
}

이제 이 배열의 내용들을 화면에 표시하려면 ArrayAdapter<T>를 사용해야 합니다.

아까 만들었던 list_item_forecast 레이아웃에 있는 ID가 list_item_forecast_textview인 TextVIew 에 이 내용을 뿌려 줍니다.


/**
* 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_main, container, false);
String[] forecastArray = {
"Today - Sunny - 88/63",
"Tomorrow - Foggy - 70/40",
"Weds - Cloudy - 72/63",
"Thurs - Asteroids - 75/65",
"Fri - Heavy Rain - 65/56",
"Sat - HELP TRAPPED IN WEATHERSTATION - 60/51",
"Sun - Sunny - 80/68"
};

List<String> weekForecast = new ArrayList<String>(
Arrays.asList(forecastArray)
);

ArrayAdapter<String> mForecastAdapter =
new ArrayAdapter<String>(
getActivity(),
R.layout.list_item_forecast,
R.id.list_item_forecast_textview,
weekForecast
);

ListView listView = (ListView) rootView.findViewById(R.id.listview_forecast);
listView.setAdapter(mForecastAdapter);

return rootView;

}

이렇게 하면 list view 를 아래와 같이 볼 수 있습니다.




반응형
이전 1 다음