Android 之 MVVM架构

发布于:2025-08-04 ⋅ 阅读:(8) ⋅ 点赞:(0)

以下是一个基于 ​​Java 语言​​ 的 Android MVVM 登录模块详细实现,结合 ViewModelLiveDataDataBinding 和 Repository 模式,确保代码解耦、可维护性强。

一个完整的MVVM登录模块方案,会包括以下核心组件:

Model层使用Repository模式管理数据源,包括网络数据源(使用Retrofit)和本地数据源(使用Room)。

ViewModel层处理业务逻辑,使用LiveData暴露UI状态。

View层(Activity)使用DataBinding实现数据绑定,观察ViewModel的LiveData状态变化。使用单向和双向数据绑定技术简化UI更新。


​项目结构概览

app/
├── src/
│   ├── main/
│   │   ├── java/com/example/login/
│   │   │   ├── data/
│   │   │   │   ├── UserRepository.java       # 数据仓库
│   │   │   │   ├── local/UserDao.java        # Room数据库操作
│   │   │   │   ├── remote/ApiService.java    # 网络请求接口
│   │   │   ├── model/
│   │   │   │   ├── User.java                 # 用户实体类
│   │   │   ├── ui/
│   │   │   │   ├── LoginActivity.java        # View层
│   │   │   │   ├── LoginViewModel.java       # ViewModel层
│   │   │   ├── utils/
│   │   │   │   ├── BindingAdapters.java      # 自定义绑定适配器
│   │   ├── res/
│   │   │   ├── layout/activity_login.xml     # 使用DataBinding的布局

1. 添加依赖(build.gradle)

dependencies {
    // MVVM核心组件
    implementation 'androidx.lifecycle:lifecycle-viewmodel:2.6.0'
    implementation 'androidx.lifecycle:lifecycle-livedata:2.6.0'
    implementation 'androidx.databinding:databinding-runtime:7.0.0'

    // Room数据库(可选,用于本地缓存)
    implementation 'androidx.room:room-runtime:2.5.0'
    annotationProcessor 'androidx.room:room-compiler:2.5.0'

    // 网络请求(Retrofit + Gson)
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
}

2. Model层实现​

​2.1 用户实体类(User.java)
public class User {
    private String username;
    private String password;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // Getters & Setters
    public String getUsername() { return username; }
    public String getPassword() { return password; }
}
2.2 数据仓库(UserRepository.java)​
public class UserRepository {
    private final ApiService apiService;
    private final UserDao userDao;

    public UserRepository(ApiService apiService, UserDao userDao) {
        this.apiService = apiService;
        this.userDao = userDao;
    }

    public LiveData<Boolean> login(String username, String password) {
        MutableLiveData<Boolean> result = new MutableLiveData<>();
        
        // 优先检查本地数据库
        userDao.getUser(username, password).observeForever(localUser -> {
            if (localUser != null) {
                result.setValue(true); // 本地验证成功
            } else {
                // 本地无数据则发起网络请求
                apiService.login(new User(username, password)).enqueue(new Callback<Boolean>() {
                    @Override
                    public void onResponse(Call<Boolean> call, Response<Boolean> response) {
                        if (response.isSuccessful() && response.body()) {
                            userDao.saveUser(new User(username, password)); // 缓存登录成功的用户
                        }
                        result.setValue(response.body());
                    }
                    @Override
                    public void onFailure(Call<Boolean> call, Throwable t) {
                        result.setValue(false);
                    }
                });
            }
        });
        return result;
    }
}
2.3. UserDao (本地数据库访问)​

位于 data/local/UserDao.java,使用 ​​Room​​ 持久化框架实现本地缓存

import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.lifecycle.LiveData;

@Dao
public interface UserDao {
    // 根据用户名和密码查询用户(用于登录验证)
    @Query("SELECT * FROM users WHERE username = :username AND password = :password")
    LiveData<User> getUser(String username, String password);

    // 插入或更新用户(用于缓存登录成功的用户数据)
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void saveUser(User user);

    // 删除用户(可选)
    @Query("DELETE FROM users WHERE id = :userId")
    void deleteUser(int userId);
}
2.4配套实体类 User.java
import androidx.room.Entity;
import androidx.room.PrimaryKey;

@Entity(tableName = "users")
public class User {
    @PrimaryKey(autoGenerate = true)
    private int id;
    private String username;
    private String password;

    // 构造方法
    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // Getters & Setters
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getUsername() { return username; }
    public String getPassword() { return password; }
}
2.5. ApiService (网络请求接口)

位于 data/remote/ApiService.java,使用 ​​Retrofit​​ 定义网络API

import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.POST;

public interface ApiService {
    // 登录API(POST请求,提交User对象)
    @POST("auth/login")
    Call<Boolean> login(@Body User user);

    // 获取用户信息(可选扩展)
    @POST("user/profile")
    Call<User> getUserProfile(@Body String userId);
}
2.6Retrofit实例化工具类 RetrofitClient.java
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class RetrofitClient {
    private static final String BASE_URL = "https://your-api-domain.com/";
    private static Retrofit retrofit;

    public static Retrofit getInstance() {
        if (retrofit == null) {
            retrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        }
        return retrofit;
    }

    public static ApiService getApiService() {
        return getInstance().create(ApiService.class);
    }
}
2.7 BindingAdapters.java​

以下是基于 MVVM 架构的 BindingAdapters 工具类实现,包含图片加载、视图显隐控制等常用功能,解决 DataBinding 自定义属性绑定问题:

import android.graphics.drawable.Drawable;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
import androidx.annotation.DrawableRes;
import androidx.databinding.BindingAdapter;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;

public class BindingAdapters {

    // 1. 图片加载(支持URL、错误占位图)[4,7](@ref)
    @BindingAdapter(value = {"imageUrl", "errorPlaceholder"}, requireAll = false)
    public static void loadImage(ImageView view, String imageUrl, Drawable errorPlaceholder) {
        if (imageUrl != null && !imageUrl.isEmpty()) {
            Glide.with(view.getContext())
                .load(imageUrl)
                .apply(new RequestOptions().error(errorPlaceholder))
                .into(view);
        } else if (errorPlaceholder != null) {
            view.setImageDrawable(errorPlaceholder);
        }
    }

    // 2. 资源ID加载图片(避免直接传int导致资源混淆)[4](@ref)
    @BindingAdapter("android:src")
    public static void setImageResource(ImageView view, @DrawableRes int resId) {
        view.setImageResource(resId);
    }

    // 3. 控制视图显隐(支持布尔值)[5](@ref)
    @BindingAdapter("visibleIf")
    public static void setVisible(View view, boolean visible) {
        view.setVisibility(visible ? View.VISIBLE : View.GONE);
    }

    // 4. 进度条绑定(支持双向绑定)[5](@ref)
    @BindingAdapter("android:progress")
    public static void setProgress(ProgressBar progressBar, int progress) {
        if (progressBar.getProgress() != progress) {
            progressBar.setProgress(progress);
        }
    }
}

3. ViewModel层(LoginViewModel.java)​

public class LoginViewModel extends ViewModel {
    private UserRepository userRepository;
    private MutableLiveData<Boolean> loginResult = new MutableLiveData<>();
    private MutableLiveData<String> errorMessage = new MutableLiveData<>();
    private MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);

    // 双向绑定支持:用户名和密码
    public MutableLiveData<String> username = new MutableLiveData<>();
    public MutableLiveData<String> password = new MutableLiveData<>();

    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 登录操作
    public void onLoginClick() {
        if (username.getValue() == null || password.getValue() == null) {
            errorMessage.setValue("用户名或密码不能为空");
            return;
        }

        isLoading.setValue(true);
        userRepository.login(username.getValue(), password.getValue())
            .observeForever(new Observer<Boolean>() {
                @Override
                public void onChanged(Boolean success) {
                    isLoading.setValue(false);
                    if (success) loginResult.setValue(true);
                    else errorMessage.setValue("登录失败,请重试");
                }
            });
    }

    // Getters for LiveData
    public LiveData<Boolean> getLoginResult() { return loginResult; }
    public LiveData<String> getErrorMessage() { return errorMessage; }
    public LiveData<Boolean> getIsLoading() { return isLoading; }
}

4. View层实现​

​4.1 布局文件(activity_login.xml)使用DataBinding​
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="viewModel" type="com.example.login.ui.LoginViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="16dp">

        <!-- 双向绑定用户名 -->
        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={viewModel.username}"
            android:hint="用户名" />

        <!-- 双向绑定密码 -->
        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={viewModel.password}"
            android:inputType="textPassword"
            android:hint="密码" />

        <!-- 登录按钮绑定ViewModel方法 -->
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{() -> viewModel.onLoginClick()}"
            android:text="登录"
            android:enabled="@{!viewModel.isLoading}" />

        <!-- 加载状态提示 -->
        <ProgressBar
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" />

        <!-- 错误信息提示 -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.errorMessage}"
            android:textColor="#FF0000" />
    </LinearLayout>
</layout>
4.2 Activity(LoginActivity.java)
public class LoginActivity extends AppCompatActivity {
    private ActivityLoginBinding binding;
    private LoginViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_login);

        // 初始化ViewModel(需注入Repository)
        UserRepository repository = new UserRepository(RetrofitClient.getApiService(), AppDatabase.get(this).userDao());
        viewModel = new ViewModelProvider(this, new ViewModelProvider.Factory() {
            @NonNull
            @Override
            public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
                return (T) new LoginViewModel(repository);
            }
        }).get(LoginViewModel.class);

        binding.setViewModel(viewModel);
        binding.setLifecycleOwner(this); // 支持LiveData自动更新UI

        // 监听登录结果
        viewModel.getLoginResult().observe(this, success -> {
            if (success) startActivity(new Intent(this, MainActivity.class));
        });
    }
}

总结​

  • ​MVVM核心优势​​:
    通过 LiveData + DataBinding 实现​​数据驱动UI​​,ViewModel 管理业务逻辑,Repository 统一数据源。
  • ​关键实践​​:
    双向绑定简化输入处理、SingleLiveEvent 处理一次性事件、避免在 ViewModel 中持有 View 引用。
  • ​扩展性​​:
    可轻松集成 Room 持久化、Retrofit 网络请求、Dagger/Hilt 依赖注入。