主頁 > 移動端開發 > 《第一行代碼:Android篇》學習筆記(六)

《第一行代碼:Android篇》學習筆記(六)

2022-05-12 08:43:15 移動端開發

本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了,
每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以后閱讀和查閱,最后,感激感激郭霖先生提供這么好的書籍,

第6章 資料存盤全方案——詳解持久化技術

任何一個應用程式,其實說白了就是在不停地和資料打交道,聊QQ、看新聞、刷微博,所關心的都是里面的資料,沒有資料的應用程式就變成了一個空殼子,對用戶來說沒有任何實際用途,現在多數的資料基本都是由用戶產生的,比如你發微博、評論新聞,其實都是在產生資料,

瞬時資料就是指那些存盤在記憶體當中,有可能會因為程式關倍訓其他原因導致記憶體被回收而丟失的資料,這對于一些關鍵性的資料資訊來說是絕對不能容忍的,誰都不希望自己剛發出去的一條微博,重繪一下就沒了吧,那么怎樣才能保證一些關鍵性的資料不會丟失呢?這就需要用到資料持久化技術了,

6.1 持久化技術簡介

資料持久化就是指將那些記憶體中的瞬時資料保存到存盤設備中,保證即使在手機或電腦關機的情況下,這些資料仍然不會丟失,保存在記憶體中的資料是處于瞬時狀態的,而保存在存盤設備中的資料是處于持久狀態的,持久化技術則提供了一種機制可以讓資料在瞬時狀態和持久狀態之間進行轉換,

持久化技術被廣泛應用于各種程式設計的領域當中,而本書中要探討的自然是Android中的資料持久化技術,Android系統中主要提供了3種方式用于簡單地實作資料持久化功能,即檔案存盤、SharedPreferences存盤以及資料庫存盤,

當然,除了這3種方式之外,你還可以將資料保存在手機的SD卡中,不過使用檔案、SharedPreferences或資料庫來保存資料會相對更簡單一些,而且比起將資料保存在SD卡中會更加地安全,

6.2 檔案存盤

檔案存盤是Android中最基本的一種資料存盤方式,它不對存盤的內容進行任何的格式化處理,所有資料都是原封不動地保存到檔案當中的,因而它比較適合用于存盤一些簡單的文本資料或二進制資料,

如果你想使用檔案存盤的方式來保存一些較為復雜的文本資料,就需要定義一套自己的格式規范,這樣可以方便之后將資料從檔案中重新決議出來,那么首先我們就來看一看,Android中是如何通過檔案來保存資料的,

6.2.1 將資料存盤到檔案中

Context類中提供了一個openFileOutput()方法,可以用于將資料存盤到指定的檔案中,

  • 第一個引數是檔案名,在檔案創建的時候使用的就是這個名稱,注意這里指定的檔案名不可以包含路徑,因為所有的檔案都是默認存盤到/data/data//files/目錄下的,
  • 第二個引數是檔案的操作模式,主要有兩種模式可選,MODE_PRIVATE和MODE_APPEND,其中MODE_PRIVATE是默認的操作模式,表示當指定同樣檔案名的時候,所寫入的內容將會覆寫原檔案中的內容,而MODE_APPEND則表示如果該檔案已存在,就往檔案里面追加內容,不存在就創建新檔案,

其實檔案的操作模式本來還有另外兩種:MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE,這兩種模式表示允許其他的應用程式對我們程式中的檔案進行讀寫操作,不過由于這兩種模式過于危險,很容易引起應用的安全性漏洞,已在Android 4.2版本中被廢棄,

openFileOutput()方法回傳的是一個FileOutputStream物件,得到了這個物件之后就可以使用Java流的方式將資料寫入到檔案中了,以下是一段簡單的代碼示例,展示了如何將一段文本內容保存到檔案中:

public void save() {
	String data = "https://www.cnblogs.com/1693977889zz/archive/2022/05/11/Data to save";
	FileOutputStream out = null;
	BufferedWriter writer = null;
	try {
		out = openFileOutput("data",Context.MODE_PRIVATE);
		writer = new BufferedWriter(OutputStreamWriter(out));
		writer.write(data);
	} catch (IOException e) {
		e.printStackTrace();
	} finally {
		try {
			if (writer !=null) {
				writer.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

上面通過openFileOutput()方法能夠得到一個FileOutputStream物件,然后再借助它構建出一個OutputStreamWriter物件,接著再使用OutputStreamWriter構建出一個BufferedWriter物件,這樣你就可以通過BufferedWriter來將文本內容寫入到檔案中了,

下面就撰寫一個完整的例子,借此學習一下如何在Android專案中使用檔案存盤的技術,首先創建一個FilePersistenceTest專案,并修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
    <EditText
        android:id="@+id/edit"
        android:layout_
        android:layout_height="wrap_content"
        android:hint="Type something here"/>
</LinearLayout>

在布局中加入了一個EditText,用于輸入文本內容,現在運行一下程式,界面上會有一個文本輸入框,然后在文本輸入框中隨意輸入點什么內容,再按下Back鍵,這時輸入的內容肯定就已經丟失了,因為它只是瞬時資料,在活動被銷毀后就會被回收,而這里我們要做的,就是在資料被回收之前,將它存盤到檔案當中,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.filepersistencetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.os.Bundle;
import android.widget.EditText;

import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;

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);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        String inputText = edit.getText().toString();
        save(inputText);
    }
    public void save(String inputText) {
        FileOutputStream out = null;
        BufferedWriter writer = null;
        try {
            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();
            }
        }
    }
}

首先在onCreate()方法中獲取了EditText的實體,然后重寫了onDestroy()方法,這樣就可以保證在活動銷毀之前一定會呼叫這個方法,在onDestroy()方法中我們獲取了EditText中輸入的內容,并呼叫save()方法把輸入的內容存盤到檔案中,檔案命名為data,

現在重新運行一下程式,并在EditText中輸入一些內容,如圖所示:

image

然后按下Back鍵關閉程式,這時我們輸入的內容就已經保存到檔案中了,

那么如何才能證實資料確實已經保存成功了呢?

書中描述:“我們可以借助Android DeviceMonitor工具來查看一下,點擊Android Studio導航欄中的Tools→Android,會看到如圖所示的工具串列”,已經過時了,不是這樣的的,

現在如下圖所示尋找:

image

點擊 Device File Explorer 進入其標簽頁,在這里找到/data/data/com.zhouzhou.filepersistencetest/files/目錄,可以看到生成了一個data檔案,(注:Android 7.0系統的模擬器可能無法正常查看FileExplorer中的內容,這或許是新版模擬器的一個bug,可能會在未來的版本中修復,如果你遇到了這種情況,創建一個Android 6.0系統的模擬器即可解決,)如圖所示:

image

然后書中所述:“點擊下圖中左邊的按鈕” 然后,選中檔案,滑鼠右擊——save as 可將這個檔案匯出到電腦上:

image

image

使用記事本打開這個檔案,里面的內容如圖:

image

這樣就證實了,在EditText中輸入的內容確實已經成功保存到檔案中了,不過只是成功將資料保存下來還不夠,我們還需要想辦法在下次啟動程式的時候讓這些資料能夠還原到EditText中,因此接下來我們就要學習一下如何從檔案中讀取資料,

6.2.2 從檔案中讀取資料

類似于將資料存盤到檔案中,Context類中還提供了一個openFileInput()方法,用于從檔案中讀取資料,這個方法要比openFileOutput()簡單一些,它只接收一個引數,即要讀取的檔案名,然后系統會自動到/data/data//files/目錄下去加載這個檔案,并回傳一個FileInputStream物件,得到了這個物件之后再通過Java流的方式就可以將資料讀取出來了,

以下是一段簡單的代碼示例,展示了如何從檔案中讀取文本資料:

    public 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();
    }

在這段代碼中,首先通過openFileInput()方法獲取到了一個FileInputStream物件,然后借助它又構建出了一個InputStreamReader物件,接著再使用InputStreamReader構建出一個BufferedReader物件,這樣我們就可以通過BufferedReader進行一行行地讀取,把檔案中所有的文本內容全部讀取出來,并存放在一個StringBuilder物件中,最后將讀取到的內容回傳就可以了,

了解了從檔案中讀取資料的方法,來繼續完善上一小節中的例子,使得重新啟動程式時EditText中能夠保留我們上次輸入的內容,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.filepersistencetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.EditText;
import android.widget.Toast;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

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);
        String inputText = load();
        if (!TextUtils.isEmpty(inputText)) {
            edit.setText(inputText);
            //將輸入游標移動到文本的末尾位置以便于繼續輸入
            edit.setSelection(inputText.length());
            Toast.makeText(this,"Restoring succeeded",Toast.LENGTH_SHORT).show();
        }
    }
    public 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();
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        String inputText = edit.getText().toString();
        save(inputText);
    }
    public void save(String inputText) {
        FileOutputStream out = null;
        BufferedWriter writer = null;
        try {
            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();
            }
        }
    }
}

這里的思路非常簡單,在onCreate()方法中呼叫load()方法來讀取檔案中存盤的文本內容,如果讀到的內容不為null,就呼叫EditText的setText()方法將內容填充到EditText里,并呼叫setSelection()方法將輸入游標移動到文本的末尾位置以便于繼續輸入,然后彈出一句還原成功的提示,

注意,上述代碼在對字串進行非空判斷的時候使用了TextUtils.isEmpty()方法,這是一個非常好用的方法,它可以一次性進行兩種空值的判斷,當傳入的字串等于null或者等于空字串的時候,這個方法都會回傳true,從而使得我們不需要先單獨判斷這兩種空值再使用邏輯運算子連接起來了,

現在重新運行一下程式,剛才保存的Content字串肯定會被填充到EditText中:

image

然后,撰寫一點其他的內容,比如在EditText中輸入Hello,接著按下Back鍵退出程式,再重新啟動程式,這時剛才輸入的內容并不會丟失,而是還原到了EditText中,如圖所示:

image

檔案存盤方面的知識,其實所用到的核心技術就是Context類中提供的openFileInput()和openFileOutput()方法,之后就是利用Java的各種流來進行讀寫操作,

檔案存盤的方式并不適合用于保存一些較為復雜的文本資料,下面學習一下Android中另一種資料持久化的方式,它比檔案存盤更加簡單易用,而且可以很方便地對某一指定的資料進行讀寫操作,

6.3 SharedPreferences存盤

不同于檔案的存盤方式,SharedPreferences是使用鍵值對的方式來存盤資料的,

也就是說,當保存一條資料的時候,需要給這條資料提供一個對應的鍵,這樣在讀取資料的時候就可以通過這個鍵把相應的值取出來,而且SharedPreferences還支持多種不同的資料型別存盤,如果存盤的資料型別是整型,那么讀取出來的資料也是整型的;如果存盤的資料是一個字串,那么讀取出來的資料仍然是字串,

這樣你應該就能明顯地感覺到,使用SharedPreferences來進行資料持久化要比使用檔案方便很多,

6.3.1 將資料存盤到SharedPreferences中

要想使用SharedPreferences來存盤資料,首先需要獲取到SharedPreferences物件,Android中主要提供了3種方法用于得到SharedPreferences物件,

  1. Context類中的getSharedPreferences()方法

此方法接收兩個引數,第一個引數用于指定SharedPreferences檔案的名稱,如果指定的檔案不存在則會創建一個,SharedPreferences檔案都是存放在/data/data//shared_prefs/目錄下的,

第二個引數用于指定操作模式,目前只有MODE_PRIVATE這一種模式可選,它是默認的操作模式,和直接傳入0效果是相同的,表示只有當前的應用程式才可以對這個SharedPreferences檔案進行讀寫,

其他幾種操作模式均已被廢棄,MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE這兩種模式是在Android 4.2版本中被廢棄的,MODE_MULTI_PROCESS模式是在Android6.0版本中被廢棄的,

  1. Activity類中的getPreferences()方法

這個方法和Context中的getSharedPreferences()方法很相似,不過它只接收一個操作模式引數,因為使用這個方法時會自動將當前活動的類名作為SharedPreferences的檔案名,

  1. PreferenceManager類中的getDefaultSharedPreferences()方法

這是一個靜態方法,它接收一個Context引數,并自動使用當前應用程式的包名作為前綴來命名SharedPreferences檔案,得到了SharedPreferences物件之后,就可以開始向SharedPreferences檔案中存盤資料了,主要可以分為3步實作,

(1) 呼叫SharedPreferences物件的edit()方法來獲取一個SharedPreferences.Editor物件,

(2) 向SharedPreferences.Editor物件中添加資料,比如添加一個布爾型資料就使用putBoolean()方法,添加一個字串則使用putString()方法,以此類推,

(3) 呼叫apply()方法將添加的資料提交,從而完成資料存盤操作,

通過一個例子來體驗一下SharedPreferences存盤的用法吧,新建一個SharedPreferencesTest專案,然后修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/save_data"
        android:text="Save data"/>
</LinearLayout>

只是簡單地放置了一個按鈕,用于將一些資料存盤到SharedPreferences檔案當中,然后修改MainActivity中的代碼,如下所示:

package com.zhouzhou.sharedpreferencestest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button saveData = https://www.cnblogs.com/1693977889zz/archive/2022/05/11/(Button) findViewById(R.id.save_data);
        saveData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SharedPreferences.Editor editor = getSharedPreferences("data",MODE_PRIVATE).edit();
                editor.putString("name","Zhouzhou");
                editor.putInt("age",28);
                editor.putBoolean("married",false);
                editor.apply();
            }
        });
    }
}

可以看到,這里首先給按鈕注冊了一個點擊事件,然后在點擊事件中通過getSharedPreferences()方法指定SharedPreferences的檔案名為data,并得到了SharedPreferences.Editor物件,

接著向這個物件中添加了3條不同型別的資料,最后呼叫apply()方法進行提交,從而完成了資料存盤的操作,

現在就可以運行一下程式了,進入程式的主界面后,點擊一下Save data按鈕,這時的資料應該已經保存成功了,證實一下,進入/data/data/com.zhouzhou.sharedpreferencestest/shared_prefs/目錄下,可以看到生成了一個data.xml檔案,如圖:

image

在按鈕的點擊事件中添加的所有資料都已經成功保存下來了,并且SharedPreferences檔案是使用XML格式來對資料進行管理的,那么接下來我們自然要看一看,如何從SharedPreferences檔案中去讀取這些資料了,

6.3.2 從SharedPreferences中讀取資料

使用SharedPreferences來存盤資料是非常簡單的,從SharedPreferences檔案中讀取資料會更加地簡單,

SharedPreferences物件中提供了一系列的get方法,用于對存盤的資料進行讀取,每種get方法都對應了SharedPreferences.Editor中的一種put方法,比如讀取一個布爾型資料就使用getBoolean()方法,讀取一個字串就使用getString()方法,

這些get方法都接收兩個引數,第一個引數是鍵,傳入存盤資料時使用的鍵就可以得到相應的值了;第二個引數是默認值,即表示當傳入的鍵找不到對應的值時會以什么樣的默認值進行回傳,

在SharedPreferencesTest專案的基礎上繼續開發,修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/save_data"
        android:text="Save data"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/restore_data"
        android:text="Restore data"/>
</LinearLayout>

增加了一個還原資料的按鈕,通過點擊這個按鈕來從SharedPreferences檔案中讀取資料,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.sharedpreferencestest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button saveData = https://www.cnblogs.com/1693977889zz/archive/2022/05/11/(Button) findViewById(R.id.save_data);
        saveData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SharedPreferences.Editor editor = getSharedPreferences("data",MODE_PRIVATE).edit();
                editor.putString("name","Zhouzhou");
                editor.putInt("age",28);
                editor.putBoolean("married",false);
                editor.apply();
            }
        });
        Button restoreData = https://www.cnblogs.com/1693977889zz/archive/2022/05/11/(Button) findViewById(R.id.restore_data);
        restoreData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SharedPreferences pref = getSharedPreferences("data",MODE_PRIVATE);
                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("MainActivity","married is " + married);
            }
        });
    }
}

在還原資料按鈕的點擊事件中首先通過getSharedPreferences()方法得到了SharedPreferences物件,然后分別呼叫它的getString()、getInt()和getBoolean()方法,去獲取前面所存盤的姓名、年齡和是否已婚,如果沒有找到相應的值,就會使用方法中傳入的默認值來代替,最后通過Log將這些值列印出來,

現在重新運行一下程式,并點擊界面上的Restore data按鈕,然后查看logcat中的列印資訊,如圖:

image

相比之下,SharedPreferences存盤確實要比文本存盤簡單方便了許多,應用場景也多了不少,比如很多應用程式中的偏好設定功能其實都使用到了SharedPreferences技術,下面我們就來撰寫一個記住密碼的功能,通過這個例子能夠加深對SharedPreferences的理解,

6.3.3 實作記住密碼功能

在上一章中的最佳實踐部分已經撰寫過一個登錄界面了,那就首先打開BroadcastBestPractice專案,來編輯一下登錄界面的布局,修改activity_login.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
    <LinearLayout
        android:orientation="horizontal"
        android:layout_
        android:layout_height="wrap_content">
        <TextView
            android:layout_
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="Account:"/>
        <EditText
            android:id="@+id/account"
            android:layout_
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center_vertical"/>
    </LinearLayout>
    <LinearLayout
        android:orientation="horizontal"
        android:layout_
        android:layout_height="60dp">
        <TextView
            android:layout_
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:textSize="18sp"
            android:text="Password"/>
        <EditText
            android:id="@+id/password"
            android:layout_
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center_vertical"
            android:inputType="textPassword"/>
    </LinearLayout>
    <LinearLayout
        android:orientation="horizontal"
        android:layout_
        android:layout_height="wrap_content">
        <CheckBox
            android:id="@+id/remember_pass"
            android:layout_
            android:layout_height="wrap_content"/>
        <TextView
            android:layout_
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:text="Remember password"/>
    </LinearLayout>
    <Button
        android:id="@+id/login"
        android:layout_
        android:layout_height="60dp"
        android:text="Login"/>
</LinearLayout>

使用到了一個新控制元件CheckBox,這是一個復選框控制元件,用戶可以通過點擊的方式來進行選中和取消,我們就使用這個控制元件來表示用戶是否需要記住密碼,然后修改LoginActivity中的代碼,如下所示:

package com.zhouzhou.broadcastbestpractice;

import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

import java.util.prefs.PreferenceChangeEvent;

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);
        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);
        boolean isRemember = pref.getBoolean("remember_password",false);
        if (isRemember) {
            //將賬號喝密碼都設定到文本框中
            String account = pref.getString("account","");
            String password = pref.getString("password","");
            accountEdit.setText(account);
            passwordEdit.setText(password);
            rememberPass.setChecked(true);
        }
        login.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String account = accountEdit.getText().toString();
                String password = passwordEdit.getText().toString();
                //如果賬號是admin且密碼是123456,就認為登錄成功
                if (account.equals("admin") && password.equals("123456")) {
                    editor = pref.edit();
                    if (rememberPass.isChecked()) {
                        //檢查復選框是否唄選中
                        editor.putBoolean("remember_password",true);
                        editor.putString("account",account);
                        editor.putString("password",password);
                    } else {
                        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();
                }
            }
        });
    }
}
  1. 首先在onCreate()方法中獲取到了SharedPreferences物件,
  2. 然后呼叫它的getBoolean()方法去獲取remember_password這個鍵對應的值,

(一開始當然不存在對應的值了,所以會使用默認值false,這樣就什么都不會發生,)

  1. 接著在登錄成功之后,會呼叫CheckBox的isChecked()方法來檢查復選框是否被選中,

(如果被選中了,則表示用戶想要記住密碼,這時將remember_password設定為true,然后把account和password對應的值都存入到SharedPreferences檔案當中并提交,如果沒有被選中,就簡單地呼叫一下clear()方法,將SharedPreferences檔案中的資料全部清除掉,)

當用戶選中了記住密碼復選框,并成功登錄一次之后,remember_password鍵對應的值就是true了,這個時候如果再重新啟動登錄界面,就會從SharedPreferences檔案中將保存的賬號和密碼都讀取出來,并填充到文本輸入框中,然后把記住密碼復選框選中,這樣就完成記住密碼的功能了,

現在重新運行一下程式,可以看到界面上多出了一個記住密碼復選框,如圖所示:

image

然后賬號輸入admin,密碼輸入123456,并選中記住密碼復選框,點擊登錄,就會跳轉到MainActivity,接著在MainActivity中發出一條強制下線廣播,會讓程式重新回到登錄界面,此時你會發現,賬號密碼都已經自動填充到界面上了,如圖所示:

image

這樣我們就使用SharedPreferences技術將記住密碼功能成功實作了,不過需要注意,這里實作的記住密碼功能仍然只是個簡單的示例,并不能在實際的專案中直接使用,因為將密碼以明文的形式存盤在SharedPreferences檔案中是非常不安全的,很容易就會被別人盜取,因此在正式的專案里還需要結合一定的加密演算法來對密碼進行保護才行,

6.4 SQLite資料庫存盤

SQLite是一款輕量級的關系型資料庫,它的運算速度非常快,占用資源很少,通常只需要幾百KB的記憶體就足夠了,因而特別適合在移動設備上使用,SQLite不僅支持標準的SQL語法,還遵循了資料庫的ACID事務,而SQLite又比一般的資料庫要簡單得多,它甚至不用設定用戶名和密碼就可以使用,Android正是把這個功能極為強大的資料庫嵌入到了系統當中,使得本地持久化的功能有了一次質的飛躍,

前面所學的檔案存盤和SharedPreferences存盤畢竟只適用于保存一些簡單的資料和鍵值對,當需要存盤大量復雜的關系型資料的時候,你就會發現以上兩種存盤方式很難應付得了,比如我們手機的短信程式中可能會有很多個會話,每個會話中又包含了很多條資訊內容,并且大部分會話還可能各自對應了電話簿中的某個聯系人,很難想象如何用檔案或者SharedPreferences來存盤這些資料量大、結構性復雜的資料吧?但是使用資料庫就可以做得到,

6.4.1 創建資料庫

Android為了讓我們能夠更加方便地管理資料庫,專門提供了一個SQLiteOpenHelper幫助類,借助這個類就可以非常簡單地對資料庫進行創建和升級,

  1. SQLiteOpenHelper是一個抽象類,這意味著如果想要使用它的話,就需要創建一個自己的幫助類去繼承它,
  • SQLiteOpenHelper中有兩個抽象方法,分別是onCreate()onUpgrade(),必須在自己的幫助類里面重寫這兩個方法,然后分別在這兩個方法中去實作創建、升級資料庫的邏輯,
  1. SQLiteOpenHelper中還有兩個非常重要的實體方法:getReadableDatabase()和get-WritableDatabase(),
  • 這兩個方法都可以創建或打開一個現有的資料庫(如果資料庫已存在則直接打開,否則創建一個新的資料庫),并回傳一個可對資料庫進行讀寫操作的物件,
  • 不同的是,當資料庫不可寫入的時候(如磁盤空間已滿),getReadableDatabase()方法回傳的物件將以只讀的方式去打開資料庫,而getWritableDatabase()方法則將出現例外,
  1. SQLiteOpenHelper中有兩個構造方法可供重寫,一般使用引數少一點的那個構造方法即可,
  • 這個構造方法中接收4個引數,第一個引數是Context,必須要有它才能對資料庫進行操作,
  • 第二個引數是資料庫名,創建資料庫時使用的就是這里指定的名稱,
  • 第三個引數允許我們在查詢資料的時候回傳一個自定義的Cursor,一般都是傳入null,
  • 第四個引數表示當前資料庫的版本號,可用于對資料庫進行升級操作,

構建出SQLiteOpenHelper的實體之后,再呼叫它的getReadableDatabase()或getWritableDatabase()方法就能夠創建資料庫了,資料庫檔案會存放在/data/data//databases/目錄下,此時,重寫的onCreate()方法也會得到執行,所以通常會在這里去處理一些創建表的邏輯,

接下來通過例子的方式來更加直觀地體會SQLiteOpenHelper的用法吧,首先新建一個DatabaseTest專案,這里我們希望創建一個名為BookStore.db的資料庫,然后在這個資料庫中新建一張Book表,表中有id(主鍵)、作者、價格、頁數和書名等列,創建資料庫表當然還是需要用建表陳述句的,這里也是要考驗一下你的SQL基本功了,Book表的建表陳述句如下所示:

create table Book (
    id integer primary key autoincrement,
    author text,
    price real,
    pages integer,
    name text)

SQLite不像其他的資料庫擁有眾多繁雜的資料型別,它的資料型別很簡單,integer表示整型,real表示浮點型,text表示文本型別,blob表示二進制型別,另外,上述建表陳述句中我們還使用了primary key將id列設為主鍵,并用autoincrement關鍵字表示id列是自增長的,

然后需要在代碼中去執行這條SQL陳述句,才能完成創建表的操作,新建MyDatabaseHelper類繼承自SQLiteOpenHelper,代碼如下所示:

package com.zhouzhou.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;

import androidx.annotation.Nullable;

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(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        mContext = context;
    }

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

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
    }
}

可以看到,我們把建表陳述句定義成了一個字串常量,然后在onCreate()方法中又呼叫了SQLiteDatabase的execSQL()方法去執行這條建表陳述句,并彈出一個Toast提示創建成功,這樣就可以保證在資料庫創建完成的同時還能成功創建Book表,現在修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/create_database"
        android:text="Create database"/>
</LinearLayout>

布局檔案很簡單,就是加入了一個按鈕,用于創建資料庫,最后修改MainActivity中的代碼,如下所示:

package com.zhouzhou.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    private MyDatabaseHelper dbHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        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) {
                dbHelper.getWritableDatabase();
            }
        });
    }
}

這里我們在onCreate()方法中構建了一個MyDatabaseHelper物件,并且通過建構式的引數將資料庫名指定為BookStore.db,版本號指定為1,然后在Create database按鈕的點擊事件里呼叫了getWritableDatabase()方法.

這樣當第一次點擊Create database按鈕時,就會檢測到當前程式中并沒有BookStore.db這個資料庫,于是會創建該資料庫并呼叫MyDatabaseHelper中的onCreate()方法,這樣Book表也就得到了創建,然后會彈出一個Toast提示創建成功,再次點擊Create database按鈕時,會發現此時已經存在BookStore.db資料庫了,因此不會再創建一次,

現在就可以運行一下代碼了,在程式主界面點擊Create database按鈕,結果如圖:

image

此時BookStore.db資料庫和Book表應該都已經創建成功了,因為當你再次點擊Create database按鈕時,不會再有Toast彈出,

怎樣才能證實它們的確創建成功了?如果還是使用FileExplorer,那么最多你只能看到databases目錄下出現了一個BookStore.db檔案,Book表是無法通過FileExplorer看到的,

image

因此這次我們準備換一種查看方式,使用adb shell來對資料庫和表的創建情況進行檢查

adb是Android SDK中自帶的一個除錯工具,使用這個工具可以直接對連接在電腦上的手機或模擬器進行除錯操作,它存放在sdk的platform-tools目錄下,如果想要在命令列中使用這個工具,就需要先把它的路徑配置到環境變數里,如果你使用的是Windows系統,可以右擊計算機→屬性→高級系統設定→環境變數,然后在系統變數里找到Path并點擊編輯,將platform-tools目錄配置進去,如圖:

image

如果你使用的是Linux或Mac系統,可以在home路徑下編輯.bash_檔案,將platform-tools目錄配置進去即可(注:這個網站https://www.androiddevtools.cn/ 下載的SDK Platform-Tools),如圖:

export PATH=$PATH:$HOME/android-sdk-linux/platform-toos

配置好了環境變數之后,就可以使用adb工具了,打開命令列界面,輸入adb shell,就會進入到設備的控制臺,如圖:

image

其中,#符號是超級管理員的意思,也就是說現在你可以訪問模擬器中的一切資料,如果你的命令列上顯示的是$符號,那么就表示你現在是普遍管理員,需輸入su命令切換成超級管理員,才能執行下面的操作,

這里出現不能切換為超級管理員的問題了,解決方式如下:

  1. 配置好環境變數后,打開cmd輸入adb shell 進入控制界面

  2. 發現是普通管理員,想利用su命令切換為超級管理員,不行

image

問題原因:這個問題是因為我們用的模擬器,帶有了Googel play 是不允許獲得管理員權限,

解決辦法:

要去下載一個Target是Google APIS的模擬器,打開AVD Manager,選擇創建一個新的設備并給它更換鏡像,

image

image

image

再嘗試一下su命令,成功解決了不存在的問題了,如圖所示:

image

接下來使用cd命令進入到/data/data/com.zhouzhou.databasetest/databases/目錄下,并使用ls命令查看到該目錄里的檔案,如圖:

image

這個目錄下出現了兩個資料庫檔案,一個正是我們創建的BookStore.db,而另一個BookStore. db-journal則是為了讓資料庫能夠支持事務而產生的臨時日志檔案,通常情況下這個檔案的大小都是0位元組,

接下來我們就要借助sqlite命令來打開資料庫了,只需要鍵入sqlite3,后面加上資料庫名即可,如圖所示:

image

這時就已經打開了BookStore.db資料庫,現在就可以對這個資料庫中的表進行管理了,首先來看一下目前資料庫中有哪些表,鍵入.table命令,如圖:

image

可以看到,此時資料庫中有兩張表,android_metadata表是每個資料庫中都會自動生成的,不用管它,而另外一張Book表就是我們在MyDatabaseHelper中創建的了,這里還可以通過.schema命令來查看它們的建表陳述句,如圖所示:

image

由此證明,BookStore.db資料庫和Book表確實已經創建成功了,之后鍵入.exit或.quit命令可以退出資料庫的編輯,再鍵入exit命令就可以退出設備控制臺了,

image

6.4.2 升級資料庫

MyDatabaseHelper中還有一個onUpgrade()方法是用于對資料庫進行升級的,它在整個資料庫的管理作業當中起著非常重要的作用,可千萬不能忽視它喲,

目前DatabaseTest專案中已經有一張Book表用于存放書的各種詳細資料,如果我們想再添加一張Category表用于記錄圖書的分類,該怎么做呢?比如Category表中有id(主鍵)、分類名和分類代碼這幾個列,那么建表陳述句就可以寫成:

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

接下來我們將這條建表陳述句添加到MyDatabaseHelper中,代碼如下所示:

package com.zhouzhou.databasetest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;

import androidx.annotation.Nullable;

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(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        mContext = context;
    }

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

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
    }
}

看上去好像都挺對的吧?現在我們重新運行一下程式,并點擊Createdatabase按鈕,竟然沒有彈出創建成功的提示,當然,你也可以通過adb工具到資料庫中再去檢查一下,這樣你會更加地

實沒有創建成功的原因不難思考,因為此時BookStore.db資料庫已經存在了,之后不管我們怎樣點擊Create database按鈕,MyDatabaseHelper中的onCreate()方法都不會再次執行,因此新添加的表也就無法得到創建了

解決這個問題的辦法也相當簡單,只需要先將程式卸載掉,然后重新運行,這時BookStore.db資料庫已經不存在了,如果再點擊Create database按鈕,MyDatabaseHelper中的onCreate()方法就會執行,這時Category表就可以創建成功了,

不過,通過卸載程式的方式來新增一張表毫無疑問是很極端的做法,其實我們只需要巧妙地運用SQLiteOpenHelper的升級功能就可以很輕松地解決這個問題,修改MyDatabaseHelper中的代碼,如下所示:

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

可以看到,我們在onUpgrade()方法中執行了兩條DROP陳述句,如果發現資料庫中已經存在Book表或Category表了,就將這兩張表洗掉掉,然后再呼叫onCreate()方法重新創建,

這里先將已經存在的表洗掉掉,因為如果在創建表時發現這張表已經存在了,就會直接報錯,

接下來的問題就是如何讓onUpgrade()方法能夠執行了,還記得SQLiteOpenHelper的構造方法里接收的第四個引數嗎?它表示當前資料庫的版本號,之前我們傳入的是1,現在只要傳入一個比1大的數,就可以讓onUpgrade()方法得到執行了,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    private MyDatabaseHelper dbHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dbHelper.getWritableDatabase();
            }
        });
    }
}

這里將資料庫版本號指定為2,表示我們對資料庫進行升級了,現在重新運行程式,并點擊Create database按鈕,這時就會再次彈出創建成功的提示,

為了驗證一下Category表是不是已經創建成功了,我們在adb shell中打開BookStore.db資料庫,然后鍵入.table命令,結果如圖:

image

接著鍵入.schema命令查看一下建表陳述句,結果如圖:

image

由此可以看出,Category表已經創建成功了,同時也說明我們的升級功能的確起到了作用,

6.4.3 添加資料

我們可以對資料進行的操作無非有4種,即CRUD,其中C代表添加(Create), R代表查詢(Retrieve), U代表更新(Update),D代表洗掉(Delete),

每一種操作又各自對應了一種SQL命令,添加資料時使用insert,查詢資料時使用select,更新資料時使用update,洗掉資料時使用delete,但是開發者的水平總會是參差不齊的,未必每一個人都能非常熟悉地使用SQL語言,因此Android也提供了一系列的輔助性方法,使得在Android中即使不去撰寫SQL陳述句,也能輕松完成所有的CRUD操作,

前面我們已經知道,呼叫SQLiteOpenHelper的getReadableDatabase()或getWritableDatabase()方法是可以用于創建和升級資料庫的,不僅如此,這兩個方法還都會回傳一個SQLiteDatabase物件,借助這個物件就可以對資料進行CRUD操作了,

SQLiteDatabase中提供了一個insert()方法,這個方法就是專門用于添加資料的,它接收3個引數:

  • 第一個引數是表名,我們希望向哪張表里添加資料,
  • 第二個引數用于在未指定添加資料的情況下給某些可為空的列自動賦值NULL,一般我們用不到這個功能,直接傳入null即可,
  • 第三個引數是一個ContentValues物件,它提供了一系列的put()方法多載,用于向ContentValues中添加資料,只需要將表中的每個列名以及相應的待添加資料傳入即可,

介紹完了基本用法,接下來還是通過例子的方式來親身體驗一下如何添加資料吧,修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/create_database"
        android:text="Create database"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/add_data"
        android:text="Add data"/>
</LinearLayout>

在布局檔案中又新增了一個按鈕,稍后就會在這個按鈕的點擊事件里撰寫添加資料的邏輯,接著修改MainActivity中的代碼,如下所示:

package com.zhouzhou.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    private MyDatabaseHelper dbHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dbHelper.getWritableDatabase();
            }
        });
        Button addButton = (Button) findViewById(R.id.add_data);
        addButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                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);
                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);//插入第二條資料
            }
        });
    }
}

在添加資料按鈕的點擊事件里面,我們先獲取到了SQLiteDatabase物件,然后使用ContentValues來對要添加的資料進行組裝,

這里只對Book表里其中四列的資料進行了組裝,id那一列并沒給它賦值,因為在前面創建表的時候,我們就將id列設定為自增長了,它的值會在入庫的時候自動生成,所以不需要手動給它賦值了,

接下來呼叫了insert()方法將資料添加到表當中,注意這里我們實際上添加了兩條資料,上述代碼中使用ContentValues分別組裝了兩次不同的內容,并呼叫了兩次insert()方法,

現在可以重新運行一下程式了,界面如圖:

image

點擊一下Add data按鈕,此時兩條資料應該都已經添加成功了,不過為了證實一下,我們還是打開BookStore.db資料庫瞧一瞧,輸入SQL查詢陳述句select * from Book;,結果如圖:(注:總是查不出來,我的辦法是,到Decive File Explorer 中來到 com.zhouzhou.databasetest包,再將databases中的BookStroe.db和BookStroe.db-journal 都手動洗掉了,再次運行程式,兩個按鈕的Demo都成立,并且查出資料了,但是,這樣做是極端且非常不正確的,

image

6.4.4 更新資料

SQLiteDatabase中也提供了一個非常好用的update()方法,用于對資料進行更新,這個方法接收4個引數:

  • 第一個引數和insert()方法一樣,也是表名,在這里指定去更新哪張表里的資料,
  • 第二個引數是ContentValues物件,要把更新資料在這里組裝進去,
  • 第三、第四個引數用于約束更新某一行或某幾行中的資料,不指定的話默認就是更新所有行,

那么接下來仍然是在DatabaseTest專案的基礎上修改,看一下更新資料的具體用法,比如說剛才添加到資料庫里的第一本書,由于過了暢銷季,賣得不是很火了,現在需要通過降低價格的方式來吸引更多的顧客,我們應該怎么操作呢?首先修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
    ...
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/update_data"
        android:text="Update data"/>
</LinearLayout>

布局檔案中的代碼已經非常簡單了,就是添加了一個用于更新資料的按鈕,然后修改MainActivity中的代碼,如下所示:

public class MainActivity extends AppCompatActivity {
    private MyDatabaseHelper dbHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
       ...
        Button updateData = https://www.cnblogs.com/1693977889zz/archive/2022/05/11/(Button) findViewById(R.id.update_data);
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("price",99.9);
                db.update("Book",values,"name = ?",new String[]{
                        "The Da Vinci Code"
                });
            }
        });
    }
}

這里在更新資料按鈕的點擊事件里面構建了一個ContentValues物件,并且只給它指定了一組資料,說明我們只是想把價格這一列的資料更新成99.9,然后呼叫了SQLiteDatabase的update()方法去執行具體的更新操作,可以看到,這里使用了第三、第四個引數來指定具體更新哪幾行,第三個引數對應的是SQL陳述句的where部分,表示更新所有name等于?的行,而?是一個占位符,可以通過第四個引數提供的一個字串陣列為第三個引數中的每個占位符指定相應的內容,

現在重新運行一下程式,點擊一下Update data按鈕后,再次輸入查詢陳述句查看表中的資料情況,可以看到,The Da Vinci Code這本書的價格已經被成功改為99.9了,

6.4.5 洗掉資料

SQLiteDatabase中提供了一個delete()方法,專門用于洗掉資料,這個方法接收3個引數,第一個引數仍然是表名,這個已經沒什么好說的了,第二、第三個引數又是用于約束洗掉某一行或某幾行的資料,不指定的話默認就是洗掉所有行,修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
    ...
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/delete_data"
        android:text="Delete data"/>
</LinearLayout>

仍然是在布局檔案中添加了一個按鈕,用于洗掉資料,然后修改MainActivity中的代碼,如下所示:

public class MainActivity extends AppCompatActivity {
    private MyDatabaseHelper dbHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
        ...
        Button deleteButton = (Button) findViewById(R.id.delete_data);
        deleteButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                db.delete("Book", "pages > ?", new String[]{"500"});
            }
        });
    }
}

可以看到,我們在洗掉按鈕的點擊事件里指明去洗掉Book表中的資料,并且通過第二、第三個引數來指定僅洗掉那些頁數超過500頁的書,

6.4.6 查詢資料

SQL的全稱是Structured Query Language,翻譯成中文就是結構化查詢語言,它的大部功能都體現在“查”這個字上的,而“增刪改”只是其中的一小部分功能,由于SQL查詢涉及的內容實在是太多了,因此在這里我不準備對它展開來講解,而是只會介紹Android上的查詢功能,

SQLiteDatabase中還提供了一個query()方法用于對資料進行查詢,這個方法的引數非常復雜,最短的一個方法多載也需要傳入7個引數,

  • 第一個引數不用說,當然還是表名,表示我們希望從哪張表中查詢資料,
  • 第二個引數用于指定去查詢哪幾列,如果不指定則默認查詢所有列,
  • 第三、第四個引數用于約束查詢某一行或某幾行的資料,不指定則默認查詢所有行的資料,
  • 第五個引數用于指定需要去group by的列,不指定則表示不對查詢結果進行group by操作,
  • 第六個引數用于對group by之后的資料進行進一步的過濾,不指定則表示不進行過濾,
  • 第七個引數用于指定查詢結果的排序方式,不指定則表示使用默認的排序方式,更多詳細的內容可以參考下表,其他幾個query()方法的多載其實也大同小異,可以自己去研究一下,

image

然query()方法的引數非常多,但是不要對它產生畏懼,因為我們不必為每條查詢陳述句都指定所有的引數,多數情況下只需要傳入少數幾個引數就可以完成查詢操作了,呼叫query()方法后會回傳一個Cursor物件,查詢到的所有資料都將從這個物件中取出,下面還是讓我們通過例子的方式來體驗一下查詢資料的具體用法,修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
    ...
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/query_data"
        android:text="Query data"/>
</LinearLayout>

添加了一個按鈕用于查詢資料,然后修改MainActivity中的代碼,如下所示:

package com.zhouzhou.databasetest;

import androidx.appcompat.app.AppCompatActivity;

import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    private MyDatabaseHelper dbHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
        Button createDatabase = (Button) findViewById(R.id.create_database);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dbHelper.getWritableDatabase();
            }
        });
        Button addButton = (Button) findViewById(R.id.add_data);
        addButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                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);
                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);//插入第二條資料
            }
        });
        Button updateData = https://www.cnblogs.com/1693977889zz/archive/2022/05/11/(Button) findViewById(R.id.update_data);
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("price", 99.9);
                db.update("Book", values, "name = ?", new String[]{
                        "The Da Vinci Code"
                });
            }
        });
        Button deleteButton = (Button) findViewById(R.id.delete_data);
        deleteButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                db.delete("Book", "pages > ?", new String[]{"500"});
            }
        });
        Button queryButton = (Button) findViewById(R.id.query_data);
        queryButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SQLiteDatabase db = dbHelper.getWritableDatabase();
                //查詢Book表中的所有資料
                Cursor cursor = db.query("Book",null,null,null,null,null,null);
                if (cursor.moveToFirst()) {
                    do {
                        //遍歷Cursor物件,取出資料并列印
                        @SuppressLint("Range") String name = cursor.getString(cursor.getColumnIndex("name"));
                        @SuppressLint("Range") String author = cursor.getString(cursor.getColumnIndex("author"));
                        @SuppressLint("Range") int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                        @SuppressLint("Range") double price = cursor.getDouble(cursor.getColumnIndex("price"));
                        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();
            }
        });
    }
}

首先在查詢按鈕的點擊事件里面呼叫了SQLiteDatabase的query()方法去查詢資料,這里的query()方法非常簡單,只是使用了第一個引數指明去查詢Book表,后面的引數全部為null,這就表示希望查詢這張表中的所有資料,雖然這張表中目前只剩下一條資料了,查詢完之后就得到了一個Cursor物件,接著我們呼叫它的moveToFirst()方法將資料的指標移動到第一行的位置,然后進入了一個回圈當中,去遍歷查詢到的每一行資料,在這個回圈中可以通過Cursor的getColumnIndex()方法獲取到某一列在表中對應的位置索引,然后將這個索引傳入到相應的取值方法中,就可以得到從資料庫中讀取到的資料了,接著我們使用Log的方式將取出的資料列印出來,借此來檢查一下讀取作業有沒有成功完成,最后別忘了呼叫close()方法來關閉Cursor,現在再次重新運行程式,點擊一下Query data按鈕后,查看logcat的列印內容,結果如圖:

image

這個例子只是對查詢資料的用法進行了最簡單的示范,在真正的專案中你可能會遇到比這要復雜得多的查詢功能,更多高級的用法還需要你自己去慢慢摸索,畢竟query()方法中還有那么多的引數我們都還沒用到呢,

6.4.7 使用SQL操作資料庫

雖然Android已經給我們提供了很多非常方便的API用于操作資料庫,不過總會有一些人不習慣去使用這些輔助性的方法,而是更加青睞于直接使用SQL來操作資料庫,Android充分考慮到這種的編程習慣,同樣提供了一系列的方法,使得可以直接通過SQL來操作資料庫,下面簡略演示一下,如何直接使用SQL來完成前面幾小節中學過的CRUD操作,

  • 添加資料的方法如下:
db.execSQL("insert into Book (name,author,pages,price) values(?,?,?,?)",new String[] {"The Da Vinci Code","Dan Brown","454","16.96"});
db.execSQL("insert ioto Book (name,author,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()方法,

以上演示的幾種方式,執行結果會和前面幾小節中我們學習的CRUD操作的結果完全相同,選擇使用哪一種方式就看你個人的喜好了,

6.5 使用LitePal操作資料庫

新建一個LitePalTest專案,然后開始我們本節的學習之旅吧,

6.5.1 LitePal簡介

現在開源的熱潮讓所有Android開發者都大大受益,GitHub上面有成百上千的優秀Android開源專案,很多之前我們要寫很久才能實作的功能,使用開源庫可能短短幾分鐘就能實作了

除此之外,公司里的代碼非常強調穩定性,而我們自己寫出的代碼往往越復雜就越容易出問題,相反,開源專案的代碼都是經過時間驗證的,通常比我們自己的代碼要穩定得多,因此,現在有很多公司為了追求開發效率以及專案穩定性,都會選擇使用開源庫,本書中我們將會學習多個開源庫的使用方法,而現在你將正式開始接觸第一個開源庫——LitePal,

LitePal是一款開源的Android資料庫框架,它采用了物件關系映射(ORM)的模式,并將我們平時開發最常用到的一些資料庫功能進行了封裝,使得不用撰寫一行SQL陳述句就可以完成各種建表和増刪改查的操作,LitePal的專案主頁上也有詳細的使用檔案,地址是:https://github.com/LitePalFramework/LitePal,

6.5.2 配置LitePal

image

那么怎樣才能在專案中使用開源庫呢?

過去的方式比較復雜,通常需要下載開源庫的Jar包或者原始碼,然后再集成到專案當中,而現在就簡單得多了,大多數的開源專案都會將版本提交到jcenter上,只需要在app/build.gradle檔案中宣告該開源庫的參考就可以了,因此,要使用LitePal的第一步,就是編輯app/build.gradle檔案,在dependencies閉包中添加如下內容:

dependencies {
    implementation 'org.litepal.guolindev:core:3.2.3'
}

這樣就把LitePal成功引入到當前專案中了,接下來需要配置litepal.xml檔案,右擊app/src/main目錄→New→Directory,創建一個assets目錄,然后在assets目錄右擊→New→File 再新建一個litepal.xml檔案(.xml后綴要保留),接著編輯litepal.xml檔案中的內容,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <!--
    	Define the database name of your application.
    	By default each database name should be end with .db.
    	If you didn't name your database end with .db,
    	LitePal would plus the suffix automatically for you.
    	For example:
    	<dbname value="https://www.cnblogs.com/1693977889zz/archive/2022/05/11/demo" />
    -->
    <dbname value="https://www.cnblogs.com/1693977889zz/archive/2022/05/11/BookStore" />

    <!--
    	Define the version of your database. Each time you want
    	to upgrade your database, the version tag would helps.
    	Modify the models you defined in the mapping tag, and just
    	make the version value plus one, the upgrade of database
    	will be processed automatically without concern.
			For example:
    	<version value="https://www.cnblogs.com/1693977889zz/archive/2022/05/11/1" />
    -->
    <version value="https://www.cnblogs.com/1693977889zz/archive/2022/05/11/1" />

    <!--
    	Define your models in the list with mapping tag, LitePal will
    	create tables for each mapping class. The supported fields
    	defined in models will be mapped into columns.
    	For example:
    	<list>
    		<mapping  />
    		<mapping  />
    	</list>
    -->
    <list>
    </list>

    <!--
        Define where the .db file should be. "internal" means the .db file
        will be stored in the database folder of internal storage which no
        one can access. "external" means the .db file will be stored in the
        path to the directory on the primary external storage device where
        the application can place persistent files it owns which everyone
        can access. "internal" will act as default.
        For example:
        <storage value="https://www.cnblogs.com/1693977889zz/archive/2022/05/11/external" />
    -->

</litepal>

其中,<dbname>標簽用于指定資料庫名,<version>標簽用于指定資料庫版本號,<list>標簽用于指定所有的映射模型,稍后就會用到,

錯誤示范:下載Litepal的依賴包(GitHub或者碼云),下載之后將后綴.zip去掉,再復制到libs中,再Add As Library...瑪雅!!!我是在哪里看到的要去這個依賴包,然后做如下圖的騷操作,這樣的錯誤操作,我找了好久好久的bug,都芭比Q了,愁死我了,整了阿里云的以賴,整了.gradle,重新安裝AS...辦法都試過了,最后,我看到一段可喜可賀的報錯資訊‘Execution failed for task ':app:checkDebugDuplicateClasses'這代表著以依賴了重復的資源檔案,例如:lib中匯入了A.aar檔案,然后又在build.gradle中依賴了相同的A檔案,或者,依賴兩個不同的資源檔案,但是,這兩個資源檔案里面包含了相同的內容,解決方式是,去除重復的資源檔案即可,淚目淚目淚目!!!我刪掉了專案中畫蛇添足下載并匯入的Litepal的依賴包,再次運行,完全OK了,嗚嗚嗚~浪費好多時間

image

最后,還需要再配置一下LitePalApplication,修改AndroidManifest.xml中的代碼,如下所示:

<manifest>
    <application
        android:name="org.litepal.LitePalApplication"
        ...
    >
        ...
    </application>
</manifest>

將專案的application配置為org.litepal.LitePalApplication,這樣才能讓LitePal的所有功能都可以正常作業,

Of course you may have your own Application and has already configured here, like:

<manifest>
    <application
        android:name="com.example.MyOwnApplication"
        ...
    >
        ...
    </application>
</manifest>

That's OK. LitePal can still live with that. Just call LitePal.initialize(context) in your own Application:

public class MyOwnApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        LitePal.initialize(this);
    }
    ...
}

Make sure to call this method as early as you can. In the onCreate()method of Application will be fine. And always remember to use the application context as parameter. Do not use any instance of activity or service as parameter, or memory leaks might happen.

現在LitePal的配置作業已經全部結束了,下面我們開始正式使用它吧,(可以使用GitHub中的例子,下面繼續書中所述,繼續記錄書中的筆記)

6.5.3 創建和升級資料庫

之前創建資料庫是通過自定義一個類繼承自SQLiteOpenHelper,然后在onCreate()方法中撰寫建表陳述句來實作的,而使用LitePal就不用再這么麻煩了,

先將activity_main.xml布局檔案從DatabaseTest專案復制到LitePalTest專案中來,

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/create_database"
        android:text="Create database"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/add_data"
        android:text="Add data"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/update_data"
        android:text="Update data"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/delete_data"
        android:text="Delete data"/>
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/query_data"
        android:text="Query data"/>
</LinearLayout>

LitePal采取的是物件關系映射(ORM)的模式,物件關系映射,簡單點說,我們使用的編程語言是面向物件語言,而使用的資料庫則是關系型資料庫,那么將面向物件的語言和面向關系的資料庫之間建立一種映射關系,這就是物件關系映射了,

物件關系映射模式,它賦予了我們一個強大的功能,就是可以用面向物件的思維來操作資料庫,而不用再和SQL陳述句打交道了,為了創建一張Book表,需要先分析表中應該包含哪些列,然后再撰寫出一條建表陳述句,最后在自定義的SQLiteOpenHelper中去執行這條建表陳述句,但是使用LitePal,就可以用面向物件的思維來實作同樣的功能了,定義一個Book類,代碼如下所示:

package com.zhouzhou.litepaltest;

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類中定義了id、author、price、pages、name這幾個欄位,并生成了相應的getter和setter方法,Book類就會對應資料庫中的Book表,而類中的每一個欄位分別對應了表中的每一個列,這就是物件關系映射最直觀的體驗,

接下來我們還需要將Book類添加到映射模型串列當中,修改litepal.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <dbname value="https://www.cnblogs.com/1693977889zz/archive/2022/05/11/BookStore" />
    <version value="https://www.cnblogs.com/1693977889zz/archive/2022/05/11/1" />
    <!--
    	Define your models in the list with mapping tag, LitePal will
    	create tables for each mapping class. The supported fields
    	defined in models will be mapped into columns.
    	For example:
    	<list>
    		<mapping  />
    		<mapping  />
    	</list>
    -->
    <list>
        <mapping />
    </list>
</litepal>

這里使用<mapping>標簽來宣告要配置的映射模型類,注意一定要使用完整的類名,不管有多少模型類需要映射,都使用同樣的方式配置在<list>標簽下即可,

這樣就已經把所有作業都完成了,現在只要進行任意一次資料庫的操作,BookStore.db資料庫應該就會自動創建出來,那么我們修改MainActivity中的代碼,如下所示:

package com.zhouzhou.litepaltest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

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()方法就是一次最簡單的資料庫操作,只要點擊一下按鈕,資料庫就會自動創建完成了,只要點擊一下按鈕,資料庫就會自動創建完成了,運行一下程式,然后點擊Createdatabase按鈕,接著通過adb shell查看一下資料庫創建情況,如圖:

image

非常棒!資料庫檔案已經創建成功了,接下來我們使用sqlite3命令打開BookStore.db檔案,然后再使用.schema命令來查看建表陳述句,如圖:

image

可以看到,這里有3張表的建表陳述句,其中android_metadata表仍然不用管,table_schema表是LitePal內部使用的,也可以直接忽視,book表就是根據我們定義的Book類以及類中的欄位來自動生成的了,

雖說功能是實作了,但你有沒有發現一個問題,就是升級資料庫的時候我們需要先把之前的表drop掉,然后再重新創建才行,這其實是一個非常嚴重的問題,因為這樣會造成資料丟失,每當升級一次資料庫,之前表中的資料就全沒了,

當然如果你是非常有經驗的程式員,也可以通過復雜的邏輯控制來避免這種情況,但是維護成本很高,而有了LitePal,這些就都不再是問題了,使用LitePal來升級資料庫非常非常簡單,你完全不用思考任何的邏輯,只需要改你想改的任何內容,然后將版本號加1就行了,比如我們想要向Book表中添加一個press(出版社)列,直接修改Book類中的代碼,添加一個press欄位即可,如下所示:

package com.zhouzhou.litepaltest2;

public class Book {
    ...
    private String press;
    ...
    public String getPress() {
        return press;
    }

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

與此同時,我們還想再添加一張Category表,那么只需要新建一個Category類就可以了,代碼如下所示:


改完了所有我們想改的東西,只需要記得將版本號加1就行了,當然由于這里還添加了一個新的模型類,因此也需要將它添加到映射模型串列中,修改litepal.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<litepal>
    <!--  資料庫名稱  -->
    <dbname value="https://www.cnblogs.com/1693977889zz/archive/2022/05/11/BookStore" />

    <!--  版本號 增加了Category資料庫,版本號由1改為2-->
    <version value="https://www.cnblogs.com/1693977889zz/archive/2022/05/11/2" />

    <list>
        <!--   資料庫表路徑     -->
        <mapping ></mapping>
        <mapping ></mapping>
    </list>
</litepal>

現在重新運行一下程式,然后點擊Create database按鈕,再查看一下最新的建表陳述句,結果如圖:

image

可以看到,book表中新增了一個press列,category表也創建成功了,當然LitePal還自動幫我們做了一項非常重要的作業,就是保留之前表中的所有資料,這樣就再也不用擔心資料丟失的問題了,

6.5.4 使用LitePal添加資料

首先回顧一下之前添加資料的方法,我們需要創建出一個ContentValues物件,然后將所有要添加的資料put到這個ContentValues物件當中,最后再呼叫SQLiteDatabase的insert()方法將資料添加到資料庫表當中,

而使用LitePal來添加資料,這些操作可以簡單到讓你驚嘆!只需要創建出模型類的實體,再將所有要存盤的資料設定好,最后呼叫一下save()方法就可以了,

下面開始來動手實作,觀察現有的模型類,你會發現它們都是沒有繼承結構的,沒錯,因為LitePal進行表管理操作時不需要模型類有任何的繼承結構,但是進行CRUD操作時就不行了,必須要繼承自LitePalSupport類(書中的DataSupport類已經被棄用)才行,因此這里我們需要先把繼承結構給加上,修改Book類中的代碼,如下所示:

package com.zhouzhou.litepaltest2;

import org.litepal.crud.LitePalSupport;

public class Book extends LitePalSupport {
    ...
}

接著我們開始向Book表中添加資料,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.litepaltest2;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

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_data);
        createDatabase.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                LitePal.getDatabase();
            }
        });
        Button addData = https://www.cnblogs.com/1693977889zz/archive/2022/05/11/(Button) findViewById(R.id.add_data);
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Book book = new Book();
                book.setName("周周的Android開發");
                book.setAuthor("周周");
                book.setPages(345);
                book.setPrice(17.79);
                book.setPress("UnKnow");
                book.save();
            }
        });
    }
}

在添加資料按鈕的點擊事件里面,首先是創建出了一個Book的實體,然后呼叫Book類中的各種set方法對資料進行設定,最后再呼叫book.save()方法就能完成資料添加操作了,

那么這個save()方法是從哪兒來的呢?當然是從LitePalSupport類中繼承而來的了,除了save()方法之外,LitePalSupport類還給我們提供了豐富的CRUD方法,現在重新運行程式,點擊一下Add data按鈕,此時資料應該已經添加成功了,打開BookStore.db資料庫,輸入SQL查詢陳述句select *fromBook,結果如圖:

image

6.5.5 使用LitePal更新資料

使用LitePal更新資料,更新資料要比添加資料稍微復雜一點,因為它的API介面比較多,這里我們只介紹最常用的幾種更新方式,

首先,最簡單的一種更新方式就是對已存盤的物件重新設值,然后重新呼叫save()方法即可,那么這里我們就要了解一個概念,什么是已存盤的物件?對于LitePal來說,物件是否已存盤就是根據呼叫model.isSaved()方法的結果來判斷的,回傳true就表示已存盤,回傳false就表示未存盤,

那么接下來的問題就是,什么情況下會回傳true,什么情況下會回傳false呢?實際上只有在兩種情況下model.isSaved()方法才會回傳true,一種情況是已經呼叫過model.save()方法去添加資料了,此時model會被認為是已存盤的物件,另一種情況是model物件是通過LitePal提供的查詢API查出來的,由于是從資料庫中查到的物件,因此也會被認為是已存盤的物件,由于查詢API我們暫時還沒學到,因此只能先通過第一種情況來進行驗證,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.litepaltest2;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        Button updateData = https://www.cnblogs.com/1693977889zz/archive/2022/05/11/(Button) findViewById(R.id.update_data);
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Book book = new Book();
                book.setName("Java開發");
                book.setAuthor("王大舸");
                book.setPages(732);
                book.setPrice(37.9);
                book.setPress("UnKnow");
                book.save();
                book.setPrice(99.9);
                book.save();
            }
        });
    }
}

在更新資料按鈕的點擊事件里面,先是通過上一小節中學習的知識添加了一條Book資料,然后呼叫setPrice()方法將這本書的價格進行了修改,之后再次呼叫了save()方法,此時LitePal會發現當前的Book物件是已存盤的,因此不會再向資料庫中去添加一條新資料,而是會直接更新當前的資料,

現在重新運行一下程式,然后點擊Update data按鈕,我們再次輸入查詢陳述句查看表中的資料情況,結果如圖:

image

可以看到,Book表中新增了一條書的資料,但這本書的價格并不是一開始設定的37.9,而是99.9,說明我們的更新操作確實生效了,但是這種更新方式只能對已存盤的物件進行操作,限制性比較大,接下來我們學習另外一種更加靈巧的更新方式,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.litepaltest2;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        Button updateData = https://www.cnblogs.com/1693977889zz/archive/2022/05/11/(Button) findViewById(R.id.update_data);
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Book book = new Book();
                book.setPrice(100);
                book.setPress("Anchor");
                book.updateAll("name = ? and author = ? ","Java開發","王大舸");
            }
        });
    }
}

可以看到,這里首先new出了一個Book的實體,然后直接呼叫setPrice()和setPress()方法來設定要更新的資料,最后再呼叫updateAll()方法去執行更新操作,

注意updateAll()方法中可以指定一個條件約束,和SQLiteDatabase中update()方法的where引數部分有點類似,但更加簡潔,如果不指定條件陳述句的話,就表示更新所有資料,現在重新運行程式并點擊Update data按鈕,我們再次查詢一下表中的資料情況,結果如圖:

image

LitePal的更新API明顯比SQLiteDatabase的update()方法要好用多,

不過,在使用updateAll()方法時,還有一個非常重要的知識點,就是當你想把一個欄位的值更新成默認值時,是不可以使用上面的方式來set資料的,

我們都知道,在Java中任何一種資料型別的欄位都會有默認值,例如int型別的默認值是0, boolean型別的默認值是false, String型別的默認值是null,那么當new出一個Book物件時,其實所有欄位都已經被初識化成默認值了,比如說pages欄位的值就是0,因此,如果我們想把資料庫表中的pages列更新成0,直接呼叫book.setPages(0)是不可以的,因為即使不呼叫這行代碼,pages欄位本身也是0, LitePal此時是不會對這個列進行更新的,

對于所有想要將為資料更新成默認值的操作,LitePal統一提供了一個setToDefault()方法,然后傳入相應的列名就可以實作了,比如我們可以這樣寫:

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

這段代碼的意思是,將所有書的頁數都更新為0,因為updateAll()方法中沒有指定約束條件,因此更新操作對所有資料都生效了,

6.5.6 使用LitePal洗掉資料

使用LitePal洗掉資料的方式主要有兩種,第一種比較簡單,就是直接呼叫已存盤物件的delete()方法就可以了,對于已存盤物件的概念,我們在上一小節中已經學習過了,也就是說,呼叫過save()方法的物件,或者是通過LitePal提供的查詢API查出來的物件,都是可以直接使用delete()方法來洗掉資料的,這種方式比較簡單,我們就不進行代碼演示了,下面直接來看另外一種洗掉資料的方式,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.litepaltest2;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        Button deleteButton = (Button) findViewById(R.id.delete_data);
        deleteButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //LitePal.deleteAll(Book.class,"price < ? ","20")
                LitePal.delete(Book.class,2);
            }
        });
    }
}

這里呼叫了LitePal.delete()方法來洗掉資料,其中deleteAll()方法的第一個引數用于指定洗掉哪張表中的資料,Book.class就意味著洗掉Book表中的資料,后面的引數用于指定約束條件,現在重新運行程式,并點擊一下Delete data按鈕,然后查詢表中的資料情況,如圖:

image

另外,deleteAll()方法如果不指定約束條件,就意味著你要洗掉表中的所有資料,這一點和updateAll()方法是比較相似的,

6.5.7 使用LitePal查詢資料

想想之前我們所使用的query()方法,冗長的引數串列讓人看得頭疼,即使多數引數都是用不到的,也不得不傳入null,如下所示:

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

像這樣的代碼恐怕是沒人會喜歡的,為此LitePal在查詢API方面做了非常多的優化,基本上可以滿足絕大多數場景的查詢需求,并且代碼十分整潔,

首先分析一下上述代碼,query()方法中使用了第一個引數指明去查詢Book表,后面的引數全部為null,這就表示希望查詢這張表中的所有資料,那么使用LitePal如何完成同樣的功能呢?非常簡單,只需要這樣寫:

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

沒有冗長的引數串列,只需要呼叫一下findAll()方法,然后通過Book.class引數指定查詢Book表就可以,

另外,findAll()方法的回傳值是一個Book型別的List集合,也就是說,我們不用像之前那樣再通過Cursor物件一行行去取值了,LitePal已經自動幫我們完成了賦值操作,下面通過一個完整的例子來實踐一下吧,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.litepaltest2;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import org.litepal.LitePal;

import java.util.List;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        Button queryButton = (Button) findViewById(R.id.show_data);
        queryButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                List<Book> books = LitePal.findAll(Book.class);
                for (Book book: books) {
                    Log.d("MainActivity","book name is " + book.getName());
                    Log.d("MainActivity","book author is " + book.getAuthor());
                    Log.d("MainActivity","book pages is " + book.getPages());
                    Log.d("MainActivity","book price is " + book.getPrice());
                    Log.d("MainActivity","book press is " + book.getPress());
                }
            }
        });
    }
}

現在重新運行一下程式,點擊Querydata按鈕,然后查看logcat的列印內容,結果如圖:

image

Book表中只剩下一條資料,由此可見,我們已經將這條資料成功查詢出來了,除了findAll()方法之外,LitePal還提供了很多其他非常有用的查詢API,

比如想要查詢Book表中的第一條資料就可以這樣寫:

Book firstBook = LitePal.findFirst(Book.class);

查詢Book表中的最后一條資料就可以這樣寫:

Book lastBook = LitePal.findLast(Book.class);

我們還可以通過連綴查詢來定制更多的查詢功能,

  • select()方法用于指定查詢哪幾列的資料,對應了SQL當中的select關鍵字,比如只查name和author這兩列的資料,就可以這樣寫:
List<Book> books = LitePal.select("name","author").find(Book.class);
  • where()方法用于指定查詢的約束條件,對應了SQL當中的where關鍵字,比如只查頁數大于400的資料,就可以這樣寫:
List<Book> books = LitePal.where("pages > ?","400").find(Book.class);
  • order()方法用于指定結果的排序方式,對應了SQL當中的order by關鍵字,比如將查詢結果按照書價從高到低排序,就可以這樣寫:
List<Book> books = LitePal.order("price desc").find(Book.class);

其中desc表示降序排列,asc或者不寫表示升序排列,

  • limit()方法用于指定查詢結果的數量,比如只查表中的前3條資料,就可以這樣寫:
List<Book> books = LitePal.limit(3).find(Book.class);
  • offset()方法用于指定查詢結果的偏移量,比如查詢表中的第2條、第3條、第4條資料,就可以這樣寫:
List<Book> books = LitePal.limit(3).offset(1).find(Book.class);

由于limit(3)查詢到的是前3條資料,這里我們再加上offset(1)進行一個位置的偏移,就能實作查詢第2條、第3條、第4條資料的功能了,limit()和offset()方法共同對應了SQL當中的limit關鍵字,當然,你還可以對這5個方法進行任意的連綴組合,來完成一個比較復雜的查詢操作:

List<Book> books = LitePal.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的查詢功能非常強大,并且代碼明顯更加簡潔,我們需要用到一個方法的時候直接連綴一下就可以了,不需要的話就可以不寫,而不是像之前的query()方法,不管需不需要用到,都必須要傳固定的引數進去才行,

當前,如果你實在有一些特殊需求,上述的API都滿足不了你的時候,LitePal仍然支持使用原生的SQL來進行查詢:

Cursor c = LitePal.findBySQL("select * from Book where pages > ? and price < ? ","400","20");

呼叫DataSupport.findBySQL()方法來進行原生查詢,其中第一個引數用于指定SQL陳述句,后面的引數用于指定占位符的值,注意findBySQL()方法回傳的是一個Cursor物件,接下來你還需要通過之前所學的老方式將資料一一取出才行,

6.6 小結與點評

本章主要是對Android常用的資料持久化方式進行了詳細的講解,包括檔案存盤、SharedPreferences存盤以及資料庫存盤,其中檔案適用于存盤一些簡單的文本資料或者二進制資料,SharedPreferences適用于存盤一些鍵值對,而資料庫則適用于存盤那些復雜的關系型資料,雖然目前你已經掌握了這3種資料持久化方式的用法,但是能夠根據專案的實際需求來選擇最合適的方式也是你未來需要繼續探索的,

個人學習筆記,針對本人在自學中遇到的問題,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/472421.html

標籤:其他

上一篇:iOS Widget

下一篇:《第一行代碼:Android篇》學習筆記(七)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more