iT邦幫忙

2021 iThome 鐵人賽

DAY 17
0
Mobile Development

挑戰 Kotlin Multiplatform Mobile 跨平台開發,透過共同的Kotlin模組同時打造iOS與Android應用!系列 第 17

Day 17: swiftUI與Coroutine強強聯手,迸出新滋味.

Keyword: swiftUI,Coroutine Scope


改寫ObservableObject

既然我們將拉取網路資料的部分下放到了shared中的新ViewModel,那們ObservableObject的工作就簡單許多,只要提供顯示使用的資訊即可.

首先.我們把網路目前的狀態利用三個Published通知UI,分別代表DataState中的各數據.讀取中,讀取到的資料,錯誤訊息

class CafeItemObservableModel: ObservableObject {
		private var viewModel: iOSCafeViewModel? = nil
		
		@Published var loading = false
    
    @Published var cafeList: Array<CafeResponseItem>? = nil

    @Published var error: String? = nil
}

然後我們昨天建立iOSCafeViewModel,需要提供一個Interface,讓資料可以溝通,我們在viewModel的建構子中提供給他.順便當有資料來的時候,印出一些訊息.

func activate(){
        viewModel = iOSBasicViewModel { [weak self] dataState in
            self?.loading = dataState.loading//讀取
            self?.cafeList = dataState.data//讀取到的資料
            self?.error = dataState.exception//發生的錯誤訊息
            
            if let cafeList = dataState.data{
                print("size: \(cafeList.count)")//印出收到的值的個數
            }
            if let errorMessage = dataState.exception{
                print("exception: \(errorMessage)")//印出錯誤內容
            }
        }
   }

最後,在離開頁面時,提供一個方法讓iOS端呼叫,避免Coroutine Leak的問題發生

func deactivate() {//在頁面離開的時候呼叫,避免Leak
      viewModel?.onDestroy()
      viewModel = nil
 }

全部就會像這樣

class CafeItemObservableModel: ObservableObject {
    private var viewModel: iOSBasicViewModel? = nil
    
    @Published var loading = false
    
    @Published var cafeList: Array<CafeResponseItem>? = nil

    @Published var error: String? = nil

    func activate(){
        viewModel = iOSBasicViewModel { [weak self] dataState in
            self?.loading = dataState.loading
            self?.cafeList = dataState.data
            self?.error = dataState.exception
            
            if let cafeList = dataState.data{
                print("size: \(cafeList.count)")
            }
            if let errorMessage = dataState.exception{
                print("exception: \(errorMessage)")
            }
        }
    }
    
    func deactivate() {
            viewModel?.onDestroy()
            viewModel = nil
    }
}

更新SwiftUI

接下來也來改寫ContentView,除了使用新的來源外,還能讓SwiftUI在讀取和呈現時有些特別的效果.

先重寫一下每一行的樣式,改為使用shared內的ViewModel的數據.

struct CafeRowView : View{
    var cafeResponseItem: CafeResponseItem

    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(cafeResponseItem.name).font(.headline)
                Text(cafeResponseItem.address).font(.subheadline)
            }
        }
    }
}

然後是使用這個樣式的ListContent,在發生錯誤與讀取時,這個ListContent會更換顯示內容,而正常狀態下,就是普通的List

struct CafeListContent : View{
    var loading: Bool
    var cafeList:  Array<CafeResponseItem>?
    var error: String?
    
    var body: some View {
        ZStack {
            VStack {
                if let cafeList = cafeList {//正常收到,使用建立的RowView
                    List(cafeList, id: \.self){cafe in
                        CafeRowView(cafeResponseItem:cafe)
                    }
                }
                if let error = error {//發生錯誤,顯示錯誤訊息
                    Text(error)
                        .foregroundColor(.red)
                }
            }
            if (loading) {//讀取中,顯示Loading的文字
                Text("Loading...")
            }
        }
    }
}

最後最外層的View,沒有實際的畫面,但是負責讓數據跟observableModel綁定,以及在View出現時進行讀取,View消失時取消Coroutine避免Leak.

struct CafeListScreen :View {
    @ObservedObject var observableModel = CafeItemObservableModel()
    var body: some View{
        CafeListContent(//讓CafeListContent的值與observableModel內的數據綁定
            loading: observableModel.loading,
            cafeList: observableModel.cafeList,
            error: observableModel.error
        )
        .onAppear(perform: {//畫面出現時,開始讀取
            observableModel.activate()
        })
        .onDisappear(perform: {//畫面消失時,停止讀取
            observableModel.deactivate()
        })
    }
}

最後讓Content View改成這個新的CafeListScreen就可以啦!如果檔名不同記得去iOSApp檔案修改,就在ContentView的同路徑下.

可以注意到雖然不像CoroutineScope或是Android的LifecycleObserver強制把起點終點都訂好,但是ContentView還是提供了一個接近的onAppear與onDisappear來管理View的生命週期.但是有個缺點,沒有強制性,所以新人仍然有可能會漏掉onDisappear,造成Leak

今天就到這裡,明天我們會回到Kotlin本身.我們會使用Koin進行注入管理.


上一篇
Day 16:自己動手,豐衣足食.IOS的Coroutine管理
下一篇
Day 18: To DI ? Or not DI? 依賴注入的存在意義
系列文
挑戰 Kotlin Multiplatform Mobile 跨平台開發,透過共同的Kotlin模組同時打造iOS與Android應用!30

尚未有邦友留言

立即登入留言