📜 ⬆️ ⬇️

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. Component says 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:


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. View should 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 . The ViewModel also stores a link to one or more Model 's and all the data it receives 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 the 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
class RepoModel { 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
 class RepoModel { fun refreshData(onDataReadyCallback: OnDataReadyCallback) { Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000) } } interface OnDataReadyCallback { fun onDataReady(data : String) } 


I created the OnDataReadyCallback interface with the onDataReady method. And now the refreshData method implements (implements) OnDataReadyCallback . To simulate expectations, I use Handler . Every 2 seconds, the onDataReady method will be called on classes that implement the OnDataReadyCallback interface.

Let's create a ViewModel :

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


As you can see, there is an instance of RepoModel , the text that will be shown, and the isLoading variable, which stores the current state. Let's create a refresh method that is responsible for receiving data:

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


The refresh method calls refreshData from the RepoModel , which takes the OnDataReadyCallback implementation in arguments. OK, but what is an 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
 class MainViewModel { 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 have to change the view to the loading state and when the data arrives, set isLoading to false .

We also need to replace text with
 ObservableField<String> 
and isLoading on
 ObservableField<Boolean> 
. ObservableField is a class from the Data Binding library that we can use instead of creating an Observable object. It wraps the object that we want to observe.

MainViewModel.kt
 class MainViewModel { 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> <variable name="viewModel" type="me.mladenrakonjac.modernandroidapp.MainViewModel" /> </data> 


Then:


main_activity.xml
 ... <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}" /> ... 


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

main_activity.xml
 <data> <import type="android.view.View"/> <variable name="viewModel" type="me.fleka.modernandroidapp.MainViewModel" /> </data> 


Ok, with the layout finished. Now finish with the binding. As I said, View should have an instance of ViewModel :

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


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 onCreate() method was called. Take a look at our activity:

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


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 class MainViewModel : ViewModel() { ... } 


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

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


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


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

MainViewModel.kt
 class MainViewModel : 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:


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"?> <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> 


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"?> <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> 



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

button.xml
 <Button android: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
 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(): 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() } } } 


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


Run the application


This is strange. What happened?


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 .

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:


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
 class MainViewModel : 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
 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. } } 


What does the word it mean? If a function has only one parameter, then this parameter can be accessed using the it keyword. 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?




...

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


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 need to know which client you are using to extract remote (remote) 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
 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>) } 


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


There is only one method that returns fake deleted data.

Now we can add some logic to our repository:

GitRepoRepository.kt
 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>) } 


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)
 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 return ni != null && ni.isConnected } } 


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

 <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" /> 

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


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
 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() { 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 constructor , from which we inherit.

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

 class MainViewModel(application: Application) : AndroidViewModel(application) { ... } 

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

GitRepoRepository.kt
 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>) } 


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.

Source: https://habr.com/ru/post/432826/


All Articles