Android Jetpack组件LiveData基本使用和原理分析

LiveData一般是和 ViewModel 配合使用的,但是本文就以单独使用 LiveData 作为例子单独使用,这样可以只关注 LiveData 而不被其他所干扰。

本文整体流程:首先要知道什么是 LiveData,然后演示一个例子,来看看 LiveData 是怎么使用的,接着提出问题为什么是这样的,最后读源码来解释原因!

LiveData 的源码比较简单,底层依赖了 Lifecycle,所以懂 Lifecycle 的源码是关键,我之前写过一篇

Android Jetpack组件Lifecycle基本使用和原理分析 最好是先看这篇文章,才能更好的理解 LiveData。

1.什么是 LiveData

LiveData是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。

2.LiveData基础使用的例子

这个例子,是点击按钮通过 LiveData 来更新 TextView 的内容

如图:

[图片上传失败...(image-90c8e1-1606802615929)]

点击按钮后

[图片上传失败...(image-cc2c98-1606802615929)]

具体代码

class LiveDataActivity : BaseActivity() {
    private val mContent = MutableLiveData<String>()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_live_data)

        btnUpdate.setOnClickListener {
            mContent.value = "最新值是:Update"
        }

        mContent.observe(this, Observer { content ->
            tvContent.text = content
        })
    }
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tvContent"
        android:layout_width="0dp"
        android:text="Hello World"
        android:layout_height="wrap_content"
        android:textColor="#f00"
        android:gravity="center"
        android:textSize="24sp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnUpdate"
        android:layout_width="wrap_content"
        android:text="Update"
        android:padding="5dp"
        android:layout_height="wrap_content"
        android:textColor="#000"
        android:textSize="18sp"
        android:layout_marginTop="20dp"
        android:textAllCaps="false"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvContent" />

</androidx.constraintlayout.widget.ConstraintLayout>

默认TextView展示的是: Hello World,点击按钮后展示的是:“最新值是:Update” 。这个就是LiveData 的简单使用。

3.抛出问题

为什么LiveData的工作机制是这样的

  • LiveData 是怎么回调的?
  • LiveData 为什么可以感知生命周期?
  • LiveData 可以感知生命周期,有什么用,或者说有什么优势?
  • LiveData 为什么只会将更新通知给活跃的观察者。非活跃观察者不会收到更改通知?
  • LiveData此外还提供了observerForever()方法,所有生命周期事件都能通知到,怎么做到的?

解析来通过分析源码,来寻找答案。文章最后我会解释这些问题的,做一个统一的总结。

4.源码分析前的准备工作

我需要了解几个类,来对接下来的源码分析做一个铺垫。

先看之前例子中的代码

class LiveDataActivity : BaseActivity() {
    private val mContent = MutableLiveData<String>()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
                
        mContent.observe(this, Observer { content ->
            tvContent.text = content
        })
    }
}

只贴出了主要代码,我们来看下主要的类以及方法,方法参数

  • 声明了一个MutableLiveData对象
  • 调用了MutableLiveData的observe方法
  • observe方法中 传入 this 和 Observer
  • this 指的是LiveDataActivity对象,其实一个是一个LifecycleOwner。Observer是一个接口

来分别看下具体内容。

4.1.MutableLiveData类
public class MutableLiveData<T> extends LiveData<T> {

    public MutableLiveData(T value) {
        super(value);
    }

    public MutableLiveData() {
        super();
    }

    @Override
    public void postValue(T value) {
        super.postValue(value);
    }

    @Override
    public void setValue(T value) {
        super.setValue(value);
    }
}
  • 继承了LiveData是一个可变的LiveData
  • 是一个被观察者,是一个数据持有者
  • 提供了 setValue 和 postValue方法,其中postValue可以在子线程调用
  • postValue方法,我们下面会具体分析
4.2.MutableLiveData的observe方法参数中的 this

当前 Activity 的对象,本质上是一个LifecycleOwner 我在这篇 Android Jetpack组件Lifecycle基本使用和原理分析中有分析过,它的源码。

public interface LifecycleOwner {
    @NonNull
    Lifecycle getLifecycle();
}
4.3MutableLiveData的observe方法参数中的 Observer
public interface Observer<T> {
    /**
     * Called when the data is changed.
     * @param t  The new data
     */
    void onChanged(T t);
}
  • Observer是一个观察者
  • Observer中有一个回调方法,在 LiveData 数据改变时会回调此方法

通过以上简单分析,我们大概了解了这个几个类的作用,接下来我们一步一步看源码,来从源码中解决我们在第 3 节提出的问题。

5.源码分析

首先我们上面示例中的 LiveData.observe()方法开始。

//LiveDataActivity.kt
private val mContent = MutableLiveData<String>()

mContent.observe(this, Observer { content ->
    tvContent.text = content
})

我们点进observe方法中去它的源码。

6.LiveData类

在LiveData的observe方法中

//LiveData.java
@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
    assertMainThread("observe");
    //1
    if (owner.getLifecycle().getCurrentState() == DESTROYED) {
        // ignore
        return;
    }
    //2
    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
    //3
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
    //4
    if (existing != null && !existing.isAttachedTo(owner)) {
        throw new IllegalArgumentException("Cannot add the same observer"
                + " with different lifecycles");
    }
    if (existing != null) {
        return;
    }
    //5
    owner.getLifecycle().addObserver(wrapper);
}

注释 1:首先会通过LifecycleOwner获取Lifecycle对象然后获取Lifecycle 的State,如果是DESTROYED直接 return 了。忽略这次订阅

注释 2 :把LifecycleOwner和Observer包装成LifecycleBoundObserver对象,至于为什么包装成这个对象,我们下面具体讲,而且这个是重点

注释 3:把观察者存到 Map 中

注释 4:之前添加过LifecycleBoundObserver,并且LifecycleOwner不是同一个,就抛异常

注释 5:通过Lifecycle和添加 LifecycleBoundObserver观察者,形成订阅关系

总结:

到现在,我们知道了LiveData的observe方法中会判断 Lifecycle 的生命周期,会把LifecycleOwner和Observer包装成LifecycleBoundObserver对象,然后 Lifecycle().addObserver(wrapper)Lifecycle 这个被观察者会在合适的实际通知观察者的回调方法。

等等,什么时候通知,咋通知的呢?这个具体流程是啥呢?

回个神,我再贴下开始的示例代码。

class LiveDataActivity : BaseActivity() {
    private val mContent = MutableLiveData<String>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_live_data)
                //1
        btnUpdate.setOnClickListener {
            mContent.value = "最新值是:Update"
        }

        mContent.observe(this, Observer { content ->
            tvContent.text = content
        })
    }
}

在点击按钮的时候 LiveData会调用setValue方法,来更新最新的值,这时候我们的观察者Observer就会收到回调,来更新 TextView。

所以接下来我们先看下 LiveData的setValue方法做了什么,LiveData还有一个postValue方法,我们也一并分析一下。

7.LiveData的setValue方法和postValue方法

7.1.先看setValue方法
//LiveData.java
@MainThread
protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    dispatchingValue(null);//1
}

调用了dispatchingValue方法,继续跟代码

//LiveData.java
void dispatchingValue(@Nullable ObserverWrapper initiator) {
    if (mDispatchingValue) {
        mDispatchInvalidated = true;
        return;
    }
    mDispatchingValue = true;
    do {
        mDispatchInvalidated = false;
        if (initiator != null) {
            //1
            considerNotify(initiator);
            initiator = null;
        } else {
            for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
                    mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                //2
                considerNotify(iterator.next().getValue());
                if (mDispatchInvalidated) {
                    break;
                }
            }
        }
    } while (mDispatchInvalidated);
    mDispatchingValue = false;
}

不管如何判断,都是调用了considerNotify()方法

//LiveData.java
private void considerNotify(ObserverWrapper observer) {
    if (!observer.mActive) {
        return;
    }
    if (!observer.shouldBeActive()) {
        observer.activeStateChanged(false);
        return;
    }
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    observer.mLastVersion = mVersion;
    observer.mObserver.onChanged((T) mData);//1
}

最终调用了observer.mObserver.onChanged((T) mData)方法,这个observer.mObserver就是我们的 Observer接口,然后调用它的onChanged方法。

到现在整个被观察者数据更新通知观察者这个流程就通了。

7.2.然后再看下postValue方法

子线程发送消息通知更新 UI,嗯?Handler 的味道,我们具体看下代码

//LiveData.java
protected void postValue(T value) {
    boolean postTask;
    synchronized (mDataLock) {
        postTask = mPendingData == NOT_SET;
        mPendingData = value;
    }
    if (!postTask) {
        return;
    }
    ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);//1
}

可以看到一行关键代码ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);

点 **postToMainThread **方法进去看下

//ArchTaskExecutor.java
private TaskExecutor mDelegate;

@Override
public void postToMainThread(Runnable runnable) {
    mDelegate.postToMainThread(runnable);//1
}

看到 mDelegate 是 TaskExecutor对象,现在目标是看下 mDelegate 的具体实例对象是谁

//ArchTaskExecutor.java
private ArchTaskExecutor() {
    mDefaultTaskExecutor = new DefaultTaskExecutor();
    mDelegate = mDefaultTaskExecutor;//1
}

好的,目前的重点是看下DefaultTaskExecutor是个啥,然后看它的postToMainThread方法

//DefaultTaskExecutor.java
private volatile Handler mMainHandler;

@Override
public void postToMainThread(Runnable runnable) {
    if (mMainHandler == null) {
        synchronized (mLock) {
            if (mMainHandler == null) {
                mMainHandler = createAsync(Looper.getMainLooper());//1
            }
        }
    }

    mMainHandler.post(runnable);//2
}

注释 1:实例了一个 Handler 对象,注意构造参数 Looper.getMainLooper()是主线的 Looper。那么就可做到线程切换了。

注释 2:调用post 方法。

下面看下这个 Runnable

ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);

这里面的方法参数是mPostValueRunnable是个 Runnable,我们看下代码

//LiveData.java
private final Runnable mPostValueRunnable = new Runnable() {
    @SuppressWarnings("unchecked")
    @Override
    public void run() {
        Object newValue;
        synchronized (mDataLock) {
            newValue = mPendingData;
            mPendingData = NOT_SET;
        }
        setValue((T) newValue);//1
    }
};

注意:postValue方法其实最终调用也是setValue方法,然后和setValue方法走的流程就是一样的了,这个上面已经分析过了。详情请看 7.1 小节

但是我们还不知道ObserverWrapper是啥,好那么接下来,我们的重点来了

我们要详细看一下LifecycleBoundObserver类了,它包装了LifecycleOwner和Observer,这就是接下来的重点内容了。

8.LifecycleBoundObserver类

再贴下一下代码,当LiveData调用observe方法时

//LiveData.java
@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
    assertMainThread("observe");
    if (owner.getLifecycle().getCurrentState() == DESTROYED) {
        // ignore
        return;
    }
    //1
    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
    if (existing != null && !existing.isAttachedTo(owner)) {
        throw new IllegalArgumentException("Cannot add the same observer"
                + " with different lifecycles");
    }
    if (existing != null) {
        return;
    }
    owner.getLifecycle().addObserver(wrapper);
}

注释 1 :用LifecycleBoundObserver对LifecycleOwner 和 Observer进行了包装

8.1.来看下LifecycleBoundObserver类,它是LiveData的内部类
//LiveData.java
class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
    @NonNull
    final LifecycleOwner mOwner;

    LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
        super(observer);
        mOwner = owner;
    }
}

两个参数,一个 owner被成员变量mOwner存储,observer参数被ObserverWrapper的 mObserver存储。

  • LifecycleEventObserver是LifecycleObserver的子接口里面有一个onStateChanged方法,这个方法会在 Activity、Fragment 生命周期回调时调用,如果这么说不明看下这篇文章Android Jetpack组件Lifecycle基本使用和原理分析
  • ObserverWrapper 是Observer包装类

我们接下来看下ObserverWrapper类

8.2.ObserverWrapper类
private abstract class ObserverWrapper {
    final Observer<? super T> mObserver;
    boolean mActive;
    int mLastVersion = START_VERSION;
        //1
    ObserverWrapper(Observer<? super T> observer) {
        mObserver = observer;
    }
        //2
    abstract boolean shouldBeActive();

    boolean isAttachedTo(LifecycleOwner owner) {
        return false;
    }

    void detachObserver() {
    }

    void activeStateChanged(boolean newActive) {
        if (newActive == mActive) {
            return;
        }
        // immediately set active state, so we'd never dispatch anything to inactive
        // owner
        mActive = newActive;
        boolean wasInactive = LiveData.this.mActiveCount == 0;
        LiveData.this.mActiveCount += mActive ? 1 : -1;
        if (wasInactive && mActive) {
            //3
            onActive();
        }
        if (LiveData.this.mActiveCount == 0 && !mActive) {
            //4
            onInactive();
        }
        if (mActive) {
            //5
            dispatchingValue(this);
        }
    }
}

注:活跃状态指的是 Activity、Fragment 等生命周期处于活跃状态

注释 1:获取了我们的 Observer 对象,存储在 成员变量mObserver身上

注释 2:抽象方法,当前是否是活跃的状态

注释 3:可以继承 LiveData 来达到扩展 LiveData 的目标,并且是在活跃的状态调用

注释 4:可以继承 LiveData 来达到扩展 LiveData 的目标,并且是在非活跃的状态调用

注释 5:活跃状态,发送最新的值,来达到通知的作用, dispatchingValue(this)方法咋这么眼熟,对之前在 LiveData 调用 setValue 方法时,最终也会调用到此方法。那ObserverWrapper类中的dispatchingValue这个方法是在activeStateChanged方法中调用,那activeStateChanged啥时候调用呢?

我来看下ObserverWrapper的子类也就是最重要的那个类LifecycleBoundObserver,现在看它的完整代码

8.3.LifecycleBoundObserver完整代码

这里是关键代码了

//LiveData.java
class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
    @NonNull
    final LifecycleOwner mOwner;

    LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) {
        super(observer);
        mOwner = owner;
    }
  
    @Override
    boolean shouldBeActive() {
                //1
        return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
    }

    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
            @NonNull Lifecycle.Event event) {
        //2
        if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
            removeObserver(mObserver);
            return;
        }
        //3
        activeStateChanged(shouldBeActive());
    }
        
    @Override
    boolean isAttachedTo(LifecycleOwner owner) {
        return mOwner == owner;
    }
        
    @Override
    void detachObserver() {
         //4
        mOwner.getLifecycle().removeObserver(this);
    }
}

注释 1:判断当前的 Lifecycle 的生命周期是否是活跃状态,会在回调观察则 Observer 的时候进行判断,只有在活跃状态,才会回调观察者Observer的onChanged方法。

直接就回答了我们上面的这个问题LiveData 为什么只会将更新通知给活跃的观察者。非活跃观察者不会收到更改通知? 首先会通过LifecycleOwner获取Lifecycle对象然后获取Lifecycle 的State,并且状态大于STARTED。这里的State是和 Activity、Fragment 的生命周期是对应的,具体看这篇文章 Android Jetpack组件Lifecycle基本使用和原理分析 的第4.4小节,有详细的解释。

注释 2:onStateChanged每次 Activity、Fragment的生命周期回调的时候,都会走这个方法。

获取Lifecycle对象然后获取Lifecycle 的State如果为DESTROYED则移除观察者,在 Activity、Fragment的生命周期走到 onDestroy 的时候,就会取消订阅,避免内存泄漏。

注释 3:调用父类ObserverWrapper 的activeStateChanged方法,层层调用到观察者Observer的onChanged方法。(自己看下源码一目了然)

重点来了:在LiveData 调用setValue方法时,会回调观察者Observer的onChanged方法,Activity、Fragment的生命周期变化的时候且为活跃也会回调观察者Observer的onChanged方法。这就是为什么你在ActivityB页面,调用setValue方法,更新了value,在ActivityA 重新获取焦点时也同样会收到这个最新的值。

注释 4:移除观察者Observer,解除订阅关系。

到这个时候,LiveData 的 observer方法setValue方法,整个流程就分析完了。

如果我们想不管生命周期,而是想在setValue的值发生改变的时候就能接受到通知,LiveData 还提供了一个observeForever方法

class LiveDataActivity : BaseActivity() {
    private val mContent = MutableLiveData<String>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_live_data)

        btnUpdate.setOnClickListener {
            mContent.value = "最新值是:Update"
        }

        //只要在值发生改变时,就能接收到
        mContent.observeForever { content ->
            tvContent.text = content
        }
    }
}
8.4LiveData 的observeForever方法

这个方法比observe方法少一个LifecycleOwner参数,为啥呢?因为这个方法不需要感知生命周期,需要在setValue 值更新时立马收到回调。

来看下具体代码

//LiveData.java
@MainThread
public void observeForever(@NonNull Observer<? super T> observer) {
    assertMainThread("observeForever");
    //1
    AlwaysActiveObserver wrapper = new AlwaysActiveObserver(observer);
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
    if (existing instanceof LiveData.LifecycleBoundObserver) {
        throw new IllegalArgumentException("Cannot add the same observer"
                + " with different lifecycles");
    }
    if (existing != null) {
        return;
    }
    wrapper.activeStateChanged(true);
}

注释 1 :这里用到的是AlwaysActiveObserverobserve方法用到是LifecycleBoundObserver

看一下这个AlwaysActiveObserver

//LiveData.java
private class AlwaysActiveObserver extends ObserverWrapper {

    AlwaysActiveObserver(Observer<? super T> observer) {
        super(observer);
    }

    @Override
    boolean shouldBeActive() {
        return true;
    }
}

代码非常的简洁,在shouldBeActive方法中,直接 return true,这也太秀了吧

为啥直接返回 true 呢?因为这里不用管生命周期,永远都是活跃状态,所以这个方法叫observeForever

LiveData 的源码非常值得读,而且量不是很大,里面有许多值得学习的地方。

9.使用 LiveData 的优势

这个是Google官方总结的

使用 LiveData 具有以下优势:

  • 确保界面符合数据状态

    LiveData 遵循观察者模式。当生命周期状态发生变化时,LiveData 会通知 Observer 对象。您可以整合代码以在这些 Observer 对象中更新界面。观察者可以在每次发生更改时更新界面,而不是在每次应用数据发生更改时更新界面。

  • 不会发生内存泄漏

    观察者会绑定到 Lifecycle 对象,并在其关联的生命周期遭到销毁后进行自我清理。

  • 不会因 Activity 停止而导致崩溃

    如果观察者的生命周期处于非活跃状态(如返回栈中的 Activity),则它不会接收任何 LiveData 事件。

  • 不再需要手动处理生命周期

    界面组件只是观察相关数据,不会停止或恢复观察。LiveData 将自动管理所有这些操作,因为它在观察时可以感知相关的生命周期状态变化。

  • 数据始终保持最新状态

    如果生命周期变为非活跃状态,它会在再次变为活跃状态时接收最新的数据。例如,曾经在后台的 Activity 会在返回前台后立即接收最新的数据。

  • 适当的配置更改

    如果由于配置更改(如设备旋转)而重新创建了 Activity 或 Fragment,它会立即接收最新的可用数据。

  • 共享资源

    您可以使用单一实例模式扩展 LiveData 对象以封装系统服务,以便在应用中共享它们。LiveData 对象连接到系统服务一次,然后需要相应资源的任何观察者只需观察 LiveData 对象。如需了解详情,请参阅扩展 LiveData

10.总结

开始回答第 3 节的抛出的问题

10.1.LiveData 是怎么回调的?
  • LiveData通过observe或者observeForever方法订阅了一个观察者
  • LiveData 通过调用setValuepostValue方法时,会取出观察者,调用它的onChanged方法
  • 当然,当 Activity、Fragment 生命周期由非活跃变化为活跃状态,也会收到最新的值回调onChanged方法,注意这个对应的是LiveData的observe方法。
10.2.LiveData 为什么可以感知生命周期?
  • 是因为LifecycleBoundObserver类
  • 以及在observe方法中调用了 owner.getLifecycle().addObserver(wrapper);这行代码,具体的看下上面的源码分析吧
10.3.LiveData 可以感知生命周期,有什么用,或者说有什么优势?
  • 可以自动取消订阅
  • 防止内存泄漏
10.4.LiveData 为什么只会将更新通知给活跃的观察者。非活跃观察者不会收到更改通知?
  • 在每次调用setValue方法时,最走到LifecycleBoundObserver的shouldBeActive这个方法的判断上
  • 这个方法返回的是状态为STARTED之后的状态才会走通知观察者回调的逻辑,否则就不执行,具体的看下上面的源码
10.5.LiveData此外还提供了observerForever()方法,所有生命周期事件都能通知到,怎么做到的?
  • 看 8.4 小节,有详细的分析。
  • 主要AlwaysActiveObserver的shouldBeActive这个方法直接返回的 true

11.源码地址

LiveDataActivity.kt

12.原文地址

Android Jetpack组件LiveData基本使用和原理分析

13.参考文章

LiveData

Android官方架构组件LiveData: 观察者模式领域二三事

推荐一下我开源的项目 WanAndroid 客户端

WanAndroidJetpack 架构图

image
  • 一个纯 Android 学习项目,WanAndroid 客户端。
  • 项目采用 MVVM 架构,用 Kotlin 语音编写。
  • Android Jetpack 的大量使用包括但不限于LifecycleLiveDataViewModelDatabindingRoomConstraintLayout等,未来可能会更多。
  • 采用 RetrofitKotlin-Coroutine 协程进行网络交互。
  • 加载图片 Glide 主流加载图片框架。
  • 数据存储主要用到了 Room 和腾讯的 MMKV

Kotlin + MVVM + Jetpack + Retrofit + Glide 的综合运用,是学习 MMVM 架构的不错的项目。

此项目本身也是一个专门学习 Android 相关知识的 APP,欢迎下载体验!

源码地址(附带下载链接)

WanAndroidJetpack

APP 整体概览

wan-gif.gif

喜欢的点个 Stars,有问题的请提 Issues。

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

推荐阅读更多精彩内容