Complete app jetpack MVVM learning

Time:2022-5-3

Source address

Project address:zskingking/Jetpack-Mvvm

Complete app jetpack MVVM learning

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

  1. 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)
        }
    }
Complete app jetpack MVVM learning

Snipaste_2021-05-25_09-35-19.png

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
                })
            }
        })
  1. 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.