3

I'm trying to implement MVVM with Data binding + Samsung Health SDK, I'm aware a little bit about the MVVM concept:

  • View: this is the UI (activity, fragment, etc)
  • View Model: this is the bridge between your view and your model
  • Model: this could be your data source (DB, network source, etc)

so if I have fragment:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable name="isLoading" type="local.team.dev.shealth.mvvm.SplashViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/cardview_light_background"
        android:orientation="vertical">

        <TextView
            android:id="@+id/loading_tv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center_vertical|center_horizontal"
            android:text="@string/loading_splash"
            android:textAlignment="center"
            app:visibleGone="@{isLoading}"/>
    </LinearLayout>
</layout>

and below is Samsung Health SDK works:

it has a service object and Datastore object as follow:

// where should i put this 2 objects in MVVM architecture?
HealthDataService healthDataService = new HealthDataService();
    try {
        healthDataService.initialize(this);
    } catch (Exception e) {
        e.printStackTrace();
    }

    // Create a HealthDataStore instance and set its listener
    mStore = new HealthDataStore(this, mConnectionListener);
    // Request the connection to the health data store
    mStore.connectService();

which mStore using listener as follow to determine the connection status:

// where should i put this listener in MVVM architecture?
// and how to bind it with databinding in View UI
private final HealthDataStore.ConnectionListener mConnectionListener = new HealthDataStore.ConnectionListener() {

    @Override
    public void onConnected() {
        Log.d(APP_TAG, "Health data service is connected.");
        mReporter = new StepCountReporter(mStore);
        if (isPermissionAcquired()) {
            // if it is connected and permission acquired then go to next activity
        } else {
            requestPermission(); // <-- UI trigger data permission here
        }
    }

    @Override
    public void onConnectionFailed(HealthConnectionErrorResult error) {
        Log.d(APP_TAG, "Health data service is not available.");
        showConnectionFailureDialog(error);
        // Can i display message to fragment ui // Can i display message to fragment ui
        // using data binding if connection failed (and perhaps show retry button)
    }

    @Override
    public void onDisconnected() {
        Log.d(APP_TAG, "Health data service is disconnected.");
        // Can i display message to fragment ui
        // and tell the status with data binding
        if (!isFinishing()) {
            mStore.connectService();
        }
    }
};

the questions here is:

  1. where should i put or expose healthDataService and HealthDataStore object? in app class? ViewModel? or Model?

healthDataService and HealthDataStore should survive when the app move to another activity to be able to use to collect data from Samsung Health

  1. how to leverage the listener to be able to 'connect' with databinding and show the message status in Fragment UI TextView? where should i put the listener? in View model or in View?

there maybe no right or wrong answer here, but eventually, i'm looking after what is the right approach to do this...

thanks!


EDIT: below is what i'm trying to approach, however, it giving me null Object Reference T_T

Beware of the long content

I post this for a discussion of my approach, but it's not work yet.

In general this is how i do it:

+--------+    +-------------+    +----------------+  +----------+  +-------+
|activity+----+ActivityClass|    |ApplicationClass+--+Repository+--+Service|
+--------+    +------+------+    +-----+----------+  +-----+----+  +-------+
                     |                 |                   |
+--------+    +------+------+    +-----+----+         +----+---+
|fragment+----+FragmentClass+----+View Model|         |Listener|
|dataBind|    |with DataBind|    |Class     |         +--------+
+--------+    +-------------+    +----------+

so here is my app Class

class SHealth extends Application {
    public static final String APP_TAG = "SimpleHealth";
    private HealthDataStore mStore;

    public HealthDataService getHealthService() { return HealthService.getInstance(); }
    public HealthRepository getRepository() { return HealthRepository.getInstance(getHealthService(), this); }
}

and in my Activity and Fragment class i have it like this:

public class SplashActivity extends AppCompatActivity {
    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_splash);
        if (savedInstanceState == null) {
            SplashFragment fragment = new SplashFragment();
            getSupportFragmentManager().beginTransaction()
                .add(R.id.fragment_container, fragment, SplashFragment.TAG).commit();
        }
    }
}

// MY FRAGMENT CLASS
public class SplashFragment extends Fragment {

    public static final String TAG = "SplashViewModel";

    private SHealth app;
    private TextView loading_tv;

    private FragmentSplashBinding mBinding;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        mBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_splash, container, false);
        return mBinding.getRoot();
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        app = (SHealth) this.getActivity().getApplication();
        loading_tv = this.loading_tv;

        SplashViewModel.Factory factory = new SplashViewModel.Factory(app);

        // i'm not sure what's the diffrent using factory or not...
        final SplashViewModel viewModel = ViewModelProviders.of(this, factory).get(SplashViewModel.class);
        //final SplashViewModel viewModel = ViewModelProviders.of(this).get(SplashViewModel.class);

        subscribeUi(viewModel);
    }

    private void subscribeUi(SplashViewModel viewModel) {
        // Update the list when the data changes
        LiveData<HealthDSConnectionListener.Status> mObservableStatus = viewModel.getConnectionStatus();
        Observer<HealthDSConnectionListener.Status> observer = new Observer<HealthDSConnectionListener.Status>() {
            @Override
            public void onChanged(@Nullable HealthDSConnectionListener.Status status) {
                if (status == HealthDSConnectionListener.Status.CONNECTED) {
                    Log.v("statusOnChange", "Connected");
                    //mBinding.setIsLoading(true);
                    //mBinding.setConnectionStatus("Connected");
                } else if(status == HealthDSConnectionListener.Status.DISCONNECTED){
                    Log.v("statusOnChange", "DISCONNECTED");
                    //mBinding.setIsLoading(true);
                    //mBinding.setConnectionStatus("DISCONNECTED");
                }else{
                    Log.v("statusOnChange", "Failed");
                    //mBinding.setIsLoading(true);
                    //mBinding.setConnectionStatus("Failed");
                }
            }
        };

        mObservableStatus.observe(this,observer);
    }
}

and below is my ViewModel Class:

public class SplashViewModel extends AndroidViewModel {

    private final MediatorLiveData<HealthDSConnectionListener.Status> mObservableStatus;
    private final HealthDSConnectionListener mConnectionListener;

    public SplashViewModel(@NonNull Application application) {
        super(application);
        HealthRepository repository = ((SHealth) application).getRepository();

        mObservableStatus = new MediatorLiveData<>();
        mObservableStatus.setValue(null);

        mConnectionListener = new HealthDSConnectionListener(repository){
            @Override
            public void onConnected() {
                super.onConnected();
                Log.v("statusOnChange", "C");
            }

            @Override
            public void onConnectionFailed(HealthConnectionErrorResult error) {
                super.onConnectionFailed(error);
                Log.v("statusOnChange", "F");
            }

            @Override
            public void onDisconnected() {
                super.onDisconnected();
                Log.v("statusOnChange", "D");
            }
        };

        repository.connectDataStore(mConnectionListener);
        LiveData<HealthDSConnectionListener.Status> status = repository.getConnectionStatus();
        mObservableStatus.addSource(status, mObservableStatus::setValue);

    }

    public LiveData<HealthDSConnectionListener.Status> getConnectionStatus(){ return mObservableStatus; }

    public static class Factory extends ViewModelProvider.NewInstanceFactory {

        @NonNull
        private final Application mApplication;
        private final HealthRepository mRepository;

        public Factory(@NonNull Application application) {
            mApplication = application;
            mRepository = ((SHealth) application).getRepository();
        }

        @Override
        public <T extends ViewModel> T create(Class<T> modelClass) {
            //noinspection unchecked
            return (T) new SplashViewModel(mApplication);
        }
    }
}

and below is my Repository Class:

public class HealthRepository {
    private static HealthRepository sHealthInstance;
    private final HealthDataService mService;
    private HealthDataStore mStore;
    private SHealth app;

    private MutableLiveData<HealthDSConnectionListener.Status> DSConnectionStatus;
    public MutableLiveData<HealthDSConnectionListener.Status> getDSConnectionStatus(){
        if (DSConnectionStatus == null) { DSConnectionStatus = new MutableLiveData<HealthDSConnectionListener.Status>(); }
        return DSConnectionStatus;
    }
    public void setDSConnectionStatus(HealthDSConnectionListener.Status status){ getDSConnectionStatus().setValue(status); }
    private MediatorLiveData<HealthDSConnectionListener.Status> mObservableConnectionStatus;

    private HealthRepository(final HealthDataService service, final SHealth context) {
        this.app = context;
        mService = service;

        try { mService.initialize(app); }
        catch (Exception e) { e.printStackTrace(); }

        mObservableConnectionStatus = new MediatorLiveData<>();
        mObservableConnectionStatus.setValue(null);
        mObservableConnectionStatus.addSource(DSConnectionStatus, mObservableConnectionStatus::setValue);
    }

    public void connectDataStore(HealthDSConnectionListener listener) {
        if(mStore == null) {
            mStore = new HealthDataStore(app, listener);
            mStore.connectService();
        }
    }

    public static HealthRepository getInstance(final HealthDataService service, final Context context) {
        if (sHealthInstance == null) {
            synchronized (HealthRepository.class) {
                if (sHealthInstance == null) {
                    sHealthInstance = new HealthRepository(service, (SHealth) context);
                }
            }
        }
        return sHealthInstance;
    }

    public LiveData<HealthDSConnectionListener.Status> getConnectionStatus() { return mObservableConnectionStatus; }
}

and here is my Service Class:

public abstract class HealthService {

    private static HealthDataService sInstance;
    public static HealthDataService getInstance() {
        if (sInstance == null) {
            synchronized (HealthService.class) {
                if (sInstance == null) {
                    sInstance = new HealthDataService();
                    return sInstance;
                }
            }
        }
        return sInstance;
    }
}

with my 'in-house' listener extending from the default API listener:

reason i'm extending default listener can be found here

public class HealthDSConnectionListener implements HealthDataStore.ConnectionListener {

    public enum Status{
        CONNECTED(1), DISCONNECTED(2), FAILED(0);
        private final int index;
        Status(int index) { this.index = index; }
        int getIndex() { return this.index; }
    }

    private final HealthRepository repo;
    public HealthDSConnectionListener(HealthRepository repo) { this.repo = repo; }

    @Override public void onConnected() {
        repo.setDSConnectionStatus(Status.CONNECTED);
    }
    @Override public void onDisconnected() {
        repo.setDSConnectionStatus(Status.DISCONNECTED);
    }
    @Override public void onConnectionFailed(HealthConnectionErrorResult healthConnectionErrorResult) {
        repo.setDSConnectionStatus(Status.FAILED);
    }
}

But I have a problem with this approach, because it keep throw me:

I dont undrestand why it happen and how to solve it...

FATAL EXCEPTION: main Process: local.team.dev.shealthdemo, PID: 3458 java.lang.RuntimeException: Unable to start activity ComponentInfo{local.team.dev.shealthdemo/local.team.dev.shealthdemo.SplashActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.arch.lifecycle.LiveData.observeForever(android.arch.lifecycle.Observer)' on a null object reference at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2416) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476) at android.app.ActivityThread.-wrap11(ActivityThread.java) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:148) at android.app.ActivityThread.main(ActivityThread.java:5417) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.arch.lifecycle.LiveData.observeForever(android.arch.lifecycle.Observer)' on a null object reference at android.arch.lifecycle.MediatorLiveData$Source.plug(MediatorLiveData.java:141) at android.arch.lifecycle.MediatorLiveData.onActive(MediatorLiveData.java:118) at android.arch.lifecycle.LiveData$LifecycleBoundObserver.activeStateChanged(LiveData.java:389) at android.arch.lifecycle.LiveData$LifecycleBoundObserver.onStateChanged(LiveData.java:378) at android.arch.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:353) at android.arch.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:180) at android.arch.lifecycle.LiveData.observe(LiveData.java:201) at android.arch.lifecycle.LiveData.observeForever(LiveData.java:220) at android.arch.lifecycle.MediatorLiveData$Source.plug(MediatorLiveData.java:141) at android.arch.lifecycle.MediatorLiveData.onActive(MediatorLiveData.java:118) at android.arch.lifecycle.LiveData$LifecycleBoundObserver.activeStateChanged(LiveData.java:389) at android.arch.lifecycle.LiveData$LifecycleBoundObserver.onStateChanged(LiveData.java:378) at android.arch.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:353) at android.arch.lifecycle.LifecycleRegistry.forwardPass(LifecycleRegistry.java:291) at android.arch.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.java:331) at android.arch.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.java:137) at android.arch.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:123) at android.support.v4.app.Fragment.performStart(Fragment.java:2391) at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1458) at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1740) at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1809) at android.support.v4.app.FragmentManagerImpl.dispatchStateChange(FragmentManager.java:3217) at android.support.v4.app.FragmentManagerImpl.dispatchStart(FragmentManager.java:3176) at android.support.v4.app.FragmentController.dispatchStart(FragmentController.java:203) at android.support.v4.app.FragmentActivity.onStart(FragmentActivity.java:570) at android.support.v7.app.AppCompatActivity.onStart(AppCompatActivity.java:177) at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1237) at android.app.Activity.performStart(Activity.java:6253) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2379) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476)  at android.app.ActivityThread.-wrap11(ActivityThread.java)  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344)  at android.os.Handler.dispatchMessage(Handler.java:102)  at android.os.Looper.loop(Looper.java:148)  at android.app.ActivityThread.main(ActivityThread.java:5417)  at java.lang.reflect.Method.invoke(Native Method)  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) 

AnD
  • 3,060
  • 8
  • 35
  • 63
  • This is something interesting, if nobody comes out with a good answer I will try to make some tests to see if a way is better than the other. Btw I can't promise you when – MatPag Dec 09 '17 at 19:39
  • Thanks @MatPag, yeah, I'm still struggling with this, I may post my approach later - but yet resolved... – AnD Dec 10 '17 at 04:11

0 Answers0