Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android 练手之旅——ViewModel&PhotoHunter(三) #7

Open
soapgu opened this issue Mar 3, 2021 · 0 comments
Open

Android 练手之旅——ViewModel&PhotoHunter(三) #7

soapgu opened this issue Mar 3, 2021 · 0 comments
Labels
Demo Demo 安卓 安卓

Comments

@soapgu
Copy link
Owner

soapgu commented Mar 3, 2021

上一章讲到了DataBinding,还是非常有感觉。光有DataBinding,没有ViewModel这个黄金搭档,MVVM是不完整的。

ViewModel

不同于windows桌面的WPF程序,Android有专门的ViewModel组件就是androidx.lifecycle:lifecycle-viewmodel
主要有以下特性

  1. 与获取ViewModel的对象(对象继承LifecycleOwner),可以是FragmentActivity或者Fragment。ViewModel和请求获取对象讲一直驻留内存直到FragmentActivity或者Fragment都完成了Finish,注意锁屏/翻转/home键,ViewModel将一直常陪伴。
    图片

  2. ViewModel使用原则,类中代码不可拥有Activity或者Fragment的引用,也不可以有任何UI层的引用View(Button,TextView等),ViewModel必须保持高度的纯粹

ViewModel与可观察对象的合体

ViewModel对象本身只解决了生命周期同步,并不能完成绑定后,UI界面更新通知的工作
所以必须实现Observable 接口,这里由于JAVA和C#一样是单继承,所以BaseObservable不能用。选择继承ViewModel和Observable

class ObservableViewModel extends ViewModel implements Observable {
        private PropertyChangeRegistry callbacks = new PropertyChangeRegistry();

        @Override
        public void addOnPropertyChangedCallback(
                Observable.OnPropertyChangedCallback callback) {
            callbacks.add(callback);
        }

        @Override
        public void removeOnPropertyChangedCallback(
                Observable.OnPropertyChangedCallback callback) {
            callbacks.remove(callback);
        }

        /**
         * Notifies observers that all properties of this instance have changed.
         */
        void notifyChange() {
            callbacks.notifyCallbacks(this, 0, null);
        }

        /**
         * Notifies observers that a specific property has changed. The getter for the
         * property that changes should be marked with the @Bindable annotation to
         * generate a field in the BR class to be used as the fieldId parameter.
         *
         * @param fieldId The generated BR id for the Bindable field.
         */
        void notifyPropertyChanged(int fieldId) {
            callbacks.notifyCallbacks(this, fieldId, null);
        }
    }

注意,这里不能照抄官挡的代码,两个override方法应该改成public
这里MVVM架构改造的必要知识已经准备差不多了。开始正式动手

PhotoHunter Demo实战

在build.gradle里增加依赖

implementation "androidx.lifecycle:lifecycle-viewmodel:2.3.0"

创建HomeViewModel继承ObservableViewModel。
在实际使用过程中,由于ViewModel还需要应用到Application的Context,所以最后ObservableViewModel继承了AndroidViewModel。为了能获取Resource和getExternalCacheDir

class ObservableViewModel extends AndroidViewModel implements Observable {
    private PropertyChangeRegistry callbacks = new PropertyChangeRegistry();

     public ObservableViewModel(@NonNull Application application) {
         super(application);
     }

     @Override
    public void addOnPropertyChangedCallback(
            Observable.OnPropertyChangedCallback callback) {
        callbacks.add(callback);
    }

    @Override
    public void removeOnPropertyChangedCallback(
            Observable.OnPropertyChangedCallback callback) {
        callbacks.remove(callback);
    }

    /**
     * Notifies observers that all properties of this instance have changed.
     */
    void notifyChange() {
        callbacks.notifyCallbacks(this, 0, null);
    }

    /**
     * Notifies observers that a specific property has changed. The getter for the
     * property that changes should be marked with the @Bindable annotation to
     * generate a field in the BR class to be used as the fieldId parameter.
     *
     * @param fieldId The generated BR id for the Bindable field.
     */
    void notifyPropertyChanged(int fieldId) {
        callbacks.notifyCallbacks(this, fieldId, null);
    }

}

照片说明,图片,下载逻辑全部移植到ViewModel来

public class HomeViewModel extends ObservableViewModel {
    private static final String url = "https://api.unsplash.com/photos/random?client_id=ki5iNzD7hebsr-d8qUlEJIhG5wxGwikU71nsqj8PcMM";
    private static OkHttpClient client = new OkHttpClient();
    Gson gson = new Gson();
    private String photoInfo = "Cow is Default Photo";
    private Bitmap bitmap;

    public HomeViewModel(@NonNull Application application) {
        super(application);
        Bitmap cowImage = BitmapFactory.decodeResource(this.getApplication().getResources(), R.drawable.homephoto);
        this.setBitmap(cowImage);
    }

    @Bindable
    public String getPhotoInfo() {
        return photoInfo;
    }

    public void setPhotoInfo(String photeInfo) {
        this.photoInfo = photeInfo;
        this.notifyPropertyChanged(BR.photoInfo);
    }

    @Bindable
    public Bitmap getBitmap() {
        return bitmap;
    }

    public void setBitmap(Bitmap bitmap) {
        this.bitmap = bitmap;
        this.notifyPropertyChanged(BR.bitmap);
    }

    public void ChangePhoto()
    {
        Request request = new Request.Builder()
                .url(url)
                .get()
                .build();
        Call call = client.newCall(request);

        call.enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                Logger.e("Get Photo Error:%s" , e.getMessage());
                setPhotoInfo("Error for Hunter Photo");
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                Logger.i("Response Code:%s",response.code());
                if( response.isSuccessful() ) {
                    Logger.i("Success fetch photo information , Thread:%s" , Thread.currentThread().getId());
                    try( ResponseBody responseBody = response.body() ) {
                        assert responseBody != null;
                        String jsonString = responseBody.string();
                        Photo photo = gson.fromJson(jsonString, Photo.class);
                        setPhotoInfo(photo.alt_description);
                        DownloadImage(photo);
                    }
                }
            }
        });
        setPhotoInfo("Requesting...");
    }

    private void DownloadImage( Photo photo )
    {
        Request imageRequest = new Request.Builder()
                .url(photo.urls.small)
                .get()
                .build();
        Call downloadCall = client.newCall(imageRequest);
        downloadCall.enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                Logger.e("download image Error:%s", e.getMessage());
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) {
                if (response.isSuccessful()) {
                    Logger.i("Success response  photo image resource , Thread:%s" , Thread.currentThread().getId());
                    //FileOutputStream output = null;
                    try( ResponseBody downloadBody = response.body() ) {
                        assert downloadBody != null;
                        InputStream inputStream = downloadBody.byteStream();
                        BufferedInputStream input = new BufferedInputStream(inputStream);

                        File file = new File( getApplication().getExternalCacheDir(), String.format( "%s.jpg" ,photo.id ) );
                        try( FileOutputStream output = new FileOutputStream(file) )
                        {
                            int len;
                            byte[] data  = new byte[1024];
                            while ((len = input.read(data)) != -1) {
                                output.write(data, 0, len);
                            }
                            output.flush();
                            input.close();
                        }
                        Logger.i("----Success download photo image to cache---" );
                        Bitmap bitmap = BitmapFactory.decodeFile(file.getPath());
                        Logger.i("----Fetch photo image from cache---" );
                        setBitmap(bitmap);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
    }
}

惊喜下,完全不用管跨线程更新UI的问题

layout端改造部分

<?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>
        <variable
            name="viewmodel"
            type="com.soapdemo.photohunter.viewmodels.HomeViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/msg_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:layout_marginBottom="10dp"
            android:text="@{viewmodel.photoInfo}"
            android:textSize="24sp"
            app:layout_constraintBottom_toTopOf="@+id/photo_view"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <ImageView
            android:id="@+id/photo_view"
            app:imageBitmap="@{viewmodel.bitmap}"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginTop="10dp"
            android:contentDescription="@string/image_desc"
            android:scaleType="fitXY"
            app:layout_constraintBottom_toTopOf="@+id/panel_button"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/msg_view" />

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/panel_button"
            android:layout_width="0dp"
            android:layout_height="114dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent">

            <Button
                android:id="@+id/new_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/new_button_text"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                android:onClick="@{()->viewmodel.ChangePhoto()}"/>
        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Bitmap不能直接绑定,这里用到了适配器,StackOverflow上的回答还需要改一点点,属性前缀已经被取消了,如“aaa:imageBitmap”已经不能用了

public class BindingAdapters {
    @BindingAdapter("imageBitmap")
    public static void loadImage(ImageView iv, Bitmap bitmap) {
        iv.setImageBitmap(bitmap);
    }
}

layout里面的id其实可以去掉了
最后就是MainActivity.java了

public class MainActivity extends AppCompatActivity {

    @SuppressLint("SetTextI18n")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        HomeViewModel viewModel = new ViewModelProvider(this,
                ViewModelProvider.AndroidViewModelFactory.getInstance(this.getApplication()))
                .get(HomeViewModel.class);
        ActivityMainBinding binding  = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setViewmodel( viewModel );
    }
}

代码量少的惊人
另外手痒改了一个图标
图片

总结部分

Android拥有比较好的MVVM的培育的土壤。初步再理解回顾下Android的MVVM

  1. View层
    其实就是layout,以及layout生成的{layout}Bind(非常非常雷同XXX.xaml.cs)负责UI呈现

  2. ViewModel层
    负责数据组织,及界面上的逻辑交互

  3. UI Controls层
    Activity或者Fragment。官网博客上说UI控制层,我个人理解更加像一个装配员,负责View和ViewModel的装配,但是本身代码逻辑已经大部移交给View和ViewModel层

图标

参考链接

ViewModel
官档
viewmodel
Bind layout views to Architecture Components
ViewModels : A Simple Example
ImageView适配器
Databinding an in-memory Bitmap to an ImageView

@soapgu soapgu added Demo Demo 安卓 安卓 labels Mar 3, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Demo Demo 安卓 安卓
Projects
None yet
Development

No branches or pull requests

1 participant