androidretrofit2android-viewmodeldagger-hiltandroid-mvvm

Why network call is not triggered while using retrofit with jetpack compose mvvvm


I have an endpoint

https://api.openweathermap.org/data/2.5/weather?q=sukkur,pk&appid=5*****************c

I want to use retrofit to make call to this end point

interface WeatherApiService {

    @GET(K.CURRENT_WEATHER_URL)
    suspend fun getCurrentWeather(
        @Query(ApiParameters.Q) city: String,
        @Query(ApiParameters.APP_ID) appId: String = K.API_KEY,
        @Query(ApiParameters.UNITS) units: String = K.METRIC
    ): CurrentCondition
}

and I have

interface CurrentConditionRepository {
    suspend fun getCurrentCondition(city: String): Flow<Response<CurrentConditionDto>>
}

and

class CurrentConditionRepositoryImpl @Inject constructor(
    private val apiService: WeatherApiService
): CurrentConditionRepository {
    override suspend fun getCurrentCondition(
        city: String
    ): Flow<Response<CurrentConditionDto>> = flow {
        // Loading state
        emit(Response.Loading)

        // Get current condition data from API
        val currentConditionData = apiService.getCurrentWeather(city)

        // Convert current condition data to DTO
        val currentConditionDto = currentConditionData.toDto()

        // Success state
        emit(Response.Success(currentConditionDto))
    }.catch { e ->
        // Error state
        e.printStackTrace()
        emit(Response.Error(e.message.orEmpty()))
    }
}

I have

class GetCurrentConditionUseCase(
    private val repository: CurrentConditionRepository
) {
    suspend operator fun invoke(city: String) = repository.getCurrentCondition(city)
}

in the end in view model I have

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val getCurrentConditionUseCase: GetCurrentConditionUseCase
): ViewModel(){

    var getCurrentConditionWithState: StateFlow<Response<CurrentConditionDto>> = MutableStateFlow(Response.Loading)

    fun getCurrentCondition(city: String){
        viewModelScope.launch {
            Log.e("HomeViewModel", "getCurrentCondition()")
            getCurrentConditionWithState = getCurrentConditionUseCase.invoke(city)
                .stateIn(
                    scope = viewModelScope,
                    started = SharingStarted.WhileSubscribed(),
                    initialValue = Response.Loading
                )
        }
    }

}

}

all the Hilt DI is

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    private val json = Json {
        coerceInputValues = true
        ignoreUnknownKeys = true
    }

    @Provides
    @Singleton
    fun provideApi(
        builder: Retrofit.Builder,
    ): WeatherApiService {
        return builder
            .build()
            .create(WeatherApiService::class.java)
    }

    @Provides
    @Singleton
    fun provideRetrofitBuilder(): Retrofit.Builder {
        return Retrofit.Builder().baseUrl(K.API_BASE_URL)
            .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))

    }

    @Provides
    @Singleton
    fun providesCurrentConditionRepository(
        apiService: WeatherApiService
    ): CurrentConditionRepository {
        return CurrentConditionRepositoryImpl(apiService)
    }

    @Provides
    @Singleton
    fun providesCurrentConditionUseCase(
        repository: CurrentConditionRepository
    ): GetCurrentConditionUseCase {
        return GetCurrentConditionUseCase(repository)
    }

}

when I use all that in

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    private val viewModel: HomeViewModel by viewModels()
    private val TAG = "MainActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            NimbleWeatherTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Column(
                        modifier = Modifier
                            .padding(innerPadding)
                    ) {
                        CurrentConditionScreen(viewModel, Modifier.padding(innerPadding))
                    }
                }
            }
        }
    }
}

@Composable
fun CurrentConditionScreen(viewModel: HomeViewModel, modifier: Modifier = Modifier) {
    val currentConditionState by viewModel.getCurrentConditionWithState.collectAsState(initial = Response.Loading)

    Column(modifier = modifier) {
        Button(onClick = { viewModel.getCurrentCondition("sukkur") }) {
            Text(text = "Search for Sukkur")
        }

        // Display the current condition based on the state
        when (currentConditionState) {
            is Response.Loading -> Text("Loading...")
            is Response.Success -> {
                val currentCondition = (currentConditionState as Response.Success<CurrentConditionDto>).data
                Log.e("CURRENT_CONDITION", currentCondition.name)
                Text("Current Condition: ${currentCondition.name}")
            }
            is Response.Error -> {
                val error = (currentConditionState as Response.Error).message
                Text("Error: $error")
            }
        }
    }
}

only Loading is shown and no any network call is made

Update

I have shared the project here on GitHub

Kindly have a look and find the issue please.


Solution

  • Your getCurrentCondition is essentially a one-shot operation and is supposed to return a single value, not a Flow. Considering that, a possible implementation might look like this:

    HomeViewModel:

        private val _currentConditionState =
            MutableStateFlow<Response<CurrentConditionDto>>(Response.Loading)
        val getCurrentConditionWithState: StateFlow<Response<CurrentConditionDto>> =
            _currentConditionState
    
        fun getCurrentCondition(city: String) {
            viewModelScope.launch {
                _currentConditionState.value = Response.Loading
                _currentConditionState.value = getCurrentConditionUseCase.invoke(city)
            }
        }
    

    CurrentConditionRepository:

    interface CurrentConditionRepository {
        suspend fun getCurrentCondition(city: String): Response<CurrentConditionDto>
    }
    

    CurrentConditionRepositoryImpl.getCurrentCondition:

    override suspend fun getCurrentCondition(
        city: String
    ): Response<CurrentConditionDto> {
        // Get current condition data from API
        val currentConditionData = try {
            apiService.getCurrentWeather(city)
        } catch (e: Exception) {
            return Response.Error(e.message.orEmpty())
        }
        // Convert current condition data to DTO
        val currentConditionDto = currentConditionData.toDto()
        // Success state
        return Response.Success(currentConditionDto)
    }