Jetpack Compose: UnsupportedOperationException when adding Entries to MPAndroidCharts dynamically - android-view

I try to display live data in an MPAndroidChart hosted in an AndroidView.
I get the graph but an UnsupportedOperationException happens when I call addEntry() to update the graph dynamically. Am I doing something wrong?
You find a demo repo in the comments.
#Composable
fun MyLineChart() {
val mainViewModel = viewModel()
val sensorData = mainViewModel.sensorFlow.collectAsState(SensorModel(0F,0F)).value
AndroidView(modifier = Modifier.fillMaxSize(),
factory = { context ->
val lineChart = LineChart(context)
var entries = listOf(Entry(1f,1f))
val dataSet = LineDataSet(entries, "Label").apply { color = Color.Red.toArgb() }
val lineData = LineData(dataSet)
lineChart.data = lineData
lineChart.invalidate()
lineChart
}){
try {
Log.d("TAG", "MyLineChart: Update --- Current thread id: ${Thread.currentThread()}")
it.data.dataSets[0].addEntry(Entry(sensorData.x, sensorData.y))
it.lineData.notifyDataChanged()
it.notifyDataSetChanged()
it.invalidate()
} catch(ex: Exception) {
Log.d("TAG", "MyLineChart: $ex")
}
}
}
The data is sent to the view via the following ViewModel:
#HiltViewModel
class MainViewModel #Inject constructor(#ApplicationContext var appContext: Context) : ViewModel() {
private var rand: Random = Random(1)
val sensorFlow: Flow<SensorModel> = flow<SensorModel> {
while (true) {
delay(1000L)
Log.d("TAG", "sensorFlow: Current thread id: ${Thread.currentThread()}")
emit(SensorModel(rand.nextFloat(), rand.nextFloat()))
}
}
}

You pass entries to LineDataSet, which is an immutable list.
This library seems to have a pretty bad API, because it doesn't ask for a modifiable list as a parameter, but at the same time it doesn't make it modifiable on its side. This causes you to try to modify the immutable list, which leads to an exception.
Replace
var entries = listOf(Entry(1f,1f))
with
val entries = mutableListOf(Entry(1f,1f))
p.s. I can't advise you another graph library as I haven't worked with any, but I would advise you to look for a library with a better API.

try this code:
// it.data.dataSets[0].addEntry(Entry(sensorData.x, sensorData.y))
val entryList = mutableListOf<Entry>()
entryList.add(Entry(sensorData.x, sensorData.y))
val dataSet = LineDataSet(entryList, "Label").apply {
color = Color.Red.toArgb()
}
it.data = LineData(dataSet)

Related

How to use a same instance of hiltViewModel among nested composable?

I have a composable function named 'Page' as a basic composable to hold NavHost for my app, please see architecture below:
#Composable
fun Page(viewModel: LdvToolViewModel = hiltViewModel(), scaffoldState: ScaffoldState, navController: NavHostController){
val statusBarMode = viewModel.statusBarUiState
val uiController = rememberSystemUiController()
LaunchedEffect(statusBarMode){
uiController.run {
if(statusBarMode.isDarkContent){
setStatusBarColor(color = Color.White, darkIcons = true)
}else{
setStatusBarColor(color = LdvOrange, darkIcons = false)
}
}
}
val navBuilder: NavGraphBuilder.() -> Unit = {
composable(LdvPages.SEARCHING.name) { SearchUi(viewModel, scaffoldState = scaffoldState) }
composable(LdvPages.ERROR.name) { ErrorUi(viewModel,scaffoldState = scaffoldState) }
composable(LdvPages.PANEL.name) { PanelUi(scaffoldState,viewModel, mBaseViewModel) }
composable(LdvPages.PrivacyPolicy.name){ PrivacyPolicy(scaffoldState)}
composable(LdvPages.TermsOfUse.name){ TermsOfUse(scaffoldState)}
composable(LdvPages.OpenSourceLicense.name){ OpenSourceLicense(scaffoldState)}
composable(LdvPages.DebugPage.name){ DebugPage(viewModel)}
}
val start by derivedStateOf {
if (...){
LdvPages.PANEL.name }else if(...){
LdvPages.ERROR.name
}else{LdvPages.SEARCHING.name}
}
NavHost(navController = navController, startDestination = start, builder = navBuilder)
if(!isNfcEnable){
viewModel.setNfcDisableContent()
ErrorDialog(viewModel = viewModel){
startActivity(Intent(Settings.ACTION_NFC_SETTINGS));
}
}
}
As you can see that 'LdvToolViewModel' has been injected to 'Page' as hiltViewModel. To keep 'LdvToolViewModel' as one instance among lifecycles of nested-composable functions in navBuilder, I have to pass it as parameter to those functions. Is there any better way like I can somehow inject 'LdvToolViewModel' in those functions as hiltViewModel and meanwhile I can still have the injected hiltViewModel as a same instance?
Imagine you have a "HomeGraph", with "Home" as a parent destination, and few destination screens that should share the same ViewModel instance.
First get a NavBackStackEntry, by passing your parent route
val parentEntry: NavBackStackEntry = remember(navBackStackEntry) {
navController.getBackStackEntry(Destination.HomeGraph.route)
}
Then get an instance of a ViewModel by passing the parent NavBackStackEntry
val userViewModel = hiltViewModel<HomeViewModel>(parentEntry)
Also, remember that if you navigate to Destination.HomeGraph.route either from nested navigation or from a different graph a new instance of ViewModel will be created, so if you navigate within a single graph, navigate to startDestination e.g Destination.Home.route - this way you will keep the same ViewModel instance.
I don't thing we have a well-defined ViewModel sharing in compose as we had with a view system e.g by activityViewModels(), but keeping ViewModel state in graphs while user is not accessing them is a bad practice.
You can always pass the ViewModel in one of the graph extension function if necessary.
fun NavGraphBuilder.homeGraph(navController: NavHostController) {
navigation(
startDestination = Destination.Home.route,
route = Destination.HomeGraph.route
) {
composable(Destination.Home.route) { navBackStackEntry ->
val parentEntry = remember(navBackStackEntry) {
navController.getBackStackEntry(Destination.HomeGraph.route)
}
val homeViewModel = hiltViewModel<HomeViewModel>(parentEntry)
HomeRoute(
viewModel = homeViewModel,
onNavigate = { dest ->
navController.navigate(dest.route)
})
}
composable(Destination.Search.route) { navBackStackEntry ->
val parentEntry = remember(navBackStackEntry) {
navController.getBackStackEntry(Destination.HomeGraph.route)
}
val homeViewModel = hiltViewModel<HomeViewModel>(parentEntry)
UserSupportRoute(
viewModel = userViewModel,
onNavigate = { dest ->
navController.navigate(dest.route) {
popUpTo(Destination.Search.route) {
inclusive = true
}
}
})
}
}

How to bind string (update that string when change him) from viewmodel with Jetpack Compose

I want to implement MVVM arquitecture and i am trying to bind two string variables from my model but I cannot
I have this model and viewmodel:
data class LoginModel(var correo:String = "", val password:String = "")
class LoginViewModel:ViewModel() {
var model by mutableStateOf(LoginModel())
fun setEmail(text:String){
model.correo = text
}
}
And i have this #Composable function:
#Composable
fun LoginPage(navController: NavController,viewModel: LoginViewModel = viewModel()) {
val paddingBox=30.dp //padding for box
BoxWithConstraints(modifier = Modifier
.fillMaxSize()
.padding(paddingBox)) {
Column(modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceEvenly) {
Row(modifier = Modifier
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))) {
//var text by remember { mutableStateOf(TextFieldValue("")) }
OutlinedTextField(
value = viewModel.model.correo,
onValueChange = {viewModel.setEmail(it)},
modifier= Modifier
.fillMaxWidth()
.background(androidx.compose.ui.graphics.Color.White),
shape = RoundedCornerShape(percent = 20),
trailingIcon = {
Icon(imageVector= Icons.Filled.Email,"correo")
},
singleLine = true)
}
//container of elements
}
}
}
but UI is not being updated. I write in emulator but nothing is changed. why ??
Your viewmodel contains LoginModel's object as the mutable state. But inside the setEmail method, you are just updating the underlying value of that mutable state's object. The underlying change will not be considered by the compose compiler as the object is still the same.
You can do something like this,
Change the correo to val from var.
data class LoginModel(val correo: String = "", val password: String = "")
And the viewmodel
class LoginViewModel : ViewModel() {
var model by mutableStateOf(LoginModel())
fun setEmail(text: String) {
model = model.copy(correo = text)
}
}
If you update the object itself, it will reflect in UI as this will make the recomposition.

Ui is not updating from viewmodel kotlin flow

I am quite new in Android Flow and JetPack compose,
I am trying to update my UI when mutable state is being changed , but this is not calling our composable , here is my code
#Composable
fun Grid() {
val mainViewModel by viewModels<DashBoardViewModel>()
mainViewModel.getData()
when (val result = mainViewModel.mutableState.value) {
is Resource.Success -> {
LazyVerticalGrid(
cells = GridCells.Adaptive(100.dp)
) {
items(result.device.items.first().devices.count()) {
EachItem(it)
}
}
}
is Resource.Error -> { Text(text = result.message) }
Resource.Loading -> { CircularProgressIndicator() }
Resource.Empty -> {}
else -> { CircularProgressIndicator() }
}
}
ViewModel:
#HiltViewModel
class DashBoardViewModel #Inject constructor(
private val dashBoardRepository: DashBoardRepository
) : ViewModel() {
val mutableState = MutableLiveData<Resource>()
fun getData() = viewModelScope.launch {
flow {
emit(Resource.Loading)
try {
val mResponse = dashBoardRepository.getDevice()
emit(Resource.Success(mResponse))
} catch (e: Exception) {
e.printStackTrace()
emit(Resource.Error("Error"))
}
}.flowOn(Dispatchers.IO).collect {
mutableState.value = it
}
}
}
There are two problems in your code:
mainViewModel.mutableState.value gets only the current value from your mutable state. Your composable will not be notified when this value changes and thus it cannot reflect the change. If you want to use LiveData in your viewmodel, you have to use observeAsState() extension function which converts LivaData to State that can be automatically observed by composable function. Other option is to have (Mutable)State directly in your viewmodel. See this state explanation.
Your mainViewModel.getData() function will be called every time your Grid() function recomposes, which will be every time your mainViewModel.mutableState changes (once you observe it correctly). You definitely don't want that. Better solution would be to call getData() from your viewModel's init block, or, if you really need to call it from your composable function, use LaunchedEffect.
And, as a side note, the way you are creating flow and then collecting it into LiveData is really odd and unnecessary. You can do something like this instead:
fun getData() = viewModelScope.launch {
mutableState.value = Resource.Loading
try {
val mResponse = dashBoardRepository.getDevice()
mutableState.value = Resource.Success(mResponse)
} catch (e: Exception) {
e.printStackTrace()
mutableState.value = Resource.Error("Error")
}
}

How to pass a composable content parameter in data class

I need to pass a compose content parameter in data class. For example a button can render when added into this content.
data class ContentData {
val content: #Composable ()-> Unit
}
This is working but when I get the app background I am getting parcelable exception. How to solve this problem.
One possible explanation I think that will occur related with a parcelable error, happens if you try to pass such object between activities as extras through Intent. Consider not use Composable as parameters in objects. Instead, try to represent the parameters of your Composable with a model which contains the parameters.
// your compose function
#Composable
fun Item(content: String = "Default", padding: Dp){
// ...
}
// Ui Model which contains your data (instead of have a weird composable reference) as a parcelable.
data class ContentData(
val content: String = "Default",
val paddingRaw: Int = 0
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString().orEmpty(),
parcel.readInt()
) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(content)
parcel.writeInt(paddingRaw)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<ContentData> {
override fun createFromParcel(parcel: Parcel): ContentData {
return ContentData(parcel)
}
override fun newArray(size: Int): Array<ContentData?> {
return arrayOfNulls(size)
}
}
}
// Example if you need the model between activities through the intent as an extra.
val data = ContentData("your content", 11)
val intent = Intent().apply {
putExtra("keyContentData", data)
}
//The way of get and use your model.
val contentData = intent.extras?.get("keyContentData") as ContentData
#Composable
fun ParentComponent(){
// ...
Item(
contentData?.content.orEmpty(),
contentData?.paddingRaw?.dp ?: 0.dp
)
// ...
}

Typescript, How to avoid code duplication in constructor?

Consider this class that is used as a data model in a Model-View-Controller scenario (I'm using TypeScript 3.5):
export class ViewSource {
private viewName : string;
private viewStruct : IViewStruct;
private rows : any[];
private rowIndex : number|null;
constructor(viewName : string) {
// Same as this.setViewName(viewName);
this.viewName = viewName;
this.viewStruct = api.meta.get_view_struct(viewName);
if (!this.viewStruct) {
throw new Error("Clould not load structure for view, name=" + (viewName));
}
this.rows = [];
this.rowIndex = null;
}
public setViewName = (viewName: string) => {
this.viewName = viewName;
this.viewStruct = api.meta.get_view_struct(viewName);
if (!this.viewStruct) {
throw new Error("Clould not load structure for view, name=" + (viewName));
}
this.rows = [];
this.rowIndex = null;
}
public getViewStruct = ():IViewStruct => { return this.viewStruct; }
public getCellValue = (rowIndex: number, columnName: string) : any => {
const row = this.rows[rowIndex] as any;
return row[columnName];
}
}
This is not a complete class, I only included a few methods to demonstrate the problem. ViewSource is a mutable object. It can be referenced from multiple parts of the application. (Please note that being a mutable object is a fact. This question is not about choosing a different data model that uses immutable objects.)
Whenever I want to change the state of a ViewSource object, I call its setViewName method. It does work, but it is also very clumsy. Every line of code in the constructor is repeated in the setViewName method.
Of course, it is not possible to use this constructor:
constructor(viewName : string) {
this.setViewName(viewName);
}
because that results in TS2564 error:
Property 'viewStruct' has no initializer and is not definitely assigned in the constructor.ts(2564)
I do not want to ignore TS2564 errors in general. But I also do not want to repeat all attribute initializations. I have some other classes with even more properties (>10), and the corresponding code duplication looks ugly, and it is error prone. (I might forget that some things have to bee modified in two methods...)
So how can I avoid duplicating many lines of code?
I think the best method to avoid code duplication in this case would be to create a function that contains the initialization code, but instead of setting the value, it retunrs the value that need to be set.
Something like the following:
export class ViewSource {
private viewName : string;
private viewStruct : IViewStruct;
private rows : any[];
private rowIndex : number|null;
constructor(viewName : string) {
const {newViewName, newViewStruct, newRows, newRowIndex} = this.getNewValues(viewName);
this.viewName = newViewName;
this.newViewStruct = newViewStruct;
// Rest of initialization goes here
}
public setViewName = (viewName: string) => {
const {newViewName, newViewStruct, newRows, newRowIndex} = this.getNewValues(viewName);
// Rest of initialization goes here
}
privat getNewValues = (viewName) => {
const newViewName = viewName;
const newViewStruct = api.meta.get_view_struct(viewName);
if (!newViewStruct) {
throw new Error("Clould not load structure for view, name=" + (viewName));
}
const newRows = [];
const newRowIndex = null;
return {newViewName, newViewStruct, newRows, newRowIndex};
}
}
This way the only thing you duplicate is setting the values, not calculating them, and if the values calculations will get more complicated you can simply expand the returned value.
A less complex approach than the accepted answer is to use the //#ts-ignore[1] comment above each member that is initialized elsewhere.
Consider this contrived example
class Foo {
// #ts-ignore TS2564 - initialized in the init method
a: number;
// #ts-ignore TS2564 - initialized in the init method
b: string;
// #ts-ignore TS2564 - initialized in the init method
c: number;
constructor(a: number, b: string) {
if(a === 0) {
this.init(a,b,100);
} else {
this.init(a,b,4912);
}
}
private init(a: number, b: string, c: number): void {
this.a = a;
this.b = b;
this.c = c;
}
}
Since TypeScript 3.9 there exists the //#ts-expect-error[2] comment, but I think #ts-ignore is suitable.
[1] Suppress errors in .ts files
[2] TS expect errors comment
Since TypeScript 2.7 you can use the definite assignment assertion modifier which means adding an exclamation mark between the variable name and the colon:
private viewName!: string
This has the same effect as adding a // #ts-ignore TS2564 comment above it as #RamblinRose suggested.

Resources