Selecting Best persistent storage for Phimpme Android and how to use it

As we are progressing in our Phimpme Android app. I added account manager part which deals with connecting all other accounts to phimpme. Showing a list of connected accounts.

We need a persistent storage to store all the details such as username, full name, profile image url, access token (to access API). I researched on various Object Relation mapping (ORMs) such as:

  1. DBFlow: https://github.com/Raizlabs/DBFlow
  2. GreenDAO: https://github.com/greenrobot/greenDAO
  3. SugarORM: http://satyan.github.io/sugar/
  4. Requery: https://github.com/requery/requery

and other NoSQL databases such as Realm Database : https://github.com/realm/realm-java.

After reading a lot from some blogs on the benchmarking of these ORMs and database, I came to know that Realm database is quite better in terms of Speed of writing data and ease of use.

Steps to integrate Realm Database:

  • Installation of Realm database in android

Following these steps https://realm.io/docs/java/latest/#installation quickly setup realm in android. Add

classpath "io.realm:realm-gradle-plugin:3.3.2"

in Project level build.gradle file and Add

apply plugin: 'realm-android' 

in app level build.gradle, That’s it for using Realm

  • Generating required Realm models

Firstly, make sure what you need to store in your database. In case of phimpme, I first go through the account section and noted down what needs to be there.  Profile image URL, username, full name, account indicator image name. Below image illustrate this better.

This is the Realm Model class I made in Kotlin to store name, username and access token for accessing API.

open class AccountDatabase(
 [email protected] var name: String = "",
       var username: String = "",
       var token: String = ""
) : RealmObject()

  • Writing data in database

In Account manager, I create a add account option from where a dialog appear with a list of accounts. Currently, Twitter is working, when onSuccess function invoke in AccountPickerFragment I start a twitter session and store values in database. Writing data in database:

// Begin realm transaction
realm.beginTransaction();

// Creating Realm object for AccountDatabase Class
account = realm.createObject(AccountDatabase.class,
       accountsList[0]);

account.setUsername(session.getUserName());
account.setToken(String.valueOf(session.getAuthToken()));
realm.commitTransaction();

Begin and commit block in necessary. There is one more way of doing this is using execute function in Realm

  • Use Separate Database Helper class for Database operations

It’s good to use a separate class for all the Database operations needed in the project. I created a DatabaseHelper Class and added a function to query the result needed. Query the database

public RealmResults<AccountDatabase> fetchAccountDetails(){
   return realm.where(AccountDatabase.class).findAll();
}

It give all of the results, stored in the database like below

  • Problems I faced with annotation processor while using Kotlin and Realm together

The Kotlin annotation processor not running due to the plugins wrong order. This issue https://github.com/realm/realm-java/pull/2568 helped me in solving that. I addded apply plugin: ‘kotlin-kapt’. In app gradle file and shift apply plugin: ‘realm-android’ In below the order.

Resources:

 

Datewise splitting of the Bookmarks in the Homescreen of Open Event Android

In the Open Event Android app we had already incorporated bookmarks in the homescreen along with an improved UI. Now there was scope for further improvement in terms of user experience. The bookmarks were already sorted date wise but we needed to place them under separate date headers. In this blog I will be talking about how this was done in the app.

Initial Design
Current Design

 

 

 

 

 

 

 

 

Initially the user had no way of knowing which session belonged to which day. This could be fixed with a simple addition of a header indicating the day each bookmark belonged to. One way to do this was to add a day header and then get the bookmarks for each day and so on. But this proved to be difficult owing to the fact the number of days could be dynamic owing to the fact that this is a generic app. Another issue was that adding change listeners for the realm results to the bookmarks list for each day produced view duplication and other unexpected results whenever the bookmark list changed. So another approach was chosen that was to get all the bookmarks first and then add the date header and traverse through the bookmarks and only add sessions which belong to the date for which the date header was added earlier.

Bookmark Item Support in GlobalSearchAdapter

The main reason why we are reusing the GlobalSearchAdapter is that we have already defined a DIVIDER type in this adapter which can be reused as the date header.

We needed to initialize a constant for the Bookmark type.

private final int BOOKMARK = 5; //Can be any number

Then we add the Bookmark type in the getItemViewType() function which would return a constant that we defined earlier to indicate that in the filteredResultList we have an object of type Bookmark.

@Override
 public int getItemViewType(int position) {
    if (filteredResultList.get(position) instanceof Track) {
        return TRACK;
    }

    //Other Cases here
    } else if(filteredResultList.get(position) instanceof Session){
        return BOOKMARK;
    } else {
        return 1;
    }
 }

Now we create the viewholder if the list item is of the type Session which in this case will be a bookmark.

@Override
 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    RecyclerView.ViewHolder resultHolder = null;
    LayoutInflater inflater = LayoutInflater.from(parent.getContext());
    //Other cases for Track,Location etc
 case BOOKMARK:
    View bookmark = inflater.inflate(R.layout.item_schedule, parent, false);
    resultHolder = new DayScheduleViewHolder(bookmark,context);
    break;
   //Some code
 
 }

Now we do the same in onBindViewHolder(). We bind the contents of the object to the ViewHolder here by calling the bindSession() function. We also pass in an argument which is our database repository i.e realmRepo here.

@Override
 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

    switch (holder.getItemViewType()) {
     //Other cases handled here

case BOOKMARK:
    DayScheduleViewHolder bookmarkTypeViewHolder = (DayScheduleViewHolder) holder;
    Session bookmarkItem = (Session) getItem(position);
    bookmarkTypeViewHolder.setSession(bookmarkItem);
    bookmarkTypeViewHolder.bindSession(realmRepo);
    break;
 }

Updating the AboutFragment

private GlobalSearchAdapter bookMarksListAdapter;
 private List<Object> mSessions = new ArrayList<>();

Earlier the DayScheduleAdapter was being used to display the list of bookmarks. Now we are reusing the GlobalSearchAdapter. Now we have also converted mSessions into a list of objects from a list of sessions.

Now we initialize the adapter so that we can start adding our date headers.

bookMarksListAdapter = new GlobalSearchAdapter(mSessions, getContext());
 bookmarksRecyclerView.setAdapter(bookMarksListAdapter);

In this function loadEventDates() we are storing the all the dates for the event. For example the list for the FOSSASIA17 sample stores the dates in the dateList as [2017-03-17,2017-03-18,2017-03-19]. We fetch the event dates by calling the getEventDateSync() function which has been defined in our Realm Database.

private void loadEventDates() {
 
    dateList.clear();
    RealmResults<EventDates> eventDates = realmRepo.getEventDatesSync();
    for (EventDates eventDate : eventDates) {
        dateList.add(eventDate.getDate());
    }
 }

Now we move on to the core logic of the feature which is to get the date headers to work correctly.

  • Fetch the list of bookmarks from the local Realm database asynchronously.
  • Remove any existing changeListeners to the bookmarkResult.
  • Add a changeListener to our list of results to notify us of the completion of the query or changes in the bookmark list.
  • After this is done, inside the changeListener we first clear the mSessions
  • We now traverse through our date list and compare it with the session startDate which we can obtain by calling the getStartDate(). If the date match occurs for the first time we add a date header after converting the date string into another format using the DateUtils class. So the function formatDay() of DateUtils converts 2017-03-17 to 17 Mar. This format is easily more readable.
  • Repeat for all dates.
private void loadData() {
    loadEventDates();
 
    bookmarksResult = realmRepo.getBookMarkedSessions();
    bookmarksResult.removeAllChangeListeners();
    bookmarksResult.addChangeListener((bookmarked, orderedCollectionInnerChangeSet) -> {
 
        mSessions.clear();
        for (String eventDate : dateList) {
            boolean headerCheck = false;
            for(Session bookmarkedSession : bookmarked){
                if(bookmarkedSession.getStartDate().equals(eventDate)){
                    if(!headerCheck){
                        String headerDate = "Invalid";
                      try {
                       headerDate = DateUtils.formatDay(eventDate);
                      }
                      catch (ParseException e){
                            e.printStackTrace();
                      }
                        mSessions.add(headerDate);
                        headerCheck = true;
                    }
                    mSessions.add(bookmarkedSession);
                }
            }
            bookMarksListAdapter.notifyDataSetChanged();
            handleVisibility();
        }
    });
 }

So, this is how the date-wise organization for the bookmarks in the homescreen was done.

Resources

Addition of Bookmarks to the Homescreen in the Open Event Android App

In the Open Event Android app we had already built the new homescreen but the users only had access to bookmarks in a separate page which could be accessed from the navbar.If the bookmarks section were to be incorporated in the homescreen itself, it would definitely improve its access to the user. In this blog post, I’ll be talking about how this was done in the app.

These 2 images show the homescreen and the bookmarks section respectively.

No Bookmark View
Bookmark View

 

 

 

 

 

 

 

 

 

This was the proposed homescreen page for the app. This would provide easy access to important stuff to the user such as event venue,date,description etc. Also the same homescreen would also have the bookmarks showing at the top if there are any.

The list of bookmarks in the first iteration of design was modeled to be a horizontal list of cards.

Bookmarks Merging Process

These are some variables for reference.

private SessionsListAdapter sessionsListAdapter;
 private RealmResults<Session> bookmarksResult;
 private List<Session> mSessions = new ArrayList<>();

The code snippet below highlights the initial setup of the bookmarks recycler view for the horizontal List of cards. All of this is being done in the onCreateView callback of the AboutFragment.java file which is the fragment file for the homescreen.

bookmarksRecyclerView.setVisibility(View.VISIBLE);
 sessionsListAdapter = new SessionsListAdapter(getContext(), mSessions, bookmarkedSessionList);
 sessionsListAdapter.setBookmarkView(true);
 bookmarksRecyclerView.setAdapter(sessionsListAdapter);
 bookmarksRecyclerView.setLayoutManager(new LinearLayoutManager(getContext(),LinearLayoutManager.HORIZONTAL,false));

The SessionListAdapter is an adapter that was built to handle multiple types of displays of the same viewholder i.e SessionViewHolder . This SessionListAdapter is given a static variable as an argument which is just notifies the adapter to switch to the bookmarks mode for the adapter.

private void loadData() {
    bookmarksResult = realmRepo.getBookMarkedSessions();
    bookmarksResult.removeAllChangeListeners();
    bookmarksResult.addChangeListener((bookmarked, orderedCollectionChangeSet) -> {
        mSessions.clear();
        mSessions.addAll(bookmarked);
 
        sessionsListAdapter.notifyDataSetChanged();
 
        handleVisibility();
    });
 }

This function loadData() is responsible for extracting the sessions that are bookmarked from the local Realm database. We the update the BookmarkAdapter on the homescreen with the list of the bookmarks obtained. Here we see that a ChangeListener is being attached to our RealmResults. This is being done so that we do our adapter notify only after the data of the bookmarked sessions has been processed from a background thread.

if(bookmarksResult != null)
    bookmarksResult.removeAllChangeListeners();

And it is good practice to remove any ChangeListeners that we attach during the fragment life cycle in the onStop() method to avoid memory leaks.

So now we have successfully added bookmarks to the homescreen.

Resources

Addition of Bookmark Icon in Schedule ViewHolder in Open Event Android App

In the Open Event Android app we only had a list of sessions in the schedule page without  the ability to bookmark the session unless we went into the SessionDetailPage or visited the session list via the tracks page or the locations page. This was obviously very inconvenient. There were several iterations of UI design for the same. Taking cues from the Google I/O 17 App I thought that the addition of the Bookmark Icon in the Schedule ViewHolder would be helpful to the user. In this blog post I will be talking about how this feature was implemented.

Layout Implementation for the Bookmark Icon

<ImageButton
    android:id="@+id/slot_bookmark"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="end"
    android:layout_alignParentEnd="true"
    android:layout_alignParentRight="true"
    android:layout_alignParentBottom="true"
    android:tint="@color/black"
    android:background="?attr/selectableItemBackgroundBorderless"
    android:contentDescription="@string/session_bookmark_status"
    android:padding="@dimen/padding_small"
    app:srcCompat="@drawable/ic_bookmark_border_white_24dp" />

The bookmark Icon was modelled as an ImageButton inside the item_schedule.xml file which serves as the layout file for the DayScheduleViewHolder.

Bookmark Icon Functionality in DayScheduleAdapter

The Bookmark Icon had mainly 3 roles :

  1. Add the session to the list of bookmarks (obviously)
  2. Generate the notification giving out the bookmarked session details.
  3. Generate a Snackbar if the icon was un-clicked which allowed the user to restore the bookmarked status of the session.
  4. Update the bookmarks widget.

Now we will be seeing how was all this done. Some of this was already done previously in the SessionListAdapter. We just had to modify some of the code to get our desired result.

       Session not bookmarked                      Session bookmarked                      

First we just set a different icon to highlight the Bookmarked and the un-bookmarked status. This code snippet highlights how this is done.

if(session.isBookmarked()) {
    slot_bookmark.setImageResource(R.drawable.ic_bookmark_white_24dp);
 } else {
 slot_bookmark.setImageResource(R.drawable.ic_bookmark_border_white_24dp);
 }
 slot_bookmark.setColorFilter(storedColor,PorterDuff.Mode.SRC_ATOP);

We check if the session is bookmarked by calling a function isBookmarked() and choose one of the 2 bookmark icons depending upon the bookmark status.

If a session was found out to be bookmarked and the Bookmark Icon was we use the WidgetUpdater.updateWidget() function to remove that particular session from the  Bookmark Widget of the app. During this a  Snackbar is also generated “Bookmark Removed” with an UNDO option which is functional.

realmRepo.setBookmark(sessionId, false).subscribe();
 slot_bookmark.setImageResource(R.drawable.ic_bookmark_border_white_24dp);
 
 if ("MainActivity".equals(context.getClass().getSimpleName())) {
    Snackbar.make(slot_content, R.string.removed_bookmark, Snackbar.LENGTH_LONG)
            .setAction(R.string.undo, view -> {
 
                realmRepo.setBookmark(sessionId, true).subscribe();
                slot_bookmark.setImageResource(R.drawable.ic_bookmark_white_24dp);
 
                WidgetUpdater.updateWidget(context);
            }).show();

else {
    Snackbar.make(slot_content, R.string.removed_bookmark, Snackbar.LENGTH_SHORT).show();
 }

If a session wasn’t bookmarked earlier but the Bookmark Icon was clicked we would firstly need to update the bookmark status within our local Realm Database.

realmRepo.setBookmark(sessionId, true).subscribe();

We would also create a notification to notify the user.

NotificationUtil.createNotification(session, context).subscribe(
        () -> Snackbar.make(slot_content,
                R.string.added_bookmark,
                Snackbar.LENGTH_SHORT)
                .show(),
        throwable -> Snackbar.make(slot_content,
                R.string.error_create_notification,
                Snackbar.LENGTH_LONG).show());

The static class Notification Util is responsible for the generation of notifications. The internal working of that class is not necessary right now. What this snippet of code does is that It creates a Snackbar upon successful notification with the text “Bookmark Added” and if any error occurs a Snackbar with the text “Error Creating Notification” is generated.

slot_bookmark.setImageResource(R.drawable.ic_bookmark_white_24dp);
 slot_bookmark.setColorFilter(storedColor,PorterDuff.Mode.SRC_ATOP);

This snippet of code is responsible for the colors that are assigned to the Bookmark Icons for different tracks and this color is obtained in the following manner.

int storedColor = currentSession.getTrack().getColor()

So now we have successfully added the Bookmark Icon to the ScheduleViewHolder inside the schedule of the app.

Resources

Global Search in Open Event Android

In the Open Event Android app we only had a single data source for searching in each page that was the content on the page itself. But it turned out that users want to search data across an event and therefore across different screens in the app. Global search solves this problem. We have recently implemented  global search in Open Event Android that enables the user to search data from the different pages i.e Tracks, Speakers, Locations etc all in a single page. This helps the user in obtaining his desired result in less time. In this blog I am describing how we implemented the feature in the app using JAVA and XML.

Implementing the Search

The first step of the work is to to add the search icon on the homescreen. We have done this with an id R.id.action_search_home.

@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
   super.onCreateOptionsMenu(menu, inflater);
   inflater.inflate(R.menu.menu_home, menu);
   // Get the SearchView and set the searchable configuration
   SearchManager searchManager = (SearchManager)getContext().    getSystemService(Context.SEARCH_SERVICE);
   searchView = (SearchView) menu.findItem(R.id.action_search_home).getActionView();
  // Assumes current activity is the searchable activity
 searchView.setSearchableInfo(searchManager.getSearchableInfo(
getActivity().getComponentName()));
searchView.setIconifiedByDefault(true); 
}

What is being done here is that the search icon on the top right of the home screen  is being designated a searchable component which is responsible for the setup of the search widget on the Toolbar of the app.

@Override
 public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.menu_home, menu);
    SearchManager searchManager =
            (SearchManager) getSystemService(Context.SEARCH_SERVICE);
    searchView = (SearchView) menu.findItem(R.id.action_search_home).getActionView();
    searchView.setSearchableInfo(
            searchManager.getSearchableInfo(getComponentName()));
 
    searchView.setOnQueryTextListener(this);
    if (searchText != null) {
        searchView.setQuery(searchText, true);
    }
    return true; }

We can see that a queryTextListener has been setup in this function which is responsible to trigger a function whenever a query in the SearchView changes.

Example of a Searchable Component

<?xml version="1.0" encoding="utf-8"?>
 <searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:hint="@string/global_search_hint"
    android:label="@string/app_name" />

For More Info : https://developer.android.com/guide/topics/search/searchable-config.html

If this searchable component is inserted into the manifest in the required destination activity’s body the destination activity is set and intent filter must be set in this activity to tell whether or not the activity is searchable.

Manifest Code for SearchActivity

<activity
        android:name=".activities.SearchActivity"
        android:launchMode="singleTop"
        android:label="Search App"
        android:parentActivityName=".activities.MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.SEARCH" />
    </intent-filter>
    <meta-data
        android:name="android.app.searchable"
        android:resource="@xml/searchable" />
 </activity>

And the attribute  android:launchMode=”singleTop is very important as if we want to search multiple times in the SearchActivity all the instances of our SearchActivity would get stored on the call stack which is not needed and would also eat up a lot of memory.

Handling the Intent to the SearchActivity

We basically need to do a standard if check in order to check if the intent is of type ACTION_SEARCH.

if (Intent.ACTION_SEARCH.equals(getIntent().getAction())) {
    handleIntent(getIntent());
 }
@Override
 protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    handleIntent(intent);
 }
 public void handleIntent(Intent intent) {
    final String query = intent.getStringExtra(SearchManager.QUERY);
    searchQuery(query);
 }

The function searchQuery is called within handleIntent in order to search for the text that we received from the Homescreen.

SearchView Trigger Functions

Next we need to add two main functions in order to get the search working:

  • onQueryTextChange
  • onQueryTextSubmit

The function names are self-explanatory. Now we will move on to the code implementation of the given functions.

@Override
 public boolean onQueryTextChange(String query) {
    if(query!=null) {
        searchQuery(query);
        searchText = query;
    }
    else{
        results.clear();
        handleVisibility();
    }
   return true;
 }
 
 @Override
 public boolean onQueryTextSubmit(String query) {
    searchView.clearFocus();
    return true;
 }

The role of the searchView.clearFocus() inside the above code snippet is to remove the keyboard popup from the screen to enable the user to have a clear view of the search result.

Here the main search logic is being handled by the function called searchQuery which I’ll talking about now.

Search Logic

private void searchQuery(String constraint) {
 
    results.clear();
 
    if (!TextUtils.isEmpty(constraint)) {
        String query = constraint.toLowerCase(Locale.getDefault());
        addResultsFromTracks(query);
        addResultsFromSpeakers(query);
        addResultsFromLocations(query);
    }
 }
//THESE ARE SOME VARIABLES FOR REFERENCE
//This is the custom recycler view adapter that has been defined for the search
private GlobalSearchAdapter globalSearchAdapter;
 //This stores the results in an Object Array
 private List<Object> result

We are assuming that we have POJO’s(Plain Old Java Objects) for Tracks , Speakers , and Locations and for the Result Type Header.

The code posted below performs the function of getting the required results from the list of tracks. All the results are being fetched asynchronously from Realm and here we have also attached a header for the result type to denote whether the result is of type Track , Speaker or Location. We also see that we have added a changeListener to notify us if any changes have occurred in the set of results.

Similarly this is being done for all the result types that we need i.e Tracks, Locations and Speakers.

public void addResultsFromTracks(String queryString) {

   RealmResults<Track> filteredTracks = realm.where(Track.class)
                                        .like("name", queryString,                     Case.INSENSITIVE).findAllSortedAsync("name");
      filteredTracks.addChangeListener(tracks -> {

       if(tracks.size()>0){
            results.add("Tracks");
        }
       results.addAll(tracks);
       globalSearchAdapter.notifyDataSetChanged();
       Timber.d("Filtering done total results %d", tracks.size());
       handleVisibility();
});}

We now have a “Global Search” feature in the Open Event Android app. Users had asked for this feature and a learning for us is, that it would have been even better to do more tests with users when we developed the first versions. So, we could have included this feedback and implemented Global Search earlier on.

Resources