最近看到了幾篇與 Jetpack MVVM 有關到文章,使我不禁也想淌一下這場混水。我是在 2017 年下半年接觸的 Jetpack 的那套開發工具,並且後來一直將其作爲開發的主要框架。在這段時間的使用過程中,我踩過一些坑,也積累了一些經驗,爲了將其推廣到其它到項目中又專門封裝出了一個庫。當然,Jetpack 所提供的組件已經比較完善,我的工作只能算是錦上添花。下面我就介紹下,現在我是如何在項目中使用 Jetpack MVVM 的。
1、後起之秀和黯然失色的 MVP
MVP 非常強大,也是或者曾經是很多大公司首選的開發框架。但是相比於如今的 MVVM,曾經的 MVP 模式在使用起來存在諸多的不便,難免顯得黯然失色。
首當其衝的是,在使用 MVP 編寫客戶端代碼的時候你要寫大量的接口和類。一般的 MVP 模式中,我們需要先通過接口定義 Model、View 和 Presenter 要實現的方法;然後,需要再編寫三個類來實現以上三個接口的邏輯。就是說,一般的,爲了實現一個界面的邏輯,你需要編寫至少六個類文件。
其次,Presenter 比較臃腫。Presenter 負責 View 和 Model 層之間的交互,請求的結果及其與 View 層的交互邏輯都在 Presenter 中完成。很多框架中,爲了解決 View 和 Model 層之間的強引用關係,在 Presenter 定義 Handler 來通過消息的方式傳遞信息。這就使得代碼的過於模版化,增加了很多與具體業務邏輯無關的代碼。此外,如果使用 Handler 作爲消息傳遞的橋樑,因爲通過更新 UI 又發生在主線程裏面,就可能會導致主線程消息堆積過多。
那 MVVM 是如何做的呢?下面的這張圖大致地詮釋了 Jetpack MVVM。實際上,Jetpack 所提供的 MVVM 核心的功能只有 ViewModel 和 LiveData 兩個。ViewModel 主要用來通過 Model 請求數據信息,然後通過 LiveData 作爲信息交互的橋樑,將信息傳遞給 View 層。從類的數量的角度上來看,使用 MVVM 實現一個頁面,你只需要寫三個類文件。
而作爲 ViewModel 和 View 層交互的中介的 LiveData 呢?你只需要將它看作一個普通的變量就可以了。它內部緩存了我們的數據。它會在你修改這個數據的時候通知所有的觀察者數據的變更。所以,LiveData 不存在 Handler 那樣是消息擁堵的問題。
此外,使用 LiveData 還可以解決頁面頻繁刷新以及刷新的時機的問題。比如,一個處於後臺的頁面通過 EventBus 監聽數據變化之後會先被保留到 LiveData,然後當頁面回到前臺的時候再一次性刷新 UI。這樣就避免了處於後臺的頁面多次刷新 UI 又控制了刷新的時機。
如果你不熟悉 MVP、MVVM 等架構模式,或者你想知道 LiveData 和 ViewModel 的實現原理,可以參考我之前的相關的文章(獲取更多技術文章,可以關注我的公衆號【Code Brick】):
2、Jetpack MVVM 的項目實踐
Jetpack 提供的 MVVM 已經足夠強大,很多人會認爲沒有必要在其基礎之上做進一步封裝。正因如此,使得 MVVM 在實際應用過程中呈現出了許多千奇百怪的姿態。比如,MVVM 和 MVP 雜糅在一起,ViewModel 和 View 層混亂的數據交互格式,ViewModel 中羅列出一堆的 LiveData 等等。實際上,通過簡單地封裝,我們可以更好地在項目中推廣和應用 MVVM。我開發 Android-VMLib 這個框架的目的也正在於此。
2.1 拒絕大雜燴,一個 Clean 的 MVVM 框架
首先,作爲一個 MVVM 的框架,Android-VMLib 所做的東西並不多,我並沒有將其與各種網絡框架等整合在一起,可以說得上是非常乾淨的一個框架。截至目前它的依賴關係如下,
也就是說,除了我的編寫的圖片壓縮庫 Compressor 以及一個工具類庫 Android-utils 之外,引入它並不會爲你的項目引入更多的庫。對於 EventBus,除了我們在項目中提供的一個封裝之外,也不會爲你引入任何類庫。至於友盟,我們只不過是爲你在頂層的 View 層裏注入了一些事件追蹤方法,也不會強迫你在項目中添加友盟依賴。也就是說,除了那兩個必選的庫之外,其它都是可選的。該框架的主要目的是賦能,是不是要在項目中應用完全取決於用戶。
好了,接下來就詳細介紹下這個類庫以及正確的使用 MVVM 的姿勢。
2.2 MVVM 和 Databinding 的曖昧關係
說到 MVVM,總是繞不開 Databinding. 我之前也瞭解過許多 MVVM 框架,很多都將 Databinding 作爲了項目必需的方案。事實上,MVVM 和 Databinding 之間沒有半毛錢的關係。
Databinding 提供了雙向的綁定功能,因此很多框架直接將 ViewModel 注入到了 xml 裏面。我個人是比較反感這種做法的。或許是早起 Databinding 不成熟的時候經常帶來莫名其妙的編譯錯誤 xxx.BR not found
以及 can't find variable
等,在使用 Databinding 的時候,我更多地通過它來獲取控件並在代碼中完成賦值。此外,xml 中的代碼缺少編譯時的檢查機制,比如當你在 xml 中將 int 類型賦值給 String 類型的時候,它不會在編譯期間報錯。另外,在 xml 中能完成的業務邏輯有限,一個三目運算符 ?:
就已經超出了代碼的寬度限制。很多時候你不得不將一部分業務放在 Java 代碼裏,一部分代碼放在 xml 裏。出現問題的時候,這就增加了排查問題的難度。記得之前在項目中使用 Databinding 的時候還出現過 UI 沒有及時刷新的問題,由於年代久遠,具體原因我已經記不大清。最後,使用 Databinding 還可能會拖慢項目編譯的速度,如果項目比較小的話或許問題不大,但對於一個模塊過百的大型項目而言,這無疑是雪上加霜。
就一個框架而言,Android-VMLib 在 Databinding 上所做的工作是賦能。我們爲你提供了應用 Databinding 的能力,同時也爲你提供了完全排除 Databinding 的方案。以 Activity 爲例,我們提供了 BaseActivity 和 CommonActivity 兩個抽象類。假如你想在項目中使用 Databinding,仿造下面的類這樣傳入佈局的 DataBinding 類,以及 ViewModel,然後通過 binding 獲取控件並使用即可:
class MainActivity : CommonActivity<MainViewModel, ActivityMainBinding>() {
override fun getLayoutResId(): Int = R.layout.activity_main
override fun doCreateView(savedInstanceState: Bundle?) {
// 通過 binding 獲取控件
setSupportActionBar(binding.toolbar)
}
}
假如你不想在項目中使用 Databinding,那麼你可以像下面的類這樣繼承 BaseActivity,然後通過傳統的 findViewById 來獲取控件並使用:
class ContainerActivity : BaseActivity<EmptyViewModel> {
override fun getLayoutResId(): Int = R.layout.vmlib_activity_container
override fun doCreateView(savedInstanceState: Bundle?) {
// 通過 findViewById 來獲取控件
// 或者,引入 kotlin-android-extensions 之後直接通過 id 使用控件
}
}
也許你看到了,我在使用 Databinding 的時候更多地將當作 ButterKinfe 來使用。我專門提供了不包含 Databinding 的能力,也是出於另一個考慮——使用 kotlin-android-extensions
之後,可以直接在代碼中通過控件的 id 來使用它。如果只是想通過 Databinding 來獲取控件的話,那麼就沒有必要使用 Databinding 的必要了。而對於確實喜歡 Databinding 的數據綁定功能的同學可以在 Android-VMLib 之上個性化封裝一層。當然了,我並不是排斥 Databinding。Databinding 是一個很好的設計理念,只是對於將其大範圍應用到項目中,我還是持觀望態度的。
2.3 統一數據交互格式
有過後端開發經驗的同學可能會知道,在後端代碼中,我們通常會將代碼按照層次分爲 DAO、Service 和 Controler 層,各個層之間進行數據交互的時候就需要對數據交互格式做統一的封裝。後端和前端之間的交互的時候也要對數據格式進行封裝。我們將其推廣到 MVVM 中,顯然,ViewModel 層和 View 層之間進行交互的時候也應該進行一層數據包裝。下面是我看到的一段代碼,
final private SingleLiveEvent<String> toast;
final private SingleLiveEvent<Boolean> loading;
public ApartmentProjectViewModel() {
toast = new SingleLiveEvent<>();
loading = new SingleLiveEvent<>();
}
public SingleLiveEvent<String> getToast() {
return toast;
}
public SingleLiveEvent<Boolean> getLoading() {
return loading;
}
public void requestData() {
loading.setValue(true);
ApartmentProjectRepository.getInstance().requestDetail(projectId, new Business.ResultListener<ProjectDetailBean>() {
@Override
public void onFailure(BusinessResponse businessResponse, ProjectDetailBean projectDetailBean, String s) {
toast.setValue(s);
loading.setValue(false);
}
@Override
public void onSuccess(BusinessResponse businessResponse, ProjectDetailBean projectDetailBean, String s) {
data.postValue(dealProjectBean(projectDetailBean));
loading.setValue(false);
}
});
}
這裏爲了通知 View 層數據的加載狀態定義了一個 Boolean 類型的 LiveData 進行交互。這樣你需要多維護一個變量,顯得代碼不夠簡潔。實際上,通過對數據交互格式的規範,我們可以更優雅地完成這個任務。
在 Android-VMLib 當中,我們通過自定義枚舉來表示數據的狀態,
public enum Status {
// 成功
SUCCESS(0),
// 失敗
FAILED(1),
// 加載中
LOADING(2);
public final int id;
Status(int id) {
this.id = id;
}
}
然後,將錯誤信息、數據結果、數據狀態以及預留字段包裝成一個 Resource 對象,來作爲固定的數據交互格式,
public final class Resources<T> {
// 狀態
public final Status status;
// 數據
public final T data;
// 狀態,成功或者錯誤的 code 以及 message
public final String code;
public final String message;
// 預留字段
public final Long udf1;
public final Double udf2;
public final Boolean udf3;
public final String udf4;
public final Object udf5;
// ...
}
解釋下這裏的預留字段的作用:它們主要用來作爲數據補充說明。比如進行分頁的時候,如果 View 層不僅想獲取真實的數據,還想得到當前的頁碼,那麼你可以將頁碼信息塞到 udf1 字段上面。以上,我對各種不同類型的基礎數據類型只提供了一個通用的選擇,比如整型的只提供了 Long 類型,浮點型的只提供了 Double 類型。另外,我們還提供了一個無約束的類型 udf5.
除了數據交互格式的封裝,Android-VMLib 還提供了交互格式的快捷操作方法。如下圖所示,
那麼,使用了 Resource 之後,代碼會變成什麼樣呢?
// View 層代碼
class MainActivity : CommonActivity<MainViewModel, ActivityMainBinding>() {
override fun getLayoutResId(): Int = R.layout.activity_main
override fun doCreateView(savedInstanceState: Bundle?) {
addSubscriptions()
vm.startLoad()
}
private fun addSubscriptions() {
vm.getObservable(String::class.java).observe(this, Observer {
when(it!!.status) {
Status.SUCCESS -> { ToastUtils.showShort(it.data) }
Status.FAILED -> { ToastUtils.showShort(it.message) }
Status.LOADING -> {/* temp do nothing */ }
else -> {/* temp do nothing */ }
}
})
}
}
// ViewModel 層代碼
class MainViewModel(application: Application) : BaseViewModel(application) {
fun startLoad() {
getObservable(String::class.java).value = Resources.loading()
ARouter.getInstance().navigation(MainDataService::class.java)
?.loadData(object : OnGetMainDataListener{
override fun onGetData() {
getObservable(String::class.java).value = Resources.loading()
}
})
}
}
這樣對數據交互格式封裝之後,代碼是不是簡潔多了呢?至於讓你的代碼更加簡潔,Android-VMLib 還爲你提供了其它的方法,請繼續閱讀。
2.4 進一步簡化代碼,優化無處不在的 LiveData
之前在使用 ViewModel+LiveData 的時候,爲了進行數據交互,每個變量我都需要定義一個 LiveData,於是代碼變成了下面這個樣子。甚至我在有的同學那裏看到過一個 ViewModel 中定義了 10+ 個 LiveData. 這讓代碼變得非常得難看,
public class ApartmentProjectViewModel extends ViewModel {
final private MutableLiveData<ProjectDetailBean> data;
final private SingleLiveEvent<String> toast;
final private SingleLiveEvent<Boolean> submit;
final private SingleLiveEvent<Boolean> loading;
public ApartmentProjectViewModel() {
data = new MutableLiveData<>();
toast = new SingleLiveEvent<>();
submit = new SingleLiveEvent<>();
loading = new SingleLiveEvent<>();
}
// ...
}
後來我的一個同事建議我考慮下如何整理一下 LiveData,於是經過不斷的推廣和演化,如今這個解決方案已經比較完善——即通過 HashMap 統一管理單例的 LiveData。後來爲了進一步簡化 ViewModel 層的代碼,我將這部分工作交給了一個 Holder 來完成。於是如下解決方案就基本成型了,
public class BaseViewModel extends AndroidViewModel {
private LiveDataHolder holder = new LiveDataHolder();
// 通過要傳遞的數據類型獲取一個 LiveData 對象
public <T> MutableLiveData<Resources<T>> getObservable(Class<T> dataType) {
return holder.getLiveData(dataType, false);
}
}
這裏的 Holder 實現如下,
public class LiveDataHolder<T> {
private Map<Class, SingleLiveEvent> map = new HashMap<>();
public MutableLiveData<Resources<T>> getLiveData(Class<T> dataType, boolean single) {
SingleLiveEvent<Resources<T>> liveData = map.get(dataType);
if (liveData == null) {
liveData = new SingleLiveEvent<>(single);
map.put(dataType, liveData);
}
return liveData;
}
}
原理很簡單吧。使用了這套方案之後你的代碼將會變得如下面這般簡潔優雅,
// ViewModel 層
class EyepetizerViewModel(application: Application) : BaseViewModel(application) {
private var eyepetizerService: EyepetizerService = ARouter.getInstance().navigation(EyepetizerService::class.java)
private var nextPageUrl: String? = null
fun requestFirstPage() {
getObservable(HomeBean::class.java).value = Resources.loading()
eyepetizerService.getFirstHomePage(null, object : OnGetHomeBeansListener {
override fun onError(errorCode: String, errorMsg: String) {
getObservable(HomeBean::class.java).value = Resources.failed(errorCode, errorMsg)
}
override fun onGetHomeBean(homeBean: HomeBean) {
nextPageUrl = homeBean.nextPageUrl
getObservable(HomeBean::class.java).value = Resources.success(homeBean)
// 再請求一頁
requestNextPage()
}
})
}
fun requestNextPage() {
eyepetizerService.getMoreHomePage(nextPageUrl, object : OnGetHomeBeansListener {
override fun onError(errorCode: String, errorMsg: String) {
getObservable(HomeBean::class.java).value = Resources.failed(errorCode, errorMsg)
}
override fun onGetHomeBean(homeBean: HomeBean) {
nextPageUrl = homeBean.nextPageUrl
getObservable(HomeBean::class.java).value = Resources.success(homeBean)
}
})
}
}
// View 層
class EyepetizerActivity : CommonActivity<EyepetizerViewModel, ActivityEyepetizerBinding>() {
private lateinit var adapter: HomeAdapter
private var loading : Boolean = false
override fun getLayoutResId() = R.layout.activity_eyepetizer
override fun doCreateView(savedInstanceState: Bundle?) {
addSubscriptions()
vm.requestFirstPage()
}
private fun addSubscriptions() {
vm.getObservable(HomeBean::class.java).observe(this, Observer { resources ->
loading = false
when (resources!!.status) {
Status.SUCCESS -> {
L.d(resources.data)
val list = mutableListOf<Item>()
resources.data.issueList.forEach {
it.itemList.forEach { item ->
if (item.data.cover != null
&& item.data.author != null
) list.add(item)
}
}
adapter.addData(list)
}
Status.FAILED -> {/* temp do nothing */ }
Status.LOADING -> {/* temp do nothing */ }
else -> {/* temp do nothing */ }
}
})
}
// ...
}
這裏我們通過 getObservable(HomeBean::class.java)
獲取一個 ViewModel 和 View 層之間交互的 LiveData<HomeBean>
,然後通過它進行數據傳遞。這種處理方式的好處是,你不需要在自己代碼中到處定義 LiveData,將 Holder 當作一個 LiveData 池,需要數據交互的時候直接從 Holder 中獲取即可。
有的同學可能會疑問,將 Class 作爲從“池”中獲取 LiveData 的唯一標記,會不會應用場景有限呢?Android-VMLib 已經考慮到了這個問題,下文踩坑部分會爲你講解。
2.5 共享 ViewModel,配置可以更簡單
如果多個 ViewModel 在同一 Activity 的 Fragment 之間進行共享,那麼該如何獲取呢?
如果是不實用 Android-VMLib,你只需要在 Fragment 中通過 Activity 獲取 ViewModel 即可,
ViewModelProviders.of(getActivity()).get(vmClass)
使用了 Android-VMLib 之後這個過程可以變得更加簡潔——直接在 Fragment 上聲明一個註解即可。比如,
@FragmentConfiguration(shareViewModel = true)
class SecondFragment : BaseFragment<SharedViewModel>() {
override fun getLayoutResId(): Int = R.layout.fragment_second
override fun doCreateView(savedInstanceState: Bundle?) {
L.d(vm)
// Get and display shared value from MainFragment
tv.text = vm.shareValue
btn_post.setOnClickListener {
Bus.get().post(SimpleEvent("MSG#00001"))
}
}
}
Android-VMLib 會讀取你的 Fragment 的註解並獲取 shareViewModel 字段的值,並決定使用 Activity 還是 Fragment 獲取 ViewModel,以此來實現 ViewModel 的共享。是不是更加簡潔了呢?
2.6 Android-VMLib 另一優勢,強大的工具類支持
我看過很多框架,它們通常會將一些常用的工具類與框架打包到一起提供給用戶使用。Android-VMLib 與之相反,我們將工具類作爲獨立項目進行支持。這樣的目的是,1). 希望工具類本身可以擺脫對框架的依賴,獨立應用到各個項目當中;2). 作爲單獨的模塊,單獨進行優化,使功能不斷完善。
截至目前,工具類庫 Android-Utils 已經提供了 22 個獨立的工具類,涉及從 IO、資源讀取、圖像處理、動畫到運行時權限獲取等各種功能,對於該庫我會在以後的文章裏進行說明。
需要說明的是,該庫在開發的過程中參考了很多其它的類庫,當然我們也開發了自己的特色工具類,比如運行時權限獲取、主題中屬性獲取、字符串拼接等等。
3、Jetpack MVVM 踩坑實錄以及 Android-VMLib 的解決方案
3.1 反覆通知,不該來的來了
這部分涉及到 ViewModel 的實現原理,如果沒有了解過其原理,可參考 《揭開 ViewModel 的生命週期控制的神祕面紗》 一文進行了解。
以我在該項目中的示例代碼爲例,MainFragment 和 SecondFragment 之間共享了 SharedViewModel,在 MainFragment 當中,我們往 LiveData 中塞了一個值。然後我們跳轉到 SecondFragment,從 SecondFragment 中回來的時候再次收到了這個值的通知。
很多時候我們只希望在調用 LiveData#setValue()
的時候通知一次數據變化。此時,我們可以通過 SingleLiveEvent 解決這個問題。這個類的原理並不難,只是通過 AtomicBoolean 來管理通知,當前僅當調用 setValue()
的時候進行通知。這解決了許多從後臺回來之後頁面的通知問題。
在 Andoird-VMLib 當中,當你通過 getObservable()
從“池”中獲取 LiveData 的時候,你可以通過帶 single
參數的方法來獲取這種類型的事件,
// 這裏通過指定 single 爲 true 來使用 SingleLiveEvent
vm.getObservable(String::class.java, FLAG_1, true).observe(this, Observer {
toast("#1.1: " + it!!.data)
L.d("#1.1: " + it.data)
})
在使用 SingleLiveEvent 中的問題,
-
從“池”中獲取 LiveData 的時候只會根據第一次獲取時的參數決定這個 LiveData 是不是 SingleLiveEvent 類型時。也就是說,當你第一次使用
vm.getObservable(String::class.java, FLAG_1, true)
獲取 LiveData 之後,再通過vm.getObservable(String::class.java, FLAG_1)
獲取到的是同一個 LiveData. -
SingleLiveEvent 本身存在一個問題:當存在多個觀察者的時候,它只能通知給其中的一個,並且你無法確定被通知的是哪一個。這和 SingleLiveEvent 的設計原理相關,因爲它通過原子的 Boolean 標記通知狀態,通知給一個觀察者之後狀態就被修改掉了。另外,註冊的觀察者會被放進 Map 裏,然後使用迭代器遍歷進行通知,因此無法確定通知的先後順序(哈希之後的坑位先後順序無法確定)。
3.2 LiveData 的本質和本職
LiveData 本質上等同於數據本身,本職是數據緩存。
參考 《揭開 LiveData 的通知機制的神祕面紗》 一文,其實現原理就是在 LiveData 內部定義了一個 Object 類型的、名爲 data 的對象,我們調用 setValue()
的時候就是爲這個對象賦值。LiveData 巧妙的地方在於利用了 LifecycleOwner 的生命週期回調,在生命週期發生變化的時候通知結果給觀察者。如果不熟悉 LiveData 的這些特性,編碼的時候就容易出現一些問題。
以文章爲例,其包含兩個主要部分:標題和內容,並且兩者都是 String 類型的。這就導致我們通過 getObservable(Class<T> dataType)
進行監聽的時候,無法判斷髮生變化的是文章的內容還是文章的標題。因此,除了 getObservable(Class<T> dataType)
,在 BaseViewModel 中,我們還加入了 getObservable(Class<T> dataType, int flag)
方法來獲取 LiveData。你可以這樣理解——指定不同的 Flag 的時候,我們會從不同的“池”中獲取 LiveData,因此,得到的是不同 LiveData 對象。
public <T> MutableLiveData<Resources<T>> getObservable(Class<T> dataType) {
return holder.getLiveData(dataType, false);
}
public <T> MutableLiveData<Resources<T>> getObservable(Class<T> dataType, int flag) {
return holder.getLiveData(dataType, flag, false);
}
有的同學可能會想到使用之前封裝的 Resource 對象的預留字段來指定發生變化的是文章的標題還是內容。我強烈建議你不要這麼做! 因爲,正如我們上面說的那樣,LiveData 和 Data 本身應該是一對一的。這樣處理的結果是文章的標題和內容被設置到了同一個對象上面,內存之中只維護了一份緩存。其後果是,當頁面出於後臺的時候,假如你先更新了文章的標題,後更新了文章的內容,那麼此時緩存之中只會保留文章的數據。當你的頁面從後臺回來的時候,標題就無法更新到 UI 上。還應該注意,假如一個數據分爲前半部分和後半部分,你不能在修改後半部分的時候覆蓋了前半部分修改的內容,這會導致前半部分修改結果無法更新到 UI。這不是 Android-VMLib 的錯,很多時候不理解 LiveData 的本質和本職就容易陷到這個坑裏去。
3.3 數據恢復問題,ViewModel 的版本差異
在 《揭開 ViewModel 的生命週期控制的神祕面紗》 一文中,我分析了無法從 ViewModel 中獲取緩存的結果的問題。這是因爲早期的 ViewModel 是通過空的 Fragment 實現生命週期控制的。所以,當頁面在後臺被殺死的時候,Fragment 被銷燬,從而導致再次獲取到的 ViewModel 與之前的 ViewModel 不是同一對象。在後來的版本中,ViewModel 的狀態恢復問題採用了另一種解決方案。下面是兩個版本類庫的差異(第一張是早期版本,第二張是近期的版本),
近期的版本拋棄了之前的 Fragment 的解決方案,改爲了通過 savedstate 保存 ViewModel 的數據。這裏再次提及這個問題是提醒你在開發的時候注意選擇的庫的版本以及早期版本中存在的問題以提前避坑。
總結
這篇文章中介紹了 Android-VMLib 以及使用 MVVM 的過程中遇到過的一些問題。如果在使用過程中你還遇到了其它的問題可以與筆者進行交流。
關於這個庫,其地址是 https://github.com/Shouheng88/Android-VMLib. 當初開發這個庫主要是爲了提升個人開發的效率,避免每次啓動新項目的時候都要 Copy 一份代碼。除了 MVVM,該項目中還加入了組件化、服務化架構的示例,感興趣的可以參考源碼。
從去年到現在,我主要在維護三個庫,除了上面的工具類庫 AndroidUtils 之外,還有一個 UI 庫 AndroidUIX,只不過目前還不夠成熟。除了爲我個人開發提升效率,我將其開放,也是爲了幫助更多個人開發者提升開發效率。畢竟現在嘛,996、裁員、35 歲……程序員已經被“十面埋伏”。我開發這些也是想要爲自己和爲其它開發者打通一條新的謀生之路。這是我的初衷,也是我的目的。如果你感興趣的話,也可以加入我們 😃
除了 Android 相關的技術文章,近期我還在整理 Java 後端以及服務器運維相關的文章,感興趣的可以直接關注我的公衆號「Code Brick」,另外感興趣的可以加入技術 QQ 交流羣:1018235573.
以上,感謝閱讀~