Tuesday, February 12, 2013

Android MVVM Implementation

Just an update for this blog. I recently published an open source project AndroidMVC to help apply MVC/MVVM design pattern for Android Development.
Please check the URLs below

http://kejunxia.github.io/AndroidMvc/

https://github.com/kejunxia/AndroidMvc


Below was the old post
========================================================================
In my lastest a few Android projects, I tried to adopt MVVM pattern. I think the event driven mode is very good for the android app. Here is a Sample Android Project in Github.

I created this MVVM variation for Android app. In this pattern, it doesn't use command wrapper thus there is no reflection which may impact the performance.

Here is the the brief graph illustrating the general idea


Here are the explainations for the objects shown above

Model
Models for holding states and data. Models can be referenced and manipulated by View models.
ViewModel State Model
ViewModels State Model represent the state of views but delegated by ViewModel. It has one to one relationship to ViewModel. ViewModel State Model can be considered as the data of ViewModel which can be bound and updated by ViewModel. As ViewModel State Model only hold the data of ViewModel, it's very easy to serialize into JSON string and re-populated from JSON String.

This is very useful because this make things easy to store the state of views in onSaveInstanceState(Bundle outState) callback in Android activity and fragment. I met the problem that when you leave the app in the background for a long time and when you active the app later, the app crashes. In that case I used static fields to hold state of views. However the Android will clear the static fields when it needs to recollect resources, and then when you are back to the app the static data become null and app crashed. Another difficulty is it's very very tricky to detect if the app if back from the background and ever been killed by the Android OS. So be careful to use static variable to store state. Instead, using a easy serializable java object and save the state in onSaveInstanceState(Bundle outState) and restore them in onCreate, onCreateView or other initializing calls to restore data. If it's too slow, try to use Parcelable.

Another reason to avoid using static field is that, it makes fragment and activity more independent. To use one you just need to give them the initial binding data in a Intent rather than relying on some classes holding the static fields.
ViewModel
ViewModels incorporate with views. Views register callbacks to ViewModels and then when they call method of ViewModels, the ViewModel execute a procedure and then fire the call back to update Views. When a ViewModel bind a data it should update the View referencing it.
As ViewModels can be registered by multiple views, it's very easy to keep all related views up to date all the time.
ViewModelEevnt
Interfaces define viewmodel events. View register these events to the ViewModel which is dependent by the View. When view wants to do something, it calls viewModel.doSomething() and then the view received the event "onSomethingHapped()". 
View
Views incorporate with ViewModels. It can be a concrete view such as a button, a text view, a custom view, a fragment, a activity and etc. Also it can be thought as a abstract concept. For example, the whole application which does have visible UI components can be considered as a view as well. Let's call it AppView. AppView can use a UserViewModel and register LoggedInEvent. And keep the UserViewModel as a app wide variable by a static field or property. Then when other activity/fragment or whatever issued a login call then the AppView will receive logged in event callback and do corresponding updates. And show the UIs for logged users by for example removing current fragments for guest users and load fragments for logged in users. In this case it works a little similar as Controller in MVC pattern.


Code example
Let's try to do a video player. The whole sample project can be find in Github


The Model
This data model hold the info about a video.

public class Video {
 private String mName;
 private int Duration;

 public String getName() {
  return mName;
 }

 public void setName(String name) {
  mName = name;
 }

 public int getDuration() {
  return Duration;
 }

 public void setDuration(int duration) {
  Duration = duration;
 }

}


The ViewModel State Model
The model holds the state the if a video is set for playing and whether or not it's playing.

public class VideoPlayerState {
        // ViewModel State Model contains a data model
 private Video mCurrentVideo;
 private boolean mPlaying;

 public Video getCurrentVideo() {
  return mCurrentVideo;
 }

 public void setCurrentVideo(Video currentVideo) {
  mCurrentVideo = currentVideo;
 }

 public boolean isPlaying() {
  return mPlaying;
 }

 public void setPlaying(boolean playing) {
  mPlaying = playing;
 }

}


A ViewModelEvent
Callbacks when video is played and paused.

public interface VideoPlayerViewModelEvent extends BaseViewModelEvent<VideoPlayerState> {
 void onVideoPlayed(Video video);

 void onVideoPaused(Video video);
}


The ViewModel

The video player view model handles play and pause videos can update the state model accordingly.

public class VideoPlayerViewModel extends
  BaseViewModel<VideoPlayerViewModelEvent, VideoPlayerState> {

 public void playVideo() {
  if (mModel != null) {
   if (!mModel.isPlaying()) {
    mModel.setPlaying(true);
    for (VideoPlayerViewModelEvent evet : this) {
     evet.onVideoPlayed(mModel.getCurrentVideo());
    }
   }
  }
 }

 public void pauseVideo() {
  if (mModel != null) {
   if (mModel.isPlaying()) {
    mModel.setPlaying(false);
    for (VideoPlayerViewModelEvent evet : this) {
     evet.onVideoPaused(mModel.getCurrentVideo());
    }
   }
  }
 }
}


The View
The view is an activity. The part that to save and restore the instance state can be abstracted out into a base class. However, this can't be done very generic as views will derive from different classes such as ViewGroup, Activity, Fragment and whatever else.
public class VideoPlayerView extends Activity implements OnClickListener {
 private static ObjectMapper sMapper;
 private static final String KEY_STATE_JSON = "KeyStateJson";

 private static final String TAG = VideoPlayerView.class.getSimpleName();
 private TextView mTextPlayingVideo;
 private TextView mTextVideoLen;
 private ImageView mPlayingButton;
 private Button mBtnLoadVideo;
 private VideoPlayerViewModel mViewModel;
 private VideoPlayerViewModelEvent mEvent;

 private VideoPlayerState mVideoPlayerState;

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

  mPlayingButton = (ImageView) findViewById(R.id.imgPlayerButton);
  mPlayingButton.setOnClickListener(this);
  mTextPlayingVideo = (TextView) findViewById(R.id.txtPlayingVideoTitle);
  mTextVideoLen = (TextView) findViewById(R.id.txtVideoLength);
  mTextPlayingVideo.setOnClickListener(this);
  mBtnLoadVideo = (Button) findViewById(R.id.btnLoadVideo);
  mBtnLoadVideo.setOnClickListener(this);

  mViewModel = new VideoPlayerViewModel();

  // Setup events handler for the view models
  mEvent = new VideoPlayerViewModelEvent() {
   @Override
   public void onDataBound(VideoPlayerState dataModel) {
    if (dataModel == null) {
     mTextPlayingVideo.setText("No Video");
     mTextVideoLen.setText("0s");
     mPlayingButton.setImageResource(R.drawable.video_play_drawable);
     mBtnLoadVideo.setText("Load Video");
    } else {
     if (dataModel.getCurrentVideo() != null) {
      mTextPlayingVideo.setText(dataModel.getCurrentVideo().getName());
      mTextVideoLen.setText(dataModel.getCurrentVideo().getDuration() + "s");
      mPlayingButton.setImageResource(R.drawable.video_pause_drawable);
     }
     mBtnLoadVideo.setText("Unload Video");
    }

   }

   @Override
   public void onVideoPlayed(Video video) {
    mTextPlayingVideo.setText(video.getName());
    mPlayingButton.setImageResource(R.drawable.video_pause_drawable);
    Toast.makeText(getApplicationContext(), video.getName() + " is played.",
      Toast.LENGTH_SHORT).show();
   }

   @Override
   public void onVideoPaused(Video video) {
    mPlayingButton.setImageResource(R.drawable.video_play_drawable);
    Toast.makeText(getApplicationContext(), video.getName() + " is paused.",
      Toast.LENGTH_SHORT).show();
   }
  };
  // Register events.
  mViewModel.registerEvent(mEvent);

  // Use JSON to serialize state which is easy and quick, if too slow
  // replace it by Parcealable instead.
  if (savedInstanceState == null) {
   // Check if invoking activity send a video to play
   if (getIntent() != null) {
    if (getIntent().hasExtra(KEY_STATE_JSON)) {
     try {
      mVideoPlayerState = getMapper().readValue(
        getIntent().getStringExtra(KEY_STATE_JSON), VideoPlayerState.class);
     } catch (IOException e) {
      Log.e(TAG, e.getMessage(), e);
     }
    }
   }
  } else {
   // Restore the instance state when
   // 1. rotating the phone,
   // 2. back from background when it's killed by OS

   // This can be done in a base class for all view
   if (savedInstanceState.containsKey(KEY_STATE_JSON)) {
    try {
     mVideoPlayerState = getMapper().readValue(
       savedInstanceState.getString(KEY_STATE_JSON), VideoPlayerState.class);
     mViewModel.bindData(mVideoPlayerState);
    } catch (IOException e) {
     Log.e(TAG, e.getMessage(), e);
    }
   }
  }
  // No pre set player state, create a new one for testing.
  if (mVideoPlayerState == null) {
   mVideoPlayerState = new VideoPlayerState();
   Video video = new Video();
   video.setName("Test Video");
   video.setDuration(90);
   mVideoPlayerState.setCurrentVideo(video);
  }
 }

 // Remember to save instance state. Using static is dangerous as Android OS
 // will clear it in background. If you rely on them, you will find they
 // suddenly turned null when app came back from the background after a long
 // time period.

 // This can be done in a base class for all view
 @Override
 protected void onSaveInstanceState(Bundle outState) {
  super.onSaveInstanceState(outState);
  if (mViewModel.getModel() != null) {
   try {
    outState.putString(KEY_STATE_JSON,
      getMapper().writeValueAsString(mViewModel.getModel()));
   } catch (IOException e) {
    Log.e(TAG, e.getMessage(), e);
   }
  }
 }

 @Override
 public void onClick(View view) {
  switch (view.getId()) {
   case R.id.btnLoadVideo:
    // Load unload button clicked.
    if (mViewModel.getModel() == null) {
     mViewModel.bindData(mVideoPlayerState);
    } else {
     mViewModel.bindData(null);
    }
    break;
   case R.id.imgPlayerButton:
    // Play/Pause button clicked.
    VideoPlayerState m = mViewModel.getModel();
    if (m != null) {
     if (!m.isPlaying()) {
      mViewModel.playVideo();
     } else {
      mViewModel.pauseVideo();
     }
    } else {
     mViewModel.bindData(mVideoPlayerState);
     mViewModel.playVideo();
    }
    break;
   default:
    break; // do nothing
  }
 }

 private static ObjectMapper getMapper() {
  if (sMapper == null) {
   sMapper = new ObjectMapper();
  }
  return sMapper;
 }
}

5 comments:

  1. This is a very interesting article about MVVM. Its reminds me of an abstraction layer I created for an APP which I never associated with a design pattern but this has definately given me ideas and I will certainly be studying this framework.

    David

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Hi Kejun,

    We were actually trying to apply some approach some years before to android also, finally we created a framework RoboBinding - https://github.com/RoboBinding/RoboBinding, which you may be interested in for your future projects. There are also some alternatives: Android-Binding, Bindroid and MvvmCross. Links to them can be found on home page of my RoboBinding.

    Cheng

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete