ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Android MVVM框架搭建(五)Navigation + Fragment + BottomNavigationView

2021-11-23 20:02:02  阅读:257  来源: 互联网

标签:代码 String MVVM Fragment void private new BottomNavigationView public


Android MVVM框架搭建(五)Navigation + Fragment + BottomNavigationView

前言

  MVVM框架的模式在这几篇文章中相比你已经熟悉很多了,具体的架构模式如下图所示:
在这里插入图片描述
上层的Activity/Fragment表示为View层,通过ViewModel去操作数据,然后由Repository去控制数据的来源,可以是本地数据库也可以是网络数据。这个模式在文章和代码中都有体现,算是比较的完整了。

本文效果图如下:

在这里插入图片描述

正文

  MVVM框架的搭建按理来说就已经完成了,但是我们既然要弄一个实用的框架,就不能只停留于框架搭建的阶段,还要有实用的场景,我喜欢我的框架可以满足绝大部分开发中的使用。现在我们的框架虽然有了Activity,但是还没有使用过Fragment,通常Fragment是在什么时候使用呢?例如主页面五个子模块Fragment,分别表示五个功能,这样是不是会很好呢,这样就完美的将Fragment融入了进去,同时我们还可以与实际的开发模式相结合起来。嗯,不错,开始行动吧。

一、添加依赖

  使用Navigation需要添加依赖,在app的build.gradle中的dependencies{}闭包中添加如下依赖:

	// navigation依赖 ui 和 fragment
    implementation 'androidx.navigation:navigation-fragment:2.3.2'
    implementation 'androidx.navigation:navigation-ui:2.3.2'

然后Sync Now同步依赖项目。

二、Fragment创建

  创建Fragment可以通过快捷的方式,自带了ViewModel的,如下图所示:
在这里插入图片描述
  这里创建两个Fragment,NewsFragment和VideoFragment,对应的布局文件是news_fragment.xml和video_fragment.xml,ViewModel是NewsViewModel和VideoViewModel。

  下面对项目的包分一下,我把Activity、Fragment、Adapter都看为ui,那么我在com.llw.mvvm包下新建一个ui包,包下新建一个fragment包,然后将NewsFragment和VideoFragment放入fragment包,然后把adapter包也移到ui包下,同时在ui包下新建一个activity包,包下将项目中所有的Activity移入,最后将NewsViewModel和VideoViewModel放到viewmodels包下。目录结构如下图所示:
在这里插入图片描述
下面依次修改一下news_fragment.xml和video_fragment.xml中的内容:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
</layout>

两个xml里面的内容都是上面的这个代码,复制粘贴即可,这两个Fragment中的内容我们待会儿再写。

三、BaseActivity创建

  因为我们的Activity比较多,而可能有些Activity中的方法有重合的,或者通用的,这种情况下我们可以将一些方法放入一个基础类里面,例如BaseActivity中,下面进行创建,在activity包下新建一个BaseActivity类,代码如下:

/**
 * 基础Activity
 *
 * @author llw
 */
public class BaseActivity extends AppCompatActivity {

    protected AppCompatActivity context;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.context = this;
    }

    protected void showMsg(CharSequence msg) {
        Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
    }

    protected void showLongMsg(CharSequence msg) {
        Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
    }

    /**
     * 跳转页面
     * @param clazz 目标页面
     */
    protected void jumpActivity(final Class<?> clazz) {
        startActivity(new Intent(context, clazz));
    }

    /**
     * 跳转页面并关闭当前页面
     * @param clazz 目标页面
     */
    protected void jumpActivityFinish(final Class<?> clazz) {
        startActivity(new Intent(context, clazz));
        finish();
    }

    /**
     * 状态栏文字图标颜色
     * @param dark 深色 false 为浅色
     */
    protected void setStatusBar(boolean dark) {
        View decor = getWindow().getDecorView();
        if (dark) {
            decor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
        } else {
            decor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
        }
    }
}

里面也是一些简单的方法,后面在开发中有新的需要可以一直加进去,根据实际情况来,不要什么都加进去,其实没必要的。

四、启动页

  我们的这个MVVM-Demo虽然只是一个Demo,但是我们要给自己一个高一点的标准,所以我打算给一个启动页,一个简单的动画,然后进入我们的登录页,虽然我们是一个假登录,但是意思已经到位了。然后我们在登录页面上记录程序是否登录过,如果登录过下次进入程序就不再进入登录页面,而是直接进入主页面了,这样的逻辑很简单,下面来实现一下吧。

在activity包下新建一个SplashActivity,对应的布局是activity_splash.xml,xml代码如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <LinearLayout
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        tools:context=".ui.activity.SplashActivity">

        <RelativeLayout
            android:layout_width="160dp"
            android:layout_height="160dp">

            <ImageView
                android:layout_width="160dp"
                android:layout_height="160dp"
                android:src="@mipmap/ic_splash_logo" />

            <TextView
                android:visibility="invisible"
                android:id="@+id/tv_mvvm"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentBottom="true"
                android:layout_centerHorizontal="true"
                android:layout_marginBottom="46dp"
                android:text="MVVM"
                android:textColor="@color/white"
                android:textSize="28sp"
                android:textStyle="bold" />
        </RelativeLayout>

        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Model View ViewModel"
                android:textColor="@color/black"
                android:textSize="24sp" />

            <TextView
                android:id="@+id/tv_translate"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@color/white"
                android:text="Model View ViewModel"
                android:textColor="@color/white"
                android:textSize="24sp" />
        </RelativeLayout>
    </LinearLayout>
</layout>

这里面有一个图标ic_splash_logo.png,我这里贴一下,不过你最好到我的源码去找,这样不会有水印,而且图片格式也是对的。
在这里插入图片描述
针对于启动页我特别弄了一个主题样式,在themes.xml下增加如下代码样式:

	<style name="SplashTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <item name="android:statusBarColor" tools:targetApi="lollipop">#00FFFFFF</item><!--设置状态栏的颜色-->
    </style>

然后我们修改AndroidManifest.xml中的代码,因为之前的启动Activity是LoginActivity,需要改一下。如下图所示:
在这里插入图片描述
下面我们增加一个动画的帮助工具类,在utils包下新建一个EasyAnimation类,里面的代码如下:

public class EasyAnimation {

    /**
     * 开始眨眼动画
     *
     * @param view 需要设置动画的View
     */
    public static void startBlink(View view) {
        AlphaAnimation alphaAnimation = new AlphaAnimation(0.0f, 1.0f);
        alphaAnimation.setDuration(500);
        alphaAnimation.setStartOffset(20);
        alphaAnimation.setRepeatMode(Animation.REVERSE);
        alphaAnimation.setRepeatCount(Animation.INFINITE);
        view.startAnimation(alphaAnimation);
    }

    /**
     * 开始眨眼动画
     *
     * @param view           需要设置动画的View
     * @param alphaAnimation 透明度动画(自行配置)
     */
    public static void startBlink(View view, AlphaAnimation alphaAnimation) {
        view.startAnimation(alphaAnimation);
    }


    /**
     * 停止眨眼动画
     *
     * @param view 需要清除动画的View
     */
    public static void stopBlink(View view) {
        if (view != null) {
            view.clearAnimation();
        }
    }

    /**
     * 移动指定View的宽度
     *
     * @param view
     */
    public static void moveViewWidth(View view, TranslateCallback callback) {
        view.post(() -> {
            //通过post拿到的tvTranslate.getWidth()不会为0。
            TranslateAnimation translateAnimation = new TranslateAnimation(0, view.getWidth(), 0, 0);
            translateAnimation.setDuration(1000);
            translateAnimation.setFillAfter(true);
            view.startAnimation(translateAnimation);

            //动画监听
            translateAnimation.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {

                }

                @Override
                public void onAnimationEnd(Animation animation) {
                    //检查Android版本
                    callback.animationEnd();
                }

                @Override
                public void onAnimationRepeat(Animation animation) {

                }
            });
        });
    }

    /**
     * 移动指定View的宽度
     *
     * @param view               需要位移的View
     * @param callback           位移动画回调
     * @param translateAnimation 位移动画 (自行配置)
     */
    public static void moveViewWidth(View view, TranslateCallback callback, TranslateAnimation translateAnimation) {
        view.post(() -> {
            //通过post拿到的tvTranslate.getWidth()不会为0。

            view.startAnimation(translateAnimation);

            //动画监听
            translateAnimation.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {

                }

                @Override
                public void onAnimationEnd(Animation animation) {
                    //检查Android版本
                    callback.animationEnd();
                }

                @Override
                public void onAnimationRepeat(Animation animation) {

                }
            });
        });
    }

    public interface TranslateCallback {
        //动画结束
        void animationEnd();
    }
}

因为在启动页需要知道程序有没有登录,因此在Constant中增加一个常量,如下所示:

	/**
     * 是否登录过
     */
    public static final String IS_LOGIN = "isLogin";

下面我们修改一下SplashActivity的代码,使用这个常量来判断需要跳转到那个页面,代码如下:

public class SplashActivity extends BaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivitySplashBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_splash);
        setStatusBar(true);
        EasyAnimation.moveViewWidth(binding.tvTranslate, () -> {
            binding.tvMvvm.setVisibility(View.VISIBLE);
            jumpActivity(MVUtils.getBoolean(Constant.IS_LOGIN) ? MainActivity.class : LoginActivity.class);
        });
    }
}

这里我继承了BaseActivity,然后设置了状态栏深色模式,因为我们的页面是白色的,如果状态栏也是白色就看不出来了,后面就是在动画结束的时候跳转页面,很简单的代码。这个页面的代码就写完了,下面我们修改LoginActivity中的代码,首先是修改继承的Activity为BaseActivity。里面的代码如下:

public class LoginActivity extends BaseActivity {

    private ActivityLoginBinding dataBinding;
    private LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //数据绑定视图
        dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_login);
        loginViewModel = new LoginViewModel();
        //Model → View
        User user = new User("admin", "123456");
        loginViewModel.getUser().setValue(user);
        //获取观察对象
        MutableLiveData<User> user1 = loginViewModel.getUser();
        user1.observe(this, user2 -> dataBinding.setViewModel(loginViewModel));

        dataBinding.btnLogin.setOnClickListener(v -> {
            if (loginViewModel.user.getValue().getAccount().isEmpty()) {
                showMsg("请输入账号");
                return;
            }
            if (loginViewModel.user.getValue().getPwd().isEmpty()) {
                showMsg("请输入密码");
                return;
            }
            //记录已经登录过
            MVUtils.put(Constant.IS_LOGIN,true);
            showMsg("登录成功");
            jumpActivity(MainActivity.class);
        });
    }
}

这里就没啥好说的,就是使用了BaseActivity中的方法。同时我修改了一下布局中的代码,我将这两个TextView隐藏了

同时我们修改一下图片显示之前的占位图或者说是默认背景图。两个图片如下:

在这里插入图片描述
在这里插入图片描述
然后一个加载图片出错时显示的图片:
在这里插入图片描述

首先是MainActivity中,显示必应图片的位置,修改一下activity_main.xml
在这里插入图片描述
然后打开CustomImageView,增加如下代码:

	private static final RequestOptions OPTIONS = new RequestOptions()
            .placeholder(R.drawable.wallpaper_bg)//图片加载出来前,显示的图片
            .fallback(R.drawable.wallpaper_bg) //url为空的时候,显示的图片
            .error(R.mipmap.ic_loading_failed);//图片加载失败后,显示的图片

将这个值配置进去,如下图所示:
在这里插入图片描述
下面我们运行一下看是什么效果。
在这里插入图片描述
效果还可以的,下面进入主页面的代码编写。

五、主页面

  当到了每日壁纸页面时,我们需要再提供一个入口可以进入下一个页面,现在的每日壁纸页面不能算是真正意义上的主页面,因此我们写一个入口,可以在MainActivity中增加一个浮动按钮,页面上下滑动时控制按钮的显示和消失。下面在activity_main.xml中增加如下布局代码:

<com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab_home"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end|bottom"
            android:layout_margin="20dp"
            android:background="@color/purple_500"
            android:onClick="toHome"
            android:src="@mipmap/ic_home"
            app:backgroundTint="@color/purple_500"
            app:fabSize="auto"
            tools:ignore="UsingOnClickInXml"
            android:contentDescription="主页" />

添加的位置如下,这里的图标到我的源码里拿就好,白色的放出来也看不见。
在这里插入图片描述
下面回到MainActivity中,继承BaseActivity,在initView方法中增加如下代码:

		//页面上下滑动监听
        dataBinding.scrollView.setOnScrollChangeListener((NestedScrollView.OnScrollChangeListener) (v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
            if (scrollY > oldScrollY) {
                //上滑
                dataBinding.fabHome.hide();
            } else {
                //下滑
                dataBinding.fabHome.show();
            }
        });

然后也增加一个方法,当点击时跳转到HomeActivity,我们将在这个HomeActivity中显示Fragment,现在还没有,下面会创建的。

	public void toHome(View view) {
        jumpActivity(HomeActivity.class);
    }

代码添加位置如下图所示:
在这里插入图片描述
下面在activity包下创建一个HomeActivity,对应的布局是activity_home.xml,在改动之前我们先做好准备的工作。

六、Navigation使用

  在res包下新建一个navigation包,包下新建一个nav_graph.xml,里面的代码如下:

<?xml version="1.0" encoding="utf-8"?>
<navigation 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"
    android:id="@+id/nav_graph"
    app:startDestination="@id/news_fragment">

    <fragment
        android:id="@+id/news_fragment"
        android:name="com.llw.mvvm.ui.fragment.NewsFragment"
        android:label="news_fragment"
        tools:layout="@layout/news_fragment" />
    <fragment
        android:id="@+id/video_fragment"
        android:name="com.llw.mvvm.ui.fragment.VideoFragment"
        android:label="video_fragment"
        tools:layout="@layout/video_fragment" />
</navigation>

这里就是将Fragment配置到Navigation中,app:startDestination表示显示的第一个Fragment。那么这一步就完成了,下面是另一个操作,就是通过点击底部导航栏菜单去进行Fragment的切换。我们在res下新建一个menu包,包下新建一个navigation_menu.xml,里面的代码如下:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/news_fragment"
        android:icon="@mipmap/ic_hot_news"
        android:title="新闻" />
    <item
        android:id="@+id/video_fragment"
        android:icon="@mipmap/ic_hot_video"
        android:title="视频" />

</menu>

这里有两个图标,同样是白色的。
在这里插入图片描述
下面我们回到activity_home.xml中,里面的代码如下:

<?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">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.activity.HomeActivity">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@color/purple_500">

            <TextView
                android:id="@+id/tv_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:text="头条新闻"
                android:textColor="@color/white"
                android:textSize="18sp"
                android:textStyle="bold" />
        </com.google.android.material.appbar.MaterialToolbar>
        <!--NavHost-->
        <fragment
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_above="@+id/bottom_navigation"
            android:layout_below="@+id/toolbar"
            app:navGraph="@navigation/nav_graph" />

        <!--底部导航-->
        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/bottom_navigation"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:layout_alignParentBottom="true"
            android:background="#FFF"
            app:menu="@menu/navigation_menu" />

    </RelativeLayout>
</layout>

这里分为三个部分,一个是标题栏、一个是装载Fragment的容器,另一个是控制Fragment切换的。
下面我们进入到HomeActivity页面,修改代码如下:

public class HomeActivity extends BaseActivity {

    private ActivityHomeBinding binding;

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

        initView();
    }

    /**
     * 初始化
     */
    private void initView() {
        //获取navController
        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
        binding.bottomNavigation.setOnNavigationItemSelectedListener(item -> {
            switch (item.getItemId()) {
                case R.id.news_fragment:
                    binding.tvTitle.setText("头条新闻");
                    navController.navigate(R.id.news_fragment);
                    break;
                case R.id.video_fragment:
                    binding.tvTitle.setText("热门视频");
                    navController.navigate(R.id.video_fragment);
                    break;
                default:
            }
            return true;
        });
    }
}

下面在fragment包下创建一个BaseFragment,里面的代码如下:

public class BaseFragment extends Fragment {
    
    protected AppCompatActivity context;

    @Override
    public View onCreateView(@NonNull @NotNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    @Override
    public void onViewCreated(@NonNull @NotNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
    }

    @Override
    public void onAttach(@NonNull @NotNull Context context) {
        super.onAttach(context);
        if(context instanceof AppCompatActivity){
            this.context = (AppCompatActivity) context;
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        context = null;
    }

    protected void showMsg(String msg) {
        Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
    }


}

然后修改NewsFragment中的代码:

public class NewsFragment extends BaseFragment {

    private NewsFragmentBinding binding;

    public static NewsFragment newInstance() {
        return new NewsFragment();
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        binding = DataBindingUtil.inflate(inflater, R.layout.news_fragment, container, false);
        return binding.getRoot();
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        NewsViewModel mViewModel = new ViewModelProvider(this).get(NewsViewModel.class);
        
    }

}

再修改VideoFragment的代码:

public class VideoFragment extends BaseFragment {

    private VideoFragmentBinding binding;

    public static VideoFragment newInstance() {
        return new VideoFragment();
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        binding = DataBindingUtil.inflate(inflater, R.layout.video_fragment, container, false);
        return binding.getRoot();
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        VideoViewModel mViewModel = new ViewModelProvider(this).get(VideoViewModel.class);

    }

}

这里我们在点击底部导航栏时切换Fragment并且更改一下标题栏的文字,下面运行一下。
在这里插入图片描述
详细的使用说明可以看看这篇文章:Android Navigation + Fragment 制作APP主页面导航(步骤 + 源码),看完后你了解的也许会更多。

七、聚合数据请求

  这里我们使用聚合的API数据,聚合API,点击进入完成注册登录,然后可以申请数据API。
在这里插入图片描述

在这里插入图片描述
申请免费的API,每天有一百次请求,因此我们可以把数据请求一次之后保存到本地数据库中。

① NetworkApi

这两个API的接口是不同的地址,修改一下NetworkApi中的setUrlType方法。

	private static void setUrlType(int type) {
        switch (type) {
            case 0:
                //必应
                BASE_URL = "https://cn.bing.com";
                break;
            case 1:
                //热门壁纸
                BASE_URL = "http://service.picasso.adesk.com";
                break;
            case 2:
                //聚合API 1
                BASE_URL = "http://v.juhe.cn";
                break;
            case 3:
                //聚合API 2
                BASE_URL = "http://apis.juhe.cn";
                break;
            default:
                break;
        }
    }

这里两个接口分别是用于请求新闻数据和视频数据的。

② ApiService

  在ApiService中增加两个接口,代码如下所示:

	/**
     * 聚合新闻数据
     */
    @GET("/toutiao/index?type=&page=&page_size=&is_filter=&key=99d3951ed32af2930afd9b38293a08a2")
    Observable<NewsResponse> news();

    /**
     * 聚合热门视频数据
     */
    @GET("/fapig/douyin/billboard?type=hot_video&size=20&key=a9c49939cae34fc7dae570b1a4824be4")
    Observable<VideoResponse> video();

针对这个情况我们同样需要对数据库进行一次升级,这一次我们增加两个表。

③ 数据库升级

  首先在bean包下新建两个实体,News和Video。里面的内容都是我根据接口返回的数据制作的,News里的代码如下:

@Entity(tableName = "news")
public class News {
    @PrimaryKey(autoGenerate = true)
    private int uid;
    private String uniquekey;
    private String title;
    private String date;
    private String category;
    private String author_name;
    private String url;
    private String thumbnail_pic_s;
    private String is_content;

    public int getUid() {
        return uid;
    }

    public void setUid(int uid) {
        this.uid = uid;
    }

    public String getUniquekey() {
        return uniquekey;
    }

    public void setUniquekey(String uniquekey) {
        this.uniquekey = uniquekey;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    public String getCategory() {
        return category;
    }

    public void setCategory(String category) {
        this.category = category;
    }

    public String getAuthor_name() {
        return author_name;
    }

    public void setAuthor_name(String author_name) {
        this.author_name = author_name;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getThumbnail_pic_s() {
        return thumbnail_pic_s;
    }

    public void setThumbnail_pic_s(String thumbnail_pic_s) {
        this.thumbnail_pic_s = thumbnail_pic_s;
    }

    public String getIs_content() {
        return is_content;
    }

    public void setIs_content(String is_content) {
        this.is_content = is_content;
    }

    public News() {}

    @Ignore
    public News(String uniquekey, String title, String date, String category, String author_name, String url, String thumbnail_pic_s, String is_content) {
        this.uniquekey = uniquekey;
        this.title = title;
        this.date = date;
        this.category = category;
        this.author_name = author_name;
        this.url = url;
        this.thumbnail_pic_s = thumbnail_pic_s;
        this.is_content = is_content;
    }
}

Video的代码如下:

@Entity(tableName = "video")
public class Video {
    @PrimaryKey(autoGenerate = true)
    private int uid;
    private String title;
    private String share_url;
    private String author;
    private String item_cover;
    private String hot_words;

    public int getUid() {
        return uid;
    }

    public void setUid(int uid) {
        this.uid = uid;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getShare_url() {
        return share_url;
    }

    public void setShare_url(String share_url) {
        this.share_url = share_url;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getItem_cover() {
        return item_cover;
    }

    public void setItem_cover(String item_cover) {
        this.item_cover = item_cover;
    }

    public String getHot_words() {
        return hot_words;
    }

    public void setHot_words(String hot_words) {
        this.hot_words = hot_words;
    }

    @Ignore
    public Video(String title, String share_url, String author, String item_cover, String hot_words) {
        this.title = title;
        this.share_url = share_url;
        this.author = author;
        this.item_cover = item_cover;
        this.hot_words = hot_words;
    }

    public Video() {}
}

然后是Dao类,在dao包下新建一个NewsDao和VideoDao的接口,NewsDao代码如下:

@Dao
public interface NewsDao {

    @Query("SELECT * FROM news")
    Flowable<List<News>> getAll();

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    Completable insertAll(List<News> news);

    @Query("DELETE FROM news")
    Completable deleteAll();
}

VideoDao代码如下:

@Dao
public interface VideoDao {

    @Query("SELECT * FROM video")
    Flowable<List<Video>> getAll();

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    Completable insertAll(List<Video> videos);

    @Query("DELETE FROM video")
    Completable deleteAll();
}

最后我们进入AppDatabase中,对数据库进行升级迁移,在AppDatabase中新增如下代码:

	/**
     * 版本升级迁移到3 新增新闻表和视频表
     */
    static final Migration MIGRATION_2_3 = new Migration(2, 3) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            //创建新闻表
            database.execSQL("CREATE TABLE `news` " +
                    "(uid INTEGER NOT NULL, " +
                    "uniquekey TEXT, " +
                    "title TEXT, " +
                    "date TEXT," +
                    "category TEXT," +
                    "author_name TEXT," +
                    "url TEXT," +
                    "thumbnail_pic_s TEXT," +
                    "is_content TEXT," +
                    "PRIMARY KEY(`uid`))");
            //创建视频表
            database.execSQL("CREATE TABLE `video` " +
                    "(uid INTEGER NOT NULL, " +
                    "title TEXT," +
                    "share_url TEXT," +
                    "author TEXT," +
                    "item_cover TEXT," +
                    "hot_words TEXT," +
                    "PRIMARY KEY(`uid`))");
        }
    };

然后再增加两个抽象方法,就是之前的两个数据操作类,我们这样写了之后通过Room的编译时技术会对这两个抽象类中的接口进行一个实现,不需要我们去管它。

	public abstract NewsDao newsDao();

    public abstract VideoDao videoDao();

下面就是对数据库进行升级了,如下图所示:
在这里插入图片描述
注意我标注的地方,少一个都会出现升级不成功,或者你直接都编译不成功或者程序运行闪退的情况。

现在我们的数据库有了,接下来要做的就是数据的或者和保存了。

④ 数据存储库

  下面就是Repository了,我们在repository包下新建NewsRspository和VideoRepository两个类,然后为了方便管理数据的请求方式,我们同样需要在Constant中增加几个常量来保存当天是否有请求网络接口数据,在Constant中增加如下代码:

	/**
     * 今日是否请求了聚合新闻数据
     */
    public static final String IS_TODAY_REQUEST_NEWS = "isTodayRequestNews";

    /**
     * 今日请求聚合新闻数据的时间戳
     */
    public static final String REQUEST_TIMESTAMP_NEWS = "newsRequestTimestamp";

    /**
     * 今日是否请求了聚合视频数据
     */
    public static final String IS_TODAY_REQUEST_VIDEO = "isTodayRequestVideo";

    /**
     * 今日请求聚合视频数据的时间戳
     */
    public static final String REQUEST_TIMESTAMP_VIDEO = "videoRequestTimestamp";

然后我们再来编辑NewsRepository的代码:

@SuppressLint("CheckResult")
public class NewsRepository {

    private static final String TAG = NewsRepository.class.getSimpleName();
    final MutableLiveData<NewsResponse> news = new MutableLiveData<>();

    public final MutableLiveData<String> failed = new MutableLiveData<>();

    /**
     * 获取新闻数据
     * @return news
     */
    public MutableLiveData<NewsResponse> getNews() {
        //今日此接口是否已经请求
        if (MVUtils.getBoolean(Constant.IS_TODAY_REQUEST_NEWS)) {
            if (DateUtil.getTimestamp() <= MVUtils.getLong(Constant.REQUEST_TIMESTAMP_NEWS)) {
                getNewsForLocalDB();
            } else {
                getNewsForNetwork();
            }
        } else {
            getNewsForNetwork();
        }
        return news;
    }

    /**
     * 从本地数据库获取新闻
     */
    private void getNewsForLocalDB() {
        Log.d(TAG, "getNewsForLocalDB: 从本地数据库获取 新闻数据");
        NewsResponse newsResponse = new NewsResponse();
        NewsResponse.ResultBean resultBean = new NewsResponse.ResultBean();

        List<NewsResponse.ResultBean.DataBean> dataBeanList = new ArrayList<>();
        Flowable<List<News>> listFlowable = BaseApplication.getDb().newsDao().getAll();
        CustomDisposable.addDisposable(listFlowable, newss -> {
            for (News news1 : newss) {
                NewsResponse.ResultBean.DataBean dataBean = new NewsResponse.ResultBean.DataBean();
                dataBean.setUniquekey(news1.getUniquekey());
                dataBean.setTitle(news1.getTitle());
                dataBean.setDate(news1.getDate());
                dataBean.setAuthor_name(news1.getAuthor_name());
                dataBean.setCategory(news1.getCategory());
                dataBean.setThumbnail_pic_s(news1.getThumbnail_pic_s());
                dataBean.setIs_content(news1.getIs_content());
                dataBeanList.add(dataBean);
            }
            resultBean.setData(dataBeanList);
            newsResponse.setResult(resultBean);
            news.postValue(newsResponse);
        });
    }

    /**
     * 从网络获取壁纸数据
     */
    private void getNewsForNetwork() {
        Log.d(TAG, "getNewsForNetwork: 从网络获取 热门壁纸");
        NetworkApi.createService(ApiService.class, 2).
                news().compose(NetworkApi.applySchedulers(new BaseObserver<NewsResponse>() {
            @Override
            public void onSuccess(NewsResponse newsResponse) {
                if (newsResponse.getError_code() == 0) {
                    //保存本地数据
                    saveNews(newsResponse);
                    news.setValue(newsResponse);
                } else {
                    failed.postValue(newsResponse.getReason());
                }
            }

            @Override
            public void onFailure(Throwable e) {
                failed.postValue("News Error: " + e.toString());
            }
        }));
    }

    /**
     * 保存热门壁纸数据
     */
    private void saveNews(NewsResponse newsResponse) {
        MVUtils.put(Constant.IS_TODAY_REQUEST_NEWS, true);
        MVUtils.put(Constant.REQUEST_TIMESTAMP_NEWS, DateUtil.getMillisNextEarlyMorning());

        Completable deleteAll = BaseApplication.getDb().newsDao().deleteAll();
        CustomDisposable.addDisposable(deleteAll, () -> {
            Log.d(TAG, "saveNews: 删除数据成功");
            List<News> newsList = new ArrayList<>();
            for (NewsResponse.ResultBean.DataBean dataBean : newsResponse.getResult().getData()) {
                newsList.add(new News(dataBean.getUniquekey(),dataBean.getTitle(),dataBean.getDate(),dataBean.getCategory(),
                        dataBean.getAuthor_name(),dataBean.getUrl(),dataBean.getThumbnail_pic_s(),dataBean.getIs_content()));
            }
            //保存到数据库
            Completable insertAll = BaseApplication.getDb().newsDao().insertAll(newsList);
            Log.d(TAG, "saveNews: 插入数据:" + newsList.size() + "条");
            //RxJava处理Room数据存储
            CustomDisposable.addDisposable(insertAll, () -> Log.d(TAG, "saveNews: 新闻数据保存成功"));
        });
    }
}

  这里的代码和之前WallPaperRepository中的代码神似,逻辑上基本一致,只不过是不同的接口和不同的数据表,同事我在当前的这个Repository中增加了一个异常信息的LiveData,因为请求接口你可能会需要很多情况,最理想的时能获取到数据,但是也有其他情况,例如接口地址错误访问不到、请求返回的数据为空,请求次数达到上限等一些异常。因此我们有必要做一个异常信息的处理,然后传递到ViewModel中,最终在Activity中对这个异常进行观察,及时通知到页面上。不然我光打印日志,用户是看不到的。

VideoRepository的代码如下:

@SuppressLint("CheckResult")
public class VideoRepository {

    public static final String TAG = VideoRepository.class.getSimpleName();

    final MutableLiveData<VideoResponse> video = new MutableLiveData<>();

    public final MutableLiveData<String> failed = new MutableLiveData<>();

    /**
     * 获取视频数据
     * @return video
     */
    public MutableLiveData<VideoResponse> getVideo() {
        //今日此接口是否已经请求
        if (MVUtils.getBoolean(Constant.IS_TODAY_REQUEST_VIDEO)) {
            if (DateUtil.getTimestamp() <= MVUtils.getLong(Constant.REQUEST_TIMESTAMP_VIDEO)) {
                getVideoForLocalDB();
            } else {
                getVideoForNetwork();
            }
        } else {
            getVideoForNetwork();
        }
        return video;
    }

    /**
     * 从本地数据库获取新闻
     */
    private void getVideoForLocalDB() {
        Log.d(TAG, "getVideoForLocalDB: 从本地数据库获取 视频数据");
        VideoResponse videoResponse = new VideoResponse();

        List<VideoResponse.ResultBean> dataBeanList = new ArrayList<>();
        Flowable<List<Video>> listFlowable = BaseApplication.getDb().videoDao().getAll();
        CustomDisposable.addDisposable(listFlowable, videos -> {
            for (Video video : videos) {
                VideoResponse.ResultBean resultBean = new VideoResponse.ResultBean();
                resultBean.setTitle(video.getTitle());
                resultBean.setShare_url(video.getShare_url());
                resultBean.setAuthor(video.getAuthor());
                resultBean.setHot_words(video.getHot_words());
                resultBean.setItem_cover(video.getItem_cover());
                dataBeanList.add(resultBean);
            }
            videoResponse.setResult(dataBeanList);
            video.postValue(videoResponse);
        });
    }

    /**
     * 从网络获取壁纸数据
     */
    private void getVideoForNetwork() {
        Log.d(TAG, "getVideoForNetwork: 从网络获取 热门壁纸");
        NetworkApi.createService(ApiService.class, 3)
                .video().compose(NetworkApi.applySchedulers(new BaseObserver<VideoResponse>() {
            @Override
            public void onSuccess(VideoResponse videoResponse) {
                if (videoResponse.getError_code() == 0) {
                    //保存本地数据
                    saveVideo(videoResponse);
                    video.postValue(videoResponse);
                } else {
                    failed.postValue(videoResponse.getReason());
                }
            }

            @Override
            public void onFailure(Throwable e) {
                failed.postValue("Video Error: " + e.toString());
            }
        }));
    }

    /**
     * 保存热门壁纸数据
     */
    private void saveVideo(VideoResponse videoResponse) {
        MVUtils.put(Constant.IS_TODAY_REQUEST_VIDEO, true);
        MVUtils.put(Constant.REQUEST_TIMESTAMP_VIDEO, DateUtil.getMillisNextEarlyMorning());

        Completable deleteAll = BaseApplication.getDb().videoDao().deleteAll();
        CustomDisposable.addDisposable(deleteAll, () -> {
            Log.d(TAG, "saveVideo: 删除数据成功");
            List<Video> videoList = new ArrayList<>();
            for (VideoResponse.ResultBean resultBean : videoResponse.getResult()) {
                videoList.add(new Video(resultBean.getTitle(),resultBean.getShare_url(),resultBean.getAuthor(),
                        resultBean.getItem_cover(), resultBean.getHot_words()));
            }
            //保存到数据库
            Completable insertAll = BaseApplication.getDb().videoDao().insertAll(videoList);
            Log.d(TAG, "saveVideo: 插入数据:" + videoList.size() + "条");
            //RxJava处理Room数据存储
            CustomDisposable.addDisposable(insertAll, () -> Log.d(TAG, "saveVideo: 视频数据保存成功"));
        });
    }
}

这里面的代码也是类似的。如果我们每一个ViewModel中都要有一个failed,那么我们可以定义一个基础ViewModel,然后所有的ViewModel去继承它,这样就会更好一些。

⑤ BaseViewModel

  在viewmodels包下新建一个BaseViewModel,里面的代码如下:

public class BaseViewModel extends ViewModel {

    public LiveData<String> failed;
}

哦豁,就这么点代码吗?是的,目前就这些,可以根据实际的需求后面再进行添加,不着急。下面我们修改一下NewsViewModel中的代码,如下所示:

public class NewsViewModel extends BaseViewModel {

    public LiveData<NewsResponse> news;

    public void getNews() {
        NewsRepository newsRepository = new NewsRepository();
        failed = newsRepository.failed;
        news = newsRepository.getNews();
    }
}

然后再修改一下VideoViewModel的代码:

public class VideoViewModel extends BaseViewModel {

    public LiveData<VideoResponse> video;

    public void getVideo() {
        VideoRepository videoRepository = new VideoRepository();
        failed = videoRepository.failed;
        video = videoRepository.getVideo();
    }
}

另外其他的ViewModel也这样修改一下,我就不重复贴代码了,不了解的看源码对着改一下就好。

八、新闻、视频数据显示

  前面做了这么多都是做准备工作,最重要的是要显示数据在Fragment上,下面我们写两个适配器,还有两个xml文件。
下面先创建xml文件,在layout下新建item_news.xml文件,里面的代码如下:

① item布局

<?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">

    <data>

        <variable
            name="news"
            type="com.llw.mvvm.model.NewsResponse.ResultBean.DataBean" />
    </data>

    <RelativeLayout
        android:background="@color/white"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingStart="12dp"
        android:paddingTop="12dp"
        android:paddingEnd="12dp">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentStart="true"
            android:layout_toStartOf="@id/image"
            android:text="@{news.title}"
            android:textColor="@color/black"
            android:textSize="14sp" />

        <com.llw.mvvm.view.CustomImageView
            android:id="@+id/image"
            android:layout_marginStart="12dp"
            networkUrl="@{news.thumbnail_pic_s}"
            android:layout_width="140dp"
            android:layout_height="80dp"
            android:layout_alignParentEnd="true"
            android:scaleType="centerCrop"
            app:shapeAppearanceOverlay="@style/roundedImageStyle_6" />

        <TextView
            android:id="@+id/tv_author"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_above="@+id/tv_date"
            android:layout_below="@+id/tv_title"
            android:layout_marginTop="4dp"
            android:text="@{news.author_name}"
            android:textSize="12sp"
            tools:ignore="NestedWeights" />

        <TextView
            android:id="@+id/tv_date"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignBottom="@id/image"
            android:text="@{news.date}"
            android:textSize="12sp" />

        <View
            android:layout_width="match_parent"
            android:layout_height="0.5dp"
            android:layout_below="@id/image"
            android:layout_marginTop="12dp"
            android:background="@color/line" />
    </RelativeLayout>

</layout>

这里用的颜色值line,是#EEEEEE,自行在colors.xml中添加就好了。

然后在layout下创建一个item_video.xml文件,里面的代码如下:

<?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">

    <data>

        <variable
            name="video"
            type="com.llw.mvvm.model.VideoResponse.ResultBean" />
    </data>

    <RelativeLayout
        android:background="@color/white"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingStart="12dp"
        android:paddingTop="12dp"
        android:paddingEnd="12dp">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentEnd="true"
            android:layout_toEndOf="@id/image"
            android:ellipsize="end"
            android:maxLines="2"
            android:text="@{video.title}"
            android:textColor="@color/black"
            android:textSize="14sp" />

        <com.llw.mvvm.view.CustomImageView
            android:id="@+id/image"
            networkUrl="@{video.item_cover}"
            android:layout_width="140dp"
            android:layout_height="80dp"
            android:layout_marginEnd="12dp"
            android:scaleType="centerCrop"
            app:shapeAppearanceOverlay="@style/roundedImageStyle_6" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignStart="@+id/image"
            android:layout_alignTop="@+id/image"
            android:layout_alignEnd="@id/image"
            android:layout_alignBottom="@+id/image"
            android:padding="20dp"
            android:src="@mipmap/ic_play" />

        <TextView
            android:id="@+id/tv_author"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_above="@+id/tv_words"
            android:layout_below="@+id/tv_title"
            android:layout_alignParentEnd="true"
            android:layout_marginTop="4dp"
            android:layout_toEndOf="@id/image"
            android:text="@{video.author}"
            android:textSize="12sp"
            tools:ignore="NestedWeights" />

        <TextView
            android:id="@+id/tv_words"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignBottom="@id/image"
            android:layout_alignParentEnd="true"
            android:layout_toEndOf="@id/image"
            android:ellipsize="end"
            android:maxLines="1"
            android:text="@{video.hot_words}"
            android:textSize="12sp" />

        <View
            android:layout_width="match_parent"
            android:layout_height="0.5dp"
            android:layout_below="@id/image"
            android:layout_marginTop="12dp"
            android:background="@color/line" />
    </RelativeLayout>

</layout>

② 适配器

下面写适配器,在adapter包下新建一个NewsAdapter类,里面的代码如下:

public class NewsAdapter extends BaseQuickAdapter<NewsResponse.ResultBean.DataBean, BaseDataBindingHolder<ItemNewsBinding>> {

    public NewsAdapter(@Nullable List<NewsResponse.ResultBean.DataBean> data) {
        super(R.layout.item_news, data);
    }

    @Override
    protected void convert(@NotNull BaseDataBindingHolder<ItemNewsBinding> bindingHolder, NewsResponse.ResultBean.DataBean dataBean) {
        ItemNewsBinding binding = bindingHolder.getDataBinding();
        if (binding != null) {
            binding.setNews(dataBean);
            binding.executePendingBindings();
        }
    }
}

常规代码了,相信你能看懂的,下面再新建一个VideoAdapter类,里面的代码如下:

public class VideoAdapter extends BaseQuickAdapter<VideoResponse.ResultBean, BaseDataBindingHolder<ItemVideoBinding>> {

    public VideoAdapter(@Nullable List<VideoResponse.ResultBean> data) {
        super(R.layout.item_video, data);
    }

    @Override
    protected void convert(@NotNull BaseDataBindingHolder<ItemVideoBinding> bindingHolder, VideoResponse.ResultBean dataBean) {
        ItemVideoBinding binding = bindingHolder.getDataBinding();
        if (binding != null) {
            binding.setVideo(dataBean);
            binding.executePendingBindings();
        }
    }
}

适配器写好了,下面进入到Fragment中去显示数据。首先是NewsFragment,修改代码如下:

	@Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        NewsViewModel mViewModel = new ViewModelProvider(this).get(NewsViewModel.class);
        //获取新闻数据
        mViewModel.getNews();
        binding.rv.setLayoutManager(new LinearLayoutManager(context));
        //数据刷新
        mViewModel.news.observe(context, newsResponse ->
                binding.rv.setAdapter(new NewsAdapter(newsResponse.getResult().getData())));
        mViewModel.failed.observe(context, this::showMsg);
    }

然后是VideoFragment,修改代码如下:

	@Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        VideoViewModel mViewModel = new ViewModelProvider(this).get(VideoViewModel.class);

        //获取视频数据
        mViewModel.getVideo();
        binding.rv.setLayoutManager(new LinearLayoutManager(context));
        //数据刷新
        mViewModel.video.observe(context, videoResponse ->
                binding.rv.setAdapter(new VideoAdapter(videoResponse.getResult())));
        mViewModel.failed.observe(context, this::showMsg);
    }

下面我们运行一下:
在这里插入图片描述

九、源码

欢迎Star和Fork

GitHub:MVVM-Demo
CSDN:MVVMDemo_5.rar

标签:代码,String,MVVM,Fragment,void,private,new,BottomNavigationView,public
来源: https://blog.csdn.net/qq_38436214/article/details/121415819

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有