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
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 Fragment
, Activity
and 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.
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
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
|
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
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
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
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
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
|
<
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
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
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"
);
}
}
}
|