Architecture is one of the important things in all types of development. Agree or disagree with my subjective opinion, you will feel the difference after you try to develop an application between using the architecture or not. Especially if you are working on a large project / product.
Here I will give a simple example (template) about using MVVM + Hilt when you build an android application. I know (perhaps) it’s not best practice, but I think it’s better practice if you don’t use architecture in previous development.
General Knowledge
MVVM (Model – View – ViewModel)
There are some people who still misunderstand about design patterns and architectural patterns, but here we will not discuss about that. Currently MVVM is the default pattern that appears when we create projects in android development. The main thing in MVVM is in the management of data flow management which is no longer executed at the view generation layer. Data management will be left to a separate viewModel class.
This is just a preface, for more details, you can read in Android Document Development
HILT
I will not chatter much, I will only quote from the official android developer page about hilt.
Hilt is a dependency injection library for Android that reduces the boilerplate of doing manual dependency injection in your project. Hilt provides a standard way to use DI in your application by providing containers for every Android class in your project and managing their lifecycles automatically. Hilt is built on top of the popular DI library Dagger to benefit from the compile-time correctness, runtime performance, scalability, and Android Studio support that Dagger provides.
For more information, see Hilt and Dagger.
Preparation
To using MVVM and Hilt, we need some related dependencies. So you can add this list into your gradle dependency:
1 |
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1' |
Afterward add this into root dependencies
1 2 3 4 5 6 7 8 9 10 |
// Hilt dependency implementation "com.google.dagger:hilt-android:2.38.1" kapt "com.google.dagger:hilt-compiler:2.38.1" // For ViewModel dependency implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' implementation 'androidx.navigation:navigation-fragment-ktx:2.4.2' // For parsing data i use Gson implementation 'com.google.code.gson:gson:2.8.6' |
And don’t forget to import plugin depndency on top root gradle file,
1 2 3 4 5 6 |
plugins { ... id 'kotlin-kapt' id 'dagger.hilt.android.plugin' ... } |
Implementation
Create our Base Project Configuration
On this step, we will providing base service that will call on next our repositories. Usualy on most examples, many developers including this on di folder, but for my self prefer using service for folder naming to this configuration. So whatever it is, let create new class with name BaseService.kt inside that folder like this,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
... @Module @InstallIn(SingletonComponent::class) object BaseService { /** * PROVIDING BASE URL * Basically we can directly write base url when providing Retrofit * But here i give example if we want to separate like this */ @Provides @Named("BASE_URL") fun provideBaseUrlOngkir(): String { return "https://yourbaseurl.com" } /** * Retrofit */ @Provides fun provideRetrofit( @Named("BASE_URL") url: String, client: OkHttpClient ): Retrofit { return Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create(Gson())) .client(client) .baseUrl(url).build() } /** * Providing our ApiService to call our service * we will setup ApiServices on next step */ @Provides fun provideApi(retrofit: Retrofit): ApiServices { return retrofit.create(ApiServices::class.java) } /** * Interceptor * We will use this set-up to providing OkHttpClient interceptor * to handling header and set-up other setting that we need */ @Provides fun createHttpClientOngkir(@ApplicationContext context: Context): OkHttpClient { synchronized(this) { val logging = HttpLoggingInterceptor() logging.level = HttpLoggingInterceptor.Level.Body // Use NONE if you want to disable loging return OkHttpClient.Builder() .connectTimeout(60, TimeUnit.SECONDS) // setup time out after 60 second .readTimeout(60, TimeUnit.SECONDS) .writeTimeout(60, TimeUnit.SECONDS) .addInterceptor(logging) .addInterceptor { chain: Interceptor.Chain -> // Here example for setting global header in our interceptor val original = chain.request() // If you using token, i recomended to not harcode here, but it is just for example // so this is just example val token = "blablabla" val requestBuilder = original.newBuilder() .addHeader("Authorization", "Bearer " + token) chain.proceed(requestBuilder.build()) }.build() } } } |
Next, we need to create ApiServices.kt to provide our service path. For the eample I just give a POST api call service with ResGlobal for global model. Why for global model? Yap! because usually for a service they have same structure for the json result.
1 2 3 4 5 6 7 8 9 |
..... interface ApiServices { @FormUrlEncoded @POST("yourPathUrl") suspend fun fetchUserData( @Field("userId") userId: Int? ): ResGlobal // This is our general model } |
And this is example for ResGlobal model
1 2 3 4 5 6 7 |
.... class ResGlobal { var status: Int? = 0 var message: String? = null var data: JsonElement? = null // Using JsonElement, we can parse object and array with same variable } |
Okkay, our base service is already done. For the next step, we will try to create ViewModel and repository, and call the view model on our class.
Implementation View Model & Repository
First, we need to create the repository that will be used on view model calss. So, create new class and give class came with UserRepository.kt. You can also give another name regarding to your needed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
... class UserRepository @Inject constructor( private val service: ApiServices, @ApplicationContext private val context: Context ) { suspend fun fetchUser(userId: Int): ResultApi<YourUserModel?, String?> = withContext(Dispatchers.IO) { try { val response = service.fetchUserData(userId) // For below condition, is beside on your data, this is just for the example if (response.status == 200) { // Here i using Gson for parsing the data val data = Gson().fromJson(response.data, YourUserModel::class.java) ResultApi(data, null) } else { ResultApi(null, "Message error, you also can get from api result") } } catch (e: Exception) { ResultApi(null, parseErrorMessage(e)) } } // This is function for parsing the error body if we get failure when reaching data // You can separate this function if you have much repositories class private fun parseErrorMessage(e: Exception): String? { return try { val err = (e as? HttpException)?.response()?.errorBody()?.string() if (err?.isEmpty() == true) "Error" else { // you can adjust it according to the data you use val message = Gson().fromJson(err, ResGlobal::class.java).message?.replace("err: ", "") return message } } catch (e: Exception) { "Please try again leter" } } } |
On above class, you can see ResultApi class for handling the result. Why I using that? We need confirmation on the UI(View) about the result is success or not. In this case, i want to showing the error message on the view side, so i bring error message if any and give that as null if the response is success. So this is for the ResultApi class that usually i use.
1 2 3 4 5 6 7 8 9 10 |
public data class ResultApi<out A, out B>( public val data: A, public val message: B ) : Serializable { /** * Returns string representation of the [Response] including its [data] and [message] values. */ public override fun toString(): String = "($data, $message)" } |
Our repository class is already done, next we will create the view model class. So create new class and call class with UserViewModel.kt.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@HiltViewModel class UserViewModel @Inject constructor(private val repo: UserRepository) : ViewModel() { private val userData: MutableLiveData<ResultApi<YourUserModel?, String?>> = MutableLiveData() val fetchUserListener: LiveData<ResultApi<YourUserModel?, String?>> = userData fun fetchuser(userId: Int) { viewModelScope.launch { userData.postValue( repo.fetchUser() ) } } } |
Yap, the view model is just a simple class. On this class, we execute function on the repository using viewModelScope that will execute class on repository asynchronously.
Setup View Model In View Class
On this step, we will setup view model on our view class. We will setup how to call the endpoint and manage the result regarding view model and repository that already prepare before. On this example,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
.... @AndroidEntryPoint class YourActivity : AppCompactActivity() { .... private val viewModelUser: UserViewModel by viewModels() .... override fun onCreate(savedInstanceState: Bundle?) { ..... // define listener for fetching user data // if you using fragment, you can use this, // viewModelUser.fetchUserListener.observe(viewLifecycleOwner) { viewModelUser.fetchUserListener.observe(this) { // Here we can check if the message is not empty, it means we failed when call the endpoint if (it.message != null) { showToast(it.message) showPupupPinLogin(it.message) return@observe } else { val data = it.data // Setup your view here, i suggest to create ne function to setup your view } } // Calling the endpoint viewModel.fetchuser(userId) .... } } |
Don’t forget to add @AndroidEntryPoint on the top our class. And for using hilt, dont forget to set hilt application and set that on the manifest file. For example here i will create class with name MyApp.kt
1 2 3 4 |
.... @HiltAndroidApp class MyApp: Application() { } |
And put class name on mainfest file
1 2 3 4 5 6 7 8 9 10 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="your package"> ... <application android:name=".MyApp" ... |
Now, just runing the app and Cheers 🥂
Give your comment if you have a hightlight with this article 🙌