This article has been authorized to be reprinted by guolin_blog, an official wechat account.
This article is about MVC, MVP, MVVM and using MVVM to build GitHub client. Here is the GitHub address of the framework:
Dagger2 Version: Dagger2
Koin version: Koin
Before I get there, I want to talk about MVC, MVP, and MVVM concepts.
MVC
The concept of MVC (Model-view-Controller) originated from the idea of Erich Gamma, Richard Helm, Raplph Johnson and John Vlissides when they discussed the observer pattern in design pattern. Trygve Reenskaug published an article called Thing-Model-View-Editor in May 1979. Although Controller was not mentioned in this article, the Editor he mentioned was very close to this concept. Seven months later, He published a paper called Models-Views-Controllers that formalized the concept of MVC.
- Model (Data layer) : Handles the data logic.
- View (View layer) : Handles the display of views, which are described using XML in Android.
- Controller (control layer) : Activities and fragments in Android are responsible for handling this layer of business logic.
It is important to note that activities and fragments are not standard controllers, because they not only handle business logic, but also control the interface display. As a result, as the complexity of the business increases, activities and fragments become very bloated. Bad for code maintenance.
MVP
MVP (Model-view-Presenter) is a further evolution of MVC, proposed by Martin Fowler of Microsoft.
- Model (Data layer) : Handles the data logic.
- View (View layer) : Is responsible for handling the View display. In Android, the View is implemented using XML or Java/Kotlin code. Activities and Fragments take responsibility for this layer.
- Presenter: Connects the Model layer to the View layer. It is the intermediary between the two layers and handles the business logic.
In MVP, there is no interaction between the Model layer and the View layer. Instead, there is interaction between the View layer and the Presenter layer. The View layer interacts with the Presenter layer through the interface. The official code is as follows:
interface AddEditTaskContract {
interface View : BaseView<Presenter> {
var isActive: Boolean
fun showEmptyTaskError(a)
fun showTasksList(a)
fun setTitle(title: String)
fun setDescription(description: String)
}
interface Presenter : BasePresenter {
var isDataMissing: Boolean
fun saveTask(title: String, description: String)
fun populateTask(a)}}Copy the code
In MVP, the View layer is thin because it does not deploy any business logic. It is called Passive View, which means it does not have any initiative, and it is also easy to unit test, but it also has the following problems:
- Although the View layer code is reduced, as business complexity increases, the Presenter layer code becomes more and more bloated.
- The View layer and Presenter layer interact through interfaces, and the number of interfaces increases as business complexity increases.
- If the View layer is updated, such as UI input and data changes, it needs to actively call the Presenter layer code, which lacks automaticity and monitoring.
- MVP is a traditional MODEL driven by UI and events. Updating the UI should ensure that references to controls are held, and updating the UI should consider the Activity or Fragment life cycle to prevent memory leaks.
MVVM
MVVM (Model-view-ViewModel) is a further evolution of MVP, which was also proposed by Martin Fowler of Microsoft.
- Model (Data layer) : Handles the data logic.
- View (View layer) : Is responsible for handling the View display. In Android, the View is implemented using XML or Java/Kotlin code. Activities and Fragments take responsibility for this layer.
- ViewModel: responsible for connecting the Model layer and View layer, is the middle link between the two layers, responsible for processing business logic, View layer and ViewModel layer is bidirectional binding, the change of View layer will automatically reflect in the ViewModel layer, the change of ViewModel layer will also automatically reflect in the View layer.
After using MVVM, the responsibility of each layer is clearer, and it is also convenient to do unit testing. At the same time, because the View layer and the ViewModel layer are bidirectional binding, the developer does not need to actively deal with part of the logic, reducing a lot of glue code. If some data binding libraries are used, DataBinding in Android, for example, can reduce even more glue code.
practice
I developed a simple client using GitHub API, which was built by MVVM and written by Kotlin. The interface is shown as follows:
Login:
Home page:
Personal Center:
Architecture design
The whole is divided into six parts, each part divided according to business logic:
data
Data stores the code related to the data, as shown in the figure:
- Local: local data stores local storage logic (mmKV-related logic), for example, UserLocalDataSource.
- Model: data class, which stores request data and response data, for example: LoginRequestData (LoginRequestData), UserAccessTokenData (UserAccessTokenData), UserInfoData (user information data), ListData (basic ListData), and Repository (GitHub Repository) Request and response data classes).
- Remote: remote data storage logic network request (OkHttp3 Retrofit2 related logic), for example: UserRemoteDataSource (user remote data sources) and RepositoryRemoteDataSource (making warehouse remote data sources).
- Repository: repository, such as UserInfoRepository (user repository) and GitHubRepository (GitHub repository).
Repository holds references to the LocalDataSource and RemoteDataSource, exposing the relevant data to the outside world without any concern about how the data is processed inside Repository.
di
Di houses dependency injection-related code.
Dagger2 version:
As shown in the figure:
- ApplicationComponent: Application components, Will AndroidSupportInjectionModule, ApplicationModule, NetworkModule, RepositoryModule, MainModule, UserModule and GitHubRepositoryMo The dule is injected into the Application.
- ApplicationModule: Provides business modules that follow the Application lifecycle, such as LocalDataSource and RemoteDataSource.
- GitHubRepositoryModule: service module that provides GitHub repository services.
- MainModule: service module that provides main (startup page and home page) services.
- NetworkModule: Network modules, such as OkHttp3 and Retrofit2.
- RepositoryModule: Repository modules, for example, UserInfoRepository (repository of user information) and GitHubRepository (Repository of GitHub).
- UserModule: service module that provides user services.
- ViewModelFactory: A ViewModel factory that creates viewModels for different businesses.
Koin version:
As shown in the figure:
- ApplicationModule: RepositoryModule, MainModule, UserModule, and GitHubRepositoryModule And the List that generates ApplicationModules is available for Koin use.
ui
UI stores UI-related code, such as Activities, fragments, ViewModels, custom Views, etc., as shown in the figure below:
- Main: The Activity and ViewModel code associated with main (launch page and home page).
- Recyclerview: RecyclerView related code, including BaseViewHolder, BaseViewType, NoDataViewType, BaseDataBindingAdapter and MultiViewTypeDataBindingAdapter.
- Repository: GitHub repository associated Activity, Fragment, ViewModel, and Adapter code.
- User: User-specific Activity, Fragment, and ViewModel code.
- BaseActivity: The base class of Acitivity.
- BaseFragment: The base class of the Fragment.
- BaseViewModel: The base class of the ViewModel.
- NoViewModel: a class that inherits from BaseViewModel and can be used if the Acitivity or Fragment does not need the ViewModel.
The ViewModel holds a reference to the Repository and retrieves the desired data from the Repository; The ViewModel does not hold any reference to the View layer (for example, activities (including XML), fragments (including XML)), and uses a two-way binding framework (DataBinding) to retrieve data from the View layer to the ViewModel layer and manipulate it.
utils
Utils holds the utility files as shown in the following figure:
- ActivityExt: An extension function that holds an Activity.
- BindingAdapters: Store code that uses the **@BindingAdapters** annotation of DataBinding.
- BooleanExt: Extension functions to store Boolean. If you want to learn more, you can read my article: Kotlin series: Generic Type variations
- DateUtils: Stores date-specific code.
- FragmentExt: Extension function that stores fragments.
- GsonExt: Stores gson-related extension functions.
- Language: Stores the names and images associated with the GitHub repository.
- OnTabSelectedListenerBuilder: deposit OnTabSelectedListener related code, used to use DSL, if want to understand, can I look at this article: Kotlin series — a DSL
- Preferences: Store the MMKV-related code. If you want to take a closer look at the Kotlin series – Encapsulating MMKV and its related Kotlin features
- SingleLiveEvent: a life-cycle-aware observation object that only sends new functions after subscribing, which can be used for events such as navigation and SnackBar messages. It avoids a common problem if the observer is active during configuration changes (e.g. This class solves this problem by calling the observable only if you explicitly call the setValue() or call() methods.
- ToastExt: Extension function that holds Toast.
Prefix AndroidGenericFramework files
As shown in the figure:
- AndroidGenericFrameworkAppGlideModule: Define the set of dependencies and options to use when you initialize Glide within an Application. Note that only one AppGlideModule can exist within an Application. LibraryGlideModule must be used if it is a library.
- The Application of the AndroidGenericFrameworkApplication: this framework.
- AndroidGenericFrameworkConfiguration: storing configuration information of this framework.
- AndroidGenericFrameworkExtra: hold the Activity, and the name of the fragments of additional data.
- AndroidGenericFrameworkFragmentTag: storing fragments of the tag name, after the tag name is to use FragmentManager findFragmentByTag retrieval fragments (String) method.
Unit testing
As shown in the figure:
- Data: FakeDataSource used to create false data source, UserRemoteDataSourceTest (user remote source test class) and RepositoryRemoteDataSourceTest (making warehouse remote source test class) are simulated API calls.
- Utils: test class for storing utility files.
- Viewmodel: The test class that holds the ViewModel.
Let me take a look at the Android architecture components and libraries used.
OkHttp3 and Retrofit2
The network request library uses Retrofit2, which is based on OkHttp3 encapsulation. The framework code is as follows:
// NetworkModule.kt
/** * Created by TanJiaJun on 2020/4/4. */
@Suppress("unused")
@Module
open class NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(localDataSource: UserLocalDataSource): OkHttpClient =
OkHttpClient.Builder()
.connectTimeout(AndroidGenericFrameworkConfiguration.CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
.readTimeout(AndroidGenericFrameworkConfiguration.READ_TIMEOUT, TimeUnit.MILLISECONDS)
.addInterceptor(BasicAuthInterceptor(localDataSource))
.build()
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit =
Retrofit.Builder()
.client(client)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(String.format("%1\$s://%2\$s/"."https", AndroidGenericFrameworkConfiguration.HOST))
.build()
}
Copy the code
Retrofit2.6 supports Kotlin’s coroutines, with the following differences:
- You can use suspend fun directly.
- Instead of returning the **Deferred** object, we can return the desired data object.
- We no longer need to call the await function in the coroutine because Retrofit already calls it for us.
The framework code is as follows:
// RepositoryRemoteDataSource.kt
interface Service {
@GET("search/repositories")
suspend fun fetchRepositories(@Query("q") query: String.@Query("sort") sort: String = "stars"): ListData<RepositoryResponseData>
}
Copy the code
Glide v4
Image loading library using Glide V4, I use the DataBinding component ** @bindingAdapter ** annotation, framework part of the code is as follows:
// BindingAdapters.kt
@BindingAdapter(value = ["url"."placeholder"."error"], requireAll = false)
fun ImageView.loadImage(url: String? , placeholder:Drawable? , error:Drawable?).= Glide .with(context) .load(url) .placeholder(placeholder ? : context.getDrawable(R.mipmap.ic_launcher)) .error(error ? : context.getDrawable(R.mipmap.ic_launcher)) .transition(DrawableTransitionOptions.withCrossFade(DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build()))
.into(this)
Copy the code
Android Jetpack
Android Jetpack is a set of libraries, tools, and guidelines that make it easier for developers to write great apps. These components help developers follow best practices, get developers out of boilerplate code, and simplify complex tasks so they can focus on the code they need. I used DataBinding, Lifecycle, LiveData, ViewModel, and I’ll go through it briefly.
DataBinding
DataBinding is the core architectural component that implements MVVM and has the following advantages:
- You can decouple layout and logic to make your code logic clearer.
- You can eliminate code like findViewById and significantly reduce the code in the View layer.
- Data can be unidirectional and bidirectional bound to layout files.
- Can automatically null judgment, can avoid null pointer exception.
The framework code is as follows:
<! -- activity_personal_center.xml -->
<ImageView
android:id="@+id/iv_head_portrait"
error="@{@drawable/ic_default_avatar}"
placeholder="@{@drawable/ic_default_avatar}"
url="@{viewModel.avatarUrl}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:contentDescription="@string/head_portrait"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider_line"
tools:background="@drawable/ic_default_avatar" />
Copy the code
Lifecycle
Lifecycle components can perform operations in response to changes in the life-cycle state of activities and fragments.
Lifecycle components are used by Both LiveData and ViewModel, and the framework code is as follows:
// LoginFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?). =
with(binding) {
lifecycleOwner = this@LoginFragment
viewModel = this@LoginFragment.viewModel
handlers = this@LoginFragment
}.also {
registerLoadingProgressBarEvent()
registerSnackbarEvent()
observe()
}
Copy the code
Let’s look at the setLifecycleOwner method of ViewDataBinding, which looks like this:
// ViewDataBinding.java
@MainThread
public void setLifecycleOwner(@Nullable LifecycleOwner lifecycleOwner) {
if (mLifecycleOwner == lifecycleOwner) {
return;
}
if(mLifecycleOwner ! =null) {
mLifecycleOwner.getLifecycle().removeObserver(mOnStartListener);
}
mLifecycleOwner = lifecycleOwner;
if(lifecycleOwner ! =null) {
if (mOnStartListener == null) {
mOnStartListener = new OnStartListener(this);
}
lifecycleOwner.getLifecycle().addObserver(mOnStartListener);
}
for(WeakListener<? > weakListener : mLocalFieldObservers) {if(weakListener ! =null) { weakListener.setLifecycleOwner(lifecycleOwner); }}}Copy the code
LifecyclerOwner here is a class with the Android lifecycle, and custom components can use its events to handle lifecycle changes without implementing any code in the Activity or Fragment.
LiveData
LiveData is an observable data store class with lifecycle awareness that follows application components (e.g. The lifecycle of activities, fragments, and Services (you can use LifecycleService, which implements the LifecycleOwner interface). This awareness ensures that LiveData updates only application component observers that are in an active lifecycle state.
I wrote an article about LiveData earlier, which you can read:
Android Jetpack series – LiveData source analysis
The framework code is as follows:
// LoginViewModel.kt
val username = MutableLiveData<String>()
val password = MutableLiveData<String>()
private val _isLoginEnable = MutableLiveData<Boolean> ()val isLoginEnable: LiveData<Boolean> = _isLoginEnable
val isLoginSuccess = MutableLiveData<Boolean> ()fun checkLoginEnable(a){ _isLoginEnable.value = ! username.value.isNullOrEmpty() && ! password.value.isNullOrEmpty() }Copy the code
ViewModel
A ViewModel is a class that prepares and manages activities or fragments, and handles their communication with the rest of the application (for example, calling business logic classes).
A ViewModel is always created in an Activity or Fragment, and is retained as long as the corresponding Activity or Fragment is active (for example, if it is an Activity, until it finished).
In other words, this means that a ViewModel will not be destroyed due to configuration changes (e.g., rotation), and all new instances will be reconnected to the existing ViewModel.
The purpose of a ViewModel is to retrieve and store information that an Activity or Fragment needs. The Activity or Fragment should be able to observe changes in the ViewModel. This information is usually exposed via LiveData or an Android Data Binding.
I wrote a previous article about viewModels, which you can read:
Android Jetpack series – ViewModel source code analysis
The framework code is as follows:
// RepositoryViewModel.kt
/** * Created by TanJiaJun on 2020-02-07. */
class RepositoryViewModel @Inject constructor(
private val repository: GitHubRepository
) : BaseViewModel() {
private val _isShowRepositoryView = MutableLiveData<Boolean> ()val isShowRepositoryView: LiveData<Boolean> = _isShowRepositoryView
private val _repositories = MutableLiveData<List<RepositoryData>>()
val repositories: LiveData<List<RepositoryData>> = _repositories
fun getRepositories(languageName: String) =
launch(
uiState = UIState(isShowLoadingView = true, isShowErrorView = true),
block = { repository.getRepositories(languageName) },
success = {
if (it.isNotEmpty()) {
_repositories.value = it
_isShowRepositoryView.value = true}})}Copy the code
coroutines
Derived from Simula and Modula-2, coroutines are a programming idea that is not limited to a particular language, having been coined by Melvin Edward Conway in 1958 to build assembler programs. Using it in Android simplifies code for asynchronous execution, and it was added to Kotlin in version 1.3.
On the Android platform, coroutines help solve two major problems:
- Manage long-running tasks that, if not managed properly, can block the main thread and cause your application interface to freeze.
- Provides mainthread security, or securely invoking network or disk operations from the main thread.
Manage long-running tasks
On the Android platform, each application has a main thread that handles the interface and manages user interaction. If your application has too much work assigned to the main thread, the interface will render slowly or freeze, and respond slowly to touch events such as network requests, JSON parsing, writing or reading to a database, and traversing large lists, all of which should be done on the worker thread.
Coroutines add two operations to regular functions to handle long-running tasks. To invoke or call and return, coroutines add suspend and resume:
- Suspend is used to suspend execution of the current coroutine and save all local variables.
- Resume is used to allow a suspended coroutine to resume execution from where it was suspended.
The suspend function can only be called from another suspend function, or by using a coroutine builder (for example, launch) to start a new coroutine.
Kotin uses stack frames to manage which functions to run and all local variables. The current stack frame is copied and saved for later use when the coroutine is paused; When the coroutine is restored, the stack frame is copied back from where it was saved, and the function starts running again.
Use coroutines to secure the main thread
The Kotlin coroutine uses the scheduler to determine which threads are used to execute the coroutine. All coroutines must run in the scheduler. The coroutine can be paused by itself, and the scheduler is responsible for restoring it.
Kotlin provides three schedulers that you can use to specify where coroutines should be run:
- Dispatchers.Main: This scheduler can be used to run coroutines on the Main Android thread. It can only be used for interface interactions and performing quick tasks, such as calling the suspend function, running Android interface framework operations, and updating LiveData objects.
- Dispatcher.Default: This scheduler is suitable for performing cpu-intensive tasks outside of the main thread, such as sorting lists and parsing JSON.
- Dispatchers.IO: This scheduler is suitable for performing disk or network I/O outside of the main thread, such as operating databases (using Room), writing to or reading from files, and performing any network operations.
Specify CoroutineScope
When defining a coroutine, you must specify its CoroutineScope. The CoroutineScope can manage one or more related coroutines and can be used to start new coroutines within a specified range.
Unlike the scheduler, the CoroutineScope does not run coroutines.
An important feature of CoroutineScope is to stop the execution of the coroutine when the user leaves the content area of the application, ensuring that all ongoing operations are stopped correctly.
On The Android platform, CoroutineScope implementations can be associated with a component’s Lifecycle, such as Lifecycle and ViewModel, to avoid memory leaks and no additional work on user-specific activities or fragments.
Start the coroutines
Coroutines can be started in two ways:
- Launch: A new coroutine can be launched, but the result is not returned to the caller.
- Async: You can start new coroutines and allow the await function to be suspended to return results.
I also used Kotlin’s Flows, which are inspired by Reactive Streams, so developers who are familiar with RxJava should be familiar with it very quickly.
I’ve written several articles about RxJava, which you can read:
RxJava2 source analysis – subscribe
RxJava2 source code analysis – thread switching
RxJava2 source analysis – Map operator
RxJava2 source code analysis – FlatMap and ConcatMap and their associated concurrent programming analysis
The framework code is as follows:
// LoginViewModel.kt
@ExperimentalCoroutinesApi
@FlowPreview
fun login(a) =
launchUI {
launchFlow {
repository.run {
cacheUsername(username.value ?: "") cachePassword(password.value ? :"")
authorizations()
}
}
.flatMapMerge {
launchFlow { repository.getUserInfo() }
}
.flowOn(Dispatchers.IO)
.onStart { uiLiveEvent.showLoadingProgressBarEvent.call() }
.catch {
val responseThrowable = ExceptionHandler.handleException(it)
uiLiveEvent.showSnackbarEvent.value = "${responseThrowable.errorCode}:${responseThrowable.errorMessage}"
}
.onCompletion { uiLiveEvent.dismissLoadingProgressBarEvent.call() }
.collect {
repository.run {
cacheUserId(it.id)
cacheName(it.login)
cacheAvatarUrl(it.avatarUrl)
}
isLoginSuccess.value = true}}Copy the code
Dagger2
Dagger2 is an all-static, compile-time dependency injection framework for Java and Android.
The Dagger library gets its name not just from its original name, Dagger, but Jake Wharton pointed out in the introduction that Dagger means dag-er, DAG stands for Directed Acyclic Graph, That is to say, Dagger is a dependency injection library based on directed acyclic graph structure, so no loop dependency can occur during use of Dagger.
Square is inspired by Guice to develop Dagger, which is a semi-static, semi-runtime dependency injection framework. Although dependency injection is completely static, generating directed acyclic graph is implemented based on reflection, which is not optimal for large server applications or Android applications. Then the Engineers at Google fork the project, take inspiration from the AutoValue project and modify it, resulting in the Dagger2. Dagger2 and Dagger have the following differences:
- Better performance: Google claims a 13% improvement in processing performance by not using reflection to generate directed acyclic graphs at compile time.
- More efficient and elegant, and easier to debug: Dagger as an updated version goes from semi-static to fully static, Map API to declarative API (e.g. @Module), resulting code is more efficient and elegant, errors can be found at compile time.
Since Dagger2 does not use reflection and lacks dynamic mechanics, it loses some flexibility, but overall the benefits far outweigh the disadvantages.
I use Dagger2 and the associated Dagger-Android for the master branch, and the framework is as follows:
// ApplicationComponent.kt
/** * Created by TanJiaJun on 2020/3/4. */
@Singleton
@Component( modules = [ AndroidSupportInjectionModule::class, ApplicationModule::class, NetworkModule::class, RepositoryModule::class, MainModule::class, UserModule::class, GitHubRepositoryModule::class ] )
interface ApplicationComponent : AndroidInjector<AndroidGenericFrameworkApplication> {
@Component.Factory
interface Factory {
fun create(@BindsInstance applicationContext: Context): ApplicationComponent
}
}
Copy the code
Koin
Koin is a lightweight dependency injection framework for Kotlin developers.
It is officially written in pure Kotlin, using only function parsing, no proxy, no code generation, and no reflection.
I used koIN in the mVVM-koin branch. The framework part is as follows:
// ApplicationModule.kt
/** * Created by TanJiaJun on 2020/5/5. */
valapplicationModule = module { single { UserLocalDataSource(MMKV.mmkvWithID( AndroidGenericFrameworkConfiguration.MMKV_ID, MMKV.SINGLE_PROCESS_MODE, AndroidGenericFrameworkConfiguration.MMKV_CRYPT_KEY )) } single { UserRemoteDataSource(get()) }
single { RepositoryRemoteDataSource(get()}}val networkModule = module {
single<OkHttpClient> {
OkHttpClient.Builder()
.connectTimeout(AndroidGenericFrameworkConfiguration.CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
.readTimeout(AndroidGenericFrameworkConfiguration.READ_TIMEOUT, TimeUnit.MILLISECONDS)
.addInterceptor(BasicAuthInterceptor(get()))
.build()
}
single<Retrofit> {
Retrofit.Builder()
.client(get())
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(String.format("%1\$s://%2\$s/", SCHEMA_HTTPS, AndroidGenericFrameworkConfiguration.HOST))
.build()
}
}
val repositoryModule = module {
single { UserInfoRepository(get(), get()) }
single { GitHubRepository(get()}}val mainModule = module {
scope<SplashActivity> {
viewModel { SplashViewModel(get()) }
}
scope<MainActivity> {
viewModel { MainViewModel(get()}}}val userModule = module {
scope<LoginFragment> {
viewModel { LoginViewModel(get()) }
}
scope<PersonalCenterActivity> {
viewModel { PersonalCenterViewModel(get()}}}val githubRepositoryModule = module {
scope<RepositoryFragment> {
viewModel { RepositoryViewModel(get()}}}val applicationModules = listOf(
applicationModule,
networkModule,
repositoryModule,
mainModule,
userModule,
githubRepositoryModule
)
private const val SCHEMA_HTTPS = "https"
Copy the code
MMKV
MMKV is based on mMAP memory mapping key-value component, the underlying serialization/deserialization using Protobuf implementation, high performance, strong stability, and Android side also supports multi-process.
I wrote an article about MMKV earlier, which you can read:
Kotlin series – Encapsulates MMKV and its associated Kotlin features
I use MMKV instead of SharedPreferences in Android components, as a local storage data component, part of the framework code is as follows:
// Preferences.kt
/** * Created by TanJiaJun on 2020-01-11. */
private inline fun <T> MMKV.delegate(
key: String? = null,
defaultValue: T.crossinline getter: MMKV. (String, T) -> T,
crossinline setter: MMKV.(String, T) -> Boolean
): ReadWriteProperty<Any, T> =
object : ReadWriteProperty<Any, T> {
override fun getValue(thisRef: Any, property: KProperty< * >): T = getter(key ? : property.name, defaultValue)override fun setValue(thisRef: Any, property: KProperty<*>, value: T){ setter(key ? : property.name, value) } }fun MMKV.boolean(
key: String? = null,
defaultValue: Boolean = false
): ReadWriteProperty<Any, Boolean> =
delegate(key, defaultValue, MMKV::decodeBool, MMKV::encode)
fun MMKV.int(key: String? = null, defaultValue: Int = 0): ReadWriteProperty<Any, Int> =
delegate(key, defaultValue, MMKV::decodeInt, MMKV::encode)
fun MMKV.long(key: String? = null, defaultValue: Long = 0L): ReadWriteProperty<Any, Long> =
delegate(key, defaultValue, MMKV::decodeLong, MMKV::encode)
fun MMKV.float(key: String? = null, defaultValue: Float = 0.0F): ReadWriteProperty<Any, Float> =
delegate(key, defaultValue, MMKV::decodeFloat, MMKV::encode)
fun MMKV.double(key: String? = null, defaultValue: Double = 0.0): ReadWriteProperty<Any, Double> =
delegate(key, defaultValue, MMKV::decodeDouble, MMKV::encode)
private inline fun <T> MMKV.nullableDefaultValueDelegate(
key: String? = null,
defaultValue: T? .crossinline getter: MMKV. (String, T?) -> T,
crossinline setter: MMKV.(String, T) -> Boolean
): ReadWriteProperty<Any, T> =
object : ReadWriteProperty<Any, T> {
override fun getValue(thisRef: Any, property: KProperty< * >): T = getter(key ? : property.name, defaultValue)override fun setValue(thisRef: Any, property: KProperty<*>, value: T){ setter(key ? : property.name, value) } }fun MMKV.byteArray(
key: String? = null,
defaultValue: ByteArray? = null
): ReadWriteProperty<Any, ByteArray> =
nullableDefaultValueDelegate(key, defaultValue, MMKV::decodeBytes, MMKV::encode)
fun MMKV.string(key: String? = null, defaultValue: String? = null): ReadWriteProperty<Any, String> =
nullableDefaultValueDelegate(key, defaultValue, MMKV::decodeString, MMKV::encode)
fun MMKV.stringSet(
key: String? = null,
defaultValue: Set<String>? = null
): ReadWriteProperty<Any, Set<String>> =
nullableDefaultValueDelegate(key, defaultValue, MMKV::decodeStringSet, MMKV::encode)
inline fun <reified T : Parcelable> MMKV.parcelable(
key: String? = null,
defaultValue: T? = null
): ReadWriteProperty<Any, T> =
object : ReadWriteProperty<Any, T> {
override fun getValue(thisRef: Any, property: KProperty< * >): T = decodeParcelable(key ? : property.name, T::class.java.defaultValue)
override fun setValue(thisRef: Any, property: KProperty<*>, value: T){ encode(key ? : property.name, value) } }Copy the code
It can be used as follows:
// UserLocalDataSource.kt
var accessToken by mmkv.string("user_access_token"."")
var userId by mmkv.int("user_id".- 1)
var username by mmkv.string("username"."")
var password by mmkv.string("password"."")
var name by mmkv.string("name"."")
var avatarUrl by mmkv.string("avatar_url"."")
Copy the code
ViewPager2
The GitHub repository uses ViewPager2, which has several advantages over ViewPager:
-
Support vertical paging: ViewPager2 supports vertical paging as well as horizontal paging. You can use the Android :orientation property or setOrientation() method to enable vertical paging.
android:orientation="vertical" Copy the code
-
RTL support: ViewPager2 automatically starts RTL paging depending on the locale. RTL paging can be started by setting the Android :layoutDirection attribute or setLayoutDirection() method as follows:
android:layoutDirection="rtl" Copy the code
The framework code is as follows:
<! -- activity_main.xml -->
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/vp_repository"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tl_repository" />
Copy the code
MockK
MockK is a testing framework built specifically for the Kotlin language. In Java, we usually use Mockito, but if we use Kotlin, we will encounter some problems. The common problems are as follows:
-
Static methods cannot be tested: You can solve this using PowerMock.
-
Mockito cannot mock/spy because:-final class: This is because in Kotlin any class is final by default and Mockito cannot mock a final class by default.
-
Java. Lang. An illegalStateException: anyObjecet () must not be null: We get this problem if we use eq(), any(), capture(), and argumentCaptor(), because these methods may return null objects and will raise the exception if applied to a non-empty argument. The solution is to use the following file:
-
Use when with backquotes: because when is the keyword in Kotlin.
Using Kotlin and Mockito at the same time would be inconvenient as mentioned above, so I finally decided to use MockK. The test-related libraries I used are as follows:
// build.gradle(:app)
testImplementation "junit:junit:$junitVersion"
testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion"
testImplementation "io.mockk:mockk:$mockkVersion"
testImplementation "com.google.truth:truth:$truthVersion"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion"
testImplementation "android.arch.core:core-testing:$coreTestingVersion"
Copy the code
-
Com. Squareup. Okhttp3: mockwebserver: used to simulate the Web server.
-
Com.google.truth: Truth: makes test assertions and failure messages more readable. Similar to AssertJ, it supports many JDK and Guava types and can be extended to other types.
I’m unit testing the data source, ViewModel, and tool files.
The framework code is as follows:
// LoginViewModelTest.kt
@ExperimentalCoroutinesApi
@FlowPreview
@Test
fun login_success(a) {
runBlocking {
viewModel.username.value = "[email protected]"
viewModel.password.value = "password"
coEvery { repository.authorizations() } returns userAccessTokenData
coEvery { repository.getUserInfo() } returns userInfoData
viewModel.login()
val observer = mockk<Observer<Boolean>>(relaxed = true) viewModel.isLoginSuccess.observeForever(observer) viewModel.viewModelScope.coroutineContext[Job]? .children? .forEach { it.join() } verify { observer.onChanged(match { it }) } } }@ExperimentalCoroutinesApi
@FlowPreview
@Test
fun login_failure(a) {
runBlocking {
viewModel.username.value = "[email protected]"
viewModel.password.value = "password"
coEvery { repository.authorizations() } returns userAccessTokenData
coEvery { repository.getUserInfo() } throws Throwable("UnknownError")
viewModel.login()
val observer = mockk<Observer<String>>(relaxed = true) viewModel.uiLiveEvent.showSnackbarEvent.observeForever(observer) viewModel.viewModelScope.coroutineContext[Job]? .children? .forEach { it.join() } verify { observer.onChanged(match { it =="0:UnknownError"})}}}Copy the code
My GitHub: TanJiaJunBeyond
Common Android Framework: Common Android framework
My nuggets: Tan Jiajun
My simple book: Tan Jiajun
My CSDN: Tan Jiajun