Jetpack Compose: Displaying data in compose using MVVM - android

Need a bit of help on why data from viewmodel is not shown in the composable function MainContent. I tried to use MVVM style with coroutine but without DI which I think will be easier but somehow, I could not get it to work.
The viewmodel is working as the log.d is showing the correct data from server but somehow, I could not get it to display in
Text(text = viewModel.posts[it].phrase)
Any help will be greatly appreciated. The github link for this program is in https://github.com/somaria/LearnChnCompose
package com.gamecrawl.learnchncompose
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.ViewModel
import com.gamecrawl.learnchncompose.ui.theme.LearnChnComposeTheme
import io.ktor.client.*
import io.ktor.client.engine.android.*
import io.ktor.client.features.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.features.logging.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel: MainViewModel by viewModels()
setContent {
LearnChnComposeTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
MainContent(viewModel)
}
}
}
}
}
class MainViewModel : ViewModel() {
private var _posts = mutableListOf(Post("12", "test phrase", true))
var posts get() = _posts; set(value) {
_posts = value
}
init {
CoroutineScope(Dispatchers.IO).launch {
_posts = KtorClient.httpClient.get("https://learnchn.herokuapp.com/") {
header("Content-Type", "application/json")
}
Log.d("HomeViewModel", "init: ${_posts[1].phrase}")
Log.d("HomeViewModel", "init: ${_posts[1].id}")
}
}
fun addPost(post: Post) {
CoroutineScope(Dispatchers.IO).launch {
val addedpost: Post = KtorClient.httpClient.post("https://learnchn.herokuapp.com/add") {
header("Content-Type", "application/json")
body = post
}
}
}
}
#Composable
fun MainContent(viewModel: MainViewModel) {
Column {
LazyColumn {
items(viewModel.posts.size) {
Text(text = viewModel.posts[it].phrase)
}
}
Button(onClick = {
viewModel.addPost(Post("test", "adding post 222", true))
}) {
Text(text = "Add Post")
}
}
}
#Serializable
data class Post(
val id: String,
val phrase: String,
val published: Boolean
)
object KtorClient {
val json = Json {
encodeDefaults = true
ignoreUnknownKeys = true
isLenient = true
}
val httpClient = HttpClient(Android) {
install(HttpTimeout) {
socketTimeoutMillis = 200000
requestTimeoutMillis = 200000
connectTimeoutMillis = 200000
}
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
Log.d("TAG", "log: $message")
}
}
}
install(JsonFeature) {
serializer = KotlinxSerializer(json)
}
defaultRequest {
contentType(ContentType.Application.Json)
accept(ContentType.Application.Json)
}
}
}

The data type of the posts is a MutableList<Post>. This means that changes to this variable will not cause the function to recompose. When the UI is loaded, then the variable does not have any data, since you fetch the data in an asynchronous coroutine. However, when the variable is updated, the UI is not recomposed.
To fix this issue, you must declare _posts to be a MutableState<List<Post>> from the compose library instead. Reconfigure your ViewModel in the following way:
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
class MainViewModel : ViewModel() {
private val _posts = mutableStateOf(listOf<Post>()) // <- requires init value
val posts: State<List<Post>> = _posts // <- keep both variables immutable 'val'
/* always expose the immutable form of State */
init {
CoroutineScope(Dispatchers.IO).launch {
/* _posts.value is used now due to the datatype change */
_posts.value = KtorClient.httpClient.get("https://learnchn.herokuapp.com/") {
header("Content-Type", "application/json")
}
Log.d("HomeViewModel", "init: ${_posts.value[1].phrase}")
Log.d("HomeViewModel", "init: ${_posts.value[1].id}")
}
}
fun addPost(post: Post) {
CoroutineScope(Dispatchers.IO).launch {
val addedpost: Post = KtorClient.httpClient.post("https://learnchn.herokuapp.com/add") {
header("Content-Type", "application/json")
body = post
}
}
}
}
Now since your public posts variable is of type State<T>, you need to make changes to your composable function:
#Composable
fun MainContent(viewModel: MainViewModel) {
val posts = viewModel.posts.value // <- grab the value of the state variable.
/* The function will recompose whenever there's a change in posts */
Column {
LazyColumn {
items(posts.size) {
Text(text = posts[it].phrase)
}
}
Button(onClick = {
viewModel.addPost(Post("test", "adding post 222", true))
}) {
Text(text = "Add Post")
}
}
}
This should help your issue.

Related

Android Studio Login Token Request Error using Retrofit and SharedPreferences

A token is sent to the client when I login, so I'm going to use the retrofit library to take the token and include it in the header of the request and send the data to the server.
When I login, I want to save data through the SharedPreferences library to store tokens delivered to the client locally.
But there is error :
FATAL EXCEPTION: main
Process: com.example.todo_android, PID: 7323
java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.SharedPreferences android.content.Context.getSharedPreferences(java.lang.String, int)' on a null object reference
at android.content.ContextWrapper.getSharedPreferences(ContextWrapper.java:217)
at com.example.todo_android.MainActivity.saveData(MainActivity.kt:30)
at com.example.todo_android.Screen.LoginScreenKt$sendLogin$1.onResponse(LoginScreen.kt:64)
at retrofit2.DefaultCallAdapterFactory$ExecutorCallbackCall$1$1.run(DefaultCallAdapterFactory.java:83)
at android.os.Handler.handleCallback(Handler.java:942)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7872)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
Actually When I login only with body json without Header and SharedPreferences, its working and it gives me token and resultCode.
resultCode : 200
token : e91389ca537f481d1937a43c49da0d5b827e5cfd
There is code:
Login Model
package com.example.todo_android.Data.Profile
data class Login(
val email: String,
val password: String
)
LoginRequest
package com.example.todo_android.Request.ProfileRequest
import com.example.todo_android.Data.Profile.Login
import com.example.todo_android.Response.ProfileResponse.LoginResponse
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST
interface LoginRequest {
#POST("/account/login/")
fun requestLogin(
// #Header("Authorization") token: String,
#Body loginRequest: Login
) : Call<LoginResponse>
}
LoginResponse
package com.example.todo_android.Response.ProfileResponse
import com.google.gson.annotations.SerializedName
data class LoginResponse(
#SerializedName("resultCode")
val resultCode: String,
#SerializedName("token")
val token: String
)
**LoginScreen**
package com.example.todo_android.Screen
import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import com.example.todo_android.Data.Profile.Login
import com.example.todo_android.MainActivity
import com.example.todo_android.Navigation.Action.RouteAction
import com.example.todo_android.Navigation.NAV_ROUTE
import com.example.todo_android.R
import com.example.todo_android.Request.ProfileRequest.LoginRequest
import com.example.todo_android.Response.ProfileResponse.LoginResponse
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
fun goCalendar(route: NAV_ROUTE, routeAction: RouteAction) {
routeAction.navTo(route)
}
#ExperimentalMaterial3Api
fun sendLogin(email: String, password: String, routeAction: RouteAction) {
var loginResponse: LoginResponse? = null
var retrofit = Retrofit.Builder()
.baseUrl("https://plotustodo-ctzhc.run.goorm.io/")
.addConverterFactory(GsonConverterFactory.create())
.build()
var loginRequest: LoginRequest = retrofit.create(LoginRequest::class.java)
loginRequest.requestLogin(Login(email, password)).enqueue(object : Callback<LoginResponse> {
//실패할 경우
override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
Log.e("LOGIN", t.message.toString())
}
//성공할 경우
override fun onResponse(call: Call<LoginResponse>, response: Response<LoginResponse>) {
loginResponse = response.body()
when (loginResponse?.resultCode) {
"200" -> {
goCalendar(NAV_ROUTE.CALENDAR, routeAction)
Log.d("LOGIN", "resultCode : " + loginResponse?.resultCode)
Log.d("LOGIN", "token : " + loginResponse?.token)
Log.d("LOGIN", "메인 화면으로 갑니다.")
MainActivity().saveData(loginResponse?.token.toString())
}
"500" -> {
Log.d("LOGIN", "non_field_errors:[Check Your Email or Password]")
}
}
}
})
}
#ExperimentalMaterial3Api
#Composable
fun LoginScreen(routeAction: RouteAction) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
)
{
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
TextField(
modifier = Modifier.width(300.dp),
value = email,
colors = TextFieldDefaults.textFieldColors(
Color(0xff9E9E9E),
disabledLabelColor = Color(0xff9E9E9E),
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
shape = RoundedCornerShape(20.dp),
onValueChange = {
email = it
})
Spacer(modifier = Modifier.height(30.dp))
TextField(
modifier = Modifier.width(300.dp),
value = password,
colors = TextFieldDefaults.textFieldColors(
Color(0xff9E9E9E),
disabledLabelColor = Color(0xffE9E9E9),
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
shape = RoundedCornerShape(20.dp),
onValueChange = {
password = it
})
Spacer(modifier = Modifier.height(60.dp))
Button(
modifier = Modifier
.width(300.dp)
.height(50.dp),
colors = ButtonDefaults.buttonColors(Color(0xffFFBE3C7)),
onClick = { sendLogin(email, password, routeAction) }
) {
Text(text = stringResource(id = R.string.login), color = Color.Black)
}
}
}
MainActivity
package com.example.todo_android
import android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.ExperimentalMaterial3Api
import com.example.todo_android.Navigation.NavigationGraph
import com.example.todo_android.ui.theme.TodoandroidTheme
#ExperimentalMaterial3Api
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TodoandroidTheme {
NavigationGraph()
}
}
loadData()
}
fun loadData() {
val pref = getSharedPreferences("UserTokenKey", Context.MODE_PRIVATE) // UserTokenKey: 파일명
val token = pref.getString("Token", "")
}
fun saveData(token: String) {
val pref = getSharedPreferences("UserTokenKey", Context.MODE_PRIVATE) // UserTokenKey: 파일명
val edit = pref.edit()
edit.putString("Token", token) // "Token"라는 이름을 사용하여 token값을 입력한다.
edit.commit()
}
}
I would appreciate it if you could tell me which part was implemented incorrectly and how to correct it.
As stated in the log, the exact problem is:
java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.SharedPreferences android.content.Context.getSharedPreferences(java.lang.String, int)' on a null object reference
at android.content.ContextWrapper.getSharedPreferences(ContextWrapper.java:217)
at com.example.todo_android.MainActivity.saveData(MainActivity.kt:30)
at com.example.todo_android.Screen.LoginScreenKt$sendLogin$1.onResponse(LoginScreen.kt:64)
which points to this line
MainActivity().saveData(loginResponse?.token.toString())
You cannot instantiate a new MainActivity class in order to call the function saveData(). The Context used to call getSharedPreferences() will be null if your MainActivity is created like that.
So you can try if the following options address the problem:
Make your saveData() as a callback function, pass it to the necessary class to call
Pass Context to the class you need to call getSharedPreferences(), and call it like Context.getSharedPreferences()

Coroutines test with firebase hanging forever

I have made a simple unit test which tests a coroutines function which uses firebase.
I've mocked all the dependencies and the methods being used in this function I'm testing, but it continues to hang. I'm not sure exactly where it's hanging, but I would assume on the mocking of firestore, and it has await().
Test Class:
import android.content.Context
import com.example.socialtoker.data.db.UserDao
import com.example.socialtoker.data.repository.UserDataRepository
import com.example.socialtoker.data.repository.UserDataRepositoryImpl
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.DatabaseReference
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.storage.FirebaseStorage
import io.mockk.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
#RunWith(JUnit4::class)
#ExperimentalCoroutinesApi
class UserDataRepositoryImplTest {
private val contextMock = mockk<Context>{
every { getExternalFilesDir(any())?.absolutePath } returns "src/SocialToker/"
}
private val firestoreMock = mockk<FirebaseFirestore>{
coEvery { collection("Users").document(any()).set(any()).await() } returns mockk()
}
private val firebaseAuthMock = mockk<FirebaseAuth>{
coEvery { createUserWithEmailAndPassword(any(), any()) } returns mockk()
every { currentUser?.uid } returns "UID"
}
private val firebaseStorageMock = mockk<FirebaseStorage>()
private val firebaseDatabaseMock = mockk<DatabaseReference>()
private val daoMock = mockk<UserDao>{
coEvery { addUser(any()) } returns mockk()
}
private lateinit var userDateRepository: UserDataRepository
private val emailAddress = "socialtoker#socialtoker.com"
private val password = "socialtokerpassword"
private val username = "socialtoker"
#Before
fun setup() {
userDateRepository = UserDataRepositoryImpl(
contextMock,
firestoreMock,
firebaseAuthMock,
firebaseStorageMock,
firebaseDatabaseMock,
daoMock
)
}
#Test
fun `createUser calls firebase and stores user info locally and remotely`() = runBlocking {
// WHEN
userDateRepository.createUser(emailAddress, password, username)
//THEN
coVerify { firebaseAuthMock.createUserWithEmailAndPassword(emailAddress, password) }
}
}
Test Subject:
override suspend fun createUser(email: String, password: String, username: String): AuthResult {
try {
val data = hashMapOf(
"name" to username
)
val authResult = firebaseAuth.createUserWithEmailAndPassword(email, password).await()
val uid = firebaseAuth.currentUser!!.uid
userDao.addUser(UserData(uid, username, "", ""))
firestoreRef.collection("Users")
.document(uid)
.set(data).await()
return authResult
} catch (error: Throwable) {
throw RepositoryError(
error.localizedMessage ?: "Unable to create user", error
)
}
}
Please note that await is an extension function on Task class.
Therefore Mocking extension functions might need to be taken into consideration.

How to create list view/grid view/recycler view with retrofit API in jetpack compose

I have created Kotlin Code for parsing APIs with retrofit in list view/grid view/recycler view, I wanted to know how can I do the same using jetpack compose? I have used retrofit to parse GET API responses using different ViewGroups. View Binding is used to interact with the views on this screen.
Code
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.util.Log
import android.view.*
import android.view.inputmethod.EditorInfo
import android.widget.*
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import okhttp3.ResponseBody
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import retrofit.Retrofit2
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import supports.*
import viewmodel.SIViewModel
import java.io.IOException
import java.net.SocketTimeoutException
import java.util.*
class TestIndex : AppCompatActivity() {
var adapter: Adapter1? = null
var dialog: AlertDialog? = null
var builder: AlertDialog.Builder? = null
private val viewModel: SIViewModel? by viewModels()
var test_arr = ArrayList<TestModel>()
var binding: TestGridBinding? = null
#SuppressLint("CommitPrefEdits", "ClickableViewAccessibility", "SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.test_grid)
setSupportActionBar(binding?.view?.toolbarr)
supportActionBar!!.elevation = 0f
viewModel
adapter = Adapter1(this#TestIndex, R.layout.row, test_arr)
//binding ViewModel retrofit API with activity, here ID1 and ID2 coming from the previous screen.
viewModel!!.getList(this#TestIndex , ID1!!, ID2!!)
binding?.gvTest?.adapter = adapter
binding?.swipeRefreshLayout?.setOnRefreshListener {
binding?.swipeRefreshLayout?.isRefreshing = true
if (ID1 != null && ID2 != null) {
// getting same server response on swipe refresh widget
getdata(ID1!!, ID2!!)
} else {
builder = AlertDialog.Builder(MyApplication.instance)
builder!!.setCancelable(false)
builder!!.setTitle("Alert")
builder!!.setNegativeButton("Cancel") { dialog: DialogInterface, which: Int ->
dialog.dismiss()
finish()
}
builder!!.setPositiveButton("OK") { dialog: DialogInterface, which: Int -> dialog.dismiss() }
dialog = builder!!.create()
dialog?.show()
}
}
subscribeObservers()
}
//this is checked on the dev portal but I don't know I could I use it //dynamically with adapters and ArrayList.
#Composable
fun LazyRowItemsDemo() {
LazyRow {
items((1..title_arr.size).toList()) {
Text(text = "Item $it")
}
}
}
private fun getdata(id1: String, id2: String) {
val mProgressDialog = ProgressDialog(this#TestIndex)
mProgressDialog.isIndeterminate = true
mProgressDialog.setMessage(Keys.KEY_pre_msg)
if (!this.isFinishing) {
mProgressDialog.show()
}
val retrofit = Retrofit.Builder()
.baseUrl(Keys.testURL)
.client(OkHttpClient().build())
.addConverterFactory(GsonConverterFactory.create())
.build()
val retrofitInterface = retrofit.create(
RetrofitInterface::class.java
)
val call = retrofitInterface.getTestdata(id1, id2)
call!!.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(call: Call<ResponseBody?>, response: Response<ResponseBody?>) {
var remoteResponse: String? = null
if (response.code() == 200) {
try {
assert(response.body() != null)
remoteResponse = response.body()!!.string()
} catch (e: Exception) {
e.printStackTrace()
}
} else {
try {
if (response.errorBody() != null) {
remoteResponse = response.errorBody()!!.string()
}
} catch (e: IOException) {
e.printStackTrace()
}
}
if (remoteResponse != null) {
//getting response fields and parsing list view or grid view/recycler view in different screens
adapter =
Adapter1(this#TestIndex, R.layout.row, test_arr)
binding!!.gvTest.adapter = adapter
adapter!!.notifyDataSetChanged()
}
}
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
Log.d(Keys.KEY_TAG, "onFailure: " + t.localizedMessage)
}
})
if (mProgressDialog.isShowing) mProgressDialog.dismiss()
}
//subscribed the Observers here from view model
private fun subscribeObservers() {
viewModel!!.lifting.observe(this, { TestModel: List<TestModel>? ->
adapter!!.updateTests(TestModel)
binding!!.swipeRefreshLayout.isRefreshing = false
}
}
Kindly let me know how can I do the same using jetpack compose for listview, grid view, recycler view. Thanks.
It's more a general example, without retrofit. You can implement your data fetch inside my getTestData method.
To begin with, in order to understand the basic principles of working with Compose, I suggest you study compose tutorials.
Compose uses view models to perform complex data manipulations. I will use the basic version, but you can also check out Hilt for more complex architecture.
In order for changing the state of an object to lead to a recomposition, you can use:
The mutableStateObject - this is a specially created container for compose that will update the view if the value has changed
you can also use LiveData and Flow, they can both be cast to mutableStateObject.
Note that mutableStateObject will not alert you to changes in the container object fields if you pass a complex class there. It will only notify you when the value itself changes, so it is recommended to use it only for simple types.
You can also use mutableStateListOf to store collections. In my example you will see both: with mutableStateListOf it is convenient to add/delete objects to the collection, while mutableStateObject with List lying inside is easier to completely replace with new objects.
Inside Composable functions you need to wrap your mutable state objects with remember to prevent reinitializing them on each composition, and inside your view model you don't need to do that, because it's not gonna be reinitialized in any case.
SwipeRefresh is not part of compose, it's a library made by compose maintainers too. To install it follow this instructions.
I'm using two columns here just to show difference between mutableStateOf and mutableStateListOf, you can remove Row and one of LazyColumn
class ScreenViewModel : ViewModel() {
var list by mutableStateOf(emptyList<String>())
var mutableList = mutableStateListOf<String>()
var isRefreshing by mutableStateOf(false)
init {
refresh()
}
fun refresh() {
isRefreshing = true
viewModelScope.launch {
list = getTestData()
mutableList.addAll(0, list)
isRefreshing = false
}
}
suspend fun getTestData(): List<String> {
// emulate network call
delay(1000)
return List(100) {
Random.nextInt(100).toString()
}
}
}
#Composable
fun TestView() {
val viewModel: ScreenViewModel = viewModel()
SwipeRefresh(
state = rememberSwipeRefreshState(viewModel.isRefreshing),
onRefresh = {
viewModel.refresh()
},
) {
Row {
LazyColumn(
modifier = Modifier.weight(1f) // this needed inly for two columns case
) {
itemsIndexed(viewModel.list) { i, item ->
Text("$i $item")
}
}
LazyColumn(
modifier = Modifier.weight(1f) // this needed inly for two columns case
) {
itemsIndexed(viewModel.mutableList) { i, item ->
Text("$i $item")
}
}
}
}
}
Result:

Can't import extension function in Android App Kotlin

Running into an Issue early on in importing a declared extension function in another Kotlin File (Extensions.kt), call the extension function From another class (ForecastsRepository.kt) it doesn't compile but when i remove it there is no problem with the build. Obviously I need it and wonder why importing it would become an issue .
Here is the class:
import com.benmohammad.climatemvvm.base.Success
import com.benmohammad.climatemvvm.custom.errors.ErrorHandler
import com.benmohammad.climatemvvm.custom.errors.NoDataException
import com.benmohammad.climatemvvm.custom.errors.NoResponseException
import com.benmohammad.climatemvvm.entitymappers.forecasts.ForecastMapper
import com.benmohammad.climatemvvm.features.home.di.HomeScope
import com.benmohammad.climatemvvm.network.api.OpenWeatherApi
import com.benmohammad.climatemvvm.network.response.ErrorResponse
import com.benmohammad.climatemvvm.room.dao.forecasts.ForecastDao
import com.benmohammad.climatemvvm.room.dao.utils.StringKeyValueDao
import com.benmohammad.climatemvvm.room.models.forecasts.DbForecast
import com.benmohammad.climatemvvm.utils.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
import javax.inject.Inject
import com.benmohammad.climatemvvm.extensions.applyCommonSideEffects//import
#HomeScope
class ForecastsRepository #Inject constructor(
private val openWeatherApi: OpenWeatherApi,
private val forecastDao: ForecastDao,
private val stringKeyValueDao: StringKeyValueDao
) {
private val forecastCacheThresholdMillis = 3 * 3600000L //3 hours//
fun getForecasts(cityId: Int) = flow {
stringKeyValueDao.get(Utils.LAST_FORECASTS_API_CALL_TIMESTAMP)
?.takeIf { !Utils.shouldCallApi(it.value, forecastCacheThresholdMillis) }
?.let { emit(getDataOrError(NoDataException())) }
?: emit((getForecastFromAPI(cityId)))
}
//.applyCommonSideEffects()
.catch {
emit(getDataOrError(it))
}
private suspend fun getForecastFromAPI(cityId: Int) = openWeatherApi.getWeatherForecast(cityId)
.run {
if (isSuccessful && body() != null) {
stringKeyValueDao.insert(
Utils.getCurrentTimeKeyValuePair(Utils.LAST_FORECASTS_API_CALL_TIMESTAMP)
)
forecastDao.deleteAllAndInsert(ForecastMapper(body()!!).map())
getDataOrError(NoDataException())
} else {
Error(
NoResponseException(
ErrorHandler.parseError<ErrorResponse>(errorBody())?.message
)
)
}
}
private suspend fun getDataOrError(throwable: Throwable) =
forecastDao.get()
?.let { dbValue -> Success(getForecastList(dbValue)) }
?: Error(throwable)
private suspend fun getForecastList(dbForecast: DbForecast) = withContext(Dispatchers.Default) {
dbForecast.list.map { it.forecast }
}
}
and here is the file for the Extension functions:
package com.benmohammad.climatemvvm.extensions
import com.benmohammad.climatemvvm.base.Progress
import com.benmohammad.climatemvvm.base.Result
import com.benmohammad.climatemvvm.utils.Utils
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.retryWhen
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.Request
import retrofit2.Retrofit
import java.io.IOException
fun String.capitalizeWords(): String = this.split(' ').joinToString(" "){it.capitalize()}
#PublishedApi
internal inline fun Retrofit.Builder.callFactory(crossinline body: (Request) -> Call) =
callFactory(object: Call.Factory {
override fun newCall(request: Request): Call = body(request)
})
#Suppress("NOTHING_TO_INLINE")
inline fun Retrofit.Builder.delegatingCallFactory(delegate: dagger.Lazy<OkHttpClient>): Retrofit.Builder =
callFactory {
delegate.get().newCall(it) }
fun < T: Any> Flow<Result<T>>.applyCommonSideEffects() = //<<-----------T H I S Function!!!!HERE
retryWhen { cause, attempt ->
when {
(cause is IOException && attempt < Utils.MAX_RETRIES) -> {
delay(Utils.getBackOffDelay(attempt))
true
}
else -> {
false
}
}
}
.onStart { emit(Progress(isLoading = true)) }
.onCompletion { emit(Progress(isLoading = false)) }
fun Job?.cancelIfActive() {
if(this?.isActive == true) {
cancel()
}
}
as it doesnt compile it leads me to think the bug is deeper.
the IDE also hunderlines the function call stating it is "Unresolved reference"
GitHub Repo
Thanks any advice appreciated.

Get the value not the unit in try catch block in Kotlin using Android

Android kotlin coroutine retrofit.
I want to get the value from the getPropeties to insert it in database. I Need a help for this? I need the value to be an instance of User not the unit value. My viewModel class is given below.
import android.app.Application
import android.content.Context
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.android.marsrealestate.database.AppDatabase
import com.example.android.marsrealestate.database.User
import com.example.android.marsrealestate.database.UserDao
import com.example.android.marsrealestate.network.UsersApi
import com.example.android.marsrealestate.network.UsersProperty
import kotlinx.coroutines.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class OverviewViewModel(val database: UserDao,
application: Application): ViewModel() {
private var viewModelJob = Job()
private val coroutineScope = CoroutineScope(
viewModelJob + Dispatchers.Main )
private var user = MutableLiveData<User?>()
// The internal MutableLiveData String that stores the most recent response
private val _response = MutableLiveData<String>()
// The external immutable LiveData for the response String
val response: LiveData<String>
get() = _response
init {
getUsersProperties()
}
private fun getUsersProperties(){
coroutineScope.launch {
var getPropertiesDeferred =
UsersApi.retrofitService.getProperties()
try {
var listResult = getPropertiesDeferred.await()
//database.insertUser(listResult)
_response.value =
"Success: ${listResult} Mars properties retrieved"
} catch (e: Exception) {
_response.value = "Failure: ${e.message}"
}
}
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
Thanks
You are using launch,
Launch is used to perform asynchronous fire and forget type of
operations where you are not interested in the result of operation.
Instead, you can use async,
Async is used to perform asynchronous computation where you expect a
result of the computation in the future
private fun getUsersProperties() =
coroutineScope.async {
var getPropertiesDeferred =
UsersApi.retrofitService.getProperties()
try {
var listResult = getPropertiesDeferred.await()
//database.insertUser(listResult)
_response.value =
"Success: ${listResult} Mars properties retrieved"
} catch (e: Exception) {
_response.value = "Failure: ${e.message}"
}
// =================================================
// ========= Return whatever result you want =======
// =================================================
}
can you also show what is the type signature of getProperties?

Categories

Resources