MVP in Loklak Wok Android using Dagger2

MVP stands for Model-View-Presenter, one of the most popular and commonly used design pattern in android apps. Where “Model” refers to data source, it can be a SharedPreference, Database or data from a Network call. Going by the word, “View” is the user interface and finally “Presenter”, it’s a mediator between model and view. Whatever events occur in a view are passed to presenter and the presenter fetches the data from the model and finally passes it back to the view, where the data is populated in ViewGroups. Now, the main question, why it is so widely used? One of the obvious reason is the simplicity to implement it and it completely separates the business logic, so, easy to write unit-tests. Though it is easy to implement, its implementation requires a lot of boilerplate code, which is one of its downpoints. But, using Dagger2 the boilerplate code can be reduced to a great extent. Let’s see how Dagger2 is used in Loklak Wok Android to implement MVP architecture.

Adding Dagger2 to the project

In app/build.gradle file

dependencies {
   ...
   compile 'com.google.dagger:dagger:2.11'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.11'
}

 

Implementation

First a contract is created which defines the behaviour or say the functionality of View and Presenter. Like showing a progress bar when data is being fetched, or the view when the network request is successful or it failed. The contract should be easy to read and going by the names of the method one should be able to know the functionality of methods. For tweet search suggestions, the contract is defined in SuggestContract interface.

public interface SuggestContract {

   interface View {

       void showProgressBar(boolean show);

       void onSuggestionFetchSuccessful(List<Query> queries);

       void onSuggestionFetchError(Throwable throwable);
   }

   interface Presenter {

       void attachView(View view);

       void createCompositeDisposable();

       void loadSuggestionsFromAPI(String query, boolean showProgressBar);

       void loadSuggestionsFromDatabase();

       void saveSuggestions(List<Query> queries);

       void suggestionQueryChanged(Observable<CharSequence> observable);

       void detachView();
   }
}

 

A SuggestPresenter class is created which implements the SuggestContract.Presenter interface. I will not be explaining how each methods in SuggestPresenter class is implemented as this blog solely deals with implementing MVP. If you are interested you can go through the source code of SuggestPresenter. Similarly, the view i.e. SuggestFragment implements SuggestContract.View interface.

So, till this point we have our presenter and view ready. The presenter needs to access the model and the view requires to have an instance of presenter. One way could be instantiating an instance of model inside presenter and an instance of presenter inside view. But, this way model, view and presenter would be coupled and that defeats our purpose. So, we just INJECT model into presenter and presenter into view using Dagger2. Injecting here means Dagger2 instantiates model and presenter and provides wherever they are requested.

ApplicationModule provides the required dependencies for accessing the “Model” i.e. a Loklak API client and realm database instance. When we want Dagger2 to provide a dependency we create a method annotated with @Provides as providesLoklakAPI and providesRealm.

@Provides
LoklakAPI providesLoklakAPI(Retrofit retrofit) {
   return retrofit.create(LoklakAPI.class);
}

@Provides
Realm providesRealm() {
   return Realm.getDefaultInstance();
}

 

If we look closely providesLoklakAPI method requires a Retrofit instance i.e. a to create an instance of LoklakAPI the required dependency is Retrofit, which is fulfilled by providesRetrofit method. Always remember that whenever a dependency is required, it should not be instantiated at the required place, rather it should be injected by Dagger2.

@Provides
Retrofit providesRetrofit() {
   Gson gson = Utility.getGsonForPrivateVariableClass();
   return new Retrofit.Builder()
           .baseUrl(mBaseUrl)
           .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
           .addConverterFactory(GsonConverterFactory.create(gson))
           .build();
}

 

As the ApplicationModule class provides these dependencies the class is annotated with @Module.

@Module
public class ApplicationModule {

   private String mBaseUrl;

   public ApplicationModule(String baseUrl) {
       this.mBaseUrl = baseUrl;
   }
   
   
   // retrofit, LoklakAPI, realm @Provides methods
}


After preparing the source to provide the dependencies, it’s time we request the dependencies.

Dependencies are requested simply by using @Inject annotation e.g. in the constructor of SuggestPresenter @Inject is used, due to which Dagger2 provides instance of LoklakAPI and Realm for constructing an object of SuggestPresenter.

public class SuggestPresenter implements SuggestContract.Presenter {

   private final Realm mRealm;
   private LoklakAPI mLoklakAPI;
   private SuggestContract.View mView;
   ...

   @Inject
   public SuggestPresenter(LoklakAPI loklakAPI, Realm realm) {
       this.mLoklakAPI = loklakAPI;
       this.mRealm = realm;
       ...
   }
   
   // implementation of methods defined in contract
}


@Inject can be used on the fields also. When @Inject is used with a constructor the class also becomes a dependency provider, this way creating a method with @Provides is not required in a Module class.

Now, it’s time to connect the dependency providers and dependency requesters. This is done by creating a Component interface, here ApplicationComponent. The component interface defines where are the dependencies required. This is only for those cases where dependencies are injected by using @Inject for the member variables. So, we define a method inject with a single parameter of type SuggestFragment, as the Presenter needs to be injected in SuggestFragment.

@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {


   void inject(SuggestFragment suggestFragment);

}

 

The component interface is instantiated in onCreate method of LoklakWokApplication class, so that it is accessible all over the project.

public class LoklakWokApplication extends Application {

   private ApplicationComponent mApplicationComponent;

   @Override
   public void onCreate() {
       super.onCreate();
      ...
       mApplicationComponent = DaggerApplicationComponent.builder()
               .applicationModule(new ApplicationModule(Constants.BASE_URL_LOKLAK))
               .build();
   }

   public ApplicationComponent getApplicationComponent() {
       return mApplicationComponent;
   }
   
   ...
}


NOTE: DaggerApplicationComponent is created after building the project. So, AndroidStudio will show “Cannot resolve symbol …”, thus build the project : Build > Make Module ‘app’.

Finally, in the onCreateView callback of SuggestFragment we call inject method of DaggerApplicationComponent to tell Dagger2 that SuggestFragment is requesting dependencies.

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                        Bundle savedInstanceState) {
...   
   LoklakWokApplication application = (LoklakWokApplication) getActivity().getApplication();
   application.getApplicationComponent().inject(this);
   suggestPresenter.attachView(this);

   return rootView;
}

Resources:

MVP in Loklak Wok Android using Dagger2

Animations in Loklak Wok Android

Imagine an Activity popping out of nowhere suddenly in front of the user. And even more irritating, the user doesn’t even know whether a button was clicked. Though these are very small animation implementations but these animations enhance the user experience to a new level. This blog deals with the animations in Loklak Wok Android, a peer message harvester of Loklak Server.

Activity transition animation

Activity transition is applied when we move from a current activity to a new activity or just go back to an old activity by pressing back button.

In Loklak Wok Android, when user navigates for search suggestions from TweetHarvestingActivity to SuggestActivity, the new activity i.e. SuggestActivity comes from right side of the screen and the old one i.e. TweetHarvestingActivity leaves the screen through the left side. This is an example of left-right activity transition. For implementing this, two xml files which define the animations are created, enter.xml and exit.xml are created.

<set
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:shareInterpolator="false">

   <translate
       android:duration="500"
       android:fromXDelta="100%"
       android:toXDelta="0%"/>
</set>

 

NOTE: The entering activity comes from right side, that’s why android:fromXDelta parameter is set to 100% and as the activity finally stays at extreme left, android:toXDelta parameter is set to 0%.

As the current activity, in this case TweetHarvestingActivity, leaves the screen from left to the negative of left. So, in exit.xml the android:fromXDelta parameter is set to 0% and android:toXDelta parameter is set to -100%.

Now, that we are done with defining the animations in xml, it’s time we apply the animations, which is really easy. The animations are applied by invoking Activity.overridePendingTransition(enterAnim, exitAnim) just after the startActivity method. For example, in openSuggestActivity

private void openSuggestActivity() {
   Intent intent = new Intent(getActivity(), SuggestActivity.class);
   startActivity(intent);
   getActivity().overridePendingTransition(R.anim.enter, R.anim.exit);
}

 

Touch Selectors

Using touch selectors background color of a button or any clickable can be changed, this way a user can see that the clickable responded to the click. The background is usually light accent color or a lighter shade of the icon present in button.

There are three states involved while a clickable is touched, pressed, activated and selected. And a default state, i.e. the clickable is not clicked. The background color of each state is defined in a xml file like media_button_selector, which is present in drawable directory.

<selector xmlns:android="http://schemas.android.com/apk/res/android">

   <item android:drawable="@color/media_button_touch_selector_backgroud" android:state_pressed="true"/>
   <item android:drawable="@color/media_button_touch_selector_backgroud" android:state_activated="true"/>
   <item android:drawable="@color/media_button_touch_selector_backgroud" android:state_selected="true"/>

   <item android:drawable="@android:color/transparent"/>
</selector>

 

The selector is applied by setting it as the background of a clickable, for example, touch selector applied on Location image button present in fragment_tweet_posting.xml .

<ImageButton
   android:layout_width="40dp"
   android:layout_height="40dp"
   
   android:background="@drawable/media_button_selector" />

 

Notice the change in the background color of the buttons when clicked.

Resources:

Some youtube videos for getting started:

Animations in Loklak Wok Android

Implementing 3 legged Authorization in Loklak Wok Android for Twitter

Loklak Wok Android is a peer harvester that posts collected tweets to the Loklak Server. Not only it is a peer harvester, but also provides users to post their tweets from the app. Posting tweets from the app requires users to authorize the Loklak Wok app, the client app created https://apps.twitter.com/ . This blog explains in detail about the authorization process.

Adding Dependencies to the project

In app/build.gradle:

apply plugin: 'com.android.application'
apply plugin: 'me.tatarka.retrolambda'

android {
   ...
   packagingOptions {
       exclude 'META-INF/rxjava.properties'
   }
}

dependencies {
   ...
   compile 'com.google.code.gson:gson:2.8.1'

   compile 'com.squareup.retrofit2:retrofit:2.3.0'
   compile 'com.squareup.retrofit2:converter-gson:2.3.0'
   compile 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'

   compile 'io.reactivex.rxjava2:rxjava:2.0.5'
   compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
}

 

In build.gradle project level:

dependencies {
   classpath 'com.android.tools.build:gradle:2.3.3'
   classpath 'me.tatarka:gradle-retrolambda:3.2.0'
}

 

Steps of Authorization

Step 1: Create client app in Twitter

Create a twitter client app at https://apps.twitter.com/. Provide the mandatory entries and also Callback url (would be used in next steps). Then go to “Keys and Access Token” and save your consumer key and consumer secret. In case you want to use Twitter API for yourself, click on “Create my access token”, which provides access token and access token secret.

Step 2: Obtaining a request token

Using the “consumer key” and “consumer secret” request token is obtained by sending a POST request to oauth/request_token. As Twitter API are Oauth1 based the sent request needs to be signed by generating oauth_signature. The oauth_signature is generated by intercepting the network request sent by retrofit rest API client, the oauth interceptor used in Loklak Wok Android is a modified version of this snippet. The retrofit TwitterAPI interface is defined

public interface TwitterAPI {

   String BASE_URL = "https://api.twitter.com/";

   @POST("/oauth/request_token")
   Observable<ResponseBody> getRequestToken();

   @FormUrlEncoded
   @POST("/oauth/access_token")
   Observable<ResponseBody> getAccessTokenAndSecret(@Field("oauth_verifier") String oauthVerifier);
}

 

And the retrofit REST client is implemented in TwitterRestClient. createTwitterAPIWithoutAccessToken method returns a twitter API client which can be called without providing access keys, this is used as we don’t have access tokens right now.

public static TwitterAPI createTwitterAPIWithoutAccessToken() {
   if (sWithoutAccessTokenRetrofit == null) {
       sLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
       // uncomment to debug network requests
       // sWithoutAccessTokenClient.addInterceptor(sLoggingInterceptor);
       sWithoutAccessTokenRetrofit = sRetrofitBuilder
               .client(sWithoutAccessTokenClient.build()).build();
   }
   return sWithoutAccessTokenRetrofit.create(TwitterAPI.class);
}

 

So, getRequestToken method is used to obtain the request token, if the request is successful oauth_token is returned.

@OnClick(R.id.twitter_authorize)
public void onClickTwitterAuthorizeButton(View view) {
   mTwitterApi.getRequestToken()
           .subscribeOn(Schedulers.io())
           .observeOn(AndroidSchedulers.mainThread())
           .subscribe(this::parseRequestTokenResponse, this::onFetchRequestTokenError);
}

 

Step 3: Redirecting the user

Using the oauth_token obtained in Step 2, the user is redirected to login page using WebView.

private void setAuthorizationView() {
   ...
   webView.setVisibility(View.VISIBLE);
   webView.loadUrl(mAuthorizationUrl);
}

 

A WebView client is created by extending WebViewClient, this is used to keep track of which webpage is opened by overriding shouldOverrideUrlLoading.

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
   if (url.contains("github")) {
       String[] tokenAndVerifier = url.split("&");
       mOAuthVerifier = tokenAndVerifier[1].substring(tokenAndVerifier[1].indexOf('=') + 1);
       getAccessTokenAndSecret();
       return true;
   }
   return false;
}

 

As the link provided in callback url while creating our twitter app is a github page. The WebViewClient checks if it is a github page or not. If yes, then it parses the oauth_verifier from the github url.

Step 4: Converting the request token to an access token

A new rest client is created using the access token obtained in step 2, as implemented in createTwitterAPIWithAccessToken method.

public static TwitterAPI createTwitterAPIWithAccessToken(String token) {
   TwitterOAuthInterceptor withAccessTokenInterceptor =
           sInterceptorBuilder.accessToken(token).accessSecret("").build();
   OkHttpClient withAccessTokenClient = new OkHttpClient.Builder()
           .addInterceptor(withAccessTokenInterceptor)
           //.addInterceptor(loggingInterceptor) // uncomment to debug network requests
           .build();
   Retrofit withAccessTokenRetrofit = sRetrofitBuilder.client(withAccessTokenClient).build();
   return withAccessTokenRetrofit.create(TwitterAPI.class);
}

 

Now, to obtain access token and access token secret oauth_verifier obtained in step 3 is passed as a parameter to getAccessTokenAndSecret method defined in TwitterAPI interface which calls oauth/access_token endpoint from the rest client created above. This is implemented in getAccessTokenAndSecret method of WebViewClient class

private void getAccessTokenAndSecret() {
   mTwitterApi = TwitterRestClient.createTwitterAPIWithAccessToken(mOauthToken);
   mTwitterApi.getAccessTokenAndSecret(mOAuthVerifier)
           .flatMap(this::saveAccessTokenAndSecret)
           ....
}

 

Finally the obtained access_token and access_token_secret is saved in SharedPreference so that it can be used to call other Twitter API endpoints as in saveAccessTokenAndSecret

private Observable<Integer> saveAccessTokenAndSecret(ResponseBody responseBody)
       throws IOException {
   String[] responseValues = responseBody.string().split("&");

   String token = responseValues[0].substring(responseValues[0].indexOf("=") + 1);
   SharedPrefUtil.setSharedPrefString(getActivity(), OAUTH_ACCESS_TOKEN_KEY, token);
   mOauthToken = token; // here access_token that would be used for API calls

   String tokenSecret = responseValues[1].substring(responseValues[1].indexOf("=") + 1);
   SharedPrefUtil.setSharedPrefString(
           getActivity(), OAUTH_ACCESS_TOKEN_SECRET_KEY, tokenSecret);
   mOauthTokenSecret = tokenSecret;
   return Observable.just(1);
}

 

Resources:

Implementing 3 legged Authorization in Loklak Wok Android for Twitter

Implementing Tweet Search feature in Loklak Wok Android

Loklak Wok Android is a peer harvester that posts collected tweets to the Loklak Server. Along with that tweets can be searched using the app. This post describes how search API endpoint and TabLayout is used to implement the tweet searching feature.

Adding Dependencies to the project

This feature uses Retrofit2, Reactive extensions(RxJava2, RxAndroid and Retrofit RxJava adapter) and RetroLambda (for Java lambda support in Android).

In app/build.gradle:

apply plugin: 'com.android.application'
apply plugin: 'me.tatarka.retrolambda'

android {
   ...
   packagingOptions {
       exclude 'META-INF/rxjava.properties'
   }
}

dependencies {
   ...
   compile 'com.google.code.gson:gson:2.8.1'

   compile 'com.squareup.retrofit2:retrofit:2.3.0'
   compile 'com.squareup.retrofit2:converter-gson:2.3.0'
   compile 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'

   compile 'io.reactivex.rxjava2:rxjava:2.0.5'
   compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
}

 

In build.gradle project level:

dependencies {
   classpath 'com.android.tools.build:gradle:2.3.3'
   classpath 'me.tatarka:gradle-retrolambda:3.2.0'
}

 

Implementation

The search API endpoint is defined in LoklakApi interface which would provide the tweet search result.

public interface LoklakApi {

   @GET("api/search.json")
   Observable<Search> getSearchedTweets(
           @Query("q") String query,
           @Query("filter") String filter,
           @Query("count") int count);
}

 

The POJOs (Plain Old Java Objects) for the result of search API endpoint are obtained using jsonschema2pojo, Gson uses POJOs to convert JSON to Java objects and vice-versa.

The REST client is created by Retrofit2 and is implemented in RestClient class. The Gson converter and RxJava adapter for retrofit is added in the retrofit builder. create method is called to generate the API methods(retrofit implements LoklakApi Interface).

public class RestClient {

   private RestClient() {
   }

   private static void createRestClient() {
       sRetrofit = new Retrofit.Builder()
               .baseUrl(BASE_URL)
               // gson converter
               .addConverterFactory(GsonConverterFactory.create(gson))
               // retrofit adapter for rxjava
               .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
               .build();
   }

   private static Retrofit getRetrofitInstance() {
       if (sRetrofit == null) {
           createRestClient();
       }
       return sRetrofit;
   }

   public static <T> T createApi(Class<T> apiInterface) {
       // create method to generate API methods
       return getRetrofitInstance().create(apiInterface);
   }

}

 

As search API endpoint provides filter parameter which can be used to filter out tweets containing images and videos. So, the tweets are displayed in three categories i.e. latest, images and videos.

The tweets of different category are displayed using a ViewPager. The fragments in ViewPager are inflated by a class that extends FragmentPagerAdapter. SearchFragmentPagerAdapter extends FragmentPagerAdapter, at least two methods getItem and getCount needs to be overridden. Going by the name of methods, getItem provides ith fragment to the  ViewPager and based on the value returned by getCount number of tabs are inflated in TabLayout, a ViewGroup to display fragments in ViewPager in an elegant way. For better UI, the names (here the category of tweets) are displayed, for which we override getPageTitle method.

public class SearchFragmentPagerAdapter extends FragmentPagerAdapter {

   private List<Fragment>  mFragmentList = new ArrayList<>();
   private List<String> mFragmentNameList = new ArrayList<>();

   public SearchFragmentPagerAdapter(FragmentManager fm) {
       super(fm);
   }

   @Override
   public Fragment getItem(int position) {
       return mFragmentList.get(position);
   }

   @Override
   public int getCount() {
       return mFragmentList.size();
   }

   @Override
   public CharSequence getPageTitle(int position) {
       return mFragmentNameList.get(position);
   }

   public void addFragment(Fragment fragment, String pageTitle) {
       mFragmentList.add(fragment);
       mFragmentNameList.add(pageTitle);
   }
}

 

For easy understanding an analogy with RecyclerView can be made. The TabLayout here functions as a RecyclerView, ViewPager does the work of LayoutManager and FragmentPagerAdapter is analogous to RecyclerView.Adapter.

Now, the fragments which contain the categorical tweets are inflated in the parent fragment. Firstly, the ViewPager of TabLayout is set. Then fragments and their names are added to the FragmentPagerAdapter using the addFragment method implemented in SearchFragmentAdapter class above and finally the created adapter is set as the adapter of ViewPager, which is implemented in setupWithViewPager method.

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                        Bundle savedInstanceState) {
   // Inflate the layout for this fragment
   View rootView = inflater.inflate(R.layout.fragment_search, container, false);
   ButterKnife.bind(this, rootView);
   ...
   tabLayout.setupWithViewPager(viewPager);
   setupViewPager(viewPager);

   return rootView;
}

private void setupViewPager(ViewPager viewPager) {
   FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
   SearchFragmentPagerAdapter pagerAdapter = new SearchFragmentPagerAdapter(fragmentManager);
   pagerAdapter.addFragment(SearchCategoryFragment.newInstance("", mQuery), "LATEST");
   pagerAdapter.addFragment(SearchCategoryFragment.newInstance("image", mQuery), "PHOTOS");
   pagerAdapter.addFragment(SearchCategoryFragment.newInstance("video", mQuery), "VIDEOS");
   viewPager.setAdapter(pagerAdapter);
}

 

SearchCategoryFragment are child fragments displayed as tabs in TabLayout. These child fragments are created using newInstance method which takes two parameters, category of tweets and the tweet search query respectively, the reason a constructor with these parameters are not used is that during a orientation change only the default constructor i.e. with no parameters is restored by Android system. So, these parameters are stored in a data structure called Bundle, once the fragment object is created using the default parameter the arguments present in the bundle are passed to fragment using setArguments method. These parameter are retrieved in onCreate lifecycle callback method of fragment which are used to fetch search results.

public static SearchCategoryFragment newInstance(String category, String query) {
   Bundle args = new Bundle();
   // query and category stored in bundle
   args.putString(Constants.TWEET_SEARCH_SUGGESTION_QUERY_KEY, query);
   args.putString(TWEET_SEARCH_CATEGORY_KEY, category);
   // fragment with default constructor created
   SearchCategoryFragment fragment = new SearchCategoryFragment();
   // arguments in bundle are passed to fragment
   fragment.setArguments(args);
   return fragment;
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   Bundle bundle = getArguments();
   if (bundle != null) {
       // arguments retrieved
       mTweetSearchCategory = bundle.getString(TWEET_SEARCH_CATEGORY_KEY);
       mSearchQuery = bundle.getString(Constants.TWEET_SEARCH_SUGGESTION_QUERY_KEY);
   }
}

 

As we have search query and category we can now obtain the search result and pass the obtained result – a List of type Status – to the adapter of RecyclerView which shows the tweets beautifully inside a CardView. The adapter and LayoutManager of RecyclerView are instantiated and set in onCreateView lifecycle callback method. Finally, network request is sent by calling fetchSearchedTweets method to obtain the search results.

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                        Bundle savedInstanceState) {
   // Inflate the layout for this fragment
   View view = inflater.inflate(R.layout.fragment_search_category, container, false);
   ButterKnife.bind(this, view);

   mSearchCategoryAdapter = new SearchCategoryAdapter(getActivity(), new ArrayList<>());
   recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
   recyclerView.setAdapter(mSearchCategoryAdapter);

   // request sent to obtain search result.
   fetchSearchedTweets();

   return view;
}

 

The LoklakApi interface is implemented using the created Rest client and then getSearchedTweets method is invoked which takes in search query, category of tweets and maximum number of results in the mentioned order. If the network request is successful then setSearchResultView is invoked else setNetworkErrorView.

private void fetchSearchedTweets() {
   LoklakApi loklakApi = RestClient.createApi(LoklakApi.class);
   loklakApi.getSearchedTweets(mSearchQuery, mTweetSearchCategory, 30)
           .subscribeOn(Schedulers.io()) // network request sent in a background thread
           .observeOn(AndroidSchedulers.mainThread())
           .subscribe(this::setSearchResultView, this::setNetworkErrorView);
}

 

setSearchResultView displays the obtained result if any in RecyclerView else shows a message that there is no result for the search query.

private void setSearchResultView(Search search) {
   List<Status> statusList = search.getStatuses();
   networkErrorTextView.setVisibility(View.GONE);
   if (statusList.size() == 0) { // request successful but no results
       recyclerView.setVisibility(View.GONE);

       Resources res = getResources();
       String noSearchResultMessage = res.getString(R.string.no_search_match, mSearchQuery); // no result matched message
       // no result message displayed
       noSearchResultFoundTextView.setVisibility(View.VISIBLE);
       noSearchResultFoundTextView.setText(noSearchResultMessage);
   } else { // there are some results, so display them in RecyclerView
       recyclerView.setVisibility(View.VISIBLE);
       mSearchCategoryAdapter.setStatuses(statusList);
   }
}

 

In case of a failed network request, a TextView is displayed asking the user to check the network connections and click on it to retry.

private void setNetworkErrorView(Throwable throwable) {
   Log.e(LOG_TAG, throwable.toString());
   recyclerView.setVisibility(View.GONE);
   networkErrorTextView.setVisibility(View.VISIBLE);
}

 

When the TextView, networkErrorTextView, is clicked a network request is sent again and the visibility of networkErrorTextView is changed to GONE, as implemented in setOnClickNetworkErrorTextViewListner

@OnClick(R.id.network_error)
public void setOnClickNetworkErrorTextViewListener() {
   networkErrorTextView.setVisibility(View.GONE);
   fetchSearchedTweets();
}

 

References:

Resources

Implementing Tweet Search feature in Loklak Wok Android