15

i am storing user information in a local room database. In activities and fragments I use AndroidViewModel and LiveData to listen to changes made to the database and update the UI.

Now I want to analyze all of the past user data to give recommendations for future decisions. My recommendations change on every change to the database made by the user so I need to update my reommendations frequently while doing the same calculations over and over again.

I was thinking about starting a service on app start that listens to database changes via ViewModel and LiveData and updates my recommendations (which are also stored in the same database). But somehow a Service cannot

  1. get a ViewModel via viewModel = ViewModelProviders.of(this).get(DataViewModel.class);
  2. observe a LiveData object as it is not a LifecycleOwner.

Basically I simply need to read all entries from the database, analyze the data and update 5-10 values every time the database content changes.

How and where should I do my calculations if not in a service? Maybe I am trapped in a wrong thought and a service is not the right way to do it so any idea on how to do this is very much appreciated!

steasy
  • 203
  • 1
  • 2
  • 8
  • is RxJava an option to you? – Stefano Mtangoo Jun 18 '18 at 19:24
  • @StefanoMtangoo I don't know much about RxJava except that it offers Observables similar to LiveData. Can you give me some detail on how you would approach this with RxJava? – steasy Jun 18 '18 at 20:34
  • How are "changes made to the database"? And are we talking about a bound Service which will be running only as long as the app is in the foreground? – Bö macht Blau Jun 19 '18 at 16:38
  • @0X0nosugar Changes to the database are made via user interaction (EditTexts, Buttons, Switches etc.) and are then immediately written to the db. So changes can only be made while the app is in the foreground. Bound Service -> yes, IF I can finish my calculations before the Service is killed as soon as the app goes to the background. Is that possible with bound services? – steasy Jun 19 '18 at 17:22
  • "IF I can finish my calculations before the Service is killed" - that depends on how long the calculations will take. To be on the safe side, one could start an IntentService (which uses another Thread for its work and will be restarted if killed prematurely). The IntentService usually somehow persists its results and sends a local Broadcast to let interested parties know that it has finished its task. Modern variant: JobIntentService. As soon as your app comes to the foreground again it could be the job of some RecommendationsViewModel to fetch the results (if any) from storage – Bö macht Blau Jun 19 '18 at 17:32
  • I think I should be fine with starting a bound service in onCreate of my application object from what I've learned. I'll have a look at JobIntentService though to be sure, thanks for the advise! – steasy Jun 19 '18 at 19:48

3 Answers3

10

observe a LiveData object as it is not a LifecycleOwner

Use observeForever() on the LiveData, manually unregistering via removeObserver() when appropriate (onDestroy() of the service, if not sooner).

Bear in mind that standard service limitations apply here (e.g., services run for ~1 minute on Android 8.0+ unless they are foreground services), so it may be that you need to consider other approaches anyway.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • Does this mean the Service has to implement [Observer](https://developer.android.com/reference/android/arch/lifecycle/Observer)? And if yes, how does the Service get the correct LiveData instance for [un]registering? Should one pass the LiveData instance via a Message? – Bö macht Blau Jun 19 '18 at 18:42
  • 1
    @0X0nosugar: "Does this mean the Service has to implement Observer?" -- it can use an inner class, if you wish. "how does the Service get the correct LiveData instance for [un]registering?" -- it would retrieve the `LiveData` from your repository or DAO, and hold onto it in a field. "Should one pass the LiveData instance via a Message?" -- I doubt that is possible. – CommonsWare Jun 19 '18 at 19:05
  • 1
    @CommonsWare ObserveForever was the key, thank you so much! Also, I was able to receive an instance of ViewModel in a Service via `ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication)).create(MyViewModel.class);`. – steasy Jun 19 '18 at 19:51
5

I ended up using a Service and solved my problem as follows:

In the onCreate method of my Application object I bind MyService to it:

serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder iBinder) {
            service = ((MyService.MyLocalBinder) iBinder ).getService();
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    };
Intent intent = new Intent(this, MyService.class);
getApplicationContext().bindService(intent, serviceConnection, BIND_AUTO_CREATE);

Binding a Service to the application context should keep the Service alive as long as the application is not destroyed. In MyService I get an instance of ViewModel via AndroidViewModelFactory like this

MyViewModel myViewModel = ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication()).create(MyViewModel.class);

and I am able to observe the fetched LiveData from the ViewModel via observeForever like this

Observer<List<Entry>> obsEntries = new Observer<List<Entry>>() {
        @Override
        public void onChanged(@Nullable List<Entry> entries) {
            //perform calculations with entries in here
        }
    };
    viewModel.getEntries().observeForever(obsEntries);

Important: Remove the observer from the LiveData reference in onDestroy of the Service (that is why I keep a local reference to the Observer object):

@Override
public void onDestroy() {
    super.onDestroy();
    viewModel.getEntries().removeObserver(obsEntries);
}

Thanks everybody!

steasy
  • 203
  • 1
  • 2
  • 8
  • Can you elaborate a bit more on MyService.class that you create. I am getting error on this line for MyService.MyLocalBinder : service = ((MyService.MyLocalBinder) iBinder ).getService() Thnx in advance – Sukesh Saxena Dec 21 '18 at 07:46
4

Nothing to change in Entity.

In DAO, say for e.g.,

@Dao
public interface TimeDao {
    //    ...
    //    ...

    // for a started Service using startService() in background
    @Query("select * from times where name = :bName")
    List<TimeEntity> getServiceTimes(String bName);

}

which is not a LiveData

In Database, for e.g.,

@Database(entities = {DataEntity.class, TimeEntity.class}, version = 1)
public abstract class BrRoomDatabase extends RoomDatabase {

    public abstract TimeDao iTimeDao();

    public static BrRoomDatabase getDatabase(final Context context) {

        if (INSTANCE == null) {        
            //          ...
        }
        return INSTANCE;
    }
}

An interface class

public interface SyncServiceSupport {
    List<TimeEntity> getTimesEntities(String brName);
}

An implementation class for it.

public class SyncServiceSupportImpl implements SyncServiceSupport {

    private TimeDao timeDao;

    public SyncServiceSupportImpl(Context context) {

        timeDao = BrRoomDatabase.getDatabase(context).iTimeDao(); 
        // getDatabase() from where we get DB instance.
        // .. and .. 
        // public abstract TimeDao iTimeDao();  <= defined abstract type in RoomDatabase abstract class
    }

    @Override
    public List<TimeEntity> getTimesEntities(String name) {
        return timeDao.getServiceTimes(name);
    }

}

And finally... in the service..

public class SyncService extends Service {

    //..

    // now, iEntities will have a List of TimeEntity objects from DB .. 
    // .. retrieved using @Query 

    private static List<TimeEntity> iEntities = new ArrayList<>();  
    private static SyncServiceSupportImpl iSyncService;

    private static void getFromDB(String brName) {

        new AsyncTask<String, Void, Void>() {
            @Override
            protected Void doInBackground(String... params) {
                iEntities = iSyncService.getTimesEntities(params[0]);
                return null;
            }

            @Override
            protected void onPostExecute(Void agentsCount) {

            }
        }.execute(brName);
    }

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

        //init service
        iSyncService = new SyncServiceSupportImpl(SyncService.this);

    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        // this can be elsewhere also !..

        getFromDB(myName);
    }

}
Pramod P K
  • 41
  • 3