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

최근에 올라온 글

최근에 달린 댓글

최근에 받은 트랙백

글 보관함

카테고리

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 에서 받는 작업을 할 겁니다.



반응형