Book Excerpt: Full-Text Indexing and Searching (Part 3)

The following sections are excerpts from Version 6.9 of “The Busy Coder’s Guide to Android Development”, with slight modifications to fit the blog format. It continues the blog post series begun Monday.


As noted previously, this sample app is a revised version of the Stack Overflow questions list from the chapter on Internet access. It is specifically derived from the Picasso version of the sample. However, this version is designed to allow the user to full-text search the downloaded question data (e.g., title), above and beyond just seeing the list of latest questions.

The original sample had a very simple data model: a list of questions retrieved via Retrofit. Hence, the sample did not include much in the way of model management.

The FTS sample needs a database, which implies more local disk I/O that we are responsible for, which in turn leads us in the direction of implementing a model fragment (ModelFragment), much as the book’s tutorials and a few other samples do:

package com.commonsware.android.fts;

import android.app.Activity;
import android.app.Fragment;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.util.Log;
import de.greenrobot.event.EventBus;
import retrofit.RestAdapter;

public class ModelFragment extends Fragment {
  private Context app=null;

  @Override
  public void onCreate(Bundle state) {
    super.onCreate(state);

    setRetainInstance(true);
  }

  @Override
  public void onAttach(Activity host) {
    super.onAttach(host);

    EventBus.getDefault().register(this);

    if (app==null) {
      app=host.getApplicationContext();
      new FetchQuestionsThread().start();
    }
  }

  @Override
  public void onDetach() {
    EventBus.getDefault().unregister(this);

    super.onDetach();
  }

  public void onEventBackgroundThread(SearchRequestedEvent event) {
    try {
      Cursor results=DatabaseHelper.getInstance(app).loadQuestions(app, event.match);

      EventBus.getDefault().postSticky(new ModelLoadedEvent(results));
    }
    catch (Exception e) {
      Log.e(getClass().getSimpleName(),
          "Exception searching database", e);
    }
  }

  class FetchQuestionsThread extends Thread {
    @Override
    public void run() {
      RestAdapter restAdapter=
          new RestAdapter.Builder().setEndpoint("https://api.stackexchange.com")
              .build();
      StackOverflowInterface so=
          restAdapter.create(StackOverflowInterface.class);

      SOQuestions questions=so.questions("android");

      try {
        DatabaseHelper
            .getInstance(app)
            .insertQuestions(app, questions.items);
      }
      catch (Exception e) {
        Log.e(getClass().getSimpleName(),
            "Exception populating database", e);
      }

      try {
        Cursor results=DatabaseHelper.getInstance(app).loadQuestions(app, null);

        EventBus.getDefault().postSticky(new ModelLoadedEvent(results));
      }
      catch (Exception e) {
        Log.e(getClass().getSimpleName(),
            "Exception populating database", e);
      }
    }
  }
}

In onCreate(), we mark this fragment as retained, as that is key to the model fragment pattern, so the fragment retains the model data across configuration changes.

In onAttach(), we register for the greenrobot EventBus, plus kick off a FetchQuestionsThread if we have not done so already (i.e., this is the first onAttach() call we have received). onDetach() unregisters us from the event bus.

FetchQuestionsThread, in turn, uses Retrofit to download the questions from Stack Overflow, then uses DatabaseHelper to insert the questions into the FTS-enabled database table, then uses the DatabaseHelper again to retrieve all existing questions in the form of a Cursor, which it wraps in a ModelLoadedEvent and posts to the EventBus. This time, though, it posts it as a sticky event.

That sticky event is consumed by a revised version of the QuestionsFragment, in its onEventMainThread() method:

public void onEventMainThread(ModelLoadedEvent event) {
  ((SimpleCursorAdapter)getListAdapter()).changeCursor(event.model);

  if (sv!=null) {
    sv.setEnabled(true);
  }
}

Here, sv is a SearchView; we will look more at it tomorrow.

But because this is a sticky event, we will get this event both when it is raised (because the data is loaded) and any time thereafter when the fragment registers with the EventBus. This allows QuestionsFragment to not be retained, as it will get back the bulk of its model data automatically from greenrobot’s EventBus.

QuestionsFragment also is modified from the Picasso sample to deal with the fact that its model data is now a Cursor, so it uses SimpleCursorAdapter to populate the list. To handle loading avatar images from the URLs, QuestionsFragment adds a QuestionBinder implementation of ViewBinder to the SimpleCursorAdapter, where QuestionBinder handles the Picasso logic from before:

private class QuestionBinder implements SimpleCursorAdapter.ViewBinder {
  int size;

  QuestionBinder() {
    size=getActivity()
            .getResources()
            .getDimensionPixelSize(R.dimen.icon);
  }

  @Override
  public boolean setViewValue (View view, Cursor cursor, int columnIndex) {
    switch (view.getId()) {
      case R.id.title:
        ((TextView)view).setText(Html.fromHtml(cursor.getString(columnIndex)));

        return(true);

      case R.id.icon:
        Picasso.with(getActivity()).load(cursor.getString(columnIndex))
            .resize(size, size).centerCrop()
            .placeholder(R.drawable.owner_placeholder)
            .error(R.drawable.owner_error).into((ImageView)view);

        return(true);
    }

    return(false);
  }
}

The main activity (MainActivity) sets up the ModelFragment in onCreate(), at least when one does not already exist due to a configuration change:

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);

  if (getFragmentManager().findFragmentById(android.R.id.content) == null) {
    getFragmentManager().beginTransaction()
                        .add(android.R.id.content,
                             new QuestionsFragment()).commit();
  }

  model=(ModelFragment)getFragmentManager().findFragmentByTag(MODEL);

  if (model==null) {
    model=new ModelFragment();
    getFragmentManager().beginTransaction().add(model, MODEL).commit();
  }
}

This description, though, has skipped over the onEventBackgroundThread() method on the ModelFragment, which we will get to later in this blog post series.


In tomorrow’s post, we will look at how the SearchView is integrated, to perform the full-text search and show the results.