这学期写了个基于MVVM与jetpack新技术的安卓项目,但由于时间与精力因素,我是在是没空去写了,今天来总结一下项目代码与开发时的经验。

以页面的布局路线来讲好了

1.登陆页面

界面如图(注册实在没时间写了,主要技术难度不高,还有个打开本地相册取图片当头像的代码也做过了,只不过不在这个项目...)

屏幕截图 2025-05-28 084119.png

介绍一下界面,普通的输入框按钮就不介绍了,这边验证码是通过后端接口Kaptch拿来的

@RequestMapping("/checkcode")
@RestController
public class Checkcode {
    @Resource
    DefaultKaptcha defaultKaptcha;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    //生成验证码
    @GetMapping("/Code")
    public void defaultKaptcha(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
            throws Exception {

        byte[] captchaChallengeAsJpeg = null;
        ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
        try {
            // 生产验证码字符串并保存到session中
            String createText = defaultKaptcha.createText();
            System.out.println("生成的是"+createText);
            createText = createText.toLowerCase();
            ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
            operations.set(createText, "0", 5, TimeUnit.MINUTES);//未验证

            httpServletRequest.getSession().setAttribute("picCode", createText);
            // 使用生成的验证码字符串返回一个BufferedImage对象并转为byte写入到byte数组中
            BufferedImage challenge = defaultKaptcha.createImage(createText);
            ImageIO.write(challenge, "jpg", jpegOutputStream);
        } catch (IllegalArgumentException e) {
            httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 定义response输出类型为image/jpeg类型,使用response输出流输出图片的byte数组
        captchaChallengeAsJpeg = jpegOutputStream.toByteArray();
        httpServletResponse.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
        httpServletResponse.setHeader("Pragma", "no-cache");
        httpServletResponse.setDateHeader("Expires", 0);
        httpServletResponse.setContentType("image/jpeg");
        ServletOutputStream responseOutputStream = httpServletResponse.getOutputStream();
        responseOutputStream.write(captchaChallengeAsJpeg);
        responseOutputStream.flush();
        responseOutputStream.close();
    }
    //    @ApiOperation(value = "校对验证码")
    @PostMapping("/check")
    public Result checkVerificationCode(@RequestParam String picCode, HttpServletRequest httpServletRequest) {
        String verificationCodeIn = (String)httpServletRequest.getSession().getAttribute("picCode");
        httpServletRequest.getSession().removeAttribute("picCode");
        if ((StringUtils.isEmpty(verificationCodeIn)) || (!verificationCodeIn.equals(picCode))) {
            return Result.error("验证码错误,或已失效");
        } else {
            //移除验证码
            stringRedisTemplate.delete(verificationCodeIn);
            return Result.success();
        }
    }
}

还有记住密码实现--就是从本地sharedPreferences读取而已

以下是全部代码

public class MainActivity extends AppCompatActivity {

    private MainViewModel viewModel;
    private ImageView captchaImageView;
    private Button loginEntryBtn;
    private TextInputEditText loginAccount;
    private TextInputEditText loginPassword;
    private TextInputEditText loginVerify;

    private CheckBox rememberPasswordCheckbox;
    private SharedPreferences sharedPreferences;

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

        // 初始化 TokenManager
        try {
            TokenManager.init(this);
        } catch (Exception e) {
            e.printStackTrace();
            Tools.Toast(MainActivity.this, "初始化 TokenManager 失败");
            return;
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.POST_NOTIFICATIONS}, 1);
        }
        // 使用工具类设置状态栏
        StatusBarUtil.setImmersiveStatusBarR(this);

        // 初始化控件
        captchaImageView = findViewById(R.id.captchaImageView);
        loginEntryBtn = findViewById(R.id.login_entry_btn);
        loginAccount = findViewById(R.id.login_account);
        loginPassword = findViewById(R.id.login_password);
        loginVerify = findViewById(R.id.login_verify);
        rememberPasswordCheckbox =  findViewById(R.id.remember_password_checkbox);
        sharedPreferences = getSharedPreferences("LoginPrefs", MODE_PRIVATE);
        // 读取保存的账号和密码
        String savedAccount = sharedPreferences.getString("account", "");
        String savedPassword = sharedPreferences.getString("password", "");
        boolean isRemembered = sharedPreferences.getBoolean("remember", false);
        if (isRemembered) {
            loginAccount.setText(savedAccount);
            loginPassword.setText(savedPassword);
            rememberPasswordCheckbox.setChecked(true);
        }

        // 初始化 ViewModel
        viewModel = new ViewModelProvider(this).get(MainViewModel.class);

        // 观察验证码图片的变化
        viewModel.getCaptchaImage().observe(this, bitmap -> captchaImageView.setImageBitmap(bitmap));

        // 观察 toast 消息的变化
        viewModel.getToastMessage().observe(this, message -> Tools.Toast(MainActivity.this, message));

        // 观察认证 token 的变化
        viewModel.getAuthToken().observe(this, token -> {
            if (token != null) {
                try {
                    // 将 token 存储到本地
                    TokenManager.encryptAndStoreToken(MainActivity.this, token);
                    //获取User
                    viewModel.getUserInfo();
                    // 启动 UserManageLayout 并销毁 MainActivity
                    Intent intent = new Intent(MainActivity.this, UserManageLayout.class);
                    //Service启动
                    Intent serviceIntent = new Intent(this, GoodSyncService.class);
                    startService(serviceIntent);
                    startActivity(intent);
                    finish();//销毁界面
                } catch (Exception e) {
                    e.printStackTrace();
                    Tools.Toast(MainActivity.this, "存储 token 失败");
                }
            }
        });

        // 获取验证码图片
        viewModel.getCheckcodeImage();
        captchaImageView.setOnClickListener(v -> viewModel.getCheckcodeImage());

        // 登录按钮逻辑
        loginEntryBtn.setOnClickListener(v -> {
            String account = Objects.requireNonNull(loginAccount.getText()).toString();
            String password = Objects.requireNonNull(loginPassword.getText()).toString();
            String verify = Objects.requireNonNull(loginVerify.getText()).toString();

            if (account.isEmpty()) {
                Tools.Toast(MainActivity.this, "请输入账号!");
                return;
            }
            if (password.isEmpty()) {
                Tools.Toast(MainActivity.this, "请输入密码!");
                return;
            }
            if (verify.isEmpty()) {
                Tools.Toast(MainActivity.this, "请输入验证码!");
                return;
            }

            // 保存账号和密码
            SharedPreferences.Editor editor = sharedPreferences.edit();
            if (rememberPasswordCheckbox.isChecked()) {
                editor.putString("account", account);
                editor.putString("password", password);
                editor.putBoolean("remember", true);
            } else {
                editor.remove("account");
                editor.remove("password");
                editor.putBoolean("remember", false);
            }
            editor.apply();
            viewModel.verifyLogin(account, password, verify);
        });

        // 添加 TextWatcher 到 loginVerify
        loginVerify.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                // 不需要实现
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                // 不需要实现
            }

            @Override
            public void afterTextChanged(Editable s) {
                if (s.length() == 4) {
                    hideKeyboard();
                }
            }
        });
    }

    // 关闭软键盘的方法
    private void hideKeyboard() {
        InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
        if (imm != null && getCurrentFocus() != null) {
            imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
        }
    }
}

登陆后将token存入本地,后期请求其他接口要用

屏幕截图 2025-05-28 103411.png

2.主界面

界面展示

屏幕截图 2025-05-28 084153.png

主界面(商品界面)主要由viewpager2与自定义tabLayout组成

商品个体item xml界面代码如下

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="horizontal"
    android:background="@drawable/good_item_bg"
    android:layout_margin="5dp"
    android:padding="10dp">

    <androidx.cardview.widget.CardView
        android:layout_width="120dp"
        android:layout_height="match_parent"
        app:cardCornerRadius="8dp"
        >
        <ImageView
            android:id="@+id/goodImage"
            android:layout_width="120dp"
            android:layout_height="match_parent"
            android:src="@drawable/bg"
            android:scaleType="centerCrop"

            />
    </androidx.cardview.widget.CardView>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:paddingHorizontal="10dp"
        >
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_marginBottom="5dp"
            >
            <TextView
                android:id="@+id/goodTitle"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:textColor="@android:color/black"
                android:textSize="16sp"
                android:text="啦啦啦"
                android:layout_weight="0.55"
                android:textStyle="bold"
                android:ellipsize="end"
                android:maxLines="1"/>

            <TextView
                android:id="@+id/goodPrice"
                android:layout_width="0dp"
                android:layout_weight="0.45"
                android:layout_height="wrap_content"
                android:textColor="@android:color/black"
                android:textSize="14sp"
                android:text="原价:$2500000"
                android:gravity="center"
                android:textStyle="bold"
                android:ellipsize="end"
                android:maxLines="1"/>
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_marginBottom="5dp"
            >
            <TextView
                android:id="@+id/goodInventory"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:textColor="@android:color/black"
                android:textSize="14sp"
                android:text="库存:1300件"
                android:layout_weight="0.55"
                android:textStyle="bold"
                android:ellipsize="end"
                android:maxLines="1"/>

            <TextView
                android:id="@+id/goodSellingPrice"
                android:layout_width="0dp"
                android:layout_weight="0.45"
                android:layout_height="wrap_content"
                android:textColor="@android:color/black"
                android:textSize="14sp"
                android:text="现价:$2500000"
                android:gravity="center"
                android:textStyle="bold"
                android:ellipsize="end"
                android:maxLines="1"/>
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            >

            <TextView
                android:id="@+id/goodNowVisits"
                android:layout_width="0dp"
                android:layout_weight="0.55"
                android:layout_height="wrap_content"
                android:textColor="@android:color/black"
                android:textSize="12sp"
                android:text="目前关注人数:25000+人"
                android:gravity="start"
                android:textStyle="bold"
                android:ellipsize="end"
                android:maxLines="1"/>
            <TextView
                android:id="@+id/goodSoldNum"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:textColor="@android:color/black"
                android:textSize="12sp"
                android:text="已售:1300+件"
                android:gravity="center"
                android:layout_weight="0.45"
                android:textStyle="bold"
                android:ellipsize="end"
                android:maxLines="1"/>


        </LinearLayout>
    </LinearLayout>

</LinearLayout>

外框架tablayout和pageView2

public class GoodListFragment extends Fragment {

    private RecyclerView recyclerView;
    private GoodAdapter goodAdapter;
    private GoodViewModel goodViewModel;
    private List<Good> goodList;
    private List<GoodDetailInfo> goodDetailInfoList;
    private List<HeatData> heatDataList;
    private static List<Category> categoryListS;
    private Integer categoryId;

    // 使用 newInstance 方法来接收 categoryId
    public static GoodListFragment newInstance(Integer categoryId) {
        GoodListFragment fragment = new GoodListFragment();
        Bundle args = new Bundle();
        if(categoryId != null){
            args.putInt("categoryId", categoryId);
        }
       else {
           args.putInt("categoryId", -1);
        }
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(@androidx.annotation.Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        goodViewModel = new ViewModelProvider(this).get(GoodViewModel.class);
        goodViewModel.getGoodCategoryList();
        //监听数据变化
        goodViewModel.getCategoryListLiveData().observe(this, categoryList -> {
            categoryListS = categoryList;
        });
        // 初始化为一个空列表
        goodList = new ArrayList<>();
        goodDetailInfoList = new ArrayList<>();
        heatDataList = new ArrayList<>();

        // 获取 categoryId
        if (getArguments() != null) {
            categoryId = getArguments().getInt("categoryId");
        }
        if(categoryId == -1){
            categoryId = null;
        }

        // 调用接口获取数据
        goodViewModel.getGoodList(1, 20, categoryId, null, null, null, null);

        // 监听数据变化
        goodViewModel.getGoodListLiveData().observe(this, newGoodList -> {
            goodList = newGoodList;
            updateAdapter();
        });

        goodViewModel.getGoodDetailInfoListLiveData().observe(this, newGoodDetailInfoList -> {
            goodDetailInfoList = newGoodDetailInfoList;
            updateAdapter();
        });

        goodViewModel.getHeatDataListLiveData().observe(this, newHeatDataList -> {
            heatDataList = newHeatDataList;
            updateAdapter();
        });
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.page_layout, container, false);

        recyclerView = view.findViewById(R.id.recyclerView);
        recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));

        goodAdapter = new GoodAdapter(goodList, goodDetailInfoList, heatDataList);

        goodAdapter.setOnItemClickListener((item, position) -> {
            // 处理点击事件
//            Toast.makeText(getContext(),  String.valueOf(item.getId()), Toast.LENGTH_SHORT).show();
            // 这里可以跳转到详情页或执行其他操作
            if (getContext() != null) {
                Intent intent = new Intent(getContext(), GoodClickDetail.class);
                intent.putExtra("goodId", item.getId());
                AtomicReference<String> categoryName = new AtomicReference<>("");
                categoryListS.stream()
                        .skip(1) // 跳过第一个元素
                        .filter(category -> category.getId().equals(item.getCategoryId()))
                        .findFirst()
                        .ifPresent(category -> {
                            categoryName.set(category.getCategoryName());
                        });
                intent.putExtra("categoryName", categoryName.get());
                startActivity(intent);
            }
        });

        recyclerView.setAdapter(goodAdapter);

        return view;
    }
    private void updateAdapter() {
        if (goodAdapter != null) {
            goodAdapter.updateData(goodList, goodDetailInfoList, heatDataList);
            goodAdapter.notifyDataSetChanged();
        }
    }
}

这个就是简单的绑定了

3.搜索界面

屏幕截图 2025-05-28 084311.png

屏幕截图 2025-05-28 084339.png

这里主要对搜索历史进行操作,通过ROOM对sqlite进行操作,增删改查

屏幕截图 2025-05-28 103335.png

排序条件为

1.根据搜索次数排序

2.次数相同按时间倒序

可惜没完成后续界面没时间了

4.商品详情界面

这个界面我花费的时间挺长的,从构思到数据绑定,databinding也是在这个界面不断试错学会了

屏幕截图 2025-05-28 084234.png

这个界面上面是Toolbar,视频播放使用了StyledPlayerView,好像之前我用的的videoView貌似只能播放本地视频,还是现成组件好用,也做了按钮的点击变色事件(增删改查没做,道理相同的,重复无意义),下面是购买按钮、价格、简介和其他相关信息,我还做了评论显示,最多支持二级评论,不同级别评论以缩进不同距离显示,比较简陋,说实在,没达到我预期效果,要下方弹出抽屉,我不知道为什么还有间距...

屏幕截图 2025-05-28 084411.png

具体的用户名,点赞数,数据里有但我还没绑定...哭了工程量好大...,大概就这些了,这学期主要中间我跑去学react了,后面又强化uniCloud去了...要学的好多...

这次主要用到了MVVM架构,底层的ViewModel类负责处理与管理基本数据类和数据请求等,而Activity负责observice这些ViewModel的属性,当属性发生变化时,Activity会随之变化,这样即使翻转数据也可以有效保存,,databinding除了有时候奇怪的报错(对了,包名一定要小写不然databinding认不出...),其他的感觉没什么和java一样,请求是用retrofit(okhttp的封装加强版),以后试试kotlin好了(如果有时间)

gitee仓库代码链接:android_test_first_work: 第一次安卓项目尝试