Android MVVM. Is life-cycle aware Model a code smell or a proper design?

542 views Asked by At

For my activity I need to know the connectivity state of the phone. Since this is the data that my UI needs to react to, it falls into a Model realm of MVVM (please let me know if you disagree).

To not execute code when not needed, my model code is currently subscribing to phone connectivity changes in onCreate() and de-registers in onDestroy() by implementing LifecycleObserver

To do so, I instantiate and wire up my model with viewModel inside of the Activity code.

Somehow it feels wrong.

In the ideal world, Activity would be part of a View layer (V in MVVM) and should only know about the viewModel, but in the case above the lifecycle-awareness makes an activity to know about Model too.

So, is a lifecycle-aware model a proper concept? Or should I re-think the design?

1

There are 1 answers

1
zapl On BEST ANSWER

I really like that pattern. An example of implementing state listening in a global sharable object is

public class WifiState {
    private static WifiState instance;
    public static synchronized WifiState getInstance(Context context) {
        if (instance == null) instance = new WifiState(context.getApplicationContext());
        return instance;
    }

    private final Context context;
    private final WifiManager wm;
    private WifiState(Context context) {
        this.context = context;
        wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
    }

    private final BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            int state = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN);
            wifiStateLiveData.setValue(state);
        }
    };

    public LiveData<Integer> wifiState() { return wifiStateLiveData; }

    private final MutableLiveData<Integer> wifiStateLiveData = new MutableLiveData<Integer>() {

        @Override
        protected void onActive() {
            setValue(wm.getWifiState()); // update immediately
            context.registerReceiver(receiver, new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION));
        }
        @Override
        protected void onInactive() {
            context.unregisterReceiver(receiver);
        }

        @Override
        public void setValue(Integer value) { // debounce non-change
            Integer old = getValue();
            if (old == null && value != null || old != null && !old.equals(value)) {
                super.setValue(value);
            }
        }
    };
}

That allows you to use the same source from multiple places at once without the overhead of creating multiple broadcast receivers etc

public class SomeActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        WifiState.getInstance(this).wifiState().observe(this, state -> {
            if (state != WifiManager.WIFI_STATE_ENABLED) {
                Toast.makeText(this, "Please enable WIFI", Toast.LENGTH_LONG);
            }
        });
    }
}

The big advantage of using the Lifecycle capabilities is that you don't have to litter your code with onStart / onStop calls, plus you can no longer miss onStop calls and leak receivers for example. The magic happens in some library and your code can become fairly simple and clean. Whether you implement LifecycleObserver or use LiveData, when done right, you end up with cleaner code.

Letting the framework handle the lifetime of your ViewModel is also nice but it's not really changing much in terms of the MV* schema since you had a ViewModel one way or another before. You probably calculated properties in the Activity / Fragment directly. What's new here is that the framwork now retains that model for you for just the right amount of time. You could have done that with a retained Fragment before, and that's exactly what happens under the hood now.