admin 管理员组

文章数量: 887021

Android第一行代码学习思考笔记(碎片、广播、持久化技术和Android数据库

  • 第四章 手机平板要兼顾——探究碎片
    • 4.1碎片是什么(Fragment)
    • 4.2碎片的使用方式
      • 4.2.1碎片的简单用法
      • 4.2.2动态添加碎片
      • 4.2.3在碎片中模拟返回栈
      • 4.2.4碎片和活动之间进行通信
    • **4.3 碎片的生命周期**
      • 4.3.1碎片的状态和回调
      • 4.3.2体验碎片的生命周期
    • **4.4动态加载布局的技巧**
      • 4.4.1使用限定符
      • 4.4.2使用最小宽度限定符
    • 4.5Fragment的最佳实践——一个简易版的新闻应用
    • 4.6小结与点评
  • 第五章全局大喇叭——详解广播机制
    • 5.1广播机制简介
    • 5.2接收系统广播
      • 5.2.1动态注册监听网络变化
      • 5.2.2静态注册实现开机启动
    • 5.3发送自定义广播
      • 5.3.1发送标准广播
      • 5.3.2发送有序广播
    • 5.4使用本地广播
    • 5.5广播的最佳实践——实现强制下线功能
    • 5.7小结与点评
  • 第六章数据存储全方案——详解持久化技术
    • 6.1持久化技术简介
    • 6.2文件存储
      • 6.2.1将数据存储到文件中
      • 6.2.2从文件中读取数据
    • 6.3SharedPreferences存储
      • 6.3.1将数据存储到SharedPreferences中
      • 6.3.2从SharedPreferences中读取数据
      • 6.3.3实现记住密码功能
    • 6.4SQLite数据库存储
      • 6.4.1创建数据库
      • 6.4.2升级数据库
      • 6.4.3添加数据
      • 已解决
      • 6.4.4更新数据
      • 6.4.5删除数据
      • 6.4.6查询数据
      • 6.4.7使用SQL操作数据库
    • 6.5使用LitePal操作数据库
      • 6.5.1LitePal简介
      • 6.5.2配置LitePal
      • 6.5.3创建和升级数据库
      • 6.5.4使用LitePal添加数据
      • 6.5.5使用LitePal更新数据
      • 6.5.6使用LitePal删除数据
      • 6.5.7使用LitePal查询数据
    • 6.6小结与点评

第四章 手机平板要兼顾——探究碎片

碎片可以让界面在平板上不出现控件被过分拉长、元素之间空隙过大等情况。

4.1碎片是什么(Fragment)

Fragment是一种可以嵌入在活动当中的UI片段,让程序更合理和充分地利用大屏幕的空间。同样包含布局,有自己的生命周期。

将新闻标题列表界面和新闻详细内容界面分别放在两个碎片中。

4.2碎片的使用方式

碎片通常都是在平板开发中使用的。

4.2.1碎片的简单用法

新建左侧碎片布局left_fragment.xml
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
				android:layout_gravity="center_horizontal"//水平居中d额按钮
				android:text="Button"
/>
</LinearLayout>

新建右侧碎片布局right_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    android:orientation="vertical"
		//颜色
    android:background="#00ff00"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="20sp"
        android:text="This is right fragment"/>
</LinearLayout>

activity_main中的代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:app="http://schemas.android/apk/res-auto"
    xmlns:tools="http://schemas.android/tools"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/left_fragment"
//显式指明要添加的碎片类名
        android:name="com.example.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
				//占比
        android:layout_weight="1"/>
    <fragment
        android:id="@+id/right_fragment"
//显式指明要添加的碎片类名
        android:name="com.example.fragmenttest.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"/>

</LinearLayout>
新建LeftFragment类继承自Fragment类
public class LeftFragment extends Fragment {

		//重写Fragment的onCreateView()方法
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        //通过LayoutInflater的inflate方法将**left_fragment布局动态加载进来**。
        View view = inflater.inflate(R.layout.left_fragment, container, false);
        return view;
    }
}

public class RightFragment extends Fragment {
//重写Fragment的onCreateView()方法
    public View onCreateView(LayoutInflater inflater,ViewGroup container, Bundle savedInstanceState) {
       //通过LayoutInflater的inflate方法将**left_fragment布局动态加载进来**。
			  View view = inflater.inflate(R.layout.right_fragment, container, false);
        return view;
    }

两个碎片平分了整个活动的布局。

4.2.2动态添加碎片

碎片可以在程序运行时动态地添加到活动当中,将程序界面制定得更加多样化。

新建another_right_fragment.xml与right_fragment文字和颜色不同
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    android:orientation="vertical"
    android:background="#ffff00"//黄色
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:layout_gravity="center_horizontal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        android:text="This is another right fragment"/>
</LinearLayout>
//新建AnotherRightFragment作为另一个右侧碎片
public class AnotherRightFragment extends Fragment {
    public View onCreateView( LayoutInflater inflater,  ViewGroup container, Bundle savedInstanceState) {
        //加载刚刚创建的another_right_fragment布局
				View view = inflater.inflate(R.layout.another_right_fragment, container, false);//加载了another_right_fragment布局
        return view;
    }
}

修改activity_main
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:app="http://schemas.android/apk/res-auto"
    xmlns:tools="http://schemas.android/tools"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/left_fragment"
        android:name="com.example.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"/>

**这里仅需要在布局里放入一个碎片,不需要任何定位,因此非常适合使用FrameLayout**
//将右侧碎片替换成FrameLayout,帧布局所有的默认控件摆在布局的左上角
    <FrameLayout
        **android:id="@+id/right_layout"**
        android:layout_height="match_parent"
        android:layout_width="0dp"
        android:layout_weight="1"
        />
</LinearLayout>

public class MainActivity extends AppCompatActivity **implements View.OnClickListener**{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        **Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(this);        //首先给左侧按钮注册点击事件
//动态给FrameLayout添加RightFragment碎片
        replaceFragment(new RightFragment());**
    }

    **@Override
    public void onClick(View view) {
        switch (view.getId()){//点击按钮
            case R.id.button:
                replaceFragment(new AnotherRightFragment());//点击按钮将右侧碎片替换成AnotherRightFragment
                break;
            default:
                break;
        }
    }

    private void replaceFragment(Fragment fragment){
        FragmentManager fragmentManager = getSupportFragmentManager();
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        //将右侧碎片替换成AnotherRightFragment
        transaction.replace(R.id.right_layout,fragment);

        transaction.commit();
    }**
}

结合replaFragment()方法。动态添加碎片主要分为5步。

1.创建待添加的碎片实例。
2.获取FragmentManager,在活动中可以直接通过调用getSupportFragmentManager()方法得到。
3.开启一个事务 FragmentTransaction,通过调用FragmentManager的beginTransaction()方法开启。
4.向容器内添加或替换碎片,一般使用replace()方法实现,需要传入容器的id和待添加的碎片实例。
5.提交事务,调用FragmentTransaction的commit()方法来完成。

4.2.3在碎片中模拟返回栈

想要实现按下Back键返回上一个碎片而不是直接退出。FragmentTramsaction中提供了一个addToBackStack()方法可以将一个事务添加到返回栈

...
private void replaceFragment(Fragment fragment){
    FragmentManager fragmentManager = getSupportFragmentManager();
    FragmentTransaction transaction = fragmentManager.beginTransaction();
    //将右侧碎片替换成AnotherRightFragment
    transaction.replace(R.id.right_layout,fragment);
    **transaction.addToBackStack(null);**
    transaction.commit();//提交事务
}

在事务提交前调用了FragmentTransaction的addToBackStack()方法,addToBackStack()接收一个名字用于描述返回栈状态,一般传入null,重新运行程序并点击按钮将AnotherRightFragment添加到活动中,按下Bakc键活动回到了RightFragment界面,继续按Back键RightFragment界面消失,再按Back键才会退出。

4.2.4碎片和活动之间进行通信

虽然碎片嵌入在活动中显示,可它们实际上关系没有很紧密。碎片和活动各自存在于独立的类中,没有明显通信方式。如果想在活动中调用碎片里的方法,或在碎片中调用活动里的方法。为方便碎片和活动之间通信,FragmentManager提供了类似于findViewById()的方法,专门用于从布局文件中获取碎片实例,然后调用碎片的方法。

RightFragment rightFragment = (RightFragment)getSupportFragmentManager().**findFragmentByid**(R.id.right_fragment);

每个碎片可调用getActivity()方法来得到和当前碎片相关联的活动实例。

MainActivity activity = (ManActivity)getActivity();

在碎片中需要使用Context对象时,也可以使用getActivity()方法,因为活动本身就是一个Context对象。

碎片与碎片之间通信:一个碎片通过getActivity()方法得到与它相关联的Activity,再通过这个Activity去getSupportFragmentManager().findFragmentByid()获取一个碎片的实例,就实现了不同碎片之间的通信功能。

4.3 碎片的生命周期

4.3.1碎片的状态和回调

每个活动在生命周期内可以有四种状态:运行状态、暂停状态、停止状态和销毁状态。每个碎片也一样。

1.运行状态

当一个Fragment所关联的Activity正处于运行状态时,该Fragment也处于运行状态。

2.暂停状态

当一个Activity进入暂停状态(另一个未占满屏幕的Acitivty添加到了栈顶),与暂停状态Acitivty相关的可见Fragment就进入到暂停状态。

3.停止状态

当一个Activit进入到停止状态,与进入停止状态Activity相关联的Fragment也进入到停止状态,或者通过调用FragmentTransaction的remove()、replace()方法将碎片从活动中移除,但如果在事务提交之前调用了addToBackStack()方法,这时的Fragment也会进入到停止状态。总的来说,停止状态的Fragment对用户来说是完全不可见的,有可能会被系统回收。

4.销毁状态

Fragment总依附于Activity而存在,因此当Activity被销毁与之相关联的Fragment也进入到销毁状态。或者通过调用FragmentTransaction的remove()、replace()方法将碎片从活动中移除,但在事物提交之前并没有调用addToBackStack()方法,这时的碎片也会进入到销毁状态。

Fragment类中也提供了一系列的回调方法,以覆盖碎Fragment生命周期的每个环节。其中,Activity中有的回调方法,Fragment中几乎都有,不过碎片中还提供了一些附加的回调方法:

  • onAttach()。当Fragment和Activity建立关联的时候调用。
  • onCreateView()。为Fragment创建视图(加载布局)时调用。
  • onActivityCreated()。确保与Fragment相关联的Activity已经创建完毕的时候调用。
  • onDestoryView()。当与Fragment关联的视图被移除的时候调用。
  • onDetach()。当Fragment和Activity解除关联时调用。

4.3.2体验碎片的生命周期

在FragmentTest项目基础上改动
public class RightFragment extends Fragment {
    **public static final String TAG = "RightFragment";**

    **@Override
    public void onAttach( Context context) {
        super.onAttach(context);
        Log.d(TAG,"onAttach");
    }**

    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        **Log.d(TAG,"onCreateView");**
        View view = inflater.inflate(R.layout.right_fragment, container, false);
        return view;
    }

    **@Override
    public void onActivityCreated( Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.d(TAG,"onActivityCreated");
    }

    @Override
    public void onStart() {
        super.onStart();
        Log.d(TAG,"onStart");
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.d(TAG,"onResume");
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.d(TAG,"onResume");
    }

    @Override
    public void onStop() {
        super.onStop();
        Log.d(TAG,"onStop");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG,"onDestoryView");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.d(TAG,"onDestory");
    }

    @Override
    public void onDetach() {
        super.onDetach();
        Log.d(TAG,"onDetach");
    }**
}

当RightFragment第一次被加载到屏幕上时,会依次执行onAttach()、onCreate()、onCreateView()、onActivityCreated()、onStart()和onResume()方法。

由于AnotherRightFragment替换了RightFragment,此时的RightFragment进入了停止状态,因此onPause()、onStop()和onDestoryView()方法会得到执行。如果在替换的时候没有调用addToBackStack()方法,此时的RightFragment就会进入销毁状态,onDestroy()和onDetach()方法就会得到执行。

按下Back键RightFragment重新回到运行状态回到屏幕,onCreateView()、onActivityCreated()、onStart()和onResume()方法会得到执行。注意此时onCreate()方法并不会执行,因为我们借助了addToBackStack()方法使RightFragment并没有被销毁。

再次按下Back键,依次执行onPause()、onStop()、onDestoryView()、onDestory()和onDetach()方法,最终将碎片销毁掉。

另外值得一提的是,在Fragment中也可以通过onSaveInstanceState()(保存实例状态)方法来保存数据,因为进入停止状态的Fragment有可能在系统内存不足时被回收。保存下来的数据在onCreate()、onCreateView()和onActivityCreated()3个方法中都可以重新得到,都含有一个Bundle类型的savedInstanceState参数。可以参考2.4.5小节。

4.4动态加载布局的技巧

动态添加Fragment只是在一个布局文件中进行一些添加和替换操作。如果程序能根据设备分辨率或屏幕大小在运行时来决定加载哪个布局,那么发挥空间就更多了。

4.4.1使用限定符

很多平板应用采用双页模式(程序会在左侧的面板上显示一个包含子项的列表,在右侧的面板上显示内容),因为平板屏幕足够大,但手机屏幕一次只能显示一夜内容。需要借助限定符(qualifier)来实现判断程序该使用双页模式还是单页模式。Fragmenttest修改activity_main.xml文件。

<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:app="http://schemas.android/apk/res-auto"
    xmlns:tools="http://schemas.android/tools"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

只留下一个左侧Fragment充满整个父布局。

    <fragment
        android:id="@+id/left_fragment"
        android:name="com.example.fragmenttest.LeftFragment"
        **android:layout_width="match_parent"**
        android:layout_height="match_parent"/>

</LinearLayout>

res下新建layout_large新建布局也叫activity_main
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment
        android:id="@+id/left_fragment"

        android:transitionName="example_LeftFragment"
        android:layout_weight="1"
        android:layout_height="match_parent"
        android:layout_width="0dp" />
    <fragment
        android:id="@+id/right_fragment"
        android:transitionName="example_RightFragment"
        android:layout_weight="1"
        android:layout_height="match_parent"
        android:layout_width="0dp" />
</LinearLayout>

res下新建layout_large新建布局也叫activity_main,layout下的activity_main只包含一个碎片,即单页模式,layout_large/activity_main却包含两个碎片,即双页模式。large是一个限定符,被认为是large的设备就会自动加载layout_large/activity_main。然后将ManActivity中replaceFragment()方法里的代码注释掉,并在平板运行(方法中有R.id.right_layout)

Android常见限定符

4.4.2使用最小宽度限定符

上一小节中使用large限定符成功解决了单双页判断问题。有时我们不知道large到底是多大?所以使用最小宽度限定符(Smallest-width Qualifier)更灵活地为不同设备加载布局,不管它们是不是被系统认定为large,

最小宽度限定符允许我们对屏幕的宽度指定一个最小值(以dp为单位),然后以这个最小值为临界点,屏幕宽度大于这个值的设备就加载一个布局,屏幕宽度小于这个值的设备加载另一个布局。

在res目录下创建layout-**sw600dp**文件夹,然后在这个文件夹下新建activity_main.xml布局

**意味着,当程序运行在屏幕宽度大于600dp的设备上时会自动加载layout-sw600dp/activity_main布局,
当程序运行在屏幕宽度小于600dp的设备上时,仍加载默认的layout/activity_main布局。**

<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <fragment
        android:id="@+id/left_fragment"
        android:name="com.example.fragmenttest.LeftFragment"
        android:layout_weight="1"
        android:layout_width="0dp"
        android:layout_height="match_parent"/>
    <fragment
        android:id="@+id/right_fragment"
        android:name="com.example.fragmenttest.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3"/>
</LinearLayout>

4.5Fragment的最佳实践——一个简易版的新闻应用

同时兼容手机和平板的应用。有bug时不用修改两份代码。

准备好一个新闻的实体类,新建类News
public class News {
    private String title;//title字段表示新闻标题
    private String content;//content字段表示新闻内容

    public String getTitle() {
        return title;
    }

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

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

/*新建布局文件news_content_frag.xml作为新闻内容的布局*/
<RelativeLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:tools="http://schemas.android/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:ignore="MissingConstraints">

    <LinearLayout
        android:id="@+id/visibility_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:visibility="invisible">

        <TextView
头部显示新闻标题
            android:id="@+id/news_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:padding="10dp"
            android:textSize="20sp"/>
        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#000"/>
水平方向细线是用View来实现的,宽或高设置为1dp,background设置颜色
        <TextView
正文显示新闻内容
            android:id="@+id/news_content"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:padding="15dp"
            android:textSize="18sp"/>
    </LinearLayout>

    <View
        android:layout_width="1dp"
        android:layout_height="match_parent"
        android:layout_alignParentLeft="true"
        android:background="#000" />

</RelativeLayout>

新建NewsContentFragment类extendsFragment
public class NewsContentFragment extends Fragment {
    private View view;

    @Override
    public View onCreateView( LayoutInflater inflater,ViewGroup container, Bundle savedInstanceState) {
//onCreateView()方法中加载我们刚刚创建的news_content_frag布局
        view = inflater.inflate(R.layout.news_content_frag,container,false);
        return view;
    }

//提供refresh()方法用于将新闻的标题和内容显示在界面上
    public void refresh (String newsTitle,String newsContent){
        /*此方法用于将新闻标题和内容显示在界面上。通过findViewById()方法分别获取到新闻标题和内容的控件,然后将方法传递进来的参数设置进去*/
        View visbilityLayout = view.findViewById(R.id.visibility_layout);
        visbilityLayout.setVisibility(View.VISIBLE);
        TextView newsTitleText = (TextView)view.findViewById(R.id.news_title);
        TextView newsContentText = (TextView) view.findViewById(R.id.news_content);
        newsTitleText.setText(newsTitle);//刷新新闻标题
        newsContentText.setText(newsContent);//刷新新闻内容
    }
}

双页模式中使用的碎片和布局都创建好了。

再创建一个在单页中使用的活动

<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:app="http://schemas.android/apk/res-auto"
    xmlns:tools="http://schemas.android/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".NewsContentActivity">
	<fragment
	    android:id="@+id/news_content_fragment"
	    android:layout_width="match_parent"
	    android:layout_height="match_parent"
	    **android:name="com.example.fragmentbestpractice.NewsContentFragment"**
	    />
</LinearLayout>

发挥了代码的复用性,在布局中引入了NewsContentFragment,相当于把news_content_frag布局内容自动加了进来。

修改NewsContentActivity
public class NewsContentActivity extends AppCompatActivity {

    public static void acionStart(Context context,String newsTitle,String newsContent){
        Intent intent = new Intent(context,NewsContentActivity.class);
        intent.putExtra("news_title",newsTitle);
        intent.putExtra("news_content",newsContent);
        context.startActivity(intent);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.news_content);
        String newsTitle = getIntent().getStringExtra("news_title");//获取传入的新闻内容
        String newsContent = getIntent().getStringExtra("news_content");//获取传入的新闻标题
        //调用FragmentManager的findFragmentById方法
//得到了**NewsContentFragment实例,接着调用NewsContentFragment的refresh方法
//**并将新闻的标题和内容传入,这样就可以把这些数据显示出来了
        NewsContentFragment newsContentFragment = (NewsContentFragment)getSupportFragmentManager().findFragmentById(R.id.news_content_fragment);
        newsContentFragment.refresh(newsTitle,newsContent);//刷新NewsContent-Fragment界面
    }

}

新建news_item.xml用于显示新闻列表的布局

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
//里面只有一个用于显示新闻列表的RecyclerView。
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/news_title_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

既然要用RecyclerView,少不了子项的布局,新建news_item.xml。

<TextView xmlns:android="http://schemas.android/apk/res/android"
    android:id="@+id/news_title"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:maxLines="1"
    android:ellipsize="end"
    android:textSize="18sp"
    android:paddingLeft="10dp"
    android:paddingRight="10dp"
    android:paddingTop="15dp"
    android:paddingBottom="15dp">
</TextView>

android:ellipsize用于设定文本内容超出控件宽度时,文本的缩略方式,end表示在尾部进行缩略。

新建NewsTitleFragment作为展示新闻列表的碎片
public class NewsTitleFragment extends Fragment {
    private boolean isTwoPane;

    @Override
    public View onCreateView( LayoutInflater inflater,  ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.news_title_frag,container,false);//加载了news_title_frag布局
        return view;
    }

    public void onActivityCreated(Bundle savedInstaceState){
        super.onActivityCreated(savedInstaceState);
        if(getActivity().findViewById(R.id.news_content_layout)!=null){
            isTwoPane = true;//可以找到**news_content_layout布局**时,为双页模式//因此我们要让这个id为news_content_layout的View只在双页模式出现
        }else {
            isTwoPane = false;//找不到为单页模式
        }
    }
}

修改activity_main中的代码
表示单页模式下只会加载一个新闻标题碎片
<FrameLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:app="http://schemas.android/apk/res-auto"
    xmlns:tools="http://schemas.android/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/news_title_fragment"
        android:name="com.example.fragmentbestpractice.NewsTitleFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

</FrameLayout>

新建layour-sw600dp文件夹,layour-sw600dp文件夹中新建一个activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment
        android:id="@+id/news_title_fragment"
        android:name="com.example.fragmentbestpractice.NewsTitleFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1">
    </fragment>

    <FrameLayout
        android:id="@+id/news_content_layout"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3">
        <fragment
            android:id="@+id/news_content_fragment"
            android:name="com.example.fragmentbestpractice.NewsContentFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </FrameLayout>

</LinearLayout>
双页模式下引入两个碎片,新闻内容放在FrameLayout布局下,
这个FrameLayout布局id为news_content_fragment
能找到这个id说明为双页模式。
public class NewsTitleFragment extends Fragment {
    private boolean isTwoPane;
		...
    **class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder> {

        private List<News> mNewsList;**
        **class ViewHolder extends RecyclerView.ViewHolder {
            TextView newsTitleText;

            public ViewHolder(View itemView) {
                super(itemView);
//ViewHolder中只一个TextView,因为子项布局中只有一个TextView
                newsTitleText = (TextView) itemView.findViewById(R.id.news_title);
            }
        }
        @Override
        public ViewHolder onCreateViewHolder( ViewGroup parent, int viewType) {
//**onCreateViewHolder()方法中注册的点击事件,
//首先获取到了点击项的News实例,
//然后通过isTwoPane变量来判断当前是单页还是双页模式,
//如果是单页模式,就启动一个新的活动去显示新闻内容,如果是双页模式,就更新新闻内容碎片里的数据。
            **View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.news_item,parent,false);
            final ViewHolder holder = new ViewHolder(view);
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    News news = mNewsList.get(holder.getAdapterPosition());
                    if(isTwoPane){
                        //如果是双页模式,则刷新NewsContentFragment中的内容
                        NewsContentFragment newsContentFragment = (NewsContentFragment)getFragmentManager().findFragmentById(R.id.news_content_fragment);
                        newsContentFragment.refresh(news.getTitle(),news.getContent());
                    } else {
                        //如果是单页模式,则直接从MainactivityActivity启动NewsContentActivity
                        NewsContentActivity.acionStart(getActivity(),news.getTitle(),news.getContent());
                    }
                }
            });
            return holder;
        }

        @Override
        public void onBindViewHolder( NewsTitleFragment.NewsAdapter.ViewHolder holder, int position) {
            News news  = mNewsList.get(position);
            holder.newsTitleText.setText(news.getTitle());
        }**

        @Override
        public int getItemCount() {
            return mNewsList.size();
        }

        public NewsAdapter(List<News> mNewsList) {
            this.mNewsList = mNewsList;
        }
    }
}

适配器写成内部类的好处是可以直接访问NewsTitileFragment的变量,比如isTwoPane。onCreateViewHolder()方法中注册的点击事件,首先获取到了点击项的News实例,然后通过isTwoPane变量来判断当前是单页还是双页模式,如果是单页模式,就启动一个新的活动去显示新闻内容,如果是双页模式,就更新新闻内容碎片里的数据。

最后一步,向RecyclerView中填充数据。修改NewsTitle-Fragment

public class NewsTitleFragment extends Fragment {
		...

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.news_title_frag, container, false);//加载了news_title_frag布局
        **RecyclerView newsTitleRecyclerView = (RecyclerView)view.findViewById(R.id.news_title_recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
        newsTitleRecyclerView.setLayoutManager(layoutManager);

        NewsAdapter adapter = new NewsAdapter(getNews());
        newsTitleRecyclerView.setAdapter(adapter);
        return view;**
    }

    private List<News> getNews(){
        List<News> newsList = new ArrayList<>();
        for(int i = 0;i<=50;i++){
            News news = new News();
            news.setTitle("This is news title "+ i);
            news.setContent(getRandomLengthContent("This is news content" + i + "."));
            newsList.add(news);
        }
        return newsList;
    }

    private String getRandomLengthContent(String content){
        Random random = new Random();
        int length = random.nextInt(20) + 1;
        StringBuilder builder = new StringBuilder();
        for(int i = 0;i<length;i++){
            builder.append(content);
        }
        return builder.toString();
    }
	  ...
}

MainActivity中有NewsTitleFragment小于600dp则直接用NewsTitleFragment的RecyclerView加载出来,屏幕大于60dp为双页模式,就调用layout-sw600dp中的activity_main布局。。NewsTitleFragmen的onCreateViewHolder()方法中注册的点击事件,首先获取到了点击项的News实例,然后通过isTwoPane变量来判断当前是单页还是双页模式,如果是单页模式,就启动NewsContentAcitivty去显示新闻内容,如果是双页模式,就更新新闻内容碎片里的数据。

4.6小结与点评

在开发的过程中多付出一些,在以后的代码维护中可以轻松很多。

目前我们讲了Android UI的相关重要知识点,我们目前只学了Android四大组件中的活动。

第五章全局大喇叭——详解广播机制

在一个IP网络范围中,最大的IP地址是被保留作为广播地址来使用的。比如某个网络的IP范围是192.168.0.XXX,子网掩码是255.255.255.0,那么这个网络的广播地址就是192.168.0.255。广播数据包会被发送到同一网络上所有端口,在该网络中每台主机都会收到这条广播。
为了便于系统级别的消息通知,Android也引入了一套类似的广播消息机制,且会显得更加灵活。

5.1广播机制简介

为什么说Andorid广播机制更加灵活?因为Andorid中每个应用程序都可以对自己感兴趣的广播进行注册,只接收自己关心的广播内容,这些广播可能来自系统或者其他应用程序。发送广播的方法借助Intent,接收广播的方法——广播接收器(Broadcast Receiver**)**

  • 标准广播(Normal broadcasts)
    是一种完全异步执行的广播,在广播发出后所有BroadcastReceiver几乎同时接收这条广播消息,之间没有任何先后顺序。这种广播效率比较高,但也意味着无法被截断。

                           标准广播工作示意图
  • 有序广播(Ordered broadcasts)
    是一种同步执行的广播,在广播发出之后,同一时刻只会有一个BroadcastReceiver能够收到这条广播消息,此BroadcastReceiver中的逻辑执行完毕后,广播才会继续传递。所以此时BroadcastReceiver有先后顺序,优先级高的BroadcastReceiver先收到广播消息,并且前面的BroadcastReceiver可以截断正在传递的广播,这样后面的BroadcastReceiver无法收到广播消息。

                                                           有序广播工作示意图

5.2接收系统广播

Android内置了很多了系统级别的广播,我们可以在应用程序中通过监听这些广播来得到各种系统的状态信息。比如手机开机完成后会发出一条广播,电池的电量发出变化会发出一条广播,系统时间或时区发生改变也会发出一条广播,等等。如果想要接收到这些广播,就需要使用BroadcastReceiver。

5.2.1动态注册监听网络变化

BroadcastReceiver可以自由地对感兴趣的广播进行注册,这样当有相应广播发出时,BroadcastReceiver就能收到该广播,并在内部进行逻辑处理。注册BroadcastReceiver的方式一般有两种,在代码中注册和在AndroidManifest.xml中注册,其中前者也被称为动态注册,后者被称为静态注册。

如何创建一个BroadcastReceiver呢?新建一个类,让它继承自BroadcastReceiver,并重写父类onReceive()方法,这样当有广播到来时,onReceive()方法就会得到执行,具体的逻辑就可以在这个方法中处理。

public class MainActivity extends AppCompatActivity {

    **private IntentFilter intentFilter;

    private NetworkChangeReceiver newworkChangeReceiver;**
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
//首先创建了IntentFilter的实例,
        **intentFilter = new IntentFilter();
//**并给它添加一个值为android.conn.CONNECTIVITY_CHANGE的action
//**因为当网络状态发生变化时系统发出的正是一条值为android.conn.CONNECTIVITY_CHANGE的广播
//也就是说我们的广播接收器想要监听什么广播,就在这里添加相应的action。
        intentFilter.addAction("android.conn.CONNECTIVITY_CHANGE");

//接下来创建一个NetworkChangeReceiver实例,
        newworkChangeReceiver = new NetworkChangeReceiver();
//调用registerReceiver()方法进行注册,将NetworkChangeReceiver的实例和IntentFilter的实例都传了进去,
        registerReceiver(newworkChangeReceiver,intentFilter);
//这样NetworkChangeReceiver就会收到所有值为android.conn.CONNECTIVITY_CHANGE的广播,
//也就实现了监听网络变化的功能。**
    }

    **@Override**
    protected void onDestory(){
**//最后要记得,动态注册的广播接收器一定都要取消注册才行,
//这里我们是在onDestroy()方法中通过调用unregisterReceiver()方法来实现的。
        super.onDestroy();
        unregisterReceiver(newworkChangeReceiver);
    }

//**在MainActivity中定义内部类NewworkChangeReceiver继承自BroadcastReceiver
    **class NetworkChangeReceiver extends BroadcastReceiver{
//**并重写了父类的onReceive()方法,这样每当网络状态发生变化,
//onReceive()方法就会得到执行。
        **@Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(context,"network changes",Toast.LENGTH_SHORT).show();//**这里只是简单地使用了Toast一段文本信息
        **}
    }**
}

首先会在注册完成时收到一条广播,按下Home键回到主界面(不能按Back键,否则onDestroy()方法会执行),接着打开Settings程序→Data usage进入到数据使用详情界面,然后尝试着开关Cellular data按钮来启动和禁用网络,就会看到Toast提醒网络发生了变化。

不过,只提醒网络发生了变化还不够人性化,最好是能准确地告诉用户当前是有网络还是无网络,因此修改MainActivity中的代码。

public class MainActivity extends AppCompatActivity {
		...

    //在MainActivity中定义内部类NewworkChangeReceiver继承自BroadcastReceiver
    class NetworkChangeReceiver extends BroadcastReceiver {
        //并重写了父类的onReceive()方法,这样每当网络状态发生变化,
//onReceive()方法就会得到执行。

        @Override
        public void onReceive(Context context, Intent intent) {
//onReceiver()中
//首先通过getSystemService()方法得到了ConnectivityManager实例,
//ConnectivityManager是一个系统服务类,专门用于管理网络连接,
            ConnectivityManager connectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
            @SuppressLint("MissingPermission")
//调用ConnectivityManage的getActiveNetworkInfo()方法可以得到NetworkInfo实例
            NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
//调用NetworkInfo的isAvailable()方法可以判断出当前是否有网络
            if(networkInfo!=null&&networkInfo.isAvailable()){
                Toast.makeText(context,"network is available",Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(context,"network is unavailable",Toast.LENGTH_SHORT).show();
            }
        }
    }
}

如果程序需要进行一些对用户来说比较敏感的操作就必须在配置文件中声明权限,比如这里访问系统网络状态就需要声明权限。

怎么动态注册BroadcastReceiver? 新建一个类继承BroadcastReceiver,重写onReceiver()方法中收到广播后执行的逻辑,在活动onCreate()方法里实例化IntentFilter只用来add响应的Action("广播的值")调用registerReceiver()方法,第一个参数传入receiver,第二个参数传入IntentFilter。在onDestroy()方法中调用unregisterReceiver()方法传入receiver实例即可。

public class MainActivity extends AppCompatActivity {
    private ConnectivityManager.NetworkCallback netCallback;
    private ConnectivityManager connectivityManager;

    @RequiresApi(api = Build.VERSION_CODES.N)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        netCallback = new ConnectivityManager.NetworkCallback(){
            @Override
            public void onAvailable(@NonNull Network network) {
                super.onAvailable(network);
                Toast.makeText(MainActivity.this, "network is available", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onLost(@NonNull Network network) {
                super.onLost(network);
                Toast.makeText(MainActivity.this, "network is unavailable", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onUnavailable() {
                super.onUnavailable();
                Toast.makeText(MainActivity.this, "network is unavailable", Toast.LENGTH_SHORT).show();
            }
        };
        connectivityManager.registerDefaultNetworkCallback(netCallback);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onDestroy() {
        super.onDestroy();
        connectivityManager.unregisterNetworkCallback(netCallback);
    }
}

书上方法废弃了。

5.2.2静态注册实现开机启动

动态注册的BroadcastReceiver可以自由地控制注册与注销,灵活性方面有很大优势,但它存在一个缺点,即必须要在程序启动之后才能接收到广播,因为注册逻辑是写在onCreate()方法中的。静态可以让程序在未启动的情况下就能收到广播。

Android8.0后,所有隐式广播不允许用静态注册的方式来接收,隐式广播指没有具体指定发送给哪个应用程序的广播,大多数系统广播属于隐式广播,少数特殊广播目前仍允许使用静态注册的方式来接收。

让程序接收一条开机广播,这条广播可以在onReceive()方法里执行相应的逻辑,从而实现开机启动的功能。

新建Broadcast Receiver

public class BootCompleteReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context,"Boot Complete",Toast.LENGTH_SHORT).show();
    }
}

只是在onReceive()方法中使用Toast弹出一段提示信息

静态广播接收器一定要在AndroidManifest.xml文件中注册才可以使用

标签内出现了新的标签,所有静态广播接收器都是在这里进行注册的。

修改AndroidManifest.xml文件才行

<manifest xmlns:android="http://schemas.android/apk/res/android"
    xmlns:tools="http://schemas.android/tools"
    package="com.example.broadcasttest">

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--监听系统开机广播也需要声明权限,可以看到,我们使用<user-permission>
标签里又加入了一条android.permission.RECEIVE_BOOT_COMPLETED权限。-->
    **<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />**

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Broadcasttest">
        ...
        <receiver
            android:name=".MainActivty$BootCompleteReceiver"
            android:enabled="true"
            android:exported="true"
            tools:ignore="Instantiatable">
<!--由于Android系统启动完成后会发出一条值为android.intent.action.BOOT_COMPLETED的广播,
因此我们在<intent-filter>标签里添加了相应的action。-->
            **<intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>**
        </receiver>
    </application>
</manifest>

需要注意的是,不要在onReceive()方法中添加过多的逻辑或进行任何的耗时操作,因为广播接收器不允许开启线程,当onReceive()方法运行了较长时间还没有结束,程序就会报错。因此广播接收器多用来打开程序其他组件,比如创建一条状态栏通知,或者启动一个服务。

5.3发送自定义广播

已经学会了通过BroadcastReceiver接收系统广播,接下来学习如何在应用程序中发送自定义的广播。我们通过实践的方式来看看标准广播和有序广播的区别。

5.3.1发送标准广播

在发送广播前,我们需要先定义一个接收器来准备接收广播才行。

//新建MyBroadcastReceiver
public class MyBroadcastReceiver  extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context,"received in MyBroadcastReceiver",Toast.LENGTH_SHORT).show();
        //当MyBroadcastReceiver收到自定义广播时,就会弹出“received in MyBroadcastReceiver”的提示
    }
}

在AndroidManifest.xml中对广播接收器进行修改
<manifest xmlns:android="http://schemas.android/apk/res/android"
    ...
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.BroadcastTest2">
        ...
				<!--让MyBroadcastReceiver接收一条值为com.example.broadcasttest2.MY_BROADCAST的广播
        因为待会儿发送广播,我们需要发出这样这样一条广播-->
        <receiver android:name=".MyBroadcastReceiver"
                android:enabled="true"
                android:exported="true">
            **<intent-filter>
                <action android:name="com.example.broadcasttest2.MY_BROADCAST"/>
            </intent-filter>**              
        </receiver>
    </application>
</manifest>

修改activity_main.xml
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:app="http://schemas.android/apk/res-auto"
    xmlns:tools="http://schemas.android/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
定义一个按钮作为广播的触发点
    <Button
        android:id="@+id/button"
         android:layout_height="wrap_content" 
        android:layout_width="match_parent"
        android:text="Send Broadcast"/>

</LinearLayout>

public class MainActivity extends AppCompatActivity {
		...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        **Button button = (Button)findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
//我们在按钮的点击事件里加入发送自定义广播的逻辑
//首先构建一个Intent对象,把要发送广播的值传入
                Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
//调用Context的sendBroadcast()方法将广播发送出去
                sendBroadcast(intent);
            }
        });
				...
    }
	...**
}

注意这里隐式注册改为显示注册

5.3.2发送有序广播

从前面接收系统广播时就可以看出广播是一种可以跨进程的通信方式,在应用程序内发出的广播其他应用程序也可以收到。我们新建BroadcastTest3

新建AnotherBroadcastReceiver
public class AnotherBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        **Toast.makeText(context,"received in AnotherBroadcastReceiver",Toast.LENGTH_SHORT).show();**
    }
}

在AndroidManifest.xml对广播接收器进行修改
<manifest xmlns:android="http://schemas.android/apk/res/android"
    xmlns:tools="http://schemas.android/tools"
    package="com.example.broadcasttest">

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--监听系统开机广播也需要声明权限,可以看到,我们使用<user-permission>标签里又加入了一条android.permission.RECEIVE_BOOT_COMPLETED权限。-->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Broadcasttest">
				...
        <receiver android:name=".AnotherBroadcastReceiver"
            android:enabled="true"
            android:exported="true">
            <!--让AnotherBroadcastReceiver接收一条值为com.example.broadcasttest2.MY_BROADCAST的广播
            因为待会儿发送广播,我们需要发出这样这样一条广播-->
            **<intent-filter>
                <action android:name="com.example.broadcasttest2.MY_BROADCAST"/>
            </intent-filter>**
        </receiver>
    </application>
</manifest>

public class MainActivity extends AppCompatActivity {
		...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        localBroadcastManager = LocalBroadcastManager.getInstance(this);
        Button button = (Button)findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //通过LocalBroadcastManager的getInstance()得到它的一个实例
                Intent intent = new Intent("com.example.broadcasttest.My_BROADCAST");
                //发送有序广播只需要改动一行代码,将sendBroadcast()改成sendOrderedBroadcast()方法
                //sendOrderedBroadcast()第一个参数是Intent,第二个是与权限相关的字符串,传入null
								sendOrderedBroadcast(intent,null);
            }
        });
        ...
    }
		...
}

设定广播接收器先后顺序需要再注册的时候设定,修改AndroidManifest.xml
<manifest xmlns:android="http://schemas.android/apk/res/android"
    package="com.example.broadcasttest2">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.BroadcastTest2">
        ...
        <receiver android:name=".MyBroadcastReceiver"
                android:enabled="true"
                android:exported="true">
            <!--让MyBroadcastReceiver接收一条值为com.example.broadcasttest2.MY_BROADCAST的广播
            因为待会儿发送广播,我们需要发出这样这样一条广播-->
            <intent-filter **android:priority="100"**>
我们通过android:priority属性给广播接收器设置了优先级,优先级高的广播接收器可以先收到广播。
                <action android:name="com.example.broadcasttest2.MY_BROADCAST"/>
            </intent-filter>
        </receiver>
    </application>
</manifest>

public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context,"received in MyBroadcastReceiver",Toast.LENGTH_SHORT).show();
//如果在onReceiver()方法中调用了abortBroadcast()方法,就表示这条广播截断,后面的BroadcastReceiver无法再接收这条广播
        **abortBroadcast();**
    }
}

5.4使用本地广播

前面我们发送和接收的广播全部属于系统全局广播,即发出的广播可以被其他任何应用程序接收到,并且我们可以接收到来自于其他任何应用程序的广播,这样容易引起安全性问题比如我们发送的携带关键性数据的广播被其他应用程序截获,或其他程序不停向我们的BroadcastReceiver发送各种垃圾广播。

为了简单地解决广播的安全性问题,Android引入了一套本地广播机制,使用这个机制发出的广播只能够在应用程序内部进行传递,并且BroadcastReceiver也只能接收来自本应用程序发出的广播,这样所有的安全性问题就都不存在了。

本地广播的用法并不复杂,主要用一个LocalBroadcastManager来对广播进行管理,并提供了发送广播和注册广播接收器的方法。

修改MainActivity中的代码
public class MainActivity extends AppCompatActivity {

    private IntentFilter intentFilter;
    **private LocalReceiver localReceiver;
    private LocalBroadcastManager localBroadcastManager;**

    /**
     * 和所学动态注册广播接收器以及发送广播代码一样
     *
     * @param savedInstanceState
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
**//先通过LocalBroadcastManager的getInstance()方法得到LocalBroadcastManager的一个实例**
        **localBroadcastManager = LocalBroadcastManager.getInstance(this);**
        Button button = (Button)findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                **//通过LocalBroadcastManager的getInstance()得到它的一个实例
                Intent intent = new Intent("com.example.broadcasttest.LOACL_BROADCAST");
  //发送广播调用的localBroadcastManager的sendBroadcast()方法
                localBroadcastManager.sendBroadcast(intent);**
            }
        });
        IntentFilter intentFilter = new IntentFilter();
        **intentFilter.addAction("com.example.broadcasttest.LOACL_BROADCAST");**
        **localReceiver = new LocalReceiver();
        localBroadcastManager.registerReceiver(localReceiver,intentFilter); // 注册本地广播监听器**
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        **localBroadcastManager.unregisterReceiver(localReceiver);**
    }

    **class LocalReceiver extends BroadcastReceiver{
        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(context, "received local broadcast", Toast.LENGTH_SHORT).show();
        }
    }**
}

本地广播只会在本应用程序内传播。

另外需要说明的一点是本地广播无法通过静态注册的方式来接收,因为静态注册主要是为了让程序员在未启动的情况下也能收到广播,而发送本地广播时,程序肯定已经启动了所以完全不需要使用静态注册的功能。

本地广播的优势:

  • 可以明确地知道正在发送的广播不会离开程序,因此不用担心机密数据泄露
  • 其他的程序无法将广播发送到程序内部,因此不需要担心安全漏洞的隐患
  • 发送本地广播比发送系统全局广播更高效

5.5广播的最佳实践——实现强制下线功能

很多应用程序都具备强制下线功能,比如QQ号在别处登录就会将你强制下线。只需要在界面上弹出一个对话框,让用户无法进行任何操作,必须要点击对话框中确定按钮,然后回到登陆界面即可。
借助广播,我们可以让用户在任何一个界面收到这个弹出对话框的逻辑。

强制下线功能需要先关闭掉所有的活动,然后回到登陆界面。

先创建一个ActivityCollector类用户管理所有活动。
public class ActivityCollector {
    public static List<Activity> activities = new ArrayList<>();

    public static void addActivity(Activity activity){
        activities.add(activity);
    }

    public static void removeActivity(Activity activity){
        activities.remove(activity);
    }

    public static void finishAll(){
        for(Activity activity: activities){
            if(!activity.isFinishing()){
                activity.finish();
            }
        }
    }
}

创建BaseActivity类作为所有活动的父类
public class BaseActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityCollector.addActivity(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        ActivityCollector.removeActivity(this);
    }
}

新建LoginActivity,修改activity_login.xml
//最外层垂直LinearLayout 第一行是一个横向LinearLayout用于输入账号信息
// 第二行密码 第三行登录按钮
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:app="http://schemas.android/apk/res-auto"
    xmlns:tools="http://schemas.android/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".LoginActivity"
    android:orientation="vertical"
    >
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="60dp">
        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="Account:"
            />
        <EditText
            android:id="@+id/account"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center_vertical"/>
    </LinearLayout>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="60dp">
        <TextView
            android:layout_width="90dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="Password:"    />
        <EditText
            android:id="@+id/password"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center_vertical"/>

    </LinearLayout>

    <Button
        android:id="@+id/login"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:text="Login"/>
</LinearLayout>

LoginActivity中模拟简单的登录功能,首先继承自BaseAcivity
public class LoginActivity extends **BaseActivity** {

    **private EditText accountEdit;
    private EditText passwordEdit;
    private Button login;**

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
				//分别获取到账号输入框,密码输入框以及登录按钮的实例。
        **accountEdit = (EditText)findViewById(R.id.account);
        passwordEdit = (EditText)findViewById(R.id.password);
        login = (Button)findViewById(R.id.login);
				
        login.setOnClickListener(new View.OnClickListener() {
						//在登录按钮的点击事件中对输入的账号和密码进行判断
            String account = accountEdit.getText().toString();
            String password = passwordEdit.getText().toString();
            @Override
            public void onClick(View v) {
                String account = accountEdit.getText().toString();
                String password = passwordEdit.getText().toString();
                //如果账号是admin且密码为123456就认为登录成功
                if(account.equals("admin") && password.equals("123456")){
							//跳转到MainActivity
                    Intent intent = new Intent(LoginActivity.this,MainActivity.class);
                    startActivity(intent);
                    finish();
                }else {
                    Toast.makeText(LoginActivity.this,"account or password is invalid",Toast.LENGTH_SHORT).show();
                }
            }
        });**
    }
}

可以将MainActivity理解成登陆成功后进入程序的主界面,只在主界面加入强制下线功能。修改activity_main.xml
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:app="http://schemas.android/apk/res-auto"
    xmlns:tools="http://schemas.android/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
只有一按钮用于触发强制下线功能
    <Button
        android:id="@+id/force_offline"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Send force offline broadcast"/>

</LinearLayout>

//触发强制下线功能
public class MainActivity extends **BaseActivity** {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        **Button forceOffline = (Button)findViewById(R.id.force_offline);
        forceOffline.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
//在按钮的点击事件里发送一条值为com.example.broadcastbestpractice.FORCE_OFFLINE的广播
//这条广播用于通知程序强制用户下线。也就是说强制用户下线的逻辑不在MainActivity中,而在接收这条广播的BroadcastReceiver里。
//这样强制下线的功能不会依附于任何界面,不管是在程序的任何地方,
                Intent intent = new Intent("com.example.broadcastbestpractice.FORCE_OFFLINE");
                sendBroadcast(intent);
            }
        });**
    }
}

接下来需要创建一个BroadcastReceiver来接收这条强制下线广播,如果静态注册就没办法在onReceive()方法里弹出对话框那样控制UI控件。动态注册,我们不可能在每个活动中都注册。
所以需要在BaseActivity中动态注册一个BroadcastReceiver。

public class BaseActivity extends AppCompatActivity {
    **private ForceOfflineReceiver receiver;**
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityCollector.addActivity(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        ActivityCollector.removeActivity(this);
    }
/*
我们重写了onResume()和onPause()这两个生命周期方法,然后分别在这两个方法中注册和取消注册Force**OfflineReceiver**
为什么要这样?之前不都是在onCreate()和onDestroy()方法里注册和取消注册BroadcastReceiver,
这是因为我们始终需要保证只有栈顶的Activity才能接收到这条强制下线的广播,非栈顶的Activity不应该也没必要接收这条广播
当一个活动失去栈顶位置就会自动取消广播接收器的注册
*/
    **@Override
    protected void onResume() {
        super.onResume();
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("com.example.broadcastbestpractice.FORCE_OFFLINE");
        receiver = new ForceOfflineReceiver();
        registerReceiver(receiver,intentFilter);
    }

    @Override
    protected void onPause() {
        super.onPause();
        if(receiver != null){
            unregisterReceiver(receiver);
            receiver = null;
        }**
    **}**

    **class ForceOfflineReceiver extends BroadcastReceiver{

        @Override
        public void onReceive(final Context context, Intent intent) {
//使用AlertDialog.Builder来构建一个对话框,
            AlertDialog.Builder builder = new AlertDialog.Builder(context);
            builder.setTitle("Warning");
            builder.setMessage("You are forced to be offline.Please try to login again");
//一定要用setCancelable()方法将对话框设为不可取消。否则用户按下Back就可以关闭对话框继续使用程序了
						builder.setCancelable(false);
//使用setPositiveButton()方法给对话框注册确定按钮
            builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int which) {
//当用户点击了确定按钮,调用ActivityCollector的finishAll()方法销毁掉所有活动,并重新启动LoginActivity打开登陆界面
                    ActivityCollector.finishAll();
                    Intent intent = new Intent(context,LoginActivity.class);
                    context.startActivity(intent);
                }
            });
            builder.show();
        }
    }**
}

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.ActivityCollector">
        <activity android:name=".LoginActivity">
将主活动设为LoginActivity而不是MainActivity,因为是先登录后进入程序主界面。
            **<intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>**
        </activity>
        <activity android:name=".MainActivity">
        </activity>
    </application>

</manifest>

5.7小结与点评

已学习四大组件中的两个,Activity活动和BroadcastReceiver广播接收器

第六章数据存储全方案——详解持久化技术

任何一个应用程序说白了就是在不停地和数据打交道,我们聊QQ、看新闻、刷微博、所关心的都是里面的数据,没有数据的应用程序就变成了空壳子,对用户来说没有实际意义。数据从哪里来?现在多数数据都是由用户产生的,比如发微博,评论新闻都是在产生数据。

第三章最佳实践部分在聊天界面编写的聊天内容,第五章最佳实践部分在登录界面输入的账号和密码属于瞬时数据。瞬时数据指存储在内存中,也可能会因为程序关闭或其他原因导致内存被回收而丢失的数据。使用持久化技术可以保证一些关键性数据不会丢失

6.1持久化技术简介

数据持久化指将内存中瞬时数据保存在存储设备中,保证手机或计算机关机情况下数据仍不会丢失。保存在内存中的数据是瞬时状态的,而保存在存储设备中的数据是处于持久状态的。持久化技术提供了一种机制让数据在顺势状态和持久状态之间进行转换。

持久化技术提供了一种机制可以让数据在瞬时状态和持久状态之间进行转换。被广泛应用于各种程序设计领域中,Android系统中主要提供了3种方式用于简单地实现数据持久化功能:文件存储、SharedPreference存储以及数据库存储。

6.2文件存储

文件存储是Android中最基本的数据存储方式,它不对存储的内容进行任何的格式化处理,所有数据原封不动保存到文件当中,因而它比较适合存储简单的文本数据或二进制数据。如果想使用文件存储的方式来保存一些较为复杂的结构化数据,就需要定义一套自己的格式规范,可以方便之后将数据从文件中重新解析出来。

6.2.1将数据存储到文件中

Context类中提供了一个openFileOutput()方法,可以用于将数据存储到指定的文件中。这个方法接收两个参数,第一个参数是文件名,在文件创建的时候使用,指定的文件名不能包含路径,所有的文件都默认存储到/data/data//files/目录下。第二个参数是文件的操作模式,主要有MODE_PRIVATEMODE_APPEND两种模式可选。其中MODE_PRIVATE是默认的操作模式,表示当指定同样的文件名的时候,所写入的内容将会覆盖原文件中的内容,MODE_APPEND表示如果该文件已存在,就往文件里追加内容,不存在就创建新文件。文件操作模式还有两种MODE_WORLD_READABLEMODE_WORLD_WRITEABLE,这两种模式表示允许其他的应用程序对我们程序中的文件进行读写操作,不过由于这两种模式过于危险,很容易引起应用的安全性漏洞已被废弃。

openFileOutput()方法返回的是一个FileOutputStream对象,得到对象之后就可以使用Java流的方式将数据写入到文件中了。

public void save() {
	String data = "Data to save" ;
	FileOutputStream out = null;
	Bufferedwriter writer = null;
	try {
**//先通过openFileoutput()方法得到一个FileOutputStream对象**
		out = openFileoutput ( "data" , Context. MODE_PRIVATE);
**//传入借助FileOutputStream构建出一个0utputStreamWriter对象,
//再使用0utputStreamWriter构建出一个Bufferedwriter对象**
		writer = new Bufferedwriter(new 0utputStreamWriter(out) );
		writer.write(data) ;
	}catch ( IOException e) {
		e.printStackTrace( ) ;
	}finally {
		try {
			if (writer != null) {
				writer.close( ) ;
			}catch ( IOException e) {
				e.printStackTrace( );
			}
	}
}

新建FilePersistenceTest项目

修改activity_main.xml
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:app="http://schemas.android/apk/res-auto"
    xmlns:tools="http://schemas.android/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/edit"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Type something here"
        tools:ignore="MissingConstraints" />

</LinearLayout>

在文本框中随意输入什么按下Back输入的内容肯定就丢失了,因为是瞬时数据,在活动被销毁后就会被回收。
修改MainActivity
public class MainActivity extends AppCompatActivity {

    private EditText edit;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        edit = (EditText)findViewById(R.id.edit);
    }

/*重写onDestroy()方法,保证活动销毁之前一定会调用这个方法*/
    @Override
    protected void onDestroy() {
        super.onDestroy();
        String inputText = edit.getText().toString();
        try {
//用save()方法把输入的内容存储到文件中
            save(inputText);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void save(String inputText) throws IOException {
        FileOutputStream out  = null;
        BufferedWriter writer = null;
        try{
//文件命名为data
            out = openFileOutput("data", Context.MODE_PRIVATE);
            writer = new BufferedWriter(new OutputStreamWriter(out));
            writer.write(inputText);
        } catch (IOException e){
            e.printStackTrace();
        }finally {
            try {
                if (writer != null) {
                    writer.close();
                }
            } catch(IOException e){
                e.printStackTrace();
            }
        }
    }
}

6.2.2从文件中读取数据

Context类中还提供了一个openFileInput()方法,用于从文件中读取数据,openFileInput()只接受一个参数,即要读取的文件名,然后系统会自动到/data/data//files/目录下去加载这个文件,并返回一个FileInputStream对象,得到了这个对象之后再通过Java流方式读取出来就行。

public String load( ) {
	FileInputstream in = null;
	BufferedReader reader = null;
	StringBuilder content = new StringBuilder( ) ;
	try {
**//通过openFileInput()方法获取到了一个FileInputStream对象
			in = openFileInput ("data" ) ;
//借助FileInputStream构建一个InputStreamReader对象
//再使用InputStreamReader构建出一个BufferedReader对象
			reader = new BufferedReader(new InputStreamReader(in) );
//这样我们就可以通过BufferedReader进行一行行地读取,
//把文件中全部内容存在一个StringBuilder对象中,最后将读取到的内容返回。**
			String line = " ";
			while ( ( line = reader. readLine( )) != null) {
					content.append(line) ;
		}catch (IOException e) {
				e.printstackTrace() ;
		}finally {
				if ( reader != null) {
					try {
						reader.close( );
					} catch ( IOException e) {
						e.printStackTrace( ) ;
					}
				}
			}
			return content.toString();
}
修改MainActivity

修改MainActivity

public class MainActivity extends AppCompatActivity {

    private EditText edit;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        edit = (EditText)findViewById(R.id.edit);
**//在onCreate()中调用load()方法读取文件中存储的文本内容,
        String inputText = load();
//如果读到的内容不为null,
//就调用EditText的SetText()方法将内容填充到EditText里,
//这个(isEmpty())方法可以一次性进行两种空值的判断,当传入的字符串等于null或者等于空字符串时,都会返回true
        if(!TextUtils.isEmpty(inputText)){
            edit.setText(inputText);
            edit.setSelection(inputText.length());
//并调用setSelection()方法将输入光标移动到文本的末尾位置以便于继续输入
//然后弹出一句还原成功的提示。
            Toast.makeText(this,"Resoring succeeded",Toast.LENGTH_SHORT).show();
        }
    }

		...

    private String load() {
        FileInputStream in = null;
        BufferedReader reader = null;
        StringBuilder content = new StringBuilder();
        try{
            in = openFileInput("data");
            reader = new BufferedReader(new InputStreamReader(in));
            String line = "";
            while((line = reader.readLine())!= null ){
                content.append(line);
            }
        }catch (IOException e){
            e.printStackTrace();
        } finally {
            if(reader != null){
                try{
                    reader.close();
                } catch (IOException e){
                    e.printStackTrace();
                }
            }
        }
        return  content.toString();
    }**
}

6.3SharedPreferences存储

不同于文件的存储方式,SharedPreferences使用键值对的方式来存储数据,当保存一条数据时,需要给这条数据提供一个对应的键。这样读取数据时可以通过这个键把对应的值取出来。SharedPreferences存储还支持多种不同数据类型存储,如果存储的数据类型是整形,那么读取出来数据也是整形,如果存储数据是字符串,读取出来仍然是字符串。

6.3.1将数据存储到SharedPreferences中

想用SharedPreference存储数据,首先要获取SharedPreferences对象

1.Context类中的getSharedPreferences()方法。

Context类中的getSharedPreferences()方法接收两个参数,第一个参数用于指定SharedPreferences文件的名称,如果指定文件不存在则创建一个,SharedPreferences文件存放在/data/data//shared_prefs/目录下的。第二个参数用于指定操作模式,只有默认的MODE_PRIVATE这一种模式可选,和直接传入0效果相同,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。其他几种操作模式均已废弃。

2.Activity类种的getPreferences()方法

这个方法和Context中的getSharedPreferences()方法很相似,不过它接收一个操作模式参数,因为使用这个方法时会自动将当前活动类名作为SharedPreferences的文件名。

3.PreferenceManager类中的getDefaultSharedPreferences()方法

PreferenceManager类中的getDefaultSharedPreferences()方法是一个静态方法,接收一个Context参数,并自动使用当前应用程序的包名作为前缀来命名SharedPreferences文件。得到了SharedPreferences对象之后,就可以开始向SharedPreferences文件中存储数据了,主要可以分3步实现。

(1)调用SharedPreferences对象的edit()方法来获取一个SharedPreferences.Editor对象
(2)向SharedPreferences.Editor对象中添加数据,比如添加一个布尔型数据就使用**putBoolean()方法,添加一个字符串就使用putString()方法,以此类推。
(3)调用
apply()**方法将添加的数据提交,从而完成数据存储操作。

新建SharedPreferencesTest项目,修改activity_main.xml
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:app="http://schemas.android/apk/res-auto"
    xmlns:tools="http://schemas.android/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/save_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Save data"/>
</LinearLayout>

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button saveData = (Button)findViewById(R.id.save_data);
        saveData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
//点击事件中通过getSharedPreferences()方法指定SharedPreferences文件名为data,
//并用edit()方法得到edit对象,
                SharedPreferences.Editor editor = getSharedPreferences("data",MODE_PRIVATE).edit();
//接着向edit对象中添加了3条不同类型数据最后调用apply()方法提交
								editor.putString("name","Tom");
                editor.putInt("age",28);
                editor.putBoolean("married",false);
//完成数据存储操作
                editor.apply();
            }
        });
    }
}

我们刚刚在按钮的点击事件中添加所有数据都被保存下来并且SharedPreferences文件是用XML格式来对数据进行管理。接下来要从SharedPreferences文件中去读取这些数据。

6.3.2从SharedPreferences中读取数据

SharedPreferences提供了一系列的get方法,用于对存储的数据进行读取,每种get方法对应了SharedPreferences.Editor中的一种put方法,如读取一个布尔型数据就使用getBoolean()方法,这些get方法都接收两个参数,第一个参数是键,传入存储数据时使用的键就可以得到相应的值了。第二个参数是默认值,即表示当传入的键找不到对应的值时会以什么样的默认值进行返回


修改activity_main.xml
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:app="http://schemas.android/apk/res-auto"
    xmlns:tools="http://schemas.android/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/save_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Save data"/>
增加了还原数据按钮,我们希望通过点击这个按钮来从SharedPreferences文件中读取数据
    <**Button
        android:id="@+id/restore_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Restore data"/>**
</LinearLayout>

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        **Button restoreData = (Button)findViewById(R.id.restore_data);
        restoreData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
//首先通过getSharedPreferences()方法得到SharedPreferences对象
                SharedPreferences pref = getSharedPreferences("data",MODE_PRIVATE);
//分别调用它的getString(),getInt(),getBoolean()方法获取前面存储的姓名、年龄和是否已婚
//如果没有找到相应的值就用方法中传入的默认值来代替
                String name = pref.getString("name","");
                int age = pref.getInt("age",0);
                boolean married = pref.getBoolean("married",false);
                Log.d("MainActivity","name is" + name);
                Log.d("MainActivity","age is" + age);
                Log.d("MainAcitivty","married is" + married);
            }
        });
    }**
}

6.3.3实现记住密码功能

打开BroadcastBestPractice项目,修改activity_login.xml,编辑登陆界面的布局。

<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:app="http://schemas.android/apk/res-auto"
    xmlns:tools="http://schemas.android/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".LoginActivity"
    android:orientation="vertical"
    >
		...
    **<LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
使用了新控件CheckBox,是一个复选框控件,用户可以通过点击的方式进行选中和取消。
        <CheckBox
            android:id="@+id/remember_pass"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"   />
        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:text="Remember password"/>
    </LinearLayout>**

    <Button
        android:id="@+id/login"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:text="Login"/>
</LinearLayout>

public class LoginActivity extends BaseActivity {

    **private SharedPreferences pref;
    private SharedPreferences.Editor editor;**
    private EditText accountEdit;
    private EditText passwordEdit;
    private Button login;
    **private CheckBox rememberPass;**

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
**//首先在onCreate()方法中获取到SharedPreferences对象,用了PreferenceManager**
        **pref = PreferenceManager.getDefaultSharedPreferences(this);**
        accountEdit = (EditText)findViewById(R.id.account);
        passwordEdit = (EditText)findViewById(R.id.password);
        **rememberPass = (CheckBox)findViewById(R.id.remember_pass);**
        login = (Button)findViewById(R.id.login);
**//调用SharedPreferences的getBoolean方法去获取remember_password这个键对应的值,
//一开始默认值false**
        **boolean isRemember = pref.getBoolean("remember_pass",false);
        if(!isRemember){

//当用户选中了记住密码复选框,并成功登陆一次之后,remem_password键对应的值就是true了,
//如果再重新启动登录界面,就会从SharedPreferences文件中将保存的账号和密码读取出来填充到文本输入框中,
            String account = pref.getString("account","");
            String password = pref.getString("password","");
            accountEdit.setText(account);
            passwordEdit.setText(password);
            rememberPass.setChecked(true);
        }**
        login.setOnClickListener(new View.OnClickListener() {
            String account = accountEdit.getText().toString();
            String password = passwordEdit.getText().toString();
            @Override
            public void onClick(View v) {
                String account = accountEdit.getText().toString();
                String password = passwordEdit.getText().toString();
                //如果账号是admin且密码为123456就认为登录成功
                if(account.equals("admin") && password.equals("123456")){
                    **editor  = pref.edit();
//登陆成功后,对调用CheckBox的isChecked()检查复选框是否被选中,
//选中了表示用户想要记住密码,这时将remember_password设置为true
//然后把acount和password对应的值都存入到SharedPreferences文件当中并提交。
                    if(rememberPass.isChecked()){
                        editor.putBoolean("remember_password",true);
                        editor.putString("account",account);
                        editor.putString("password",password);
                    } else{
//如果没有被选中,就简单地调用一下clear()方法,将SharedPreferences文件中数据全部清除掉。
                        editor.clear();
                    }
                    editor.apply();**
                    Intent intent = new Intent(LoginActivity.this,MainActivity.class);
                    startActivity(intent);
                    finish();
                }else {
                    Toast.makeText(LoginActivity.this,"account or password is invalid",Toast.LENGTH_SHORT).show();
                }
            }
        });
    }
}

实际项目中将密码以明文存储在SharedPreferences文件中非常不安全,要结合加密算法使用。

接下来学习Android中的数据库技术。

6.4SQLite数据库存储

Android内置数据库。SQLite是一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,通常只需要几百KB的内存,因而特别适合在移动设备上使用。SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务。SQLite又比一般的数据库简单得多,它甚至不用设置用户名和密码就可以使用,Android把这个功能极为强大的数据库嵌入到了系统当中使得本地持久化的功能有了一次质的飞跃。

文件存储和SharedPreferences只适用于存储简单数据和键值对,不适用于存储大量复杂的关系型数据。比如手机短信程序有很多个会话,每个会话中又包含了很多条信息内容,并且大部分会话还可能各自对应了电话簿中某个联系人。使用数据库就可以做得到。

6.4.1创建数据库

Android提供了SQLiteOpenHelper帮助类让我们更加方便地管理数据库,借助这个类就可以非常简单地对数据库进行创建和升级。

SQLiteOpenHelper是个抽象类,我们需要创建一个自己的帮助类去继承它才能使用,SQLiteOpenHelper中有两个抽象方法,分别是onCreate()和onUpgrade(),我们必须在自己的帮助类里重写这两个方法,然后分别在这两个方法中去创建、升级数据库的逻辑

SQLiteOpenHelper中还有两个非常重要的实例方法:getReadableDatabase()getWritableDatabase()。这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则创建一个新的数据库)并返回一个可对数据库进行读写操作的对象。不同的是,当数据库不可写入的时候(如磁盘空间已满),getReadableDatabase()方法返回的对象只读,而gerWritableDatabase() 方法将出现异常。

SQLiteOpenHelper有两个构造方法可重写,一般使用参数少的,参数少的构造方法接收4个参数。第一个参数是Context,有Context才能对数据库进行操作。第二个是数据库名,创建数据库时使用的就是这里指定的名称,第三个参数允许我们在查询数据时返回一个自定义的Cursor,一般传入null。第四个参数表示当前数据库用的版本号,可用于对数据库进行升级操作。构建出SQLiteOpenHelper的实例之后再调用它的getReadableDatabase()或getWritableDatabase()方法就能创建数据库了。数据库文件会存放在/data/data//databases/目录下。此时重写的onCreate() 方法也会得到执行,通常会在这里处理一些创建表的逻辑。

新建DatabaseTest项目。这里我们希望创建一个名为BookStore.db的数据库,然后在这个数据库中新建一张Book表,SQLite的数据类型很简单:integer表示整型、real表示浮点型、text表示文本类型、blob表示二进制类型。

create table Book (
//主键,p**rimary key将id列设为主键**,并用autoincrement关键字表示id列时自增长的
	id integer primary key autoincrement,
	author text,//作者
	price real,//价格,real表示浮点型
	pages integer,//页数,integer表示整形,blob表示二进制类型
	name text)//书名
新建MyDatabaseHelper类继承自SQLiteOpenHelper,要在代码中执行这条SQL语句才能完成建表的操作。
public class MyDatabaseHelper extends SQLiteOpenHelper {

//我们把建表语句定义成了字符串常量
    public static final String CREATE_BOOK = "create table book ( " + "id integer primary key autoincrement, " +
            "author text, " + "price real, " + "pages integer," + "name text)";

    private Context mContext;

    public MyDatabaseHelper(Context context,String name,SQLiteDatabase.CursorFactory factory,int version){
        super(context, name, factory, version);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
//然后在onCreate()方法中又调用了SQLiteDatabase的execSQL()方法执行这条建表语句
        db.execSQL(CREATE_BOOK);
        Toast.makeText(mContext,"Create succeeded",Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
    }
}
//修改activity_main.xml
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:app="http://schemas.android/apk/res-auto"
    xmlns:tools="http://schemas.android/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
创建数据按钮
    <Button
        android:id="@+id/create_database"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Create database"
        tools:ignore="MissingConstraints" />

</LinearLayout>

//修改MainActivity
public class MainActivity extends AppCompatActivity {

    **private MyDatabaseHelper dbHelper;**
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
**//在onCreate()方法中构建了一个MyDatabaseHelper对象,并且
//通过构造函数的参数将数据库名指定为BookStore.db,版本号指定为1**
        **dbHelper = new MyDatabaseHelper(this,"BookStore.db",null,1);**

        Button createDatabase = (Button)findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            **@Override
            public void onClick(View view) {
//在createDatabase按钮的点击事件里调用了getWritableDatabase()方法,
//这样第一次点击Create database按钮时会检测到程序没有BookStore.db这个数据库,
//于是会创建该数据库并调用MyDatabaseHelper中的onCreate()方法,这样Book表就得到了创建,然后弹出一个Toast提示创建成功,再次点击createDatabase按钮时
//会发现此时已经存在BookStore.db数据库了不会再创建
                dbHelper.getWritableDatabase();
            }
        });**
    }
}

使用File Explorer只能看到databases目录下出现了一个BookStore.db文件,Book表是无法通过File Explorer看到的,因此这次我们准备使用adb shell来对数据库和表的创建情况进行检查。

adb是Android SDK中自带的一个调试工具,使用这个工具可以直接对连接在电脑上的手机或模拟器进行调试操作,存放在sdk的platform-tools目录下,如果想要在命令行中使用这个工具,需要先把它的路径配置到环境变量里。

6.4.2升级数据库

MyDatabaseHelper()中另一个需要重写的方法是onUpgrade()方法是用于对数据库进行升级的,它在整个数据库的管理工作中起着非常重要的作用,可千万不能忽视。我们想再添加一张Category表用于记录图书的分类,Category表中有id(主键)、分类名和分类代码这几个列,那么建表语句:

create table Category(
	id integer primary key autoincrement,
	category_name text.
	category_code integer)
}

这条建表语句添加到MyDatabaseHelper中
public class MyDatabaseHelper extends SQLiteOpenHelper {

    public static final String CREATE_BOOK = "create table book ( " + "id integer primary key autoincrement, " +
            "author text, " + "price real, " + "pages integer," + "name text)";

    **public static final String CREATE_CATEGORY = "create table Category (" + "id integer primary key autoincrement," +
            "category_name text, " + "category_code integer)";**
    private Context mContext;

    public MyDatabaseHelper(Context context,String name,SQLiteDatabase.CursorFactory factory,int version){
        super(context, name, factory, version);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK);
        **db.execSQL(CREATE_CATEGORY);**
        Toast.makeText(mContext,"Create succeeded",Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("drop table if exists Book");
        db.execSQL("drop table if exists Category");
        onCreate(db);
    }
}

点击按钮没有弹出创建成功提示,因为此时BookStore.db数据库已经存在了,之后不管怎么点,onCreate()方法都不会再次执行,新添加的表就无法创建了

要先将程序卸载掉,然后重新运行,这时BookStore.db数据库已经不存在了,再点击Create database按钮,MyDatabaseHelper中的onCreate()方法就会执行,就创建成功了。

不过通过卸载程序的方式来新增一张表很极端,我们只要运用SQLiteOpenHelper的升级功能就能很轻松解决这个问题。

public class MyDatabaseHelper extends SQLiteOpenHelper {
		...
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
//执行了两条DROP语句,如果发现数据库已经存在BOOK表或Category表,
//就将两张表删除掉,再调用onCreate()方法重新创建,
//这里先将已经存在的表删除掉,因为如果在创建时发现这张表已经存在了,就会直接报错、
        **db.execSQL("drop table if exists Book");
        db.execSQL("drop table if exists Category");
        onCreate(db);**
    }
}

第四个参数我们传入一个比1大的数就可以让onUpgrade()方法执行,修改MainActivity数据库版本号指定为2表示我们对数据库进行升级了,然后重新运行程序,再adb shell中打开BookStore.db数据库然后键入.table命令,接着键入.schema命令查看建表语句。

6.4.3添加数据

已经掌握了创建和升级数据库的方法,接下来学习一下如何对表中数据进行操作。我们对数据进行的操作无非4种,CRUD。其中**C代表添加(Create),R代表查询(Retrieve),U代表更新(Update),D代表删除(Delete)。**每一种操作又各自对应了一种SQL命令,添加数据时使用insert,查询数据时使用select,更新数据时使用update,删除数据时使用delete。Android提供了一系列辅助性方法,使得在Android中即使不编写SQL语句,也能轻松完成所有CRUD操作。

SQLiteOpenHelper的getReadableDatabase()或getWritableDatabase()方法用于创建和升级数据库,还能返回一个对象SQLiteDatabase对象,借助SQLiteDatabase对象就可以对数据进行CRUD操作。

SQLiteDatabase中提供了一个insert()方法专门用于添加数据,接收3个参数,第一个参数是表名(希望向哪张表添加数据),第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL。第三个参数是个ContentValues对象,它提供了一系列put()方法重载,用于向ContentValues中添加数据,只需要将表中每个列名以及相应待添加数据传入。

添加add_data按钮用于添加数据
public class MainActivity extends AppCompatActivity {

    private MyDatabaseHelper dbHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //在onCreate方法中构建了一个MyDatabaseHelper对象,并且通过构造函数的参数将数据库名指定为BookStore.db
        dbHelper = new MyDatabaseHelper(this,"BookStore.db",null,2);
        ...
        **Button addData = (Button)findViewById(R.id.add_data);
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
        //在**add_data按钮**的点击事件里,我们先获取到了SQLiteDatabase对象,
                SQLiteDatabase db= dbHelper.getWritableDatabase();
//然后使用ContentValues来对要添加的数据进行组装,只对Book表里其中四列数据进行组装
//id那一列并没给它赋值,这是因为创建表时我们将id列设为自增长了
//id列的值在入库时会自动生成,所以不需要手动赋值,
                ContentValues values = new ContentValues();
                //开始组装第一条数据
                values.put("name","The Da Vinci Code");
                values.put("author","Dan Brown");
                values.put("pages",454);
                values.put("price",16.96);

//调用insert()方法将数据添加到表当中,使用ContentValues分别组装了两次不同内容,
//并调用了两次insert()方法
                db.insert("Book",null,values);//插入第一条数据

//先清空再组装
                values.clear();
                //开始组装第二条数据
                values.put("name","The Lost Symbol");
                values.put("author","Dan Brown");
                values.put("pages",510);
                values.put("price",19.95);
                db.insert("Book",null,values);//插入第二条数据
            }
        });**
    }
}

已解决

select *from book切记加分号。。。。

6.4.4更新数据

学完了如何向表中添加数据,接下来学如何修改已有数据,update()方法接收4个参数,第一个参数和insert()方法一样是表名,指定去更新哪张表中的数据,第二个参数是ContentValues对象,要把更新数据在这里组装进去,第三四个参数用于约束更新某一行或几行中的数据不指定默认更新所有行

修改activity_main.xml
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:app="http://schemas.android/apk/res-auto"
    xmlns:tools="http://schemas.android/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
		...
    **<Button
        android:id="@+id/update_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Update data" />**

</LinearLayout>

public class MainActivity extends AppCompatActivity {

    private MyDatabaseHelper dbHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
	       ...
        **Button updateData = (Button)findViewById(R.id.update_data);
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
    //更新数据点击按钮里构建一个ContentValues
                ContentValues values = new ContentValues();
    //给ContentValues指定了一组数据把价格这一列更新为10.99
                values.put("price",10.9999999999);

//调用SQLiteDatabase的update()方法执行具体的更新操作。
//第三、四个参数用来指定具体更新那几行,
//第三个参数对应的是SQL语句的where部分,表示更新name等于?的行,
//?是一个占位符
//可以通过第四个参数提供的一个字符串数组为第三个参数中的每个占位符指定相应的内容
//因此意图是将名字是The Da Vinci Code这本书的价格改为10.99
                db.update("Book",values,"name = ?" , new String[]{"The Da Vinci Code"});
            }
        });**
    }
}

6.4.5删除数据

SQLiteDatabase类中提供了delete()方法,专门用于删除数据,这个方法接收3个参数,第一个参数仍是表名,第二三个参数用于约束删除某一行或某几行的数据,不指定的话默认删除所有行

<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:app="http://schemas.android/apk/res-auto"
    xmlns:tools="http://schemas.android/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
		...
    <Button
        android:id="@+id/delete_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Delete data" />

</LinearLayout>

public class MainActivity extends AppCompatActivity {

    private MyDatabaseHelper dbHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //在onCreate方法中构建了一个MyDatabaseHelper对象,并且通过构造函数的参数将数据库名指定为BookStore.db
        dbHelper = new MyDatabaseHelper(this,"BookStore.db",null,2);
        ...
        **Button deleteButton = (Button)findViewById(R.id.delete_data);
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                deleteButton.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        SQLiteDatabase db = dbHelper.getWritableDatabase();
//通过第一个参数指明删去Book表中的数据第二、第三个参数来指定仅删除那些页数超过50页的书
                        db.delete("Book","pages > ?",new String[]{"500"});
                    }
                });
            }
        });**
    }
}

6.4.6查询数据

SQL的全称是Structured Query Language,中文是结构化查询语言,它的大部分功能都体现在**”查“**字上,而”增删改“只是其中一小部分功能。查询涉及的内容太多了,这里只介绍Android上的查询功能。

SQLiteDatabase提供了query()方法用于对数据进行查询,最短的方法重载也要传入7个参数,第一个还是表名表示想在哪张表中查询数据。第二个参数用于指定查询哪几列,不指定默认查询所有列,第三四个参数用于约束查询某一行或者某几行的数据,不指定则默认查询所有行,第五个参数用于指定需要去group by的列,不指定则表示不对查询结果进行group by操作,第六个参数用于对group by之后的数据进行进一步的过滤,不指定则表示不进行过滤。第七个参数用于指定查询结果的排序方式,不指定则使用默认排序方式。

Untitled

虽然query()方法参数很多,但我们不必为每条查询语句都指定所有参数,多数情况下只需要传入少数几个参数就可以完成查询操作,调用query()方法后会返回一个Cursor对象,查询到的所有数据都将从这个对象中取出。

public class MainActivity extends AppCompatActivity {

    private MyDatabaseHelper dbHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
	      ...
        **Button queryButton = (Button)findViewById(R.id.query_data);
        queryButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                //查询Book表中所有的数据
//查询按钮的点击事件调用SQLiteDatabase的query()方法去查询数据,只使用了第一个参数知名查询Book表,
//后面全为null表示希望查询这张表中所有数据。查完后得到一个Cursor对象
                Cursor cursor = db.query("Book",null,null,null,null,null,null);
//接着调用Cursor对象的moveToFirst()方法将数据指针移动到第一行,
//然后进入一个循环,去遍历查询到的每一行数据。
                if(cursor.moveToFirst()){
                    do{
//遍历Cursor对象,取出数据并打印
//在这个循环中可以通过Cursor的getColumnIndex()方法获取某一列在表中对应的位置索引
//然后将这个索引传入到相应的取值方法中,就可以得到从数据库中读取到的数据了。
                        String name =  cursor.getString(cursor.getColumnIndex("name"));
                        String author = cursor.getString(cursor.getColumnIndex("author"));
                        int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                        double price = cursor.getDouble(cursor.getColumnIndex("price"));
                        //接着我们使用Log的方式将取出的数据打印出来。
                        Log.d("MainActivity","book name is"+name);
                        Log.d("MainActivity","book author is"+author);
                        Log.d("MainActivity","book pages is"+pages);
                        Log.d("MainActivity","book price is"+price);
                    }while(cursor.moveToNext());
                }
                cursor.close();
            }
        });**
    }
}

6.4.7使用SQL操作数据库

可以直接使用SQL来操作数据库。

  • 添加数据的方法如下:
db.execSQL("insert into Book (name,author,pages,price)values(?,?,?,?))",
				new String[]{"The Da Vinci Code","Dan Brown","454","16.96"});
db.execSQL("insert into Book"(name,author,pages,pages,price)values(?,?,?,?)",
				new String[]{"The Lost Symbol","Dan Brown","510","19.95"});
  • 更新数据的方法如下:

db.execSQL("update Book set price = ? where name = ?" , new String[]{"10.99","The Da Vinci Code"});

  • 删除数据的方法如下:

db.execSQL("delete from Book where pages > ?",new String[]{"500"});

  • 查询数据的方法如下:

db.rawQuery("select * from Book",null);

除了查询数据调用的是SQLiteDatabase的rawQuery()方法,其他的操作都是调用的execSQL()方法。

6.5使用LitePal操作数据库

新建一个LitePalTest项目

6.5.1LitePal简介

正式开始接触第一个开源库——LitePal。LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式、并将我们平时开发最常用的一些数据库进行了封装,使得不用编写一行SQL语句就可以完成各种建表和增删改查的操作。LitePal项目主页上也有详细的使用文档。

6.5.2配置LitePal

新建asset目录,下新建一个litepal.xml文件

//build.gradle
dependencies {
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.2.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    compile 'org.litepal.android:core:1.4.1'
}
<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <dbname value = "BookStore"></dbname>//dbname用于指定数据库名
    <version value="1"/>//version用于指定数据库版本号
    <list>//用于指定所有映射模型
    </list>
</litepal>
<manifest xmlns:android="http://schemas.android/apk/res/android"
    xmlns:tools="http://schemas.android/tools"
    package="com.example.litepaltest">

    <application
//application配置为org.litepal.LitePalApplication这样才能让LitePal所有功能都可以正常工作
//13章学习application更多内容
        android:name="org.litepal.LitePalApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.LitePalTest"
        tools:ignore="MissingClass">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

6.5.3创建和升级数据库

之前你创建数据库自定义一个类继承自SQLiteOpenHelper,然后在onCreate()方法中编写建表语句来实现,复制DatabaseTest项目activity_main.xml布局

我们使用的编程语言是面向对象语言,使用的数据库是关系型数据库(关系映射(ORM)模式),面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是对象关系映射。

对象关系映射模式赋予我们一个强大的功能就是可以用面向对象的思维来操作数据库,而不用再和SQL语句打交道,在SQL中建一张book表需要先去分析表中应该包含哪些列,然后再编写出一条建表语句最后在自定义的SQLiteOpenHelper中去执行这条建表语句。但是使用LitePal,可以用面向对象思维实现同样功能,定义一个Book类

package com.example.mydatabasehelper;

public class Book {
    private int id;
    private String author;
    private double price;
    private int pages;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getAuthor() {
        return author;
    }

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

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public int getPages() {
        return pages;
    }

    public void setPages(int pages) {
        this.pages = pages;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

这是一个典型的Java bean,Book就对应了数据库中的Book表,而类中的每一个字段分别对应了表中的每一列。修改litepal.xml

<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <dbname value = "BookStore"></dbname>
    <version value="1"/>
    <list>
//<mapping>标签来声明我们要配置的映射模型类,注意一定要使用完整的类名,不管有多少模型类需要映射,都使用同样方式配置在<list>标签下
        <mapping class="com.example.litepaltest.Book"></mapping>
    </list>
</litepal>

工作已完成,现在任意一次数据库操作,BookStore.db类数据库应该就会自动创建出来,修改MainAcitivty

public class MainActivity extends AppCompatActivity {

     @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button createDatabase  = (Button)findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                LitePal.getDatabase();
            }
        });
    }
}

调用LitePal.getDatabase()方法就是一次最简单的数据库操作,只要点击一下按钮,数据库就会自动创建完成。

SQLiteOpenHelper升级数据库需要先把之前的表drop掉,然后再重新创建,每当重新升级一次数据库,之前表中的数据全没了。

可以通过复杂逻辑控制避免这种情况,但是维护成本很高,有了LitePal升级数据库只需要改想改的内容然后版本号加1。

在Book表中添加一个press(出版社列),直接修改Book类中的代码,添加一个press字段

public class Book {
    private int id;
    private String author;
    private double price;
    private int pages;
    private String name;
    private String press;

    public String getPress() {
        return press;
    }

    public void setPress(String press) {
        this.press = press;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getAuthor() {
        return author;
    }

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

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public int getPages() {
        return pages;
    }

    public void setPages(int pages) {
        this.pages = pages;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
public class Ctegory {
    private int id;
    private String categoryCode;
    private int categoryName;

    public void setId(int id) {
        this.id = id;
    }

    public void setCategoryCode(String categoryCode) {
        this.categoryCode = categoryCode;
    }

    public void setCategoryName(int categoryName) {
        this.categoryName = categoryName;
    }
}
<LinearLayout xmlns:android="http://schemas.android/apk/res/android"
    xmlns:app="http://schemas.android/apk/res-auto"
    xmlns:tools="http://schemas.android/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/create_database"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Create database"
        tools:ignore="MissingConstraints" />
    <Button
        android:id="@+id/add_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Add data" />
    <Button
        android:id="@+id/update_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Update data" />
    <Button
        android:id="@+id/delete_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Delete data" />
    <Button
        android:id="@+id/query_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Query data" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <dbname value = "BookStore"></dbname>
    <version value="1"/>
    <list>
        <mapping class="com.example.litepaltest.Book"></mapping>
        <mapping class="com.example.litepaltest.Category"></mapping>
    </list>
</litepal>

6.5.4使用LitePal添加数据

之前添加数据需要创建出一个ContentValues对象,然后将所有要添加的数据put到这个ContentValues对象当中,最后调用SQLiteDatabase的insert()方法将数据添加到数据库中。

LitePal添加数据只要创建出实例将所要存储的数据设置好,最后调用save()方法

LitePal进行表管理操作时不需要模型类有任何的继承结构,但是进行CRUD操作就必须继承自DataSupport类,因此我们需要先把继承结构加上。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button createDatabase  = (Button)findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                LitePal.getDatabase();
            }
        });
        Button addData = (Button)findViewById(R.id.add_data);
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book = new Book();
                book.setName("The Da Vinci Code");
                book.setAuthor("Dan Brown");
                book.setPages(454);
                book.setPrice(16.96);
                book.setPress("Unknow");
                book.save();
            }
        });
    }

}

创建出一个Book实例,然后调用Book类中的各种set方法对数据进行设置,最后调用book.save()方法就能完成数据添加操作。save方法是从DataSupport类中继承来的,除了saveDataSupport类还给我们提供了丰富的CRUD方法

6.5.5使用LitePal更新数据

最简单一种更新方式是对已存储对象重新设值然后调用model.isSaved()方法结果来判断的,返回true就表示已存储,返回false就表示未存储。

两种情况下model.isSaved()方法会返回true,一种情况是已经调用过model.save()方法添加数据,此时model会被认为是已存储的对象,另一种情况是model对象是通过LitePal提供的API查出来的,由于是从数据库中查到的对象,因此会被认为是已存储的对象。

用第一种情况:只能对已存储的对象进行操作

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button createDatabase  = (Button)findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                LitePal.getDatabase();
            }
        });
        Button addData = (Button)findViewById(R.id.add_data);
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book = new Book();
                book.setName("The Da Vinci Code");
                book.setAuthor("Dan Brown");
                book.setPages(510);
                book.setPrice(19.95);
                book.setPress("Unknow");
                book.save();
                book.setPrice(10.99);
                book.save();
            }
        });
        Button updataData = (Button)findViewById(R.id.update_data);
        updataData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book = new Book();
                book.setName("The Da Vinci Code");
                book.setAuthor("Dan Brown");
                book.setPages(510);
                book.setPrice(19.95);
                book.setPress("Unknow");
                book.save();
                book.setPrice(10.99);
                book.save();
                
//                Book book = new Book();
//                book.setPrice(14.95);
//                book.setPress("Anchor");
//                book.updateAll("name = ? and author = ?","The Lost Symbol","Dan Brown");
            }
        });
    }
}

另一种方式:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button createDatabase  = (Button)findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                LitePal.getDatabase();
            }
        });
        Button addData = (Button)findViewById(R.id.add_data);
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book = new Book();
                book.setName("The Da Vinci Code");
                book.setAuthor("Dan Brown");
                book.setPages(510);
                book.setPrice(19.95);
                book.setPress("Unknow");
                book.save();
                book.setPrice(10.99);
                book.save();
            }
        });
        Button updataData = (Button)findViewById(R.id.update_data);
        updataData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
//                Book book = new Book();
//                book.setName("The Da Vinci Code");
//                book.setAuthor("Dan Brown");
//                book.setPages(510);
//                book.setPrice(19.95);
//                book.setPress("Unknow");
//                book.save();
//                book.setPrice(10.99);
//                book.save();

                Book book = new Book();
                book.setPrice(14.95);
                book.setPress("Anchor");
                book.updateAll("name = ? and author = ?","The Lost Symbol","Dan Brown");
            }
        });
    }
}

updateAll()方法中可以指定一个条件约束,和SQLiteDatabase中update()方法的where参数部分有点类似,但更加简洁,如果不指定条件语句表示更新所有数据。这里指定所有书名是The Lost Symbol并且作者是Dan Brown的书价格更新为14.95,出版社更新为Anchor。

想要将数据更新成默认值,LitePal统一提供了一个setToDefault()方法,然后传入相应的列名。

Book book = new Book();
book.setToDefault("pages);
book.updateAll();

意思是将所有书页数更新成0.因为updateAll()方法中没有指定约束条件。

6.5.6使用LitePal删除数据

使用LitePal删除数据方式主要有两种,一种直接调用已存储对象的delete()方法,已存储对象:调用过save()方法的对象,或是通过LitePal提供的查询API查出来的对象。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button createDatabase  = (Button)findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                LitePal.getDatabase();
            }
        });
        Button addData = (Button)findViewById(R.id.add_data);
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Book book = new Book();
                book.setName("The Da Vinci Code");
                book.setAuthor("Dan Brown");
                book.setPages(510);
                book.setPrice(19.95);
                book.setPress("Unknow");
                book.save();
                book.setPrice(10.99);
                book.save();
            }
        });
        Button updataData = (Button)findViewById(R.id.update_data);
        updataData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
//                Book book = new Book();
//                book.setName("The Da Vinci Code");
//                book.setAuthor("Dan Brown");
//                book.setPages(510);
//                book.setPrice(19.95);
//                book.setPress("Unknow");
//                book.save();
//                book.setPrice(10.99);
//                book.save();

                Book book = new Book();
                book.setPrice(14.95);
                book.setPress("Anchor");
                book.updateAll("name = ? and author = ?","The Lost Symbol","Dan Brown");
            }
        });
        Button deleteButton = (Button)findViewById(R.id.delete_data);
        deleteButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                DataSupport.deleteAll(Book.class,"price < ?","15");
            }
        });
    }
}

这里调用了 DataSupport.deleteAll()方法来删除数据,deleteAll()方法第一个参数用于指定哪张表中的数据,Book.class意味着删除Book表中的数据,后面的参数用于指定约束条件。删除价格低于15的书。deleteAll()方法如果不指定约束条件,意味着删除表中所有数据,和updateAll()方法比较相似。

6.5.7使用LitePal查询数据

LitePal在查询API方面的设计极为人性化,之前的query()即使多数参数用不到也必须传入null

Cursor cursor = db.query("Book",null,null,null,null,null,null);

LitePal只需要这样写

List<Book> books = DataSupport.findAll(Book.class);

findAll()方法返回值是一个Book类型的List集合,我们不用像之前通过Cursor对象一行行取值

修改MainActivity

select()方法用于指定查询哪几列的数据,对应了SQL当中的select关键字。比如只查name和 author这两列的数据,就可以这样写:
List books = DataSupport.select ( “name” ,“author” ).find (Book.class) ;

where()方法用于指定查询的约束条件,对应了SQL当中的 where关键字。比如只查页数大于400的数据,就可以这样写:
List books = DataSupport.where(“pages > ?”,“400” ). find(Book.class);

order()方法用于指定结果的排序方式,对应了SQL当中的 order by关键字。比如将查询结果按照书价从高到低排序,就可以这样写:
List books = DataSupport.order ( “price desc” ).find (Book.class) ;其中desc表示降序排列,asc或者不写表示升序排列。

limit()方法用于指定查询结果的数量,比如只查表中的前3条数据,就可以这样写:List books = DataSupport.limit(3).find ( Book.class);

offset()方法用于指定查询结果的偏移量,比如查询表中的第2条、第3条、第4条数据,就可以这样写:
List books = DataSupport.limit(3).offset(1).find(Book.class);
由于limit(3)查询到的是前3条数据,这里我们再加上offset(1)进行一个位置的偏移,就能实现查询第2条、第3条、第4条数据的功能了。limit()和offset()方法共同对应了SQL当中的limit关键字。
当然,你还可以对这5个方法进行任意的连缀组合,来完成一个比较复杂的查询操作:
List books = DataSupport. select(" name",“author”, “pages")
. where(“pages > ?”,“400” )
. order ( " pages")
, limit(10)
. offset(10)
, find ( Book.class);
这段代码就表示,查询Book表中第11~20 条满足页数大于400这个条件的name、author
和pages这3列数据,并将查询结果按照页数升序排列。

LitePal仍然支持使用原生的SQL来进行查询:
Cursor C = DataSupport. findBySQL(“select * from Book where pages > ? and price < ?”,
“400”, “20”);第一个参数用于指定SQL语句,后面的参数用于指定占位符的值。findBySQL()方法返回的是一个Cursor对象,接下来还要通过之前所学方法一一取出。

6.6小结与点评

本章主要详细学习了Android常用的数据持久化方式,包括文件存储、SharedPreferences存储以及数据库存储。文件存储适用于存储简单的文本数据或二进制数据,SharedPreferences适用于存储键值对,数据库适用于存储复杂的关系型数据。掌握了Android中的持久化技术,接下来继续学习Android剩余的四大组件内容提供器。

本文标签: 碎片 持久 代码 数据库 笔记