Source address
Project address:zskingking/Jetpack-Mvvm

Introduction: Android community app and music player realized by jetpack family bucket + kotlin. Don’t write obscure code, mark each line of comments as clearly as possible, strictly abide by the six basic principles, and make extensive use of design patterns. This project can quickly help you start kotlin and jetpack. If you think it’s helpful to you, point a star in the upper right corner and thank you in advance
content
- Theme switching, black theme and white theme, mainly two..
/**
*Dynamically switch topics
*/
private fun changeTheme() {
val theme = PrefUtils.getBoolean(Constants.SP_THEME_KEY, false)
if (theme) {
setTheme(R.style.AppTheme_Night)
} else {
setTheme(R.style.AppTheme)
}
}
<!-- Daytime theme -- >
<style name="AppTheme" parent="Theme.Design.Light.NoActionBar">
<item name="colorPrimary">#666666</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">#EF3939</item>
<item name="main_bg_1">#ffffff</item>
<item name="main_bg_2">#EFEFEF</item>
<item name="main_bg_3">#CACACA</item>
<item name="theme_color_1">#333333</item>
<item name="theme_color_2">#666666</item>
<item name="theme_color_3">#999999</item>
<item name="ripple_gray">#D1D1D1</item>
<item name="float_bg">#F2FFFFFF</item>
<item name="division_line">#EDEDED</item>
<item name="android:navigationBarColor">#ffffff</item>
</style>
<!-- Night theme -- >
<style name="AppTheme_Night" parent="Theme.Design.Light.NoActionBar">
<item name="colorPrimary">#999999</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">#EF3939</item>
<item name="main_bg_1">#2C2C2C</item>
<item name="main_bg_2">#FA3C3C3C</item>
<item name="main_bg_3">#5E5E5E</item>
<item name="theme_color_1">#BDBDBD</item>
<item name="theme_color_2">#939393</item>
<item name="theme_color_3">#737272</item>
<item name="ripple_gray">#808080</item>
<item name="float_bg">#D64C4C4C</item>
<item name="division_line">#424242</item>
<item name="android:navigationBarColor">#2C2C2C</item>
</style>
The main change is to save the local state.
This is on the interface
override fun onCreate(savedInstanceState: Bundle?) {
changeTheme()
super.onCreate(savedInstanceState)
}
Oncreate is called directly before super.
I thought it was
Destination addresshttps://www.jianshu.com/p/af7c0585dd5b
A new project is created in Android studio, which includes these two styles in a separate style file. The author wrote it together.
Switching code
/**
*But switch to night / day mode
*/
private fun setNightMode() {
val theme = PrefUtils.getBoolean(Constants.SP_THEME_KEY,false)
scDayNight.isChecked = theme
//Cannot use switch to listen, otherwise it will recurse
scDayNight.clickNoRepeat {
it.isSelected = !theme
PrefUtils.setBoolean(Constants.SP_THEME_KEY, it.isSelected)
mActivity.recreate()
}
}
Because there are only two activities as a whole, one is the welcome interface and the other is the main interface. Just reload the activity directly.
2. Permission application
Why mention this? Because good applications remind users first, rather than directly applying for permission.
/**
*Apply for permission
*/
private fun requestPermission(){
//Applied
if (EasyPermissions.hasPermissions(this, *perms)) {
startIntent()
}else{
//For the application, the application prompt is displayed
DialogUtils.tips(this,tips){
RequestLocationAndCallPermission()
}
}
}
In this way, first pop up the dialog, and then pop up the requested permission. Then enter.
3. Countdown
①
CoroutineScope(job).launch {
delay(splashDuration)
MainActivity.start([email protected])
finish()
}
②
ThreadUtils.runOnUiThreadDelayed({
val token = mmkv. Decodestring ("token", "") // check whether the inventory exists and log in
if (TextUtils.isEmpty(token)) {
ViewUtils.goLoginActivity()
} else {
goGreenActivity(Bundle(), "/main/activity")
}
finish()
}, 3)
③
disposable = Observable.timer(2000,TimeUnit.MILLISECONDS)
.subscribe {
startActivity(Intent(this,MainActivity::class.java))
finish()
}
4. Use of navigation
//navigation
implementation 'android.arch.navigation:navigation-fragment:1.0.0'
implementation 'android.arch.navigation:navigation-ui:1.0.0'
Process creation
https://www.cnblogs.com/guanxinjing/p/11555217.html
Page Jump
— Navigation.findNavController(getView()).navigate(R.id.action_one_to_two);
Just exit the fragment in the main activity
override fun onBackPressed() {
//Get hostfragment
val mMainNavFragment: Fragment? =
supportFragmentManager.findFragmentById(R.id.host_fragment)
//Get the current fragment
val fragment =
mMainNavFragment?.childFragmentManager?.primaryNavigationFragment
//If you are currently in the root fragment, that is, hostfragment
if (fragment is MainFragment) {
//Activity exits without destroying
moveTaskToBack(false)
} else {
super.onBackPressed()
}
}
5. MVVM learning
Layout can inherit data and ViewModel
First, update the data in the adapter
holder.itemView.clickNoRepeat {
onItemClickListener?.invoke(position,it)
}
//Collect
holder.itemView.findViewById<View>(R.id.ivCollect)?.clickNoRepeat {
onItemChildClickListener?.invoke(position,it)
}
val binding = if (holder is ArticleViewHolder){
//Get viewdatabinding
DataBindingUtil.getBinding<ItemHomeArticleBinding>(holder.itemView)?.apply {
dataBean = getItem(position)
}
}else{
DataBindingUtil.getBinding<ItemProjectBinding>(holder.itemView)?.apply {
dataBean = getItem(position)
}
}
binding?.executePendingBindings()
binding?. Update instructions for executebindings()
For example, collection processing
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:id="@+id/ivCollect"
android:padding="5dp"
android:layout_alignTop="@+id/tvChapterName"
android:layout_alignParentRight="true"
android:layout_marginRight="@dimen/padding"
articleCollect="@{dataBean.collect}" />
/**
*Load the picture and do Gaussian blur
*/
@BindingAdapter(value = ["articleCollect"])
@JvmStatic
fun imgPlayBlur(view: ImageView, collect: Boolean) {
if (collect) {
view.setImageResource(R.mipmap._collect)
} else {
view.setImageResource(R.mipmap.un_collect)
}
}

Click to find it directly.
I wrote a custom view, which can also be done..
class MaterialView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
var binding: ItemMaterialBinding? = null
init {
binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.item_material, this, false)
addView(binding?.root)
}
fun setData(data: String) {
binding?.data = data
binding?.executePendingBindings()
}
}
Content in layout
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="data"
type="String" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{data}" />
<Button
android:id="@+id/btApplay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
Android: text = "application" / >
</LinearLayout>
</layout>
It can be used in custom view and adapter….
6. Use of recyclerview.
<com.scwang.smartrefresh.layout.SmartRefreshLayout
android:id="@+id/smartRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="54dp">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<cn.bingoogolapple.bgabanner.BGABanner
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="200dp"
app:banner_indicatorGravity="left"
app:banner_pageChangeDuration="1000"
app:banner_pointAutoPlayAble="true"
app:banner_pointAutoPlayInterval="3000"
app:banner_pointContainerBackground="@color/transparent"
app:banner_pointContainerLeftRightPadding="@dimen/padding"
app:banner_pointDrawable="@drawable/bga_banner_selector_point_hollow"
app:banner_pointTopBottomMargin="40dp"
app:layout_scrollFlags="scroll"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvHomeList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</com.scwang.smartrefresh.layout.SmartRefreshLayout>
There is a bean processing on recyclerview. I thought I would add header processing in the adapter
I thought that Scrollview and recyclerview could be used at the same time, just set the height of recyclerview. This is the easiest way to deal with it.
android:overScrollMode="never"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
It’s done.
6. Tab switch and use in combination with viewpager2
https://www.jianshu.com/p/f3022211821c
const val banner = "cn.bingoogolapple:bga-banner:2.2.7"
const val magic = "com.github.hackware1993:MagicIndicator:1.6.0"
Using a third party is mainly to combine data and styles.
But using the apply keyword is great
mutableListOf<String>().apply {
Add ("system")
Add ("navigation")
initViewPager(this)
}
It can also transmit data through fragment
arrayListOf<Fragment>().apply {
tabList.forEach {
add(ArticleListFragment().apply {
//Send information to each fragment
val bundle = Bundle()
bundle.putInt("type", type)
bundle.putInt("tabId", it.id)
bundle.putString("name", it.name)
arguments = bundle
})
}
})
- Exception handling encapsulation
Handled in baseviewmodel
/**
*Wrong method
*/
typealias VmError = (e: ApiException) -> Unit
/**
*Des basic VM
* @date 2020/5/13
* @author zs
*/
open class BaseViewModel:ViewModel() {
/**
*Error message livedata
*/
val errorLiveData = MutableLiveData<ApiException>()
/**
*No more data
*/
val footLiveDate = MutableLiveData<Any>()
/**
*No data
*/
val emptyLiveDate = MutableLiveData<Any>()
/**
*Processing error
*/
fun handleError(e: Throwable){
val error = getApiException(e)
toast(error.errorMessage)
errorLiveData.postValue(error)
}
protected fun <T> launch(
block: () -> T
, error:VmError? = null) {
viewModelScope.launch {
runCatching {
block()
}.onFailure {
it.printStackTrace()
getApiException(it).apply {
withContext(Dispatchers.Main){
error?.invoke([email protected])
toast(errorMessage)
}
}
}
}
}
protected fun <T> launch(block: suspend () -> T) {
viewModelScope.launch {
runCatching {
block()
}.onFailure {
if (BuildConfig.DEBUG) {
it.printStackTrace()
[email protected]
}
getApiException(it).apply {
withContext(Dispatchers.Main){
toast(errorMessage)
//Unified response error message
errorLiveData.value = [email protected]
}
}
}
}
}
/**
*Capture exception information
*/
private fun getApiException(e: Throwable): ApiException {
return when (e) {
is UnknownHostException -> {
Apiexception ("network exception", - 100)
}
is JSONException -> {//|| e is JsonParseException
Apiexception ("data exception", - 100)
}
is SocketTimeoutException -> {
Apiexception ("connection timeout", - 100)
}
is ConnectException -> {
Apiexception ("connection error", - 100)
}
is HttpException -> {
ApiException("http code ${e.code()}", -100)
}
is ApiException -> {
e
}
/**
*If the collaboration process is still running, when individual models exit the current interface, ViewModel will throw cancelationexception,
*Forcibly terminate the collaboration process, which is similar to the interruptexception in Java, so you don't have to ignore it. Just hide the toast
*/
is CancellationException -> {
ApiException("", -10)
}
else -> {
Apiexception ("unknown error", - 100)
}
}
}
/**
*Is there more data in the processing list
*/
protected fun<T> handleList(listLiveData: LiveData<MutableList<T>>,pageSize:Int = 20){
val listSize = listLiveData.value?.size?:0
if (listSize % pageSize != 0){
footLiveDate.value = 1
}
}
}
Add onfailure after Ctrip
viewModelScope.launch {
runCatching {
block()
}.onFailure {
}We can solve all the problems and deal with them in a unified way.
8. Collect and cancel collection… Ability to process local data
/**
*Collect
*/
suspend fun collect(id: Int) {
repo.collect(id)
val list = listLiveData.value
list?.map {
if (id == it.id) {
it.copy(collect = true)
} else {
it
}
}?.toMutableList().let {
listLiveData.value = it
}
}
/**
*Cancel collection
*/
suspend fun unCollect(id: Int) {
repo.unCollect(id)
val list = listLiveData.value
list?.map {
if (id == it.id) {
it.copy(collect = false)
} else {
it
}
}?.toMutableList().let {
listLiveData.value = it
}
}
Because the type mutablelivedata is revised, the page will also change.
9. The place where kotlin is easy to report errors envelopepic Isnullorempty() to determine null
Method of transferring mutablelist
list.map {
ArticleListBean().apply {
id = it.originId
author = it.author
collect = true
desc = it.desc
picUrl = it.envelopePic
link = it.link
date = it.niceDate
title = Html.fromHtml(it.title).toString()
articleTag = it.chapterName
topTitle = ""
}
}.toMutableList()
10. Modification of navhostfragment. The main purpose is to modify the fragment and switch directly, so that the life cycle can be changed.
The default life cycle is directly killed, and then recreated, taking the whole life cycle.
After modification, only the onhiddenchanged method has changed.
androidx. navigation. fragment. The content of navhostfragment package has been modified..
Remove implementation 'Android X navigation:navigation-fragment-ktx:2.3.0'
Need to add
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="nav_host_fragment_container" type="id"/>
<declare-styleable name="DialogFragmentNavigator">
<attr name="android:name"/>
</declare-styleable>
<declare-styleable name="FragmentNavigator">
<attr name="android:name"/>
</declare-styleable>
<declare-styleable name="NavHostFragment">
<attr format="boolean" name="defaultNavHost"/>
</declare-styleable>
</resources>
These data values XML data
Just use the inside of the project.