Android RSS Reader – Part Two (Offline reading, Swipe through detail views)
This post is in continuation with the previous Post (Android Simple RSS Reader with Splash Screen and Custom List View.), where we learnt to parse an xml and show that in a ListView followed by a Detail View.
In this tutorial we will add some features like ‘offline reading’ and ‘swipe through detail views’.
Here’s the final video demo:
First lets get in to the creation of swipe detail view:
Using swipe views we can navigate to other details just by swiping (left and right) unlike going back to list and selecting the item.
To achieve this we use ‘ViewPager‘:
‘ViewPager’ takes in a adapter where we set all the views (In this example we set the fragment as the view).
So we create a detail fragment where it holds all the detail information. From the Adapter we set this fragment each time the user swipes to and fro.
We create a separate layout for this fragment so the Main detail layout (detail.xml) will look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
android:layout_width = "match_parent" android:layout_height = "match_parent" android:orientation = "vertical" android:paddingLeft = "5dp" android:paddingRight = "5dp" > < android.support.v4.view.ViewPager android:id = "@+id/pager" android:layout_width = "match_parent" android:layout_height = "match_parent" > </ android.support.v4.view.ViewPager > </ RelativeLayout > |
Here’s the details fragment layout (detail_fragment.xml):
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
|
android:layout_width = "match_parent" android:layout_height = "match_parent" android:orientation = "vertical" android:paddingLeft = "5dp" android:paddingRight = "5dp" > < TextView android:id = "@+id/title" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_centerHorizontal = "true" android:text = "@string/title" android:textAppearance = "?android:attr/textAppearanceMedium" android:textStyle = "bold" /> < ScrollView android:id = "@+id/sv" android:layout_width = "fill_parent" android:layout_height = "fill_parent" android:layout_below = "@+id/title" android:layout_marginTop = "10dp" > < RelativeLayout android:layout_width = "fill_parent" android:layout_height = "wrap_content" > < WebView android:id = "@+id/desc" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_marginTop = "5dp" android:scrollbarStyle = "insideOverlay" android:text = "@string/desc" /> </ RelativeLayout > </ ScrollView > </ RelativeLayout > |
The main detail activity extends ‘FragmentActivity’. Here we have a sub class ‘DescAdapter’ which extends ‘FragmentStatePagerAdapter’. we can also extend ‘FragmentPagerAdapter’ instead, but the difference is that ‘FragmentStatePagerAdapter’ will destroy the views once the are out of the window.
Here’s the final detail Activity (DetailActivity.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
|
public class DetailActivity extends FragmentActivity { RSSFeed feed; int pos; private DescAdapter adapter; private ViewPager pager; @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.detail); // Get the feed object and the position from the Intent feed = (RSSFeed) getIntent().getExtras().get( "feed" ); pos = getIntent().getExtras().getInt( "pos" ); // Initialize the views adapter = new DescAdapter(getSupportFragmentManager()); pager = (ViewPager) findViewById(R.id.pager); // Set Adapter to pager: pager.setAdapter(adapter); pager.setCurrentItem(pos); } public class DescAdapter extends FragmentStatePagerAdapter { public DescAdapter(FragmentManager fm) { super (fm); } @Override public int getCount() { return feed.getItemCount(); } @Override public Fragment getItem( int position) { DetailFragment frag = new DetailFragment(); Bundle bundle = new Bundle(); bundle.putSerializable( "feed" , feed); bundle.putInt( "pos" , position); frag.setArguments(bundle); return frag; } } } |
In the detail activity adapter, we pass the feed bundle and the position of the item to the detail fragment. In the detail fragment activity which extends a fragment, we receive the bundle and the position and set all the views in the fragment layout.
The detail fragment activity (DetailFragment.java) will look like this:
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
|
public class DetailFragment extends Fragment { private int fPos; RSSFeed fFeed; @Override public void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); fFeed = (RSSFeed) getArguments().getSerializable( "feed" ); fPos = getArguments().getInt( "pos" ); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater .inflate(R.layout.detail_fragment, container, false ); // Initializr views TextView title = (TextView) view.findViewById(R.id.title); WebView desc = (WebView) view.findViewById(R.id.desc); // Enable the vertical fading edge (by default it is disabled) ScrollView sv = (ScrollView) view.findViewById(R.id.sv); sv.setVerticalFadingEdgeEnabled( true ); // Set webview properties WebSettings ws = desc.getSettings(); ws.setLayoutAlgorithm(LayoutAlgorithm.SINGLE_COLUMN); ws.setLightTouchEnabled( false ); ws.setPluginState(PluginState.ON); ws.setJavaScriptEnabled( true ); // Set the views title.setText(fFeed.getItem(fPos).getTitle()); .getItem(fPos).getDescription(), "text/html" , "UTF-8" , null ); return view; } } |
With this we have created the horizontal swipe detail views. Run the app to test! (Any issues please use comments, i would be pleased to help you).
————————————————————————————————————————————————
Now will move on to create the offline support. Achieving this is very simple we just need to save the feed object which we get each time we launch the App.
Steps involved in this are:
1. Check for the internet connectivity.
2. If Internet is available then allow to parse, once you get the final feed write that to the file.
3. If connection is not available then first check if the feed file exists if exists then read the feed from the file and proceed.
4. Else if the feed file doesn’t exist (for the first launch), then show an alert to check for connectivity with only option to exit.
We write the feed to the Applications default files directory so that it can’t be accessed by other apps and deleted when the app is uninstalled.
The final ‘SplashActivity.java‘ will looks like this:
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
|
public class SplashActivity extends Activity { RSSFeed feed; String fileName; @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.splash); fileName = "TDRSSFeed.td" ; File feedFile = getBaseContext().getFileStreamPath(fileName); ConnectivityManager conMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); if (conMgr.getActiveNetworkInfo() == null ) { // No connectivity. Check if feed File exists if (!feedFile.exists()) { // No connectivity & Feed file doesn't exist: Show alert to exit // & check for connectivity AlertDialog.Builder builder = new AlertDialog.Builder( this ); builder.setMessage( "Unable to reach server, \nPlease check your connectivity." ) .setTitle( "TD RSS Reader" ) .setCancelable( false ) .setPositiveButton( "Exit" , new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { finish(); } }); AlertDialog alert = builder.create(); alert.show(); } else { // No connectivty and file exists: Read feed from the File Toast toast = Toast.makeText( this , "No connectivity! Reading last update..." , Toast.LENGTH_LONG); toast.show(); feed = ReadFeed(fileName); startLisActivity(feed); } } else { // Connected - Start parsing new AsyncLoadXMLFeed().execute(); } } private void startLisActivity(RSSFeed feed) { Bundle bundle = new Bundle(); bundle.putSerializable( "feed" , feed); // launch List activity Intent intent = new Intent(SplashActivity. this , ListActivity. class ); intent.putExtras(bundle); startActivity(intent); // kill this activity finish(); } private class AsyncLoadXMLFeed extends AsyncTask<Void, Void, Void> { @Override protected Void doInBackground(Void... params) { // Obtain feed DOMParser myParser = new DOMParser(); feed = myParser.parseXml(RSSFEEDURL); if (feed != null && feed.getItemCount() > 0 ) WriteFeed(feed); return null ; } @Override protected void onPostExecute(Void result) { super .onPostExecute(result); startLisActivity(feed); } } // Method to write the feed to the File private void WriteFeed(RSSFeed data) { FileOutputStream fOut = null ; ObjectOutputStream osw = null ; try { fOut = openFileOutput(fileName, MODE_PRIVATE); osw = new ObjectOutputStream(fOut); osw.writeObject(data); osw.flush(); } catch (Exception e) { e.printStackTrace(); } finally { try { fOut.close(); } catch (IOException e) { e.printStackTrace(); } } } // Method to read the feed from the File private RSSFeed ReadFeed(String fName) { FileInputStream fIn = null ; ObjectInputStream isr = null ; RSSFeed _feed = null ; File feedFile = getBaseContext().getFileStreamPath(fileName); if (!feedFile.exists()) return null ; try { fIn = openFileInput(fName); isr = new ObjectInputStream(fIn); _feed = (RSSFeed) isr.readObject(); } catch (Exception e) { e.printStackTrace(); } finally { try { fIn.close(); } catch (IOException e) { e.printStackTrace(); } } return _feed; } } |
Last but no least add the required permissions to the Manifest:
1
2
3
4
|
< uses-permission android:name = "android.permission.INTERNET" /> < uses-permission android:name = "android.permission.ACCESS_NETWORK_STATE" /> < uses-permission android:name = "android.permission.WRITE_EXTERNAL_STORAGE" /> < uses-permission android:name = "android.permission.ACCESS_WIFI_STATE" /> |
Done! Run the App and test in all cases (with connectivity, without connectivity, first time launch with out connectivity). If any problems Please use comments also refer to the sources attached.
In the next tutorial we will introduce the ‘Action Bar’ with some features like refresh, share … Stay tuned!