安卓学习

本篇博客主要分析Android架构组件的官方范例中的Basic Sample,主要介绍3个组件的使用:

  • Room
  • ViewModels
  • LiveData

整体架构

自上而下进行分析,APP参照下图架构。

请注意,每个组件仅依赖于其下一级的组件。例如,Activity 和 Fragment 仅依赖于视图模型。存储区是唯一依赖于其他多个类的类;在本例中,存储区依赖于持久性数据模型和远程后端数据源。
这种设计打造了一致且愉快的用户体验。无论用户上次使用应用是在几分钟前还是几天之前,现在回到应用时都会立即看到应用在本地保留的用户信息。如果此数据已过时,则应用的存储区模块将开始在后台更新数据。
官方文档链接

Application

创建AppExecutors,可以获取AppDatabaseDataRepository

public class BasicApp extends Application {
private AppExecutors mAppExecutors;
@Override
public void onCreate() {
super.onCreate();
mAppExecutors = new AppExecutors();
}
public AppDatabase getDatabase() {
return AppDatabase.getInstance(this, mAppExecutors);
}
public DataRepository getRepository() {
return DataRepository.getInstance(getDatabase());
}
}

应用xml配置

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

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

<uses-feature android:name="android.hardware.location.gps"/>

<application
android:name=".BasicApp"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name="com.example.android.persistence.ui.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>

</manifest>

线程池和线程通信

AppExecutors: Global executor pools for the whole application.

public class AppExecutors {
private final Executor mDiskIO;
private final Executor mNetworkIO;
private final Executor mMainThread;
private AppExecutors(Executor diskIO, Executor networkIO, Executor mainThread) {
this.mDiskIO = diskIO;
this.mNetworkIO = networkIO;
this.mMainThread = mainThread;
}
public AppExecutors() {
this(Executors.newSingleThreadExecutor(), Executors.newFixedThreadPool(3),
new MainThreadExecutor());
}
// public Executor diskIO()
// public Executor networkIO()
// public Executor mainThread()
private static class MainThreadExecutor implements Executor {
private Handler mainThreadHandler = new Handler(Looper.getMainLooper());
@Override
public void execute(@NonNull Runnable command) {
mainThreadHandler.post(command);
}
}
}

其他线程可以通过mainThreadHandler与主线程(ui线程)进行通信。

Handler可以用来在多线程间进行通信,在另一个线程中去更新UI线程中的UI控件只是Handler使用中的一种典型案例,除此之外,Handler可以做很多其他的事情。每个Handler都绑定了一个线程,假设存在两个线程ThreadA和ThreadB,并且HandlerA绑定了 ThreadA,在ThreadB中的代码执行到某处时,出于某些原因,我们需要让ThreadA执行某些代码,此时我们就可以使用Handler,我们可以在ThreadB中向HandlerA中加入某些信息以告知ThreadA中该做某些处理了。由此可以看出,Handler是Thread的代言人,是多线程之间通信的桥梁,通过Handler,我们可以在一个线程中控制另一个线程去做某事。
————————————————
版权声明:本文为CSDN博主「孙群」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/iispring/article/details/47115879

Activity

MainActivity创建ProductListFragment并显示,可调用show生成特定的ProductFragment并显示。

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);

// Add product list fragment if this is first creation
if (savedInstanceState == null) {
ProductListFragment fragment = new ProductListFragment();

getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, fragment, ProductListFragment.TAG).commit();
}
}

/** Shows the product detail fragment */
public void show(Product product) {
ProductFragment productFragment = ProductFragment.forProduct(product.getId());

getSupportFragmentManager()
.beginTransaction()
.addToBackStack("product")
.replace(R.id.fragment_container,
productFragment, null).commit();
}
}

Fragment

ProductListFragment,进行视图绑定RecyclerView内容的生成,并创建ProductListViewModel

  • 视图绑定部分,R.layout.list_fragment->ListFragmentBinding
  • RecyclerView内容的生成需要ProductAdapter
  • ProductListViewModelliveData的变化会更新ProductAdapterProductList
public class ProductListFragment extends Fragment {

public static final String TAG = "ProductListFragment";
private ProductAdapter mProductAdapter;
private ListFragmentBinding mBinding;

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

mProductAdapter = new ProductAdapter(mProductClickCallback);
mBinding.productsList.setAdapter(mProductAdapter);

return mBinding.getRoot();
}

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
final ProductListViewModel viewModel =
new ViewModelProvider(this).get(ProductListViewModel.class);

mBinding.productsSearchBtn.setOnClickListener(v -> {
Editable query = mBinding.productsSearchBox.getText();
viewModel.setQuery(query);
});

subscribeUi(viewModel.getProducts());
}

private void subscribeUi(LiveData<List<ProductEntity>> liveData) {
// Update the list when the data changes
liveData.observe(getViewLifecycleOwner(), myProducts -> {
if (myProducts != null) {
mBinding.setIsLoading(false);
mProductAdapter.setProductList(myProducts);
} else {
mBinding.setIsLoading(true);
}
// espresso does not know how to wait for data binding's loop so we execute changes sync.
mBinding.executePendingBindings();
});
}
/*@Override
public void onDestroyView()*/
private final ProductClickCallback mProductClickCallback = product -> {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
((MainActivity) requireActivity()).show(product);
}
};
}

ProductAdapter

继承RecyclerView.Adapter,包括List<? extends Product>,存储产品列表。

setProductList可以更新List<? extends Product>

public class ProductAdapter extends RecyclerView.Adapter<ProductAdapter.ProductViewHolder> {
List<? extends Product> mProductList;
@Nullable
private final ProductClickCallback mProductClickCallback;

public ProductAdapter(@Nullable ProductClickCallback clickCallback) {
mProductClickCallback = clickCallback;
setHasStableIds(true);
}
public void setProductList(final List<? extends Product> productList) {
if (mProductList == null) {
mProductList = productList;
notifyItemRangeInserted(0, productList.size());
} else {
DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback());/* @Override...*/
mProductList = productList;
result.dispatchUpdatesTo(this);
}
}
}

数据绑定

layout xml->mProductClickCallback(ProductListFragment)->Activity

<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="product"
type="com.example.android.persistence.model.Product"/>
<variable name="callback"
type="com.example.android.persistence.ui.ProductClickCallback"/>
</data>

<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/product_item_min_height"
android:onClick="@{() -> callback.onClick(product)}"
android:orientation="horizontal"
android:layout_marginStart="@dimen/item_horizontal_margin"
android:layout_marginEnd="@dimen/item_horizontal_margin"
app:cardUseCompatPadding="true">

ViewModel

  • LiveData mProducts->ProductAdapterList<? extends Product>
  • DataRepository->LiveData mProducts
public class ProductListViewModel extends AndroidViewModel {
private static final String QUERY_KEY = "QUERY";

private final SavedStateHandle mSavedStateHandler;
private final DataRepository mRepository;
private final LiveData<List<ProductEntity>> mProducts;

public ProductListViewModel(@NonNull Application application,
@NonNull SavedStateHandle savedStateHandle) {
super(application);
mSavedStateHandler = savedStateHandle;

mRepository = ((BasicApp) application).getRepository();

// Use the savedStateHandle.getLiveData() as the input to switchMap,
// allowing us to recalculate what LiveData to get from the DataRepository
// based on what query the user has entered
mProducts = Transformations.switchMap(
savedStateHandle.getLiveData("QUERY", null),
(Function<CharSequence, LiveData<List<ProductEntity>>>) query -> {
if (TextUtils.isEmpty(query)) {
return mRepository.getProducts();
}
return mRepository.searchProducts("*" + query + "*");
});
}
public void setQuery(CharSequence query) {
// Save the user's query into the SavedStateHandle.
// This ensures that we retain the value across process death
// and is used as the input into the Transformations.switchMap above
mSavedStateHandler.set(QUERY_KEY, query);
}
/**
* Expose the LiveData Products query so the UI can observe it.
*/
public LiveData<List<ProductEntity>> getProducts() {
return mProducts;
}
}

DataRepository

RoomDatabase(AppDatabase)->DataRepository(MediatorLiveData)

public class DataRepository {
private static DataRepository sInstance;
private final AppDatabase mDatabase;
private MediatorLiveData<List<ProductEntity>> mObservableProducts;

private DataRepository(final AppDatabase database) {
mDatabase = database;
mObservableProducts = new MediatorLiveData<>();

mObservableProducts.addSource(mDatabase.productDao().loadAllProducts(),
productEntities -> {
if (mDatabase.getDatabaseCreated().getValue() != null) {
mObservableProducts.postValue(productEntities);
}
});
}

public static DataRepository getInstance(final AppDatabase database) {
if (sInstance == null) {
synchronized (DataRepository.class) {
if (sInstance == null) {
sInstance = new DataRepository(database);
}
}
}
return sInstance;
}

/**
* Get the list of products from the database and get notified when the data changes.
*/
public LiveData<List<ProductEntity>> getProducts() {
return mObservableProducts;
}

public LiveData<ProductEntity> loadProduct(final int productId) {
return mDatabase.productDao().loadProduct(productId);
}

public LiveData<List<CommentEntity>> loadComments(final int productId) {
return mDatabase.commentDao().loadComments(productId);
}

public LiveData<List<ProductEntity>> searchProducts(String query) {
return mDatabase.productDao().searchAllProducts(query);
}
}

RoomDatabase

关于synchronized的锁

synchronized的锁本质上2种,写法3种:

  • Object.class 类.class锁
  • this 当前对象锁,等同于同步方法思路,这个对象就是容器对象。
  • object 成员变量锁代码块中使用 Object.class 相当于把这个方法标记为静态同步的。
    android 多线程 — synchronized

关于Room的组件

Room 包含 3 个主要组件:

  • 数据库:包含数据库持有者,并作为应用已保留的持久关系型数据的底层连接的主要接入点。
    使用 @Database 注释的类应满足以下条件:
    • 是扩展 RoomDatabase 的抽象类。
    • 在注释中添加与数据库关联的实体列表。
    • 包含具有 0 个参数且返回使用 @Dao 注释的类的抽象方法。
      在运行时,您可以通过调用 Room.databaseBuilder() 或 Room.inMemoryDatabaseBuilder() 获取 Database 的实例。
  • Entity:表示数据库中的表。
  • DAO:包含用于访问数据库的方法。
    应用使用 Room 数据库来获取与该数据库关联的数据访问对象 (DAO)。然后,应用使用每个 DAO 从数据库中获取实体,然后再将对这些实体的所有更改保存回数据库中。 最后,应用使用实体来获取和设置与数据库中的表列相对应的值。
    使用 Room 将数据保存到本地数据库

@Database(entities = {ProductEntity.class, ProductFtsEntity.class, CommentEntity.class}, version = 2)
@TypeConverters(DateConverter.class)
public abstract class AppDatabase extends RoomDatabase {
private static AppDatabase sInstance;
@VisibleForTesting
public static final String DATABASE_NAME = "basic-sample-db";

public abstract ProductDao productDao();
public abstract CommentDao commentDao();

private final MutableLiveData<Boolean> mIsDatabaseCreated = new MutableLiveData<>();

public static AppDatabase getInstance(final Context context, final AppExecutors executors) {
if (sInstance == null) {
synchronized (AppDatabase.class) {
if (sInstance == null) {
sInstance = buildDatabase(context.getApplicationContext(), executors);
sInstance.updateDatabaseCreated(context.getApplicationContext());
}
}
}
return sInstance;
}

/**
* Build the database. {@link Builder#build()} only sets up the database configuration and
* creates a new instance of the database.
* The SQLite database is only created when it's accessed for the first time.
*/
private static AppDatabase buildDatabase(final Context appContext,
final AppExecutors executors) {
return Room.databaseBuilder(appContext, AppDatabase.class, DATABASE_NAME)
.addCallback(new Callback() {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
executors.diskIO().execute(() -> {
// Add a delay to simulate a long-running operation
addDelay();
// Generate the data for pre-population
AppDatabase database = AppDatabase.getInstance(appContext, executors);
List<ProductEntity> products = DataGenerator.generateProducts();
List<CommentEntity> comments =
DataGenerator.generateCommentsForProducts(products);

insertData(database, products, comments);
// notify that the database was created and it's ready to be used
database.setDatabaseCreated();
});
}
})
.addMigrations(MIGRATION_1_2)
.build();
}

/**
* Check whether the database already exists and expose it via {@link #getDatabaseCreated()}
*/
private void updateDatabaseCreated(final Context context)
private void setDatabaseCreated(){
mIsDatabaseCreated.postValue(true);
}

private static void insertData(final AppDatabase database, final List<ProductEntity> products,
final List<CommentEntity> comments) {
database.runInTransaction(() -> {
database.productDao().insertAll(products);
database.commentDao().insertAll(comments);
});
}
private static void addDelay() // Thread.sleep(4000);
// public LiveData<Boolean> getDatabaseCreated()
// private static final Migration MIGRATION_1_2 = new Migration(1, 2) { };
}

DAO

@Dao
public interface ProductDao {
@Query("SELECT * FROM products")
LiveData<List<ProductEntity>> loadAllProducts();

@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertAll(List<ProductEntity> products);

@Query("select * from products where id = :productId")
LiveData<ProductEntity> loadProduct(int productId);

@Query("select * from products where id = :productId")
ProductEntity loadProductSync(int productId);

@Query("SELECT products.* FROM products JOIN productsFts ON (products.id = productsFts.rowid) "
+ "WHERE productsFts MATCH :query")
LiveData<List<ProductEntity>> searchAllProducts(String query);
}

ProductEntity

@Entity(tableName = "products")
public class ProductEntity implements Product {
@PrimaryKey
private int id;
private String name;
private String description;
private int price;

@Override
public int getId() {
return id;
}
//

@Ignore
public ProductEntity(int id, String name, String description, int price) {
this.id = id;
this.name = name;
this.description = description;
this.price = price;
}
public ProductEntity(Product product) {
this.id = product.getId();
this.name = product.getName();
this.description = product.getDescription();
this.price = product.getPrice();
}
}

其他

LiveData结合Room数据库使用以及线程问题