MvvmLazy Android懶人框架

MvvmLazy Android懶人框架(kotlin版)

目前,android流行的MVC、MVP模式的開發(fā)框架很多,然而一款基于MVVM模式開發(fā)框架卻很少。
個人搜尋了市面上大量的開源框架,秉承減少重復造輪子的原則,汲取了各位大神的框架優(yōu)點,集成了大量常用的開源框架和工具類,進行了部分公用模塊封裝,豐富了BindingAdapter自定義數(shù)據(jù)綁定,創(chuàng)建了這套Android懶人開發(fā)框架,已在多個商業(yè)項目中經(jīng)過檢驗,可靠性值得信賴.
MvvmLazy是以谷歌DataBinding+LiveData+ViewModel框架為基礎,整合Okhttp+協(xié)程+Retrofit+Coil等流行模塊,加上各種原生控件自定義的BindingAdapter,讓事件與數(shù)據(jù)源完美綁定的一款容易上癮的實用性MVVM快速開發(fā)框架。從此告別findViewById(),告別setText(),告別setOnClickListener()...

github地址

框架特點

  • 快速開發(fā)

    只需要寫項目的業(yè)務邏輯,不用再去關心網(wǎng)絡請求、權限申請、View的生命周期等問題,擼起袖子就是干。

  • 維護方便

    MVVM開發(fā)模式,低耦合,邏輯分明。Model層負責將請求的數(shù)據(jù)交給ViewModel;ViewModel層負責將請求到的數(shù)據(jù)做業(yè)務邏輯處理,最后交給View層去展示,與View一一對應;View層只負責界面繪制刷新,不處理業(yè)務邏輯,非常適合分配獨立模塊開發(fā)。

  • 流行框架

    retrofit+ okhttp+
    gson 負責解析json數(shù)據(jù);
    coil 負責加載圖片;
    permissionx 負責Android 6.0權限申請;
    xpopup 多種樣式Dialog框架
    LiveEventBus LiveEventBus是一款Android消息總線,基于LiveData,具有生命周期感知能力,支持Sticky,支持AndroidX,支持跨進程,支持跨APP。
    BaseRecyclerViewAdapterHelper 大名鼎鼎的BaseRecyclerViewAdapterHelper RecyclerView適配器管理框架
    TabLayout 一個功能強大的TabLayout框架
    youth.banner 一個功能強大的banner框架
    immersionbar 一個沉浸式管理框架
    TitleBar 公用標題欄框架
    SmartRefreshLayout 下拉刷新框架
    RWidgetHelper 代替selector,各個state狀態(tài)背景/邊框/文字變色,不用再寫大量的shape文件了
    ARouter 阿里路由框架

  • 數(shù)據(jù)綁定

    滿足google目前控件支持的databinding雙向綁定,并擴展原控件一些不支持的數(shù)據(jù)綁定。例如將圖片的url路徑綁定到ImageView控件中,在BindingAdapter方法里面則使用Glide加載圖片;View的OnClick事件在BindingAdapter中方法處理防重復點擊,再把事件回調到ViewModel層,實現(xiàn)xml與ViewModel之間數(shù)據(jù)和事件的綁定。

  • 基類封裝

    專門針對MVVM模式打造的BaseActivity、BaseFragment、BaseViewModel,在View層中不再需要定義ViewDataBinding和ViewModel,直接在BaseActivity、BaseFragment上限定泛型即可使用。普通界面只需要編寫Fragment,然后使用ContainerActivity盛裝(代理),這樣就不需要每個界面都在AndroidManifest中注冊一遍。

  • 全局操作

    1. 全局的Activity堆棧式管理,在程序任何地方可以打開、結束指定的Activity,一鍵退出應用程序。
    2. LoggingInterceptor全局攔截網(wǎng)絡請求日志,打印Request和Response,格式化json、xml數(shù)據(jù)顯示,方便與后臺調試接口。
    3. 全局Cookie,支持SharedPreferences和內存兩種管理模式。
    4. 通用的網(wǎng)絡請求異常監(jiān)聽,根據(jù)不同的狀態(tài)碼或異常設置相應的message。
    5. 全局的異常捕獲,程序發(fā)生異常時不會崩潰,可跳入異常界面重啟應用。
    6. 全局事件回調,提供LiveEventBus回調方式。
    7. 全局任意位置一行代碼實現(xiàn)文件下載進度監(jiān)聽(暫不支持多文件進度監(jiān)聽)。
    8. 全局點擊事件防抖動處理,防止點擊過快。

1、準備工作

網(wǎng)上的很多有關MVVM的資料,在此就不再闡述什么是MVVM了,不清楚的朋友可以先去了解一下。todo-mvvm-live

1.1、啟用databinding

在主工程app的build.gradle的android {}中加入:

dataBinding {
    enabled true
}

1.2、依賴Library

從遠程依賴:

在根目錄的build.gradle中加入

allprojects {
    repositories {
        ...
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
    }
}

在主項目app的build.gradle中依賴

dependencies {  
    ...
    api project(':mvvmlazy')
}

1.3、配置config.gradle

如果不是遠程依賴,而是下載的例子程序,那么還需要將例子程序中的config.gradle放入你的主項目根目錄中,然后在根目錄build.gradle的第一行加入:

apply from: "config.gradle"

注意: config.gradle中的

android = [] 是你的開發(fā)相關版本配置,可自行修改

android_x = [] 是android_x相關配置,可自行修改

dependencies = [] 是依賴第三方庫的配置,可以加新庫,但不要去修改原有第三方庫的版本號,不然可能會編譯不過

1.4、配置AndroidManifest

添加權限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

配置Application:

可以在你的自己AppApplication中配置

//是否開啟日志打印
KLog.init(true);
//配置全局異常崩潰操作
CaocConfig.Builder.create()
    .backgroundMode(CaocConfig.BACKGROUND_MODE_SILENT) //背景模式,開啟沉浸式
    .enabled(true) //是否啟動全局異常捕獲
    .showErrorDetails(true) //是否顯示錯誤詳細信息
    .showRestartButton(true) //是否顯示重啟按鈕
    .trackActivities(true) //是否跟蹤Activity
    .minTimeBetweenCrashesMs(2000) //崩潰的間隔時間(毫秒)
    .errorDrawable(R.mipmap.ic_launcher) //錯誤圖標
    .restartActivity(LoginActivity.class) //重新啟動后的activity
    //.errorActivity(YourCustomErrorActivity.class) //崩潰后的錯誤activity
    //.eventListener(new YourCustomEventListener()) //崩潰后的錯誤監(jiān)聽
    .apply()

2、快速上手

2.1、第一個Activity

以大家都熟悉的登錄操作為例:三個文件LoginActivty.javaLoginViewModel.java、activity_login.xml

2.1.1、關聯(lián)ViewModel

在activity_login.xml中關聯(lián)LoginViewModel。

<layout>
    <data>
        <variable
            type="com.rui.MvvmLazy.ui.login.LoginViewModel"
            name="viewModel"
        />
    </data>
    .....

</layout>

variable - type:類的全路徑
variable - name:變量名

2.1.2、繼承BaseActivity

LoginActivty



class LoginActivty : BaseVmDbActivity<ActivityloginBinding, LoginViewModel>() {
    override fun initContentView(): Int {
        return R.layout.activity_login
    }

    override fun initVariableId(): Int {
        return BR.viewModel
    }

    override fun initData() {
        super.initData()
    }

    override fun initTitleBar(titleBar: TitleBar?) {
        super.initTitleBar(titleBar)
        titleBar!!.title = "登錄頁面"
    }
}

保存activity_login.xml后databinding會生成一個ActivityloginBinding類。(如果沒有生成,試著點擊Build->Clean Project)

BaseActivity是一個抽象類,有兩個泛型參數(shù),一個是ViewDataBinding,另一個是BaseViewModel,上面的ActivityLoginBinding則是繼承的ViewDataBinding作為第一個泛型約束,LoginViewModel繼承BaseViewModel作為第二個泛型約束。

重寫B(tài)aseActivity的二個抽象方法

initContentView() 返回界面layout的id

initVariableId() 返回變量的id,對應activity_login中name="viewModel",就像一個控件的id,可以使用R.id.xxx,這里的BR跟R文件一樣,由系統(tǒng)生成,使用BR.xxx找到這個ViewModel的id。

2.1.3、繼承BaseViewModel

LoginViewModel繼承BaseViewModel

class LoginViewModel : BaseViewModel() {
    override fun initData() {
        super.initData()
    }
}

BaseViewModel與BaseActivity通過LiveData來處理常用UI邏輯,即可在ViewModel中使用父類的showDialog()、startActivity()等方法。在這個MainViewModel中就可以盡情的寫你的邏輯了!

BaseFragment的使用和BaseActivity一樣,詳情參考Demo。

2.2、數(shù)據(jù)綁定

擁有databinding框架自帶的雙向綁定,也有擴展

2.2.1、傳統(tǒng)綁定

綁定用戶名:

在ViewModel中定義

//用戶名的綁定
var userName = MutableLiveData<String>()

在用戶名EditText標簽中綁定

android:text="@={viewModel.userName}"

這樣一來,輸入框中輸入了什么,userName.get()的內容就是什么,userName.set("")設置什么,輸入框中就顯示什么。
注意: @符號后面需要加=號才能達到雙向綁定效果;userName需要是public的,不然viewModel無法找到它。

點擊事件綁定:

在在ViewModel中定義

//登錄按鈕的點擊事件

var loginOnClick: () -> Unit = {
        ToastUtils.showShort("登錄")
    }

在登錄按鈕標簽中綁定

android:onClick="@{viewModel.loginOnClick}"

這樣一來,用戶的點擊事件直接被回調到ViewModel層了,更好的維護了業(yè)務邏輯

這就是強大的databinding框架雙向綁定的特性,不用再給控件定義id,setText(),setOnClickListener()。

但是,光有這些,完全滿足不了我們復雜業(yè)務的需求??!MvvmLazy閃亮登場:它有一套自定義的綁定規(guī)則,可以滿足大部分的場景需求,請繼續(xù)往下看。

2.2.2、自定義綁定

還拿點擊事件說吧,不用傳統(tǒng)的綁定方式,使用自定義的點擊事件綁定。

在LoginViewModel中定義

//登錄按鈕的點擊事件
var loginOnClick: () -> Unit = {
        ToastUtils.showShort("登錄")
    }

在activity_login中定義命名空間

xmlns:binding="http://schemas.android.com/apk/res-auto"

在登錄按鈕標簽中綁定

binding:onClickCommand="@{viewModel.loginOnClick}"

這和原本傳統(tǒng)的綁定不是一樣嗎?不,這其實是有差別的。使用這種形式的綁定,在原本事件綁定的基礎之上,帶有防重復點擊的功能,1秒內多次點擊也只會執(zhí)行一次操作。如果不需要防重復點擊,可以加入這條屬性

binding:isThrottleFirst="@{Boolean.TRUE}"

那這功能是在哪里做的呢?答案在下面的代碼中。

/**
* requireAll 是意思是是否需要綁定全部參數(shù), false為否
* View的onClick事件綁定
* onClickCommand 綁定的命令,
* isThrottleFirst 是否開啟防止過快點擊
*/
   @JvmStatic
    @BindingAdapter(value = ["onClickCommand", "isThrottleFirst"], requireAll = false)
    fun onClickCommand(view: View, clickCommand: () -> Unit, isThrottleFirst: Boolean) {
        if (isThrottleFirst) {
            view.setOnClickListener { clickCommand.invoke() }
        } else {
            val mHits = LongArray(2)
            view.setOnClickListener {
                System.arraycopy(mHits, 1, mHits, 0, mHits.size - 1)
                mHits[mHits.size - 1] = SystemClock.uptimeMillis()
                if (mHits[0] < SystemClock.uptimeMillis() - 500) {
                    clickCommand.invoke()
                }
            }
        }
    }

onClickCommand方法是自定義的,使用@BindingAdapter注解來標明這是一個綁定方法。在方法中使用了RxView來增強view的clicks事件,.throttleFirst()限制訂閱者在指定的時間內重復執(zhí)行,最后通過BindingCommand將事件回調出去,就好比有一種攔截器,在點擊時先做一下判斷,然后再把事件沿著他原有的方向傳遞。

是不是覺得有點意思,好戲還在后頭呢!

2.2.3、自定義ImageView圖片加載

綁定圖片路徑:

在ViewModel中定義

var  imgUrl = "http://img0.imgtn.bdimg.com/it/u=2183314203,562241301&fm=26&gp=0.jpg";

在ImageView標簽中

加載普通圖片
binding:bindImgUrl="@{viewModel.imgUrl}"
加載圓形圖片
binding:bindCircleImgUrl="@{viewModel.imgUrl}"
加載圓角圖片
binding:bindCorners="@{20}"
binding:bindCornersImgUrl="@{viewModel.imgUrl}"

url是圖片路徑,這樣綁定后,這個ImageView就會去顯示這張圖片,不限網(wǎng)絡圖片還是本地圖片。

如果需要給一個默認加載中的圖片,可以加這一句

binding:placeholderRes="@{R.mipmap.ic_launcher_round}"

R文件需要在data標簽中導入使用,如:<import type="com.goldze.MvvmLazy.R" />
如果需要圖片居中剪切

binding:centerCrop="@{true}"

BindingAdapter中的實現(xiàn)

    @JvmStatic
    @BindingAdapter(value = ["bindImgUrl", "placeholderRes", "centerCrop"], requireAll = false)
    fun bindImgUrl(imageView: ImageView, url: String?, placeholderRes: Int?, centerCrop: Boolean?) {
        var requestBuilder = Glide.with(imageView.context).asDrawable().load(url)
        if (centerCrop == null || centerCrop) {
            requestBuilder = requestBuilder.centerCrop()
        }
        requestBuilder.apply(
            RequestOptions().placeholder(
                createDefPlaceHolder(
                    imageView.context,
                    placeholderRes,
                    0f
                )
            ).override(imageView.width, imageView.height)
        )
            .into(imageView)
    }

很簡單就自定義了一個ImageView圖片加載的綁定,學會這種方式,可自定義擴展。

如果你對這些感興趣,可以下載源碼,在binding包中可以看到各類控件的綁定實現(xiàn)方式

2.2.4、RecyclerView綁定

RecyclerView也是很常用的一種控件,傳統(tǒng)的方式需要針對各種業(yè)務要寫各種Adapter,如果你使用了MvvmLazy,則可大大簡化這種工作量,從此告別setAdapter()。
使用大名鼎鼎的BaseRecyclerViewAdapterHelper負責管理RecyclerView的適配器;

在ViewModel中定義:

//聲明adapter
 var lineAdapter = object :
        DataBindingAdapter<JokeInfo, TestLayoutItemJokeBinding>(R.layout.test_layout_item_joke) {
        override fun convertItem(
            holder: BaseViewHolder,
            binding: TestLayoutItemJokeBinding?,
            item: JokeInfo
        ) {
            binding!!.entity = item
        }
    }

在xml中綁定

 <androidx.recyclerview.widget.RecyclerView
                bindAdapter="@{viewModel.lineAdapter}"
                layoutManager="@{LayoutManagers.linear()}"
                lineManager="@{LineManagers.divider(@color/divider,1)}"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"/>

layoutManager控制是線性(包含水平和垂直)排列還是網(wǎng)格排列,lineManager是設置分割線

水平布局的寫法:binding:layoutManager="@{LayoutManagers.linear(LinearLayoutManager.HORIZONTAL,Boolean.FALSE)}"</br>
網(wǎng)格布局的寫法:binding:layoutManager="@{LayoutManagers.grid(3)}</br>
瀑布流布局的寫法:`binding:layoutManager="@{LayoutManagers.staggeredGrid(3,LayoutManagers.VERTICAL)}"</br>

使用到相關類,則需要導入該類才能使用,和導入Java類相似

<import type="com.rui.mvvmlazy.binding.viewadapter.recyclerview.LayoutManagers" /></br>
<import type="com.rui.mvvmlazy.binding.viewadapter.recyclerview.LineManagers" /></br>

詳細可以參考例子程序中ListViewModel類。

2.3、網(wǎng)絡請求

網(wǎng)絡請求一直都是一個項目的核心,現(xiàn)在的項目基本都離不開網(wǎng)絡,一個好用網(wǎng)絡請求框架可以讓開發(fā)事半功倍。

2.3.1、Retrofit+Okhttp+RxJava3

現(xiàn)今,這三個組合基本是網(wǎng)絡請求的標配,如果你對這三個框架不了解,建議先去查閱相關資料。

square出品的框架,用起來確實非常方便。MvvmLazy中引入了

api "com.squareup.okhttp3:okhttp:4.9.1"
api "com.squareup.retrofit2:retrofit:2.9.0"
api "com.squareup.retrofit2:converter-gson:2.9.0"
api "com.squareup.retrofit2:adapter-rxjava3:2.9.0"

構建Retrofit時加入

var retrofit = Retrofit.Builder()
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
    .build();

或者直接使用例子程序中封裝好的RetrofitClient

2.3.2、網(wǎng)絡攔截器

LoggingInterceptor: 全局攔截請求信息,格式化打印Request、Response,可以清晰的看到與后臺接口對接的數(shù)據(jù),

var mLoggingInterceptor = LoggingInterceptor
    .Builder()//構建者模式
    .loggable(true) //是否開啟日志打印
    .setLevel(Level.BODY) //打印的等級
    .log(Platform.INFO) // 打印類型
    .request("Request") // request的Tag
    .response("Response")// Response的Tag
    .addHeader("version", BuildConfig.VERSION_NAME)//打印版本
    .build()

構建okhttp時加入

var okHttpClient =OkHttpClient.Builder()
    .addInterceptor(mLoggingInterceptor)
    .build()

CacheInterceptor: 緩存攔截器,當沒有網(wǎng)絡連接的時候自動讀取緩存中的數(shù)據(jù),緩存存放時間默認為3天。</br>
創(chuàng)建緩存對象

//緩存時間
var CACHE_TIMEOUT = 10 * 1024 * 1024
//緩存存放的文件
var httpCacheDirectory = new File(mContext.getCacheDir(), "goldze_cache");
//緩存對象
var cache = Cache(httpCacheDirectory, CACHE_TIMEOUT);

構建okhttp時加入

var okHttpClient =  OkHttpClient.Builder()
    .cache(cache)
    .addInterceptor(new CacheInterceptor(mContext))
    .build()

2.3.3、Cookie管理

MvvmLazy提供兩種CookieStore:PersistentCookieStore (SharedPreferences管理)和MemoryCookieStore (內存管理),可以根據(jù)自己的業(yè)務需求,在構建okhttp時加入相應的cookieJar

var okHttpClient =  OkHttpClient.Builder()
    .cookieJar( CookieJarImpl( PersistentCookieStore(mContext)))
    .build()

或者

var okHttpClient = OkHttpClient.Builder()
    .cookieJar()
    .build()

2.3.4、網(wǎng)絡請求

請求在ViewModel層。默認在BaseActivity中注入了LifecycleProvider對象到ViewModel,用于綁定請求的生命周期,View與請求共存亡。

  request({ repository.getJoke(pageIndex, 10, "video") }, {
                  //處理請求結果
                }, {
                //處理異常情況
                })

2.3.5、網(wǎng)絡異常處理

網(wǎng)絡異常在網(wǎng)絡請求中非常常見,比如請求超時、解析錯誤、資源不存在、服務器內部錯誤等,在客戶端則需要做相應的處理(當然,你可以把一部分異常甩鍋給網(wǎng)絡,比如當出現(xiàn)code 500時,提示:請求超時,請檢查網(wǎng)絡連接,此時偷偷將異常信息發(fā)送至后臺(手動滑稽))。

MvvmLazy中自定義了一個ExceptionHandle,已為你完成了大部分網(wǎng)絡異常的判斷,也可自行根據(jù)項目的具體需求調整邏輯。

注意: 這里的網(wǎng)絡異常code,并非是與服務端協(xié)議約定的code。網(wǎng)絡異??梢苑譃閮刹糠郑徊糠质菂f(xié)議異常,即出現(xiàn)code = 404、500等,屬于HttpException,另一部分為請求異常,即出現(xiàn):連接超時、解析錯誤、證書驗證失等。而與服務端約定的code規(guī)則,它不屬于網(wǎng)絡異常,它是屬于一種業(yè)務異常。在請求中可以使用RxJava的filter(過濾器),也可以自定義BaseSubscriber統(tǒng)一處理網(wǎng)絡請求的業(yè)務邏輯異常。由于每個公司的業(yè)務協(xié)議不一樣,所以具體需要你自己來處理該類異常。

3、輔助功能

一個完整的快速開發(fā)框架,當然也少不了常用的輔助類。下面來介紹一下MVVLazy中有哪些輔助功能。

3.1、事件總線

事件總線存在的優(yōu)點想必大家都很清楚了,android自帶的廣播機制對于組件間的通信而言,使用非常繁瑣,通信組件彼此之間的訂閱和發(fā)布的耦合也比較嚴重,特別是對于事件的定義,廣播機制局限于序列化的類(通過Intent傳遞),不夠靈活。

3.3.1、LiveEventBus

LiveEventBus是一款Android消息總線,基于LiveData,具有生命周期感知能力,支持Sticky,支持AndroidX,支持跨進程,支持跨APP

使用方法:

//發(fā)送消息
LiveEventBus.get("key").post("value");
//發(fā)送一條延時消息 3秒跳轉
LiveEventBus.get("key").postDelay("value",3000);

//接收消息
LiveEventBus.get("key",String.class).observe(this) {

    
}

更多使用方法請參考 https://github.com/JeremyLiao/LiveEventBus

3.2、ContainerActivity

一個盛裝Fragment的一個容器(代理)Activity,普通界面只需要編寫Fragment,使用此Activity盛裝,這樣就不需要每個界面都在AndroidManifest中注冊一遍

使用方法:

在ViewModel中調用BaseViewModel的方法開一個Fragment

startContainerActivity(你的Fragment類名.class.getCanonicalName())

在ViewModel中調用BaseViewModel的方法,攜帶一個序列化實體打開一個Fragment

var mBundle = Bundle()
mBundle.putParcelable("entity", entity)
startContainerActivity(你的Fragment類名.class.getCanonicalName(), mBundle)

在你的Fragment中取出實體

var mBundle = getArguments()
if (mBundle != null) {
    entity = mBundle.getParcelable("entity")
}

3.3、6.0權限申請

對PermissionX已經(jīng)熟悉的朋友可以跳過。

使用方法:

例如請求相機權限,在ViewModel中調用

  PermissionX.init(activity)
            .permissions(
                Manifest.permission.READ_CONTACTS,
                Manifest.permission.CAMERA,
                Manifest.permission.CALL_PHONE
            )
            .onExplainRequestReason { scope, deniedList ->
                scope.showRequestReasonDialog(
                    deniedList,
                    "Core fundamental are based on these permissions",
                    "OK",
                    "Cancel"
                )
            }
            .request { allGranted, grantedList, deniedList ->
                if (allGranted) {
                    toast("All permissions are granted")
                } else {
                    toast("These permissions are denied: $deniedList")
                }
            }

更多權限申請方式請參考PermissionX原項目地址

3.4、其他輔助類

ToastUtils: 吐司工具類

SPUtils: SharedPreferences工具類

SDCardUtils: SD卡相關工具類

ConvertUtils: 轉換相關工具類

StringUtils: 字符串相關工具類

RegexUtils: 正則相關工具類

KLog: 日志打印,含json格式打印

更多工具類查看mvvmlazy下面utils目錄

3.5、demo示例

項目中提供了大量的demo示例,可自行下載源碼查看

3.6、組件化方案

項目組件化方案參考了[MVVMHabitComponent] [https://github.com/goldze/MVVMHabitComponent]
組件初始化方案采用Startup方案.

About

** 本人喜歡嘗試新的技術,以后發(fā)現(xiàn)有好用的東西,我將會在企業(yè)項目中實戰(zhàn),沒有問題了就會把它引入到MvvmLazy中,一直維護著這套框架,謝謝各位朋友的支持。如果覺得這套框架不錯的話,麻煩點個 star,你的支持則是我前進的動力!

郵箱664209769@qq.com

License

Copyright 2021 趙繼瑞

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 更新日志 v3.0.0:2018年10月8日 全面升級AAC,引入谷歌lifecycle組件; 修改Base基類,...
    goldze閱讀 28,102評論 10 74
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月,有人笑有人哭,有人歡樂有人憂愁,有人驚喜有人失落,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,822評論 28 54
  • 人工智能是什么?什么是人工智能?人工智能是未來發(fā)展的必然趨勢嗎?以后人工智能技術真的能達到電影里機器人的智能水平嗎...
    ZLLZ閱讀 4,095評論 0 5
  • 首先介紹下自己的背景: 我11年左右入市到現(xiàn)在,也差不多有4年時間,看過一些關于股票投資的書籍,對于巴菲特等股神的...
    瞎投資閱讀 5,935評論 3 8
  • ![Flask](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAW...
    極客學院Wiki閱讀 7,767評論 0 3

友情鏈接更多精彩內容