Android JetPack应用架构指南

标签 :JetPack

Android开发中经常面临的问题

  • 在界面控制器中编写大量代码,造成界面类非常臃肿,难以维护;
  • 在横竖屏切换时,界面控制器中存储的数据丢失,需要重新初始化
  • ActivityFragment中经常需要开启异步线程去获取数据,界面控制器需要管理这些调用,并确保系统在其销毁后清理这些调用以避免潜在的内存泄漏;
  • 当多个地方使用相同数据的时候,需要主动去获取同步数据,增加逻辑复杂度;

MVP模式中的解决方案

  • 将网络请求和逻辑处理放到对应的Presenter
  • 在界面控制器的生命周期中处理维护Presenter
  • 使用RxJava处理异步操作,并在生命周期中解绑

常见的架构原则

1、分离关注点

任何不处理界面或操作系统交互的代码都不应该写在ActivityFragment中,这样可以避免许多与生命周期相关的问题。

2、通过模型驱动界面

模型是负责为应用处理数据的组件。它们独立于应用中的视图和应用组件,因此不受这些组件的生命周期问题的影响。 同时模型类应明确定义数据管理职责,这样将使这些模型类可测试,并且使应用保持一致。

Google推荐的MVVM模式

定义Repository管理数据来源(Model),使用LiveData驱动界面(View)更新,使用ViewModel代替Presenter管理数据(VM)

看一下google推荐的架构图:


final-architecture.png

ViewModel

ViewModel是什么

架构组件为界面控制器提供了 ViewModel辅助程序类,该类负责为界面准备数据。在配置更改期间会自动保留 ViewModel 对象,以便它们存储的数据立即可供下一个 ActivityFragment实例使用

  • 旨在以注重生命周期的方式存储和管理界面相关的数据。
  • 允许数据在屏幕旋转等配置更改后继续存在。

ViewModel的使用

class WeatherViewModel : ViewModel() {
    val weatherResult: LiveData<WeatherResult>? = null
    fun getWeather(cityCode: String): WeatherResult {
        if (weatherResult != null) {
            weatherResult = LiveData<WeatherResult>()
        }
        loadWeather(cityCode)
        return weatherResult
    }
    fun loadWeather(cityCode: String) {
        //...获取天气数据
    }
}

然后在你的Activity中使用:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val weatherModel = ViewModelProviders.of(this).get(WeatherViewModel::class.java)
    val weather = weatherModel.getWeather("101010100")
    updateWeather(weather)
}

ViewMode的实现
ViewModel存储在ActivityFragment
从源码中分析ViewModel的生命周期

@NonNull
@MainThread
public static ViewModelProvider of(@NonNull FragmentActivity activity,
        @Nullable Factory factory) {
    Application application = checkApplication(activity);
    if (factory == null) {
        factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
    }
    return new ViewModelProvider(activity.getViewModelStore(), factory);
}

该静态方法是实际上是从界面控制器中获取到ViewModelStore来创建一个ViewModelProvider,此处的factory约定了ViewModel的实例化方式

Activity中源码

@NonNull
@Override
public ViewModelStore getViewModelStore() {
    if (getApplication() == null) {
        throw new IllegalStateException("Your activity is not yet attached to the "
                + "Application instance. You can't request ViewModel before onCreate call.");
    }
    if (mViewModelStore == null) {
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            // Restore the ViewModelStore from NonConfigurationInstances
            mViewModelStore = nc.viewModelStore;
        }
        if (mViewModelStore == null) {
            mViewModelStore = new ViewModelStore();
        }
    }
    return mViewModelStore;
}

ViewModelProvider中获取ViewModel

@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
    ViewModel viewModel = mViewModelStore.get(key);

    if (modelClass.isInstance(viewModel)) {
        //noinspection unchecked
        return (T) viewModel;
    } else {
        //noinspection StatementWithEmptyBody
        if (viewModel != null) {
            // TODO: log a warning.
        }
    }

    viewModel = mFactory.create(modelClass);
    mViewModelStore.put(key, viewModel);
    //noinspection unchecked
    return (T) viewModel;
}

可以看出来界面Activity持有一个ViewModelStore来存储ViewModeViewModelStore中使用HashMap来存储ViewMode

ViewModel的销毁

@Override
protected void onDestroy() {
    super.onDestroy();

    if (mViewModelStore != null && !isChangingConfigurations()) {
        mViewModelStore.clear();
    }

    mFragments.dispatchDestroy();
}

ViewModel的存活范围

viewmodel-lifecycle.png

ViewModel更重要的是提供一种Android开发的规范

需要注意的地方

  • 单一责任原则,维护对外提供所需的数据,获取数据最好使用另外单独的Repository
  • 默认构造函数没有参数,有参数的构造函数需要自定义ViewModelProvider.Factory,重写create方法
  • context不能传入ViewModel(不应该关心界面),AndroidViewModel可以直接使用Application
  • ViewModel不能代替onSaveInstanceState

LifeCycle

Lifecycle

Lifecycle是一个类,它包含有ActivityFragment生命周期状态的信息,并允许其他对象观察此状态。

Lifecycle使用两个主要枚举来跟踪其关联组件的生命周期状态:

  1. EventLifecycle所跟踪组件(ActivityFragment)回调的生命周期事件。
  2. StateLifecycle所跟踪组件(ActivityFragment)的当前状态。

LifecycleOwner
LifecycleOwner是一个单方法接口,表示该类具有生命周期。它仅有一个方法getLifecycle(),通过该方法提供一个Lifecycle实例用来接收和存储当前UI控制器的生命周期状态。

实现LifecycleObserver的组件与实现LifecycleOwner的组件可以无缝协作,因为所有者(ActivityFragment)可以提供生命周期,而观察者可以监听生命周期回调。

LiveData

LiveData 是一种可观察的封装容器,可以用于任何数据,包括实现 Collections 的对象,如 List。具有生命周期感知能力,感知应用组件(Activity、Fragment 或 Service)的生命周期确保 LiveData 仅更新处于活跃状态的应用组件

优势

  • 响应式更新界面:数据发生变化时自动更新相关联的UI
  • 不会发生内存泄漏:观察者绑定到 Lifecycle对象,并且在其关联的生命周期被销毁后,会自我清理
  • 不会因 Activity停止而导致崩溃:如果观察者的生命周期处于非活跃状态(如返回栈中的 Activity),则它不会接收任何 LiveData 事件
  • 数据始终保持最新状态:如果生命周期变为非活跃状态,它会在再次变为活跃状态时接收最新的数据。例如,曾经在后台的 Activity 会在返回前台后立即接收最新的数据。
  • 适当的配置更改:如果由于配置更改(如设备旋转)而重新创建了 ActivityFragment,它会立即接收最新的可用数据。

LiveData的相关操作

  1. 更新数据
    setValue(value) 主线程中使用
    postValue(value) 子线程中使用

  2. 转换数据
    Transformations.map()

    val weather = LiveData<Weather>()
    val result = Transformations.map(weather) { it: Weather ->
        it.date + ":" + weather.pm25
    }

Transformations.switchMap()

    val cityData = LiveData<String>()
    val result = Transformations.switchMap(cityData) { it: String ->
        getWeather(it)
    }
    private fun getWeather(cityCode: String): LiveData<WeatherResult> {
        //...
    }

LiveData的使用
按照如下步骤使用:

  1. 创建LiveData对象,并让其持有一个具体类型的数据。通常在ViewModel中使用。
  2. 创建Observer对象,并重写onChange()方法,当LiveData持有的数据发生改变时会回调此方法。Observer对象通常在UI控制器(ActivityFragment)中使用。
  3. 使用LiveDataobserver()方法来订阅一个Observer对象,这样LiveData持有的数据发生变化时观察者就能够收到通知。通常在UI控制器中订阅观察者对象。

我们之前的ViewModel可以改成如下:

class WeatherViewModel : ViewModel() {

    @Inject
    lateinit var weatherRepository: WeatherRepository
    private var cityData = MutableLiveData<String>()
    val weatherResult: LiveData<WeatherResult> by lazy {
        Transformations.switchMap(cityLiveData) {
            weatherRepository.getWeatherResult(it)
        }
    }
    
    init {
        DaggerRepositoryComponent.builder().build().inject(this)
        Log.i("ViewModel", "created")
    }
    
    fun setCity(cityCode: String) {
         cityData.setValue(cityCode)
    }
}

Repository类:

class WeatherRepository {
    fun getWeatherResult(cityCode: String): LiveData<WeatherResult> {
        Log.i("Weather update", "获取数据code=$cityCode")
        val result = MutableLiveData<WeatherResult>()
        //...获取数据源
        //...可以来自缓存、数据库、网络
        //网络获取
        DDHttp.get(Api.GET_WEATHER + cityCode)
            .build()
            .enqueue(object : ResponseCallback<WeatherResult> {
                override fun onSuccess(model: WeatherResult) {
                    result.value = model
                }
                override fun onFailure(throwable: Throwable) {
                }
            })

        return result
    }
}

FragmentActivity中调用:

override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)
       weatherModel.setCity("101010100")
       val weatherModel = ViewModelProviders.of(this).get(WeatherViewModel::class.java)
       weatherModel.weatherResult.observe(this, Observer<WeatherResult> { updateWeather(it) })
       
       btn_bj.setOnClickListener { weatherModel.setCity("101010100") }
   }

   private fun updateWeather(result: WeatherResult) {
       Log.i("Weather Update", result.time)
    //...
   }

由于FragmentActivity已经实现了LifecycleOwner接口,所以可以直接使用
如果在Activity中使用,需要实现LifecycleOwner接口,并提供一个LifecycleRegistry即可

class ThirdActivity : Activity(), LifecycleOwner {

    private var lifecycleRegistry = LifecycleRegistry(this)

    override fun getLifecycle(): Lifecycle {
        return lifecycleRegistry
    }
}

LiveData还提供另一种添加观察者的方法observeForever(Observer),通过该方法添加观察者后,要手动调用removeObserver()方法来停止观察者接收回调通知

扩展LiveData
LiveData 对象具有活跃观察者时,会调用 onActive() 方法
LiveData 对象没有任何活跃观察者时,会调用 onInactive() 方法

public class StockLiveData extends LiveData<BigDecimal> {
    private StockManager mStockManager;

    private SimplePriceListener mListener = new SimplePriceListener() {
        @Override
        public void onPriceChanged(BigDecimal price) {
            setValue(price);
        }
    };

    public StockLiveData(String symbol) {
        mStockManager = new StockManager(symbol);
    }

    @Override
    protected void onActive() {
        mStockManager.requestPriceUpdates(mListener);
    }

    @Override
    protected void onInactive() {
        mStockManager.removeUpdates(mListener);
    }
}

自定义LiveData主要是在onActive时接受数据变化,在onInactive时停止接受新数据

Room数据库

上述 WeatherRepository 实现的问题是,获取数据后,不会将数据保留在任何位置。如果用户在离开后再回到该类,应用将重新获取数据,这样做很体验不太好,同时也浪费流量

Room 是一个对象映射库,可利用最少的样板代码实现本地数据持久性。在编译时,它会根据架构验证每个查询,使损坏的 SQL 查询导致编译时错误而不是运行时失败。Room 可以抽象化处理原始 SQL 表格和查询的一些底层实现细节。它还允许观察对数据库数据(包括集合和连接查询)的更改,并通过 LiveData 对象公开此类更改。此外,它还明确定义了解决一些常见问题(如访问主线程上的存储空间)的线程约束。

Room的使用

要使用 Room,我们需要定义本地model类。使用 @Entity(tableName = String)进行注解,以将其标记为数据库中的表格

@Entity(tableName = "weather")
data class WeatherResult(
    @PrimaryKey
    @ColumnInfo(name = "city_code")
    var cityCode: String,
    @ColumnInfo(name = "weather_date")
    var date: String,
    var sunrise: String,
    var high: String,
    var low: String,
    var sunset: String,
    var aqi: Float,
    var ymd: String,
    var week: String,
    var fx: String,
    var fl: String,
    var type: String,
    var notice: String
)

创建一个数据访问对象 (DAO)。

@Dao
public interface WeatherDao {
    @Insert(onConflict = REPLACE)
    void save(weather WeatherResult);
    @Query("SELECT * FROM weather WHERE city_code = :cityCode")
    LiveData<WeatherResult> load(String cityCode);
    @Delete
    fun delete(weather: WeatherResult)
}

请注意,load方法将返回 LiveData<WeatherResult>Room知道何时修改了数据库,并且在数据发生更改时会自动通知所有活跃的观察者。

为我们的App创建数据库:

@Database(entities = [WeatherResult::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun getWeatherDao(): WeatherDao
}

WeatherDatabase是抽象类。Room将自动提供它的实现。

获取AppDatabase实例

val db = Room.databaseBuilder(applicationContext,AppDatabase::class.java,"weather.db").build();

注意:Room不允许在主线程中访问数据库,除了创建实例的时候

表中常用注解
@Entity 创建一张表
@PrimaryKey主键
@ColumnInfo(name = String) 标记字段名
@IgnoreRoom默认会在表中创建所有字段,如果不需要可以使用Ignore
@Embedded(prefix = String) 用来注解Object类型字段(非基础类型数据)如果有多个Object字段,且包含相同的字段名,需要指定prefix

常规操作
插入 @Insert(onConflict = OnConflictStrategy.REPLACE)

更新 @Update

删除 @Delete

查询,需要自定义查询语句,其中cityCode为参数
@Query("SELECT * FROM weather WHERE city_code = :cityCode")

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容