Following up on the previous article: write a modern Android project -Part1 – from scratch using Kotlin

5. MVVM architecture +Repository schema +Android Manager

5.1 About Architecture in Android

Architecture has long been a rarity in Android development projects, but over the past few years architecture has been widely publicized in the Android community. The days of Activity are over, and Google has released a repository called Android Architecture Blueprints that contains many examples and descriptions of different architectures. Finally, at Google IO/17, we announced the Android Architecture Components family of architectural Components that help us write simpler, higher-quality applications. You can build your application using one or both of these components, but I’ve found them all useful, so I’ll show you how to use them in the rest of this article and the next two. First, I’ll write some problematic code and then use the components to refactor to see what the libraries can do for us.

There are two main architectural patterns

  • MVP
  • MVVM

It’s hard to say which one is better. You should try both before you decide. I personally prefer the MVVM architecture with lifecycle components, and this series will focus on it. If you haven’t used the MVP architecture, there are plenty of good articles about it on Medium.

5.2 What is MVVM Mode?

The MVVM pattern is an architectural pattern. It stands for model-view-viewModel. I think the name is confusing to developers. If I were to name it, I would name it view-ViewModel-model, because ViewModel is the middleman between the View and Model.

View is the abstract name of your Activity/Fragment/ or other custom View. It is important not to confuse it with the Android View. The View should be clean, it should not contain any logical code, it should not hold any data, it should hold a ViewModel instance, all data should be fetched from the instance. In addition, the View should observe this data, and the layout should refresh once when the data in the ViewModel changes. In summary, the View’s job is: how the layout finds different data and states.

The ViewModel is the abstract name of the class that holds the data and has logic about when the data should be fetched and when it should be displayed. The ViewModel remains in the current state. In addition, the ViewModel should hold one or more Model instances from which all data should be fetched. For example, the ViewModel should not know whether the data is coming from a database or a remote server. Furthermore, the ViewModel should not know anything about the View at all. Also, the ViewModel shouldn’t know anything about the Android framework layer at all.

Model is the abstract name of the data layer. This is the class where we will fetch data from a remote server and cache it in memory or store it in a local database. Note, however, that unlike the Car, User, and Square Model classes, which only hold data, the Model is an implementation of the Repository schema, which will be covered later, and the Model should not know about the ViewModel.

If implemented correctly, MVVM is a great way to separate code and make it more testable. It helps us follow SOLID principles, so our code is easier to maintain.

Code sample

Now, I’m going to write the simplest example of how this works, right

First, let’s create a simple Model that returns some strings:

class RepoModel {

    fun refreshData(a) : String {
        return "Some new data"}}Copy the code

Typically, retrieving data is an asynchronous call, so we have to wait for the data to load. To simulate it, I changed the class to the following:

class RepoModel {

    fun refreshData(onDataReadyCallback: OnDataReadyCallback) {
        Handler().postDelayed({ onDataReadyCallback.onDataReady("new data")},2000)}}interface OnDataReadyCallback {
    fun onDataReady(data : String)
}
Copy the code

First, we create an interface OnDataReadyCallback, which has a method onDataReady, and then use OnDataReadyCallback as the parameter of refreshData, and use Handler to simulate waiting. After 2000ms, Call the onDataReady method of the interface instance.

Let’s look at the ViewModel:

class MainViewModel {
    var repoModel: RepoModel = RepoModel()
    var text: String = ""
    var isLoading: Boolean = false
}
Copy the code

As you can see, there is an instance of RepoModel, a text we want to display, and a Boolean value isLoading to save the state. Now, let’s create a Refresh method that is responsible for getting the data

class MainViewModel {...val onDataReadyCallback = object : OnDataReadyCallback {
        override fun onDataReady(data: String) {
            isLoading.set(false)
            text.set(data)}}fun refresh(a){
        isLoading.set(true)
        repoModel.refreshData(onDataReadyCallback)
    }
}
Copy the code

The refresh method calls the refreshData method of repoModel, passing an onDataReadyCallback. But wait a minute, what the hell is object? Object declarations are used whenever you want to implement an interface or extend a class without subclassing it. What if you want to use it as an anonymous class? In this case, you must use object expressions:

class MainViewModel {
    var repoModel: RepoModel = RepoModel()
    var text: String = ""
    var isLoading: Boolean = false

    fun refresh(a) {
        repoModel.refreshData( object : OnDataReadyCallback {
        override fun onDataReady(data: String) {
            text = data}}})Copy the code

When we call Refresh, we should change the view to the loaded state and set isLoading to false once the data arrives.

In addition, we should change text to ObservableField

and isLoading to ObservableField

. ObservableField is a class in the Data Binding library that can be used instead of creating an Observable object. It encapsulates what we want to observe.

class MainViewModel {
    var repoModel: RepoModel = RepoModel()

    val text = ObservableField<String>()

    val isLoading = ObservableField<Boolean> ()fun refresh(a){
        isLoading.set(true)
        repoModel.refreshData(object : OnDataReadyCallback {
            override fun onDataReady(data: String) {
                isLoading.set(false)
                text.set(data)}})}}Copy the code

Note that I use val instead of var because we are only changing the value in the field, not the field itself, and if we want to initialize it, we should do the following:

 val text = ObservableField("old data")
 val isLoading = ObservableField(false)
Copy the code

We changed the layout so that it could observe text and isLoading. First, we will bind MainViewModel instead of Repository:

<data>
    <variable
        name="viewModel"
        type="me.mladenrakonjac.modernandroidapp.MainViewModel" />
</data>
Copy the code

Then, do the following:

  • Change the TextView to look at the MainViewModel instancetext
  • Add only inisLoadingfortrueIs visible to the ProgressBar
  • Clicking the Add button calls the Refresh function from the MainViewModel instance and only atisLoadingforfalseTo click
.<TextView
            android:id="@+id/repository_name"
            android:text="@{viewModel.text}"
            .
            />.<ProgressBar
            android:id="@+id/loading"
            android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}"
            .
            />

        <Button
            android:id="@+id/refresh_button"
            android:onClick="@{() -> viewModel.refresh()}"
            android:clickable="@{viewModel.isLoading ? false : true}"
            />.Copy the code

If you run the program at this point, an error will be reported because view.visible and view.gone cannot be used if the View is not imported. Therefore, we must import it:

<data>
        <import type="android.view.View"/>

        <variable
            name="viewModel"
            type="me.fleka.modernandroidapp.MainViewModel" />
</data>
Copy the code

Ok, the layout is done here. Now it’s time to complete the binding. As we said, the View should hold an instance of the ViewModel:

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    var mainViewModel = MainViewModel()

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = mainViewModel
        binding.executePendingBindings()

    }
}
Copy the code

Finally, we can run it.

You can see that the old data has been changed to new data.

This is the simplest example of MVVM.

There’s a problem with that, let’s now rotate the phone:

The new data goes back to the old data. How is that possible? Take a look at the Activity lifecycle:

After the screen is rotated, a new instance of the Activity is created and the onCreate () method is called. Now, look at our Activity:

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    var mainViewModel = MainViewModel()

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = mainViewModel
        binding.executePendingBindings()

    }
}
Copy the code

As you can see, once a new Activity instance is created, a new MainViewModel instance is also created. Wouldn’t it be nice if somehow we could have the same MainViewModel instance for every Re-created MainActivity?

Lifecycle- Aware component introduction

Since many developers have encountered this problem, the developers on the Android Framework Team decided to develop a library that would help us solve it. The ViewModel class is one of them. It is the class that all of our ViewModels should extend.

To make our MainViewModel inherit from our own lifecycle aware component ViewModel, we should first add the lifecycle aware component library to our build.gradle file:

dependencies {
    ... 

    implementation "Android. Arch. Lifecycle: the runtime: 1.0.0 - alpha9"
    implementation "Android. Arch. Lifecycle: extensions: 1.0.0 - alpha9"
    kapt "Android. Arch. Lifecycle: the compiler: 1.0.0 - alpha9"
}
Copy the code

MainViewModel inherits from ViewModel as follows:

package me.mladenrakonjac.modernandroidapp

import android.arch.lifecycle.ViewModel

class MainViewModel : ViewModel() {... }Copy the code

In the Activity’s onCreate method, you should change it to:

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding
    
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        binding.executePendingBindings()

    }
}
Copy the code

Note that we don’t create a new MainViewModel instance. We get it from the ViewModelProvider, which is a utility class that has methods to get ViewModel instances. Scope-dependent, if you call viewModelproviders.of (this) in your Activity, the ViewModel will persist until the Activity is completely destroyed. Similarly, if you call it in your Fragment, The ViewModel will persist until the Fragment is completely destroyed. Take a look at the image below:

The ViewModelProvider is responsible for creating a new instance on the first invocation or returning the old instance when an Activity/Fragment is recreated.

Not to be confused with:

MainViewModel::class.java
Copy the code

In Kotlin, if you write the following:

MainViewModel::class
Copy the code

It will return a KClass, which is different from a Class in Java. So, we need a.java suffix.

Returns the Java Class instance corresponding to the given KClass instance.

Let’s see, what happens when YOU rotate the screen?

We have the same data as we did with the rotation.

In the last article, I said that our application would take the Github repository list and display it. To do this, we must add the getRepositories function, which returns a list of mock repositories:

class RepoModel {

    fun refreshData(onDataReadyCallback: OnDataReadyCallback) {
        Handler().postDelayed({ onDataReadyCallback.onDataReady("new data")},2000)}fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
        var arrayList = ArrayList<Repository>()
        arrayList.add(Repository("First"."Owner 1".100 , false))
        arrayList.add(Repository("Second"."Owner 2".30 , true))
        arrayList.add(Repository("Third"."Owner 3".430 , false))
        
        Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) },2000)}}interface OnDataReadyCallback {
    fun onDataReady(data : String)
}

interface OnRepositoryReadyCallback {
    fun onDataReady(data : ArrayList<Repository>)
}
Copy the code

Accordingly, in the ViewModel, there should also be a function that calls the getRepositories function in RepoModel.

class MainViewModel : ViewModel() {...var repositories = ArrayList<Repository>()

    fun refresh(a){... }fun loadRepositories(a){
        isLoading.set(true)
        repoModel.getRepositories(object : OnRepositoryReadyCallback{
            override fun onDataReady(data: ArrayList<Repository>) {
                isLoading.set(false)
                repositories = data}}}})Copy the code

Finally, we should display the list of warehouses in RecyclerView. To do this, we will have to:

  • Add arv_item_repository.xmllayout
  • inactivity_main.xmlAdd RecyclerView
  • addRepositoryRecyclerViewAdapterThe adapter
  • Create an Adapter for RecycklerView

To make rv_item_repository.xml use CardView, add the following to build.gradle:

implementation 'com. Android. Support: cardview - v7:26.0.1'
Copy the code

Then, the layout looks like this:

<?xml version="1.0" encoding="utf-8"? >
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <import type="android.view.View" />

        <variable
            name="repository"
            type="me.mladenrakonjac.modernandroidapp.uimodels.Repository" />
    </data>

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="96dp"
        android:layout_margin="8dp">

        <android.support.constraint.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:id="@+id/repository_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="16dp"
                android:layout_marginStart="16dp"
                android:text="@{repository.repositoryName}"
                android:textSize="20sp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintHorizontal_bias="0.0"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintVertical_bias="0.083"
                tools:text="Modern Android App" />

            <TextView
                android:id="@+id/repository_has_issues"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="16dp"
                android:layout_marginStart="16dp"
                android:layout_marginTop="8dp"
                android:text="@string/has_issues"
                android:textStyle="bold"
                android:visibility="@{repository.hasIssues ? View.VISIBLE : View.GONE}"
                app:layout_constraintBottom_toBottomOf="@+id/repository_name"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="1.0"
                app:layout_constraintStart_toEndOf="@+id/repository_name"
                app:layout_constraintTop_toTopOf="@+id/repository_name"
                app:layout_constraintVertical_bias="1.0" />

            <TextView
                android:id="@+id/repository_owner"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginBottom="8dp"
                android:layout_marginEnd="16dp"
                android:layout_marginStart="16dp"
                android:text="@{repository.repositoryOwner}"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/repository_name"
                app:layout_constraintVertical_bias="0.0"
                tools:text="Mladen Rakonjac" />

            <TextView
                android:id="@+id/number_of_starts"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginBottom="8dp"
                android:layout_marginEnd="16dp"
                android:layout_marginStart="16dp"
                android:layout_marginTop="8dp"
                android:text="@{String.valueOf(repository.numberOfStars)}"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="1"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/repository_owner"
                app:layout_constraintVertical_bias="0.0"
                tools:text="0 stars" />

        </android.support.constraint.ConstraintLayout>

    </android.support.v7.widget.CardView>

</layout>
Copy the code

The next step is to add RecyclerView to main_activity.xml. Before that, don’t forget to add:

Implementation 'com. Android. Support: recyclerview - v7:26.0.1'Copy the code
<?xml version="1.0" encoding="utf-8"? >
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <import type="android.view.View"/>

        <variable
            name="viewModel"
            type="me.fleka.modernandroidapp.MainViewModel" />
    </data>

    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="me.fleka.modernandroidapp.MainActivity">

        <ProgressBar
            android:id="@+id/loading"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:indeterminate="true"
            android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}"
            app:layout_constraintBottom_toTopOf="@+id/refresh_button"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <android.support.v7.widget.RecyclerView
            android:id="@+id/repository_rv"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:indeterminate="true"
            android:visibility="@{viewModel.isLoading ? View.GONE : View.VISIBLE}"
            app:layout_constraintBottom_toTopOf="@+id/refresh_button"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:listitem="@layout/rv_item_repository" />

        <Button
            android:id="@+id/refresh_button"
            android:layout_width="160dp"
            android:layout_height="40dp"
            android:layout_marginBottom="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:onClick="@{() -> viewModel.loadRepositories()}"
            android:clickable="@{viewModel.isLoading ? false : true}"
            android:text="Refresh"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="1.0" />

    </android.support.constraint.ConstraintLayout>

</layout>
Copy the code

Note that we removed some TextView elements from the previous layout, and that the button now fires the loadRepositories function instead of refresh:

<Button
    android:id="@+id/refresh_button"
    android:onClick="@{() -> viewModel.loadRepositories()}" 
    .
    />
Copy the code

Then, we remove the refresh function in MainViewModel and the refreshData function in RepoModel because we no longer need them.

Now, let’s add an Adapter

class RepositoryRecyclerViewAdapter(private var items: ArrayList<Repository>,
                                    private var listener: OnItemClickListener)
    : RecyclerView.Adapter<RepositoryRecyclerViewAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup? , viewType:Int): ViewHolder {
        val layoutInflater = LayoutInflater.from(parent?.context)
        val binding = RvItemRepositoryBinding.inflate(layoutInflater, parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int)
            = holder.bind(items[position], listener)

    override fun getItemCount(a): Int = items.size

    interface OnItemClickListener {
        fun onItemClick(position: Int)
    }

    class ViewHolder(private var binding: RvItemRepositoryBinding) :
            RecyclerView.ViewHolder(binding.root) {

        fun bind(repo: Repository, listener: OnItemClickListener?). {
            binding.repository = repo
            if(listener ! =null) { binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) }) } binding.executePendingBindings() } }}Copy the code

Note that the ViewHolder holds an instance of the RvItemRepositoryBinding type, not the View type, so that we can use a Data Binding for the Item. Also, don’t be confused by this line:

override fun onBindViewHolder(holder: ViewHolder, position: Int)            = holder.bind(items[position], listener)
Copy the code

It is simply a shorthand for the following function:

override fun onBindViewHolder(holder: ViewHolder, position: Int){
    return holder.bind(items[position], listener)
}
Copy the code

Items [position] is an implementation of the index operator. It is the same as items.get (position).

Another line of code may also confuse you:

binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) })
Copy the code

You can replace the argument with _, which is cool, right?

We added the adapter, but still haven’t set it to recyclerView in MainActivity:

class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        binding.viewModel = viewModel
        binding.executePendingBindings()

        binding.repositoryRv.layoutManager = LinearLayoutManager(this)
        binding.repositoryRv.adapter = RepositoryRecyclerViewAdapter(viewModel.repositories, this)}override fun onItemClick(position: Int) {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.}}Copy the code

It’s weird. What’s going on here?

  • The Activity is created, so the actual null is usedrepositoriesA new adapter is created
  • We clicked the button
  • callloadRepositoriesFunction to display progress
  • Two seconds later, we get Repositories.the progress is hidden, but the list is not shown. This is because it is not called on the adapternotifyDataSetChanged
  • After you rotate the screen, a new Activity will be created, so you’ll use arepositoriesParameter to create a new adapter

So, how should MainViewModel notify MainActivity about the new Item so that we can call notifyDataSetChanged?

This shouldn’t be done by the ViewModel

This is very important, MainViewModel should understand MainActivity, MainActivity is having MainViewModel instance, so it is should listen to changes and Adapter informed about changes.

The? So how do you do that?

We should be able to observe repositories and notify the list of repositories as it changes.

What’s wrong with the solution?

Let’s take a look at the following scenario:

  • Observe in MainActivityrepositoriesOnce it releases the change, we callnotifyDataSetChanged
  • Let’s click on the button
  • While we wait for the data to change, MainActivity can be recreated due to configuration changes.
  • But our MainViewModel still exists.
  • Two seconds later, the “Repositories” field retrieves the new project and notifies the observer that the data has changed
  • An observer attempts to hold an adapter that no longer existsLine notifyDataSetChangedBecause MainActivity has been recreated.

And then the program crashes, so it’s not good enough. We have to introduce a new component called LiveData

LiveData introduction

LiveData is another life-cycle aware component that can observe the life cycle of a View, so once an Activity is destroyed due to a configuration change, LiveData will sense it and then unsubscribe an observer from the destroyed Activity.

Let’s implement it in MainViewModel:

class MainViewModel : ViewModel() {
    var repoModel: RepoModel = RepoModel()

    val text = ObservableField("old data")

    val isLoading = ObservableField(false)

    var repositories = MutableLiveData<ArrayList<Repository>>()

    fun loadRepositories(a) {
        isLoading.set(true)
        repoModel.getRepositories(object : OnRepositoryReadyCallback {
            override fun onDataReady(data: ArrayList<Repository>) {
                isLoading.set(false)
                repositories.value = data}}}})Copy the code

And observe the change of MainActivity:

class MainActivity : LifecycleActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {

    private lateinit var binding: ActivityMainBinding
    private val repositoryRecyclerViewAdapter = RepositoryRecyclerViewAdapter(arrayListOf(), this)


    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        binding.viewModel = viewModel
        binding.executePendingBindings()

        binding.repositoryRv.layoutManager = LinearLayoutManager(this)
        binding.repositoryRv.adapter = repositoryRecyclerViewAdapter
        viewModel.repositories.observe(this, Observer<ArrayList<Repository>> { it? .let{ repositoryRecyclerViewAdapter.replaceData(it)} }) }override fun onItemClick(position: Int) {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.}}Copy the code

What does the it keyword stand for? In Kotlin, if the function has only one argument, then by default you are replaced with it, assuming we have a lamuda expression multiplied by 2:

((a) -> 2 * a) 
Copy the code

It can be written as follows:

(it * 2)
Copy the code

Now you run the program and everything works fine!

Why do I like MVVM and not MVP

  • There’s no boring interface to the View, because the ViewModel has no reference to the View
  • There is no interface for Presenter, because it is not required
  • It handles configuration changes so easily
  • With MVVM, the code in our Activity/Fragment is much cleaner

The Repository model

As I mentioned earlier, the Model is only an abstraction of the data layer. In general, it contains repositories and a data class, each of which should have a Repository class. For example, if we have a User and a Post data class, then we should also have UserRepository and PostRepository, and all data should be retrieved from Repository. We should not use Shared Preferences or DB directly in a View or ViewModel.

Therefore, we can rename RepoModel to GitRepoRepository, where GitRepo comes from the Github Repository and Repository comes from the Repository schema.

class GitRepoRepository {

    fun getGitRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
        var arrayList = ArrayList<Repository>()
        arrayList.add(Repository("First"."Owner 1".100.false))
        arrayList.add(Repository("Second"."Owner 2".30.true))
        arrayList.add(Repository("Third"."Owner 3".430.false))

        Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) }, 2000)}}interface OnRepositoryReadyCallback {
    fun onDataReady(data: ArrayList<Repository>)
}
Copy the code

Okay, MainViewModel gets the Github repository list from GitRepoRepsitories, but where does the GitRepoRepositories data come from?

You can call client or database instances directly from Repository, but this is still not a good practice. Your application should be as modular as possible. What if you decide to use another Client, Retrofit instead of Volley? If you have some logic in it, it’s hard to refactor it. Your repository does not need to know which client you are using to fetch remote data.

  • Repository only needs to know whether the data comes from a remote or local source, but does not need to know how to get it from a remote or local source
  • The ViewModel just needs the data
  • The View just needs to display the data

When I first started developing Android, I was thinking, how does an APP work offline? How is data synchronization implemented? Good architecture makes it easy for us to do that. For example, when loadRepositories is called in ViewModel, GitRepoRepositories will fetch data remotely and save it to a local data source if the network connection is right. Once the phone is in offline mode, GitRepoRepositories can obtain data from a LocalDataSource. Therefore, GitRepoRepositories needs to have the logic to hold RemoteDataSource instance RemoteDataSource and LocalDataSource instance LocalDataSource and handle where the data comes from.

Let’s add a local data source:

class GitRepoLocalDataSource {

    fun getRepositories(onRepositoryReadyCallback: OnRepoLocalReadyCallback) {
        var arrayList = ArrayList<Repository>()
        arrayList.add(Repository("First From Local"."Owner 1".100.false))
        arrayList.add(Repository("Second From Local"."Owner 2".30.true))
        arrayList.add(Repository("Third From Local"."Owner 3".430.false))

        Handler().postDelayed({ onRepositoryReadyCallback.onLocalDataReady(arrayList) }, 2000)}fun saveRepositories(arrayList: ArrayList<Repository>){
        //todo save repositories in DB}}interface OnRepoLocalReadyCallback {
    fun onLocalDataReady(data: ArrayList<Repository>)
}
Copy the code

Here, we have two methods: the first returns the forged local data, and the second is used to store the forged data.

Let’s add a remote data source:

class GitRepoRemoteDataSource {

    fun getRepositories(onRepositoryReadyCallback: OnRepoRemoteReadyCallback) {
        var arrayList = ArrayList<Repository>()
        arrayList.add(Repository("First from remote"."Owner 1".100.false))
        arrayList.add(Repository("Second from remote"."Owner 2".30.true))
        arrayList.add(Repository("Third from remote"."Owner 3".430.false))

        Handler().postDelayed({ onRepositoryReadyCallback.onRemoteDataReady(arrayList) }, 2000)}}interface OnRepoRemoteReadyCallback {
    fun onRemoteDataReady(data: ArrayList<Repository>)
}
Copy the code

It has only one method to return remote simulated data

Next add some logic to repository:

class GitRepoRepository {

    val localDataSource = GitRepoLocalDataSource()
    val remoteDataSource = GitRepoRemoteDataSource()

    fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
       remoteDataSource.getRepositories( object : OnRepoRemoteReadyCallback {
           override fun onDataReady(data: ArrayList<Repository>) {
               localDataSource.saveRepositories(data)
               onRepositoryReadyCallback.onDataReady(data)}})}}interface OnRepositoryReadyCallback {
    fun onDataReady(data: ArrayList<Repository>)
}
Copy the code

Therefore, by separating the data sources, we can easily save the data locally.

What if all you need is data from the web? Do YOU still need to use the Repository schema? Is. It makes your code easier to test, other developers can understand it better, and it can be maintained faster! 🙂

Android Manager wrapper

What if you want to check your Internet connection in GitRepoRepository so you know which data source to query? We’ve already said that we shouldn’t put any Android-related code in ViewModels and Models, so how do we handle this?

We write a wrapper for the network connection

class NetManager(private var applicationContext: Context) {
    private var status: Boolean? = false

    val isConnectedToInternet: Boolean?
        get() {
            val conManager = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
            val ni = conManager.activeNetworkInfo
            returnni ! =null && ni.isConnected
        }
}
Copy the code

The code above requires us to add permissions in the Manifest to work

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
Copy the code

But how do you create an instance in Repository? Because we don’t have a context, of course, we can pass it in from the constructor

class GitRepoRepository (context: Context){

    val localDataSource = GitRepoLocalDataSource()
    val remoteDataSource = GitRepoRemoteDataSource()
    val netManager = NetManager(context)

    fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
        remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback {
            override fun onDataReady(data: ArrayList<Repository>) {
                localDataSource.saveRepositories(data)
                onRepositoryReadyCallback.onDataReady(data)}})}}interface OnRepositoryReadyCallback {
    fun onDataReady(data: ArrayList<Repository>)
}
Copy the code

How do we get a NetManager before we create a new GitRepoRepository instance for the ViewModel? Because we need to pass a Context to NetManager, you can use the AndroidViewModel in the lifecycle aware component, which has Contenxt, and its Context is an Application Context instead of an Activity Context.

class MainViewModel : AndroidViewModel {

    constructor(application: Application) : super(application)

    var gitRepoRepository: GitRepoRepository = GitRepoRepository(NetManager(getApplication()))

    val text = ObservableField("old data")

    val isLoading = ObservableField(false)

    var repositories = MutableLiveData<ArrayList<Repository>>()

    fun loadRepositories(a) {
        isLoading.set(true)
        gitRepoRepository.getRepositories(object : OnRepositoryReadyCallback {
            override fun onDataReady(data: ArrayList<Repository>) {
                isLoading.set(false)
                repositories.value = data}}}})Copy the code

This line of code:

constructor(application: Application) : super(application)
Copy the code

We are defining the constructor for MainViewModel. This is required because AndroidViewModel requires an Application instance in its constructor. Therefore, in our constructor, we need to call the super method of the constructor of AndroidViewModel so that we can call the constructor of our inherited class.

Note: We can abbreviate it to one line:

class MainViewModel(application: Application) : AndroidViewModel(application) {
... 
}
Copy the code

We now have an instance of NetManager in our GitRepoRepository to check the network connection status.

class GitRepoRepository(val netManager: NetManager) {

    val localDataSource = GitRepoLocalDataSource()
    val remoteDataSource = GitRepoRemoteDataSource()

    fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback){ netManager.isConnectedToInternet? .let {if (it) {
                remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback {
                    override fun onRemoteDataReady(data: ArrayList<Repository>) {
                        localDataSource.saveRepositories(data)
                        onRepositoryReadyCallback.onDataReady(data)}}}else {
                localDataSource.getRepositories(object : OnRepoLocalReadyCallback {
                    override fun onLocalDataReady(data: ArrayList<Repository>) {
                        onRepositoryReadyCallback.onDataReady(data)}})}}}}interface OnRepositoryReadyCallback {
    fun onDataReady(data: ArrayList<Repository>)
}
Copy the code

So, if we have a network connection, we will grab remote data and save it locally. On the other hand, if there is no network connection, we will fetch local data.

Kotlin notes that the let operator checks for controllability and returns a value from it

trailer

In later articles, I’ll cover dependency injection, why creating repository instances in ViewModel sucks, and how to avoid using AndroidViewModel. Also, there are some problems with the code that has been written so far, and there are reasons for that, and I’m trying to get you to face them so that you can understand why all of these libraries are popular and why you should use them.

Finally, thank you for reading!

This series has been updated:

Write a modern Android project -Part1 – from scratch using Kotlin

Write a modern Android project -Part2 – from scratch using Kotlin

Write a modern Android project -Part3 – from scratch using Kotlin

Write a modern Android project -Part4 – from scratch using Kotlin

The article was first published on the public account: “Technology TOP”, there are dry articles updated every day, you can search wechat “technology TOP” first time to read, reply [mind map] [interview] [resume] yes, I prepare some Android advanced route, interview guidance and resume template for you