使用ViewModel和DataBinding更新UI

20

我正在尝试学习Android中的ViewModel,在我的学习的第一阶段,我正在尝试使用ViewModel和DataBinding来更新UI(TextView)。在ViewModel中,我有一个AsyncTask回调,它将调用REST API调用。我可以从API调用中获取响应,但是textview中的值没有得到更新。

我的ViewModel类:

public class ViewModelData extends ViewModel {

    private MutableLiveData<UserData> users;

    public LiveData<UserData> getUsers() {
        if (users == null) {
            users = new MutableLiveData<UserData>();
            loadUsers();
        }

        return users;
    }

    public void loadUsers() {
        ListTask listTask =new ListTask (taskHandler);
        listTask .execute();

    }

    public Handler taskHandler= new Handler() {
        @Override
        public void handleMessage(Message msg) {


            UserData  userData = (UserData) msg.obj;
        
            users.setValue(userData);
        }
    };
}

以及我的MainActivity类:

public class MainActivity extends AppCompatActivity implements LifecycleOwner {
    private LifecycleRegistry mLifecycleRegistry;
    private TextView fName;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        fName = (TextView)findViewById(R.id.text_name);
        mLifecycleRegistry = new LifecycleRegistry(this);
        mLifecycleRegistry.markState(Lifecycle.State.CREATED);
        ViewModelData model = ViewModelProviders.of(this).get(ViewModelData.class);
        model.getUsers().observe(this, new Observer<UserData>() {
            @Override
            public void onChanged(@Nullable UserData userData) {
                Log.d("data"," =  - - - - ="+userData.getFirstName());

            }
        });

    }

    @Override
    public Lifecycle getLifecycle() {
        return mLifecycleRegistry;
    }
}

以及我的数据类:

public class UserData extends BaseObservable{
    private String firstName ;
@Bindable
    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
        notifyPropertyChanged(BR.firstName);
    }
}

和布局文件

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <import type="android.view.View" />
        <variable name="data" type="com.cgi.viewmodelexample.UserData"/>
    </data>
    <RelativeLayout
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context="com.cgi.viewmodelexample.MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{data.firstName}"
            android:id="@+id/text_name"/>
    </RelativeLayout>
</layout>

是的,我已经在主类中完成了这个。 - karthik selvaraj
3个回答

30
我建议遵循以下基本原则:
  • 不要通过业务或演示逻辑过载数据对象
  • 仅在表示层中需要视图模型以获取数据
  • 视图模型应向表示层公开仅“可立即使用”的数据
  • (可选) 后台任务应公开LiveData以传递数据
实施说明:
  • 在视图上只读取firstName
  • 在视图上可以编辑lastName
  • loadUser()方法不是线程安全的
  • 在加载数据之前,调用save()方法会出现错误消息
不要通过业务或演示逻辑过载数据对象
假设我们有一个带有名字和姓氏的UserData对象。因此,通常我们只需要获取其getter:
public class UserData {

    private String firstName;
    private String lastName;

    public UserData(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}

只需要视图模型来获取表现层数据

为了遵循这个建议,我们应该在数据绑定布局中仅使用视图模型:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.example.vmtestapplication.MainActivity">

    <data>

        <import type="android.view.View" />

        <!-- Only view model required -->
        <variable
            name="vm"
            type="com.example.vmtestapplication.UserDataViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:animateLayoutChanges="true"
        android:orientation="vertical">

        <!-- Primitive error message -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{vm.error}"
            android:visibility="@{vm.error == null ? View.GONE : View.VISIBLE}"/>

        <!-- Read only field (only `@`) -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{vm.firstName}" />

        <!-- Two-way data binding (`@=`) -->
        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@={vm.lastName}" />

    </LinearLayout>
</layout>

注意:你可以在一个布局中使用几个视图模型,但不要使用原始数据。
视图模型应该只向表示层公开准备好的数据。
这意味着,你不应该直接从视图模型中暴露复杂的数据对象(在我们的例子中为UserData)。更好的方法是暴露私有类型,视图可以直接使用。在下面的示例中,我们不需要保存UserData对象,因为它仅用于加载分组数据。我们可能需要创建UserData来保存它,但这取决于你的存储库实现。
public class UserDataViewModel extends ViewModel {

    private ListTask loadTask;

    private final MutableLiveData<String> firstName = new MediatorLiveData<>();
    private final MutableLiveData<String> lastName = new MediatorLiveData<>();
    private final MutableLiveData<String> error = new MutableLiveData<>();

    /**
     * Expose LiveData if you do not use two-way data binding
     */
    public LiveData<String> getFirstName() {
        return firstName;
    }

    /**
     * Expose MutableLiveData to use two-way data binding
     */
    public MutableLiveData<String> getLastName() {
        return lastName;
    }

    public LiveData<String> getError() {
        return error;
    }

    @MainThread
    public void loadUser(String userId) {
        // cancel previous running task
        cancelLoadTask();
        loadTask = new ListTask();
        Observer<UserData> observer = new Observer<UserData>() {
            @Override
            public void onChanged(@Nullable UserData userData) {
                // transform and deliver data to observers
                firstName.setValue(userData == null? null : userData.getFirstName());
                lastName.setValue(userData == null? null : userData.getLastName());
                // remove subscription on complete
                loadTask.getUserData().removeObserver(this);
            }
        };
        // it can be replaced to observe() if LifeCycleOwner is passed as argument
        loadTask.getUserData().observeForever(observer);
        // start loading task
        loadTask.execute(userId);
    }

    public void save() {
        // clear previous error message
        error.setValue(null);
        String fName = firstName.getValue(), lName = lastName.getValue();
        // validate data (in background)
        if (fName == null || lName == null) {
            error.setValue("Opps! Data is invalid");
            return;
        }
        // create and save object
        UserData newData = new UserData(fName, lName);
        // ...
    }

    @Override
    protected void onCleared() {
        super.onCleared();
        cancelLoadTask();
    }

    private void cancelLoadTask() {
        if (loadTask != null)
            loadTask.cancel(true);
        loadTask = null;
    }
}

后台任务应该使用 LiveData 传递数据

public class ListTask extends AsyncTask<String, Void, UserData> {

    private final MutableLiveData<UserData> data= new MediatorLiveData<>();

    public LiveData<UserData> getUserData() {
        return data;
    }

    @Override
    protected void onPostExecute(UserData userData) {
        data.setValue(userData);
    }

    @Override
    protected UserData doInBackground(String[] userId) {
        // some id validations
        return loadRemoiteUser(userId[0]);
    }
}

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private UserDataViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // get view model
        viewModel = ViewModelProviders.of(this).get(UserDataViewModel.class);
        // create binding
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        // set view model to data binding
        binding.setVm(viewModel);
        // don't forget to set LifecycleOwner to data binding
        binding.setLifecycleOwner(this);

        // start user loading (if necessary)
        viewModel.loadUser("user_id");
        // ...
    }
}

PS:尝试使用RxJava库来执行后台工作,而不是AsyncTask


binding.setLifecycleOwner(this)方法无法使用,是否有其他可用的替代方法?在重建时我还遇到了数据绑定错误(提示DataBinding已存在)。 - karthik selvaraj
@karthikselvaraj 您使用哪个版本的Android Studio和gradle插件?能否发布原始错误消息或完整堆栈跟踪? - Sergei Bubenshchikov
1
哇,将MutableLiveData公开以使用双向数据绑定这个评论真的很有帮助!谢谢! - Tanasis
有关具有用户身份验证并显示用户预订的应用程序,最好的建议是什么?因此,登录/注册页面将必须与UserRepo进行交互,Dashboard页面必须与UserRepo和BookingsRepo进行交互,而Profile页面只需要处理UserRepo即可。 - Suyash Dixit
此外,我看到过一些帖子,其中他们在视图模型中使用类似 fun onLoginClick(view: View){} 的语法,然后将其与 XML 绑定。但在其他地方,我读到 View 永远不应该传递到 ViewModel 中。我感到困惑! - Suyash Dixit
@SuyashDixit,通过 onLoginClick(view: View){},你可以使用方法引用绑定代替监听器绑定。它使你的绑定代码更短,但不会更好。仍然不应该在视图模型内部存储视图链接。此外,通常会忽略此视图参数。 - Sergei Bubenshchikov

0

当您像这样设置值时,需要通知观察者:

public class UserData extends BaseObservable{
private String firstName ;
@Bindable
public String getFirstName() {
    return firstName;
}

public void setFirstName(String firstName) {
    this.firstName = firstName;
    notifyPropertyChanged(BR.firstName) // call like this
}
}

1
由于数据绑定,您在XML中将UserData设置为变量。因此,任何与UserData相关的更改都将通过数据绑定反映到UI上。 - Jeel Vankhede

0

如果您想要绑定布局正常工作,那么您必须以绑定方式设置您的视图。还要在绑定类中设置数据。

public class MainActivity extends AppCompatActivity implements LifecycleOwner {
    ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        ...
        ViewModelData model = ViewModelProviders.of(this).get(ViewModelData.class);
        ...
        binding.setData(model.getUsers());
    }
}

你能再解释一下吗?我已经在布局文件中进行了更改,并在主类中绑定了viewModel(就像你在代码中提到的那样),但我仍然不理解。 - karthik selvaraj
你是否在 setFirstName() 中设置了 notifyPropertyChanged - Khemraj Sharma
是的,我使用过“notifyPropertyChanged(BR.firstName);”。 - karthik selvaraj
我只能在你的代码中看到上述问题,现在它应该按预期工作了。当响应到来时,你是如何设置数据的? - Khemraj Sharma
我将在ViewModel类中使用setvalue来接收处理程序中的数据并设置数据(所有这些操作都已完成)。现在,在进行绑定“binding.setData(model.getUsers());”时,我遇到了编译时错误。我尝试了您在代码中提到的相同方法。 - karthik selvaraj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接