Recently, @mathemandy and I started working on re-implementing the CryptoNews app built by @Johnesan in native android.
The aim of the app is to fetch news from different wordpress websites and display in the application. While the news are displayed, the app gives you the ability to click and view each news within the app. Instead of taking you out of the app, it gives you the comfort of viewing within the app.
History
Update
Storing of news data in the database
In the previous implementation, the news were fetched directly from internet and displayed. But anytime the app is restarted or the device screen is rotated, the news are re-fetched from the source, this is a bad user-experience and also wasteful because this consumes more of the user's mobile data.
So, the proper way to handle this is persisting the data .i.e storing the news in the database. How will that be accomplished? In 2017, Google released a new persisting component called Room.
From the above image, the news are fetched from the internet and while it's getting data, it displays a Progress Bar to indicate to the user that something is happening. At the restart of the app, the news are fetched from the database. You could notice the mobile data is switched off to confirm that it's actually working.
This is how it was implemented
- Firstly, the News POJO class was annotated with
@Entity(tableName = "news")
The @Entity represents a table within the db.
@Entity(tableName = "news")
public class News{
@SerializedName("date")
private String date;
@SerializedName("link")
private String link;
@SerializedName("title")
private Title title;
@SerializedName("_embedded")
private Embedded embedded;
@NonNull
@PrimaryKey()
@SerializedName("id")
private int id;
@SerializedName("guid")
private Guid guid;
...
}
- Secondly, an interface class called @Dao was created. This contains the methods used in accessing the db like @Insert, @Query, @Delete, etc.
@Dao
public interface NewsDao {
@Insert(onConflict = REPLACE)
void save(News news);
@Query("SELECT * FROM news")
LiveData<List<News>> queryNews();
}
- Thirdly, an abstract class @Database was created as the main access point for the underlying connection to the app's persisted data.
@Database(entities = {News.class}, version = 1)
@TypeConverters({Converters.class})
public abstract class NewsDatabase extends RoomDatabase {
public abstract NewsDao newsDao();
}
- Fourthly, in the repository class, all logic is done in fetching and storing data.
ThesetupNewsListener
method is called to check if more news should be fetched in order to update the UI. Else it fetches the stored data from the db.
public class NewsRepository {
@Inject
NewsApiService newsApiService;
@Inject
PreferenceUtils preferenceUtils;
private Executor diskExecutor;
private Executor mainThreadExecutor;
AppExecutors appExecutors;
NewsDao newsDao;
private MediatorLiveData<List<News>> newsLiveData = new MediatorLiveData<>();
private LiveData<List<News>> newdDbSource;
private static final String TAG = NewsRepository.class.getSimpleName();
@Inject
NewsRepository(AppExecutors appExecutors, NewsDao newsDao) {
this.appExecutors = appExecutors;
diskExecutor = appExecutors.getDiskExecutor();
mainThreadExecutor = appExecutors.getMainThreadExecutor();
this.newsDao = newsDao;
newdDbSource = loadUserNewsFromDb();
setupNewsListener();
}
@WorkerThread
private LiveData<List<News>> loadUserNewsFromDb() {
Log.d("TAG", "Load User Deals from DB");
return newsDao.queryNews();
}
public void getAllNews() {
Observable<List<News>> getNews = NetworkModule.api.getLatestNews().subscribeOn(Schedulers.io());
Observable<List<News>> getNews2 = NetworkModule.api2.getLatestNews().subscribeOn(Schedulers.io());
Observable<List<News>> getNews3 = NetworkModule.api3.getLatestNews().subscribeOn(Schedulers.io());
Observable<List<News>> getNews4 = NetworkModule.api4.getLatestNews().subscribeOn(Schedulers.io());
Observable.zip(getNews, getNews2, getNews3, getNews4,
new Function4<List<News>, List<News>, List<News>, List<News>, List<News>>() {
@Override
public List<News> apply(List<News> news, List<News> news2, List<News> news3, List<News> news4) throws Exception {
List<News> newsList = new ArrayList<>();
for (News newsObj : news) {
newsList.add(newsObj);
}
for (News newsObj : news2) {
newsList.add(newsObj);
}
for (News newsObj : news3) {
newsList.add(newsObj);
}
for (News newsObj : news4) {
newsList.add(newsObj);
}
return newsList;
}
})
// Run on a background thread
.subscribeOn(Schedulers.io())
// Be notified on the main thread
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<List<News>>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(List<News> newsList) {
getNewsForuser(newsList);
}
@Override
public void onError(Throwable e) {
}
@Override
public void onComplete() {
}
});
}
private void getNewsForuser(List<News> newsList) {
Log.e(TAG, "Loading News for user was successful");
Log.e(TAG, "Now Saving");
preferenceUtils.storeTimeNewswasReceived();
diskExecutor.execute(() -> {
saveUserDealsTODb(newsList);
mainThreadExecutor.execute(() -> {
newsLiveData.addSource(loadUserNewsFromDb(), this::setValueNews);
});
});
}
@WorkerThread
private void saveUserDealsTODb(List<News> newsList) {
Log.d("TAG", "Saving User Deals to DB");
for (News news : newsList){
newsDao.save(news);
}
}
private void fetchFromServer(){
Log.d("TAG", "Fetching Deals From Server");
getAllNews();
}
private void setupNewsListener(){
newsLiveData.addSource(newdDbSource, newsList -> {
Log.d("TAG", "News Livedata Listener");
if (shouldFetchNews(newsList)){
fetchFromServer();
}else {
setValueNews(newsList);
}
});
}
private void setValueNews(List<News> newsValue) {
if (newsValue != null){
mainThreadExecutor.execute(() -> newsLiveData.setValue(newsValue));
}
}
private boolean shouldFetchNews(List<News> newsList){
long lastFetched = preferenceUtils.getLastTimeNewsWasStored();
long timeOut = TimeUnit.MINUTES.toMillis(30) + lastFetched;
return newsList.size() <1 || Calendar.getInstance().getTimeInMillis() > timeOut;
}
public LiveData<List<News>> asLiveData(){
return newsLiveData;
}
}
IMPORTANT RESOURCES
Github Pull Request: https://github.com/Johnesan/CryptoNews/pull/3
Android Apk File: https://github.com/Johnesan/CryptoNews/releases/download/1.0/CryptoNews.apk
Roadmap
- Searching all news
Persisting user news- Providing different layouts
- Providing different themes for user
- Push Notifications
- News Posts sharing
- Incorporating more news and giving the user the flexibility of deciding what he wants to read per time.
Posted on Utopian.io - Rewarding Open Source Contributors