ANDROID DATABINDING: GOODBYE PRESENTER, HELLO VIEWMODEL!

from http://tech.vg.no/2015/07/17/android-databinding-goodbye-presenter-hello-viewmodel/


The Model-View-Presenter-pattern (MVP) has been the dominating trend lately when it comes the UI-layer architecture of Android applications. Frameworks like Ted Mosby,Nucleus and Mortar have all talked about Presenters to help you achieving a clean architecture of your app. They also (to a varying degree) help you with the infamous issues of device rotation and state persistence on the Android platform. This isn’t directly related to the concept of MVP, but the pattern helps you isolate the boiler plate code.

Data Binding, as announced on Google I/O 2015 and shipped with the Android M preview as a support library, changes everything. According to the Wikipedia article on MVP, the Presenter has the following tasks:

The presenter acts upon the model and the view. It retrieves data from repositories (the model), and formats it for display in the view.

The thing is that Data Binding framework will take over the main responsibilities of the Presenter (“acting upon the model and the view”), while the remainder is left to the enhanced Model – the ViewModel (“retreiving data from repositories and formatting”). The ViewModel is a standard Java class whose sole responsibility is to represent the data behind a single View. It can merge data from multiple sources (Models) and prepare that data for presentation. I dida short writeup on the ViewModel and how it differs from Data Model or Transport Model.

The architecture we end up with is MVVM – Model-View-ViewModel, and is a proven concept originally coined by Microsoft back in 2005 (don’t let that scare you ;-) ). Let me illustrate the change from MVP to MVVM for you, shamelessly copying Hanne Dorfmann’s illustration from the introduction of his Ted Mosby framework

mvp
mvvm

So all the binding and updating of data to the view is done through the Data Binding Framework. The ObservableField class allow the View to react on changes to the model, and the XML references to fields allow the framework to push changes back to the ViewModel when the user acts upon the View. You can also programatically subscribe to changes in fields so that for instance a TextView is disabled when a CheckBox is clicked. One great advantage of representing the visual state of the View in a standard Java class like this is clear: You can easily unit test the visual behaviour.

Note that in the MVP illustration above there is a method call to Presenter.loadUsers(). This is a Command. In MVVM these are methods defined in the ViewModel. From the Wikipedia article:

The view model is an abstraction of the view that exposes public properties andcommands

So this may or may not be a change to what you’re used to. In the MVP pattern it’s likely that your models were “dumb” classes only holding data. Don’t be afraid of putting business logic in your Models or View Models as well. This is a core principle of Object Oriented Programming. So back to Presenter.loadUsers() – this will now be a method in the ViewModel and may be invoked programmatically by the code-behind* of your View or though a data bound command in the XML of your View. That is – if the promises of this issuein the android-developer-preview issue tracker hold up. If we don’t get data binding to commands, we have to resort to the old android:onClick-syntax, or manually adding listeners in the View code as before.

*) “Code-behind” is a term from Microsoft, often with negative associations to early ASP.NET or WinForms. I think it’s a describing term also on Android where the View is composed of two source elements: the View Layout (XML) and the Code-Behind (Java), represented by Fragments, Activities and classes extending View.java.

Dealing with system calls

There is one set of use cases which still have to be done in the code-behind of the View – functions which initate system calls, opens dialogs or basically any call which require reference the Context object of Android. Don’t put code like this in the ViewModel. If it contains the line import android.content.Context;, you’re doing it wrong. Don’t do it. Kittens die.

I haven’t quite made up my mind on the best approach to tackle this yet, but that’s because there are several good options. One way would be to keep elements of the presenter concept from Mosby by referencing an interface to the View in the ViewModel. This way you won’t reduce the testability. But instead of having a seperate Presenter class as in Mosby, I’d stick to the View as the concrete implementation of that interface just to keep it simple. Another approach could be to use an event bus like Square’s Otto to initiate commands like new ShowToastMessage("hello world"). This will yield a greater separation of the view and the viewmodel – but is that a good thing?

Don’t we need frameworks now?

So is the Data Binding framework taking over the job of framworks like Mosby or Mortar? Only partly. What I hope to see is these frameworks evolve or fork into MVVM-style frameworks so that we can leverage the best of Data Binding while keeping the dependencies to 3rd-party frameworks to a minimum, and keeping the frameworks small and simple. While the era of the Presenter might be over, the framworks do a just as important job with lifecycle management and view state (ViewModel) persistence. This has not changed (unfortunately*).

*) Wouldn’t it be cool if Google introduced an interface LifeCycleAffected which FragmentActivityand View implemented? And that interface had methods named addOnPauseListener() andaddOnResumeListener()? I leave it up to you to what that might have done with our code base, and the code base of MVP/MVVM frameworks.

update: I’ve recently learned about the AndroidViewModel framework, which actually might be a very good fit for MVVM and Android Data Binding. I’ve not had the time to try this out yet though.

Summary

When I first heard that Android M was all about improving the SDK and focusing on the developers, I was really excited. When I heard they were introducing Data Binding, I was stoked. I have worked for years with data binding on other platforms : WinForms, WPF, Silverlight and Windows Phone. I know it will help us write cleaner architecture and less boiler plate code. The framework works with us instead of against us, as I’ve felt it has been for a long time.

But it’s not a silver bullet – there are downsides. The fact that you define stuff in XML is an issue. XML is not compiled. XML cannot be unit tested. Hence you’ll often end up noticing errors run-time instead of compile time. Forgot to bind the field to the view? Tough luck. But the tooling can help a great deal here – that’s why I hope Google is focusing hard on making Android Studio support data binding to the max. Syntax and reference checking of the binding XML, auto complete and navigation support. Field renaming support which propagates to XML. From my testing of Android Studio 1.3 beta – I’m at least assured that the are thinking about it. Some things are supported. A lot isn’t, but we’re still in beta so here’s hoping.

Code example

So here is a quick sample I threw together where I try to illustrate the conseqeunces of going from MVP to an MVVM architecture. I used Mosby as a framework in the MVP version with Butterknife for view injection. In the MVVM example I’m using Android M databinding and drop dependencis to both Mosby and Butterknife. The result is that the Presenter can be dropped, the Fragment gets less code but the ViewModel takes over a lot of the code.

In this example I directly reference the View in order to produce toast messages. This may or may not be an approach I advocate later, but in theory we’re not doing anything wrong here. It’s testable if you mock the Fragment using Robolectric and Mockito, and doesn’t leak memory unless you start referencing ViewModels where you should’n’t.

The app is basically a login screen with som async data loading and inter-view dependencies just for illustration purposes.

illustration

If you prefer to read the code in Android Studio – here’s the github repo. Check out the MVP and MVVM tags respectively.

Now, prepare for wall of code:

MVP – VIEW – XML

Show code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
< RelativeLayout xmlns:android = "http://schemas.android.com/apk/res/android"
                 xmlns:tools = "http://schemas.android.com/tools"
                 android:layout_width = "match_parent"
                 android:layout_height = "match_parent"
                 android:paddingLeft = "@dimen/activity_horizontal_margin"
                 android:paddingRight = "@dimen/activity_horizontal_margin"
                 android:paddingTop = "@dimen/activity_vertical_margin"
                 android:paddingBottom = "@dimen/activity_vertical_margin"
                 tools:context = ".MainActivityFragment" >
 
     < TextView
         android:text = "..."
         android:layout_width = "wrap_content"
         android:layout_height = "wrap_content"
         android:layout_alignParentEnd = "true"
         android:id = "@+id/loggedInUserCount" />
 
     < TextView
         android:text = "# logged in users:"
         android:layout_width = "wrap_content"
         android:layout_height = "wrap_content"
         android:layout_alignParentEnd = "false"
         android:layout_toLeftOf = "@+id/loggedInUserCount" />
 
     < RadioGroup
         android:layout_marginTop = "40dp"
         android:id = "@+id/existingOrNewUser"
         android:gravity = "center"
         android:layout_width = "wrap_content"
         android:layout_height = "wrap_content"
         android:layout_centerHorizontal = "true"
         android:orientation = "horizontal" >
 
         < RadioButton
             android:layout_width = "wrap_content"
             android:layout_height = "wrap_content"
             android:text = "Returning user"
             android:id = "@+id/returningUserRb" />
 
         < RadioButton
             android:layout_width = "wrap_content"
             android:layout_height = "wrap_content"
             android:text = "New user"
             android:id = "@+id/newUserRb"
             />
 
     </ RadioGroup >
 
     < LinearLayout
         android:orientation = "horizontal"
         android:layout_width = "match_parent"
         android:layout_height = "wrap_content"
         android:id = "@+id/username_block"
         android:layout_below = "@+id/existingOrNewUser" >
 
         < TextView
             android:layout_width = "wrap_content"
             android:layout_height = "wrap_content"
             android:textAppearance = "?android:attr/textAppearanceMedium"
             android:text = "Username:"
             android:id = "@+id/textView"
             android:minWidth = "100dp" />
 
         < EditText
             android:layout_width = "wrap_content"
             android:layout_height = "wrap_content"
             android:id = "@+id/username"
             android:minWidth = "200dp" />
     </ LinearLayout >
 
     < LinearLayout
         android:orientation = "horizontal"
         android:layout_width = "match_parent"
         android:layout_height = "wrap_content"
         android:layout_alignParentStart = "false"
         android:id = "@+id/password_block"
         android:layout_below = "@+id/username_block" >
 
         < TextView
             android:layout_width = "wrap_content"
             android:layout_height = "wrap_content"
             android:textAppearance = "?android:attr/textAppearanceMedium"
             android:text = "Password:"
             android:minWidth = "100dp" />
 
         < EditText
             android:layout_width = "wrap_content"
             android:layout_height = "wrap_content"
             android:inputType = "textPassword"
             android:ems = "10"
             android:id = "@+id/password" />
 
     </ LinearLayout >
 
     < LinearLayout
         android:orientation = "horizontal"
         android:layout_width = "match_parent"
         android:layout_height = "wrap_content"
         android:layout_below = "@+id/password_block"
         android:id = "@+id/email_block" >
 
         < TextView
             android:layout_width = "wrap_content"
             android:layout_height = "wrap_content"
             android:textAppearance = "?android:attr/textAppearanceMedium"
             android:text = "Email:"
             android:minWidth = "100dp" />
 
         < EditText
             android:layout_width = "wrap_content"
             android:layout_height = "wrap_content"
             android:inputType = "textEmailAddress"
             android:ems = "10"
             android:id = "@+id/email" />
     </ LinearLayout >
 
     < Button
         android:layout_width = "wrap_content"
         android:layout_height = "wrap_content"
         android:text = "Log in"
         android:id = "@+id/loginOrCreateButton"
         android:layout_below = "@+id/email_block"
         android:layout_centerHorizontal = "true" />
</ RelativeLayout >
   

MVP – VIEW – JAVA

Show code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package com.nilzor.presenterexample;
 
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.RadioButton;
import android.widget.TextView;
import android.widget.Toast;
import com.hannesdorfmann.mosby.mvp.MvpFragment;
import com.hannesdorfmann.mosby.mvp.MvpView;
import butterknife.InjectView;
import butterknife.OnClick;
 
public class MainActivityFragment extends MvpFragment implements MvpView {
     @InjectView (R.id.username)
     TextView mUsername;
 
     @InjectView (R.id.password)
     TextView mPassword;
 
     @InjectView (R.id.newUserRb)
     RadioButton mNewUserRb;
 
     @InjectView (R.id.returningUserRb)
     RadioButton mReturningUserRb;
 
     @InjectView (R.id.loginOrCreateButton)
     Button mLoginOrCreateButton;
 
     @InjectView (R.id.email_block)
     ViewGroup mEmailBlock;
 
     @InjectView (R.id.loggedInUserCount)
     TextView mLoggedInUserCount;
 
     public MainActivityFragment() {
     }
 
     @Override
     public MainPresenter createPresenter() {
         return new MainPresenter();
     }
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         return inflater.inflate(R.layout.fragment_main, container, false );
     }
 
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         super .onViewCreated(view, savedInstanceState);
         attachEventListeners();
     }
 
     private void attachEventListeners() {
         mNewUserRb.setOnCheckedChangeListener( new CompoundButton.OnCheckedChangeListener() {
             @Override
             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                 updateDependentViews();
             }
         });
         mReturningUserRb.setOnCheckedChangeListener( new CompoundButton.OnCheckedChangeListener() {
             @Override
             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                 updateDependentViews();
             }
         });
     }
 
     /** Prepares the initial state of the view upon startup */
     public void setInitialState() {
         mReturningUserRb.setChecked( true );
         updateDependentViews();
     }
 
     /** Shows/hides email field and sets correct text of login button depending on state of radio buttons */
     public void updateDependentViews() {
         if (mReturningUserRb.isChecked()) {
             mEmailBlock.setVisibility(View.GONE);
             mLoginOrCreateButton.setText(R.string.log_in);
         }
         else {
             mEmailBlock.setVisibility(View.VISIBLE);
             mLoginOrCreateButton.setText(R.string.create_user);
         }
     }
 
     public void setNumberOfLoggedIn( int numberOfLoggedIn) {
         mLoggedInUserCount.setText( ""  + numberOfLoggedIn);
     }
 
     @OnClick (R.id.loginOrCreateButton)
     public void loginOrCreate() {
         if (mNewUserRb.isChecked()) {
             Toast.makeText(getActivity(), "Please enter a valid email address" , Toast.LENGTH_SHORT).show();
         } else {
             Toast.makeText(getActivity(), "Invalid username or password" , Toast.LENGTH_SHORT).show();
         }
     }
}

MVP – PRESENTER

Show code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.nilzor.presenterexample;
 
import android.os.Handler;
import android.os.Message;
import com.hannesdorfmann.mosby.mvp.MvpPresenter;
 
public class MainPresenter implements MvpPresenter {
     MainModel mModel;
     private MainActivityFragment mView;
 
     public MainPresenter() {
         mModel = new MainModel();
     }
 
     @Override
     public void attachView(MainActivityFragment view) {
         mView = view;
         view.setInitialState();
         updateViewFromModel();
         ensureModelDataIsLoaded();
     }
 
     @Override
     public void detachView( boolean retainInstance) {
         mView = null ;
     }
 
     private void ensureModelDataIsLoaded() {
         if (!mModel.isLoaded()) {
             mModel.loadAsync( new Handler.Callback() {
                 @Override
                 public boolean handleMessage(Message msg) {
                     updateViewFromModel();
                     return true ;
                 }
             });
         }
     }
 
     /** Notifies the views of the current value of "numberOfUsersLoggedIn", if any */
     private void updateViewFromModel() {
         if (mView != null && mModel.isLoaded()) {
             mView.setNumberOfLoggedIn(mModel.numberOfUsersLoggedIn);
         }
     }
}

MVP – MODEL

Show code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.nilzor.presenterexample;
 
import android.os.AsyncTask;
import android.os.Handler;
import java.util.Random;
 
public class MainModel {
     public Integer numberOfUsersLoggedIn;
     private boolean mIsLoaded;
     public boolean isLoaded() {
         return mIsLoaded;
     }
 
     public void loadAsync( final Handler.Callback onDoneCallback) {
         new AsyncTask() {
             @Override
             protected Void doInBackground(Void... params) {
                 // Simulating some asynchronous task fetching data from a remote server
                 try {Thread.sleep( 2000 );} catch (Exception ex) {};
                 numberOfUsersLoggedIn = new Random().nextInt( 1000 );
                 mIsLoaded = true ;
                 return null ;
             }
 
             @Override
             protected void onPostExecute(Void aVoid) {
                 onDoneCallback.handleMessage( null );
             }
         }.execute((Void) null );
     }
}

MVVM – VIEW – XML

Show code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
< layout xmlns:android = "http://schemas.android.com/apk/res/android"
     xmlns:tools = "http://schemas.android.com/tools" >
     < data >
         < variable name = "data" type = "com.nilzor.presenterexample.MainModel" />
     </ data >
     < RelativeLayout
         android:layout_width = "match_parent"
         android:layout_height = "match_parent"
         android:paddingLeft = "@dimen/activity_horizontal_margin"
         android:paddingRight = "@dimen/activity_horizontal_margin"
         android:paddingTop = "@dimen/activity_vertical_margin"
         android:paddingBottom = "@dimen/activity_vertical_margin"
         tools:context = ".MainActivityFragment" >
 
         < TextView
             android:text = "@{data.numberOfUsersLoggedIn}"
             android:layout_width = "wrap_content"
             android:layout_height = "wrap_content"
             android:layout_alignParentEnd = "true"
             android:id = "@+id/loggedInUserCount" />
 
         < TextView
             android:text = "# logged in users:"
             android:layout_width = "wrap_content"
             android:layout_height = "wrap_content"
             android:layout_alignParentEnd = "false"
             android:layout_toLeftOf = "@+id/loggedInUserCount" />
 
         < RadioGroup
             android:layout_marginTop = "40dp"
             android:id = "@+id/existingOrNewUser"
             android:gravity = "center"
             android:layout_width = "wrap_content"
             android:layout_height = "wrap_content"
             android:layout_centerHorizontal = "true"
             android:orientation = "horizontal" >
 
             < RadioButton
                 android:layout_width = "wrap_content"
                 android:layout_height = "wrap_content"
                 android:text = "Returning user"
                 android:checked = "@{data.isExistingUserChecked}"
                 android:id = "@+id/returningUserRb" />
 
             < RadioButton
                 android:layout_width = "wrap_content"
                 android:layout_height = "wrap_content"
                 android:text = "New user"
                 android:id = "@+id/newUserRb"
                 />
 
         </ RadioGroup >
 
         < LinearLayout
             android:orientation = "horizontal"
             android:layout_width = "match_parent"
             android:layout_height = "wrap_content"
             android:id = "@+id/username_block"
             android:layout_below = "@+id/existingOrNewUser" >
 
             < TextView
                 android:layout_width = "wrap_content"
                 android:layout_height = "wrap_content"
                 android:textAppearance = "?android:attr/textAppearanceMedium"
                 android:text = "Username:"
                 android:id = "@+id/textView"
                 android:minWidth = "100dp" />
 
             < EditText
                 android:layout_width = "wrap_content"
                 android:layout_height = "wrap_content"
                 android:id = "@+id/username"
                 android:minWidth = "200dp" />
         </ LinearLayout >
 
         < LinearLayout
             android:orientation = "horizontal"
             android:layout_width = "match_parent"
             android:layout_height = "wrap_content"
             android:layout_alignParentStart = "false"
             android:id = "@+id/password_block"
             android:layout_below = "@+id/username_block" >
 
             < TextView
                 android:layout_width = "wrap_content"
                 android:layout_height = "wrap_content"
                 android:textAppearance = "?android:attr/textAppearanceMedium"
                 android:text = "Password:"
                 android:minWidth = "100dp" />
 
             < EditText
                 android:layout_width = "wrap_content"
                 android:layout_height = "wrap_content"
                 android:inputType = "textPassword"
                 android:ems = "10"
                 android:id = "@+id/password" />
 
         </ LinearLayout >
 
         < LinearLayout
             android:orientation = "horizontal"
             android:layout_width = "match_parent"
             android:layout_height = "wrap_content"
             android:layout_below = "@+id/password_block"
             android:id = "@+id/email_block"
             android:visibility = "@{data.emailBlockVisibility}" >
 
             < TextView
                 android:layout_width = "wrap_content"
                 android:layout_height = "wrap_content"
                 android:textAppearance = "?android:attr/textAppearanceMedium"
                 android:text = "Email:"
                 android:minWidth = "100dp" />
 
             < EditText
                 android:layout_width = "wrap_content"
                 android:layout_height = "wrap_content"
                 android:inputType = "textEmailAddress"
                 android:ems = "10"
                 android:id = "@+id/email" />
         </ LinearLayout >
 
         < Button
             android:layout_width = "wrap_content"
             android:layout_height = "wrap_content"
             android:text = "@{data.loginOrCreateButtonText}"
             android:id = "@+id/loginOrCreateButton"
             android:layout_below = "@+id/email_block"
             android:layout_centerHorizontal = "true" />
     </ RelativeLayout >
</ layout >

MVVM – VIEW – JAVA

Show code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.nilzor.presenterexample;
 
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.Toast;
 
import com.nilzor.presenterexample.databinding.FragmentMainBinding;
 
public class MainActivityFragment extends Fragment {
     private FragmentMainBinding mBinding;
     private MainModel mViewModel;
 
     public MainActivityFragment() {
     }
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         View view = inflater.inflate(R.layout.fragment_main, container, false );
         mBinding = FragmentMainBinding.bind(view);
         mViewModel = new MainModel( this , getResources());
         mBinding.setData(mViewModel);
         attachButtonListener();
         return view;
     }
 
     private void attachButtonListener() {
         mBinding.loginOrCreateButton.setOnClickListener( new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 mViewModel.logInClicked();
             }
         });
     }
 
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         ensureModelDataIsLodaded();
     }
 
     private void ensureModelDataIsLodaded() {
         if (!mViewModel.isLoaded()) {
             mViewModel.loadAsync();
         }
     }
 
     public void showShortToast(String text) {
         Toast.makeText(getActivity(), text, Toast.LENGTH_SHORT).show();
     }
}

MVVM – VIEWMODEL

Show code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package com.nilzor.presenterexample;
 
import android.content.res.Resources;
import android.databinding.ObservableField;
import android.os.AsyncTask;
import android.view.View;
 
import java.util.Random;
 
public class MainModel {
     public ObservableField numberOfUsersLoggedIn = new ObservableField();
     public ObservableField isExistingUserChecked = new ObservableField();
     public ObservableField emailBlockVisibility = new ObservableField();
     public ObservableField loginOrCreateButtonText = new ObservableField();
     private boolean mIsLoaded;
     private MainActivityFragment mView;
     private Resources mResources;
 
     public MainModel(MainActivityFragment view, Resources resources) {
         mView = view;
         mResources = resources; // You might want to abstract this for testability
         setInitialState();
         updateDependentViews();
         hookUpDependencies();
     }
     public boolean isLoaded() {
         return mIsLoaded;
     }
 
     private void setInitialState() {
         numberOfUsersLoggedIn.set( "..." );
         isExistingUserChecked.set( true );
     }
 
     private void hookUpDependencies() {
         isExistingUserChecked.addOnPropertyChangedCallback( new android.databinding.Observable.OnPropertyChangedCallback() {
             @Override
             public void onPropertyChanged(android.databinding.Observable sender, int propertyId) {
                 updateDependentViews();
             }
         });
     }
 
     public void updateDependentViews() {
         if (isExistingUserChecked.get()) {
             emailBlockVisibility.set(View.GONE);
             loginOrCreateButtonText.set(mResources.getString(R.string.log_in));
         }
         else {
             emailBlockVisibility.set(View.VISIBLE);
             loginOrCreateButtonText.set(mResources.getString(R.string.create_user));
         }
     }
 
     public void loadAsync() {
         new AsyncTask() {
             @Override
             protected Void doInBackground(Void... params) {
                 // Simulating some asynchronous task fetching data from a remote server
                 try {Thread.sleep( 2000 );} catch (Exception ex) {};
                 numberOfUsersLoggedIn.set( "" + new Random().nextInt( 1000 ));
                 mIsLoaded = true ;
                 return null ;
             }
         }.execute((Void) null );
     }
 
     public void logInClicked() {
         // Illustrating the need for calling back to the view though testable interfaces.
         if (isExistingUserChecked.get()) {
             mView.showShortToast( "Invalid username or password" );
         }
         else {
             mView.showShortToast( "Please enter a valid email address" );
         }
     }
}

Android developer at VG

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值