Search code examples
androidandroid-sqliteandroid-cursorloaderasynctaskloader

Android. Fetch data from database and then make network query. How to implement?


I'm pretty new to an Android development and currently trying to write an app that will show tomorrow's weather of multiple cities. Sorry for any incorrent termins that I might use in this question.

What I want to reach:

App will fetch data from local database, then build a HTTP query on the data fetched from a DB, get JSON response and form a list elements.

What I currently have:

Everything except SQL functionality.

Here is the snapshot of my main activity code. I use LoaderCallbacks<List<Weather>> to build URI with needed parameters in onCreateLoader(int i, Bundle bundle), send HTTP query and get the data via WeatherLoader(this, uriList), and form elements results in a List using WeatherAdapter.

public class WeatherActivity extends AppCompatActivity
implements LoaderCallbacks<List<Weather>>,
SharedPreferences.OnSharedPreferenceChangeListener {

private static final int WEATHER_LOADER_ID = 1;
private WeatherAdapter mAdapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.weather_activity);

    ListView weatherListView = (ListView) findViewById(R.id.list);

    mEmptyStateTextView = (TextView) findViewById(R.id.empty_view);
    weatherListView.setEmptyView(mEmptyStateTextView);
    mAdapter = new WeatherAdapter(this, new ArrayList<Weather>());
    weatherListView.setAdapter(mAdapter);

    ...

    weatherListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
            Weather currentWeather = mAdapter.getItem(position);
            Uri forecastUri = Uri.parse(currentWeather.getUrl());
            Intent websiteIntent = new Intent(Intent.ACTION_VIEW, forecastUri);
            startActivity(websiteIntent);
        }
    });

    ConnectivityManager connMgr = (ConnectivityManager)
            getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
    if (networkInfo != null && networkInfo.isConnected()) {
        LoaderManager loaderManager = getLoaderManager();
        loaderManager.initLoader(WEATHER_LOADER_ID, null, this);
    } else {
        View loadingIndicator = findViewById(R.id.loading_indicator);
        loadingIndicator.setVisibility(View.GONE);
        mEmptyStateTextView.setText(R.string.no_internet_connection);
    }
}

@Override
public Loader<List<Weather>> onCreateLoader(int i, Bundle bundle) {

    SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
    String tempUnit = sharedPrefs.getString(
            getString(R.string.settings_temp_unit_key),
            getString(R.string.settings_temp_unit_default));

    List<String> uriList = new ArrayList<>();

    /*** 
     *
     * Here we input cities for which we want to see the forecast
     *
     * ***/

    List<String> cities = new ArrayList<>();
    cities.add("London,uk");
    cities.add("Kiev,ua");
    cities.add("Berlin,de");
    cities.add("Dubai,ae");

    //For each city in the list generate URI and put it in the URIs list
    for (String city : cities){
        Uri baseUri = Uri.parse(OWM_REQUEST_URL);
        Uri.Builder uriBuilder = baseUri.buildUpon();

        uriBuilder.appendQueryParameter("q", city);
        uriBuilder.appendQueryParameter("cnt", "16");
        uriBuilder.appendQueryParameter("units", tempUnit);
        uriBuilder.appendQueryParameter("appid", "some_key");

        uriList.add(uriBuilder.toString());
    }

    return new WeatherLoader(this, uriList);
}

@Override
public void onLoadFinished(Loader<List<Weather>> loader, List<Weather> weatherList) {

    mAdapter.clear();

    // If there is a valid list of forecasts, then add them to the adapter's
    // data set. This will trigger the ListView to update.
    if (weatherList != null && !weatherList.isEmpty()) {
        mAdapter.addAll(weatherList);
    }
}

@Override
public void onLoaderReset(Loader<List<Weather>> loader) {
    mAdapter.clear();
}

As you see, cities are "hardcoded" via List<String> cities = new ArrayList<>(); in onCreateLoader(int i, Bundle bundle). That's why I've decided to implement SQL storage of cities in my app. I know how to implement SQL functionality in android app using ContentProvider and CursorAdapter.

So what's the problem?

If I am correct we should use LoaderManager.LoaderCallbacks<Cursor> if we want to make a query to a local DB.

Unfortunately, I can't imagine how to merge current LoaderCallbacks<List<Weather>> and LoaderCallbacks<Cursor> in one activity to make it work as I want.

Actually, I want to change List<String> cities = new ArrayList<>(); on something like Cursor cursor = new CursorLoader(this, WeatherEntry.CONTENT_URI, projection, null, null, null); to build the URI on the results that CursorLoader returns.

But, we should make SQL query in separate thread and HTTP query also(!) in separate thread. Should we do nested threads/loaders (http query in a scope of sql fetching data and return a List<T>)? Even can't imagine how it's possible to do, if so...

Help me please, I've stuck!


Solution

  • Ok, it was not obvious to me at the first sight, but I finally solved the problem.

    In the question above we had a list of cities that were hardcoded:

    List<String> cities = new ArrayList<>();
    cities.add("London,uk");
    cities.add("Kiev,ua");
    cities.add("Berlin,de");
    cities.add("Dubai,ae");
    

    Even if we assume that we will change it to a DB query, like this:

    // Connect to a DB 
    ...
    Cursor forecastCitiesDataCursor = mDb.query(true, WeatherContract.WeatherEntry.TABLE_NAME, projection,
                        null, null, null,
                        null, null, null);
    ...
    // Fetch data from cursor
    

    ...we will have that SQL query on the main thread. So we need a solution.

    The best thing that I've found for me, it is put that SQL query in CustomLoader class and pass needed parameters in a constructor (in my case, it is SharedPreferences parameter to built an HTTP query).

    Here is my code:

    WeatherActivity.java

    public class WeatherActivity extends AppCompatActivity implements LoaderCallbacks<List<Weather>>,
            SharedPreferences.OnSharedPreferenceChangeListener {
        ...
        @Override
        public Loader<List<Weather>> onCreateLoader(int i, Bundle bundle) {
    
            SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
            String tempUnit = sharedPrefs.getString(
                    getString(R.string.settings_temp_unit_key),
                    getString(R.string.settings_temp_unit_default));
    
            return new WeatherLoader(this, tempUnit);
        }
    
        @Override
        public void onLoadFinished(Loader<List<Weather>> loader, List<Weather> weatherList) {
            // Hide loading indicator because the data has been loaded
            View loadingIndicator = findViewById(R.id.loading_indicator);
            loadingIndicator.setVisibility(View.GONE);
    
            // Set empty state text to display "No forecasts found."
            mEmptyStateTextView.setText(R.string.no_forecasts);
    
            // Clear the adapter of previous forecasts data
            mAdapter.clear();
    
            // If there is a valid list of forecasts, then add them to the adapter's
            // data set. This will trigger the ListView to update.
            if (weatherList != null && !weatherList.isEmpty()) {
                mAdapter.addAll(weatherList);
            }
        }
    

    WeatherLoader.java

    public class WeatherLoader extends AsyncTaskLoader<List<Weather>> {
    ...
        // Pass parameters here from WeatherActivity
        public WeatherLoader(Context context, String tmpUnit) {
            super(context);
            mTempUnit = tmpUnit;
        }
    
        @Override
        protected void onStartLoading() {
            forceLoad();
        }
    
        /**
         * This is on a background thread.
         */
        @Override
        public List<Weather> loadInBackground() {
    
            // List for storing built URIs
            List<String> uriList = new ArrayList<>();
            // List for storing forecast cities
            List<String> cities = new ArrayList<>();
    
            // Define a projection that specifies the columns from the table we care about.
            ...
    
            Cursor forecastCitiesDataCursor = mDb.query(true, WeatherContract.WeatherEntry.TABLE_NAME, projection,
                    null, null, null,
                    null, null, null);
    
            // Get list of cities from cursor
            ...
    
            //For each city in the list generate URI and put it in the URIs list
            for (String city : cities){
                Uri baseUri = Uri.parse(OWM_REQUEST_URL);
                Uri.Builder uriBuilder = baseUri.buildUpon();
    
                uriBuilder.appendQueryParameter("q", city);
                uriBuilder.appendQueryParameter("cnt", "16");
                uriBuilder.appendQueryParameter("units", mTempUnit);
                uriBuilder.appendQueryParameter("appid", /*some id*/);
    
                uriList.add(uriBuilder.toString());
            }
    
            if (uriList == null) {
                return null;
            }
    
            // Perform the network request, parse the response, and extract a list of forecasts.
            List<Weather> forecasts = QueryUtils.fetchForecastData(uriList);
            return forecasts;
        }
    

    So what we've got?

    We've implemented persistent data storage within the work with ArrayAdapter that are used to do an HTTP query then. SQL query are on the separate thread and we'll have no problem with app performance.

    Hope that solution will help somebody, have a nice day!