您的位置:首页 > 移动开发 > Android开发

读《50 Android Hacks》笔记整理Hack 18~Hack 23

2015-11-29 15:44 525 查看

第四章 实用工具

Hack 18 在发布正式版本前移除日志语句

移除日志的最佳方法是使用ProGuard工具。

ProGuard开发文档说明:

“ProGuard可以移除无用代码,或者使用语义模糊的名称来重命名类、变量和方法,以此达到压缩、优化和混淆代码的目的。这样,生成的APK体积更小,并且更不易被逆向工程(Engineer)。“

使用方法:

在项目根目录中找到default.properties文件并添加如下代码:

proguard.config=proguard.cfg


这样ProGuard的功能就生效了。但该功能只在导出签名版的APK(选定项目,右击,选择Android Tools——Export Android Application)时才起作用。为了移除日志,还需要在proguard.cfg文件中添加如下代码:

-assumenosideeffects class android.util.Log{
public static *** d(...);
}


这段代码就是告诉ProGuard移除所有使用android.util.Log类中d()方法的地方,不管这个方法的参数和返回类型是什么。这个配置与Log类的d()方法匹配,因此所有调试日志都会被移除。

但使用ProGuard时要注意,有些代码在混淆后就会出异常,所以我们还要判断是否要配置保留的代码。

外链地址1

外链地址2

外链地址3

Hack 19 使用Hierarchy Viewer工具移除不必要的视图

Hierarchy Viewer工具使用地址

该应用程序只是对默认创建对应用程序做一个小的改动。

Hierarchy Viewer是查看视图树的强大工具。开发应用程序时,使用该工具分析视图树的层次结构,确保当前布局能生成响应灵敏的UI界面并使用了最低的树层次。

外链地址1

第五章 模式

Hack 20 模型-视图-主导器模式

MVC:Model-View-Controller(模型-视图-控制器)

MVP:Model-View-Presenter(模型-视图-主导器)

在MVC模式中,视图可以包含访问模型的逻辑,在MVP模式中,视图与模型是隔离的,所有与视图和模型的交互操作都在主导器中完成,因此主导器在整个MVP模式中处于“主导“地位,所以这里就是将Presenter翻译为主导器的原因。

MVP与MVC的根本区别是:在MVP模式中,视图中的业务逻辑被放入主导器中,主导器通过接口与视图交互。

这个模式虽然看上去繁琐些,但可以让代码更易组织且更易测试,可以更容易地实施TDD(test-driven development,测试驱动开发).

外链地址1

外链地址2——Mockito

Hack 21 与Activity生命周期绑定的BroadcastReceiver

我们可以通过BroadcastReceiver监听操作系统发出的不同通知,但开发者不能在BroadcastReceiver中访问Activity。

假设需要根据网络链接状态更新UI,想在Activity中获取BroadcastReceiver的信息。这个时候我们就需要将BroadcastReceiver作为Activity的内部类使用以获取广播对应的Intent。

将BroadcastReceiver做为Activity的内部类使用,可以实现两个重要功能:

1.在BroadcastReceiver内部访问Activity的方法

2.根据Activity的状态开启或关闭BroadcastReceiver

示例:根据Service发送的信息更新ui。

public class MainActivity extends Activity{
private ProgressDialog mProgressDialog;
private TextView mTextView;
private BroadcastReceiver mReceiver;
private IntentFilter mIntentFilter;

@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//1.创建BroadcastReceiver对象
mReceiver = new MyServiceReceiver();
//2.创建IntentFilter,定义该BroadcastReceiver对象可以接受哪种类型的Intent
mIntentFilter = new IntentFilter(MyService.ACTION);
startService(new Intent(this,MyService.class));
}
@Override
protected void onResume(){
super.onResume();
//3.在onResume()方法中注册广播监听器
registerReceiver(mReceiver,mIntentFilter);
}
@Override
public void onPause(){
super.onPause();
//4.在onPause()方法中取消注册广播监听器
unregisterReceiver(mReceiver);
}
private void update(String msg){}
class MyServiceReceiver extends BroadcastReceiver{
//5.调用Activity的update()方法
update(intent.getExtras().getString(MyService.MSG_KEY));
}
}


这里首先创建BroadcastReceiver对象,然后创建IntentFilter,通过该对象定义BroadcastReceiver对象可以监听哪种类型的Intent。因为只在Activity内部使用BroadcastReceiver对象,因此需要在onResume()方法中注册,在onPause()方法中取消注册。当BroadcastReceiver执行时,会从Intent的extra成员中获取日期信息,并以此为参数调用Activity的update()方法。

我们这里创建了一个BroadcastReceiver,只在Activity显示的时候才去更新UI。

在Activity中以内部类的形式使用BroadcastReceiver,可以将Intent携带的信息反馈到界面上。此外,在适当的时候取消注册BroadcastReceiver是避免不必要的UI更新操作的好方法。

外链地址1

外链地址2

Hack 22 使用Android库项目时适用的架构模式

在Android库项目(Library Project)发布之前,在不同Android项目间共享代码是很困难甚至是不可能的。通常是使用jar包共享Java代码,但却无法共享那些需要引用资源文件的代码。

为了解决这个问题,Android库项目产生了。

Android库项目类似于JAR包,但却可以使用Android资源文件。将Android库项目设置为某个应用程序的依赖项目时,该应用程序就拥有了第二个R类。

使用Android库项目的好处是让代码变得可重用且易维护。

外链地址1

外链地址2

Hack 23 同步适配器模式

几乎所有Android应用程序都会使用互联网获取信息或者同步数据,很多方法实现建立网络链接并在加载数据时显示一个进度动画的功能。

23.1 一般方法

23.1.1 使用AsyncTask

AsyncTask是Android用于处理线程的类,通过该类,开发者可以很容易地把代码逻辑从一个线程移到另一个线程。

可是如果使用AsyncTask的地方不恰当就会出现问题:

1.当AsyncTask执行时转动设备,应用程序崩溃。

2.应用程序运行一段时间后还是崩溃,崩溃原因是AsyncTask支持的并发任务的数量有限。

所以如果要使用AsyncTask的情景是:后台任务比较简单或不依赖于其执行结果。

23.1.2 使用Service

使用Service是否适合:

1.与Activity交互

2.决定什么时候以及如何启动Service

3.运行时需要检查链接状态

4.持久化数据

使用Service会出现的问题主要是由系统的灵活性造成的。

23.2 同步适配器(SyncAdapter)

Gmail应用是如何运作?是如何同步数据,并能离线工作,却不出问题?

这时因为Google通过同步适配器实现的。

23.2.1 同步适配器是什么

同步适配器是由Android平台启动的Service。在这个Service中,可以放置同步代码。

同步适配器的优点:

1.后台自动同步数据(即便并未打开应用程序)

2.处理服务器身份验证

3.处理网络重连

4.遵守用户关于后台同步的偏好设置

23.2.2 以数据库代替服务器

暂时先不考虑同步,只创建一个在本地运行,并在数据库保存信息的应用程序。因此,我们需要创建3个类:DatabaseHelper、TodoContentProvider和TodoDao。

DatabaseHelper代码如下:

//1.继承SQLiteOpenHelper
public class DatabaseHelper extends SQLiteOpenHelper{
public static final String DATABASE_NAME = "todo.db";
private static final int DATABASE_VERSION = 1;

public DatabaseHelper(Context context){
//2.指定数据库名和版本号
super(context,DATABASE_NAME,null,DATABASE_VERSION);
}
//3.是否需要创建数据库表
@Override
public void onCreate(SQLiteDatabase db){
db.execSQL("CREATE TABLE"
+TodoContentProvider.TODO_TABLE_NAME+"("
+TodoContentProvider.COLUMN_ID
+"INTEGER PRIMARY KEY AUTOINCREMENT,"
+TodoContentProvider.COLUMN_SERVER_ID+"INTEGER,"
+TodoContentProvider.COLUMN_TITLE+"LONGTEXT,"
+TodoContentProvider.COLUMN_STATUS_FLAG+"INTEGER"
+");"
);
}
//4.升级数据库模式(schema)
@Override
public void onUpgrade(){
db.execSQL("DROP TABLE IF EXISTS"+TodoContentProvider.TODO_TABLE_NAME);
onCreate(db);
}
}


创建DatabaseHelper需要指定数据库名和版本号,该类需要根据上述信息判断是否需要创建数据库表以及是否需要更新数据库模式。

TodoContentProvider代码如下:

//1.继承ContentProvider
public class TodoContentProvider extends ContentProvider{
public static final String TODO_TABLE_NAME = "todos";
public static final String AUTHORITY = TodoContentProvider.class.getCanonicalName();
public static final String COLUMN_ID ="_id";
public static final String COLUMN_SERVER_ID = "server_id";
public static final String COLUMN_TITLE = "title";
public static final String COLUMN_STATUS_FLAG = "status_flag";
private static final int TODO = 1;
private static final int TODO_ID = 2;

private static HashMap<String,String> projectionMap;
private static final UriMatcher sUriMatcher;

public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.androidhacks.todo";
public static final String CONTENT_TYPE_ID = "vnd.android.cursor.item/vnd.androidhacks.todo";

public static final Uri CONTENT_URI = Uri.parse("content://"+AUTHORITY+"/"+TODO_TABLE_NAME);

private DatabaseHelper dbHelper;
static{
//2.根据内容URI判断执行流程
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(AUTHORITY,TODO_TABLE_NAME,TODO);
sUriMatcher.addURI(AUTHORITY,TODO_TABLE-NAME+"/#",TODO_ID);

//3.改变列映射
projectionMap = new HashMap<String,String>();
projectionMap.put(COLUMN_ID,COLUMN_ID);
projectionMap.put(COLUMN_SERVER_ID,COLUMN_SERVER_ID);
projectionMap.put(COLUMN_TITLE,COLUMN_TITLE);
projectionMap.put(COLUMN_STATUS_FLAG,COLUMN_STATUS_FLAG);
}
@Override
public boolean onCreate(){
//4.创建ContentProvider
dbHelper = new DatabaseHelper(getContext());
return true;
}
@Override
public Cursor query(Uri uri,String[] projection,String selection,String[] selectionArgs,String sortOrder){
//5.根据URI匹配switch分支,并构建查询条件
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
switch(sUriMatcher.match(uri)){
case TODO:
qb.setTables(TODO_TABLE_NAME);
qb.setProjectionMap(projectionMap);
break;
case TODO_ID:
qb.setTables(TODO_TABLE_NAME);
qb.setProjectionMap(projectionMap);
qb.appendWhere(COLUMN_ID+"="+uri.getPathSegments().get(1));
break;
default:
throw new RuntimeException("Unknown URI");
}
SQLiteDatabase db = dbHelper.getReadableDatabase();
//6.从数据库获取Cursor
Cursor c = qb.query(db,projection,selection,selectionArgs,null,null,sortOrder);
//7.设置URI内容变动通知:Cursor会获知URI内容的变化情况当发现URI内容发生改变时,Cursor会自动更新数据
c.setNotificationUri(getContext().getContentResolver(),uri);
return c;
}
...
}


最后,TodoDao负责通过ContentResolver调用上述TodoContentProvider,用于java对象和数据库对象的相互转化,代码如下:

public class TodoDao{
private static final TodoDao instance = new TodoDao();
private TodoDao(){}
private static TodoDao getInstance(){return instance;}
public void addNewTodo(ContentResolver contentResolver,Todo list,int flag){
ContentValues contentValue = getTodoContentValues(list,flag);
contentResolver.insert(TodoContentProvider.CONTENT_URI,contentValue);
}

private ContentValues getTodoContentValues(Todo todo,int flag){
ContentValue cv = new ContentValues();
cv.put(TodoContentProvider.COLUMN_SERVER_ID,todo.getId());
cv.put(TodoContentProvider.COLUMN_TITLE,todo.getTitle());
cv.put(TodoContentProvider.COLUMN_STATUS_FLAG,flag);
return cv;
}
...
}


从上述代码中可以看出,TodoDao通过单例模式实现。我们在该类中实现了类似addNewTodo()这样的业务方法,该方法将java对象转化成ContentValues后,会执行一次数据库插入操作。

23.2.3 填充数据库

操作数据库我们会用到如下两个类:

1.MainActivity——显示TODO列表

2.AddNewActivity——显示一个表单,用于添加新的TODO条目

MainActivity代码如下:

public class MainActivity extends Activity{
private ListView mListView;
private TodoAdapter mAdapter;
@Override
public void onCreate(){
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mListView = (ListView)findViewById(R.id.main_activity_listview);
mAdater = new TodoAdapter(this);
mListView.setAdapter(mAdapter);
}
public void addNew(View v){
startActivity(new Intent(this,AddNewActivity.class));
}
}


上述只是常规代码,创建一个ListVeiw,让其适配器是TodoAdapter。当用户点击“Add New”按钮,AddNewActivity这个Activity就会启动。

TodoAdapter代码如下:

public class TodoAdapter extends CursorAdapter{
...
private static final String[] PROJECTION_IDS_TITLE_AND_STATUS = new String[]{
TodoContentProvider.COLUMN_ID,
TodoContentProvider.COLUMN_TITLE,
TodoContentProvider.COLUMN_STATUS_FLAG
};

public TodoAdapter(Activity activity){
super(activity,getManagedCursor(activity),true);
mActivity = activity;
...
}

//1.获取一个Cursor
private static Cursor getManagedCursor(Activity activity){
//2.留意TodoContentProvider的URI和列映射(projection)的用法
return activity.managedQuery(TodoContentProvider.CONTENT_URI,PROJECTION_IDS_TITLE_AND_STATUS,TodoContentProvider.COLUMN_STATUS_FLAG+" != "+StatusFlag.DELETE,null,TodoContentProvider.DEFAULT_SORT_ORDER);
}

@Override
public void bindView(View view,Context context,Cursor c){
final ViewHolder holder = (ViewHolder)view.getTag();
holder.id.setText(c.getString(mInternalIdIndex));
holder.title.setText(c.getString(mTitleIndex));

final int status = c.getInt(mInternalStatusIndex);
//3.改变文本背景
if(StatusFlag.CLEAN != status){
holder.title.setBackgroundColor(Color.RED);
}else{
holder.title.setBackgroundColor(Color.GREEN);
}
final Long id = Long.valueOf(holder.id.getText().toString());
holder.deleteButton.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v){

//4.从列表中删除TODO条目    TodoDao.getInstance().deleteTodo(mActivity.getContentResolver(),id);
}
});
}
}


在这里没有notifyDataSetChanged()方法,因为不需要,我们在TodoContentProvider中调用过setNotificationUri()方法,当通过ContentProvider更新数据库的时候,TodoContentProvider返回的Cursor也会被更新。

到这里我们创建了要给可以向数据库保存数据的应用程序。

23.2.4 实现登录功能

这里需要处理服务器身份验证,我们不会把登录信息存储于数据库或者SharedPreferences中,而是存储到Android的账户(Account)中。处理账号要用到Android提供的AccountManager类,该类用于管理账号中的用户凭证。

基本原理是:一旦用户输入用户凭证,这些信息会被保存到账户中。具备USE_CREDENTIALS权限的应用程序可以通过AccountManager查询到账户信息,进而获取保存在账户中的身份验证令牌或者其他可以用于服务器身份验证的必要信息。

登录功能会在以下情况用到:

1.应用程序启动时账户信息还没有被创建

2.用户在“账户&同步”菜单中点击“新建账户”

3.同步适配器尝试同步数据时出现身份验证错误

情况1需要创建类BootstrapActivity

代码如下:

public class BootstrapActivity extends Activity{
private static final int NEW_ACCOUNT = 0;
private static final int EXISTING_ACCOUNT = 1;
private AccountManager mAccountManager;

@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.bootstrap);
mAccountManager = AccountManager.get(this);
//1.根据类型获取账户列表
Account[] accounts = mAccountManager.getAccountsByType(AuthenticatorActivity.PARAM_ACCOUNT_TYPE);
//2.创建新账户
if(accounts.length == 0){
final Intent i = new Intent(this,AuthenticatorActivity.class);
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
startActivityForResult(i,NEW_ACCOUNT);
}else{
String password = mAccountManager.getPassword(accounts[0]);
//3.要求用户输入密码
if(password == null){
final Intent i = new Intent(this,AuthenticatorActivity.class);
i.putExtra(AuthenticatorActivity.PARAM_USER,accounts[0].name);
startActivityForResult(i,EXISTING_ACCOUNT);
}else{
//4.切换到MainActivity
startActivity(new Intent(this,MainActivity.class));
finish();
}
}
}
...
}


情况2:需要添加一个继承AbstractAccountAuthenticator的类,AbstractAccountAuthenticator是用于创建账户认证器(AccountAuthenticator)的基类。要提供一个新的认证器,必须继承该类并实现其抽象方法。而且还需要创建一个Service(该Service即AuthenticationService),当通过Action为AccountManager.ACTION_AUTHENTICATOR_INTENT的Intent激活该服务时,需要在该Service的onBind(android.content.Intent)方法中返回getIBinder()的方法结果。

我们创建一个继承自AbstractAccountAuthenticator的Authenticator类,对于用不到的方法,令其返回null就可以了。其中比较重要的是addAcount()方法和getAuthToken()方法,代码如下:

public class Authenticator extends AbstractAccountAuthenticator{
private final Context mContext;
public Authenticator(Context context){
super(context);
mContext = context;
}

@Override
public Bundle addAccount(AccountAuthenticatorResponse response,String accountType,String authTokenType,String[] requiredFeatures,Bundle options)throws NetworkErrorException{
final Intent intent = new Intent(mContext,AuthenticatorActivity.class);
intent.putExtra(AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE,authTokenType);
intent.putExtra(AuthenticatorActivity.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,response);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT,intent);
return bundle;
}
...
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response,Account account,String authTokenType,Bundle options)throws NetworkErrorException{
//1.检查请求的令牌是否相同
if(!authTokenType.equals(AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE)){
final Bundle result = new Bundle();
result.putString(AccountManager.KEY_ERROR_MESSAGE,"invalid authTokenType");
return result;
}
final AccountManager am = AccountManager.get(mContext);
final String password = am.getPassword(account);
if(password != null){
//2.获取密码
boolean verified = false;
String loginResponse = null;
try{
loginResponse = LoginServiceImpl.sendCredentials(account.name,password);
verified = LoginServiceImpl.hasLoggedIn(loginResponse);
}catch(AndroidHacksException e){
verified = false;
}
//3.返回结果
if(verified){
final Bundle result = new Bundle();
result.putString(AccountManager.KEY_ACCOUNT_NAME,account.name);
result.putString(AccountManager.KEY_ACCOUNT_TYPE,AuthenticatorActivity.PARAM_ACCOUNT_TYPE);
return result;
}
}
//4.让调用方知道应该启动哪个activity让用户登录
final Intent intent = new Intent(mContext,AuthenticatorActivity.class);
intent.putExtra(AuthenticatorActivity.PARAM_USER,account.name);
intent.putExtra(AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE,authTokenType);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,response);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT,intent);
return bundle;
}


addAccount()方法创建一个Intent,AccountManager会使用该Intent创建新账户。当需要使用账户中存储的用户凭证信息登录服务器时,就会调用getAuthToken()方法,该方法首先检查请求的令牌是否与当前处理的令牌相同,然后通过AccountManager获取密码,如果获取到密码,就以此登录服务器。如果登录成功,就返回登陆结果;如果不成功,就返回一个Intent,通过该Intent告诉调用者启动哪个Activity让用户重新登陆。当密码改变或者用户凭证失效时会出现上述情况。

下一个要创建AuthenticatorActivity,用于显示登录表单,代码如下:

public class AuthenticatorActivity extends AccountAuthenticatorActivity{
public static final String PARAM_ACCOUNT_TYPE = "com.manning.androidhacks.hack023";
public static final String PARAM_AUTHTOKEN_TYPE="authtokenType";
public static final String PARAM_USER = "user";
public static final String PARAM_CONFIRMCREDENTIALS = "confirmCredentials";
private AccountManager mAccountManager;
private Thread mAuthThread;
private String mAuthToken;
private String mAuthTokenType;
private Boolean mConfirmCredentials = false;
private final Handler mHandler = new Handler();
protected boolean mRequestNewAccount = false;
private String mUser;
...
private void handleLogin(View view){
if(mRequestNewAccount){
mUsername = mUsernameEdit.getText().toString();
}
mPassword = mPasswordEdit.getText().toString();
if(TextUtils.isEmpty(mUsername) || TextUtils.isEmpty(mPassword)){
mMessage.setText(getMessage());
}
showProgress();
//1.开启线程与服务器通信
mAuthThread = NetworkUtilities.attemptAuth(mUsername,mPassword,mHandler,AuthenticatorActivity.this);
}
//2.向AuthenticatorActivity返回验证结果
public void onAuthenticationResult(Boolean result){
hideProgress();
if(result){
if(!mConfirmCredentials){
finishLogin();
}
}else{
mMessage.setText("User and/or password are incorrect");
}
}
//3.调用finishLogin()方法
private void finishLogin(){
final Account account = new Account(mUsername,PARAM_ACCOUNT_TYPE);
//4.设置新密码
if(mRequestNewAccount){
mAccountManager.addAccountExplicitly(account,mPassword,null);
}else{
mAccountManager.setPassword(account,mPassword);
}
final Intent intent = new Intent();
intent.putExtra(AccountManager.KEY_ACCOUNT_NAME,mUsername);
intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE,PARAM_ACCOUNT_TYPE);
if(mAuthTokenType != null && mAuthTokenType.equals(PARAM_AUTHTOKEN_TYPE)){
intent.putExtra(AccountManager.KEY_AUTHTOKEN,mAuthToken);
}
//5.设置结束
setAccountAuthenticatorResult(intent.getExtras());
setResult(RESULT_OK,intent);
finish();
}
...
}


当用户输入登录信息并点击”OK”按钮后,handleLogin()会调用。在该方法中启动一个线程与服务器通信,通信结果会通过onAuthenticationResult()方法返回给AuthenticatorActivity。如果服务验证成功,调用finishLogin()方法,否则向用户显示一条错误信息提示用户重新登录。在finishLogin()方法中,如果设置了请求创建新账户的标记(mRequestNewAccount),就通过AccountManager创建一个账户;如果账号存在,就设置新密码,最后设置请求结束。

最后一步在AndroidManifest.xml中注册一个Service,代码如下:

<service android:name=".authenticator.AuthenticationService" android:exported="true">
<!-- 1.该服务返回一个账号验证器 -->
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
<!-- 2.附加信息 -->
<meta-data android:name="android.accounts.AccountAuthenticator" android:resource = "@xml/authenticator"/>
</service>


这里定义了action为android.accounts.AccountAuthenticator的Intent过滤器,告诉系统该Service会返回一个账号验证器。此外,还需要一个单独的XML文件提供一些附加信息,名为authenticator的XML文件内容如下:

<account-authenticator
xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="com.manning.androidhacks.hack023"
android:icon="@drawable/ic_launcher"
android:smallIcon="@drawable/ic_launcher"
android:label="@string/app_name"/>


上述文件中最重要的是android:accountType,该属性表示当前Service返回的账户验证器只用于验证类型为com.manning.androidhacks.hack023的账户,其他属性定义是菜单项的外观。

23.2.5 添加同步适配器

到这里,前期准备就已经完成了。

同步适配器是什么呢?

同步适配器是一个由Android平台处理的Service,该Service通过账户实现与服务器的身份验证,使用ContentProvider同步数据。实现同步适配器后,应用程序可以自动与服务器同步数据。操作系统将当前同步适配器连同其他同步适配器一起注册到设备中。同步适配器每次只会运行一个,这样避免了网络阻塞。

实现方法:

首先需要在AndroidMainfest.xml文件中声明该同步适配器:

<service android:name=".service.TodoSyncService"
android:exported="true">
<intent-filter>
<action android:name="android.content.SynAdapter"/>
</intent-filter>
<meta-data android:name="android.content.SyncAdapter"
android:resource="@xml/todo_sync_adapter"/>
</service>


与AuthenticationService类似,我们定义名为android.content.SyncAdapter的Action,表明该TodoSyncService是一个同步适配器。此外,还需要定义一个附加XML文件,内容如下:

<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="com.manning.androidhacks.hack023.provider.TodoContentProvider"
android:accountType="com.manning.androidhacks.hack023"/>


上述XML文件表示TodoSyncService会用到TodoContentProvider的authority,并且需要使用的账户类型是com.manning.androidhacks.hack023。

下一步需要创建继承AbstractThreadedSyncAdapter的类,代码如下:

public class TodoSyncAdapter extends AbstractThreadedSyncAdapter{
private final ContentResolver mContentResolver;
private AccountManager mAccountManager;
private final static TodoDao mTodoDao = TodoDao.getInstance();

@Override
public void onPerformSync(Account account,Bundle extras,String authority,ContentProviderClient provider,SyncResult syncResult){
try{
//1.从服务器获取所有TODO条目
List<Todo> data = fetchData();
//2.从本地删除服务器端已经删除的TODO条目
syncRemoteDeleted(data);
//3.调用syncFromServerToLocalStorage
syncFromServerToLocalStorage(data);
//4.从数据库中查询所有状态不一致的TODO条目,
syncDirtyToServer(mTodoDao.getDirtyList(mContentResolver));
}catch(Exception e){
handleException(e,syncResult);
}
}
...
//5.异常处理方式
private void handleException(Exception e,SyncResult syncResult){
if(e instanceof AuthenticatorException){
syncResult.stats.numParseExceptions++;
}else if(e instanceof IOException){
syncResult.stats.numIoExceptions++;
}
...
}
}


onPerformSync()方法是在后台线程中执行的,与服务器同步的逻辑就放在该方法中。

TODO数据库表定义的属性列:

id——本地ID

server_id——同步后,每行数据都会从服务器获取一个远程ID

status_flag——数据状态,可以是CLEAN、MOD、ADD、和DELETE

title——TODO条目的文本内容

同步开始后,首先从服务器端获取所有的TODO条目。注意,如果TODO条目太多可以使用分页显示。第二步从本地数据库移除服务器端已经不存在的TODO条目,实现这一步首先需要从本地数据库中获取所有状态标记为CLEAN的条目,然后判断服务器端是否存在该条目,如果不存在,就从本地数据库删除该条目。之后调用syncFromServerToLocalStorage()方法,在该方法中,首先遍历服务器端的TODO条目,并通过server_id判断该条目是否存在于本地数据库。如果存在,就以服务器端的信息更新本地条目,如果不存在,就在本地数据库中获取所有状态标记为dirty(非CLEAN)的条目,然后根据状态标记或在服务器端添加条目,或在服务器端更新或删除条目。

留意异常处理方式。根据异常类型的不同修改syncResult对象。这样做的目的是帮助同步管理器选择在合适的时机重新调用同步适配器。

最后步骤在TodoSyncService中封装同步适配器,代码如下:

public class TodoSyncService extends Service{
private static final Object sSyncAdapterLook = new Object();
private static TodoSyncAdapter sSyncAdapter = null;

@Override
public void onCreate(){
synchronized (sSyncAdapterLook){
if(sSyncAdapter == null){
sSyncAdapter = new TodoSyncAdapter(
getApplicationContext(),true);
}
}
}
@Override
public IBinder onBind(Intent intent){
return sSyncAdapter.getSyncAdapterBinder();
}
}


到这里同步适配器就完成了,用户可以在离线和在线状态使用应用程序,他们不会注意到差别。

外链地址1

外链地址2

外链地址3

外链地址4

外链地址5

外链地址6

外链地址7

外链地址8
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: