Modern Android development on Kotlin. Part 2

Hi, Habr! I present to your attention the translation of the article " Modern Android development with Kotlin (Part 2) " by Mladen Rakonjac.

Note. This article is a translation of cycles of articles from Mladen Rakonjac , article date: 09/23/2017. GitHub . Starting to read the first part of the SemperPeritus found that the other parts for some reason were not translated. Therefore, I offer you the second part. The article turned out to be voluminous.

image

"It is very difficult to find one project that would cover everything new in the development for Android in Android Studio 3.0, so I decided to write it."

In this article we will sort the following:

  1. Android Studio 3, beta 1 Part 1
  2. Kotlin programming language Part 1
  3. Assembly options Part 1
  4. ConstraintLayout Part 1
  5. Data Binding Library Part 1
  6. MVVM architecture + Pattern Repository + Android Manager Wrappers
  7. RxJava2 and how it helps us in the architecture of Part 3
  8. Dagger 2.11, what is dependency injection, why should you use this Part 4
  9. Retrofit (with Rx Java2)
  10. Room (with Rx Java2)

MVVM architecture + Pattern Repository + Android Manager Wrappers


A few words about architecture in the world of Android


For quite a long time, android developers have not used any architecture in their projects. In the past three years, a lot of hype has risen around her in the community of android developers. The time for God Activity has passed and Google has published the Android Architecture Blueprints repository , with many examples and instructions on various architectural approaches. Finally, at Google IO '17, they introduced the Android Architecture Components , a collection of libraries designed to help us create cleaner code and improve applications. Componentsays that you can use them all, or just one of them. However, I found them all really useful. Further in the text and in the following parts we will use them. First, I will get to the problem in code, and then I will refactor using these components and libraries to see what problems they are designed to solve.

There are two main architectural patterns that share GUI code:

  • MVP
  • MVVM

It's hard to say which is better. You should try both and decide. I prefer MVVM using lifecycle-aware components and I will write about it. If you have never tried MVP, Medium has a bunch of good articles about it.

What is the MVVM pattern?


MVVM is an architectural pattern , disclosed as Model-View-ViewModel. I think this name confuses the developers. If I were the one who coined his name, I would call it View-ViewModel-Model, because ViewModel is in the middle, connecting View and Model .

View is an abstraction for Activity , Fragment 'or any other custom View ( Android Custom View ). Please note it is important not to confuse this View with the Android View. View should be stupid, we should not write any logic in it. Viewshould not contain data. It should store a link to the ViewModel instance and all the data that the View needs , should come from there. In addition, the View should monitor this data and the layout should change when the data from the ViewModel changes. To summarize, the View is responsible for the following: the layout of the layout for various data and states.

ViewModel is the abstract name for the class containing the data and logic, when this data should be received and when shown. ViewModel stores the current state . ViewModel also stores a link to one or more ModelShe also receives all the data from them. She does not need to know, for example, where the data came from, from the database or from the server. In addition, the ViewModel should not know anything about the View . Moreover, the ViewModel does not need to know anything about the Android framework at all.

Model is the abstract name for the layer that prepares data for the ViewModel . This is the class in which we will receive data from the server and cache it, or save it to a local database. Note that these are not the same classes as User, Car, Square, other model classes that simply store data. As a rule, this is an implementation of the Repository template, which we will look at next. Model should not know anything about ViewModel.

MVVM , if implemented correctly, is a great way to break your code and make it more testable. This helps us to follow the principles of SOLID , so our code is easier to maintain.

Code example


Now I will write the simplest example showing how it works.

To begin with, let's create a simple Model , which returns a certain line:

RepoModel.kt
classRepoModel{
    fun refreshData() : String {
        return"Some new data"
    }
}


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

RepoModel.kt
classRepoModel{
    fun refreshData(onDataReadyCallback: OnDataReadyCallback){
        Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000)
    }
}
interfaceOnDataReadyCallback{
    fun onDataReady(data : String)
}


I created an interface OnDataReadyCallbackwith a method onDataReady. And now the method refreshDataimplements (implements) OnDataReadyCallback. To simulate the expectations I use Handler. Once every 2 seconds, the method onDataReadywill be called by classes that implement the interface OnDataReadyCallback.

Let's create a ViewModel :

MainViewModel.kt
classMainViewModel{
    var repoModel: RepoModel = RepoModel()
    var text: String = ""var isLoading: Boolean = false
}


As you can see, there is an instance RepoModel, textwhich will be shown, and a variable isLoadingthat stores the current state. Let's create a method refreshresponsible for getting the data:

MainViewModel.kt
classMainViewModel{
    ...
    val onDataReadyCallback = object : OnDataReadyCallback {
        override fun onDataReady(data: String){
            isLoading.set(false)
            text.set(data)
        }
    }
    fun refresh(){
        isLoading.set(true)
        repoModel.refreshData(onDataReadyCallback)
    }
}


The method refreshcalls refreshDatau RepoModel, which in arguments takes an implementation OnDataReadyCallback. OK, but what is it object? Whenever you want to implement (implement) an interface or inherit (extend) a class without creating a subclass, you will use an object declaration . And if you want to use this as an anonymous class? In this case, you use object expression :

MainViewModel.kt
classMainViewModel{
    var repoModel: RepoModel = RepoModel()
    var text: String = ""var isLoading: Boolean = false
    fun refresh(){
        repoModel.refreshData( object : OnDataReadyCallback {
        override fun onDataReady(data: String){
            text = data
        })
    }
}


When we call refresh, we need to change the view to the load state and when the data arrives, set the isLoadingvalue to y false.

Also we have to replace textwith
ObservableField<String>
, and isLoadingwith
ObservableField<Boolean>
. ObservableFieldthis is a class from the Data Binding library that we can use instead of creating an Observable object. It wraps the object we want to observe.

MainViewModel.kt
classMainViewModel{
    var repoModel: RepoModel = RepoModel()
    val text = ObservableField<String>()
    val isLoading = ObservableField<Boolean>()
    fun refresh(){
        isLoading.set(true)
        repoModel.refreshData(object : OnDataReadyCallback {
            override fun onDataReady(data: String){
                isLoading.set(false)
                text.set(data)
            }
        })
    }
}


Notice that I use val instead of var , because we only change the value in the field, not the field itself. And if you want to initialize it, use the following:

initobserv.kt
val text = ObservableField("old data")
val isLoading = ObservableField(false)



Let's change our layout so that it can observe the text and isLoading . First of all, let's link the MainViewModel instead of the Repository :

activity_main.xml
<data><variablename="viewModel"type="me.mladenrakonjac.modernandroidapp.MainViewModel" /></data>


Then:

  • Change TextView to monitor text from MainViewModel
  • Add a ProgressBar, which will only be visible if isLoading is true
  • Add a Button, which, when clicked, will call the refresh method from the MainViewModel and will be clickable only if isLoading is false

main_activity.xml
...
        <TextViewandroid:id="@+id/repository_name"android:text="@{viewModel.text}"...
            />
        ...
        <ProgressBarandroid:id="@+id/loading"android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}"...
            /><Buttonandroid:id="@+id/refresh_button"android:onClick="@{() -> viewModel.refresh()}"android:clickable="@{viewModel.isLoading ? false : true}"
            />
...


If you run now, you will get an error View.VISIBLE and View.GONE cannot be used if View is not imported. Well, let's import:

main_activity.xml
<data><importtype="android.view.View"/><variablename="viewModel"type="me.fleka.modernandroidapp.MainViewModel" /></data>


Ok, with the layout finished. Now finish with the binding. As I said, Viewshould have a copy ViewModel:

MainActivity.kt
classMainActivity : 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()
    }
}


Finally, we can run


You can see that old data is replaced with new data .

This was a simple MVVM example.

But there is one problem, let's turn the screen


old data replaced new data . How is this possible? Take a look at the life cycle of the Activity:

Activity lifecycle
image

When you turned the phone, a new instance of Activity was created and the method onCreate()was called. Take a look at our activity:

MainActivity.kt
classMainActivity : 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()
    }
}


As you can see, when the Activity instance was created, the MainViewModel instance was created too. Is it good if somehow we have the same instance of MainViewModel for each recreated MainActivity ?

Introduction to Lifecycle-aware components


Since Many developers are faced with this problem, the developers of the Android Framework Team have decided to make a library designed to help solve this. The ViewModel class is one of them. This is the class from which all our ViewModel should be inherited.

Let's inherit the MainViewModel from ViewModel from lifecycle-aware components. First we need to add the lifecycle-aware components library to our build.gradle file:

build.gradle
dependencies {
    ... 
    implementation "android.arch.lifecycle:runtime:1.0.0-alpha9"
    implementation "android.arch.lifecycle:extensions:1.0.0-alpha9"
    kapt "android.arch.lifecycle:compiler:1.0.0-alpha9"


Make the MainViewModel the heir to the ViewModel :

MainViewModel.kt
package me.mladenrakonjac.modernandroidapp
import android.arch.lifecycle.ViewModel
classMainViewModel : ViewModel() {
    ...
}


The onCreate () method of our MainActivity will look like this:

MainActivity.kt
classMainActivity : 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()
    }
}


Notice that we have not created a new instance of MainViewModel . We get it using ViewModelProviders . ViewModelProviders is a utility class (Utility) that has a method for obtaining a ViewModelProvider . It's all about scope . If you call ViewModelProviders.of (this) in the Activity, then your ViewModel will live as long as this Activity is alive (until it is destroyed without being re-created). Therefore, if you call it in a fragment, then your ViewModel will live while the Fragment is alive, etc. Take a look at the chart:

Life cycle
image

ViewModelProvider is responsible for creating a new instance in the case of the first call or returning the old one if your Activity or Fragment is recreated.

Don't get confused with

MainViewModel::class.java

In Kotlin, if you do

MainViewModel::class

this will give you a KClass , which is not the same as the Class from Java. So if we write .java , then the documentation is:
Returns a Java Class instance corresponding to this KClass instance.
Let's see what happens when you rotate the screen.


We have the same data as before turning the screen.

In the last article, I said that our application will receive a list of Github repositories and show them. To do this, we need to add the getRepositories function , which will return the fake list of repositories:

RepoModel.kt
classRepoModel{
    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)
    }
}
interfaceOnDataReadyCallback{
    fun onDataReady(data : String)
}
interface OnRepositoryReadyCallback {
    fun onDataReady(data : ArrayList<Repository>)
}


We must also have a method in MainViewModel that will call getRepositories from RepoModel :

MainViewModel.kt
classMainViewModel : ViewModel() {
    ...
    var repositories = ArrayList<Repository>()
    fun refresh(){
        ...
    }
    fun loadRepositories(){
        isLoading.set(true)
        repoModel.getRepositories(object : OnRepositoryReadyCallback{
            override fun onDataReady(data: ArrayList<Repository>){
                isLoading.set(false)
                repositories = data
            }
        })
    }
}


Finally, we need to show these repositories in RecyclerView. To do this, we must:

  • Create layout rv_item_repository.xml
  • Add RecyclerView to layout activity_main.xml
  • Create RepositoryRecyclerViewAdapter
  • Install adapter from recyclerview

To create rv_item_repository.xml, I used the CardView library, so we need to add it to build.gradle (app):

implementation 'com.android.support:cardview-v7:26.0.1'

Here is what it looks like:

rv_item_repository.xml
<?xml version="1.0" encoding="utf-8"?><layoutxmlns: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><importtype="android.view.View" /><variablename="repository"type="me.mladenrakonjac.modernandroidapp.uimodels.Repository" /></data><android.support.v7.widget.CardViewandroid:layout_width="match_parent"android:layout_height="96dp"android:layout_margin="8dp"><android.support.constraint.ConstraintLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid: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" /><TextViewandroid: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" /><TextViewandroid: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" /><TextViewandroid: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>


The next step is to add the RecyclerView to activity_main.xml . Before you do this, remember to add the RecyclerView library:

implementation 'com.android.support:recyclerview-v7:26.0.1'

activity_main.xml
<?xml version="1.0" encoding="utf-8"?><layoutxmlns: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><importtype="android.view.View"/><variablename="viewModel"type="me.fleka.modernandroidapp.MainViewModel" /></data><android.support.constraint.ConstraintLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"tools:context="me.fleka.modernandroidapp.MainActivity"><ProgressBarandroid: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.RecyclerViewandroid: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" /><Buttonandroid: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>



Notice that we have deleted some TextView elements and now the button starts loadRepositories instead of refresh :

button.xml
<Buttonandroid:id="@+id/refresh_button"android:onClick="@{() -> viewModel.loadRepositories()}"...
    />


Let's remove the refresh method from MainViewModel and refreshData from RepoModel as unnecessary.

Now you need to create an Adapter for the RecyclerView:

RepositoryRecyclerViewAdapter.kt
classRepositoryRecyclerViewAdapter(privatevaritems: ArrayList<Repository>,
                                    privatevarlistener: 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(): Int = items.size
    interfaceOnItemClickListener{
        fun onItemClick(position: Int)
    }
    class ViewHolder(privatevar 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()
        }
    }
}


Note that the ViewHolder takes an instance of the RvItemRepositoryBinding type , instead of the View , so we can implement the Data Binding in the ViewHolder for each item. Do not be confused by the single-line function (oneline):

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

This is just a brief entry for:

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

And items [position] is the implementation for the index operator. It is similar to items.get (position) .

Another line that can confuse you:

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

You can replace the parameter with _ if you do not use it. Nice, huh?

We created the adapter, but still have not applied it to the recyclerView in MainActivity :

MainActivity.kt
classMainActivity : 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.
    }
}


Run the application


This is strange. What happened?

  • The activity was created, so the new adapter was also created with repositories that are actually empty.
  • We press a button
  • Called by loadRepositories, progress is shown
  • After 2 seconds we get the repositories, the progress is hidden, but they do not appear. This is because notifyDataSetChanged is not called in the adapter .
  • When we rotate the screen, a new Activity is created, so a new adapter is created with the repositories parameter with some data.

So, how should the MainViewModel notify the MainActivity about new items, can we call notifyDataSetChanged ?

Can not.

This is really important, the MainViewModel does not need to know about MainActivity at all .

A MainActivity is one who has an instance of MainViewModel , so he must listen to the changes and notify the Adapter of the changes.

But how to do that?

We can monitor the repositories , so after changing the data, we can change our adapter.

What is wrong with this decision?

Let's consider the following case:

  • In MainActivity, we observe the repositories: when a change occurs, we perform a notifyDataSetChanged
  • We press a button
  • While we are waiting for data changes, MainActivity may be re-created due to configuration changes.
  • Our MainViewModel is still alive.
  • After 2 seconds, the repositories field receives new items and notifies the observer that the data has changed.
  • The observer is trying to perform notifyDataSetChanged on the adapter ’s, which no longer exists, because MainActivity has been re-created

Well, our solution is not good enough.

Introduction to LiveData


LiveData is another Lifecycle-aware component. It is based on observable (observable), which is aware of the View life cycle. So when an Activity is destroyed due to a configuration change , LiveData knows about it, so it removes the observer from the destroyed Activity too.

Implement in MainViewModel :

MainViewModel.kt
classMainViewModel : ViewModel() {
    var repoModel: RepoModel = RepoModel()
    val text = ObservableField("old data")
    val isLoading = ObservableField(false)
    var repositories = MutableLiveData<ArrayList<Repository>>()
    fun loadRepositories(){
        isLoading.set(true)
        repoModel.getRepositories(object : OnRepositoryReadyCallback {
            override fun onDataReady(data: ArrayList<Repository>){
                isLoading.set(false)
                repositories.value = data
            }
        })
    }
}


and start watching the MainActivity:

MainActivity.kt
classMainActivity : 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.
    }
}


What does the word it mean ? If a function has only one parameter, the access to this parameter can be obtained by using the keyword IT . So, suppose we have a lambda expression for multiplying by 2:

((a) -> 2 * a)

Can be replaced as follows:

(it * 2)

If you start the application now, you can make sure that everything works.


...

Why do I prefer MVVM over MVP?



  • There is no boring interface for View, since ViewModel has no link to View
  • No boring Presenter interface and no need for that.
  • Much easier to handle configuration changes
  • Using MVVM, we have less code for Activity, Fragments etc.

...

Repository Pattern


Scheme
image

As I said earlier, Model is just the abstract name for the layer where we prepare the data. It usually contains repositories and data classes. Each class of entity (data) has a corresponding class Repository . For example, if we have the User and Post classes , we should also have a UserRepository and PostRepository . All data comes from there. We should never call an instance of Shared Preferences or DB from View or ViewModel.

So we can rename our RepoModel to GitRepoRepository , where GitRepo comes from the Github repository and Repository comes from the Repository pattern.

RepoRepositories.kt
classGitRepoRepository{
    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)
    }
}
interfaceOnRepositoryReadyCallback{
    fun onDataReady(data: ArrayList<Repository>)
}


Well, MainViewModel gets the Github repository list from GitRepoRepsitories , but where does one get the GitRepoRepositories from ?

You can call the client instance or DB directly in the repository, but still not the best practice. Your application should be as modular as you can get it. What if you decide to use different clients to replace Volley with Retrofit? If you have some kind of logic inside, it will be difficult to refactor. Your repository does not have to know which client you are using to retrieve remote (remote) data.

  • The only thing that the repository needs to know is that the data is received remotely or locally. No need to know how we get this remote or local data.
  • The only thing that requires a ViewModel is data
  • The only thing that a View should do is to show this data.

When I first started developing on Android, I was wondering how applications work in offline mode and how data synchronization works. Good application architecture allows us to do this with ease. For example, when loadRepositories in ViewModel is called, if there is an Internet connection, GitRepoRepositories can receive data from a remote data source and save it to a local data source. When the phone is offline, GitRepoRepository can retrieve data from local storage. So, Repositories should have instances of RemoteDataSource and LocalDataSource and the logic processing where this data comes from.

Add a local data source :

GitRepoLocalDataSource.kt
classGitRepoLocalDataSource{
    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
    }
}
interfaceOnRepoLocalReadyCallback{
    fun onLocalDataReady(data: ArrayList<Repository>)
}


Here we have two methods: the first, which returns fake local data and the second, for dummy data storage.

Add a remote data source :

GitRepoRemoteDataSource.kt
classGitRepoRemoteDataSource{
    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)
    }
}
interfaceOnRepoRemoteReadyCallback{
    fun onRemoteDataReady(data: ArrayList<Repository>)
}


There is only one method that returns fake deleted data.

Now we can add some logic to our repository:

GitRepoRepository.kt
classGitRepoRepository{
    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)
           }
       })
    }
}
interfaceOnRepositoryReadyCallback{
    fun onDataReady(data: ArrayList<Repository>)
}


Thus, separating the sources, we easily save the data locally.

What if you only need data from the network, still need to use the repository template? Yes. It simplifies code testing, other developers can understand your code better, and you can maintain it faster!

...

Android Manager Wrappers


What if you want to check the internet connection in the GitRepoRepository to know where to request data from? We have already said that we should not place any code associated with Android in the ViewModel and Model , so how to handle this problem?

Let's write a wrapper for an Internet connection:

NetManager.kt (The same solution applies to other managers, for example, to NfcManager)
classNetManager(privatevarapplicationContext: Context) {
    privatevar status: Boolean? = false
    val isConnectedToInternet: Boolean?
        get() {
            val conManager = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
            val ni = conManager.activeNetworkInfo
            return ni != null && ni.isConnected
        }
}


This code will only work if you add permission to manifest:

<uses-permissionandroid:name="android.permission.INTERNET" /><uses-permissionandroid:name="android.permission.ACCESS_NETWORK_STATE" /><uses-permissionandroid:name="android.permission.ACCESS_WIFI_STATE" />

But how to create an instance in the Repository, if we do not have the context ( context The )? We can request it in the constructor:

GitRepoRepository.kt
classGitRepoRepository (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)
            }
        })
    }
}
interfaceOnRepositoryReadyCallback{
    fun onDataReady(data: ArrayList<Repository>)
}


We created a new GitRepoRepository instance in the ViewModel. How can we have NetManager in ViewModel now when we need context for NetManager ? You can use AndroidViewModel from the Lifecycle-aware components library, which has context . This is an application context, not an Activity.

MainViewModel.kt
classMainViewModel : 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(){
        isLoading.set(true)
        gitRepoRepository.getRepositories(object : OnRepositoryReadyCallback {
            override fun onDataReady(data: ArrayList<Repository>){
                isLoading.set(false)
                repositories.value = data
            }
        })
    }
}


In this line

constructor(application: Application) : super(application)

we defined a constructor for the MainViewModel . This is necessary because AndroidViewModel requests an application instance in its constructor. So, in our constructor, we call the super method, which calls the AndroidViewModel's constructor , from which we inherit.

Note: we can get rid of one line if we do:

classMainViewModel(application: Application) : AndroidViewModel(application) {
... 
}

And now, when we have an instance of NetManager in the GitRepoRepository , we can check the internet connection:

GitRepoRepository.kt
classGitRepoRepository(valnetManager: 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)
                    }
                })
            }
        }
    }
}
interfaceOnRepositoryReadyCallback{
    fun onDataReady(data: ArrayList<Repository>)
}


Thus, if we have an internet connection, we will retrieve the deleted data and save it locally. If we do not have an internet connection, we will get local data.

Kotlin note : the let statement checks for null and returns the value inside it .

In one of the following articles, I will write about dependency injection, how bad it is to create repository instances in ViewModel and how to avoid using AndroidViewModel. Also I will write about a large number of problems that now exist in our code. I left them for the reason ...

I am trying to show you the problems so that you can understand why all these libraries are popular and why you should use them.

PS I have changed my mind about the mapper ( mappers ). I decided to cover it in the following articles.

Also popular now: