유명한 SNS 앱에는 모두 각자의 FEED를 가지고 있습니다. 이번 포스트에서는 VOLLEY 라이브러리를 사용해 앱에서 FEED를 구현해보겠습니다.
명시되지 않은 모든 소스코드 출처 : www.androidhive.info
기본적인 구조는 다음과 같이 구성됩니다.
Activity 측면
1 2 3 4 5 6 7 8 | //ADAPTER FeedListAdapter.java //MAIN MainAcitivy.java //POJO FeedItem.java | cs |
Layout 측면
1 2 3 4 5 | //MAIN activity_main.xml //FEED COMPONETS feed_item.xml | cs |
이외
1 2 3 4 5 6 7 8 | //FOR FEED PICTURE LruBitmapCache.java //FOR VOLLEY AppController.java //FOR FEED IMAGE FeedImageView.java | cs |
기본적인 동작은 다음과 같습니다.
1. MainActivity에서 Feed 데이터를 불러옴
2. 불러온 Feed 데이터를 FeedItem에 저장한 후, FeedListAdapter에 전달
3. FeedListAdapter에서 feed_item (List에 표시될 피드 객체)의 컴포넌트를 초기화 시키고 데이터를 SET
4. activity_main 에서 ListView를 통해 FeedListAdapter 연결
첫째로 AppController.java, LruBitmapCahche.java, FeedImageView.java, feed_item.xml을 각각 프로젝트에 추가합니다.
AppController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | public class AppController extends Application { public static final String TAG = AppController.class.getSimpleName(); private RequestQueue mRequestQueue; private ImageLoader mImageLoader; LruBitmapCache mLruBitmapCache; private static AppController mInstance; @Override public void onCreate() { super.onCreate(); mInstance = this; } public static synchronized AppController getInstance() { return mInstance; } public RequestQueue getRequestQueue() { if (mRequestQueue == null) { mRequestQueue = Volley.newRequestQueue(getApplicationContext()); } return mRequestQueue; } public ImageLoader getImageLoader() { getRequestQueue(); if (mImageLoader == null) { getLruBitmapCache(); mImageLoader = new ImageLoader(this.mRequestQueue, mLruBitmapCache); } return this.mImageLoader; } public LruBitmapCache getLruBitmapCache() { if (mLruBitmapCache == null) mLruBitmapCache = new LruBitmapCache(); return this.mLruBitmapCache; } public <T> void addToRequestQueue(Request<T> req, String tag) { req.setTag(TextUtils.isEmpty(tag) ? TAG : tag); getRequestQueue().add(req); } public <T> void addToRequestQueue(Request<T> req) { req.setTag(TAG); getRequestQueue().add(req); } public void cancelPendingRequests(Object tag) { if (mRequestQueue != null) { mRequestQueue.cancelAll(tag); } } } | cs |
추가 후 반드시 다음과 같이 AndroidMenifest에 추가합니다.
1 2 3 4 | <uses-permission android:name="android.permission.INTERNET"/> <application android:name="com.myPackage.AppController"> | cs |
LruBitmapCache.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | public class LruBitmapCache extends LruCache<String, Bitmap> implements ImageCache { public static int getDefaultLruCacheSize() { final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); final int cacheSize = maxMemory / 8; return cacheSize; } public LruBitmapCache() { this(getDefaultLruCacheSize()); } public LruBitmapCache(int sizeInKiloBytes) { super(sizeInKiloBytes); } @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight() / 1024; } @Override public Bitmap getBitmap(String url) { return get(url); } @Override public void putBitmap(String url, Bitmap bitmap) { put(url, bitmap); } } } | cs |
FeedImageView.java와 feed_item.xml는 링크에서 확인하세요.
완료했다면 이제 FeedListAdapter.java, MainActivity.java, activity_main.xml를 추가합니다.
FeedListAdapter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | public class FeedListAdapter extends BaseAdapter { private Activity activity; private LayoutInflater inflater; private List<FeedItem> feedItems; ImageLoader imageLoader = AppController.getInstance().getImageLoader(); public FeedListAdapter(Activity activity, List<FeedItem> feedItems) { this.activity = activity; this.feedItems = feedItems; } @Override public int getCount() { return feedItems.size(); } @Override public Object getItem(int location) { return feedItems.get(location); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (inflater == null) inflater = (LayoutInflater) activity .getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (convertView == null) convertView = inflater.inflate(R.layout.feed_item, null); if (imageLoader == null) imageLoader = AppController.getInstance().getImageLoader(); //Componets initializing TextView name = (TextView) convertView .findViewById(R.id.name); TextView timestamp = (TextView) convertView .findViewById(R.id.timestamp); TextView statusMsg = (TextView) convertView .findViewById(R.id.txtStatusMsg); TextView url = (TextView) convertView .findViewById(R.id.txtUrl); NetworkImageView profilePic = (NetworkImageView) convertView .findViewById(R.id.profilePic); FeedImageView feedImageView = (FeedImageView) convertView .findViewById(R.id.feedImage1); //end FeedItem item = feedItems.get(position); name.setText(item.getName()); // Converting timestamp into x ago format CharSequence timeAgo = DateUtils.getRelativeTimeSpanString( Long.parseLong(item.getTimeStamp()), System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS); timestamp.setText(timeAgo); // Chcek for empty status message if (!TextUtils.isEmpty(item.getStatus())) { statusMsg.setText(item.getStatus()); statusMsg.setVisibility(View.VISIBLE); } else { // status is empty, remove from view statusMsg.setVisibility(View.GONE); } // Checking for null feed url if (item.getUrl() != null) { url.setText(Html.fromHtml("<a href=\"" + item.getUrl() + "\">" + item.getUrl() + "</a> ")); // Making url clickable url.setMovementMethod(LinkMovementMethod.getInstance()); url.setVisibility(View.VISIBLE); } else { // url is null, remove from the view url.setVisibility(View.GONE); } // user profile pic profilePic.setImageUrl(item.getProfilePic(), imageLoader); // Feed image if (item.getImge() != null) { feedImageView.setImageUrl(item.getImge(), imageLoader); feedImageView.setVisibility(View.VISIBLE); feedImageView .setResponseObserver(new FeedImageView.ResponseObserver() { @Override public void onError() { } @Override public void onSuccess() { } }); } else { feedImageView.setVisibility(View.GONE); } return convertView; } } | cs |
MainAcitivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | //Writed By www.androidhive.info @ravi //Modified By Trendy Develope public class MainActivity extends Activity { private static final String TAG = MainActivity.class.getSimpleName(); private ListView listView; private FeedListAdapter listAdapter; private List<FeedItem> feedItems; private int offSet = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); swipeRefreshLayout = (SwipyRefreshLayout) findViewById (R.id.swipy_refresh_layout_feed); listView = (ListView) findViewById(R.id.list); feedItems = new ArrayList<FeedItem>(); listAdapter = new FeedListAdapter(this, feedItems); listView.setAdapter(listAdapter); // These two lines not needed, // just to get the look of face**** sns (changing background color & hiding the icon) getActionBar().setBackgroundDrawable(new ColorDrawable(Color.parseColor("#3b5998"))); getActionBar().setIcon( new ColorDrawable(getResources().getColor(android.R.color.transparent))); swipeRefreshLayout.setColorSchemeResources (R.color.feed_bg, R.color.feed_bg); swipeRefreshLayout.setOnRefreshListener(new SwipyRefreshLayout.OnRefreshListener() { @Override public void onRefresh(SwipyRefreshLayoutDirection direction) { catchPosts(); } }); //start load feed data swipeRefreshLayout.post(new Runnable() { @Override public void run() { swipeRefreshLayout.setRefreshing(true); catchPosts(); } }); } private void catchPosts() { swipeRefreshLayout.setRefreshing(true); // appending offset to url String url = "my_feed_url" + offset; JsonArrayRequest req = new JsonArrayRequest(url, new Response.Listener<JSONArray>() { @Override public void onResponse(JSONArray response) { if (response.length() > 0) { //JSONArray feedArray = response.getJSONArray("feed"); for (int i = 0; i < response.length(); i++) { try { //JSONObject feedObj = (JSONObject) feedArray.get(i); JSONObject feedObj = response.getJSONObject(i); String id = feedObj.getString("id"); String name = URLDecoder.decode (URLEncoder.encode(feedObj.getString("name"), "iso8859-1"), "UTF-8"); String image= feedObj.getString("image"); String status = URLDecoder.decode (URLEncoder.encode(feedObj.getString("status"), "iso8859-1"), "UTF-8"); String profilePic = feedObj.getString("profilePic"); String timestamp = feedObj.getString("timestamp"); FeedItem item = new FeedItem(id, name, image, status, profilePic, timestamp); feedItems.add(listAdapter.getCount(), item); offset++; } catch (Exception e) { Log.e("TAG", "JSON Parsing error: " + e.getMessage()); } } listAdapter.notifyDataSetChanged(); }else{ if(offset == 0){ Toast.makeText(getApplicationContext(), "준비된 피드가 없습니다", Toast.LENGTH_LONG).show(); }else { Toast.makeText(getApplicationContext(), "더 이상 표시할 게시물이 없습니다", Toast.LENGTH_LONG).show(); } } swipeRefreshLayout.setRefreshing(false); }//OnResponse End }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.e("TAG", "Server Error: " + error.getMessage()); // stopping swipe refresh swipeRefreshLayout.setRefreshing(false); } }); AppController.getInstance().addToRequestQueue(req); } } | cs |
activity_main.xml
이것을 추가하기 전에 SwipyRefreshLayout 라이브러리를 앱에 추가합니다. 라이브러리에 대한 설명은 링크을 참고하세요.
1 | implementation 'com.github.orangegangsters:swipy:1.2.3@aar' | cs |
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 | <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical"> <com.orangegangsters.github.swipyrefreshlayout.library.SwipyRefreshLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/swipy_refresh_layout_feed" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" app:direction="bottom"> <ListView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/list" android:layout_alignTop="@+id/view" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" /> </com.orangegangsters.github.swipyrefreshlayout.library.SwipyRefreshLayout> </RelativeLayout> | cs |
이제 data를 출력하는 php파일을 작성할 차례입니다. 관련 강의는 포스트 [#G1 : PHP를 이용해 DB[MySQL]에 데이터를 저장하는 방법]]과 [#G2 : PHP를 이용해 DB [MySQL]의 데이터를 내보내는 방법]를 참고하세요.
아래의 코드는 작성 후 검증되지 않았습니다. 따라서 발생할 수 있는 오류에 따라 적절하게 대응하시기 바랍니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | //Inspired By www.androidhive.info //Edited By Trendy Develope <?php // db information $db_host = "localhost"; $db_id = "root"; $db_password = "password"; $db_dbname = "my_database"; // sleep for 2 sec show that the androd swipe refresh will be visible for sometime sleep(2); // reading offset from get parameter $offset = isset($_GET['offset']) && $_GET['offset'] != '' ? $_GET['offset'] : 0; $uid = $_GET["uid"]; // load limit $limit = 5; //read feed data from 'feed' table by uid $query = "select * from feed where uid = '".$uid."'"; $result = mysqli_query ( $db_conn, $query, MYSQLI_STORE_RESULT ); if (! $result) { $message = 'Invalid query: ' . mysql_error () . "\n"; $message .= 'Whole query: ' . $query; die ( $message ); } // make json from database result set $resultArray = array (); while ( $row = mysqli_fetch_assoc ( $result ) ) { $arrayMiddle = array ( "id" => $row ['id'], "name" => $row ['name'], "image" => $row ['image'], "status" => $row ['image'], "profilePic" => $row ['profilePic'], "timestamp" => $row ['timestamp'] ); array_push ( $resultArray, $arrayMiddle ); } //slice array //start //Length print_r( urldecode ( json_encode ( array_slice($resultArray, $offset, $offset+$limit) ) ) ); //한글 출력을 위해 ?> | cs |
우선 MainActivity.class의 offSet 변수는 0으로 초기화되어있습니다. 이후 catchPosts 메소드에서 피드 한개를 불러올때마다 값이 +1 증가됩니다.
위의 PHP에서 LIMIT을 5로 정했기 때문에 피드 데이터가 부족하지 않은 이상 offSet변수는 한번의 사이클마다 값이 +5 증가하게 됩니다.
( 한번 당겨서 새로고침할 때마다 5개의 피드가 로드됩니다 )
이런 알고리즘을 감안하여 array_slice 메소드를 사용하면 데이터를 쉽게 잘라내어 로드할 수 있습니다.
urldecode 메소드와 json_encode 메소드는 각각 한글 출력, json 데이터 형식으로 변환을 위한 것입니다.
모든 절차를 완료했다면 다음과 같은 FEED를 볼 수 있습니다.
'안드로이드 > 러닝패스' 카테고리의 다른 글
[#A4 DEPRECATED] 내 앱에서 쉽게 외부와 JSON 데이터를 주고 받는 방법 <Volley> (0) | 2018.11.30 |
---|---|
[#A3 DEPRECATED] JSON 데이터를 내 앱에 저장하는 방법 (0) | 2018.11.30 |
[#A2 DEPRECATED] 간단한 저장소가 필요할 때, SharedPreference (0) | 2018.11.29 |
[#A1 DEPRECATED] 내 앱에 원형 프로필 사진 위젯을 추가하는 가장 손쉬운 방법 (0) | 2018.11.28 |